1<?php 2 3/** 4 * Simple LR(1) parser generator. Generally, build a parser by setting terminals 5 * and rules, then calling @{method:processGrammar}. For example, here is a 6 * simple grammar which accepts one or more "a" followed by exactly one "b": 7 * 8 * $parser = id(new PhutilParserGenerator()) 9 * ->setTerminals(array('a', 'b')) 10 * ->setStartRule('S') 11 * ->setRules( 12 * array( 13 * 'S' => 'A b', 14 * 'A' => array( 15 * 'A a', 16 * 'a', 17 * ))) 18 * ->processGrammar(); 19 * 20 * To actually parse token streams, use @{method:parseTokens}. 21 * 22 * $tokens = get_tokens(); // Usually from PhutilLexer 23 * $callback = 'some_callback'; 24 * $tree = $parser->parseTokens($tokens, $callback); 25 * 26 * The callback is invoked when a grammar rule matches. It should have this 27 * signature: 28 * 29 * function parser_callback($rule, $production, array $tokens) { 30 * // ... 31 * } 32 * 33 * The `$rule` is the matching rule; the `$production` is the matching 34 * production, and `$tokens` is the matching tokens (for terminal rules) or the 35 * return value of previous parse callbacks (for nonterminal rules). 36 * 37 * You should either return a result of evaluation, or some sort of abstract 38 * representation of the parse tree (this is more likely to be useful for more 39 * complex grammars). 40 * 41 * NOTE: This class generates LR(1) parsers, which perform less-than-optimally 42 * on large grammars. Worse, it is written in PHP. It is suitable only for 43 * very simple grammars with few states. 44 * 45 * NOTE: These parsers silently resolve reduce/reduce conflicts by choosing the 46 * first reduction, and silently resolve shift/reduce conflicts by shifting. 47 * These are the same rules used by Yacc, but are implicit. 48 * 49 * @task rules Grammar Rules 50 * @task rvalidation Rule Validation 51 * @task first Computing First() 52 * @task tables Computing Action and Goto Tables 53 * @task inspect Inspecting Generator State 54 */ 55final class PhutilParserGenerator extends Phobject { 56 57 private $terminals; 58 private $rules; 59 private $startRule = 'start'; 60 private $states = array(); 61 private $sets = array(); 62 private $successor = array(); 63 private $setHashes = array(); 64 private $actionTable; 65 private $gotoTable; 66 67 private $rulesValidated = false; 68 private $eofSymbol; 69 private $initSymbol; 70 private $epsilonSymbol; 71 private $endSymbol; 72 73 private $firstTable; 74 75 public function processGrammar() { 76 $this->validateRules(); 77 $this->buildFirstTable(); 78 79 $init = $this->getInitSymbol(); 80 $eof = $this->getEOFSymbol(); 81 $end = $this->getEndSymbol(); 82 83 $this->rules[$init] = array( 84 array($this->startRule, $end), 85 ); 86 list($is_new, $state) = $this->addState( 87 array( 88 array($this->getInitSymbol(), 0, 0, $eof), 89 )); 90 $this->buildSuccessors($state); 91 92 $this->buildTables(); 93 94 return $this; 95 } 96 97 98/* -( Grammar Rules )------------------------------------------------------ */ 99 100 101 public function setTerminals(array $terminals) { 102 $this->terminals = array_fill_keys($terminals, true); 103 return $this; 104 } 105 106 public function setRules(array $rules) { 107 $this->rules = $rules; 108 return $this; 109 } 110 111 public function setStartRule($rule_name) { 112 $this->startRule = $rule_name; 113 return $this; 114 } 115 116 public function getStartRule() { 117 return $this->startRule; 118 } 119 120 public function getEOFSymbol() { 121 if ($this->eofSymbol === null) { 122 throw new PhutilInvalidStateException('processGrammar'); 123 } 124 return $this->eofSymbol; 125 } 126 127 public function getInitSymbol() { 128 if ($this->initSymbol === null) { 129 throw new PhutilInvalidStateException('processGrammar'); 130 } 131 return $this->initSymbol; 132 } 133 134 public function getEpsilonSymbol() { 135 if ($this->epsilonSymbol === null) { 136 throw new PhutilInvalidStateException('processGrammar'); 137 } 138 return $this->epsilonSymbol; 139 } 140 141 public function getEndSymbol() { 142 if ($this->endSymbol === null) { 143 throw new PhutilInvalidStateException('processGrammar'); 144 } 145 return $this->endSymbol; 146 } 147 148 public function isTerminal($symbol) { 149 return isset($this->terminals[$symbol]); 150 } 151 152 public function isRule($symbol) { 153 return isset($this->rules[$symbol]); 154 } 155 156 157/* -( Rule Validation )---------------------------------------------------- */ 158 159 160 /** 161 * Perform a battery of tests on the provided rules to detect problems which 162 * would prevent us from generating a parser. 163 * 164 * @return void 165 * @task rvalidation 166 */ 167 private function validateRules() { 168 // Rules must be specified in the right format. 169 $this->parseRules(); 170 171 // Rules must contain only known symbols. 172 $this->validateRuleSymbols(); 173 174 // The start rule must exist and be valid. 175 $this->validateStartRule(); 176 177 // Now, we select printable names for special symbols (EOF, epsilon, etc) 178 // that don't conflict with any symbols in the grammar. 179 $this->chooseSpecialSymbols(); 180 181 // Make sure every terminal can be reached by some rule. 182 $this->validateAllTerminalsReachable(); 183 184 // Make sure every rule can be reached. 185 $this->validateAllRulesReachable(); 186 187 // Make sure every rule has some valid reduction. 188 $this->validateAllRulesReducible(); 189 190 $this->rulesValidated = true; 191 } 192 193 194 /** 195 * @task rvalidation 196 */ 197 private function parseRules() { 198 foreach ($this->rules as $rule_name => $rule_variants) { 199 if (!is_array($rule_variants)) { 200 $rule_variants = array($rule_variants); 201 $this->rules[$rule_name] = $rule_variants; 202 } 203 foreach ($rule_variants as $vkey => $variant) { 204 if ($variant === null) { 205 $variant = array(null); 206 } else if (!is_array($variant)) { 207 $variant = preg_split('/\s+/', $variant); 208 } else { 209 foreach ($variant as $symbol) { 210 if (($symbol === null) && count($variant) > 1) { 211 throw new PhutilInvalidRuleParserGeneratorException( 212 pht( 213 "Rule '%s' contains a production '%s' which is ". 214 "nonempty but has a null in it. A rule with other ". 215 "may not contain null.", 216 $rule_name, 217 $vkey)); 218 } 219 } 220 } 221 $this->rules[$rule_name][$vkey] = array_values($variant); 222 } 223 } 224 } 225 226 227 /** 228 * @task rvalidation 229 */ 230 private function validateRuleSymbols() { 231 foreach ($this->rules as $rule => $productions) { 232 foreach ($productions as $production_name => $production) { 233 foreach ($production as $symbol) { 234 if ($symbol === null) { 235 continue; 236 } 237 if ($this->isTerminal($symbol)) { 238 continue; 239 } 240 if ($this->isRule($symbol)) { 241 continue; 242 } 243 $production_string = implode(' ', $production); 244 throw new PhutilUnknownSymbolParserGeneratorException( 245 pht( 246 "Symbol '%s' in production '%s' ('%s') of rule '%s' does not ". 247 "name a rule or terminal. Did you misspell a symbol, fail to ". 248 "specify a terminal, or forget a rule?", 249 $symbol, 250 $production_name, 251 $production_string, 252 $rule)); 253 } 254 } 255 } 256 } 257 258 259 /** 260 * @task rvalidation 261 */ 262 private function validateStartRule() { 263 $start_rule = $this->getStartRule(); 264 if (!$this->isRule($start_rule)) { 265 throw new PhutilUnknownSymbolParserGeneratorException( 266 pht( 267 "Start rule '%s' does not appear in the rules for the grammar. Use ". 268 "%s to choose a different start rule, or add a rule named '%s'.", 269 $start_rule, 270 'setStartRule()', 271 $start_rule)); 272 } 273 } 274 275 276 /** 277 * @task rvalidation 278 */ 279 private function chooseSpecialSymbols() { 280 $special = array( 281 'eofSymbol' => '(end-of-file)', 282 'epsilonSymbol' => '(epsilon)', 283 'initSymbol' => '(init)', 284 'endSymbol' => '(end)', 285 ); 286 287 foreach ($special as $key => $value) { 288 while ($this->isRule($value) || $this->isTerminal($value)) { 289 $value .= "'"; 290 } 291 $special[$key] = $value; 292 } 293 294 $this->eofSymbol = $special['eofSymbol']; 295 $this->epsilonSymbol = $special['epsilonSymbol']; 296 $this->initSymbol = $special['initSymbol']; 297 $this->endSymbol = $special['endSymbol']; 298 299 foreach ($this->rules as $rule => $productions) { 300 foreach ($productions as $production_name => $production) { 301 foreach ($production as $key => $symbol) { 302 if ($symbol === null) { 303 $this->rules[$rule][$production_name][$key] = $this->epsilonSymbol; 304 } 305 } 306 $this->rules[$rule][$production_name][] = $this->endSymbol; 307 } 308 } 309 310 $this->terminals[$this->getEOFSymbol()] = true; 311 } 312 313 314 /** 315 * @task rvalidation 316 */ 317 private function validateAllTerminalsReachable() { 318 $seen = array(); 319 foreach ($this->rules as $rule => $productions) { 320 foreach ($productions as $production) { 321 foreach ($production as $symbol) { 322 $seen[$symbol] = true; 323 } 324 } 325 } 326 327 $missing = array_diff_key($this->terminals, $seen); 328 unset($missing[$this->getEOFSymbol()]); 329 if ($missing) { 330 $missing_terminals = array_keys($missing); 331 $missing_terminals = implode(', ', $missing_terminals); 332 throw new PhutilUnreachableTerminalParserGeneratorException( 333 pht( 334 'Some terminals do not appear in any rule: %s', 335 $missing_terminals)); 336 } 337 } 338 339 340 /** 341 * @task rvalidation 342 */ 343 private function validateAllRulesReachable() { 344 $stack = array(); 345 $reachable = $this->computeReachableRules($this->getStartRule(), $stack); 346 347 $missing = array_diff_key($this->rules, $reachable); 348 unset($missing[$this->getStartRule()]); 349 350 if ($missing) { 351 $missing_rules = array_keys($missing); 352 $missing_rules = implode(', ', $missing_rules); 353 throw new PhutilUnreachableRuleParserGeneratorException( 354 pht( 355 'Some rules can never be reached from any production: %s', 356 $missing_rules)); 357 } 358 } 359 360 361 /** 362 * @task rvalidation 363 */ 364 private function computeReachableRules($rule, array &$stack) { 365 if (isset($stack[$rule])) { 366 return $stack[$rule]; 367 } 368 369 $stack[$rule] = array(); 370 371 foreach ($this->rules[$rule] as $production) { 372 foreach ($production as $symbol) { 373 if ($this->isRule($symbol)) { 374 $stack[$rule][$symbol] = true; 375 $stack[$rule] += $this->computeReachableRules($symbol, $stack); 376 } 377 } 378 } 379 380 return $stack[$rule]; 381 } 382 383 384 /** 385 * @task rvalidation 386 */ 387 private function validateAllRulesReducible() { 388 $reducible = array(); 389 foreach ($this->rules as $rule => $productions) { 390 if (!$this->isRuleReducible($rule, $reducible)) { 391 throw new PhutilIrreducibleRuleParserGeneratorException( 392 pht( 393 "Rule '%s' can never be reduced: it recurses indefinitely ". 394 "and reaches no production of terminals.", 395 $rule)); 396 } 397 } 398 } 399 400 401 /** 402 * @task rvalidation 403 */ 404 private function isRuleReducible($rule, array &$reducible) { 405 if (isset($reducible[$rule])) { 406 return $reducible[$rule]; 407 } 408 409 // Set this ahead of time so we don't end up in an infinite loop if 410 // rules recurse. We'll overwrite it if we find a reduction. 411 $reducible[$rule] = false; 412 $reducible[$rule] = $this->computeRuleReducible($rule, $reducible); 413 return $reducible[$rule]; 414 } 415 416 417 /** 418 * @task rvalidation 419 */ 420 private function computeRuleReducible($rule, array &$reducible) { 421 $epsilon = $this->getEpsilonSymbol(); 422 $end = $this->getEndSymbol(); 423 424 $productions = $this->rules[$rule]; 425 426 // In the first pass, try to find a trivially reducible production, e.g. one 427 // with epsilon or only terminals. Also, remove recursive productions (those 428 // which directly involve the rule itself) because we know we won't be able 429 // to reduce them. If we're lucky, this will allow us to determine that the 430 // rule is reducible without recursion. For example, we can immediately 431 // reduce these productions: 432 // 433 // R -> a 434 // R -> b c d 435 // R -> (epsilon) 436 // 437 // We can never reduce these productions: 438 // 439 // R -> R 440 // R -> a R b 441 // 442 // We might be able to reduce these productions, but they aren't as cheap 443 // or easy to figure out, since we need to first determine if other rules 444 // can be reduced: 445 // 446 // R -> X Y 447 // R -> X a 448 // 449 // If we find a reduction, we return immediately. 450 451 foreach ($productions as $key => $production) { 452 $has_only_terminals = true; 453 foreach ($production as $symbol) { 454 if ($symbol == $end) { 455 break; 456 } else if ($symbol == $epsilon) { 457 // The rule contains an epsilon production, which can always reduce 458 // it. 459 return true; 460 } else if ($symbol == $rule) { 461 // The rule contains itself; this production is never reducible. We 462 // must find another reducible production. 463 unset($productions[$key]); 464 continue 2; 465 } else if ($this->isTerminal($symbol)) { 466 // This is a terminal; keep looking. We'll be able to reduce the 467 // production if it contains only terminals. 468 continue; 469 } else { 470 // This is a rule, so we can't trivially reduce it. We'll keep it 471 // for the next round if we can't find any trivial reductions. 472 $has_only_terminals = false; 473 break; 474 } 475 } 476 477 if ($has_only_terminals) { 478 return true; 479 } 480 } 481 482 // If we have no productions left, this rule can't be reduced. 483 if (empty($productions)) { 484 return false; 485 } 486 487 // We have remaining productions which include other rules. Look for a 488 // nontrivial reduction. For example: 489 // 490 // R -> X Y 491 // X -> x 492 // Y -> y 493 // 494 // In this case, X and Y are both reducible, so "X Y" is reducible and thus 495 // R is reducible. 496 foreach ($productions as $production) { 497 $can_reduce = true; 498 foreach ($production as $symbol) { 499 // NOTE: We don't need to check for epsilon here, because we would 500 // already have determined the rule was reducible if we had an epsilon 501 // production. 502 if ($symbol == $end) { 503 break; 504 } else if ($this->isTerminal($symbol)) { 505 continue; 506 } else if (!$this->isRuleReducible($symbol, $reducible)) { 507 $can_reduce = false; 508 break; 509 } 510 } 511 512 if ($can_reduce) { 513 // The production contained only terminals and reducible rules, so it 514 // is reducible. We're good and don't need to examine remaining 515 // productions. 516 return true; 517 } 518 } 519 520 // We didn't find any reducible productions. 521 return false; 522 } 523 524 525/* -( Computing First() )-------------------------------------------------- */ 526 527 528 private function buildFirstTable() { 529 $this->firstTable = array(); 530 foreach ($this->rules as $rule => $productions) { 531 $this->buildRuleFirst($rule); 532 } 533 } 534 535 private function buildRuleFirst($rule) { 536 if (isset($this->firstTable[$rule])) { 537 return $this->firstTable[$rule]; 538 } 539 540 $this->firstTable[$rule] = array(); 541 $productions = $this->rules[$rule]; 542 foreach ($productions as $key => $production) { 543 $this->firstTable[$rule] += $this->getFirstForProduction($production); 544 } 545 546 return $this->firstTable[$rule]; 547 } 548 549 private function getFirstForProduction(array $production) { 550 $set = array(); 551 552 $end = $this->getEndSymbol(); 553 $epsilon = $this->getEpsilonSymbol(); 554 $eof = $this->getEOFSymbol(); 555 556 $accept_epsilon = true; 557 foreach ($production as $symbol) { 558 if ($symbol === $end) { 559 break; 560 } else if ($symbol === $epsilon) { 561 break; 562 } else if ($this->isTerminal($symbol)) { 563 $set[$symbol] = true; 564 $accept_epsilon = false; 565 break; 566 } else { 567 $symbol_set = $this->buildRuleFirst($symbol); 568 569 $has_epsilon = isset($symbol_set[$epsilon]); 570 unset($symbol_set[$epsilon]); 571 $set += $symbol_set; 572 if (!$has_epsilon) { 573 $accept_epsilon = false; 574 break; 575 } 576 } 577 } 578 579 if ($accept_epsilon) { 580 $set[$epsilon] = true; 581 } 582 583 return $set; 584 } 585 586 587/* -( Computing States )--------------------------------------------------- */ 588 589 590 private function addState(array $set) { 591 $seen = array(); 592 foreach ($set as $item) { 593 $seen[$item[0]][$item[1]][$item[2]][$item[3]] = true; 594 } 595 596 $end = $this->getEndSymbol(); 597 $epsilon = $this->getEpsilonSymbol(); 598 599 for ($ii = 0; $ii < count($set); $ii++) { 600 $item = $set[$ii]; 601 602 $production = $this->rules[$item[0]][$item[1]]; 603 $next = $production[$item[2]]; 604 if ($this->isTerminal($next)) { 605 continue; 606 } else if ($next === $epsilon) { 607 continue; 608 } else if ($next === $end) { 609 continue; 610 } 611 612 $v = array_slice($production, $item[2] + 1, -1); 613 $v[] = $item[3]; 614 $v[] = $end; 615 616 $firsts = $this->getFirstForProduction($v); 617 618 foreach ($firsts as $nfirst => $ignored) { 619 if (!$this->isTerminal($nfirst)) { 620 unset($firsts[$nfirst]); 621 } 622 } 623 624 foreach ($this->rules[$next] as $pkey => $nproduction) { 625 foreach ($firsts as $nfirst => $ignored) { 626 if (isset($seen[$next][$pkey][0][$nfirst])) { 627 continue; 628 } 629 $set[] = array($next, $pkey, 0, $nfirst); 630 $seen[$next][$pkey][0][$nfirst] = true; 631 } 632 } 633 } 634 635 $hash = $this->hashSet($set); 636 if (isset($this->setHashes[$hash])) { 637 return array(false, $this->setHashes[$hash]); 638 } 639 640 $this->states[] = $set; 641 $state = last_key($this->states); 642 $this->setHashes[$hash] = $state; 643 644 return array(true, $state); 645 } 646 647 private function buildSuccessors($start_state) { 648 $end = $this->getEndSymbol(); 649 650 $nexts = array(); 651 foreach ($this->states[$start_state] as $item) { 652 $next = $this->rules[$item[0]][$item[1]][$item[2]]; 653 if ($next === $end) { 654 continue; 655 } 656 $nexts[$next][] = array( 657 $item[0], 658 $item[1], 659 $item[2] + 1, 660 $item[3], 661 ); 662 } 663 664 foreach ($nexts as $next => $items) { 665 list($is_new, $state) = $this->addState($items); 666 $this->successor[$start_state][$next] = $state; 667 if ($is_new) { 668 $this->buildSuccessors($state); 669 } 670 } 671 } 672 673 private function hashSet(array $set) { 674 foreach ($set as $k => $item) { 675 $set[$k] = implode("\0", $item); 676 } 677 sort($set); 678 $set = implode("\1", $set); 679 680 return md5($set); 681 } 682 683 684 private function buildTables() { 685 $action = array(); 686 $goto = array(); 687 688 $end = $this->getEndSymbol(); 689 $eof = $this->getEOFSymbol(); 690 $init = $this->getInitSymbol(); 691 692 foreach ($this->states as $state => $items) { 693 $shift = array(); 694 $reduce = array(); 695 $accept = false; 696 foreach ($items as $item) { 697 $next = $this->rules[$item[0]][$item[1]][$item[2]]; 698 if ($next == $end) { 699 if ($item[0] !== $init) { 700 $reduce[$item[3]][] = $item; 701 } else if ($item[0] === $init && $item[3] === $eof) { 702 $accept = $item; 703 } 704 } else if ($this->isTerminal($next)) { 705 $shift[$next] = $item; 706 } else { 707 $goto[$state][$next] = $this->successor[$state][$next]; 708 } 709 } 710 711 foreach ($reduce as $next => $reductions) { 712 if (count($reductions) > 1) { 713 $ways = array(); 714 foreach ($reductions as $reduction) { 715 $ways[] = "{$reduction[0]}/{$reduction[1]}"; 716 } 717 $ways = implode('; ', $ways); 718 719 // TODO: As below, we should have more explicit handling of 720 // reduce/reduce conflicts. For now, just pick the first one. 721 722 if (false) { 723 throw new Exception( 724 pht( 725 "Reduce/reduce conflict: from state '%s', when a ". 726 "'%s' is encountered, it may be reduced in multiple ". 727 "ways: %s", 728 $state, 729 $next, 730 $ways)); 731 } 732 } 733 $reduce[$next] = head($reductions); 734 } 735 736 $srconflicts = array_intersect_key($shift, $reduce); 737 foreach ($srconflicts as $next => $ignored) { 738 739 // TODO: We should probably have better or more explicit handling of 740 // shift/reduce conflicts. For now, we just shift. 741 742 if (false) { 743 $what = $reduce[$next][0]; 744 throw new Exception( 745 pht( 746 "Shift/reduce conflict: from state '%s', when a '%s' ". 747 "is encountered, shifting conflicts with reducing '%s'.", 748 $state, 749 $next, 750 $what)); 751 } else { 752 // Resolve the shift/reduce by shifting. 753 $reduce = array(); 754 } 755 } 756 757 if ($accept && isset($shift[$eof])) { 758 throw new Exception(pht('Accept/shift conflict!')); 759 } 760 761 if ($accept && isset($reduce[$eof])) { 762 throw new Exception(pht('Accept/reduce conflict!')); 763 } 764 765 foreach ($reduce as $next => $item) { 766 $action[$state][$next] = array( 767 'R', 768 array( 769 $item[0], 770 $item[1], 771 count($this->rules[$item[0]][$item[1]]) - 1, 772 ), 773 ); 774 } 775 776 foreach ($shift as $next => $item) { 777 $action[$state][$next] = array( 778 'S', 779 $this->successor[$state][$next], 780 ); 781 } 782 783 if ($accept) { 784 $action[$state][$eof] = array('A'); 785 } 786 } 787 788 $this->actionTable = $action; 789 $this->gotoTable = $goto; 790 } 791 792 public function generateParserFunction($name) { 793 $out = array(); 794 $out[] = 'function '.$name.'(array $tokens, $callback) {'; 795 $out[] = ' return '.__CLASS__.'::parseTokensWithTables('; 796 $out[] = ' '.$this->formatAndIndent($this->actionTable, 4).','; 797 $out[] = ' '.$this->formatAndIndent($this->gotoTable, 4).','; 798 $out[] = ' '.$this->formatAndIndent($this->getEOFSymbol(), 4).','; 799 $out[] = ' $tokens,'; 800 $out[] = ' $callback);'; 801 $out[] = '}'; 802 return implode("\n", $out); 803 } 804 805 private function formatAndIndent($var, $depth) { 806 $var = phutil_var_export($var); 807 $var = str_replace("\n", "\n".str_repeat(' ', $depth), $var); 808 809 return $var; 810 } 811 812 public function parseTokens(array $tokens, $callback) { 813 return self::parseTokensWithTables( 814 $this->actionTable, 815 $this->gotoTable, 816 $this->getEOFSymbol(), 817 $tokens, 818 $callback); 819 } 820 821 public static function parseTokensWithTables( 822 $action_table, 823 $goto_table, 824 $eof_symbol, 825 array $tokens, 826 $callback) { 827 828 $state_stack = array(0); 829 $token_stack = array(); 830 831 $tokens = array_reverse($tokens); 832 while (true) { 833 $state = end($state_stack); 834 835 if (empty($tokens)) { 836 $next = $eof_symbol; 837 } else { 838 $next_token = end($tokens); 839 $next = $next_token[0]; 840 } 841 842 if (!isset($action_table[$state][$next])) { 843 $expected = implode(', ', array_keys($action_table[$state])); 844 throw new Exception( 845 pht( 846 "Unexpected '%s' in state %s! Expected: %s", 847 $next, 848 $state, 849 $expected)); 850 } 851 852 $action = $action_table[$state][$next]; 853 854 switch ($action[0]) { 855 case 'S': 856 $state_stack[] = $action[1]; 857 $token_stack[] = array_pop($tokens); 858 break; 859 case 'R': 860 $r_rule = $action[1][0]; 861 $r_prod = $action[1][1]; 862 $r_size = $action[1][2]; 863 864 $token_v = array(); 865 while ($r_size--) { 866 $token_v[] = array_pop($token_stack); 867 array_pop($state_stack); 868 } 869 $token_v = array_reverse($token_v); 870 $token_stack[] = call_user_func_array( 871 $callback, 872 array($r_rule, $r_prod, $token_v)); 873 $goto = $goto_table[end($state_stack)][$r_rule]; 874 $state_stack[] = $goto; 875 break; 876 case 'A': 877 break 2; 878 } 879 } 880 881 return head($token_stack); 882 } 883 884 885/* -( Inspecting Generator State )----------------------------------------- */ 886 887 888 /** 889 * @task inspect 890 */ 891 public function inspectRules() { 892 if (!$this->rulesValidated) { 893 throw new PhutilInvalidStateException('processGrammar'); 894 } 895 return $this->rules; 896 } 897 898 899 /** 900 * @task inspect 901 */ 902 public function inspectFirstTable() { 903 if ($this->firstTable === null) { 904 throw new PhutilInvalidStateException('processGrammar'); 905 } 906 return $this->firstTable; 907 } 908 909 910} 911