From 18b23229c4c2a1724f90d608d0c4fbb0c8a2eb13 Mon Sep 17 00:00:00 2001 From: Oscar Villavicencio <9220505+odvcencio@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:30:28 -0700 Subject: [PATCH 1/2] add: Add parity probes, retry profiles, and diagnostic token forks - Add locked C oracle parity checks for Doxygen, Templ, Wolfram, WGSL, and Objective-C inputs. - Introduce benchmark cases for incremental recovery deletion and early newline edits. - Provide diagnostic token source forking to capture parser state and range digests during retries. - Guard against cyclic node references in trees produced by repeated concurrent parses. - Record Doxygen normalization blocker receipts and expected dispatch rewrite counts. Buckley-Change-Hash: sha256:9f86426c9a11d6035e0f27923664d3c8dea2ae32d0e89f688c26220058a61e19 Buckley-Change-Stats: files=20 insertions=4265 deletions=0 binaries=0 --- ...benchmark_go_canonical_incremental_test.go | 113 +++ cgo_harness/doxygen_probe_parity_test.go | 103 +++ cgo_harness/p14_profile_test.go | 64 ++ cgo_harness/p25b_issue454_137k_parity_test.go | 85 +++ cgo_harness/p25v_retry_paired_test.go | 452 ++++++++++++ cgo_harness/p25w_exact_language_route_test.go | 150 ++++ cgo_harness/templ_next_live_probe_test.go | 388 ++++++++++ cgo_harness/wgsl_next_live_probe_test.go | 440 ++++++++++++ cgo_harness/wolfram_retirement_parity_test.go | 146 ++++ diagnostic_token_source_fork.go | 88 +++ diagnostic_token_source_fork_test.go | 165 +++++ ...-normalization-blocker-receipt-proposal.md | 129 ++++ doxygen_probe_bridge.go | 21 + doxygen_probe_test.go | 254 +++++++ grammars/c_token_source_diagnostic_fork.go | 101 +++ .../c_token_source_diagnostic_fork_test.go | 308 ++++++++ p17_attribution_test.go | 671 ++++++++++++++++++ p25b_issue454_profile_test.go | 184 +++++ p25c_objc_probe_test.go | 324 +++++++++ ...o_materialization_cycle_regression_test.go | 79 +++ 20 files changed, 4265 insertions(+) create mode 100644 cgo_harness/doxygen_probe_parity_test.go create mode 100644 cgo_harness/p14_profile_test.go create mode 100644 cgo_harness/p25b_issue454_137k_parity_test.go create mode 100644 cgo_harness/p25v_retry_paired_test.go create mode 100644 cgo_harness/p25w_exact_language_route_test.go create mode 100644 cgo_harness/templ_next_live_probe_test.go create mode 100644 cgo_harness/wgsl_next_live_probe_test.go create mode 100644 cgo_harness/wolfram_retirement_parity_test.go create mode 100644 diagnostic_token_source_fork.go create mode 100644 diagnostic_token_source_fork_test.go create mode 100644 docs/doxygen-normalization-blocker-receipt-proposal.md create mode 100644 doxygen_probe_bridge.go create mode 100644 doxygen_probe_test.go create mode 100644 grammars/c_token_source_diagnostic_fork.go create mode 100644 grammars/c_token_source_diagnostic_fork_test.go create mode 100644 p17_attribution_test.go create mode 100644 p25b_issue454_profile_test.go create mode 100644 p25c_objc_probe_test.go create mode 100644 parser_go_materialization_cycle_regression_test.go diff --git a/cgo_harness/benchmark_go_canonical_incremental_test.go b/cgo_harness/benchmark_go_canonical_incremental_test.go index ef042714d..f6a53585e 100644 --- a/cgo_harness/benchmark_go_canonical_incremental_test.go +++ b/cgo_harness/benchmark_go_canonical_incremental_test.go @@ -4,6 +4,7 @@ package cgoharness import ( "bytes" + "context" "crypto/sha256" _ "embed" "encoding/hex" @@ -12,6 +13,7 @@ import ( "io" "os" "path/filepath" + "runtime/pprof" "strings" "testing" "time" @@ -298,6 +300,117 @@ func BenchmarkParityGoCanonicalIncremental(b *testing.B) { } } +// BenchmarkP25tRecoveryDeletionForwardOnly repeats the forward recovery edit. +// Keep this temporary benchmark outside the canonical tree. +func BenchmarkP25tRecoveryDeletionForwardOnly(b *testing.B) { + cases := loadCanonicalGoIncrementalCases(b) + var tc *canonicalGoIncrementalCase + for i := range cases { + if cases[i].spec.Name == "recovery_deletion" { + tc = &cases[i] + break + } + } + if tc == nil { + b.Fatal("canonical recovery_deletion case is missing") + } + goLang := canonicalIncrementalGoLanguage(b, tc.spec.Language) + cLang := canonicalIncrementalCLanguage(b, tc.spec.Language) + goParser := gotreesitter.NewParser(goLang) + cParser := sitter.NewParser() + defer cParser.Close() + if err := cParser.SetLanguage(cLang); err != nil { + b.Fatalf("recovery_deletion set pinned Go C reference: %v", err) + } + direction := tc.directions()[0] + admitCanonicalGoIncrementalDirection(b, tc.spec.Name, direction, goParser, cParser, goLang) + + parser := gotreesitter.NewParser(goLang) + tree, err := parser.Parse(tc.source) + requireCanonicalGoIncrementalTree(b, tree, tc.source, tc.spec.Name+" initial Go", err) + b.ReportAllocs() + b.SetBytes(int64((len(tc.source) + len(tc.edited)) / 2)) + b.ResetTimer() + b.StopTimer() + var totals realCorpusIncrementalProfileTotals + iterationLogPath := os.Getenv("P25T_ITERATION_LOG") + var iterationLog *os.File + if iterationLogPath != "" { + iterationLog, err = os.Create(iterationLogPath) + if err != nil { + b.Fatalf("create P25t iteration log: %v", err) + } + defer iterationLog.Close() + } + labels := pprof.Labels("phase", "incremental") + for i := 0; i < b.N; i++ { + if i > 0 { + releaseCanonicalGoTree(tree) + tree, err = parser.Parse(tc.source) + requireCanonicalGoIncrementalTree(b, tree, tc.source, tc.spec.Name+" reset Go", err) + } + var editElapsed, parseElapsed time.Duration + var profile gotreesitter.IncrementalParseProfile + var newTree *gotreesitter.Tree + var oldTree *gotreesitter.Tree + pprof.Do(context.Background(), labels, func(context.Context) { + b.StartTimer() + editStart := time.Now() + tree.Edit(tc.forward) + editElapsed = time.Since(editStart) + oldTree = tree + parseStart := time.Now() + newTree, profile, err = parser.ParseIncrementalProfiled(tc.edited, oldTree) + parseElapsed = time.Since(parseStart) + requireCanonicalGoIncrementalTree(b, newTree, tc.edited, tc.spec.Name+" timed Go", err) + b.StopTimer() + }) + totals.addEdit(editElapsed) + totals.addParseWall(parseElapsed) + totals.add(profile) + if newTree != oldTree { + releaseCanonicalGoTree(oldTree) + } + tree = newTree + if iterationLog != nil { + _, _ = fmt.Fprintf(iterationLog, + "iteration=%d edit_ns=%d parse_wall_ns=%d reuse_ns=%d reparse_ns=%d reused_subtrees=%d reused_bytes=%d new_nodes=%d tokens=%d max_stacks=%d single_iters=%d multi_iters=%d parser_loop_ns=%d token_next_ns=%d result_tree_build_ns=%d result_finalize_root_ns=%d result_compatibility_ns=%d gss_alloc=%d gss_retained=%d gss_dropped=%d parent_alloc=%d parent_retained=%d parent_dropped=%d leaf_alloc=%d leaf_retained=%d leaf_dropped=%d retry_attempts=%d retry_adopted=%t old_tree_reuse=%t\n", + i+1, + editElapsed.Nanoseconds(), + parseElapsed.Nanoseconds(), + profile.ReuseCursorNanos, + profile.ReparseNanos, + profile.ReusedSubtrees, + profile.ReusedBytes, + profile.NewNodesAllocated, + profile.TokensConsumed, + profile.MaxStacksSeen, + profile.SingleStackIterations, + profile.MultiStackIterations, + profile.ParserLoopNanos, + profile.TokenNextNanos, + profile.ResultTreeBuildNanos, + profile.ResultFinalizeRootNanos, + profile.ResultCompatibilityNanos, + profile.GSSNodesAllocated, + profile.GSSNodesRetained, + profile.GSSNodesDroppedSameToken, + profile.ParentNodesAllocated, + profile.ParentNodesRetained, + profile.ParentNodesDroppedSameToken, + profile.LeafNodesAllocated, + profile.LeafNodesRetained, + profile.LeafNodesDroppedSameToken, + profile.AcceptedErrorRetryAttempts, + profile.AcceptedErrorRetryAdopted, + profile.OldTreeReuseRoute, + ) + } + } + totals.report(b, b.N) + releaseCanonicalGoTree(tree) +} + func loadCanonicalGoIncrementalCases(tb testing.TB) []canonicalGoIncrementalCase { tb.Helper() decoder := json.NewDecoder(bytes.NewReader(canonicalGoIncrementalManifestJSON)) diff --git a/cgo_harness/doxygen_probe_parity_test.go b/cgo_harness/doxygen_probe_parity_test.go new file mode 100644 index 000000000..ec39be91d --- /dev/null +++ b/cgo_harness/doxygen_probe_parity_test.go @@ -0,0 +1,103 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "crypto/sha256" + "os" + "path/filepath" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +func TestDoxygenRegisteredWitnessesLockedCDeepParity(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + entry, ok := parityEntriesByName["doxygen"] + if !ok { + t.Fatal("missing Doxygen grammar entry") + } + goLang := entry.Language() + cLang, err := COracleLanguage("doxygen") + if err != nil { + t.Fatal(err) + } + witnesses := []struct { + name, path, source string + }{ + {name: "a0_CMakeLists", path: "../testdata/dispatcher_census_a0/doxygen/medium__CMakeLists.txt"}, + {name: "a0_metrics", path: "../testdata/dispatcher_census_a0/doxygen/medium__metrics.py"}, + {name: "a0_example_cfg", path: "../testdata/dispatcher_census_a0/doxygen/small__example.cfg"}, + {name: "registered_smoke", source: grammars.ParseSmokeSample("doxygen")}, + {name: "historical_childless_error", source: "/** Adds all words in \\a s to document \\a doc with weight \\a wfd */"}, + {name: "historical_recovered_document", source: "/**\n * @param {int} value\n * @brief Example\n */"}, + } + for _, witness := range witnesses { + witness := witness + t.Run(witness.name, func(t *testing.T) { + source := []byte(witness.source) + if witness.path != "" { + var err error + source, err = os.ReadFile(filepath.Join("..", "testdata", "dispatcher_census_a0", "doxygen", filepath.Base(witness.path))) + if err != nil { + t.Fatal(err) + } + } + t.Logf("source_sha256=%x bytes=%d", sha256.Sum256(source), len(source)) + + rawParser := gotreesitter.NewParser(goLang) + rawParser.SetAdmissionCandidateRoute(false) + raw, err := rawParser.ParseNoResultCompatibilityBenchmarkOnly(source) + if err != nil { + t.Fatalf("raw parse: %v", err) + } + t.Cleanup(raw.Release) + productionParser := gotreesitter.NewParser(goLang) + productionParser.SetAdmissionCandidateRoute(false) + production, err := productionParser.Parse(source) + if err != nil { + t.Fatalf("production parse: %v", err) + } + t.Cleanup(production.Release) + + cParser := sitter.NewParser() + t.Cleanup(cParser.Close) + if err := cParser.SetLanguage(cLang); err != nil { + t.Fatal(err) + } + cTree := cParser.Parse(source, nil) + if cTree == nil || cTree.RootNode() == nil { + t.Fatal("C oracle returned a nil tree") + } + t.Cleanup(cTree.Close) + cDigest, err := COracleDeepDigest(cTree) + if err != nil { + t.Fatalf("C digest: %v", err) + } + rawInspection, err := benchfixtures.InspectGoTree(raw.RootNode(), goLang) + if err != nil { + t.Fatalf("raw digest: %v", err) + } + productionInspection, err := benchfixtures.InspectGoTree(production.RootNode(), goLang) + if err != nil { + t.Fatalf("production digest: %v", err) + } + t.Logf("raw_digest=%s production_digest=%s c_digest=%s raw_rewrites=%d production_rewrites=%d raw_root=%s raw_error=%t production_root=%s production_error=%t", rawInspection.SHA256, productionInspection.SHA256, cDigest, raw.ParseRuntime().NormalizationNodesRewritten, production.ParseRuntime().NormalizationNodesRewritten, raw.RootNode().Type(goLang), raw.RootNode().HasError(), production.RootNode().Type(goLang), production.RootNode().HasError()) + if diff := FirstDivergenceDumpV1(production.RootNode(), goLang, cTree.RootNode()); diff != nil { + t.Fatalf("production diverges from locked C: %+v", diff) + } + if diff := firstLockedCTreeFlagDivergence(production.RootNode(), goLang, cTree.RootNode(), "/"+production.RootNode().Type(goLang)); diff != nil { + t.Fatalf("production flags diverge from locked C: %v", diff) + } + if productionInspection.SHA256 != cDigest { + t.Fatalf("production deep digest=%s C=%s", productionInspection.SHA256, cDigest) + } + if rawInspection.SHA256 != productionInspection.SHA256 { + t.Errorf("raw and production deep digests differ: raw=%s production=%s", rawInspection.SHA256, productionInspection.SHA256) + } + }) + } +} diff --git a/cgo_harness/p14_profile_test.go b/cgo_harness/p14_profile_test.go new file mode 100644 index 000000000..d1e533ea2 --- /dev/null +++ b/cgo_harness/p14_profile_test.go @@ -0,0 +1,64 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" +) + +func benchmarkP14EarlyNewlineDirection(b *testing.B, reverse bool) { + b.Helper() + cases := loadCanonicalGoIncrementalCases(b) + var tc canonicalGoIncrementalCase + for _, candidate := range cases { + if candidate.spec.Name == "early_newline" { + tc = candidate + break + } + } + if tc.spec.Name == "" { + b.Fatal("canonical early_newline case is missing") + } + var from, target []byte + var edit gotreesitter.InputEdit + if reverse { + from, target, edit = tc.edited, tc.source, tc.reverse + } else { + from, target, edit = tc.source, tc.edited, tc.forward + } + parser := gotreesitter.NewParser(canonicalIncrementalGoLanguage(b, "go")) + tree, err := parser.Parse(from) + requireCanonicalGoIncrementalTree(b, tree, from, "P14 profile initial", err) + defer releaseCanonicalGoTree(tree) + + b.ReportAllocs() + b.SetBytes(int64((len(tc.source) + len(tc.edited)) / 2)) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if i > 0 { + b.StopTimer() + releaseCanonicalGoTree(tree) + tree, err = parser.Parse(from) + requireCanonicalGoIncrementalTree(b, tree, from, "P14 profile reset", err) + b.StartTimer() + } + tree.Edit(edit) + newTree, _, err := parser.ParseIncrementalProfiled(target, tree) + requireCanonicalGoIncrementalTree(b, newTree, target, "P14 profile direction", err) + if newTree != tree { + tree.Release() + } + tree = newTree + } + b.StopTimer() +} + +func BenchmarkP14EarlyNewlineForward(b *testing.B) { + benchmarkP14EarlyNewlineDirection(b, false) +} + +func BenchmarkP14EarlyNewlineReverse(b *testing.B) { + benchmarkP14EarlyNewlineDirection(b, true) +} diff --git a/cgo_harness/p25b_issue454_137k_parity_test.go b/cgo_harness/p25b_issue454_137k_parity_test.go new file mode 100644 index 000000000..1d198656b --- /dev/null +++ b/cgo_harness/p25b_issue454_137k_parity_test.go @@ -0,0 +1,85 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "bytes" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +func TestP25BIssue454C137KiBParityBaseline(t *testing.T) { + source := benchfixtures.Issue454CSource() + site := bytes.Index(source, []byte("x0")) + if site < 0 { + t.Fatal("C edit marker is absent") + } + edited := append(append([]byte(nil), source[:site]...), source[site+1:]...) + + goBase, goLang, err := parseWithGo(parityCase{name: "c", source: string(source)}, source, nil) + if err != nil { + t.Fatalf("Go base parse: %v", err) + } + defer releaseGoTree(goBase) + goFresh, _, err := parseWithGo(parityCase{name: "c", source: string(edited)}, edited, nil) + if err != nil { + t.Fatalf("Go fresh edited parse: %v", err) + } + defer releaseGoTree(goFresh) + goIncremental, _, err := parseWithGo(parityCase{name: "c", source: string(edited)}, edited, goBase) + if err != nil { + t.Fatalf("Go incremental edited parse: %v", err) + } + defer releaseGoTree(goIncremental) + + cLang, err := ParityCLanguage("c") + if err != nil { + t.Fatalf("load C oracle: %v", err) + } + cParser := sitter.NewParser() + defer cParser.Close() + if err := cParser.SetLanguage(cLang); err != nil { + t.Fatalf("set C oracle language: %v", err) + } + cBase := cParser.Parse(source, nil) + if cBase == nil || cBase.RootNode() == nil { + t.Fatal("C base parse returned no tree") + } + defer cBase.Close() + cFresh := cParser.Parse(edited, nil) + if cFresh == nil || cFresh.RootNode() == nil { + t.Fatal("C fresh edited parse returned no tree") + } + defer cFresh.Close() + + goBaseDigest := p25bGoDigest(t, goBase, goLang) + goFreshDigest := p25bGoDigest(t, goFresh, goLang) + goIncrementalDigest := p25bGoDigest(t, goIncremental, goLang) + cBaseDigest, err := COracleDeepDigest(cBase) + if err != nil { + t.Fatalf("C base digest: %v", err) + } + cFreshDigest, err := COracleDeepDigest(cFresh) + if err != nil { + t.Fatalf("C edited digest: %v", err) + } + baseDiff := FirstDivergenceDumpV1(goBase.RootNode(), goLang, cBase.RootNode()) + freshDiff := FirstDivergenceDumpV1(goFresh.RootNode(), goLang, cFresh.RootNode()) + incrementalDiff := FirstDivergenceDumpV1(goIncremental.RootNode(), goLang, cFresh.RootNode()) + t.Logf("P25B_C137K bytes=%d site=%d go_base_digest=%s c_base_digest=%s go_fresh_digest=%s c_fresh_digest=%s go_incremental_digest=%s base_diff=%+v fresh_c_diff=%+v incremental_c_diff=%+v incremental_fresh_go_equal=%v go_base_error=%v go_fresh_error=%v go_incremental_error=%v c_base_error=%v c_fresh_error=%v", len(source), site, goBaseDigest, cBaseDigest, goFreshDigest, cFreshDigest, goIncrementalDigest, baseDiff, freshDiff, incrementalDiff, goIncrementalDigest == goFreshDigest, goBase.RootNode().HasError(), goFresh.RootNode().HasError(), goIncremental.RootNode().HasError(), cBase.RootNode().HasError(), cFresh.RootNode().HasError()) + if goIncrementalDigest != goFreshDigest { + t.Fatalf("incremental Go digest %s differs from fresh Go digest %s", goIncrementalDigest, goFreshDigest) + } +} + +func p25bGoDigest(t *testing.T, tree *gotreesitter.Tree, lang *gotreesitter.Language) string { + t.Helper() + inspection, err := benchfixtures.InspectGoTree(tree.RootNode(), lang) + if err != nil { + t.Fatalf("inspect Go tree: %v", err) + } + return inspection.SHA256 +} diff --git a/cgo_harness/p25v_retry_paired_test.go b/cgo_harness/p25v_retry_paired_test.go new file mode 100644 index 000000000..a80d26555 --- /dev/null +++ b/cgo_harness/p25v_retry_paired_test.go @@ -0,0 +1,452 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "encoding/json" + "os" + "testing" + "time" + + gotreesitter "github.com/odvcencio/gotreesitter" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +const p25vRetryBypassEnv = "GOT_DIAGNOSTIC_SKIP_ACCEPTED_ERROR_BASE_MERGE_RETRY" + +type p25vSpan struct { + Start uint32 `json:"start_byte"` + End uint32 `json:"end_byte"` + Kind string `json:"kind"` +} + +type p25vRoot struct { + StartByte uint32 `json:"start_byte"` + EndByte uint32 `json:"end_byte"` + HasError bool `json:"has_error"` + ChildCount int `json:"child_count"` + FirstError *p25vSpan `json:"first_error,omitempty"` +} + +type p25vRuntime struct { + StopReason string `json:"stop_reason"` + Truncated bool `json:"truncated"` + TokenSourceEOFEarly bool `json:"token_source_eof_early"` + RootEndByte uint32 `json:"root_end_byte"` + RetryAttempts uint8 `json:"retry_attempts"` + RetryAdopted bool `json:"retry_adopted"` + RetryMergePerKey int `json:"retry_merge_per_key"` + RetryCause string `json:"retry_cause"` + OldTreeReuseRoute bool `json:"old_tree_reuse_route"` + CRecoveryEnteredErrorState bool `json:"c_recovery_entered_error_state"` + CRecoveryDroppedErrorForClean bool `json:"c_recovery_dropped_error_for_clean"` + CRecoverySwallowedFallbackAttempted bool `json:"c_recovery_swallowed_fallback_attempted"` + TokensConsumed uint64 `json:"tokens_consumed"` + Iterations int `json:"iterations"` + NodesAllocated int `json:"nodes_allocated"` + MaxStacksSeen int `json:"max_stacks_seen"` + SingleStackIterations int `json:"single_stack_iterations"` + MultiStackIterations int `json:"multi_stack_iterations"` + GSSNodesAllocated uint64 `json:"gss_nodes_allocated"` + GSSNodesRetained uint64 `json:"gss_nodes_retained"` + GSSNodesDropped uint64 `json:"gss_nodes_dropped_same_token"` + ParentNodesAllocated uint64 `json:"parent_nodes_allocated"` + ParentNodesRetained uint64 `json:"parent_nodes_retained"` + ParentNodesDropped uint64 `json:"parent_nodes_dropped_same_token"` + LeafNodesAllocated uint64 `json:"leaf_nodes_allocated"` + LeafNodesRetained uint64 `json:"leaf_nodes_retained"` + LeafNodesDropped uint64 `json:"leaf_nodes_dropped_same_token"` + ArenaBytesAllocated int64 `json:"arena_bytes_allocated"` + ScratchBytesAllocated int64 `json:"scratch_bytes_allocated"` + GSSBytesAllocated int64 `json:"gss_bytes_allocated"` + MergeStacksIn uint64 `json:"merge_stacks_in"` + MergeStacksOut uint64 `json:"merge_stacks_out"` + MergeSlotsUsed uint64 `json:"merge_slots_used"` +} + +type p25vProfile struct { + ReuseCursorNanos int64 `json:"reuse_cursor_nanos"` + ReparseNanos int64 `json:"reparse_nanos"` + ReusedSubtrees uint64 `json:"reused_subtrees"` + ReusedBytes uint64 `json:"reused_bytes"` + NewNodesAllocated uint64 `json:"new_nodes_allocated"` + RetryAttempts uint8 `json:"retry_attempts"` + RetryAdopted bool `json:"retry_adopted"` + RetryMergePerKey int `json:"retry_merge_per_key"` + RetryCause string `json:"retry_cause"` + OldTreeReuseRoute bool `json:"old_tree_reuse_route"` + ReuseRejectDirty uint64 `json:"reuse_reject_dirty"` + ReuseRejectHasError uint64 `json:"reuse_reject_has_error"` + ReuseRejectFragileNonLeaf uint64 `json:"reuse_reject_fragile_non_leaf"` + RecoverSearches uint64 `json:"recover_searches"` + RecoverStateChecks uint64 `json:"recover_state_checks"` + RecoverHits uint64 `json:"recover_hits"` + TokensConsumed uint64 `json:"tokens_consumed"` + MaxStacksSeen int `json:"max_stacks_seen"` + SingleStackIterations int `json:"single_stack_iterations"` + MultiStackIterations int `json:"multi_stack_iterations"` + GSSNodesAllocated uint64 `json:"gss_nodes_allocated"` + GSSNodesRetained uint64 `json:"gss_nodes_retained"` + GSSNodesDropped uint64 `json:"gss_nodes_dropped_same_token"` + ParentNodesAllocated uint64 `json:"parent_nodes_allocated"` + ParentNodesRetained uint64 `json:"parent_nodes_retained"` + ParentNodesDropped uint64 `json:"parent_nodes_dropped_same_token"` + LeafNodesAllocated uint64 `json:"leaf_nodes_allocated"` + LeafNodesRetained uint64 `json:"leaf_nodes_retained"` + LeafNodesDropped uint64 `json:"leaf_nodes_dropped_same_token"` + ArenaBytesAllocated int64 `json:"arena_bytes_allocated"` + ScratchBytesAllocated int64 `json:"scratch_bytes_allocated"` + GSSBytesAllocated int64 `json:"gss_bytes_allocated"` + MergeStacksIn uint64 `json:"merge_stacks_in"` + MergeStacksOut uint64 `json:"merge_stacks_out"` + MergeSlotsUsed uint64 `json:"merge_slots_used"` + GlobalCullStacksIn uint64 `json:"global_cull_stacks_in"` + GlobalCullStacksOut uint64 `json:"global_cull_stacks_out"` +} + +type p25vRoute struct { + DigestEqualFreshGoC bool `json:"digest_equal_fresh_go_c"` + DigestEqualIncrementalGoC bool `json:"digest_equal_incremental_go_c"` + DigestEqualFreshIncremental bool `json:"digest_equal_fresh_incremental"` + AllDeepDigestsEqual bool `json:"all_deep_digests_equal"` + GoFreshDigest string `json:"go_fresh_digest"` + CFreshDigest string `json:"c_fresh_digest"` + GoIncrementalDigest string `json:"go_incremental_digest"` + CIncrementalDigest string `json:"c_incremental_digest"` + FreshFirstDifference string `json:"fresh_first_difference,omitempty"` + IncrementalFirstDifference string `json:"incremental_first_difference,omitempty"` +} + +type p25vCaseResult struct { + Name string `json:"name"` + Role string `json:"role"` + Mode string `json:"mode"` + SourceBytes int `json:"source_bytes"` + EditedBytes int `json:"edited_bytes"` + SourceSHA256 string `json:"source_sha256"` + EditedSHA256 string `json:"edited_sha256"` + GoFreshWallNanos int64 `json:"go_fresh_wall_nanos"` + CFreshWallNanos int64 `json:"c_fresh_wall_nanos"` + GoIncrementalWallNanos int64 `json:"go_incremental_wall_nanos"` + CIncrementalWallNanos int64 `json:"c_incremental_wall_nanos"` + GoFreshRoot p25vRoot `json:"go_fresh_root"` + CFreshRoot p25vRoot `json:"c_fresh_root"` + GoIncrementalRoot p25vRoot `json:"go_incremental_root"` + CIncrementalRoot p25vRoot `json:"c_incremental_root"` + GoFreshRuntime p25vRuntime `json:"go_fresh_runtime"` + GoIncrementalRuntime p25vRuntime `json:"go_incremental_runtime"` + GoIncrementalProfile p25vProfile `json:"go_incremental_profile"` + Route p25vRoute `json:"route"` +} + +type p25vReport struct { + Schema string `json:"schema"` + Mode string `json:"mode"` + BypassEnv string `json:"bypass_env"` + Cases []p25vCaseResult `json:"cases"` +} + +func TestP25vRetryPaired(t *testing.T) { + bypass := os.Getenv(p25vRetryBypassEnv) == "1" + mode := "retry-enabled" + if bypass { + mode = "retry-bypassed" + } + caseNames := []struct { + name string + role string + }{ + {name: "recovery_deletion", role: "authenticated-forward-malformed-recovery"}, + {name: "same_length_leaf_validation", role: "clean-incremental-control"}, + {name: "same_line_length_change", role: "accepted-error-recovery-control"}, + } + cases := loadCanonicalGoIncrementalCases(t) + report := p25vReport{Schema: "p25v-retry-paired-v1", Mode: mode, BypassEnv: os.Getenv(p25vRetryBypassEnv)} + allEqual := true + for _, want := range caseNames { + var tc *canonicalGoIncrementalCase + for i := range cases { + if cases[i].spec.Name == want.name { + candidate := cases[i] + tc = &candidate + break + } + } + if tc == nil { + t.Fatalf("canonical case %q is missing", want.name) + } + result := runP25vCase(t, tc, want.role, mode) + report.Cases = append(report.Cases, result) + if !result.Route.AllDeepDigestsEqual { + allEqual = false + } + } + encoded, err := json.Marshal(report) + if err != nil { + t.Fatalf("marshal P25v report: %v", err) + } + t.Logf("P25V_RESULT %s", encoded) + if path := os.Getenv("P25V_RESULT_OUT"); path != "" { + if err := os.WriteFile(path, encoded, 0o644); err != nil { + t.Fatalf("write P25v report %q: %v", path, err) + } + } + if !allEqual { + t.Fatalf("P25v %s mode changed a fresh, incremental, or locked-C deep digest", mode) + } +} + +func runP25vCase(t *testing.T, tc *canonicalGoIncrementalCase, role, mode string) p25vCaseResult { + t.Helper() + direction := tc.directions()[0] + goLang := canonicalIncrementalGoLanguage(t, tc.spec.Language) + cLang := canonicalIncrementalCLanguage(t, tc.spec.Language) + goParser := gotreesitter.NewParser(goLang) + cParser := sitter.NewParser() + if err := cParser.SetLanguage(cLang); err != nil { + t.Fatalf("%s set C language: %v", tc.spec.Name, err) + } + + goFreshStart := time.Now() + goFresh, err := goParser.Parse(direction.to) + goFreshWall := time.Since(goFreshStart) + if err != nil { + t.Fatalf("%s fresh Go parse: %v", tc.spec.Name, err) + } + requireCanonicalGoIncrementalTree(t, goFresh, direction.to, tc.spec.Name+" P25v fresh Go", nil) + goFreshDigest := canonicalGoTreeDigest(t, goFresh, goLang, tc.spec.Name+" P25v fresh Go") + + cFreshStart := time.Now() + cFresh := cParser.Parse(direction.to, nil) + cFreshWall := time.Since(cFreshStart) + requireCanonicalCIncrementalTree(t, cFresh, direction.to, tc.spec.Name+" P25v fresh C") + cFreshDigest := canonicalCTreeDigest(t, cFresh, tc.spec.Name+" P25v fresh C") + + goOld, err := goParser.Parse(direction.from) + if err != nil { + t.Fatalf("%s old Go parse: %v", tc.spec.Name, err) + } + requireCanonicalGoIncrementalTree(t, goOld, direction.from, tc.spec.Name+" P25v old Go", nil) + goOld.Edit(direction.goEdit) + goIncrementalStart := time.Now() + goIncremental, profile, err := goParser.ParseIncrementalProfiled(direction.to, goOld) + goIncrementalWall := time.Since(goIncrementalStart) + if err != nil { + t.Fatalf("%s incremental Go parse: %v", tc.spec.Name, err) + } + if goIncremental != goOld { + releaseCanonicalGoTree(goOld) + } + requireCanonicalGoIncrementalTree(t, goIncremental, direction.to, tc.spec.Name+" P25v incremental Go", nil) + goIncrementalDigest := canonicalGoTreeDigest(t, goIncremental, goLang, tc.spec.Name+" P25v incremental Go") + + cOld := cParser.Parse(direction.from, nil) + requireCanonicalCIncrementalTree(t, cOld, direction.from, tc.spec.Name+" P25v old C") + cOld.Edit(&direction.cEdit) + cIncrementalStart := time.Now() + cIncremental := cParser.Parse(direction.to, cOld) + cIncrementalWall := time.Since(cIncrementalStart) + if cIncremental != cOld { + closeCanonicalCTree(cOld) + } + requireCanonicalCIncrementalTree(t, cIncremental, direction.to, tc.spec.Name+" P25v incremental C") + cIncrementalDigest := canonicalCTreeDigest(t, cIncremental, tc.spec.Name+" P25v incremental C") + + goFreshRoot := p25vGoRoot(goFresh.RootNode()) + cFreshRoot := p25vCRoot(cFresh.RootNode()) + goIncrementalRoot := p25vGoRoot(goIncremental.RootNode()) + cIncrementalRoot := p25vCRoot(cIncremental.RootNode()) + goFreshRuntime := p25vRuntimeFrom(goFresh.ParseRuntime()) + goIncrementalRuntime := p25vRuntimeFrom(goIncremental.ParseRuntime()) + freshFirstDifference := "" + incrementalFirstDifference := "" + if diff := FirstDivergenceDumpV1(goFresh.RootNode(), goLang, cFresh.RootNode()); diff != nil { + freshFirstDifference = formatRealCorpusDivergence(diff) + } + if diff := FirstDivergenceDumpV1(goIncremental.RootNode(), goLang, cIncremental.RootNode()); diff != nil { + incrementalFirstDifference = formatRealCorpusDivergence(diff) + } + route := p25vRoute{ + DigestEqualFreshGoC: goFreshDigest == cFreshDigest, + DigestEqualIncrementalGoC: goIncrementalDigest == cIncrementalDigest, + DigestEqualFreshIncremental: goFreshDigest == goIncrementalDigest, + AllDeepDigestsEqual: goFreshDigest == cFreshDigest && goFreshDigest == goIncrementalDigest && goFreshDigest == cIncrementalDigest, + GoFreshDigest: goFreshDigest, + CFreshDigest: cFreshDigest, + GoIncrementalDigest: goIncrementalDigest, + CIncrementalDigest: cIncrementalDigest, + FreshFirstDifference: freshFirstDifference, + IncrementalFirstDifference: incrementalFirstDifference, + } + + result := p25vCaseResult{ + Name: tc.spec.Name, + Role: role, + Mode: mode, + SourceBytes: len(direction.from), + EditedBytes: len(direction.to), + SourceSHA256: direction.fromSHA256(), + EditedSHA256: direction.toSHA256(), + GoFreshWallNanos: goFreshWall.Nanoseconds(), + CFreshWallNanos: cFreshWall.Nanoseconds(), + GoIncrementalWallNanos: goIncrementalWall.Nanoseconds(), + CIncrementalWallNanos: cIncrementalWall.Nanoseconds(), + GoFreshRoot: goFreshRoot, + CFreshRoot: cFreshRoot, + GoIncrementalRoot: goIncrementalRoot, + CIncrementalRoot: cIncrementalRoot, + GoFreshRuntime: goFreshRuntime, + GoIncrementalRuntime: goIncrementalRuntime, + GoIncrementalProfile: p25vProfileFrom(profile), + Route: route, + } + + t.Logf("P25V_CASE mode=%s case=%s role=%s all_deep_equal=%t fresh_go=%s fresh_c=%s incremental_go=%s incremental_c=%s go_inc_wall_ns=%d c_inc_wall_ns=%d retry_attempts=%d retry_adopted=%t retry_cap=%d stop=%s root_error=%t", mode, result.Name, result.Role, route.AllDeepDigestsEqual, goFreshDigest, cFreshDigest, goIncrementalDigest, cIncrementalDigest, result.GoIncrementalWallNanos, result.CIncrementalWallNanos, profile.AcceptedErrorRetryAttempts, profile.AcceptedErrorRetryAdopted, profile.AcceptedErrorRetryMergePerKey, goIncrementalRuntime.StopReason, goIncrementalRoot.HasError) + + releaseCanonicalGoTree(goFresh) + releaseCanonicalGoTree(goIncremental) + closeCanonicalCTree(cFresh) + closeCanonicalCTree(cIncremental) + cParser.Close() + return result +} + +func (d canonicalGoIncrementalDirection) fromSHA256() string { + return sha256Hex(d.from) +} + +func (d canonicalGoIncrementalDirection) toSHA256() string { + return sha256Hex(d.to) +} + +func p25vGoRoot(root *gotreesitter.Node) p25vRoot { + if root == nil { + return p25vRoot{} + } + return p25vRoot{StartByte: root.StartByte(), EndByte: root.EndByte(), HasError: root.HasError(), ChildCount: root.ChildCount(), FirstError: p25vFirstGoError(root)} +} + +func p25vCRoot(root *sitter.Node) p25vRoot { + if root == nil { + return p25vRoot{} + } + return p25vRoot{StartByte: uint32(root.StartByte()), EndByte: uint32(root.EndByte()), HasError: root.HasError(), ChildCount: int(root.ChildCount()), FirstError: p25vFirstCError(root)} +} + +func p25vFirstGoError(node *gotreesitter.Node) *p25vSpan { + if node == nil { + return nil + } + if node.IsError() || node.IsMissing() { + kind := "ERROR" + if node.IsMissing() { + kind = "MISSING" + } + return &p25vSpan{Start: node.StartByte(), End: node.EndByte(), Kind: kind} + } + for i := 0; i < node.ChildCount(); i++ { + if span := p25vFirstGoError(node.Child(i)); span != nil { + return span + } + } + return nil +} + +func p25vFirstCError(node *sitter.Node) *p25vSpan { + if node == nil { + return nil + } + if node.IsError() || node.IsMissing() { + kind := "ERROR" + if node.IsMissing() { + kind = "MISSING" + } + return &p25vSpan{Start: uint32(node.StartByte()), End: uint32(node.EndByte()), Kind: kind} + } + for i := uint(0); i < node.ChildCount(); i++ { + if span := p25vFirstCError(node.Child(i)); span != nil { + return span + } + } + return nil +} + +func p25vRuntimeFrom(runtime gotreesitter.ParseRuntime) p25vRuntime { + return p25vRuntime{ + StopReason: string(runtime.StopReason), + Truncated: runtime.Truncated, + TokenSourceEOFEarly: runtime.TokenSourceEOFEarly, + RootEndByte: runtime.RootEndByte, + RetryAttempts: runtime.IncrementalAcceptedErrorRetryAttempts, + RetryAdopted: runtime.IncrementalAcceptedErrorRetryAdopted, + RetryMergePerKey: runtime.IncrementalAcceptedErrorRetryMergePerKey, + RetryCause: canonicalIncrementalRetryCause(runtime.IncrementalAcceptedErrorRetryCause), + OldTreeReuseRoute: runtime.IncrementalOldTreeReuseRoute, + CRecoveryEnteredErrorState: runtime.CRecoveryEnteredErrorState, + CRecoveryDroppedErrorForClean: runtime.CRecoveryDroppedErrorForClean, + CRecoverySwallowedFallbackAttempted: runtime.CRecoverySwallowedErrorFallbackAttempted, + TokensConsumed: runtime.TokensConsumed, + Iterations: runtime.Iterations, + NodesAllocated: runtime.NodesAllocated, + MaxStacksSeen: runtime.MaxStacksSeen, + SingleStackIterations: runtime.SingleStackIterations, + MultiStackIterations: runtime.MultiStackIterations, + GSSNodesAllocated: runtime.GSSNodesAllocated, + GSSNodesRetained: runtime.GSSNodesRetained, + GSSNodesDropped: runtime.GSSNodesDroppedSameToken, + ParentNodesAllocated: runtime.ParentNodesAllocated, + ParentNodesRetained: runtime.ParentNodesRetained, + ParentNodesDropped: runtime.ParentNodesDroppedSameToken, + LeafNodesAllocated: runtime.LeafNodesAllocated, + LeafNodesRetained: runtime.LeafNodesRetained, + LeafNodesDropped: runtime.LeafNodesDroppedSameToken, + ArenaBytesAllocated: runtime.ArenaBytesAllocated, + ScratchBytesAllocated: runtime.ScratchBytesAllocated, + GSSBytesAllocated: runtime.GSSBytesAllocated, + MergeStacksIn: runtime.MergeStacksIn, + MergeStacksOut: runtime.MergeStacksOut, + MergeSlotsUsed: runtime.MergeSlotsUsed, + } +} + +func p25vProfileFrom(profile gotreesitter.IncrementalParseProfile) p25vProfile { + return p25vProfile{ + ReuseCursorNanos: profile.ReuseCursorNanos, + ReparseNanos: profile.ReparseNanos, + ReusedSubtrees: profile.ReusedSubtrees, + ReusedBytes: profile.ReusedBytes, + NewNodesAllocated: profile.NewNodesAllocated, + RetryAttempts: profile.AcceptedErrorRetryAttempts, + RetryAdopted: profile.AcceptedErrorRetryAdopted, + RetryMergePerKey: profile.AcceptedErrorRetryMergePerKey, + RetryCause: canonicalIncrementalRetryCause(profile.AcceptedErrorRetryCause), + OldTreeReuseRoute: profile.OldTreeReuseRoute, + ReuseRejectDirty: profile.ReuseRejectDirty, + ReuseRejectHasError: profile.ReuseRejectHasError, + ReuseRejectFragileNonLeaf: profile.ReuseRejectFragileNonLeaf, + RecoverSearches: profile.RecoverSearches, + RecoverStateChecks: profile.RecoverStateChecks, + RecoverHits: profile.RecoverHits, + TokensConsumed: profile.TokensConsumed, + MaxStacksSeen: profile.MaxStacksSeen, + SingleStackIterations: profile.SingleStackIterations, + MultiStackIterations: profile.MultiStackIterations, + GSSNodesAllocated: profile.GSSNodesAllocated, + GSSNodesRetained: profile.GSSNodesRetained, + GSSNodesDropped: profile.GSSNodesDroppedSameToken, + ParentNodesAllocated: profile.ParentNodesAllocated, + ParentNodesRetained: profile.ParentNodesRetained, + ParentNodesDropped: profile.ParentNodesDroppedSameToken, + LeafNodesAllocated: profile.LeafNodesAllocated, + LeafNodesRetained: profile.LeafNodesRetained, + LeafNodesDropped: profile.LeafNodesDroppedSameToken, + ArenaBytesAllocated: profile.ArenaBytesAllocated, + ScratchBytesAllocated: profile.ScratchBytesAllocated, + GSSBytesAllocated: profile.GSSBytesAllocated, + MergeStacksIn: profile.MergeStacksIn, + MergeStacksOut: profile.MergeStacksOut, + MergeSlotsUsed: profile.MergeSlotsUsed, + GlobalCullStacksIn: profile.GlobalCullStacksIn, + GlobalCullStacksOut: profile.GlobalCullStacksOut, + } +} diff --git a/cgo_harness/p25w_exact_language_route_test.go b/cgo_harness/p25w_exact_language_route_test.go new file mode 100644 index 000000000..344a75f80 --- /dev/null +++ b/cgo_harness/p25w_exact_language_route_test.go @@ -0,0 +1,150 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "encoding/json" + "os" + "testing" + "time" + + gotreesitter "github.com/odvcencio/gotreesitter" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +type p25wExactLanguageResult struct { + Mode string `json:"mode"` + Case string `json:"case"` + SourceSHA256 string `json:"source_sha256"` + EditedSHA256 string `json:"edited_sha256"` + GoFreshDigest string `json:"go_fresh_digest"` + GoIncrementalDigest string `json:"go_incremental_digest"` + CFreshDigest string `json:"c_fresh_digest"` + CIncrementalDigest string `json:"c_incremental_digest"` + GoFreshRoot p25vRoot `json:"go_fresh_root"` + GoIncrementalRoot p25vRoot `json:"go_incremental_root"` + CFreshRoot p25vRoot `json:"c_fresh_root"` + CIncrementalRoot p25vRoot `json:"c_incremental_root"` + GoIncrementalRuntime p25vRuntime `json:"go_incremental_runtime"` + GoIncrementalProfile p25vProfile `json:"go_incremental_profile"` + FreshDifference string `json:"fresh_difference,omitempty"` + IncrementalDifference string `json:"incremental_difference,omitempty"` + GoFreshWallNanos int64 `json:"go_fresh_wall_nanos"` + GoIncrementalWallNanos int64 `json:"go_incremental_wall_nanos"` +} + +func TestP25wExactLanguageRoute(t *testing.T) { + mode := "retry-enabled" + if os.Getenv(p25vRetryBypassEnv) == "1" { + mode = "retry-bypassed" + } + cases := loadCanonicalGoIncrementalCases(t) + var tc *canonicalGoIncrementalCase + for i := range cases { + if cases[i].spec.Name == "same_line_length_change" { + candidate := cases[i] + tc = &candidate + break + } + } + if tc == nil { + t.Fatal("same_line_length_change case is missing") + } + direction := tc.directions()[0] + goLang := canonicalIncrementalGoLanguage(t, tc.spec.Language) + cLang := canonicalIncrementalCLanguage(t, tc.spec.Language) + + goFreshParser := gotreesitter.NewParser(goLang) + goFreshParser.SetAdmissionCandidateRoute(false) + goFreshStart := time.Now() + goFresh, err := goFreshParser.Parse(direction.to) + goFreshWall := time.Since(goFreshStart) + if err != nil { + t.Fatalf("fresh Go parse: %v", err) + } + requireCanonicalGoIncrementalTree(t, goFresh, direction.to, "P25w fresh Go", nil) + goFreshDigest := canonicalGoTreeDigest(t, goFresh, goLang, "P25w fresh Go") + + goOldParser := gotreesitter.NewParser(goLang) + goOldParser.SetAdmissionCandidateRoute(false) + goOld, err := goOldParser.Parse(direction.from) + if err != nil { + t.Fatalf("old Go parse: %v", err) + } + requireCanonicalGoIncrementalTree(t, goOld, direction.from, "P25w old Go", nil) + goOld.Edit(direction.goEdit) + goIncrementalParser := gotreesitter.NewParser(goLang) + goIncrementalParser.SetAdmissionCandidateRoute(false) + goIncrementalStart := time.Now() + goIncremental, profile, err := goIncrementalParser.ParseIncrementalProfiled(direction.to, goOld) + goIncrementalWall := time.Since(goIncrementalStart) + if err != nil { + t.Fatalf("incremental Go parse: %v", err) + } + if goIncremental != goOld { + releaseCanonicalGoTree(goOld) + } + requireCanonicalGoIncrementalTree(t, goIncremental, direction.to, "P25w incremental Go", nil) + goIncrementalDigest := canonicalGoTreeDigest(t, goIncremental, goLang, "P25w incremental Go") + + cParser := sitter.NewParser() + if err := cParser.SetLanguage(cLang); err != nil { + t.Fatalf("set C language: %v", err) + } + cFresh := cParser.Parse(direction.to, nil) + requireCanonicalCIncrementalTree(t, cFresh, direction.to, "P25w fresh C") + cFreshDigest := canonicalCTreeDigest(t, cFresh, "P25w fresh C") + cOld := cParser.Parse(direction.from, nil) + requireCanonicalCIncrementalTree(t, cOld, direction.from, "P25w old C") + cOld.Edit(&direction.cEdit) + cIncremental := cParser.Parse(direction.to, cOld) + if cIncremental != cOld { + closeCanonicalCTree(cOld) + } + requireCanonicalCIncrementalTree(t, cIncremental, direction.to, "P25w incremental C") + cIncrementalDigest := canonicalCTreeDigest(t, cIncremental, "P25w incremental C") + + result := p25wExactLanguageResult{ + Mode: mode, + Case: tc.spec.Name, + SourceSHA256: direction.fromSHA256(), + EditedSHA256: direction.toSHA256(), + GoFreshDigest: goFreshDigest, + GoIncrementalDigest: goIncrementalDigest, + CFreshDigest: cFreshDigest, + CIncrementalDigest: cIncrementalDigest, + GoFreshRoot: p25vGoRoot(goFresh.RootNode()), + GoIncrementalRoot: p25vGoRoot(goIncremental.RootNode()), + CFreshRoot: p25vCRoot(cFresh.RootNode()), + CIncrementalRoot: p25vCRoot(cIncremental.RootNode()), + GoIncrementalRuntime: p25vRuntimeFrom(goIncremental.ParseRuntime()), + GoIncrementalProfile: p25vProfileFrom(profile), + GoFreshWallNanos: goFreshWall.Nanoseconds(), + GoIncrementalWallNanos: goIncrementalWall.Nanoseconds(), + } + if diff := FirstDivergenceDumpV1(goFresh.RootNode(), goLang, cFresh.RootNode()); diff != nil { + result.FreshDifference = formatRealCorpusDivergence(diff) + } + if diff := FirstDivergenceDumpV1(goIncremental.RootNode(), goLang, cIncremental.RootNode()); diff != nil { + result.IncrementalDifference = formatRealCorpusDivergence(diff) + } + encoded, err := json.Marshal(result) + if err != nil { + t.Fatalf("marshal P25w result: %v", err) + } + t.Logf("P25W_RESULT %s", encoded) + if path := os.Getenv("P25W_RESULT_OUT"); path != "" { + if err := os.WriteFile(path, encoded, 0o644); err != nil { + t.Fatalf("write P25w result: %v", err) + } + } + if goIncrementalDigest != goFreshDigest || goIncrementalDigest != cFreshDigest || goIncrementalDigest != cIncrementalDigest { + t.Errorf("P25w exact route deep digest mismatch: Go fresh=%s Go incremental=%s C fresh=%s C incremental=%s", goFreshDigest, goIncrementalDigest, cFreshDigest, cIncrementalDigest) + } + + releaseCanonicalGoTree(goFresh) + releaseCanonicalGoTree(goIncremental) + closeCanonicalCTree(cFresh) + closeCanonicalCTree(cIncremental) + cParser.Close() +} diff --git a/cgo_harness/templ_next_live_probe_test.go b/cgo_harness/templ_next_live_probe_test.go new file mode 100644 index 000000000..41d55eeb4 --- /dev/null +++ b/cgo_harness/templ_next_live_probe_test.go @@ -0,0 +1,388 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "bytes" + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +type templNextWitness struct { + name string + source []byte +} + +// TestTemplNextLiveArmLockedCRoutes records Templ on raw, production, compact, +// forest, and incremental routes. It keeps rewrite and recovery evidence visible. +func TestTemplNextLiveArmLockedCRoutes(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + t.Setenv("GOT_PARSE_PHASE_TIMING", "1") + goLanguage := grammars.TemplLanguage() + cLanguage, err := COracleLanguage("templ") + if err != nil { + t.Fatal(err) + } + + witnesses := []templNextWitness{ + {name: "a0-medium-main", source: templNextReadA0(t, "medium__main.templ")}, + {name: "a0-medium-template", source: templNextReadA0(t, "medium__template.templ")}, + {name: "a0-small-template", source: templNextReadA0(t, "small__template.templ")}, + {name: "positive-no-op-control", source: []byte("templ T() {
ok
}\n")}, + {name: "qualified-component-import", source: []byte("package main\n\ntempl T() {\n\t@templ.JSONScript(\"scriptData\", scriptData)\n}\n")}, + {name: "malformed-dangling-attribute-quote", source: []byte("templ Broken() {\n\t\n}\n")}, + {name: "malformed-component-import", source: []byte("package main\n\ntempl Broken() {\n\t@counts(global,\n}\n")}, + } + + for _, witness := range witnesses { + witness := witness + t.Run(witness.name, func(t *testing.T) { + cParser := sitter.NewParser() + t.Cleanup(cParser.Close) + if err := cParser.SetLanguage(cLanguage); err != nil { + t.Fatal(err) + } + cTree := cParser.Parse(witness.source, nil) + if cTree == nil || cTree.RootNode() == nil { + t.Fatal("locked C parser returned no tree") + } + t.Cleanup(cTree.Close) + cDigest, err := COracleDeepDigest(cTree) + if err != nil { + t.Fatal(err) + } + if want := templNextExpectedCDigest(witness.name); want != "" && cDigest != want { + t.Fatalf("%s locked-C digest=%s, want %s", witness.name, cDigest, want) + } + + raw := templNextParseRoute(t, goLanguage, witness.source, "raw", func(p *gotreesitter.Parser, source []byte) (*gotreesitter.Tree, error) { + p.SetAdmissionCandidateRoute(false) + return p.ParseNoResultCompatibilityBenchmarkOnly(source) + }) + production := templNextParseRoute(t, goLanguage, witness.source, "production", func(p *gotreesitter.Parser, source []byte) (*gotreesitter.Tree, error) { + p.SetAdmissionCandidateRoute(false) + return p.Parse(source) + }) + + routedBefore, fallbackBefore := gotreesitter.AdmissionCandidateCounters() + compactParser := gotreesitter.NewParser(goLanguage) + compactParser.SetAdmissionCandidateRoute(true) + compact, err := compactParser.Parse(witness.source) + if err != nil { + t.Fatalf("compact parse: %v", err) + } + t.Cleanup(compact.Release) + routedAfter, fallbackAfter := gotreesitter.AdmissionCandidateCounters() + compactRoute := "accepted" + if fallbackAfter > fallbackBefore { + compactRoute = "fallback:" + gotreesitter.AdmissionCandidateLastFallbackReason() + } + + forestParser := gotreesitter.NewParser(goLanguage) + forest, forestOK := forestParser.ParseForestExperimental(witness.source) + forestRoute := "declined" + forestDetail := "" + if forestOK && forest != nil { + forestRoute = "accepted" + t.Cleanup(forest.Release) + } else { + offset, symbol, reason, _ := forestParser.ForestDeclineInfo() + forestDetail = fmt.Sprintf("offset=%d symbol=%d reason=%q", offset, symbol, reason) + } + + incremental, profile := templNextIncrementalRoute(t, goLanguage, witness.source) + + templNextLogRoute(t, "raw", raw, goLanguage, cTree, cDigest, "") + templNextAssertKnownRoute(t, witness.name, "raw", raw, goLanguage, cTree) + templNextLogRoute(t, "production", production, goLanguage, cTree, cDigest, "") + templNextAssertKnownRoute(t, witness.name, "production", production, goLanguage, cTree) + templNextLogRoute(t, "compact", compact, goLanguage, cTree, cDigest, compactRoute) + templNextAssertKnownRoute(t, witness.name, "compact", compact, goLanguage, cTree) + if forest != nil { + templNextLogRoute(t, "forest", forest, goLanguage, cTree, cDigest, forestRoute) + templNextAssertKnownRoute(t, witness.name, "forest", forest, goLanguage, cTree) + } else { + t.Logf("route=forest witness=%s result=declined %s", witness.name, forestDetail) + } + templNextLogRoute(t, "incremental", incremental, goLanguage, cTree, cDigest, fmt.Sprintf("reuse=%t unsupported=%t reason=%q reused_subtrees=%d reused_bytes=%d", profile.OldTreeReuseRoute, profile.ReuseUnsupported, profile.ReuseUnsupportedReason, profile.ReusedSubtrees, profile.ReusedBytes)) + templNextAssertKnownRoute(t, witness.name, "incremental", incremental, goLanguage, cTree) + if profile.ReuseUnsupportedReason != "external_scanner_unsupported" || profile.OldTreeReuseRoute || !profile.ReuseUnsupported { + t.Fatalf("incremental route profile = reuse=%t unsupported=%t reason=%q", profile.OldTreeReuseRoute, profile.ReuseUnsupported, profile.ReuseUnsupportedReason) + } + templNextAssertForestRoute(t, witness.name, forestParser, forestOK) + t.Logf("witness=%s bytes=%d source_sha256=%x c_digest=%s compact=%s counters=%d/%d->%d/%d forest=%s %s incremental_reuse=%t incremental_unsupported=%t incremental_reason=%q", witness.name, len(witness.source), sha256.Sum256(witness.source), cDigest, compactRoute, routedBefore, fallbackBefore, routedAfter, fallbackAfter, forestRoute, forestDetail, profile.OldTreeReuseRoute, profile.ReuseUnsupported, profile.ReuseUnsupportedReason) + templNextAssertCompactRoute(t, witness.name, compactRoute, routedAfter-routedBefore, fallbackAfter-fallbackBefore) + }) + } +} + +func templNextReadA0(t *testing.T, name string) []byte { + t.Helper() + source, err := os.ReadFile(filepath.Join("..", "testdata", "dispatcher_census_a0", "templ", name)) + if err != nil { + t.Fatal(err) + } + return source +} + +func templNextParseRoute(t *testing.T, language *gotreesitter.Language, source []byte, route string, parse func(*gotreesitter.Parser, []byte) (*gotreesitter.Tree, error)) *gotreesitter.Tree { + t.Helper() + parser := gotreesitter.NewParser(language) + tree, err := parse(parser, source) + if err != nil { + t.Fatalf("%s parse: %v", route, err) + } + if tree == nil || tree.RootNode() == nil { + t.Fatalf("%s returned no tree", route) + } + t.Cleanup(tree.Release) + return tree +} + +func templNextIncrementalRoute(t *testing.T, language *gotreesitter.Language, source []byte) (*gotreesitter.Tree, gotreesitter.IncrementalParseProfile) { + t.Helper() + if !bytes.HasSuffix(source, []byte{'\n'}) { + t.Fatalf("incremental witness does not end with newline") + } + base := bytes.TrimSuffix(source, []byte{'\n'}) + parser := gotreesitter.NewParser(language) + oldTree, err := parser.Parse(base) + if err != nil { + t.Fatalf("incremental base parse: %v", err) + } + t.Cleanup(oldTree.Release) + point := templNextPointAtByte(base) + oldTree.Edit(gotreesitter.InputEdit{ + StartByte: uint32(len(base)), + OldEndByte: uint32(len(base)), + NewEndByte: uint32(len(source)), + StartPoint: point, + OldEndPoint: point, + NewEndPoint: templNextPointAtByte(source), + }) + tree, profile, err := parser.ParseIncrementalProfiled(source, oldTree) + if err != nil { + t.Fatalf("incremental parse: %v", err) + } + if tree == nil || tree.RootNode() == nil { + t.Fatal("incremental returned no tree") + } + t.Cleanup(tree.Release) + return tree, profile +} + +func templNextPointAtByte(source []byte) gotreesitter.Point { + var point gotreesitter.Point + for _, b := range source { + if b == '\n' { + point.Row++ + point.Column = 0 + continue + } + point.Column++ + } + return point +} + +func templNextLogRoute(t *testing.T, route string, tree *gotreesitter.Tree, language *gotreesitter.Language, cTree *sitter.Tree, cDigest, detail string) { + t.Helper() + root := tree.RootNode() + inspection, err := benchfixtures.InspectGoTree(root, language) + if err != nil { + t.Fatalf("%s inspect Go tree: %v", route, err) + } + diff := FirstDivergenceDumpV1(root, language, cTree.RootNode()) + checked, run, visited, rewritten := templNextDispatchStats(tree) + if detail != "" { + detail = " " + detail + } + t.Logf("route=%s error=%t digest=%s c_digest=%s divergence=%+v dispatch_checked=%d dispatch_run=%d dispatch_visited=%d dispatch_rewritten=%d%s", route, root.HasError(), inspection.SHA256, cDigest, diff, checked, run, visited, rewritten, detail) +} + +func templNextDispatchStats(tree *gotreesitter.Tree) (checked, run, visited, rewritten uint64) { + runtime := tree.ParseRuntime() + if runtime.NormalizationPasses == nil { + return 0, 0, 0, 0 + } + for _, pass := range *runtime.NormalizationPasses { + if pass.Name != "dispatch.templ" { + continue + } + checked += pass.Checked + run += pass.Run + visited += pass.NodesVisited + rewritten += pass.NodesRewritten + } + return checked, run, visited, rewritten +} + +type templNextExpectedRoute struct { + digest string + error bool + rewrite uint64 + diff *DumpV1Divergence +} + +func templNextExpectedDiff(path, category, goValue, cValue string) *DumpV1Divergence { + return &DumpV1Divergence{Path: path, Category: category, GoValue: goValue, CValue: cValue} +} + +func templNextExpectedCDigest(witness string) string { + switch witness { + case "a0-medium-main": + return "efab90f3a4a75a4deba8c94d67c741dd842a7c8c6708bed3f59e37e0a994a11f" + case "a0-medium-template": + return "7de9788750436a485bee98ec6200da09d5062700368333fe380562d71f171891" + case "a0-small-template": + return "cb81fe10587416eae568216d16d2f7258bda32d00136030d8a4fcd2198e12594" + case "positive-no-op-control": + return "ac11fb7f49572a2132e31c3a46328e17a103c4ae73fd75745427a188c5987e11" + case "qualified-component-import": + return "631955c93c466e736f59aa22039f2558b7685d3b4641b71b874cac52b5e70a23" + case "malformed-dangling-attribute-quote": + return "3e6eb2d96ca843d122d8f1e952fcd465feee7f67d289c8de118c08436f1b4491" + case "malformed-component-import": + return "922cf68ffcfd8e890f27d0cb8fb995b5a5fdfb10c3853bc661bd8c722bbd34cb" + default: + return "" + } +} + +func templNextExpectedRouteFor(witness, route string) (templNextExpectedRoute, bool) { + shapeRaw := func(digest, path, goValue, cValue string) templNextExpectedRoute { + return templNextExpectedRoute{digest: digest, diff: templNextExpectedDiff(path, "shape", goValue, cValue)} + } + shapeNormalized := func(digest string) templNextExpectedRoute { + return shapeRaw(digest, "/source_file/component_declaration[26]/component_block[3]", "children=12", "children=11") + } + errorRoot := templNextExpectedDiff("/source_file", "error", "true", "false") + malformedRoot := templNextExpectedDiff("/source_file", "error", "false", "true") + switch witness { + case "a0-medium-main": + return templNextExpectedRoute{digest: "895657a1c4978896653cf968b2dedddf7badd40f464d558e97dd95a9d9675595", error: true, diff: errorRoot}, true + case "a0-medium-template": + if route == "raw" { + return shapeRaw("33a54940b5da62255e5a03056b2ed7935994773b53a746b9a7e706b60a1a8dcb", "/source_file/component_declaration[26]/component_block[3]", "children=20", "children=11"), true + } + want := shapeNormalized("2499953c81a152ca9db474f121b1a8a9de0c888c6f00a25125301c157bcb0b0e") + want.rewrite = 53 + return want, true + case "a0-small-template": + if route == "raw" { + return shapeRaw("80e67baee0a78d252f4621c42b4eab3e1334bc919bdcba000d17034d04f954f3", "/source_file/component_declaration[3]/component_block[3]/element[2]/element[1]", "children=4", "children=3"), true + } + return templNextExpectedRoute{digest: "cb81fe10587416eae568216d16d2f7258bda32d00136030d8a4fcd2198e12594", rewrite: 23}, true + case "positive-no-op-control": + return templNextExpectedRoute{digest: "ac11fb7f49572a2132e31c3a46328e17a103c4ae73fd75745427a188c5987e11"}, true + case "qualified-component-import": + if route == "raw" { + return shapeRaw("ed7d7f4155a6bdc6153038f7637dd2a14878abfcb26d27bbfc4a30529a4e0da7", "/source_file/component_declaration[1]/component_block[3]", "children=4", "children=3"), true + } + want := templNextExpectedRoute{digest: "631955c93c466e736f59aa22039f2558b7685d3b4641b71b874cac52b5e70a23"} + if route == "production" || route == "forest" || route == "incremental" { + want.rewrite = 15 + } + return want, true + case "malformed-dangling-attribute-quote": + return templNextExpectedRoute{digest: "3e6eb2d96ca843d122d8f1e952fcd465feee7f67d289c8de118c08436f1b4491", error: true}, true + case "malformed-component-import": + return templNextExpectedRoute{digest: "8954432deb8e607319a63b17b665508434a8d87fac8c6fe9923ad1bbe062760f", diff: malformedRoot}, true + default: + return templNextExpectedRoute{}, false + } +} + +func templNextAssertKnownRoute(t *testing.T, witness, route string, tree *gotreesitter.Tree, language *gotreesitter.Language, cTree *sitter.Tree) { + t.Helper() + want, ok := templNextExpectedRouteFor(witness, route) + if !ok { + return + } + root := tree.RootNode() + if root == nil { + t.Fatalf("%s/%s returned no root", witness, route) + } + if root.HasError() != want.error { + t.Fatalf("%s/%s error_root=%t, want %t", witness, route, root.HasError(), want.error) + } + inspection, err := benchfixtures.InspectGoTree(root, language) + if err != nil { + t.Fatalf("%s/%s inspect: %v", witness, route, err) + } + if inspection.SHA256 != want.digest { + t.Fatalf("%s/%s digest=%s, want %s", witness, route, inspection.SHA256, want.digest) + } + diff := FirstDivergenceDumpV1(root, language, cTree.RootNode()) + if want.diff == nil { + if diff != nil { + t.Fatalf("%s/%s divergence=%+v, want exact locked-C parity", witness, route, diff) + } + } else if diff == nil || *diff != *want.diff { + t.Fatalf("%s/%s divergence=%+v, want %+v", witness, route, diff, want.diff) + } + _, _, _, rewritten := templNextDispatchStats(tree) + if rewritten != want.rewrite { + t.Fatalf("%s/%s dispatch.templ rewrites=%d, want %d", witness, route, rewritten, want.rewrite) + } +} + +func templNextAssertCompactRoute(t *testing.T, witness, route string, routedDelta, fallbackDelta uint64) { + t.Helper() + if routedDelta+fallbackDelta != 1 { + t.Fatalf("%s compact counters routed_delta=%d fallback_delta=%d", witness, routedDelta, fallbackDelta) + } + wantFallback := witness == "a0-medium-main" || witness == "a0-medium-template" || witness == "a0-small-template" || witness == "malformed-dangling-attribute-quote" + if wantFallback && (route == "accepted" || routedDelta != 0 || fallbackDelta != 1) { + t.Fatalf("%s compact route=%q counters=%d/%d, want fallback", witness, route, routedDelta, fallbackDelta) + } + if !wantFallback && (route != "accepted" || routedDelta != 1 || fallbackDelta != 0) { + t.Fatalf("%s compact route=%q counters=%d/%d, want accepted", witness, route, routedDelta, fallbackDelta) + } +} + +func templNextAssertForestRoute(t *testing.T, witness string, parser *gotreesitter.Parser, accepted bool) { + t.Helper() + wantDeclined := witness == "a0-medium-main" || witness == "malformed-dangling-attribute-quote" + if wantDeclined != !accepted { + t.Fatalf("%s forest accepted=%t, want %t", witness, accepted, !wantDeclined) + } + if wantDeclined { + offset, symbol, reason, _ := parser.ForestDeclineInfo() + if witness == "a0-medium-main" && (offset != 1025 || symbol != 74 || reason != "dead_end") { + t.Fatalf("%s forest decline=%d/%d/%q, want 1025/74/dead_end", witness, offset, symbol, reason) + } + if witness == "malformed-dangling-attribute-quote" && (offset != 47 || symbol != 24 || reason != "dead_end") { + t.Fatalf("%s forest decline=%d/%d/%q, want 47/24/dead_end", witness, offset, symbol, reason) + } + } +} + +// TestTemplNextLiveArmReceiptDocument guards the final blocker receipt markers. +func TestTemplNextLiveArmReceiptDocument(t *testing.T) { + raw, err := os.ReadFile("../docs/root-normalization-retirement.md") + if err != nil { + t.Fatal(err) + } + document := strings.ReplaceAll(strings.Join(strings.Fields(string(raw)), " "), "`", "") + for _, marker := range []string{ + "Status: NO-GO. KEEP LIVE: dispatch.templ.", + "The authenticated Templ A0 receipt records three files, three checks, three runs, 1138 visited nodes, 76 rewritten nodes, one error root, and zero parse errors.", + "The medium template witness rewrites 53 nodes on production, compact, forest, and incremental routes.", + "The full real-corpus census is unavailable because cgo_harness/corpus_real is absent.", + "Do not change the registry or production state.", + } { + marker = strings.Join(strings.Fields(marker), " ") + if !strings.Contains(document, marker) { + t.Fatalf("Templ blocker receipt lacks marker %q", marker) + } + } +} diff --git a/cgo_harness/wgsl_next_live_probe_test.go b/cgo_harness/wgsl_next_live_probe_test.go new file mode 100644 index 000000000..80095d6fc --- /dev/null +++ b/cgo_harness/wgsl_next_live_probe_test.go @@ -0,0 +1,440 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "bytes" + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +type wgslNextWitness struct { + name string + source []byte + class string +} + +type wgslNextRouteExpectation struct { + digest string + receipt string + diff *DumpV1Divergence + rootError bool + rootErrorSet bool +} + +type wgslNextExpectation struct { + cDigest string + wantError bool + compactRoute string + forestRoute string + reuseSubtrees uint64 + reuseBytes uint64 + routes map[string]wgslNextRouteExpectation + forest *wgslNextRouteExpectation +} + +// TestWGSLNextLiveArmLockedCRoutes records every WGSL route for A0 and controls. +func TestWGSLNextLiveArmLockedCRoutes(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + t.Setenv("GOT_PARSE_PHASE_TIMING", "1") + goLanguage := grammars.WgslLanguage() + cLanguage, err := COracleLanguage("wgsl") + if err != nil { + t.Fatal(err) + } + + witnesses := []wgslNextWitness{ + {name: "a0-small-fragmentTextureQuad", source: wgslReadA0(t, "small__fragmentTextureQuad.wgsl"), class: "a0"}, + {name: "a0-medium-normalMap", source: wgslReadA0(t, "medium__normalMap.wgsl"), class: "a0"}, + {name: "a0-medium-radiosity", source: wgslReadA0(t, "medium__radiosity.wgsl"), class: "a0"}, + {name: "recovery-empty-return", source: []byte("fn malformed() { return; }\n"), class: "recovery"}, + {name: "malformed-missing-expression", source: []byte("fn malformed() { let value: f32 = ; }\n"), class: "malformed"}, + {name: "malformed-argument-list", source: []byte("fn malformed() { textureLoad(texture, coord,); }\n"), class: "malformed"}, + {name: "positive-control", source: []byte("fn identity(value: f32) -> f32 { return value; }\n"), class: "control"}, + } + + for _, witness := range witnesses { + witness := witness + t.Run(witness.name, func(t *testing.T) { + cParser := sitter.NewParser() + t.Cleanup(cParser.Close) + if err := cParser.SetLanguage(cLanguage); err != nil { + t.Fatal(err) + } + cTree := cParser.Parse(witness.source, nil) + if cTree == nil || cTree.RootNode() == nil { + t.Fatal("locked C parser returned no tree") + } + t.Cleanup(cTree.Close) + cDigest, err := COracleDeepDigest(cTree) + if err != nil { + t.Fatal(err) + } + want := wgslNextWant(witness.name) + if cDigest != want.cDigest { + t.Fatalf("locked C digest=%s, want %s", cDigest, want.cDigest) + } + + raw := wgslParseRoute(t, goLanguage, witness.source, "raw", func(p *gotreesitter.Parser, source []byte) (*gotreesitter.Tree, error) { + return p.ParseNoResultCompatibilityBenchmarkOnly(source) + }) + production := wgslParseRoute(t, goLanguage, witness.source, "production", func(p *gotreesitter.Parser, source []byte) (*gotreesitter.Tree, error) { + return p.Parse(source) + }) + compactParser := gotreesitter.NewParser(goLanguage) + compactParser.SetAdmissionCandidateRoute(true) + routedBefore, fallbackBefore := gotreesitter.AdmissionCandidateCounters() + compact := wgslParseRoute(t, goLanguage, witness.source, "compact", func(_ *gotreesitter.Parser, source []byte) (*gotreesitter.Tree, error) { + return compactParser.Parse(source) + }) + routedAfter, fallbackAfter := gotreesitter.AdmissionCandidateCounters() + compactRoute := "accepted" + if fallbackAfter > fallbackBefore { + compactRoute = "fallback:" + gotreesitter.AdmissionCandidateLastFallbackReason() + } + + forestParser := gotreesitter.NewParser(goLanguage) + forest, forestOK := forestParser.ParseForestExperimental(witness.source) + forestRoute := "declined" + if forestOK && forest != nil { + forestRoute = "accepted" + t.Cleanup(forest.Release) + } else { + offset, symbol, reason, _ := forestParser.ForestDeclineInfo() + forestRoute = fmt.Sprintf("declined:%s@%d/%d", reason, offset, symbol) + } + + base := bytes.TrimSuffix(witness.source, []byte{'\n'}) + incrementalParser := gotreesitter.NewParser(goLanguage) + oldTree, err := incrementalParser.Parse(base) + if err != nil { + t.Fatalf("incremental base parse: %v", err) + } + t.Cleanup(oldTree.Release) + oldTree.Edit(gotreesitter.InputEdit{ + StartByte: uint32(len(base)), + OldEndByte: uint32(len(base)), + NewEndByte: uint32(len(witness.source)), + StartPoint: wgslPointAtByte(base), + OldEndPoint: wgslPointAtByte(base), + NewEndPoint: wgslPointAtByte(witness.source), + }) + incremental, profile, err := incrementalParser.ParseIncrementalProfiled(witness.source, oldTree) + if err != nil { + t.Fatalf("incremental parse: %v", err) + } + t.Cleanup(incremental.Release) + + for _, route := range []struct { + name string + tree *gotreesitter.Tree + }{ + {name: "raw", tree: raw}, + {name: "production", tree: production}, + {name: "compact", tree: compact}, + {name: "incremental", tree: incremental}, + } { + if route.tree == nil || route.tree.RootNode() == nil { + t.Fatalf("%s route returned no root", route.name) + } + inspection, err := benchfixtures.InspectGoTree(route.tree.RootNode(), goLanguage) + if err != nil { + t.Fatalf("%s inspect Go tree: %v", route.name, err) + } + diff := FirstDivergenceDumpV1(route.tree.RootNode(), goLanguage, cTree.RootNode()) + expectation, ok := want.routes[route.name] + if !ok { + t.Fatalf("missing %s expectation", route.name) + } + wantError := want.wantError + if expectation.rootErrorSet { + wantError = expectation.rootError + } + if route.tree.RootNode().HasError() != wantError { + t.Fatalf("%s root error=%t, want %t", route.name, route.tree.RootNode().HasError(), wantError) + } + if inspection.SHA256 != expectation.digest { + t.Fatalf("%s digest=%s, want %s", route.name, inspection.SHA256, expectation.digest) + } + if got := wgslDispatchReceipt(route.tree); got != expectation.receipt { + t.Fatalf("%s dispatch receipt=%s, want %s", route.name, got, expectation.receipt) + } + wgslRequireDivergence(t, route.name, diff, expectation.diff) + t.Logf("witness=%s class=%s route=%s bytes=%d source_sha256=%x root_error=%t digest=%s c_digest=%s divergence=%+v dispatch=%s", witness.name, witness.class, route.name, len(witness.source), sha256.Sum256(witness.source), route.tree.RootNode().HasError(), inspection.SHA256, cDigest, diff, wgslDispatchReceipt(route.tree)) + } + if forest != nil { + inspection, err := benchfixtures.InspectGoTree(forest.RootNode(), goLanguage) + if err != nil { + t.Fatalf("forest inspect Go tree: %v", err) + } + diff := FirstDivergenceDumpV1(forest.RootNode(), goLanguage, cTree.RootNode()) + if want.forest == nil { + t.Fatalf("forest returned a tree, want decline %s", want.forestRoute) + } + if forest.RootNode().HasError() != want.wantError { + t.Fatalf("forest root error=%t, want %t", forest.RootNode().HasError(), want.wantError) + } + if inspection.SHA256 != want.forest.digest { + t.Fatalf("forest digest=%s, want %s", inspection.SHA256, want.forest.digest) + } + if got := wgslDispatchReceipt(forest); got != want.forest.receipt { + t.Fatalf("forest dispatch receipt=%s, want %s", got, want.forest.receipt) + } + wgslRequireDivergence(t, "forest", diff, want.forest.diff) + t.Logf("witness=%s class=%s route=forest bytes=%d source_sha256=%x root_error=%t digest=%s c_digest=%s divergence=%+v dispatch=%s", witness.name, witness.class, len(witness.source), sha256.Sum256(witness.source), forest.RootNode().HasError(), inspection.SHA256, cDigest, diff, wgslDispatchReceipt(forest)) + } else if want.forest != nil { + t.Fatalf("forest declined, want digest %s", want.forest.digest) + } + if compactRoute != want.compactRoute { + t.Fatalf("compact route=%q, want %q", compactRoute, want.compactRoute) + } + if forestRoute != want.forestRoute { + t.Fatalf("forest route=%q, want %q", forestRoute, want.forestRoute) + } + if profile.OldTreeReuseRoute != true || profile.ReuseUnsupported { + t.Fatalf("incremental reuse route=%t unsupported=%t reason=%q, want reuse without fallback", profile.OldTreeReuseRoute, profile.ReuseUnsupported, profile.ReuseUnsupportedReason) + } + if profile.ReusedSubtrees != want.reuseSubtrees || profile.ReusedBytes != want.reuseBytes { + t.Fatalf("incremental reuse=%d subtrees/%d bytes, want %d/%d", profile.ReusedSubtrees, profile.ReusedBytes, want.reuseSubtrees, want.reuseBytes) + } + t.Logf("witness=%s compact=%s counters=%d/%d->%d/%d forest=%s incremental_reuse=%t reuse_unsupported=%t reuse_reason=%q reused_subtrees=%d reused_bytes=%d", witness.name, compactRoute, routedBefore, fallbackBefore, routedAfter, fallbackAfter, forestRoute, profile.OldTreeReuseRoute, profile.ReuseUnsupported, profile.ReuseUnsupportedReason, profile.ReusedSubtrees, profile.ReusedBytes) + }) + } +} + +func wgslRequireDivergence(t *testing.T, route string, got, want *DumpV1Divergence) { + t.Helper() + if got == nil || want == nil { + if got != want { + t.Fatalf("%s divergence=%+v, want %+v", route, got, want) + } + return + } + if *got != *want { + t.Fatalf("%s divergence=%+v, want %+v", route, got, want) + } +} + +func wgslDiff(path, category, goValue, cValue string) *DumpV1Divergence { + return &DumpV1Divergence{Path: path, Category: category, GoValue: goValue, CValue: cValue} +} + +func wgslNextWant(name string) wgslNextExpectation { + switch name { + case "a0-small-fragmentTextureQuad": + const digest = "d3e58954c750ed560edd3177a165bbf701c159467a1b4677996bec620c377804" + return wgslNextExpectation{ + cDigest: digest, + compactRoute: "accepted", + forestRoute: "accepted", + reuseSubtrees: 49, + reuseBytes: 192, + routes: map[string]wgslNextRouteExpectation{ + "raw": {digest: digest, receipt: "none"}, + "production": {digest: digest, receipt: "none"}, + "compact": {digest: digest, receipt: "none"}, + "incremental": {digest: digest, receipt: "1/1/112/51"}, + }, + forest: &wgslNextRouteExpectation{digest: digest, receipt: "1/1/111/0"}, + } + case "a0-medium-normalMap": + return wgslNextExpectation{ + cDigest: "231e10ca2215945a5fb51670620c9f5ba2ea1ca7d445cb2c9443fb51b8e0e18a", + wantError: true, + compactRoute: "fallback:compact route declined at recovery [mechanism=recovery-entered]: did not accept EOF: generic scheduler has no table action for the elected token", + forestRoute: "declined:dead_end@0/1", + reuseSubtrees: 452, + reuseBytes: 2318, + routes: map[string]wgslNextRouteExpectation{ + "raw": {digest: "77fdfd002d6937e6f5784fc19e21a6f63ab8f2280ca8ba0dcfc1ee5b1d3d42cc", receipt: "none", diff: wgslDiff("/source_file", "shape", "children=70", "children=79")}, + "production": {digest: "9d802a0e9af71176c0520496ae99425406aa76dc8560f8c7e939e366f9fbbd44", receipt: "1/1/1289/120", diff: wgslDiff("/source_file", "shape", "children=70", "children=79")}, + "compact": {digest: "9d802a0e9af71176c0520496ae99425406aa76dc8560f8c7e939e366f9fbbd44", receipt: "1/1/1289/120", diff: wgslDiff("/source_file", "shape", "children=70", "children=79")}, + "incremental": {digest: "fe0cb9f758eaace140619c81a9ef3347d89633580b18492e46dcfae6fb8f57c7", receipt: "1/1/1287/684", diff: wgslDiff("/source_file", "shape", "children=69", "children=79")}, + }, + } + case "a0-medium-radiosity": + return wgslNextExpectation{ + cDigest: "22b9d004c33c6a8229b56876282125e04efddf59deef6224eddd61f38c9952b2", + wantError: true, + compactRoute: "fallback:compact route declined at recovery [mechanism=recovery-entered]: did not accept EOF: generic scheduler has no table action for the elected token", + forestRoute: "declined:dead_end@247/43", + reuseSubtrees: 185, + reuseBytes: 1197, + routes: map[string]wgslNextRouteExpectation{ + "raw": {digest: "c591b9329ad2fc946b6b8b7c4bc80adb7305f41934dd51d4505e4f606787b127", receipt: "none", diff: wgslDiff("/source_file/global_variable_declaration[3]/variable_declaration[2]/variable_identifier_declaration[2]/type_declaration[2]/ERROR[1]", "type", "ERROR", "<")}, + "production": {digest: "592abfad21a9a3170c11fd2e18888a9c5ecac7f681af5cf81e2d1c352873df63", receipt: "1/1/1230/51", diff: wgslDiff("/source_file/function_declaration[29]", "error", "false", "true")}, + "compact": {digest: "592abfad21a9a3170c11fd2e18888a9c5ecac7f681af5cf81e2d1c352873df63", receipt: "1/1/1230/51", diff: wgslDiff("/source_file/function_declaration[29]", "error", "false", "true")}, + "incremental": {digest: "9e0113134b748e66ba22390bdf09e6e083a7f09dc0fc3b1dec67a407fed979cd", receipt: "1/1/1230/56", diff: wgslDiff("/source_file", "shape", "children=46", "children=48")}, + }, + } + case "recovery-empty-return": + const digest = "909c5e3b5efd2201372daf83848ea91cc459c1069537f89fcebf1f7c49cd58f4" + return wgslNextExpectation{ + cDigest: digest, + compactRoute: "accepted", + forestRoute: "accepted", + reuseSubtrees: 6, + reuseBytes: 20, + routes: map[string]wgslNextRouteExpectation{ + "raw": {digest: digest, receipt: "none"}, + "production": {digest: digest, receipt: "none"}, + "compact": {digest: digest, receipt: "none"}, + "incremental": {digest: digest, receipt: "1/1/12/0"}, + }, + forest: &wgslNextRouteExpectation{digest: digest, receipt: "1/1/12/0"}, + } + case "malformed-missing-expression": + const digest = "dcdd782dbf759ef627faf475817ba14b68ae5b0783e431fc06a1bee5a879e73b" + const normalizedDigest = "88373bc4d0bc650c42c159273d5cc2a6915b2c69334adc3d5e8923a421fce02b" + return wgslNextExpectation{ + cDigest: digest, + wantError: true, + compactRoute: "fallback:compact route declined at recovery [mechanism=recovery-entered]: did not accept EOF: generic scheduler has no table action for the elected token", + forestRoute: "declined:dead_end@34/3", + reuseSubtrees: 9, + reuseBytes: 26, + routes: map[string]wgslNextRouteExpectation{ + "raw": {digest: digest, receipt: "none"}, + "production": {digest: normalizedDigest, receipt: "1/1/19/5", diff: wgslDiff("/source_file", "error", "false", "true"), rootErrorSet: true, rootError: false}, + "compact": {digest: normalizedDigest, receipt: "1/1/19/5", diff: wgslDiff("/source_file", "error", "false", "true"), rootErrorSet: true, rootError: false}, + "incremental": {digest: normalizedDigest, receipt: "1/1/19/5", diff: wgslDiff("/source_file", "error", "false", "true"), rootErrorSet: true, rootError: false}, + }, + } + case "malformed-argument-list": + return wgslNextExpectation{ + cDigest: "4a43e477628bba014be5e5863dc87ab460c14c7232c96aa737414f299a407e81", + wantError: true, + compactRoute: "fallback:compact route declined at recovery [mechanism=recovery-entered]: did not accept EOF: generic scheduler has no table action for the elected token", + forestRoute: "declined:dead_end@28/7", + reuseSubtrees: 6, + reuseBytes: 25, + routes: map[string]wgslNextRouteExpectation{ + "raw": {digest: "40a24a35095668102b40bd1bd92f5576211eaf92b3aac01f24b5e81a6ab755a9", receipt: "none", diff: wgslDiff("/source_file/function_declaration[0]/compound_statement[4]/assignment_statement[1]/parenthesized_expression[2]/ERROR[2]/,[0]", "error", "true", "false")}, + "production": {digest: "2555f0b34dfb5e295bc72d8d5532ceadfd6807686edc2e417a5e8ae911aa9df7", receipt: "1/1/23/5", diff: wgslDiff("/source_file/function_declaration[0]/compound_statement[4]/assignment_statement[1]/compound_assignment_operator[1]", "error", "false", "true")}, + "compact": {digest: "2555f0b34dfb5e295bc72d8d5532ceadfd6807686edc2e417a5e8ae911aa9df7", receipt: "1/1/23/5", diff: wgslDiff("/source_file/function_declaration[0]/compound_statement[4]/assignment_statement[1]/compound_assignment_operator[1]", "error", "false", "true")}, + "incremental": {digest: "2555f0b34dfb5e295bc72d8d5532ceadfd6807686edc2e417a5e8ae911aa9df7", receipt: "1/1/23/5", diff: wgslDiff("/source_file/function_declaration[0]/compound_statement[4]/assignment_statement[1]/compound_assignment_operator[1]", "error", "false", "true")}, + }, + } + case "positive-control": + const digest = "7868a6b43efc8d14b73746e2472a596c9eeb3cd215f734021528aaa3668f05ea" + return wgslNextExpectation{ + cDigest: digest, + compactRoute: "accepted", + forestRoute: "accepted", + reuseSubtrees: 11, + reuseBytes: 37, + routes: map[string]wgslNextRouteExpectation{ + "raw": {digest: digest, receipt: "none"}, + "production": {digest: digest, receipt: "none"}, + "compact": {digest: digest, receipt: "none"}, + "incremental": {digest: digest, receipt: "1/1/24/0"}, + }, + forest: &wgslNextRouteExpectation{digest: digest, receipt: "1/1/24/0"}, + } + default: + panic("missing WGSL expectation for " + name) + } +} + +func wgslReadA0(t *testing.T, name string) []byte { + t.Helper() + source, err := os.ReadFile(filepath.Join("..", "testdata", "dispatcher_census_a0", "wgsl", name)) + if err != nil { + t.Fatal(err) + } + return source +} + +func wgslParseRoute(t *testing.T, language *gotreesitter.Language, source []byte, route string, parse func(*gotreesitter.Parser, []byte) (*gotreesitter.Tree, error)) *gotreesitter.Tree { + t.Helper() + parser := gotreesitter.NewParser(language) + tree, err := parse(parser, source) + if err != nil { + t.Fatalf("%s parse: %v", route, err) + } + if tree == nil || tree.RootNode() == nil { + t.Fatalf("%s parse returned no tree", route) + } + t.Cleanup(tree.Release) + return tree +} + +func wgslDispatchReceipt(tree *gotreesitter.Tree) string { + if tree == nil || tree.ParseRuntime().NormalizationPasses == nil { + return "none" + } + for _, pass := range *tree.ParseRuntime().NormalizationPasses { + if pass.Name == "dispatch.wgsl" { + return fmt.Sprintf("%d/%d/%d/%d", pass.Checked, pass.Run, pass.NodesVisited, pass.NodesRewritten) + } + } + return "none" +} + +func wgslPointAtByte(source []byte) gotreesitter.Point { + var point gotreesitter.Point + for _, value := range source { + if value == '\n' { + point.Row++ + point.Column = 0 + } else { + point.Column++ + } + } + return point +} + +// TestWGSLNextLiveArmReceiptDocument guards the blocker markers. +func TestWGSLNextLiveArmReceiptDocument(t *testing.T) { + raw, err := os.ReadFile("../docs/root-normalization-retirement.md") + if err != nil { + t.Fatal(err) + } + document := strings.Join(strings.Fields(string(raw)), " ") + for _, marker := range []string{ + "Status: NO-GO. KEEP LIVE: `dispatch.wgsl`.", + "The WGSL A0 receipt contains three files, three checks, three runs, 2,630 visited nodes, 171 rewrites, two error roots, and zero parse errors.", + "The tracked census has seven fixtures across six languages. It excludes WGSL.", + "The focused receipt uses seven witnesses.", + "The `normalMap` A0 witness exposes the first live blocker.", + "The malformed missing-expression control matches locked C on its raw route.", + "The malformed argument-list control keeps a live rewrite.", + "The two malformed controls each add five rewrites.", + "The probe changes only the receipt test, changelog, and retirement ledger.", + "Keep `dispatch.wgsl` live until the producer emits the locked-C trees for all registered witnesses and all listed routes.", + } { + marker = strings.Join(strings.Fields(marker), " ") + if !strings.Contains(document, marker) { + t.Fatalf("WGSL blocker receipt lacks marker %q", marker) + } + } +} + +// TestWGSLNextLiveChangelogReceipt guards the changelog marker. +func TestWGSLNextLiveChangelogReceipt(t *testing.T) { + raw, err := os.ReadFile("../CHANGELOG.md") + if err != nil { + t.Fatal(err) + } + document := strings.Join(strings.Fields(string(raw)), " ") + for _, marker := range []string{ + "Record the `dispatch.wgsl` blocker receipt at base `7498a678c52029a82f312e9637ecb66b15defa0b`.", + "The A0 manifest records three WGSL files, three checks, three runs, and 171 rewrites.", + "Both malformed controls rewrite five nodes on normalized routes. The production, compact, and incremental routes for both controls diverge from locked C.", + "No registry or production code changes are included.", + } { + marker = strings.Join(strings.Fields(marker), " ") + if !strings.Contains(document, marker) { + t.Fatalf("WGSL changelog lacks marker %q", marker) + } + } +} diff --git a/cgo_harness/wolfram_retirement_parity_test.go b/cgo_harness/wolfram_retirement_parity_test.go new file mode 100644 index 000000000..0df495a46 --- /dev/null +++ b/cgo_harness/wolfram_retirement_parity_test.go @@ -0,0 +1,146 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +// TestWolframDispatchRetirementLockedCParity compares all three parser-produced +// Wolfram A0 witnesses with the locked C parser on raw and production routes. +func TestWolframDispatchRetirementLockedCParity(t *testing.T) { + entry, ok := parityEntriesByName["wolfram"] + if !ok { + t.Fatal("missing Wolfram grammar entry") + } + language := entry.Language() + cLanguage, err := COracleLanguage("wolfram") + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + file string + sha256 string + }{ + { + name: "evaluation-utilities", + file: "large__EvaluationUtilities.wl", + sha256: "e03c8588214ce3a0a5ba48d1f1335276c1826356052c33df1f3184a6d6303a53", + }, + { + name: "output-handling-utilities", + file: "medium__OutputHandlingUtilities.wl", + sha256: "45a6287c3c8ad5f4f37298d4915d1bfb29e6e91ee0eccde1c842efb7c90e3dec", + }, + { + name: "paclet-info", + file: "small__PacletInfo.m", + sha256: "55be9b6143e5dd68ddb433bb9c95c0388a505b65c452fb6036e064d537e3f602", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + source, err := os.ReadFile(filepath.Join( + "..", "testdata", "dispatcher_census_a0", "wolfram", test.file, + )) + if err != nil { + t.Fatal(err) + } + if got := fmt.Sprintf("%x", sha256.Sum256(source)); got != test.sha256 { + t.Fatalf("source SHA-256 = %s, want %s", got, test.sha256) + } + + rawParser := gotreesitter.NewParser(language) + rawParser.SetAdmissionCandidateRoute(false) + rawTree, err := rawParser.ParseNoResultCompatibilityBenchmarkOnly(source) + if err != nil { + t.Fatalf("raw parse: %v", err) + } + t.Cleanup(rawTree.Release) + rawRuntime := rawTree.ParseRuntime() + if rawRuntime.NormalizationNodesRewritten != 0 { + t.Fatalf("raw normalization rewrote %d nodes", rawRuntime.NormalizationNodesRewritten) + } + + productionParser := gotreesitter.NewParser(language) + productionParser.SetAdmissionCandidateRoute(false) + productionTree, err := productionParser.Parse(source) + if err != nil { + t.Fatalf("production parse: %v", err) + } + t.Cleanup(productionTree.Release) + productionRuntime := productionTree.ParseRuntime() + if productionRuntime.NormalizationNodesRewritten != 0 { + t.Fatalf("production normalization rewrote %d nodes", productionRuntime.NormalizationNodesRewritten) + } + + cParser := sitter.NewParser() + t.Cleanup(cParser.Close) + if err := cParser.SetLanguage(cLanguage); err != nil { + t.Fatal(err) + } + cTree := cParser.Parse(source, nil) + if cTree == nil || cTree.RootNode() == nil { + t.Fatal("C oracle returned a nil tree") + } + t.Cleanup(cTree.Close) + + cDigest, err := COracleDeepDigest(cTree) + if err != nil { + t.Fatalf("inspect C deep tree: %v", err) + } + rawDigest := assertWolframLockedCTreeExact(t, "raw", rawTree, language, cTree, cDigest) + productionDigest := assertWolframLockedCTreeExact(t, "production", productionTree, language, cTree, cDigest) + t.Logf( + "witness=%s bytes=%d source_sha256=%s raw_digest=%s production_digest=%s raw_rewrites=%d production_rewrites=%d", + test.file, + len(source), + test.sha256, + rawDigest, + productionDigest, + rawRuntime.NormalizationNodesRewritten, + productionRuntime.NormalizationNodesRewritten, + ) + }) + } +} + +func assertWolframLockedCTreeExact( + t *testing.T, + label string, + goTree *gotreesitter.Tree, + goLang *gotreesitter.Language, + cTree *sitter.Tree, + wantDigest string, +) string { + t.Helper() + goRoot := goTree.RootNode() + cRoot := cTree.RootNode() + if diff := FirstDivergenceDumpV1(goRoot, goLang, cRoot); diff != nil { + t.Fatalf("%s tree diverges from the locked C oracle: %+v", label, diff) + } + if diff := firstLockedCTreeFlagDivergence(goRoot, goLang, cRoot, "/"+goRoot.Type(goLang)); diff != nil { + t.Fatalf("%s tree has a missing or error flag divergence: %v", label, diff) + } + inspection, err := benchfixtures.InspectGoTree(goRoot, goLang) + if err != nil { + t.Fatalf("inspect %s Go deep tree: %v", label, err) + } + if inspection.SHA256 != wantDigest { + t.Fatalf("%s deep digest Go=%s C=%s", label, inspection.SHA256, wantDigest) + } + t.Logf("%s route matches locked C exactly: symbols, fields, spans, points, extras, missing/error flags, deep digest=%s", label, inspection.SHA256) + return inspection.SHA256 +} diff --git a/diagnostic_token_source_fork.go b/diagnostic_token_source_fork.go new file mode 100644 index 000000000..bd1da46cf --- /dev/null +++ b/diagnostic_token_source_fork.go @@ -0,0 +1,88 @@ +//go:build gts_workcount + +package gotreesitter + +import ( + "crypto/sha256" + "encoding/binary" + "fmt" +) + +// DiagnosticTokenSourceForkReceipt records the state at a diagnostic fork. +// Production builds do not include this type. +type DiagnosticTokenSourceForkReceipt struct { + TokenSourceKind string + CursorByte uint32 + CursorPoint Point + ParserState StateID + ActiveGLRStates []StateID + PendingTokenCount int + PendingTokenDigest [sha256.Size]byte + ZeroWidthOffset int64 + IncludedRangeIndex int + IncludedRangeCount int + IncludedRangeDigest [sha256.Size]byte + ExternalScannerCheckpointPresent bool + ExternalScannerCheckpointDigest [sha256.Size]byte +} + +type diagnosticTokenSourceForker interface { + ForkTokenSourceForDiagnostic() (TokenSource, DiagnosticTokenSourceForkReceipt, error) +} + +func forkTokenSourceForDiagnostic(source TokenSource) (TokenSource, DiagnosticTokenSourceForkReceipt, error) { + if source == nil { + return nil, DiagnosticTokenSourceForkReceipt{}, fmt.Errorf("diagnostic token fork: source is nil") + } + forker, ok := source.(diagnosticTokenSourceForker) + if !ok { + return nil, DiagnosticTokenSourceForkReceipt{}, fmt.Errorf("diagnostic token fork: source %T cannot fork", source) + } + return forker.ForkTokenSourceForDiagnostic() +} + +func (s *includedRangeTokenSource) ForkTokenSourceForDiagnostic() (TokenSource, DiagnosticTokenSourceForkReceipt, error) { + if s == nil || s.base == nil { + return nil, DiagnosticTokenSourceForkReceipt{}, fmt.Errorf("diagnostic token fork: included-range source is nil") + } + if s.idx < 0 || s.idx > len(s.ranges) { + return nil, DiagnosticTokenSourceForkReceipt{}, fmt.Errorf("diagnostic token fork: included-range index %d is outside 0..%d", s.idx, len(s.ranges)) + } + + base, receipt, err := forkTokenSourceForDiagnostic(s.base) + if err != nil { + return nil, DiagnosticTokenSourceForkReceipt{}, err + } + ranges := append([]Range(nil), s.ranges...) + fork := &includedRangeTokenSource{ + base: base, + ranges: ranges, + idx: s.idx, + } + receipt.TokenSourceKind = "included-range/" + receipt.TokenSourceKind + receipt.IncludedRangeIndex = s.idx + receipt.IncludedRangeCount = len(s.ranges) + receipt.IncludedRangeDigest = diagnosticIncludedRangeDigest(s.ranges) + return fork, receipt, nil +} + +func diagnosticIncludedRangeDigest(ranges []Range) [sha256.Size]byte { + hash := sha256.New() + var value [8]byte + write := func(v uint64) { + binary.LittleEndian.PutUint64(value[:], v) + _, _ = hash.Write(value[:]) + } + write(uint64(len(ranges))) + for _, item := range ranges { + write(uint64(item.StartByte)) + write(uint64(item.EndByte)) + write(uint64(item.StartPoint.Row)) + write(uint64(item.StartPoint.Column)) + write(uint64(item.EndPoint.Row)) + write(uint64(item.EndPoint.Column)) + } + var digest [sha256.Size]byte + copy(digest[:], hash.Sum(nil)) + return digest +} diff --git a/diagnostic_token_source_fork_test.go b/diagnostic_token_source_fork_test.go new file mode 100644 index 000000000..38261bb52 --- /dev/null +++ b/diagnostic_token_source_fork_test.go @@ -0,0 +1,165 @@ +//go:build gts_workcount + +package gotreesitter + +import ( + "crypto/sha256" + "reflect" + "testing" +) + +type diagnosticForkStub struct { + tokens []Token + index int + state StateID + glrStates []StateID +} + +func (s *diagnosticForkStub) Next() Token { + if s.index >= len(s.tokens) { + return Token{} + } + token := s.tokens[s.index] + s.index++ + return token +} + +func (s *diagnosticForkStub) SetParserState(state StateID) { + s.state = state +} + +func (s *diagnosticForkStub) SetGLRStates(states []StateID) { + s.glrStates = append(s.glrStates[:0], states...) +} + +func (s *diagnosticForkStub) ForkTokenSourceForDiagnostic() (TokenSource, DiagnosticTokenSourceForkReceipt, error) { + fork := *s + fork.tokens = append([]Token(nil), s.tokens...) + fork.glrStates = append([]StateID(nil), s.glrStates...) + return &fork, DiagnosticTokenSourceForkReceipt{ + TokenSourceKind: "stub", + ParserState: s.state, + ActiveGLRStates: append([]StateID(nil), s.glrStates...), + IncludedRangeIndex: -1, + ExternalScannerCheckpointPresent: true, + ExternalScannerCheckpointDigest: sha256.Sum256([]byte("stub scanner")), + }, nil +} + +func TestDiagnosticIncludedRangeForkIsIndependent(t *testing.T) { + base := &diagnosticForkStub{ + tokens: []Token{ + {Symbol: 1, Text: "outside", StartByte: 0, EndByte: 7}, + {Symbol: 2, Text: "first", StartByte: 12, EndByte: 17}, + {Symbol: 3, Text: "second", StartByte: 31, EndByte: 37}, + {Symbol: 4, Text: "third", StartByte: 38, EndByte: 39}, + {}, + }, + state: 77, + glrStates: []StateID{7, 11}, + } + ranges := []Range{ + {StartByte: 10, EndByte: 20, StartPoint: Point{Column: 10}, EndPoint: Point{Column: 20}}, + {StartByte: 30, EndByte: 40, StartPoint: Point{Row: 1}, EndPoint: Point{Row: 1, Column: 10}}, + } + live := newIncludedRangeTokenSource(base, ranges).(*includedRangeTokenSource) + if token := live.Next(); token.Symbol != 2 { + t.Fatalf("first included token symbol = %d, want 2", token.Symbol) + } + if live.idx != 0 { + t.Fatalf("live range index = %d, want 0", live.idx) + } + + liveBaseIndex := base.index + liveRanges := append([]Range(nil), live.ranges...) + forkSource, receipt, err := forkTokenSourceForDiagnostic(live) + if err != nil { + t.Fatalf("forkTokenSourceForDiagnostic failed: %v", err) + } + fork, ok := forkSource.(*includedRangeTokenSource) + if !ok { + t.Fatalf("fork type = %T, want *includedRangeTokenSource", forkSource) + } + forkBase, ok := fork.base.(*diagnosticForkStub) + if !ok { + t.Fatalf("fork base type = %T, want *diagnosticForkStub", fork.base) + } + if !reflect.DeepEqual(fork, live) { + t.Fatalf("fork state differs before advance:\n got: %#v\nwant: %#v", fork, live) + } + if fork == live || forkBase == base { + t.Fatal("fork shares a mutable source object with the live source") + } + if &fork.ranges[0] == &live.ranges[0] { + t.Fatal("fork shares the included-range backing array") + } + if &forkBase.tokens[0] == &base.tokens[0] { + t.Fatal("fork shares the base token backing array") + } + if &forkBase.glrStates[0] == &base.glrStates[0] { + t.Fatal("fork shares the base GLR-state backing array") + } + if got, want := receipt.TokenSourceKind, "included-range/stub"; got != want { + t.Fatalf("receipt kind = %q, want %q", got, want) + } + if receipt.IncludedRangeIndex != live.idx || receipt.IncludedRangeCount != len(live.ranges) { + t.Fatalf("receipt range state = %d/%d, want %d/%d", receipt.IncludedRangeIndex, receipt.IncludedRangeCount, live.idx, len(live.ranges)) + } + if receipt.ParserState != base.state { + t.Fatalf("receipt parser state = %d, want %d", receipt.ParserState, base.state) + } + if !reflect.DeepEqual(receipt.ActiveGLRStates, base.glrStates) { + t.Fatalf("receipt GLR states = %v, want %v", receipt.ActiveGLRStates, base.glrStates) + } + if got, want := receipt.IncludedRangeDigest, diagnosticIncludedRangeDigest(live.ranges); got != want { + t.Fatalf("receipt range digest = %x, want %x", got, want) + } + if !receipt.ExternalScannerCheckpointPresent { + t.Fatal("wrapper discarded the base scanner checkpoint state") + } + + fork.SetParserState(99) + fork.SetGLRStates([]StateID{13, 17}) + if base.state != 77 || !reflect.DeepEqual(base.glrStates, []StateID{7, 11}) { + t.Fatalf("fork state update changed live base: state=%d glr=%v", base.state, base.glrStates) + } + forkToken := fork.Next() + if forkToken.Symbol != 3 || fork.idx != 1 { + t.Fatalf("fork next state = symbol %d at range %d, want symbol 3 at range 1", forkToken.Symbol, fork.idx) + } + if base.index != liveBaseIndex || live.idx != 0 || !reflect.DeepEqual(live.ranges, liveRanges) { + t.Fatalf("fork advance changed live state: base index=%d range index=%d ranges=%v", base.index, live.idx, live.ranges) + } + if liveToken := live.Next(); !reflect.DeepEqual(liveToken, forkToken) { + t.Fatalf("live next token = %+v, want fork token %+v", liveToken, forkToken) + } + + liveBaseIndex = base.index + forkToken = fork.Next() + if forkToken.Symbol != 4 { + t.Fatalf("fork next symbol = %d, want 4", forkToken.Symbol) + } + if base.index != liveBaseIndex || live.idx != 1 { + t.Fatalf("second fork advance changed live state: base index=%d range index=%d", base.index, live.idx) + } + if liveToken := live.Next(); !reflect.DeepEqual(liveToken, forkToken) { + t.Fatalf("second live token = %+v, want fork token %+v", liveToken, forkToken) + } + + fork.ranges[0].EndByte++ + if reflect.DeepEqual(fork.ranges, live.ranges) { + t.Fatal("fork range mutation reached the live wrapper") + } +} + +func TestDiagnosticIncludedRangeForkRejectsUnsupportedBase(t *testing.T) { + base := &stubTokenSource{} + live := newIncludedRangeTokenSource(base, []Range{{StartByte: 1, EndByte: 2}}).(*includedRangeTokenSource) + indexBefore := live.idx + if _, _, err := forkTokenSourceForDiagnostic(live); err == nil { + t.Fatal("fork with an unsupported base succeeded") + } + if base.nextCalls != 0 || base.skipCalls != 0 || live.idx != indexBefore { + t.Fatalf("rejected fork changed live source: next=%d skip=%d range index=%d, want 0/0/%d", base.nextCalls, base.skipCalls, live.idx, indexBefore) + } +} diff --git a/docs/doxygen-normalization-blocker-receipt-proposal.md b/docs/doxygen-normalization-blocker-receipt-proposal.md new file mode 100644 index 000000000..dea3cfceb --- /dev/null +++ b/docs/doxygen-normalization-blocker-receipt-proposal.md @@ -0,0 +1,129 @@ +# Doxygen normalization blocker receipt proposal + +Receipt date: 2026-08-22. + +Base commit: `0c34a681db29a3e8d27e488c5f26d6d7a5f02592`. + +Disposition: `NO-GO`. Keep `dispatch.doxygen` live. Do not remove the arm, +its source file, or its registry entry. + +## Registry denominator + +The registry test passed with these values: + +| Measure | Value | +| --- | ---: | +| Dispatcher arms | 31 | +| Dispatcher languages | 33 | +| Dispatcher predicates | 1 | +| Generic passes | 0 | +| Post-finalization arms | 0 | +| Post-finalization languages | 0 | +| Live entries | 32 | +| Retired entries | 56 | +| Live language labels | 35 | + +The A0 manifest test passed with 14 languages and 14 receipts after JSDoc +retirement. Doxygen retains three A0 fixtures. + +## A0 raw and production receipt + +All three registered A0 Doxygen fixtures produced equal raw and production +trees. Each route reported zero rewrites. + +| Witness | Source SHA-256 | Raw and production digest | Root | Error | +| --- | --- | --- | --- | --- | +| `medium__CMakeLists.txt` | `66408d6539b27d7c49b1e51777605c38c91b6d924267db5109ee00e2a1cfcf41` | `01d09d1ffd9d09af0333bcd887c35e68bcb4a96d15ff0d96c29a1780971b7e04` | `document` | true | +| `medium__metrics.py` | `31622a6c075ffa6f78a16af6e379f517213d42ff67729bbd0d10551c5fca9702` | `5adbacb1ec949237a802a56a5c95c3c7a1ce17fe9c8db5423b63f083da62d5d1` | `ERROR` | true | +| `small__example.cfg` | `86998161914382f8152e4984db091e7bf486799c1091fc6c57db4e704eee4a3b` | `3b803e3d4b9ffcf99c771c352118f3f7026420ea5f26c8d934349ac848789b23` | `document` | true | + +The A0 census receipt is `files=3 checked=2 run=2 visited=4 rewritten=0 +error_roots=3 parse_errors=0`. The arm does not record a pass for the +`ERROR` root in `medium__metrics.py`. + +The production, compact, forest, and incremental route receipt passed for all +three fixtures. The compact routes fell back with `parser-core fresh-full +runner did not accept EOF`. The forest routes fell back at `dead_end`. The +incremental routes fell back at `external_scanner_unsupported`. Each covered +route recorded zero Doxygen rewrites. + +## Locked-C blocker + +The one-grammar locked-C deep probe used the Doxygen lock +`https://github.com/amaanq/tree-sitter-doxygen` at commit +`ccd998f378c3f9345ea4eeb223f56d7b84d16687`. It used the C runtime contract +`tree-sitter-c-v1`, binding `github.com/tree-sitter/go-tree-sitter@v0.25.0`, +runtime `0.25.1`, and the locked C deep digest. + +The production route diverges from locked C for every A0 fixture: + +- `medium__CMakeLists.txt`: Go digest `01d09d1ffd9d09af0333bcd887c35e68bcb4a96d15ff0d96c29a1780971b7e04`; C digest `d6f623d2b87344001e98de5528b44e38b102e564491871a9ffb64c1b73d193c5`; first divergence `/document`, type `document` versus `ERROR`. +- `medium__metrics.py`: Go digest `5adbacb1ec949237a802a56a5c95c3c7a1ce17fe9c8db5423b63f083da62d5d1`; C digest `6660931c2bf1bf1e0f909a1cac1e4cd8446853ae4466781c943e28fbcc61e860`; first divergence `/ERROR`, children `0` versus `279`. +- `small__example.cfg`: Go digest `3b803e3d4b9ffcf99c771c352118f3f7026420ea5f26c8d934349ac848789b23`; C digest `f1938d5c7bc544856a5df6c204af75af10a5395bd1f89f560c74caef5acf191f`; first divergence `/document`, type `document` versus `ERROR`. + +The registered smoke witness passes locked C exactly. Its source SHA-256 is +`e2d564b999c40b0a53450771ffa82adf7880375449e8628fefd118aae21056d7`, and +its raw, production, and C digest is +`1ae089a98760be594f06d0820951e01714097e99621cc2cd4428ce09ba867083`. + +## Historical trigger evidence + +The live arm has actual rewrites on the historical trigger shapes that its +unit test protects: + +- Childless whole-comment `ERROR`: source SHA-256 + `ff90d209911d0d32bf44ebff0742e6f42ff40a6f4978860a00ec3f7228b2af24`. + The direct arm changes `(ERROR (ERROR (_multiline_begin)))` to `(ERROR)`; + the transcript records a root `child_count_delta=-1` and the census records + `3` rewritten nodes. The production digest is + `0e1129b2130636e62dd05b2494c22a9a2b5b6ec044aea2eeb4dc836380e38b38`, which + matches locked C. Raw digest is + `6c16ff1b99a3b116d575f90aa0fe5456381b442a58af021dac36e6954345ce4c`. +- Recovered document: source SHA-256 + `f6deae068bcf0fe684f8623d671ee5dfbfab47c93d7827ec03c3b4b5330f8309`. + The direct arm changes the root from `ERROR` to `document`, removes two + root children, and records `14` rewritten nodes. The production digest is + `21374502deb13653ec081dd59a4e21311f501aa9adfd34ea1fe3a2f09bc5f8d5`. + Locked C reports digest + `05813d8b13788902a7f9b9322ca16127ecf5e9c3694d60726acc7a511be622fe` and + first diverges at `/document`, children `4` versus `3`. + +The route receipt confirms that these rewrites remain live on every covered +route. The childless witness reports `3` rewrites on compact fallback at +`compact route error`, forest fallback at `eof_no_root`, and incremental +fallback at `external_scanner_unsupported`. The recovered-document witness +reports `14` rewrites on compact fallback at `accepted-leaf-tiling-gap`, +forest fallback at `dead_end`, and incremental fallback at +`external_scanner_unsupported`. + +## Census and structural trace + +The mounted real-corpus census passed with 20 languages, 5 inert arms, 15 +active arms, 14 uncovered registered arms, and 31 languages without a +dispatcher arm. Doxygen was uncovered because the mounted real corpus has no +Doxygen directory. The dedicated A0 fixtures supply its current firing +receipt. + +Canopy traced the call path as follows: + +`runLanguageResultCompatibility` -> `dispatcherArmCensus` -> +`normalizeDoxygenCompatibility` -> `normalizeDoxygenWholeBlockCommentError` +-> `doxygenErrorTreeHasRecoveredStructure` and +`retypeDoxygenRecoveredErrorRoot`. + +## Docker artifacts + +- A0 registry and route receipt: + `harness_out/docker/20260822T201143Z-doxygen-a0-routes-final-main`. +- Locked-C deep parity receipt: + `harness_out/docker/20260822T200654Z-doxygen-locked-c-deep-main`. +- Historical rewrite route receipt: + `harness_out/docker/20260822T201217Z-doxygen-historical-routes-final-main`. +- Direct-arm transcript receipt: + `harness_out/docker/20260822T201002Z-doxygen-direct-arm-transcript-main`. +- Post-JSDoc full census with the corpus mounted: + `harness_out/docker/20260822T200749Z-doxygen-census-after-jsdoc-main-mounted`. +- Post-JSDoc registry and A0 manifest gates: + `harness_out/docker/20260822T200855Z-doxygen-registry-post-jsdoc-main2`. + +No production source, registry entry, changelog, or normalizer arm changed. diff --git a/doxygen_probe_bridge.go b/doxygen_probe_bridge.go new file mode 100644 index 000000000..da6350f16 --- /dev/null +++ b/doxygen_probe_bridge.go @@ -0,0 +1,21 @@ +//go:build doxygen_probe + +package gotreesitter + +// DoxygenProbeNormalization applies the live result normalizer to a raw root +// and returns the direct arm counters for the temporary probe. +func DoxygenProbeNormalization(root *Node, source []byte, lang *Language) (before, after string, checked, run, visited, rewritten uint64) { + if root == nil || lang == nil { + return "", "", 0, 0, 0, 0 + } + before = root.SExpr(lang) + p := &Parser{language: lang} + _ = normalizeResultCompatibility(root, source, p, nil) + after = root.SExpr(lang) + for _, pass := range p.normalizationStats.namedPasses { + if pass.name == "dispatch.doxygen" { + return before, after, pass.checked, pass.run, pass.nodesVisited, pass.nodesRewritten + } + } + return before, after, p.normalizationStats.passesChecked, p.normalizationStats.passesRun, p.normalizationStats.nodesVisited, p.normalizationStats.nodesRewritten +} diff --git a/doxygen_probe_test.go b/doxygen_probe_test.go new file mode 100644 index 000000000..111b18e1a --- /dev/null +++ b/doxygen_probe_test.go @@ -0,0 +1,254 @@ +//go:build doxygen_probe + +package gotreesitter_test + +import ( + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" +) + +func TestDoxygenA0RawProductionProbe(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + lang := grammars.DoxygenLanguage() + paths := []string{ + "testdata/dispatcher_census_a0/doxygen/medium__CMakeLists.txt", + "testdata/dispatcher_census_a0/doxygen/medium__metrics.py", + "testdata/dispatcher_census_a0/doxygen/small__example.cfg", + } + for _, rel := range paths { + rel := rel + t.Run(filepath.Base(rel), func(t *testing.T) { + source, err := os.ReadFile(rel) + if err != nil { + t.Fatal(err) + } + probeDoxygenRawProduction(t, lang, source, rel) + }) + } +} + +func TestDoxygenPositiveRewriteProbe(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + lang := grammars.DoxygenLanguage() + for _, tc := range []struct{ name, source string }{ + {"registered_smoke", grammars.ParseSmokeSample("doxygen")}, + {"childless_error", "/** Adds all words in \\a s to document \\a doc with weight \\a wfd */"}, + {"recovered_document", "/**\\n * @param {int} value\\n * @brief Example\\n */"}, + } { + t.Run(tc.name, func(t *testing.T) { probeDoxygenRawProduction(t, lang, []byte(tc.source), tc.name) }) + } +} + +func TestDoxygenDirectArmMutationProbe(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + lang := grammars.DoxygenLanguage() + for _, tc := range []struct { + name, source string + }{ + {"childless_error", "/** Adds all words in \\a s to document \\a doc with weight \\a wfd */"}, + {"recovered_document", "/**\n * @param {int} value\n * @brief Example\n */"}, + } { + t.Run(tc.name, func(t *testing.T) { + rawParser := gotreesitter.NewParser(lang) + rawParser.SetAdmissionCandidateRoute(false) + tree, err := rawParser.ParseNoResultCompatibilityBenchmarkOnly([]byte(tc.source)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(tree.Release) + before, after, checked, run, visited, rewritten := gotreesitter.DoxygenProbeNormalization(tree.RootNode(), []byte(tc.source), lang) + t.Logf("source=%q before=%s after=%s checked=%d run=%d visited=%d rewritten=%d", tc.source, before, after, checked, run, visited, rewritten) + if transcriptPath := os.Getenv("GTS_DISPATCHER_TRANSCRIPT_OUT"); transcriptPath != "" { + transcript, readErr := os.ReadFile(transcriptPath) + if readErr != nil { + t.Fatalf("read dispatcher transcript: %v", readErr) + } + t.Logf("dispatcher_transcript=%s", transcript) + } + }) + } +} + +func probeDoxygenRawProduction(t *testing.T, lang *gotreesitter.Language, source []byte, label string) { + t.Helper() + rawParser := gotreesitter.NewParser(lang) + rawParser.SetAdmissionCandidateRoute(false) + raw, err := rawParser.ParseNoResultCompatibilityBenchmarkOnly(source) + if err != nil { + t.Fatalf("raw parse: %v", err) + } + t.Cleanup(raw.Release) + productionParser := gotreesitter.NewParser(lang) + productionParser.SetAdmissionCandidateRoute(false) + production, err := productionParser.Parse(source) + if err != nil { + t.Fatalf("production parse: %v", err) + } + t.Cleanup(production.Release) + rawInspect, err := benchfixtures.InspectGoTree(raw.RootNode(), lang) + if err != nil { + t.Fatalf("raw inspect: %v", err) + } + productionInspect, err := benchfixtures.InspectGoTree(production.RootNode(), lang) + if err != nil { + t.Fatalf("production inspect: %v", err) + } + rawRuntime, productionRuntime := raw.ParseRuntime(), production.ParseRuntime() + rootRaw, rootProduction := raw.RootNode(), production.RootNode() + t.Logf("label=%s bytes=%d source_sha256=%x raw_digest=%s production_digest=%s raw_rewrites=%d production_rewrites=%d raw_passes=%d production_passes=%d raw_root=%s[%d,%d) raw_error=%t production_root=%s[%d,%d) production_error=%t raw_sexpr=%s production_sexpr=%s", label, len(source), sha256.Sum256(source), rawInspect.SHA256, productionInspect.SHA256, rawRuntime.NormalizationNodesRewritten, productionRuntime.NormalizationNodesRewritten, rawRuntime.NormalizationPassesRun, productionRuntime.NormalizationPassesRun, rootRaw.Type(lang), rootRaw.StartByte(), rootRaw.EndByte(), rootRaw.HasError(), rootProduction.Type(lang), rootProduction.StartByte(), rootProduction.EndByte(), rootProduction.HasError(), rootRaw.SExpr(lang), rootProduction.SExpr(lang)) +} + +func TestDoxygenA0RouteTrace(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + lang := grammars.DoxygenLanguage() + paths := []string{ + "testdata/dispatcher_census_a0/doxygen/medium__CMakeLists.txt", + "testdata/dispatcher_census_a0/doxygen/medium__metrics.py", + "testdata/dispatcher_census_a0/doxygen/small__example.cfg", + } + for _, rel := range paths { + rel := rel + t.Run(filepath.Base(rel), func(t *testing.T) { + source, err := os.ReadFile(rel) + if err != nil { + t.Fatal(err) + } + productionParser := gotreesitter.NewParser(lang) + productionParser.SetAdmissionCandidateRoute(false) + production, err := productionParser.Parse(source) + if err != nil { + t.Fatalf("production parse: %v", err) + } + t.Cleanup(production.Release) + want, err := benchfixtures.InspectGoTree(production.RootNode(), lang) + if err != nil { + t.Fatal(err) + } + traceDoxygenRoutes(t, lang, source, want.SHA256) + }) + } +} + +func TestDoxygenHistoricalRouteTrace(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + lang := grammars.DoxygenLanguage() + for _, tc := range []struct { + name, source string + }{ + {"childless_error", "/** Adds all words in \\a s to document \\a doc with weight \\a wfd */"}, + {"recovered_document", "/**\n * @param {int} value\n * @brief Example\n */"}, + } { + t.Run(tc.name, func(t *testing.T) { + productionParser := gotreesitter.NewParser(lang) + productionParser.SetAdmissionCandidateRoute(false) + production, err := productionParser.Parse([]byte(tc.source)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(production.Release) + want, err := benchfixtures.InspectGoTree(production.RootNode(), lang) + if err != nil { + t.Fatal(err) + } + t.Logf("source_sha256=%x production_digest=%s production_sexpr=%s", sha256.Sum256([]byte(tc.source)), want.SHA256, production.RootNode().SExpr(lang)) + traceDoxygenRoutes(t, lang, []byte(tc.source), want.SHA256) + }) + } +} + +func traceDoxygenRoutes(t *testing.T, lang *gotreesitter.Language, source []byte, wantDigest string) { + t.Helper() + check := func(route string, tree *gotreesitter.Tree) { + t.Helper() + inspection, err := benchfixtures.InspectGoTree(tree.RootNode(), lang) + if err != nil { + t.Fatalf("%s inspect: %v", route, err) + } + runtime := tree.ParseRuntime() + var named []gotreesitter.NormalizationPassRuntime + if runtime.NormalizationPasses != nil { + named = *runtime.NormalizationPasses + } + t.Logf("route=%s digest=%s root=%s[%d,%d) error=%t passes=%d checked=%d visited=%d rewritten=%d forest=%t named=%+v", route, inspection.SHA256, tree.RootNode().Type(lang), tree.RootNode().StartByte(), tree.RootNode().EndByte(), tree.RootNode().HasError(), runtime.NormalizationPassesRun, runtime.NormalizationPassesChecked, runtime.NormalizationNodesVisited, runtime.NormalizationNodesRewritten, runtime.ForestFastPath, named) + if inspection.SHA256 != wantDigest { + t.Fatalf("%s digest=%s want production=%s", route, inspection.SHA256, wantDigest) + } + t.Cleanup(tree.Release) + } + compactParser := gotreesitter.NewParser(lang) + compactParser.SetAdmissionCandidateRoute(true) + routedBefore, fallbackBefore := gotreesitter.AdmissionCandidateCounters() + compact, err := compactParser.Parse(source) + if err != nil { + t.Fatalf("compact parse: %v", err) + } + routedAfter, fallbackAfter := gotreesitter.AdmissionCandidateCounters() + compactRoute := "compact-direct" + if routedAfter == routedBefore && fallbackAfter == fallbackBefore+1 { + compactRoute = "compact-fallback:" + gotreesitter.AdmissionCandidateLastFallbackReason() + } + check(compactRoute, compact) + forestParser := gotreesitter.NewParser(lang) + forest, ok := forestParser.ParseForestExperimental(source) + if ok && forest != nil { + check("forest-direct", forest) + } else { + if forest != nil { + forest.Release() + } + _, _, reason, _ := forestParser.ForestDeclineInfo() + fallback, fallbackErr := forestParser.Parse(source) + if fallbackErr != nil { + t.Fatalf("forest fallback parse: %v", fallbackErr) + } + check("forest-fallback:"+reason, fallback) + } + if len(source) == 0 { + return + } + oldSource := append([]byte(nil), source...) + oldSource = append(oldSource, ' ') + incrementalParser := gotreesitter.NewParser(lang) + incrementalParser.SetAdmissionCandidateRoute(false) + oldTree, err := incrementalParser.Parse(oldSource) + if err != nil { + t.Fatalf("incremental base parse: %v", err) + } + t.Cleanup(oldTree.Release) + p := doxygenProbePointAtByte(source, len(source)) + oldTree.Edit(gotreesitter.InputEdit{StartByte: uint32(len(source)), OldEndByte: uint32(len(oldSource)), NewEndByte: uint32(len(source)), StartPoint: p, OldEndPoint: doxygenProbePointAtByte(oldSource, len(oldSource)), NewEndPoint: p}) + incremental, profile, err := incrementalParser.ParseIncrementalProfiled(source, oldTree) + if err != nil { + t.Fatalf("incremental parse: %v", err) + } + route := "incremental-fresh" + if profile.ReuseUnsupported { + route = "incremental-fallback:" + profile.ReuseUnsupportedReason + } else if profile.OldTreeReuseRoute && profile.ReusedSubtrees > 0 { + route = fmt.Sprintf("incremental-reuse:%d:%d", profile.ReusedSubtrees, profile.ReusedBytes) + } + check(route, incremental) +} + +func doxygenProbePointAtByte(source []byte, offset int) gotreesitter.Point { + var p gotreesitter.Point + for i, b := range source { + if i >= offset { + break + } + if b == '\n' { + p.Row++ + p.Column = 0 + } else { + p.Column++ + } + } + return p +} diff --git a/grammars/c_token_source_diagnostic_fork.go b/grammars/c_token_source_diagnostic_fork.go new file mode 100644 index 000000000..a0cf39f80 --- /dev/null +++ b/grammars/c_token_source_diagnostic_fork.go @@ -0,0 +1,101 @@ +//go:build gts_workcount && (!grammar_subset || grammar_subset_c || grammar_subset_cpp) + +package grammars + +import ( + "crypto/sha256" + "encoding/binary" + "fmt" + "hash" + + "github.com/odvcencio/gotreesitter" +) + +// ForkTokenSourceForDiagnostic copies the current C token-source state. +// The fork shares only immutable source bytes, language data, and lexer tables. +func (ts *CTokenSource) ForkTokenSourceForDiagnostic() (gotreesitter.TokenSource, gotreesitter.DiagnosticTokenSourceForkReceipt, error) { + if ts == nil { + return nil, gotreesitter.DiagnosticTokenSourceForkReceipt{}, fmt.Errorf("diagnostic C token fork: source is nil") + } + if ts.cur.offset < 0 || ts.cur.offset > len(ts.cur.src) { + return nil, gotreesitter.DiagnosticTokenSourceForkReceipt{}, fmt.Errorf("diagnostic C token fork: cursor %d is outside source length %d", ts.cur.offset, len(ts.cur.src)) + } + if uint64(ts.cur.offset) > uint64(^uint32(0)) { + return nil, gotreesitter.DiagnosticTokenSourceForkReceipt{}, fmt.Errorf("diagnostic C token fork: cursor %d exceeds the token offset limit", ts.cur.offset) + } + if len(ts.src) != len(ts.cur.src) || (len(ts.src) > 0 && &ts.src[0] != &ts.cur.src[0]) { + return nil, gotreesitter.DiagnosticTokenSourceForkReceipt{}, fmt.Errorf("diagnostic C token fork: source cursor does not share the immutable source") + } + + fork := *ts + fork.pending = diagnosticCloneCTokens(ts.pending) + fork.glrStates = diagnosticCloneCStates(ts.glrStates) + + receipt := gotreesitter.DiagnosticTokenSourceForkReceipt{ + TokenSourceKind: "c", + CursorByte: uint32(ts.cur.offset), + CursorPoint: ts.cur.point(), + ParserState: ts.parserState, + ActiveGLRStates: diagnosticCloneCStates(ts.glrStates), + PendingTokenCount: len(ts.pending), + PendingTokenDigest: diagnosticCTokenDigest(ts.pending), + ZeroWidthOffset: int64(ts.lastSyntheticOffset), + IncludedRangeIndex: -1, + } + return &fork, receipt, nil +} + +func diagnosticCloneCTokens(tokens []gotreesitter.Token) []gotreesitter.Token { + if tokens == nil { + return nil + } + clone := make([]gotreesitter.Token, len(tokens)) + copy(clone, tokens) + return clone +} + +func diagnosticCloneCStates(states []gotreesitter.StateID) []gotreesitter.StateID { + if states == nil { + return nil + } + clone := make([]gotreesitter.StateID, len(states)) + copy(clone, states) + return clone +} + +func diagnosticCTokenDigest(tokens []gotreesitter.Token) [sha256.Size]byte { + digest := sha256.New() + diagnosticWriteUint64(digest, uint64(len(tokens))) + for _, token := range tokens { + diagnosticWriteUint64(digest, uint64(token.Symbol)) + diagnosticWriteUint64(digest, uint64(len(token.Text))) + _, _ = digest.Write([]byte(token.Text)) + diagnosticWriteUint64(digest, uint64(token.StartByte)) + diagnosticWriteUint64(digest, uint64(token.EndByte)) + diagnosticWriteUint64(digest, uint64(token.StartPoint.Row)) + diagnosticWriteUint64(digest, uint64(token.StartPoint.Column)) + diagnosticWriteUint64(digest, uint64(token.EndPoint.Row)) + diagnosticWriteUint64(digest, uint64(token.EndPoint.Column)) + diagnosticWriteBool(digest, token.Missing) + diagnosticWriteBool(digest, token.NoLookahead) + diagnosticWriteBool(digest, token.ExternalScannerToken) + diagnosticWriteUint64(digest, uint64(token.ExternalScannerStartByte)) + } + var out [sha256.Size]byte + copy(out[:], digest.Sum(nil)) + return out +} + +func diagnosticWriteUint64(digest hash.Hash, value uint64) { + var encoded [8]byte + binary.LittleEndian.PutUint64(encoded[:], value) + _, _ = digest.Write(encoded[:]) +} + +func diagnosticWriteBool(digest hash.Hash, value bool) { + if value { + _, _ = digest.Write([]byte{1}) + return + } + _, _ = digest.Write([]byte{0}) +} diff --git a/grammars/c_token_source_diagnostic_fork_test.go b/grammars/c_token_source_diagnostic_fork_test.go new file mode 100644 index 000000000..b51bbd7f8 --- /dev/null +++ b/grammars/c_token_source_diagnostic_fork_test.go @@ -0,0 +1,308 @@ +//go:build gts_workcount && (!grammar_subset || grammar_subset_c || grammar_subset_cpp) + +package grammars + +import ( + "bytes" + "crypto/sha256" + "reflect" + "testing" + + "github.com/odvcencio/gotreesitter" +) + +type cDiagnosticMutableState struct { + cursor sourceCursor + done bool + pending []gotreesitter.Token + preprocState int + parserState gotreesitter.StateID + glrStates []gotreesitter.StateID + lastSyntheticOffset int + preprocDefineNameEnd int + preprocOpaqueArgPending bool + preprocOpaqueArgActive bool +} + +func captureCDiagnosticMutableState(source *CTokenSource) cDiagnosticMutableState { + return cDiagnosticMutableState{ + cursor: source.cur, + done: source.done, + pending: append([]gotreesitter.Token(nil), source.pending...), + preprocState: source.preprocState, + parserState: source.parserState, + glrStates: append([]gotreesitter.StateID(nil), source.glrStates...), + lastSyntheticOffset: source.lastSyntheticOffset, + preprocDefineNameEnd: source.preprocDefineNameEnd, + preprocOpaqueArgPending: source.preprocOpaqueArgPending, + preprocOpaqueArgActive: source.preprocOpaqueArgActive, + } +} + +func assertCDiagnosticStateEqual(t *testing.T, got, want cDiagnosticMutableState) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Fatalf("C token-source state changed:\n got: %#v\nwant: %#v", got, want) + } +} + +func assertCDiagnosticForkStateEqual(t *testing.T, fork, live *CTokenSource) { + t.Helper() + if !reflect.DeepEqual(fork, live) { + t.Fatalf("C fork state differs before advance:\n got: %#v\nwant: %#v", fork, live) + } +} + +func advanceCDiagnosticTokens(source *CTokenSource, count int) []gotreesitter.Token { + out := make([]gotreesitter.Token, count) + for index := range out { + out[index] = source.Next() + } + return out +} + +func requireCDiagnosticTokenSequenceEqual(t *testing.T, got, want []gotreesitter.Token) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Fatalf("token sequence differs:\n got: %#v\nwant: %#v", got, want) + } +} + +func TestCTokenSourceDiagnosticForkPreservesPendingTokens(t *testing.T) { + lang := CLanguage() + sourceBytes := []byte("const char *value = \"a\\n\"; int tail;\n") + live, err := NewCTokenSource(sourceBytes, lang) + if err != nil { + t.Fatalf("NewCTokenSource failed: %v", err) + } + t.Cleanup(live.Close) + live.SetParserState(101) + live.SetGLRStates([]gotreesitter.StateID{4, 8, 15}) + + foundOpener := false + for index := 0; index < 12; index++ { + if token := live.Next(); token.Text == "\"" { + foundOpener = true + break + } + } + if !foundOpener { + t.Fatal("setup did not reach a string opener") + } + if len(live.pending) < 2 { + t.Fatalf("pending token count = %d, want at least 2", len(live.pending)) + } + liveBefore := captureCDiagnosticMutableState(live) + sourceBefore := append([]byte(nil), live.src...) + + forkSource, receipt, err := live.ForkTokenSourceForDiagnostic() + if err != nil { + t.Fatalf("ForkTokenSourceForDiagnostic failed: %v", err) + } + fork, ok := forkSource.(*CTokenSource) + if !ok { + t.Fatalf("fork type = %T, want *CTokenSource", forkSource) + } + t.Cleanup(fork.Close) + assertCDiagnosticForkStateEqual(t, fork, live) + if fork == live { + t.Fatal("fork shares the live C token-source object") + } + if &fork.pending[0] == &live.pending[0] { + t.Fatal("fork shares the pending-token backing array") + } + if &fork.glrStates[0] == &live.glrStates[0] { + t.Fatal("fork shares the GLR-state backing array") + } + if &receipt.ActiveGLRStates[0] == &live.glrStates[0] || &receipt.ActiveGLRStates[0] == &fork.glrStates[0] { + t.Fatal("receipt shares a mutable GLR-state backing array") + } + if got, want := receipt.TokenSourceKind, "c"; got != want { + t.Fatalf("receipt kind = %q, want %q", got, want) + } + if receipt.CursorByte != uint32(live.cur.offset) || receipt.CursorPoint != live.cur.point() { + t.Fatalf("receipt cursor = %d/%+v, want %d/%+v", receipt.CursorByte, receipt.CursorPoint, live.cur.offset, live.cur.point()) + } + if receipt.ParserState != live.parserState { + t.Fatalf("receipt parser state = %d, want %d", receipt.ParserState, live.parserState) + } + if !reflect.DeepEqual(receipt.ActiveGLRStates, live.glrStates) { + t.Fatalf("receipt GLR states = %v, want %v", receipt.ActiveGLRStates, live.glrStates) + } + if receipt.PendingTokenCount != len(live.pending) || receipt.PendingTokenDigest != diagnosticCTokenDigest(live.pending) { + t.Fatalf("receipt pending state = %d/%x, want %d/%x", receipt.PendingTokenCount, receipt.PendingTokenDigest, len(live.pending), diagnosticCTokenDigest(live.pending)) + } + if receipt.ExternalScannerCheckpointPresent || receipt.ExternalScannerCheckpointDigest != [sha256.Size]byte{} { + t.Fatalf("C receipt reports external scanner state: present=%v digest=%x", receipt.ExternalScannerCheckpointPresent, receipt.ExternalScannerCheckpointDigest) + } + + forkTokens := advanceCDiagnosticTokens(fork, 7) + assertCDiagnosticStateEqual(t, captureCDiagnosticMutableState(live), liveBefore) + if !bytes.Equal(live.src, sourceBefore) { + t.Fatalf("fork advance changed live source bytes: got %q, want %q", live.src, sourceBefore) + } + liveTokens := advanceCDiagnosticTokens(live, 7) + requireCDiagnosticTokenSequenceEqual(t, forkTokens, liveTokens) + + fork.glrStates[0] = 99 + receipt.ActiveGLRStates[0] = 100 + if live.glrStates[0] != 4 { + t.Fatalf("fork or receipt mutation changed live GLR state to %d", live.glrStates[0]) + } +} + +func TestCTokenSourceDiagnosticForkPreservesPreprocessorState(t *testing.T) { + lang := CLanguage() + sourceBytes := []byte("#if __has_include()\nint enabled;\n") + live, err := NewCTokenSource(sourceBytes, lang) + if err != nil { + t.Fatalf("NewCTokenSource failed: %v", err) + } + t.Cleanup(live.Close) + + setup := advanceCDiagnosticTokens(live, 3) + if got, want := setup[2].Text, "("; got != want { + t.Fatalf("third setup token text = %q, want %q", got, want) + } + if live.preprocState != cPreprocConditionalExpr || !live.preprocOpaqueArgActive || live.preprocOpaqueArgPending { + t.Fatalf("setup preprocessor state = %d/%v/%v", live.preprocState, live.preprocOpaqueArgPending, live.preprocOpaqueArgActive) + } + liveBefore := captureCDiagnosticMutableState(live) + + forkSource, _, err := live.ForkTokenSourceForDiagnostic() + if err != nil { + t.Fatalf("ForkTokenSourceForDiagnostic failed: %v", err) + } + fork := forkSource.(*CTokenSource) + t.Cleanup(fork.Close) + assertCDiagnosticForkStateEqual(t, fork, live) + if fork.preprocState != live.preprocState || fork.preprocOpaqueArgPending != live.preprocOpaqueArgPending || fork.preprocOpaqueArgActive != live.preprocOpaqueArgActive { + t.Fatalf("fork preprocessor state = %d/%v/%v, want %d/%v/%v", fork.preprocState, fork.preprocOpaqueArgPending, fork.preprocOpaqueArgActive, live.preprocState, live.preprocOpaqueArgPending, live.preprocOpaqueArgActive) + } + + forkTokens := advanceCDiagnosticTokens(fork, 6) + assertCDiagnosticStateEqual(t, captureCDiagnosticMutableState(live), liveBefore) + liveTokens := advanceCDiagnosticTokens(live, 6) + requireCDiagnosticTokenSequenceEqual(t, forkTokens, liveTokens) +} + +func TestCTokenSourceDiagnosticForkPreservesDefinitionBoundary(t *testing.T) { + lang := CLanguage() + sourceBytes := []byte("#define FLAG(x) x\nint tail;\n") + live, err := NewCTokenSource(sourceBytes, lang) + if err != nil { + t.Fatalf("NewCTokenSource failed: %v", err) + } + t.Cleanup(live.Close) + + setup := advanceCDiagnosticTokens(live, 2) + if got, want := setup[1].Text, "FLAG"; got != want { + t.Fatalf("second setup token text = %q, want %q", got, want) + } + if live.preprocState != cPreprocAfterDefineName || live.preprocDefineNameEnd != live.cur.offset { + t.Fatalf("definition boundary state = %d/%d, want %d/%d", live.preprocState, live.preprocDefineNameEnd, cPreprocAfterDefineName, live.cur.offset) + } + liveBefore := captureCDiagnosticMutableState(live) + + forkSource, _, err := live.ForkTokenSourceForDiagnostic() + if err != nil { + t.Fatalf("ForkTokenSourceForDiagnostic failed: %v", err) + } + fork := forkSource.(*CTokenSource) + t.Cleanup(fork.Close) + assertCDiagnosticForkStateEqual(t, fork, live) + if fork.preprocDefineNameEnd != live.preprocDefineNameEnd { + t.Fatalf("fork definition boundary = %d, want %d", fork.preprocDefineNameEnd, live.preprocDefineNameEnd) + } + + forkTokens := advanceCDiagnosticTokens(fork, 6) + assertCDiagnosticStateEqual(t, captureCDiagnosticMutableState(live), liveBefore) + liveTokens := advanceCDiagnosticTokens(live, 6) + requireCDiagnosticTokenSequenceEqual(t, forkTokens, liveTokens) +} + +func TestCTokenSourceDiagnosticForkPreservesZeroWidthState(t *testing.T) { + lang := CLanguage() + sourceBytes := []byte("#ifdef __cplusplus\nextern \"C\" {\n#endif\n\nint x;\n\n#ifdef __cplusplus\n}\n#endif\n") + live, err := NewCTokenSource(sourceBytes, lang) + if err != nil { + t.Fatalf("NewCTokenSource failed: %v", err) + } + t.Cleanup(live.Close) + live.cur.advanceBytes(66) + live.SetParserState(10) + liveBefore := captureCDiagnosticMutableState(live) + + forkSource, receipt, err := live.ForkTokenSourceForDiagnostic() + if err != nil { + t.Fatalf("ForkTokenSourceForDiagnostic failed: %v", err) + } + fork := forkSource.(*CTokenSource) + t.Cleanup(fork.Close) + assertCDiagnosticForkStateEqual(t, fork, live) + if got, want := receipt.ZeroWidthOffset, int64(-1); got != want { + t.Fatalf("receipt zero-width offset = %d, want %d", got, want) + } + + forkSynthetic := fork.Next() + if forkSynthetic.Symbol != live.endifSymbol || forkSynthetic.StartByte != 66 || forkSynthetic.EndByte != 66 || !forkSynthetic.Missing { + t.Fatalf("fork synthetic token = %+v, want a missing #endif at byte 66", forkSynthetic) + } + assertCDiagnosticStateEqual(t, captureCDiagnosticMutableState(live), liveBefore) + liveSynthetic := live.Next() + if !reflect.DeepEqual(liveSynthetic, forkSynthetic) { + t.Fatalf("live synthetic token = %+v, want %+v", liveSynthetic, forkSynthetic) + } + + guardedForkSource, guardedReceipt, err := live.ForkTokenSourceForDiagnostic() + if err != nil { + t.Fatalf("guarded ForkTokenSourceForDiagnostic failed: %v", err) + } + guardedFork := guardedForkSource.(*CTokenSource) + t.Cleanup(guardedFork.Close) + assertCDiagnosticForkStateEqual(t, guardedFork, live) + if got, want := guardedReceipt.ZeroWidthOffset, int64(66); got != want { + t.Fatalf("guarded receipt zero-width offset = %d, want %d", got, want) + } + liveBeforeGuardedAdvance := captureCDiagnosticMutableState(live) + forkNext := guardedFork.Next() + assertCDiagnosticStateEqual(t, captureCDiagnosticMutableState(live), liveBeforeGuardedAdvance) + if liveNext := live.Next(); !reflect.DeepEqual(liveNext, forkNext) { + t.Fatalf("guarded live token = %+v, want fork token %+v", liveNext, forkNext) + } +} + +func TestCTokenSourceDiagnosticForkPreservesDoneState(t *testing.T) { + lang := CLanguage() + live, err := NewCTokenSource([]byte("int"), lang) + if err != nil { + t.Fatalf("NewCTokenSource failed: %v", err) + } + t.Cleanup(live.Close) + _ = live.Next() + _ = live.Next() + if !live.done { + t.Fatal("setup source is not done") + } + liveBefore := captureCDiagnosticMutableState(live) + + forkSource, receipt, err := live.ForkTokenSourceForDiagnostic() + if err != nil { + t.Fatalf("ForkTokenSourceForDiagnostic failed: %v", err) + } + fork := forkSource.(*CTokenSource) + t.Cleanup(fork.Close) + assertCDiagnosticForkStateEqual(t, fork, live) + if !fork.done { + t.Fatal("fork did not preserve the done state") + } + if receipt.CursorByte != uint32(len(live.src)) || receipt.CursorPoint != (gotreesitter.Point{Column: 3}) { + t.Fatalf("done receipt cursor = %d/%+v", receipt.CursorByte, receipt.CursorPoint) + } + + forkToken := fork.Next() + assertCDiagnosticStateEqual(t, captureCDiagnosticMutableState(live), liveBefore) + if liveToken := live.Next(); !reflect.DeepEqual(liveToken, forkToken) { + t.Fatalf("done live token = %+v, want fork token %+v", liveToken, forkToken) + } +} diff --git a/p17_attribution_test.go b/p17_attribution_test.go new file mode 100644 index 000000000..9fd99c76b --- /dev/null +++ b/p17_attribution_test.go @@ -0,0 +1,671 @@ +//go:build p17_attribution + +package gotreesitter + +// This file is a temporary P17 attribution harness. It must not remain in the +// worktree after the evidence packet is frozen. + +import ( + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "runtime/pprof" + "sort" + "strings" + "testing" + "time" + "unsafe" +) + +type p17Manifest struct { + Schema string `json:"schema"` + Edits []p17EditSpec `json:"edits"` +} + +type p17EditSpec struct { + Name string `json:"name"` + Language string `json:"language"` + Fixture string `json:"fixture"` + Role string `json:"role"` + StartByte int `json:"start_byte"` + OldEndByte int `json:"old_end_byte"` + OldText string `json:"old_text"` + NewText string `json:"new_text"` + SourceSHA256 string `json:"source_sha256"` + EditedSHA256 string `json:"edited_sha256"` +} + +type p17Case struct { + Spec p17EditSpec + Source []byte + Edited []byte +} + +type p17ArenaSample struct { + CaseName string `json:"case"` + Direction string `json:"direction"` + Mode string `json:"mode"` + Repeat int `json:"repeat"` + SourceLen int `json:"source_len"` + TargetLen int `json:"target_len"` + HintBefore int `json:"incremental_arena_hint_before"` + HintAfter int `json:"incremental_arena_hint_after"` + CapacityTarget int `json:"parse_incremental_arena_node_capacity_target"` + InitialPrimary int `json:"initial_primary_arena_capacity"` + ArenaClass string `json:"returned_arena_class"` + PrimaryLen int `json:"primary_arena_len"` + PrimaryCap int `json:"primary_arena_cap"` + ArenaUsed int `json:"arena_used"` + PrimaryBytes int64 `json:"primary_arena_bytes"` + OverflowCaps []int `json:"overflow_slab_capacities"` + OverflowBytes int64 `json:"overflow_node_bytes"` + TotalNodeBytes int64 `json:"total_node_bytes"` + OldTarget int `json:"old_geometric_target"` + OldCrossesUsed bool `json:"old_geometric_target_crosses_used"` + ExactCrosses bool `json:"exact_target_crosses_used"` + BytesPerOp uint64 `json:"bytes_per_op"` + AllocsPerOp uint64 `json:"allocs_per_op"` + NewNodes uint64 `json:"new_nodes_allocated"` + ReusedSubtrees uint64 `json:"reused_subtrees"` + ReusedBytes uint64 `json:"reused_bytes"` + TokensConsumed uint64 `json:"tokens_consumed"` + MaxStacksSeen int `json:"max_stacks_seen"` + EntryPeak uint64 `json:"entry_scratch_peak"` + StopReason string `json:"stop_reason"` + ArenaBytes int64 `json:"arena_bytes_allocated"` + ScratchBytes int64 `json:"scratch_bytes_allocated"` + EntryBytes int64 `json:"entry_scratch_bytes_allocated"` + GSSBytes int64 `json:"gss_bytes_allocated"` + ParserLoopNs int64 `json:"parser_loop_nanos"` + DispatchNs int64 `json:"action_dispatch_nanos"` + MergeNs int64 `json:"glr_merge_nanos"` + ProfilePath string `json:"profile_path,omitempty"` + ArenaEvents []p17ArenaEvent `json:"arena_events,omitempty"` +} + +type p17Aggregate struct { + CaseName string `json:"case"` + Direction string `json:"direction"` + Mode string `json:"mode"` + SourceLen int `json:"source_len"` + TargetLen int `json:"target_len"` + HintBefore int `json:"incremental_arena_hint_before"` + HintAfter int `json:"incremental_arena_hint_after"` + CapacityTarget int `json:"parse_incremental_arena_node_capacity_target"` + InitialPrimary int `json:"initial_primary_arena_capacity"` + ArenaClass string `json:"returned_arena_class"` + PrimaryLen int `json:"primary_arena_len"` + PrimaryCap int `json:"primary_arena_cap"` + ArenaUsed int `json:"arena_used"` + PrimaryBytes int64 `json:"primary_arena_bytes"` + OverflowCaps []int `json:"overflow_slab_capacities"` + OverflowBytes int64 `json:"overflow_node_bytes"` + TotalNodeBytes int64 `json:"total_node_bytes"` + OldTarget int `json:"old_geometric_target"` + OldCrossesUsed bool `json:"old_geometric_target_crosses_used"` + ExactCrosses bool `json:"exact_target_crosses_used"` + ArenaBytes int64 `json:"arena_bytes_allocated"` + ScratchBytes int64 `json:"scratch_bytes_allocated"` + EntryBytes int64 `json:"entry_scratch_bytes_allocated"` + GSSBytes int64 `json:"gss_bytes_allocated"` + MedianBytes uint64 `json:"median_bytes_per_op"` + MedianAllocs uint64 `json:"median_allocs_per_op"` + MinBytes uint64 `json:"min_bytes_per_op"` + MaxBytes uint64 `json:"max_bytes_per_op"` + MinAllocs uint64 `json:"min_allocs_per_op"` + MaxAllocs uint64 `json:"max_allocs_per_op"` + Samples []p17ArenaSample `json:"samples"` +} + +type p17Receipt struct { + Schema string `json:"schema"` + GeneratedUTC string `json:"generated_utc"` + GoVersion string `json:"go_version"` + NodeSize int `json:"node_size"` + ManifestSHA string `json:"manifest_sha256"` + SourceSHA string `json:"tracked_source_sha256,omitempty"` + Samples []p17Aggregate `json:"aggregates"` + Sequences []p17SequenceReceipt `json:"sequences,omitempty"` +} + +type p17SequenceStep struct { + Iteration int `json:"iteration"` + Direction string `json:"direction"` + SourceLen int `json:"source_len"` + TargetLen int `json:"target_len"` + HintBefore int `json:"incremental_arena_hint_before"` + HintAfter int `json:"incremental_arena_hint_after"` + CapacityTarget int `json:"parse_incremental_arena_node_capacity_target"` + InitialPrimary int `json:"initial_primary_arena_capacity"` + ArenaClass string `json:"returned_arena_class"` + PrimaryLen int `json:"primary_arena_len"` + ArenaUsed int `json:"arena_used"` + OverflowCaps []int `json:"overflow_slab_capacities"` + OverflowBytes int64 `json:"overflow_node_bytes"` + OldTarget int `json:"old_geometric_target"` + OldCrossesUsed bool `json:"old_geometric_target_crosses_used"` + ExactCrosses bool `json:"exact_target_crosses_used"` + BytesPerOp uint64 `json:"bytes_per_op"` + AllocsPerOp uint64 `json:"allocs_per_op"` + NewNodes uint64 `json:"new_nodes_allocated"` + ArenaBytes int64 `json:"arena_bytes_allocated"` + StopReason string `json:"stop_reason"` + ArenaEvents []p17ArenaEvent `json:"arena_events,omitempty"` +} + +type p17SequenceReceipt struct { + CaseName string `json:"case"` + Cycles int `json:"cycles"` + ProfilePath string `json:"profile_path,omitempty"` + Steps []p17SequenceStep `json:"steps"` +} + +type p17ArenaEvent struct { + Kind string `json:"kind"` + Class string `json:"arena_class"` + Before int `json:"before"` + Requested int `json:"requested"` + After int `json:"after"` + Used int `json:"used"` +} + +type p17ArenaTrace struct { + Events []p17ArenaEvent + Count int +} + +var p17CurrentArenaTrace *p17ArenaTrace + +func p17TraceArenaEvent(kind string, class arenaClass, before, requested, after, used int) { + trace := p17CurrentArenaTrace + if trace == nil || trace.Count >= len(trace.Events) { + return + } + trace.Events[trace.Count] = p17ArenaEvent{ + Kind: kind, Class: p17ArenaClassName(class), Before: before, + Requested: requested, After: after, Used: used, + } + trace.Count++ +} + +func p17ArenaTraceSnapshot(trace *p17ArenaTrace) []p17ArenaEvent { + if trace == nil || trace.Count == 0 { + return nil + } + out := make([]p17ArenaEvent, trace.Count) + copy(out, trace.Events[:trace.Count]) + return out +} + +func TestP17CanonicalArenaAttribution(t *testing.T) { + if os.Getenv("P17_OUT_DIR") == "" { + t.Skip("P17_OUT_DIR is required") + } + manifestBytes, err := os.ReadFile("cgo_harness/testdata/canonical_go_incremental_edits.json") + if err != nil { + t.Fatal(err) + } + var manifest p17Manifest + if err := json.Unmarshal(manifestBytes, &manifest); err != nil { + t.Fatal(err) + } + fixtureByID := make(map[string][]byte) + var cases []p17Case + for _, spec := range manifest.Edits { + if spec.Language != "go" || spec.Role != "representative" { + continue + } + source, ok := fixtureByID[spec.Fixture] + if !ok { + asset := map[string]string{ + "query_compile": "internal/benchfixtures/testdata/query_compile.go.gz", + "language": "internal/benchfixtures/testdata/language.go.gz", + "grammargen_lr": "internal/benchfixtures/testdata/grammargen_lr.go.gz", + "rewrite": "internal/benchfixtures/testdata/rewrite.go.gz", + }[spec.Fixture] + if asset == "" { + t.Fatalf("fixture %q not found", spec.Fixture) + } + var loadErr error + source, loadErr = p17LoadGzip(asset) + if loadErr != nil { + t.Fatalf("load fixture %s: %v", spec.Fixture, loadErr) + } + fixtureByID[spec.Fixture] = source + } + if got := sha256HexP17(source); got != spec.SourceSHA256 { + t.Fatalf("%s source sha=%s want=%s", spec.Name, got, spec.SourceSHA256) + } + if spec.StartByte < 0 || spec.OldEndByte < spec.StartByte || spec.OldEndByte > len(source) { + t.Fatalf("%s invalid edit range", spec.Name) + } + if got := string(source[spec.StartByte:spec.OldEndByte]); got != spec.OldText { + t.Fatalf("%s old text=%q want=%q", spec.Name, got, spec.OldText) + } + edited := make([]byte, 0, len(source)-(spec.OldEndByte-spec.StartByte)+len(spec.NewText)) + edited = append(edited, source[:spec.StartByte]...) + edited = append(edited, spec.NewText...) + edited = append(edited, source[spec.OldEndByte:]...) + if got := sha256HexP17(edited); got != spec.EditedSHA256 { + t.Fatalf("%s edited sha=%s want=%s", spec.Name, got, spec.EditedSHA256) + } + cases = append(cases, p17Case{Spec: spec, Source: source, Edited: edited}) + } + sort.Slice(cases, func(i, j int) bool { return cases[i].Spec.Name < cases[j].Spec.Name }) + if len(cases) != 4 { + t.Fatalf("representative Go cases=%d want=4", len(cases)) + } + + selectedCase := os.Getenv("P17_CASE") + selectedDirection := os.Getenv("P17_DIRECTION") + selectedMode := os.Getenv("P17_MODE") + profile := strings.TrimSpace(os.Getenv("P17_PROFILE")) != "" + if selectedMode == "alternating" { + sequences := make([]p17SequenceReceipt, 0, len(cases)) + for _, tc := range cases { + if selectedCase != "" && selectedCase != tc.Spec.Name { + continue + } + sequences = append(sequences, p17RunSequence(t, tc, profile, os.Getenv("P17_OUT_DIR"))) + } + if len(sequences) == 0 { + t.Fatal("P17 alternating selection matched no cases") + } + receipt := p17Receipt{ + Schema: "p17-arena-attribution-v1", GeneratedUTC: time.Now().UTC().Format(time.RFC3339Nano), + GoVersion: runtime.Version(), NodeSize: int(unsafe.Sizeof(Node{})), + ManifestSHA: sha256HexP17(manifestBytes), Sequences: sequences, + } + name := "p17-sequence" + if len(sequences) == 1 { + name += "-" + sequences[0].CaseName + } + p17WriteReceipt(t, receipt, os.Getenv("P17_OUT_DIR"), name) + return + } + repeats := 3 + if profile { + repeats = 1 + } + outDir := os.Getenv("P17_OUT_DIR") + if err := os.MkdirAll(outDir, 0o755); err != nil { + t.Fatal(err) + } + var aggregates []p17Aggregate + for _, tc := range cases { + if selectedCase != "" && selectedCase != tc.Spec.Name { + continue + } + for _, direction := range []string{"forward", "reverse"} { + if selectedDirection != "" && selectedDirection != direction { + continue + } + for _, mode := range []string{"first_call", "warmed_parser"} { + if selectedMode != "" && selectedMode != mode { + continue + } + from, to, edit := p17Direction(tc, direction) + aggregate := p17Measure(t, tc.Spec.Name, direction, mode, from, to, edit, repeats, profile, outDir) + aggregates = append(aggregates, aggregate) + } + } + } + if len(aggregates) == 0 { + t.Fatal("P17 selection matched no cases") + } + receipt := p17Receipt{ + Schema: "p17-arena-attribution-v1", + GeneratedUTC: time.Now().UTC().Format(time.RFC3339Nano), + GoVersion: runtime.Version(), + NodeSize: int(unsafe.Sizeof(Node{})), + ManifestSHA: sha256HexP17(manifestBytes), + Samples: aggregates, + } + name := "p17-arena-attribution" + if selectedCase != "" { + name += "-" + selectedCase + } + if selectedDirection != "" { + name += "-" + selectedDirection + } + if selectedMode != "" { + name += "-" + selectedMode + } + p17WriteReceipt(t, receipt, outDir, name) +} + +func p17WriteReceipt(t *testing.T, receipt p17Receipt, outDir, name string) { + t.Helper() + receiptBytes, err := json.MarshalIndent(receipt, "", " ") + if err != nil { + t.Fatal(err) + } + path := filepath.Join(outDir, name+".json") + if err := os.WriteFile(path, append(receiptBytes, '\n'), 0o644); err != nil { + t.Fatal(err) + } + t.Logf("P17 receipt=%s sha256=%s", path, sha256HexP17(append(receiptBytes, '\n'))) +} + +func p17Direction(tc p17Case, direction string) ([]byte, []byte, InputEdit) { + if direction == "forward" { + return tc.Source, tc.Edited, p17InputEdit(tc.Source, tc.Edited, tc.Spec.StartByte, tc.Spec.OldEndByte, tc.Spec.StartByte+len(tc.Spec.NewText)) + } + return tc.Edited, tc.Source, p17InputEdit(tc.Edited, tc.Source, tc.Spec.StartByte, tc.Spec.StartByte+len(tc.Spec.NewText), tc.Spec.StartByte+len(tc.Spec.OldText)) +} + +func p17InputEdit(oldSource, newSource []byte, start, oldEnd, newEnd int) InputEdit { + return InputEdit{ + StartByte: uint32(start), + OldEndByte: uint32(oldEnd), + NewEndByte: uint32(newEnd), + StartPoint: p17PointAt(oldSource, start), + OldEndPoint: p17PointAt(oldSource, oldEnd), + NewEndPoint: p17PointAt(newSource, newEnd), + } +} + +func p17PointAt(source []byte, offset int) Point { + if offset < 0 { + offset = 0 + } + if offset > len(source) { + offset = len(source) + } + row, col := uint32(0), uint32(0) + for _, b := range source[:offset] { + if b == '\n' { + row++ + col = 0 + } else { + col++ + } + } + return Point{Row: row, Column: col} +} + +func p17Measure(t *testing.T, caseName, direction, mode string, from, to []byte, edit InputEdit, repeats int, profile bool, outDir string) p17Aggregate { + t.Helper() + lang, err := LoadLanguage(parserCoreCertifiedGoBlob) + if err != nil { + t.Fatalf("load Go language: %v", err) + } + samples := make([]p17ArenaSample, 0, repeats) + for repeat := 0; repeat < repeats; repeat++ { + DrainArenaPools() + runtime.GC() + parser := NewParser(lang) + initialPrimary := nodeCapacityForClass(arenaClassIncremental) + if mode == "warmed_parser" { + warmOld, err := parser.Parse(from) + if err != nil { + t.Fatalf("%s/%s warm old parse: %v", caseName, direction, err) + } + warmOld.Edit(edit) + warmNew, _, err := parser.ParseIncrementalProfiled(to, warmOld) + if err != nil { + t.Fatalf("%s/%s warm incremental: %v", caseName, direction, err) + } + if warmNew != warmOld { + warmOld.Release() + } + if warmNew.arena != nil { + initialPrimary = len(warmNew.arena.nodes) + } + warmNew.Release() + runtime.GC() + } + oldTree, err := parser.Parse(from) + if err != nil { + t.Fatalf("%s/%s old parse: %v", caseName, direction, err) + } + oldTree.Edit(edit) + hintBefore := parser.incrementalArenaHintCapacity() + target := parseIncrementalArenaNodeCapacity(len(to), hintBefore) + // The full-parse old tree owns a different arena pool. The incremental + // pool is cold for first_call and retains the warm parse for warmed_parser. + // The old tree owns a full-parse arena. Draining the pool is safe here, + // and the incremental parse acquires a fresh incremental arena below. + trace := &p17ArenaTrace{Events: make([]p17ArenaEvent, 128)} + var before, after runtime.MemStats + runtime.ReadMemStats(&before) + if profile { + runtime.MemProfileRate = 1 + } + p17CurrentArenaTrace = trace + newTree, prof, err := parser.ParseIncrementalProfiled(to, oldTree) + p17CurrentArenaTrace = nil + if profile { + runtime.MemProfileRate = 0 + } + if err != nil { + t.Fatalf("%s/%s %s incremental: %v", caseName, direction, mode, err) + } + runtime.ReadMemStats(&after) + if newTree == nil || newTree.arena == nil { + t.Fatalf("%s/%s %s returned tree without arena", caseName, direction, mode) + } + arena := newTree.arena + hintAfter := parser.incrementalArenaHintCapacity() + caps := make([]int, len(arena.nodeSlabs)) + var overflowBytes int64 + for i, slab := range arena.nodeSlabs { + caps[i] = len(slab.data) + overflowBytes += int64(len(slab.data)) * int64(unsafe.Sizeof(Node{})) + } + primaryBytes := int64(len(arena.nodes)) * int64(unsafe.Sizeof(Node{})) + used := arena.used + oldTarget := p17GeometricCapacity(initialPrimary, target) + profilePath := "" + if profile { + profilePath = filepath.Join(outDir, fmt.Sprintf("p17-alloc-%s-%s-%s-%d.pprof", caseName, direction, mode, repeat)) + f, openErr := os.Create(profilePath) + if openErr != nil { + t.Fatalf("create allocation profile: %v", openErr) + } + if writeErr := pprof.Lookup("allocs").WriteTo(f, 0); writeErr != nil { + _ = f.Close() + t.Fatalf("write allocation profile: %v", writeErr) + } + if closeErr := f.Close(); closeErr != nil { + t.Fatalf("close allocation profile: %v", closeErr) + } + } + sample := p17ArenaSample{ + CaseName: caseName, Direction: direction, Mode: mode, Repeat: repeat, + SourceLen: len(from), TargetLen: len(to), HintBefore: hintBefore, HintAfter: hintAfter, + CapacityTarget: target, InitialPrimary: initialPrimary, + ArenaClass: p17ArenaClassName(arena.class), + PrimaryLen: len(arena.nodes), PrimaryCap: cap(arena.nodes), ArenaUsed: used, + PrimaryBytes: primaryBytes, OverflowCaps: caps, OverflowBytes: overflowBytes, + TotalNodeBytes: primaryBytes + overflowBytes, OldTarget: oldTarget, + OldCrossesUsed: used > oldTarget, ExactCrosses: used > target, + BytesPerOp: after.TotalAlloc - before.TotalAlloc, AllocsPerOp: after.Mallocs - before.Mallocs, + NewNodes: prof.NewNodesAllocated, ReusedSubtrees: prof.ReusedSubtrees, ReusedBytes: prof.ReusedBytes, + TokensConsumed: prof.TokensConsumed, MaxStacksSeen: prof.MaxStacksSeen, EntryPeak: prof.EntryScratchPeak, + StopReason: string(prof.StopReason), ParserLoopNs: prof.ParserLoopNanos, + DispatchNs: prof.ActionDispatchNanos, MergeNs: prof.GLRMergeNanos, ProfilePath: profilePath, + ArenaBytes: prof.ArenaBytesAllocated, ScratchBytes: prof.ScratchBytesAllocated, + EntryBytes: prof.EntryScratchBytesAllocated, GSSBytes: prof.GSSBytesAllocated, + ArenaEvents: p17ArenaTraceSnapshot(trace), + } + samples = append(samples, sample) + if newTree != oldTree { + oldTree.Release() + } + newTree.Release() + DrainArenaPools() + runtime.GC() + } + bytes := make([]uint64, len(samples)) + allocs := make([]uint64, len(samples)) + for i, sample := range samples { + bytes[i] = sample.BytesPerOp + allocs[i] = sample.AllocsPerOp + } + sort.Slice(bytes, func(i, j int) bool { return bytes[i] < bytes[j] }) + sort.Slice(allocs, func(i, j int) bool { return allocs[i] < allocs[j] }) + median := func(values []uint64) uint64 { return values[len(values)/2] } + first := samples[0] + return p17Aggregate{ + CaseName: caseName, Direction: direction, Mode: mode, + SourceLen: first.SourceLen, TargetLen: first.TargetLen, + HintBefore: first.HintBefore, HintAfter: first.HintAfter, CapacityTarget: first.CapacityTarget, + InitialPrimary: first.InitialPrimary, PrimaryLen: first.PrimaryLen, PrimaryCap: first.PrimaryCap, + ArenaClass: first.ArenaClass, + ArenaUsed: first.ArenaUsed, PrimaryBytes: first.PrimaryBytes, OverflowCaps: first.OverflowCaps, + OverflowBytes: first.OverflowBytes, TotalNodeBytes: first.TotalNodeBytes, OldTarget: first.OldTarget, + OldCrossesUsed: first.OldCrossesUsed, ExactCrosses: first.ExactCrosses, + ArenaBytes: first.ArenaBytes, ScratchBytes: first.ScratchBytes, + EntryBytes: first.EntryBytes, GSSBytes: first.GSSBytes, + MedianBytes: median(bytes), MedianAllocs: median(allocs), MinBytes: bytes[0], MaxBytes: bytes[len(bytes)-1], + MinAllocs: allocs[0], MaxAllocs: allocs[len(allocs)-1], Samples: samples, + } +} + +func p17RunSequence(t *testing.T, tc p17Case, profile bool, outDir string) p17SequenceReceipt { + t.Helper() + lang, err := LoadLanguage(parserCoreCertifiedGoBlob) + if err != nil { + t.Fatalf("load Go language: %v", err) + } + parser := NewParser(lang) + tree, err := parser.Parse(tc.Source) + if err != nil { + t.Fatalf("%s initial parse: %v", tc.Spec.Name, err) + } + const cycles = 12 + steps := make([]p17SequenceStep, 0, cycles) + var lastIncrementalPrimary int + for i := 0; i < cycles; i++ { + direction := "forward" + from, to, edit := tc.Source, tc.Edited, p17InputEdit(tc.Source, tc.Edited, tc.Spec.StartByte, tc.Spec.OldEndByte, tc.Spec.StartByte+len(tc.Spec.NewText)) + if i%2 == 1 { + direction = "reverse" + from, to, edit = tc.Edited, tc.Source, p17InputEdit(tc.Edited, tc.Source, tc.Spec.StartByte, tc.Spec.StartByte+len(tc.Spec.NewText), tc.Spec.StartByte+len(tc.Spec.OldText)) + } + if tree.Source() == nil { + t.Fatalf("%s sequence tree has nil source before iteration %d", tc.Spec.Name, i) + } + tree.Edit(edit) + hintBefore := parser.incrementalArenaHintCapacity() + target := parseIncrementalArenaNodeCapacity(len(to), hintBefore) + initialPrimary := nodeCapacityForClass(arenaClassIncremental) + if i > 0 && lastIncrementalPrimary > 0 { + initialPrimary = lastIncrementalPrimary + } + trace := &p17ArenaTrace{Events: make([]p17ArenaEvent, 128)} + var before, after runtime.MemStats + runtime.ReadMemStats(&before) + if profile && i == 0 { + runtime.MemProfileRate = 1 + } + p17CurrentArenaTrace = trace + newTree, prof, err := parser.ParseIncrementalProfiled(to, tree) + p17CurrentArenaTrace = nil + if err != nil { + t.Fatalf("%s sequence iteration %d: %v", tc.Spec.Name, i, err) + } + runtime.ReadMemStats(&after) + if newTree == nil || newTree.arena == nil { + t.Fatalf("%s sequence iteration %d returned nil arena", tc.Spec.Name, i) + } + arena := newTree.arena + caps := make([]int, len(arena.nodeSlabs)) + var overflowBytes int64 + for j, slab := range arena.nodeSlabs { + caps[j] = len(slab.data) + overflowBytes += int64(len(slab.data)) * int64(unsafe.Sizeof(Node{})) + } + used := arena.used + oldTarget := p17GeometricCapacity(initialPrimary, target) + steps = append(steps, p17SequenceStep{ + Iteration: i, Direction: direction, SourceLen: len(from), TargetLen: len(to), + HintBefore: hintBefore, HintAfter: parser.incrementalArenaHintCapacity(), CapacityTarget: target, + InitialPrimary: initialPrimary, ArenaClass: p17ArenaClassName(arena.class), + PrimaryLen: len(arena.nodes), ArenaUsed: used, OverflowCaps: caps, OverflowBytes: overflowBytes, + OldTarget: oldTarget, OldCrossesUsed: used > oldTarget, ExactCrosses: used > target, + BytesPerOp: after.TotalAlloc - before.TotalAlloc, AllocsPerOp: after.Mallocs - before.Mallocs, + NewNodes: prof.NewNodesAllocated, ArenaBytes: prof.ArenaBytesAllocated, StopReason: string(prof.StopReason), + ArenaEvents: p17ArenaTraceSnapshot(trace), + }) + if newTree != tree { + if tree.arena != nil && tree.arena.class == arenaClassIncremental { + lastIncrementalPrimary = len(tree.arena.nodes) + } + tree.Release() + } + tree = newTree + } + if profile { + runtime.MemProfileRate = 0 + } + profilePath := "" + if profile { + profilePath = filepath.Join(outDir, "p17-sequence-"+tc.Spec.Name+".pprof") + f, createErr := os.Create(profilePath) + if createErr != nil { + t.Fatal(createErr) + } + if writeErr := pprof.Lookup("allocs").WriteTo(f, 0); writeErr != nil { + _ = f.Close() + t.Fatal(writeErr) + } + if closeErr := f.Close(); closeErr != nil { + t.Fatal(closeErr) + } + } + tree.Release() + DrainArenaPools() + runtime.GC() + return p17SequenceReceipt{CaseName: tc.Spec.Name, Cycles: cycles, ProfilePath: profilePath, Steps: steps} +} + +func p17GeometricCapacity(initial, target int) int { + if initial < minArenaNodeCap { + initial = minArenaNodeCap + } + if target <= initial { + return initial + } + for initial < target { + initial *= 2 + } + return initial +} + +func p17ArenaClassName(class arenaClass) string { + if class == arenaClassFull { + return "full" + } + return "incremental" +} + +func sha256HexP17(data []byte) string { + digest := sha256.Sum256(data) + return hex.EncodeToString(digest[:]) +} + +func p17LoadGzip(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + zr, err := gzip.NewReader(f) + if err != nil { + return nil, err + } + data, readErr := io.ReadAll(zr) + closeErr := zr.Close() + if readErr != nil { + return nil, readErr + } + return data, closeErr +} diff --git a/p25b_issue454_profile_test.go b/p25b_issue454_profile_test.go new file mode 100644 index 000000000..c6ed31676 --- /dev/null +++ b/p25b_issue454_profile_test.go @@ -0,0 +1,184 @@ +package gotreesitter_test + +import ( + "bytes" + "fmt" + "os" + "runtime" + "strconv" + "testing" + "time" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" +) + +func TestP25BIssue454Profile(t *testing.T) { + mode := os.Getenv("P25B_MODE") + if mode == "" { + t.Skip("set P25B_MODE=fresh or incremental") + } + runs := 3 + if raw := os.Getenv("P25B_RUNS"); raw != "" { + value, err := strconv.Atoi(raw) + if err != nil || value < 1 { + t.Fatalf("invalid P25B_RUNS=%q", raw) + } + runs = value + } + + source := benchfixtures.Issue454CSource() + site := bytes.Index(source, []byte("x0")) + if site < 0 { + t.Fatal("C edit marker is absent") + } + edited := append(append([]byte(nil), source[:site]...), source[site+1:]...) + point := p25bPointAt(source, site) + edit := gotreesitter.InputEdit{ + StartByte: uint32(site), + OldEndByte: uint32(site + 1), + NewEndByte: uint32(site), + StartPoint: point, + OldEndPoint: p25bPointAt(source, site+1), + NewEndPoint: point, + } + + gotreesitter.EnableRecoveryRuntimeTelemetry(true) + t.Cleanup(func() { gotreesitter.EnableRecoveryRuntimeTelemetry(false) }) + + lang := grammars.CLanguage() + switch mode { + case "fresh": + p25bRunFresh(t, lang, edited, runs) + case "incremental": + p25bRunIncremental(t, lang, source, edited, edit, runs) + default: + t.Fatalf("unsupported P25B_MODE=%q", mode) + } +} + +func p25bRunFresh(t *testing.T, lang *gotreesitter.Language, source []byte, runs int) { + t.Helper() + parser := gotreesitter.NewParser(lang) + for run := 0; run < runs; run++ { + runtime.GC() + before := p25bMemStats() + started := time.Now() + tree, err := parser.Parse(source) + wall := time.Since(started) + if err != nil { + t.Fatalf("fresh run %d: %v", run, err) + } + if tree == nil || tree.RootNode() == nil { + t.Fatalf("fresh run %d returned no tree", run) + } + runtimeStats := tree.ParseRuntime() + recoveryStats := parser.DebugRecoveryRuntimeStats() + digest := p25bDigest(t, tree, lang) + after := p25bMemStats() + fmt.Printf("P25B_FRESH run=%d bytes=%d wall_ns=%d mallocs=%d total_alloc=%d heap_alloc_delta=%d digest=%s root_end=%d has_error=%v stop=%s nodes=%d arena=%d scratch=%d gss=%d memory_budget=%d memory_stop_source=%s runtime_heap_growth=%d runtime_sys_growth=%d tokens=%d max_stacks=%d parser_loop_ns=%d recovery_entries=%d recovery_cost_competitions=%d recovery_cost_walk_ns=%d\n", + run, len(source), wall.Nanoseconds(), after.Mallocs-before.Mallocs, after.TotalAlloc-before.TotalAlloc, + int64(after.HeapAlloc)-int64(before.HeapAlloc), digest, tree.RootNode().EndByte(), tree.RootNode().HasError(), + runtimeStats.StopReason, runtimeStats.NodesAllocated, runtimeStats.ArenaBytesAllocated, runtimeStats.ScratchBytesAllocated, + runtimeStats.GSSBytesAllocated, runtimeStats.MemoryBudgetBytes, runtimeStats.MemoryBudgetStopSource, + runtimeStats.RuntimeHeapGrowthBytes, runtimeStats.RuntimeSysGrowthBytes, runtimeStats.TokensConsumed, + runtimeStats.MaxStacksSeen, runtimeStats.ParserLoopNanos, recoveryStats.RecoveryEntryCount, + recoveryStats.RecoveryCostCompetitionCount, recoveryStats.RecoveryCostWalkNanos) + p25bPrintAttempts(parser, "fresh", run) + tree.Release() + } +} + +func p25bRunIncremental(t *testing.T, lang *gotreesitter.Language, source, edited []byte, edit gotreesitter.InputEdit, runs int) { + t.Helper() + for run := 0; run < runs; run++ { + parser := gotreesitter.NewParser(lang) + oldTree, err := parser.Parse(source) + if err != nil { + t.Fatalf("base run %d: %v", run, err) + } + baseDigest := p25bDigest(t, oldTree, lang) + runtime.GC() + beforeEdit := p25bMemStats() + editStarted := time.Now() + oldTree.Edit(edit) + editWall := time.Since(editStarted) + afterEdit := p25bMemStats() + parseStarted := time.Now() + incremental, profile, err := parser.ParseIncrementalProfiled(edited, oldTree) + parseWall := time.Since(parseStarted) + if err != nil { + oldTree.Release() + t.Fatalf("incremental run %d: %v", run, err) + } + if incremental == nil || incremental.RootNode() == nil { + t.Fatalf("incremental run %d returned no tree", run) + } + afterParse := p25bMemStats() + runtimeStats := incremental.ParseRuntime() + recoveryStats := parser.DebugRecoveryRuntimeStats() + digest := p25bDigest(t, incremental, lang) + fmt.Printf("P25B_INCREMENTAL run=%d bytes=%d site=%d base_digest=%s incremental_digest=%s edit_ns=%d edit_mallocs=%d edit_total_alloc=%d parse_wall_ns=%d parse_mallocs=%d parse_total_alloc=%d parse_heap_alloc_delta=%d reuse_ns=%d reparse_ns=%d reused_subtrees=%d reused_bytes=%d new_nodes=%d reuse_unsupported=%v reuse_reason=%s old_tree_reuse_route=%v reject_dirty=%d reject_ancestor_dirty=%d reject_root_nonleaf=%d reject_fragile=%d reject_scanner=%d block_splice_steps=%d recover_searches=%d recover_state_checks=%d recover_state_skips=%d recover_symbol_skips=%d recover_lookups=%d recover_hits=%d tokens=%d max_stacks=%d entry_scratch_peak=%d stop=%s nodes=%d arena=%d scratch=%d gss=%d memory_budget=%d memory_stop_source=%s runtime_heap_growth=%d runtime_sys_growth=%d c_recovery_entered=%v c_recovery_dropped_clean=%v retry_passes=%d recovery_entries=%d recovery_cost_competitions=%d recovery_cost_walk_ns=%d parser_loop_ns=%d result_selection_ns=%d result_tree_build_ns=%d normalization_ns=%d root_end=%d has_error=%v\n", + run, len(edited), edit.StartByte, baseDigest, digest, editWall.Nanoseconds(), + afterEdit.Mallocs-beforeEdit.Mallocs, afterEdit.TotalAlloc-beforeEdit.TotalAlloc, + parseWall.Nanoseconds(), afterParse.Mallocs-afterEdit.Mallocs, afterParse.TotalAlloc-afterEdit.TotalAlloc, + int64(afterParse.HeapAlloc)-int64(afterEdit.HeapAlloc), profile.ReuseCursorNanos, profile.ReparseNanos, + profile.ReusedSubtrees, profile.ReusedBytes, profile.NewNodesAllocated, profile.ReuseUnsupported, + profile.ReuseUnsupportedReason, profile.OldTreeReuseRoute, profile.ReuseRejectDirty, + profile.ReuseRejectAncestorDirtyBeforeEdit, profile.ReuseRejectRootNonLeafChanged, + profile.ReuseRejectFragileNonLeaf, profile.ReuseRejectScannerUnquiescent, profile.BlockSpliceSteps, + profile.RecoverSearches, profile.RecoverStateChecks, profile.RecoverStateSkips, profile.RecoverSymbolSkips, + profile.RecoverLookups, profile.RecoverHits, profile.TokensConsumed, profile.MaxStacksSeen, + profile.EntryScratchPeak, profile.StopReason, runtimeStats.NodesAllocated, runtimeStats.ArenaBytesAllocated, + runtimeStats.ScratchBytesAllocated, runtimeStats.GSSBytesAllocated, runtimeStats.MemoryBudgetBytes, + runtimeStats.MemoryBudgetStopSource, runtimeStats.RuntimeHeapGrowthBytes, runtimeStats.RuntimeSysGrowthBytes, + runtimeStats.CRecoveryEnteredErrorState, runtimeStats.CRecoveryDroppedErrorForClean, + runtimeStats.IncrementalAcceptedErrorRetryAttempts, recoveryStats.RecoveryEntryCount, + recoveryStats.RecoveryCostCompetitionCount, recoveryStats.RecoveryCostWalkNanos, runtimeStats.ParserLoopNanos, + runtimeStats.ResultSelectionNanos, runtimeStats.ResultTreeBuildNanos, profile.NormalizationNanos, + incremental.RootNode().EndByte(), incremental.RootNode().HasError()) + p25bPrintAttempts(parser, "incremental", run) + if incremental != oldTree { + oldTree.Release() + } + incremental.Release() + } +} + +func p25bDigest(t *testing.T, tree *gotreesitter.Tree, lang *gotreesitter.Language) string { + t.Helper() + inspection, err := benchfixtures.InspectGoTree(tree.RootNode(), lang) + if err != nil { + t.Fatalf("inspect tree: %v", err) + } + return inspection.SHA256 +} + +func p25bPointAt(source []byte, offset int) gotreesitter.Point { + row := bytes.Count(source[:offset], []byte{'\n'}) + column := offset + if newline := bytes.LastIndexByte(source[:offset], '\n'); newline >= 0 { + column = offset - newline - 1 + } + return gotreesitter.Point{Row: uint32(row), Column: uint32(column)} +} + +func p25bMemStats() runtime.MemStats { + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + return stats +} + +func p25bPrintAttempts(parser *gotreesitter.Parser, phase string, run int) { + for _, attempt := range parser.DebugRecoveryRuntimeAttempts() { + fmt.Printf("P25B_ATTEMPT phase=%s run=%d ordinal=%d rung=%s cause=%s stop=%s truncated=%v full_span=%v has_error=%v wall_ns=%d heap_delta=%d total_alloc=%d mallocs=%d recovery_entries=%d recovery_cost_competitions=%d recovery_cost_walk_ns=%d materialization_ns=%d arena_peak=%d scratch_peak=%d entry_scratch_peak=%d gss_peak=%d nodes=%d max_stacks=%d peak_depth=%d live_versions=%d peak_live_versions=%d selected=%v replaced=%v\n", + phase, run, attempt.Ordinal, attempt.Rung, attempt.Cause, attempt.StopReason, attempt.Truncated, + attempt.AttemptFullSpan, attempt.AttemptHasError, attempt.WallNanos, attempt.HeapAllocDeltaBytes, + attempt.TotalAllocDeltaBytes, attempt.MallocsDelta, attempt.RecoveryEntryCount, + attempt.RecoveryCostCompetitionCount, attempt.RecoveryCostWalkNanos, attempt.MaterializationNanos, + attempt.ArenaBytesPeak, attempt.ScratchBytesPeak, attempt.EntryScratchBytesPeak, attempt.GSSBytesPeak, + attempt.NodesAllocated, attempt.MaxStacksSeen, attempt.PeakStackDepth, attempt.LiveVersions, + attempt.PeakLiveVersions, attempt.CandidateSelected, attempt.CandidateReplacedIncumbent) + } +} diff --git a/p25c_objc_probe_test.go b/p25c_objc_probe_test.go new file mode 100644 index 000000000..ae7d976ec --- /dev/null +++ b/p25c_objc_probe_test.go @@ -0,0 +1,324 @@ +package gotreesitter_test + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "os" + "runtime" + "strconv" + "strings" + "testing" + "time" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" +) + +type p25cOperationReceipt struct { + Label string `json:"label"` + Bytes int `json:"bytes"` + SourceSHA256 string `json:"source_sha256"` + WallNanos int64 `json:"wall_nanos"` + TreeEditNanos int64 `json:"tree_edit_nanos,omitempty"` + HeapAllocDelta int64 `json:"heap_alloc_delta"` + TotalAllocDelta uint64 `json:"total_alloc_delta"` + MallocsDelta uint64 `json:"mallocs_delta"` + TreeDigest string `json:"tree_digest"` + FreshDigest string `json:"fresh_digest,omitempty"` + IncrementalDigest string `json:"incremental_digest,omitempty"` + FreshEqual bool `json:"fresh_equal,omitempty"` + Profile gotreesitter.IncrementalParseProfile `json:"profile,omitempty"` + Runtime gotreesitter.ParseRuntime `json:"runtime"` + Recovery gotreesitter.RecoveryRuntimeStats `json:"recovery"` + Attempts gotreesitter.RecoveryRuntimeAttempts `json:"attempts,omitempty"` + Error string `json:"error,omitempty"` +} + +func TestP25CObjcProbe(t *testing.T) { + target := p25cTargetBytes(t) + source, editOffset := p25cObjcSource(target) + if len(source) != target { + t.Fatalf("source bytes=%d want=%d", len(source), target) + } + if editOffset <= 0 || editOffset >= len(source) || source[editOffset] != 'x' { + t.Fatalf("edit offset=%d does not identify x", editOffset) + } + gotreesitter.EnableRecoveryRuntimeTelemetry(true) + defer gotreesitter.EnableRecoveryRuntimeTelemetry(false) + + t.Logf("P25C_FIXTURE bytes=%d edit_offset=%d source_sha256=%x", len(source), editOffset, sha256.Sum256(source)) + base, baseReceipt := p25cFresh("full", source, editOffset, nil) + if baseReceipt.Error != "" { + t.Fatal(baseReceipt.Error) + } + if base == nil || base.RootNode() == nil { + t.Fatal("base parse returned no root") + } + base.Release() + + for _, kind := range []string{"insert", "delete"} { + t.Run(kind, func(t *testing.T) { + edited, edit := p25cApplyEdit(source, editOffset, kind) + fresh, freshReceipt := p25cFresh("fresh_"+kind, edited, editOffset, nil) + if freshReceipt.Error != "" { + t.Fatal(freshReceipt.Error) + } + if fresh == nil || fresh.RootNode() == nil { + t.Fatal("fresh edited parse returned no root") + } + freshDigest := p25cTreeDigest(fresh, grammars.ObjcLanguage()) + fresh.Release() + old, oldReceipt := p25cFresh("old_"+kind, source, editOffset, nil) + if oldReceipt.Error != "" { + t.Fatal(oldReceipt.Error) + } + if old == nil || old.RootNode() == nil { + t.Fatal("old parse returned no root") + } + t.Cleanup(old.Release) + beforeEdit := time.Now() + old.Edit(edit) + treeEditNanos := time.Since(beforeEdit).Nanoseconds() + parser := gotreesitter.NewParser(grammars.ObjcLanguage()) + parser.SetAdmissionCandidateRoute(false) + var before, after runtime.MemStats + runtime.GC() + runtime.ReadMemStats(&before) + started := time.Now() + incremental, profile, err := parser.ParseIncrementalProfiled(edited, old) + wall := time.Since(started).Nanoseconds() + runtime.ReadMemStats(&after) + receipt := p25cOperationReceipt{ + Label: "incremental_" + kind, + Bytes: len(edited), + SourceSHA256: fmt.Sprintf("%x", sha256.Sum256(edited)), + WallNanos: wall, + TreeEditNanos: treeEditNanos, + HeapAllocDelta: int64(after.HeapAlloc) - int64(before.HeapAlloc), + TotalAllocDelta: after.TotalAlloc - before.TotalAlloc, + MallocsDelta: after.Mallocs - before.Mallocs, + Profile: profile, + } + if err != nil { + receipt.Error = err.Error() + } + if incremental != nil && incremental.RootNode() != nil { + receipt.TreeDigest = p25cTreeDigest(incremental, grammars.ObjcLanguage()) + receipt.Runtime = incremental.ParseRuntime() + receipt.Recovery = parser.DebugRecoveryRuntimeStats() + receipt.Attempts = parser.DebugRecoveryRuntimeAttempts() + } + receipt.FreshDigest = freshDigest + receipt.IncrementalDigest = receipt.TreeDigest + receipt.FreshEqual = receipt.FreshDigest != "" && receipt.FreshDigest == receipt.IncrementalDigest + p25cLogReceipt(t, receipt) + if incremental != nil && incremental != old { + t.Cleanup(incremental.Release) + } + }) + } +} + +func TestP25CObjcCorrectness(t *testing.T) { + target := p25cTargetBytes(t) + source, editOffset := p25cObjcSource(target) + if len(source) != target { + t.Fatalf("source bytes=%d want=%d", len(source), target) + } + for _, kind := range []string{"insert", "delete"} { + t.Run(kind, func(t *testing.T) { + edited, edit := p25cApplyEdit(source, editOffset, kind) + fresh, err := gotreesitter.NewParser(grammars.ObjcLanguage()).Parse(edited) + if err != nil { + t.Fatal(err) + } + if fresh == nil || fresh.RootNode() == nil { + t.Fatal("fresh parse returned no root") + } + t.Cleanup(fresh.Release) + old, err := gotreesitter.NewParser(grammars.ObjcLanguage()).Parse(source) + if err != nil { + t.Fatal(err) + } + if old == nil || old.RootNode() == nil { + t.Fatal("old parse returned no root") + } + t.Cleanup(old.Release) + old.Edit(edit) + parser := gotreesitter.NewParser(grammars.ObjcLanguage()) + parser.SetAdmissionCandidateRoute(false) + incremental, _, err := parser.ParseIncrementalProfiled(edited, old) + if err != nil { + t.Fatal(err) + } + if incremental == nil || incremental.RootNode() == nil { + t.Fatal("incremental parse returned no root") + } + t.Cleanup(func() { + if incremental != old { + incremental.Release() + } + }) + freshRoot := fresh.RootNode() + incrementalRoot := incremental.RootNode() + if got, want := freshRoot.EndByte(), uint32(len(edited)); got != want { + t.Fatalf("fresh root end=%d want=%d", got, want) + } + if got, want := incrementalRoot.EndByte(), uint32(len(edited)); got != want { + t.Fatalf("incremental root end=%d want=%d", got, want) + } + if fresh.ParseRuntime().StopReason != gotreesitter.ParseStopAccepted || + incremental.ParseRuntime().StopReason != gotreesitter.ParseStopAccepted { + t.Fatalf("stop reasons fresh=%s incremental=%s", fresh.ParseRuntime().StopReason, incremental.ParseRuntime().StopReason) + } + if fresh.ParseRuntime().Truncated || incremental.ParseRuntime().Truncated { + t.Fatal("correctness parse truncated") + } + if got, want := freshRoot.HasError(), kind == "delete"; got != want { + t.Fatalf("fresh has_error=%v want=%v", got, want) + } + freshDigest := p25cTreeDigest(fresh, grammars.ObjcLanguage()) + incrementalDigest := p25cTreeDigest(incremental, grammars.ObjcLanguage()) + if freshDigest != incrementalDigest { + t.Fatalf("fresh/incremental digest mismatch fresh=%s incremental=%s", freshDigest, incrementalDigest) + } + t.Logf("P25C_CORRECTNESS kind=%s bytes=%d fresh=%s incremental=%s has_error=%v", kind, len(edited), freshDigest, incrementalDigest, freshRoot.HasError()) + }) + } +} + +func p25cFresh(label string, source []byte, editOffset int, _ *gotreesitter.Parser) (*gotreesitter.Tree, p25cOperationReceipt) { + parser := gotreesitter.NewParser(grammars.ObjcLanguage()) + parser.SetAdmissionCandidateRoute(false) + runtime.GC() + var before, after runtime.MemStats + runtime.ReadMemStats(&before) + started := time.Now() + tree, err := parser.Parse(source) + wall := time.Since(started).Nanoseconds() + runtime.ReadMemStats(&after) + receipt := p25cOperationReceipt{ + Label: label, + Bytes: len(source), + SourceSHA256: fmt.Sprintf("%x", sha256.Sum256(source)), + WallNanos: wall, + HeapAllocDelta: int64(after.HeapAlloc) - int64(before.HeapAlloc), + TotalAllocDelta: after.TotalAlloc - before.TotalAlloc, + MallocsDelta: after.Mallocs - before.Mallocs, + } + if err != nil { + receipt.Error = err.Error() + } + if tree != nil && tree.RootNode() != nil { + receipt.TreeDigest = p25cTreeDigest(tree, grammars.ObjcLanguage()) + receipt.Runtime = tree.ParseRuntime() + receipt.Recovery = parser.DebugRecoveryRuntimeStats() + receipt.Attempts = parser.DebugRecoveryRuntimeAttempts() + } + if os.Getenv("P25C_LOG_FRESH") != "0" { + fmt.Printf("P25C_RECEIPT %s\n", p25cJSON(receipt)) + } + _ = editOffset + return tree, receipt +} + +func p25cApplyEdit(source []byte, offset int, kind string) ([]byte, gotreesitter.InputEdit) { + replacement := []byte("x") + oldEnd := offset + switch kind { + case "insert": + replacement = []byte("x") + case "delete": + oldEnd = offset + 1 + replacement = nil + default: + panic("unknown edit kind " + kind) + } + edited := make([]byte, 0, len(source)-oldEnd+offset+len(replacement)) + edited = append(edited, source[:offset]...) + edited = append(edited, replacement...) + edited = append(edited, source[oldEnd:]...) + line := uint32(bytesLine(source, offset)) + point := gotreesitter.Point{Row: line, Column: uint32(bytesColumn(source, offset))} + return edited, gotreesitter.InputEdit{ + StartByte: uint32(offset), + OldEndByte: uint32(oldEnd), + NewEndByte: uint32(offset + len(replacement)), + StartPoint: point, + OldEndPoint: point, + NewEndPoint: point, + } +} + +func p25cObjcSource(target int) ([]byte, int) { + const prefix = "@interface P25CBox : NSObject\n@end\n@implementation P25CBox\n" + const suffix = "@end\n" + var b strings.Builder + b.Grow(target) + b.WriteString(prefix) + editOffset := -1 + for i := 0; ; i++ { + line := fmt.Sprintf("- (int)m%d:(int)arg { int x%d = %d; return x%d; }\n", i, i, i, i) + if b.Len()+len(line)+len(suffix) > target { + break + } + if editOffset < 0 { + editOffset = b.Len() + strings.Index(line, "x0") + } + b.WriteString(line) + } + b.WriteString(suffix) + if b.Len() < target { + b.WriteString(strings.Repeat(" ", target-b.Len())) + } + return []byte(b.String()), editOffset +} + +func p25cTargetBytes(t *testing.T) int { + t.Helper() + value := os.Getenv("P25C_BYTES") + if value == "" { + return 4 * 1024 + } + n, err := strconv.Atoi(value) + if err != nil || n < 256 { + t.Fatalf("invalid P25C_BYTES=%q", value) + } + return n +} + +func p25cTreeDigest(tree *gotreesitter.Tree, lang *gotreesitter.Language) string { + if tree == nil || tree.RootNode() == nil { + return "" + } + sum := sha256.Sum256([]byte(tree.RootNode().SExpr(lang))) + return fmt.Sprintf("%x", sum) +} + +func p25cLogReceipt(t *testing.T, receipt p25cOperationReceipt) { + t.Helper() + line := "P25C_RECEIPT " + p25cJSON(receipt) + t.Log(line) + fmt.Println(line) +} + +func p25cJSON(value any) string { + encoded, err := json.Marshal(value) + if err != nil { + return fmt.Sprintf("json_error:%v", err) + } + return string(encoded) +} + +func bytesLine(source []byte, offset int) int { + return strings.Count(string(source[:offset]), "\n") +} + +func bytesColumn(source []byte, offset int) int { + last := strings.LastIndexByte(string(source[:offset]), '\n') + if last < 0 { + return offset + } + return offset - last - 1 +} diff --git a/parser_go_materialization_cycle_regression_test.go b/parser_go_materialization_cycle_regression_test.go new file mode 100644 index 000000000..d05802926 --- /dev/null +++ b/parser_go_materialization_cycle_regression_test.go @@ -0,0 +1,79 @@ +package gotreesitter_test + +import ( + "os" + "testing" + "time" + + "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" +) + +func TestGoRepeatedNestedSubtestsReturnedTreeAcyclic(t *testing.T) { + fixture, err := os.ReadFile("internal/parsercorephase0/core_test.go") + if err != nil { + t.Fatal(err) + } + const start, end = 60255, 68583 + if len(fixture) < end { + t.Fatalf("fixture size = %d, want at least %d", len(fixture), end) + } + source := append([]byte("package p\n"), fixture[start:end]...) + function := append([]byte(nil), source[len("package p\n"):]...) + for range 7 { + source = append(source, function...) + } + + type parseResult struct { + tree *gotreesitter.Tree + err error + } + done := make(chan parseResult, 1) + go func() { + tree, err := gotreesitter.NewParser(grammars.GoLanguage()).Parse(source) + done <- parseResult{tree: tree, err: err} + }() + + var tree *gotreesitter.Tree + select { + case result := <-done: + if result.err != nil { + t.Fatal(result.err) + } + tree = result.tree + case <-time.After(30 * time.Second): + t.Fatal("parse did not return") + } + defer tree.Release() + + root := tree.RootNode() + type frame struct { + node *gotreesitter.Node + child int + } + active := map[*gotreesitter.Node]struct{}{root: {}} + stack := []frame{{node: root}} + for len(stack) > 0 { + top := &stack[len(stack)-1] + if top.child >= top.node.ChildCount() { + delete(active, top.node) + stack = stack[:len(stack)-1] + continue + } + child := top.node.Child(top.child) + top.child++ + if child == nil { + continue + } + if _, ok := active[child]; ok { + t.Fatalf( + "returned tree contains a back-edge to %s [%d,%d)", + child.Type(grammars.GoLanguage()), + child.StartByte(), + child.EndByte(), + ) + } + active[child] = struct{}{} + stack = append(stack, frame{node: child}) + } +} From 3416e9a1175433961ed63b1fb898fccb8bb0bb42 Mon Sep 17 00:00:00 2001 From: Oscar Villavicencio <9220505+odvcencio@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:47:09 -0700 Subject: [PATCH 2/2] add: Add external scanner checkpoint lifecycle - Add external scanner checkpoint lifecycle to manage payload ownership across parse epochs and GLR branches. - Verify checkpoint deserialization before accepting scan results. Roll back state on failed restores. - Track version forks and merge exact duplicates. Discard mismatched branches after failed scans. - Add unit tests for lifecycle opt-in, failed restore cleanup, exact versus mismatched fork handling, and payload isolation. - Extend CGO parity harness with locked-C verification for BitBake and Corn normalization fixtures. - Add Swift probe tests for associated type conformance witnesses and masking shift operators. - Validate Rust dispatch routes against the locked C deep digest. - Expand Issue 454 diagnostics across five file sizes and four target languages. Buckley-Change-Hash: sha256:31565164b2d60bd430f5c14f7d419d021adbe92d98b486f18ce23a9d673e5bb9 Buckley-Change-Stats: files=12 insertions=2026 deletions=0 binaries=0 --- cgo_harness/bitbake_retirement_parity_test.go | 205 ++++++++ cgo_harness/c26b_swift_assoc_minimal.swift | 3 + .../c26b_swift_associatedtype_probe_test.go | 180 +++++++ cgo_harness/c26c_swift_mask_minimal.swift | 1 + .../c26c_swift_masking_shift_probe_test.go | 116 +++++ cgo_harness/corn_retirement_parity_test.go | 148 ++++++ .../issue454_variant_cguard_fivesize_test.go | 60 +++ .../issue454_variant_locked_c_diag_test.go | 94 ++++ .../rust_dispatch_locked_c_probe_test.go | 216 +++++++++ external_scanner_checkpoint_lifecycle.go | 447 ++++++++++++++++++ external_scanner_checkpoint_lifecycle_test.go | 273 +++++++++++ rust_dispatch_probe_test.go | 283 +++++++++++ 12 files changed, 2026 insertions(+) create mode 100644 cgo_harness/bitbake_retirement_parity_test.go create mode 100644 cgo_harness/c26b_swift_assoc_minimal.swift create mode 100644 cgo_harness/c26b_swift_associatedtype_probe_test.go create mode 100644 cgo_harness/c26c_swift_mask_minimal.swift create mode 100644 cgo_harness/c26c_swift_masking_shift_probe_test.go create mode 100644 cgo_harness/corn_retirement_parity_test.go create mode 100644 cgo_harness/issue454_variant_cguard_fivesize_test.go create mode 100644 cgo_harness/issue454_variant_locked_c_diag_test.go create mode 100644 cgo_harness/rust_dispatch_locked_c_probe_test.go create mode 100644 external_scanner_checkpoint_lifecycle.go create mode 100644 external_scanner_checkpoint_lifecycle_test.go create mode 100644 rust_dispatch_probe_test.go diff --git a/cgo_harness/bitbake_retirement_parity_test.go b/cgo_harness/bitbake_retirement_parity_test.go new file mode 100644 index 000000000..f408e24e3 --- /dev/null +++ b/cgo_harness/bitbake_retirement_parity_test.go @@ -0,0 +1,205 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +const bitbakeAddtaskTriggerSource = `SUMMARY = "Test recipe for fetching git submodules" +HOMEPAGE = "http://git.yoctoproject.org/cgit/cgit.cgi/git-submodule-test/" +LICENSE = "MIT" +LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" + +INHIBIT_DEFAULT_DEPS = "1" + +# Note: this is intentionally not the latest version in the original .bb +SRCREV = "f280847494763cdcf71197557a81ba7d8a6bce42" +PV = "0.1+git" +PR = "r2" + +SRC_URI = "gitsm://git.yoctoproject.org/git-submodule-test;branch=master;protocol=https" +UPSTREAM_CHECK_COMMITS = "1" +RECIPE_NO_UPDATE_REASON = "This recipe is used to test devtool upgrade feature" + +EXCLUDE_FROM_WORLD = "1" + +do_test_git_as_user() { + cd ${S} + git status + git submodule status +} +addtask test_git_as_user after do_unpack + +fakeroot do_test_git_as_root() { + cd ${S} + git status + git submodule status +} +do_test_git_as_root[depends] += "virtual/fakeroot-native:do_populate_sysroot" +addtask test_git_as_root after do_unpack` + +const bitbakeFunctionFlagTriggerSource = `do_run_tests () { + meson test -C "${B}" --no-rebuild +} +do_run_tests[doc] = "Run meson test using qemu-user" +addtask do_run_tests after do_compile` + +const bitbakeAdjacentOverrideTriggerSource = `do_install:append() { + ln -sf am335x-bonegreen-ext.dtb "${D}/boot/devicetree/am335x-bonegreen-ext-alias.dtb" +} + +do_deploy:append() { + ln -sf am335x-bonegreen-ext.dtb "${DEPLOYDIR}/devicetree/am335x-bonegreen-ext-alias.dtb" +}` + +// TestBitbakeNormalizationCensusLockedCExact compares all A0 BitBake +// fixtures and the existing parser trigger sources against the pinned C grammar. +func TestBitbakeNormalizationCensusLockedCExact(t *testing.T) { + entry, ok := parityEntriesByName["bitbake"] + if !ok { + t.Fatal("missing BitBake grammar entry") + } + language := entry.Language() + cLanguage, err := COracleLanguage("bitbake") + if err != nil { + t.Fatal(err) + } + identity, err := COracleIdentity("bitbake") + if err != nil { + t.Fatal(err) + } + t.Logf("C oracle contract=%s binding=%s runtime=%s grammar_commit=%s artifact_sha256=%s", identity.Contract, identity.BindingVersion, identity.RuntimeVersion, identity.GrammarCommit, identity.GrammarArtifactSHA256) + + tests := []struct { + name string + file string + sha256 string + source []byte + }{ + { + name: "a0-small-error", + file: "small__error.bb", + sha256: "fbdb85e443edd378e944e5a1416c0c4a1e485f0cd38f70a5ac75748073a15d12", + }, + { + name: "a0-medium-clang-git", + file: "medium__clang_git.bb", + sha256: "7deb41efd839d8b5b8b2c98589614377d12fd81fa6033824330084e07c5eaf9f", + }, + { + name: "a0-large-linux-firmware", + file: "large__linux-firmware_20260519.bb", + sha256: "eaa9e3f2354345d558717c4791a67144d8b27767674bf5468e157a0e0a332ff6", + }, + { + name: "trigger-addtask-error-wrapper", + sha256: "35ccf9d007ef76548258088b18adb39d7c0509452b4fb62bd7dfdc94cdcdf780", + source: []byte(bitbakeAddtaskTriggerSource), + }, + { + name: "trigger-function-flag-assignment", + sha256: "8655832f5acd3b4ada197881f41b0110657572aa0c791ecf4524fbc10603a1eb", + source: []byte(bitbakeFunctionFlagTriggerSource), + }, + { + name: "trigger-adjacent-override-functions", + sha256: "068037e13b101cf01d0a36da586f798972fa39b99ba0c2a86dc17996bc34d185", + source: []byte(bitbakeAdjacentOverrideTriggerSource), + }, + } + + for _, test := range tests { + test := test + if !t.Run(test.name, func(t *testing.T) { + source := test.source + if source == nil { + var err error + source, err = os.ReadFile(filepath.Join( + "..", "testdata", "dispatcher_census_a0", "bitbake", test.file, + )) + if err != nil { + t.Fatal(err) + } + } + if got := fmt.Sprintf("%x", sha256.Sum256(source)); got != test.sha256 { + t.Fatalf("source SHA-256 = %s, want %s", got, test.sha256) + } + + cParser := sitter.NewParser() + t.Cleanup(cParser.Close) + if err := cParser.SetLanguage(cLanguage); err != nil { + t.Fatal(err) + } + cTree := cParser.Parse(source, nil) + if cTree == nil || cTree.RootNode() == nil { + t.Fatal("locked C parser returned a nil tree") + } + t.Cleanup(cTree.Close) + cDigest, err := COracleDeepDigest(cTree) + if err != nil { + t.Fatalf("inspect locked C deep tree: %v", err) + } + + rawParser := gotreesitter.NewParser(language) + rawParser.SetAdmissionCandidateRoute(false) + rawTree, err := rawParser.ParseNoResultCompatibilityBenchmarkOnly(source) + if err != nil { + t.Fatalf("raw parse: %v", err) + } + t.Cleanup(rawTree.Release) + rawRuntime := rawTree.ParseRuntime() + rawDigest := assertBitbakeLockedCTreeExact(t, "raw", rawTree, language, cTree, cDigest) + + productionParser := gotreesitter.NewParser(language) + productionParser.SetAdmissionCandidateRoute(false) + productionTree, err := productionParser.Parse(source) + if err != nil { + t.Fatalf("production parse: %v", err) + } + t.Cleanup(productionTree.Release) + productionRuntime := productionTree.ParseRuntime() + productionDigest := assertBitbakeLockedCTreeExact(t, "production", productionTree, language, cTree, cDigest) + + t.Logf("witness=%s bytes=%d source_sha256=%s c_digest=%s raw_digest=%s production_digest=%s raw_rewrites=%d production_rewrites=%d", test.name, len(source), test.sha256, cDigest, rawDigest, productionDigest, rawRuntime.NormalizationNodesRewritten, productionRuntime.NormalizationNodesRewritten) + }) { + return + } + } +} + +func assertBitbakeLockedCTreeExact( + t *testing.T, + label string, + goTree *gotreesitter.Tree, + goLang *gotreesitter.Language, + cTree *sitter.Tree, + wantDigest string, +) string { + t.Helper() + goRoot := goTree.RootNode() + cRoot := cTree.RootNode() + if diff := FirstDivergenceDumpV1(goRoot, goLang, cRoot); diff != nil { + t.Fatalf("%s tree diverges from the locked C oracle: %+v", label, diff) + } + if diff := firstLockedCTreeFlagDivergence(goRoot, goLang, cRoot, "/"+goRoot.Type(goLang)); diff != nil { + t.Fatalf("%s tree has a missing or error flag divergence: %v", label, diff) + } + inspection, err := benchfixtures.InspectGoTree(goRoot, goLang) + if err != nil { + t.Fatalf("inspect %s Go deep tree: %v", label, err) + } + if inspection.SHA256 != wantDigest { + t.Fatalf("%s deep digest Go=%s C=%s", label, inspection.SHA256, wantDigest) + } + t.Logf("%s route matches locked C exactly: symbols, fields, spans, points, child order, named/extra/missing/error flags, deep digest=%s", label, inspection.SHA256) + return inspection.SHA256 +} diff --git a/cgo_harness/c26b_swift_assoc_minimal.swift b/cgo_harness/c26b_swift_assoc_minimal.swift new file mode 100644 index 000000000..66f415e49 --- /dev/null +++ b/cgo_harness/c26b_swift_assoc_minimal.swift @@ -0,0 +1,3 @@ +protocol P { + associatedtype Stride: SignedNumeric, Comparable +} diff --git a/cgo_harness/c26b_swift_associatedtype_probe_test.go b/cgo_harness/c26b_swift_associatedtype_probe_test.go new file mode 100644 index 000000000..84643cb90 --- /dev/null +++ b/cgo_harness/c26b_swift_associatedtype_probe_test.go @@ -0,0 +1,180 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +func TestC26BAssociatedtypeConformanceWitness(t *testing.T) { + cases := []struct { + name string + src []byte + }{ + { + name: "minimal", + src: []byte("protocol P {\n associatedtype Stride: SignedNumeric, Comparable\n}\n"), + }, + { + name: "corpus", + src: func() []byte { + src, err := os.ReadFile(filepath.Join("..", "grammars", "testdata", "swift_corpus", "stdlib_Stride.swift")) + if err != nil { + t.Fatalf("read Swift corpus witness: %v", err) + } + return src + }(), + }, + } + + goLang := grammars.SwiftLanguage() + cLang, err := ParityCLanguage("swift") + if err != nil { + t.Fatalf("load locked Swift C parser: %v", err) + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + goParser := gotreesitter.NewParser(goLang) + goTree, err := goParser.Parse(test.src) + if err != nil { + t.Fatalf("parse Swift with Go: %v", err) + } + defer goTree.Release() + + cParser := sitter.NewParser() + defer cParser.Close() + if err := cParser.SetLanguage(cLang); err != nil { + t.Fatalf("set locked Swift language: %v", err) + } + cTree := cParser.Parse(test.src, nil) + if cTree == nil || cTree.RootNode() == nil { + t.Fatal("locked C parser returned no tree") + } + defer cTree.Close() + + goInspection, err := benchfixtures.InspectGoTree(goTree.RootNode(), goLang) + if err != nil { + t.Fatalf("inspect Go tree: %v", err) + } + cDigest, err := COracleDeepDigest(cTree) + if err != nil { + t.Fatalf("inspect locked C tree: %v", err) + } + fmt.Printf("C26B_ASSOC name=%s bytes=%d source_sha256=%x go_sha256=%s c_sha256=%s go_root=%d:%d go_error=%t c_root=%d:%d c_error=%t go_error_node=%s c_error_node=%s first_diff=%+v\n", + test.name, + len(test.src), + sha256.Sum256(test.src), + goInspection.SHA256, + cDigest, + goTree.RootNode().StartByte(), + goTree.RootNode().EndByte(), + goTree.RootNode().HasError(), + cTree.RootNode().StartByte(), + cTree.RootNode().EndByte(), + cTree.RootNode().HasError(), + c26bFirstGoError(goTree.RootNode(), goLang, "/source_file"), + c26bFirstCError(cTree.RootNode(), "/source_file"), + FirstDivergenceDumpV1(goTree.RootNode(), goLang, cTree.RootNode()), + ) + if target := strings.Index(string(test.src), "associatedtype Stride: SignedNumeric, Comparable"); target >= 0 { + fmt.Printf("C26B_TARGET name=%s target=%d go_context=%s c_context=%s\n", + test.name, + target, + strings.Join(c26bGoErrorContext(goTree.RootNode(), goLang, uint32(target), "/source_file"), " -> "), + strings.Join(c26bCErrorContext(cTree.RootNode(), uint32(target), "/source_file"), " -> "), + ) + } + if FirstDivergenceDumpV1(goTree.RootNode(), goLang, cTree.RootNode()) == nil { + t.Fatal("associatedtype witness unexpectedly matches locked C") + } + }) + } +} + +func c26bGoErrorContext(node *gotreesitter.Node, lang *gotreesitter.Language, target uint32, path string) []string { + if node == nil || target < node.StartByte() || target > node.EndByte() { + return nil + } + current := []string(nil) + if node.Type(lang) == "ERROR" { + current = append(current, fmt.Sprintf("%s[%d:%d] children=%d", path, node.StartByte(), node.EndByte(), node.ChildCount())) + } + for i := 0; i < node.ChildCount(); i++ { + child := node.Child(i) + if child == nil { + continue + } + if nested := c26bGoErrorContext(child, lang, target, fmt.Sprintf("%s/%s[%d]", path, child.Type(lang), i)); nested != nil { + current = append(current, nested...) + } + } + return current +} + +func c26bCErrorContext(node *sitter.Node, target uint32, path string) []string { + if node == nil || target < uint32(node.StartByte()) || target > uint32(node.EndByte()) { + return nil + } + current := []string(nil) + if node.Kind() == "ERROR" { + current = append(current, fmt.Sprintf("%s[%d:%d] children=%d", path, node.StartByte(), node.EndByte(), node.ChildCount())) + } + for i := 0; i < int(node.ChildCount()); i++ { + child := node.Child(uint(i)) + if child == nil { + continue + } + if nested := c26bCErrorContext(child, target, fmt.Sprintf("%s/%s[%d]", path, child.Kind(), i)); nested != nil { + current = append(current, nested...) + } + } + return current +} + +func c26bFirstGoError(node *gotreesitter.Node, lang *gotreesitter.Language, path string) string { + if node == nil { + return "" + } + if node.Type(lang) == "ERROR" { + return fmt.Sprintf("%s[%d:%d] children=%d", path, node.StartByte(), node.EndByte(), node.ChildCount()) + } + for i := 0; i < node.ChildCount(); i++ { + child := node.Child(i) + if child == nil { + continue + } + if got := c26bFirstGoError(child, lang, fmt.Sprintf("%s/%s[%d]", path, child.Type(lang), i)); got != "" { + return got + } + } + return "" +} + +func c26bFirstCError(node *sitter.Node, path string) string { + if node == nil { + return "" + } + if node.Kind() == "ERROR" { + return fmt.Sprintf("%s[%d:%d] children=%d", path, node.StartByte(), node.EndByte(), node.ChildCount()) + } + for i := 0; i < int(node.ChildCount()); i++ { + child := node.Child(uint(i)) + if child == nil { + continue + } + if got := c26bFirstCError(child, fmt.Sprintf("%s/%s[%d]", path, child.Kind(), i)); got != "" { + return got + } + } + return "" +} diff --git a/cgo_harness/c26c_swift_mask_minimal.swift b/cgo_harness/c26c_swift_mask_minimal.swift new file mode 100644 index 000000000..82de7eb86 --- /dev/null +++ b/cgo_harness/c26c_swift_mask_minimal.swift @@ -0,0 +1 @@ +let x = 1&<<7 diff --git a/cgo_harness/c26c_swift_masking_shift_probe_test.go b/cgo_harness/c26c_swift_masking_shift_probe_test.go new file mode 100644 index 000000000..d4843223a --- /dev/null +++ b/cgo_harness/c26c_swift_masking_shift_probe_test.go @@ -0,0 +1,116 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +func TestC26CMaskingShiftWitness(t *testing.T) { + tests := []struct { + name string + src []byte + }{ + { + name: "minimal", + src: []byte("let x = 1&<<7\n"), + }, + { + name: "corpus", + src: func() []byte { + path := filepath.Join("..", "grammars", "testdata", "swift_corpus", "stdlib_ASCII.swift") + src, err := os.ReadFile(path) + if err != nil { + panic(err) + } + return src + }(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + source := test.src + sourceDigest := sha256.Sum256(source) + goTree, goLang, err := parseWithGo(parityCase{name: "swift", source: string(source)}, source, nil) + if err != nil { + t.Fatalf("parse Swift with Go: %v", err) + } + defer releaseGoTree(goTree) + + cLang, err := ParityCLanguage("swift") + if err != nil { + t.Fatalf("load locked Swift C parser: %v", err) + } + cParser := sitter.NewParser() + defer cParser.Close() + if err := cParser.SetLanguage(cLang); err != nil { + t.Fatalf("set locked Swift C parser language: %v", err) + } + cTree := cParser.Parse(source, nil) + if cTree == nil || cTree.RootNode() == nil { + t.Fatal("locked C parser returned no tree") + } + defer cTree.Close() + + goRoot := goTree.RootNode() + cRoot := cTree.RootNode() + goInspection, err := benchfixtures.InspectGoTree(goRoot, goLang) + if err != nil { + t.Fatalf("inspect Go tree: %v", err) + } + cDigest, err := COracleDeepDigest(cTree) + if err != nil { + t.Fatalf("inspect C tree: %v", err) + } + diff := FirstDivergenceDumpV1(goRoot, goLang, cRoot) + t.Logf("C26C_MASK name=%s bytes=%d source_sha256=%x go_sha256=%s c_sha256=%s go_root=%d:%d go_children=%d go_error=%v c_root=%d:%d c_children=%d c_error=%v first_diff=%+v target=%d", test.name, len(source), sourceDigest, goInspection.SHA256, cDigest, goRoot.StartByte(), goRoot.EndByte(), goRoot.ChildCount(), goRoot.HasError(), cRoot.StartByte(), cRoot.EndByte(), int(cRoot.ChildCount()), cRoot.HasError(), diff, strings.Index(string(source), "1&<<7")) + c26cLogGoRootChildren(t, "go", goRoot, goLang, source) + c26cLogCRootChildren(t, "c", cRoot, source) + }) + } +} + +func c26cLogGoRootChildren(t *testing.T, label string, root *gotreesitter.Node, lang *gotreesitter.Language, source []byte) { + t.Helper() + limit := root.ChildCount() + if limit > 12 { + limit = 12 + } + for i := 0; i < limit; i++ { + child := root.Child(i) + t.Logf("%s.root.child[%d] type=%q span=%d:%d named=%v extra=%v children=%d text=%s", label, i, child.Type(lang), child.StartByte(), child.EndByte(), child.IsNamed(), child.IsExtra(), child.ChildCount(), c26cText(source, child.StartByte(), child.EndByte())) + } +} + +func c26cLogCRootChildren(t *testing.T, label string, root *sitter.Node, source []byte) { + t.Helper() + limit := int(root.ChildCount()) + if limit > 12 { + limit = 12 + } + for i := 0; i < limit; i++ { + child := root.Child(uint(i)) + t.Logf("%s.root.child[%d] kind=%q span=%d:%d named=%v extra=%v children=%d text=%q", label, i, child.Kind(), child.StartByte(), child.EndByte(), child.IsNamed(), child.IsExtra(), int(child.ChildCount()), c26cText(source, uint32(child.StartByte()), uint32(child.EndByte()))) + } +} + +func c26cText(source []byte, start, end uint32) string { + if start > end || end > uint32(len(source)) { + return "" + } + text := string(source[start:end]) + if len(text) > 100 { + text = text[:100] + } + return fmt.Sprintf("%q", text) +} diff --git a/cgo_harness/corn_retirement_parity_test.go b/cgo_harness/corn_retirement_parity_test.go new file mode 100644 index 000000000..37748a349 --- /dev/null +++ b/cgo_harness/corn_retirement_parity_test.go @@ -0,0 +1,148 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +const cornQuotedKeysSource = "{\n 'foo.bar' = 42\n 'green.eggs'.and.ham = \"hello world\"\n 'with spaces' = true\n 'escaped\\'quote' = false\n 'escaped=equals' = -3\n}" + +// TestCornNormalizationCensusLockedCExact compares all A0 Corn witnesses and +// the existing quoted-keys trigger against the pinned C grammar. +func TestCornNormalizationCensusLockedCExact(t *testing.T) { + entry, ok := parityEntriesByName["corn"] + if !ok { + t.Fatal("missing Corn grammar entry") + } + language := entry.Language() + cLanguage, err := COracleLanguage("corn") + if err != nil { + t.Fatal(err) + } + identity, err := COracleIdentity("corn") + if err != nil { + t.Fatal(err) + } + t.Logf("C oracle contract=%s binding=%s runtime=%s grammar_commit=%s artifact_sha256=%s", identity.Contract, identity.BindingVersion, identity.RuntimeVersion, identity.GrammarCommit, identity.GrammarArtifactSHA256) + + tests := []struct { + name string + file string + sha256 string + source []byte + }{ + { + name: "a0-compact", + file: "small__compact.corn", + sha256: "e9793277f21b19593024cf7f670934333deefb54eae122caaa1f66cf41c7606a", + }, + { + name: "a0-complex", + file: "small__complex.corn", + sha256: "98aaba0d478418a7855fa538cb9cd52ab81b2d2e581402d212bc3951a6a5db02", + }, + { + name: "a0-readme-example", + file: "small__readme_example.corn", + sha256: "7d412d6e3c5e396818885601df14ad092b8cbe4aac690a45d7a4c29e0410da94", + }, + { + name: "quoted-keys-trigger", + sha256: "1e541539a857d075420cd73d64381b4f452d98a44884356a09b3e3ca7faf0b68", + source: []byte(cornQuotedKeysSource), + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + source := test.source + if source == nil { + var err error + source, err = os.ReadFile(filepath.Join( + "..", "testdata", "dispatcher_census_a0", "corn", test.file, + )) + if err != nil { + t.Fatal(err) + } + } + if got := fmt.Sprintf("%x", sha256.Sum256(source)); got != test.sha256 { + t.Fatalf("source SHA-256 = %s, want %s", got, test.sha256) + } + + cParser := sitter.NewParser() + t.Cleanup(cParser.Close) + if err := cParser.SetLanguage(cLanguage); err != nil { + t.Fatal(err) + } + cTree := cParser.Parse(source, nil) + if cTree == nil || cTree.RootNode() == nil { + t.Fatal("locked C parser returned a nil tree") + } + t.Cleanup(cTree.Close) + cDigest, err := COracleDeepDigest(cTree) + if err != nil { + t.Fatalf("inspect locked C deep tree: %v", err) + } + + rawParser := gotreesitter.NewParser(language) + rawParser.SetAdmissionCandidateRoute(false) + rawTree, err := rawParser.ParseNoResultCompatibilityBenchmarkOnly(source) + if err != nil { + t.Fatalf("raw parse: %v", err) + } + t.Cleanup(rawTree.Release) + rawRuntime := rawTree.ParseRuntime() + rawDigest := assertCornLockedCTreeExact(t, "raw", rawTree, language, cTree, cDigest) + + productionParser := gotreesitter.NewParser(language) + productionParser.SetAdmissionCandidateRoute(false) + productionTree, err := productionParser.Parse(source) + if err != nil { + t.Fatalf("production parse: %v", err) + } + t.Cleanup(productionTree.Release) + productionRuntime := productionTree.ParseRuntime() + productionDigest := assertCornLockedCTreeExact(t, "production", productionTree, language, cTree, cDigest) + + t.Logf("witness=%s bytes=%d source_sha256=%s c_digest=%s raw_digest=%s production_digest=%s raw_rewrites=%d production_rewrites=%d", test.name, len(source), test.sha256, cDigest, rawDigest, productionDigest, rawRuntime.NormalizationNodesRewritten, productionRuntime.NormalizationNodesRewritten) + }) + } +} + +func assertCornLockedCTreeExact( + t *testing.T, + label string, + goTree *gotreesitter.Tree, + goLang *gotreesitter.Language, + cTree *sitter.Tree, + wantDigest string, +) string { + t.Helper() + goRoot := goTree.RootNode() + cRoot := cTree.RootNode() + if diff := FirstDivergenceDumpV1(goRoot, goLang, cRoot); diff != nil { + t.Fatalf("%s tree diverges from the locked C oracle: %+v", label, diff) + } + if diff := firstLockedCTreeFlagDivergence(goRoot, goLang, cRoot, "/"+goRoot.Type(goLang)); diff != nil { + t.Fatalf("%s tree has a missing or error flag divergence: %v", label, diff) + } + inspection, err := benchfixtures.InspectGoTree(goRoot, goLang) + if err != nil { + t.Fatalf("inspect %s Go deep tree: %v", label, err) + } + if inspection.SHA256 != wantDigest { + t.Fatalf("%s deep digest Go=%s C=%s", label, inspection.SHA256, wantDigest) + } + t.Logf("%s route matches locked C exactly: symbols, fields, spans, points, extras, missing/error flags, deep digest=%s", label, inspection.SHA256) + return inspection.SHA256 +} diff --git a/cgo_harness/issue454_variant_cguard_fivesize_test.go b/cgo_harness/issue454_variant_cguard_fivesize_test.go new file mode 100644 index 000000000..2e9a0c246 --- /dev/null +++ b/cgo_harness/issue454_variant_cguard_fivesize_test.go @@ -0,0 +1,60 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "bytes" + "strconv" + "testing" + + "github.com/odvcencio/gotreesitter/internal/benchfixtures" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +func TestIssue454VariantCGuardFiveSizes(t *testing.T) { + sizes := []int{1024, 4096, 16384, 65536, benchfixtures.Issue454CFixtureBytes} + for _, size := range sizes { + size := size + t.Run(sizeLabel(size), func(t *testing.T) { + source := append([]byte(nil), benchfixtures.Issue454CSource()[:size]...) + site := bytes.Index(source, []byte("x0")) + if site < 0 { + t.Fatal("C edit marker is absent") + } + edited := append(append([]byte(nil), source[:site]...), source[site+1:]...) + + goTree, goLang, err := parseWithGo(parityCase{name: "c"}, edited, nil) + if err != nil { + t.Fatalf("parse edited C witness with Go: %v", err) + } + t.Cleanup(goTree.Release) + + cLang, err := ParityCLanguage("c") + if err != nil { + t.Fatalf("load locked C grammar: %v", err) + } + cParser := sitter.NewParser() + t.Cleanup(cParser.Close) + if err := cParser.SetLanguage(cLang); err != nil { + t.Fatalf("set locked C grammar: %v", err) + } + cTree := cParser.Parse(edited, nil) + if cTree == nil || cTree.RootNode() == nil { + t.Fatal("locked C parser returned no tree") + } + t.Cleanup(cTree.Close) + + if diff := FirstDivergenceDumpV1(goTree.RootNode(), goLang, cTree.RootNode()); diff != nil { + t.Fatalf("size=%d locked-C parity diverged: %+v", size, *diff) + } + t.Logf("size=%d locked-C parity passed", size) + }) + } +} + +func sizeLabel(size int) string { + if size == benchfixtures.Issue454CFixtureBytes { + return "137KiB" + } + return strconv.Itoa(size) + "B" +} diff --git a/cgo_harness/issue454_variant_locked_c_diag_test.go b/cgo_harness/issue454_variant_locked_c_diag_test.go new file mode 100644 index 000000000..abfd6e60e --- /dev/null +++ b/cgo_harness/issue454_variant_locked_c_diag_test.go @@ -0,0 +1,94 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "crypto/sha256" + "fmt" + "os" + "testing" + + sitter "github.com/tree-sitter/go-tree-sitter" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" +) + +func TestIssue454VariantLockedCDiag(t *testing.T) { + cases := []struct { + name string + lang string + load func() *gotreesitter.Language + path string + source string + }{ + {name: "cobol_a0_small", lang: "cobol", load: grammars.CobolLanguage, path: "../testdata/dispatcher_census_a0/cobol/small__CVERSNP1.cpy"}, + {name: "cobol_a0_medium", lang: "cobol", load: grammars.CobolLanguage, path: "../testdata/dispatcher_census_a0/cobol/medium__DBANK02P.cbl"}, + {name: "cobol_a0_large", lang: "cobol", load: grammars.CobolLanguage, path: "../testdata/dispatcher_census_a0/cobol/large__MBANK30.cpy"}, + {name: "wgsl_a0_small", lang: "wgsl", load: grammars.WgslLanguage, path: "../testdata/dispatcher_census_a0/wgsl/small__fragmentTextureQuad.wgsl"}, + {name: "wgsl_a0_medium_normal", lang: "wgsl", load: grammars.WgslLanguage, path: "../testdata/dispatcher_census_a0/wgsl/medium__normalMap.wgsl"}, + {name: "wgsl_a0_medium_radiosity", lang: "wgsl", load: grammars.WgslLanguage, path: "../testdata/dispatcher_census_a0/wgsl/medium__radiosity.wgsl"}, + {name: "cooklang_punctuation", lang: "cooklang", load: grammars.CooklangLanguage, source: "Add @salt{1%tsp}.\n"}, + {name: "cooklang_recovered", lang: "cooklang", load: grammars.CooklangLanguage, source: "---\nservings: 4\nemoji: 🥟\ntags: warm, fried, starter\n---\n\nServe hot.\n"}, + {name: "cooklang_no_newline", lang: "cooklang", load: grammars.CooklangLanguage, source: "Add @salt{1%tsp}."}, + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + var source []byte + if tc.path != "" { + var err error + source, err = os.ReadFile(tc.path) + if err != nil { + t.Fatalf("read source: %v", err) + } + } else { + source = []byte(tc.source) + } + cLang, err := ParityCLanguage(tc.lang) + if err != nil { + t.Fatalf("load locked C language: %v", err) + } + cParser := sitter.NewParser() + defer cParser.Close() + if err := cParser.SetLanguage(cLang); err != nil { + t.Fatalf("set C language: %v", err) + } + goLang := tc.load() + goParser := gotreesitter.NewParser(goLang) + goParser.SetAdmissionCandidateRoute(false) + goTree, err := goParser.ParseNoResultCompatibilityBenchmarkOnly(source) + if err != nil { + t.Fatalf("parse Go tree: %v", err) + } + defer goTree.Release() + cTree := cParser.Parse(source, nil) + if cTree == nil || cTree.RootNode() == nil { + t.Fatal("C parse returned no tree") + } + defer cTree.Close() + goRoot := goTree.RootNode() + cRoot := cTree.RootNode() + var diffs []compactT3StructuralDivergence + compactT3WalkStructuralDivergences(goRoot, goLang, cRoot, "root", &diffs) + goDigest := sha256.Sum256([]byte(goRoot.SExpr(goLang))) + cDigest := sha256.Sum256([]byte(dumpCTree(cRoot, 0))) + goInspection, err := benchfixtures.InspectGoTree(goRoot, goLang) + if err != nil { + t.Fatalf("inspect Go tree: %v", err) + } + cDeepDigest, err := COracleDeepDigest(cTree) + if err != nil { + t.Fatalf("inspect C tree: %v", err) + } + t.Logf("raw_error=%v c_error=%v raw_sexpr_sha256=%x c_tree_sha256=%x go_deep_digest=%s c_deep_digest=%s structural_diffs=%d", goRoot.HasError(), cRoot.HasError(), goDigest, cDigest, goInspection.SHA256, cDeepDigest, len(diffs)) + for i, diff := range diffs { + if i == 8 { + break + } + t.Logf("diff[%d]=%s", i, fmt.Sprint(diff)) + } + }) + } +} diff --git a/cgo_harness/rust_dispatch_locked_c_probe_test.go b/cgo_harness/rust_dispatch_locked_c_probe_test.go new file mode 100644 index 000000000..a310c5807 --- /dev/null +++ b/cgo_harness/rust_dispatch_locked_c_probe_test.go @@ -0,0 +1,216 @@ +//go:build cgo && treesitter_c_parity + +package cgoharness + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" + sitter "github.com/tree-sitter/go-tree-sitter" +) + +type rustDispatchLockedCWitness struct { + name string + src []byte +} + +// TestRustDispatchRegisteredWitnessLockedCDeepParity checks every Rust +// witness named by the ownership registry against the locked C oracle. +// It also checks the production, compact, forest, and incremental routes. +func TestRustDispatchRegisteredWitnessLockedCDeepParity(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + fixtures := []rustDispatchLockedCWitness{ + {name: "ownership-registry-smoke", src: []byte(grammars.ParseSmokeSample("rust"))}, + {name: "tracked-census-rust_ast.rs", src: rustDispatchReadFixture(t, "../testdata/incremental_gate/rust_ast.rs")}, + } + goLanguage := grammars.RustLanguage() + cLanguage, err := ParityCLanguage("rust") + if err != nil { + t.Fatalf("load locked Rust C oracle: %v", err) + } + + for _, fixture := range fixtures { + fixture := fixture + t.Run(fixture.name, func(t *testing.T) { + sourceHash := sha256.Sum256(fixture.src) + t.Logf("witness=%s bytes=%d source_sha256=%s", fixture.name, len(fixture.src), hex.EncodeToString(sourceHash[:])) + + raw := rustDispatchParseRaw(t, goLanguage, fixture.src) + defer raw.Release() + production := rustDispatchParseProduction(t, goLanguage, fixture.src) + defer production.Release() + rawDigest := rustDispatchGoDigest(t, raw, goLanguage) + productionDigest := rustDispatchGoDigest(t, production, goLanguage) + if rawDigest != productionDigest { + t.Fatalf("raw/production deep digest mismatch: raw=%s production=%s", rawDigest, productionDigest) + } + cDigest := rustDispatchCDigest(t, cLanguage, fixture.src) + if rawDigest != cDigest { + t.Fatalf("raw/locked-C deep digest mismatch: go=%s c=%s", rawDigest, cDigest) + } + t.Logf("source route=raw production raw_digest=%s c_digest=%s raw_rewrites=%d production_rewrites=%d", rawDigest, cDigest, raw.ParseRuntime().NormalizationNodesRewritten, production.ParseRuntime().NormalizationNodesRewritten) + rLogRustDispatchLockedC(t, "production", production.ParseRuntime()) + + routeSource := append(append([]byte(nil), fixture.src...), '\n') + routeCDigest := rustDispatchCDigest(t, cLanguage, routeSource) + productionRoute := rustDispatchParseProduction(t, goLanguage, routeSource) + defer productionRoute.Release() + rustDispatchAssertGoDigest(t, "production-route", productionRoute, goLanguage, routeCDigest) + rLogRustDispatchLockedC(t, "production-route", productionRoute.ParseRuntime()) + + beforeCandidate, beforeFallback := gotreesitter.AdmissionCandidateCounters() + compactParser := gotreesitter.NewParser(goLanguage) + compactParser.SetAdmissionCandidateRoute(true) + compact, err := compactParser.Parse(routeSource) + if err != nil { + t.Fatalf("compact route: %v", err) + } + defer compact.Release() + rustDispatchAssertGoDigest(t, "compact", compact, goLanguage, routeCDigest) + afterCandidate, afterFallback := gotreesitter.AdmissionCandidateCounters() + t.Logf("route=compact candidate_routed_delta=%d candidate_fallback_delta=%d digest=%s", afterCandidate-beforeCandidate, afterFallback-beforeFallback, rustDispatchGoDigest(t, compact, goLanguage)) + if afterCandidate <= beforeCandidate { + t.Fatalf("compact route did not enter the admission candidate") + } + if afterFallback != beforeFallback { + t.Fatalf("compact route fell back: before=%d after=%d reason=%q", beforeFallback, afterFallback, gotreesitter.AdmissionCandidateLastFallbackReason()) + } + + forestParser := gotreesitter.NewParser(goLanguage) + forest, ok := forestParser.ParseForestExperimental(routeSource) + if !ok || forest == nil { + offset, symbol, reason, states := forestParser.ForestDeclineInfo() + t.Fatalf("forest route declined: offset=%d symbol=%d reason=%q states=%v", offset, symbol, reason, states) + } + defer forest.Release() + rustDispatchAssertGoDigest(t, "forest", forest, goLanguage, routeCDigest) + t.Logf("route=forest outcome=accepted digest=%s forest_fast_path=%t", rustDispatchGoDigest(t, forest, goLanguage), forest.ParseRuntime().ForestFastPath) + rLogRustDispatchLockedC(t, "forest", forest.ParseRuntime()) + + oldTree := rustDispatchParseProduction(t, goLanguage, fixture.src) + defer oldTree.Release() + oldEnd := rustDispatchPointAtByte(fixture.src, len(fixture.src)) + oldTree.Edit(gotreesitter.InputEdit{ + StartByte: uint32(len(fixture.src)), + OldEndByte: uint32(len(fixture.src)), + NewEndByte: uint32(len(routeSource)), + StartPoint: oldEnd, + OldEndPoint: oldEnd, + NewEndPoint: rustDispatchPointAtByte(routeSource, len(routeSource)), + }) + incrementalParser := gotreesitter.NewParser(goLanguage) + incremental, profile, err := incrementalParser.ParseIncrementalProfiled(routeSource, oldTree) + if err != nil { + t.Fatalf("incremental route: %v", err) + } + defer incremental.Release() + rustDispatchAssertGoDigest(t, "incremental", incremental, goLanguage, routeCDigest) + t.Logf("route=incremental digest=%s reused_subtrees=%d reused_bytes=%d reuse_unsupported=%t reason=%q", rustDispatchGoDigest(t, incremental, goLanguage), profile.ReusedSubtrees, profile.ReusedBytes, profile.ReuseUnsupported, profile.ReuseUnsupportedReason) + rLogRustDispatchLockedC(t, "incremental", incremental.ParseRuntime()) + }) + } +} + +func rustDispatchReadFixture(t *testing.T, path string) []byte { + t.Helper() + source, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read Rust fixture %s: %v", path, err) + } + return source +} + +func rustDispatchParseRaw(t *testing.T, language *gotreesitter.Language, source []byte) *gotreesitter.Tree { + t.Helper() + parser := gotreesitter.NewParser(language) + tree, err := parser.ParseNoResultCompatibilityBenchmarkOnly(source) + if err != nil { + t.Fatalf("raw parse: %v", err) + } + return tree +} + +func rustDispatchParseProduction(t *testing.T, language *gotreesitter.Language, source []byte) *gotreesitter.Tree { + t.Helper() + parser := gotreesitter.NewParser(language) + parser.SetAdmissionCandidateRoute(false) + tree, err := parser.Parse(source) + if err != nil { + t.Fatalf("production parse: %v", err) + } + return tree +} + +func rustDispatchGoDigest(t *testing.T, tree *gotreesitter.Tree, language *gotreesitter.Language) string { + t.Helper() + inspection, err := benchfixtures.InspectGoTree(tree.RootNode(), language) + if err != nil { + t.Fatalf("inspect Go deep tree: %v", err) + } + return inspection.SHA256 +} + +func rustDispatchCDigest(t *testing.T, language *sitter.Language, source []byte) string { + t.Helper() + parser := sitter.NewParser() + defer parser.Close() + if err := parser.SetLanguage(language); err != nil { + t.Fatalf("set C oracle language: %v", err) + } + tree := parser.Parse(source, nil) + if tree == nil || tree.RootNode() == nil { + t.Fatal("C oracle returned a nil tree") + } + defer tree.Close() + digest, err := COracleDeepDigest(tree) + if err != nil { + t.Fatalf("inspect C deep tree: %v", err) + } + return digest +} + +func rustDispatchAssertGoDigest(t *testing.T, route string, tree *gotreesitter.Tree, language *gotreesitter.Language, want string) { + t.Helper() + got := rustDispatchGoDigest(t, tree, language) + if got != want { + t.Fatalf("route=%s deep digest Go=%s, want locked C %s", route, got, want) + } + if tree.RootNode().HasError() { + t.Fatalf("route=%s produced an error root", route) + } +} + +func rLogRustDispatchLockedC(t *testing.T, route string, runtime gotreesitter.ParseRuntime) { + t.Helper() + if runtime.NormalizationPasses == nil { + t.Logf("route=%s dispatch.rust=absent", route) + return + } + for _, pass := range *runtime.NormalizationPasses { + if pass.Name == "dispatch.rust" { + t.Logf("route=%s pass=%s checked=%d run=%d visited=%d rewritten=%d", route, pass.Name, pass.Checked, pass.Run, pass.NodesVisited, pass.NodesRewritten) + } + } +} + +func rustDispatchPointAtByte(source []byte, offset int) gotreesitter.Point { + if offset < 0 || offset > len(source) { + panic(fmt.Sprintf("point offset %d outside source length %d", offset, len(source))) + } + var point gotreesitter.Point + for _, value := range source[:offset] { + if value == '\n' { + point.Row++ + point.Column = 0 + } else { + point.Column++ + } + } + return point +} diff --git a/external_scanner_checkpoint_lifecycle.go b/external_scanner_checkpoint_lifecycle.go new file mode 100644 index 000000000..0a7d06656 --- /dev/null +++ b/external_scanner_checkpoint_lifecycle.go @@ -0,0 +1,447 @@ +package gotreesitter + +// externalScannerCheckpointVersionKey identifies one parser version inside a +// single checkpoint lifecycle. It is not a parser stack field. +type externalScannerCheckpointVersionKey struct { + parseEpoch uint64 + versionID uint64 +} + +// externalScannerCheckpointVersion keeps scanner ownership beside, rather +// than inside, a parser stack. The lifecycle is opt-in and has no production +// parser caller yet. +type externalScannerCheckpointVersion struct { + key externalScannerCheckpointVersionKey + payload any + checkpoint externalScannerCheckpointRecord + alive bool +} + +// externalScannerCheckpointLifecycle is a bounded ownership ledger for the +// synthetic parser lifecycle proof. It does not wire scanner state into GLR. +type externalScannerCheckpointLifecycle struct { + scanner ExternalScanner + epoch uint64 + nextID uint64 + versions map[externalScannerCheckpointVersionKey]*externalScannerCheckpointVersion +} + +// newExternalScannerCheckpointLifecycle requires an explicit opt-in. A +// missing or disabled capability returns no lifecycle and no ledger. +func newExternalScannerCheckpointLifecycle(scanner ExternalScanner, enabled bool) (*externalScannerCheckpointLifecycle, bool) { + if !enabled || scanner == nil { + return nil, false + } + provider, ok := scanner.(ExternalScannerCheckpointIdentityProvider) + if !ok || !provider.UsesExternalScannerCheckpoints() { + return nil, false + } + return &externalScannerCheckpointLifecycle{ + scanner: scanner, + epoch: 1, + nextID: 1, + versions: make(map[externalScannerCheckpointVersionKey]*externalScannerCheckpointVersion), + }, true +} + +func (l *externalScannerCheckpointLifecycle) nextVersionKey() (externalScannerCheckpointVersionKey, bool) { + if l == nil || l.epoch == 0 || l.nextID == 0 { + return externalScannerCheckpointVersionKey{}, false + } + key := externalScannerCheckpointVersionKey{ + parseEpoch: l.epoch, + versionID: l.nextID, + } + if _, exists := l.versions[key]; exists { + return externalScannerCheckpointVersionKey{}, false + } + if l.nextID == ^uint64(0) { + l.nextID = 0 + } else { + l.nextID++ + } + return key, true +} + +func (l *externalScannerCheckpointLifecycle) close() { + if l == nil { + return + } + for _, version := range l.versions { + if version != nil && version.payload != nil { + l.scanner.Destroy(version.payload) + version.payload = nil + } + if version != nil { + version.alive = false + } + } + clear(l.versions) +} + +func (l *externalScannerCheckpointLifecycle) version(key externalScannerCheckpointVersionKey) (*externalScannerCheckpointVersion, bool) { + if l == nil || l.versions == nil { + return nil, false + } + version, ok := l.versions[key] + if !ok || version == nil || !version.alive || !version.checkpoint.complete() { + return nil, false + } + return version, true +} + +func (l *externalScannerCheckpointLifecycle) addRoot( + payload any, + sourceByte uint32, + sourcePoint Point, + externalLexState uint16, + tokenStartByte uint32, + tokenEndByte uint32, +) (externalScannerCheckpointVersionKey, bool) { + if l == nil { + return externalScannerCheckpointVersionKey{}, false + } + record, ok := captureExternalScannerCheckpointRecord( + l.scanner, + payload, + sourceByte, + sourcePoint, + externalLexState, + tokenStartByte, + tokenEndByte, + ) + if !ok { + return externalScannerCheckpointVersionKey{}, false + } + key, ok := l.nextVersionKey() + if !ok { + if payload != nil { + l.scanner.Destroy(payload) + } + return externalScannerCheckpointVersionKey{}, false + } + l.versions[key] = &externalScannerCheckpointVersion{ + key: key, + payload: payload, + checkpoint: record, + alive: true, + } + return key, true +} + +// restoreAndVerify restores the owned bytes, then captures them again. The +// second capture proves that Deserialize accepted the complete checkpoint. +func (l *externalScannerCheckpointLifecycle) restoreAndVerify(version *externalScannerCheckpointVersion) bool { + if l == nil || version == nil || !version.alive || !version.checkpoint.complete() { + return false + } + if !version.checkpoint.restore(l.scanner, version.payload) { + return false + } + restored, ok := captureExternalScannerCheckpointRecord( + l.scanner, + version.payload, + version.checkpoint.sourceByte, + version.checkpoint.sourcePoint, + version.checkpoint.externalLexState, + version.checkpoint.tokenStartByte, + version.checkpoint.tokenEndByte, + ) + return ok && restored.equal(version.checkpoint) +} + +// discardVersion destroys a payload after failed restore verification. It +// also removes an owned version from the lifecycle ledger. +func (l *externalScannerCheckpointLifecycle) discardVersion(version *externalScannerCheckpointVersion) { + if version == nil { + return + } + if l != nil && l.versions != nil { + if current, ok := l.versions[version.key]; ok && current == version { + version.alive = false + _ = l.deleteDead(version.key) + return + } + } + if version.payload != nil { + if l != nil && l.scanner != nil { + l.scanner.Destroy(version.payload) + } + version.payload = nil + } + version.alive = false +} + +// elect runs one synthetic token election. A failed scan restores the prior +// checkpoint and keeps the version only after verified restoration. +func (l *externalScannerCheckpointLifecycle) elect( + key externalScannerCheckpointVersionKey, + sourceByte uint32, + sourcePoint Point, + externalLexState uint16, + scan func(any) (Token, bool), +) (Token, bool) { + version, ok := l.version(key) + if !ok || scan == nil { + return Token{}, false + } + before, ok := captureExternalScannerCheckpointRecord( + l.scanner, + version.payload, + sourceByte, + sourcePoint, + externalLexState, + sourceByte, + sourceByte, + ) + if !ok { + return Token{}, false + } + tok, ok := scan(version.payload) + if !ok { + if !before.restore(l.scanner, version.payload) { + l.discardVersion(version) + return Token{}, false + } + if restored, restoreOK := captureExternalScannerCheckpointRecord( + l.scanner, + version.payload, + before.sourceByte, + before.sourcePoint, + before.externalLexState, + before.tokenStartByte, + before.tokenEndByte, + ); !restoreOK || !restored.equal(before) { + l.discardVersion(version) + return Token{}, false + } + return Token{}, false + } + after, ok := captureExternalScannerCheckpointRecord( + l.scanner, + version.payload, + sourceByte, + sourcePoint, + externalLexState, + tok.StartByte, + tok.EndByte, + ) + if !ok { + if !before.restore(l.scanner, version.payload) { + l.discardVersion(version) + return Token{}, false + } + restored, restoreOK := captureExternalScannerCheckpointRecord( + l.scanner, + version.payload, + before.sourceByte, + before.sourcePoint, + before.externalLexState, + before.tokenStartByte, + before.tokenEndByte, + ) + if !restoreOK || !restored.equal(before) { + l.discardVersion(version) + } + return Token{}, false + } + version.checkpoint = after + return tok, true +} + +func (l *externalScannerCheckpointLifecycle) fork(parentKey externalScannerCheckpointVersionKey) (externalScannerCheckpointVersionKey, bool) { + parent, ok := l.version(parentKey) + if !ok { + return externalScannerCheckpointVersionKey{}, false + } + childPayload := l.scanner.Create() + child := &externalScannerCheckpointVersion{ + payload: childPayload, + checkpoint: parent.checkpoint.clone(), + alive: true, + } + if !l.restoreAndVerify(child) { + l.discardVersion(child) + return externalScannerCheckpointVersionKey{}, false + } + key, ok := l.nextVersionKey() + if !ok { + l.discardVersion(child) + return externalScannerCheckpointVersionKey{}, false + } + child.key = key + l.versions[key] = child + return key, true +} + +func (l *externalScannerCheckpointLifecycle) canShare(aKey, bKey externalScannerCheckpointVersionKey) bool { + a, aOK := l.version(aKey) + b, bOK := l.version(bKey) + if !aOK || !bOK { + return false + } + if !l.currentStateMatches(a) { + l.discardVersion(a) + return false + } + if !l.currentStateMatches(b) { + l.discardVersion(b) + return false + } + return canShareExternalScannerCheckpoint(a.checkpoint, b.checkpoint) +} + +func (l *externalScannerCheckpointLifecycle) currentStateMatches(version *externalScannerCheckpointVersion) bool { + if l == nil || version == nil || !version.alive || !version.checkpoint.complete() { + return false + } + record, ok := captureExternalScannerCheckpointRecord( + l.scanner, + version.payload, + version.checkpoint.sourceByte, + version.checkpoint.sourcePoint, + version.checkpoint.externalLexState, + version.checkpoint.tokenStartByte, + version.checkpoint.tokenEndByte, + ) + return ok && record.equal(version.checkpoint) +} + +// merge removes the candidate only when its complete record equals the keep +// record. A mismatch leaves both versions alive for a safe fallback. +func (l *externalScannerCheckpointLifecycle) merge(keepKey, candidateKey externalScannerCheckpointVersionKey) bool { + if keepKey == candidateKey || !l.canShare(keepKey, candidateKey) { + return false + } + return l.markDeadAndDelete(candidateKey) +} + +// condense selects one version and deletes only dead or exact-record siblings. +// It rejects a live sibling with a different scanner state. +func (l *externalScannerCheckpointLifecycle) condense(selectedKey externalScannerCheckpointVersionKey, siblingKeys []externalScannerCheckpointVersionKey) (any, bool) { + selected, ok := l.version(selectedKey) + if !ok || !l.restoreAndVerify(selected) { + if ok { + l.discardVersion(selected) + } + return nil, false + } + for _, siblingKey := range siblingKeys { + if siblingKey == selectedKey { + continue + } + sibling, siblingOK := l.version(siblingKey) + if !siblingOK { + continue + } + if sibling.alive && !l.canShare(selectedKey, siblingKey) { + return nil, false + } + } + for _, siblingKey := range siblingKeys { + if siblingKey == selectedKey { + continue + } + if sibling, siblingOK := l.version(siblingKey); siblingOK { + sibling.alive = false + _ = l.deleteDead(siblingKey) + } + } + return selected.payload, true +} + +// resume restores the selected version before the recovery callback. A failed +// callback keeps the version only after verified restoration. +func (l *externalScannerCheckpointLifecycle) resume( + key externalScannerCheckpointVersionKey, + sourceByte uint32, + sourcePoint Point, + externalLexState uint16, + recover func(any) (Token, bool), +) (Token, bool) { + version, ok := l.version(key) + if !ok || recover == nil { + return Token{}, false + } + if !l.restoreAndVerify(version) { + l.discardVersion(version) + return Token{}, false + } + before := version.checkpoint.clone() + tok, ok := recover(version.payload) + if !ok { + if !before.restore(l.scanner, version.payload) { + l.discardVersion(version) + return Token{}, false + } + if !l.restoreAndVerify(version) { + l.discardVersion(version) + } + return Token{}, false + } + after, ok := captureExternalScannerCheckpointRecord( + l.scanner, + version.payload, + sourceByte, + sourcePoint, + externalLexState, + tok.StartByte, + tok.EndByte, + ) + if !ok { + if !before.restore(l.scanner, version.payload) { + l.discardVersion(version) + return Token{}, false + } + restored, restoreOK := captureExternalScannerCheckpointRecord( + l.scanner, + version.payload, + before.sourceByte, + before.sourcePoint, + before.externalLexState, + before.tokenStartByte, + before.tokenEndByte, + ) + if !restoreOK || !restored.equal(before) { + l.discardVersion(version) + } + return Token{}, false + } + version.checkpoint = after + return tok, true +} + +func (l *externalScannerCheckpointLifecycle) markDead(key externalScannerCheckpointVersionKey) bool { + version, ok := l.version(key) + if !ok { + return false + } + version.alive = false + return true +} + +func (l *externalScannerCheckpointLifecycle) deleteDead(key externalScannerCheckpointVersionKey) bool { + if l == nil { + return false + } + version, ok := l.versions[key] + if !ok || version == nil || version.alive { + return false + } + if version.payload != nil { + payload := version.payload + version.payload = nil + l.scanner.Destroy(payload) + } + delete(l.versions, key) + return true +} + +func (l *externalScannerCheckpointLifecycle) markDeadAndDelete(key externalScannerCheckpointVersionKey) bool { + version, ok := l.versions[key] + if !ok || version == nil || !version.alive { + return false + } + version.alive = false + return l.deleteDead(key) +} diff --git a/external_scanner_checkpoint_lifecycle_test.go b/external_scanner_checkpoint_lifecycle_test.go new file mode 100644 index 000000000..b884427cc --- /dev/null +++ b/external_scanner_checkpoint_lifecycle_test.go @@ -0,0 +1,273 @@ +package gotreesitter + +import ( + "bytes" + "testing" +) + +type c26nCheckpointPayload struct { + state []byte +} + +type c26nCheckpointScanner struct { + scannerID []byte + grammarID []byte + destroyed int + deserializeMismatch bool +} + +func (s *c26nCheckpointScanner) Create() any { + return &c26nCheckpointPayload{state: []byte{1, 2, 3}} +} + +func (s *c26nCheckpointScanner) Destroy(any) { s.destroyed++ } + +func (s *c26nCheckpointScanner) Serialize(payload any, buf []byte) int { + p, ok := payload.(*c26nCheckpointPayload) + if !ok || len(p.state) == 0 || len(p.state) > len(buf) { + return 0 + } + return copy(buf, p.state) +} + +func (s *c26nCheckpointScanner) Deserialize(payload any, buf []byte) { + p, ok := payload.(*c26nCheckpointPayload) + if !ok { + return + } + if s.deserializeMismatch { + p.state = []byte{9, 9, 9} + return + } + p.state = append(p.state[:0], buf...) +} + +func (*c26nCheckpointScanner) Scan(any, *ExternalLexer, []bool) bool { + return false +} + +func (*c26nCheckpointScanner) UsesExternalScannerCheckpoints() bool { + return true +} + +func (s *c26nCheckpointScanner) CheckpointIdentity() (ExternalScannerCheckpointIdentity, bool) { + return ExternalScannerCheckpointIdentity{ + Scanner: s.scannerID, + Grammar: s.grammarID, + }, true +} + +func newC26nCheckpointScanner() *c26nCheckpointScanner { + return &c26nCheckpointScanner{ + scannerID: []byte("scanner-c26n"), + grammarID: []byte("grammar-c26n"), + } +} + +func c26nPayloadState(t *testing.T, payload any) []byte { + t.Helper() + p, ok := payload.(*c26nCheckpointPayload) + if !ok { + t.Fatalf("payload type = %T, want *c26nCheckpointPayload", payload) + } + return append([]byte(nil), p.state...) +} + +func TestC26nCheckpointLifecycleRequiresExplicitOptIn(t *testing.T) { + scanner := newC26nCheckpointScanner() + if lifecycle, ok := newExternalScannerCheckpointLifecycle(scanner, false); ok || lifecycle != nil { + t.Fatal("disabled lifecycle allocated an ownership ledger") + } + if lifecycle, ok := newExternalScannerCheckpointLifecycle(nil, true); ok || lifecycle != nil { + t.Fatal("nil scanner enabled a lifecycle") + } +} + +func TestC26nCheckpointLifecycleDiscardsFailedRestore(t *testing.T) { + scanner := newC26nCheckpointScanner() + lifecycle, ok := newExternalScannerCheckpointLifecycle(scanner, true) + if !ok { + t.Fatal("explicit lifecycle opt-in was rejected") + } + + rootPayload := scanner.Create() + rootKey, ok := lifecycle.addRoot(rootPayload, 10, Point{Row: 1}, 7, 10, 10) + if !ok { + t.Fatal("root checkpoint was rejected") + } + scanner.deserializeMismatch = true + if _, ok := lifecycle.elect(rootKey, 10, Point{Row: 1}, 7, func(payload any) (Token, bool) { + payload.(*c26nCheckpointPayload).state = []byte{9, 9, 9} + return Token{}, false + }); ok { + t.Fatal("failed election unexpectedly succeeded") + } + if _, exists := lifecycle.versions[rootKey]; exists { + t.Fatal("failed election retained an unverified payload") + } + if scanner.destroyed != 1 { + t.Fatalf("destroy count after failed election = %d, want 1", scanner.destroyed) + } + + scanner.deserializeMismatch = false + resumePayload := scanner.Create() + resumeKey, ok := lifecycle.addRoot(resumePayload, 11, Point{Row: 1}, 7, 11, 11) + if !ok { + t.Fatal("resume checkpoint was rejected") + } + scanner.deserializeMismatch = true + if _, ok := lifecycle.resume(resumeKey, 11, Point{Row: 1}, 7, func(any) (Token, bool) { + return Token{}, false + }); ok { + t.Fatal("failed resume unexpectedly succeeded") + } + if _, exists := lifecycle.versions[resumeKey]; exists { + t.Fatal("failed resume retained an unverified payload") + } + if scanner.destroyed != 2 { + t.Fatalf("destroy count after failed resume = %d, want 2", scanner.destroyed) + } + lifecycle.close() + if scanner.destroyed != 2 { + t.Fatalf("close destroyed discarded payloads again: count = %d", scanner.destroyed) + } +} + +func TestC26nCheckpointLifecycle(t *testing.T) { + scanner := newC26nCheckpointScanner() + lifecycle, ok := newExternalScannerCheckpointLifecycle(scanner, true) + if !ok { + t.Fatal("explicit lifecycle opt-in was rejected") + } + defer lifecycle.close() + + rootPayload := scanner.Create() + rootKey, ok := lifecycle.addRoot(rootPayload, 10, Point{Row: 1, Column: 2}, 7, 10, 10) + if !ok { + t.Fatal("root checkpoint was rejected") + } + + selectedToken, ok := lifecycle.elect(rootKey, 10, Point{Row: 1, Column: 2}, 7, func(payload any) (Token, bool) { + p := payload.(*c26nCheckpointPayload) + p.state = []byte{4, 5, 6} + return Token{Symbol: 11, StartByte: 10, EndByte: 12}, true + }) + if !ok || selectedToken.Symbol != 11 || selectedToken.EndByte != 12 { + t.Fatalf("election = (%+v, %t), want symbol 11 and success", selectedToken, ok) + } + if got := c26nPayloadState(t, rootPayload); !bytes.Equal(got, []byte{4, 5, 6}) { + t.Fatalf("elected payload = %v, want [4 5 6]", got) + } + rootRecord := lifecycle.versions[rootKey].checkpoint.clone() + + failedToken, ok := lifecycle.elect(rootKey, 12, Point{Row: 1, Column: 4}, 7, func(payload any) (Token, bool) { + p := payload.(*c26nCheckpointPayload) + p.state = []byte{9, 9, 9} + return Token{}, false + }) + if ok || failedToken != (Token{}) { + t.Fatalf("failed election = (%+v, %t), want zero token and failure", failedToken, ok) + } + if got := c26nPayloadState(t, rootPayload); !bytes.Equal(got, []byte{4, 5, 6}) { + t.Fatalf("failed-election payload = %v, want [4 5 6]", got) + } + if !lifecycle.versions[rootKey].checkpoint.equal(rootRecord) { + t.Fatal("failed election changed the owned checkpoint") + } + + exactFork, ok := lifecycle.fork(rootKey) + if !ok { + t.Fatal("exact fork was rejected") + } + if !lifecycle.canShare(rootKey, exactFork) { + t.Fatal("exact fork did not share its copied checkpoint") + } + childPayload := lifecycle.versions[exactFork].payload + if got := c26nPayloadState(t, childPayload); !bytes.Equal(got, []byte{4, 5, 6}) { + t.Fatalf("fork payload = %v, want [4 5 6]", got) + } + childPayload.(*c26nCheckpointPayload).state[0] = 99 + if got := c26nPayloadState(t, rootPayload); !bytes.Equal(got, []byte{4, 5, 6}) { + t.Fatalf("fork mutation changed root payload = %v, want [4 5 6]", got) + } + if !lifecycle.restoreAndVerify(lifecycle.versions[exactFork]) { + t.Fatal("exact fork did not restore after an independent payload mutation") + } + staleFork, ok := lifecycle.fork(rootKey) + if !ok { + t.Fatal("stale fork was rejected") + } + lifecycle.versions[staleFork].payload.(*c26nCheckpointPayload).state[0] = 77 + if lifecycle.merge(rootKey, staleFork) { + t.Fatal("stale checkpoint merge was accepted") + } + if _, exists := lifecycle.versions[staleFork]; exists { + t.Fatal("stale checkpoint remained shareable") + } + if !lifecycle.merge(rootKey, exactFork) { + t.Fatal("exact checkpoint merge was rejected") + } + if _, exists := lifecycle.versions[exactFork]; exists { + t.Fatal("merged fork was not deleted") + } + if got := c26nPayloadState(t, rootPayload); !bytes.Equal(got, []byte{4, 5, 6}) { + t.Fatalf("root payload after merge = %v, want [4 5 6]", got) + } + + mismatchedFork, ok := lifecycle.fork(rootKey) + if !ok { + t.Fatal("mismatched fork was rejected") + } + if _, ok := lifecycle.elect(mismatchedFork, 12, Point{Row: 1, Column: 4}, 7, func(payload any) (Token, bool) { + p := payload.(*c26nCheckpointPayload) + p.state = []byte{8, 8, 8} + return Token{Symbol: 12, StartByte: 12, EndByte: 13}, true + }); !ok { + t.Fatal("mismatched election was rejected") + } + if lifecycle.merge(rootKey, mismatchedFork) { + t.Fatal("mismatched checkpoint merge was accepted") + } + if _, exists := lifecycle.versions[mismatchedFork]; !exists { + t.Fatal("mismatched version was dropped after merge rejection") + } + + if _, ok := lifecycle.condense(rootKey, []externalScannerCheckpointVersionKey{mismatchedFork}); ok { + t.Fatal("condense dropped a live mismatched version") + } + if !lifecycle.markDead(mismatchedFork) { + t.Fatal("mismatched version was not marked dead") + } + if !lifecycle.deleteDead(mismatchedFork) { + t.Fatal("dead version was not deleted") + } + if _, exists := lifecycle.versions[mismatchedFork]; exists { + t.Fatal("dead version remains in the ledger") + } + + condenseFork, ok := lifecycle.fork(rootKey) + if !ok { + t.Fatal("condense fork was rejected") + } + if _, ok := lifecycle.condense(rootKey, []externalScannerCheckpointVersionKey{condenseFork}); !ok { + t.Fatal("exact condense selection was rejected") + } + if _, exists := lifecycle.versions[condenseFork]; exists { + t.Fatal("exact condense sibling was not deleted") + } + + resumedToken, ok := lifecycle.resume(rootKey, 13, Point{Row: 1, Column: 5}, 7, func(payload any) (Token, bool) { + p := payload.(*c26nCheckpointPayload) + if !bytes.Equal(p.state, []byte{4, 5, 6}) { + return Token{}, false + } + p.state = []byte{7, 7, 7} + return Token{Symbol: 13, StartByte: 13, EndByte: 14}, true + }) + if !ok || resumedToken.Symbol != 13 { + t.Fatalf("recovery resume = (%+v, %t), want symbol 13 and success", resumedToken, ok) + } + if got := c26nPayloadState(t, rootPayload); !bytes.Equal(got, []byte{7, 7, 7}) { + t.Fatalf("resumed payload = %v, want [7 7 7]", got) + } +} diff --git a/rust_dispatch_probe_test.go b/rust_dispatch_probe_test.go new file mode 100644 index 000000000..17660121c --- /dev/null +++ b/rust_dispatch_probe_test.go @@ -0,0 +1,283 @@ +package gotreesitter_test + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "testing" + + gotreesitter "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" + "github.com/odvcencio/gotreesitter/internal/benchfixtures" +) + +type rustDispatchProbeWitness struct { + name string + src []byte +} + +func TestRustDispatchRawProductionAndRouteProbe(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + fixtures := []rustDispatchProbeWitness{ + {name: "ownership-registry-smoke", src: []byte(grammars.ParseSmokeSample("rust"))}, + {name: "tracked-census-rust_ast.rs", src: mustRustProbeFile(t, "testdata/incremental_gate/rust_ast.rs")}, + {name: "admission-depth", src: []byte("fn main() {\n let values = [1, 2, 3];\n println!(\"{}\", values.len());\n}\n")}, + {name: "token-tree", src: []byte("fn main() {\n let v = vec![1 + 2, 3 * 4, 5 - 6, a & b, c | d];\n println!(\"{} {}\", x && y, p || q);\n assert_eq!(lhs << 2, rhs >> 1);\n my_macro!(a : b, c => d, e $ f, g ? h);\n nested!(inner!(deep!(x % y ^ z, !flag)));\n paths!(std::collections::HashMap, core::mem);\n mixed! { key: value; arr[idx] = func(arg) + 1 }\n}\n")}, + {name: "recovered-impl-item", src: []byte("pub type ExplicitSelf = Spanned;\n\nimpl Arg {\n pub fn to_self(&self) -> Option {\n if let PatKind::Ident(BindingMode::ByValue(mutbl), ident, _) = self.pat.node {\n if ident.node.name == keywords::SelfValue.name() {\n return match self.ty.node {\n TyKind::ImplicitSelf => Some(respan(self.pat.span, SelfKind::Value(mutbl))),\n _ => None,\n };\n }\n }\n None\n }\n}\n")}, + {name: "doc-comment", src: []byte("//! crate docs\n/// item docs\nfn f() {}\n")}, + {name: "token-binding", src: []byte("macro_rules! m { ($e:expr) => {} }\n")}, + {name: "pattern-statement", src: []byte("if let A(x) | B(x) = expr {\n do_stuff_with(x);\n}\n")}, + {name: "struct-expression", src: []byte("let a = SomeStruct { field1, field2: expression, field3, };\n")}, + } + + lang := grammars.RustLanguage() + for _, witness := range fixtures { + witness := witness + t.Run(witness.name, func(t *testing.T) { + sourceSHA := sha256.Sum256(witness.src) + rawParser := gotreesitter.NewParser(lang) + raw, err := rawParser.ParseNoResultCompatibilityBenchmarkOnly(witness.src) + if err != nil { + t.Fatalf("raw parse: %v", err) + } + defer raw.Release() + productionParser := gotreesitter.NewParser(lang) + productionParser.SetAdmissionCandidateRoute(false) + production, err := productionParser.Parse(witness.src) + if err != nil { + t.Fatalf("production parse: %v", err) + } + defer production.Release() + rawInspection, err := benchfixtures.InspectGoTree(raw.RootNode(), lang) + if err != nil { + t.Fatal(err) + } + productionInspection, err := benchfixtures.InspectGoTree(production.RootNode(), lang) + if err != nil { + t.Fatal(err) + } + rawRuntime := raw.ParseRuntime() + productionRuntime := production.ParseRuntime() + t.Logf("witness=%s bytes=%d source_sha256=%s raw_digest=%s production_digest=%s raw_rewrites=%d production_rewrites=%d raw_errors=%t production_errors=%t production_runtime=%s", witness.name, len(witness.src), hex.EncodeToString(sourceSHA[:]), rawInspection.SHA256, productionInspection.SHA256, rawRuntime.NormalizationNodesRewritten, productionRuntime.NormalizationNodesRewritten, raw.RootNode().HasError(), production.RootNode().HasError(), productionRuntime.Summary()) + if rawInspection.SHA256 != productionInspection.SHA256 { + t.Errorf("raw and production deep digests differ: raw=%s production=%s", rawInspection.SHA256, productionInspection.SHA256) + } + logRustDispatchPass(t, "production", productionRuntime) + + routeSource := append(append([]byte(nil), witness.src...), '\n') + routeProduction := parseRustProbeProduction(t, lang, routeSource) + defer routeProduction.Release() + routeProductionInspection, err := benchfixtures.InspectGoTree(routeProduction.RootNode(), lang) + if err != nil { + t.Fatal(err) + } + logRustRoute(t, "production-route", routeProduction, routeProductionInspection.SHA256) + + compactParser := gotreesitter.NewParser(lang) + compactParser.SetAdmissionCandidateRoute(true) + compact, err := compactParser.Parse(routeSource) + if err != nil { + t.Fatalf("compact route: %v", err) + } + defer compact.Release() + compactInspection, err := benchfixtures.InspectGoTree(compact.RootNode(), lang) + if err != nil { + t.Fatal(err) + } + routed, fallback := gotreesitter.AdmissionCandidateCounters() + t.Logf("route=compact candidate_routed=%d candidate_fallback=%d fallback_reason=%q digest=%s errors=%t runtime=%s", routed, fallback, gotreesitter.AdmissionCandidateLastFallbackReason(), compactInspection.SHA256, compact.RootNode().HasError(), compact.ParseRuntime().Summary()) + logRustDispatchPass(t, "compact", compact.ParseRuntime()) + if compactInspection.SHA256 != routeProductionInspection.SHA256 { + t.Errorf("compact digest=%s, want production-route %s", compactInspection.SHA256, routeProductionInspection.SHA256) + } + + forestParser := gotreesitter.NewParser(lang) + forest, forestOK := forestParser.ParseForestExperimental(routeSource) + if !forestOK || forest == nil { + offset, symbol, reason, states := forestParser.ForestDeclineInfo() + t.Logf("route=forest outcome=declined offset=%d symbol=%d reason=%q states=%v", offset, symbol, reason, states) + } else { + defer forest.Release() + forestInspection, inspectErr := benchfixtures.InspectGoTree(forest.RootNode(), lang) + if inspectErr != nil { + t.Fatal(inspectErr) + } + t.Logf("route=forest outcome=accepted digest=%s errors=%t runtime=%s", forestInspection.SHA256, forest.RootNode().HasError(), forest.ParseRuntime().Summary()) + logRustDispatchPass(t, "forest", forest.ParseRuntime()) + if forestInspection.SHA256 != routeProductionInspection.SHA256 { + t.Errorf("forest digest=%s, want production-route %s", forestInspection.SHA256, routeProductionInspection.SHA256) + } + } + + oldTree := parseRustProbeProduction(t, lang, witness.src) + defer oldTree.Release() + oldEnd := rustProbePointAtByte(witness.src, len(witness.src)) + oldTree.Edit(gotreesitter.InputEdit{StartByte: uint32(len(witness.src)), OldEndByte: uint32(len(witness.src)), NewEndByte: uint32(len(routeSource)), StartPoint: oldEnd, OldEndPoint: oldEnd, NewEndPoint: rustProbePointAtByte(routeSource, len(routeSource))}) + incrementalParser := gotreesitter.NewParser(lang) + incremental, profile, err := incrementalParser.ParseIncrementalProfiled(routeSource, oldTree) + if err != nil { + t.Fatalf("incremental route: %v", err) + } + defer incremental.Release() + incrementalInspection, err := benchfixtures.InspectGoTree(incremental.RootNode(), lang) + if err != nil { + t.Fatal(err) + } + t.Logf("route=incremental digest=%s errors=%t profile=%+v runtime=%s", incrementalInspection.SHA256, incremental.RootNode().HasError(), profile, incremental.ParseRuntime().Summary()) + logRustDispatchPass(t, "incremental", incremental.ParseRuntime()) + if incrementalInspection.SHA256 != routeProductionInspection.SHA256 { + t.Errorf("incremental digest=%s, want production-route %s", incrementalInspection.SHA256, routeProductionInspection.SHA256) + } + }) + } +} + +// TestRustDispatchRewriteTraceExpandedCorpus expands the receipt beyond the +// registered smoke and tracked witnesses. It records every Rust arm rewrite. +func TestRustDispatchRewriteTraceExpandedCorpus(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + fixtures := []rustDispatchProbeWitness{ + {name: "admission-direct-external-payload", src: mustRustProbeFile(t, "testdata/admission_direct/external_payload/rust.rs")}, + {name: "outline-rust-lib", src: mustRustProbeFile(t, "testdata/outline/rust/lib.rs")}, + {name: "parity-lifetime-and-abstract-types", src: []byte("fn main() {}\n\nfn add(x: i32, y: i32) -> i32 { return x + y; }\n\nfn foo(x: impl FnOnce() -> result::Result) {}\n\nfn foo(bar: impl for<'a> Baz>) {}\n")}, + {name: "parity-pattern-statements", src: []byte("if let A(x) | B(x) = expr { do_stuff_with(x); }\nwhile let A(x) | B(x) = expr { do_stuff_with(x); }\nlet Ok(index) | Err(index) = slice.binary_search(&x);\nfor A | B | C in c {}\n")}, + {name: "parity-macro-invocations", src: []byte("a!(* a *);\na!(& a &);\na!(- a -);\na!(b + c + +);\na!('a'..='z');\na!($);\na!($());\na!($ a $);\na!(${$([ a ])});\na!($a $a:ident $($a);*);\n")}, + {name: "parity-weird-expressions", src: []byte("fn angrydome() {\n loop { if break { } }\n let mut i = 0;\n loop { i += 1; if i == 1 { match (continue) { 1 => { }, _ => panic!(\"wat\") } }\n break; }\n}\n\nfn special_characters() {\n let val = !((|(..):(_,_),(|__@_|__)|__)((&*\"\\\\\",'🤔')/**/,{})=={&[..=..][..];})//\n ;\n assert!(!val);\n}\n\nfn function() {\n struct foo;\n impl Deref for foo {\n type Target = fn() -> Self;\n fn deref(&self) -> &Self::Target {\n &((|| foo) as _)\n }\n }\n let foo = foo () ()() ()()() ()()()() ()()()()();\n}\n\nfn closure_matching() {\n let x = |_| Some(1);\n let (|x| x) = match x(..) {\n |_| Some(2) => |_| Some(3),\n |_| _ => unreachable!(),\n };\n assert!(matches!(x(..), |_| Some(4)));\n}\n")}, + {name: "parity-weird-top-level", src: []byte("// Just a grab bag of stuff that you would not want to write.\n\nfn strange() -> bool { let _x: bool = return true; }\n\nfn what() {\n fn the(x: &Cell) {\n return while !x.get() { x.set(true); };\n }\n let i = &Cell::new(false);\n let dont = {||the(i)};\n dont();\n assert!((i.get()));\n}\n\nfn punch_card() -> impl std::fmt::Debug {\n ..=..=.. .. .. .. .. .. .. .. .. .. .. ..=.. ..\n ..=.. ..=.. .. .. .. .. .. .. .. .. ..=..=..=..\n ..=.. ..=.. ..=.. ..=.. .. ..=..=.. .. ..=.. ..\n ..=..=.. .. ..=.. ..=.. ..=.. .. .. .. ..=.. ..\n ..=.. ..=.. ..=.. ..=.. .. ..=.. .. .. ..=.. ..\n ..=.. ..=.. ..=.. ..=.. .. .. ..=.. .. ..=.. ..\n ..=.. ..=.. .. ..=..=.. ..=..=.. .. .. ..=.. ..\n}\n")}, + } + lang := grammars.RustLanguage() + for _, fixture := range fixtures { + fixture := fixture + t.Run(fixture.name, func(t *testing.T) { + rawParser := gotreesitter.NewParser(lang) + raw, err := rawParser.ParseNoResultCompatibilityBenchmarkOnly(fixture.src) + if err != nil { + t.Fatalf("raw parse: %v", err) + } + defer raw.Release() + productionParser := gotreesitter.NewParser(lang) + productionParser.SetAdmissionCandidateRoute(false) + production, err := productionParser.Parse(fixture.src) + if err != nil { + t.Fatalf("production parse: %v", err) + } + defer production.Release() + rawInspection, err := benchfixtures.InspectGoTree(raw.RootNode(), lang) + if err != nil { + t.Fatal(err) + } + productionInspection, err := benchfixtures.InspectGoTree(production.RootNode(), lang) + if err != nil { + t.Fatal(err) + } + t.Logf("expanded=%s bytes=%d raw_digest=%s production_digest=%s raw_rewrites=%d production_rewrites=%d raw_errors=%t production_errors=%t", fixture.name, len(fixture.src), rawInspection.SHA256, productionInspection.SHA256, raw.ParseRuntime().NormalizationNodesRewritten, production.ParseRuntime().NormalizationNodesRewritten, raw.RootNode().HasError(), production.RootNode().HasError()) + logRustDispatchPass(t, "expanded-production", production.ParseRuntime()) + if rawInspection.SHA256 != productionInspection.SHA256 { + t.Logf("expanded raw/production deep mismatch: raw=%s production=%s", rawInspection.SHA256, productionInspection.SHA256) + } + }) + } +} + +// TestRustDispatchRewriteTraceMalformedCorpus probes the recovery-only paths +// that the clean corpus does not reach. Keep this diagnostic receipt separate. +func TestRustDispatchRewriteTraceMalformedCorpus(t *testing.T) { + t.Setenv("GTS_DISPATCHER_CENSUS", "1") + fixtures := []rustDispatchProbeWitness{ + {name: "malformed-function-impl-type", src: []byte("fn foo(bar: impl for<'a> Baz>) {\n")}, + {name: "malformed-top-level-impl", src: []byte("impl Arg {\n pub fn f(&self) -> bool { self.value }\n")}, + {name: "malformed-let-closure", src: []byte("fn f() { let x = (|a| a + 1; }\n")}, + {name: "malformed-token-tree", src: []byte("macro_rules! m { ($e:expr => {\n")}, + {name: "malformed-pattern-statement", src: []byte("if let A(x) | B(x) = expr {\n")}, + {name: "malformed-struct-expression", src: []byte("let a = SomeStruct { field1, field2: expression\n")}, + {name: "malformed-doc-comment", src: []byte("/// docs\nfn f(\n")}, + } + lang := grammars.RustLanguage() + for _, fixture := range fixtures { + fixture := fixture + t.Run(fixture.name, func(t *testing.T) { + rawParser := gotreesitter.NewParser(lang) + raw, err := rawParser.ParseNoResultCompatibilityBenchmarkOnly(fixture.src) + if err != nil { + t.Fatalf("raw parse: %v", err) + } + defer raw.Release() + productionParser := gotreesitter.NewParser(lang) + productionParser.SetAdmissionCandidateRoute(false) + production, err := productionParser.Parse(fixture.src) + if err != nil { + t.Fatalf("production parse: %v", err) + } + defer production.Release() + rawInspection, err := benchfixtures.InspectGoTree(raw.RootNode(), lang) + if err != nil { + t.Fatal(err) + } + productionInspection, err := benchfixtures.InspectGoTree(production.RootNode(), lang) + if err != nil { + t.Fatal(err) + } + t.Logf("malformed=%s bytes=%d raw_digest=%s production_digest=%s raw_rewrites=%d production_rewrites=%d raw_errors=%t production_errors=%t", fixture.name, len(fixture.src), rawInspection.SHA256, productionInspection.SHA256, raw.ParseRuntime().NormalizationNodesRewritten, production.ParseRuntime().NormalizationNodesRewritten, raw.RootNode().HasError(), production.RootNode().HasError()) + logRustDispatchPass(t, "malformed-production", production.ParseRuntime()) + if rawInspection.SHA256 != productionInspection.SHA256 { + t.Logf("malformed raw/production deep mismatch: raw=%s production=%s", rawInspection.SHA256, productionInspection.SHA256) + } + }) + } +} + +func mustRustProbeFile(t *testing.T, path string) []byte { + t.Helper() + source, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return source +} + +func parseRustProbeProduction(t *testing.T, lang *gotreesitter.Language, source []byte) *gotreesitter.Tree { + t.Helper() + parser := gotreesitter.NewParser(lang) + parser.SetAdmissionCandidateRoute(false) + tree, err := parser.Parse(source) + if err != nil { + t.Fatalf("production parse: %v", err) + } + return tree +} + +func logRustDispatchPass(t *testing.T, route string, runtime gotreesitter.ParseRuntime) { + t.Helper() + if runtime.NormalizationPasses == nil { + t.Logf("route=%s dispatch.rust=absent", route) + return + } + for _, pass := range *runtime.NormalizationPasses { + if pass.Name == "dispatch.rust" || pass.Name == "rust_source_file_root_pre" || pass.Name == "rust_recovered_pattern_statements_root" || pass.Name == "rust_recovered_function_items" || pass.Name == "rust_recovered_struct_expression_root" || pass.Name == "rust_token_binding_patterns" || pass.Name == "rust_source_file_root_post" || pass.Name == "rust_doc_comment_ranges" { + t.Logf("route=%s pass=%s checked=%d run=%d visited=%d rewritten=%d", route, pass.Name, pass.Checked, pass.Run, pass.NodesVisited, pass.NodesRewritten) + } + } +} + +func logRustRoute(t *testing.T, route string, tree *gotreesitter.Tree, digest string) { + t.Helper() + t.Logf("route=%s digest=%s errors=%t runtime=%s", route, digest, tree.RootNode().HasError(), tree.ParseRuntime().Summary()) + logRustDispatchPass(t, route, tree.ParseRuntime()) +} + +func rustProbePointAtByte(source []byte, offset int) gotreesitter.Point { + if offset < 0 || offset > len(source) { + panic(fmt.Sprintf("point offset %d outside source length %d", offset, len(source))) + } + var point gotreesitter.Point + for _, value := range source[:offset] { + if value == '\n' { + point.Row++ + point.Column = 0 + } else { + point.Column++ + } + } + return point +}