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