1 module graphql.lexer; 2 3 import std.experimental.logger; 4 import std.format : format; 5 import std.typecons :Flag; 6 import std.stdio; 7 8 import graphql.tokenmodule; 9 10 alias QueryParser = Flag!"QueryParser"; 11 12 struct Lexer { 13 const QueryParser qp; 14 string input; 15 size_t stringPos; 16 17 size_t line; 18 size_t column; 19 20 Token cur; 21 22 this(string input, QueryParser qp = QueryParser.yes) @safe { 23 this.input = input; 24 this.stringPos = 0; 25 this.line = 1; 26 this.column = 1; 27 this.qp = qp; 28 this.buildToken(); 29 } 30 31 bool isNotQueryParser() @safe const { 32 return this.qp == QueryParser.no; 33 } 34 35 private bool isTokenStop() const @safe { 36 return this.stringPos >= this.input.length 37 || this.isTokenStop(this.input[this.stringPos]); 38 } 39 40 private bool isTokenStop(const(char) c) const @safe { 41 return 42 c == ' ' || c == '\t' || c == '\n' || c == ';' || c == '(' 43 || c == ')' || c == '{' || c == '}' || c == '!' || c == '=' 44 || c == '|' || c == '*' || c == '/' || c == '[' || c == ':' 45 || c == ']' || c == ',' || c == '@' || c == '#' || c == '$'; 46 } 47 48 private void eatComment() @safe { 49 if(this.stringPos < this.input.length && 50 this.input[this.stringPos] == '#') 51 { 52 while(this.stringPos < this.input.length && 53 this.input[this.stringPos] != '\n') 54 { 55 ++this.stringPos; 56 } 57 ++this.stringPos; 58 ++this.line; 59 } 60 } 61 62 private void eatWhitespace() @safe { 63 import std.ascii : isWhite; 64 while(this.stringPos < this.input.length) { 65 this.eatComment(); 66 if(this.input[this.stringPos] == ' ') { 67 ++this.column; 68 } else if(this.input[this.stringPos] == '\t') { 69 ++this.column; 70 } else if(this.input[this.stringPos] == '\n') { 71 this.column = 1; 72 ++this.line; 73 } else { 74 break; 75 } 76 ++this.stringPos; 77 } 78 } 79 80 private void buildToken() @safe { 81 import std.ascii : isAlphaNum; 82 this.eatWhitespace(); 83 84 if(this.stringPos >= this.input.length) { 85 this.cur = Token(TokenType.undefined); 86 return; 87 } 88 89 if(this.input[this.stringPos] == ')') { 90 this.cur = Token(TokenType.rparen, this.line, this.column); 91 ++this.column; 92 ++this.stringPos; 93 } else if(this.input[this.stringPos] == '(') { 94 this.cur = Token(TokenType.lparen, this.line, this.column); 95 ++this.column; 96 ++this.stringPos; 97 } else if(this.input[this.stringPos] == ']') { 98 this.cur = Token(TokenType.rbrack, this.line, this.column); 99 ++this.column; 100 ++this.stringPos; 101 } else if(this.input[this.stringPos] == '[') { 102 this.cur = Token(TokenType.lbrack, this.line, this.column); 103 ++this.column; 104 ++this.stringPos; 105 } else if(this.input[this.stringPos] == '}') { 106 this.cur = Token(TokenType.rcurly, this.line, this.column); 107 ++this.column; 108 ++this.stringPos; 109 } else if(this.input[this.stringPos] == '$') { 110 this.cur = Token(TokenType.dollar, this.line, this.column); 111 ++this.column; 112 ++this.stringPos; 113 } else if(this.input[this.stringPos] == '!') { 114 this.cur = Token(TokenType.exclamation, this.line, this.column); 115 ++this.column; 116 ++this.stringPos; 117 } else if(this.input[this.stringPos] == '{') { 118 this.cur = Token(TokenType.lcurly, this.line, this.column); 119 ++this.column; 120 ++this.stringPos; 121 } else if(this.input[this.stringPos] == '|') { 122 this.cur = Token(TokenType.pipe, this.line, this.column); 123 ++this.column; 124 ++this.stringPos; 125 } else if(this.input[this.stringPos] == '@') { 126 this.cur = Token(TokenType.at, this.line, this.column); 127 ++this.column; 128 ++this.stringPos; 129 } else if(this.input[this.stringPos] == ',') { 130 this.cur = Token(TokenType.comma, this.line, this.column); 131 ++this.column; 132 ++this.stringPos; 133 } else if(this.input[this.stringPos] == '=') { 134 this.cur = Token(TokenType.equal, this.line, this.column); 135 ++this.column; 136 ++this.stringPos; 137 } else if(this.input[this.stringPos] == ':') { 138 this.cur = Token(TokenType.colon, this.line, this.column); 139 ++this.column; 140 ++this.stringPos; 141 } else { 142 size_t b = this.stringPos; 143 size_t e = this.stringPos; 144 switch(this.input[this.stringPos]) { 145 case 'm': 146 ++this.stringPos; 147 ++this.column; 148 ++e; 149 if(this.testCharAndInc('u', e)) { 150 if(this.testCharAndInc('t', e)) { 151 if(this.testCharAndInc('a', e)) { 152 if(this.testCharAndInc('t', e)) { 153 if(this.testCharAndInc('i', e)) { 154 if(this.testCharAndInc('o', e)) { 155 if(this.testCharAndInc('n', e)) { 156 if(this.isTokenStop()) { 157 this.cur = 158 Token(TokenType.mutation, 159 this.line, 160 this.column); 161 return; 162 } 163 } 164 } 165 } 166 } 167 } 168 } 169 } 170 goto default; 171 case 's': 172 ++this.stringPos; 173 ++this.column; 174 ++e; 175 if(this.isNotQueryParser() && this.testCharAndInc('u', e)) { 176 if(this.testCharAndInc('b', e)) { 177 if(this.testCharAndInc('s', e)) { 178 if(this.testCharAndInc('c', e)) { 179 if(this.testCharAndInc('r', e)) { 180 if(this.testCharAndInc('i', e)) { 181 if(this.testCharAndInc('p', e)) { 182 if(this.testCharAndInc('t', e)) { 183 if(this.testCharAndInc('i', e)) { 184 if(this.testCharAndInc('o', e)) { 185 if(this.testCharAndInc('n', e)) { 186 if(this.isTokenStop()) { 187 this.cur = 188 Token(TokenType.subscription, 189 this.line, 190 this.column); 191 return; 192 } 193 } 194 } 195 } 196 } 197 } 198 } 199 } 200 } 201 } 202 } 203 } else if(this.isNotQueryParser() 204 && this.testCharAndInc('c', e)) 205 { 206 if(this.testCharAndInc('a', e)) { 207 if(this.testCharAndInc('l', e)) { 208 if(this.testCharAndInc('a', e)) { 209 if(this.testCharAndInc('r', e)) { 210 if(this.isTokenStop()) { 211 this.cur = Token(TokenType.scalar, this.line, this.column); 212 return; 213 } 214 } 215 } 216 } 217 } else if(this.isNotQueryParser() 218 && this.testCharAndInc('h', e)) 219 { 220 if(this.testCharAndInc('e', e)) { 221 if(this.testCharAndInc('m', e)) { 222 if(this.testCharAndInc('a', e)) { 223 if(this.isTokenStop()) { 224 this.cur = Token(TokenType.schema, this.line, this.column); 225 return; 226 } 227 } 228 } 229 } 230 } 231 } 232 goto default; 233 case 'o': 234 ++this.stringPos; 235 ++this.column; 236 ++e; 237 if(this.testCharAndInc('n', e)) { 238 if(this.isTokenStop()) { 239 this.cur = Token(TokenType.on_, this.line, 240 this.column); 241 return; 242 } 243 } 244 goto default; 245 case 'd': 246 ++this.stringPos; 247 ++this.column; 248 ++e; 249 if(this.testCharAndInc('i', e)) { 250 if(this.testCharAndInc('r', e)) { 251 if(this.testCharAndInc('e', e)) { 252 if(this.testCharAndInc('t', e)) { 253 if(this.testCharAndInc('i', e)) { 254 if(this.testCharAndInc('v', e)) { 255 if(this.testCharAndInc('e', e)) { 256 if(this.isTokenStop()) { 257 this.cur = Token(TokenType.directive, 258 this.line, this.column); 259 return; 260 } 261 } 262 } 263 } 264 } 265 } 266 } 267 } 268 goto default; 269 case 'e': 270 ++this.stringPos; 271 ++this.column; 272 ++e; 273 if(this.testCharAndInc('n', e)) { 274 if(this.testCharAndInc('u', e)) { 275 if(this.testCharAndInc('m', e)) { 276 if(this.isTokenStop()) { 277 this.cur = Token(TokenType.enum_, 278 this.line, this.column); 279 return; 280 } 281 } 282 } 283 } else if(this.testCharAndInc('x', e)) { 284 if(this.testCharAndInc('t', e)) { 285 if(this.testCharAndInc('e', e)) { 286 if(this.testCharAndInc('n', e)) { 287 if(this.testCharAndInc('d', e)) { 288 if(this.isTokenStop()) { 289 this.cur = Token(TokenType.extend, 290 this.line, this.column); 291 return; 292 } 293 } 294 } 295 } 296 } 297 } 298 goto default; 299 case 'i': 300 ++this.stringPos; 301 ++this.column; 302 ++e; 303 if(this.testCharAndInc('n', e)) { 304 if(this.isNotQueryParser() 305 && this.testCharAndInc('p', e) 306 ) 307 { 308 if(this.testCharAndInc('u', e)) { 309 if(this.testCharAndInc('t', e)) { 310 if(this.isTokenStop()) { 311 this.cur = Token(TokenType.input, 312 this.line, this.column); 313 return; 314 } 315 } 316 } 317 } else if(this.testCharAndInc('t', e)) { 318 if(this.testCharAndInc('e', e)) { 319 if(this.testCharAndInc('r', e)) { 320 if(this.testCharAndInc('f', e)) { 321 if(this.testCharAndInc('a', e)) { 322 if(this.testCharAndInc('c', e)) { 323 if(this.testCharAndInc('e', e)) { 324 if(this.isTokenStop()) { 325 this.cur = Token(TokenType.interface_, 326 this.line, this.column); 327 return; 328 } 329 } 330 } 331 } 332 } 333 } 334 } 335 } 336 } else if(this.testCharAndInc('m', e)) { 337 if(this.testCharAndInc('p', e)) { 338 if(this.testCharAndInc('l', e)) { 339 if(this.testCharAndInc('e', e)) { 340 if(this.testCharAndInc('m', e)) { 341 if(this.testCharAndInc('e', e)) { 342 if(this.testCharAndInc('n', e)) { 343 if(this.testCharAndInc('t', e)) { 344 if(this.testCharAndInc('s', e)) { 345 if(this.isTokenStop()) { 346 this.cur = Token(TokenType.implements, 347 this.line, this.column); 348 return; 349 } 350 } 351 } 352 } 353 } 354 } 355 } 356 } 357 } 358 } 359 360 goto default; 361 case 'f': 362 ++this.stringPos; 363 ++this.column; 364 ++e; 365 if(this.testCharAndInc('a', e)) { 366 if(this.testCharAndInc('l', e)) { 367 if(this.testCharAndInc('s', e)) { 368 if(this.testCharAndInc('e', e)) { 369 if(this.isTokenStop()) { 370 this.cur = Token(TokenType.false_, 371 this.line, this.column); 372 return; 373 } 374 } 375 } 376 } 377 } else if(this.testCharAndInc('r', e)) { 378 if(this.testCharAndInc('a', e)) { 379 if(this.testCharAndInc('g', e)) { 380 if(this.testCharAndInc('m', e)) { 381 if(this.testCharAndInc('e', e)) { 382 if(this.testCharAndInc('n', e)) { 383 if(this.testCharAndInc('t', e)) { 384 if(this.isTokenStop()) { 385 this.cur = 386 Token(TokenType.fragment, 387 this.line, 388 this.column); 389 return; 390 } 391 } 392 } 393 } 394 } 395 } 396 } 397 } 398 goto default; 399 /*case '@': 400 ++this.stringPos; 401 ++this.column; 402 ++e; 403 if(isTokenStop()) { 404 this.cur = Token(TokenType.at, this.line, this.column); 405 return; 406 } 407 goto default;*/ 408 case 'q': 409 ++this.stringPos; 410 ++this.column; 411 ++e; 412 if(this.testCharAndInc('u', e)) { 413 if(this.testCharAndInc('e', e)) { 414 if(this.testCharAndInc('r', e)) { 415 if(this.testCharAndInc('y', e)) { 416 if(this.isTokenStop()) { 417 this.cur = Token(TokenType.query, 418 this.line, this.column); 419 return; 420 } 421 } 422 } 423 } 424 } 425 goto default; 426 case 't': 427 ++this.stringPos; 428 ++this.column; 429 ++e; 430 if(this.testCharAndInc('r', e)) { 431 if(this.testCharAndInc('u', e)) { 432 if(this.testCharAndInc('e', e)) { 433 if(this.isTokenStop()) { 434 this.cur = Token(TokenType.true_, 435 this.line, this.column); 436 return; 437 } 438 } 439 } 440 } else if(this.isNotQueryParser() && 441 this.testCharAndInc('y', e)) { 442 if(this.testCharAndInc('p', e)) { 443 if(this.testCharAndInc('e', e)) { 444 if(this.isTokenStop()) { 445 this.cur = Token(TokenType.type, 446 this.line, this.column); 447 return; 448 } 449 } 450 } 451 } 452 goto default; 453 case 'n': 454 ++this.stringPos; 455 ++this.column; 456 ++e; 457 if(this.testCharAndInc('u', e)) { 458 if(this.testCharAndInc('l', e)) { 459 if(this.testCharAndInc('l', e)) { 460 if(this.isTokenStop()) { 461 this.cur = Token(TokenType.null_, 462 this.line, this.column); 463 return; 464 } 465 } 466 } 467 } 468 goto default; 469 case 'u': 470 ++this.stringPos; 471 ++this.column; 472 ++e; 473 if(this.testCharAndInc('n', e)) { 474 if(this.testCharAndInc('i', e)) { 475 if(this.testCharAndInc('o', e)) { 476 if(this.testCharAndInc('n', e)) { 477 if(this.isTokenStop()) { 478 this.cur = Token(TokenType.union_, 479 this.line, this.column); 480 return; 481 } 482 } 483 } 484 } 485 } 486 goto default; 487 case '.': 488 ++this.stringPos; 489 ++this.column; 490 ++e; 491 if(this.testCharAndInc('.', e)) { 492 if(this.testCharAndInc('.', e)) { 493 //if(this.stringPos < this.input.length 494 // && isAlphaNum(this.input[this.stringPos])) 495 if(this.isTokenStop() 496 || (this.stringPos < this.input.length 497 && isAlphaNum(this.input[this.stringPos]) 498 ) 499 ) 500 { 501 this.cur = Token(TokenType.dots, this.line, 502 this.column); 503 return; 504 } 505 } 506 } 507 throw new Exception(format( 508 "failed to parse \"...\" at line %s column %s", 509 this.line, this.column 510 )); 511 case '-': 512 ++this.stringPos; 513 ++this.column; 514 ++e; 515 goto case '0'; 516 case '+': 517 ++this.stringPos; 518 ++this.column; 519 ++e; 520 goto case '0'; 521 case '0': .. case '9': 522 do { 523 ++this.stringPos; 524 ++this.column; 525 ++e; 526 } while(this.stringPos < this.input.length 527 && this.input[this.stringPos] >= '0' 528 && this.input[this.stringPos] <= '9'); 529 530 if(this.stringPos >= this.input.length 531 || this.input[this.stringPos] != '.') 532 { 533 this.cur = Token(TokenType.intValue, this.input[b .. 534 e], this.line, this.column); 535 return; 536 } else if(this.stringPos < this.input.length 537 && this.input[this.stringPos] == '.') 538 { 539 do { 540 ++this.stringPos; 541 ++this.column; 542 ++e; 543 } while(this.stringPos < this.input.length 544 && this.input[this.stringPos] >= '0' 545 && this.input[this.stringPos] <= '9'); 546 547 this.cur = Token(TokenType.floatValue, this.input[b .. 548 e], this.line, this.column); 549 return; 550 } 551 goto default; 552 case '"': 553 ++this.stringPos; 554 ++this.column; 555 ++e; 556 while(this.stringPos < this.input.length 557 && (this.input[this.stringPos] != '"' 558 || (this.input[this.stringPos] == '"' 559 && this.input[this.stringPos - 1U] == '\\') 560 ) 561 ) 562 { 563 ++this.stringPos; 564 ++this.column; 565 ++e; 566 } 567 ++this.stringPos; 568 ++this.column; 569 this.cur = Token(TokenType.stringValue, this.input[b + 1 570 .. e], this.line, this.column); 571 break; 572 default: 573 while(!this.isTokenStop()) { 574 //writefln("455 '%s'", this.input[this.stringPos]); 575 ++this.stringPos; 576 ++this.column; 577 ++e; 578 } 579 //writefln("%s %s %s '%s'", b, e, this.stringPos, this.input[b .. e]); 580 //do { 581 // writefln("'%s'", this.input[this.stringPos]); 582 // ++this.stringPos; 583 // ++this.column; 584 // ++e; 585 //} while(!this.isTokenStop()); 586 //writefln("%s %s", TokenType.name, this.input[b .. e]); 587 this.cur = Token(TokenType.name, this.input[b .. e], 588 this.line, this.column 589 ); 590 break; 591 } 592 } 593 } 594 595 bool testCharAndInc(const(char) c, ref size_t e) @safe { 596 if(this.stringPos < this.input.length 597 && this.input[this.stringPos] == c) 598 { 599 ++this.column; 600 ++this.stringPos; 601 ++e; 602 return true; 603 } else { 604 return false; 605 } 606 } 607 608 @property bool empty() const @safe { 609 return this.stringPos >= this.input.length 610 && this.cur.type == TokenType.undefined; 611 } 612 613 Token front() @property @safe { 614 return this.cur; 615 } 616 617 @property Token front() const @safe @nogc pure { 618 return this.cur; 619 } 620 621 void popFront() @safe { 622 this.buildToken(); 623 } 624 625 string getRestOfInput() const @safe { 626 return this.input[this.stringPos .. $]; 627 } 628 } 629 630 unittest { 631 string f = "f "; 632 auto l = Lexer(f); 633 assert(!l.empty); 634 assert(l.front.type == TokenType.name); 635 assert(l.front.value == "f", format("'%s'", l.front.value)); 636 } 637 638 unittest { 639 string f = "... "; 640 641 auto l = Lexer(f); 642 assert(!l.empty); 643 assert(l.front.type == TokenType.dots); 644 l.popFront(); 645 assert(l.empty); 646 } 647 648 unittest { 649 string f = "name! "; 650 auto l = Lexer(f); 651 assert(!l.empty); 652 assert(l.front.type == TokenType.name); 653 assert(l.front.value == "name", format("'%s'", l.front.value)); 654 l.popFront(); 655 assert(!l.empty); 656 assert(l.front.type == TokenType.exclamation); 657 l.popFront(); 658 assert(l.empty); 659 } 660 661 unittest { 662 string f = "fragment"; 663 auto l = Lexer(f); 664 assert(!l.empty); 665 assert(l.front.type == TokenType.fragment); 666 } 667 668 unittest { 669 string f = ` 670 mutation { 671 likeStory(storyID: 12345) { 672 story { 673 likeCount 674 } 675 } 676 }`; 677 auto l = Lexer(f); 678 assert(!l.empty); 679 assert(l.front.type == TokenType.mutation); 680 l.popFront(); 681 assert(!l.empty); 682 assert(l.front.type == TokenType.lcurly); 683 l.popFront(); 684 assert(!l.empty); 685 assert(l.front.type == TokenType.name); 686 l.popFront(); 687 assert(!l.empty); 688 assert(l.front.type == TokenType.lparen); 689 l.popFront(); 690 assert(!l.empty); 691 assert(l.front.type == TokenType.name); 692 l.popFront(); 693 assert(!l.empty); 694 assert(l.front.type == TokenType.colon, format("%s", l.front.type)); 695 l.popFront(); 696 assert(!l.empty); 697 assert(l.front.type == TokenType.intValue); 698 l.popFront(); 699 assert(!l.empty); 700 assert(l.front.type == TokenType.rparen); 701 l.popFront(); 702 assert(!l.empty); 703 assert(l.front.type == TokenType.lcurly); 704 l.popFront(); 705 assert(!l.empty); 706 assert(l.front.type == TokenType.name); 707 l.popFront(); 708 assert(!l.empty); 709 assert(l.front.type == TokenType.lcurly); 710 l.popFront(); 711 assert(!l.empty); 712 assert(l.front.type == TokenType.name); 713 l.popFront(); 714 assert(!l.empty); 715 assert(l.front.type == TokenType.rcurly); 716 l.popFront(); 717 assert(!l.empty); 718 assert(l.front.type == TokenType.rcurly); 719 l.popFront(); 720 assert(!l.empty); 721 assert(l.front.type == TokenType.rcurly); 722 l.popFront(); 723 assert(l.empty); 724 } 725 726 unittest { 727 string f = ` 728 query withFragments { 729 user(id: +4) { 730 # super cool comment 731 friends(first: -10.3) { 732 ...friendFields 733 null false true 734 } 735 mutualFriends(first: 10) { 736 ...friendFields 737 } 738 } 739 } 740 741 fragment friendFields on User { 742 id 743 name 744 profilePic(size: 50) 745 }`; 746 auto l = Lexer(f); 747 assert(!l.empty); 748 assert(l.front.type == TokenType.query); 749 l.popFront(); 750 assert(!l.empty); 751 assert(l.front.type == TokenType.name); 752 assert(l.front.value == "withFragments"); 753 l.popFront(); 754 l.popFront(); 755 assert(!l.empty); 756 assert(l.front.type == TokenType.name); 757 assert(l.front.value == "user"); 758 l.popFront(); 759 l.popFront(); 760 assert(!l.empty); 761 assert(l.front.type == TokenType.name); 762 assert(l.front.value == "id", l.front.value); 763 l.popFront(); 764 assert(!l.empty); 765 assert(l.front.type == TokenType.colon); 766 l.popFront(); 767 assert(!l.empty); 768 assert(l.front.type == TokenType.intValue); 769 assert(l.front.value == "+4"); 770 l.popFront(); 771 assert(!l.empty); 772 assert(l.front.type == TokenType.rparen); 773 l.popFront(); 774 l.popFront(); 775 assert(!l.empty); 776 assert(l.front.type == TokenType.name); 777 assert(l.front.value == "friends"); 778 l.popFront(); 779 assert(!l.empty); 780 assert(l.front.type == TokenType.lparen); 781 l.popFront(); 782 assert(!l.empty); 783 assert(l.front.type == TokenType.name); 784 assert(l.front.value == "first"); 785 l.popFront(); 786 l.popFront(); 787 assert(!l.empty); 788 assert(l.front.type == TokenType.floatValue, format("%s", l.front.type)); 789 assert(l.front.value == "-10.3", l.front.value); 790 l.popFront(); 791 l.popFront(); 792 l.popFront(); 793 assert(!l.empty); 794 assert(l.front.type == TokenType.dots, format("%s", l.front.type)); 795 l.popFront(); 796 assert(!l.empty); 797 assert(l.front.type == TokenType.name, format("%s", l.front.type)); 798 assert(l.front.value == "friendFields"); 799 l.popFront(); 800 assert(!l.empty); 801 assert(l.front.type == TokenType.null_, format("%s", l.front.type)); 802 l.popFront(); 803 assert(!l.empty); 804 assert(l.front.type == TokenType.false_, format("%s", l.front.type)); 805 l.popFront(); 806 assert(!l.empty); 807 assert(l.front.type == TokenType.true_, format("%s", l.front.type)); 808 while(!l.empty) { 809 l.popFront(); 810 } 811 } 812 813 unittest { 814 string f = ` 815 query withFragments { 816 user(id: "hello") { 817 } 818 }`; 819 820 auto l = Lexer(f); 821 assert(!l.empty); 822 assert(l.front.type == TokenType.query); 823 l.popFront(); 824 assert(!l.empty); 825 assert(l.front.type == TokenType.name); 826 assert(l.front.value == "withFragments"); 827 l.popFront(); 828 l.popFront(); 829 assert(!l.empty); 830 assert(l.front.type == TokenType.name); 831 assert(l.front.value == "user"); 832 l.popFront(); 833 l.popFront(); 834 assert(!l.empty); 835 assert(l.front.type == TokenType.name); 836 assert(l.front.value == "id", l.front.value); 837 l.popFront(); 838 assert(!l.empty); 839 assert(l.front.type == TokenType.colon); 840 l.popFront(); 841 assert(!l.empty); 842 assert(l.front.type == TokenType.stringValue); 843 assert(l.front.value == "hello", format("'%s' '%s'", l.front.value, "hello")); 844 l.popFront(); 845 assert(!l.empty); 846 assert(l.front.type == TokenType.rparen); 847 }