Skip to content

parse cost forensics - #6367

Merged
briansrls merged 33 commits into
mainfrom
session/nimble-otter-476
Jul 8, 2026
Merged

briansrls merged 33 commits into
mainfrom
session/nimble-otter-476

Conversation

@briansrls

@briansrls briansrls commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

✅ #6365 has merged; origin/main is merged in — this PR's diff is now just two files: src/v1/stage0/src/v1_interpreter.rs (caller-attribution probe + measured-zero note) and src/v1/stage0/src/cli_run.rs (probe dump section).

Parse-cost forensics: pin the interpreted-parse DNF root cause + a measured land-or-zero on the interpreter fold-site cut

Continuation of the adhoc-c328b166-bca residual-driver hunt (neat-stag-780's #6365 named it "next-session work"). This lane took the interpreter-side free_monoid_to_vec fold chokepoint; the .dag-side accumulator fixes stay in #6365. Verdict up front: the datetime DNF root cause is pinned to one function, and the interpreter cut is a measured clean zero (dropped), leaving a reusable caller-attribution probe and a permanent no-op note.

Root cause — PINNED: lex_repeat_loop (src/v2/compiler/01_tokenize.dag:158)

A new caller-attribution probe (below) attributes each large fold_list to the .dag function that contains it. On datetime.dag (the DNF file), one caller dominates:

caller = lex_repeat_loop [left fold]   max_len=19021   elem=Int
  calls   330 -> 710 -> 1204   (climbing, timeboxed dumps)
  items   6.0M -> 12.3M -> 19.2M   (climbing => DNF)

19021 is datetime.dag's exact character count. lex_repeat_loop does fold_list(xs: init.remaining) — it folds the entire remaining source char-list purely to bound an iteration, and after done=true it still visits every remaining char (the true => state pass-through). So each call is O(remaining source length) regardless of the actual token length; once per repeat-token ⇒ O(n²) in source length. This is a second, deeper tokenizer O(n²), distinct from neat-stag's list_snoc reverse-cons fix — it confirms the "dies upstream of parse-proper" reframe and names the culprit. The root-cause dataflow fix (bounded recursion/Loop, bound = lexeme length not source length) is a .dag tokenizer change routed to the tokenize lane (neat-stag right-of-first-refusal); this probe receipt is the acceptance baseline.

The interpreter fold-site cut — MEASURED CLEAN ZERO (dropped)

Candidate: stream eval_fold_list_native (the left fold) over the Cons-chain instead of materializing an intermediate Vec (DESIGN §2 redundancy removal). It was built, proven byte-identical, and measured against two independently-built release binaries (base = neat-stag head; fix = base + streaming):

Check base fix (streaming)
parse-tree content hash, tiny 9afb740ee43f9e61 9afb740ee43f9e61 ✓
parse-tree content hash, small ed071cb2baeaeb61 ed071cb2baeaeb61 ✓
wall-clock, small (end-to-end) 20.5 / 19.5 s 19.2 / 20.9 s
peak RSS, small (end-to-end) ~168.3 MiB ~168.2 MiB

Byte-identical, but neither wall-clock nor peak RSS moves (both within run-to-run noise). Why it must be zero: the datetime driver folds elem=Int codepoint lists (a trivial copy, not a deep clone — so the removed materialization is pure alloc churn of the cheapest element), and the intermediate Vec is transient (freed each fold, so it never contributes to peak RSS). The real O(n²) is the caller re-folding the whole source (lex_repeat_loop), not this per-call materialization — so no interpreter change to fold_list can reprice the DNF.

Per DESIGN §6 (denominate in displaced cost; a no-op displaces nothing) and the operator's "root-cause dataflow only" steer, the streaming rewrite is dropped. A permanent in-place note records that it was tried and measured, so the next hunter does not re-derive it.

Two-axis site data (why tiny/small looked different from datetime)

free_monoid_to_vec per-site (by items), tiny → small → datetime:

  • tiny/small are fixed-cost-dominated by the witness harness's 43-module import-closure resolution (identical in both runs): value_hash (284) 427k→455k (1.06×, ~flat), right-fold (2580) call-heavy on ~2-element lists, contains (5434), left-fold (2556) small. The parsed file grew 4.65× (43→200 memo positions) while these grew ≤1.6× — they are not the file-size driver.
  • datetime is left-fold-dominated: site 2556 (left fold) alone reaches 30M+ items and climbing, everything else negligible — and the caller probe attributes 2556 entirely to lex_repeat_loop.

This de-lists all four free_monoid_to_vec heavy sites (value_hash, left/right fold, contains) for future hunts: none is the datetime driver; the driver is a .dag caller.

What lands

  • Caller-attribution probe (v1_interpreter.rs): DagFnGuard pushes the innermost .dag fn name on each call_function; record_fold_caller attributes large folds (≥100 items) to that caller with element type_label. Gated behind GUNBC_FLATTEN_SITE_DUMP_SECS (a no-op otherwise — the disabled overhead is within the ~20s noise, confirmed since the fix binary carrying it matched base). Extends neat-stag's flatten-site instrument with the .dag-caller axis that pinned lex_repeat_loop. dissolve-on: the residual-hunt work item closes.
  • Measured-zero note in eval_fold_list_native — records the streaming attempt + result so it is not re-investigated.

Test plan

  • Byte parity: parse_tree_content_hash_witness tiny+small, base vs fix binaries — identical.
  • Verdict: wall-clock + peak RSS, base vs fix, small end-to-end (both complete identical work — no progress confound) — zero within noise.
  • cargo fmt --all --check, cargo clippy --all-targets -- -D warnings (local).
  • CI (ci / ci_regen / rust_tests) — on ready.

🤖 Generated with Claude Code

briansrls and others added 30 commits July 7, 2026 19:56
…+ empty-list semantics pin (fix 2)

Manager-mandated merge-bar demands on #6362, addressed with durable,
re-runnable witnesses rather than a one-off manual diff:

- parse_tree_content_hash_witness.dag: content_hash(ParseTree) over the
  tiny/small corpus sample, computed once against the true PR base
  (0f98a26, built as its own release binary in an isolated worktree)
  and once against HEAD. Both hashes are byte-identical
  (witness.dag=9afb740ee43f9e61, execution_mode.dag=ed071cb2baeaeb61),
  proving fix 2 (list_at_optional -> list_head) and fix 3
  (fold_list -> parse_match_arm_stmt_step) preserve parse semantics
  exactly, not just "no test regressions."
- parse_token_first_empty_semantics_test.dag: two `test fn` claims
  pinning parse_token_first (post-fix, list_head-based) equal to
  list_at_optional(xs, index: 0) (pre-fix arm) on both the empty-list
  boundary case that motivated the rewrite and the ordinary present
  case. Both PASS via claim_batch.

Note: the local `main` ref was stale (21fb5d1); the true PR base is
origin/main's 0f98a26 (merge-base origin/main HEAD) — using the
stale ref for the pre-fix binary produced a false "grammar_invalid"
rejection unrelated to this PR (fixed upstream by #6337, landed after
21fb5d1 but before 0f98a26).
…us + token-stream witness

lex_walk_step accumulated tokens via list_snoc_item every token (O(n^2)
before parsing even starts) -- 4th instance of the same shape, this time
in a different pipeline stage than the three already-landed 02_parse.dag
fixes. Rewritten as reverse-cons + single .reverse() at tokenize()'s exit.

Verified byte-equal via a new order-sensitive token-stream content hash
(class+lexeme, in stream order) run against independently-built pre-fix
and post-fix binaries on tiny/small corpus samples -- identical both
sides, confirming a pure algorithmic rewrite.

Full read-based census of 02_parse.dag's 9 list_snoc_item call sites,
each dispositioned (dead/test-only, bounded grammar-analysis cost, or
already-fixed) -- none explain the file-size-scaling growth still seen
deeper in live parsing on datetime.dag. That residual is not resolved by
this PR; grammar_coverage_p8_wall_clock_blocker records the honest state
(tokenize confirmed non-bottleneck, DNF persists past grammar-validation,
driver not yet pinned) so the marker does not claim a false completion.

Added parse_table_record_hit/miss/lookup_call and
parse_choice_residue_backtrack to the interpreter's call-frequency
watchlist for the continuing in-session hunt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The auto-committer snapshotted a mid-edit state (b16a357) that deleted
the ProductionFirstRow type but left its consumers referencing it,
breaking typecheck. This completes that edit -- not a new change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…istinct-key discriminator

parse_match_arm_stmt_step and parse_expr_repeat_step both accumulated
per-iteration diagnostics via list_append(left: diags.reverse(), right:
acc.acc_diags) -- the acc_nodes field in the same accumulators was
already reverse-cons fixed, but acc_diags was missed by the prior
list_snoc_item-scoped census (this is a different primitive). Converted
to List<List<Diagnostic>> batches (Cons-prepended, O(1) per step) plus a
single flatten pass at the end (parse_flatten_diag_batches). Verified
byte-equal via parse_tree_content_hash_witness.dag on tiny/small
(9afb740ee43f9e61 / ed071cb2baeaeb61, unchanged).

Honest caveat: list_append(left: small, right: large) is O(len(left))
per the native fold_list_right implementation (it walks `left`, not
`right`), so this may not be the O(n^2) site the growing list_append
call count suggested -- kept as a correctness-neutral cleanup bringing
acc_diags in line with the already-safe acc_nodes pattern, not claimed
as a proven complexity fix.

Removed the temporary witness_forensics_scale_datetime_half_report
(scratch-path fixture, not for the repo).

Added a cross-thread parse-memo effectiveness discriminator
(PARSE_MEMO_LOOKUPS/HITS/DISTINCT_KEYS globals + record_parse_memo_lookup,
wired into try_parse_table_memo_dispatch's parse_table_lookup arm) per
the manager's decisive-discriminator request: lookups >> distinct with
hits == 0 would prove the native memo cache never serves a re-attempted
span. Result on the datetime.dag half-truncation: lookups=0 hits=0
distinct_keys=0 -- the native fast-path memo cache never engages at all
on this run, a separate real finding (not proven to be the DNF driver,
since the .dag-level table's own stats are never reached either, given
the run still doesn't finish).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…_SITE_DUMP_SECS, drop debug eprintln

Addresses cursor/composer-2.5's REQUEST_CHANGES on #6365: record_call_frequency,
record_flatten_site, record_list_cons_tail_split, and record_parse_memo_lookup
ran unconditionally on hot paths (eval_call, every free_monoid_to_vec
materialization, every native List Cons match, every packrat memo lookup) --
only the stderr dump was env-gated, not the recording itself. Added a
residual_hunt_forensics_enabled() OnceLock check (same env var the dump
already uses) and an early return at the top of each recorder, so the
default/production path pays one relaxed load per call site instead of a
mutex lock + HashMap/HashSet write. Added a SCAFFOLD/dissolve-on marker per
DESIGN Sec6 (dissolve when adhoc-c328b166-bca closes).

Also removed the ADHOC-DEBUG eprintln arms in parse_table_memo_scope_and_key
-- a local one-off diagnostic (auto-committer captured a mid-investigation
snapshot before I could revert it) that already served its purpose: it
proved the extraction logic is sound (zero branches fire on a working file,
lookups/hits/distinct_keys populate correctly), so it's not needed going
forward and was correctly flagged as ambient unconditional stderr noise on a
production code path.

Re-verified byte-equal (9afb740ee43f9e61) and forensics gating with/without
GUNBC_FLATTEN_SITE_DUMP_SECS set. cargo fmt --all applied (pre-existing
formatting drift on this file, unrelated to this change, caught by the
--check gate).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…+ measured-zero on interpreter fold-site cut

Continuation of adhoc-c328b166-bca residual-driver hunt (interpreter-side
free_monoid_to_vec fold chokepoint; .dag-side fixes stay in #6365).

Root cause PINNED via a new caller-attribution probe: the datetime interpreted-
parse DNF is lex_repeat_loop (src/v2/compiler/01_tokenize.dag:158) folding the
whole remaining source char-list (max_len=19021=file char count, elem=Int) once
per repeat-token => O(n²) in source length. A .dag tokenizer dataflow bug, routed
to the tokenize lane; this receipt is its acceptance baseline.

Interpreter fold-site cut = MEASURED CLEAN ZERO, dropped: a streaming left-fold
(no intermediate Vec) was built, proven byte-identical (parse-tree hash equal,
tiny+small, two independently-built binaries), and moved neither wall-clock (~20s
both) nor peak RSS (~168 MiB both) on small — elem=Int makes the removed alloc
churn negligible and the Vec is transient (no peak-RSS contribution). Kept
free_monoid_to_vec + a permanent note (DESIGN §6: a no-op displaces nothing).

Lands: caller-attribution probe (DagFnGuard + record_fold_caller, gated behind
GUNBC_FLATTEN_SITE_DUMP_SECS, dissolve-on the hunt) extending neat-stag's
flatten-site instrument with the .dag-caller axis; + the measured-zero note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review July 8, 2026 02:19
Brian Searls and others added 2 commits July 8, 2026 02:29
Reconciles the caller-attribution probe + measured-zero note onto the
squash-merged #6365 tokenizer/parse fixes. Conflicts were purely additive
(my probe blocks sit adjacent to neat-stag's flatten-site instrument); kept
both sides. Diff vs main is now only v1_interpreter.rs + cli_run.rs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Addressed — the conflict markers were transient, never in a pushed commit. Timeline: #6365 squash-merged into main at 02:28:04, which put my then-head a33dfcc into merge conflict with main; this review ran at 02:30:28 inside that window, so its merge-preview injected the <<<<<<< / ======= / >>>>>>> markers at cli_run.rs:2847 and v1_interpreter.rs:6674. I resolved the conflict with a merge commit (d24205d6, current head): the two probe blocks are purely additive next to neat-stag's now-merged flatten-site instrument, so I kept both sides and dropped the markers.

Verified on the current head d24205d6:

  • git grep '<<<<<<<' / >>>>>>> over both files → empty (no markers)
  • cargo build --release --bin gunbc → clean
  • cargo fmt --all --check and cargo clippy --bin gunbc -- -D warnings → clean
  • diff vs origin/main is now just the two files (v1_interpreter.rs +99, cli_run.rs +9)

DagFnGuard::enter (call_function) and record_fold_caller (both fold sites) are the intended additive wiring, not a broken intermediate state. No code change needed beyond the already-pushed merge resolution; a re-review of d24205d6 should come back clean.

— sent from nimble-otter-476

@briansrls
briansrls merged commit 7f4fb4a into main Jul 8, 2026
4 checks passed
@briansrls
briansrls deleted the session/nimble-otter-476 branch July 8, 2026 02:56
briansrls pushed a commit that referenced this pull request Jul 8, 2026
…6369/#6370)

Resolution choices:
- 02_parse diag accumulation: main's O(1) batch-cons + single flatten wins
  over per-step list_append (kills the 252k-appends cost the self-time
  profile measured); my O(1) memo position key preserved
- 01_tokenize: my superset kept (repeat short-circuit == main's #6368
  semantics, plus delimited short-circuit + CompiledLexRule precompile)
- v1 interpreter: both instrument sets kept (main's fold-caller/memo
  discriminator + my big-fold site/builtin-time/self-time profiler);
  main's Map-grounding fast path moved into call_function_inner so both
  profiled and unprofiled paths hit it; env gate consolidated on main's
  residual_hunt_forensics_enabled (same var, one authority)
- grammar_coverage ledger: main's fixes (1)-(4) text + my RESIDUAL CLOSED
  continuation renumbered (5)-(9)

Gates: build green, fmt/clippy green, regen divergence 0, tiny/small
packrat stats byte-identical (0/43/43, 0/200/200).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZSaWfysRfg28bNVS7ueX
briansrls pushed a commit that referenced this pull request Jul 8, 2026
… as a RewriteRule)

Corpus fix (the #6367-probe datetime driver): v2.std.provenance
span_index_record snoc-copied the whole entries list per minted
occurrence (one near-corpus-sized copy per token from the 02_parse mint
sites) and span_index_merge_entries snoc-copied per merged entry - both
converted to O(1) Cons-prepend (entries newest-first). Order-
insensitivity is a consumer-census fact carried on the carrier
(span_index_entries_prepend_note): every consumer resolves by unique
occurrence id (lookup first-match, merge presence-scan keeping base,
resolve-textual-locus via lookup), ids minted unique by the monotone
allocator. New witnesses (span_index_prepend_test): multi-record
round trip with wrong-extent red discriminator, merge-dedupe-keeps-base,
duplicate-id -> most-recent pinned (unreachable under unique minting),
merge lookups insensitive to incoming record order. No reverse added:
no order-sensitive consumer exists, pinned by execution.

Catalog row (first corpus-derived R1 row): v2.lens.rewrite.
accumulator_copy_to_prepend recognizes a measured Loop whose iteration
body feeds the loop-carried accumulator port through the copied port of
a registered copying append (lowered list_snoc_item xs=acc /
list_append left=acc; WHICH port copies read from the single authority
copied_port_fact_registry, realization pair in the row-local grounded
registry). apply -> prepend + reverse-once Bind form; claimed
Polynomial{2} -> Linear with the CLRS-style aggregate/arithmetic-series
accumulator-reversal citation; registered as row 3 of
rewrite_rule_registry. Witnesses (accumulator_copy_to_prepend_test, 12):
green - snoc and append subjects rewrite to independently hand-built
expected forms (content-hash + changed-hash discriminator), fixpoint,
strict drop via rewrite_rule_claims_strict_drop with rewritten-form
Linear oracle-measured and before-form Linear pinned as the honest
copy-blindness residue (executable dissolution trigger), equivalence by
execution on order-discriminating inputs with unreversed-prepend pinned
UNEQUAL; red - prepend output form not recognized, non-accumulator
append not recognized, effectful lookalike / acc-re-read /
binder-capture refuse typed, non-Loop silent; floor names the rule on
the snoc subject over the real registry.

Verified by execution: 12 new rewrite witnesses PASS; 4 new provenance
witnesses PASS; regressions all PASS - occurrence_id_model,
parse_stamp_span_index, edit_locus_resolver (2), cost_shape_lowering
recovery pins (2), rewrite_pass admission (8), registry integration (5),
nested_membership floor verdict, floor_coverage census incl.
dag/std/logic.dag counts pin (5), cost_coverage (3), R1 lens (3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ZJjv8DKX4PpywPNTy6wpc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant