package loop; import loop.Token.Kind; import loop.ast.*; import loop.ast.script.ArgDeclList; import loop.ast.script.FunctionDecl; import loop.ast.script.ModuleDecl; import loop.ast.script.RequireDecl; import loop.ast.script.Unit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Takes the tokenized form of a raw string and converts it to a CoffeeScript parse tree (an * optimized form of its AST). * * @author dhanji@gmail.com (Dhanji R. Prasanna) */ public class LexprParser implements Parser { private final List errors = new ArrayList(); private final List tokens; private Set aliasedModules; private Node last = null; private int i = 0; private Unit scope; private static final Set RIGHT_ASSOCIATIVE = new HashSet(); private static final Set LEFT_ASSOCIATIVE = new HashSet(); static { RIGHT_ASSOCIATIVE.add(Token.Kind.PLUS); RIGHT_ASSOCIATIVE.add(Token.Kind.MINUS); RIGHT_ASSOCIATIVE.add(Token.Kind.DIVIDE); RIGHT_ASSOCIATIVE.add(Token.Kind.STAR); RIGHT_ASSOCIATIVE.add(Token.Kind.MODULUS); RIGHT_ASSOCIATIVE.add(Token.Kind.AND); RIGHT_ASSOCIATIVE.add(Token.Kind.OR); RIGHT_ASSOCIATIVE.add(Token.Kind.NOT); RIGHT_ASSOCIATIVE.add(Token.Kind.EQUALS); RIGHT_ASSOCIATIVE.add(Token.Kind.LEQ); RIGHT_ASSOCIATIVE.add(Token.Kind.GEQ); RIGHT_ASSOCIATIVE.add(Token.Kind.LESSER); RIGHT_ASSOCIATIVE.add(Token.Kind.GREATER); LEFT_ASSOCIATIVE.add(Token.Kind.DOT); LEFT_ASSOCIATIVE.add(Token.Kind.UNARROW); } public LexprParser(List tokens) { this.tokens = tokens; } public LexprParser(List tokens, Unit shellScope) { this.tokens = tokens; this.scope = shellScope; } public List getErrors() { return errors; } public void addError(String message, Token token) { errors.add(new StaticError(message, token)); } public void addError(String message, int line, int column) { errors.add(new StaticError(message, line, column)); } /** * if := IF computation *

* assign := computation ASSIGN computation *

* computation := chain (op chain)+ chain := term call* *

* call := DOT|UNARROW IDENT (LPAREN RPAREN)? *

* term := (literal | variable) literal := (regex | string | number) variable := IDENT *

*

* Examples -------- *

* (assign) *

* x = "hi".tos().tos() x = 1 *

* (computation) *

* 1 + 2 1 + 2 - 3 * 4 1.int + 2.y() - 3.a.b * 4 *

* -------------------- *

* parse := module | require | line */ @Override public Node parse() { Node parsed = require(); if (null == parsed) { parsed = module(); } if (null == parsed) { parsed = line(); } return last = parsed; } /** * The top level parsing rule. Do not use parse() to parse entire programs, it is more for * one-line expressions. *

* script := module? require* (functionDecl | classDecl)* (computation EOL)* */ @Override public Unit script(String file) { chewEols(); ModuleDecl module = module(); if (null == module) module = ModuleDecl.DEFAULT; chewEols(); Unit unit = new Unit(file, module); scope = unit; RequireDecl require; do { require = require(); chewEols(); if (null != require) { if (unit.imports().contains(require) && require.alias == null) { addError("Duplicate module import: " + require.toSymbol(), require.sourceLine, require.sourceColumn); throw new LoopCompileException(); } unit.declare(require); } } while (require != null); FunctionDecl function; ClassDecl classDecl = null; do { function = functionDecl(); if (function == null) { classDecl = classDecl(); } chewEols(); if (null != function) { if (unit.resolveFunction(function.name(), false) != null) { addError("Duplicate function definition: " + function.name(), function.sourceLine, function.sourceColumn); throw new LoopCompileException(); } unit.declare(function); } else if (null != classDecl) { if (unit.getType(classDecl.name) != null) { addError("Duplicate type definition: " + classDecl.name, classDecl.sourceLine, classDecl.sourceColumn); throw new LoopCompileException(); } unit.declare(classDecl); } } while (function != null || classDecl != null); // Now slurp up any freeform expressions into the module initializer. Node expression; while ((expression = computation()) != null) { unit.addToInitializer(expression); if (match(Kind.EOL) == null) break; chewEols(); } chewEols(); if (i < tokens.size() && errors.isEmpty()) { addError("Expected end of script, but additional statements found", tokens.get(i)); throw new LoopCompileException(); } scope = null; return unit; } private void chewEols() { // Chew up end-of-lines. //noinspection StatementWithEmptyBody while (match(Token.Kind.EOL) != null) ; } /*** Class parsing rules ***/ /** * Type declaration with inline constructors. */ public ClassDecl classDecl() { boolean isImmutable = match(Kind.IMMUTABLE) != null; if (match(Kind.CLASS) == null) { return null; } List className = match(Kind.TYPE_IDENT); if (null == className) { addError("Expected type identifier (Hint: Types must be upper CamelCase)", tokens.get(i)); throw new LoopCompileException(); } if (null == match(Kind.ARROW, Kind.LBRACE)) { addError("Expected '->' after type identifier", tokens.get(i)); throw new LoopCompileException(); } ClassDecl classDecl = new ClassDecl(className.iterator().next().value, isImmutable) .sourceLocation(className); Node line; do { chewEols(); withIndent(); line = line(); if (line != null) { classDecl.add(line); } chewEols(); } while (line != null); if (!endOfInput() && match(Token.Kind.RBRACE) == null) { addError("Expected end of type, additional statements found", tokens.get(i)); throw new LoopCompileException(); } return classDecl; } /** * Named function parsing rule. */ public FunctionDecl functionDecl() { return internalFunctionDecl(false); } private FunctionDecl anonymousFunctionDecl() { return internalFunctionDecl(true); } /** * Dual purpose parsing rule. Functions and anonymous functions. *

* anonymousFunctionDecl := ANONYMOUS_TOKEN argDeclList? 'except' IDENT ARROW EOL (INDENT+ line EOL) *

* functionDecl := (PRIVATE_FIELD | IDENT) argDeclList? ('in' SYMBOL)? 'except' IDENT ARROW EOL (INDENT+ line EOL) *

* patternFunctionDecl := (PRIVATE_FIELD | IDENT) argDeclList? ('in' SYMBOL)? 'except' IDENT HASHROCKET EOL (INDENT+ line EOL)* */ private FunctionDecl internalFunctionDecl(boolean anonymous) { List funcName = null; List startTokens = null; if (!anonymous) { funcName = match(Token.Kind.PRIVATE_FIELD); if (null == funcName) funcName = match(Token.Kind.IDENT); // Not a function if (null == funcName) { return null; } } else { if ((startTokens = match(Token.Kind.ANONYMOUS_TOKEN)) == null) return null; } // Scan ahead to ensure this is a function decl, coz once we start parsing the arg list // we can't go back. boolean isFunction = false; for (int k = i; k - i < 200 /* panic */ && k < tokens.size(); k++) { Token token = tokens.get(k); if ((token.kind == Kind.ARROW || token.kind == Kind.HASHROCKET) && k < tokens.size() + 1 && tokens.get(k + 1).kind == Kind.LBRACE) { isFunction = true; break; } if (token.kind == Kind.LBRACE || token.kind == Kind.EOL) break; } // Refuse to proceed if there does not appear to be a '->' at the end of the current line. if (!isFunction) { // Reset the parser in case we've already parsed an identifier. i--; return null; } ArgDeclList arguments = argDeclList(); String name = anonymous ? null : funcName.get(0).value; startTokens = funcName != null ? funcName : startTokens; FunctionDecl functionDecl = new FunctionDecl(name, arguments).sourceLocation(startTokens); // We need to set the module name here because closures are not declared as // top level functions in the module. if (anonymous) functionDecl.setModule(scope.getModuleName()); // Before we match the start of the function, allow for cell declaration. List inCellTokens = match(Kind.IN, Kind.PRIVATE_FIELD); if (inCellTokens != null) { functionDecl.cell = inCellTokens.get(1).value; } // Before we match the arrow and start the function, slurp up any exception handling logic. List exceptHandlerTokens = match(Kind.IDENT, Kind.IDENT); if (exceptHandlerTokens == null) exceptHandlerTokens = match(Kind.IDENT, Kind.PRIVATE_FIELD); if (exceptHandlerTokens != null) { Token exceptToken = exceptHandlerTokens.get(0); if (!RestrictedKeywords.EXCEPT.equals(exceptToken.value)) { addError("Expected 'expect' keyword after function signature", exceptToken); } functionDecl.exceptionHandler = exceptHandlerTokens.get(1).value; } // If it doesn't have a thin or fat arrow, then it's not a function either. if (match(Token.Kind.ARROW, Token.Kind.LBRACE) == null) { // Fat arrow, pattern matcher. if (match(Token.Kind.HASHROCKET, Token.Kind.LBRACE) == null) { return null; } else return patternMatchingFunctionDecl(functionDecl); } // Optionally match eols here. chewEols(); Node line; // Absorb indentation level. withIndent(); boolean hasBody = false; while ((line = line()) != null) { hasBody = true; functionDecl.add(line); // Multiple lines are allowed if terminated by a comma. if (match(Kind.EOL) == null) break; // EOLs are optional (probably should discourage this though). withIndent(); } if (hasBody) { chewEols(); // Look for a where block attached to this function. whereBlock(functionDecl); // A function body must be terminated by } (this is ensured by the token-stream rewriter) if (!endOfInput() && match(Token.Kind.RBRACE) == null) { addError("Expected end of function, additional statements found (did you mean '=>')", tokens.get(i)); throw new LoopCompileException(); } } return functionDecl; } private FunctionDecl patternMatchingFunctionDecl(FunctionDecl functionDecl) { chewEols(); PatternRule rule = new PatternRule().sourceLocation(functionDecl); do { withIndent(); // Detect pattern first. Maps supercede lists. Node pattern = emptyMapPattern(); if (null == pattern) pattern = emptyListPattern(); if (null == pattern) pattern = listOrMapPattern(); if (null == pattern) pattern = stringGroupPattern(); if (null == pattern) pattern = regexLiteral(); if (pattern == null) pattern = term(); // Try "otherwise" default fall thru. if (pattern == null) { List starToken = match(Kind.STAR); if (starToken != null) pattern = new WildcardPattern().sourceLocation(starToken); } // Look for a where block at the end of this pattern matching decl. int currentToken = i; if (pattern == null) if (whereBlock(functionDecl)) { if (endOfInput() || match(Token.Kind.RBRACE) != null) break; } if (pattern == null) { addError("Pattern syntax error. Expected a pattern rule", tokens.get(currentToken)); return null; } rule.patterns.add(pattern); rule.sourceLocation(pattern); boolean guarded = false; while (match(Token.Kind.PIPE) != null) { guarded = true; Node guardExpression = computation(); if (guardExpression == null) if (match(Token.Kind.ELSE) != null) guardExpression = new OtherwiseGuard(); if (match(Token.Kind.ASSIGN) == null) addError("Expected ':' after guard expression", tokens.get(i - 1)); Node line = line(); chewEols(); withIndent(); Guard guard = new Guard(guardExpression, line); rule.add(guard); } if (!guarded) { if (match(Kind.COMMA) == null) { if (match(Token.Kind.ASSIGN) == null) addError("Expected ':' after pattern", tokens.get(i)); } else continue; rule.rhs = line(); chewEols(); } functionDecl.add(rule); rule = new PatternRule().sourceLocation(functionDecl); if (endOfInput() || match(Token.Kind.RBRACE) != null) break; } while (true); functionDecl.patternMatching = true; return functionDecl; } private boolean whereBlock(FunctionDecl functionDecl) { withIndent(); boolean hasWhere = false; if (match(Token.Kind.WHERE) != null) { FunctionDecl helperFunction = null; Node assignment; do { chewEols(); withIndent(); assignment = variableAssignment(); if (null == assignment) helperFunction = functionDecl(); chewEols(); if (null != helperFunction) { hasWhere = true; functionDecl.declareLocally(helperFunction); } else if (null != assignment) { hasWhere = true; functionDecl.declareLocally(assignment); } } while (helperFunction != null || assignment != null); } return hasWhere; } private Node stringGroupPattern() { if (match(Token.Kind.LPAREN) == null) return null; StringPattern pattern = new StringPattern(); Node term; while ((term = term()) != null) { pattern.add(term); if (match(Token.Kind.ASSIGN) == null) break; } if (match(Token.Kind.RPAREN) == null) { addError("Expected ')' at end of string group pattern rule.", tokens.get(i - 1)); throw new LoopCompileException(); } return pattern; } private Node emptyMapPattern() { List startTokens = match(Kind.LBRACKET, Kind.ASSIGN, Kind.RBRACKET); return startTokens != null ? new MapPattern().sourceLocation(startTokens) : null; } private Node emptyListPattern() { List startTokens = match(Kind.LBRACKET, Kind.RBRACKET); return startTokens != null ? new ListDestructuringPattern().sourceLocation(startTokens) : null; } /** * listOrMapPattern := (LBRACKET term ((ASSIGN term)* | UNARROW term (COMMA term UNARROW term)*) * RBRACKET) */ private Node listOrMapPattern() { Node pattern; // We should allow the possibility of matching a type identifier. List type = match(Token.Kind.TYPE_IDENT); TypeLiteral typeLiteral = null; if (null != type) { typeLiteral = new TypeLiteral(type.get(0).value).sourceLocation(type); } Token lbracketTokens = anyOf(Kind.LBRACKET, Kind.LBRACE); if (lbracketTokens == null) return typeLiteral; Node term = term(); if (term == null) { addError("Expected term after '[' in pattern rule", tokens.get(i)); throw new LoopCompileException(); } // This is a list denaturing rule. if (match(Token.Kind.ASSIGN) != null) { pattern = new ListDestructuringPattern().sourceLocation(lbracketTokens); pattern.add(term); term = term(); if (term == null) { addError("Expected term after ':' in list pattern rule", tokens.get(i - 1)); throw new LoopCompileException(); } pattern.add(term); while (match(Token.Kind.ASSIGN) != null) pattern.add(term()); if (match(Token.Kind.RBRACKET) == null) { addError("Expected ']' at end of list pattern rule", tokens.get(i - 1)); return null; } return pattern; } // This is a list literal rule. boolean endList = match(Token.Kind.RBRACKET) != null; if (endList || match(Token.Kind.COMMA) != null) { pattern = new ListStructurePattern().sourceLocation(lbracketTokens); pattern.add(term); if (endList) return pattern; term = term(); if (null == term) { addError("Expected term after ',' in list pattern rule", tokens.get(i - 1)); throw new LoopCompileException(); } pattern.add(term); while (match(Token.Kind.COMMA) != null) { term = term(); if (null == term) { addError("Expected term after ',' in list pattern rule", tokens.get(i - 1)); throw new LoopCompileException(); } pattern.add(term); } if (match(Token.Kind.RBRACKET) == null) { addError("Expected ']' at end of list pattern rule", tokens.get(i - 1)); throw new LoopCompileException(); } return pattern; } // This is a map pattern. pattern = new MapPattern().sourceLocation(lbracketTokens); if (typeLiteral != null) pattern.add(typeLiteral); if (match(Token.Kind.UNARROW) == null) throw new RuntimeException("Expected '<-' in object pattern rule"); if (!(term instanceof Variable)) throw new RuntimeException( "Must select into a valid variable name in object pattern rule: " + term.toSymbol()); Node rhs = term(); if (rhs == null) throw new RuntimeException("Expected term after '<-' in object pattern rule"); if (rhs instanceof Variable) { // See if we can keep slurping a dot-chain. CallChain callChain = new CallChain(); callChain.nullSafe(false); callChain.add(rhs); while (match(Token.Kind.DOT) != null) { Node variable = variable(); if (null == variable) throw new RuntimeException("Expected term after '.' in object pattern rule"); callChain.add(variable); } rhs = callChain; } pattern.add(new DestructuringPair(term, rhs).sourceLocation(term)); while (match(Token.Kind.COMMA) != null) { term = variable(); if (null == term) throw new RuntimeException("Expected variable after ',' in object pattern rule"); if (match(Token.Kind.UNARROW) == null) throw new RuntimeException("Expected '<-' in object pattern rule"); rhs = term(); if (rhs == null) throw new RuntimeException("Expected term after '<-' in object pattern rule"); if (rhs instanceof Variable) { // See if we can keep slurping a dot-chain. CallChain callChain = new CallChain().sourceLocation(rhs); callChain.add(rhs); while (match(Token.Kind.DOT) != null) { Node variable = variable(); if (null == variable) throw new RuntimeException("Expected term after '.' in object pattern rule"); callChain.add(variable); } rhs = callChain; } pattern.add(new DestructuringPair(term, rhs).sourceLocation(term)); } if (match(Token.Kind.RBRACKET) == null && match(Kind.RBRACE) == null) { throw new RuntimeException("Expected '}' at end of object pattern"); } return rewriteObjectPattern(pattern); } private Node rewriteObjectPattern(Node pattern) { for (Node node : pattern.children()) { if (node instanceof DestructuringPair) { DestructuringPair pair = (DestructuringPair) node; // We need to rewrite the chain of variables as a chain of property calls. if (pair.rhs instanceof CallChain) { CallChain chain = (CallChain) pair.rhs; CallChain rewritten = new CallChain(); rewritten.add(chain.children().remove(0)); for (Node element : chain.children()) { Variable var = (Variable) element; rewritten.add(new Dereference(var.name).sourceLocation(var)); } pair.rhs = rewritten; } } } return pattern; } /** * argDeclList := LPAREN IDENT (ASSIGN TYPE_IDENT)? (COMMA IDENT (ASSIGN TYPE_IDENT)? )* RPAREN */ private ArgDeclList argDeclList() { List lparenTokens = match(Kind.LPAREN); if (lparenTokens == null) { return null; } List first = match(Token.Kind.IDENT); if (null == first) { if (null == match(Token.Kind.RPAREN)) { addError("Expected ')' or identifier in function argument list", tokens.get(i - 1)); throw new LoopCompileException(); } return new ArgDeclList().sourceLocation(lparenTokens); } List optionalType = match(Token.Kind.ASSIGN, Token.Kind.TYPE_IDENT); ArgDeclList arguments = new ArgDeclList().sourceLocation(lparenTokens); String firstTypeName = optionalType == null ? null : optionalType.get(1).value; arguments.add(new ArgDeclList.Argument(first.get(0).value, firstTypeName)); while (match(Token.Kind.COMMA) != null) { List nextArg = match(Token.Kind.IDENT); if (null == nextArg) { addError("Expected identifier after ',' in function arguments", tokens.get(i - 1)); throw new LoopCompileException(); } optionalType = match(Token.Kind.ASSIGN, Token.Kind.TYPE_IDENT); firstTypeName = optionalType == null ? null : optionalType.get(1).value; arguments.add(new ArgDeclList.Argument(nextArg.get(0).value, firstTypeName)); } if (match(Token.Kind.RPAREN) == null) { addError("Expected ')' at end of function arguments", tokens.get(i - 1)); throw new LoopCompileException(); } return arguments; } /** * require := REQUIRE IDENT (DOT IDENT)* ('as' IDENT)? EOL */ public RequireDecl require() { if (match(Token.Kind.REQUIRE) == null) { return null; } List module = match(Kind.JAVA_LITERAL); if (null == module) module = match(Token.Kind.IDENT); else { if (match(Kind.EOL) == null) { addError("Expected newline after require (are you trying to alias Java imports?)", tokens.get(i - 1)); throw new LoopCompileException(); } return new RequireDecl(module.get(0).value).sourceLocation(module); } if (null == module) { addError("Expected module identifier", tokens.get(i - 1)); throw new LoopCompileException(); } List requires = new ArrayList(); requires.add(module.get(0).value); boolean aliased, javaImport = false; while (match(Token.Kind.DOT) != null) { module = match(Token.Kind.IDENT); if (null == module) { module = match(Kind.TYPE_IDENT); javaImport = true; } if (null == module) { addError("Expected module identifier part after '.'", tokens.get(i - 1)); throw new LoopCompileException(); } requires.add(module.get(0).value); } List asToken = match(Kind.IDENT); aliased = asToken != null && RestrictedKeywords.AS.equals(asToken.get(0).value); List aliasTokens = match(Kind.IDENT); if (aliased) { if (aliasedModules == null) aliasedModules = new HashSet(); if (aliasTokens == null) { addError("Expected module alias after '" + RestrictedKeywords.AS + "'", tokens.get(i - 1)); throw new LoopCompileException(); } // Cache the aliases for some smart parsing of namespaced function calls. aliasedModules.add(aliasTokens.get(0).value); } if (match(Token.Kind.EOL) == null) { addError("Expected newline after require declaration", tokens.get(i - 1)); throw new LoopCompileException(); } // We also allow java imports outside using the backticks syntax. String alias = aliased ? aliasTokens.get(0).value : null; if (javaImport) { return new RequireDecl(requires.toString().replace(", ", "."), alias) .sourceLocation(module); } return new RequireDecl(requires, alias) .sourceLocation(module); } /** * module := MODULE IDENT (DOT IDENT)* EOL */ private ModuleDecl module() { if (match(Token.Kind.MODULE) == null) { return null; } List module = match(Token.Kind.IDENT); if (null == module) { addError("Expected module identifier after 'module' keyword", tokens.get(i - 1)); throw new LoopCompileException(); } List modules = new ArrayList(); modules.add(module.get(0).value); while (match(Token.Kind.DOT) != null) { module = match(Token.Kind.IDENT); if (null == module) { addError("Expected module identifier part after '.'", tokens.get(i - 1)); throw new LoopCompileException(); } modules.add(module.get(0).value); } if (match(Token.Kind.EOL) == null) { addError("Expected newline after module declaration", tokens.get(i - 1)); throw new LoopCompileException(); } return new ModuleDecl(modules).sourceLocation(module); } /*** In-function instruction parsing rules ***/ /** * line := assign */ public Node line() { return assign(); } /** * Assign a variable an expression. *

* variableAssignment := variable ASSIGN computation */ private Node variableAssignment() { List startTokens = match(Token.Kind.IDENT, Token.Kind.ASSIGN); if (null == startTokens) return null; Node left = new Variable(startTokens.get(0).value); Node right = computation(); if (right == null) { addError("Expected expression after ':' in assignment", tokens.get(i - 1)); throw new LoopCompileException(); } Assignment assignment = new Assignment(); assignment.add(left); assignment.add(right); return assignment.sourceLocation(startTokens); } /** * This is really both "free standing expression" and "assignment". *

* assign := computation (ASSIGN (computation (IF computation | comprehension)?) | (IF computation * THEN computation ELSE computation) )? */ private Node assign() { Node left = computation(); if (null == left) { return null; } List assignTokens = match(Kind.ASSIGN); if (assignTokens == null) { return left; } // Ternary operator if-then-else Node ifThenElse = ternaryIf(); if (null != ifThenElse) { return new Assignment().add(left).add(ifThenElse); } // OTHERWISE-- continue processing a normal assignment. Node right = computation(); if (null == right) { addError("Expected expression after '=' assign operator", assignTokens.get(0)); throw new LoopCompileException(); } // Is this a conditional assignment? Node condition = null; if (match(Token.Kind.IF) != null) { condition = computation(); } else { // Is this a list comprehension? Node comprehension = comprehension(); if (null != comprehension) { return new Assignment().add(left).add(right); } } return new Assignment(condition).add(left).add(right); } /** * Ternary operator, like Java's ?: *

* ternaryIf := IF computation then computation else computation */ private Node ternaryIf() { List ifTokens = match(Kind.IF); if (ifTokens == null) ifTokens = match(Kind.UNLESS); if (ifTokens != null) { Token operator = ifTokens.get(0); Node ifPart = computation(); if (match(Token.Kind.THEN) == null) { addError(operator.kind + " expression missing THEN clause", tokens.get(i - 1)); throw new LoopCompileException(); } Node thenPart = computation(); // Allow user not to specify else (equivalent of "... else Nothing"). Node elsePart; if (match(Token.Kind.ELSE) == null) { // addError(operator.kind + " expression missing ELSE clause", tokens.get(i - 1)); // throw new LoopCompileException(); elsePart = new TypeLiteral(TypeLiteral.NOTHING); } else elsePart = computation(); Node expr = operator.kind == Kind.IF ? new TernaryIfExpression() : new TernaryUnlessExpression(); return expr .add(ifPart) .add(thenPart) .add(elsePart) .sourceLocation(ifTokens); } return null; } /** * comprehension := FOR variable IN computation (AND computation)? */ private Node comprehension() { List forTokens = match(Kind.FOR); if (forTokens == null) { return null; } Node variable = variable(); if (null == variable) { addError("Expected variable identifier after 'for' in list comprehension", tokens.get(i - 1)); throw new LoopCompileException(); } if (match(Token.Kind.IN) == null) { addError("Expected 'in' after identifier in list comprehension", tokens.get(i - 1)); throw new LoopCompileException(); } Node inList = computation(); if (null == inList) { addError("Expected list clause after 'in' in list comprehension", tokens.get(i - 1)); throw new LoopCompileException(); } if (match(Token.Kind.IF) == null) { return new Comprehension(variable, inList, null).sourceLocation(forTokens); } Node filter = computation(); if (filter == null) { addError("Expected filter expression after 'if' in list comprehension", tokens.get(i - 1)); throw new LoopCompileException(); } return new Comprehension(variable, inList, filter).sourceLocation(forTokens); } /** * group := LPAREN computation RPAREN */ private Node group() { if (match(Token.Kind.LPAREN) == null) { return null; } Node computation = computation(); if (null == computation) { addError("Expected expression after '(' in group", tokens.get(i - 1)); throw new LoopCompileException(); } if (match(Token.Kind.RPAREN) == null) { addError("Expected ')' to close group expression", tokens.get(i - 1)); throw new LoopCompileException(); } return computation; } /** * computation := (group | chain) (comprehension | (rightOp (group | chain)) )* */ public Node computation() { Node node = group(); if (node == null) { node = chain(); } // Production failed. if (null == node) { return null; } Computation computation = new Computation(); computation.add(node); // See if there is a call here. MemberAccess postfixCall = call(); if (postfixCall != null) computation.add(postfixCall.postfix(true)); Node rightOp; Node comprehension = null; Node operand; while ((rightOp = rightOp()) != null || (comprehension = comprehension()) != null) { if (comprehension != null) { computation.add(comprehension); continue; } operand = group(); if (null == operand) { operand = chain(); } if (null == operand) { break; } rightOp.add(operand); computation.add(rightOp); } return computation; } /** * chain := listOrMapDef | ternaryIf | anonymousFunctionDecl | (term arglist? (call | * indexIntoList)*) */ private Node chain() { Node node = listOrMapDef(); // If not a list, maybe a ternary if? if (null == node) { node = ternaryIf(); } if (null == node) { node = anonymousFunctionDecl(); } // If not an ternary IF, maybe a term? if (null == node) node = term(); // Production failed. if (null == node) { return null; } // If args exist, then we should turn this simple term into a free method call. CallArguments args = arglist(); if (null != args) { String functionName = (node instanceof Variable) ? ((Variable) node).name : ((PrivateField) node).name(); node = new Call(functionName, args).sourceLocation(node); } CallChain chain = new CallChain(); chain.add(node); // Is this a static method call being set up? I.e. NOT a reference to a constant. boolean isJavaStaticRef = node instanceof JavaLiteral && ((JavaLiteral) node).staticFieldAccess == null; Node call, indexIntoList = null; boolean isFirst = true; while ((call = call()) != null || (indexIntoList = indexIntoList()) != null) { if (call != null) { MemberAccess postfixCall = (MemberAccess) call; postfixCall.javaStatic((isFirst && isJavaStaticRef) || postfixCall.isJavaStatic()); postfixCall.postfix(true); // Once we have marked a call as java static, the rest of the chain is not. I.e.: // `java.lang.Class`.forName('..').newInstance() <-- the last call is non-static. isFirst = false; } chain.add(call != null ? call : indexIntoList); } // Smart prediction of whether this is a namespaced call or not. List children = chain.children(); if (aliasedModules != null && !isJavaStaticRef && children.size() == 2 && node instanceof Variable) { Variable namespace = (Variable) node; if (aliasedModules.contains(namespace.name)) { ((Call) children.get(1)).namespace(namespace.name); children.remove(0); } } return chain; } /** * arglist := LPAREN (computation (COMMA computation)*)? RPAREN */ private CallArguments arglist() { // Test if there is a leading paren. List parenthetical = match(Token.Kind.LPAREN); if (null == parenthetical) { return null; } // Slurp arguments while commas exist. CallArguments callArguments; // See if this may be a named-arg invocation. List named = match(Token.Kind.IDENT, Token.Kind.ASSIGN); boolean isPositional = (null == named); callArguments = new CallArguments(isPositional); Node arg = computation(); if (null != arg) { // If this is a named arg, wrap it in a name. if (isPositional) { callArguments.add(arg); } else { callArguments.add(new CallArguments.NamedArg(named.get(0).value, arg)); } } // Rest of argument list, comma separated. while (match(Token.Kind.COMMA) != null) { named = null; if (!isPositional) { named = match(Token.Kind.IDENT, Token.Kind.ASSIGN); if (null == named) { addError("Cannot mix named and positional arguments in a function call", tokens.get(i - 1)); throw new LoopCompileException(); } } arg = computation(); if (null == arg) { addError("Expected expression after ',' in function call argument list", tokens.get(i - 1)); throw new LoopCompileException(); } if (isPositional) { callArguments.add(arg); } else { callArguments.add(new CallArguments.NamedArg(named.get(0).value, arg)); } } // Ensure the method invocation is properly closed. if (match(Token.Kind.RPAREN) == null) { addError("Expected ')' at end of function call argument list", tokens.get(i - 1)); throw new LoopCompileException(); } return callArguments; } /** * An array deref. *

* indexIntoList := LBRACKET (computation | computation? DOT DOT computation?)? RBRACKET */ private Node indexIntoList() { List lbracketTokens = match(Kind.LBRACKET); if (lbracketTokens == null) { return null; } Node index = computation(); // This is a list slice with a range specifier. Node to = null; boolean slice = false; if (match(Token.Kind.DOT) != null) { if (match(Token.Kind.DOT) == null) { addError("Syntax error, range specifier incomplete. Expected '..'", tokens.get(i - 1)); throw new LoopCompileException(); } slice = true; to = computation(); } else if (index == null) { throw new RuntimeException("Expected symbol or '..' list slice operator."); } if (match(Token.Kind.RBRACKET) == null) { addError("Expected ']' at the end of list index expression", tokens.get(i - 1)); throw new LoopCompileException(); } return new IndexIntoList(index, slice, to).sourceLocation(lbracketTokens); } /** * Inline list/map definition. *

* listOrMapDef := LBRACKET (computation ((COMMA computation)* | computation? DOT DOT * computation?)) | (computation COLON computation (COMMA computation COLON computation)*) | COLON * RBRACKET */ private Node listOrMapDef() { boolean isBraced = false; List lbracketTokens = match(Kind.LBRACKET); if (lbracketTokens == null) { if ((lbracketTokens = match(Token.Kind.LBRACE)) == null) { return null; } else { isBraced = true; } } Node index = computation(); Node list = new InlineListDef(isBraced).sourceLocation(lbracketTokens); if (null != index) { boolean isMap = match(Token.Kind.ASSIGN) != null; if (isMap) { list = new InlineMapDef(!isBraced).sourceLocation(lbracketTokens); // This map will be stored as a list of alternating keys/values (in pairs). list.add(index); Node value = computation(); if (null == value) { addError("Expected expression after ':' in map definition", tokens.get(i - 1)); throw new LoopCompileException(); } list.add(value); } else { list.add(index); } // Slurp up all list or map argument values as a comma-separated sequence. while (match(Token.Kind.COMMA) != null) { Node listElement = computation(); if (null == listElement) { addError("Expected expression after ',' in " + (isMap ? "map" : "list") + " definition", tokens.get(i - 1)); throw new LoopCompileException(); } list.add(listElement); // If the first index contained a hashrocket, then this is a map. if (isMap) { if (null == match(Token.Kind.ASSIGN)) { addError("Expected ':' after map key in map definition", tokens.get(i - 1)); throw new LoopCompileException(); } Node value = computation(); if (null == value) { addError("Expected value expression after ':' in map definition", tokens.get(i - 1)); throw new LoopCompileException(); } list.add(value); } } // OTHERWISE--- // This is a list slice with a range specifier. Node to; boolean slice; if (match(Token.Kind.DOT) != null) { if (match(Token.Kind.DOT) == null) { addError("Syntax error, range specifier incomplete. Expected '..'", tokens.get(i - 1)); throw new LoopCompileException(); } slice = true; to = computation(); list = new ListRange(index, slice, to).sourceLocation(lbracketTokens); } } // Is there a hashrocket? if (match(Token.Kind.ASSIGN) != null) { // This is an empty hashmap. list = new InlineMapDef(!isBraced); } if (anyOf(Token.Kind.RBRACKET, Token.Kind.RBRACE) == null) { addError("Expected '" + (isBraced ? "}" : "]'"), tokens.get(i - 1)); return null; } return list; } /** * dereference := '::' (IDENT | TYPE_IDENT) argList? */ private MemberAccess staticCall() { List staticOperator = match(Kind.ASSIGN, Kind.ASSIGN); boolean isStatic = RestrictedKeywords.isStaticOperator(staticOperator); if (!isStatic) return null; List ident = match(Kind.IDENT); boolean constant = false; if (ident == null) { ident = match(Kind.TYPE_IDENT); constant = true; } if (ident == null) { addError("Expected static method identifier after '::'", staticOperator.get(0)); throw new RuntimeException("Expected static method identifier after '::'"); } CallArguments arglist = arglist(); if (arglist == null) return new Dereference(ident.get(0).value) .constant(constant) .javaStatic(isStatic) .sourceLocation(ident); // Use the ident as name, and it is a method if there are () at end. return new Call(ident.get(0).value, arglist) .callJava(true) .javaStatic(isStatic) .sourceLocation(ident); } /** * A method call production rule. *

* call := staticCall | (DOT|UNARROW (IDENT | PRIVATE_FIELD) arglist?) */ private MemberAccess call() { MemberAccess dereference = staticCall(); if (dereference != null) return dereference; List call = match(Token.Kind.DOT, Token.Kind.IDENT); if (null == call) call = match(Token.Kind.DOT, Token.Kind.PRIVATE_FIELD); boolean forceJava = false, javaStatic = false; if (null == call) { call = match(Token.Kind.UNARROW, Token.Kind.IDENT); forceJava = true; } // Production failed. if (null == call) { return null; } CallArguments callArguments = arglist(); if (callArguments == null) return new Dereference(call.get(1).value) .sourceLocation(call); // Use the ident as name, and it is a method if there are () at end. return new Call(call.get(1).value, callArguments) .callJava(forceJava) .javaStatic(javaStatic) .sourceLocation(call); } /** * constructorCall := NEW TYPE_IDENT arglist */ private Node constructorCall() { if (match(Kind.NEW) == null) return null; String modulePart = null; List module; do { module = match(Kind.IDENT, Kind.DOT); if (module != null) { if (modulePart == null) modulePart = ""; modulePart += module.iterator().next().value + "."; } } while (module != null); List typeName = match(Kind.TYPE_IDENT); if (null == typeName) { addError("Expected type identifer after 'new'", tokens.get(i - 1)); throw new LoopCompileException(); } CallArguments arglist = arglist(); if (null == arglist) { addError("Expected '(' after constructor call", tokens.get(i - 1)); throw new LoopCompileException(); } return new ConstructorCall(modulePart, typeName.iterator().next().value, arglist) .sourceLocation(typeName); } /** * term := (literal | variable | field | constructorCall) */ private Node term() { Node term = literal(); if (null == term) { term = variable(); } if (null == term) { term = field(); } if (null == term) { term = constructorCall(); } return term; } /** * regexLiteral := DIVIDE ^(DIVIDE|EOL|EOF) DIVIDE */ private Node regexLiteral() { int cursor = i; Token token = tokens.get(cursor); if (Token.Kind.DIVIDE == token.kind) { // Look ahead until we find the ending terminal, EOL or EOF. boolean noTerminal = false; do { cursor++; token = tokens.get(cursor); if (Token.Kind.EOL == token.kind) { noTerminal = true; break; } } while (notEndOfRegex(token) && cursor < tokens.size() /* EOF check */); // Skip the last divide. cursor++; if (noTerminal) return null; } else return null; int start = i; i = cursor; // Compress tokens into regex literal. StringBuilder builder = new StringBuilder(); for (Token part : tokens.subList(start, i)) builder.append(part.value); String expression = builder.toString(); if (expression.startsWith("/") && expression.endsWith("/")) expression = expression.substring(1, expression.length() - 1); return new RegexLiteral(expression).sourceLocation(token); } private static boolean notEndOfRegex(Token token) { return (Token.Kind.DIVIDE != token.kind && Token.Kind.REGEX != token.kind); } /** * (lexer super rule) literal := string | MINUS? (integer | long | ( integer DOT integer )) * | TYPE_IDENT | JAVA_LITERAL */ private Node literal() { Token token = anyOf(Token.Kind.STRING, Token.Kind.INTEGER, Kind.BIG_INTEGER, Kind.LONG, Token.Kind.TYPE_IDENT, Token.Kind.JAVA_LITERAL, Token.Kind.TRUE, Token.Kind.FALSE); if (null == token) { List match = match(Token.Kind.MINUS, Token.Kind.INTEGER); if (null != match) { List additional = match(Kind.DOT, Kind.INTEGER); if (additional != null) return new DoubleLiteral('-' + match.get(1).value + '.' + additional.get(1).value) .sourceLocation(additional.get(1)); additional = match(Kind.DOT, Kind.FLOAT); if (additional != null) return new FloatLiteral('-' + match.get(1).value + '.' + additional.get(1).value + 'F') .sourceLocation(additional.get(1)); return new IntLiteral('-' + match.get(1).value).sourceLocation(match.get(1)); } else if ((match = match(Kind.MINUS, Kind.LONG)) != null) return new LongLiteral('-' + match.get(1).value).sourceLocation(match.get(1)); else if ((match = match(Kind.MINUS, Kind.BIG_INTEGER)) != null) { List additional = match(Kind.DOT, Kind.INTEGER); if (additional != null) return new BigDecimalLiteral('-' + match.get(1).value + '.' + additional.get(1).value) .sourceLocation(additional.get(1)); return new BigIntegerLiteral('-' + match.get(1).value).sourceLocation(match.get(1)); } return null; } switch (token.kind) { case TRUE: case FALSE: return new BooleanLiteral(token).sourceLocation(token); case INTEGER: List additional = match(Kind.DOT, Kind.INTEGER); if (additional != null) return new DoubleLiteral(token.value + '.' + additional.get(1).value) .sourceLocation(additional.get(1)); additional = match(Kind.DOT, Kind.FLOAT); if (additional != null) return new FloatLiteral(token.value + '.' + additional.get(1).value + 'F') .sourceLocation(additional.get(1)); return new IntLiteral(token.value).sourceLocation(token); case BIG_INTEGER: additional = match(Kind.DOT, Kind.INTEGER); if (additional != null) return new BigDecimalLiteral(token.value + '.' + additional.get(1).value) .sourceLocation(additional.get(1)); return new BigIntegerLiteral(token.value).sourceLocation(token); case LONG: return new LongLiteral(token.value).sourceLocation(token); case STRING: return new StringLiteral(token.value).sourceLocation(token); case TYPE_IDENT: return new TypeLiteral(token.value).sourceLocation(token); case JAVA_LITERAL: return new JavaLiteral(token.value).sourceLocation(token); } return null; } private Node variable() { List var = match(Token.Kind.IDENT); return (null != var) ? new Variable(var.get(0).value).sourceLocation(var) : null; } private Node field() { List var = match(Token.Kind.PRIVATE_FIELD); return (null != var) ? new PrivateField(var.get(0).value).sourceLocation(var) : null; } /** * Right associative operator (see Token.Kind). */ private Node rightOp() { if (i >= tokens.size()) { return null; } Token token = tokens.get(i); if (isRightAssociative(token)) { i++; return new BinaryOp(token).sourceLocation(token); } // No right associative op found. return null; } // Production tools. private Token anyOf(Token.Kind... ident) { if (i >= tokens.size()) { return null; } for (Token.Kind kind : ident) { Token token = tokens.get(i); if (kind == token.kind) { i++; return token; } } // No match =( return null; } private List match(Token.Kind... ident) { int cursor = i; for (Token.Kind kind : ident) { // What we want is more than the size of the token stream. if (cursor >= tokens.size()) { return null; } Token token = tokens.get(cursor); if (token.kind != kind) { return null; } cursor++; } // Forward cursor in token stream to match point. int start = i; i = cursor; return tokens.subList(start, i); } /** * Returns true if this is the end of the text sequence. */ private boolean endOfInput() { return i == tokens.size(); } /** * Slurps any leading whitespace and returns the count. */ private int withIndent() { int indent = 0; while (match(Token.Kind.INDENT) != null) { indent++; } return indent; } public Node ast() { return last; } private static boolean isLeftAssociative(Token token) { return null != token && LEFT_ASSOCIATIVE.contains(token.kind); } private static boolean isRightAssociative(Token token) { return null != token && RIGHT_ASSOCIATIVE.contains(token.kind); } public static String stringify(List list) { StringBuilder builder = new StringBuilder(); for (int i = 0, listSize = list.size(); i < listSize; i++) { builder.append(stringify(list.get(i))); if (i < listSize - 1) builder.append(' '); } return builder.toString(); } /** * recursively walks a parse tree and turns it into a symbolic form that is test-readable. */ public static String stringify(Node tree) { StringBuilder builder = new StringBuilder(); boolean shouldWrapInList = hasChildren(tree); if (shouldWrapInList) builder.append('('); builder.append(tree.toSymbol()); if (shouldWrapInList) builder.append(' '); for (Node child : tree.children()) { String s = stringify(child); if (s.length() == 0) continue; builder.append(s); builder.append(' '); } // chew last ' ' if (shouldWrapInList) { builder.deleteCharAt(builder.length() - 1); builder.append(')'); } return builder.toString(); } private static boolean hasChildren(Node tree) { return (null != tree.children()) && !tree.children().isEmpty(); } }