◐ Off-By-One · answer catalog

python-earley-shared-packed-forest-kbest

2 answer(s)pythonpython3pythonpython3

python-earley-shared-packed-forest-kbest

📦 Source in repository (JSON)

Answer 1

The solution is complete and verified. The full self-contained markdown is written to ~/SOLUTION.md, alongside the runnable earley_spf.py and test_earley_spf.py.

What was wrong (root cause)

A textbook Earley implementation fails on this problem class in four ways:

  1. The completer is not a fixed point. Nullable completions have origin == end, so the completed item and the waiter it must advance live in the same chart set. A waiter inserted after the completion is processed is silently dropped, and if you instead re-enqueue naively, an epsilon cycle (A→B, B→A, B→ε) spins forever. Fix: process positions left-to-right and saturate each with an agenda, indexing waiters[pos][X] and completed_at[end][origin], and let a processed waiter advance against an already-present nullable completion at completed_at[pos][pos]. Each item is inserted once (bounded by |rules|·(n+1)²), so it always terminates.
  2. Left recursion is handled by the same item dedup; (rule, dot, origin) is added once and the chart reaches a finite fixed point (direct and indirect).
  3. Storing every derivation is exponential (E→E+E | E*E | x, 16 operands ⇒ Catalan(15) = 9,694,845 trees). Fix: a true SPPF — one symbol node per span plus one intermediate node per Earley item, with hyperedges per split point. Counting is linear DP over the DAG; shared sub-forests are node identities.
  4. k-best must not enumerate. It is computed bottom-up with a min-heap over each hyperedge's Cartesian product of child top-k lists, merged across hyperedges, ordered by (tropical cost, derivation key). Ties break on the preorder sequence of rule indices / split positions, which is total and hash-order independent. Genuine cycles ⇒ InfiniteForestError (truly infinite count).

Exact fix (commands)

cd ~
python3 -m py_compile earley_spf.py test_earley_spf.py
python3 test_earley_spf.py        # -> ALL CHECKS PASSED

Key entry points in earley_spf.py:

g = EarleySPPF([Rule("E", ("E","+","E"), 1.0),
                Rule("E", ("E","*","E"), 1.5),
                Rule("E", ("x",), 0.7)], "E")
g.recognize(tokens)          # bool; terminates on nullable/cyclic/left-recursive grammars
g.build_forest()             # SPPF DAG, shared by node identity
g.count()                    # exact arbitrary-precision int (all 10^6 derivations)
g.count_mod(1_000_000_007)   # exact count modulo a prime
g.kbest(5)                   # [(cost, key), ...] Viterbi order, deterministic ties
g.stats()                    # {'nodes': ..., 'edges': ...}

Verification (independent checks)

An independent brute-force enumerator is compared against the forest for small inputs. Results:

Check Result
Nullable S→A B, A→ε\|a, B→ε\|a on a count = 2, matches brute force
Direct left rec A→A a \| b on b a a recognized, count = 1
Indirect left rec A→B a\|b, B→A (± nullable) terminates, matches brute force
Dangling else exactly 2 parses, mod count agrees
E→E+E \| E*E \| x, 16 operands exact Catalan(15) = 9,694,845; forest only 574 nodes / 1134 edges
Epsilon cycle A→B, B→A, B→ε recognized; count()/kbest() raise InfiniteForestError
Dead cycle (no base) terminates, count = 0
30 random k-best trials vs brute force all match
Tie-breaking strictly increasing keys, lower rule index first, bit-identical reruns
Modulo consistency (n=2,5,8,12,16) count_mod(p) == count % p

Performance on the ambiguous arithmetic grammar (single run):

n=16  nodes=574   edges=1134   count=9,694,845        recognize=0.000s build=0.001s count=0.000s kbest=0.007s
n=20  nodes=878   edges=2018   count=1,767,263,190    recognize=0.002s build=0.001s count=0.000s kbest=0.016s
n=24  nodes=1246  edges=3270   count=343,059,613,650  recognize=0.001s build=0.002s count=0.001s kbest=0.031s

The SPPF stays polynomial while the derivation count grows exponentially, confirming no parse-tree enumeration occurs. SOLUTION.md contains the full analysis, complete source for both files, and all verification output.

Evidence & signatures

# Evidence
- Problem class: python-earley-shared-packed-forest-kbest
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-11T16:11:21.061Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement an Earley recognizer/parser that handles nullable nonterminals, epsilon cycles, and direct plus indirect left recursion without looping forever, and that stores every derivation in a shared packed forest so an ambiguous grammar with roughly 10^6 derivations is never expanded into a parse-tree enumeration. From that forest, return the k-best parses in Viterbi order under per-rule weights, deduplicating shared subtrees by identity while still reporting the exact derivation count (arbitrary precision) for the whole forest. Verify on the dangling-else grammar, an ambiguous arithmetic grammar, and a nullable-heavy grammar where the naive Earley completer diverges, asserting parse counts modulo a large prime and the correct tie-breaking order for the k-best list.", "environment": "python3", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "python-earley-shared-packed-forest-kbest", "provider": "openrouter", "solved_at": "2026-09-11T16:11:21.061Z", "version": "3.12"}

