feat(gfql/cypher): Earley → LALR(1) parser (~70× faster parse) with machine-checked grammar invariants#1682
Merged
Merged
Conversation
This was referenced Jul 5, 2026
…ows routing via post-parse lift
…s stay on where_rows
…rser Three grammar-level guards (test_grammar_invariants.py) so the correctness argument lives in the grammar, not the Python: 1. Pinned LALR conflict profile: 8 shift/reduce (DOT x5, IN, WHERE, RPAR), all resolved as shift, ZERO reduce/reduce. R/R is the genuine-ambiguity line a grammar edit must never cross silently. 2. Semantic ambiguity = 0: every Earley derivation (ambiguity='explicit' + CollapseAmbiguities) of every corpus query transforms to the same AST -- except the one characterized WITH..WHERE attachment ambiguity, pinned as exactly binary. (Raw _ambig-node counting is NOT usable: Earley reports harmless alternative derivations even for trivial queries; the invariant that matters is that they collapse to one AST.) 3. LALR == Earley differential: the production pipeline run under both parsers yields byte-identical ASTs and identical rejections across a corpus covering every grammar construct family. Also: honest CHANGELOG entry for the Earley->LALR switch (~70x parse, full WHERE language preserved, parse-equivalence machine-checked). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ull differential Full repo-corpus differential on dgx-spark (1839 scraped queries, production pipeline under LALR vs Earley): 0 AST divergences, 0 error-class differences, 1 language divergence -- 'MATCH (n) RETURN DISTINCT' (no items). Earley's dynamic lexer re-lexed DISTINCT (absent from the NAME reserved lookahead) as a NAME and accepted it as returning a variable named DISTINCT with distinct=False: a lexing accident, not Cypher. LALR's contextual lexer keeps it a keyword, so it's now a syntax error. Pinned as a rejection test. Full cypher suite on dgx-spark: 1671 passed, 5 skipped, 15 xfailed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/R conflicts -> 2 Two structural fixes so the grammar, not Python, carries clause structure: 1. WHERE binds to its preceding clause IN THE GRAMMAR: bundled into match_clause (with_stage already owned WHERE-after-WITH); the standalone where_clause query item is gone. This ELIMINATES the WITH..WHERE attachment ambiguity (previously tie-broken by a rule priority + shift preference) rather than resolving it. The clause-sequence assembler is unchanged: bundled MATCH..WHERE is re-flattened to the exact item sequence the old state machine was written against, and the MatchClause span is reconstructed to end at the last pre-WHERE character, so ASTs are byte-identical (old-vs-new production differential: 0 AST diffs). 2. Name-rooted dot chains derive ONLY via qualified_name; property_access applies only to composite roots (f(x).y, (e).y, xs[0].y). Kills the property_access/qualified_name derivation redundancy and all five DOT shift/reduce conflicts. Result: 2 shift/reduce conflicts (IN, RPAR), 0 reduce/reduce, each mapped 1:1 to a characterized binary AST-NEUTRAL derivation ambiguity ([x IN xs] comprehension-vs-list, (n:Admin) grouped-label), pinned as witnesses. Semantic ambiguity is now ZERO unconditionally (every Earley derivation of every corpus query -> same AST, no exceptions). The obsolete with_where_clause priority is dropped. Language fixes (accept-by-accident shapes with ill-defined attachment, now honest syntax errors, all pinned): double WHERE (old code kept BOTH predicates in different AST fields), double post-WITH WHERE, WHERE after UNWIND, WHERE before MATCH in a graph constructor. Verified on dgx-spark: full cypher suite 1677 passed; LALR-vs-Earley full-repo differential (1846 queries) 0 AST divergences; old-vs-new production differential 0 AST diffs, exactly the 4 pinned language fixes. Raw parse speed unchanged (0.57ms). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_differential_grammar_parity.py scrapes every cypher-looking string in the package (1,800+) and runs each through the production pipeline under both parsers: byte-identical ASTs, rejection parity, error-class parity, with the 5 pinned deliberate language fixes as the only allowed divergences. Named to match the test_differential*.py glob of the existing cypher-frontend-differential-parity CI job (zero workflow changes); the grammar invariants file is already auto-discovered by test-gfql-core. Verified on dgx-spark: 11 grammar tests pass in 30s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the safety net test_every_grammar_rule_is_exercised_by_the_corpus asserts every tree-producing grammar rule (aliases + non-inlined origins, across all three entry points) appears in some corpus parse. Adding a grammar rule without a DIFFERENTIAL_CORPUS query now FAILS with the rule's name — which forces the new construct through every other invariant (ambiguity probe, LALR==Earley differential) and the full-repo differential. Closes the gap where a new rule got only conflict-profile coverage until someone remembered to write queries. Backfilled the corpus to 100% rule coverage (arithmetic/comparison operators, unary +/- on non-literal operands, composite-root property access, list-slice subscripts, quantifiers, map/list literals, DISTINCT-agg, simple relationship arrows, path binding, count(*)). Documented the safe-by-construction edit flow at the top of _GRAMMAR in parser.py. Verified on dgx-spark: 182 grammar+parser tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dropped the redundant `label_predicate_expr: "(" NAME labels ")"` rule from
return_expr. A parenthesized label predicate `(n:Admin)` already parses via
`expr -> grouped_expr` over `bare_label_predicate_expr` with a byte-identical
AST, so the dedicated rule was pure derivation redundancy — exactly the RPAR
shift/reduce conflict. Verified 0 AST diffs on the probe corpus + full suite.
Conflict profile is now **1 S/R (IN), 0 R/R**. The IN conflict is inherent to
Cypher (`[NAME IN ...` is a comprehension or a list whose first element is an
in_op — indistinguishable at `[ NAME . IN` with one lookahead); empirically
unremovable without duplicating the expr hierarchy (dropping lc_source does
NOT remove it and breaks `[x IN xs]`), so it stays pinned as a characterized,
AST-neutral witness `[x IN xs]`.
Renamed the transformer method grouped_label_predicate -> bare_label_predicate
(its only remaining binding) now that the "grouped" rule is gone.
Verified on dgx-spark: full cypher suite 1679 passed / 0 failed; grammar
invariants + full-repo differential green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Investigating whether the single residual IN shift/reduce could be closed surfaced a narrow latent divergence: `[x IN xs, y IN ys]` — a list literal whose FIRST element is a bare-name membership test with 2+ elements — is accepted by same-grammar Earley but rejected by LALR (the IN conflict resolves as shift toward a comprehension, so the parser commits at `[ x IN` and chokes on the comma). Deliberately NOT closing it: resolution needs an out-of-grammar bracket scan (scan for `|`/WHERE before the matching `]`) or a duplicated expr hierarchy, and the construct is non-executable in GFQL regardless — `[1 IN a, 2 IN b]` already raises downstream. LALR's parse-time rejection is cleaner than Earley's accept-then-cryptic-runtime-error. Parenthesize for a real list of booleans: `[(x IN xs), y IN ys]` parses; int-/dotted-first lists are unaffected. Pinned in DELIBERATE_LANGUAGE_FIXES so the full-repo differential stays honest if such a query ever enters the corpus. Test-only; no parser change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iguous LALR(1)
Your question ("if it's invalid, can we just eliminate it from the grammar?")
was right. The last conflict (IN) existed because a list-literal element could
be a bare top-level `IN` expression, overlapping list-comprehension syntax
`[x IN xs ...`. Excluding top-level IN from list elements removes it:
list_literal: "[" [list_element_list] "]"
list_element: <expr hierarchy minus in_op at the comparable level>
Net: **8 shift/reduce conflicts -> 0**. The grammar builds under Lark
`strict=True` — a build-time PROOF of unambiguity (single derivation for every
input). This is the "ambiguity is machine-checkable" property the design was
aiming for, now literally true rather than approximated by a pinned conflict
set.
Bonus: fixes an inconsistency. `[1 IN a, 2]` and `[n.x IN a, y]` used to parse
(then fail downstream with GFQLTypeError) while `[x IN xs, y]` was rejected —
now the invalid "list of IN-booleans" is rejected UNIFORMLY at grammar level.
A genuine list of membership booleans stays expressible with parens:
`[(x IN xs), y]`.
Tests: replaced the pinned-conflict-profile + residual-witness tests with
test_grammar_has_zero_lalr_conflicts (dependency-free, always-on CI guard) and
test_grammar_builds_under_strict_mode (skips without the optional interegular
dep). test_semantic_ambiguity_zero now asserts exactly one derivation per
query, no residual exceptions. The 3 list-of-IN-booleans forms pinned in
DELIBERATE_LANGUAGE_FIXES.
Verified on dgx-spark: full cypher suite 1681 passed / 0 failed; zero-conflict
+ strict build + full-repo differential all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t, 1681 tests Correct stale bits in the Earley->LALR entry: conflict profile is now ZERO conflicts + strict-build (not a pinned nonzero set), no residual witnesses, the full deliberate-fix list including the list-of-IN-booleans, the lift correctness boundary, 1681-test count, and the #1683 3VL cross-reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0d55b16 to
136ba28
Compare
lmeyerov
added a commit
that referenced
this pull request
Jul 5, 2026
#1682 switched WHERE parsing to LALR (contextual lexer). The =~ rules textually merged clean but the contextual lexer tokenized '=~' as COMP_OP '=' + '~' (Earley's dynamic lexer had tried all terminals), so every =~ query failed to parse. Add a filtered, higher-priority terminal (_REGEX_MATCH.5) so the lexer prefers '=~' over '='; leading underscore keeps the op token out of the tree, preserving regex_where/regex_op arity. Add =~ corpus entries to satisfy #1682's new rule-coverage gate (regex_op via expr path, regex_where via the lift chain). Grammar invariants (zero LALR conflicts, rule coverage, Earley differential) + parser + lowering suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 5, 2026
#1682's test_every_grammar_rule_is_exercised_by_the_corpus requires every grammar rule to appear in DIFFERENTIAL_CORPUS. #1679's exists_subquery_atom rule needs coverage — add representative EXISTS / NOT EXISTS queries (single Earley derivation, so the semantic-ambiguity invariant still holds). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 5, 2026
#1682's test_every_grammar_rule_is_exercised_by_the_corpus requires every grammar rule to appear in DIFFERENTIAL_CORPUS. #1679's exists_subquery_atom rule needs coverage — add representative EXISTS / NOT EXISTS queries (single Earley derivation, so the semantic-ambiguity invariant still holds). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 5, 2026
…e flatten seam) The grammar bundles a trailing WHERE onto its MATCH clause. The transformer previously split it back out into a synthetic standalone WhereClause item and let the legacy clause-sequence assembler re-attach it — a temporary seam that kept the Earley→LALR switch (#1682) byte-identical without touching the gnarliest transformer method. Now the assembler consumes MatchClause.where directly: - primary MATCH keeps its WHERE on the clause (and it becomes the query's top-level `where`, last-clause-wins for per-clause scoping) — the grammar already set it, so this is a pure deletion of the split/re-attach round-trip; - a post-WITH re-entry MATCH's WHERE moves into `reentry_wheres` with the clause carrying none (the one case that still needs a split). The now-unreachable standalone-WhereClause branches in both query_body and graph_constructor are removed. Net −23 lines. Pure internal refactor, NO behavior change (closes #1687). Verified: - old-vs-new production AST differential over a 1,989-query repo corpus: 1989/1989 byte-identical, 0 AST diffs, 0 language diffs; - grammar invariants (zero conflicts / strict / semantic-ambiguity-zero) + full-repo LALR≡Earley differential pass; - full cypher suite on dgx-spark: 1737 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 5, 2026
…e flatten seam) The grammar bundles a trailing WHERE onto its MATCH clause. The transformer previously split it back out into a synthetic standalone WhereClause item and let the legacy clause-sequence assembler re-attach it — a temporary seam that kept the Earley→LALR switch (#1682) byte-identical without touching the gnarliest transformer method. Now the assembler consumes MatchClause.where directly: - primary MATCH keeps its WHERE on the clause (and it becomes the query's top-level `where`, last-clause-wins for per-clause scoping) — the grammar already set it, so this is a pure deletion of the split/re-attach round-trip; - a post-WITH re-entry MATCH's WHERE moves into `reentry_wheres` with the clause carrying none (the one case that still needs a split). The now-unreachable standalone-WhereClause branches in both query_body and graph_constructor are removed. Net −23 lines. Pure internal refactor, NO behavior change (closes #1687). Verified: - old-vs-new production AST differential over a 1,989-query repo corpus: 1989/1989 byte-identical, 0 AST diffs, 0 language diffs; - grammar invariants (zero conflicts / strict / semantic-ambiguity-zero) + full-repo LALR≡Earley differential pass; - full cypher suite on dgx-spark: 1737 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 5, 2026
…e flatten seam) The grammar bundles a trailing WHERE onto its MATCH clause. The transformer previously split it back out into a synthetic standalone WhereClause item and let the legacy clause-sequence assembler re-attach it — a temporary seam that kept the Earley→LALR switch (#1682) byte-identical without touching the gnarliest transformer method. Now the assembler consumes MatchClause.where directly: - primary MATCH keeps its WHERE on the clause (and it becomes the query's top-level `where`, last-clause-wins for per-clause scoping) — the grammar already set it, so this is a pure deletion of the split/re-attach round-trip; - a post-WITH re-entry MATCH's WHERE moves into `reentry_wheres` with the clause carrying none (the one case that still needs a split). The now-unreachable standalone-WhereClause branches in both query_body and graph_constructor are removed. Net −23 lines. Pure internal refactor, NO behavior change (closes #1687). Verified: - old-vs-new production AST differential over a 1,989-query repo corpus: 1989/1989 byte-identical, 0 AST diffs, 0 language diffs; - grammar invariants (zero conflicts / strict / semantic-ambiguity-zero) + full-repo LALR≡Earley differential pass; - full cypher suite on dgx-spark: 1737 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 5, 2026
…e flatten seam) The grammar bundles a trailing WHERE onto its MATCH clause. The transformer previously split it back out into a synthetic standalone WhereClause item and let the legacy clause-sequence assembler re-attach it — a temporary seam that kept the Earley→LALR switch (#1682) byte-identical without touching the gnarliest transformer method. Now the assembler consumes MatchClause.where directly: - primary MATCH keeps its WHERE on the clause (and it becomes the query's top-level `where`, last-clause-wins for per-clause scoping) — the grammar already set it, so this is a pure deletion of the split/re-attach round-trip; - a post-WITH re-entry MATCH's WHERE moves into `reentry_wheres` with the clause carrying none (the one case that still needs a split). The now-unreachable standalone-WhereClause branches in both query_body and graph_constructor are removed. Net −23 lines. Pure internal refactor, NO behavior change (closes #1687). Verified: - old-vs-new production AST differential over a 1,989-query repo corpus: 1989/1989 byte-identical, 0 AST diffs, 0 language diffs; - grammar invariants (zero conflicts / strict / semantic-ambiguity-zero) + full-repo LALR≡Earley differential pass; - full cypher suite on dgx-spark: 1737 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 6, 2026
…e flatten seam) The grammar bundles a trailing WHERE onto its MATCH clause. The transformer previously split it back out into a synthetic standalone WhereClause item and let the legacy clause-sequence assembler re-attach it — a temporary seam that kept the Earley→LALR switch (#1682) byte-identical without touching the gnarliest transformer method. Now the assembler consumes MatchClause.where directly: - primary MATCH keeps its WHERE on the clause (and it becomes the query's top-level `where`, last-clause-wins for per-clause scoping) — the grammar already set it, so this is a pure deletion of the split/re-attach round-trip; - a post-WITH re-entry MATCH's WHERE moves into `reentry_wheres` with the clause carrying none (the one case that still needs a split). The now-unreachable standalone-WhereClause branches in both query_body and graph_constructor are removed. Net −23 lines. Pure internal refactor, NO behavior change (closes #1687). Verified: - old-vs-new production AST differential over a 1,989-query repo corpus: 1989/1989 byte-identical, 0 AST diffs, 0 language diffs; - grammar invariants (zero conflicts / strict / semantic-ambiguity-zero) + full-repo LALR≡Earley differential pass; - full cypher suite on dgx-spark: 1737 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Replaces the GFQL Cypher Earley parser with a single LALR(1) parser (contextual lexer): ~70× faster parse (~0.25ms vs ~17ms raw per query), full WHERE language preserved (OR/XOR/NOT, parens, arithmetic, IN, pattern predicates — no subset restriction), byte-identical ASTs, and
filter_dict/where_rowsexecution routing unchanged. The parser is now backed by a grammar that is provably unambiguous LALR(1) (zero conflicts, builds under Larkstrict=True) with the parse-equivalence machine-checked in CI.This is a mashup: it re-cuts @mj3cheun's #1653 as its foundation, then adds a grammar-purification pass and a machine-checked invariant suite on top. Provenance below is explicit so the review can separate "same as #1653" from "new here."
Provenance — what's kept, added, dropped, deferred
Kept from #1653 (@mj3cheun's 3 commits, first on the branch, essentially verbatim):
where_clause: "WHERE" expr, and the post-parse lift that recovers structuredfilter_dictpredicates by re-parsing the WHERE body with a LALR sub-parser (start="where_predicate_chain").036278ce).b7b43248): only a flatA AND B AND …chain lifts tofilter_dict; parens / OR / XOR / NOT stay onwhere_rows. This is a correctness boundary, not just routing (see Edge cases).Added here (8 commits on top):
test_grammar_invariants.py): zero-conflict + strict-build, semantic-ambiguity-zero, rule-coverage gate, LALR≡Earley differentials, pinned language fixes.test_differential_grammar_parity.py).Dropped:
test_where_metamorphic.py(#1653's 225-line characterization test). It characterizes an execution-engine<>-over-null divergence betweenfilter_dictandwhere_rows, which is downstream of the AST and orthogonal to the parser. Refiled as #1683 (see Deferred), so the finding stays loud without riding the parser PR as a permanent xfail.Deferred: the flatten seam removal, the TCK-corpus differential, and the #1683 execution-semantics decision (see the Deferred section at the bottom).
Design
where_clause: "WHERE" expr. The oldwhere_predicates | exprdual rule was a genuine reduce/reduce ambiguity (the structured predicate chain is a syntactic subset ofexpr) — the thing that forced Earley. Structured flat-AND predicates are recovered by a post-parse lift through a LALR sub-parser (start="where_predicate_chain").filter_dictvswhere_rowsrouting downstream is unchanged.match_clause;with_stageowns WHERE-after-WITH). The WITH..WHERE attachment ambiguity is eliminated, not tie-broken by priorities.qualified_name;property_accessapplies only to composite roots (f(x).y,(e).y,xs[0].y). Kills the dotted-name derivation redundancy.label_predicate_exprdropped —(n:Admin)parses viagrouped_exproverbare_label_predicate_expr(identical AST).IN—[x IN xs …is comprehension syntax; allowing a bareINlist element overlapped it. Restricting list elements to a no-top-level-INexpr chain also rejects the invalid "list ofIN-booleans" uniformly ([1 IN a, 2],[n.x IN a, y]too — these used to parse then fail downstream;[(x IN xs), y]still works for a genuine bool list).strict=True(a build-time proof; also checks lexer collisions). Every input has a single derivation.MATCH..WHEREis re-flattened to the item sequence the assembler was written against (that's what kept this AST-byte-identical). Removing that seam is a scoped follow-up.Machine-checked invariants (
test_grammar_invariants.py+test_differential_grammar_parity.py)test_grammar_has_zero_lalr_conflictsasserts the build emits no conflicts (dependency-free, always-on in CI), andtest_grammar_builds_under_strict_modeconfirmsLark(strict=True)builds (a build-time proof; needs the optionalinteregulardep, skips without it). Any grammar edit that introduces an ambiguity produces a conflict and fails CI.cypher-frontend-differential-parityCI job via itstest_differential*.pyglob).DELIBERATE_LANGUAGE_FIXESand asserted both as production rejections and as the only allowed divergences in the full-repo differential (details in Edge cases below). Everything else is accept/reject- and AST-identical to the old parser.Edge cases & how they're handled
WHERE n.x = 1 AND n.y = 2lifts tofilter_dict; anything with parens / OR / XOR / NOT / arithmetic stays onwhere_rows. This is a correctness boundary (from earley to lalr parsing for cypher #1653'sb7b43248), not just routing:where_rowstreats an absent property as null (openCypher semantics), whilefilter_dictrequires the column to exist and would raise — so e.g.a IS NULL AND (b = 1)withbabsent must stay onwhere_rows. The lift only fires when both paths agree.DELIBERATE_LANGUAGE_FIXESso the differential stays honest):RETURN DISTINCT(no items) — Earley's dynamic lexer re-lexedDISTINCTas a variable name (returned a var named DISTINCT,distinct=False); LALR's contextual lexer keeps it a keyword.MATCH (a) WHERE p WHERE q— the old flat-clause assembler accepted it and kept both predicates in different AST fields (a latent wrong-answer); WHERE now binds to its clause in the grammar.IN-booleans"[x IN xs, y]/[1 IN a, 2]/[n.x IN a, y]—[x IN xs …is comprehension syntax; a bareINlist element overlapped it, so it's rejected uniformly now (some of these used to parse and then fail downstream withGFQLTypeError). Parenthesize for a genuine bool list:[(x IN xs), y].MATCH..WHEREreconstructs theMatchClausespan to end at the last pre-WHEREcharacter, and lifted predicate spans are retargeted from atom-relative to absolute, so downstream error positions still point at the real source location.Extending the grammar is safe-by-construction
The whole point of moving correctness into the grammar is that syntax edits catch their own mistakes. The flow is documented at the top of
_GRAMMARinparser.py(where an editor lands first):DIFFERENTIAL_CORPUS— forced:test_every_grammar_rule_is_exercised_by_the_corpusfails with the new rule's name until you do (every tree-producing rule must appear in some corpus parse). This is the gate that pulls new syntax through all the other checks.DELIBERATE_LANGUAGE_FIXES, else the differential failsA new feature's own tests also enter the full-repo scrape automatically, so its queries get swept into the differential even without explicit registration. Net: you cannot extend the grammar and silently skip the safety net — the build fails loudly and tells you which knob to turn.
Performance
Raw parse time, LALR vs Earley over the same grammar (warm, mean of N iterations):
MATCH (n) WHERE n.x = 1 RETURN nAND+ORDER BY/LIMITMATCH (a)-[r:R*1..3]->(b) WITH … WHERE … OR … IN […]~70–90× on the parse step. (Parse memoization already in master via #1652/#1657 caches repeat identical queries and is orthogonal — this speeds up the first/uncached parse of every query.) The grammar-purification pass is parse-neutral (raw parse 0.565 → 0.573 ms; the 8→0 conflict reduction is about analyzability, not speed).
Verification (dgx-spark)
strict=True(provably unambiguous)CI note
The only red check is
test-networkx-scipy-policy (nx-upper-scipy, py3.12)— a pre-existing, unrelated flake in the networkx HITS test (a degenerate cycle graph interacting with a newer scipy; this branch touches zero networkx code, and master's last run was green only because its fresh lockfile pulled an older scipy). Fixed independently in #1686; once that lands, rebasing this PR onto master turns CI green.Deferred / follow-ups
Tracked in the branch plan (
plans/gfql-cypher-lalr-parser/plan.md, local —plans/is gitignored):<>/null 3VL execution divergence → filed as GFQL Cypher WHERE:<>over null diverges between filter_dict (table semantics) and where_rows (Cypher 3VL) #1683 (cc @mj3cheun).filter_dict(table semantics) treatsnull <> 'ab'as TRUE;where_rows(openCypher 3VL) treats it as null/drop — so parenthesization alone can change row membership. This is the finding earley to lalr parsing for cypher #1653's metamorphic test characterized; it is pre-existing execution-engine behavior, orthogonal to this parser PR, and needs a semantics-intent decision (the issue lays out three options). Not blocking here.MATCH..WHEREis currently re-flattened so the legacy clause-sequence assembler runs untouched (that's what kept this change AST-byte-identical). Follow-up: have the assembler consumeMatchClause.wheredirectly and delete the positional inference, re-proven with the same differentials. Enabled by the "AST is an internal contract" framing.tck-gfqlconformance suite already runs green on this branch (semantic axis). Follow-up: also feed the TCK query corpus through the LALR≡Earley differential harness (parser axis) in tck-gfql CI. The harness is corpus-parameterizable; no parser-side work needed.🤖 Generated with Claude Code
https://claude.ai/code/session_01W5vkD2ZCyv3bmecBYoYYQy