|
12 | 12 | with test_tools.imports_under_tool("peg_generator"): |
13 | 13 | from pegen.grammar_parser import GeneratedParser as GrammarParser |
14 | 14 | from pegen.testutil import parse_string, generate_parser, make_parser |
15 | | - from pegen.grammar import GrammarVisitor, GrammarError, Grammar, RuleKind |
| 15 | + from pegen.grammar import ( |
| 16 | + GrammarVisitor, GrammarError, Grammar, NameLeaf, RuleKind, |
| 17 | + ) |
16 | 18 | from pegen.grammar_visualizer import ASTGrammarPrinter |
17 | 19 | from pegen.parser import Parser |
18 | 20 | from pegen.parser_generator import compute_nullables, compute_left_recursives |
@@ -751,6 +753,56 @@ def test_opt_sequence(self) -> None: |
751 | 753 | # of a line in the generated source. See bpo-41044 |
752 | 754 | make_parser(grammar) |
753 | 755 |
|
| 756 | + def test_left_recursion_leader_order(self) -> None: |
| 757 | + grammar = parse_string(""" |
| 758 | + start: zeta NEWLINE |
| 759 | + zeta: alpha '+' | NUMBER |
| 760 | + alpha: zeta '-' | NUMBER |
| 761 | + """, GrammarParser) |
| 762 | + PythonParserGenerator(grammar, io.StringIO()) |
| 763 | + self.assertTrue(grammar.rules["alpha"].leader) |
| 764 | + self.assertFalse(grammar.rules["zeta"].leader) |
| 765 | + |
| 766 | + def test_large_left_recursive_grammar(self) -> None: |
| 767 | + size = 12 |
| 768 | + lines = ["start: r0 NEWLINE ENDMARKER"] |
| 769 | + for i in range(size): |
| 770 | + children = list(range(i + 1, size)) |
| 771 | + if i: |
| 772 | + children.append(0) |
| 773 | + alternatives = [f"r{j} '+'" for j in children] + ["NUMBER"] |
| 774 | + lines.append(f"r{i}: " + " | ".join(alternatives)) |
| 775 | + parser_class = make_parser("\n".join(lines) + "\n") |
| 776 | + node = parse_string("1\n", parser_class) |
| 777 | + self.assertEqual(node[0].string, "1") |
| 778 | + |
| 779 | + def test_left_recursion_analysis_work(self) -> None: |
| 780 | + class CountedName(str): |
| 781 | + comparisons = 0 |
| 782 | + __hash__ = str.__hash__ |
| 783 | + |
| 784 | + def __eq__(self, other): |
| 785 | + type(self).comparisons += 1 |
| 786 | + return super().__eq__(other) |
| 787 | + |
| 788 | + size = 8 |
| 789 | + lines = ["start: r0 NEWLINE ENDMARKER"] |
| 790 | + for i in range(size): |
| 791 | + alternatives = [ |
| 792 | + f"r{j} '+'" for j in range(size) if j != i |
| 793 | + ] + ["NUMBER"] |
| 794 | + lines.append(f"r{i}: " + " | ".join(alternatives)) |
| 795 | + grammar = parse_string("\n".join(lines) + "\n", GrammarParser) |
| 796 | + for rule in grammar.rules.values(): |
| 797 | + for alt in rule.rhs.alts: |
| 798 | + for item in alt.items: |
| 799 | + if isinstance(item.item, NameLeaf): |
| 800 | + item.item.value = CountedName(item.item.value) |
| 801 | + CountedName.comparisons = 0 |
| 802 | + with self.assertRaisesRegex(ValueError, "no leadership candidate"): |
| 803 | + PythonParserGenerator(grammar, io.StringIO()) |
| 804 | + self.assertLess(CountedName.comparisons, 2_000) |
| 805 | + |
754 | 806 | def test_left_recursion_too_complex(self) -> None: |
755 | 807 | grammar = """ |
756 | 808 | start: foo |
|
0 commit comments