/* ***** 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): Richard Hundt * * 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. * * Lexical scanner and parser. */ // jrh //module('JS.Parse'); // Build a regexp that recognizes operators and punctuators (except newline). var opRegExp = /^;|^,|^\?|^:|^\|\||^\&\&|^\||^\^|^\&|^===|^==|^=|^!==|^!=|^<<|^<=|^<|^>>>|^>>|^>=|^>|^\+\+|^\-\-|^\+|^\-|^\*|^\/|^%|^!|^~|^\.|^\[|^\]|^\{|^\}|^\(|^\)/; // A regexp to match floating point literals (but not integer literals). var fpRegExp = /^\d+\.\d*(?:[eE][-+]?\d+)?|^\d+(?:\.\d*)?[eE][-+]?\d+|^\.\d+(?:[eE][-+]?\d+)?/; function Tokenizer(s, f, l) { this.cursor = 0; this.source = String(s); this.tokens = []; this.tokenIndex = 0; this.lookahead = 0; this.scanNewlines = false; this.scanOperand = true; this.filename = f || ""; this.lineno = l || 1; } Tokenizer.prototype = { input : function() { return this.source.substring(this.cursor); }, done : function() { return this.peek() == END; }, token : function() { return this.tokens[this.tokenIndex]; }, match: function (tt) { return this.get() == tt || this.unget(); }, mustMatch: function (tt) { if (!this.match(tt)) throw this.newSyntaxError("Missing " + this.tokens[tt].toLowerCase()); return this.token(); }, peek: function () { var tt; if (this.lookahead) { tt = this.tokens[(this.tokenIndex + this.lookahead) & 3].type; } else { tt = this.get(); this.unget(); } return tt; }, peekOnSameLine: function () { this.scanNewlines = true; var tt = this.peek(); this.scanNewlines = false; return tt; }, get: function () { var token; while (this.lookahead) { --this.lookahead; this.tokenIndex = (this.tokenIndex + 1) & 3; token = this.tokens[this.tokenIndex]; if (token.type != NEWLINE || this.scanNewlines) return token.type; } for (;;) { var input = this.input(); var rx = this.scanNewlines ? /^[ \t]+/ : /^\s+/; var match = input.match(rx); if (match) { var spaces = match[0]; this.cursor += spaces.length; var newlines = spaces.match(/\n/g); if (newlines) this.lineno += newlines.length; input = this.input(); } if (!(match = input.match(/^\/(?:\*(?:.|\n)*?\*\/|\/.*)/))) break; var comment = match[0]; this.cursor += comment.length; newlines = comment.match(/\n/g); if (newlines) this.lineno += newlines.length } this.tokenIndex = (this.tokenIndex + 1) & 3; token = this.tokens[this.tokenIndex]; if (!token) this.tokens[this.tokenIndex] = token = {}; if (!input) return token.type = END; if ((match = input.match(fpRegExp))) { token.type = NUMBER; token.value = parseFloat(match[0]); } else if ((match = input.match(/^0[xX][\da-fA-F]+|^0[0-7]*|^\d+/))) { token.type = NUMBER; token.value = parseInt(match[0]); } else if ((match = input.match(/^((\$\w*)|(\w+))/))) { var id = match[0]; token.type = keywords[id] || IDENTIFIER; token.value = id; } else if ((match = input.match(/^"(?:\\.|[^"])*"|^'(?:[^']|\\.)*'/))) { token.type = STRING; token.value = eval(match[0]); } else if (this.scanOperand && (match = input.match(/^\/((?:\\.|[^\/])+)\/([gi]*)/))) { token.type = REGEXP; token.value = new RegExp(match[1], match[2]); } else if ((match = input.match(opRegExp))) { var op = match[0]; if (assignOps[op] && input[op.length] == '=') { token.type = ASSIGN; token.assignOp = GLOBAL[opTypeNames[op]]; match[0] += '='; } else { token.type = GLOBAL[opTypeNames[op]]; if (this.scanOperand && (token.type == PLUS || token.type == MINUS)) { token.type += UNARY_PLUS - PLUS; } token.assignOp = null; } //debug('token.value => '+op+', token.type => '+token.type); token.value = op; } else { throw this.newSyntaxError("Illegal token"); } token.start = this.cursor; this.cursor += match[0].length; token.end = this.cursor; token.lineno = this.lineno; return token.type; }, unget: function () { if (++this.lookahead == 4) throw "PANIC: too much lookahead!"; this.tokenIndex = (this.tokenIndex - 1) & 3; }, newSyntaxError: function (m) { var e = new SyntaxError(m, this.filename, this.lineno); e.source = this.source; e.cursor = this.cursor; return e; } }; function CompilerContext(inFunction) { this.inFunction = inFunction; this.stmtStack = []; this.funDecls = []; this.varDecls = []; } var CCp = CompilerContext.prototype; CCp.bracketLevel = CCp.curlyLevel = CCp.parenLevel = CCp.hookLevel = 0; CCp.ecmaStrictMode = CCp.inForLoopInit = false; function Script(t, x) { var n = Statements(t, x); n.type = SCRIPT; n.funDecls = x.funDecls; n.varDecls = x.varDecls; return n; } // Node extends Array, which we extend slightly with a top-of-stack method. Array.prototype.top = function() { return this.length && this[this.length-1]; } function NarcNode(t, type) { var token = t.token(); if (token) { this.type = type || token.type; this.value = token.value; this.lineno = token.lineno; this.start = token.start; this.end = token.end; } else { this.type = type; this.lineno = t.lineno; } this.tokenizer = t; for (var i = 2; i < arguments.length; i++) this.push(arguments[i]); } var Np = NarcNode.prototype = new Array(); Np.constructor = NarcNode; Np.$length = 0; Np.toSource = Object.prototype.toSource; // Always use push to add operands to an expression, to update start and end. Np.push = function (kid) { if (kid.start < this.start) this.start = kid.start; if (this.end < kid.end) this.end = kid.end; //debug('length before => '+this.$length); this[this.$length] = kid; this.$length++; //debug('length after => '+this.$length); } NarcNode.indentLevel = 0; function tokenstr(tt) { var t = tokens[tt]; return /^\W/.test(t) ? opTypeNames[t] : t.toUpperCase(); } Np.toString = function () { var a = []; for (var i in this) { if (this.hasOwnProperty(i) && i != 'type') a.push({id: i, value: this[i]}); } a.sort(function (a,b) { return (a.id < b.id) ? -1 : 1; }); INDENTATION = " "; var n = ++NarcNode.indentLevel; var s = "{\n" + INDENTATION.repeat(n) + "type: " + tokenstr(this.type); for (i = 0; i < a.length; i++) s += ",\n" + INDENTATION.repeat(n) + a[i].id + ": " + a[i].value; n = --NarcNode.indentLevel; s += "\n" + INDENTATION.repeat(n) + "}"; return s; } Np.getSource = function () { return this.tokenizer.source.slice(this.start, this.end); }; Np.filename = function () { return this.tokenizer.filename; }; String.prototype.repeat = function (n) { var s = "", t = this + s; while (--n >= 0) s += t; return s; } // Statement stack and nested statement handler. function nest(t, x, node, func, end) { x.stmtStack.push(node); var n = func(t, x); x.stmtStack.pop(); end && t.mustMatch(end); return n; } function Statements(t, x) { var n = new NarcNode(t, BLOCK); x.stmtStack.push(n); while (!t.done() && t.peek() != RIGHT_CURLY) n.push(Statement(t, x)); x.stmtStack.pop(); return n; } function Block(t, x) { t.mustMatch(LEFT_CURLY); var n = Statements(t, x); t.mustMatch(RIGHT_CURLY); return n; } DECLARED_FORM = 0; EXPRESSED_FORM = 1; STATEMENT_FORM = 2; function Statement(t, x) { var i, label, n, n2, ss, tt = t.get(); // Cases for statements ending in a right curly return early, avoiding the // common semicolon insertion magic after this switch. switch (tt) { case FUNCTION: return FunctionDefinition(t, x, true, (x.stmtStack.length > 1) ? STATEMENT_FORM : DECLARED_FORM); case LEFT_CURLY: n = Statements(t, x); t.mustMatch(RIGHT_CURLY); return n; case IF: n = new NarcNode(t); n.condition = ParenExpression(t, x); x.stmtStack.push(n); n.thenPart = Statement(t, x); n.elsePart = t.match(ELSE) ? Statement(t, x) : null; x.stmtStack.pop(); return n; case SWITCH: n = new NarcNode(t); t.mustMatch(LEFT_PAREN); n.discriminant = Expression(t, x); t.mustMatch(RIGHT_PAREN); n.cases = []; n.defaultIndex = -1; x.stmtStack.push(n); t.mustMatch(LEFT_CURLY); while ((tt = t.get()) != RIGHT_CURLY) { switch (tt) { case DEFAULT: if (n.defaultIndex >= 0) throw t.newSyntaxError("More than one switch default"); // FALL THROUGH case CASE: n2 = new NarcNode(t); if (tt == DEFAULT) n.defaultIndex = n.cases.length; else n2.caseLabel = Expression(t, x, COLON); break; default: throw t.newSyntaxError("Invalid switch case"); } t.mustMatch(COLON); n2.statements = new NarcNode(t, BLOCK); while ((tt=t.peek()) != CASE && tt != DEFAULT && tt != RIGHT_CURLY) n2.statements.push(Statement(t, x)); n.cases.push(n2); } x.stmtStack.pop(); return n; case FOR: n = new NarcNode(t); n.isLoop = true; t.mustMatch(LEFT_PAREN); if ((tt = t.peek()) != SEMICOLON) { x.inForLoopInit = true; if (tt == VAR || tt == CONST) { t.get(); n2 = Variables(t, x); } else { n2 = Expression(t, x); } x.inForLoopInit = false; } if (n2 && t.match(IN)) { n.type = FOR_IN; if (n2.type == VAR) { if (n2.$length != 1) { throw new SyntaxError("Invalid for..in left-hand side", t.filename, n2.lineno); } // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name. n.iterator = n2[0]; n.varDecl = n2; } else { n.iterator = n2; n.varDecl = null; } n.object = Expression(t, x); } else { n.setup = n2 || null; t.mustMatch(SEMICOLON); n.condition = (t.peek() == SEMICOLON) ? null : Expression(t, x); t.mustMatch(SEMICOLON); n.update = (t.peek() == RIGHT_PAREN) ? null : Expression(t, x); } t.mustMatch(RIGHT_PAREN); n.body = nest(t, x, n, Statement); return n; case WHILE: n = new NarcNode(t); n.isLoop = true; n.condition = ParenExpression(t, x); n.body = nest(t, x, n, Statement); return n; case DO: n = new NarcNode(t); n.isLoop = true; n.body = nest(t, x, n, Statement, WHILE); n.condition = ParenExpression(t, x); if (!x.ecmaStrictMode) { //