/* -*- Mode: JS; tab-width: 4; indent-tabs-mode: nil; -*- * vim: set sw=4 ts=4 et tw=78: * ***** BEGIN LICENSE BLOCK ***** * * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is the Narcissus JavaScript engine. * * The Initial Developer of the Original Code is * Brendan Eich . * Portions created by the Initial Developer are Copyright (C) 2004 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Tom Austin * Brendan Eich * Shu-Yu Guo * Dave Herman * Dimitris Vardoulakis * Patrick Walton * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ /* * Narcissus - JS implemented in JS. * * Parser. */ define(function(require, exports, module) { var lexer = require('./lexer'); var definitions = require('./definitions'); var options = require('./options'); var Tokenizer = lexer.Tokenizer; var Dict = definitions.Dict; var Stack = definitions.Stack; // Set constants in the local scope. eval(definitions.consts); /* * pushDestructuringVarDecls :: (node, hoisting node) -> void * * Recursively add all destructured declarations to varDecls. */ function pushDestructuringVarDecls(n, s) { for (var i in n) { var sub = n[i]; if (sub.type === IDENTIFIER) { s.varDecls.push(sub); } else { pushDestructuringVarDecls(sub, s); } } } function Parser(tokenizer) { tokenizer.parser = this; this.t = tokenizer; this.x = null; this.unexpectedEOF = false; options.mozillaMode && (this.mozillaMode = true); options.parenFreeMode && (this.parenFreeMode = true); } function StaticContext(parentScript, parentBlock, inModule, inFunction, strictMode) { this.parentScript = parentScript; this.parentBlock = parentBlock || parentScript; this.inModule = inModule || false; this.inFunction = inFunction || false; this.inForLoopInit = false; this.topLevel = true; this.allLabels = new Stack(); this.currentLabels = new Stack(); this.labeledTargets = new Stack(); this.defaultLoopTarget = null; this.defaultTarget = null; this.strictMode = strictMode; } StaticContext.prototype = { // non-destructive update via prototype extension update: function(ext) { var desc = {}; for (var key in ext) { desc[key] = { value: ext[key], writable: true, enumerable: true, configurable: true } } return Object.create(this, desc); }, pushLabel: function(label) { return this.update({ currentLabels: this.currentLabels.push(label), allLabels: this.allLabels.push(label) }); }, pushTarget: function(target) { var isDefaultLoopTarget = target.isLoop; var isDefaultTarget = isDefaultLoopTarget || target.type === SWITCH; if (this.currentLabels.isEmpty()) { if (isDefaultLoopTarget) this.update({ defaultLoopTarget: target }); if (isDefaultTarget) this.update({ defaultTarget: target }); return this; } target.labels = new Dict(); this.currentLabels.forEach(function(label) { target.labels.set(label, true); }); return this.update({ currentLabels: new Stack(), labeledTargets: this.labeledTargets.push(target), defaultLoopTarget: isDefaultLoopTarget ? target : this.defaultLoopTarget, defaultTarget: isDefaultTarget ? target : this.defaultTarget }); }, nest: function() { return this.topLevel ? this.update({ topLevel: false }) : this; }, canImport: function() { return this.topLevel && !this.inFunction; }, canExport: function() { return this.inModule && this.topLevel && !this.inFunction; }, banWith: function() { return this.strictMode || this.inModule; }, modulesAllowed: function() { return this.topLevel && !this.inFunction; } }; var Pp = Parser.prototype; Pp.mozillaMode = false; Pp.parenFreeMode = false; Pp.withContext = function(x, f) { var x0 = this.x; this.x = x; var result = f.call(this); // NB: we don't bother with finally, since exceptions trash the parser this.x = x0; return result; }; Pp.newNode = function newNode(opts) { return new Node(this.t, opts); }; Pp.fail = function fail(msg) { throw this.t.newSyntaxError(msg); }; Pp.match = function match(tt, scanOperand, keywordIsName) { return this.t.match(tt, scanOperand, keywordIsName); }; Pp.mustMatch = function mustMatch(tt, keywordIsName) { return this.t.mustMatch(tt, keywordIsName); }; Pp.peek = function peek(scanOperand) { return this.t.peek(scanOperand); }; Pp.peekOnSameLine = function peekOnSameLine(scanOperand) { return this.t.peekOnSameLine(scanOperand); }; Pp.done = function done() { return this.t.done; }; /* * Script :: (boolean, boolean, boolean) -> node * * Parses the toplevel and module/function bodies. */ Pp.Script = function Script(inModule, inFunction, expectEnd) { var node = this.newNode(scriptInit()); var x2 = new StaticContext(node, node, inModule, inFunction); this.withContext(x2, function() { this.Statements(node, true); }); if (expectEnd && !this.done()) this.fail("expected end of input"); return node; }; /* * Pragma :: (expression statement node) -> boolean * * Checks whether a node is a pragma and annotates it. */ function Pragma(n) { if (n.type === SEMICOLON) { var e = n.expression; if (e.type === STRING && e.value === "use strict") { n.pragma = "strict"; return true; } } return false; } /* * Node :: (tokenizer, optional init object) -> node */ function Node(t, init) { var token = t.token; if (token) { // If init.type exists it will override token.type. this.type = token.type; this.value = token.value; this.lineno = token.lineno; // Start and end are file positions for error handling. this.start = token.start; this.end = token.end; } else { this.lineno = t.lineno; } this.filename = t.filename; this.children = []; for (var prop in init) this[prop] = init[prop]; } /* * SyntheticNode :: (optional init object) -> node */ function SyntheticNode(init) { this.children = []; for (var prop in init) this[prop] = init[prop]; this.synthetic = true; } var Np = Node.prototype = SyntheticNode.prototype = {}; Np.constructor = Node; var TO_SOURCE_SKIP = { type: true, value: true, lineno: true, start: true, end: true, tokenizer: true, assignOp: true }; function unevalableConst(code) { var token = definitions.tokens[code]; var constName = definitions.opTypeNames.hasOwnProperty(token) ? definitions.opTypeNames[token] : token in definitions.keywords ? token.toUpperCase() : token; return { toSource: function() { return constName } }; } Np.toSource = function toSource() { var mock = {}; var self = this; mock.type = unevalableConst(this.type); // avoid infinite recursion in case of back-links if (this.generatingSource) return mock.toSource(); this.generatingSource = true; if ("value" in this) mock.value = this.value; if ("lineno" in this) mock.lineno = this.lineno; if ("start" in this) mock.start = this.start; if ("end" in this) mock.end = this.end; if (this.assignOp) mock.assignOp = unevalableConst(this.assignOp); for (var key in this) { if (this.hasOwnProperty(key) && !(key in TO_SOURCE_SKIP)) mock[key] = this[key]; } try { return mock.toSource(); } finally { delete this.generatingSource; } }; // Always use push to add operands to an expression, to update start and end. Np.push = function (kid) { // kid can be null e.g. [1, , 2]. if (kid !== null) { if (kid.start < this.start) this.start = kid.start; if (this.end < kid.end) this.end = kid.end; } return this.children.push(kid); } Node.indentLevel = 0; function tokenString(tt) { var t = definitions.tokens[tt]; return /^\W/.test(t) ? definitions.opTypeNames[t] : t.toUpperCase(); } Np.toString = function () { var a = []; for (var i in this) { if (this.hasOwnProperty(i) && i !== 'type' && i !== 'target') a.push({id: i, value: this[i]}); } a.sort(function (a,b) { return (a.id < b.id) ? -1 : 1; }); var INDENTATION = " "; var n = ++Node.indentLevel; var s = "{\n" + INDENTATION.repeat(n) + "type: " + tokenString(this.type); for (i = 0; i < a.length; i++) s += ",\n" + INDENTATION.repeat(n) + a[i].id + ": " + a[i].value; n = --Node.indentLevel; s += "\n" + INDENTATION.repeat(n) + "}"; return s; } Np.synth = function(init) { var node = new SyntheticNode(init); node.filename = this.filename; node.lineno = this.lineno; node.start = this.start; node.end = this.end; return node; }; /* * Helper init objects for common nodes. */ var LOOP_INIT = { isLoop: true }; function blockInit() { return { type: BLOCK, varDecls: [] }; } function scriptInit() { return { type: SCRIPT, funDecls: [], varDecls: [], modDefns: new Dict(), modAssns: new Dict(), modDecls: new Dict(), modLoads: new Dict(), impDecls: [], expDecls: [], exports: new Dict(), hasEmptyReturn: false, hasReturnWithValue: false, hasYield: false }; } definitions.defineGetter(Np, "length", function() { throw new Error("Node.prototype.length is gone; " + "use n.children.length instead"); }); definitions.defineProperty(String.prototype, "repeat", function(n) { var s = "", t = this + s; while (--n >= 0) s += t; return s; }, false, false, true); Pp.MaybeLeftParen = function MaybeLeftParen() { if (this.parenFreeMode) return this.match(LEFT_PAREN) ? LEFT_PAREN : END; return this.mustMatch(LEFT_PAREN).type; }; Pp.MaybeRightParen = function MaybeRightParen(p) { if (p === LEFT_PAREN) this.mustMatch(RIGHT_PAREN); } /* * Statements :: (node[, boolean]) -> void * * Parses a sequence of Statements. */ Pp.Statements = function Statements(n, topLevel) { var prologue = !!topLevel; try { while (!this.done() && this.peek(true) !== RIGHT_CURLY) { var n2 = this.Statement(); n.push(n2); if (prologue && Pragma(n2)) { this.x.strictMode = true; n.strict = true; } else { prologue = false; } } } catch (e) { try { if (this.done()) this.unexpectedEOF = true; } catch(e) {} throw e; } } Pp.Block = function Block() { this.mustMatch(LEFT_CURLY); var n = this.newNode(blockInit()); var x2 = this.x.update({ parentBlock: n }).pushTarget(n); this.withContext(x2, function() { this.Statements(n); }); this.mustMatch(RIGHT_CURLY); return n; } var DECLARED_FORM = 0, EXPRESSED_FORM = 1, STATEMENT_FORM = 2; /* * Export :: (binding node, boolean) -> Export * * Static semantic representation of a module export. */ function Export(node, isDefinition) { this.node = node; // the AST node declaring this individual export this.isDefinition = isDefinition; // is the node an 'export'-annotated definition? this.resolved = null; // resolved pointer to the target of this export } /* * registerExport :: (Dict, EXPORT node) -> void */ function registerExport(exports, decl) { function register(name, exp) { if (exports.has(name)) throw new SyntaxError("multiple exports of " + name); exports.set(name, exp); } switch (decl.type) { case MODULE: case FUNCTION: register(decl.name, new Export(decl, true)); break; case VAR: for (var i = 0; i < decl.children.length; i++) register(decl.children[i].name, new Export(decl.children[i], true)); break; case LET: case CONST: throw new Error("NYI: " + definitions.tokens[decl.type]); case EXPORT: for (var i = 0; i < decl.pathList.length; i++) { var path = decl.pathList[i]; switch (path.type) { case OBJECT_INIT: for (var j = 0; j < path.children.length; j++) { // init :: IDENTIFIER | PROPERTY_INIT var init = path.children[j]; if (init.type === IDENTIFIER) register(init.value, new Export(init, false)); else register(init.children[0].value, new Export(init.children[1], false)); } break; case DOT: register(path.children[1].value, new Export(path, false)); break; case IDENTIFIER: register(path.value, new Export(path, false)); break; default: throw new Error("unexpected export path: " + definitions.tokens[path.type]); } } break; default: throw new Error("unexpected export decl: " + definitions.tokens[exp.type]); } } /* * Module :: (node) -> Module * * Static semantic representation of a module. */ function Module(node) { var exports = node.body.exports; var modDefns = node.body.modDefns; var exportedModules = new Dict(); exports.forEach(function(name, exp) { var node = exp.node; if (node.type === MODULE) { exportedModules.set(name, node); } else if (!exp.isDefinition && node.type === IDENTIFIER && modDefns.has(node.value)) { var mod = modDefns.get(node.value); exportedModules.set(name, mod); } }); this.node = node; this.exports = exports; this.exportedModules = exportedModules; } /* * Statement :: () -> node * * Parses a Statement. */ Pp.Statement = function Statement() { var i, label, n, n2, p, c, ss, tt = this.t.get(true), tt2, x0, x2, x3; var comments = this.t.blockComments; // Cases for statements ending in a right curly return early, avoiding the // common semicolon insertion magic after this switch. switch (tt) { case IMPORT: if (!this.x.canImport()) this.fail("illegal context for import statement"); n = this.newNode(); n.pathList = this.ImportPathList(); this.x.parentScript.impDecls.push(n); break; case EXPORT: if (!this.x.canExport()) this.fail("export statement not in module top level"); switch (this.peek()) { case MODULE: case FUNCTION: case LET: case VAR: case CONST: n = this.Statement(); n.blockComments = comments; n.exported = true; this.x.parentScript.expDecls.push(n); registerExport(this.x.parentScript.exports, n); return n; } n = this.newNode(); n.pathList = this.ExportPathList(); this.x.parentScript.expDecls.push(n); registerExport(this.x.parentScript.exports, n); break; case FUNCTION: // DECLARED_FORM extends funDecls of x, STATEMENT_FORM doesn't. return this.FunctionDefinition(true, this.x.topLevel ? DECLARED_FORM : STATEMENT_FORM, comments); case LEFT_CURLY: n = this.newNode(blockInit()); x2 = this.x.update({ parentBlock: n }).pushTarget(n).nest(); this.withContext(x2, function() { this.Statements(n); }); this.mustMatch(RIGHT_CURLY); return n; case IF: n = this.newNode(); n.condition = this.HeadExpression(); x2 = this.x.pushTarget(n).nest(); this.withContext(x2, function() { n.thenPart = this.Statement(); n.elsePart = this.match(ELSE, true) ? this.Statement() : null; }); return n; case SWITCH: // This allows CASEs after a DEFAULT, which is in the standard. n = this.newNode({ cases: [], defaultIndex: -1 }); n.discriminant = this.HeadExpression(); x2 = this.x.pushTarget(n).nest(); this.withContext(x2, function() { this.mustMatch(LEFT_CURLY); while ((tt = this.t.get()) !== RIGHT_CURLY) { switch (tt) { case DEFAULT: if (n.defaultIndex >= 0) this.fail("More than one switch default"); // FALL THROUGH case CASE: n2 = this.newNode(); if (tt === DEFAULT) n.defaultIndex = n.cases.length; else n2.caseLabel = this.Expression(COLON); break; default: this.fail("Invalid switch case"); } this.mustMatch(COLON); n2.statements = this.newNode(blockInit()); while ((tt=this.peek(true)) !== CASE && tt !== DEFAULT && tt !== RIGHT_CURLY) n2.statements.push(this.Statement()); n.cases.push(n2); } }); return n; case FOR: n = this.newNode(LOOP_INIT); n.blockComments = comments; if (this.match(IDENTIFIER)) { if (this.t.token.value === "each") n.isEach = true; else this.t.unget(); } if (!this.parenFreeMode) this.mustMatch(LEFT_PAREN); x2 = this.x.pushTarget(n).nest(); x3 = this.x.update({ inForLoopInit: true }); n2 = null; if ((tt = this.peek(true)) !== SEMICOLON) { this.withContext(x3, function() { if (tt === VAR || tt === CONST) { this.t.get(); n2 = this.Variables(); } else if (tt === LET) { this.t.get(); if (this.peek() === LEFT_PAREN) { n2 = this.LetBlock(false); } else { // Let in for head, we need to add an implicit block // around the rest of the for. this.x.parentBlock = n; n.varDecls = []; n2 = this.Variables(); } } else { n2 = this.Expression(); } }); } if (n2 && this.match(IN)) { n.type = FOR_IN; this.withContext(x3, function() { n.object = this.Expression(); if (n2.type === VAR || n2.type === LET) { c = n2.children; // Destructuring turns one decl into multiples, so either // there must be only one destructuring or only one // decl. if (c.length !== 1 && n2.destructurings.length !== 1) { // FIXME: this.fail ? throw new SyntaxError("Invalid for..in left-hand side", this.filename, n2.lineno); } if (n2.destructurings.length > 0) { n.iterator = n2.destructurings[0]; } else { n.iterator = c[0]; } n.varDecl = n2; } else { if (n2.type === ARRAY_INIT || n2.type === OBJECT_INIT) { n2.destructuredNames = this.checkDestructuring(n2); } n.iterator = n2; } }); } else { x3.inForLoopInit = false; n.setup = n2; this.mustMatch(SEMICOLON); if (n.isEach) this.fail("Invalid for each..in loop"); this.withContext(x3, function() { n.condition = (this.peek(true) === SEMICOLON) ? null : this.Expression(); this.mustMatch(SEMICOLON); tt2 = this.peek(true); n.update = (this.parenFreeMode ? tt2 === LEFT_CURLY || definitions.isStatementStartCode[tt2] : tt2 === RIGHT_PAREN) ? null : this.Expression(); }); } if (!this.parenFreeMode) this.mustMatch(RIGHT_PAREN); this.withContext(x2, function() { n.body = this.Statement(); }); return n; case WHILE: n = this.newNode({ isLoop: true }); n.blockComments = comments; n.condition = this.HeadExpression(); x2 = this.x.pushTarget(n).nest(); this.withContext(x2, function() { n.body = this.Statement(); }); return n; case DO: n = this.newNode({ isLoop: true }); n.blockComments = comments; x2 = this.x.pushTarget(n).next(); this.withContext(x2, function() { n.body = this.Statement(); }); this.mustMatch(WHILE); n.condition = this.HeadExpression(); //