Answer 2

The solution is complete and verified. The full self-contained markdown is written to ~/SOLUTION.md, alongside the runnable earley_spf.py and test_earley_spf.py.

What was wrong (root cause)

A textbook Earley implementation fails on this problem class in four ways:

  1. The completer is not a fixed point. Nullable completions have origin == end, so the completed item and the waiter it must advance live in the same chart set. A waiter inserted after the completion is processed is silently dropped, and if you instead re-enqueue naively, an epsilon cycle (A→B, B→A, B→ε) spins forever. Fix: process positions left-to-right and saturate each with an agenda, indexing waiters[pos][X] and completed_at[end][origin], and let a processed waiter advance against an already-present nullable completion at completed_at[pos][pos]. Each item is inserted once (bounded by |rules|·(n+1)²), so it always terminates.
  2. Left recursion is handled by the same item dedup; (rule, dot, origin) is added once and the chart reaches a finite fixed point (direct and indirect).
  3. Storing every derivation is exponential (E→E+E | E*E | x, 16 operands ⇒ Catalan(15) = 9,694,845 trees). Fix: a true SPPF — one symbol node per span plus one intermediate node per Earley item, with hyperedges per split point. Counting is linear DP over the DAG; shared sub-forests are node identities.
  4. k-best must not enumerate. It is computed bottom-up with a min-heap over each hyperedge's Cartesian product of child top-k lists, merged across hyperedges, ordered by (tropical cost, derivation key). Ties break on the preorder sequence of rule indices / split positions, which is total and hash-order independent. Genuine cycles ⇒ InfiniteForestError (truly infinite count).

Exact fix (commands)

cd ~
python3 -m py_compile earley_spf.py test_earley_spf.py
python3 test_earley_spf.py        # -> ALL CHECKS PASSED

Key entry points in earley_spf.py:

g = EarleySPPF([Rule("E", ("E","+","E"), 1.0),
                Rule("E", ("E","*","E"), 1.5),
                Rule("E", ("x",), 0.7)], "E")
g.recognize(tokens)          # bool; terminates on nullable/cyclic/left-recursive grammars
g.build_forest()             # SPPF DAG, shared by node identity
g.count()                    # exact arbitrary-precision int (all 10^6 derivations)
g.count_mod(1_000_000_007)   # exact count modulo a prime
g.kbest(5)                   # [(cost, key), ...] Viterbi order, deterministic ties
g.stats()                    # {'nodes': ..., 'edges': ...}

Verification (independent checks)

An independent brute-force enumerator is compared against the forest for small inputs. Results:

Check Result
Nullable S→A B, A→ε\|a, B→ε\|a on a count = 2, matches brute force
Direct left rec A→A a \| b on b a a recognized, count = 1
Indirect left rec A→B a\|b, B→A (± nullable) terminates, matches brute force
Dangling else exactly 2 parses, mod count agrees
E→E+E \| E*E \| x, 16 operands exact Catalan(15) = 9,694,845; forest only 574 nodes / 1134 edges
Epsilon cycle A→B, B→A, B→ε recognized; count()/kbest() raise InfiniteForestError
Dead cycle (no base) terminates, count = 0
30 random k-best trials vs brute force all match
Tie-breaking strictly increasing keys, lower rule index first, bit-identical reruns
Modulo consistency (n=2,5,8,12,16) count_mod(p) == count % p

Performance on the ambiguous arithmetic grammar (single run):

n=16  nodes=574   edges=1134   count=9,694,845        recognize=0.000s build=0.001s count=0.000s kbest=0.007s
n=20  nodes=878   edges=2018   count=1,767,263,190    recognize=0.002s build=0.001s count=0.000s kbest=0.016s
n=24  nodes=1246  edges=3270   count=343,059,613,650  recognize=0.001s build=0.002s count=0.001s kbest=0.031s

The SPPF stays polynomial while the derivation count grows exponentially, confirming no parse-tree enumeration occurs. SOLUTION.md contains the full analysis, complete source for both files, and all verification output.

Evidence & signatures

# Evidence
- Problem class: python-earley-shared-packed-forest-kbest
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-11T16:11:21.061Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement an Earley recognizer/parser that handles nullable nonterminals, epsilon cycles, and direct plus indirect left recursion without looping forever, and that stores every derivation in a shared packed forest so an ambiguous grammar with roughly 10^6 derivations is never expanded into a parse-tree enumeration. From that forest, return the k-best parses in Viterbi order under per-rule weights, deduplicating shared subtrees by identity while still reporting the exact derivation count (arbitrary precision) for the whole forest. Verify on the dangling-else grammar, an ambiguous arithmetic grammar, and a nullable-heavy grammar where the naive Earley completer diverges, asserting parse counts modulo a large prime and the correct tie-breaking order for the k-best list.", "environment": "python3", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "python-earley-shared-packed-forest-kbest", "provider": "openrouter", "solved_at": "2026-09-11T16:11:21.061Z", "version": "3.12"}
Generated from the verified corpus · MIT licensedBack to the catalog