1 /**
2 This module implements the Parser struct, which generates Nodes from tokens. The resulting syntax tree
3 is processed by the Compiler.
4 
5 ────────────────────────────────────────────────────────────────────────────────
6 
7 Copyright (C) 2021 pillager86.rf.gd
8 
9 This program is free software: you can redistribute it and/or modify it under 
10 the terms of the GNU General Public License as published by the Free Software 
11 Foundation, either version 3 of the License, or (at your option) any later 
12 version.
13 
14 This program is distributed in the hope that it will be useful, but WITHOUT ANY
15 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
16 PARTICULAR PURPOSE.  See the GNU General Public License for more details.
17 
18 You should have received a copy of the GNU General Public License along with 
19 this program.  If not, see <https://www.gnu.org/licenses/>.
20 */
21 module mildew.parser;
22 
23 import std.conv: to, parse;
24 debug
25 {
26     import std.stdio;
27 }
28 
29 import mildew.exceptions: ScriptCompileException;
30 import mildew.lexer: Token;
31 import mildew.nodes;
32 import mildew.types.any: ScriptAny;
33 import mildew.util.stack;
34 
35 private int unaryOpPrecedence(Token opToken, bool isPost = false)
36 {
37     if(opToken.isKeyword("typeof"))
38     {
39         if(!isPost)
40             return 17;
41     }
42 
43     // see grammar.txt for explanation of magic constants
44     switch(opToken.type)
45     {
46         case Token.Type.BIT_NOT: 
47         case Token.Type.NOT:
48         case Token.Type.PLUS:
49         case Token.Type.DASH:
50             if(!isPost)
51                 return 17;
52             else
53                 return 0;
54         case Token.Type.INC:
55         case Token.Type.DEC:
56             if(isPost)
57                 return 18;
58             else
59                 return 17;
60         default: 
61             return 0;
62     }
63 }
64 
65 private int binaryOpPrecedence(Token opToken)
66 {
67     // TODO handle keywords in as 12 here (???)
68     if(opToken.isKeyword("instanceof"))
69         return 12;
70 
71     // see grammar.txt for explanation of magic constants
72     switch(opToken.type)
73     {
74         case Token.Type.LBRACKET:
75         case Token.Type.DOT: 
76         case Token.Type.LPAREN:
77             return 20;
78         case Token.Type.POW:
79             return 16;
80         case Token.Type.STAR: 
81         case Token.Type.FSLASH: 
82         case Token.Type.PERCENT:
83             return 15;
84         case Token.Type.PLUS: 
85         case Token.Type.DASH:
86             return 14;
87         case Token.Type.BIT_LSHIFT: 
88         case Token.Type.BIT_RSHIFT: 
89         case Token.Type.BIT_URSHIFT:
90             return 13;
91         case Token.Type.LT: 
92         case Token.Type.LE: 
93         case Token.Type.GT: 
94         case Token.Type.GE:
95             return 12;
96         case Token.Type.EQUALS: 
97         case Token.Type.NEQUALS: 
98         case Token.Type.STRICT_EQUALS: 
99         case Token.Type.STRICT_NEQUALS:
100             return 11;
101         case Token.Type.BIT_AND:
102             return 10;
103         case Token.Type.BIT_XOR:
104             return 9;
105         case Token.Type.BIT_OR:
106             return 8;
107         case Token.Type.AND:
108             return 7;
109         case Token.Type.OR:
110             return 6;
111         case Token.Type.NULLC:
112             return 5;
113         case Token.Type.QUESTION:
114             return 4;
115         case Token.Type.ASSIGN:
116         case Token.Type.POW_ASSIGN:
117         case Token.Type.STAR_ASSIGN:
118         case Token.Type.FSLASH_ASSIGN:
119         case Token.Type.PERCENT_ASSIGN:
120         case Token.Type.PLUS_ASSIGN:
121         case Token.Type.DASH_ASSIGN:
122         case Token.Type.BAND_ASSIGN:
123         case Token.Type.BXOR_ASSIGN:
124         case Token.Type.BOR_ASSIGN:
125         case Token.Type.BLS_ASSIGN:
126         case Token.Type.BRS_ASSIGN:
127         case Token.Type.BURS_ASSIGN:
128             return 3;
129         default:
130             return 0;
131     }
132     // yield and commma are already handled differently
133 }
134 
135 private bool isBinaryOpLeftAssociative(in Token opToken)
136 {
137     if(opToken.isKeyword("instanceof"))
138         return true;
139 
140     switch(opToken.type)
141     {
142         case Token.Type.LBRACKET:
143         case Token.Type.DOT: 
144         case Token.Type.LPAREN:
145             return true;
146         case Token.Type.POW:
147             return false;
148         case Token.Type.STAR: 
149         case Token.Type.FSLASH: 
150         case Token.Type.PERCENT:
151             return true;
152         case Token.Type.PLUS: 
153         case Token.Type.DASH:
154             return true;
155         case Token.Type.BIT_LSHIFT: 
156         case Token.Type.BIT_RSHIFT: 
157         case Token.Type.BIT_URSHIFT:
158             return true;
159         case Token.Type.LT: 
160         case Token.Type.LE: 
161         case Token.Type.GT: 
162         case Token.Type.GE:
163             return true;
164         case Token.Type.EQUALS: 
165         case Token.Type.NEQUALS: 
166         case Token.Type.STRICT_EQUALS: 
167         case Token.Type.STRICT_NEQUALS:
168             return true;
169         case Token.Type.BIT_AND:
170             return true;
171         case Token.Type.BIT_XOR:
172             return true;
173         case Token.Type.BIT_OR:
174             return true;
175         case Token.Type.AND:
176             return true;
177         case Token.Type.OR:
178             return true;
179         case Token.Type.QUESTION:
180             return false;
181         case Token.Type.NULLC:
182             return true;
183         case Token.Type.ASSIGN:
184         case Token.Type.POW_ASSIGN:
185         case Token.Type.STAR_ASSIGN:
186         case Token.Type.FSLASH_ASSIGN:
187         case Token.Type.PERCENT_ASSIGN:
188         case Token.Type.PLUS_ASSIGN:
189         case Token.Type.DASH_ASSIGN:
190         case Token.Type.BAND_ASSIGN:
191         case Token.Type.BXOR_ASSIGN:
192         case Token.Type.BOR_ASSIGN:
193         case Token.Type.BLS_ASSIGN:
194         case Token.Type.BRS_ASSIGN:
195         case Token.Type.BURS_ASSIGN:
196             return false;
197         default:
198             return false;
199     }   
200 }
201 
202 private bool tokenBeginsLoops(const Token tok)
203 {
204     return tok.type == Token.Type.LABEL 
205         || tok.isKeyword("while")
206         || tok.isKeyword("do")
207         || tok.isKeyword("for");
208 }
209 
210 /**
211  * The parser is used by the Compiler to generate a syntax tree out of tokens.
212  */
213 struct Parser
214 {
215     /**
216      * The constructor takes all tokens so that looking ahead for specific tokens
217      * can allow support for lambdas and other complex language features.
218      */
219     this(Token[] tokens)
220     {
221         _tokens = tokens;
222         nextToken(); // prime
223     }
224 
225     /**
226      * The main starting point. Also the "program" grammar rule. This method generates a block statement
227      * node where the Compiler iterates through each statement and emits bytecode for it.
228      */
229     BlockStatementNode parseProgram()
230     {
231         immutable lineNo = _currentToken.position.line;
232         _functionContextStack.push(FunctionContext(FunctionContextType.NORMAL, 0, 0, []));
233         auto statements = parseStatements(Token.Type.EOF);
234         _functionContextStack.pop();
235         assert(_functionContextStack.size == 0, "Sanity check failed: _functionContextStack");
236         return new BlockStatementNode(lineNo, statements);
237     }
238 
239 package:
240 
241     /// parse a single expression. See https://eli.thegreenplace.net/2012/08/02/parsing-expressions-by-precedence-climbing
242     /// for algorithm.
243     ExpressionNode parseExpression(int minPrec = 1)
244     {      
245         ExpressionNode primaryLeft = null;
246 
247         immutable unOpPrec = _currentToken.unaryOpPrecedence;
248         if(unOpPrec > minPrec)
249         {
250             auto opToken = _currentToken;
251             nextToken();
252             // TODO handle nested unary operations such as -++foo. This will require building an array
253             // of unary ops before parsing the primary expression. and further complicate unary operations on
254             // member access, array access, and function call nodes. 
255             primaryLeft = parsePrimaryExpression();
256             primaryLeft = new UnaryOpNode(opToken, primaryLeft);
257         }
258         else
259         {
260             primaryLeft = parsePrimaryExpression();
261         }
262 
263         while(_currentToken.binaryOpPrecedence >= minPrec || _currentToken.unaryOpPrecedence(true) >= minPrec)
264         {
265             if(_currentToken.unaryOpPrecedence(true) >= minPrec)
266             {
267                 // if primary left is a unary op node, we need to apply the postfix and recreate the uop
268                 if(auto uopNode = cast(UnaryOpNode)primaryLeft)
269                     primaryLeft = new UnaryOpNode(uopNode.opToken, new PostfixOpNode(_currentToken, 
270                         uopNode.operandNode));
271                 else
272                     primaryLeft = new PostfixOpNode(_currentToken, primaryLeft);
273                 nextToken();
274             }
275             else 
276             {
277                 auto opToken = _currentToken;
278                 immutable prec = opToken.binaryOpPrecedence;
279                 immutable isLeftAssoc = opToken.isBinaryOpLeftAssociative;
280                 immutable nextMinPrec = isLeftAssoc? prec + 1 : prec;
281                 nextToken();
282                 if(opToken.type == Token.Type.QUESTION)
283                 {
284                     // primaryLeft is our condition node
285                     auto onTrue = parseExpression();
286                     if(_currentToken.type != Token.Type.COLON)
287                         throw new ScriptCompileException("Expected ':' in terniary operator expression", _currentToken);
288                     nextToken();
289                     auto onFalse = parseExpression();
290                     primaryLeft = new TerniaryOpNode(primaryLeft, onTrue, onFalse);
291                 }
292                 // this is monstrous. There has to be a better way to do this
293                 else if(opToken.type == Token.Type.DOT)
294                 {
295                     auto right = parsePrimaryExpression();
296                     if(!cast(VarAccessNode)right)
297                         throw new ScriptCompileException("Right hand side of `.` operator must be identifier", opToken);
298                     if(cast(VarAccessNode)right is null)
299                         throw new ScriptCompileException("Object members must be valid identifiers", _currentToken);
300                     if(unOpPrec != 0 && prec > unOpPrec)
301                     {
302                         auto uon = cast(UnaryOpNode)primaryLeft;
303                         primaryLeft = new UnaryOpNode(uon.opToken, 
304                                 new MemberAccessNode(uon.operandNode, opToken, right));
305                     }
306                     else
307                         primaryLeft = new MemberAccessNode(primaryLeft, opToken, right);
308                 }
309                 else if(opToken.type == Token.Type.LBRACKET)
310                 {
311                     auto index = parseExpression();
312                     if(_currentToken.type != Token.Type.RBRACKET)
313                         throw new ScriptCompileException("Missing ']'", _currentToken);
314                     nextToken();
315                     if(unOpPrec != 0 && prec > unOpPrec)
316                     {
317                         auto uon = cast(UnaryOpNode)primaryLeft;
318                         primaryLeft = new UnaryOpNode(uon.opToken, new ArrayIndexNode(uon.operandNode, index));
319                     }
320                     else
321                         primaryLeft = new ArrayIndexNode(primaryLeft, index);
322                 }
323                 else if(opToken.type == Token.Type.LPAREN)
324                 {
325                     auto params = parseCommaSeparatedExpressions(Token.Type.RPAREN);
326                     nextToken();
327                     if(unOpPrec != 0 && prec > unOpPrec)
328                     {
329                         auto uon = cast(UnaryOpNode)primaryLeft;
330                         primaryLeft = new UnaryOpNode(uon.opToken, new FunctionCallNode(uon.operandNode, params));
331                     }
332                     else
333                         primaryLeft = new FunctionCallNode(primaryLeft, params);
334                 }
335                 else 
336                 {
337                     ExpressionNode primaryRight = parseExpression(nextMinPrec);
338                     // catch invalid assignments
339                     if(opToken.isAssignmentOperator)
340                     {
341                         if(!(cast(VarAccessNode)primaryLeft 
342                           || cast(MemberAccessNode)primaryLeft 
343                           || cast(ArrayIndexNode)primaryLeft))
344                         {
345                             throw new ScriptCompileException("Invalid left hand operand for assignment "
346                                     ~ primaryLeft.toString(), opToken);
347                         }
348                     }
349                     primaryLeft = new BinaryOpNode(opToken, primaryLeft, primaryRight);
350                 }
351             }
352         }
353 
354         return primaryLeft;
355     }
356 
357 private:
358 
359     // TODO: parser needs to take an optional compiler object so that lambdas can be switch-case
360     // expressions and work at runtime.
361     ScriptAny evaluateCTFE(ExpressionNode expr)
362     {
363         import mildew.environment: Environment;
364         import mildew.compiler: Compiler;
365         import mildew.vm.program: Program;
366         import mildew.vm.virtualmachine: VirtualMachine;
367         auto ret = new ReturnStatementNode(0, expr);
368         auto compiler = new Compiler();
369         auto program = compiler.compile([ret]);
370         auto vm = new VirtualMachine(new Environment(null, "<ctfe>"));
371         try 
372         {
373             return vm.runProgram(program, []);
374         }
375         catch(Exception ex)
376         {
377             return ScriptAny.UNDEFINED;
378         }
379     }
380 
381     /// parses a single statement
382     StatementNode parseStatement()
383     {
384         StatementNode statement;
385         immutable lineNumber = _currentToken.position.line;
386         // check for var declaration
387         if(_currentToken.isKeyword("var") || _currentToken.isKeyword("let") || _currentToken.isKeyword("const"))
388         {
389             statement = parseVarDeclarationStatement();
390         }
391         // check for {} block
392         else if(_currentToken.type == Token.Type.LBRACE)
393         {
394             // TODO: peek two tokens ahead for a ':' to indicate whether or not this is an object literal expression
395             nextToken();
396             auto statements = parseStatements(Token.Type.RBRACE);
397             nextToken();
398             statement = new BlockStatementNode(lineNumber, statements);
399         }
400         // check for if statement
401         else if(_currentToken.isKeyword("if"))
402         {
403             statement = parseIfStatement();
404         }
405         // check for switch
406         else if(_currentToken.isKeyword("switch"))
407         {
408             statement = parseSwitchStatement();
409         }
410         // check for loops
411         else if(_currentToken.tokenBeginsLoops())
412         {
413             statement = parseLoopStatement();
414         }
415         // break statement?
416         else if(_currentToken.isKeyword("break"))
417         {
418             if(_functionContextStack.top.loopStack == 0 && _functionContextStack.top.switchStack == 0)
419                 throw new ScriptCompileException("Break statements only allowed in loops or switch-case bodies", 
420                     _currentToken);
421             nextToken();
422             string label = "";
423             if(_currentToken.type == Token.Type.IDENTIFIER)
424             {
425                 label = _currentToken.text;
426                 bool valid = false;
427                 // label must exist on stack
428                 for(size_t i = _functionContextStack.top.labelStack.length; i > 0; --i)
429                 {
430                     if(_functionContextStack.top.labelStack[i-1] == label)
431                     {
432                         valid = true;
433                         break;
434                     }
435                 }
436                 if(!valid)
437                     throw new ScriptCompileException("Break label does not refer to valid label", _currentToken);
438                 nextToken();
439             }
440             if(_currentToken.type != Token.Type.SEMICOLON)
441                 throw new ScriptCompileException("Expected ';' after break", _currentToken);
442             nextToken();
443             statement = new BreakStatementNode(lineNumber, label);
444         }
445         // continue statement
446         else if(_currentToken.isKeyword("continue"))
447         {
448             if(_functionContextStack.top.loopStack == 0)
449                 throw new ScriptCompileException("Continue statements only allowed in loops", _currentToken);
450             nextToken();
451             string label = "";
452             if(_currentToken.type == Token.Type.IDENTIFIER)
453             {
454                 label = _currentToken.text;
455                 bool valid = false;
456                 for(size_t i = _functionContextStack.top.labelStack.length; i > 0; --i)
457                 {
458                     if(_functionContextStack.top.labelStack[i-1] == label)
459                     {
460                         valid = true;
461                         break;
462                     }
463                 }
464                 if(!valid)
465                     throw new ScriptCompileException("Continue label does not refer to valid label", _currentToken);
466                 nextToken();
467             }
468             if(_currentToken.type != Token.Type.SEMICOLON)
469                 throw new ScriptCompileException("Expected ';' after continue", _currentToken);
470             nextToken();
471             statement = new ContinueStatementNode(lineNumber, label);
472         }
473         // return statement with optional expression
474         else if(_currentToken.isKeyword("return"))
475         {
476             nextToken();
477             ExpressionNode expression = null;
478             if(_currentToken.type != Token.Type.SEMICOLON)
479                 expression = parseExpression();
480             if(_currentToken.type != Token.Type.SEMICOLON)
481                 throw new ScriptCompileException("Expected ';' after return", _currentToken);
482             nextToken();
483             statement = new ReturnStatementNode(lineNumber, expression);
484         }
485         else if(_currentToken.isKeyword("function"))
486         {
487             statement = parseFunctionDeclarationStatement();
488         }
489         else if(_currentToken.isKeyword("throw"))
490         {
491             nextToken();
492             auto expr = parseExpression();
493             if(_currentToken.type != Token.Type.SEMICOLON)
494                 throw new ScriptCompileException("Expected ';' after throw expression", _currentToken);
495             nextToken();
496             statement = new ThrowStatementNode(lineNumber, expr);
497         }
498         else if(_currentToken.isKeyword("try"))
499         {
500             statement = parseTryCatchBlockStatement();
501         }
502         else if(_currentToken.isKeyword("delete"))
503         {
504             immutable delToken = _currentToken;
505             nextToken();
506             auto tok = _currentToken;
507             auto expression = parseExpression();
508             if(cast(MemberAccessNode)expression is null && cast(ArrayIndexNode)expression is null)
509                 throw new ScriptCompileException("Invalid operand for delete operation", tok);
510             statement = new DeleteStatementNode(lineNumber, delToken, expression);
511         }
512         else if(_currentToken.isKeyword("class"))
513         {
514             statement = parseClassDeclaration();
515         }
516         else // for now has to be one expression followed by semicolon or EOF
517         {
518             if(_currentToken.type == Token.Type.SEMICOLON)
519             {
520                 // empty statement
521                 statement = new ExpressionStatementNode(lineNumber, null);
522                 nextToken();
523             }
524             else 
525             {
526                 auto expression = parseExpression();
527                 if(_currentToken.type != Token.Type.SEMICOLON && _currentToken.type != Token.Type.EOF)
528                     throw new ScriptCompileException("Expected semicolon after expression", _currentToken);
529                 nextToken(); // eat semicolon
530                 statement = new ExpressionStatementNode(lineNumber, expression);
531             }
532         }
533         return statement;
534     }
535 
536     ExpressionNode parsePrimaryExpression()
537     {
538         import std.conv: to, ConvException;
539 
540         ExpressionNode left = null;
541         switch(_currentToken.type)
542         {
543             case Token.Type.LPAREN: {
544                 // first check if this is a lambda
545                 auto lookahead = peekTokens(3);
546                 if((lookahead[1].type == Token.Type.COMMA ||
547                    lookahead[1].type == Token.Type.ARROW ||
548                    lookahead[2].type == Token.Type.ARROW) && lookahead[0].type != Token.Type.LPAREN)
549                 {
550                     left = parseLambda(true);
551                 }
552                 else
553                 {
554                     nextToken();
555                     left = parseExpression();
556                     if(_currentToken.type != Token.Type.RPAREN)
557                         throw new ScriptCompileException("Missing ')' in primary expression", _currentToken);
558                     nextToken();
559                 }
560                 break;
561             }
562             case Token.Type.LBRACE:
563                 left = parseObjectLiteral();
564                 break;
565             case Token.Type.DOUBLE:
566                 if(_currentToken.literalFlag == Token.LiteralFlag.NONE)
567                     left = new LiteralNode(_currentToken, ScriptAny(to!double(_currentToken.text)));
568                 else
569                     throw new ScriptCompileException("Malformed decimal number token", _currentToken);
570                 nextToken();
571                 break;
572             case Token.Type.INTEGER:
573                 try 
574                 {
575                     if(_currentToken.literalFlag == Token.LiteralFlag.NONE)
576                         left = new LiteralNode(_currentToken, ScriptAny(to!long(_currentToken.text)));
577                     else if(_currentToken.literalFlag == Token.LiteralFlag.HEXADECIMAL)
578                         left = new LiteralNode(_currentToken, ScriptAny(_currentToken.text[2..$].to!long(16)));
579                     else if(_currentToken.literalFlag == Token.LiteralFlag.OCTAL)
580                         left = new LiteralNode(_currentToken, ScriptAny(_currentToken.text[2..$].to!long(8)));
581                     else if(_currentToken.literalFlag == Token.LiteralFlag.BINARY)
582                         left = new LiteralNode(_currentToken, ScriptAny(_currentToken.text[2..$].to!long(2)));
583                 }
584                 catch(ConvException ex)
585                 {
586                     throw new ScriptCompileException("Integer literal is too long", _currentToken);
587                 }
588                 nextToken();
589                 break;
590             case Token.Type.STRING:
591                 if(_currentToken.literalFlag != Token.LiteralFlag.TEMPLATE_STRING)
592                     left = new LiteralNode(_currentToken, ScriptAny(_currentToken.text));
593                 else
594                     left = parseTemplateStringNode();
595                 nextToken();
596                 break;
597             case Token.Type.REGEX:
598                 left = new LiteralNode(_currentToken, ScriptAny(_currentToken.text));
599                 nextToken();
600                 break;
601             case Token.Type.KEYWORD:
602                 if(_currentToken.text == "true" || _currentToken.text == "false")
603                 {
604                     left = new LiteralNode(_currentToken, ScriptAny(to!bool(_currentToken.text)));
605                     nextToken();
606                 }
607                 else if(_currentToken.text == "null")
608                 {
609                     left = new LiteralNode(_currentToken, ScriptAny(null));
610                     nextToken();
611                 }
612                 else if(_currentToken.text == "undefined")
613                 {
614                     left = new LiteralNode(_currentToken, ScriptAny.UNDEFINED);
615                     nextToken();
616                 }
617                 else if(_currentToken.text == "function") // function literal
618                 {
619                     bool isGenerator = false;
620                     immutable token = _currentToken;
621                     nextToken();
622                     if(_currentToken.type == Token.Type.STAR)
623                     {
624                         isGenerator = true;
625                         nextToken();
626                     }
627 
628                     string optionalName = "";
629                     if(_currentToken.type == Token.Type.IDENTIFIER)
630                     {
631                         optionalName = _currentToken.text;
632                         nextToken();
633                     }
634 
635                     if(_currentToken.type != Token.Type.LPAREN)
636                         throw new ScriptCompileException("Argument list expected after anonymous function", 
637                             _currentToken);
638                     nextToken();
639                     string[] argNames = [];
640                     ExpressionNode[] defaultArgs;
641                     argNames = parseArgumentList(defaultArgs);
642                     nextToken(); // eat the )
643                     if(_currentToken.type != Token.Type.LBRACE)
644                         throw new ScriptCompileException("Expected '{' before anonymous function body", _currentToken);
645                     nextToken(); // eat the {
646                     _functionContextStack.push(FunctionContext(
647                         isGenerator? FunctionContextType.GENERATOR : FunctionContextType.NORMAL, 
648                         0, 0, []));
649                     auto statements = parseStatements(Token.Type.RBRACE);
650                     _functionContextStack.pop();
651                     nextToken();
652                     // auto func = new ScriptFunction(name, argNames, statements, null);
653                     left = new FunctionLiteralNode(token, argNames, defaultArgs, statements, optionalName, 
654                             false, isGenerator);
655                 }
656                 else if(_currentToken.text == "class")
657                 {
658                     left = parseClassExpression();
659                 }
660                 else if(_currentToken.text == "new")
661                 {
662                     immutable newToken = _currentToken;
663                     nextToken();
664                     auto expression = parseExpression();
665                     auto fcn = cast(FunctionCallNode)expression;
666                     if(fcn is null)
667                     {
668                         // if this isn't a function call, turn it into one
669                         fcn = new FunctionCallNode(expression, [], true);
670                     }
671                     fcn.returnThis = true;
672                     left = new NewExpressionNode(fcn);                    
673                 }
674                 else if(_currentToken.text == "super")
675                 {
676                     immutable stoken = _currentToken;
677                     if(_baseClassStack.length < 1)
678                         throw new ScriptCompileException("Super expression only allowed in derived classes", stoken);
679                     left = new SuperNode(stoken, _baseClassStack[$-1]);
680                     nextToken();
681                 }
682                 else if(_currentToken.text == "yield")
683                 {
684                     // check context stack
685                     if(_functionContextStack.top.fct != FunctionContextType.GENERATOR)
686                         throw new ScriptCompileException("Yield may only be used in Generator functions", 
687                             _currentToken);
688                     
689                     immutable ytoken = _currentToken;
690                     nextToken();
691                     ExpressionNode expr;
692                     if(_currentToken.type != Token.Type.RBRACE && _currentToken.type != Token.Type.SEMICOLON)
693                         expr = parseExpression();
694                     left = new YieldNode(ytoken, expr);
695                 }
696                 else
697                     throw new ScriptCompileException("Unexpected keyword in primary expression", _currentToken);
698                 break;
699                 // TODO function
700             case Token.Type.IDENTIFIER: {
701                 immutable lookahead = peekToken();
702                 if(lookahead.type == Token.Type.ARROW)
703                 {
704                     left = parseLambda(false);
705                 }
706                 else
707                 {
708                     left = new VarAccessNode(_currentToken);
709                     nextToken();
710                 }
711                 break;
712             }
713             case Token.Type.LBRACKET: // an array
714             {
715                 nextToken(); // eat the [
716                 auto values = parseCommaSeparatedExpressions(Token.Type.RBRACKET);
717                 nextToken(); // eat the ]
718                 left = new ArrayLiteralNode(values);
719                 break;
720             }
721             default:
722                 throw new ScriptCompileException("Unexpected token in primary expression", _currentToken);
723         }
724         return left;
725     }
726     
727     // TODO optimize this to use indexing instead of string building.
728     TemplateStringNode parseTemplateStringNode()
729     {
730         import mildew.lexer: Lexer;
731 
732         enum TSState { LIT, EXPR }
733 
734         size_t textIndex;
735         string currentExpr = "";
736         string currentLiteral = "";
737         ExpressionNode[] nodes;
738         TSState state = TSState.LIT;
739         int bracketStack = 0;
740 
741         string peekTwo(in string text, size_t i)
742         {
743             immutable char first = i >= text.length ? '\0' : text[i];
744             immutable char second = i+1 >= text.length ? '\0' : text[i+1];
745             return cast(string)[first,second];
746         }
747 
748         while(textIndex < _currentToken.text.length)
749         {
750             if(state == TSState.LIT)
751             {
752                 if(peekTwo(_currentToken.text, textIndex) == "${")
753                 {
754                     currentExpr = "";
755                     textIndex += 2;
756                     state = TSState.EXPR;
757                     if(currentLiteral.length > 0)
758                         nodes ~= new LiteralNode(_currentToken, ScriptAny(currentLiteral));
759                 }
760                 else
761                 {
762                     currentLiteral ~= _currentToken.text[textIndex++];
763                 }
764             }
765             else
766             {
767                 if(_currentToken.text[textIndex] == '}')
768                 {
769                     if(bracketStack)
770                     {
771                         currentExpr ~= _currentToken.text[textIndex++];
772                         --bracketStack;
773                     }
774                     else
775                     {
776                         currentLiteral = "";
777                         textIndex++;
778                         state = TSState.LIT;
779                         if(currentExpr.length > 0)
780                         {
781                             auto lexer = Lexer(currentExpr);
782                             auto tokens = lexer.tokenize();
783                             auto parser = Parser(tokens);
784                             nodes ~= parser.parseExpression();
785                             if(parser._currentToken.type != Token.Type.EOF)
786                             {
787                                 parser._currentToken.position = _currentToken.position;
788                                 throw new ScriptCompileException("Unexpected token in template expression", 
789                                         parser._currentToken);
790                             }
791                         }
792                     }
793                 }
794                 else
795                 {
796                     if(_currentToken.text[textIndex] == '{')
797                         ++bracketStack;
798                     currentExpr ~= _currentToken.text[textIndex++];
799                 }
800             }
801         }
802         if(state == TSState.EXPR)
803             throw new ScriptCompileException("Unclosed template expression", _currentToken);
804         if(currentLiteral.length > 0)
805             nodes ~= new LiteralNode(_currentToken, ScriptAny(currentLiteral));
806         return new TemplateStringNode(nodes);
807     }
808 
809     /// after class ? extend base this can begin
810     ClassDefinition parseClassDefinition(Token classToken, string className, ExpressionNode baseClass)
811     {
812         if(_currentToken.type != Token.Type.LBRACE)
813             throw new ScriptCompileException("Expected '{' after class", _currentToken);
814         nextToken();
815         FunctionLiteralNode constructor;
816         string[] methodNames;
817         FunctionLiteralNode[] methods;
818         string[] getMethodNames;
819         FunctionLiteralNode[] getMethods;
820         string[] setMethodNames;
821         FunctionLiteralNode[] setMethods;
822         string[] staticMethodNames;
823         FunctionLiteralNode[] staticMethods;
824         enum PropertyType { NONE, GET, SET, STATIC }
825         while(_currentToken.type != Token.Type.RBRACE && _currentToken.type != Token.Type.EOF)
826         {
827             PropertyType ptype = PropertyType.NONE;
828             string currentMethodName = "";
829             // could be a get or set
830             if(_currentToken.isIdentifier("get"))
831             {
832                 ptype = PropertyType.GET;
833                 nextToken();
834             }
835             else if(_currentToken.isIdentifier("set"))
836             {
837                 ptype = PropertyType.SET;
838                 nextToken();
839             }
840             else if(_currentToken.isIdentifier("static"))
841             {
842                 ptype = PropertyType.STATIC;
843                 nextToken();
844             }
845             // then an identifier
846             immutable idToken = _currentToken;
847             if(_currentToken.type != Token.Type.IDENTIFIER)
848                 throw new ScriptCompileException("Method names must be valid identifiers", _currentToken);
849             currentMethodName = _currentToken.text;
850             nextToken();
851             // then a (
852             if(_currentToken.type != Token.Type.LPAREN)
853                 throw new ScriptCompileException("Expected '(' after method name", _currentToken);
854             nextToken(); // eat the (
855             string[] argNames;
856             ExpressionNode[] defaultArgs;
857             argNames = parseArgumentList(defaultArgs);
858             nextToken(); // eat the )
859             // then a {
860             if(_currentToken.type != Token.Type.LBRACE)
861                 throw new ScriptCompileException("Method bodies must begin with '{'", _currentToken);
862             nextToken();
863             if(currentMethodName != "constructor")
864                 _functionContextStack.push(FunctionContext(FunctionContextType.METHOD, 0, 0, []));
865             else
866                 _functionContextStack.push(FunctionContext(FunctionContextType.CONSTRUCTOR, 0, 0, []));
867             auto statements = parseStatements(Token.Type.RBRACE);
868             _functionContextStack.pop();
869             nextToken(); // eat }
870             // now we have a method but if this is the constructor
871             if(currentMethodName == "constructor")
872             {
873                 if(ptype != PropertyType.NONE)
874                     throw new ScriptCompileException("Get, set, or static not allowed for constructor", classToken);
875                 if(constructor !is null)
876                     throw new ScriptCompileException("Classes may only have one constructor", classToken);
877                 // if this is extending a class it MUST have ONE super call
878                 if(baseClass !is null)
879                 {
880                     ulong numSupers = 0;
881                     foreach(stmt ; statements)
882                     {
883                         if(auto exprStmt = cast(ExpressionStatementNode)stmt)
884                         {
885                             if(auto fcn = cast(FunctionCallNode)exprStmt.expressionNode)
886                             {
887                                 if(auto supernode = cast(SuperNode)fcn.functionToCall)
888                                     numSupers++;
889                             }
890                         }
891                     }
892                     if(numSupers != 1)
893                         throw new ScriptCompileException(
894                             "Derived class constructors must have one super call at the top level", 
895                             classToken
896                         );
897                 }
898                 constructor = new FunctionLiteralNode(idToken, argNames, defaultArgs, statements, className, true);
899             }
900             else // it's a normal method or getter/setter
901             {
902                 if(ptype == PropertyType.NONE)
903                 {
904                     auto trueName = currentMethodName;
905                     if(className != "<anonymous class>" && className != "")
906                         trueName = className ~ ".prototype." ~ currentMethodName;
907                     methods ~= new FunctionLiteralNode(idToken, argNames, defaultArgs, statements, trueName);
908                     methodNames ~= currentMethodName;
909                 }
910                 else if(ptype == PropertyType.GET)
911                 {
912                     getMethods ~= new FunctionLiteralNode(idToken, argNames, defaultArgs, statements, 
913                             currentMethodName);
914                     getMethodNames ~= currentMethodName;                    
915                 }
916                 else if(ptype == PropertyType.SET)
917                 {
918                     setMethods ~= new FunctionLiteralNode(idToken, argNames, defaultArgs, statements, 
919                             currentMethodName);
920                     setMethodNames ~= currentMethodName;                    
921                 }
922                 else if(ptype == PropertyType.STATIC)
923                 {
924                     auto trueName = currentMethodName;
925                     if(className != "<anonymous class>" && className != "")
926                         trueName = className ~ "." ~ currentMethodName;
927                     staticMethods ~= new FunctionLiteralNode(idToken, argNames, defaultArgs, statements, trueName);
928                     staticMethodNames ~= currentMethodName;
929                 }
930             }
931         }
932         nextToken(); // eat the class body }
933 
934         // check for duplicate methods
935         bool[string] mnameMap;
936         foreach(mname ; methodNames)
937         {
938             if(mname in mnameMap)
939                 throw new ScriptCompileException("Duplicate methods are not allowed", classToken);
940             mnameMap[mname] = true;
941         }
942 
943         if(constructor is null)
944         {
945             // probably should enforce required super when base class exists. or not
946             constructor = new FunctionLiteralNode(classToken, [], [], [], className, true);
947         }
948 
949         if(baseClass !is null)
950             _baseClassStack = _baseClassStack[0..$-1];
951        	return new ClassDefinition(className, constructor, methodNames, methods, getMethodNames, getMethods, 
952 	   		setMethodNames, setMethods, staticMethodNames, staticMethods, baseClass);
953     }
954 
955     ClassLiteralNode parseClassExpression()
956     {
957         immutable classToken = _currentToken;
958         nextToken();
959         string className = "<anonymous class>";
960         if(_currentToken.type == Token.Type.IDENTIFIER)
961         {
962             className = _currentToken.text;
963             nextToken();
964         }
965         ExpressionNode baseClass = null;
966         if(_currentToken.isKeyword("extends"))
967         {
968             nextToken();
969             baseClass = parseExpression(); // let's hope this is an expression that results in a ScriptFunction value
970             _baseClassStack ~= baseClass;
971         }
972         auto classDef = parseClassDefinition(classToken, className, baseClass);
973         return new ClassLiteralNode(classToken, classDef);
974     }
975 
976     /// parses multiple statements until reaching stop
977     StatementNode[] parseStatements(in Token.Type stop)
978     {
979         StatementNode[] statements;
980         while(_currentToken.type != stop && _currentToken.type != Token.Type.EOF)
981         {
982             statements ~= parseStatement();
983             // each statement parse should eat the semicolon or } so there's nothing to do here
984         }
985         return statements;
986     }
987 
988     VarDeclarationStatementNode parseVarDeclarationStatement(bool consumeSemicolon = true)
989     {
990         auto specifier = _currentToken;
991         nextToken();
992         // auto expressions = parseCommaSeparatedExpressions(Token.Type.SEMICOLON);
993         ExpressionNode[] expressions;
994         while(_currentToken.type != Token.Type.SEMICOLON && _currentToken.type != Token.Type.EOF 
995           && !_currentToken.isIdentifier("of") && !_currentToken.isKeyword("in"))
996         {
997             if(_currentToken.type == Token.Type.IDENTIFIER)
998             {
999                 auto varName = _currentToken.text;
1000                 nextToken();
1001                 if(_currentToken.type == Token.Type.ASSIGN)
1002                 {
1003                     nextToken();
1004                     auto assignment = parseExpression();
1005                     expressions ~= new BinaryOpNode(Token.createFakeToken(Token.Type.ASSIGN, "="), 
1006                         new VarAccessNode(Token.createFakeToken(Token.Type.IDENTIFIER, varName)),
1007                         assignment);
1008                 }
1009                 else
1010                 {
1011                     expressions ~= new VarAccessNode(Token.createFakeToken(Token.Type.IDENTIFIER, varName));
1012                 }
1013             }
1014             else if(_currentToken.type == Token.Type.LBRACE || _currentToken.type == Token.Type.LBRACKET)
1015             {
1016                 // TODO: replace destructure node with [name|/{name| variables
1017                 immutable isObj = _currentToken.type == Token.Type.LBRACE;
1018                 immutable endTokenType = isObj ? Token.Type.RBRACE : Token.Type.RBRACKET;
1019                 nextToken();
1020                 string[] names;
1021                 string remainderName;
1022                 while(_currentToken.type != endTokenType && _currentToken.type != Token.Type.EOF)
1023                 {
1024                     if(_currentToken.type == Token.Type.TDOT)
1025                     {
1026                         nextToken();
1027                         if(_currentToken.type != Token.Type.IDENTIFIER)
1028                             throw new ScriptCompileException("Spread variable must be identifier", _currentToken);
1029                         if(remainderName != "")
1030                             throw new ScriptCompileException("Only one spread variable allowed", _currentToken);
1031                         remainderName = _currentToken.text;
1032                     }
1033                     else if(_currentToken.type == Token.Type.IDENTIFIER)
1034                     {
1035                         auto currentName = _currentToken.text;
1036                         names ~= currentName;
1037                     }
1038                     nextToken();
1039                     if(_currentToken.type == Token.Type.COMMA)
1040                         nextToken(); // eat the ,
1041                     else if(_currentToken.type != endTokenType && _currentToken.type != Token.Type.EOF)
1042                         throw new ScriptCompileException("Destructuring variable names must be separated by comma",
1043                             _currentToken);
1044                 }
1045                 if(names.length == 0 && remainderName == "")
1046                     throw new ScriptCompileException("Destructuring declaration cannot be empty", _currentToken);
1047                 nextToken(); // eat the } or ]
1048                 if(_currentToken.type != Token.Type.ASSIGN)
1049                     throw new ScriptCompileException("Destructuring declarations must be assignments", _currentToken);
1050                 immutable assignToken = _currentToken;
1051                 nextToken(); // eat the =
1052                 auto assignment = parseExpression();
1053                 expressions ~= new BinaryOpNode(assignToken, new DestructureTargetNode(names, remainderName, isObj),
1054                     assignment);
1055             }
1056 
1057             if(_currentToken.type == Token.Type.COMMA)
1058             {
1059                 nextToken();
1060             }
1061             else if(_currentToken.type != Token.Type.SEMICOLON && _currentToken.type != Token.Type.EOF
1062                 && !_currentToken.isIdentifier("of") && !_currentToken.isKeyword("in"))
1063             {
1064                 throw new ScriptCompileException("Expected ',' between variable declarations (or missing ';')", 
1065                     _currentToken);
1066             }
1067         }
1068         // make sure all expressions are valid BinaryOpNodes or VarAccessNodes
1069         foreach(expression; expressions)
1070         {
1071             if(auto node = cast(BinaryOpNode)expression)
1072             {
1073                 if(!(cast(VarAccessNode)node.leftNode || cast(DestructureTargetNode)node.leftNode))
1074                     throw new ScriptCompileException("Invalid assignment node", _currentToken);
1075                 if(node.opToken.type != Token.Type.ASSIGN)
1076                     throw new ScriptCompileException("Invalid assignment statement", node.opToken);
1077             }
1078             else if(!cast(VarAccessNode)expression)
1079             {
1080                 throw new ScriptCompileException("Invalid variable name in declaration", _currentToken);
1081             }
1082         }
1083         if(consumeSemicolon)
1084             nextToken(); // eat semicolon
1085         return new VarDeclarationStatementNode(specifier, expressions);
1086     }
1087 
1088     IfStatementNode parseIfStatement()
1089     {
1090         immutable lineNumber = _currentToken.position.line;
1091         nextToken();
1092         if(_currentToken.type != Token.Type.LPAREN)
1093             throw new ScriptCompileException("Expected '(' after if keyword", _currentToken);
1094         nextToken();
1095         auto condition = parseExpression();
1096         if(_currentToken.type != Token.Type.RPAREN)
1097             throw new ScriptCompileException("Expected ')' after if condition", _currentToken);
1098         nextToken();
1099         auto ifTrueStatement = parseStatement();
1100         StatementNode elseStatement = null;
1101         if(_currentToken.isKeyword("else"))
1102         {
1103             nextToken();
1104             elseStatement = parseStatement();
1105         }
1106         return new IfStatementNode(lineNumber, condition, ifTrueStatement, elseStatement);
1107     }
1108 
1109     StatementNode parseLoopStatement()
1110     {
1111         string label = "";
1112         if(_currentToken.type == Token.Type.LABEL)
1113         {
1114             label = _currentToken.text;
1115             _functionContextStack.top.labelStack ~= label;
1116             nextToken();
1117         }
1118         StatementNode statement;
1119         if(_currentToken.isKeyword("while"))
1120         {
1121             ++_functionContextStack.top.loopStack;
1122             statement = parseWhileStatement(label);
1123             --_functionContextStack.top.loopStack;
1124         }
1125         // check for do-while statement TODO check for label
1126         else if(_currentToken.isKeyword("do"))
1127         {
1128             ++_functionContextStack.top.loopStack;
1129             statement = parseDoWhileStatement(label);
1130             --_functionContextStack.top.loopStack;
1131         }
1132         // check for for loop TODO check label
1133         else if(_currentToken.isKeyword("for"))
1134         {
1135             ++_functionContextStack.top.loopStack;
1136             statement = parseForStatement(label);
1137             --_functionContextStack.top.loopStack;
1138         }
1139         else
1140         {
1141             throw new ScriptCompileException("Labels may only be used before loop statements", 
1142                 _currentToken);
1143         }
1144         if(label != "")
1145         {
1146             _functionContextStack.top.labelStack = _functionContextStack.top.labelStack[0..$-1];
1147         }
1148         return statement;
1149     }
1150 
1151     WhileStatementNode parseWhileStatement(string label = "")
1152     {
1153         immutable lineNumber = _currentToken.position.line;
1154         nextToken();
1155         if(_currentToken.type != Token.Type.LPAREN)
1156             throw new ScriptCompileException("Expected '(' after while keyword", _currentToken);
1157         nextToken();
1158         auto condition = parseExpression();
1159         if(_currentToken.type != Token.Type.RPAREN)
1160             throw new ScriptCompileException("Expected ')' after while condition", _currentToken);
1161         nextToken();
1162         auto loopBody = parseStatement();
1163         return new WhileStatementNode(lineNumber, condition, loopBody, label);
1164     }
1165 
1166     DoWhileStatementNode parseDoWhileStatement(string label = "")
1167     {
1168         immutable lineNumber = _currentToken.position.line;
1169         nextToken();
1170         auto loopBody = parseStatement();
1171         if(!_currentToken.isKeyword("while"))
1172             throw new ScriptCompileException("Expected while keyword after do statement", _currentToken);
1173         nextToken();
1174         if(_currentToken.type != Token.Type.LPAREN)
1175             throw new ScriptCompileException("Expected '(' before do-while condition", _currentToken);
1176         nextToken();
1177         auto condition = parseExpression();
1178         if(_currentToken.type != Token.Type.RPAREN)
1179             throw new ScriptCompileException("Expected ')' after do-while condition", _currentToken);
1180         nextToken();
1181         if(_currentToken.type != Token.Type.SEMICOLON)
1182             throw new ScriptCompileException("Expected ';' after do-while statement", _currentToken);
1183         nextToken();
1184         return new DoWhileStatementNode(lineNumber, loopBody, condition, label);
1185     }
1186 
1187     StatementNode parseForStatement(string label = "")
1188     {
1189         immutable lineNumber = _currentToken.position.line;
1190         nextToken();
1191         if(_currentToken.type != Token.Type.LPAREN)
1192             throw new ScriptCompileException("Expected '(' after for keyword", _currentToken);
1193         nextToken();
1194         VarDeclarationStatementNode decl = null;
1195         if(_currentToken.type != Token.Type.SEMICOLON)
1196             decl = parseVarDeclarationStatement(false);
1197         if(_currentToken.isKeyword("in") || _currentToken.isIdentifier("of"))
1198         {
1199             immutable ofInToken = _currentToken;
1200             // first we need to validate the VarDeclarationStatementNode to make sure it only consists
1201             // of let or const and VarAccessNodes
1202             if(decl is null)
1203                 throw new ScriptCompileException("Invalid for in statement", _currentToken);
1204             Token qualifier;
1205             VarAccessNode[] vans;
1206             if(decl.qualifier.text != "const" && decl.qualifier.text != "let")
1207                 throw new ScriptCompileException("Global variable declaration invalid in for in statement",
1208                     decl.qualifier);
1209             int vanCount = 0;
1210             foreach(va ; decl.varAccessOrAssignmentNodes)
1211             {
1212                 auto valid = cast(VarAccessNode)va;
1213                 if(valid is null)
1214                     throw new ScriptCompileException("Invalid variable declaration in for in statement", 
1215                         _currentToken);
1216                 vans ~= valid;
1217                 ++vanCount;
1218             }
1219             if(vanCount > 2)
1220                 throw new ScriptCompileException("For in loops may only have one or two variable declarations", 
1221                         _currentToken);
1222             nextToken();
1223             auto objToIterateExpr = parseExpression();
1224             if(_currentToken.type != Token.Type.RPAREN)
1225                 throw new ScriptCompileException("Expected ')' after array or object", _currentToken);
1226             nextToken();
1227             auto bodyStatement = parseStatement();
1228             return new ForOfStatementNode(lineNumber, qualifier, ofInToken, vans, objToIterateExpr, 
1229                     bodyStatement, label);
1230         }
1231         else if(_currentToken.type == Token.Type.SEMICOLON)
1232         {
1233             nextToken();
1234             ExpressionNode condition = null;
1235             if(_currentToken.type != Token.Type.SEMICOLON)
1236             {
1237                 condition = parseExpression();
1238                 if(_currentToken.type != Token.Type.SEMICOLON)
1239                     throw new ScriptCompileException("Expected ';' after for condition", _currentToken);
1240             }
1241             else
1242             {
1243                 condition = new LiteralNode(_currentToken, ScriptAny(true));
1244             }
1245             nextToken();
1246             ExpressionNode increment = null;
1247             if(_currentToken.type != Token.Type.RPAREN)
1248             {
1249                 increment = parseExpression();
1250             }
1251             else
1252             {
1253                 increment = new LiteralNode(_currentToken, ScriptAny(true));
1254             }
1255             if(_currentToken.type != Token.Type.RPAREN)
1256                 throw new ScriptCompileException("Expected ')' before for loop body", _currentToken);
1257             nextToken();
1258             auto bodyNode = parseStatement();
1259             return new ForStatementNode(lineNumber, decl, condition, increment, bodyNode, label);
1260         }
1261         else
1262             throw new ScriptCompileException("Invalid for statement", _currentToken);
1263     }
1264 
1265     FunctionDeclarationStatementNode parseFunctionDeclarationStatement()
1266     {
1267         import std.algorithm: uniq, count;
1268         immutable lineNumber = _currentToken.position.line;
1269         bool isGenerator = false;
1270         nextToken(); // eat the function keyword
1271 
1272         if(_currentToken.type == Token.Type.STAR)
1273         {
1274             isGenerator = true;
1275             nextToken();
1276         }
1277 
1278         if(_currentToken.type != Token.Type.IDENTIFIER)
1279             throw new ScriptCompileException("Expected identifier after function keyword", _currentToken);
1280         string name = _currentToken.text;
1281         nextToken();
1282 
1283         if(_currentToken.type == Token.Type.STAR)
1284         {
1285             if(isGenerator)
1286                 throw new ScriptCompileException("Only one asterisk allowed to described Generators", _currentToken);
1287             isGenerator = true;
1288             nextToken();
1289         }
1290         
1291         if(_currentToken.type != Token.Type.LPAREN)
1292             throw new ScriptCompileException("Expected '(' after function name", _currentToken);
1293         nextToken(); // eat the (
1294         string[] argNames = [];
1295         ExpressionNode[] defaultArgs;
1296         argNames = parseArgumentList(defaultArgs);
1297         nextToken(); // eat the )
1298 
1299         // make sure there are no duplicate parameter names
1300         if(argNames.uniq.count != argNames.length)
1301             throw new ScriptCompileException("Function argument names must be unique", _currentToken);
1302 
1303         if(_currentToken.type != Token.Type.LBRACE)
1304             throw new ScriptCompileException("Function definition must begin with '{'", _currentToken);
1305         nextToken();
1306         _functionContextStack.push(FunctionContext(
1307             isGenerator? FunctionContextType.GENERATOR: FunctionContextType.NORMAL, 
1308             0, 0, []));
1309         auto statements = parseStatements(Token.Type.RBRACE);
1310         _functionContextStack.pop();
1311         nextToken(); // eat the }
1312         return new FunctionDeclarationStatementNode(lineNumber, name, argNames, defaultArgs, statements, isGenerator);
1313     }
1314 
1315     TryCatchBlockStatementNode parseTryCatchBlockStatement()
1316     {
1317         immutable lineNumber = _currentToken.position.line;
1318         auto tryToken = _currentToken;
1319         nextToken(); // eat the 'try'
1320         auto tryBlock = parseStatement();
1321         StatementNode catchBlock = null;
1322         StatementNode finallyBlock = null;
1323         auto name = "";
1324         if(_currentToken.isKeyword("catch"))
1325         {
1326             nextToken(); // eat the catch
1327             if(_currentToken.type == Token.Type.LPAREN)
1328             {
1329                 nextToken(); // eat (
1330                 if(_currentToken.type != Token.Type.IDENTIFIER)
1331                     throw new ScriptCompileException("Name of exception required after '('", _currentToken);
1332                 name = _currentToken.text;
1333                 nextToken();
1334                 if(_currentToken.type != Token.Type.RPAREN)
1335                     throw new ScriptCompileException("')' required after exception name", _currentToken);
1336                 nextToken();
1337             }
1338             catchBlock = parseStatement();
1339         }
1340         if(_currentToken.isKeyword("finally"))
1341         {
1342             nextToken();
1343             finallyBlock = parseStatement();
1344         }
1345         // can't be missing both catch and finally
1346         if(catchBlock is null && finallyBlock is null)
1347             throw new ScriptCompileException("Try statements must have catch and/or finally block", tryToken);
1348         return new TryCatchBlockStatementNode(lineNumber, tryBlock, name, catchBlock, finallyBlock);
1349     }
1350 
1351     ObjectLiteralNode parseObjectLiteral()
1352     {
1353         nextToken(); // eat the {
1354         string[] keys = [];
1355         ExpressionNode[] valueExpressions = [];
1356         while(_currentToken.type != Token.Type.RBRACE)
1357         {
1358             // first must be an identifier token or string literal token
1359             immutable idToken = _currentToken;
1360             if(_currentToken.type != Token.Type.IDENTIFIER && _currentToken.type != Token.Type.STRING
1361                 && _currentToken.type != Token.Type.LABEL)
1362                 throw new ScriptCompileException("Invalid key for object literal", _currentToken);
1363             keys ~= _currentToken.text;
1364 
1365             nextToken();
1366             // next must be a :
1367             if(idToken.type != Token.Type.LABEL)
1368             {
1369                 if(_currentToken.type != Token.Type.COLON)
1370                     throw new ScriptCompileException("Expected ':' after key in object literal", _currentToken);
1371                 nextToken();
1372             }
1373             // next can be any valid expression
1374             valueExpressions ~= parseExpression();
1375             // if next is not a comma it must be a closing brace to exit
1376             if(_currentToken.type == Token.Type.COMMA)
1377                 nextToken();
1378             else if(_currentToken.type != Token.Type.RBRACE)
1379                 throw new ScriptCompileException("Key value pairs must be separated by ','", _currentToken);
1380         }
1381         nextToken(); // eat the }
1382         if(keys.length != valueExpressions.length)
1383             throw new ScriptCompileException("Number of keys must match values in object literal", _currentToken);
1384         return new ObjectLiteralNode(keys, valueExpressions);
1385     }
1386 
1387     ClassDeclarationStatementNode parseClassDeclaration()
1388     {
1389         immutable lineNumber = _currentToken.position.line;
1390         immutable classToken = _currentToken;
1391         nextToken();
1392         if(_currentToken.type != Token.Type.IDENTIFIER)
1393             throw new ScriptCompileException("Class name must be valid identifier", _currentToken);
1394         auto className = _currentToken.text;
1395         nextToken();
1396         ExpressionNode baseClass = null;
1397         if(_currentToken.isKeyword("extends"))
1398         {
1399             nextToken();
1400             baseClass = parseExpression(); // let's hope this is an expression that results in a ScriptFunction value
1401             _baseClassStack ~= baseClass;
1402         }
1403         auto classDef = parseClassDefinition(classToken, className, baseClass);
1404         return new ClassDeclarationStatementNode(lineNumber, classToken, classDef);
1405     }
1406 
1407     SwitchStatementNode parseSwitchStatement() 
1408     {
1409         ++_functionContextStack.top.switchStack;
1410         immutable lineNumber = _currentToken.position.line;
1411         immutable switchToken = _currentToken;
1412         nextToken();
1413         if(_currentToken.type != Token.Type.LPAREN)
1414             throw new ScriptCompileException("Expected '(' after switch keyword", _currentToken);
1415         nextToken();
1416         auto expression = parseExpression();
1417         if(_currentToken.type != Token.Type.RPAREN)
1418             throw new ScriptCompileException("Expected ')' after switch expression", _currentToken);
1419         nextToken();
1420         if(_currentToken.type != Token.Type.LBRACE)
1421             throw new ScriptCompileException("Expected '{' to begin switch body", _currentToken);
1422         nextToken();
1423         bool caseStarted = false;
1424         size_t statementCounter = 0;
1425         StatementNode[] statementNodes;
1426         size_t defaultStatementID = size_t.max;
1427         size_t[ScriptAny] jumpTable;
1428         while(_currentToken.type != Token.Type.RBRACE)
1429         {
1430             if(_currentToken.isKeyword("case"))
1431             {
1432                 nextToken();
1433                 caseStarted = true;
1434                 auto caseExpression = parseExpression();
1435                 // it has to be evaluatable at compile time
1436                 auto result = evaluateCTFE(caseExpression);
1437                 if(result == ScriptAny.UNDEFINED)
1438                     throw new ScriptCompileException(
1439                         "Case expression must be determined at compile time and cannot be undefined", 
1440                         switchToken);
1441                 if(_currentToken.type != Token.Type.COLON)
1442                     throw new ScriptCompileException("Expected ':' after case expression", _currentToken);
1443                 nextToken();
1444                 if(result in jumpTable)
1445                     throw new ScriptCompileException("Duplicate case entries not allowed", switchToken);
1446                 jumpTable[result] = statementCounter;
1447             }
1448             else if(_currentToken.isKeyword("default"))
1449             {
1450                 caseStarted = true;
1451                 nextToken();
1452                 if(_currentToken.type != Token.Type.COLON)
1453                     throw new ScriptCompileException("':' expected after default keyword", _currentToken);
1454                 nextToken();
1455                 defaultStatementID = statementCounter;
1456             }
1457             else
1458             {
1459                 if(!caseStarted)
1460                     throw new ScriptCompileException("Case condition required before any statements", _currentToken);
1461                 statementNodes ~= parseStatement();
1462                 ++statementCounter;
1463             }
1464         }
1465         nextToken(); // consume }
1466         --_functionContextStack.top.switchStack;
1467         return new SwitchStatementNode(lineNumber, expression, new SwitchBody(statementNodes, defaultStatementID, 
1468             jumpTable));
1469     }
1470 
1471     LambdaNode parseLambda(bool hasParentheses)
1472     {
1473         string[] argList;
1474         ExpressionNode[] defaultArgs;
1475         if(hasParentheses)
1476         {
1477             nextToken(); // eat the (
1478             argList = parseArgumentList(defaultArgs);
1479             nextToken(); // eat the )
1480         }
1481         else
1482         {
1483             if(_currentToken.type != Token.Type.IDENTIFIER)
1484                 throw new ScriptCompileException("Lambda argument name must be valid identifier", _currentToken);
1485             argList ~= _currentToken.text;
1486             nextToken();
1487         }
1488         // make sure arrow
1489         if(_currentToken.type != Token.Type.ARROW)
1490             throw new ScriptCompileException("Arrow expected after lambda argument list", _currentToken);
1491         auto arrow = _currentToken;
1492         nextToken();
1493         // either a single expression, or body marked by {
1494         if(_currentToken.type == Token.Type.LBRACE)
1495         {
1496             nextToken(); // eat the {
1497             auto stmts = parseStatements(Token.Type.RBRACE);
1498             nextToken(); // eat the }
1499             return new LambdaNode(arrow, argList, defaultArgs, stmts);
1500         }
1501         else
1502         {
1503             auto expr = parseExpression();
1504             return new LambdaNode(arrow, argList, defaultArgs, expr);
1505         }
1506     }
1507 
1508     ExpressionNode[] parseCommaSeparatedExpressions(in Token.Type stop)
1509     {
1510         ExpressionNode[] expressions;
1511 
1512         while(_currentToken.type != stop && _currentToken.type != Token.Type.EOF && !_currentToken.isIdentifier("of")
1513           && !_currentToken.isKeyword("in"))
1514         {
1515             auto expression = parseExpression();
1516             expressions ~= expression;
1517             if(_currentToken.type == Token.Type.COMMA)
1518                 nextToken();
1519             else if(_currentToken.type != stop 
1520               && !_currentToken.isIdentifier("of")
1521               && !_currentToken.isKeyword("in"))
1522                 throw new ScriptCompileException("Comma separated list items must be separated by ','" 
1523                     ~ " (or missing '" ~ Token.createFakeToken(stop, "").symbol ~ "')", 
1524                     _currentToken);
1525         }
1526 
1527         return expressions;
1528     }
1529 
1530     string[] parseArgumentList(out ExpressionNode[] defaultArgs)
1531     {
1532         string[] argList;
1533         defaultArgs = [];
1534         while(_currentToken.type != Token.Type.RPAREN && _currentToken.type != Token.Type.EOF)
1535         {
1536             if(_currentToken.type != Token.Type.IDENTIFIER)
1537                 throw new ScriptCompileException("Argument name must be identifier", _currentToken);
1538             argList ~= _currentToken.text;
1539             nextToken(); // eat the identifier
1540 
1541             if(_currentToken.type == Token.Type.ASSIGN)
1542             {
1543                 nextToken(); // eat =
1544                 defaultArgs ~= parseExpression();
1545             }
1546             else if(defaultArgs.length != 0)
1547             {
1548                 throw new ScriptCompileException("Default arguments must be last arguments", _currentToken);
1549             }
1550 
1551             if(_currentToken.type == Token.Type.COMMA)
1552                 nextToken(); // eat ,
1553             else if(_currentToken.type != Token.Type.RPAREN)
1554                 throw new ScriptCompileException("Argument names must be separated by comma", _currentToken);
1555         }
1556         return argList;
1557     }
1558 
1559     void nextToken()
1560     {
1561         if(_tokenIndex >= _tokens.length)
1562             _currentToken = Token(Token.Type.EOF);
1563         else
1564             _currentToken = _tokens[_tokenIndex++];
1565     }
1566 
1567     void putbackToken()
1568     {
1569         if(_tokenIndex > 0)
1570             _currentToken = _tokens[--_tokenIndex];
1571     }
1572 
1573     Token peekToken()
1574     {
1575         return peekTokens(1)[0];
1576     }
1577 
1578     Token[] peekTokens(int numToPeek)
1579     {
1580         Token[] list;
1581         for(size_t i = _tokenIndex; i < _tokenIndex+numToPeek; ++i)
1582         {
1583             if(i < _tokens.length)
1584                 list ~= _tokens[i];
1585             else
1586                 list ~= Token.createFakeToken(Token.Type.EOF, "");
1587         }
1588         return list;
1589     }
1590 
1591     enum FunctionContextType {NORMAL, CONSTRUCTOR, METHOD, GENERATOR}
1592 
1593     struct FunctionContext
1594     {
1595         FunctionContextType fct;
1596         int loopStack;
1597         int switchStack;
1598         string[] labelStack;
1599     }
1600 
1601     Token[] _tokens;
1602     size_t _tokenIndex = 0;
1603     Token _currentToken;
1604     Stack!FunctionContext _functionContextStack;
1605     ExpressionNode[] _baseClassStack; // in case we have nested class declarations
1606 }