Skip to content

feat(gfql/cypher): Earley → LALR(1) parser (~70× faster parse) with machine-checked grammar invariants#1682

Merged
lmeyerov merged 12 commits into
masterfrom
dev/gfql-cypher-lalr-clean
Jul 5, 2026
Merged

feat(gfql/cypher): Earley → LALR(1) parser (~70× faster parse) with machine-checked grammar invariants#1682
lmeyerov merged 12 commits into
masterfrom
dev/gfql-cypher-lalr-clean

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

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_rows execution routing unchanged. The parser is now backed by a grammar that is provably unambiguous LALR(1) (zero conflicts, builds under Lark strict=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):

  • The core switch: Earley → LALR(1), unified where_clause: "WHERE" expr, and the post-parse lift that recovers structured filter_dict predicates by re-parsing the WHERE body with a LALR sub-parser (start="where_predicate_chain").
  • The predicate span-offset fix (his 036278ce).
  • The flat-AND-chain restriction on the lift (his b7b43248): only a flat A AND B AND … chain lifts to filter_dict; parens / OR / XOR / NOT stay on where_rows. This is a correctness boundary, not just routing (see Edge cases).

Added here (8 commits on top):

  • Grammar purification → 8 shift/reduce conflicts to 0, provably unambiguous (details in Design).
  • A machine-checked invariant suite (test_grammar_invariants.py): zero-conflict + strict-build, semantic-ambiguity-zero, rule-coverage gate, LALR≡Earley differentials, pinned language fixes.
  • A full-repo differential wired into existing CI (test_differential_grammar_parity.py).

