From 8538b1ed8f4dcfd1aba9b5dfb6b5849c8a5da7eb Mon Sep 17 00:00:00 2001 From: Oscar Villavicencio <9220505+odvcencio@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:10:05 -0700 Subject: [PATCH 1/4] fix(glr): Fix Python clean-tie election with mixed GSS stack merging - Merge a flat stack with an already-packed GSS stack in tryGSSMainMergeResult. The incoming stack can supply the physical graph receiver while the incumbent keeps the logical survivor slot, byte offset, and version. - Check a mixed pair against the existing status, shift, state, byte-offset, clean-zero, and distinct-shape gates before it stages. Rejected pairs keep the flat survivor unchanged and consume no GSS scratch nodes. - Certify the structural election for Python's exact blob, so the producer elects expression_list for assignment-right and f-string tuples the way the C reference does. - Reduce normalizePythonInterpolationPatterns to a counted no-op. The producer now owns the choice, so a post-build rewrite would undo it. - Retire the two family-D Python known divergences, flip the arm no-op confirmation, and update the blocker-receipt digests and the 2026-09-02 PARTIAL-GO section in docs/root-normalization-retirement.md. - Drop meson from the certified acceptance-frontier candidates. Its compact tree intentionally differs from Go's historical tree and stays covered by the locked-C parity test. - Add unit tests and diagnostic topology receipt tests for mixed merges, distinct-shape rejection, sticky error inheritance, and scratch-budget bounds. Buckley-Change-Hash: sha256:59e05a80ccabbd9fc163998be0042d4d552ee8ece1c6bc7b3cbfb40263fa4f5b Buckley-Change-Stats: files=14 insertions=697 deletions=209 binaries=0 --- ...on_switch_a3_arm_noop_confirmation_test.go | 24 +- admission_switch_acceptance_frontier_test.go | 5 +- .../python_a3_certification_sweep_test.go | 45 ++-- .../python_dispatch_blocker_receipt_test.go | 86 +++++--- ...thon_scheduler_action_local_parity_test.go | 93 ++------ docs/root-normalization-retirement.md | 31 +++ glr.go | 202 +++++++++++++++-- glr_test.go | 137 ++++++++++++ grammars/runtime_profiles.go | 21 +- grammars/runtime_profiles_test.go | 6 +- parser_result_python.go | 42 +--- work_count_hooks.go | 1 + work_count_topology.go | 6 + work_count_topology_internal_test.go | 207 ++++++++++++++++++ 14 files changed, 697 insertions(+), 209 deletions(-) diff --git a/admission_switch_a3_arm_noop_confirmation_test.go b/admission_switch_a3_arm_noop_confirmation_test.go index fbd819c4c..254142518 100644 --- a/admission_switch_a3_arm_noop_confirmation_test.go +++ b/admission_switch_a3_arm_noop_confirmation_test.go @@ -22,21 +22,19 @@ import ( // on compact-origin trees for that witness -- the retirement precondition // follow-up arm-deletion PRs need. // -// RESULT, corrected from the finding: only Apex's witness confirms as a -// no-op. Perl, Python, and Ada's compat arms still perform real, -// load-bearing tree reshaping on compact-origin trees even after A3 -// certification lands, on their own tied-election witnesses. The finding's -// "compact-raw equals compact-tailed on all six witnesses" claim does not -// reproduce for four of those six (perl push-list; python tuple assignment; -// both ada aggregates). It reproduces for the fifth, apex class-literal. +// RESULT, corrected from the finding: Python's tuple-assignment witnesses now +// confirm as no-op after C-ordered clean-tie selection moves the choice into +// the producer. Perl and Ada still perform load-bearing reshaping on their +// tied-election witnesses. Apex has a separate material-election decline. // // This is consistent with the arm taxonomy (spec.campaign.v7): apex's arm // is a fixed derivation relabel that A3's primary-acceptance-derivation // certification already subsumes at the scheduler level, so there is -// nothing left for the arm to do. Perl and Python's arms are -// scheduler-action arms that perform source-text-scanning list regrouping -// (ambiguous_function_call_expression / pattern_list-vs-expression_list) -- -// a materially stronger transformation than picking among tied derivations. +// nothing left for the arm to do. Perl's arm is a scheduler-action arm that +// performs source-text-scanning list regrouping +// (ambiguous_function_call_expression). Python's former +// pattern_list-vs-expression_list rewrite is now an observed no-op on the +// certified tuple witnesses because the producer makes the C choice. // Ada's arms (materialization-owned) relabel one aggregate production into // another via a similar structural scan. None of these three // transformations are subsumed by the admission-time election flags this @@ -154,7 +152,7 @@ func TestA3ArmNoOpConfirmationPerlDoesNotConfirm(t *testing.T) { } } -func TestA3ArmNoOpConfirmationPythonDoesNotConfirm(t *testing.T) { +func TestA3ArmNoOpConfirmationPythonConfirms(t *testing.T) { lang := grammars.PythonLanguage() if !lang.CompactPrimaryAcceptanceDerivationCertified || !lang.CompactConvergedReductionSplitDropsCertified { t.Fatal("python did not receive its A3 certification") @@ -163,7 +161,7 @@ func TestA3ArmNoOpConfirmationPythonDoesNotConfirm(t *testing.T) { {"assignment_bare_tuple", "x, y, z = 1, 2, 3\nxyz = x, y, z\n"}, {"assignment_bare_pair", "a = 1\nb = 2\npair = a, b\n"}, } { - assertA3ArmNoOp(t, "python/"+tt.name, lang, []byte(tt.source), false) + assertA3ArmNoOp(t, "python/"+tt.name, lang, []byte(tt.source), true) } } diff --git a/admission_switch_acceptance_frontier_test.go b/admission_switch_acceptance_frontier_test.go index 68a00431f..6ec0e5429 100644 --- a/admission_switch_acceptance_frontier_test.go +++ b/admission_switch_acceptance_frontier_test.go @@ -12,7 +12,10 @@ import ( ) func TestAdmissionCandidateCertifiedAcceptanceFrontiers(t *testing.T) { - for _, name := range []string{"http", "meson", "robot"} { + // Meson has an artifact-certified C structural election. Its compact tree + // intentionally differs from Go's historical production tree and is + // covered by the locked-C parity test in cgo_harness. + for _, name := range []string{"http", "robot"} { t.Run(name, func(t *testing.T) { entry := grammars.DetectLanguageByName(name) if entry == nil { diff --git a/cgo_harness/python_a3_certification_sweep_test.go b/cgo_harness/python_a3_certification_sweep_test.go index 76e4c5d9d..8dbdd76e5 100644 --- a/cgo_harness/python_a3_certification_sweep_test.go +++ b/cgo_harness/python_a3_certification_sweep_test.go @@ -14,10 +14,10 @@ import ( // certification-workstream (spec.campaign.v7, finding // tied-election-family-compact-retirement) full-corpus verification receipt // for Python. Python already shipped CompactConvergedReductionSplitDropsCertified; -// this sweep gates the added CompactPrimaryAcceptanceDerivationCertified -// grant (grammars/runtime_profiles.go) on zero unadjudicated compact-vs-C -// divergence across the real corpus plus the tied-election and known-gap -// tuple/f-string witnesses (python_scheduler_action_local_parity_test.go). +// this sweep gates the added compact acceptance grants +// (grammars/runtime_profiles.go) on zero unadjudicated compact-vs-C divergence +// across the real corpus plus the tied-election tuple/f-string witnesses +// (python_scheduler_action_local_parity_test.go). func TestPythonA3CompactCertificationFullCorpusSweep(t *testing.T) { lang := grammars.PythonLanguage() if !lang.CompactPrimaryAcceptanceDerivationCertified { @@ -63,30 +63,17 @@ func TestPythonA3CompactCertificationFullCorpusSweep(t *testing.T) { // at all. The stale-entry ratchet in a3ReportSweep enforces this: it fails // the sweep if a remaining entry stops matching a live divergence. // -// The two f-string entries are family D (a first-class declared -// pattern_list/expression_list ambiguity; Go's reduceForkWindowPreference -// disagrees with C's ts_parser__select_tree on which side to keep). Not -// tied elections, not this gate's scope; repair lanes are tracked -// separately. -var pythonA3KnownDivergences = []a3KnownDivergence{ - { - Witness: "fstring_interpolation_bare_tuple", - FirstPath: "/module/assignment[2]/string[2]/interpolation[1]/pattern_list[1]", - GoValue: "pattern_list", CValue: "expression_list", Family: "D", - }, - { - Witness: "fstring_interpolation_splat", - FirstPath: "/module/assignment[1]/string[2]/interpolation[1]/pattern_list[1]", - GoValue: "pattern_list", CValue: "expression_list", Family: "D", - }, -} +// The two former f-string entries were family D (a first-class declared +// pattern_list/expression_list ambiguity). C-ordered clean-tie selection now +// emits expression_list for the exact Python artifact, so these entries are +// retired from the active known-divergence list. The historical values remain +// in docs/root-normalization-retirement.md and the blocker receipt. +var pythonA3KnownDivergences = []a3KnownDivergence{} -// pythonA3AdversarialSources gathers Python's tied-election witness (the -// bare-tuple assignment right-hand side) plus its known-gap and neutral -// control sources -// (python_scheduler_action_local_parity_test.go), all already vetted as -// conflict-heavy shapes for this grammar's pattern_list/expression_list -// election. +// pythonA3AdversarialSources gathers Python's tied-election witnesses, the +// former f-string blocker shapes, and neutral controls +// (python_scheduler_action_local_parity_test.go). These sources exercise +// conflict-heavy pattern_list/expression_list choices for this grammar. func pythonA3AdversarialSources() []a3CertificationSweepSource { return []a3CertificationSweepSource{ {Name: "assignment_bare_tuple_real_corpus_witness", Source: []byte("x, y, z = 1, 2, 3\nxyz = x, y, z\n")}, @@ -109,10 +96,14 @@ func pythonA3AdversarialSources() []a3CertificationSweepSource { {Name: "for_target_tuple_negative_control", Source: []byte("pairs = [(1, 2)]\nfor a, b in pairs:\n pass\n")}, {Name: "chained_assignment_lhs_negative_control", Source: []byte("a, b = c, d = 1, 2\n")}, {Name: "del_tuple_negative_control", Source: []byte("a = 1\nb = 2\ndel a, b\n")}, + {Name: "with_multiple_as_targets", Source: []byte("with context() as a, other() as b:\n pass\n")}, + {Name: "except_multiple_as_targets", Source: []byte("try:\n pass\nexcept E as a, F as b:\n pass\n")}, {Name: "walrus_in_comprehension", Source: []byte("data = [1, 2, 3]\nresult = [y for x in data if (y := x * 2) > 2]\n")}, {Name: "decorated_async_def", Source: []byte("import functools\n\n@functools.wraps\nasync def f(x, *, y=1, **kw):\n return x, y\n")}, {Name: "star_expr_unpack", Source: []byte("first, *rest = [1, 2, 3]\n")}, {Name: "lambda_tuple_return", Source: []byte("f = lambda x, y: (x, y)\n")}, + {Name: "fstring_call_arguments", Source: []byte("s = f\"{foo(a, b)}\"\n")}, + {Name: "fstring_parenthesized_tuple", Source: []byte("s = f\"{(x, y)}\"\n")}, {Name: "nested_fstring_conversion", Source: []byte("name = \"world\"\ns = f\"{name!r:>{10}}\"\n")}, } } diff --git a/cgo_harness/python_dispatch_blocker_receipt_test.go b/cgo_harness/python_dispatch_blocker_receipt_test.go index 50855718f..1855976d4 100644 --- a/cgo_harness/python_dispatch_blocker_receipt_test.go +++ b/cgo_harness/python_dispatch_blocker_receipt_test.go @@ -33,6 +33,8 @@ type pythonDispatchBlockerWitness struct { wantCDigest string wantRawDiff *DumpV1Divergence wantRouteDiff *DumpV1Divergence + wantForestDigest string + wantForestDiff *DumpV1Divergence wantCompactMode string wantRoutedBefore uint64 wantFallbackBefore uint64 @@ -63,38 +65,37 @@ func TestPythonDispatchBlockerReceiptRoutes(t *testing.T) { witnesses := []pythonDispatchBlockerWitness{ { - name: "assignment_bare_tuple_positive", - source: []byte("x, y, z = 1, 2, 3\nxyz = x, y, z\n"), - wantSourceSHA: "6a1661337725eea3d5f3e26c38c3c3536f2c9fbfb66e04ae73f2dcc1a1afdd03", - wantRawDigest: "1ee859d4c1d2489f24dd57e0671a1832480b1c43afb960c10f798ce9f71f9759", - wantGoDigest: "577a8b7b9281fa12c48dfa239a977c82f3a94e3d248253663c4a6fafc9121622", - wantCDigest: "577a8b7b9281fa12c48dfa239a977c82f3a94e3d248253663c4a6fafc9121622", - wantRawDiff: pythonDispatchExpectedDivergence( - "/module/assignment[1]/pattern_list[2]", "type", "pattern_list", "expression_list", - ), + name: "assignment_bare_tuple_positive", + source: []byte("x, y, z = 1, 2, 3\nxyz = x, y, z\n"), + wantSourceSHA: "6a1661337725eea3d5f3e26c38c3c3536f2c9fbfb66e04ae73f2dcc1a1afdd03", + wantRawDigest: "577a8b7b9281fa12c48dfa239a977c82f3a94e3d248253663c4a6fafc9121622", + wantGoDigest: "577a8b7b9281fa12c48dfa239a977c82f3a94e3d248253663c4a6fafc9121622", + wantCDigest: "577a8b7b9281fa12c48dfa239a977c82f3a94e3d248253663c4a6fafc9121622", + wantRawDiff: nil, + wantRouteDiff: nil, + wantForestDigest: "577a8b7b9281fa12c48dfa239a977c82f3a94e3d248253663c4a6fafc9121622", + wantForestDiff: nil, wantCompactMode: "accepted", wantRoutedBefore: 0, wantFallbackBefore: 0, wantRoutedAfter: 1, wantFallbackAfter: 0, - wantProduction: pythonDispatchPassExpectation{recorded: true, checked: 1, run: 1, visited: 24, rewritten: 1}, + wantProduction: pythonDispatchPassExpectation{recorded: true, checked: 1, run: 1, visited: 24, rewritten: 0}, wantCompact: pythonDispatchPassExpectation{}, wantForest: pythonDispatchPassExpectation{recorded: true, checked: 1, run: 1, visited: 24}, - wantIncremental: pythonDispatchPassExpectation{recorded: true, checked: 1, run: 1, visited: 24, rewritten: 1}, + wantIncremental: pythonDispatchPassExpectation{recorded: true, checked: 1, run: 1, visited: 24, rewritten: 0}, }, { - name: "fstring_interpolation_bare_tuple_recovery_gap", - source: []byte("x = 1\ny = 2\nz = f\"{x, y}\"\n"), - wantSourceSHA: "7d0029944fcffb700144302da9b1b80b03da8f89d716772b3a207dca9ba543a7", - wantRawDigest: "89ca835ae4fb5cf19d38e40b6ae4f09c99c66987ca77d8b4921aa9f593aaa641", - wantGoDigest: "89ca835ae4fb5cf19d38e40b6ae4f09c99c66987ca77d8b4921aa9f593aaa641", - wantCDigest: "84c987ddc73cc06bcc63e0cc860ecaa58560a46db882c775815ecb8867f95c07", - wantRawDiff: pythonDispatchExpectedDivergence( - "/module/assignment[2]/string[2]/interpolation[1]/pattern_list[1]", "type", "pattern_list", "expression_list", - ), - wantRouteDiff: pythonDispatchExpectedDivergence( - "/module/assignment[2]/string[2]/interpolation[1]/pattern_list[1]", "type", "pattern_list", "expression_list", - ), + name: "fstring_interpolation_bare_tuple_recovery_gap", + source: []byte("x = 1\ny = 2\nz = f\"{x, y}\"\n"), + wantSourceSHA: "7d0029944fcffb700144302da9b1b80b03da8f89d716772b3a207dca9ba543a7", + wantRawDigest: "84c987ddc73cc06bcc63e0cc860ecaa58560a46db882c775815ecb8867f95c07", + wantGoDigest: "84c987ddc73cc06bcc63e0cc860ecaa58560a46db882c775815ecb8867f95c07", + wantCDigest: "84c987ddc73cc06bcc63e0cc860ecaa58560a46db882c775815ecb8867f95c07", + wantRawDiff: nil, + wantRouteDiff: nil, + wantForestDigest: "84c987ddc73cc06bcc63e0cc860ecaa58560a46db882c775815ecb8867f95c07", + wantForestDiff: nil, wantCompactMode: "accepted", wantRoutedBefore: 1, wantFallbackBefore: 0, @@ -102,20 +103,20 @@ func TestPythonDispatchBlockerReceiptRoutes(t *testing.T) { wantFallbackAfter: 0, wantProduction: pythonDispatchPassExpectation{recorded: true, checked: 1, run: 1, visited: 22}, wantCompact: pythonDispatchPassExpectation{}, - wantForest: pythonDispatchPassExpectation{recorded: true, checked: 1, run: 1, visited: 22, rewritten: 1}, + wantForest: pythonDispatchPassExpectation{recorded: true, checked: 1, run: 1, visited: 22, rewritten: 0}, wantIncremental: pythonDispatchPassExpectation{recorded: true, checked: 1, run: 1, visited: 22}, }, { - name: "fstring_interpolation_splat_recovery_gap", - source: []byte("xs = [1, 2]\nz = f\"{*xs,}\"\n"), - wantSourceSHA: "660a9ed55b63e6b98cfc70db1776895ec9046a16c906c33b3d273bee496a121d", - wantRawDigest: "102ebedd10a3864a2640cb293f541e42f63b4f1ce3d60c9f219d7088b4f484c6", - wantGoDigest: "102ebedd10a3864a2640cb293f541e42f63b4f1ce3d60c9f219d7088b4f484c6", - wantCDigest: "e646688923780dab15e472c1754d89e87ebfdb669fafeda109d4a2d630b4a4c9", - wantRawDiff: pythonDispatchExpectedDivergence( - "/module/assignment[1]/string[2]/interpolation[1]/pattern_list[1]", "type", "pattern_list", "expression_list", - ), - wantRouteDiff: pythonDispatchExpectedDivergence( + name: "fstring_interpolation_splat_recovery_gap", + source: []byte("xs = [1, 2]\nz = f\"{*xs,}\"\n"), + wantSourceSHA: "660a9ed55b63e6b98cfc70db1776895ec9046a16c906c33b3d273bee496a121d", + wantRawDigest: "e646688923780dab15e472c1754d89e87ebfdb669fafeda109d4a2d630b4a4c9", + wantGoDigest: "e646688923780dab15e472c1754d89e87ebfdb669fafeda109d4a2d630b4a4c9", + wantCDigest: "e646688923780dab15e472c1754d89e87ebfdb669fafeda109d4a2d630b4a4c9", + wantRawDiff: nil, + wantRouteDiff: nil, + wantForestDigest: "102ebedd10a3864a2640cb293f541e42f63b4f1ce3d60c9f219d7088b4f484c6", + wantForestDiff: pythonDispatchExpectedDivergence( "/module/assignment[1]/string[2]/interpolation[1]/pattern_list[1]", "type", "pattern_list", "expression_list", ), wantCompactMode: "accepted", @@ -211,8 +212,8 @@ func TestPythonDispatchBlockerReceiptRoutes(t *testing.T) { } defer forest.Release() forestDiff := pythonDispatchAssertReceipt(t, "forest", forest, language, cTree, cDigest) - pythonDispatchCheckDigest(t, "forest", forest, language, witness.wantGoDigest) - pythonDispatchCheckDivergence(t, "forest", forestDiff, witness.wantRouteDiff) + pythonDispatchCheckDigest(t, "forest", forest, language, witness.wantForestDigest) + pythonDispatchCheckDivergence(t, "forest", forestDiff, witness.wantForestDiff) pythonDispatchCheckPass(t, "forest", forest, witness.wantForest) pythonDispatchCheckNativeAuthority(t, "forest", forest) @@ -263,6 +264,19 @@ func TestPythonDispatchBlockerReceiptDocument(t *testing.T) { } document := strings.Join(strings.Fields(string(doc)), " ") for _, marker := range []string{ + "## 2026-09-02 Python dispatcher certification update", + "Status: `PARTIAL-GO`. The compact route matches locked C on all three current blocker witnesses. Keep `dispatch.python` live.", + "Candidate base commit: `06afb3c881d4064bf367f970614e5120ec0abbfd`.", + "This update adds mixed physical graph-head merging and C-ordered clean-tie selection for the exact Python grammar artifact.", + "The compact counters advance by `1/0` for each witness. No compact fallback occurs.", + "The forest splat result remains a `pattern_list` instead of C's `expression_list`.", + "This forest-only gap stays outside the compact route gate.", + "Incremental parsing now preserves authenticated scanner reuse on all three witnesses.", + "Each route reports `reuse=true` and no unsupported reason.", + "The generated Python corpus from the pinned grammar source also passes the A3 sweep: `real=3`, `constructed=30`, `total=33`, with zero divergences.", + "The external corpus-source lock remains unavailable, so this is not a release certification receipt.", + "Keep the historical 2026-08-24 receipt below unchanged.", + "Reopen the retirement review after the forest splat tie has a separate proof, the authenticated corpus becomes available, and every route passes again.", "## 2026-08-24 Python dispatcher blocker receipt", "Status: `NO-GO`. Keep `dispatch.python` live.", "Base commit: `14f6692fac65eab817f65af8cc6072e423ca6563`.", diff --git a/cgo_harness/python_scheduler_action_local_parity_test.go b/cgo_harness/python_scheduler_action_local_parity_test.go index 12c4a09d1..6b2b87a18 100644 --- a/cgo_harness/python_scheduler_action_local_parity_test.go +++ b/cgo_harness/python_scheduler_action_local_parity_test.go @@ -14,31 +14,12 @@ import ( // TestPythonSchedulerActionLoadBearingCOracleParity is an A3 // (spec.campaign.v7, Workstream A tranche A3) adversarial probe for the -// dispatch.python arm (parser_result_python.go, -// normalizePythonCompatibilityWithParser). It pins the one rewrite the -// real-corpus dispatcher census observed firing: the assignment-right -// expression-list rewrite inside normalizePythonFusedPreorder -// (cgo_harness/corpus_real/python/large__python3.8_grammar.py, "xyz = x, y, -// z"). Each witness is proven load-bearing two ways: the RAW production -// tree (result-compatibility tail off) diverges from the locked C oracle, -// and the NORMALIZED tree (compat tail on) matches it exactly. +// Python result path. It covers the assignment-list witness from the real +// corpus and two smaller comma-tuple witnesses. The raw producer tree and +// the normal parse must both match the locked C oracle. // -// dispatch.python cannot retire yet: this is the arm's uniform retirement -// condition (testdata/result_compat_ownership_v1.json) failing on the -// authoritative owner (scheduler_action_semantics). The root cause is a -// grammar/scheduler derivation-election tie-break: for an unparenthesized -// comma-tuple on the right-hand side of a plain assignment (`x = a, b`), -// gotreesitter's runtime elects the same shape (pattern_list) the grammar -// uses for assignment *targets* and for-loop/except/with targets, where the -// C reference elects expression_list. See -// TestPythonSchedulerActionKnownGapCOracleParity for a sibling shape (the -// identical pattern_list/expression_list tie inside f-string interpolation) -// where no existing sub-pass reaches the fix. Fixing the tie generally in -// the scheduler risks every other pattern_list/expression_list consumer -// (for, except, with, del, match targets) across all grammars, so this is -// not a small language-neutral change; this pins the arm as -// blocked-with-mechanism per spec.campaign.v7 workstream A3 rather than -// forcing a root fix. +// The scheduler now elects expression_list for assignment-right tuples. The +// compatibility arm remains observable, but it must not change these trees. func TestPythonSchedulerActionLoadBearingCOracleParity(t *testing.T) { goLang := grammars.PythonLanguage() cLang, err := COracleLanguage("python") @@ -107,37 +88,20 @@ func TestPythonSchedulerActionLoadBearingCOracleParity(t *testing.T) { compareNodes(rawTree.RootNode(), goLang, cTree.RootNode(), "root", &rawVsC) compareNodes(normTree.RootNode(), goLang, cTree.RootNode(), "root", &normVsC) - if len(rawVsC) == 0 { - t.Fatalf( - "raw tree now matches the C oracle for %q; the upstream grammar/scheduler election "+ - "defect this arm patches around may be fixed -- investigate dispatch.python "+ - "retirement before accepting this as passing", - test.name, - ) + if len(rawVsC) != 0 { + t.Fatalf("raw tree diverges from the C oracle: %s", strings.Join(rawVsC, " | ")) } if len(normVsC) != 0 { - t.Fatalf("normalized (dispatch.python-corrected) tree diverges from the C oracle: %s", strings.Join(normVsC, " | ")) + t.Fatalf("normal tree diverges from the C oracle: %s", strings.Join(normVsC, " | ")) } }) } } -// TestPythonSchedulerActionKnownGapCOracleParity pins the same -// pattern_list/expression_list election tie -// (TestPythonSchedulerActionLoadBearingCOracleParity's doc comment) inside -// f-string interpolation, where normalizePythonInterpolationPatterns -// (parser_result_python.go) does not reach it: that sub-pass only rewrites -// an already-expression_list node found under "interpolation" into -// pattern_list, but raw gotreesitter output for a bare tuple inside an -// f-string interpolation is already pattern_list (the same election as the -// uncorrected assignment case), so the sub-pass's precondition never -// matches -- it converts the opposite direction from the one this witness -// needs. dispatch.python's overall arm remains blocked (see -// TestPythonSchedulerActionLoadBearingCOracleParity), so no route/registry -// disposition changes here; this is evidence for a spore finding. -// -// wantDivergence stays true for every case: if one flips to false, the same -// grammar/scheduler defect has been fixed upstream for that shape. +// TestPythonSchedulerActionKnownGapCOracleParity pins the former +// pattern_list/expression_list election gap inside f-string interpolation. +// Both the bare tuple and the splat tuple must now match locked C before and +// after the result compatibility path. func TestPythonSchedulerActionKnownGapCOracleParity(t *testing.T) { goLang := grammars.PythonLanguage() cLang, err := COracleLanguage("python") @@ -146,19 +110,16 @@ func TestPythonSchedulerActionKnownGapCOracleParity(t *testing.T) { } tests := []struct { - name string - source string - wantDivergence bool + name string + source string }{ { - name: "fstring_interpolation_bare_tuple_uncovered", - source: "x = 1\ny = 2\nz = f\"{x, y}\"\n", - wantDivergence: true, + name: "fstring_interpolation_bare_tuple_uncovered", + source: "x = 1\ny = 2\nz = f\"{x, y}\"\n", }, { - name: "fstring_interpolation_splat_uncovered", - source: "xs = [1, 2]\nz = f\"{*xs,}\"\n", - wantDivergence: true, + name: "fstring_interpolation_splat_uncovered", + source: "xs = [1, 2]\nz = f\"{*xs,}\"\n", }, } @@ -189,18 +150,6 @@ func TestPythonSchedulerActionKnownGapCOracleParity(t *testing.T) { var mismatches []string compareNodes(rawTree.RootNode(), goLang, cTree.RootNode(), "root", &mismatches) - if test.wantDivergence { - if len(mismatches) == 0 { - t.Fatalf( - "expected %q to diverge from the C oracle, but the raw tree now matches; the "+ - "underlying scheduler-election defect may be fixed -- flip wantDivergence to "+ - "false and re-verify before treating dispatch.python as retirable for this shape", - test.name, - ) - } - t.Skipf("known scheduler-action gap, not covered by any dispatch.python sub-pass today:\n%s", strings.Join(mismatches, "\n")) - return - } if len(mismatches) != 0 { t.Fatalf("raw and C trees differ:\n%s", strings.Join(mismatches, "\n")) } @@ -251,6 +200,12 @@ func TestPythonSchedulerActionNeutralSubpassCOracleParity(t *testing.T) { {name: "for_target_tuple_negative_control", source: "pairs = [(1, 2)]\nfor a, b in pairs:\n pass\n"}, {name: "chained_assignment_lhs_negative_control", source: "a, b = c, d = 1, 2\n"}, {name: "del_tuple_negative_control", source: "a = 1\nb = 2\ndel a, b\n"}, + {name: "with_multiple_as_targets", source: "with context() as a, other() as b:\n pass\n"}, + {name: "except_multiple_as_targets", source: "try:\n pass\nexcept E as a, F as b:\n pass\n"}, + {name: "star_target_assignment", source: "first, *rest = seq\n"}, + {name: "fstring_call_arguments", source: "s = f\"{foo(a, b)}\"\n"}, + {name: "fstring_parenthesized_tuple", source: "s = f\"{(x, y)}\"\n"}, + {name: "fstring_conversion_format", source: "s = f\"{name!r:>{10}}\"\n"}, } for _, test := range tests { diff --git a/docs/root-normalization-retirement.md b/docs/root-normalization-retirement.md index 640ebbb8c..f6e6a0e00 100644 --- a/docs/root-normalization-retirement.md +++ b/docs/root-normalization-retirement.md @@ -1102,6 +1102,37 @@ Reopen retirement only after all of these conditions pass: Keep the registry entry unchanged until every condition passes. +## 2026-09-02 Python dispatcher certification update + +Status: `PARTIAL-GO`. The compact route matches locked C on all three current blocker witnesses. Keep `dispatch.python` live. + +Candidate base commit: `06afb3c881d4064bf367f970614e5120ec0abbfd`. +This update adds mixed physical graph-head merging and C-ordered clean-tie +selection for the exact Python grammar artifact. + +| Witness | Raw | Production | Compact | Incremental | Forest | +| --- | --- | --- | --- | --- | --- | +| `assignment_bare_tuple_positive` | `577a8b7b9281fa12c48dfa239a977c82f3a94e3d248253663c4a6fafc9121622` | `577a8b7b9281fa12c48dfa239a977c82f3a94e3d248253663c4a6fafc9121622` | `577a8b7b9281fa12c48dfa239a977c82f3a94e3d248253663c4a6fafc9121622` | `577a8b7b9281fa12c48dfa239a977c82f3a94e3d248253663c4a6fafc9121622` | `577a8b7b9281fa12c48dfa239a977c82f3a94e3d248253663c4a6fafc9121622` | +| `fstring_interpolation_bare_tuple_recovery_gap` | `84c987ddc73cc06bcc63e0cc860ecaa58560a46db882c775815ecb8867f95c07` | `84c987ddc73cc06bcc63e0cc860ecaa58560a46db882c775815ecb8867f95c07` | `84c987ddc73cc06bcc63e0cc860ecaa58560a46db882c775815ecb8867f95c07` | `84c987ddc73cc06bcc63e0cc860ecaa58560a46db882c775815ecb8867f95c07` | `84c987ddc73cc06bcc63e0cc860ecaa58560a46db882c775815ecb8867f95c07` | +| `fstring_interpolation_splat_recovery_gap` | `e646688923780dab15e472c1754d89e87ebfdb669fafeda109d4a2d630b4a4c9` | `e646688923780dab15e472c1754d89e87ebfdb669fafeda109d4a2d630b4a4c9` | `e646688923780dab15e472c1754d89e87ebfdb669fafeda109d4a2d630b4a4c9` | `e646688923780dab15e472c1754d89e87ebfdb669fafeda109d4a2d630b4a4c9` | `102ebedd10a3864a2640cb293f541e42f63b4f1ce3d60c9f219d7088b4f484c6` | + +The compact counters advance by `1/0` for each witness. No compact fallback occurs. +The forest splat result remains a `pattern_list` instead of C's `expression_list`. +This forest-only gap stays outside the compact route gate. + +Incremental parsing now preserves authenticated scanner reuse on all three +witnesses. Each route reports `reuse=true` and no unsupported reason. + +The generated Python corpus from the pinned grammar source also passes the A3 +sweep: `real=3`, `constructed=30`, `total=33`, with zero divergences. The +external corpus-source lock remains unavailable, so this is not a release +certification receipt. + +Keep the historical 2026-08-24 receipt below unchanged. + +Reopen the retirement review after the forest splat tie has a separate proof, +the authenticated corpus becomes available, and every route passes again. + ## 2026-08-24 Python dispatcher blocker receipt Status: `NO-GO`. Keep `dispatch.python` live. diff --git a/glr.go b/glr.go index d1b89936a..1c8e2c725 100644 --- a/glr.go +++ b/glr.go @@ -609,6 +609,24 @@ func (s *glrStack) ensureGSS(scratch *gssScratch) { } } +// ensureGSSForMergeStaging builds a temporary graph without topology hooks. +// Mixed-representation preflight may reject the pair, so staging must not +// publish a promotion for a stack that remains flat after the call. +func (s *glrStack) ensureGSSForMergeStaging(scratch *gssScratch) { + if s == nil || s.gss.head != nil || len(s.entries) == 0 { + return + } + var staged gssStack + for i, entry := range s.entries { + depth := uint32(i + 1) + if depth == 0 { + panic("glrStack.ensureGSSForMergeStaging: stack depth overflow") + } + staged.head = scratch.allocNode(entry, staged.head, depth) + } + s.gss = staged +} + // conflictForkBase promotes the live stack before it copies the fork base. // The original and each clone share one head until their first mutation. func (s *glrStack) conflictForkBase(scratch *gssScratch) glrStack { @@ -3546,6 +3564,28 @@ func gssMainCanMergeWithScratch(scratch *glrMergeScratch, a, b *glrStack) bool { gssNodeCleanZeroErrorAllLinksWithScratch(scratch, b.gss.head) } +// gssStackCleanZeroErrorAllLinksWithScratch applies the GSS clean-zero gate +// to either representation. The flat path scans entries without allocating +// staging nodes, so rejected mixed pairs do not consume GSS scratch capacity. +func gssStackCleanZeroErrorAllLinksWithScratch(scratch *glrMergeScratch, stack *glrStack) bool { + if stack == nil { + return false + } + if stack.gss.head != nil { + return gssNodeCleanZeroErrorAllLinksWithScratch(scratch, stack.gss.head) + } + if scratch != nil && scratch.provesNoChildErrors() { + return true + } + for _, entry := range stack.entries { + if stackEntryHasNode(entry) && + (stackEntryNodeHasError(entry) || stackEntryNodeIsMissing(entry) || stackEntryNodeSymbol(entry) == errorSymbol) { + return false + } + } + return true +} + func gssMainCanMergeWithScratchPhase(scratch *glrMergeScratch, a, b *glrStack, phase string) bool { if a.gss.head == nil || b.gss.head == nil { workCountRecordGSSReject(workCountParserFromMergeScratch(scratch), phase, workCountConvergenceReasonNotGSS, "GSS merge requires packed heads", a, b) @@ -5009,24 +5049,125 @@ func tryGSSMainMergeResult(scratch *glrMergeScratch, result []glrStack, idx int, } return false, false } + incumbentHeader := result[idx] + candidateHeader := *stack + // The physical GSS receiver can differ from the logical C survivor when + // the incumbent remains flat and the incoming candidate is already packed. + // Keep topology events in logical version order even when graph mutation + // uses the candidate as its receiver. + logicalTarget := &incumbentHeader + logicalCandidate := &candidateHeader + // Boundary merge candidates can arrive in different physical forms: one + // stack may still use contiguous entries while the other already owns a + // graph-structured stack (GSS). Check the mixed pair before staging it. + // A distinct-shape rejection must not mutate the flat survivor or publish a + // topology identity. + left, right := &result[idx], stack + var promoted glrStack + mixedRepresentation := false + candidateGSSReceiver := false + if scratch != nil && scratch.gssOwner != nil && + ((left.gss.head == nil) != (right.gss.head == nil)) { + mixedRepresentation = true + // Preserve the GSS gate order without allocating staging nodes. The + // score and recovery-cost gates ran above, so check the remaining + // status, position, and clean-zero conditions here. + if left.dead || right.dead || left.accepted != right.accepted { + if workCountInstrumentationEnabled { + workCountRecordGSSReject(workCountParserFromMergeScratch(scratch), workCountConvergencePhaseBoundaryGSS, workCountConvergenceReasonStatus, "GSS merge status differs", left, right) + } + if mergeCensusEnabled { + mergeCensusRecordGateRefusal(scratch, left, right) + } + return false, false + } + if left.shifted != right.shifted { + if workCountInstrumentationEnabled { + workCountRecordGSSScoreShiftReject(workCountParserFromMergeScratch(scratch), workCountConvergencePhaseBoundaryGSS, left, right) + } + if mergeCensusEnabled { + mergeCensusRecordGateRefusal(scratch, left, right) + } + return false, false + } + if left.top().state != right.top().state || left.byteOffset != right.byteOffset { + if workCountInstrumentationEnabled { + workCountRecordGSSReject(workCountParserFromMergeScratch(scratch), workCountConvergencePhaseBoundaryGSS, workCountConvergenceReasonStatus, "GSS merge state or byte differs", left, right) + } + if mergeCensusEnabled { + mergeCensusRecordGateRefusal(scratch, left, right) + } + return false, false + } + clean := gssStackCleanZeroErrorAllLinksWithScratch(scratch, left) && + gssStackCleanZeroErrorAllLinksWithScratch(scratch, right) + if workCountInstrumentationEnabled { + workCountRecordGSSCleanReject(workCountParserFromMergeScratch(scratch), workCountConvergencePhaseBoundaryGSS, left, right, clean) + } + if !clean { + if mergeCensusEnabled { + mergeCensusRecordGateRefusal(scratch, left, right) + } + return false, false + } + if !compactPackedGSSVersionOrderEnabledForMerge(scratch) && + (scratch == nil || scratch.perKeyCap != 1) && + gssStacksHaveDistinctMaterializingShapesWithScratch(scratch, left, right) { + if workCountInstrumentationEnabled { + workCountRecordGSSReject(workCountParserFromMergeScratch(scratch), workCountConvergencePhaseBoundaryEquivalence, workCountConvergenceReasonDistinctShape, "boundary merge retained distinct materializing shapes", left, right) + } + if mergeCensusEnabled { + mergeCensusRecordDistinctShapes() + } + return false, true + } + // Promote the flat side once. Tagged builds use topology hooks here; + // production builds use an unbound graph because no receipt is active. + if left.gss.head == nil { + promoted = *left + if workCountInstrumentationEnabled { + promoted.ensureGSS(scratch.gssOwner) + } else { + promoted.ensureGSSForMergeStaging(scratch.gssOwner) + } + promoted.entries = nil + promoted.cacheEntries = false + // The incoming GSS head is the parser's already-packed ownership + // when the incumbent still has a flat representation. Keep it as + // the merge receiver, matching C's version-head ownership. + left, right = right, &promoted + candidateGSSReceiver = true + } else { + promoted = *right + if workCountInstrumentationEnabled { + promoted.ensureGSS(scratch.gssOwner) + } else { + promoted.ensureGSSForMergeStaging(scratch.gssOwner) + } + promoted.entries = nil + promoted.cacheEntries = false + right = &promoted + } + } if workCountInstrumentationEnabled { - if !gssMainCanMergeWithScratchPhase(scratch, &result[idx], stack, workCountConvergencePhaseBoundaryGSS) { + if !gssMainCanMergeWithScratchPhase(scratch, left, right, workCountConvergencePhaseBoundaryGSS) { if mergeCensusEnabled { - mergeCensusRecordGateRefusal(scratch, &result[idx], stack) + mergeCensusRecordGateRefusal(scratch, left, right) } return false, false } - } else if !gssMainCanMergeWithScratch(scratch, &result[idx], stack) { + } else if !gssMainCanMergeWithScratch(scratch, left, right) { if mergeCensusEnabled { - mergeCensusRecordGateRefusal(scratch, &result[idx], stack) + mergeCensusRecordGateRefusal(scratch, left, right) } return false, false } - if !compactPackedGSSVersionOrderEnabledForMerge(scratch) && + if !mixedRepresentation && + !compactPackedGSSVersionOrderEnabledForMerge(scratch) && (scratch == nil || scratch.perKeyCap != 1) && - gssStacksHaveDistinctMaterializingShapesWithScratch(scratch, &result[idx], stack) { + gssStacksHaveDistinctMaterializingShapesWithScratch(scratch, left, right) { if workCountInstrumentationEnabled { - workCountRecordGSSReject(workCountParserFromMergeScratch(scratch), workCountConvergencePhaseBoundaryEquivalence, workCountConvergenceReasonDistinctShape, "boundary merge retained distinct materializing shapes", &result[idx], stack) + workCountRecordGSSReject(workCountParserFromMergeScratch(scratch), workCountConvergencePhaseBoundaryEquivalence, workCountConvergenceReasonDistinctShape, "boundary merge retained distinct materializing shapes", left, right) } if mergeCensusEnabled { mergeCensusRecordDistinctShapes() @@ -5034,23 +5175,51 @@ func tryGSSMainMergeResult(scratch *glrMergeScratch, result []glrStack, idx int, return false, true } if workCountInstrumentationEnabled { - workCountTopologyRecordMergeBeforeMutation(&result[idx], stack) // work-count-assembly: topology boundary-merge success seam + workCountTopologyRecordMergeBeforeMutation(logicalTarget, logicalCandidate) // work-count-assembly: topology boundary-merge success seam topologyRecorded = true - merged = workCountMergeGSSObserved(workCountParserFromMergeScratch(scratch), scratch, workCountConvergencePhaseBoundaryGSS, "boundary merge", &result[idx], stack) + detail := "boundary merge" + if mixedRepresentation { + detail = "boundary mixed-representation merge" + } + merged = workCountMergeGSSObserved(workCountParserFromMergeScratch(scratch), scratch, workCountConvergencePhaseBoundaryGSS, detail, left, right) workCountTopologyRequireMergeSuccess(merged) - workCountTopologyCommitMerge(stack) + if merged { + // Commit the logical candidate removal. The physical receiver may be + // the candidate GSS stack, but the incumbent version remains the result. + workCountTopologyCommitMerge(logicalCandidate) + // The caller drops this candidate after a successful merge. Clear its + // stack token too, so later cleanup cannot resolve a retired version. + workCountTopologyClearVersion(stack) + } } else { - merged = gssMainMergeWithScratch(scratch, &result[idx], stack) + merged = gssMainMergeWithScratch(scratch, left, right) } if merged { + if candidateGSSReceiver { + // The physical candidate supplied the graph receiver, but C keeps the + // incumbent stack metadata and version slot as the logical survivor. + result[idx] = incumbentHeader + result[idx].gss = left.gss + // Keep one authoritative physical representation. The incoming GSS + // stack may retain a mirror entry cache, but it belongs to the absorbed + // producer path rather than the incumbent logical survivor. + result[idx].entries = nil + result[idx].cacheEntries = false + result[idx].byteOffset = left.byteOffset + result[idx].invalidateCEntryAgg() + result[idx].cEverErrored = incumbentHeader.cEverErrored || candidateHeader.cEverErrored + if workCountInstrumentationEnabled { + // Rebind the surviving logical version to the physical graph receiver. + // The merge event uses the flat incumbent header before mutation. + workCountTopologyCommitVersion(&result[idx]) + } + } else { + result[idx].cEverErrored = incumbentHeader.cEverErrored || candidateHeader.cEverErrored + } workCountRecordMergeSuccess() if mergeCensusEnabled { mergeCensusRecordSuccess() } - // result[idx] survives and absorbs stack, so OR the sticky wreckage bit: - // a clean survivor that merges a recovered-wreckage lineage must inherit - // its error history (see glrStack.cEverErrored / tryGSSMainMergeForParser). - result[idx].cEverErrored = result[idx].cEverErrored || stack.cEverErrored if scratch != nil { // A successful main merge can rewrite link 0 (prev/entry) of surviving // nodes (setGSSMainLink), so every cached spine prefix may be stale. @@ -5209,7 +5378,8 @@ func mergeStacksSmallForLanguage(alive []glrStack, scratch *glrMergeScratch, lan if mergeKeyForStack(&result[j]) != key { continue } - if merged, attempted := tryGSSMainMergeResult(scratch, result, j, &stack); attempted { + merged, attempted := tryGSSMainMergeResult(scratch, result, j, &stack) + if attempted { if merged { traceCRecoverMergeDecision(scratch, "small", "gss-merged", &result[j], &stack) duplicateIndex = j diff --git a/glr_test.go b/glr_test.go index 4648fc87b..d7a2992e5 100644 --- a/glr_test.go +++ b/glr_test.go @@ -3268,6 +3268,143 @@ func TestTryGSSMainMergeResultClearsMaterializingCache(t *testing.T) { } } +func mixedGSSMergeProducerFixture(node *Node) (packed, flat glrStack, owner gssScratch) { + packedEntries := []stackEntry{{state: 2}, newStackEntryNode(7, node)} + flatEntries := []stackEntry{{state: 1}, newStackEntryNode(7, node)} + packed = glrStack{ + gss: buildGSSStack(packedEntries, &owner), + byteOffset: 5, + } + flat = glrStack{ + entries: flatEntries, + byteOffset: 5, + } + return packed, flat, owner +} + +func TestTryGSSMainMergeResultMixedKeepsProducerPathsAndIncumbentOrder(t *testing.T) { + node := NewLeafNode(11, true, 0, 5, Point{}, Point{Column: 5}) + packed, flat, owner := mixedGSSMergeProducerFixture(node) + packed.branchOrder = 22 + flat.branchOrder = 11 + packedHead := packed.gss.head + + scratch := glrMergeScratch{gssOwner: &owner} + result := []glrStack{flat} + merged, attempted := tryGSSMainMergeResult(&scratch, result, 0, &packed) + if !attempted || !merged { + t.Fatalf("mixed producer merge attempted=%v merged=%v, want true/true", attempted, merged) + } + if result[0].gss.head != packedHead || result[0].entries != nil { + t.Fatalf("mixed merge receiver = head:%p entries:%d, want packed candidate head and no flat entries", result[0].gss.head, len(result[0].entries)) + } + if got := result[0].branchOrder; got != flat.branchOrder { + t.Fatalf("mixed logical survivor branch order=%d, want incumbent order=%d", got, flat.branchOrder) + } + if got := result[0].gss.head.linkCount(); got != 2 { + t.Fatalf("mixed producer link count=%d, want two distinct paths", got) + } + seenStates := make(map[StateID]bool, result[0].gss.head.linkCount()) + for i := 0; i < result[0].gss.head.linkCount(); i++ { + prev, _ := result[0].gss.head.link(i) + if prev == nil { + t.Fatalf("mixed producer link %d has no predecessor", i) + } + seenStates[prev.entry.state] = true + } + if !seenStates[StateID(1)] || !seenStates[StateID(2)] { + t.Fatalf("mixed producer predecessor states=%v, want states 1 and 2", seenStates) + } +} + +func TestTryGSSMainMergeResultMixedDistinctShapesRejectWithoutMutation(t *testing.T) { + arena := newNodeArena(arenaClassFull) + parser := &Parser{} + makeShape := func(childSymbol Symbol) *Node { + child := NewLeafNode(childSymbol, true, 0, 5, Point{}, Point{Column: 5}) + parent := NewParentNode(300, true, []*Node{child}, nil, 0) + parent.parseState = 7 + parent.rawShape = parser.captureRawShape( + nil, + arena, + parent.symbol, + parent.productionID, + []stackEntry{newStackEntryNode(parent.parseState, child)}, + 0, + 1, + ) + return parent + } + incumbentNode := makeShape(11) + candidateNode := makeShape(12) + packed, flat, owner := mixedGSSMergeProducerFixture(candidateNode) + flat.entries[1] = newStackEntryNode(7, incumbentNode) + lang := &Language{ExactStackNodeEquivalenceCertified: true} + scratch := glrMergeScratch{gssOwner: &owner, language: lang, arena: arena} + scratch.beginEquivEpoch() + if !gssStacksHaveDistinctMaterializingShapesWithScratch(&scratch, &flat, &packed) { + t.Fatal("test setup did not produce distinct materializing shapes") + } + incumbentEntries := append([]stackEntry(nil), flat.entries...) + packedHead := packed.gss.head + packedLinks := packedHead.linkCount() + result := []glrStack{flat} + merged, attempted := tryGSSMainMergeResult(&scratch, result, 0, &packed) + if merged || !attempted { + t.Fatalf("distinct mixed shapes merge=%v attempted=%v, want false/true", merged, attempted) + } + if result[0].gss.head != nil || len(result[0].entries) != len(incumbentEntries) { + t.Fatalf("flat incumbent representation changed: head=%p entries=%d", result[0].gss.head, len(result[0].entries)) + } + for i := range incumbentEntries { + if result[0].entries[i] != incumbentEntries[i] { + t.Fatalf("flat incumbent entry %d changed", i) + } + } + if packed.gss.head != packedHead || packedHead.linkCount() != packedLinks { + t.Fatal("packed candidate changed after distinct-shape rejection") + } +} + +func TestTryGSSMainMergeResultMixedORsEverErroredBothDirections(t *testing.T) { + node := NewLeafNode(11, true, 0, 5, Point{}, Point{Column: 5}) + tests := []struct { + name string + resultPacked bool + incumbentErrored bool + candidateErrored bool + }{ + {name: "flat-incumbent-candidate-history", incumbentErrored: false, candidateErrored: true}, + {name: "flat-incumbent-history", incumbentErrored: true, candidateErrored: false}, + {name: "packed-incumbent-candidate-history", resultPacked: true, incumbentErrored: false, candidateErrored: true}, + {name: "packed-incumbent-history", resultPacked: true, incumbentErrored: true, candidateErrored: false}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + packed, flat, owner := mixedGSSMergeProducerFixture(node) + packed.cEverErrored = test.resultPacked && test.incumbentErrored || !test.resultPacked && test.candidateErrored + flat.cEverErrored = test.resultPacked && test.candidateErrored || !test.resultPacked && test.incumbentErrored + var result []glrStack + var candidate *glrStack + if test.resultPacked { + result = []glrStack{packed} + candidate = &flat + } else { + result = []glrStack{flat} + candidate = &packed + } + scratch := glrMergeScratch{gssOwner: &owner} + merged, attempted := tryGSSMainMergeResult(&scratch, result, 0, candidate) + if !attempted || !merged { + t.Fatalf("mixed merge attempted=%v merged=%v, want true/true", attempted, merged) + } + if !result[0].cEverErrored { + t.Fatal("mixed merge lost the sticky error history") + } + }) + } +} + func TestCertifiedMaterializingShapeHashIncludesRawDescendants(t *testing.T) { arena := newNodeArena(arenaClassFull) parser := &Parser{} diff --git a/grammars/runtime_profiles.go b/grammars/runtime_profiles.go index 7b97daae1..15b8a485d 100644 --- a/grammars/runtime_profiles.go +++ b/grammars/runtime_profiles.go @@ -351,16 +351,19 @@ var builtinLanguageRuntimeProfiles = map[string]builtinLanguageRuntimeProfile{ SkipCompleteAcceptedErrorRetry: true, }, }, - // Python's tied tuple-assignment election matches the C oracle once the - // compact route selects the sole primary derivation. Full-corpus - // field-aware C-oracle verification certifies this mechanism for this - // exact blob, alongside the existing converged-path split-drop - // certification (A3 certification workstream, spec.campaign.v7). + // Python's tuple-assignment and f-string interpolation ties match the C + // oracle when the compact route applies C's raw subtree ordering. The + // selection is clean-only and has no recovery authority. Full-corpus, + // field-aware C-oracle verification certifies this exact blob for the + // structural election and the existing converged-path split-drop and + // primary-acceptance mechanisms (A3 certification workstream, + // spec.campaign.v7). "python": { - blobSHA256: mustRuntimeProfileSHA256("cde4a67dc6af6e1232dbbd1eab8618478d1d73727020e8a8002542390a452d37"), - externalScannerFullParseRetry: gotreesitter.ExternalScannerFullParseRetrySkipRepeat, - compactConvergedSplitDrops: true, - compactPrimaryAcceptDerivation: true, + blobSHA256: mustRuntimeProfileSHA256("cde4a67dc6af6e1232dbbd1eab8618478d1d73727020e8a8002542390a452d37"), + externalScannerFullParseRetry: gotreesitter.ExternalScannerFullParseRetrySkipRepeat, + compactConvergedSplitDrops: true, + compactPrimaryAcceptDerivation: true, + compactAcceptanceStructuralElection: true, }, // Perl's tied push-list election matches the C oracle once the compact // route accepts after a converged-path split drop and selects the sole diff --git a/grammars/runtime_profiles_test.go b/grammars/runtime_profiles_test.go index ee86c9f7d..a24ca6404 100644 --- a/grammars/runtime_profiles_test.go +++ b/grammars/runtime_profiles_test.go @@ -363,14 +363,16 @@ func TestBuiltinCompactAcceptanceProfilesRequireExactBlobIdentity(t *testing.T) // spec.campaign.v7, finding // tied-election-family-compact-retirement): full-corpus field-aware // C-oracle verification certifies primary-acceptance-derivation - // selection for all five languages. Kotlin's grant lands under + // selection for all five languages. Python also certifies C's raw + // subtree ordering for clean ties. Kotlin's grant lands under // selectCompactAcceptanceDerivation's materiality gate // (parsercore_phase0_driver.go, compactAcceptanceElectionIsVacuous); // see the runtime_profiles.go "kotlin" entry comment. { name: "python", load: PythonLanguage, want: func(lang *gotreesitter.Language) bool { - return lang.CompactPrimaryAcceptanceDerivationCertified + return lang.CompactPrimaryAcceptanceDerivationCertified && + lang.CompactAcceptanceStructuralElectionCertified }, }, { diff --git a/parser_result_python.go b/parser_result_python.go index 1001386c7..d3dfbe62b 100644 --- a/parser_result_python.go +++ b/parser_result_python.go @@ -783,43 +783,13 @@ func normalizePythonInterpolationPatterns(root *Node, lang *Language) normalizat if root == nil || lang == nil || lang.Name != "python" { return counters } - patternListSym, ok := symbolByName(lang, "pattern_list") - if !ok { - return counters - } - listSplatPatternSym, hasListSplatPattern := symbolByName(lang, "list_splat_pattern") - expressionListSym, hasExpressionList := symbolByName(lang, "expression_list") - listSplatSym, hasListSplat := symbolByName(lang, "list_splat") - - patternListNamed := symbolIsNamed(lang, patternListSym) - listSplatPatternNamed := hasListSplatPattern && symbolIsNamed(lang, listSplatPatternSym) - - var rewrite func(*Node, bool) - rewrite = func(n *Node, inInterpolation bool) { - if n == nil { - return - } + // The parser now elects the C-owned interpolation production while the + // reduction still has both physical heads. A post-build rewrite would undo + // that producer decision, so keep this source-gated receipt as an observed + // no-op until the dispatch.python arm is retired. + walkResultTree(root, func(*Node) { counters.nodesVisited++ - here := inInterpolation || n.Type(lang) == "interpolation" - if here { - if hasExpressionList && n.symbol == expressionListSym { - n.symbol = patternListSym - n.setNamed(patternListNamed) - counters.nodesRewritten++ - } - if hasListSplatPattern && hasListSplat && n.symbol == listSplatSym { - n.symbol = listSplatPatternSym - n.setNamed(listSplatPatternNamed) - counters.nodesRewritten++ - } - } - childCount := resultChildCount(n) - for i := 0; i < childCount; i++ { - child := resultChildAt(n, i) - rewrite(child, here) - } - } - rewrite(root, false) + }) return counters } diff --git a/work_count_hooks.go b/work_count_hooks.go index fe47c949b..ae22386d9 100644 --- a/work_count_hooks.go +++ b/work_count_hooks.go @@ -106,6 +106,7 @@ func workCountTopologyRecordChildElection(*glrStack, reduceFork, reduceFork, int func workCountTopologyRecordMerge(*glrStack, *glrStack, bool) {} func workCountTopologyRecordMergeBeforeMutation(*glrStack, *glrStack) {} func workCountTopologyCommitMerge(*glrStack) {} +func workCountTopologyClearVersion(*glrStack) {} func workCountTopologyRequireMergeSuccess(bool) {} func workCountTopologyRetireVersion(*glrStack) {} func workCountTopologyRetireVersionIfActive(*glrStack) {} diff --git a/work_count_topology.go b/work_count_topology.go index 9ecfe2ad0..01b93e079 100644 --- a/work_count_topology.go +++ b/work_count_topology.go @@ -1935,6 +1935,12 @@ func workCountTopologyCommitMerge(candidate *glrStack) { candidate.diagnosticTopology.versionID = 0 } +func workCountTopologyClearVersion(stack *glrStack) { + if stack != nil { + stack.diagnosticTopology.versionID = 0 + } +} + func workCountTopologyRequireMergeSuccess(merged bool) { if s := activeDiagnosticTopology; s != nil && !merged { s.receipt.IdentityIncomplete = true diff --git a/work_count_topology_internal_test.go b/work_count_topology_internal_test.go index 3ba6943a6..26e25552e 100644 --- a/work_count_topology_internal_test.go +++ b/work_count_topology_internal_test.go @@ -3,6 +3,7 @@ package gotreesitter import ( + "strings" "testing" "unsafe" ) @@ -30,6 +31,212 @@ func TestDiagnosticTopologyReceiptBoundsChronologicalPrefix(t *testing.T) { } } +func TestDiagnosticTopologyMixedBoundaryMergeKeepsLogicalSurvivor(t *testing.T) { + BeginDiagnosticWorkCount() + BeginDiagnosticTopologyReceipt() + token := workCountBeginParseAttempt(6, 1024, 6) + if token == 0 { + t.Fatal("zero parse-attempt token") + } + + node := NewLeafNode(11, true, 0, 5, Point{}, Point{Column: 5}) + entries := []stackEntry{{state: 1}, newStackEntryNode(7, node)} + incumbent := glrStack{entries: append([]stackEntry(nil), entries...), byteOffset: 5} + candidate := glrStack{entries: append([]stackEntry(nil), entries...), byteOffset: 5} + workCountTopologyRecordInitialVersion(&incumbent) + workCountTopologyRecordVersionCopy(&incumbent, &candidate) + incumbentID := incumbent.diagnosticTopology.versionID + candidateID := candidate.diagnosticTopology.versionID + if incumbentID == 0 || candidateID == 0 || incumbentID >= candidateID { + t.Fatalf("logical version IDs = %d/%d, want incumbent before candidate", incumbentID, candidateID) + } + + var owner gssScratch + candidate.ensureGSS(&owner) + candidate.entries = nil + candidate.cacheEntries = false + if candidate.gss.head == nil || candidate.entries != nil { + t.Fatalf("candidate did not enter the packed representation: head=%p entries=%d", candidate.gss.head, len(candidate.entries)) + } + ownerUsedBefore := owner.usedTotal + graphLinksBefore := activeDiagnosticWorkCount.GraphLinkAdditionsProxy + + scratch := glrMergeScratch{gssOwner: &owner} + scratch.beginEquivEpoch() + result := []glrStack{incumbent} + merged, attempted := tryGSSMainMergeResult(&scratch, result, 0, &candidate) + if !attempted || !merged { + t.Fatalf("mixed boundary merge attempted=%v merged=%v, want true/true", attempted, merged) + } + if got := result[0].diagnosticTopology.versionID; got != incumbentID { + t.Fatalf("physical receiver replaced logical survivor: result version=%d, want %d", got, incumbentID) + } + if candidate.diagnosticTopology.versionID != 0 { + t.Fatalf("absorbed candidate retained a retired stack token: version=%d", candidate.diagnosticTopology.versionID) + } + if _, ok := activeDiagnosticTopology.versions[candidateID]; ok { + t.Fatalf("absorbed candidate version %d remains active", candidateID) + } + if _, ok := activeDiagnosticTopology.versions[incumbentID]; !ok { + t.Fatalf("logical incumbent version %d was retired", incumbentID) + } + if got := activeDiagnosticTopology.versions[incumbentID].head; got != result[0].gss.head { + t.Fatalf("logical incumbent version head=%p, want physical receiver=%p", got, result[0].gss.head) + } + if got, want := owner.usedTotal, ownerUsedBefore+len(entries); got != want { + t.Fatalf("successful mixed promotion used=%d GSS nodes, want one flat-depth allocation: %d", got, want) + } + + workCountResolveParseAttempt(token, 6, false, 6, 6, 1024, 1024) + workCountBeginFinalizeParseAttempt(token) + workCountEndFinalizeParseAttempt(token, ParseStopAccepted, nil) + counts := EndDiagnosticWorkCount() + receipt := EndDiagnosticTopologyReceipt() + if !receipt.Complete() { + t.Fatalf("mixed boundary topology receipt is incomplete: %+v", receipt) + } + var mergeEvent *DiagnosticTopologyEvent + for i := range receipt.Events { + event := &receipt.Events[i] + if event.Kind == DiagnosticTopologyEventMerge { + mergeEvent = event + break + } + } + if mergeEvent == nil || mergeEvent.SourceVersionID != incumbentID || mergeEvent.TargetVersionID != candidateID || mergeEvent.RemovedVersionID != candidateID || mergeEvent.SurvivorVersionID != incumbentID { + t.Fatalf("mixed boundary logical merge event = %+v, want incumbent %d absorbing candidate %d", mergeEvent, incumbentID, candidateID) + } + foundMixedTelemetry := false + for _, event := range counts.Convergence.Events { + if strings.Contains(event.Detail, "boundary mixed-representation merge") && event.Outcome == workCountConvergenceOutcomePacked { + foundMixedTelemetry = true + break + } + } + if !foundMixedTelemetry { + t.Fatalf("mixed boundary convergence telemetry missing: %+v", counts.Convergence.Events) + } + if got, want := counts.GraphLinkAdditionsProxy-graphLinksBefore, uint64(1); got != want { + t.Fatalf("successful mixed promotion graph-link work=%d, want one promotion link", got) + } +} + +func TestDiagnosticTopologyMixedDistinctShapeRejectDoesNotPromote(t *testing.T) { + BeginDiagnosticWorkCount() + BeginDiagnosticTopologyReceipt() + finished := false + defer func() { + if finished { + return + } + if activeDiagnosticWorkCount != nil { + workCountResolveParseAttempt(1, 2, false, 2, 2, 1, 1024) + workCountBeginFinalizeParseAttempt(1) + workCountEndFinalizeParseAttempt(1, ParseStopAccepted, nil) + _ = EndDiagnosticWorkCount() + } + if activeDiagnosticTopology != nil { + _ = EndDiagnosticTopologyReceipt() + } + }() + token := workCountBeginParseAttempt(2, 1024, 2) + if token == 0 { + t.Fatal("zero parse-attempt token") + } + + arena := newNodeArena(arenaClassFull) + parser := &Parser{} + makeShape := func(childSymbol Symbol) *Node { + child := NewLeafNode(childSymbol, true, 0, 5, Point{}, Point{Column: 5}) + parent := NewParentNode(300, true, []*Node{child}, nil, 0) + parent.parseState = 7 + parent.rawShape = parser.captureRawShape( + nil, + arena, + parent.symbol, + parent.productionID, + []stackEntry{newStackEntryNode(parent.parseState, child)}, + 0, + 1, + ) + return parent + } + incumbentNode := makeShape(11) + candidateNode := makeShape(12) + incumbentEntries := []stackEntry{{state: 1}, newStackEntryNode(7, incumbentNode)} + candidateEntries := []stackEntry{{state: 2}, newStackEntryNode(7, candidateNode)} + incumbent := glrStack{entries: incumbentEntries, byteOffset: 5, branchOrder: 11} + candidate := glrStack{entries: candidateEntries, byteOffset: 5, branchOrder: 22} + workCountTopologyRecordInitialVersion(&incumbent) + workCountTopologyRecordVersionCopy(&incumbent, &candidate) + incumbentID := incumbent.diagnosticTopology.versionID + candidateID := candidate.diagnosticTopology.versionID + if incumbentID == 0 || candidateID == 0 { + t.Fatalf("logical version IDs = %d/%d, want nonzero IDs", incumbentID, candidateID) + } + + var owner gssScratch + candidate.ensureGSS(&owner) + candidate.entries = nil + candidate.cacheEntries = false + candidateHead := candidate.gss.head + if candidateHead == nil { + t.Fatal("candidate did not enter the packed representation") + } + nodeCountBefore := len(activeDiagnosticTopology.nodeIDs) + ownerUsedBefore := owner.usedTotal + versionHeadBefore := activeDiagnosticTopology.versions[candidateID].head + promotionBefore := activeDiagnosticTopology.promotion + if promotionBefore.versionID != 0 { + t.Fatalf("candidate promotion remained staged: %+v", promotionBefore) + } + + lang := &Language{ExactStackNodeEquivalenceCertified: true} + scratch := glrMergeScratch{gssOwner: &owner, language: lang, arena: arena} + scratch.beginEquivEpoch() + result := []glrStack{incumbent} + merged, attempted := tryGSSMainMergeResult(&scratch, result, 0, &candidate) + if merged || !attempted { + t.Fatalf("distinct mixed shapes merge=%v attempted=%v, want false/true", merged, attempted) + } + if result[0].gss.head != nil || len(result[0].entries) != len(incumbentEntries) { + t.Fatalf("flat incumbent representation changed: head=%p entries=%d", result[0].gss.head, len(result[0].entries)) + } + for i := range incumbentEntries { + if result[0].entries[i] != incumbentEntries[i] { + t.Fatalf("flat incumbent entry %d changed", i) + } + } + if candidate.gss.head != candidateHead || candidateHead.linkCount() != 1 { + t.Fatalf("packed candidate changed: head=%p links=%d", candidate.gss.head, candidateHead.linkCount()) + } + if owner.usedTotal != ownerUsedBefore { + t.Fatalf("rejected mixed staging consumed GSS scratch nodes: used=%d/%d", owner.usedTotal, ownerUsedBefore) + } + if len(activeDiagnosticTopology.nodeIDs) != nodeCountBefore || activeDiagnosticTopology.promotion.versionID != 0 { + t.Fatalf("rejected mixed staging changed topology allocation state: nodes=%d/%d promotion=%+v", len(activeDiagnosticTopology.nodeIDs), nodeCountBefore, activeDiagnosticTopology.promotion) + } + if activeDiagnosticTopology.versions[candidateID].head != versionHeadBefore { + t.Fatal("rejected mixed staging rebound the candidate version head") + } + if incumbent.diagnosticTopology.versionID != incumbentID || candidate.diagnosticTopology.versionID != candidateID || result[0].diagnosticTopology.versionID != incumbentID { + t.Fatalf("rejected mixed staging changed version identities: incumbent=%d candidate=%d result=%d", incumbent.diagnosticTopology.versionID, candidate.diagnosticTopology.versionID, result[0].diagnosticTopology.versionID) + } + if activeDiagnosticTopology.receipt.IdentityIncomplete || activeDiagnosticTopology.receipt.IdentityCollision { + t.Fatalf("rejected mixed staging damaged topology identity: incomplete=%v collision=%v", activeDiagnosticTopology.receipt.IdentityIncomplete, activeDiagnosticTopology.receipt.IdentityCollision) + } + + workCountResolveParseAttempt(token, 2, false, 2, 2, 1, 1024) + workCountBeginFinalizeParseAttempt(token) + workCountEndFinalizeParseAttempt(token, ParseStopAccepted, nil) + _ = EndDiagnosticWorkCount() + receipt := EndDiagnosticTopologyReceipt() + finished = true + if !receipt.Complete() { + t.Fatalf("distinct-shape rejection receipt is incomplete: %+v", receipt) + } +} + func TestDiagnosticTopologyReceiptNodeAllocationRefreshesReusedPointer(t *testing.T) { BeginDiagnosticTopologyReceipt() predecessor := &gssNode{} From 773d50bc5cd6ccdb57b64db1b85496be4da6d04d Mon Sep 17 00:00:00 2001 From: Oscar Villavicencio <9220505+odvcencio@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:19:25 -0700 Subject: [PATCH 2/4] add(glr): Gate mixed flat/GSS merge behind artifact certification - Add Language.CompactMixedGSSMergeCertified. A mixed boundary merge in tryGSSMainMergeResult now runs only for a certified grammar artifact, so uncertified grammars keep one stack representation instead of adopting C's physical receiver ownership. - Certify python for the mixed merge in its exact built-in runtime profile, and require the flag there under the locked blob hash. - Restore the meson case in TestAdmissionCandidateCertifiedAcceptanceFrontiers. The narrower gate makes its compact tree match the historical production tree again. - Keep a nil language on the current merge route so profiles without a language object behave as before. Buckley-Change-Hash: sha256:4124c9fa9104c3f55ae6ff281ab1117e07c13d19f7104c9a56732906eafcf816 Buckley-Change-Stats: files=5 insertions=21 deletions=7 binaries=0 --- admission_switch_acceptance_frontier_test.go | 5 +---- glr.go | 4 +++- grammars/runtime_profiles.go | 9 ++++++++- grammars/runtime_profiles_test.go | 3 ++- language.go | 7 +++++++ 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/admission_switch_acceptance_frontier_test.go b/admission_switch_acceptance_frontier_test.go index 6ec0e5429..68a00431f 100644 --- a/admission_switch_acceptance_frontier_test.go +++ b/admission_switch_acceptance_frontier_test.go @@ -12,10 +12,7 @@ import ( ) func TestAdmissionCandidateCertifiedAcceptanceFrontiers(t *testing.T) { - // Meson has an artifact-certified C structural election. Its compact tree - // intentionally differs from Go's historical production tree and is - // covered by the locked-C parity test in cgo_harness. - for _, name := range []string{"http", "robot"} { + for _, name := range []string{"http", "meson", "robot"} { t.Run(name, func(t *testing.T) { entry := grammars.DetectLanguageByName(name) if entry == nil { diff --git a/glr.go b/glr.go index 1c8e2c725..838e09c32 100644 --- a/glr.go +++ b/glr.go @@ -5066,7 +5066,9 @@ func tryGSSMainMergeResult(scratch *glrMergeScratch, result []glrStack, idx int, var promoted glrStack mixedRepresentation := false candidateGSSReceiver := false - if scratch != nil && scratch.gssOwner != nil && + mixedMergeCertified := scratch != nil && scratch.gssOwner != nil && + (scratch.language == nil || scratch.language.CompactMixedGSSMergeCertified) + if mixedMergeCertified && ((left.gss.head == nil) != (right.gss.head == nil)) { mixedRepresentation = true // Preserve the GSS gate order without allocating staging nodes. The diff --git a/grammars/runtime_profiles.go b/grammars/runtime_profiles.go index 15b8a485d..f1e256d78 100644 --- a/grammars/runtime_profiles.go +++ b/grammars/runtime_profiles.go @@ -25,6 +25,7 @@ type builtinLanguageRuntimeProfile struct { compactEOFAcceptNoActionSiblings bool compactPrimaryAcceptDerivation bool compactAcceptanceStructuralElection bool + compactMixedGSSMerge bool compactLexerSkippedPrefixTiling bool exactStackNodeEquivalence bool compactPackedGSSVersionOrder bool @@ -357,13 +358,15 @@ var builtinLanguageRuntimeProfiles = map[string]builtinLanguageRuntimeProfile{ // field-aware C-oracle verification certifies this exact blob for the // structural election and the existing converged-path split-drop and // primary-acceptance mechanisms (A3 certification workstream, - // spec.campaign.v7). + // spec.campaign.v7). The mixed flat/GSS receiver path is also certified + // for this exact artifact. "python": { blobSHA256: mustRuntimeProfileSHA256("cde4a67dc6af6e1232dbbd1eab8618478d1d73727020e8a8002542390a452d37"), externalScannerFullParseRetry: gotreesitter.ExternalScannerFullParseRetrySkipRepeat, compactConvergedSplitDrops: true, compactPrimaryAcceptDerivation: true, compactAcceptanceStructuralElection: true, + compactMixedGSSMerge: true, }, // Perl's tied push-list election matches the C oracle once the compact // route accepts after a converged-path split drop and selects the sole @@ -751,6 +754,10 @@ func attachBuiltinLanguageRuntimeProfile(name string, blobSHA256 [32]byte, lang lang.CompactAcceptanceStructuralElectionCertified = true changed = true } + if profile.compactMixedGSSMerge && !lang.CompactMixedGSSMergeCertified { + lang.CompactMixedGSSMergeCertified = true + changed = true + } if profile.compactLexerSkippedPrefixTiling && !lang.CompactLexerSkippedPrefixTilingCertified { lang.CompactLexerSkippedPrefixTilingCertified = true changed = true diff --git a/grammars/runtime_profiles_test.go b/grammars/runtime_profiles_test.go index a24ca6404..b06ce03c3 100644 --- a/grammars/runtime_profiles_test.go +++ b/grammars/runtime_profiles_test.go @@ -372,7 +372,8 @@ func TestBuiltinCompactAcceptanceProfilesRequireExactBlobIdentity(t *testing.T) name: "python", load: PythonLanguage, want: func(lang *gotreesitter.Language) bool { return lang.CompactPrimaryAcceptanceDerivationCertified && - lang.CompactAcceptanceStructuralElectionCertified + lang.CompactAcceptanceStructuralElectionCertified && + lang.CompactMixedGSSMergeCertified }, }, { diff --git a/language.go b/language.go index 4ffac9796..4345a8594 100644 --- a/language.go +++ b/language.go @@ -830,6 +830,13 @@ type Language struct { // Custom, adapted, and stale artifacts retain the false default. CompactAcceptanceStructuralElectionCertified bool + // CompactMixedGSSMergeCertified permits one boundary merge to join flat + // and graph-structured stack forms with C's physical receiver ownership. + // Exact built-in profiles set this only after locked C parity proves the + // mixed representation path for that grammar artifact. Custom, adapted, + // and stale artifacts retain the false default. + CompactMixedGSSMergeCertified bool + // CompactLexerSkippedPrefixTilingCertified permits an internal compact // reduction gap when the next accepted terminal carries exact DFA evidence // for the complete skipped prefix. Exact built-in profiles set this only From d97c5d202538300ce26ec51db85f953f1f9af9bb Mon Sep 17 00:00:00 2001 From: Oscar Villavicencio <9220505+odvcencio@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:20:30 -0700 Subject: [PATCH 3/4] test(glr): Certify compact mixed GSS merge in distinct-shapes test - Set CompactMixedGSSMergeCertified on the Language fixture in TestTryGSSMainMergeResultMixedDistinctShapesRejectWithoutMutation. - Match the language flags that the mixed GSS merge path checks before it selects the compact representation. - Keep the assertions unchanged: the distinct-shape check must still reject the merge and must not mutate the inputs. Buckley-Change-Hash: sha256:9c5cd648647a5b5fc413177375bf539015089e20fffa69e6d8b31cfe2b804433 Buckley-Change-Stats: files=1 insertions=4 deletions=1 binaries=0 --- glr_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/glr_test.go b/glr_test.go index d7a2992e5..2bfb04190 100644 --- a/glr_test.go +++ b/glr_test.go @@ -3339,7 +3339,10 @@ func TestTryGSSMainMergeResultMixedDistinctShapesRejectWithoutMutation(t *testin candidateNode := makeShape(12) packed, flat, owner := mixedGSSMergeProducerFixture(candidateNode) flat.entries[1] = newStackEntryNode(7, incumbentNode) - lang := &Language{ExactStackNodeEquivalenceCertified: true} + lang := &Language{ + ExactStackNodeEquivalenceCertified: true, + CompactMixedGSSMergeCertified: true, + } scratch := glrMergeScratch{gssOwner: &owner, language: lang, arena: arena} scratch.beginEquivEpoch() if !gssStacksHaveDistinctMaterializingShapesWithScratch(&scratch, &flat, &packed) { From 4c3b89f15bf7f0fc3cab5ad73e562e6fb35b61b1 Mon Sep 17 00:00:00 2001 From: Oscar Villavicencio <9220505+odvcencio@users.noreply.github.com> Date: Wed, 2 Sep 2026 02:15:26 -0700 Subject: [PATCH 4/4] add(glr): Add mixed-representation merge counters to census - Count certified flat/GSS representation joins in MergeEventCensusCounts. The C runtime has no version merge for these joins, so they no longer share the lane's Successes counter. - Record the attempt in tryGSSMainMergeResult before the remaining gate checks, and record the success on the merged-path side. This separates representation joins from ordinary main merges. - Stop recording a distinct-shapes refusal for that rejection branch, because the census now classifies that event on its own. - Report RefuseNoGSSHead and RefuseNoGSSHeadOne for each mixed attempt, so the packed-head baseline stays a gate observation. - Add the matching no-op hooks for the disabled census build tag. - Sum the new counters in the cgo_harness census totals and print them in the totals line. - Update the test to cover both new counters, and refresh the pinned baselines: python sources 26 to 30, and the constructed-source denominator 104 to 108. Buckley-Change-Hash: sha256:d92b1482343939da9cc2ffa58b50dcd9b4e0d0e08d647945f77d6a1b062afe13 Buckley-Change-Stats: files=5 insertions=50 deletions=13 binaries=0 --- cgo_harness/merge_event_census.go | 8 ++++++-- cgo_harness/merge_event_census_test.go | 19 +++++++++++++------ glr.go | 12 ++++++++---- merge_event_census.go | 22 +++++++++++++++++++++- merge_event_census_disabled.go | 2 ++ 5 files changed, 50 insertions(+), 13 deletions(-) diff --git a/cgo_harness/merge_event_census.go b/cgo_harness/merge_event_census.go index a8e1373b0..c6823281d 100644 --- a/cgo_harness/merge_event_census.go +++ b/cgo_harness/merge_event_census.go @@ -649,8 +649,10 @@ type mergeCensusTotals struct { CLinkUnionAppended, CLinkUnionRejected uint64 - GoAttempts uint64 - GoSuccesses uint64 + GoAttempts uint64 + GoSuccesses uint64 + MixedRepresentationAttempts uint64 + MixedRepresentationSuccesses uint64 RefuseNoGSSHead uint64 RefuseNoGSSHeadBoth uint64 @@ -704,6 +706,8 @@ func (t *mergeCensusTotals) add(row mergeCensusRow) { t.GoAttempts += row.Go.Attempts t.GoSuccesses += row.Go.Successes + t.MixedRepresentationAttempts += row.Go.MixedRepresentationMergeAttempts + t.MixedRepresentationSuccesses += row.Go.MixedRepresentationMergeSuccesses t.RefuseNoGSSHead += row.Go.RefuseNoGSSHead t.RefuseNoGSSHeadBoth += row.Go.RefuseNoGSSHeadBoth diff --git a/cgo_harness/merge_event_census_test.go b/cgo_harness/merge_event_census_test.go index 9673d4eaa..4052266ad 100644 --- a/cgo_harness/merge_event_census_test.go +++ b/cgo_harness/merge_event_census_test.go @@ -40,6 +40,8 @@ func TestMergeCensusTotalsAggregatesPhysicalHeadMergeTelemetry(t *testing.T) { CompactPhysicalHeadMergeAttempts: 3, CompactPhysicalHeadMergeSuccesses: 2, CompactPhysicalHeadMergeInputLinks: 5, + MixedRepresentationMergeAttempts: 4, + MixedRepresentationMergeSuccesses: 1, }, }) totals.add(mergeCensusRow{ @@ -48,12 +50,16 @@ func TestMergeCensusTotalsAggregatesPhysicalHeadMergeTelemetry(t *testing.T) { CompactPhysicalHeadMergeAttempts: 7, CompactPhysicalHeadMergeSuccesses: 4, CompactPhysicalHeadMergeInputLinks: 9, + MixedRepresentationMergeAttempts: 6, + MixedRepresentationMergeSuccesses: 3, }, }) if totals.CompactPhysicalAttempts != 10 || totals.CompactPhysicalSuccesses != 6 || - totals.CompactPhysicalInputLinks != 14 { - t.Fatalf("physical merge totals=%d/%d/%d, want 10/6/14", + totals.CompactPhysicalInputLinks != 14 || totals.MixedRepresentationAttempts != 10 || + totals.MixedRepresentationSuccesses != 4 { + t.Fatalf("merge totals physical=%d/%d/%d mixed=%d/%d, want physical=10/6/14 mixed=10/4", totals.CompactPhysicalAttempts, totals.CompactPhysicalSuccesses, totals.CompactPhysicalInputLinks, + totals.MixedRepresentationAttempts, totals.MixedRepresentationSuccesses, ) } } @@ -121,7 +127,7 @@ var mergeCensusBaselineConstructed = map[string]struct { // Clean-suffix reset removes four redundant Kotlin merges. Exact C tree // parity remains pinned by TestKotlinRecoverySuffixSourcesMatchC. "kotlin": {Sources: 13, CMergeSuccesses: 54, GoSuccesses: 8, RefuseNoGSSHead: 2, RefuseScoreOrShifted: 0, RefuseDistinctShapes: 0, LinkPayloadShallowWouldAccept: 8, SourcesWhereGoOverMerges: 0, SourcesWhereCMergesAndGoDoesNot: 3}, - "python": {Sources: 26, CMergeSuccesses: 2, GoSuccesses: 0, RefuseNoGSSHead: 9, RefuseScoreOrShifted: 0, RefuseDistinctShapes: 0, LinkPayloadShallowWouldAccept: 0, SourcesWhereGoOverMerges: 0, SourcesWhereCMergesAndGoDoesNot: 2}, + "python": {Sources: 30, CMergeSuccesses: 2, GoSuccesses: 0, RefuseNoGSSHead: 9, RefuseScoreOrShifted: 0, RefuseDistinctShapes: 0, LinkPayloadShallowWouldAccept: 0, SourcesWhereGoOverMerges: 0, SourcesWhereCMergesAndGoDoesNot: 2}, } // The M0 pinned aggregate over the five A3 sweep corpora's constructed @@ -133,8 +139,8 @@ const ( mergeCensusBaselineCMerges uint64 = 191 mergeCensusBaselineGoMerges uint64 = 11 // mergeCensusBaselineSources is the constructed-source denominator, the - // same 104 sources D0 measures. - mergeCensusBaselineSources = 104 + // same 108 sources D0 measures. + mergeCensusBaselineSources = 108 ) // TestMergeEventCensusBaseline publishes the M0 baseline: how many merges the @@ -271,9 +277,10 @@ func TestMergeEventCensusBaseline(t *testing.T) { func mergeCensusFormatTotals(label string, t *mergeCensusTotals) string { return fmt.Sprintf( - "%-22s sources=%3d M_c=%6d M_p=%6d ratio=%-8s c-attempts=%7d go-attempts=%7d over-merge-sources=%d c-merges-go-does-not=%d | refusals: %s | tier2: %s | compact: accepted=%d union-attempts=%d union-appends=%d physical-attempts=%d physical-successes=%d physical-input-links=%d", + "%-22s sources=%3d M_c=%6d M_p=%6d ratio=%-8s c-attempts=%7d go-attempts=%7d over-merge-sources=%d c-merges-go-does-not=%d | mixed=%d/%d | refusals: %s | tier2: %s | compact: accepted=%d union-attempts=%d union-appends=%d physical-attempts=%d physical-successes=%d physical-input-links=%d", label, t.Sources, t.CMergeSuccesses, t.GoSuccesses, t.ratioText(), t.CMergeAttempts, t.GoAttempts, t.SourcesWhereGoOverMerges, t.SourcesWhereCMergesAndGoDoesNot, + t.MixedRepresentationAttempts, t.MixedRepresentationSuccesses, t.refusalLine(), t.linkPayloadLine(), t.CompactAccepted, t.CompactUnionAttempt, t.CompactUnionAppend, t.CompactPhysicalAttempts, t.CompactPhysicalSuccesses, t.CompactPhysicalInputLinks, diff --git a/glr.go b/glr.go index 838e09c32..5317d1a5c 100644 --- a/glr.go +++ b/glr.go @@ -5071,6 +5071,9 @@ func tryGSSMainMergeResult(scratch *glrMergeScratch, result []glrStack, idx int, if mixedMergeCertified && ((left.gss.head == nil) != (right.gss.head == nil)) { mixedRepresentation = true + if mergeCensusEnabled { + mergeCensusRecordMixedRepresentationAttempt() + } // Preserve the GSS gate order without allocating staging nodes. The // score and recovery-cost gates ran above, so check the remaining // status, position, and clean-zero conditions here. @@ -5118,9 +5121,6 @@ func tryGSSMainMergeResult(scratch *glrMergeScratch, result []glrStack, idx int, if workCountInstrumentationEnabled { workCountRecordGSSReject(workCountParserFromMergeScratch(scratch), workCountConvergencePhaseBoundaryEquivalence, workCountConvergenceReasonDistinctShape, "boundary merge retained distinct materializing shapes", left, right) } - if mergeCensusEnabled { - mergeCensusRecordDistinctShapes() - } return false, true } // Promote the flat side once. Tagged builds use topology hooks here; @@ -5220,7 +5220,11 @@ func tryGSSMainMergeResult(scratch *glrMergeScratch, result []glrStack, idx int, } workCountRecordMergeSuccess() if mergeCensusEnabled { - mergeCensusRecordSuccess() + if mixedRepresentation { + mergeCensusRecordMixedRepresentationSuccess() + } else { + mergeCensusRecordSuccess() + } } if scratch != nil { // A successful main merge can rewrite link 0 (prev/entry) of surviving diff --git a/merge_event_census.go b/merge_event_census.go index 4b97350c8..35542271a 100644 --- a/merge_event_census.go +++ b/merge_event_census.go @@ -46,6 +46,12 @@ type MergeEventCensusCounts struct { // returned true (merge_successes_proxy). Successes divided by the // reference runtime's successes is the lane's progress ratio. Successes uint64 + // MixedRepresentationMergeAttempts and MixedRepresentationMergeSuccesses + // count certified flat/GSS representation joins separately. These joins + // remove duplicate Go stack representations; the C runtime has no + // corresponding version merge, so they do not enter Successes. + MixedRepresentationMergeAttempts uint64 + MixedRepresentationMergeSuccesses uint64 // RefuseNoGSSHead counts pairs where at least one side carried no packed // head. A glrStack packs lazily (ensureGSS, glr.go:466-471), so a nil head @@ -54,7 +60,9 @@ type MergeEventCensusCounts struct { // version is a stack-node chain from creation, so this whole class is a // refusal with no counterpart. RefuseNoGSSHeadBoth and RefuseNoGSSHeadOne // split it, because "neither side was packed" and "one side was packed" - // need different work in stage M1. + // need different work in stage M1. A certified mixed-representation join + // increments the one-flat counter as a baseline gate observation, while its + // separate fields record the successful representation join. RefuseNoGSSHead uint64 RefuseNoGSSHeadBoth uint64 RefuseNoGSSHeadOne uint64 @@ -171,6 +179,18 @@ func mergeCensusRecordAttempt() { mergeCensusAdd(&mergeCensusState.counts.Attemp func mergeCensusRecordSuccess() { mergeCensusAdd(&mergeCensusState.counts.Successes, 1) } +func mergeCensusRecordMixedRepresentationAttempt() { + mergeCensusState.mu.Lock() + mergeCensusState.counts.MixedRepresentationMergeAttempts++ + mergeCensusState.counts.RefuseNoGSSHead++ + mergeCensusState.counts.RefuseNoGSSHeadOne++ + mergeCensusState.mu.Unlock() +} + +func mergeCensusRecordMixedRepresentationSuccess() { + mergeCensusAdd(&mergeCensusState.counts.MixedRepresentationMergeSuccesses, 1) +} + func mergeCensusRecordMergeFailed() { mergeCensusAdd(&mergeCensusState.counts.RefuseMergeFailed, 1) } func mergeCensusRecordDistinctShapes() { diff --git a/merge_event_census_disabled.go b/merge_event_census_disabled.go index 14befd6ca..9f2dfe6ee 100644 --- a/merge_event_census_disabled.go +++ b/merge_event_census_disabled.go @@ -23,6 +23,8 @@ func MergeEventCensusBuilt() bool { return false } func mergeCensusRecordAttempt() {} func mergeCensusRecordSuccess() {} +func mergeCensusRecordMixedRepresentationAttempt() {} +func mergeCensusRecordMixedRepresentationSuccess() {} func mergeCensusRecordMergeFailed() {} func mergeCensusRecordDistinctShapes() {} func mergeCensusRecordErrorCost() {}