From d7c5cae1a98a45d0f2fd918c01494b47d4655a32 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 16:30:16 +0000 Subject: [PATCH] Drop unit self-loop rules (X : X) during grammar compile Fixes #1585. EBNF expansion of `X*` inside an `X` rule produces a bare `X : X` alternative once the empty branch is folded in. That rule never consumes input, so when LALR's priority-based conflict resolution selects it the parser's reduce/shift loop spins forever; CYK's unit-rule elimination loops for the same reason. Without a priority annotation the same grammar surfaced as a Reduce/Reduce GrammarError. Filter such non-progressing self-references at rule construction so no backend ever sees them. --- lark/load_grammar.py | 3 +++ tests/test_grammar.py | 5 +++++ tests/test_parser.py | 17 +++++++++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/lark/load_grammar.py b/lark/load_grammar.py index a2968e92d..f729ed6cc 100644 --- a/lark/load_grammar.py +++ b/lark/load_grammar.py @@ -766,6 +766,9 @@ def compile(self, start, terminals_to_keep) -> Tuple[List[TerminalDef], List[Rul else: exp_options = options + if len(expansion) == 1 and expansion[0] == NonTerminal(name): + continue + for sym in expansion: assert isinstance(sym, Symbol) if sym.is_term and exp_options and exp_options.keep_all_tokens: diff --git a/tests/test_grammar.py b/tests/test_grammar.py index 52425db51..ce2e60b37 100644 --- a/tests/test_grammar.py +++ b/tests/test_grammar.py @@ -302,6 +302,11 @@ def test_symbol_eq(self): self.assertNotEqual(a, b) + def test_issue_1585_cyk(self): + l = Lark('start.1: "a" | start start*', parser='cyk') + for s in ('aa', 'aaaa'): + self.assertEqual(l.parse(s).data, 'start') + if __name__ == '__main__': main() diff --git a/tests/test_parser.py b/tests/test_parser.py index 533ae7890..963a3b8ce 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -72,11 +72,15 @@ def test_infinite_recurse(self): a: a | "a" """ - self.assertRaises(GrammarError, Lark, g, parser='lalr') + for parser in ('lalr', 'earley'): + l = Lark(g, parser=parser) + tree = l.parse('a') + self.assertEqual(tree, Tree('start', [Tree('a', [])])) - # TODO: should it? shouldn't it? - # l = Lark(g, parser='earley', lexer='dynamic') - # self.assertRaises(ParseError, l.parse, 'a') + g = """start: a + a: a + """ + self.assertRaises(GrammarError, Lark, g, parser='lalr') def test_propagate_positions(self): g = Lark("""start: a @@ -2256,6 +2260,11 @@ def test_prioritization_sum(self): self.assertEqual(''.join(child.data for child in res.children), 'indirection') + def test_issue_1585_priority_recursive_star(self): + l = _Lark('start.1: "a" | start start*') + for s in ('a', 'aa', 'aaaa'): + self.assertEqual(l.parse(s).data, 'start') + def test_utf8(self): g = u"""start: a a: "±a"