Skip to content

One heads reading per file: pool census projects it instead of re-parsing (resolver cost, step 2) - #12656

Merged
gunbai-bot[bot] merged 5 commits into
mainfrom
session/bold-bat-516-heads
Sep 30, 2026
Merged

gunbai-bot[bot] merged 5 commits into
mainfrom
session/bold-bat-516-heads

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Step 2 of the resolver-cost item (node adhoc-a1f0bab0-a69, under jolly-boar-500). Stacked on #12643, which adds the attribution probe; the base changes to main once #12643 lands. The shape follows jolly-boar-500's decision (option A): one heads reading per file, projected rather than parsed again.

Defect

Every pool file was heads-parsed twice in one process:

  • once by module_path_index::parse_module_binding, in a fresh per-file space, to index module paths;
  • once more by the pool census (parse_module_heads_for_pool_census), threading one intern table and occurrence space across the pool.

The two reads parse the same bytes with the same grammar reading. They differ only in which intern/occurrence space the results are numbered in.

Change

  • pool_acquire::heads_reading_for: one heads reading per (spelling, bytes), parsed in a file-local space (empty intern table, occurrence ordinals from 0). It is a pure function of its key, so two consumers can share it.
  • parse_module_binding reads it as-is. It only uses the module name, its span and the refusal, and none of those carry an id.
  • The pool census maps it with census_heads::project_heads_reading into its threaded space instead of parsing again. The projection is total:
    • occurrence ids are allocated sequentially from the entry base, so local k becomes base + k, and the allocator leaves at base + local_next;
    • idents are intern ids. The local table holds the file's strings in first-seen order, which is the order the threaded parse interns the strings the pool hasn't seen yet. So interning them in local-id order reproduces the threaded table, and relabel[k] is the pool id.
    • The walk covers every nested node: children, params, uses, body, transport, properties, type annotation, match patterns, and InferredNode::Resolved, which the parser uses for written type expressions.
    • It refuses what the parser never produces: projected occurrences, and expression data carrying a semantic payload.

Controls

  • Identity differential, live pool: entry_resolve::heads_projection_live_differential::projected_heads_equal_the_threaded_parse_on_the_live_pool. It feeds every file of the [dag, src/v2] shared index, in pool_parse order, through both the old threaded parse and the new projection. It compares:
    • the whole module Node (every occurrence id and every ident);
    • the refusal;
    • the intern table and occurrence allocator each file leaves behind.
    • Executed: compared=7012, divergent=0. Resolutions and refusals are functions of these values, so they are unchanged.
  • Fixture, required unit run: census_heads::heads_projection_tests::projected_heads_equal_the_threaded_parse_at_identity_grain covers strings repeated across files, a variant match and a malformed head. the_unprojected_local_reading_is_not_the_threaded_reading is the red the comparison exists to catch: without the projection, the local reading differs from the threaded one from the second file on.
  • The existing census and heads unit tests pass, and clippy -D warnings is clean.
  • The first projection attempt refused on the parser's InferredNode::Resolved field types. That is the fail-closed arm working as designed; the projection now maps them.

Per-term table

Measured by entry_resolve::live_pool_entry_resolve_attribution (see #12643 for the runner setup), one cold process per column. Totals are not compared: two runs of the same tree differ by more than this change moves them.

term #12643 this PR
module_path_index (the one heads parse) 6.4s 7.0s
pool_census_parse (now the projection) 7.1s 4.2s

The remaining 4.2s is the projection itself (node rebuild plus interning into persistent maps), not a parse. It could be reduced further, but that would be a constant-factor change, not a cost-shape one.

Found while reading (next PR, not here)

tree_bare_census_for_root rebuilds build_symbol_index_census_raw_nodes over tree_census_nodes(index, root). That is the same pure function on the same arguments that closure_name_census(index, Some(root)) already built and memoized, so it's a second raw census per root. The typed-census PR will derive the tree census from that memo and then scope the signature upgrade to what the closure demands.

Hand-Rust receipt (v1 seed)

  • Admission: serves the v2 program (gunbc.v1_maintenance_standing v1_seed_standing). It removes a duplicated whole-pool parse from every --entry resolve.
  • Growth: no new module or scaffold. The code sits inside the existing seed-retained cli_run::{pool_acquire, census_heads} and module_path_index surface, and dissolves with the census when it moves to .dag.

Do not merge; landing goes through the operator.

🤖 Generated with Claude Code

gunbc-ci-auto-heal and others added 3 commits September 29, 2026 18:08
The via-index parse and the parse-cache miss arm re-lexed every closure file the pool
census had already lexed under the same spelling. Both now ask pool_acquire. Adds the
per-term attribution probe and a live identity differential (pooled vs fresh artifact
over the whole pool).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-parsing

pool_acquire holds one file-local heads reading (empty intern table, occurrence
ordinals from zero). module_path_index reads it as-is; the pool census maps it into
its threaded intern/occurrence space by a total projection (occurrence ids offset by
the entry base, idents relabeled through the file's string list) instead of parsing
the file a second time. Anything the parser does not produce refuses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every Node field and every ExprData / MatchPattern / InferredNode arm is named with
no '..' or wildcard, so a field or variant added later fails to compile at the walker
instead of passing through with file-local ids (review 72735).

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

gunbai-bot Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

Addressing review 72735.

Catch-all arms: fixed in c927086. project_node now destructures Node with no .., and names every arm of ExprData, MatchPattern and InferredNode with no wildcard. A new field or variant now fails to compile at the walker, so it can no longer pass through carrying file-local ids. The required generated lane's clippy step compiles this, so the wall is on the merge path. Only its execution-level evidence is not: the fixture differential runs in the non-required unit lane (declared drop gunbc.rung_drop rust_unit_tests_off_the_merge_path), and the live differential is #[ignore], receipts lane only. I'm stating that here rather than claiming more. Clippy -D warnings is clean and heads_projection_tests pass.

Seed receipt (advisory): no before/after seed census was run. That is a gap, and I'm naming it. The admission is the v2 program (gunbc.v1_maintenance_standing v1_seed_standing): it removes a duplicated whole-pool parse from every --entry resolve. The code sits inside the seed-retained resolver surface and dissolves with the pool census's .dag migration, which is the census's existing §7 marker (multi-entry SymbolIndex authority modeled in .dag). No scaffold path is deleted.

— sent from bold-bat-516

Base automatically changed from session/bold-bat-516 to main September 29, 2026 21:04
gunbc-ci-auto-heal and others added 2 commits September 29, 2026 21:16
…heads

# Conflicts:
#	src/v1/stage0/src/cli_run/entry_resolve.rs
…ites it, so Some refuses

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

gunbai-bot Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

Merged main in (no force-push) and retargeted this PR onto main after #12643 landed. Main's #12612 added Node.declaration (DeclarationRef, written by resolve). The exhaustive destructure from review 72735 refused to compile until the walker named it, which is that wall doing its job. The projection now refuses Some, because the heads parser never writes it, and carries None. Clippy is clean and heads_projection_tests pass.

— sent from bold-bat-516

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.

0 participants