/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * The contents of this file are subject to the Netscape 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/NPL/ * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express oqr * implied. See the License for the specific language governing * rights and limitations under the License. * * The Original Code is Rhino code, released * May 6, 1999. * * The Initial Developer of the Original Code is Netscape * Communications Corporation. Portions created by Netscape are * Copyright (C) 1997-1999 Netscape Communications Corporation. All * Rights Reserved. * * Contributor(s): * Mike Ang * Mike McCabe * * Alternatively, the contents of this file may be used under the * terms of the GNU Public License (the "GPL"), in which case the * provisions of the GPL are applicable instead of those above. * If you wish to allow use of your version of this file only * under the terms of the GPL and not to allow others to use your * version of this file under the NPL, indicate your decision by * deleting the provisions above and replace them with the notice * and other provisions required by the GPL. If you do not delete * the provisions above, a recipient may use your version of this * file under either the NPL or the GPL. */ // Modified by Google package com.google.gwt.dev.js.rhino; import java.io.IOException; /** * This class implements the JavaScript parser. * * It is based on the C source files jsparse.c and jsparse.h in the jsref * package. * * @see TokenStream * * @author Mike McCabe * @author Brendan Eich */ public class Parser { public Parser(IRFactory nf) { this.nf = nf; } private void mustMatchToken(TokenStream ts, int toMatch, String messageId) throws IOException, JavaScriptException { int tt; if ((tt = ts.getToken()) != toMatch) { reportError(ts, messageId); ts.ungetToken(tt); // In case the parser decides to continue } } private void reportError(TokenStream ts, String messageId) throws JavaScriptException { this.ok = false; ts.reportSyntaxError(messageId, null); /* * Throw an exception to unwind the recursive descent parse. We use * JavaScriptException here even though it is really a different use of the * exception than it is usually used for. */ throw new JavaScriptException(messageId); } /* * Build a parse tree from the given TokenStream. * * @param ts the TokenStream to parse * * @return an Object representing the parsed program. If the parse fails, null * will be returned. (The parse failure will result in a call to the current * Context's ErrorReporter.) */ public Object parse(TokenStream ts) throws IOException { this.ok = true; sourceTop = 0; functionNumber = 0; int tt; // last token from getToken(); int baseLineno = ts.getLineno(); // line number where source starts /* * so we have something to add nodes to until we've collected all the source */ Object tempBlock = nf.createLeaf(TokenStream.BLOCK); ((Node) tempBlock).setIsSyntheticBlock(true); while (true) { ts.flags |= ts.TSF_REGEXP; tt = ts.getToken(); ts.flags &= ~ts.TSF_REGEXP; if (tt <= ts.EOF) { break; } if (tt == ts.FUNCTION) { try { nf.addChildToBack(tempBlock, function(ts, false)); } catch (JavaScriptException e) { this.ok = false; break; } } else { ts.ungetToken(tt); nf.addChildToBack(tempBlock, statement(ts)); } } if (!this.ok) { // XXX ts.clearPushback() call here? return null; } Object pn = nf.createScript(tempBlock, ts.getSourceName(), baseLineno, ts .getLineno(), sourceToString(0)); ((Node) pn).setIsSyntheticBlock(true); return pn; } /* * The C version of this function takes an argument list, which doesn't seem * to be needed for tree generation... it'd only be useful for checking * argument hiding, which I'm not doing anyway... */ private Object parseFunctionBody(TokenStream ts) throws IOException { int oldflags = ts.flags; ts.flags &= ~(TokenStream.TSF_RETURN_EXPR | TokenStream.TSF_RETURN_VOID); ts.flags |= TokenStream.TSF_FUNCTION; Object pn = nf.createBlock(ts.getLineno()); try { int tt; while ((tt = ts.peekToken()) > ts.EOF && tt != ts.RC) { if (tt == TokenStream.FUNCTION) { ts.getToken(); nf.addChildToBack(pn, function(ts, false)); } else { nf.addChildToBack(pn, statement(ts)); } } } catch (JavaScriptException e) { this.ok = false; } finally { // also in finally block: // flushNewLines, clearPushback. ts.flags = oldflags; } return pn; } private Object function(TokenStream ts, boolean isExpr) throws IOException, JavaScriptException { int baseLineno = ts.getLineno(); // line number where source starts String name; Object memberExprNode = null; if (ts.matchToken(ts.NAME)) { name = ts.getString(); if (!ts.matchToken(ts.LP)) { if (Context.getContext().hasFeature( Context.FEATURE_MEMBER_EXPR_AS_FUNCTION_NAME)) { // Extension to ECMA: if 'function <name>' does not follow // by '(', assume <name> starts memberExpr sourceAddString(ts.NAME, name); Object memberExprHead = nf.createName(name); name = null; memberExprNode = memberExprTail(ts, false, memberExprHead); } mustMatchToken(ts, ts.LP, "msg.no.paren.parms"); } } else if (ts.matchToken(ts.LP)) { // Anonymous function name = null; } else { name = null; if (Context.getContext().hasFeature( Context.FEATURE_MEMBER_EXPR_AS_FUNCTION_NAME)) { // Note that memberExpr can not start with '(' like // in (1+2).toString, because 'function (' already // processed as anonymous function memberExprNode = memberExpr(ts, false); } mustMatchToken(ts, ts.LP, "msg.no.paren.parms"); } if (memberExprNode != null) { // transform 'function' <memberExpr> to <memberExpr> = function // even in the decompilated source sourceAdd((char) ts.ASSIGN); sourceAdd((char) ts.NOP); } // save a reference to the function in the enclosing source. sourceAdd((char) ts.FUNCTION); sourceAdd((char) functionNumber); ++functionNumber; // Save current source top to restore it on exit not to include // function to parent source int savedSourceTop = sourceTop; int savedFunctionNumber = functionNumber; Object args; Object body; String source; try { functionNumber = 0; // FUNCTION as the first token in a Source means it's a function // definition, and not a reference. sourceAdd((char) ts.FUNCTION); if (name != null) { sourceAddString(ts.NAME, name); } sourceAdd((char) ts.LP); args = nf.createLeaf(ts.LP); if (!ts.matchToken(ts.GWT)) { boolean first = true; do { if (!first) sourceAdd((char) ts.COMMA); first = false; mustMatchToken(ts, ts.NAME, "msg.no.parm"); String s = ts.getString(); nf.addChildToBack(args, nf.createName(s)); sourceAddString(ts.NAME, s); } while (ts.matchToken(ts.COMMA)); mustMatchToken(ts, ts.GWT, "msg.no.paren.after.parms"); } sourceAdd((char) ts.GWT); mustMatchToken(ts, ts.LC, "msg.no.brace.body"); sourceAdd((char) ts.LC); sourceAdd((char) ts.EOL); body = parseFunctionBody(ts); mustMatchToken(ts, ts.RC, "msg.no.brace.after.body"); sourceAdd((char) ts.RC); // skip the last EOL so nested functions work... // name might be null; source = sourceToString(savedSourceTop); } finally { sourceTop = savedSourceTop; functionNumber = savedFunctionNumber; } Object pn = nf.createFunction(name, args, body, ts.getSourceName(), baseLineno, ts.getLineno(), source, isExpr || memberExprNode != null); if (memberExprNode != null) { pn = nf.createBinary(ts.ASSIGN, ts.NOP, memberExprNode, pn); } // Add EOL but only if function is not part of expression, in which // case it gets SEMI + EOL from Statement. if (!isExpr) { if (memberExprNode != null) { // Add ';' to make 'function x.f(){}' and 'x.f = function(){}' // to print the same strings when decompiling sourceAdd((char) ts.SEMI); } sourceAdd((char) ts.EOL); wellTerminated(ts, ts.FUNCTION); } return pn; } private Object statements(TokenStream ts) throws IOException { Object pn = nf.createBlock(ts.getLineno()); int tt; while ((tt = ts.peekToken()) > ts.EOF && tt != ts.RC) { nf.addChildToBack(pn, statement(ts)); } return pn; } private Object condition(TokenStream ts) throws IOException, JavaScriptException { Object pn; mustMatchToken(ts, ts.LP, "msg.no.paren.cond"); sourceAdd((char) ts.LP); pn = expr(ts, false); mustMatchToken(ts, ts.GWT, "msg.no.paren.after.cond"); sourceAdd((char) ts.GWT); // there's a check here in jsparse.c that corrects = to == return pn; } private boolean wellTerminated(TokenStream ts, int lastExprType) throws IOException, JavaScriptException { int tt = ts.peekTokenSameLine(); if (tt == ts.ERROR) { return false; } if (tt != ts.EOF && tt != ts.EOL && tt != ts.SEMI && tt != ts.RC) { int version = Context.getContext().getLanguageVersion(); if ((tt == ts.FUNCTION || lastExprType == ts.FUNCTION) && (version < Context.VERSION_1_2)) { /* * Checking against version < 1.2 and version >= 1.0 in the above line * breaks old javascript, so we keep it this way for now... XXX warning * needed? */ return true; } else { reportError(ts, "msg.no.semi.stmt"); } } return true; } // match a NAME; return null if no match. private String matchLabel(TokenStream ts) throws IOException, JavaScriptException { int lineno = ts.getLineno(); String label = null; int tt; tt = ts.peekTokenSameLine(); if (tt == ts.NAME) { ts.getToken(); label = ts.getString(); } if (lineno == ts.getLineno()) wellTerminated(ts, ts.ERROR); return label; } private Object statement(TokenStream ts) throws IOException { try { return statementHelper(ts); } catch (JavaScriptException e) { // skip to end of statement int lineno = ts.getLineno(); int t; do { t = ts.getToken(); } while (t != TokenStream.SEMI && t != TokenStream.EOL && t != TokenStream.EOF && t != TokenStream.ERROR); return nf.createExprStatement(nf.createName("error"), lineno); } } /** * Whether the "catch (e: e instanceof Exception) { ... }" syntax is * implemented. */ private Object statementHelper(TokenStream ts) throws IOException, JavaScriptException { Object pn = null; // If skipsemi == true, don't add SEMI + EOL to source at the // end of this statment. For compound statements, IF/FOR etc. boolean skipsemi = false; int tt; int lastExprType = 0; // For wellTerminated. 0 to avoid warning. tt = ts.getToken(); switch (tt) { case TokenStream.IF: { skipsemi = true; sourceAdd((char) ts.IF); int lineno = ts.getLineno(); Object cond = condition(ts); sourceAdd((char) ts.LC); sourceAdd((char) ts.EOL); Object ifTrue = statement(ts); Object ifFalse = null; if (ts.matchToken(ts.ELSE)) { sourceAdd((char) ts.RC); sourceAdd((char) ts.ELSE); sourceAdd((char) ts.LC); sourceAdd((char) ts.EOL); ifFalse = statement(ts); } sourceAdd((char) ts.RC); sourceAdd((char) ts.EOL); pn = nf.createIf(cond, ifTrue, ifFalse, lineno); break; } case TokenStream.SWITCH: { skipsemi = true; sourceAdd((char) ts.SWITCH); pn = nf.createSwitch(ts.getLineno()); Object cur_case = null; // to kill warning Object case_statements; mustMatchToken(ts, ts.LP, "msg.no.paren.switch"); sourceAdd((char) ts.LP); nf.addChildToBack(pn, expr(ts, false)); mustMatchToken(ts, ts.GWT, "msg.no.paren.after.switch"); sourceAdd((char) ts.GWT); mustMatchToken(ts, ts.LC, "msg.no.brace.switch"); sourceAdd((char) ts.LC); sourceAdd((char) ts.EOL); while ((tt = ts.getToken()) != ts.RC && tt != ts.EOF) { switch (tt) { case TokenStream.CASE: sourceAdd((char) ts.CASE); cur_case = nf.createUnary(ts.CASE, expr(ts, false)); sourceAdd((char) ts.COLON); sourceAdd((char) ts.EOL); break; case TokenStream.DEFAULT: cur_case = nf.createLeaf(ts.DEFAULT); sourceAdd((char) ts.DEFAULT); sourceAdd((char) ts.COLON); sourceAdd((char) ts.EOL); // XXX check that there isn't more than one default break; default: reportError(ts, "msg.bad.switch"); break; } mustMatchToken(ts, ts.COLON, "msg.no.colon.case"); case_statements = nf.createLeaf(TokenStream.BLOCK); ((Node) case_statements).setIsSyntheticBlock(true); while ((tt = ts.peekToken()) != ts.RC && tt != ts.CASE && tt != ts.DEFAULT && tt != ts.EOF) { nf.addChildToBack(case_statements, statement(ts)); } // assert cur_case nf.addChildToBack(cur_case, case_statements); nf.addChildToBack(pn, cur_case); } sourceAdd((char) ts.RC); sourceAdd((char) ts.EOL); break; } case TokenStream.WHILE: { skipsemi = true; sourceAdd((char) ts.WHILE); int lineno = ts.getLineno(); Object cond = condition(ts); sourceAdd((char) ts.LC); sourceAdd((char) ts.EOL); Object body = statement(ts); sourceAdd((char) ts.RC); sourceAdd((char) ts.EOL); pn = nf.createWhile(cond, body, lineno); break; } case TokenStream.DO: { sourceAdd((char) ts.DO); sourceAdd((char) ts.LC); sourceAdd((char) ts.EOL); int lineno = ts.getLineno(); Object body = statement(ts); sourceAdd((char) ts.RC); mustMatchToken(ts, ts.WHILE, "msg.no.while.do"); sourceAdd((char) ts.WHILE); Object cond = condition(ts); pn = nf.createDoWhile(body, cond, lineno); break; } case TokenStream.FOR: { skipsemi = true; sourceAdd((char) ts.FOR); int lineno = ts.getLineno(); Object init; // Node init is also foo in 'foo in Object' Object cond; // Node cond is also object in 'foo in Object' Object incr = null; // to kill warning Object body; mustMatchToken(ts, ts.LP, "msg.no.paren.for"); sourceAdd((char) ts.LP); tt = ts.peekToken(); if (tt == ts.SEMI) { init = nf.createLeaf(ts.VOID); } else { if (tt == ts.VAR) { // set init to a var list or initial ts.getToken(); // throw away the 'var' token init = variables(ts, true); } else { init = expr(ts, true); } } tt = ts.peekToken(); if (tt == ts.RELOP && ts.getOp() == ts.IN) { ts.matchToken(ts.RELOP); sourceAdd((char) ts.IN); // 'cond' is the object over which we're iterating cond = expr(ts, false); } else { // ordinary for loop mustMatchToken(ts, ts.SEMI, "msg.no.semi.for"); sourceAdd((char) ts.SEMI); if (ts.peekToken() == ts.SEMI) { // no loop condition cond = nf.createLeaf(ts.VOID); } else { cond = expr(ts, false); } mustMatchToken(ts, ts.SEMI, "msg.no.semi.for.cond"); sourceAdd((char) ts.SEMI); if (ts.peekToken() == ts.GWT) { incr = nf.createLeaf(ts.VOID); } else { incr = expr(ts, false); } } mustMatchToken(ts, ts.GWT, "msg.no.paren.for.ctrl"); sourceAdd((char) ts.GWT); sourceAdd((char) ts.LC); sourceAdd((char) ts.EOL); body = statement(ts); sourceAdd((char) ts.RC); sourceAdd((char) ts.EOL); if (incr == null) { // cond could be null if 'in obj' got eaten by the init node. pn = nf.createForIn(init, cond, body, lineno); } else { pn = nf.createFor(init, cond, incr, body, lineno); } break; } case TokenStream.TRY: { int lineno = ts.getLineno(); Object tryblock; Object catchblocks = null; Object finallyblock = null; skipsemi = true; sourceAdd((char) ts.TRY); sourceAdd((char) ts.LC); sourceAdd((char) ts.EOL); tryblock = statement(ts); sourceAdd((char) ts.RC); sourceAdd((char) ts.EOL); catchblocks = nf.createLeaf(TokenStream.BLOCK); boolean sawDefaultCatch = false; int peek = ts.peekToken(); if (peek == ts.CATCH) { while (ts.matchToken(ts.CATCH)) { if (sawDefaultCatch) { reportError(ts, "msg.catch.unreachable"); } sourceAdd((char) ts.CATCH); mustMatchToken(ts, ts.LP, "msg.no.paren.catch"); sourceAdd((char) ts.LP); mustMatchToken(ts, ts.NAME, "msg.bad.catchcond"); String varName = ts.getString(); sourceAddString(ts.NAME, varName); Object catchCond = null; if (ts.matchToken(ts.IF)) { sourceAdd((char) ts.IF); catchCond = expr(ts, false); } else { sawDefaultCatch = true; } mustMatchToken(ts, ts.GWT, "msg.bad.catchcond"); sourceAdd((char) ts.GWT); mustMatchToken(ts, ts.LC, "msg.no.brace.catchblock"); sourceAdd((char) ts.LC); sourceAdd((char) ts.EOL); nf.addChildToBack(catchblocks, nf.createCatch(varName, catchCond, statements(ts), ts.getLineno())); mustMatchToken(ts, ts.RC, "msg.no.brace.after.body"); sourceAdd((char) ts.RC); sourceAdd((char) ts.EOL); } } else if (peek != ts.FINALLY) { mustMatchToken(ts, ts.FINALLY, "msg.try.no.catchfinally"); } if (ts.matchToken(ts.FINALLY)) { sourceAdd((char) ts.FINALLY); sourceAdd((char) ts.LC); sourceAdd((char) ts.EOL); finallyblock = statement(ts); sourceAdd((char) ts.RC); sourceAdd((char) ts.EOL); } pn = nf.createTryCatchFinally(tryblock, catchblocks, finallyblock, lineno); break; } case TokenStream.THROW: { int lineno = ts.getLineno(); sourceAdd((char) ts.THROW); pn = nf.createThrow(expr(ts, false), lineno); if (lineno == ts.getLineno()) wellTerminated(ts, ts.ERROR); break; } case TokenStream.BREAK: { int lineno = ts.getLineno(); sourceAdd((char) ts.BREAK); // matchLabel only matches if there is one String label = matchLabel(ts); if (label != null) { sourceAddString(ts.NAME, label); } pn = nf.createBreak(label, lineno); break; } case TokenStream.CONTINUE: { int lineno = ts.getLineno(); sourceAdd((char) ts.CONTINUE); // matchLabel only matches if there is one String label = matchLabel(ts); if (label != null) { sourceAddString(ts.NAME, label); } pn = nf.createContinue(label, lineno); break; } case TokenStream.DEBUGGER: { int lineno = ts.getLineno(); sourceAdd((char) ts.DEBUGGER); pn = nf.createDebugger(lineno); break; } case TokenStream.WITH: { // bruce: we don't support this is JSNI code because it's impossible // to identify bindings even passably well // reportError(ts, "msg.jsni.unsupported.with"); skipsemi = true; sourceAdd((char) ts.WITH); int lineno = ts.getLineno(); mustMatchToken(ts, ts.LP, "msg.no.paren.with"); sourceAdd((char) ts.LP); Object obj = expr(ts, false); mustMatchToken(ts, ts.GWT, "msg.no.paren.after.with"); sourceAdd((char) ts.GWT); sourceAdd((char) ts.LC); sourceAdd((char) ts.EOL); Object body = statement(ts); sourceAdd((char) ts.RC); sourceAdd((char) ts.EOL); pn = nf.createWith(obj, body, lineno); break; } case TokenStream.VAR: { int lineno = ts.getLineno(); pn = variables(ts, false); if (ts.getLineno() == lineno) wellTerminated(ts, ts.ERROR); break; } case TokenStream.RETURN: { Object retExpr = null; int lineno = 0; sourceAdd((char) ts.RETURN); // bail if we're not in a (toplevel) function if ((ts.flags & ts.TSF_FUNCTION) == 0) reportError(ts, "msg.bad.return"); /* This is ugly, but we don't want to require a semicolon. */ ts.flags |= ts.TSF_REGEXP; tt = ts.peekTokenSameLine(); ts.flags &= ~ts.TSF_REGEXP; if (tt != ts.EOF && tt != ts.EOL && tt != ts.SEMI && tt != ts.RC) { lineno = ts.getLineno(); retExpr = expr(ts, false); if (ts.getLineno() == lineno) wellTerminated(ts, ts.ERROR); ts.flags |= ts.TSF_RETURN_EXPR; } else { ts.flags |= ts.TSF_RETURN_VOID; } // XXX ASSERT pn pn = nf.createReturn(retExpr, lineno); break; } case TokenStream.LC: skipsemi = true; pn = statements(ts); mustMatchToken(ts, ts.RC, "msg.no.brace.block"); break; case TokenStream.ERROR: // Fall thru, to have a node for error recovery to work on case TokenStream.EOL: case TokenStream.SEMI: pn = nf.createLeaf(ts.VOID); skipsemi = true; break; default: { lastExprType = tt; int tokenno = ts.getTokenno(); ts.ungetToken(tt); int lineno = ts.getLineno(); pn = expr(ts, false); if (ts.peekToken() == ts.COLON) { /* * check that the last thing the tokenizer returned was a NAME and * that only one token was consumed. */ if (lastExprType != ts.NAME || (ts.getTokenno() != tokenno)) reportError(ts, "msg.bad.label"); ts.getToken(); // eat the COLON /* * in the C source, the label is associated with the statement that * follows: nf.addChildToBack(pn, statement(ts)); */ String name = ts.getString(); pn = nf.createLabel(name, lineno); // bruce: added to make it easier to bind labels to the // statements they modify // nf.addChildToBack(pn, statement(ts)); // depend on decompiling lookahead to guess that that // last name was a label. sourceAdd((char) ts.COLON); sourceAdd((char) ts.EOL); return pn; } if (lastExprType == ts.FUNCTION) { if (nf.getLeafType(pn) != ts.FUNCTION) { reportError(ts, "msg.syntax"); } } pn = nf.createExprStatement(pn, lineno); /* * Check explicitly against (multi-line) function statement. * * lastExprEndLine is a hack to fix an automatic semicolon insertion * problem with function expressions; the ts.getLineno() == lineno check * was firing after a function definition even though the next statement * was on a new line, because speculative getToken calls advanced the * line number even when they didn't succeed. */ if (ts.getLineno() == lineno || (lastExprType == ts.FUNCTION && ts.getLineno() == lastExprEndLine)) { wellTerminated(ts, lastExprType); } break; } } ts.matchToken(ts.SEMI); if (!skipsemi) { sourceAdd((char) ts.SEMI); sourceAdd((char) ts.EOL); } return pn; } private Object variables(TokenStream ts, boolean inForInit) throws IOException, JavaScriptException { Object pn = nf.createVariables(ts.getLineno()); boolean first = true; sourceAdd((char) ts.VAR); for (;;) { Object name; Object init; mustMatchToken(ts, ts.NAME, "msg.bad.var"); String s = ts.getString(); if (!first) sourceAdd((char) ts.COMMA); first = false; sourceAddString(ts.NAME, s); name = nf.createName(s); // omitted check for argument hiding if (ts.matchToken(ts.ASSIGN)) { if (ts.getOp() != ts.NOP) reportError(ts, "msg.bad.var.init"); sourceAdd((char) ts.ASSIGN); sourceAdd((char) ts.NOP); init = assignExpr(ts, inForInit); nf.addChildToBack(name, init); } nf.addChildToBack(pn, name); if (!ts.matchToken(ts.COMMA)) break; } return pn; } private Object expr(TokenStream ts, boolean inForInit) throws IOException, JavaScriptException { Object pn = assignExpr(ts, inForInit); while (ts.matchToken(ts.COMMA)) { sourceAdd((char) ts.COMMA); pn = nf.createBinary(ts.COMMA, pn, assignExpr(ts, inForInit)); } return pn; } private Object assignExpr(TokenStream ts, boolean inForInit) throws IOException, JavaScriptException { Object pn = condExpr(ts, inForInit); if (ts.matchToken(ts.ASSIGN)) { // omitted: "invalid assignment left-hand side" check. sourceAdd((char) ts.ASSIGN); sourceAdd((char) ts.getOp()); pn = nf .createBinary(ts.ASSIGN, ts.getOp(), pn, assignExpr(ts, inForInit)); } return pn; } private Object condExpr(TokenStream ts, boolean inForInit) throws IOException, JavaScriptException { Object ifTrue; Object ifFalse; Object pn = orExpr(ts, inForInit); if (ts.matchToken(ts.HOOK)) { sourceAdd((char) ts.HOOK); ifTrue = assignExpr(ts, false); mustMatchToken(ts, ts.COLON, "msg.no.colon.cond"); sourceAdd((char) ts.COLON); ifFalse = assignExpr(ts, inForInit); return nf.createTernary(pn, ifTrue, ifFalse); } return pn; } private Object orExpr(TokenStream ts, boolean inForInit) throws IOException, JavaScriptException { Object pn = andExpr(ts, inForInit); if (ts.matchToken(ts.OR)) { sourceAdd((char) ts.OR); pn = nf.createBinary(ts.OR, pn, orExpr(ts, inForInit)); } return pn; } private Object andExpr(TokenStream ts, boolean inForInit) throws IOException, JavaScriptException { Object pn = bitOrExpr(ts, inForInit); if (ts.matchToken(ts.AND)) { sourceAdd((char) ts.AND); pn = nf.createBinary(ts.AND, pn, andExpr(ts, inForInit)); } return pn; } private Object bitOrExpr(TokenStream ts, boolean inForInit) throws IOException, JavaScriptException { Object pn = bitXorExpr(ts, inForInit); while (ts.matchToken(ts.BITOR)) { sourceAdd((char) ts.BITOR); pn = nf.createBinary(ts.BITOR, pn, bitXorExpr(ts, inForInit)); } return pn; } private Object bitXorExpr(TokenStream ts, boolean inForInit) throws IOException, JavaScriptException { Object pn = bitAndExpr(ts, inForInit); while (ts.matchToken(ts.BITXOR)) { sourceAdd((char) ts.BITXOR); pn = nf.createBinary(ts.BITXOR, pn, bitAndExpr(ts, inForInit)); } return pn; } private Object bitAndExpr(TokenStream ts, boolean inForInit) throws IOException, JavaScriptException { Object pn = eqExpr(ts, inForInit); while (ts.matchToken(ts.BITAND)) { sourceAdd((char) ts.BITAND); pn = nf.createBinary(ts.BITAND, pn, eqExpr(ts, inForInit)); } return pn; } private Object eqExpr(TokenStream ts, boolean inForInit) throws IOException, JavaScriptException { Object pn = relExpr(ts, inForInit); while (ts.matchToken(ts.EQOP)) { sourceAdd((char) ts.EQOP); sourceAdd((char) ts.getOp()); pn = nf.createBinary(ts.EQOP, ts.getOp(), pn, relExpr(ts, inForInit)); } return pn; } private Object relExpr(TokenStream ts, boolean inForInit) throws IOException, JavaScriptException { Object pn = shiftExpr(ts); while (ts.matchToken(ts.RELOP)) { int op = ts.getOp(); if (inForInit && op == ts.IN) { ts.ungetToken(ts.RELOP); break; } sourceAdd((char) ts.RELOP); sourceAdd((char) op); pn = nf.createBinary(ts.RELOP, op, pn, shiftExpr(ts)); } return pn; } private Object shiftExpr(TokenStream ts) throws IOException, JavaScriptException { Object pn = addExpr(ts); while (ts.matchToken(ts.SHOP)) { sourceAdd((char) ts.SHOP); sourceAdd((char) ts.getOp()); pn = nf.createBinary(ts.SHOP, ts.getOp(), pn, addExpr(ts)); } return pn; } private Object addExpr(TokenStream ts) throws IOException, JavaScriptException { int tt; Object pn = mulExpr(ts); while ((tt = ts.getToken()) == ts.ADD || tt == ts.SUB) { sourceAdd((char) tt); // flushNewLines pn = nf.createBinary(tt, pn, mulExpr(ts)); } ts.ungetToken(tt); return pn; } private Object mulExpr(TokenStream ts) throws IOException, JavaScriptException { int tt; Object pn = unaryExpr(ts); while ((tt = ts.peekToken()) == ts.MUL || tt == ts.DIV || tt == ts.MOD) { tt = ts.getToken(); sourceAdd((char) tt); pn = nf.createBinary(tt, pn, unaryExpr(ts)); } return pn; } private Object unaryExpr(TokenStream ts) throws IOException, JavaScriptException { int tt; ts.flags |= ts.TSF_REGEXP; tt = ts.getToken(); ts.flags &= ~ts.TSF_REGEXP; switch (tt) { case TokenStream.UNARYOP: sourceAdd((char) ts.UNARYOP); sourceAdd((char) ts.getOp()); return nf.createUnary(ts.UNARYOP, ts.getOp(), unaryExpr(ts)); case TokenStream.ADD: case TokenStream.SUB: sourceAdd((char) ts.UNARYOP); sourceAdd((char) tt); return nf.createUnary(ts.UNARYOP, tt, unaryExpr(ts)); case TokenStream.INC: case TokenStream.DEC: sourceAdd((char) tt); return nf.createUnary(tt, ts.PRE, memberExpr(ts, true)); case TokenStream.DELPROP: sourceAdd((char) ts.DELPROP); return nf.createUnary(ts.DELPROP, unaryExpr(ts)); case TokenStream.ERROR: break; default: ts.ungetToken(tt); int lineno = ts.getLineno(); Object pn = memberExpr(ts, true); /* * don't look across a newline boundary for a postfix incop. * * the rhino scanner seems to work differently than the js scanner here; * in js, it works to have the line number check precede the peekToken * calls. It'd be better if they had similar behavior... */ int peeked; if (((peeked = ts.peekToken()) == ts.INC || peeked == ts.DEC) && ts.getLineno() == lineno) { int pf = ts.getToken(); sourceAdd((char) pf); return nf.createUnary(pf, ts.POST, pn); } return pn; } return nf.createName("err"); // Only reached on error. Try to continue. } private Object argumentList(TokenStream ts, Object listNode) throws IOException, JavaScriptException { boolean matched; ts.flags |= ts.TSF_REGEXP; matched = ts.matchToken(ts.GWT); ts.flags &= ~ts.TSF_REGEXP; if (!matched) { boolean first = true; do { if (!first) sourceAdd((char) ts.COMMA); first = false; nf.addChildToBack(listNode, assignExpr(ts, false)); } while (ts.matchToken(ts.COMMA)); mustMatchToken(ts, ts.GWT, "msg.no.paren.arg"); } sourceAdd((char) ts.GWT); return listNode; } private Object memberExpr(TokenStream ts, boolean allowCallSyntax) throws IOException, JavaScriptException { int tt; Object pn; /* Check for new expressions. */ ts.flags |= ts.TSF_REGEXP; tt = ts.peekToken(); ts.flags &= ~ts.TSF_REGEXP; if (tt == ts.NEW) { /* Eat the NEW token. */ ts.getToken(); sourceAdd((char) ts.NEW); /* Make a NEW node to append to. */ pn = nf.createLeaf(ts.NEW); nf.addChildToBack(pn, memberExpr(ts, false)); if (ts.matchToken(ts.LP)) { sourceAdd((char) ts.LP); /* Add the arguments to pn, if any are supplied. */ pn = argumentList(ts, pn); } /* * XXX there's a check in the C source against "too many constructor * arguments" - how many do we claim to support? */ /* * Experimental syntax: allow an object literal to follow a new * expression, which will mean a kind of anonymous class built with the * JavaAdapter. the object literal will be passed as an additional * argument to the constructor. */ tt = ts.peekToken(); if (tt == ts.LC) { nf.addChildToBack(pn, primaryExpr(ts)); } } else { pn = primaryExpr(ts); } return memberExprTail(ts, allowCallSyntax, pn); } private Object memberExprTail(TokenStream ts, boolean allowCallSyntax, Object pn) throws IOException, JavaScriptException { lastExprEndLine = ts.getLineno(); int tt; while ((tt = ts.getToken()) > ts.EOF) { if (tt == ts.DOT) { sourceAdd((char) ts.DOT); mustMatchToken(ts, ts.NAME, "msg.no.name.after.dot"); String s = ts.getString(); sourceAddString(ts.NAME, s); pn = nf.createBinary(ts.DOT, pn, nf.createName(ts.getString())); /* * pn = nf.createBinary(ts.DOT, pn, memberExpr(ts)) is the version in * Brendan's IR C version. Not in ECMA... does it reflect the 'new' * operator syntax he mentioned? */ lastExprEndLine = ts.getLineno(); } else if (tt == ts.LB) { sourceAdd((char) ts.LB); pn = nf.createBinary(ts.LB, pn, expr(ts, false)); mustMatchToken(ts, ts.RB, "msg.no.bracket.index"); sourceAdd((char) ts.RB); lastExprEndLine = ts.getLineno(); } else if (allowCallSyntax && tt == ts.LP) { /* make a call node */ pn = nf.createUnary(ts.CALL, pn); sourceAdd((char) ts.LP); /* Add the arguments to pn, if any are supplied. */ pn = argumentList(ts, pn); lastExprEndLine = ts.getLineno(); } else { ts.ungetToken(tt); break; } } return pn; } private Object primaryExpr(TokenStream ts) throws IOException, JavaScriptException { int tt; Object pn; ts.flags |= ts.TSF_REGEXP; tt = ts.getToken(); ts.flags &= ~ts.TSF_REGEXP; switch (tt) { case TokenStream.FUNCTION: return function(ts, true); case TokenStream.LB: { sourceAdd((char) ts.LB); pn = nf.createLeaf(ts.ARRAYLIT); ts.flags |= ts.TSF_REGEXP; boolean matched = ts.matchToken(ts.RB); ts.flags &= ~ts.TSF_REGEXP; if (!matched) { boolean first = true; do { ts.flags |= ts.TSF_REGEXP; tt = ts.peekToken(); ts.flags &= ~ts.TSF_REGEXP; if (!first) sourceAdd((char) ts.COMMA); else first = false; if (tt == ts.RB) { // to fix [,,,].length behavior... break; } if (tt == ts.COMMA) { nf.addChildToBack(pn, nf.createLeaf(ts.PRIMARY, ts.UNDEFINED)); } else { nf.addChildToBack(pn, assignExpr(ts, false)); } } while (ts.matchToken(ts.COMMA)); mustMatchToken(ts, ts.RB, "msg.no.bracket.arg"); } sourceAdd((char) ts.RB); return nf.createArrayLiteral(pn); } case TokenStream.LC: { pn = nf.createLeaf(ts.OBJLIT); sourceAdd((char) ts.LC); if (!ts.matchToken(ts.RC)) { boolean first = true; commaloop : do { Object property; if (!first) sourceAdd((char) ts.COMMA); else first = false; tt = ts.getToken(); switch (tt) { // map NAMEs to STRINGs in object literal context. case TokenStream.NAME: case TokenStream.STRING: String s = ts.getString(); sourceAddString(ts.NAME, s); property = nf.createString(ts.getString()); break; case TokenStream.NUMBER: double n = ts.getNumber(); sourceAddNumber(n); property = nf.createNumber(n); break; case TokenStream.RC: // trailing comma is OK. ts.ungetToken(tt); break commaloop; default: reportError(ts, "msg.bad.prop"); break commaloop; } mustMatchToken(ts, ts.COLON, "msg.no.colon.prop"); // OBJLIT is used as ':' in object literal for // decompilation to solve spacing ambiguity. sourceAdd((char) ts.OBJLIT); nf.addChildToBack(pn, property); nf.addChildToBack(pn, assignExpr(ts, false)); } while (ts.matchToken(ts.COMMA)); mustMatchToken(ts, ts.RC, "msg.no.brace.prop"); } sourceAdd((char) ts.RC); return nf.createObjectLiteral(pn); } case TokenStream.LP: /* * Brendan's IR-jsparse.c makes a new node tagged with TOK_LP here... * I'm not sure I understand why. Isn't the grouping already implicit in * the structure of the parse tree? also TOK_LP is already overloaded (I * think) in the C IR as 'function call.' */ sourceAdd((char) ts.LP); pn = expr(ts, false); sourceAdd((char) ts.GWT); mustMatchToken(ts, ts.GWT, "msg.no.paren"); return pn; case TokenStream.NAME: String name = ts.getString(); sourceAddString(ts.NAME, name); return nf.createName(name); case TokenStream.NUMBER: double n = ts.getNumber(); sourceAddNumber(n); return nf.createNumber(n); case TokenStream.STRING: String s = ts.getString(); sourceAddString(ts.STRING, s); return nf.createString(s); case TokenStream.REGEXP: { String flags = ts.regExpFlags; ts.regExpFlags = null; String re = ts.getString(); sourceAddString(ts.REGEXP, '/' + re + '/' + flags); return nf.createRegExp(re, flags); } case TokenStream.PRIMARY: sourceAdd((char) ts.PRIMARY); sourceAdd((char) ts.getOp()); return nf.createLeaf(ts.PRIMARY, ts.getOp()); case TokenStream.RESERVED: reportError(ts, "msg.reserved.id"); break; case TokenStream.ERROR: /* the scanner or one of its subroutines reported the error. */ break; default: reportError(ts, "msg.syntax"); break; } return null; // should never reach here } /** * The following methods save decompilation information about the source. * Source information is returned from the parser as a String associated with * function nodes and with the toplevel script. When saved in the constant * pool of a class, this string will be UTF-8 encoded, and token values will * occupy a single byte. * * Source is saved (mostly) as token numbers. The tokens saved pretty much * correspond to the token stream of a 'canonical' representation of the input * program, as directed by the parser. (There were a few cases where tokens * could have been left out where decompiler could easily reconstruct them, * but I left them in for clarity). (I also looked adding source collection to * TokenStream instead, where I could have limited the changes to a few lines * in getToken... but this wouldn't have saved any space in the resulting * source representation, and would have meant that I'd have to duplicate * parser logic in the decompiler to disambiguate situations where newlines * are important.) NativeFunction.decompile expands the tokens back into their * string representations, using simple lookahead to correct spacing and * indentation. * * Token types with associated ops (ASSIGN, SHOP, PRIMARY, etc.) are saved as * two-token pairs. Number tokens are stored inline, as a NUMBER token, a * character representing the type, and either 1 or 4 characters representing * the bit-encoding of the number. String types NAME, STRING and OBJECT are * currently stored as a token type, followed by a character giving the length * of the string (assumed to be less than 2^16), followed by the characters of * the string inlined into the source string. Changing this to some reference * to to the string in the compiled class' constant pool would probably save a * lot of space... but would require some method of deriving the final * constant pool entry from information available at parse time. * * Nested functions need a similar mechanism... fortunately the nested * functions for a given function are generated in source order. Nested * functions are encoded as FUNCTION followed by a function number (encoded as * a character), which is enough information to find the proper generated * NativeFunction instance. * */ private void sourceAdd(char c) { if (sourceTop == sourceBuffer.length) { increaseSourceCapacity(sourceTop + 1); } sourceBuffer[sourceTop] = c; ++sourceTop; } private void sourceAddString(int type, String str) { int L = str.length(); // java string length < 2^16? if (Context.check && L > Character.MAX_VALUE) Context.codeBug(); if (sourceTop + L + 2 > sourceBuffer.length) { increaseSourceCapacity(sourceTop + L + 2); } sourceAdd((char) type); sourceAdd((char) L); str.getChars(0, L, sourceBuffer, sourceTop); sourceTop += L; } private void sourceAddNumber(double n) { sourceAdd((char) TokenStream.NUMBER); /* * encode the number in the source stream. Save as NUMBER type (char | char * char char char) where type is 'D' - double, 'S' - short, 'J' - long. * * We need to retain float vs. integer type info to keep the behavior of * liveconnect type-guessing the same after decompilation. (Liveconnect * tries to present 1.0 to Java as a float/double) OPT: This is no longer * true. We could compress the format. * * This may not be the most space-efficient encoding; the chars created * below may take up to 3 bytes in constant pool UTF-8 encoding, so a Double * could take up to 12 bytes. */ long lbits = (long) n; if (lbits != n) { // if it's floating point, save as a Double bit pattern. // (12/15/97 our scanner only returns Double for f.p.) lbits = Double.doubleToLongBits(n); sourceAdd('D'); sourceAdd((char) (lbits >> 48)); sourceAdd((char) (lbits >> 32)); sourceAdd((char) (lbits >> 16)); sourceAdd((char) lbits); } else { // we can ignore negative values, bc they're already prefixed // by UNARYOP SUB if (Context.check && lbits < 0) Context.codeBug(); // will it fit in a char? // this gives a short encoding for integer values up to 2^16. if (lbits <= Character.MAX_VALUE) { sourceAdd('S'); sourceAdd((char) lbits); } else { // Integral, but won't fit in a char. Store as a long. sourceAdd('J'); sourceAdd((char) (lbits >> 48)); sourceAdd((char) (lbits >> 32)); sourceAdd((char) (lbits >> 16)); sourceAdd((char) lbits); } } } private void increaseSourceCapacity(int minimalCapacity) { // Call this only when capacity increase is must if (Context.check && minimalCapacity <= sourceBuffer.length) Context.codeBug(); int newCapacity = sourceBuffer.length * 2; if (newCapacity < minimalCapacity) { newCapacity = minimalCapacity; } char[] tmp = new char[newCapacity]; System.arraycopy(sourceBuffer, 0, tmp, 0, sourceTop); sourceBuffer = tmp; } private String sourceToString(int offset) { if (Context.check && (offset < 0 || sourceTop < offset)) Context.codeBug(); return new String(sourceBuffer, offset, sourceTop - offset); } private int lastExprEndLine; // Hack to handle function expr termination. private IRFactory nf; private ErrorReporter er; private boolean ok; // Did the parse encounter an error? private char[] sourceBuffer = new char[128]; private int sourceTop; private int functionNumber; }