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