1 /**
2  * This module implements the Parser struct, which generates Nodes from tokens that are used internally.
3  */
4 module mildew.parser;
5 
6 import std.conv: to, parse;
7 debug
8 {
9     import std.stdio;
10 }
11 
12 import mildew.exceptions: ScriptCompileException;
13 import mildew.lexer: Token;
14 import mildew.nodes;
15 import mildew.types.any: ScriptAny;
16 import mildew.types.func: ScriptFunction;
17 
18 private int unaryOpPrecedence(Token opToken, bool isPost = false)
19 {
20     if(opToken.isKeyword("typeof"))
21     {
22         if(!isPost)
23             return 17;
24     }
25 
26     // see grammar.txt for explanation of magic constants
27     switch(opToken.type)
28     {
29         // TODO handle ++, -- postfix
30         case Token.Type.BIT_NOT: 
31         case Token.Type.NOT:
32         case Token.Type.PLUS:
33         case Token.Type.DASH:
34             if(!isPost)
35                 return 17;
36             else
37                 return 0;
38         case Token.Type.INC:
39         case Token.Type.DEC:
40             if(isPost)
41                 return 18;
42             else
43                 return 17;
44         default: 
45             return 0;
46     }
47 }
48 
49 private int binaryOpPrecedence(Token opToken)
50 {
51     // TODO handle keywords in as 12 here
52     if(opToken.isKeyword("instanceof"))
53         return 12;
54 
55     // see grammar.txt for explanation of magic constants
56     switch(opToken.type)
57     {
58         case Token.Type.LBRACKET:
59         case Token.Type.DOT: 
60         case Token.Type.LPAREN:
61             return 20;
62         case Token.Type.POW:
63             return 16;
64         case Token.Type.STAR: 
65         case Token.Type.FSLASH: 
66         case Token.Type.PERCENT:
67             return 15;
68         case Token.Type.PLUS: 
69         case Token.Type.DASH:
70             return 14;
71         case Token.Type.BIT_LSHIFT: 
72         case Token.Type.BIT_RSHIFT: 
73         case Token.Type.BIT_URSHIFT:
74             return 13;
75         case Token.Type.LT: 
76         case Token.Type.LE: 
77         case Token.Type.GT: 
78         case Token.Type.GE:
79             return 12;
80         case Token.Type.EQUALS: 
81         case Token.Type.NEQUALS: 
82         case Token.Type.STRICT_EQUALS: 
83         case Token.Type.STRICT_NEQUALS:
84             return 11;
85         case Token.Type.BIT_AND:
86             return 10;
87         case Token.Type.BIT_XOR:
88             return 9;
89         case Token.Type.BIT_OR:
90             return 8;
91         case Token.Type.AND:
92             return 7;
93         case Token.Type.OR:
94             return 6;
95         case Token.Type.QUESTION:
96             return 4;
97         case Token.Type.ASSIGN:
98         case Token.Type.PLUS_ASSIGN:
99         case Token.Type.DASH_ASSIGN:
100             return 3;
101         default:
102             return 0;
103     }
104     // TODO null coalesce 5,yield 2, comma 1?
105 }
106 
107 private bool isBinaryOpLeftAssociative(in Token opToken)
108 {
109     switch(opToken.type)
110     {
111         case Token.Type.LBRACKET:
112         case Token.Type.DOT: 
113         case Token.Type.LPAREN:
114             return true;
115         case Token.Type.POW:
116             return false;
117         case Token.Type.STAR: 
118         case Token.Type.FSLASH: 
119         case Token.Type.PERCENT:
120             return true;
121         case Token.Type.PLUS: 
122         case Token.Type.DASH:
123             return true;
124         case Token.Type.BIT_LSHIFT: 
125         case Token.Type.BIT_RSHIFT: 
126         case Token.Type.BIT_URSHIFT:
127             return true;
128         case Token.Type.LT: 
129         case Token.Type.LE: 
130         case Token.Type.GT: 
131         case Token.Type.GE:
132             return true;
133         case Token.Type.EQUALS: 
134         case Token.Type.NEQUALS: 
135         case Token.Type.STRICT_EQUALS: 
136         case Token.Type.STRICT_NEQUALS:
137             return true;
138         case Token.Type.BIT_AND:
139             return true;
140         case Token.Type.BIT_XOR:
141             return true;
142         case Token.Type.BIT_OR:
143             return true;
144         case Token.Type.AND:
145             return true;
146         case Token.Type.OR:
147             return true;
148         case Token.Type.QUESTION:
149             return false;
150         case Token.Type.ASSIGN:
151         case Token.Type.PLUS_ASSIGN:
152         case Token.Type.DASH_ASSIGN:
153             return false;
154         default:
155             return false;
156     }   
157 }
158 
159 private bool tokenBeginsLoops(const Token tok)
160 {
161     return tok.type == Token.Type.LABEL 
162         || tok.isKeyword("while")
163         || tok.isKeyword("do")
164         || tok.isKeyword("for");
165 }
166 
167 /**
168  * The parser is used by the interpreter to generate a syntax tree out of tokens.
169  */
170 struct Parser
171 {
172     /**
173      * The constructor takes all tokens so that in the future, looking ahead for specific tokens
174      * can allow support for lambdas and other complex language features.
175      */
176     this(Token[] tokens)
177     {
178         _tokens = tokens;
179         nextToken(); // prime
180     }
181 
182     /**
183      * The main starting point. Also the "program" grammar rule. The generates a block statement
184      * node where the interpreter iterates through each statement and executes it.
185      */
186     BlockStatementNode parseProgram()
187     {
188         immutable lineNo = _currentToken.position.line;
189         auto statements = parseStatements(Token.Type.EOF);
190         return new BlockStatementNode(lineNo, statements);
191     }
192 
193 package:
194 
195     /// parse a single expression. See https://eli.thegreenplace.net/2012/08/02/parsing-expressions-by-precedence-climbing
196     /// for algorithm.
197     ExpressionNode parseExpression(int minPrec = 1)
198     {      
199         ExpressionNode primaryLeft = null;
200 
201         immutable unOpPrec = _currentToken.unaryOpPrecedence;
202         if(unOpPrec > minPrec)
203         {
204             auto opToken = _currentToken;
205             nextToken();
206             primaryLeft = parsePrimaryExpression();
207             primaryLeft = new UnaryOpNode(opToken, primaryLeft);
208         }
209         else
210         {
211             primaryLeft = parsePrimaryExpression();
212         }
213 
214         while(_currentToken.binaryOpPrecedence >= minPrec || _currentToken.unaryOpPrecedence(true) >= minPrec)
215         {
216             if(_currentToken.unaryOpPrecedence(true) >= minPrec)
217             {
218                 // writeln("We must handle postfix " ~ _currentToken.symbol ~ " for " ~ primaryLeft.toString);
219                 primaryLeft = new PostfixOpNode(_currentToken, primaryLeft);
220                 nextToken();
221             }
222             else 
223             {
224                 auto opToken = _currentToken;
225                 immutable prec = opToken.binaryOpPrecedence;
226                 immutable isLeftAssoc = opToken.isBinaryOpLeftAssociative;
227                 immutable nextMinPrec = isLeftAssoc? prec + 1 : prec;
228                 nextToken();
229                 if(opToken.type == Token.Type.QUESTION)
230                 {
231                     // primaryLeft is our condition node
232                     auto onTrue = parseExpression();
233                     if(_currentToken.type != Token.Type.COLON)
234                         throw new ScriptCompileException("Expected ':' in terniary operator expression", _currentToken);
235                     nextToken();
236                     auto onFalse = parseExpression();
237                     primaryLeft = new TerniaryOpNode(primaryLeft, onTrue, onFalse);
238                 }
239                 else if(opToken.type == Token.Type.DOT)
240                 {
241                     auto right = parsePrimaryExpression();
242                     if(cast(VarAccessNode)right is null)
243                         throw new ScriptCompileException("Object members must be valid identifiers", _currentToken);
244                     if(unOpPrec != 0 && prec > unOpPrec)
245                     {
246                         auto uon = cast(UnaryOpNode)primaryLeft;
247                         primaryLeft = new UnaryOpNode(uon.opToken, new MemberAccessNode(uon.operandNode, right));
248                     }
249                     else
250                         primaryLeft = new MemberAccessNode(primaryLeft, right);
251                 }
252                 else if(opToken.type == Token.Type.LBRACKET)
253                 {
254                     auto index = parseExpression();
255                     if(_currentToken.type != Token.Type.RBRACKET)
256                         throw new ScriptCompileException("Missing ']'", _currentToken);
257                     nextToken();
258                     if(unOpPrec != 0 && prec > unOpPrec)
259                     {
260                         auto uon = cast(UnaryOpNode)primaryLeft;
261                         primaryLeft = new UnaryOpNode(uon.opToken, new ArrayIndexNode(uon.operandNode, index));
262                     }
263                     else
264                         primaryLeft = new ArrayIndexNode(primaryLeft, index);
265                 }
266                 else if(opToken.type == Token.Type.LPAREN)
267                 {
268                     auto params = parseCommaSeparatedExpressions(Token.Type.RPAREN);
269                     nextToken();
270                     if(unOpPrec != 0 && prec > unOpPrec)
271                     {
272                         auto uon = cast(UnaryOpNode)primaryLeft;
273                         primaryLeft = new UnaryOpNode(uon.opToken, new FunctionCallNode(uon.operandNode, params));
274                     }
275                     else
276                         primaryLeft = new FunctionCallNode(primaryLeft, params);
277                 }
278                 else 
279                 {
280                     ExpressionNode primaryRight = parseExpression(nextMinPrec);
281                     primaryLeft = new BinaryOpNode(opToken, primaryLeft, primaryRight);
282                 }
283             }
284         }
285 
286         return primaryLeft;
287     }
288 
289 private:
290 
291     /// parses a single statement
292     StatementNode parseStatement()
293         in { assert(_loopStack >= 0); } do
294     {
295         StatementNode statement;
296         immutable lineNumber = _currentToken.position.line;
297         // check for var declaration
298         if(_currentToken.isKeyword("var") || _currentToken.isKeyword("let") || _currentToken.isKeyword("const"))
299         {
300             statement = parseVarDeclarationStatement();
301         }
302         // check for {} block
303         else if(_currentToken.type == Token.Type.LBRACE)
304         {
305             // TODO: peek two tokens ahead for a ':' to indicate whether or not this is an object literal expression
306             nextToken();
307             auto statements = parseStatements(Token.Type.RBRACE);
308             nextToken();
309             statement = new BlockStatementNode(lineNumber, statements);
310         }
311         // check for if statement
312         else if(_currentToken.isKeyword("if"))
313         {
314             statement = parseIfStatement();
315         }
316         // check for switch
317         else if(_currentToken.isKeyword("switch"))
318         {
319             statement = parseSwitchStatement();
320         }
321         // check for loops
322         else if(_currentToken.tokenBeginsLoops())
323         {
324             statement = parseLoopStatement();
325         }
326         // break statement?
327         else if(_currentToken.isKeyword("break"))
328         {
329             if(_loopStack == 0 && _switchStack == 0)
330                 throw new ScriptCompileException("Break statements only allowed in loops or switch-case bodies", 
331                     _currentToken);
332             nextToken();
333             string label = "";
334             if(_currentToken.type == Token.Type.IDENTIFIER)
335             {
336                 label = _currentToken.text;
337                 nextToken();
338             }
339             if(_currentToken.type != Token.Type.SEMICOLON)
340                 throw new ScriptCompileException("Expected ';' after break", _currentToken);
341             nextToken();
342             statement = new BreakStatementNode(lineNumber, label);
343         }
344         // continue statement
345         else if(_currentToken.isKeyword("continue"))
346         {
347             if(_loopStack == 0)
348                 throw new ScriptCompileException("Continue statements only allowed in loops", _currentToken);
349             nextToken();
350             string label = "";
351             if(_currentToken.type == Token.Type.IDENTIFIER)
352             {
353                 label = _currentToken.text;
354                 nextToken();
355             }
356             if(_currentToken.type != Token.Type.SEMICOLON)
357                 throw new ScriptCompileException("Expected ';' after continue", _currentToken);
358             nextToken();
359             statement = new ContinueStatementNode(lineNumber, label);
360         }
361         // return statement with optional expression
362         else if(_currentToken.isKeyword("return"))
363         {
364             nextToken();
365             ExpressionNode expression = null;
366             if(_currentToken.type != Token.Type.SEMICOLON)
367                 expression = parseExpression();
368             if(_currentToken.type != Token.Type.SEMICOLON)
369                 throw new ScriptCompileException("Expected ';' after return", _currentToken);
370             nextToken();
371             statement = new ReturnStatementNode(lineNumber, expression);
372         }
373         else if(_currentToken.isKeyword("function"))
374         {
375             statement = parseFunctionDeclarationStatement();
376         }
377         else if(_currentToken.isKeyword("throw"))
378         {
379             nextToken();
380             auto expr = parseExpression();
381             if(_currentToken.type != Token.Type.SEMICOLON)
382                 throw new ScriptCompileException("Expected ';' after throw expression", _currentToken);
383             nextToken();
384             statement = new ThrowStatementNode(lineNumber, expr);
385         }
386         else if(_currentToken.isKeyword("try"))
387         {
388             statement = parseTryCatchBlockStatement();
389         }
390         else if(_currentToken.isKeyword("delete"))
391         {
392             nextToken();
393             auto tok = _currentToken;
394             auto expression = parseExpression();
395             if(cast(MemberAccessNode)expression is null && cast(ArrayIndexNode)expression is null)
396                 throw new ScriptCompileException("Invalid operand for delete operation", tok);
397             statement = new DeleteStatementNode(lineNumber, expression);
398         }
399         else if(_currentToken.isKeyword("class"))
400         {
401             statement = parseClassDeclaration();
402         }
403         else if(_currentToken.isKeyword("super"))
404         {
405             statement = parseSuperCallStatement();
406         }
407         else // for now has to be one expression followed by semicolon or EOF
408         {
409             if(_currentToken.type == Token.Type.SEMICOLON)
410             {
411                 // empty statement
412                 statement = new ExpressionStatementNode(lineNumber, null);
413                 nextToken();
414             }
415             else 
416             {
417                 auto expression = parseExpression();
418                 if(_currentToken.type != Token.Type.SEMICOLON && _currentToken.type != Token.Type.EOF)
419                     throw new ScriptCompileException("Expected semicolon after expression", _currentToken);
420                 nextToken(); // eat semicolon
421                 statement = new ExpressionStatementNode(lineNumber, expression);
422             }
423         }
424         return statement;
425     }
426 
427     ExpressionNode parsePrimaryExpression()
428     {
429         import std.conv: to, ConvException;
430 
431         ExpressionNode left = null;
432         switch(_currentToken.type)
433         {
434             case Token.Type.LPAREN:
435                 nextToken();
436                 left = parseExpression();
437                 if(_currentToken.type != Token.Type.RPAREN)
438                     throw new ScriptCompileException("Missing ')' in primary expression", _currentToken);
439                 nextToken();
440                 break;
441             case Token.Type.LBRACE:
442                 left = parseObjectLiteral();
443                 break;
444             case Token.Type.DOUBLE:
445                 if(_currentToken.literalFlag == Token.LiteralFlag.NONE)
446                     left = new LiteralNode(_currentToken, ScriptAny(to!double(_currentToken.text)));
447                 else
448                     throw new ScriptCompileException("Malformed floating point token detected", _currentToken);
449                 nextToken();
450                 break;
451             case Token.Type.INTEGER:
452                 try 
453                 {
454                     if(_currentToken.literalFlag == Token.LiteralFlag.NONE)
455                         left = new LiteralNode(_currentToken, ScriptAny(to!long(_currentToken.text)));
456                     else if(_currentToken.literalFlag == Token.LiteralFlag.HEXADECIMAL)
457                         left = new LiteralNode(_currentToken, ScriptAny(_currentToken.text[2..$].to!long(16)));
458                     else if(_currentToken.literalFlag == Token.LiteralFlag.OCTAL)
459                         left = new LiteralNode(_currentToken, ScriptAny(_currentToken.text[2..$].to!long(8)));
460                     else if(_currentToken.literalFlag == Token.LiteralFlag.BINARY)
461                         left = new LiteralNode(_currentToken, ScriptAny(_currentToken.text[2..$].to!long(2)));
462                 }
463                 catch(ConvException ex)
464                 {
465                     throw new ScriptCompileException("Integer literal is too long", _currentToken);
466                 }
467                 nextToken();
468                 break;
469             case Token.Type.STRING:
470                 left = new LiteralNode(_currentToken, ScriptAny(_currentToken.text));
471                 nextToken();
472                 break;
473             case Token.Type.KEYWORD:
474                 if(_currentToken.text == "true" || _currentToken.text == "false")
475                 {
476                     left = new LiteralNode(_currentToken, ScriptAny(to!bool(_currentToken.text)));
477                     nextToken();
478                 }
479                 else if(_currentToken.text == "null")
480                 {
481                     left = new LiteralNode(_currentToken, ScriptAny(null));
482                     nextToken();
483                 }
484                 else if(_currentToken.text == "undefined")
485                 {
486                     left = new LiteralNode(_currentToken, ScriptAny.UNDEFINED);
487                     nextToken();
488                 }
489                 else if(_currentToken.text == "function") // function literal
490                 {
491                     nextToken();
492                     if(_currentToken.type != Token.Type.LPAREN)
493                         throw new ScriptCompileException("Argument list expected after anonymous function", 
494                             _currentToken);
495                     nextToken();
496                     string[] argNames = [];
497                     while(_currentToken.type != Token.Type.RPAREN)
498                     {
499                         if(_currentToken.type != Token.Type.IDENTIFIER)
500                             throw new ScriptCompileException("Argument list must be valid identifier", _currentToken);
501                         argNames ~= _currentToken.text;
502                         nextToken();
503                         if(_currentToken.type == Token.Type.COMMA)
504                             nextToken();
505                         else if(_currentToken.type !=  Token.Type.RPAREN)
506                             throw new ScriptCompileException("Missing ')' after argument list", _currentToken);
507                     }
508                     nextToken(); // eat the )
509                     if(_currentToken.type != Token.Type.LBRACE)
510                         throw new ScriptCompileException("Expected '{' before anonymous function body", _currentToken);
511                     nextToken(); // eat the {
512                     auto statements = parseStatements(Token.Type.RBRACE);
513                     nextToken();
514                     // auto func = new ScriptFunction(name, argNames, statements, null);
515                     left = new FunctionLiteralNode(argNames, statements);
516                 }
517                 else if(_currentToken.text == "class")
518                 {
519                     left = parseClassExpression();
520                 }
521                 else if(_currentToken.text == "new")
522                 {
523                     immutable newToken = _currentToken;
524                     nextToken();
525                     auto expression = parseExpression();
526                     auto fcn = cast(FunctionCallNode)expression;
527                     if(fcn is null)
528                     {
529                         // if this isn't a function call, turn it into one
530                         fcn = new FunctionCallNode(expression, [], true);
531                     }
532                     fcn.returnThis = true;
533                     left = new NewExpressionNode(fcn);                    
534                 }
535                 else
536                     throw new ScriptCompileException("Unexpected keyword in primary expression", _currentToken);
537                 break;
538                 // TODO function
539             case Token.Type.IDENTIFIER:
540                 left = new VarAccessNode(_currentToken);
541                 nextToken();
542                 break;
543             case Token.Type.LBRACKET: // an array
544             {
545                 nextToken(); // eat the [
546                 auto values = parseCommaSeparatedExpressions(Token.Type.RBRACKET);
547                 nextToken(); // eat the ]
548                 left = new ArrayLiteralNode(values);
549                 break;
550             }
551             default:
552                 throw new ScriptCompileException("Unexpected token in primary expression", _currentToken);
553         }
554         return left;
555     }
556     
557     /// after class ? extend base this can begin
558     ClassDefinition parseClassDefinition(Token classToken, string className, ExpressionNode baseClass)
559     {
560         if(_currentToken.type != Token.Type.LBRACE)
561             throw new ScriptCompileException("Expected '{' after class", _currentToken);
562         nextToken();
563         FunctionLiteralNode constructor;
564         string[] methodNames;
565         FunctionLiteralNode[] methods;
566         string[] getMethodNames;
567         FunctionLiteralNode[] getMethods;
568         string[] setMethodNames;
569         FunctionLiteralNode[] setMethods;
570         string[] staticMethodNames;
571         FunctionLiteralNode[] staticMethods;
572         enum PropertyType { NONE, GET, SET, STATIC }
573         while(_currentToken.type != Token.Type.RBRACE && _currentToken.type != Token.Type.EOF)
574         {
575             PropertyType ptype = PropertyType.NONE;
576             string currentMethodName = "";
577             // could be a get or set
578             if(_currentToken.isIdentifier("get"))
579             {
580                 ptype = PropertyType.GET;
581                 nextToken();
582             }
583             else if(_currentToken.isIdentifier("set"))
584             {
585                 ptype = PropertyType.SET;
586                 nextToken();
587             }
588             else if(_currentToken.isIdentifier("static"))
589             {
590                 ptype = PropertyType.STATIC;
591                 nextToken();
592             }
593             // then an identifier
594             if(_currentToken.type != Token.Type.IDENTIFIER)
595                 throw new ScriptCompileException("Method names must be valid identifiers", _currentToken);
596             currentMethodName = _currentToken.text;
597             nextToken();
598             // then a (
599             if(_currentToken.type != Token.Type.LPAREN)
600                 throw new ScriptCompileException("Expected '(' after method name", _currentToken);
601             nextToken();
602             string[] argNames;
603             while(_currentToken.type != Token.Type.RPAREN)
604             {
605                 if(_currentToken.type != Token.Type.IDENTIFIER)
606                     throw new ScriptCompileException("Method arguments must be valid identifiers", _currentToken);
607                 argNames ~= _currentToken.text;
608                 nextToken();
609                 if(_currentToken.type == Token.Type.COMMA)
610                     nextToken();
611                 else if(_currentToken.type != Token.Type.RPAREN)
612                     throw new ScriptCompileException("Method arguments must be separated by ','", _currentToken);
613             }
614             nextToken(); // eat the )
615             // then a {
616             if(_currentToken.type != Token.Type.LBRACE)
617                 throw new ScriptCompileException("Method bodies must begin with '{'", _currentToken);
618             nextToken();
619             auto statements = parseStatements(Token.Type.RBRACE);
620             nextToken(); // eat }
621             // now we have a method but if this is the constructor
622             if(currentMethodName == "constructor")
623             {
624                 if(ptype != PropertyType.NONE)
625                     throw new ScriptCompileException("Get and set not allowed for constructor", classToken);
626                 if(constructor !is null)
627                     throw new ScriptCompileException("Classes may only have one constructor", classToken);
628                 // if this is extending a class it MUST have ONE super call
629                 if(baseClass !is null)
630                 {
631                     ulong numSupers = 0;
632                     foreach(stmt ; statements)
633                     {
634                         if(cast(SuperCallStatementNode)stmt)
635                             numSupers++;
636                     }
637                     if(numSupers != 1)
638                         throw new ScriptCompileException("Derived class constructors must have one super call", 
639                                 classToken);
640                 }
641                 constructor = new FunctionLiteralNode(argNames, statements);
642             }
643             else // it's a normal method or getter/setter
644             {
645                 if(ptype == PropertyType.NONE)
646                 {
647                     methods ~= new FunctionLiteralNode(argNames, statements);
648                     methodNames ~= currentMethodName;
649                 }
650                 else if(ptype == PropertyType.GET)
651                 {
652                     getMethods ~= new FunctionLiteralNode(argNames, statements);
653                     getMethodNames ~= currentMethodName;                    
654                 }
655                 else if(ptype == PropertyType.SET)
656                 {
657                     setMethods ~= new FunctionLiteralNode(argNames, statements);
658                     setMethodNames ~= currentMethodName;                    
659                 }
660                 else if(ptype == PropertyType.STATIC)
661                 {
662                     staticMethods ~= new FunctionLiteralNode(argNames, statements);
663                     staticMethodNames ~= currentMethodName;
664                 }
665             }
666         }
667         nextToken(); // eat the class body }
668 
669         // check for duplicate methods
670         bool[string] mnameMap;
671         foreach(mname ; methodNames)
672         {
673             if(mname in mnameMap)
674                 throw new ScriptCompileException("Duplicate methods are not allowed", classToken);
675             mnameMap[mname] = true;
676         }
677 
678         if(baseClass !is null)
679             _baseClassStack = _baseClassStack[0..$-1];
680        	return new ClassDefinition(className, constructor, methodNames, methods, getMethodNames, getMethods, 
681 	   		setMethodNames, setMethods, staticMethodNames, staticMethods, baseClass);
682     }
683 
684     ClassLiteralNode parseClassExpression()
685     {
686         immutable classToken = _currentToken;
687         nextToken();
688         immutable className = "<anonymous class>";
689         ExpressionNode baseClass = null;
690         if(_currentToken.isKeyword("extends"))
691         {
692             nextToken();
693             baseClass = parseExpression(); // let's hope this is an expression that results in a ScriptFunction value
694             _baseClassStack ~= baseClass;
695         }
696         auto classDef = parseClassDefinition(classToken, className, baseClass);
697         return new ClassLiteralNode(classDef);
698     }
699 
700     /// parses multiple statements until reaching stop
701     StatementNode[] parseStatements(in Token.Type stop)
702     {
703         StatementNode[] statements;
704         while(_currentToken.type != stop && _currentToken.type != Token.Type.EOF)
705         {
706             statements ~= parseStatement();
707             // each statement parse should eat the semicolon or } so there's nothing to do here
708         }
709         return statements;
710     }
711 
712     VarDeclarationStatementNode parseVarDeclarationStatement(bool consumeSemicolon = true)
713     {
714         auto specifier = _currentToken;
715         nextToken();
716         auto expressions = parseCommaSeparatedExpressions(Token.Type.SEMICOLON);
717         // make sure all expressions are valid BinaryOpNodes or VarAccessNodes
718         foreach(expression; expressions)
719         {
720             if(auto node = cast(BinaryOpNode)expression)
721             {
722                 if(!cast(VarAccessNode)node.leftNode)
723                     throw new ScriptCompileException("Invalid assignment node", _currentToken);
724             }
725             else if(!cast(VarAccessNode)expression)
726             {
727                 throw new ScriptCompileException("Invalid variable name in declaration", _currentToken);
728             }
729         }
730         if(consumeSemicolon)
731             nextToken(); // eat semicolon
732         return new VarDeclarationStatementNode(specifier, expressions);
733     }
734 
735     IfStatementNode parseIfStatement()
736     {
737         immutable lineNumber = _currentToken.position.line;
738         nextToken();
739         if(_currentToken.type != Token.Type.LPAREN)
740             throw new ScriptCompileException("Expected '(' after if keyword", _currentToken);
741         nextToken();
742         auto condition = parseExpression();
743         if(_currentToken.type != Token.Type.RPAREN)
744             throw new ScriptCompileException("Expected ')' after if condition", _currentToken);
745         nextToken();
746         auto ifTrueStatement = parseStatement();
747         StatementNode elseStatement = null;
748         if(_currentToken.isKeyword("else"))
749         {
750             nextToken();
751             elseStatement = parseStatement();
752         }
753         return new IfStatementNode(lineNumber, condition, ifTrueStatement, elseStatement);
754     }
755 
756     StatementNode parseLoopStatement()
757     {
758         string label = "";
759         if(_currentToken.type == Token.Type.LABEL)
760         {
761             label = _currentToken.text;
762             nextToken();
763         }
764         StatementNode statement;
765         if(_currentToken.isKeyword("while"))
766         {
767             ++_loopStack;
768             statement = parseWhileStatement(label);
769             --_loopStack;
770         }
771         // check for do-while statement TODO check for label
772         else if(_currentToken.isKeyword("do"))
773         {
774             ++_loopStack;
775             statement = parseDoWhileStatement(label);
776             --_loopStack;
777         }
778         // check for for loop TODO check label
779         else if(_currentToken.isKeyword("for"))
780         {
781             ++_loopStack;
782             statement = parseForStatement(label);
783             --_loopStack;
784         }
785         return statement;
786     }
787 
788     WhileStatementNode parseWhileStatement(string label = "")
789     {
790         immutable lineNumber = _currentToken.position.line;
791         nextToken();
792         if(_currentToken.type != Token.Type.LPAREN)
793             throw new ScriptCompileException("Expected '(' after while keyword", _currentToken);
794         nextToken();
795         auto condition = parseExpression();
796         if(_currentToken.type != Token.Type.RPAREN)
797             throw new ScriptCompileException("Expected ')' after while condition", _currentToken);
798         nextToken();
799         auto loopBody = parseStatement();
800         return new WhileStatementNode(lineNumber, condition, loopBody, label);
801     }
802 
803     DoWhileStatementNode parseDoWhileStatement(string label = "")
804     {
805         immutable lineNumber = _currentToken.position.line;
806         nextToken();
807         auto loopBody = parseStatement();
808         if(!_currentToken.isKeyword("while"))
809             throw new ScriptCompileException("Expected while keyword after do statement", _currentToken);
810         nextToken();
811         if(_currentToken.type != Token.Type.LPAREN)
812             throw new ScriptCompileException("Expected '(' before do-while condition", _currentToken);
813         nextToken();
814         auto condition = parseExpression();
815         if(_currentToken.type != Token.Type.RPAREN)
816             throw new ScriptCompileException("Expected ')' after do-while condition", _currentToken);
817         nextToken();
818         if(_currentToken.type != Token.Type.SEMICOLON)
819             throw new ScriptCompileException("Expected ';' after do-while statement", _currentToken);
820         nextToken();
821         return new DoWhileStatementNode(lineNumber, loopBody, condition, label);
822     }
823 
824     StatementNode parseForStatement(string label = "")
825     {
826         immutable lineNumber = _currentToken.position.line;
827         nextToken();
828         if(_currentToken.type != Token.Type.LPAREN)
829             throw new ScriptCompileException("Expected '(' after for keyword", _currentToken);
830         nextToken();
831         VarDeclarationStatementNode decl = null;
832         if(_currentToken.type != Token.Type.SEMICOLON)
833             decl = parseVarDeclarationStatement(false);
834         if(_currentToken.isKeyword("of") || _currentToken.isKeyword("in"))
835         {
836             // first we need to validate the VarDeclarationStatementNode to make sure it only consists
837             // of let or const and VarAccessNodes
838             if(decl is null)
839                 throw new ScriptCompileException("Invalid for of statement", _currentToken);
840             Token qualifier;
841             VarAccessNode[] vans;
842             if(decl.qualifier.text != "const" && decl.qualifier.text != "let")
843                 throw new ScriptCompileException("Global variable declaration invalid in for of statement",
844                     decl.qualifier);
845             foreach(va ; decl.varAccessOrAssignmentNodes)
846             {
847                 auto valid = cast(VarAccessNode)va;
848                 if(valid is null)
849                     throw new ScriptCompileException("Invalid variable declaration in for of statement", 
850                         _currentToken);
851                 vans ~= valid;
852             }
853             nextToken();
854             auto objToIterateExpr = parseExpression();
855             if(_currentToken.type != Token.Type.RPAREN)
856                 throw new ScriptCompileException("Expected ')' after array or object", _currentToken);
857             nextToken();
858             auto bodyStatement = parseStatement();
859             return new ForOfStatementNode(lineNumber, qualifier, vans, objToIterateExpr, bodyStatement, label);
860         }
861         else if(_currentToken.type == Token.Type.SEMICOLON)
862         {
863             nextToken();
864             ExpressionNode condition = null;
865             if(_currentToken.type != Token.Type.SEMICOLON)
866             {
867                 condition = parseExpression();
868                 if(_currentToken.type != Token.Type.SEMICOLON)
869                     throw new ScriptCompileException("Expected ';' after for condition", _currentToken);
870             }
871             else
872             {
873                 condition = new LiteralNode(_currentToken, ScriptAny(true));
874             }
875             nextToken();
876             ExpressionNode increment = null;
877             if(_currentToken.type != Token.Type.RPAREN)
878             {
879                 increment = parseExpression();
880             }
881             else
882             {
883                 increment = new LiteralNode(_currentToken, ScriptAny(true));
884             }
885             if(_currentToken.type != Token.Type.RPAREN)
886                 throw new ScriptCompileException("Expected ')' before for loop body", _currentToken);
887             nextToken();
888             auto bodyNode = parseStatement();
889             return new ForStatementNode(lineNumber, decl, condition, increment, bodyNode, label);
890         }
891         else
892             throw new ScriptCompileException("Invalid for statement", _currentToken);
893     }
894 
895     FunctionDeclarationStatementNode parseFunctionDeclarationStatement()
896     {
897         import std.algorithm: uniq, count;
898         immutable lineNumber = _currentToken.position.line;
899         nextToken();
900         if(_currentToken.type != Token.Type.IDENTIFIER)
901             throw new ScriptCompileException("Expected identifier after function keyword", _currentToken);
902         string name = _currentToken.text;
903         nextToken();
904         if(_currentToken.type != Token.Type.LPAREN)
905             throw new ScriptCompileException("Expected '(' after function name", _currentToken);
906         nextToken();
907         string[] argNames = [];
908         while(_currentToken.type != Token.Type.RPAREN)
909         {
910             if(_currentToken.type != Token.Type.IDENTIFIER)
911                 throw new ScriptCompileException("Function argument names must be valid identifiers", _currentToken);
912             argNames ~= _currentToken.text;
913             nextToken();
914             if(_currentToken.type == Token.Type.COMMA)
915                 nextToken();
916             else if(_currentToken.type != Token.Type.RPAREN)
917                 throw new ScriptCompileException("Function argument names must be separated by comma", _currentToken);
918         }
919         nextToken(); // eat the )
920 
921         // make sure there are no duplicate parameter names
922         if(argNames.uniq.count != argNames.length)
923             throw new ScriptCompileException("Function argument names must be unique", _currentToken);
924 
925         if(_currentToken.type != Token.Type.LBRACE)
926             throw new ScriptCompileException("Function definition must begin with '{'", _currentToken);
927         nextToken();
928         auto statements = parseStatements(Token.Type.RBRACE);
929         nextToken(); // eat the }
930         return new FunctionDeclarationStatementNode(lineNumber, name, argNames, statements);
931     }
932 
933     TryCatchBlockStatementNode parseTryCatchBlockStatement()
934     {
935         immutable lineNumber = _currentToken.position.line;
936         nextToken(); // eat the 'try'
937         auto tryBlock = parseStatement();
938         if(!_currentToken.isKeyword("catch"))
939             throw new ScriptCompileException("Catch block required after try block", _currentToken);
940         nextToken(); // eat the catch
941         if(_currentToken.type != Token.Type.LPAREN)
942             throw new ScriptCompileException("Missing '(' after catch", _currentToken);
943         nextToken(); // eat the '('
944         if(_currentToken.type != Token.Type.IDENTIFIER)
945             throw new ScriptCompileException("Name of exception required in catch block", _currentToken);
946         auto name = _currentToken.text;
947         nextToken();
948         if(_currentToken.type != Token.Type.RPAREN)
949             throw new ScriptCompileException("Missing ')' after exception name", _currentToken);
950         nextToken(); // eat the ')'
951         auto catchBlock = parseStatement();
952         return new TryCatchBlockStatementNode(lineNumber, tryBlock, name, catchBlock);
953     }
954 
955     ObjectLiteralNode parseObjectLiteral()
956     {
957         nextToken(); // eat the {
958         string[] keys = [];
959         ExpressionNode[] valueExpressions = [];
960         while(_currentToken.type != Token.Type.RBRACE)
961         {
962             // first must be an identifier token or string literal token
963             immutable idToken = _currentToken;
964             if(_currentToken.type != Token.Type.IDENTIFIER && _currentToken.type != Token.Type.STRING
965                 && _currentToken.type != Token.Type.LABEL)
966                 throw new ScriptCompileException("Invalid key for object literal", _currentToken);
967             keys ~= _currentToken.text;
968 
969             nextToken();
970             // next must be a :
971             if(idToken.type != Token.Type.LABEL)
972             {
973                 if(_currentToken.type != Token.Type.COLON)
974                     throw new ScriptCompileException("Expected ':' after key in object literal", _currentToken);
975                 nextToken();
976             }
977             // next can be any valid expression
978             valueExpressions ~= parseExpression();
979             // if next is not a comma it must be a closing brace to exit
980             if(_currentToken.type == Token.Type.COMMA)
981                 nextToken();
982             else if(_currentToken.type != Token.Type.RBRACE)
983                 throw new ScriptCompileException("Key value pairs must be separated by ','", _currentToken);
984         }
985         nextToken(); // eat the }
986         if(keys.length != valueExpressions.length)
987             throw new ScriptCompileException("Number of keys must match values in object literal", _currentToken);
988         return new ObjectLiteralNode(keys, valueExpressions);
989     }
990 
991     ClassDeclarationStatementNode parseClassDeclaration()
992     {
993         immutable lineNumber = _currentToken.position.line;
994         immutable classToken = _currentToken;
995         nextToken();
996         if(_currentToken.type != Token.Type.IDENTIFIER)
997             throw new ScriptCompileException("Class name must be valid identifier", _currentToken);
998         auto className = _currentToken.text;
999         nextToken();
1000         ExpressionNode baseClass = null;
1001         if(_currentToken.isKeyword("extends"))
1002         {
1003             nextToken();
1004             baseClass = parseExpression(); // let's hope this is an expression that results in a ScriptFunction value
1005             _baseClassStack ~= baseClass;
1006         }
1007         auto classDef = parseClassDefinition(classToken, className, baseClass);
1008         return new ClassDeclarationStatementNode(lineNumber, classDef);
1009     }
1010 
1011     SuperCallStatementNode parseSuperCallStatement()
1012     {
1013         immutable lineNumber = _currentToken.position.line;
1014         if(_baseClassStack.length == 0)
1015             throw new ScriptCompileException("Super keyword may only be used in constructors of derived classes", 
1016                     _currentToken);
1017         nextToken();
1018         if(_currentToken.type != Token.Type.LPAREN)
1019             throw new ScriptCompileException("Super call parameters must begin with '('", _currentToken);
1020         nextToken();
1021         auto expressions = parseCommaSeparatedExpressions(Token.Type.RPAREN);
1022         nextToken(); // eat the )
1023         if(_currentToken.type != Token.Type.SEMICOLON)
1024             throw new ScriptCompileException("Missing ';' at end of super statement", _currentToken);
1025         nextToken();
1026         size_t topClass = _baseClassStack.length - 1; // @suppress(dscanner.suspicious.length_subtraction)
1027         return new SuperCallStatementNode(lineNumber, _baseClassStack[topClass], expressions);
1028     }
1029 
1030     SwitchStatementNode parseSwitchStatement() 
1031         in { assert(_switchStack >= 0); } do
1032     {
1033         import std.variant: Variant;
1034         import mildew.interpreter: Interpreter;
1035         import mildew.exceptions: ScriptRuntimeException;
1036 
1037         ++_switchStack;
1038         immutable lineNumber = _currentToken.position.line;
1039         immutable switchToken = _currentToken;
1040         nextToken();
1041         if(_currentToken.type != Token.Type.LPAREN)
1042             throw new ScriptCompileException("Expected '(' after switch keyword", _currentToken);
1043         nextToken();
1044         auto expression = parseExpression();
1045         if(_currentToken.type != Token.Type.RPAREN)
1046             throw new ScriptCompileException("Expected ')' after switch expression", _currentToken);
1047         nextToken();
1048         if(_currentToken.type != Token.Type.LBRACE)
1049             throw new ScriptCompileException("Expected '{' to begin switch body", _currentToken);
1050         nextToken();
1051         bool caseStarted = false;
1052         size_t statementCounter = 0;
1053         StatementNode[] statementNodes;
1054         size_t defaultStatementID = size_t.max;
1055         size_t[ScriptAny] jumpTable;
1056         Interpreter interpreter = new Interpreter();
1057         while(_currentToken.type != Token.Type.RBRACE)
1058         {
1059             if(_currentToken.isKeyword("case"))
1060             {
1061                 nextToken();
1062                 caseStarted = true;
1063                 auto caseExpression = parseExpression();
1064                 // it has to be evaluatable at compile time
1065                 auto vr = caseExpression.accept(interpreter).get!(Interpreter.VisitResult);
1066                 if(vr.exception !is null || vr.result == ScriptAny.UNDEFINED)
1067                     throw new ScriptCompileException("Case expression must be determined at compile time", switchToken);
1068                 if(_currentToken.type != Token.Type.COLON)
1069                     throw new ScriptCompileException("Expected ':' after case expression", _currentToken);
1070                 nextToken();
1071                 if(vr.result in jumpTable)
1072                     throw new ScriptCompileException("Duplicate case entries not allowed", switchToken);
1073                 jumpTable[vr.result] = statementCounter;
1074             }
1075             else if(_currentToken.isKeyword("default"))
1076             {
1077                 caseStarted = true;
1078                 nextToken();
1079                 if(_currentToken.type != Token.Type.COLON)
1080                     throw new ScriptCompileException("':' expected after default keyword", _currentToken);
1081                 nextToken();
1082                 defaultStatementID = statementCounter;
1083             }
1084             else
1085             {
1086                 if(!caseStarted)
1087                     throw new ScriptCompileException("Case condition required before any statements", _currentToken);
1088                 statementNodes ~= parseStatement();
1089                 ++statementCounter;
1090             }
1091         }
1092         nextToken(); // consume }
1093         --_switchStack;
1094         return new SwitchStatementNode(lineNumber, expression, new SwitchBody(statementNodes, defaultStatementID, 
1095             jumpTable));
1096     }
1097 
1098     ExpressionNode[] parseCommaSeparatedExpressions(in Token.Type stop)
1099     {
1100         ExpressionNode[] expressions;
1101 
1102         while(_currentToken.type != stop && _currentToken.type != Token.Type.EOF && !_currentToken.isKeyword("of")
1103           && !_currentToken.isKeyword("in"))
1104         {
1105             auto expression = parseExpression();
1106             expressions ~= expression;
1107             if(_currentToken.type == Token.Type.COMMA)
1108                 nextToken();
1109             else if(_currentToken.type != stop && !_currentToken.isKeyword("of")
1110               && !_currentToken.isKeyword("in"))
1111                 throw new ScriptCompileException("Comma separated list items must be separated by ','", _currentToken);
1112         }
1113 
1114         return expressions;
1115     }
1116 
1117     void nextToken()
1118     {
1119         if(_tokenIndex >= _tokens.length)
1120             _currentToken = Token(Token.Type.EOF);
1121         else
1122             _currentToken = _tokens[_tokenIndex++];
1123     }
1124 
1125     Token[] _tokens;
1126     size_t _tokenIndex = 0;
1127     Token _currentToken;
1128     int _loopStack = 0;
1129     int _switchStack = 0;
1130     ExpressionNode[] _baseClassStack; // in case we have nested class declarations
1131 }