Repository navigation
v2 native fold: stop per-module corpus rebuilds and per-token parse/lex rescans (R1, C1–C4) - #11401
Conversation
Recomposes #11217's net diff (royal-hawk-241, head b7d1403) onto main. validate_module_roots now returns ValidatedModuleRoots (ordered roots + by_name map, replacing the O(N^2) duplicate scan); ResolutionContext { lm, roots, symbol_index } is built once in native_test_context_from_ingest and native_test_resolve_module consumes it. Measured cause (fold11291 T, 716 modules over 2289 closure roots): resolve is a flat ~8.1 s per module (corr with decls 0.03), ~91 of ~135 min. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
…ne step per unit parse_token_stream_digest ran byte_limb_hash_peano_digest over every lexeme codepoint and both token offsets: a unary recursion, one combine_hash per unit of the value. perf on a bounded native-route root (K=16 tests, 142 roots) put it at 28% of the process, about half of context; parse_table_subject is its only reader. The unary digest also mapped every value above 255 to one tag, so token offsets and memo positions collided in the subject key. parse_int_digest / parse_lexeme_digest are single content atoms (decimal text, prefixed lexeme text): injective, and cost proportional to the bytes. Witness: witness_subject_position_sensitive_above_one_limb (300 vs 301). Local gunbc run: all six subject witnesses pass; mutation back to the unary digest reds exactly above_one_limb. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
…seed-read field Review amendment to C1. token_stream_digest stays an eager ParseTable field: the seed interpreter's cross-parse memo reads it (v1_interpreter parse_table_memo_scope_and_key), so it is a consumed key, not a dangling one. The parser no longer invents digests: - integer_int_content_digest (v2.std.integer): tagged content atom of the canonical decimal spelling; injective over Int, cost proportional to digits. Every existing integer digest in the corpus is unary (byte_limb_hash_peano_digest, and the byte-offset cache key built on it). - lexemes: symbol_identity_digest(symbol_intern_lexeme(..)), the precedent in test/manual/token_stream_content_hash_witness. byte_limb_hash_peano_digest itself is untouched. Collision class closed beyond offsets: lexeme codepoints above 255 shared one tag, so the seed memo key could not tell non-Latin-1 lexemes apart. Witnesses: positions 255/256/257 pairwise distinct; stream digest distinct for offsets 300 vs 301 and lexemes λ vs μ; stream digest deterministic. Local gunbc run: 9/9 subject+stream witnesses pass. Mutation M1 (integer digest back to unary) reds positions_255_256_257; M2 (lexeme back to per-codepoint unary) reds lexemes_past_latin1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
… index parse_nonterminal_memoized_core merged a memo entry's provenance with span_index_merge, which walks map_keys of the stored index -- every entry minted before the memoized production started, i.e. the file so far -- on every hit. perf --children on the bounded K=16 native-route root put span_index_merge (via parse_prov_merge) at 27.9% of the process. ParseProvenanceState now carries the memo frame: FrameMinted per recorded id, FrameNested per memo hit or miss inside the frame (a snoc sharing the inner list). A miss parses in a fresh frame, so its memo entry records only its own ids; a hit replays that frame through span_index_adopt (the same base-wins rule as span_index_merge, one lookup pair per id). Exactness, stated because it is not entry-for-entry: the stored index also held entries from the parse path that led to the memo miss. When the hit arrives by a different path those entries belong to no node on this path, and the old merge imported them anyway. Every node a hit returns carries only ids minted or replayed inside the memoized frame, so every id this path can look up is replayed; the index is read only by lookup(id). Equal on every reachable lookup, not on unreachable entries. Witness parse_memo_hit_resolves_every_node_like_a_memo_free_parse: real .dag specimen, real .dag grammar; Memoize (hits > 0) vs Recompute (hits == 0); every minted node has an entry and the pre-order textual-locus resolutions are equal. Local gunbc run: passes on C2; passes on the old whole-index merge (the oracle agrees with pre-C2 behaviour); mutant replaying no nested frame reds warm_entry_missing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
CI floor (declarations): gunbc.lambda_argument_typing_gate cited v2.compiler.parse parse_lexeme_digest, which C1/C1b deleted. The row was a real E0282 position -- the fold_list over chars(s:) in the lexeme digest -- and that position no longer exists, so its citation was stale and the carrier's dissolution trigger had nowhere left to be satisfied for it. The site moves from lambda_typing_gate_sites to a typed lambda_typing_gate_departed_sites row (CallerDeclarationDeleted, with the cause and cargo-probe receipt it was measured at and what deleted it). The carrier's 'a site does not stop being a member of the set it was found in' rule governs a still-red site whose cause differed; it does not keep a citation to a declaration that is gone. The dissolution trigger now names the tokenize row as the String declaration-site alias row; the long-lane population witness counts live plus departed sites (>= 3). Local gunbc run: instance_gap_carrier_declaration_refs_all_resolve passes. instance_gap_carrier_ref_population_is_not_empty is red, but on its list_membership_gap_declaration_refs >= 3 conjunct (that carrier projects 2); this change does not touch that carrier, and the lambda conjunct holds (2 live + 1 departed). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
…nce claim
CORRECTION. C2's message says parse_memo_hit_resolves_every_node_like_a_memo_free_parse
passes on C2, on the old merge, and reds on a mutant. Those runs called the
witness's helpers from a scratch driver; the test fn itself never evaluated. Called
directly, it failed on the seed interpreter with 'error type cascade' at its first
field read. The runs below call both test fns directly.
DEFECT FOUND (seed interpreter inference, not fixed here): inside a module, a field
read on a variable bound by Present { value: r } from an Optional of that module's
own record type evaluates to an error type. The same read on a declared parameter,
or from another module, types correctly. Isolated by bisection: position-independent,
name-independent, reproduced with a one-arm match on the first specimen. The witnesses
read the field through parse_memo_frame_replay_hits (a typed parameter).
NEW WITNESS parse_memo_hit_carries_no_abandoned_path_provenance. The real .dag
specimen cannot show that a memo hit no longer imports abandoned-path entries: a hit
path that mints as many ids before the memoized production as the miss path did
overwrites them (base wins). A hand grammar forces the difference --
S = R X z | a a X y, R = a a, X = b, tokens a a b y: alternative one mints four ids
before X and fails at z; alternative two mints three and hits X's memo entry, whose
stored index carries alternative one's fourth. Asserts hits > 0 / 0, equal pre-order
locus resolutions, and equal entry counts memoized vs memo-free.
Local gunbc run, test fns called directly:
C2: real_specimen=pass abandoned_path=pass (entries 8 vs 8)
pre-C2 (a99fb82 merge): real_specimen=pass abandoned_path=FAIL (entries 9 vs 8)
mutant (no nested replay): real_specimen=FAIL abandoned_path=pass
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
… grammar parse_choice_on_present_token decided left/right/backtrack at every choice node and token position with up to four symbol_list_contains scans of the two FIRST lists. perf --children on the bounded K=16 native-route root after C2: symbol_list_contains 17.3% of the process. The decision reads only the FIRST folds and the token class, so PreparedChoice now carries routes: Map<Symbol, ChoiceTokenRoute>, folded by prepare_grammar_expr with the same rule in the same order. An absent class is a backtrack answer, including every class in neither FIRST set. Witness prepared_choice_routes_agree_with_the_inline_dispatch_rule restates the inline rule as its oracle (not the table's builder) and checks every choice of the real .dag grammar, every class in either FIRST set plus one in neither; it asserts choices > 0 and classes > choices so an empty census cannot pass. Local gunbc run, test fn called directly: 462 choices, 3502 classes, 0 disagreements, pass. Mutant folding routes from the left FIRST list only: 777 disagreements, fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
lex_try_rules ran every compiled rule's matcher at every position (~56 rules for .dag); each matcher's first step splits the persistent source vector. perf --children on the bounded K=16 native-route root after C2: tokenize 20.6% of the process, mostly im::Vector skip/split_off under is_prefix_of. lex_compile_rules now records each rule's declared index and FIRST set (first characters, first character classes, nullable), folded over LexPattern. Rules whose FIRST is only literal characters are reached through a Map<Char, rules>; class-based or nullable rules sit in a residual list tested per position. Candidates are merged back into declared order, because the longest-match fold breaks ties toward the later rule. Exact because a rule whose FIRST excludes the head character and that cannot match empty returns NoMatch there, and prefer_longer keeps its accumulator on NoMatch; the FIRST set only over-approximates. With no head character every rule is tried, as before. Compile-time indexing uses a counter, not length(acc). Two lambda predicates were written as recursion on the typed list: a lambda over List<CompiledLexRule> read its parameter as T at the field access (the class gunbc.lambda_argument_typing_gate records). Witness lex_rule_dispatch_agrees_with_trying_every_rule: at every suffix of a .dag specimen (keywords vs keyword-prefixed identifiers, shared-first-char operators, the nullable scrutinee literal, classes, an escaped non-ASCII string, a comment, an atom) the fold over dispatched candidates equals the fold over every rule; asserts every position checked and some pruned. Local gunbc run, test fn called directly: 226/226 positions, 226 pruned, 0 disagreements, pass. Mutant dropping the residual list: 178 disagreements, fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
find_named_child built a Diagnostic (interned reason, node locus) on every miss, and seeded its fold with one on the hit path, so callers that only asked 'is it there' paid a diagnostic per probe. parse_production_emitted_identity_optional does exactly that under body lowering's subtree search (body_lower_find_captured): perf --children on the bounded K=64 native-route root after C4 put find_named_child at 12% of the process. It also counted occurrences in one pass and searched in a second. v2.std.node_query now owns NamedChildLookup = Found | Missing | Ambiguous, decided in one pass by named_child_lookup; find_named_child projects it with the same two refusal reasons and locus, building a diagnostic only when it returns one. The two parse-production optional queries in extdeps.languages.dag read the lookup directly. std.bounded_lattice_completeness's BlNamedChildLookup -- the same three states, recovered by decoding find_named_child's diagnostic reasons -- is deleted for the primitive. Witnesses (v2.test.std.named_child_lookup): all three states on a conjunction with a unique name, a repeated name and a positional child, plus a non-conjunction; and the projection's accepted target and exact refusal reasons. Local gunbc run, test fns called directly: both pass, and the bounded-lattice anchor (sg6_missing_meet_consumer_infer_rejects) still passes. Mutant that keeps the first match instead of reporting ambiguity: both witnesses fail, anchor unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
…orrected required-witnesses-floor on e5448f4 refused four identities. Over the 72,300-step budget for new witnesses (and two refused at enrolment): - choice routes (109k): the real-grammar census is replaced by the rule's case space, exhaustively -- left {a,b}, right {b,c} over {a,b,c,d} under all four nullable pairings, so every branch of the route rule is reached, including a both-sides class under a nullable left; oracle membership is map-backed. 4 choices x 16 classes, pass. Mutants: table from the left FIRST list only (4 disagreements), rule without the nullable-left branch (2) -- both fail. - memo replay real specimen (489k): replaced by a hand grammar whose memo hit carries a NESTED memoized frame (S = X z | X y, X = W b, W = a over a b y), the case the real specimen uniquely reached. Passes; the no-nested-replay mutant fails it (the abandoned-path witness is unaffected and still passes). - lexer dispatch (831k): every suffix of a long specimen replaced by the head of 21 short specimens, one per FIRST-set kind (keyword vs keyword-prefixed identifier, shared-first-character operators, classes, non-ASCII string, comment, atom, a character no rule starts on, the empty source). 21 checked, 20 pruned, 0 disagreements; the no-residual mutant gives 5 and fails. Changed-witness verdict, instance_gap_carrier_ref_population_is_not_empty: red on main (held) on list_membership_gap_declaration_refs >= 3. gunbc#9483 deleted one citable site with the session_auto_publish lane and lowered the site floor 5 -> 4 but not this one: 4 sites, 2 outside the index, 2 citable. Floor set to 2 with that provenance recorded beside it. Local gunbc run: population and outside-index witnesses pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
…next token An ambiguous choice (some class starts both branches) always backtracked: try left, on rejection try right from the same tokens and provenance -- for every token, including classes only the right branch can start. perf --children on the bounded K=64 native-route root after C4: parse_choice_residue_backtrack 22.7% of the process. The ambiguity census of the .dag grammar finds 10 such choices; five are in dag_production_expr, a chain of seven alternatives whose keyword forms overlap the binary-expression tail, so an expression starting with an identifier, literal, paren or operator failed through each keyword alternative first. For a class not in the left branch's FIRST set, with a left branch that cannot match empty, left rejects with certainty and backtracking returns exactly what parsing right returns. PreparedChoice now carries left_starts (the left FIRST set as a map), and the ambiguous arm of parse_choice_first_dispatch goes straight to right in that case; otherwise it backtracks as before. The skipped attempt could only have left memo entries for productions rejecting at their first token. Witness ambiguous_choice_skip_answers_what_backtracking_answered: the oracle is backtracking on the same choice and tokens (left fold marked nullable forces the try-left path); compares acceptance, remaining count, captured content hash and diagnostics, and pins each case's expected outcome: right-only class accepting (c d) and rejecting (c x), a nullable right accepting empty (x), and a left-start class (a b). Local gunbc run, test fn called directly: pass. Mutant that skips whenever left is not nullable (ignoring left_starts): fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
…ICE-0, not part of this PR B1 (096299b) reached session/fierce-seal-607 although it was pushed only to scratch/fierce-seal-607-b1; a commit on the session branch does not stay local. It adds a third field (left_starts) to the PreparedChoice shape that PREPARED-CHOICE-0 replaces wholesale, so landing it here would be a scaffold. It remains on scratch/fierce-seal-607-b1 as the measured baseline for what choice narrowing buys. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
…ady carry Review finding on #11401: the carrier's comments still said 'ten occurrences ... five producers' and 'these five module paths' after gunbc#9483 deleted a row -- the paragraph that forbids storing derivable totals had transcribed them in prose, and they went stale exactly as it warns. The totals now read as derivations; the merge-time specimen is rephrased without a count. Annotation-only change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
…ess under budget required-witnesses-floor refused two cost rows, no wrong verdict: - instance_gap_carrier_declaration_refs_all_resolve (withheld cost debt on main) PASSED-OVER-BUDGET at 34.9 s, almost all of it the shared module_path_index fill it paid (629 eval steps of its own). It ran because the explanatory comment added in 18d6aa6 sat between its closing brace and the next test fn, and changed-witness selection attributes changed LINES to declarations (claim_edit_changes_execution_policy_outside_reported_cost_delta). The comment is removed; the #9483 provenance already lives in the carrier prose, the commit and the PR body. The file's diff is now the import and the two conjuncts inside instance_gap_carrier_ref_population_is_not_empty only. - lex_rule_dispatch_agrees_with_trying_every_rule: 72,615 steps vs 72,300. Two redundant specimens dropped (fnord duplicates iffy's keyword-prefix case; !x duplicates the !/!= shared first character). Local gunbc run: 19 checked, 18 pruned, 0 disagreements, pass; no-residual mutant 4 disagreements, fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
|
Exact-head landing receipt, started on srv1 (declared in the body): T = Prediction, recorded before the result:
The M-vs-merge-base gap is a known confound. I am not re-folding main, which would take about 2 h. Instead, every non-added diff row gets attributed by module. |
Conflicts in v2.compiler.name_resolve and its admission_fail_closed witness came from #11367, which landed the ValidatedModuleRoots/by_name half of #11217 on main. This branch's R1 already carries that change plus the shared ResolutionContext built once per fold, so both conflicted hunks resolve to this branch's side: every declaration #11367 added (keyed_validation_* witnesses, validated index lookup) is already present here, differing only in layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar
|
The head moved, so the receipt started above for The move was the dashboard's merge-conflict notice. #11367 landed the The exact-head T fold now runs at |
…tells Operator ruling 2026-09-15: black-boxing a -> d and optimizing a measured axis is locally sound and globally inefficient (moves cost, suppresses the deletion signal, sets scope by target); the qualitative stance is the obligatory one toward a process one owns. Receipts: R1 (#11401), B1 vs the prepared choice plan (#11422), the TCO loop (#11444). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
The v2 native fold took ~2 h. Three constant-factor and cost-shape defects account for almost all of it, found by reading an existing fold log and then bounded, profiled runs (no full fold). This PR removes them, one commit per step, each with its own bounded receipt.
fbf06b05bebdd53f→ C1ba99fb82e--children, K=16 root: 28% of process1d2425f0+ validationa46cf13amap_keysover the file so far)--children, K=16 root: 27.9% of process2a4e2cd7--children, K=16 root after C2:symbol_list_contains17.3%47b10505--children, K=16 root after C2: tokenize 20.6%e5448f43find_named_childbuilt a diagnostic per probe;std.bounded_lattice_completenessre-decoded its three states from diagnostic reasons18d6aa65e5448f43d06c65a2gunbc.lambda_argument_typing_gatecitedparse_lexeme_digest, which C1 deleted (CI floor, declarations)1d2425f0R1 recomposes #11217 (royal-hawk-241) onto main; that PR is superseded by this one.
Instrument
Bounded A/B on srv1:
mkroot.shhardlinksdag/+src/v2/from a built tree keeping only the first K of 16 fixed accepted test modules (the driver ingests only their import closure); both emitted compilers run concurrently on same-shape roots, 600 s cap,/usr/bin/time -v; rows compared withcmp. Numbers below are from those runs' own[native-cost-partition]/[native-prepare-split]lines.Receipts
R1 — K=16, 143 roots, main
cf37d1e6vs R1fbf06b05compiler_materialization_witness; the first module resolved takes 0.053 s, so no per-context build is hiding in it)C1 — K=16, R1
fbf06b05vs C1bebdd53fC1b — K=16, C1
bebdd53fvs C1ba99fb82egunbc run, paired mutations): positions 255/256/257 pairwise distinct; stream digest distinct for offsets 300 vs 301 and lexemes λ vs μ; deterministic — 9/9 pass. M1 (Int digest back to unary) redspositions_255_256_257; M2 (lexeme back to per-codepoint unary) redslexemes_past_latin1token_stream_digeststays eager: the seed interpreter's cross-parse memo reads it (v1_interpreterparse_table_memo_scope_and_key), so the >255 collisions were in a live keyC2 — K=16, C1b
a99fb82evs C21d2425f0context 205.9 s → 114.6 s (−44%); wall 3:45 → 2:14; peak RSS +2.7%
driver rows byte-identical
predicted a further 40–50%: held
witnesses, test fns called directly (local
gunbc run):18d6aa65)correction: C2's own commit message claimed the real-specimen witness passed; those runs called its helpers from a driver, and the test fn did not evaluate (see defect below).
a46cf13arecords the correction and the direct runs above.exactness is per reachable lookup, not per entry: entries an abandoned parse path minted before the memoized production are no longer imported. The only non-test production reader of the span index outside parse/provenance is
identity_captured_navigation, keyed by occurrence ids from accepted nodes.Cumulative on K=16 (143 roots): main → C2
Larger closure — K=64 (301 roots), each pair concurrent
The C2 binary measured 207.5 s and 249.9 s context in two different runs: srv1 load moves absolute numbers, so only same-run pairs are compared.
C3 — witness
prepared_choice_routes_agree_with_the_inline_dispatch_rule: the inline rule restated as oracle over the rule's whole case space (left {a,b}, right {b,c}, universe {a,b,c,d}, all four nullable pairings) — 4 choices × 16 classes, 0 disagreements. Mutants: routes from the left FIRST list only (4), rule without the nullable-left branch (2) — both fail. (First version censused the real grammar — 462 choices, 0 disagreements, left-only mutant 777 — and was over the floor's step budget.)C4 — witness
lex_rule_dispatch_agrees_with_trying_every_rule: at the head of 19 short specimens, one per FIRST-set kind, the fold over dispatched candidates equals the fold over every rule — 19 checked, 18 pruned, 0 disagreements. Mutant dropping the residual rules: 4 disagreements, fail.Found along the way, not fixed here
List<CompiledLexRule>typed its parameter asTat a field access; written as recursion on the typed list (the classgunbc.lambda_argument_typing_gaterecords).Present { value: r }from anOptionalof that module's own record type types as an error ("error type cascade"); a declared parameter or another module types correctly. The C2 witnesses read through a typed-parameter accessor.18d6aa65):test.claim.long.carrier_reference_integrity_witness_testinstance_gap_carrier_ref_population_is_not_emptywas held red on main onlist_membership_gap_declaration_refs >= 3; gunbc#9483 deleted one citable site and lowered the site floor but not this one (4 sites, 2 outside the index, 2 citable). Floor set to 2 with that provenance; the carrier's own prose, which had transcribed the stale totals, is corrected in the head commit.Follow-ups
node://adhoc-583fffb6-357): parse backtracking is the largest remaining context cost (22.7%); every ordered choice compiles into one prepared plan, whole cutover, not per-production rewrites.Not yet done
Scope note
B1 (ambiguous-choice skip,
096299b2) reached the session branch by accident and is reverted in9c81ebab; it lives onscratch/fierce-seal-607-b1as the measured baseline for PREPARED-CHOICE-0 and is not part of this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01NHwsRfYwr6M5rXcCQFNdar