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