Dropped: test_where_metamorphic.py (#1653's 225-line characterization test). It characterizes an execution-engine <>-over-null divergence between filter_dict and where_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

  • Unified WHERE grammar: where_clause: "WHERE" expr. The old where_predicates | expr dual rule was a genuine reduce/reduce ambiguity (the structured predicate chain is a syntactic subset of expr) — 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_dict vs where_rows routing downstream is unchanged.
  • Grammar purification (new in this PR):
    • WHERE binds to its preceding clause in the grammar (bundled into match_clause; with_stage owns WHERE-after-WITH). The WITH..WHERE attachment ambiguity is eliminated, not tie-broken by priorities.
    • 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 dotted-name derivation redundancy.
    • Redundant label_predicate_expr dropped(n:Admin) parses via grouped_expr over bare_label_predicate_expr (identical AST).
    • List elements exclude a top-level IN[x IN xs … is comprehension syntax; allowing a bare IN list element overlapped it. Restricting list elements to a no-top-level-IN expr chain also rejects the invalid "list of IN-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).
    • Net: 8 shift/reduce conflicts → ZERO. The grammar is now provably unambiguous LALR(1) and builds under Lark strict=True (a build-time proof; also checks lexer collisions). Every input has a single derivation.
    • The clause-sequence assembler is unchanged: bundled MATCH..WHERE is 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)

  1. Zero LALR conflicts / strict-cleantest_grammar_has_zero_lalr_conflicts asserts the build emits no conflicts (dependency-free, always-on in CI), and test_grammar_builds_under_strict_mode confirms Lark(strict=True) builds (a build-time proof; needs the optional interegular dep, skips without it). Any grammar edit that introduces an ambiguity produces a conflict and fails CI.
  2. Semantic ambiguity = 0 — every corpus query has exactly one Earley derivation and it transforms to a single AST; an independent AST-level confirmation of (1), no residual exceptions.
  3. Parser-choice neutrality — the production pipeline under LALR vs Earley yields byte-identical ASTs + identical rejections, on a constructed all-constructs corpus AND a full-repo scrape (1,800+ queries, runs in the existing cypher-frontend-differential-parity CI job via its test_differential*.py glob).
  4. Pinned deliberate language fixes — the handful of accept-by-accident shapes that LALR (correctly) rejects are enumerated in DELIBERATE_LANGUAGE_FIXES and 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

  • The lift is an internal optimization, never observableWHERE n.x = 1 AND n.y = 2 lifts to filter_dict; anything with parens / OR / XOR / NOT / arithmetic stays on where_rows. This is a correctness boundary (from earley to lalr parsing for cypher #1653's b7b43248), not just routing: where_rows treats an absent property as null (openCypher semantics), while filter_dict requires the column to exist and would raise — so e.g. a IS NULL AND (b = 1) with b absent must stay on where_rows. The lift only fires when both paths agree.
  • Deliberate language fixes (accept-by-accident shapes, now honest syntax errors, each pinned in DELIBERATE_LANGUAGE_FIXES so the differential stays honest):
    • RETURN DISTINCT (no items) — Earley's dynamic lexer re-lexed DISTINCT as a variable name (returned a var named DISTINCT, distinct=False); LALR's contextual lexer keeps it a keyword.
    • Double WHERE 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.
    • Double post-WITH WHERE, WHERE after UNWIND, WHERE before MATCH in a graph constructor — same class, all now grammar-level rejections.
    • "List of IN-booleans" [x IN xs, y] / [1 IN a, 2] / [n.x IN a, y][x IN xs … is comprehension syntax; a bare IN list element overlapped it, so it's rejected uniformly now (some of these used to parse and then fail downstream with GFQLTypeError). Parenthesize for a genuine bool list: [(x IN xs), y].
  • Span fidelity across the lift — bundled MATCH..WHERE reconstructs the MatchClause span to end at the last pre-WHERE character, and lifted predicate spans are retargeted from atom-relative to absolute, so downstream error positions still point at the real source location.
  • String/comment masking — pattern-existence and unsupported-syntax pre-scans mask quoted/backticked/commented regions, so lexemes inside string literals aren't misread as syntax.

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 _GRAMMAR in parser.py (where an editor lands first):

  1. Edit/add a rule.
  2. Add a query exercising it to DIFFERENTIAL_CORPUSforced: test_every_grammar_rule_is_exercised_by_the_corpus fails 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.
  3. The machine invariants then catch the rest automatically:
The edit… …is caught by
adds a rule with no test rule-coverage gate names the rule
introduces ANY ambiguity / conflict zero-conflict + strict-build checks fail
introduces an AST-affecting ambiguity semantic-ambiguity-zero (derivations disagree)
makes LALR ≠ Earley the two differentials (constructed + full-repo scrape)
deliberately changes accept/reject must be pinned in DELIBERATE_LANGUAGE_FIXES, else the differential fails

A 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):

query LALR Earley speedup
MATCH (n) WHERE n.x = 1 RETURN n 0.157 ms 10.6 ms 67×
flat-AND + ORDER BY/LIMIT 0.415 ms 38.7 ms 93×
MATCH (a)-[r:R*1..3]->(b) WITH … WHERE … OR … IN […] 0.612 ms 50.9 ms 83×

~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)

  • Full cypher suite: 1,681 passed / 0 failed (6 skipped, 15 xfailed)
  • Grammar builds with zero LALR conflicts and under strict=True (provably unambiguous)
  • LALR-vs-Earley full-repo differential (1,850+ queries): 0 AST divergences, 0 error-class differences
  • Old-vs-new production differential (for the purification pass): 0 AST diffs, exactly the pinned language fixes
  • Raw parse speed unchanged by purification (0.57ms)

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):

🤖 Generated with Claude Code

https://claude.ai/code/session_01W5vkD2ZCyv3bmecBYoYYQy

mj3cheun and others added 12 commits July 5, 2026 14:22
…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>
@lmeyerov lmeyerov force-pushed the dev/gfql-cypher-lalr-clean branch from 0d55b16 to 136ba28 Compare July 5, 2026 21:23
@lmeyerov lmeyerov merged commit 60ab479 into master Jul 5, 2026
69 checks passed
@lmeyerov lmeyerov deleted the dev/gfql-cypher-lalr-clean branch July 5, 2026 21:37
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants