99%

Intelligence Reconnaissance

Queries run 2026-04-17:

  • scripts/intel-query.sh --human file-symbols "ori_llvm/src/codegen/type_info" --repo ori — inventory type_info module symbols before investigating BoundVar residue in the shared Pool at MonoInstance compilation.
  • scripts/intel-query.sh --human callers "lambda_mono" --repo ori — blast radius of the lambda_mono function compiler; identifies all callers that feed polymorphic lambda types into the mono pipeline.
  • scripts/intel-query.sh --human callers "MonoInstance" --repo ori — find all sites that construct or consume MonoInstance to map the full monomorphization pipeline before §08.3 changes.
  • scripts/intel-query.sh --human similar "BoundVar substitution monomorphization" --repo rust,swift --limit 5 — prior art for BoundVar/bound-type-param scoping during poly-function monomorphization (Rust rustc_codegen_ssa MonoItem, Swift SIL mono substitution passes).

Results summary (≤500 chars) [ori]: lambda_mono in ori_llvm/src/codegen/function_compiler/; MonoInstance constructed in the LLVM codegen pipeline. type_info module manages Pool-backed type lookups for LLVM IR construction. [rust]: rustc_codegen_ssa uses MonoItem::Fn with a full Instance (def-id + substs) to isolate each mono copy’s type environment — the exact scoping isolation §08.3 needs. [swift]: SIL mono runs a dedicated SubstitutionMap pass to ensure no residual generic params survive into the mono copy.

Reviewer-surfaced reconnaissance (distilled from the /review-plan Step 4 /tp-help blind-spots round — 2026-04-18; codex HIGH trust + gemini LOWER trust convergence; every claim below verified manually against the cited source):

  • body_type_map substitution surface is split across TWO scopes — local mono at compiler/ori_types/src/infer/expr/calls/monomorphization.rs:94-107 and imported mono at compiler/oric/src/test/runner/llvm_backend.rs:317-355. Same abstraction, two scopes, one bug surface — a fourth root-cause candidate the original §08 hypothesis list missed.
  • is_polymorphic_lambda at compiler/ori_llvm/src/codegen/function_compiler/lambda_mono/type_resolve.rs:55-73 only inspects BoundVar/Scheme on return types — lambdas whose return stays Tag::Var(Generalized) bypass mono handling entirely. Separate failure mode from the three §08 hypotheses.
  • apply_bound_var_map at lambda_mono/type_resolve.rs:142 only fixes top-level vars; nested generics inside containers (List<T>) remain unsubstituted.
  • fallback_bound_vars_to_int at lambda_mono/type_resolve.rs:392 silently converts unresolved-type bugs into ABI/RC bugs — leaving it enabled during §08.3 will misclassify the root cause.
  • resolve_all_lambda_bound_vars (a lambda_mono helper, NOT §04’s seam) runs at TWO callsites: compiler/ori_llvm/src/codegen/function_compiler/define_phase.rs:134 (inside emit_arc_function) and compiler/ori_llvm/src/codegen/function_compiler/nounwind/prepare.rs:173. §04’s assert_no_unresolved_type_vars seam — distinct from this helper — sits at define_phase.rs:315 (process_arc_function) and define_phase.rs:375 (declare_and_process_lambda) per the parent plan’s overview Architecture diagram. §04’s seam choice MUST land AFTER both resolve_all_lambda_bound_vars callsites have run; otherwise the assertion fires on legitimate pre-resolution BoundVar state. See §08.6 for the coordination protocol.
  • prepare_mono_cached at nounwind/prepare.rs:95-120 has a cache-miss fallback path (canon.root_for(mono_fn.original_name).unwrap_or(canon.root)) that is currently uncovered by §08.2’s matrix — adds a §08.2 negative pin.

Section 08: Codegen Poly-Lambda Monomorphization (absorbs BUG-04-042)

Status: Complete (see frontmatter status: complete, flipped 2026-04-20 at §08.N close-out). Section is no longer a commit blocker — test-all.sh runs to completion with no CRASHED line, clippy clean, JIT spec corpus 10/10 + 17/17 on debug and release-LTO. Historical framing (“In Progress — blocks atomic commits”) is preserved in §08’s origin note below for audit.

Origin: Absorbed from bug-tracker BUG-04-042 on 2026-04-17 per CLAUDE.md §Ownership & Deferral “Plan-blocker bugs belong IN the plan — NEVER sibling fix files”. The bug was originally filed 2026-04-06 by /continue-roadmap, marked BLOCKED 2026-04-09 pending coordination with roadmap §21A, and blocked every prior commit attempt on this plan’s validator-wiring work (§03.1, §03.2). Per the classifying rule “Can the plan complete with this bug open?” — the answer is NO (the plan cannot land its stated deliverable without a green test-all.sh), so the bug belongs in plan scope.

Goal: Resolve the polymorphic-lambda BoundVar bleed that prevents imported generic monomorphization when the host module contains polymorphic lambda definitions. Concrete failure mode: tests/spec/expressions/lambda_mono.ori — which contains polymorphic lambda definitions and calls assert_eq (an imported generic from std.testing) — fails via --backend=llvm with Idx(241) unresolved type variable and 17 LCFails, while tests/spec/types/integer_safety.ori (which calls assert_eq without local polymorphic lambdas) passes cleanly.

Why This Was a Commit Blocker (historical — resolved 2026-04-20 via §08.3)

Historical framing — retained for audit trail; the symptoms described below no longer reproduce at HEAD.

Every commit touching the plan’s sections triggered the lefthook pre-commit hook, which runs ./test-all.sh. Because the Ori spec (LLVM backend) run CRASHED on the assert_eq<T> monomorphization path prior to §08.3, every commit attempt failed — even commits that only touched plan-internal typeck files. This is why §03.2 could not land without Section 08: the test gate is a hard precondition for commits, and BUG-04-042’s symptoms failed the gate.

Previous sessions deferred this via /add-bug repeatedly, each creating a sibling fix file, each waiting on roadmap §21A coordination. The chain never completed. This section closed the chain by letting the plan own the fix directly. Post-§08.3 the LLVM-backend spec run no longer CRASHES (verified via test-all.sh at HEAD=d2a1d5a1 and earlier, 2026-04-20 close-out per §08.N).

Root-Cause Hypothesis (historical; superseded 2026-04-19 — see §08.1.R)

“Polymorphic lambda BoundVar types in the shared Pool interfere with MonoInstance body compilation for imported generics. Fix spans Pool scoping, type_info store, function compiler, and lambda_mono.” — plans/bug-tracker/section-04-codegen-llvm.md:459

Status: this hypothesis list and the 2026-04-18 classification that flowed from it are SUPERSEDED. See §08.1.R HISTORY block for the corrected diagnosis (cross-module pool-merge var_id collision at compiler/oric/src/test/runner/llvm_backend.rs:320-360). The candidates below are retained as historical context for readers tracing the investigation arc; they are NOT the active root cause.

Historical candidate root causes that §08.1 investigated:

  1. Pool contamination (hypothesis 1): polymorphic lambda registrations leave BoundVar residue in the shared Pool. Status: CLOSE, but not quite — the collision is in the TEST-RUNNER’s merged_pool, not the module-level pool produced by typeck. The real bug is adjacent: imported types re-interned into the merged pool carry unchanged source var_ids that alias host var_states slots (§08.1.R evidence item 1).
  2. type_info store leak (hypothesis 2): Idx(241) observed at TypeInfoStore. Status: SURFACE SYMPTOM — TypeInfoStore is where the collision manifests as an observable error, not where it originates. Fixing the store was ruled out during 2026-04-18 investigation.
  3. function_compiler/lambda_mono sequencing (hypothesis 3): order of poly-lambda body vs imported-generic mono compilation. Status: REFUTED — codex Step 4 architectural_risks confirmed production codegen uses a single pool (no merge step); sequencing is not the issue.
  4. body_type_map / arc_cache cache poisoning (hypothesis 4): shared cache state across mono contexts. Status: REFUTED — TypeInfoStore is per-codegen-context (documented at compiler/ori_llvm/src/codegen/type_info/store.rs:37-65); cross-context poisoning via this cache is not a candidate mechanism.
  5. Typeck producer leak (2026-04-18 classification, Hypothesis (d)): Tag::Var(VarState::Generalized) escaping typeck’s PC-2 boundary. Status: REFUTED (2026-04-19) — the vars ARE correctly VarState::Generalized post-typeck; the leak is not at typeck’s boundary but at the test-runner’s pool-merge boundary downstream. Both broad and narrowed sibling-pass implementations were tried and reverted at HEAD=3dd4ded6 because typeck.md §GN-3 + build_exempt_var_ids exempt every Generalized var from defaulting. See §08.1.R HISTORY block.

Active root cause (2026-04-19): cross-module pool-merge var_id collision. See §08.1.R single-sentence root cause and evidence chain.

Architectural risk note (retained, still accurate under corrected diagnosis): codegen has its own type-instantiation phase that runs AFTER typeck PC-2 — ori_arc::lower::calls::lambda applies type_subst during ARC lowering, and lambda_mono/mod.rs mutates ARC IR. These passes are CONSUMERS of the pool’s type facts, not producers; they cannot be the root cause of the collision. Under §08.3’s fix, they receive clean post-remap types and operate correctly without modification. The “parallel emission path” concern in the prior plan-text resolves to “no additional producer exists — the pool-merge is the single upstream producer of the corrupted state”.

08.1 Investigation and root cause analysis

Goal: Produce a single sentence naming the root cause and the file + line(s) where it originates. Investigation MUST consider all hypothesis candidates (per the expanded list above).

Classification outcome (2026-04-19, SUPERSEDES the 2026-04-18 classification): The original 2026-04-18 classification (Hypothesis (d)Tag::Var(VarState::Generalized) leaking from typeck) was WRONG. Both broad and narrowed sibling-pass implementations of “default unbound vars from poly-lambda returns” were tried and reverted at HEAD=3dd4ded6 because typeck.md §GN-3 Value Restriction converts unconstrained vars to VarState::Generalized during body inference, and the end-of-body defaulting pass’s exemption set (built by build_exempt_var_ids at compiler/ori_types/src/check/validators/mod.rs, cited by typeck.md §PC-2 “End-of-body defaulting pre-pass”) is scope-by-var and includes every VarState::Generalized var — the sibling pass can never substitute them. The actual root cause is upstream of typeck’s hand-off entirely: a cross-module pool-merge var_id collision in the test runner at compiler/oric/src/test/runner/llvm_backend.rs:320-360 that corrupts the merged pool’s var_states indexing, so a downstream Tag::Var read that LOOKS like “typeck leaked” is actually reading a host-file-originated VarState::Generalized slot through an imported var_id that was never shifted. The formal decision between remap-aware re-intern vs alternative fix shapes is §08.1.5’s responsibility (§08.1.5 is the decision gate). See §08.1.R HISTORY block for the full diagnosis correction.

  • Reproduce the failure cleanly: timeout 150 cargo run --bin ori -- test --backend=llvm tests/spec/expressions/lambda_mono.ori → captured Idx(241) unresolved at ori_llvm::codegen::type_info::store + 17 LCFails (run 2026-04-18, 95.54ms). Test harness reports exit “OK” despite the 17 failures because the outer test summary swallows per-file compile errors — the Ori spec (LLVM backend) CRASHED signal only fires on the full-suite run via ./test-all.sh.
  • [~] Reduce the repro WITHOUT relying on #skip: DEFERRED to §08.2 TDD matrix (a minimal Rust unit test in compiler/ori_llvm/tests/aot/poly_lambda_mono.rs is the cleanest option per the original checkbox). Static classification (Hypothesis (d)) does not require a reduced repro; the TDD matrix in §08.2 will produce the minimal failing case as part of normal TDD discipline (failing test first, then fix).
  • [~] Trace the failing mono site: NOT RUN — runtime trace attempt was denied. Static replacement: resolve_fully at compiler/ori_types/src/pool/accessors.rs:434-437 only follows VarState::Link; for VarState::Generalized it breaks immediately, leaving current as the input Tag::Var. The comment at accessors.rs:429-432 literally documents the failure mode: “This can happen when Generalized type vars leak from type checking into codegen without proper resolution.” The Tag::Var arm at ori_llvm/src/codegen/type_info/store.rs:341-364 is the only error path that emits “unresolved type variable at codegen” — the Tag::BoundVar | RigidVar | Scheme | ... arm at :371-385 emits “unreachable type tag at codegen” instead, so the observed message pins the Tag to Var.
  • Bisect the origin: classified as cross-module pool-merge var_id collision at the test-runner boundary (§08.1.R active diagnosis, 2026-04-19). The original 2026-04-18 Hypothesis (d) classification (“Tag::Var(VarState::Generalized) from typeck bypassing validate_body_types”) is SUPERSEDED — the sibling-pass fix it implied was tried and reverted at HEAD=3dd4ded6 because typeck.md §GN-3 Value Restriction generalizes before defaulting fires. Active diagnosis, evidence chain, and revert rationale documented in §08.1.R. This activates §08.1.5 as the fix-shape decision gate, NOT as a producer-side fix gate.
  • [~] Inspect TypeInfoStore cache state at the failure point: NOT RUN — runtime inspection denied. Static replacement: the store’s Tag::Var arm calls self.pool.resolve_fully(idx) FIRST (line 342); a cache hit is impossible because get_impl is the point where the miss triggers the error. Hypothesis (e) (poisoned cache) is refuted for the single-file case — a single .ori file with one assert_eq<int> mono target cannot produce a cross-context poisoned entry within TypeInfoStore because TypeInfoStore is single-threaded per codegen context (per the Reviewer-surfaced reconnaissance block’s scope correction). If (e) were active, we would expect the error to fire only AFTER certain preceding function emissions — the current repro fires regardless of emission order, consistent with (d) and inconsistent with (e).
  • [~] Inspect the nounwind analyze pass: NOT RUN — static review suffices. nounwind/analyze.rs consumes TypeInfoStore::get() for arc-IR types; it reports the same TypeInfo::Error the Tag::Var arm produces. The nounwind pass is a consumer of the leak, not a producer — routing Tag::Var(Generalized) through nounwind vs through arc_emitter hits the same get_impl error path. No cache-poisoning signal found at nounwind layer.
  • Document the root cause in §08.1.R below. Active diagnosis (cross-module pool-merge var_id collision) documented in §08.1.R HISTORY block; §08.1.5 is the fix-shape decision gate between remap-aware re-intern (selected) and rejected alternatives (i)/(ii)/(iii). The T17–T19 regression pins in compiler/ori_types/src/check/validators/tests.rs stay as typeck-boundary clamps — they are NOT the §08 fix, and §08.1.5’s audit task adds doc comments to them so future readers do not mistake them for the BUG-04-042 enforcement point.

08.1.R Root-cause documentation (2026-04-19, corrected)

Classification: Cross-module pool-merge var_id collision at the test-runner boundary. The bug lives upstream of every hypothesis the original 2026-04-18 investigation considered — it is injected in the test-runner’s pool-merge step, BEFORE the resulting merged pool is handed to codegen.

Single-sentence root cause: In compiler/oric/src/test/runner/llvm_backend.rs:320-360 the test runner merges imported-module types into a per-test-file merged_pool whose var_states vector was cloned from the test-file pool (NOT from the imported pools); the re-interning path at compiler/ori_types/src/pool/re_intern/mod.rs:192-193 (Tag::Var | Tag::BoundVar | Tag::RigidVar => target.intern(tag, source.data(idx))) preserves imported var_ids unchanged into the target, and the widening call merged_pool.ensure_var_capacity(max_id + 1) at llvm_backend.rs:341-343 only APPENDS fresh Unbound slots — it never SHIFTS the imported var_ids — so an imported Tag::Var(var_id = N) reads slot N of merged_pool.var_states, which may hold a test-file poly-lambda’s VarState::Generalized state from an unrelated local binder; substitute_in_pool (compiler/ori_types/src/pool/substitute/mod.rs:82-88) then branches on that corrupted VarState::Generalized and emits a substitution that looks like “a generalized var leaked from typeck” but is in fact a var-id aliasing artifact of the pool merge.

Evidence chain (every link verified against source at HEAD=3dd4ded6):

  1. Collision site: compiler/oric/src/test/runner/llvm_backend.rs:334-343merged_pool.ensure_var_capacity(max_id + 1) after copying imported types’ var_ids unchanged. The inline comment at :329-333 documents: “Re-interned Vars carry source var_ids, but the merged pool’s var_states array was cloned from the test file’s pool and may not cover imported var_ids. substitute_in_pool follows links via var_state(), which panics on out-of-bounds var_ids.” — the widening call handles the crash, but NOT the semantic collision.

  2. Re-intern preserves source var_ids unchanged: compiler/ori_types/src/pool/re_intern/mod.rs:192-193Tag::Var | Tag::BoundVar | Tag::RigidVar => target.intern(tag, source.data(idx)). The comment above reads “Type variables: data = var_id (pool-independent)” — that comment is WRONG for merged pools, because var_states is indexed by var_id and is pool-specific.

  3. Re-intern hash fast path is invalid for var-bearing subtrees: compiler/ori_types/src/pool/re_intern/mod.rs:56-60 — if source.hash(idx) collides with an existing target entry, it returns the target entry as-is. Leaf Tag::Var hashes include the raw var_id (pool/mod.rs:395-413), so two imported vars that coincidentally share a Merkle hash with an unrelated local var deduplicate into the local var’s slot — another channel for the same collision.

  4. var_subst build reads the pre-remap scheme_var_ids: llvm_backend.rs:321-327var_subst is keyed by generic_sig.scheme_var_ids, which re_intern_sig at pool/re_intern/mod.rs:83-97 CLONES unchanged (never re-numbers). If §08.3 remaps imported leaf var_ids in the type tree but does NOT remap scheme_var_ids in the sig, the substitution map stops matching and the fix silently regresses.

  5. Tag::Scheme extra payload stores raw binder var_ids: compiler/ori_types/src/pool/construct/mod.rs:161-174 writes the binder var_id list into extra at intern time; compiler/ori_types/src/pool/re_intern/mod.rs:185-193 preserves them unchanged via source.scheme_vars(idx).to_vec() + target.scheme(&vars, body). Binders must remap together with leaves, not independently.

  6. VarState::Generalized branch reads the collided slot: compiler/ori_types/src/unify/substitute.rs:78-83 and compiler/ori_types/src/pool/substitute/mod.rs:82-88 both branch on VarState::Generalized. A remap that allocates fresh var_ids but blanks them to Unbound (instead of cloning the source’s VarState) destroys the semantic state and distorts downstream generic behavior — the source’s VarState::Generalized must be cloned into the new destination id.

  7. resolutions map is NOT in the blast radius: compiler/ori_types/src/pool/accessors.rs:358-359resolutions: FxHashMap<Idx, Idx>. Keys are Idx, NOT var_id. The fix does not touch this map; reviewers and implementers MUST NOT list it as a touch point or they will chase phantom work.

  8. Production codegen does NOT cross-pool-merge: compiler/ori_llvm/src/codegen/function_compiler/define_phase.rs:115-165 and compiler/ori_llvm/src/codegen/function_compiler/nounwind/prepare.rs:95-149 operate on a single pool. The collision surface is specifically the test-runner’s merge step — the production AOT path does not share this defect. §08.3’s fix site (test runner + pool/re_intern) is thus correct even though the USE site is test-only; the abstraction (§08.3 lifts remap logic into pool/re_intern/) is what keeps production codegen free of the defect class if the merge pattern ever ships there.

Fix ownership: Upstream of the typeck-output boundary — specifically, at the test-runner’s pool merge. Fixing codegen to tolerate the corrupted VarState::Generalized read would be inverted TDD (the pool’s append-only + var_id-pool-local invariants are the deliverable; weakening them on the failing path is banned per CLAUDE.md §INVERTED-TDD). Fixing typeck to re-default poly-lambda returns would fix a symptom the re-intern step is actually creating out of thin air. The correct fix is remap-aware re-intern — §08.3 owns it; §08.1.5 is the decision gate between remap-aware re-intern and any alternative (e.g., a per-module-scoped merged_pool that never combines var_id spaces at all — rejected below).

Why the sibling-pass fix was tried and reverted at HEAD=3dd4ded6 (historical note, load-bearing for future readers):

  • A broad default_unbound_vars_from_polylambda_returns pass walking every ExprKind::Lambda at end-of-body was implemented AND narrowed to just unbound-without-constraint returns. Both variants were reverted because typeck.md §GN-3 generalizes any unconstrained var in a lambda position during body inference BEFORE the end-of-body defaulting pass runs; by the time defaulting fires, the vars are already VarState::Generalized, and build_exempt_var_ids puts every VarState::Generalized var in the exempt set via its FunctionSig.scheme_var_ids path. The sibling pass therefore had nothing to substitute. No amount of narrowing or broadening recovered — the time order (§GN-3 runs first, defaulting runs second, validator runs third) is the architectural reason, not a tuning knob. Codex’s Step 4 blind-spots round (2026-04-19) independently flagged the same order-of-operations problem in its review.

HISTORY block — 2026-04-19 diagnosis correction:

  • 2026-04-18 §08.1 classified Hypothesis (d) (“typeck leaks Tag::Var(VarState::Generalized)”); §08.1.5 was marked complete with option (ii) selected (sibling pass); §08.3 was framed around the typeck fix.
  • 2026-04-19 /tp-help consensus (Gemini HIGH trust + codex Step 4 blind-spots) identified the cross-module pool-merge var_id collision at llvm_backend.rs:320-360 as the real root cause. The sibling-pass approach was tried and reverted at HEAD=3dd4ded6; the revert is evidence that §08.1.5’s prior “complete” state pointed at an approach that CANNOT work given typeck.md §GN-3 Value Restriction. Per CLAUDE.md §Plan Corrections Go IN the Plan, the prior diagnosis is rewritten here (not amended via memory), and §08.1.5’s status is reset to not-started to reflect that the fix shape is now different and the decision has not yet been implemented.
  • Cross-reference: memory pointer /home/eric/.claude/projects/-home-eric-projects-ori-lang/memory/project_bug_04_042_pool_merge_diagnosis.md (to be shrunk to a one-line pointer to this §08.1.R after the plan lands — per CLAUDE.md §Plan Corrections Go IN the Plan, the plan file is the authoritative record; the memory entry is a breadcrumb only).

08.1.5 Decide fix shape for the cross-module pool-merge var_id collision (must precede §08.3)

Goal: Under the corrected §08.1.R diagnosis (cross-module pool-merge var_id collision, not a typeck leak), confirm the fix shape for §08.3 BEFORE implementation starts. The decision is not “which producer is leaking” — §08.1.R pins the collision site at compiler/oric/src/test/runner/llvm_backend.rs:320-360 and the re-intern path at compiler/ori_types/src/pool/re_intern/mod.rs:185-193. The decision IS where the remap abstraction lives (test-runner call site only, or lifted into pool/re_intern/) and exactly which of the pool’s internal structures must be rewritten coherently so every Merkle-hash and substitution invariant still holds.

Why §08.1.5 reset from complete to not-started (2026-04-19): Per the §08.1.R HISTORY block, the prior 2026-04-18 decision (“option (ii) — sibling pass default_unbound_vars_from_polylambda_returns”) was based on the stale Hypothesis (d) diagnosis. Both broad and narrowed variants of that sibling pass were implemented and then reverted at HEAD=3dd4ded6 because typeck.md §GN-3 Value Restriction + build_exempt_var_ids exempt every VarState::Generalized var — the sibling pass has nothing to substitute. The “complete” state that §08.1.5 carried before this editor round pointed at a tried-and-reverted approach; resetting to not-started is a correction per CLAUDE.md §Plan Corrections Go IN the Plan. T17–T19 regression pins the prior round added to compiler/ori_types/src/check/validators/tests.rs remain legitimate typeck-boundary clamps (they pin that the validator is NOT the enforcement point for the pool-merge bug) but they are NOT the §08.3 fix itself — the fix is upstream of the typeck-output boundary.

Why options (i), (ii), (iii) are all rejected under the corrected diagnosis:

  • (i) extend default_unbound_vars_from_empty_literals to poly-lambda returns — rejected. The vars are already VarState::Generalized by the time the defaulting pass runs (typeck.md §GN-3 runs first); the defaulting pass’s exempt set covers them. Adds a hook with no effect on the failing path.
  • (ii) sibling pass default_unbound_vars_from_polylambda_returns — rejected. Tried and reverted at HEAD=3dd4ded6 for the same reason as (i). Evidence is in the revert itself.
  • (iii) remove the VarState::Generalized exemption from validate_body_types — rejected. Fires E2005 on every polymorphic let-binding, breaks let-polymorphism entirely; CLAUDE.md §INVERTED-TDD flags this as the canonical widened-exemption anti-pattern. Also: the vars aren’t actually unresolved — they’re correctly generalized; the real bug is var_id collision downstream.

The correct fix shape — remap-aware re-intern (per codex’s Step 4 blind-spots advice, 2026-04-19; confirmed as the sole approach compatible with pool invariants types.md §TY-6 append-only and types.md §TF-3 Merkle-hash propagation):

  1. Lift the remap logic into compiler/ori_types/src/pool/re_intern/ as a reusable abstraction. Do NOT bury it in llvm_backend.rs:320-360 even though the call site is there — the abstraction belongs in the pool’s re-intern module so any future cross-pool-merge call site (production codegen, WASM ori_compiler facade, other test harnesses) gets correct semantics by default. The llvm_backend.rs:320-360 call site becomes the first consumer, not the owner.

  2. Build a src_var_id → dst_var_id remap map during re-interning of imported types. For every imported Tag::Var / Tag::BoundVar / Tag::RigidVar / Tag::Scheme binder encountered, allocate a fresh var_id via merged_pool.next_var_id and record the mapping.

  3. Rebuild the imported type tree with the remapped var_ids via full re-intern (NOT Item.data rewrite-in-place — the pool is append-only per types.md §TY-6; rewriting payloads in place would corrupt the intern map and the hashes column).

  4. Rewrite FunctionSig.scheme_var_ids (compiler/ori_types/src/output/mod.rs:423-428, consumed by llvm_backend.rs:321-327 to build var_subst) from the same remap map. re_intern_sig at pool/re_intern/mod.rs:83-97 currently clones these unchanged — that is the hidden coherence bug that would silently regress §08.3 if missed.

  5. Rewrite the Tag::Scheme binder list in extra (stored as raw var_ids at pool/construct/mod.rs:161-174, preserved unchanged at pool/re_intern/mod.rs:185-193) from the same remap map.

  6. For each remapped var_id, REBUILD a destination-local VarState from the source’s variant — do NOT blank-init to Unbound, and do NOT perform a whole-enum literal byte-level clone (a literal clone preserves pool-local identities inside variant payloads — id on Unbound/Generalized, target on Link — which defeats the remap). The shipped enum layout (compiler/ori_types/src/pool/mod.rs:80-109) is: Unbound { id: u32, rank: Rank, name: Option<Name> }, Link { target: Idx }, Rigid { name: Name }, Generalized { id: u32, name: Option<Name> }. Variant-by-variant rebuild rule, per shipped fields:

    • Unbound { id, rank, name }: id MUST be the newly-allocated dst_var_id (pool-local identity — NOT source.id); rank and name are pool-independent and clone verbatim. Copying source.id literally reintroduces exactly the var_id collision this fix eliminates.
    • Generalized { id, name }: id MUST be the newly-allocated dst_var_id (pool-local identity — NOT source.id); name clones verbatim. Same reasoning as Unbound. Note: Generalized has NO rank field in the shipped enum.
    • Rigid { name }: name is a global Name intern (pool-independent); literal clone is correct. No id field exists on Rigid; no rank field either.
    • Link { target }: target: Idx is source-pool-local; rebuild as VarState::Link { target: re_intern_type(source, source.target, target_pool, cache, var_remap) } — recursively re-intern the Link target on demand. Do NOT rely on cache.get(&source.target) with an expect(...) assertion; the cache is only populated for types already visited by the traversal, and a Tag::Var with VarState::Link(T) where T is reachable ONLY via this Link (not transitively via any other branch of the tree being re-interned) will panic the expect. Recursive re_intern_type handles cache hits and on-demand construction uniformly, matching the recursion pattern used for child types in every other re_intern_by_tag arm (pool/re_intern/mod.rs:120-122, :127-128, :133-134, etc.). No extra cycle guard is required beyond the shipped unifier invariant: unify_var_with installs VarState::Link only after the occurs check (compiler/ori_types/src/unify/mod.rs:271-291), and resolve / resolve_readonly assume acyclic link chains (:125-170), so the source pool presented to re-intern is expected to contain finite Link chains rather than arbitrary cycles.

    unify/substitute.rs:78-83 and pool/substitute/mod.rs:82-88 branch on VarState::Generalized; wiping it to Unbound distorts generic behavior. Preserving the variant with a variant-aware rebuild — and critically remapping id on Unbound/Generalized and target on Link (via recursive re-intern, not cache lookup) — fixes the aliasing without introducing a new source-pool-reference leak or a latent traversal-order panic.

  7. The re_intern_type hash fast path at pool/re_intern/mod.rs:56-60 is INVALID for var-bearing subtrees once var_ids are remapped (leaf Tag::Var hashes include the raw var_id per pool/mod.rs:395-413). A remap-aware path is required for var-bearing types; the fast path may only be used when the source type has no var-bearing descendants (TypeFlags::HAS_VAR + HAS_BOUND_VAR + HAS_RIGID_VAR all clear).

  8. Out-of-scope (explicitly not part of §08.3’s blast radius): the resolutions map at pool/accessors.rs:358-359 is keyed by Idx, NOT by var_id. DO NOT list it as a touch point; reviewers chasing phantom work there is a known failure mode flagged in codex’s Step 4 blind-spots.

Decision gate tasks:

  • Confirm the remap-aware re-intern shape against types.md §TY-6 append-only invariant: re-read types.md §TY-6 and §TF-3 Merkle-hash propagation; confirm that fresh-var-id allocation + full re-intern (not Item.data rewrite-in-place) is the only pool-invariant-preserving fix shape. Record the confirmation as a comment on the §08.3 implementation checkbox. Confirmed (2026-04-19): types.md §TY-6 mandates pool append-only during a checking session with pool/re_intern/ as the documented cross-pool migration exception that constructs fresh Idx in the destination and never mutates the source; Item.data rewrite-in-place would corrupt intern_map (§TY-2), the hashes column, and flags. §TF-3 PROPAGATE_MASK plus the var-id-bearing leaf hash (pool/mod.rs:395-413) makes the fast-path skip at pool/re_intern/mod.rs:56-60 invalid for var-bearing subtrees once leaves are remapped — §08.1.5 step 7 captures the unconditional Tag::Scheme skip needed for the binder-only-var case (§08.2 cell e5). Recorded as the HTML pool-invariant confirmation comment on §08.3 step 1.
  • Confirm production codegen is unaffected by re-reading compiler/ori_llvm/src/codegen/function_compiler/define_phase.rs:115-165 and nounwind/prepare.rs:95-149 — both operate on a single pool and never invoke re_intern_*. Document that the fix site (test-runner pool merge) is the ONLY current caller; lifting the remap abstraction into pool/re_intern/ is defensive, not reactive. Confirmed (2026-04-19): define_phase.rs:115-165 (emit_arc_function) uses self.pool throughout, calls super::lambda_mono::resolve_all_lambda_bound_vars, compile_lambda_arc, purity_analysis::remap_partial_apply_names, and process_arc_function — no re_intern_* calls anywhere on the production path. nounwind/prepare.rs:95-149 (prepare_mono_cached) likewise uses self.pool and dispatches to lower_function_can with mono_fn.body_type_map for type substitution — no re_intern_* calls. The single current consumer of pool/re_intern/ remains compiler/oric/src/test/runner/llvm_backend.rs:167-251 (the three call sites enumerated in §08.3’s primary surface). Lifting the remap-aware abstraction into pool/re_intern/ is defensive: it makes the abstraction available to any future cross-pool-merge call site (production codegen, WASM ori_compiler facade, other test harnesses) without those consumers needing to re-derive the remap policy.
  • Audit the T17–T19 regression pins added 2026-04-18 to compiler/ori_types/src/check/validators/tests.rs: they pin typeck-boundary behavior that is correct and unchanged by §08.3. They STAY as legitimate typeck clamps. Add a doc comment on each test noting // Not the enforcement point for BUG-04-042 — see plans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.md §08.1.R so future readers don’t mistake them for the fix. Done (2026-04-19): Pointer doc-comments added to all three tests at compiler/ori_types/src/check/validators/tests.rs lines 580, 612, 657 — polylambda_return_type_with_boundvar_emits_no_diagnostic (T17), polylambda_return_type_with_generalized_var_emits_no_diagnostic (T18), and polylambda_return_type_with_unbound_var_emits_one_e2005 (T19). Each carries /// Not the enforcement point for BUG-04-042 — see plans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.md §08.1.R. as the final doc-comment line before the #[test] attribute. The three tests remain in place as legitimate typeck-boundary clamps verifying that the VarState::Generalized exemption (typeck.md §SC-1 shipped divergence) holds — they are NOT the §08.3 fix surface.
  • Confirm memory pointer handling: /home/eric/.claude/projects/-home-eric-projects-ori-lang/memory/project_bug_04_042_pool_merge_diagnosis.md MUST be reduced to a one-line pointer to §08.1.R after §08.3 lands (per CLAUDE.md §Plan Corrections Go IN the Plan — plan is authoritative, memory is breadcrumb). Confirmed (2026-04-19): The breadcrumb memory file does NOT exist on disk (ls returns MISSING). The post-§08.3 reduction protocol therefore has no shrink target — if the file is created during §08.3 implementation work, it MUST be reduced to a one-line pointer to this section before §08.N close-out. If §08.3 lands without ever creating it, this checkbox is automatically satisfied. The plan file (this section) remains the authoritative record per CLAUDE.md §Plan Corrections Go IN the Plan; no memory-file shrink action is required at §08.1.5 close.

Decision gate: §08.1.5 MUST close before §08.3 starts. The §08.3 fix shape depends on the decision recorded here.

Tooling retrospective (2026-04-19, per /improve-tooling per-subsection workflow): No gaps. §08.1.5 was a decision-gate subsection (plan-text + 3 doc-comments + 1 HTML invariant note); no diagnostic scripts ran, no compiler/test invocations. Three candidates surfaced + filtered: (1) auto-cross-reference plan↔test name → line number REJECTED (plan-text convention by design; line numbers drift); (2) memory-file existence checker REJECTED (out of scope for per-subsection retrospective); (3) state.sh commits_behind field REJECTED (informational only; existing fresh/stale/obsolete labels sufficient). No new tools created; no awareness gaps; no doc-sync gaps. Documentation Awareness Checks 1-3 all clean.

08.2 TDD matrix: poly-lambda + imported generics + Scheme PROPAGATE_MASK pin

Goal: Write failing tests BEFORE implementing the fix.

  • Spec test (TDD): tests/spec/expressions/poly_lambda_with_imported_generic.ori — created 2026-04-18. Defines polymorphic identity lambdas paired with imported assert_eq<T> calls across four type instantiations (int/str/bool/float) plus lambda-flavor (a)/(b)/(c), locally-defined generic counterpart (import dim b), Tag::Scheme PROPAGATE_MASK regression pin (nested [T] lambda body), and nested-container parameter pin (xs -> xs[0] with [T] param). 10 attached tests total. Verified TDD signal: interpreter 10/10 pass, LLVM backend 10/10 compile-fail with unresolved type variable at codegen — type inference bug idx=Idx(238) + Idx(241) — matching §08.1’s documented Idx(241) symptom exactly.
  • Rust unit test in ori_llvm: satisfied by the AOT integration test below (compiler/ori_llvm/tests/aot/poly_lambda_mono.rs), which IS a Rust test in the ori_llvm crate that exercises LLVM codegen end-to-end via the AOT pipeline. A separate lower-level test that constructs a Pool manually and calls TypeInfoStore::get() directly would duplicate the coverage with less realistic fixture data; the plan’s intent (reproduce the bug via LLVM codegen with poly-lambda + imported-mono interaction) is met by the AOT test file.
  • AOT integration test in compiler/ori_llvm/tests/aot/ (retained as a failing TDD pin for a SIBLING bug — see next note): added compiler/ori_llvm/tests/aot/poly_lambda_mono.rs with two assert_aot_success-based tests (test_poly_lambda_with_imported_assert_eq_int and ..._str) driving ori build → linked binary → runtime exit-0 on fixtures fixtures/poly_lambda_mono/poly_lambda_with_imported_assert_eq_{int,str}.ori. Registered in compiler/ori_llvm/tests/aot/main.rs. Verified TDD signal (2026-04-19, Round 5 resolution of TPR-08-R4-01): both tests FAIL today with E5001 unresolved function 'assert_eq' in apply/invoke — missing mono instance? — this is NOT the Idx(241) unresolved-type-variable symptom §08.1.R diagnoses. Direct code inspection (Round 5) confirms ori build goes through compiler/oric/src/commands/compile_common.rs:70-110,184-240 + compiler/ori_types/src/check/mod.rs:341-407 with NO re_intern_* call — the cross-module pool-merge is a JIT-test-runner-only code path. The AOT failure is a DIFFERENT root cause (AOT lacks imported generic support entirely — collect_mono_functions does not traverse import_sigs for mono-instance emission). §08 scope is therefore JIT-only; the AOT tests stay in-tree as failing TDD pins so they green when the sibling AOT-imported-generic bug lands (see §08.R TPR-08-R4-01 closure note + new bug-tracker entry). Test-suite protection (grep-verified 2026-04-20 per editor review): both test_poly_lambda_with_imported_assert_eq_int and test_poly_lambda_with_imported_assert_eq_str carry #[ignore = "BUG-04-AOT-MONO"] attributes (compiler/ori_llvm/tests/aot/poly_lambda_mono.rs:46,59), so cargo test -p ori_llvm --test aot and test-all.sh’s AOT suite do NOT count them as failing — the failing-TDD-pin invariant is preserved without a red gate. The #[ignore] markers retire atomically when BUG-04-AOT-MONO lands.
  • Matrix cells (10 total — coverage decisions documented below):
    • Type dimension: int, str, bool, float — four mono instantiations of assert_eq<T> in the same file ✓
    • Lambda dimension:
      • (a) poly-lambda defined but unused ✓ (poly_unused_int, poly_unused_str)
      • (b) poly-lambda defined and called monomorphically ✓ (poly_mono_{int,str,bool,float})
      • (c) poly-lambda defined and called with different types at different sites ✓ (poly_multi_type_same_fn)
      • (d) DEFERRED to §08.5 broadened parity audit — the .map(transform: s -> s) iterator-callback cell was found at §08.2 authoring time (2026-04-18) to be blocked on the interpreter by BUG-04-030 interference (tests/spec/patterns/data.ori already fails the same way today). Running cell (d) inside §08.2 would make the test fail for the wrong reason (BUG-04-030, not §08). §08.5 is the correct home — it runs dual-exec-verify on tests/spec/expressions/lambda_mono.ori and tests/spec/traits/iterator/ AFTER §08.3 closes the producer-side leak, which is when cell (d)‘s intent (verify is_polymorphic_lambda’s contains_bound_var gate continues to route generalized-return callbacks around the mono pipeline) becomes testable. Concrete anchor: §08.5 checklist’s “Dual-execution parity audit on poly-lambda paths beyond §08.2” item.
    • Import dimension: (a) std.testing.assert_eq ✓ + (b) locally-defined generic that mimics the same shape ✓ (check_eq_local<T: Eq + Debug>)
    • Tag::Scheme PROPAGATE_MASK regression pin ✓ (propagate_mask_nested_listlet $wrap = x -> [x, x, x] exercises Tag::Scheme HAS_VAR propagation through [T] lambda body)
    • prepare_mono_cached cache-miss fallback negative pin: covered implicitly — every test runs from a fresh compilation context, so every assert_eq<T> mono call lowers through the cache-miss path at nounwind/prepare.rs:119-139. The plan’s original framing required a scenario “where the imported metadata is unavailable” via metadata stripping at llvm_backend.rs:448-450; that framing applied when the cache-miss path was an optimization fallback. Current behavior is that cache-miss IS the primary path on first mono emission — no special scenario needed. If §08.3 introduces a persistent cache, this item escalates to an explicit scenario.
    • Nested-container substitution pin ✓ (nested_container_paramlet $first_of = xs -> xs[0] with xs: [T] nested generic parameter)
  • [~] Negative pin: DEFERRED to §08.3 close-out — after §08.3’s remap-aware re-intern lands and tests go green, use the scoped-patch reversal workflow (per CLAUDE.md §NEVER Use Destructive Git Commands, avoiding worktree-wide git stash that would touch parallel-session work): (1) git diff compiler/ori_types/src/pool/re_intern/ compiler/oric/src/test/runner/llvm_backend.rs > /tmp/section-08-3.patch, (2) git apply -R /tmp/section-08-3.patch, (3) confirm the §08.2 tests fail with the §08.1.R collision symptoms, (4) git apply /tmp/section-08-3.patch to restore, (5) confirm they pass again. This is an EXECUTION-TIME verification that cannot run until §08.3 lands. Concrete anchor: §08.3 checklist item “Run §08.2 negative pin” is the single canonical execution entry for this verification; this [~] marker tracks the §08.2-owned contract, the [ ] in §08.3 is the actual execution checkbox.
  • Verify all tests fail before starting §08.3 implementation — verified 2026-04-18. Spec test: interpreter 10/10 pass + LLVM backend 10/10 compile-fail with Idx(238)/Idx(241) unresolved. AOT tests: 2/2 FAIL with E5001 missing mono instance. TDD discipline per tests.md §TDD for Bugs satisfied: failing tests are the exact shape §08.3 must make pass.

08.2 Matrix extension (2026-04-19, codex Step 4 blind-spots)

Under the corrected §08.1.R diagnosis (cross-module pool-merge var_id collision), five additional matrix cells pin the remap-aware re-intern semantics at the pool-crate boundary, where the fix actually lives. Each cell carries positive + negative pins per tests.md §Matrix Clamping. These cells extend (do NOT replace) the 10 cells above, which remain valid integration coverage for the end-to-end poly-lambda × imported-generic interaction.

  • (e1) Leaf var remap across pools — covers all three variable tags (Tag::Var, Tag::BoundVar, Tag::RigidVar) — add test cells to compiler/ori_types/src/pool/re_intern/tests.rs for EACH tag, re-interning a standalone Tag::<variant>(id=N) from a source pool into a target pool whose var_states[N] is a different pool’s Generalized slot. §08.1.5 step 2 explicitly remaps all three tags; the matrix MUST pin all three — pinning only Tag::Var leaves the other two unprotected against regression. Landed 2026-04-19: 3 negative pins GREEN today (legacy_re_intern_{var,bound_var,rigid_var}_leaf_reads_target_generalized_slot_on_id_collision) + 3 positive pins #[cfg(any())]’d out for §08.3 (remap_aware_re_intern_{var,bound_var,rigid_var}_leaf_*).
    • Positive pin (per tag): re_intern_type_with_var_remap allocates a fresh dst_id via target.next_var_id AND var_remap.get(&N) == Some(dst_id) AND target.var_states[dst_id] is a variant-aware rebuild of source.var_states[N] (per §08.1.5 step 6 — rank/name preserved for Unbound/Rigid/Generalized; Link target Idx remapped through the type cache).
    • Negative pin (per tag): the legacy re_intern_type path (without var_remap) reproduces the collision — asserts that target now contains a Tag::<variant>(N) whose var_states slot was cloned from the target pool (demonstrating why the remap-aware variant is load-bearing for every variable-carrying tag).
  • (e2) Scheme binder remap together with body leaves — add test cells for Tag::Scheme([7, 9], Tag::Function([Tag::Var(7), Tag::Var(9)], Tag::Var(7))) re-interned across pools. Landed 2026-04-19: negative pin legacy_re_intern_scheme_preserves_source_binder_ids_unchanged GREEN + positive pin remap_aware_re_intern_scheme_remaps_binders_and_body_leaves_coherently #[cfg(any())]’d out for §08.3.
    • Positive pin: the re-interned scheme’s binder list matches the remapped leaves (scheme.binders == [remap[7], remap[9]] AND every Tag::Var leaf in the body uses the same mapped ids).
    • Negative pin: a variant that re-interns leaves but clones binders unchanged; asserts the resulting scheme is internally inconsistent (binder list references a var_id absent from the body) — proves pool/re_intern/mod.rs:185-193’s current source.scheme_vars(idx).to_vec() pattern IS the bug when the enclosing pool merges var-id spaces.
  • (e3) FunctionSig.scheme_var_ids coherence with remapped type tree — add test cells in pool/re_intern/tests.rs (or a new sig_remap_tests.rs sibling if tests.rs grows past the §BLOAT threshold) exercising re_intern_sig on a sig whose scheme_var_ids = [7] and whose param_types / return_type reference Tag::Var(7). Landed 2026-04-19: negative pin legacy_re_intern_sig_preserves_source_scheme_var_ids_unchanged GREEN + positive pin remap_aware_re_intern_sig_remaps_scheme_var_ids_coherently_with_leaves #[cfg(any())]’d out for §08.3. Tests co-located in pool/re_intern/tests.rs (now ~870 lines; authored-.md 500-line limit does not apply to test files per impl-hygiene.md §File Organization).
    • Positive pin: after re_intern_sig, sig.scheme_var_ids == [remap[7]] AND every leaf Tag::Var in param_types / return_type uses remap[7]; a test var_subst = HashMap::from([(sig.scheme_var_ids[0], concrete)]) + substitute_in_pool(target, leaf, &var_subst) resolves correctly.
    • Negative pin: run re_intern_sig in its pre-fix form (cloning scheme_var_ids unchanged); assert that var_subst built from the cloned ids does NOT substitute any leaves (because leaf and sig ids drifted) — proves the hidden coherence bug is exercised.
  • (e4) VarState variant-aware remap semantic preservation — add test cells verifying the VarState rebuild path for each shipped variant (compiler/ori_types/src/pool/mod.rs:80-109): Unbound { id, rank, name }, Generalized { id, name }, Rigid { name }, Link { target }. Covers the critical id-remap rule for Unbound/Generalized (pool-local identity) and the target-remap rule for Link (source-pool-local Idx). Landed 2026-04-19: 4 positive pins (remap_aware_re_intern_rebuilds_{unbound,generalized,rigid,link}_*) #[cfg(any())]’d out for §08.3. Negative-pin rationale for (i) literal-byte clone leaking source.id and (ii) blank-init flipping the substitute_in_pool branch documented inline in each positive-pin body — re_intern_type_with_var_remap is a §08.3 stub, so both negative variants are §08.3-side alternatives, not behaviors of any current code path.
    • Positive pin (per variant): after re_intern_type_with_var_remap, target.var_states[dst_id] equals the source variant with pool-local identities remapped — Unbound.id and Generalized.id hold dst_id (NOT source’s id); Link.target holds the result of recursive re_intern_type(source, source.target, target_pool, cache, var_remap) (NOT source.target, and NOT cache.get(&source.target).expect(...) — see §08.3 step 6 per the Round 2 F1 fix at aeceb167 which replaced the cache-lookup assumption with recursive re-intern to cover isolated-Link targets); Rigid.name, Generalized.name, Unbound.name, and Unbound.rank preserve source values verbatim.
    • Negative pin (per variant): (i) a whole-enum literal clone of Unbound { id: 7 } leaves target.var_states[dst_id].id == 7 — source-pool identity leaks through; assert this and prove the collision reappears at substitute_in_pool’s VarState::Generalized branch when a subsequent Generalized slot holds the colliding id. (ii) a variant that blanks the destination to VarState::Unbound makes substitute_in_pool(target, leaf, &var_subst) take the Unbound branch at pool/substitute/mod.rs:82-88 (different from the Generalized branch at unify/substitute.rs:78-83), distorting dispatch — proves why variant-aware rebuild is load-bearing rather than cosmetic.
  • (e5) Scheme with var-bearing binders AND var-free body — PROPAGATE_MASK leaves the parent’s var-bearing flags clear — add test cells for Tag::Scheme([7], body: Tag::Int) re-interned across pools. Per types.md §TF-3, PROPAGATE_MASK propagates flags from body children only; a scheme’s raw binder list in extra is NOT a PROPAGATE_MASK source. So source.flags(scheme).intersects(HAS_VAR | HAS_BOUND_VAR | HAS_RIGID_VAR) == false even though binder var_id=7 is pool-local and MUST be remapped. Pins the interaction between step 5 (binder walk in extra) and step 7 (unconditional Tag::Scheme fast-path skip) — distinct from e2, which pins schemes with var-bearing bodies (HAS_VAR set on parent by propagation from body leaves). Landed 2026-04-19: (i) negative pin scheme_with_var_bearing_binders_and_var_free_body_has_no_propagated_var_flags GREEN — TF-3 invariant itself; (ii) negative pin legacy_re_intern_scheme_with_var_free_body_preserves_source_binder_id GREEN — legacy-path leak; positive pin remap_aware_re_intern_scheme_with_var_free_body_remaps_binder_and_changes_hash #[cfg(any())]’d out for §08.3.
    • Positive pin: after re_intern_type_with_var_remap, the re-interned scheme’s binder list is [remap[7]] (NOT [7]) AND the scheme’s hash in target differs from its hash in source (scheme hashing is extra-backed per types.md §TI-3, so a remapped binder list must change the hash even when the body re-intern is a no-op).
    • Negative pin: (i) a variant that gates the fast-path skip at pool/re_intern/mod.rs:56-60 on HAS_VAR | HAS_BOUND_VAR | HAS_RIGID_VAR alone (instead of step 7’s unconditional Tag::Scheme skip) hash-hits through this scheme because the parent flags are all clear, returning the source Idx directly into the target pool; assert that the resulting binder list is [7] (source id leaked) and a subsequent target.var_state(7) query points at a slot unrelated to the source scheme’s intended binding. (ii) a variant that skips step 5’s binder walk but still takes the unconditional Tag::Scheme fast-path skip re-interns the body (a no-op for Tag::Int) but produces a scheme with scheme_vars(result) == [7] — proves step 5’s binder remap is load-bearing independent of step 7’s fast-path guard, even for schemes whose body re-intern is trivial.

These cells live at the pool crate’s re-intern boundary (where the fix actually lives), NOT at the validator boundary (where the 2026-04-18 T17–T19 pins live). The T17–T19 typeck-boundary pins stay as legitimate validator-behavior clamps; they are NOT the enforcement point for BUG-04-042 (per the §08.1.5 audit item that adds pointer comments to them).

08.2 HISTORY

  • 2026-04-18 (original) 10 integration matrix cells authored covering type × lambda × import × Tag::Scheme PROPAGATE_MASK × cache-miss × nested-container dimensions. Spec tests 10/10 failing on LLVM backend; AOT tests 2/2 failing with E5001. TDD discipline confirmed.
  • 2026-04-19 (matrix extension) Four additional cells (e1–e4) added to pin the remap-aware re-intern semantics at pool/re_intern/ under the corrected §08.1.R diagnosis. Existing 10 cells remain valid — they pin end-to-end behavior; e1–e4 pin the implementation-boundary semantics that make the end-to-end behavior reachable. Driven by codex Step 4 blind-spots advice (blind-spots.json blind_spots + cross_cutting items): pool/re_intern/tests.rs:350-360 previously pinned only scheme hash parity, NOT var-id remap semantics or scheme_var_ids coherence.
  • 2026-04-19 (Round 3 — TF-3 edge case) Fifth cell (e5) added to pin the Scheme-with-var-bearing-binders AND var-free-body edge case that types.md §TF-3 PROPAGATE_MASK does not flag on the parent scheme. Distinct from e2 (which pins schemes whose body carries vars, so HAS_VAR propagates from body children and is set on the parent). Driven by gemini Round 3 F1 — identified the gap between step 5 (binder walk in extra) and step 7 (unconditional Tag::Scheme fast-path skip): without e5, a regression to a HAS_VAR-gated fast-path guard on Tag::Scheme would silently hash-hit through schemes like Scheme([7], Tag::Int) and drop the remap.
  • 2026-04-19 (implementation landed) Cells e1–e5 authored in compiler/ori_types/src/pool/re_intern/tests.rs. 17 tests added: 7 negative pins GREEN today on the legacy re_intern_type / re_intern_sig path (collision symptom pinned forever as regression guards) + 10 positive pins #[cfg(any())]-gated (compiled-out entirely) for §08.3 to un-gate. cargo test -p ori_types --lib pool::re_intern reports 25 passed; 0 failed; 0 ignored. §08.3 simultaneously (a) adds re_intern_type_with_var_remap / re_intern_sig_with_var_remap in pool/re_intern/mod.rs AND (b) removes each #[cfg(any())] guard — the 10 positive pins go green together with the implementation. TDD RED state for §08.3 = the 10 gated-out pins that fail-to-compile without §08.3’s implementation. Design note (hook-forced re-grounding): the first commit attempt introduced todo!() stubs so positive pins could compile + #[ignore] with §08.3 pointers. The pre-commit hook rejected on clippy::todo — project-wide denied per impl-hygiene.md §Lint Discipline with no existing #[expect(clippy::todo)] precedent. Re-grounded on #[cfg(any())] gating per impl-hygiene.md §Conditional Compilation: preserves the cell-authored TDD scaffolding without introducing denied-lint exposure, and self-retires when §08.3 un-gates (the removed cfg line + the new function body land atomically).

Tooling retrospective (2026-04-18, still valid): no gaps. §08.2 was pure test-authoring backed by existing infrastructure — cargo st / ori test --backend=llvm for spec TDD signal verification, assert_aot_success in compiler/ori_llvm/tests/aot/util/compile.rs for AOT integration tests, cargo test -p ori_types validators for typeck-side T17–T19 regression pins. The 2026-04-19 matrix extension similarly leans on existing infrastructure — cargo test -p ori_types pool::re_intern is the execution entry for e1–e4 — so no new tool is required. The static-classification workflow that originally pinned Hypothesis (d) without runtime ORI_LOG / ORI_DUMP_AFTER_TYPECK traces was constrained by denied runtime access; the 2026-04-19 re-diagnosis via /tp-help dual-source review and codex blind-spots confirms that static evidence (pool/re_intern source + llvm_backend merge site) was sufficient to identify the real root cause without runtime tooling. No new diagnostic scripts or test helpers created.

Tooling retrospective (2026-04-19, cell-authoring close): one gap identified + documented. Authoring e1–e5 required five tools: (1) scripts/intel-query.sh --human file-symbols "ori_types/src/pool/re_intern" --repo ori — inventoried the 5-symbol public surface in one shot; (2) cargo test -p ori_types --lib pool::re_intern — filtered-path test run, 3.28s compile + 0.00s test, caught one borrow-error (resolved one iteration); (3) Grep for Pool::intern / VarState visibility — adequate at this scale; (4) Read on pool/mod.rs for shipped VarState variant shapes — no gap; (5) lefthook clippy::todo rejection on first commit — functioned correctly as the sanctioned enforcement point. Gap identified: no project-wide lint-policy doc explains that todo!() is project-wide denied AND that no #[expect(clippy::todo)] precedent exists, which left the first commit attempt uninformed of the constraint. The correct doc surface is either impl-hygiene.md §Lint Discipline (already lists todo as denied but does not warn against todo!() as a TDD-scaffolding idiom) or a new impl-hygiene.md §TDD Scaffolding subsection that codifies “#[cfg(any())] gates positive-pin tests whose callees are pending from a sibling subsection” as the canonical pattern. Remediation: file /add-bug subsystem=docs severity=low titled “impl-hygiene.md: document #[cfg(any())] as canonical TDD-scaffolding gate for cross-subsection positive pins (clippy::todo denied, no #[expect] precedent)”. Four retrospective candidates surfaced + filtered: (a) scripts/plan-cell-flip.py — REJECTED (5 Edit calls sufficed); (b) test-harness directive asserting #[ignore] reason strings — REJECTED and moot (no #[ignore]s remain); (c) cargo-expand on stubs — REJECTED (no stubs remain); (d) scripts/check-cfg-any.py to ensure every #[cfg(any())]-gated test body points at a concrete subsection anchor — REJECTED (10 gates all point at §08.3 in sibling comment; adding tooling for this scale would be over-engineering). Documentation Awareness Checks 1–3: clean for this session’s work; check 1 identified the lint-policy doc gap (above) as a pre-existing surface with no existing entry. No new tools created.

08.3 Implementation: remap-aware re-intern for cross-module pool merge

Goal: Fix the cross-module pool-merge var_id collision identified in §08.1.R by lifting remap logic into compiler/ori_types/src/pool/re_intern/ as a reusable abstraction. The actual cross-pool re_intern_* call sites in compiler/oric/src/test/runner/llvm_backend.rs are THREE: (a) canon arena type re-intern at :167-170 (remap_types(|type_id| re_intern_type(source_pool, source_idx, &mut merged_pool, cache))), (b) imported concrete-sig re-intern at :211-212 (re_intern_sig on non-generic imported sigs), (c) imported generic-sig re-intern at :249-251 (re_intern_sig on generic sigs for mono instantiation). All three become consumers of the new remap-aware API and share a per-import-module var_remap map. Lines :320-360 are the DOWNSTREAM substitute_in_pool site that reads scheme_var_ids from the re-interned generic_sig and builds var_subst — NOT a re-intern call site. The TDD matrix in §08.2 pins the correct behavior; the fix must make those tests pass without breaking any existing test.

Primary surface: compiler/ori_types/src/pool/re_intern/ (owner of the remap abstraction) + three consumer sites in compiler/oric/src/test/runner/llvm_backend.rs: :167-170 (canon arena re-intern), :211-212 (concrete sig re-intern), :249-251 (generic sig re-intern). The var_remap map is built per-import-module (one entry per per_module_caches slot) so all three consumers for a given import share the same var_id mapping; the substitute_in_pool site at :320-360 then reads already-remapped scheme_var_ids from the re-interned generic_sig. Do NOT scatter the remap logic into codegen or ARC passes; the pool crate owns type structure, so the re-intern module is where remap policy belongs.

Test pre-authoring note (2026-04-19 — §08.2 close): cells (e1–e5) of §08.2 already authored the full TDD matrix for this subsection in compiler/ori_types/src/pool/re_intern/tests.rs: 7 negative pins GREEN today on the legacy re_intern_type / re_intern_sig path + 10 positive pins #[cfg(any())]-gated out. The positive pins cover exactly the per-variant rebuild rules in step 6 (a)–(f) below AND the failure-mode pins (a)–(e) under “Failure modes to guard against”. §08.3’s FIRST action is to un-gate the 10 #[cfg(any())] guards (sibling comment in tests.rs names §08.3 as the un-gater) as the remap-aware functions land in mod.rs — do NOT re-author those tests. The “add unit tests” phrasing in step 6 + failure-mode bullets below is subsumed by §08.2’s pre-authoring. cargo test -p ori_types --lib pool::re_intern currently reports 25 passed; 0 failed; 0 ignored; after §08.3 lands + un-gates, the target is 35 passed; 0 failed; 0 ignored.

Implementation checklist (7 steps, each mapping 1:1 to §08.1.5’s remap-aware re-intern shape):

  • 1. Design re_intern_with_remap API in pool/re_intern/mod.rs: introduce a new public entry point (e.g., pub fn re_intern_type_with_var_remap(source: &Pool, idx: Idx, target: &mut Pool, cache: &mut FxHashMap<Idx, Idx>, var_remap: &mut FxHashMap<u32, u32>) -> Idx) that takes a src_var_id → dst_var_id map alongside the existing type cache. The existing re_intern_type stays for call sites that do NOT cross pool boundaries with variable-carrying types (structurally var-free imports); it delegates to the new entry point with an empty var_remap for backward compatibility. Document the distinction: the var-remap variant is mandatory for cross-pool-merge contexts where the target pool’s var_states was cloned from a different source than the imported types. Keep the abstraction in the pool crate — do NOT export var_id surgery to consumers.

  • 2. Build src_var_id → dst_var_id during re-intern: for every imported Tag::Var, Tag::BoundVar, Tag::RigidVar, and Tag::Scheme binder encountered during re-intern, allocate a fresh var_id via target.next_var_id (extending the existing var_states vector) and record var_remap.insert(src_var_id, dst_var_id). Replace the current Tag::Var | Tag::BoundVar | Tag::RigidVar => target.intern(tag, source.data(idx)) arm at pool/re_intern/mod.rs:192-193 with a remap-aware arm that reads var_remap.entry(src_var_id).or_insert_with(|| target.next_var_id()) before the intern.

  • 3. Rebuild the imported type tree via full re-intern (append-only per types.md §TY-6): the existing re_intern_type traversal already appends new entries to target rather than rewriting in place — preserve this. Do NOT introduce any code path that mutates target.items[i].data after interning; append-only is load-bearing for intern_map coherence and for the parallel hashes column (types.md §TY-2).

  • 4. Rewrite FunctionSig.scheme_var_ids in re_intern_sig: extend re_intern_sig at pool/re_intern/mod.rs:83-97 to take the shared var_remap map and rewrite every entry of result.scheme_var_ids through it (panic via expect if a scheme_var_id is not in the remap — that’s a soundness violation, not a recoverable case). This matches the var_subst build loop at llvm_backend.rs:321-327 so every generic_sig.scheme_var_ids[i] still resolves to the intended instance.generic_args[i] after remap.

  • 5. Rewrite the Tag::Scheme binder list in extra: modify the Tag::Scheme arm at pool/re_intern/mod.rs:185-193 — instead of let vars = source.scheme_vars(idx).to_vec(); followed by target.scheme(&vars, body), walk each src_var_id through var_remap to produce the destination binder list, then call target.scheme(&remapped_vars, body). The binders MUST be allocated BEFORE the body is re-interned (so the body’s leaf Tag::BoundVar / Tag::Var references to these binders can find them in the remap during the recursive descent).

  • 6. Rebuild destination-local VarState from source variant with variant-aware remapping (do NOT whole-enum literal-clone, do NOT blank-init to Unbound): after allocating the new dst_var_id via target.next_var_id, construct target.var_states[dst_var_id] variant-by-variant from source.var_states[src_var_id]. The shipped enum layout (compiler/ori_types/src/pool/mod.rs:80-109) is: Unbound { id: u32, rank: Rank, name: Option<Name> }, Link { target: Idx }, Rigid { name: Name }, Generalized { id: u32, name: Option<Name> }. Per-variant rule:

    • Unbound { id, rank, name }Unbound { id: dst_var_id, rank: source.rank, name: source.name.clone() }. id MUST be dst_var_id (NOT source.id) — the whole point of the remap is to give the destination a fresh pool-local id. Copying source.id literally reintroduces the collision.
    • Generalized { id, name }Generalized { id: dst_var_id, name: source.name.clone() }. Same reasoning as Unbound: id MUST be the new dst_var_id. Note: Generalized has NO rank field in the shipped enum.
    • Rigid { name }Rigid { name: source.name } (literal clone; Name is a global intern, pool-independent). No id or rank field exists on Rigid.
    • Link { target }Link { target: re_intern_type(source, source.target, target_pool, cache, var_remap) }. Recursively re-intern the Link target on demand — do NOT use cache.get(&source.target).expect(...). The cache is populated only for types already visited in the traversal, and a Tag::Var with VarState::Link(T) where T is reachable ONLY via this Link will panic the assertion. Recursive re_intern_type handles cache hits AND on-demand construction uniformly, matching the pattern used by every other re_intern_by_tag arm (pool/re_intern/mod.rs:120-122 for List children, :127-128 for Map key, :133-134 for Result ok, etc.).

    A whole-enum literal clone leaks source-pool identities via Unbound.id / Generalized.id / Link.target and reintroduces the collision the remap is meant to eliminate; blanking to Unbound destroys Generalized/Rigid semantic state and distorts generic dispatch at every downstream substitution site (unify/substitute.rs:78-83, pool/substitute/mod.rs:82-88). A cache.get(&source.target).expect(...) that assumes Link targets are visited before their referencing Link panics on any isolated Link branch. Add unit tests in pool/re_intern/tests.rs exercising all four variants: (a) Unbound { id=src } source produces Unbound { id=dst, rank=src.rank, name=src.name } in target; (b) Generalized { id=src, name } source produces Generalized { id=dst, name=src.name } in target; (c) Rigid { name } source produces Rigid { name } in target (verbatim); (d) Link { target=src_idx } source produces Link { target=dst_idx } where dst_idx came from recursive re-intern (NOT cache lookup); (e) Link-target-isolated cell: Tag::Var(id=N) with VarState::Link(T) where T is NOT otherwise reachable from the re-intern root — positive pin: recursive re-intern correctly constructs T in target; negative pin: the cache-lookup variant panics the expect; (f) negative pin: a whole-enum literal clone of Unbound { id=7 } into target.var_states[42] produces target.var_states[42] = Unbound { id=7 } — demonstrating the collision the remap is meant to eliminate.

  • 7. Guard the Merkle-hash fast path against var-bearing subtrees AND Tag::Scheme binder-only cases: modify the fast path at pool/re_intern/mod.rs:56-60. Before target.lookup_by_hash(source.hash(idx)) returns a target Idx, apply TWO guards: (a) check source.flags(idx).intersects(HAS_VAR | HAS_BOUND_VAR | HAS_RIGID_VAR) — if any var-bearing flag is set, SKIP the fast path and fall through to the remap-aware recursive traversal. (b) For source.tag(idx) == Tag::Scheme, unconditionally SKIP the fast path — per types.md §TF-3 PROPAGATE_MASK propagates flags from body children only, NOT from the scheme’s raw binder list in extra. A scheme with a var-free body but var-bearing binders (e.g., Scheme([7], int)) would have no HAS_VAR/HAS_BOUND_VAR/HAS_RIGID_VAR flag yet still embeds pool-local var_ids in its hash (scheme hashing includes the extra payload per types.md §TI-3 extra-backed class). Leaf Tag::Var hashes include the raw var_id (pool/mod.rs:395-413), so a var-bearing subtree’s hash is pool-local; treating it as pool-independent is the quiet channel by which the collision reappears even after the explicit remap lands.

Failure modes to guard against (codex Step 4 blind-spots; each gets a §08.2 matrix cell):

  • (a) Merkle hash staleness — rewriting Item.data in place leaves intern_map + hashes column stale: test that re_intern_with_remap NEVER mutates an existing target entry’s payload. Positive pin: after re-intern, every target.items[i] that was already present before the call SHALL be == to its pre-call value. Negative pin: a test that deliberately mutates target.items[i].data and asserts target.lookup_by_hash(target.hash(i)) now returns None (proves the invariant is load-bearing).
  • (b) Leaf var_id in Merkle hash — hash fast path invalid for var-bearing subtrees: test re_intern_with_remap on a [Tag::Var(id=7)] list-type when target already contains a structurally-identical [Tag::Var(id=7)] from an unrelated source. Positive pin: the remap allocates a fresh dst_id (e.g., id=42) and interns a DIFFERENT target entry — the fast path MUST NOT dedup these. Negative pin: if step 7’s guard is removed, the test demonstrates the collision (pre-remap dedup) returning the wrong target entry.
  • (c) Tag::Scheme binder remap — binders in extra payload must remap together with leaves: test re-intern of Tag::Scheme([7], Tag::Function([Tag::Var(7)], Tag::Var(7))) across pools. Positive pin: after remap, the scheme’s binder list AND every leaf Tag::Var reference resolve to the SAME fresh dst_var_id. Negative pin: a test that remaps leaves but not binders (removing step 5) and asserts the resulting scheme is internally inconsistent (binder list references a var_id absent from the body).
  • (d) FunctionSig.scheme_var_ids coherence — sig side must remap with type side: test the full re_intern_sig path on a FunctionSig whose scheme_var_ids contains [7] and whose param_types / return_type reference Tag::Var(7). Positive pin: after remap, scheme_var_ids and the leaf var references resolve to the SAME fresh dst_var_id, so var_subst = HashMap::from([(scheme_var_ids[0], concrete)]) at llvm_backend.rs:321-328 correctly substitutes every leaf. Negative pin: a test that remaps leaves but clones scheme_var_ids unchanged (step 4 omitted) and asserts substitute_in_pool leaves the leaves untouched (proving the silent regression path).
  • (e) VarState variant-aware remap — cloning preserves Generalized semantics with pool-local id remapped: test re-intern of a Tag::Var pointing at VarState::Generalized { id: src_id, name } across pools. Positive pin: the destination slot carries VarState::Generalized { id: dst_id, name } where dst_id is the newly-allocated var_id from target.next_var_id (NOT src_id); name preserves source value. Negative pin (two shapes): (i) a test that whole-enum literal-clones the source leaves target.var_states[dst_id].id == src_id — proves the collision reappears. (ii) a test that blank-inits the destination to VarState::Unbound and asserts substitute_in_pool now takes the Unbound branch instead of the Generalized branch, distorting generic dispatch.

Downstream verification:

  • Run timeout 150 cargo test -p ori_types pool::re_intern — the new unit tests in pool/re_intern/tests.rs pass (including the 5 failure-mode guard pins above). Verified: 35/0/0 (target per §08.2 HISTORY).

  • Run timeout 150 cargo test -p ori_llvm — no regressions at the LLVM integration layer. Verified: 633/0/0 (lib baseline preserved).

  • Run timeout 150 cargo st — interpreter parity preserved (interpreter does NOT use the test-runner pool merge; this is a smoke test that the pool-crate changes didn’t break the common path). Verified via canonical-baseline smoke: lambda_mono.ori 17/0/0, integer_safety.ori 30/0/0, full ori_types lib 860/0/0. §06.2-scope 843 interpreter failures unchanged per state.sh (head dbc3492c).

  • [~] Run timeout 150 ./target/release/ori test --backend=llvm tests/spec/expressions/poly_lambda_with_imported_generic.ori — DEFERRED to §08.3b §08.3’s pool-merge collision fix is necessary but not sufficient. Dual-source /tp-help consensus 2026-04-19 (codex + gemini converged on option (b)): the residual Tag::Var(Generalized) leak at codegen requires the types.md §SC-1 scheme-body migration in §08.3b (Generalized→BoundVar at scheme construction in compiler/ori_types/src/unify/generalization.rs). Corrected diagnosis: codex’s round-0 note — lambda_mono.ori:87-103 already contains an unconstrained identity lambda that passes, so the differentiator is “scheme-shape survives to mono” (multi-instantiation at poly_lambda_with_imported_generic.ori:136-140; unused scheme at lines 110-112) vs “scheme collapses at one call site”, NOT “constrained vs unconstrained body”. §08.3 unit tests (35/35 in pool::re_intern), lambda_mono.ori (17/17), and integer_safety.ori (30/30) confirm the pool-merge fix is correct and complete on its narrowed scope; this test item becomes unblocked when §08.3b lands.

  • Run timeout 150 cargo test -p ori_llvm --test aot poly_lambda_mono — the §08.2 AOT integration tests STAY failing with E5001 missing mono instance (they track BUG-04-AOT-MONO in plans/bug-tracker/section-04-codegen-llvm.md, not §08.3); verify the failure mode is unchanged by §08.3’s pool-crate edits (i.e., no new failure class introduced). Verified 2026-04-19 at HEAD 9eae468d: 2 tests FAILED with E5001: LLVM module verification failed + unresolved function 'assert_eq' in invoke/apply — missing mono instance? on test_poly_lambda_with_imported_assert_eq_{int,str}. Failure signature is the BUG-04-AOT-MONO signature verbatim; no new failure class introduced.

  • Run §08.2 negative pin (anchor for §08.2 deferral): after §08.3 tests are green, use the scoped-patch reversal workflow (NOT git stash, which touches the full working tree including parallel-session work — banned per CLAUDE.md §NEVER Use Destructive Git Commands + §NEVER Investigate “Pre-Existing?”): (1) git diff compiler/ori_types/src/pool/re_intern/ compiler/oric/src/test/runner/llvm_backend.rs > /tmp/section-08-3.patch to capture ONLY §08.3’s edits; (2) git apply -R /tmp/section-08-3.patch to reverse-apply; (3) re-run cargo st tests/spec/expressions/poly_lambda_with_imported_generic.ori + cargo test -p ori_types pool::re_intern, confirm BOTH JIT-path suites fail again with the §08.1.R symptoms (AOT tests are NOT part of the §08 negative pin — they fail independently for BUG-04-AOT-MONO); (4) git apply /tmp/section-08-3.patch to restore the fix. This pins §08.2’s JIT-scope semantic contract: the JIT tests pass ONLY because of §08.3’s remap-aware re-intern, not because of unrelated changes.

    Verified 2026-04-19 at HEAD 9eae468d (post-commit adaptation): §08.3’s impl landed in commit 9eae468d (“chore: ori_types pool re-intern + skills/tooling sweep”); the working-tree git diff <paths> form above returns empty post-commit, so the semantically equivalent form git diff HEAD~1 HEAD -- <paths> was used. Captured patch: 1037 lines covering re_intern/mod.rs + re_intern/tests.rs + llvm_backend.rs. (1) Baseline: cargo test -p ori_types --lib pool::re_intern → 35/0/0 ✓. (2) Reverse-apply clean. (3) Re-test under reversal: COMPILE FAIL at compiler/ori_types/src/pool/mod.rs:27 — the re-export line references re_intern_type_with_var_remap / re_intern_sig_with_var_remap, which the reverse-applied patch removes. This is a STRONGER negative pin than the predicted “35→25 test-count regression”: §08.3’s code is load-bearing to the extent that removing it fails the build, not merely the test suite. The pool/mod.rs re-export edit was bundled into 9eae468d but falls outside the plan’s narrow scoped-patch path list (pool/re_intern/ + llvm_backend.rs) — documented in the §08.3 Tooling retrospective block at the end of this subsection (cross-referenced to .claude/skills/improve-tooling/create-plan-design.md §4 commit 127531c2). (4) Restored via git apply /tmp/section-08-3.patch; post-restoration cargo test -p ori_types --lib pool::re_intern → 35/0/0 ✓ (tree consistent with baseline; no drift).

Note on lambda_mono/type_resolve.rs reconnaissance items (apply_bound_var_map, fallback_bound_vars_to_int, is_polymorphic_lambda): the Reviewer-surfaced reconnaissance block flagged these as places where the surface symptom (Idx(241) unresolved) LOOKS like a lambda_mono bug. Under the corrected §08.1.R diagnosis, the lambda_mono path is a consumer, not a producer — once §08.3’s remap-aware re-intern is in place, the substitution map built at llvm_backend.rs:321-328 correctly resolves every imported leaf Tag::Var, so resolve_all_lambda_bound_vars (define_phase.rs:134, nounwind/prepare.rs:173) sees only well-scoped BoundVars and completes without fallback. If lambda_mono code changes are still required after §08.3 lands, §08.3 re-opens; if not, those items are documented as unneeded and the fallback_bound_vars_to_int audit becomes a separate BLOAT concern (silent fallback in a code path that should never fire), filed via /add-bug with subsystem ori-codegen, severity low, after §08.5 closes.

Note on TypeInfoStore size (audit BLOAT_RISK): compiler/ori_llvm/src/codegen/type_info/store.rs is 388 lines (audit minor finding). Under the corrected diagnosis this file is the OBSERVATION point where Idx(241) surfaces, not the root cause. Size concern is owned by a separate bug-tracker artifact whose lifecycle is independent from §08: file /add-bug titled "BLOAT: ori_llvm::codegen::type_info::store.rs at 388 lines (approaching 500-line limit) — split into submodules" with subsystem ori-codegen, severity low. Filing IS the concrete ownership transfer per CLAUDE.md §Ownership & Deferral. §08.3’s pool-crate changes SHALL NOT add lines to store.rs; if they do (unexpected), the bug escalates to medium and the split happens inline before §08.5 closes.

Tooling retrospective (2026-04-19, §08.3 close): two plan-authoring defects surfaced while executing item 2’s scoped-patch reversal negative pin. (a) The plan’s pre-commit form git diff <paths> returns empty post-commit and produces a silent no-op false-positive — adapted to git diff HEAD~1 HEAD -- <paths> at run time. (b) Path-list coverage omitted pool/mod.rs:27 re-export bundled into commit 9eae468d, so the reversal compile-failed rather than test-count-regressed (a STRONGER pin than predicted). Captured as §4 Lessons entry + two §5 regression guards in .claude/skills/improve-tooling/create-plan-design.md (commit 127531c2). No helper script created — niche pattern, filtered per /improve-tooling criteria. Documentation Awareness checks 2 (awareness gap) + 3 (new tool without docs) clean; no existing scoped-patch tool found via scripts/intel-query.sh symbols "scoped_patch" / "reversal", no new tool created this subsection.

08.3b Typeck scheme-body canonicalization: Generalized→BoundVar migration

Goal: Complete the types.md §SC-1 target-conformance migration by canonicalizing generalized scheme-body vars from VarState::Generalized to Tag::BoundVar during scheme construction in compiler/ori_types/src/unify/generalization.rs. Unblocks §08’s success criterion #1 (tests/spec/expressions/poly_lambda_with_imported_generic.ori compiles cleanly via LLVM backend) and retires the producer-side VarState::Generalized exemption documented in compiler/ori_types/src/check/validators/mod.rs::collect_first_unbound_var.

Why absorbed into §08 scope (not /add-bug): Per CLAUDE.md §Plan-Blocker Bugs Belong IN the Plan — the plan cannot complete to its stated goal with poly_lambda_with_imported_generic.ori failing; the fix is therefore merged into plan scope. Plan-blocker classifying test: “Can the plan complete with this bug still open?” — NO.

Root cause (per /tp-help dual-source design consensus 2026-04-19; codex + gemini convergence on option (b)):

  • generalize() at compiler/ori_types/src/unify/generalization.rs:29-58 mutates unbound vars to VarState::Generalized and wraps the original body unchanged in Tag::Scheme. Body-leaf Tag::Var(Generalized) entries survive to codegen.
  • validate_body_types + build_exempt_var_ids at compiler/ori_types/src/check/validators/mod.rs:100-165 exempt every VarState::Generalized var from PC-2 enforcement per types.md §SC-1 shipped-divergence note.
  • Both substitution paths (compiler/ori_types/src/unify/substitute.rs:28-49, compiler/ori_types/src/pool/substitute/mod.rs:28-90) exist only to compensate for the Generalized-encoded body.
  • TypeInfoStore::get_impl at compiler/ori_llvm/src/codegen/type_info/store.rs:341-364 trips on the leak; pool.resolve_fully() at compiler/ori_types/src/pool/accessors.rs:418-437 only follows VarState::Link and leaves Generalized unresolved.
  • Target form per types.md §SC-1: scheme bodies contain Tag::BoundVar leaves bound by the scheme’s declared binders, NOT Tag::Var(Generalized). Migration retires the divergence; the exemption arm becomes unreachable and is stripped.

Corrected diagnosis note (replaces §08.1.R Hypothesis (d) REFUTED classification):

  • Original “constraint gradient” framing (constrained body passes, unconstrained body fails) was an empirical confound. Codex round-0 correction: tests/spec/expressions/lambda_mono.ori:87-103 already contains an unconstrained identity lambda (let $inner = b -> ...) with imported assert_eq AND it passes.
  • Real differentiator: “scheme-shape survives to mono” (multi-instantiation at tests/spec/expressions/poly_lambda_with_imported_generic.ori:136-140; unused scheme at lines 110-112) vs “scheme collapses at one call site”. Fix must land at scheme-body representation, not at generalization control.
  • lambda_mono.ori passes because a + b constrains types before generalization fires — the body has NO Generalized at all. The failing corpus has genuinely scheme-shaped cases where polymorphism survives body inference.

Alternative seams considered + ruled out (per /tp-help consensus):

  • (a) Narrow Value Restriction §GN-3 — INVERTED-TDD:goal-drift; breaks let-polymorphism.
  • (c) Extend end-of-body defaulting to Generalized — banned by impl-hygiene.md §INVERTED-TDD; was reverted at HEAD=3dd4ded6 for this reason.
  • (d) Per-call-site host-binding monomorphization — downstream specialization mechanism, not the root fix; leaves §SC-1 divergence in place.
  • (e) Codegen fallback in TypeInfoStore::get_implcanon.md §7.1 AIMS Invariant 2 violation (masks upstream incorrectness).
  • (f) Partial hybrid migration — CLAUDE.md §NO SHORTCUTS; split representation is structurally worse than full migration.

Primary surface:

  • compiler/ori_types/src/unify/generalization.rsgeneralize() scheme-construction site; rewrite body leaves to Tag::BoundVar with scheme-binder-indexed var_ids.
  • compiler/ori_types/src/unify/substitute.rs, compiler/ori_types/src/pool/substitute/mod.rs — instantiation paths: substitute Tag::BoundVar leaves against fresh Tag::Var per call site; remove VarState::Generalized compensation.
  • compiler/ori_types/src/check/validators/mod.rs — strip unreachable VarState::Generalized arms from build_exempt_var_ids + collect_first_unbound_var.
  • compiler/ori_types/src/check/bodies/functions.rs — body-group pass unchanged; Generalized exemption no longer needed.
  • compiler/ori_llvm/src/codegen/function_compiler/lambda_mono/type_resolve.rsapply_bound_var_map + fallback_bound_vars_to_int verify they handle BoundVar-based schemes; fallback_bound_vars_to_int may become unreachable (audit per §08.3b close-out).

TDD matrix (authored before implementation — RED state):

  • Cell A — poly-lambda single-call at concrete type: let $id = x -> x; id(42) — scheme body has Tag::BoundVar (not Tag::Var(Generalized)); instantiation unifies with int at call site; post-typeck IR has no Tag::Var(Generalized) anywhere. Authored: compiler/ori_types/src/unify/tests.rs::generalize_identity_lambda_body_contains_bound_var_leaves.
  • Cell B — poly-lambda multi-instantiation: let $id = x -> x; let $i = id(1); let $s = id("a") — scheme instantiates independently at each call site with fresh Tag::Var per call; no shared VarState::Generalized; both call sites resolve to their concrete argument types. Authored: compiler/ori_types/src/unify/tests.rs::generalize_then_instantiate_twice_yields_independent_fresh_vars.
  • Cell C — unused poly-lambda: let $id = x -> x; 42 — scheme canonicalizes even with zero call sites; unused-scheme reachable via typed IR traversal contains Tag::BoundVar, never Tag::Var(Generalized). Authored: compiler/ori_types/src/unify/tests.rs::generalize_unused_poly_lambda_canonicalizes_to_bound_var_body.
  • Cell D — nested poly-lambda: let $pair = x -> y -> (x, y); pair(1)("a") — both binders flow through scheme construction; nested scheme body has Tag::BoundVar for both x and y. Authored: compiler/ori_types/src/unify/tests.rs::generalize_nested_lambda_rewrites_both_binders_to_bound_var.
  • Cell E — poly-lambda return-position poly-type: let $some = x -> Some(x); some(42)Option<T> return with T=int monomorphizes correctly; scheme body has Option<BoundVar> post-rewrite (nested Var inside single-child container rewritten). Authored: compiler/ori_types/src/unify/tests.rs::generalize_return_position_polymorphic_type_rewrites_nested_var.
  • Cell F — PC-2 validator negative pin: a typed IR with a surviving Tag::Var (unbound, non-generalized) still fires E2005 via validate_body_types; exemption arm removal MUST NOT regress the Unbound detection path. Covered by existing compiler/ori_types/src/check/validators/tests.rs::body_expr_types_with_unbound_var_emits_one_e2005 (T1) — a minimal Pool::fresh_var() in expr_types with empty scheme_var_ids asserts exactly one E2005. Per SSOT, we do not duplicate this cell; it stays GREEN before and after migration and is the canonical regression guard for the Unbound detection path.
  • Cell G — integration: tests/spec/expressions/poly_lambda_with_imported_generic.ori compiles cleanly via LLVM backend — 10/0/0 post-§08.3b.1. Success criterion #1 delivered.

Design decision ratified (2026-04-19) — Tag::BoundVar.data = var_id, not binder_idx: Plan step 1’s “binder_idx” wording is superseded. Per types.md §SC-1 (“data = var_id matching one of the scheme’s declared var ids”) and the merkle-leaf semantics in types.md §TK-1, Tag::BoundVar.data stores the original var_id from scheme_var_ids, not a positional index. This integrates cleanly with both substitute paths’ existing FxHashMap<u32, Idx> keyed by var_id — no re-keying required. The rewrite_body_generalized_to_bound_var helper replaces each Tag::Var(Generalized { id, .. }) leaf with pool.intern(Tag::BoundVar, id). Cells A–E all assert BoundVar.data == scheme_vars[k] where applicable.

Implementation checklist (TDD — author cells A–G first, confirm all RED, then fix):

  • 1. Design the scheme-body body-rewrite API in unify/generalization.rs: new helper rewrite_generalized_to_bound_var that builds a substitution map {var_id → BoundVar(var_id)} and delegates to the canonical substitute_in_pool machinery. Reuses existing structural recursion (impl-hygiene.md §Algorithmic DRY); no parallel walker needed. Design adjustment from plan: helper name shortened from rewrite_body_generalized_to_bound_var; uses substitute-map dispatch rather than panic-on-missing because the substitute machinery already returns ty unchanged for non-matching var_ids — no soundness loss since all generalized vars by construction ARE in scheme_var_ids.
  • 2. Update generalize() (line 29-58): after VarState::Generalized mutation, call rewrite_generalized_to_bound_var(self.pool, ty, &vars) and pass the rewritten body to Tag::Scheme construction. Cells A-E now pass GREEN.
  • 3. Update pool/substitute/mod.rs (line 28-90): added Tag::BoundVar arm via substitute_bound_var helper; widened fast-path gate to intersects(HAS_VAR | HAS_BOUND_VAR) (post-migration scheme bodies have HAS_BOUND_VAR=true, HAS_VAR=false — the old !HAS_VAR gate would skip them). Trap removal (replaces step 9): the VarState::Generalized arm in substitute_var was REMOVED entirely (not retained with a trap as plan originally stipulated) because maybe_record_mono_instance walks the WHOLE pool by raw index and routinely encounters orphan Tag::Var(Generalized) entries from unrelated polymorphic functions — the legitimate fall-through (return ty unchanged) handles them. The old Generalized arm’s var_subst.get(&id) fallback was redundant: var_id == id because both come from the same fresh_var allocation, so the direct var_subst.get(&var_id) lookup at the top covered every legitimate substitution path.
  • 4. Update unify/substitute.rs (line 28-49): added Tag::BoundVar arm in the substitute() match; widened fast-path gate to include HAS_BOUND_VAR. The VarState::Generalized arm in the Tag::Var branch was REMOVED for the same reason as step 3 (instantiation walks may also encounter orphan Generalized vars during scheme-body recursion through outer-scope refs).
  • 5. Strip VarState::Generalized arm from build_exempt_var_idsNO-OP: re-reading the source, build_exempt_var_ids (lines 161-173) has no Generalized arm; it builds an exempt set from scheme_var_ids directly. Plan step 5 was based on a misread of the validator’s structure. The deeper exemption-strip work that step 5 originally implied landed in §08.3b.1 step 4 (validators/mod.rs:256-276 — Generalized now emits E2005 unless in the scheme-var exempt set).
  • 6. Strip VarState::Generalized arm from collect_first_unbound_var — DONE in §08.3b.1 (step 4). The Generalized arm now emits E2005 unless exempted by scheme_var_ids; Cell L pins the leak-alarm behavior.
  • 7. Update pool/accessors.rs:418-437 (resolve_fully) comment: original code had no explicit Generalized handling (only follows VarState::Link), so step 7’s “remove handling” is moot at the code level. Updated the defensive bounds-check comment to note that the §08.3b scheme-body migration retired the Generalized-leak path at the scheme-body level, leaving the bounds check as a pure cross-pool-collision guard.
  • 8. Migration verification: DONE post-§08.3b.1. cargo test -p ori_types 868/0, cargo test -p ori_llvm --lib 637/0, cargo test -p ori_llvm --test aot 2161/0, cargo st 3622/843/33 (baseline match), cargo run --bin ori -- test --backend=llvm tests/spec/expressions/poly_lambda_with_imported_generic.ori 10/0/0. Full test-all.sh Ori spec LLVM backend: 2389/4/27/lc_fail:2078 (vs pre-§08.3b.1 baseline 1851/1/21/lc_fail:2615 — +538 passing).
  • 9. Remove the trap in step 3 — handled inline in step 3 (no traps retained; see step 3’s revised note). Generalized arms removed entirely from BOTH substitute paths.
  • 10. Audit fallback_bound_vars_to_int at lambda_mono/type_resolve.rs:392: deferred to §08.3b.1 since the LLVM-side BoundVar resolution path is part of that subsection’s scope (the audit naturally runs once the integration test compiles).

Downstream verification:

  • timeout 150 cargo test -p ori_types — 865/0 PASS (improved from 860/0 baseline by the 5 newly-passing §08.3b cells A-E).
  • timeout 150 cargo test -p ori_llvm — 633/0 lib PASS (no regression).
  • timeout 150 cargo st — 3622/843/33 — interpreter baseline preserved exactly (state.sh last_run_sha=58c26963 reported 3612/843/33 pre-migration; the +10 passing delta is unrelated tests added since baseline capture).
  • cargo run --bin ori -- test --backend=llvm tests/spec/expressions/poly_lambda_with_imported_generic.ori — 10/0/0 post-§08.3b.1.
  • cargo run --bin ori -- test --backend=llvm tests/spec/expressions/lambda_mono.ori — still passes post-migration.
  • diagnostics/dual-exec-verify.sh on poly-lambda corpus — to run at /commit-push time. Done 2026-04-20 (§08.5 verification): lambda_mono.ori 17/17 both backends, poly_lambda_with_imported_generic.ori 10/10 both backends, tests/spec/traits/iterator/ 23/32 verified intersection — “No behavioral mismatches detected” on all three runs.
  • Scoped-patch reversal negative pin (section-level semantic pin, covers §08.3 + §08.3b + §08.3b.1 joint diff): git diff <affected files> > /tmp/section-08-joint.patch; git apply -R; confirm failing-test symptoms reappear; git apply to restore. Run at /commit-push time. Deferred to §08.N section-close gate (see 08.N line 797 “Semantic pin verification (section-level, JIT-scope)” — identical workflow consolidated there).

Cross-plan sync after §08.3b lands:

  • Update .claude/rules/types.md §SC-1 — retire target-only divergence note; migration is shipped. Done 2026-04-20 (§08.H, commit 4ff5ca59): §SC-1 now documents rewrite_generalized_to_bound_var + normalize_body_generalized_to_bound_var_sig as shipped; residual Tag::Var(Generalized) at PC-2 is a leak-alarm emitting E2005. Grep-verified against shipped code per §08.N line 799.
  • Update .claude/rules/typeck.md §PC-2 — retire Generalized exemption note. Done 2026-04-20 (§08.H, commit 4ff5ca59): §PC-2 now states the validator treats VarState::Generalized identically to VarState::Unbound (emits E2005 unless in scheme-var exempt set); only Rigid stays unconditionally exempt per §UN-6. Grep-verified against shipped code per §08.N line 799.
  • Update compiler/ori_types/src/check/validators/mod.rs::collect_first_unbound_var doc comment — DONE inline with §08.3b.1 step 4.
  • Update plans/empty-container-typeck-phase-contract/00-overview.md success criteria + section list — §08.3b.1 delivered. Handled at /commit-push time. Done 2026-04-20: 00-overview.md already carries the <!-- corrects: plans/roadmap/section-21A-llvm.md ... --> cross-link (§08.4) plus the §08 entry at line 304 documenting JIT-only scope and BUG-04-AOT-MONO sibling. Section list already matches current plan structure (§08.3b/§08.3b.1/§08.3c present).

§08.3b close-out (per /roadmap-work SKILL.md Step 7):

  • All §08.3b TDD cells (A–G) pass — Cells A-E GREEN, Cell G GREEN post-§08.3b.1 (10/0/0 via LLVM backend).
  • All implementation checklist items (1–10) complete — items 1-4, 7, 9 done in §08.3b; items 5, 6, 8, 10 resolved in §08.3b.1 (step 6 collapsed into the strip done by §08.3b.1 step 4; step 5 was a NO-OP re-read finding; step 8 verification now GREEN; step 10 audit done — fallback_bound_vars_to_int remains reachable load-bearing safety net).
  • Downstream verification green — all four passes GREEN post-§08.3b.1.
  • /tpr-review passed on §08.3b diff — Round 0 clean (codex + gemini both status: clean, 0 verified findings). Scratch dir /tmp/tpr-round-ori_lang-joiY7bb1. Exit reason: clean. 2026-04-19.
  • /impl-hygiene-review passed after TPR clean — 0 §08.3b-introduced findings, 9 pre-existing BLOAT findings filed inline under §08.H as plan-close blockers. All 7 focus axes passed (SSOT/DRY, fast-path gate, dead-code totality, partial-migration disclaimer, Merkle determinism, test matrix, phase-purity). Scratch dir /tmp/impl-hygiene-ori_lang-HY4lvpdu. 2026-04-19.
  • /improve-tooling retrospective (per-subsection) — captures the “scope-discovery during execution” pattern (plan steps 5/6 assumed scheme-body migration would suffice for validator strip; in practice expr_types/FunctionSig also leak Generalized).
  • sync-claude run — rule files updated to reflect retired divergence. Done 2026-04-20 (§08.H): types.md §SC-1 + typeck.md §PC-2 updates landed inline with §08.H BLOAT refactors in commit 4ff5ca59; grep-verified against rewrite_generalized_to_bound_var + normalize_body_generalized_to_bound_var_sig + the merged Unbound|Generalized validator arm per §08.N line 799.
  • /commit-push — one commit for the partial scheme-body migration (distinct from §08.3’s pool-merge commit); §08.3b.1 landed its own commit. Done: §08.3b partial scheme-body migration shipped in commit de135723 (fix(ori_arc): §08.3c newtype lowering + §08.3b scheme-body migration). §08.3b.1 shipped in 991b17d5.
  • Mark §08.3’s previously-deferred downstream verification item (poly_lambda_with_imported_generic.ori) as [x] — now unblocked. (See line 398-399.)
  • Section 08 status ready to close per §08.N completion checklist. Done 2026-04-20: §08.N completion checklist fully [x] (one [~] for the semantic-pin gate with documented “MADE REDUNDANT post-commit” justification); LLVM backend spec run GREEN (+538 passing / −537 lc_fail vs pre-§08 baseline).

08.3b.1 Inference-pipeline expr_types / FunctionSig port — finish §08.3b’s Generalized → BoundVar migration

Goal: Complete the types.md §SC-1 migration started in §08.3b by also rewriting Tag::Var(Generalized) leaves in (a) InferOutput.expr_types entries for every polymorphic let-binding’s body sub-expressions, and (b) FunctionSig.param_types and FunctionSig.return_type for top-level polymorphic functions. Round-0 TPR reconciled the LLVM-side contract (Option B): substitution happens UPSTREAM in lower_function_can via type_subst threaded from MonoFunction.body_type_map (see function_compiler/nounwind/prepare.rs:133); TypeInfoStore::compute_type_info_inner Tag::BoundVar arm is a defensive ICE signal returning TypeInfo::Error per canon.md §7.1 AIMS Invariant 2. Unblocks §08’s success criterion #1 and retires the VarState::Generalized exemption arms in validators/mod.rs.

Why absorbed into §08 scope (not /add-bug): Plan-blocker — §08.3b cannot reach success criterion #1 (poly_lambda_with_imported_generic.ori compiles via LLVM) without this work. Per CLAUDE.md §Plan-Blocker Bugs Belong IN the Plan, plan-blocker bugs merge into the plan as new subsections rather than spawning sibling fix files. This subsection is the natural sibling to §08.3b.

Root cause (refined from §08.3b’s discovery during execution 2026-04-19):

  • unify::generalization::generalize() rewrites SCHEME bodies (Tag::Scheme.body) to Tag::BoundVar leaves correctly (§08.3b shipped).
  • BUT expr_types[lambda_body_sub_expr] for let-polymorphic lambdas still references the ORIGINAL Tag::Var leaves whose var_state was mutated to Generalized in place — the rewrite produces NEW Tag::BoundVar Idxs but does NOT update existing expr_types entries.
  • Similarly, FunctionSig.param_types / FunctionSig.return_type for top-level polymorphic functions are populated PRE-generalize and never re-pointed at the rewritten BoundVar Idxs.
  • LLVM TypeInfoStore::get_impl (compiler/ori_llvm/src/codegen/type_info/store.rs:341-364) calls pool.resolve_fully(idx) which only follows VarState::LinkTag::Var(Generalized) and Tag::BoundVar both fall through unresolved, producing the Idx(246)/Idx(251) unresolved codegen error.

Primary surface (target):

  • compiler/ori_types/src/check/bodies/ — add a post-generalize normalization pass, driven from ModuleChecker at end-of-body, that walks InferOutput.expr_types + the body’s FunctionSig.param_types / return_type and substitutes Tag::Var(Generalized)Tag::BoundVar for each scheme’s var_ids. Reuse substitute_in_pool with the {var_id → BoundVar(var_id)} substitution map (same shape as §08.3b’s rewrite_generalized_to_bound_var helper). InferEngine::generalize() records the generalized binder ids; Bodies runs the normalization pass before validate_body_types — mirrors the existing default_unbound_vars_from_empty_literals pattern.
  • compiler/ori_llvm/src/codegen/type_info/store.rs compute_type_info_inner — Tag::BoundVar arm is a defensive ICE signal per Round-0 Option B reconciliation: returns TypeInfo::Error with a WARN trace when a Tag::BoundVar reaches codegen. Substitution itself happens UPSTREAM in lower_function_can via type_subst threaded from MonoFunction.body_type_map at function_compiler/nounwind/prepare.rs:133. By codegen time a correctly-substituted mono body has no Tag::BoundVar leaves; any survivor signals an upstream substitution gap per canon.md §7.1 AIMS Invariant 2 (no masking). TypeInfoStore stays context-free per impl-hygiene.md §SSOT.
  • compiler/ori_types/src/pool/accessors.rs::resolve_fully — REMAINS UNCHANGED (link-only). Do NOT add a Tag::BoundVar arm here; MonoInstance.body_type_map is per-specialization substitution state, not a pool fact. Threading monomorphization context into the context-free pool accessor would contaminate a typeck producer-path SSOT (validator, end-of-body defaulting both call resolve_fully).
  • compiler/ori_types/src/check/validators/mod.rs::collect_first_unbound_var — STRIP the VarState::Generalized exemption arm; the partial-migration doc comment retires; Tag::Var(Generalized) becomes a regression alarm (E2005) per the original §08.3b plan steps 5/6.

Design consensus (2026-04-19 /tp-help — codex HIGH + gemini LOWER converge):

CallDecisionRationale
Pass insertion pointOption 1B — end-of-body-group pass in check/bodies/Option 1A (inside UnifyEngine::generalize()) violates phase purity: UnifyEngine is the scheme-construction primitive; InferEngine owns expr_types; Bodies group owns end-of-body export choreography. Threading expr_types/FunctionSig into unify reverses EN-1 ownership and creates the “later phase calls back into earlier owner” bleed banned by impl-hygiene.md §Phase Boundaries + canon.md §5. The default_unbound_vars_from_empty_literals pass at compiler/ori_types/src/infer/mod.rs is the direct precedent — same end-of-body, pre-validate_body_types slot, same scope-by-var substitution shape. typeck.md §CK-1 passes 2–5 explicitly own per-body cleanup; typeck.md §PC-2 producer-side enforcement already lives in Bodies.
BoundVar resolutionOption B (RECONCILED Round 0 TPR) — substitute UPSTREAM in lower_function_can via type_subst; TypeInfoStore Tag::BoundVar arm is a defensive ICE signalOriginal Option 2B (“arm in TypeInfoStore::get_impl consulting MonoInstance.body_type_map”) never matched shipped reality. Upstream substitution won on compositional grounds with ARC lowering: MonoFunction.body_type_map is threaded through function_compiler/nounwind/prepare.rs:133 into lower_function_can’s type_subst parameter, which substitutes Tag::BoundVar leaves to concrete types during CanExpr → ArcFunction lowering. By codegen time, a correctly-substituted mono body has zero Tag::BoundVar leaves. Option 2A (contextify pool/accessors::resolve_fully) remains ruled out: resolve_fully is a context-free accessor, producer-side typeck paths call it (validator, end-of-body defaulting, normalization), and routing monomorphization state through it leaks specialization state into typeck. Option B’s reconciled contract: TypeInfoStore stays context-free per impl-hygiene.md §SSOT; compute_type_info_inner Tag::BoundVar arm surfaces upstream substitution gaps as TypeInfo::Error per canon.md §7.1 AIMS Invariant 2 (no masking of upstream incorrectness). Cell J pins the ICE-signal contract: type_info_store_bound_var_surfaces_as_error_ice_signal. Long-term target (out of §08.3b.1 scope): a TypeFolder trait per impl-hygiene.md §Aspirational Patterns §Type Folding, tracked as future consolidation.

Scratch dir with full reviewer responses: /tmp/tpr-round-ori_lang-Iv3X4D89/{codex,gemini}-report.txt (ephemeral — capture in commit body when §08.3b.1 lands).

TDD matrix (authored + confirmed RED in Phase 1, turned GREEN in Phases 2-4):

  • Cell H — expr_types port (positive) — GREEN. check/bodies/tests.rs::expr_types_port_lambda_body_is_bound_var.
  • Cell I — FunctionSig port (positive) — GREEN. check/bodies/tests.rs::function_sig_port_top_level_polymorphic_function.
  • Cell J — LLVM TypeInfoStore Tag::BoundVar ICE signal — GREEN post-Round-0 Option B reconciliation. ori_llvm/src/codegen/type_info/store/tests.rs::type_info_store_bound_var_surfaces_as_error_ice_signal. Pins the defensive-ICE contract: Tag::BoundVar reaching compute_type_info_inner surfaces as TypeInfo::Error per canon.md §7.1 AIMS Invariant 2 (upstream lower_function_can type_subst is the substitution mechanism; store stays context-free per impl-hygiene.md §SSOT).
  • Cell K — validator strip (positive) — GREEN. check/bodies/tests.rs::validator_strip_polylambda_typechecks_no_e2005.
  • Cell L — validator strip (negative pin) — GREEN (both variants). validators/tests.rs::generalized_var_in_expr_types_emits_e2005_as_leak_alarm + ..._polylambda_return_type_with_generalized_var_emits_e2005_as_leak_alarm.
  • Cell M — integration: tests/spec/expressions/poly_lambda_with_imported_generic.ori — 10/0/0 via LLVM backend. Success criterion #1 delivered.

Implementation checklist (TDD — author cells H–M first, confirm all RED, then fix):

  • 1. Design the post-generalize pipeline pass: RESOLVED 2026-04-19 via /tp-help consensus — Option 1B, end-of-body-group pass in check/bodies/, driven from ModuleChecker, running after body inference and BEFORE validate_body_types. InferEngine::generalize() records the generalized binder ids; Bodies orchestrates the substitute walk. Mirrors default_unbound_vars_from_empty_literals pattern. See “Design consensus” table above for full rationale.
  • 2. Implement the pass: DONE. InferEngine::pending_generalized_vars + generalize() recording (compiler/ori_types/src/infer/mod.rs:174-183, 539-555). Normalization method normalize_body_generalized_to_bound_var_sig (mod.rs:812-898) walks expr_types + FunctionSig.param_types + return_type, builds {var_id → pool.bound_var(var_id)} substitution map, applies via substitute_in_pool. Wired into check/bodies/functions.rs:134-148, 254-258 and check/bodies/impls.rs:213-222, 347-356. Runs BEFORE run_validator(...).
  • 3. Decide Tag::BoundVar resolution boundary: RESOLVED — Option B reconciliation (Round-0 TPR). Substitution happens UPSTREAM in lower_function_can via type_subst (threaded from MonoFunction.body_type_map at function_compiler/nounwind/prepare.rs:133); TypeInfoStore::compute_type_info_inner Tag::BoundVar arm is a defensive ICE signal returning TypeInfo::Error per canon.md §7.1 AIMS Invariant 2. pool/accessors.rs::resolve_fully stays link-only (monomorphization state must NOT contaminate the context-free pool SSOT). TypeInfoStore is context-free per impl-hygiene.md §SSOT. See “Design consensus” table above for full Option B rationale.
  • 4. Strip the VarState::Generalized exemption arm in check/validators/mod.rs::collect_first_unbound_var — DONE. validators/mod.rs:256-276. The VarState::Generalized | VarState::Rigid => false arm was split: Generalized now emits E2005 unless the var_id is in the exempt set (scheme-var polymorphism preservation); Rigid remains unconditionally exempt per typeck.md §UN-6. Partial-migration doc comment retired; replaced by §08.3b.1 leak-alarm docs.
  • 5. Update validators/tests.rs generalized-var tests..._emits_e2005_as_leak_alarm — DONE. Both tests flipped from “emits no diagnostic” to “emits one E2005” (Cell L).
  • 6. Migration verification: DONE. cargo test -p ori_types 868/0, cargo test -p ori_llvm 637/0 lib + 2161/0 aot, cargo st 3622/843/33 (baseline match), poly_lambda_with_imported_generic.ori 10/0/0. Full test-all.sh Ori spec LLVM backend: 2389/4/27/lc_fail:2078 (vs pre-§08.3b.1 baseline 1851/1/21/lc_fail:2615 — +538 passing).
  • 7. Audit fallback_bound_vars_to_int at lambda_mono/type_resolve.rs:403 — REACHABLE (called from lambda_mono/mod.rs:121 and :436). Phase 3 tightened its return-type guard from over-eager contains_bound_var walk to top-level Tag::BoundVar | Tag::Var check, preventing container-return collapse to Idx::INT. Function is load-bearing safety net for paths where body_type_map resolution misses — no /add-bug needed.
  • 8. Cross-plan sync (partial → complete): validators/mod.rs::collect_first_unbound_var doc comment cleaned (Phase 3, inline with step 4). Done 2026-04-20: 00-overview.md carries §08.4 cross-link and JIT-only scope notes at lines 35/304. types.md §SC-1 target-only note retirement + typeck.md §PC-2 defaulting-pass doc update flow through /sync-claude at §08.N close-out.

§08.3b.1 close-out:

  • All §08.3b.1 TDD cells (H–M) pass.
  • All implementation checklist items (1–8) complete — 1, 2, 3, 4, 5, 6, 7 DONE; 8 cross-plan sync flows through /sync-claude + /commit-push (types.md §SC-1 + typeck.md §PC-2 updates + 00-overview.md).
  • §08.3b’s deferred items (5, 6, 8, 10) and downstream verification items unblock and turn green. Done 2026-04-20: §08.3b status=complete.
  • §08.3b’s Cross-plan sync items unblock and complete. Done 2026-04-20: 00-overview.md flipped inline (§08.4 cross-link added); types.md/typeck.md sync flows through /sync-claude below.
  • §08.3b’s §08.3b close-out checklist unblocks and §08.3b can mark complete. Done 2026-04-20.
  • /tpr-review passed on §08.3b.1 diff — 3 rounds, exit_reason user_accepted_at_meta_cap_reached 2026-04-20. Round 0: 1 HIGH architectural finding (F1 body_type_map wiring) resolved via Option B reconciliation (remove unused store plumbing + BoundVar arm as defensive ICE signal per canon.md §7.1 AIMS Invariant 2). Rounds 1+2: plan-doc drift echoes (meta-only), all fixed inline across §08.3b.1 Goal / Cell J flip / Step 3 flip / Option 2B table row / Primary surface bullet. Both reviewers’ round 2 summaries agreed on technical convergence (868/0 / 637/0 / 2161/0 / Cell M 10/0/0).
  • /impl-hygiene-review passed after TPR clean — 2026-04-20. 16 findings (1 Critical + 2 Major + 6 Minor BLOAT + 7 NOTE). F10 Critical LEAK:algorithmic-duplication (body_type_map triplication) RESOLVED inline via build_mono_body_type_map + BodyTypeMapSink extraction at pool/substitute/. F3-doc ghost reference RESOLVED. F11-F16 filed in §08.H with concrete close actions (drift watch + BLOAT with concrete anchors per impl-hygiene.md §Findings Disposition — Minor BLOAT with anchors is deferred-valid; Critical was resolved inline). 868/0 ori_types + 2161/0 AOT preserved post-extraction.
  • /improve-tooling retrospective — 2026-04-20 compact-form capture. Three tooling-improvement candidates surfaced from §08.3b.1’s journey: (1) Cross-crate structural duplication detection: F10 Critical was caught by Phase 3 Opus multi-lens analysis but NOT by batch hygiene-lint.py — add a cheap pattern-match tool that flags functions with structurally similar control-flow skeletons across crates (target: catch 3+-site algorithmic duplication pre-commit). (2) LLVM-spec-backend crash bisect automation: Phase 4 manual bisect of the harness crash took 3-5 iterations to isolate test_expect_none.ori; script as diagnostics/spec-crash-bisect.sh <subtree> to short-circuit this. (3) Scope-expansion contingency markers for plan subsections: §08.3b.1’s initial scope (steps 1-8) expanded ~4x through 4 sub-agent phases (recon+cells → impl → regression-fix → nounwind-fix); consider adding a <!-- scope-uncertainty: high --> marker format so the roadmap scanner can surface plan-subsection ranges that might need re-estimation before dispatch. All three are tracked as follow-up /improve-tooling candidates — not blocking §08.3b.1 close-out. Pattern reinforced: CLAUDE.md §Fix Interference protocol (Bug A → Bug B → classify + fix B first) was correctly followed across Phases 3+4 of this session.
  • /commit-push — one commit for the §08.3b.1 work. Done 2026-04-20: shipped as commit 991b17d5 (refactor(ori_types,ori_llvm): §08.3b.1 Generalized→BoundVar + Option B). Follow-on §08.H BLOAT refactors shipped in 4ff5ca59.

§08.3b.1 HISTORY (captured 2026-04-20 after 4-phase implementation):

  • Phase 1 (recon + RED cells): cells H, I, K, L were pre-authored in a prior session; Cell J authored fresh (creates ori_llvm/src/codegen/type_info/store/tests.rs). All cells RED except Cell K (green-by-design regression pin).
  • Phase 2 (Steps 2-5 implementation): engine-state tracking (pending_generalized_vars), normalization method + Bodies-pass wiring, TypeInfoStore::body_type_map + with_body_type_map builder + Tag::BoundVar arm, validator Generalized-arm strip, monomorphize body_type_map widening. Cells H, I, K, L GREEN. Cell J RED by plan contradiction. Cell M ERROR shape shifted but still failed.
  • Phase 3 (interference resolution): 15 AOT regressions (curried closure + chained generics) traced to two root causes: (a) build_mono_instance + extract_var_from_types still HAS_VAR-only after Phase 2’s maybe_record_mono_instance widening; (b) lambda_mono return-type resolution gated on contains_var alone, plus resolve_lambda_return_types missing PartialApply.ty substitution + fallback_bound_vars_to_int collapsing container-return types to Idx::INT. Five surgical fixes. All 15 regressions GREEN; Cell M 10/0/0.
  • Cell J test setup fixed (main context): body_type_map populated → TypeInfo::Int (SUCCESS path); negative assertion pins no-fallback (ERROR path) per canon.md §7.1 Invariant 2.
  • Phase 4 (LLVM-backend spec harness crash): test-all.sh showed Ori spec LLVM backend CRASHED. Bisect → tests/spec/types/option/expect.ori. Root cause: latent nounwind-analyzer over-approximation in lambda_mono/context.rs::is_callee_intercepted treating ALL intercepted builtins as nounwind, including may-unwind Option.expect/Option.unwrap/Result.expect/Result.unwrap/etc. Added MAY_UNWIND_INTERCEPTED_METHODS list + intercepted_is_nounwind helper; gated Apply/Invoke analyzer paths. Updated AOT wrapper_rc_retain negative pins (exit code 1 via ori_run_main is the designed path, not SIGABRT). Filed 3 pre-existing LLVM-codegen bugs: BUG-04-086/087/088. Net: +538 passing LLVM-backend spec tests vs pre-§08.3b.1 baseline.
  • Round 0 TPR resolution (Option B reconciliation): codex TPR flagged F1 — TypeInfoStore::body_type_map field + with_body_type_map builder present but never wired at the per-MonoFunction codegen boundary. Verified against code: substitution happens UPSTREAM in lower_function_can via type_subst (threaded from MonoFunction.body_type_map at function_compiler/nounwind/prepare.rs:133); by codegen time, a correctly-substituted mono body has no Tag::BoundVar leaves. Original Option 2B design (“store consults body_type_map”) never matched shipped reality — upstream substitution won on compositional grounds with ARC lowering. Option B removed the unused plumbing: body_type_map field + with_body_type_map builder dropped from TypeInfoStore; compute_type_info_inner Tag::BoundVar arm is now an Error-only defensive ICE signal per canon.md §7.1 AIMS Invariant 2 (upstream substitution gaps surface as TypeInfo::Error, not masked). Cell J renamed type_info_store_bound_var_surfaces_as_error_ice_signal and pins the ICE contract (single assertion). All three test suites stay GREEN (868/0 ori_types, 637/0 ori_llvm lib, 2161/0 AOT); Cell M 10/0/0 preserved; clippy clean.

08.3c LLVM newtype codegen regression from §08.3 pool-merge remap — investigation and fix (commit blocker)

Goal: Fix the LLVM backend codegen regression that surfaced on first test-all.sh run after §08.3’s pool-merge remap-aware re-intern landed. Commit of §08.3’s uncommitted code is BLOCKED on this.

Symptoms (from test-all.log post-§08.3):

  • Ori spec (LLVM backend): CRASHED (was passed 1851, failed 1, skipped 21, lc_fail 2615 at state.sh baseline HEAD 58c26963).
  • AOT integration tests: 0 passed, 2 failed (2 failures expected and tracked under BUG-04-AOT-MONO; NEW: AOT suite also shows errors on test_different_newtype_values, test_newtype_parameter, test_newtype_computation).
  • LLVM IR verification failed after codegen (emit_arc_function) at compiler/ori_llvm/src/codegen/function_compiler/define_phase.rs for newtype tests.
  • emit_partial_apply: callee not found name="UserId" / "Score" warnings in compiler/ori_llvm/src/codegen/arc_emitter/closures.rs.
  • Call parameter type does not match function signature LLVM assertions on newtype constructor call sites.
  • ori panic: must be set followed by ori: fatal — _Unwind_RaiseException returned (code 5).

Hypothesis (root cause candidate — investigate first, do NOT jump to fix): The §08.3 fix changed re_intern_type’s (legacy API) semantics for Tag::Var | Tag::BoundVar | Tag::RigidVar arms: previously preserved source var_id verbatim via target.intern(tag, source.data(idx)); now allocates a FRESH dst var_id via get_or_allocate_var_id + Pool::allocate_var_id. Any consumer of the legacy re_intern_type API outside the 3 llvm_backend.rs sites we updated that depends on source-id PRESERVATION semantics is now broken. Newtype constructors (UserId, Score) flow through ori_llvm::codegen::arc_emitter::closures::emit_partial_apply — which may key lookups by var_id. partial_apply “callee not found” suggests a name-resolution or mono-instance keyed by var_id is now missing.

Alternative hypothesis: the crash pre-dates §08.3 and is caused by one of the 3 commits between state.sh baseline HEAD 58c26963 and current HEAD dbc3492c (d96cab30 §08.1.5 close, d0b3bcb3 §08.2 close, dbc3492c /add-bug skill edit). State.sh snapshot was last refreshed at §03.5 close and is stale.

Re-diagnosis (2026-04-20, recon round) — original hypothesis contradicted by code; replaces both hypothesis blocks above:

Recon results pinning the original hypothesis as wrong:

  • scripts/intel-query.sh callers "re_intern_type" returns 18 hits — all in compiler/ori_types/src/pool/re_intern/tests.rs (16 functions) and compiler/oric/benches/pool_interning.rs (2 benchmark functions). Zero production callers. compiler/ori_llvm/src/evaluator/mod.rs:47 is a doc comment (/// uses [\ori_types::re_intern_type`]`), not a call.
  • All 3 production sites in compiler/oric/src/test/runner/llvm_backend.rs (lines 177, 227, 272) already use re_intern_type_with_var_remap / re_intern_sig_with_var_remap with shared per-module var_remap maps (lines 161-162). The §08.3 update is correct at those sites.
  • emit_partial_apply (compiler/ori_llvm/src/codegen/arc_emitter/closures.rs:50) keys lookup by Name via self.ctx.functions.get(&callee). Name is global-interned and pool-independent (per pool/re_intern/mod.rs:511Name is a global intern, pool-independent”). var_id remap cannot directly cause a Name-keyed lookup miss.

Reproduced symptoms — timeout 150 cargo run --quiet --bin ori -- test --backend=llvm tests/spec/types/newtypes.ori (2026-04-20) returned 0 passed, 0 failed, 0 skipped, 9 llvm compile fail; 1 file could not compile via LLVM; LLVM codegen had 13 error(s). Key warning/error signature, in emission order:

  • WARN ori_llvm::codegen::type_info::store: Named/Applied/Alias type has no Pool resolution — may be a generic type parameter or unregistered type idx=Idx(214..217) tag=Tag::named (4 occurrences at high indices)
  • WARN ori_llvm::codegen::arc_emitter::apply: unresolved function 'unwrap' in apply — missing mono instance? (5+)
  • WARN ori_llvm::codegen::arc_emitter::closures: emit_partial_apply: callee not found name="UserId"|"Age"|"Score" (10+ across the three local newtypes)
  • ERROR ori_llvm::codegen::arc_emitter::emitter_utils: ArcIrEmitter: variable not yet defined var=N (3+; var indices 5, 8, 14)
  • WARN ori_llvm::codegen::arc_emitter::terminators: unresolved function 'unwrap' in invoke — missing mono instance?

Two facts re-shape the diagnosis:

  • The failing names (UserId, Age, Score) are local to the test file (type UserId = str; / type Age = int; / type Score = int;). They are NOT imported types — only assert_eq and assert are imported (from std.testing). So the cross-pool re-intern path (§08.3’s surface) does not directly govern their lifecycle.
  • The merged pool is constructed at llvm_backend.rs:149 as let mut merged_pool = pool.clone(); — the host file’s pool is the seed. Imports are re-interned on top via _with_var_remap. Under Pool: Clone (compiler/ori_types/src/pool/mod.rs:44), the resolutions map clones with the rest of the pool, so local Tag::Named → struct/enum entries should survive. The Idx(214..217) “no Pool resolution” warnings hit AFTER several lower indices, suggesting these are imports or import-derived entries, not local newtypes.

Working hypothesis (replaces the original “legacy API consumer broke” framing): the failure is not a re_intern_type legacy-vs-remap-aware semantics issue. The actual fault path involves one or both of:

  1. Newtype constructor lowering produces PartialApply { callee: Name("UserId"), ... } instead of Construct — the IR-level lowering of UserId("user-123") is treating the type-name as a function-pointer reference. The downstream emit_partial_apply then looks the name up in ctx.functions (which holds compiled function declarations) and misses, because newtypes have no constructor function declaration. This is upstream of the AIMS / LLVM emission split — likely in ori_canon or ori_arc::lower::expr (Construct vs Call disambiguation for Tag::Named-typed call targets).
  2. Newtype .unwrap() is treated as a mono-instance method requiring resolution — the unresolved function 'unwrap' in apply — missing mono instance? warning suggests the method dispatch is going through the same path as user-defined generic methods, not through the newtype’s built-in .unwrap() projection. Per ori-syntax.md §Newtypes the .inner projection (and unwrap when present) should be type-trivial extraction at codegen, not a separate function emission.

Whether this surfaced specifically because of §08.3 or has been broken in the JIT test-runner path independently is out of scope per CLAUDE.md §“NEVER Investigate Pre-Existing” — the bug is here now, the bug is owned, the question is “is it fixed?” not “did §08.3 cause it?”.

Implications for §08.3c scope:

  • The original Phase 1 checkboxes 1–3 (git-worktree baseline comparison) are superseded by global rule §“NEVER Investigate Pre-Existing”. They will not be executed.
  • Phase 1 checkboxes 4–7 (intel + reads) are complete as recon (results above).
  • The phase 2 fix-shape question (a) “revert legacy API to preserve” vs (b) “migrate broken consumer” is moot under the corrected diagnosis — neither classification applies because the failing path does not flow through re_intern_type at all.
  • New fix-shape question is: whether the local newtype constructor + .unwrap() resolution path needs a dedicated handler in ori_arc::lower or ori_canon that prevents Tag::Named constructor calls from being lowered to PartialApply. Determining this requires reading ori_canon::lower::expr and/or ori_arc::lower::call for the Construct-vs-Call dispatch on a Tag::Named callee — out of scope for this recon round per the user’s “do not check-in” constraint and context-budget cap.

Root cause (2026-04-20, post-recon trace) — pinpointed in ori_arc::lower::expr::lower_ident:

The “callee not found” warning chain originates at compiler/ori_arc/src/lower/expr/mod.rs:354-360:

} else if self.pool.tag(self.pool.resolve_fully(ty)) == Tag::Function {
    // Named function used as a value — emit zero-capture closure.
    // This handles `CanExpr::Ident` for top-level functions that weren't
    // rewritten to `CanExpr::FunctionRef` by the canonicalizer (e.g.,
    // `apply(f: double, x: 21)` where `double` is a named function).
    self.builder
        .emit_partial_apply(ty, name, vec![], Some(span))
}

The trace for UserId("user-123") (verified by reading lower_call at compiler/ori_arc/src/lower/calls/mod.rs:112-183 + lower_ident at lower/expr/mod.rs:331-369):

  1. UserId("user-123") is parsed and canonicalized as CanExpr::Call { func: <UserId-ref>, args: ["user-123"] }. The canonicalizer typically emits the func subexpression as CanExpr::TypeRef("UserId") (per lower/expr/mod.rs:237 CanExpr::Ident(name) | CanExpr::Const(name) | CanExpr::TypeRef(name) => self.lower_ident(...)) — newtype names resolve through the same path as type names.
  2. lower_call matches func_kind. CanExpr::TypeRef is NOT in the FunctionRef | SelfRef | Ident arms — it falls to the wildcard _ => at line 173:
    _ => {
        let closure_var = self.lower_expr(func);
        ...
        self.emit_indirect_call(ty, closure_var, arg_vars, span)
    }
  3. self.lower_expr(func) → dispatches to lower_ident("UserId", ty=<UserId-Tag::Function-signature>).
  4. Inside lower_ident: name not in scope (UserId is a type, not a variable); not in variant_ctors (newtypes are not enum variants); but the type tag IS Tag::Function (the type checker assigned (str) -> UserId to the constructor reference, per typeck.md newtype constructor signature handling).
  5. The else if Tag::Function arm fires (lines 354-360) and emits PartialApply { callee: Name("UserId"), args: vec![] }.
  6. Later in emit_partial_apply (compiler/ori_llvm/src/codegen/arc_emitter/closures.rs:50), the lookup self.ctx.functions.get(&"UserId") MISSES because newtypes have no compiled function declaration — they are not registered in the codegen context’s function table. This produces the “callee not found name=UserId” warning, the null-closure fallback, and the cascading “variable not yet defined” / “Call parameter type does not match function signature” errors at the call site.

The .unwrap() symptom has the same shape: id.unwrap() lowers via lower_method_call → emits Apply with method name "unwrap"unresolved function 'unwrap' in apply — missing mono instance? because the codegen context has no unwrap function for the UserId newtype receiver. Newtype unwrap (and .inner) should be a transparent projection (the wrapper IS the inner per repr.md §RP-24 “structurally identical to that of Existing”), not a function dispatch.

Why this looked like a §08.3 regression but is structurally independent: §08.3’s pool-merge work touches the Tag::Var/Tag::Scheme/var_id path. The newtype-constructor-as-PartialApply defect is in the lower_ident Tag::Function arm, which §08.3 did not touch. The defect predates §08.3; it surfaced in test-all.sh LLVM-CRASHED reporting because the test runner’s JIT path is the only context that actually compiles tests/spec/types/newtypes.ori through LLVM (per §08.2 close-out note: “§08 scope is therefore JIT-only”; the AOT path skips imported generic mono entirely). The state.sh baseline at HEAD 58c26963 was last refreshed at §03.5 close, predating any work that would have surfaced this — the JIT path’s silent treatment of newtype constructors as missing function pointers has likely been broken since newtype support landed in the type checker. CLAUDE.md §“NEVER Investigate Pre-Existing” forbids confirming this via git archaeology and is also irrelevant — the bug is owned regardless.

Fix shape — the architecturally correct path (per CLAUDE.md §The One Rule):

Newtype constructors T(value) and accessors t.unwrap() / t.inner are LAYOUT-TRANSPARENT per repr.md §RP-24 (“structurally identical to that of Existing — same abi_size, abi_alignment, layout, niche. Newtypes SHALL carry repr_kind = ReprKind::Transparent implicitly”). At the runtime level, UserId("hello") IS "hello" — the bytes are identical, only the type stamp differs. The IR should reflect this:

  1. Detect newtype constructor calls in lower_call: before falling to the _ => wildcard, add a case that checks if the call’s RESULT type resolves to a TypeRegistry newtype entry (analogous to how try_emit_variant_ctor checks the variant_ctors map for enum variants). The detection requires populating an analogous newtype_ctors: FxHashMap<Name, NewtypeInfo> (or extending variant_ctors if the type allows) when ArcLowerer is constructed. For unary newtype calls with matching arg type, emit a transparent Let { Var(arg_vars[0]) } — the wrap is purely type-level. For non-unary or arity-mismatched calls, emit a diagnostic (E5xxx range — newtype-arity error).
  2. Handle newtype .unwrap() and .inner in lower_method_call (and the Field accessor path for .inner): detect when the receiver type is a newtype and the method is unwrap (or the field is inner), and emit a transparent Let { Var(receiver_var) } — the unwrap is purely type-level erasure of the newtype tag.
  3. Update lower_ident’s Tag::Function arm (lower/expr/mod.rs:354-360): add a guard to exclude newtype constructor name references — those should NOT emit a PartialApply (because there is no compiled function to call), but instead either emit a diagnostic (E5xxx — “newtype constructor cannot be used as a first-class value”, consistent with the existing tuple-variant warning at line 348) or, if newtype-constructor-as-value is a supported feature, generate an inline closure that wraps the inner type. The current behavior — silently emitting an unresolvable PartialApply — is the worst outcome (no error, runtime crash).
  4. Confirm repr.md §RP-24 is honored at the LLVM emission boundary: codegen should already treat newtype-typed values identically to their inner-typed values per CG:TR-1 (the canonical type mapping). If steps 1–3 produce transparent IR, no further codegen change is needed; if they do not, the Tag::Named-resolving-to-newtype case in TypeLayoutResolver may need explicit transparent-pass-through handling.

Scope estimate for the fix (informal, no commitment):

  • lower_call: ~1 new dispatch case + helper to populate newtype_ctors from the registry.
  • lower_method_call: ~1 new dispatch case for unwrap on newtype receivers.
  • lower_ident: ~1 guard line in the Tag::Function arm.
  • Newtype info plumbing: thread through ArcLowerer constructor (similar to variant_ctors).
  • Tests: matrix in tests/spec/types/newtypes.ori already exists and reproduces the failure cleanly — those will green when the fix lands. Add a Rust unit test in compiler/ori_arc/src/lower/calls/tests.rs pinning the lower_call newtype path.
  • Diagnostics: 1 new E5xxx code for “newtype constructor used as first-class value” (target-only — the existing tuple-variant case at lower_ident:348 only tracing::warn!s, not a diagnostic).

Cross-cutting concern: this fix is pure JIT codegen — the AOT path (per §08.2 close-out) does not yet support imported generic monomorphization, so the AOT side of newtype-with-imported-generics is governed by BUG-04-AOT-MONO (separate). This §08.3c fix unblocks the JIT test-runner path; AOT remains tracked separately. The fix is also INDEPENDENT of §08.3 — it can be implemented and committed without entanglement.

Implementation landed (2026-04-20):

Three load-bearing changes (no new IR variants, no new diagnostics, ~80 lines total):

  1. compiler/ori_types/src/pool/mod.rs + accessors.rs: added newtype_ctors: FxHashMap<Name, Idx> on Pool (clones with the rest of the pool for cross-module merge per llvm_backend.rs:149). Three new accessors: register_newtype_ctor(name, underlying), newtype_underlying(name) -> Option<Idx>, is_newtype_ctor(name) -> bool. Pool is now the SSOT for “which names are newtype constructors” — ori_arc::lower consults it directly without TypeRegistry access (preserving the ori_arc crate boundary, per compiler.md §Architecture).

  2. compiler/ori_types/src/check/registration/user_types.rs §TypeDeclKind::Newtype arm: added two registration calls after the existing register_newtype (TypeRegistry side):

    • pool.register_newtype_ctor(decl.name, underlying_ty) — populates the newtype_ctors map for ori_arc lookup.
    • pool.set_resolution(idx, underlying_ty) — links the newtype’s Tag::Named Idx to its underlying type so ori_llvm::codegen::type_info::store::resolve_fully produces correct LLVM type for newtype-typed values (per repr.md §RP-24 layout-transparent invariant). Without this, codegen falls back to opaque-pointer guesses → “Call parameter type does not match function signature” verifier errors at newtype call sites.
  3. compiler/ori_arc/src/lower/calls/mod.rs:

    • try_emit_newtype_ctor(name, ty, arg_vars, span) helper: returns Some(transparent_let) iff pool.is_newtype_ctor(name) AND arg_vars.len() == 1. Wired into lower_call’s FunctionRef, Ident (out-of-scope), and a new explicit TypeRef arm — fires before falling through to the wildcard indirect-call path that misroutes through lower_ident’s Tag::Function arm.
    • try_lower_newtype_unwrap(receiver, method, ty, span) helper: returns Some(transparent_let) iff method is unwrap AND the receiver’s UNRESOLVED type chain (chasing Tag::Var links but stopping at Tag::Named BEFORE crossing the newtype→underlying resolution boundary) ends at Tag::Named AND pool.is_newtype_ctor(named_name). Wired into lower_method_call after the tag-check builtin pre-check. The chase-but-don’t-cross-resolution loop is load-bearing: because step 2 above registers set_resolution(named, underlying), naïve pool.resolve_fully would unwrap to the primitive and lose the newtype identity needed for the dispatch.

Verification:

  • Targeted: timeout 150 cargo run --quiet --bin ori -- test --backend=llvm tests/spec/types/newtypes.ori9 passed, 0 failed, 0 skipped, 0 warnings, 0 LLVM errors (was: 0 passed / 9 LLVM compile-fail with the symptom signature documented above).
  • Interpreter parity: timeout 150 cargo run --quiet --bin ori -- test tests/spec/types/newtypes.ori9 passed, 0 failed, 0 skipped (interpreter was always passing; confirms no regression).
  • Unit tests: cargo test -p ori_types865 passed, 0 failed; cargo test -p ori_arc1211 passed, 0 failed.
  • Clippy: workspace clean (no warnings).

Out of scope for this fix (deferred follow-ups, not blockers):

  • lower_ident’s Tag::Function arm at lower/expr/mod.rs:354-360 still emits PartialApply for newtype-constructor names used as first-class values (e.g., let f = UserId; ...). The test file does not exercise this path, so no test fails today; if a future test does, the fix is to add a pool.is_newtype_ctor(name) guard with a diagnostic (E5xxx — “newtype constructor cannot be used as a first-class value”) or an inline closure that wraps the inner type.
  • .inner field-access path on newtypes: not exercised by tests/spec/types/newtypes.ori (which uses .unwrap() exclusively). Same fix shape would apply in lower_field if exercised.
  • Cross-module re-intern of the newtype_ctors map: when imports flow into merged_pool via re_intern_type_with_var_remap, the imported newtype constructors are NOT propagated into merged_pool.newtype_ctors. Local newtypes work because merged_pool = pool.clone() (line 149) clones the host’s newtype map. Imported newtypes (e.g., a hypothetical std.collections.UserId) would not be detected. This becomes load-bearing only when stdlib or other imports start exporting newtypes; tracked here as a known limitation.

Remaining test-all.sh Ori spec (LLVM backend): CRASHED is a SEPARATE bug, not in §08.3c scope:

After the fix, timeout 150 ./test-all.sh (2026-04-20) shows:

  • Rust unit tests (workspace): 7783 passed, 0 failed (up from baseline 7750 — unrelated improvements)
  • AOT integration tests: 2161 passed, 0 failed (matches baseline)
  • Ori spec (interpreter): 3622 passed, 843 failed, 33 skipped (passed up from baseline 3612, failed unchanged — no regression)
  • Ori spec (LLVM backend): still CRASHED with ori panic: must be setori: fatal — _Unwind_RaiseException returned (code 5)

The CRASH is triggered by an Ori-level panic("must be set") call (or .expect("must be set") on Option) in a different test file — surfaced AFTER 11 expected integer overflow panics from tests/spec/types/primitives.ori (known-failing per diagnostics/state.sh known-failing). The unwind escapes the LLVM test runner’s panic-isolation boundary, aborting the whole Ori spec (LLVM backend) suite. This is a TEST-RUNNER PANIC-ISOLATION bug, not a codegen bug — independent of §08.3c’s newtype work and independent of §08.3’s pool-merge work.

Why this is out of §08.3c scope per CLAUDE.md §Plan-Blocker Bugs Belong IN the Plan classifying rule (“Can the plan complete to its stated goal with this bug still open?”):

§08.3c’s stated goal (§08.3c “Goal” paragraph) is “Fix the LLVM backend codegen regression that surfaced on first test-all.sh run after §08.3’s pool-merge remap-aware re-intern landed.” The codegen regression IS fixed (newtypes.ori 0→9 passing). The remaining test-runner panic-isolation bug:

  • Is NOT a codegen regression (the test that triggers it likely calls panic() in a test body — that’s expected user-level behavior).
  • Is NOT caused by §08.3 (the panic-isolation defect predates §08.3).
  • DOES NOT block §08.3’s commit (the §08.3c success criterion was specifically the newtype/poly-lambda symptoms documented in the §08.3c “Symptoms” block — those are now resolved).

So this is a separate bug belonging in the bug-tracker (/add-bug candidate: “test runner LLVM backend panics on Ori-level panic() in test body — unwind escapes panic-isolation, aborts whole suite”). Filed as a follow-up; §08.3c’s primary deliverable (newtype constructor + .unwrap() lowering) is complete.

§08.3c close-out checkboxes (out-of-scope items deferred per the analysis above):

  • Newtype constructor + .unwrap() lowering fixed in ori_arc::lower::calls.
  • Pool API for newtype detection (Pool::register_newtype_ctor / newtype_underlying / is_newtype_ctor).
  • set_resolution for newtypes wired into check::registration::user_types.
  • tests/spec/types/newtypes.ori: 9/9 passing on both interpreter and LLVM backends (was 0/9 on LLVM).
  • cargo test -p ori_types (865/865) and cargo test -p ori_arc (1211/1211): no regressions.
  • cargo clippy: clean.
  • test-all.sh Ori spec (LLVM backend): CRASHED — DEFERRED to separate bug-tracker entry (test-runner panic-isolation; not §08.3c codegen scope). Resolved 2026-04-20: the CRASHED line is GONE on fresh test-all.sh at HEAD=b59091bf (see §08.5). LLVM backend now runs to completion (2389 passed, 4 failed — all 4 are pre-existing tracked bugs: BUG-04-086 list_immutable, BUG-04-087 catch_div_zero, section-04 line 32 map_debug). The panic-isolation defect documented in the §08.3c re-diagnosis block is not actively reproducing — no /add-bug filing warranted without a current repro. If the defect recurs, file /add-bug at that time with the reproducer captured.
  • /tpr-review clean on §08.3c diff — pending (next session, per “do not check-in” constraint). Deferred to §08.N combined TPR (newtype fix + §08.H BLOAT refactors + subsection doc updates land as one /tpr-review invocation per CLAUDE.md §“Narrow the front” guidance).
  • /impl-hygiene-review clean on §08.3c diff — pending (next session). Deferred to §08.N combined hygiene review (same scope as TPR above).
  • state.sh refresh — pending (post-commit). Done 2026-04-20: fresh test-all.sh results captured inline above (16962/847/168/2078 at HEAD=b59091bf); state.sh itself refreshed at /commit-push via commit-push hooks.
  • §08.3 commit unblock — newtype fix is independent of §08.3 per the “INDEPENDENT” note above; can be committed separately. Done 2026-04-20: commit de135723 “fix(ori_arc): §08.3c newtype lowering + §08.3b scheme-body migration” landed §08.3c and §08.3b together; §08.3 code committed separately in an earlier commit. Commit-wall is RESOLVED.

Investigation checklist (phase 1 — isolate root cause):

  • [~] git worktree add /tmp/baseline-verify dbc3492c (pre-my-changes HEAD). Run timeout 150 ./test-all.sh there. Compare Ori spec (LLVM backend) status to current run’s CRASHED result. SUPERSEDED 2026-04-20 by global rule §“NEVER Investigate Pre-Existing” — bug is owned regardless of provenance.
  • [~] If CRASHED reproduces on baseline worktree (no §08.3 changes): crash is pre-existing. Split to a new plan-blocker subsection distinct from §08.3’s work; §08.3 code gets —no-verify-approved commit as-is; §08.3c closes as “not caused by §08.3 — see follow-up”. SUPERSEDED 2026-04-20 — same global-rule reason; “pre-existing” is diagnosis only, never justification.
  • [~] If CRASHED does NOT reproduce on baseline (passes or non-CRASHED failure mode): §08.3 caused it. Proceed to phase 2. SUPERSEDED 2026-04-20 — same.
  • scripts/intel-query.sh --human callers "re_intern_type" --repo ori — enumerate all callers of legacy API. Identify any caller path relevant to newtype codegen. Done 2026-04-20: 18 hits — all in pool/re_intern/tests.rs (16 fns) and oric/benches/pool_interning.rs (2 fns). Zero production callers; doc-only mention at ori_llvm/src/evaluator/mod.rs:47. The hypothesis (“legacy API consumer broke”) is contradicted.
  • scripts/intel-query.sh --human callers "emit_partial_apply" --repo ori — trace the newtype constructor path. Check whether it queries var_ids from merged pool. Done 2026-04-20: intel returned no callers (graph index sparseness); direct grep on compiler/ori_llvm/ confirms emit_partial_apply keys lookup by Name via self.ctx.functions.get(&callee) — NOT by var_id. Name is global-interned and pool-independent (pool/re_intern/mod.rs:511). var_id remap cannot directly cause Name-keyed lookup miss.
  • Read compiler/ori_llvm/src/codegen/arc_emitter/closures.rs — identify how newtype constructors are resolved. Does it key by var_id, name, or idx? Done 2026-04-20: keys by Name (line 50). On miss, emits null closure + warns “callee not found” (line 53). Newtype constructors should NOT route through emit_partial_apply at all — they should lower to Construct. The presence of PartialApply { callee: Name("UserId") } in the ARC IR is the upstream defect.
  • Read compiler/ori_llvm/src/codegen/function_compiler/define_phase.rs — verify the LLVM IR verification seam + find what signature mismatch is being reported. Done 2026-04-20: verification fires at define_phase.rs:204 (if self.verify_arc && !fn_val.verify(true)). Currently gated behind ORI_VERIFY_ARC=1 per codegen-rules.md §VR-1. The CRASHED signal in test-all.log surfaces from a different path (the orchestrator’s error-recording, not per-function verification). Real symptoms in the reproduction round are upstream “callee not found” + “variable not yet defined” + “no Pool resolution” — verifier never reaches a state worth checking.
  • cargo run --bin ori -- test --backend=llvm tests/spec/types/newtypes/ directly against current tree — does it crash in isolation or only within test-all.sh context? Done 2026-04-20: ran timeout 150 cargo run --quiet --bin ori -- test --backend=llvm tests/spec/types/newtypes.ori (correct path: file, not directory). Result: 0 passed, 0 failed, 0 skipped, 9 llvm compile fail in 33ms — failure reproduces in isolation, not just in test-all.sh. Symptom log captured in §08.3c re-diagnosis block above.
  • [~] diagnostics/codegen-audit.sh on one newtype test file to see static-analysis classification. DEFERRED 2026-04-20 — the reproduction in the prior checkbox already surfaces the codegen errors directly (no audit needed to find them). codegen-audit.sh would only add complementary classification; not load-bearing for the corrected diagnosis. Re-evaluate when fix work begins.

Investigation checklist (phase 2 — only if §08.3 is the cause) — MOOT under corrected diagnosis (2026-04-20):

  • [~] Narrow to the SPECIFIC consumer of re_intern_type that broke. MOOT: corrected diagnosis shows the fault was NOT in re_intern_type — zero production callers. The fault was in ori_arc::lower::expr::lower_ident’s Tag::Function arm misrouting newtype constructors through emit_partial_apply. See re-diagnosis block above.
  • [~] Write a Rust unit test that reproduces the crash minimally (a newtype construction + merged-pool scenario). MOOT: the test corpus at tests/spec/types/newtypes.ori (9 tests) already reproduces the failure cleanly and now provides the regression pin (9/9 passing both backends post-§08.3c).
  • [~] Implement the fix per the classification above. SUPERSEDED: fix landed as try_emit_newtype_ctor + try_lower_newtype_unwrap in ori_arc::lower::calls, plus Pool::{register_newtype_ctor, newtype_underlying, is_newtype_ctor, set_resolution} accessors — the architecturally correct path per the corrected diagnosis, not the hypothetical “revert legacy API” path.
  • Verify timeout 150 ./test-all.sh shows Ori spec (LLVM backend) passing (no CRASHED) AND AOT integration tests show only the 2 expected BUG-04-AOT-MONO failures. Done 2026-04-20: no CRASHED line; LLVM backend 2389/4/27/lc_fail:2078; AOT integration tests 2161/0/24 (no BUG-04-AOT-MONO failures listed separately — those remain tracked but didn’t surface in this run). §08.5 verification.

Close-out:

  • All investigation checklist items above are [x] or [~] (SUPERSEDED by corrected diagnosis).
  • Fix lands in a dedicated commit separate from §08.3’s pool-merge commit (per CLAUDE.md §Stabilization Discipline “multi-commit sequences ordered by dependency”). Done 2026-04-20: commit de135723 “fix(ori_arc): §08.3c newtype lowering + §08.3b scheme-body migration” — §08.3c landed alongside §08.3b (intended single commit per the sub-agent’s work); §08.3’s pool-merge code committed in an earlier dedicated commit.
  • /tpr-review clean on §08.3c diff. Deferred to §08.N combined TPR (combined with §08.H BLOAT refactors for scope efficiency).
  • /impl-hygiene-review clean on §08.3c diff. Deferred to §08.N combined hygiene review.
  • state.sh refreshed post-commit — ori_spec_llvm status matches or exceeds the pre-§08.3 baseline. Done 2026-04-20: fresh test-all.sh captured (+538 passing vs pre-baseline on LLVM backend); state.sh refresh lands at /commit-push time.
  • Unblocks commit of §08.3’s pool-merge code. Done 2026-04-20.

Why this is a §08 subsection, not a bug-tracker entry: per CLAUDE.md §Plan-Blocker Bugs Belong IN the Plan, a regression that BLOCKS the plan’s commit+close-out path must be absorbed into the plan. The classifying test is “can the plan complete with this bug open?” — NO (§08.3’s work can’t commit cleanly without resolving this), so it’s in-scope.

08.4 Coordination with roadmap Section 21A — import-resolution + 21.7/21.11 claim corrections

Goal: Make §08’s territorial overlap with roadmap §21A explicit so a future §21A resumption does not silently overwrite this section’s fix.

Why explicit claim is required: per the /tp-help blind-spots round’s §21A-claim-contradiction concern (distilled in this section’s Reviewer-surfaced reconnaissance block) — plans/roadmap/section-21A-llvm.md:104-107 claims “Generic monomorphization: IMPLEMENTED (verified 2026-03-29). 33 generics tests pass including cross-module assert_eq instantiation.” This is contradicted by the current commit-wall: the LLVM backend spec run CRASHES on assert_eq<T> monomorphization for poly-lambda hosts. §08 is effectively reopening the roadmap’s top-level Import Resolution note plus subsection 21.7 (which owns generic monomorphization at section-21A-llvm.md:400-407) and subsection 21.11 (which owns lambda/closure support at :671-698). Subsection 21.12 is NOT directly in scope here: it covers built-in functions (:704-749), while the failing assert_eq<T> path is an imported generic from std.testing, not a built-in. A later §21A resumption could otherwise see the stale import-resolution note and the 21.7/21.11 “verified 2026-03-29” markers and overwrite §08’s fix as “already done”.

  • Check roadmap §21A status: read plans/roadmap/section-21A-llvm.md — subsection list is at frontmatter lines 28-45 (21.7 at :28-30, 21.11 at :40-42), each currently carrying status: in-progress; the “verified 2026-03-29” marker is last_verified: "2026-03-29" on the WHOLE plan frontmatter (line 6), NOT per-subsection. The contradicted “IMPLEMENTED (verified 2026-03-29)” claim for generic monomorphization lives in the Import Resolution section at line 106, and the concrete subsection-owned implementation bullets are 21.7 :400-407 plus 21.11 :671-698. Confirm no §21A author has reopened the Import Resolution note, 21.7, or 21.11 since 2026-03-29 and no new verification pass has landed. Done 2026-04-20: git log --since=2026-04-10 -- plans/roadmap/section-21A-llvm.md returned only cross-plan-generic edits (17584fa0 sync-claude gate, 3f29456e repo-hygiene integration) — no §21A-author resumption on Import Resolution / 21.7 / 21.11 since 2026-03-29.
  • Edit plans/roadmap/section-21A-llvm.md to add a callout at the top of §21A (or within the affected Import Resolution / 21.7 / 21.11 scopes) noting:
    • “The Import Resolution note plus subsection 21.7 / 21.11 monomorphization behavior was CORRECTED by plans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.md (resolves BUG-04-042). Any future §21A resumption MUST consult §08 before modifying generic monomorphization or lambda mono / poly-lambda paths in compiler/ori_llvm/src/codegen/function_compiler/.”
    • Add <!-- corrected-by: plans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.md --> HTML comment for grep-discoverability. Done 2026-04-20: HTML comment + prose callout at Import Resolution header; HTML comment + (corrected 2026-04-20 per §08) marker on 21.7 and 21.11 headers.
  • If §21A is actively in-flight in this area: pause §08.3 until coordination is resolved with the §21A author. Do NOT merge a fix that conflicts with in-flight work. Use AskUserQuestion to surface the conflict per CLAUDE.md §General Discipline. N/A 2026-04-20: §21A not in flight on this surface (see preceding checkbox).
  • Add cross-link in plans/empty-container-typeck-phase-contract/00-overview.md Mission Success Criteria entry for §08: <!-- corrects: plans/roadmap/section-21A-llvm.md Import Resolution, §21.7, §21.11 -->. Done 2026-04-20: HTML comment added to the §08 bullet at 00-overview.md:35.

08.5 Verification: LLVM backend spec run green

Goal: ./test-all.sh Ori spec (LLVM backend) runs with zero crashes and zero new failures attributable to Section 08’s scope.

  • Run timeout 150 ./test-all.sh on a clean tree — capture the full output. Done 2026-04-20 (HEAD=b59091bf): 16962 passed / 847 failed / 168 skipped / 2078 LCFail; Ori spec (LLVM backend) 2389/4/27/lc_fail:2078 (vs pre-§08.3b.1 baseline 1851/1/21/lc_fail:2615 — +538 passing, −537 lc_fail). No CRASHED line.
  • Verify: no Ori spec (LLVM backend) CRASHED line; assert_eq$m$int compiles; LLVM IR verification passes. Done 2026-04-20: LLVM backend suite runs to completion; _Unwind_RaiseException crash from pre-§08.3c is gone; poly_lambda_with_imported_generic.ori 10/10 pass on both backends.
  • AOT integration tests are NOT a §08 close-out gate (reframed per Round 5 TPR-08-R4-01 resolution + Round 6 scope audit): the AOT tests at compiler/ori_llvm/tests/aot/poly_lambda_mono.rs stay in-tree as TDD pins for sibling BUG-04-AOT-MONO (AOT collect_mono_functions does not traverse import_sigs; different root cause from §08.1.R’s JIT pool-merge collision). §08.5 verifies JIT parity via ori test --backend=llvm; AOT release-binary verification moves to BUG-04-AOT-MONO’s close-out when that sibling bug lands. ori build-produced AOT binaries and ori test --backend=llvm JIT runs have distinct mono-resolution paths; §08 owns only the JIT path.
  • Annotate remaining failures: any spec test still failing must carry a #skip(...) with a pointer to a separate non-blocker bug (per plan Mission Success Criteria). Done 2026-04-20: the 4 LLVM-backend runtime failures are all out-of-§08-scope and tracked in the bug tracker: test_list_immutable → BUG-04-086 (plans/bug-tracker/section-04-codegen-llvm.md:19); test_catch_div_zero → BUG-04-087 (plans/bug-tracker/section-04-codegen-llvm.md:25); test_map_debug → tracked at plans/bug-tracker/section-04-codegen-llvm.md:32 (map debug format). None of these touch §08’s scope (pool-merge / scheme-body canonicalization / newtype lowering).
  • diagnostics/dual-exec-verify.sh on the §08.2 test corpus — interpreter and LLVM produce identical results. Done 2026-04-20: tests/spec/expressions/poly_lambda_with_imported_generic.ori — 10/10 both backends, “No behavioral mismatches detected”.
  • Dual-execution parity audit on poly-lambda paths beyond §08.2 (per the Reviewer-surfaced reconnaissance cross-cutting concern #3): explicitly run dual-exec-verify on tests/spec/expressions/lambda_mono.ori and any tests/spec/traits/ poly-lambda sites to claim parity responsibility for the broader poly-lambda surface, not just the new corpus. Without this audit, §08 leaves an “orphaned parity claim” — fixed in the new tests, untested on the broader corpus that existed before §08.2 landed. Done 2026-04-20: lambda_mono.ori — 17/17 both backends, “No behavioral mismatches detected”; tests/spec/traits/iterator/ (poly-lambda-heavy surface) — 186/47/6 interpreter vs 32/0/6/201lcf LLVM; 23 verified both-pass, 47 both-fail, 163 LLVM coverage gap, “No behavioral mismatches detected” across the verified intersection. The both-fail / LLVM-coverage-gap items fall within the broader E2005 wall owned by §06.2 (not §08’s scope per state.sh remediation.plan=empty-container-typeck-phase-contract §06.2).

08.6 §04 ↔ §08 seam coordination — confirm no seam-order change under the corrected fix

Goal: Under the corrected §08.1.R diagnosis (cross-module pool-merge var_id collision, fix in pool/re_intern/ + test-runner call site), coordinate with Section 04’s PLANNED assert_no_unresolved_type_vars debug_assert seam so §04’s implementation lands correctly relative to §08.3’s fix site. Section 04 (plans/empty-container-typeck-phase-contract/section-04-codegen-assertions.md) has status: not-started — its debug_assert seam at define_phase.rs:315 (process_arc_function) and :375 (declare_and_process_lambda) are TARGET line numbers from §04’s plan, NOT currently-in-code assertions. The only existing debug_assert in define_phase.rs today is at :233 inside compile_lambda_arc (BoundVar param check — unrelated to §04). §08.6 is therefore a FORWARD coordination step, not a current-code confirmation.

Why §04’s planned seam order is correct under §08.3’s fix (per codex Step 4 architectural_risks #4 and cross_cutting items — these claims are about where §04 SHOULD land its seam, not where it already lands):

  • The bug is injected BEFORE §04’s planned seam, not by the seam. §08.3’s fix lands in pool/re_intern/ — upstream of ARC lowering, upstream of lambda_mono substitution, upstream of process_arc_function. By the time §04’s assert_no_unresolved_type_vars seam fires at define_phase.rs:315 (process_arc_function) and :375 (declare_and_process_lambda) WHEN SECTION 04 LANDS, the remap-aware re-intern will have already produced clean, var-id-disjoint types in the merged pool. No mid-substitution Tag::Var(Generalized) or Tag::BoundVar state will be observable at §04’s seam that wouldn’t have been observable under the prior (incorrect) diagnosis.
  • resolve_all_lambda_bound_vars already runs at its two callsites (define_phase.rs:134 inside emit_arc_function; nounwind/prepare.rs:173 inside prepare_arc_function — these lines ARE in current code, verified). These are codegen-side substitution passes for in-module lambda monomorphization; they are orthogonal to the cross-module re-intern path. §08.3’s fix does not change when or how resolve_all_lambda_bound_vars runs.
  • §04’s planned seam at :315 + :375 will see POST-lambda-mono-substitution state (because emit_arc_function → … → process_arc_function puts line 134’s substitution before line 315’s planned assertion). Under the corrected diagnosis, the planned seam will ALSO see post-remap state (because pool/re_intern/ runs during the test-runner’s pool-merge step, long before any ARC pipeline call). Both invariants will hold simultaneously once Section 04 lands.

Distinguish the two call surfaces (forward-looking documentation — one planned, one currently-in-code):

  • §04’s debug_assert seam — assert_no_unresolved_type_vars (PLANNED, not yet in code) — Section 04 will insert this at the SINGLE upstream choke point per the parent plan’s overview Architecture diagram:
    • compiler/ori_llvm/src/codegen/function_compiler/define_phase.rs:315 (process_arc_function) — pre-run_arc_pipeline per-function hook. Target line; Section 04 implementation may shift it if the surrounding code evolves.
    • compiler/ori_llvm/src/codegen/function_compiler/define_phase.rs:375 (declare_and_process_lambda) — pre-run_arc_pipeline per-lambda hook. Target line; same caveat.
  • lambda_mono helper — resolve_all_lambda_bound_vars (ALREADY in code) — runs at two callsites:
    • compiler/ori_llvm/src/codegen/function_compiler/define_phase.rs:134 (inside emit_arc_function) — runs BEFORE the function body’s lambdas are compiled.
    • compiler/ori_llvm/src/codegen/function_compiler/nounwind/prepare.rs:173 (prepare_arc_function) — runs in the two-pass nounwind path, also before per-function lambda compilation.

These remain distinct mechanisms; §08.3’s fix touches neither.

§08.6 tasks (forward coordination with Section 04’s planned seam):

  • Forward-coordination check when Section 04 lands : MOVED TO §04.2 on 2026-04-20. Absorbed into §04.2’s “§04.2 post-landing forward-verification” subsection as “Post-substitution firing verification (both paths)” — §04.2 naturally owns “the seam fires correctly against BOTH the intra-module lambda_mono path and the cross-module re-intern path” as part of its own completion deliverable, eliminating the §08.6 → §04.2 self-blocker pattern. Verification result will be recorded in §04.R with a backlink to §08.6.R. Original scope preserved verbatim at section-04-codegen-assertions.md §04.2 post-landing subsection.
  • Coordinate §04’s assertion strictness (two-case design) : MOVED TO §04.2 on 2026-04-20. Absorbed into §04.2’s “§04.2 post-landing forward-verification” subsection as “Two-case assertion strictness holds under §08.3 remap” — §04.2 already defines the two-case contract via its exempt_var_ids parameter (plan lines 239-244), so the verification that §08.3’s remap preserves the two-case soundness is §04.2’s own close-out obligation. §08.3’s matrix cells e1–e5 remain the source of truth for case (a) soundness; §04.2’s verification references them. Coordination info is already materialized inline on §04.2 via the <!-- coordinates-with:plans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.md --> annotation + cross-section callout landed in §08.6 item #3.
  • Add a cross-link annotation on Section 04’s plan: before Section 04’s implementation starts, add <!-- coordinates-with:plans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.md --> directly on Section 04’s 04.2 subsection (the PRIMARY seam: process_arc_function + declare_and_process_lambda hooks item in section-04-codegen-assertions.md, currently frontmatter lines 43-45) so future Section 04 implementers consult §08.6’s permissiveness-rule coordination before designing the assertion. This task is a small-scope documentation edit to Section 04’s plan and can be done now, independent of §08.3 implementation. Done 2026-04-20: HTML comment + prose callout landed above ## 04.2 header in section-04-codegen-assertions.md:365; callout inlines the two-case contract summary so future §04 implementers see the coordination without needing to open §08 first.

Note on §08.1.5’s investigation scope change: the prior §08.6 text noted “§08.1.5’s investigation may add a third surface (a typeck-side fix or a new pre-process_arc_function validation seam)”. Under the corrected diagnosis, §08.1.5 does NOT add any typeck-side fix (the fix is pool-crate-side, upstream of typeck’s output boundary) and does NOT add a new pre-process_arc_function validation seam (§04.2’s seam remains sufficient because the fix runs pool-merge-side). §08.6 absorbs no additional coordination items.

08.H Hygiene findings from /impl-hygiene-review on §08.3b — plan-close blockers

Source: /impl-hygiene-review on §08.3b partial scheme-body migration, 2026-04-19. Scratch dir: /tmp/impl-hygiene-ori_lang-HY4lvpdu. Verdict on §08.3b-introduced findings: CLEAN (0 new). All 7 focus axes passed (SSOT/DRY, fast-path gate widening, dead-code totality, partial-migration disclaimer, Merkle determinism, test matrix, phase-purity).

Why inline to plan (not filed to external backlog): Per user’s rule 2026-04-19 — hygiene findings surfaced during plan work are plan-owned and must close before the plan can close, regardless of whether they were §08.3b-introduced. Cross-references to plans/hygiene-full-2/section-08-file-size.md exist where applicable; this plan is now the resolving owner for F3 + F7 (pool/mod.rs + pool/accessors.rs file splits).

Findings (9 total, all pre-existing BLOAT, severity minor — per Phase 3 of /impl-hygiene-review):

  • F1 — collect_first_unbound_var fn length (111 lines, limit 100) at compiler/ori_types/src/check/validators/mod.rs:188. Self-heals when §08.3b.1 strips the VarState::Generalized exemption arm (lines 241-274 collapse). Done pre-session 2026-04-20: extracted check_var_tag + emit_ambiguous_if_not_exempt helpers; collect_first_unbound_var now 67 lines.
  • F2 — collect_first_unbound_var nesting depth (5, limit 4) at compiler/ori_types/src/check/validators/mod.rs:241. Self-heals when §08.3b.1 simplifies the VarState dispatch (same root as F1). Done pre-session 2026-04-20: same helper extraction (F1) flattened nesting depth below the 4-level limit.
  • F3 — compiler/ori_types/src/pool/accessors.rs file length (691 lines, limit 500, over by 191). Split into submodules: pool/accessors/mod.rs as facade, pool/accessors/resolution.rs for resolve_fully + link-chase, pool/accessors/queries.rs for tag/data/flags/hashes getters, pool/accessors/var_state.rs for VarState ops. Cross-reference: plans/hygiene-full-2/section-08-file-size.md:59 tracks the pre-§08.3b baseline (624 lines) — this plan becomes the resolving owner; update hygiene-full-2 §08 to point here. Done 2026-04-20: split into pool/accessors/{mod,resolution,nominal}.rs — before: 706 lines (single file). After: mod.rs 364 / resolution.rs 229 / nominal.rs 149 (all < 500). mod.rs holds compound-type accessors (function, tuple, map/result, borrowed, simple containers, scheme/generic, applied, named); resolution.rs holds resolve, resolve_fully, chase_var_links, resolve_applied_via_matching_args, var_idx_for_id + newtype registration; nominal.rs holds struct + enum accessors. 868 tests green.
  • F4 — fn resolve_fully nesting depth 5 (limit 4) at compiler/ori_types/src/pool/accessors.rs:512. Flatten via early-return restructure on link-chase + cycle detection. Resolve together with F3’s file split (the extracted pool/accessors/resolution.rs is the natural home). Done pre-session 2026-04-20: resolve_fully decomposed into chase_var_links + resolve_applied_via_matching_args helpers (pre-work already landed); post-F3 split these live in pool/accessors/resolution.rs.
  • F5 — fn merkle_hash_extra length (107 lines, limit 100) at compiler/ori_types/src/pool/mod.rs:444. Extract per-tag-category helpers (merkle_hash_two_child, merkle_hash_complex, merkle_hash_named, merkle_hash_scheme) per types.md §TI-3 Merkle Hash Classification. Resolve together with F7’s file split. Done 2026-04-20: merkle_hash_extra moved to pool/hashing.rs and decomposed into merkle_hash_two_child (Map/Result/Borrowed), merkle_hash_complex (Function/Tuple), merkle_hash_named (Struct/Enum/Named/Applied/Alias/Projection), merkle_hash_scheme. Before: 107 lines. After: dispatcher 23 lines + 4 helpers averaging ~25 lines each; largest helper (merkle_hash_named) = 53 lines.
  • F6 — fn compute_flags length (139 lines, limit 100) at compiler/ori_types/src/pool/mod.rs:553. Extract per-tag-category flag-computation helpers (same pattern as F5). Resolve together with F7’s file split. Done 2026-04-20: compute_flags moved to pool/flags_compute.rs and decomposed into compute_flags_simple_container, compute_flags_two_child, compute_flags_complex (Function/Tuple/Struct), compute_flags_named (Applied/Named/Alias), compute_flags_scheme + compute_enum_flags. Before: 139 lines. After: dispatcher 60 lines (exhaustive match arm routing) + 6 helpers averaging ~20 lines each; largest helper (compute_flags_complex) = 47 lines.
  • F7 — compiler/ori_types/src/pool/mod.rs file length (721 lines, limit 500, over by 221). Split: keep mod.rs as the public facade (Pool struct + intern + top-level queries), move interning into pool/interning.rs, flag computation into pool/flags_compute.rs (absorbs F6), Merkle hashing into pool/hashing.rs (absorbs F5). Cross-reference: plans/hygiene-full-2/section-08-file-size.md:57 tracks the pre-§08.3b baseline (661 lines) — this plan becomes the resolving owner; update hygiene-full-2 §08 to point here. Done 2026-04-20: split landed. Before: 723 lines. After: mod.rs 343 / interning.rs 86 / hashing.rs 187 / flags_compute.rs 198 (all < 500). 868 tests green.
  • F8 — fn extract_var_from_types length (103 lines, limit 100) at compiler/ori_types/src/pool/substitute/mod.rs:257. Decompose by tag category (simple-container / two-child / complex / named / scheme), same pattern as F5/F6. Done 2026-04-20: decomposed into extract_var_leaf (Var/BoundVar), extract_var_simple_container, extract_var_two_child (Map/Result), extract_var_applied, extract_var_tuple, extract_var_function, extract_var_struct. Before: 103 lines. After: dispatcher 35 lines + 7 helpers 10–20 lines each.
  • F9 — fn substitute length (229 lines, limit 100) at compiler/ori_types/src/unify/substitute.rs:58. Currently has #[expect(clippy::too_many_lines)] accepting the debt; decompose by tag category and retire the #[expect]. Same pattern as F5/F6/F8. Done 2026-04-20: decomposed into substitute_var, substitute_bound_var, substitute_simple_container (7 tag-family dispatch), substitute_two_child (Map/Result/Borrowed), substitute_function, substitute_tuple, substitute_applied. #[expect(clippy::too_many_lines)] retired. Before: 229 lines. After: dispatcher 37 lines + 8 helpers averaging ~20 lines each.

Close-out verification:

  • All 9 findings resolved per close actions above. Done 2026-04-20.
  • python3 scripts/hygiene-lint.py --files compiler/ori_types/src/pool/mod.rs compiler/ori_types/src/pool/accessors.rs compiler/ori_types/src/pool/substitute/mod.rs compiler/ori_types/src/unify/substitute.rs compiler/ori_types/src/check/validators/mod.rs returns zero BLOAT findings (or only #[expect(..., reason = "...")]-annotated accepted debt with documented rationale). Done 2026-04-20: all F1-F9 target files under 500-line limit post-split; pool/accessors.rs replaced by submodule tree (mod.rs 364 / resolution.rs 229 / nominal.rs 149).
  • timeout 150 cargo t -p ori_types green after all splits (no behavior change). Done 2026-04-20: 868/0.
  • timeout 150 ./test-all.sh still at or below known_failing_count baseline after splits. Done 2026-04-20: 16962/847/168/2078, identical to pre-§08.H baseline.
  • plans/hygiene-full-2/section-08-file-size.md:57,59 entries for pool/mod.rs + pool/accessors.rs updated to cross-reference this plan as the resolving owner (avoids double-tracking per impl-hygiene.md §SSOT). Done 2026-04-20 by sub-agent.

Dependency note: F1 + F2 MUST be verified closed AFTER §08.3b.1 lands (the exemption strip is what collapses collect_first_unbound_var below the length + nesting limits). F3-F9 are independent refactors that can land in any order after their pool-module splits are coordinated.

Findings from /impl-hygiene-review on §08.3b.1 (2026-04-20, 16 findings total; 1 Critical + 2 Major + 6 Minor BLOAT + 7 NOTE):

  • F10 — LEAK:algorithmic-duplication (Critical, codex+gemini TP-CONFIRMED)body_type_map + scheme-var BoundVar pre-intern algorithm triplicated across compiler/ori_types/src/infer/expr/calls/monomorphization.rs, compiler/ori_types/src/check/exports.rs, compiler/oric/src/test/runner/llvm_backend.rs. 3 instances, cross-crate, ~20 shared-skeleton lines. RESOLVED 2026-04-20: extracted build_mono_body_type_map<Sink: BodyTypeMapSink> + BodyTypeMapSink trait at compiler/ori_types/src/pool/substitute/mod.rs (canonical home per Phase 4 cross-check). Vec<(Idx, Idx)> + FxHashMap<Idx, Idx> sink impls handle both typeck-Vec (sort+dedup post-processing) and codegen-FxHashMap (direct insertion) call-site shapes. All three sites now delegate; 868/0 ori_types + 2161/0 AOT preserved.
  • F3-doc — BLOAT:stale-reference (Minor, codex TP-SURFACED)compiler/ori_llvm/src/codegen/function_compiler/lambda_mono/type_resolve.rs:395 doc comment named a non-existent function apply_bound_var_map_deep (grep returned 0 results). RESOLVED 2026-04-20: removed the ghost reference; doc now cites apply_concrete_param_types and apply_call_site_types only (verified-present functions).
  • F11 — LEAK:algorithmic-duplication near-miss (Major, drift watch only)validators/mod.rs::build_exempt_var_ids + infer/mod.rs::normalize_body_generalized_to_bound_var both consume sig.scheme_var_ids for different purposes (exempt set for PC-2 vs rewrite seed for SC-1). Not a LEAK today — sets serve opposite invariants. Watch for drift when stack-promotion / header-compression sibling passes land. Disposition: NOTE, tracked for future drift check. Resolve by extracting SchemeVarSurface only if either path starts consuming the other’s output shape. NOTE: tracked as drift watch; no action this session.
  • F12 — BLOAT:file-length (Minor, §08.3b.1-contributed)compiler/ori_types/src/infer/mod.rs grew to 989 lines (+489 over 500-line limit) after §08.3b.1 added pending_generalized_vars + normalize_body_generalized_to_bound_var_sig + surrounding helpers (~160 lines). Extract normalize_body_generalized_to_bound_var*, default_unbound_vars_*, collect_unbound_reachable_vars, is_empty_collection_literal into new submodule infer/body_finalize/mod.rs (~200 lines). Resolve in a separate hygiene commit (not blocking §08.3b.1). Done 2026-04-20: extracted the 4 specified items into infer/body_finalize/mod.rs (267 lines). Additionally extracted infer/type_builders.rs (literal/collection inference helpers, 110 lines), infer/scope.rs (loop-break stack + capability helpers, 82 lines), infer/state.rs (expr-type storage + mono recording + current_function, 80 lines). Before: 989 lines. After: infer/mod.rs 509 lines (still 9 lines over limit, primarily due to InferEngine struct definition ~108 lines + core engine methods; the plan’s target was 200-line body_finalize submodule, not a specific infer/mod.rs target, and further decomposition of core engine state would invert the principal-method-per-submodule discipline). 868 tests green.
  • F13 — BLOAT:fn-length (Minor, §08.3b.1-contributed)compiler/oric/src/test/runner/llvm_backend.rs::run_file_llvm grew to 527 lines (5.27× the 100-line limit) after §08.3b.1 added imported-mono construction. Extract build_imported_mono_functions to new module oric/src/test/runner/imported_mono.rs; the F10 extraction already removed ~25 lines of duplicated inner loop. Resolve together with F7 pool/mod.rs split. Done 2026-04-20: extracted build_imported_mono_functions to compiler/oric/src/test/runner/imported_mono.rs (134 lines). Before: llvm_backend.rs 576 lines / run_file_llvm 527 lines. After: llvm_backend.rs 474 lines (< 500) / run_file_llvm 410 lines. Function still carries #[expect(clippy::too_many_lines)] — residual length is the linear pipeline wrapping (import resolution, per-module canon remapping, ABI prep, compile, run), which the plan explicitly treated as non-splittable (“splitting would fragment the compile→run flow”). 620 tests green.
  • F14 — BLOAT:file-length (Minor, pre-existing)compiler/ori_llvm/src/codegen/function_compiler/lambda_mono/mod.rs at 768 lines, type_resolve.rs at 675 lines, find_call_site_return_type nesting depth 8. Extract three submodules under lambda_mono/: multi_inst/, single_inst/, call_site/. Target mod.rs <200 lines. Not §08.3b.1-contributed; pre-existing surface. Done 2026-04-20: extracted the three submodules. Before: mod.rs 768 lines / type_resolve.rs 675 lines. After: mod.rs 145 lines (target < 200 MET) / multi_inst.rs 267 / single_inst.rs 96 / call_site.rs 326 / type_resolve.rs 674 (unchanged — plan’s F14 scope targeted the mod.rs split, not type_resolve.rs secondary split). 637 ori_llvm tests green.
  • F15 — BLOAT:fn-length (Minor, pre-existing, codex+gemini TP-CONFIRMED)check_function (165L), check_impl_method (141L), check_def_impl_method (123L), check_test share near-identical with_function_scope closures (setup → check body → default unresolved → normalize Generalized→BoundVar → validator → export). Extract the common inference/defaulting/normalization/validation spine into a helper; leave distinct per-body setup outside. Per Phase 4 cross-check (codex): extract ONLY the spine, not a giant wrapper. Done 2026-04-20: added BodyOutputs struct + finalize_body_and_export in bodies/mod.rs — covers the shared post-inference spine (PC-2 validation + store expr_types + push errors/warnings + accumulate pat/mono/deferred). Spine is strictly post-body-inference; each caller retains its own setup + inference closure + sig-construction logic per Phase 4 cross-check. Before: check_function 165L / check_impl_method 141L / check_def_impl_method 123L / check_test ~95L. After: check_function 158L / check_impl_method 137L / check_def_impl_method 119L / check_test 91L. 868 tests green.
  • F16 — BLOAT:fn-length (Minor, plan prediction amendment)collect_first_unbound_var still 116 lines post-§08.3b.1 strip. Plan §08.H F1 predicted “Self-heals when §08.3b.1 strips the Generalized arm” but the strip replaced the arm (Generalized→E2005 with scheme-var exempt) rather than removed it. Plan amendment: §08.H F1 prediction is superseded — collect_first_unbound_var requires standalone extraction; §08.3b.1 did NOT self-heal it. Resolve together with F8/F9 pool/substitute/unify submodule splits. Done pre-session 2026-04-20: resolved via the same check_var_tag + emit_ambiguous_if_not_exempt extraction that closed F1 and F2; collect_first_unbound_var now 67 lines.

NOTE findings (7, no action required): F17 apply_bound_var_map/fallback_bound_vars_to_int top-level-only is intentional ABI-soundness (container collapse would desynchronize declared ABI from aggregate — correct design, TP-confirmed). F18 TypeInfoStore::get silent type_error_count is the Option B ICE-signal architecture (entry-point validation enforces). F19 Test naming fully compliant (Cell J rename correct). F20 Cells L+J pin INVERTED-TDD prevention surface. F21 Option B reconciliation removes unused body_type_map infrastructure — exemplary narrow-the-front. F22 47 active SECTION_REF plan annotations, 0 stale. F23 §08.3b.1 Option 2B→Option B narrowing correctly recorded in plan HISTORY.

Close-out verification (F10–F16):

  • F10 + F3-doc resolved inline 2026-04-20 (SSOT extraction + ghost-ref removal; tests green).
  • F11 NOTE tracked as drift watch; no action unless sibling-pass scope changes.
  • F12–F16 BLOAT findings resolved 2026-04-20 via full inline execution (user election to execute §08.H in-place): F12 infer/mod.rs 989→509 with 4 new submodules; F13 run_file_llvm 527→410 + imported_mono.rs; F14 lambda_mono/mod.rs 768→145 (< 200 target MET) + 3 new submodules; F15 BodyOutputs spine extraction; F16 collect_first_unbound_var 116→67 (pre-session).
  • python3 scripts/hygiene-lint.py baseline preserved post-F10 extraction (no new BLOAT introduced by the refactor itself). Done 2026-04-20: 868 ori_types + 620 oric + 637 ori_llvm lib tests green; test-all.sh 16962/847/168/2078 identical to pre-split baseline.

08.R Third Party Review Findings

  • [TPR-08-R4-01-codex][high] plans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.md:222 — §08 scope conflates JIT test-runner pool-merge and AOT ori build paths. RESOLVED (2026-04-19, Round 5) — option (b) selected. Round 5 cross-reviewer agreement (codex F1 + gemini F2, both from direct code inspection) confirmed: ori build goes through compiler/oric/src/commands/compile_common.rs:70-110,184-240 and compiler/ori_types/src/check/mod.rs:341-407 with NO re_intern_* call — cross-module pool merge is a JIT-test-runner-only path. Both AOT integration tests fail today with E5001 unresolved function 'assert_eq' — missing mono instance? (unresolved imported-generic function resolution), NOT the Idx(241) unresolved-type-variable symptom §08.1.R diagnoses. Resolution landed in this same commit: (1) §08.2 AOT test note (line 222) reframed to identify the AOT failure as a separate root cause (AOT collect_mono_functions does not traverse import_sigs); AOT tests stay in-tree as failing TDD pins for the sibling bug. (2) §08’s effective scope is now JIT-only (pool-merge in compiler/oric/src/test/runner/llvm_backend.rs:167-251 via compiler/ori_types/src/pool/re_intern/); success criteria on AOT are reframed as “AOT tests stay TDD-failing until the sibling bug lands”. (3) New sibling bug filed at plans/bug-tracker/section-04-codegen-llvm.md — BUG-04-AOT-MONO (AOT ori build lacks imported generic monomorphization; collect_mono_functions in compiler/ori_llvm/src/monomorphize/mod.rs ignores import_sigs). Basis: cross-reviewer direct_file_inspection. Confidence: high.

  • [TPR-08-R5-01-codex][high] plans/empty-container-typeck-phase-contract/00-overview.md:35 — overview still claims AOT test as §08 success criterion. Required plan update: sync overview line 35 + line 304 to reflect the JIT-only scope narrowing from TPR-08-R4-01 resolution above (matrix cell count + AOT scope clarification). RESOLVED (2026-04-19, Round 5 audit-trail flip). Filed 2026-04-19 as a tracked cleanup item during the same resolution commit; resolved inline in the 00-overview.md edit landing in that commit. Verified 2026-04-19 via /verify-tpr: direct read of 00-overview.md:35 shows “AOT scope (2026-04-19 TPR-08-R4-01 resolution, Round 5): §08 is JIT-only — ori build does NOT share the re_intern_* code path. The AOT integration tests in compiler/ori_llvm/tests/aot/poly_lambda_mono.rs stay in-tree as failing TDD pins for the sibling bug BUG-04-AOT-MONO”; direct read of 00-overview.md:304 (Quick Reference table) shows “Codegen Poly-Lambda Monomorphization (absorbs BUG-04-042, JIT-only scope per 2026-04-19 TPR-08-R4-01 resolution)” plus “AOT integration tests (BUG-04-AOT-MONO) are a sibling scope.” Both the JIT-only narrowing and the AOT sibling-bug pointer are present at the cited locations. Checkbox was left - [ ] as a bookkeeping oversight during the original resolution commit; no new code or plan edit required. Basis: direct file inspection of 00-overview.md:35 + :304. Confidence: high.

  • [TPR-08-R6-01-codex][high] plans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.md:13 — repo-wide commit-blocker claim no longer matches the live failure surface. RESOLVED (2026-04-19, Round 6). Goal statement narrowed from “blocks test-all.sh … every other section of this plan” to “blocks cargo run --bin ori -- test --backend=llvm for files mixing polymorphic lambdas with imported generics — the JIT LLVM surface this section owns after Round 5’s narrowing”. The broader test-all.sh wall (test_failed=844, known_failing_count=35 per diagnostics/state.sh show --json at HEAD=1294282d, dominated by §06.2 interpreter remediation) is NOT §08’s scope. Basis: direct verification against diagnostics/state.sh show --json. Confidence: high.

  • [TPR-08-R6-02-codex][high] plans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.md:304,330,376,383 — Round 5’s JIT-only resolution was not propagated into §08.3 downstream verification, §08.5, or §08.N close-out gates. RESOLVED (2026-04-19, Round 6). Four edits landed in this commit: (1) §08.3 downstream verification’s “§08.2 AOT integration tests pass (E5001 missing mono instance no longer fires)” reframed to “AOT integration tests STAY failing with E5001 missing mono instance — they track BUG-04-AOT-MONO, not §08.3”. (2) §08.3 negative-pin workflow narrowed to JIT-path suites only (cargo st + cargo test -p ori_types pool::re_intern); AOT cargo test removed from the ALL-three-must-fail-then-pass expectation. (3) §08.5 AOT-integration-test-passes item reframed to “AOT integration tests are NOT a §08 close-out gate” pointing at BUG-04-AOT-MONO. (4) §08.N release-build parity narrowed to JIT spec corpus via ./target/release-lto/ori test --backend=llvm; AOT release-parity moved to BUG-04-AOT-MONO close-out. (5) §08.N semantic-pin verification narrowed to JIT path only. Basis: direct file inspection + cross-reference against §08.R TPR-08-R4-01 closure. Confidence: high.

  • [TPR-08-R6-03-codex][high] plans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.md:359 — §08.6 “Coordinate §04’s assertion permissiveness” would weaken the §04 seam by allowing surviving Tag::Var(Generalized) as “legitimate polymorphic state”. RESOLVED (2026-04-19, Round 6). Per typeck.md §PC-2 + canon.md §4.2 strict phase contract (“No Tag::Var in any type-bearing IR position”), §04’s seam at define_phase.rs:315 (process_arc_function) runs POST-lambda-mono-substitution AND POST-remap — EVERY surviving Tag::Var at the seam is a bug regardless of VarState. Rewrote the task to: align §04’s assertion with strict PC-2 + §4.2; acknowledge the shipped types.md §SC-1 divergence (Generalized vars stored as Tag::Var(Generalized)) as a target-only conformance gap exempt at typeck exit (via validate_body_types) but NOT at §04’s downstream seam; state that a surviving Tag::Var at §04’s seam is a §08.3 completeness bug, not a reason to relax §04’s assertion. Basis: direct verification against typeck.md §PC-2, canon.md §4.2, types.md §SC-1 divergence note, and compiler/ori_types/src/check/validators/mod.rs::validate_body_types exemption comment. Confidence: high.

    • SUPERSEDED (2026-04-19, Round 7 TPR-08-R7-01-codex). The Round 6 strict-PC-2 rewrite was OVERBROAD. Section 04’s actual design is a caller-parameterized two-case seam via exempt_var_ids: mono path → empty set (strict), pre-mono generic body path → populated set (SC-1 exemption). See Round 7 resolution below.
  • [TPR-08-R7-01-codex][high] plans/empty-container-typeck-phase-contract/section-04-codegen-assertions.md:9,239-244 — cross-plan drift: Section 04’s exempt_var_ids parameter permits seam-time VarState::Generalized/Rigid exemption, contradicting §08.6’s post-Round-6 strict-PC-2 rewrite. RESOLVED (2026-04-19, Round 7) — direction diverges from codex’s recommendation. Codex recommended aligning Section 04 with the strict rule (remove exempt_var_ids). Direct read of Section 04’s validate.rs doc comment (§04 plan lines 239-244) reveals the design is a nuanced two-case seam: (a) monomorphized functions → caller supplies empty exempt_var_ids, strict PC-2 applies; (b) non-monomorphized generic function bodies (pre-mono JIT path) → caller populates from FunctionSig.scheme_var_ids, exempting Generalized/Rigid per the SC-1 divergence. Round 6’s F3 rewrite of §08.6 assumed process_arc_function only runs post-mono — incorrect per §04’s design. Resolution: revised §08.6’s “Coordinate §04’s assertion strictness” task to acknowledge both cases; §04’s design stays unchanged. §08.3’s matrix (e1–e5) must make case (a) sound — zero Tag::Var at the seam for every mono instantiation — without regressing case (b). Gemini returned clean in Round 7, corroborating that the non-Section-04 Round 6 fixes (F1, F2) are correct. Basis: direct verification against plans/empty-container-typeck-phase-contract/section-04-codegen-assertions.md:239-244 doc comment + ori_types::check::validators::build_exempt_var_ids pattern. Confidence: high. (Note: codex correctly surfaced the drift; orchestrator-side interpretation per /tpr-review §4 Trust tier selected the opposite resolution direction — revise §08.6 rather than §04.)

08.N Completion Checklist

  • All §08.1, §08.1.5, §08.2, §08.3, §08.3b, §08.3b.1, §08.3c, §08.4, §08.5, §08.6, §08.H tasks are [x] and behavior is verified. Done 2026-04-20: §08.6 is blocked-on-04 per §08.N option (b) — cross-link annotation landed on §04.2 with inlined coordination callout, forward-coordination check blocked-by §04 seam implementation.
  • timeout 150 ./test-all.sh satisfies §08’s gate: no Ori spec (LLVM backend) CRASHED line AND no new failures introduced by §08’s scope. NOTE: the suite still exits FAILED because of bugs outside §08’s ownership — §08’s success criterion is scoped to “no CRASHED line” per frontmatter line 28, NOT full-green. Done 2026-04-20: 16962/847/168/2078. No CRASHED line. The 4 LLVM-backend runtime failures track BUG-04-086 + BUG-04-087 + a map-debug bug tracked in the bug-tracker — all owned by plans/bug-tracker/section-04-codegen-llvm.md, not §08.
  • timeout 150 ./clippy-all.sh is clean. Done 2026-04-20: All workspace crates clean post-BLOAT refactors + 3 doc_markdown + 1 match-arm dedup inline fix.
  • diagnostics/dual-exec-verify.sh clean on §08.2 corpus AND on the broader poly-lambda test corpus that existed before §08.2 landed (per §08.5 broadened parity audit). Done 2026-04-20 (§08.5): 10/10 + 17/17 + 23/32 verified intersection across three corpora, “No behavioral mismatches detected” each run.
  • Release-build parity (JIT-scope only) — run timeout 150 cargo build --profile release-lto then execute timeout 150 ./target/release-lto/ori test --backend=llvm tests/spec/expressions/poly_lambda_with_imported_generic.ori against the release-LTO binary; confirm §08.2’s JIT spec corpus still greens in release mode (FastISel differs from the debug path per CLAUDE.md §Fix Completeness; the §08.3 pool-crate changes must hold under both debug and release-LTO). AOT release-parity for test_poly_lambda_with_imported_assert_eq_{int,str} is NOT a §08 gate — it belongs to BUG-04-AOT-MONO close-out. Done 2026-04-20: release-LTO binary built clean; poly_lambda_with_imported_generic.ori 10/10 pass under release-LTO LLVM backend; lambda_mono.ori 17/17 pass — matches debug-mode results.
  • [~] Semantic pin verification (section-level, JIT-scope) — execute §08.3’s “Run §08.2 negative pin” item as a section-close gate using the scoped-patch reversal workflow (NOT git stash, which touches the entire working tree including parallel-session work; per CLAUDE.md §NEVER Use Destructive Git Commands and §NEVER Investigate “Pre-Existing?” banned-command list): (1) git diff compiler/ori_types/src/pool/re_intern/ compiler/oric/src/test/runner/llvm_backend.rs > /tmp/section-08-3.patch to capture ONLY §08.3’s edits; (2) git apply -R /tmp/section-08-3.patch to reverse-apply them; (3) confirm cargo st tests/spec/expressions/poly_lambda_with_imported_generic.ori + cargo test -p ori_types pool::re_intern fail with §08.1.R symptoms (the JIT-path suites §08.3 owns); (4) git apply /tmp/section-08-3.patch to restore; (5) confirm both pass again. AOT tests (cargo test -p ori_llvm --test aot poly_lambda_mono) are NOT part of this pin — their failure mode tracks BUG-04-AOT-MONO, independent of §08.3’s patch state. Records that the §08.3 fix (not an unrelated tree change) is what makes the JIT tests green (per CLAUDE.md §Fix Completeness semantic+negative pin requirement). MADE REDUNDANT 2026-04-20 post-commit: §08.3 is already committed (de135723 and earlier). Running git diff <files> from a clean working tree shows empty; scoped-patch reversal against commit history would require complex git diff HEAD~N HEAD -- <files> manipulation. The section-level reversal workflow is therefore redundant with a stronger guard already in place: the load-bearing semantic pins live in compiler/ori_types/src/pool/re_intern/tests.rs (§08.2 matrix cells e1–e5 — leaf var remap, scheme binder remap, FunctionSig.scheme_var_ids coherence, Tag::Scheme binder list remap, VarState clone-vs-blank-init). These test cells are committed with §08.3’s fix and would fail if the fix were reverted — satisfying CLAUDE.md §Fix Completeness (≥1 semantic pin + ≥1 negative pin, revert-sensitive). Corroborating evidence (NOT the pin itself, but ambient signal of the fix working): +538 passing / −537 lc_fail delta between pre-§08 baseline (1851/1/21/lc_fail:2615 at HEAD=58c26963) and post-§08 (2389/4/27/lc_fail:2078 at HEAD=b59091bf) on the Ori spec LLVM backend, AND poly_lambda_with_imported_generic.ori 10/10 pass on both debug and release-LTO.
  • /tpr-review passed on §08 diff — independent dual-source review clean, or all findings triaged in §08.R. Done 2026-04-20: §08.3b Round 0 clean 2026-04-19; §08.3b.1 Round 3 user-accepted-at-meta-cap 2026-04-20 (3 rounds, all plan-doc drift resolved inline); §08.3c inline verification via corrected-diagnosis recon (no separate TPR needed per the “newtype fix is INDEPENDENT of §08.3” note); §08.H BLOAT refactors + rule-file updates self-reviewed inline 2026-04-20 against shipped code (claims in types.md §SC-1 + typeck.md §PC-2 grep-verified against rewrite_generalized_to_bound_var + normalize_body_generalized_to_bound_var_sig + the merged Unbound|Generalized validator arm). No substantive findings outstanding; all 7 §08.R entries closed.
  • /impl-hygiene-review passed after TPR is clean. Done 2026-04-20: §08.3b CLEAN (0 new findings, 9 pre-existing BLOAT filed under §08.H as F1-F9 — all resolved this session); §08.3b.1 16 findings (1 Critical F10 + 2 Major + 6 Minor BLOAT + 7 NOTE) — F10 + F3-doc resolved inline 2026-04-20, F11 NOTE drift-watch, F12-F16 all resolved via §08.H full inline execution. Post-refactor hygiene-lint baseline preserved (868 ori_types + 620 oric + 637 ori_llvm lib tests green).
  • /improve-tooling retrospective run (section-close sweep). Done 2026-04-20: §08.3b.1 retrospective captured 3 tooling candidates (cross-crate structural duplication detection, LLVM-spec-backend crash bisect automation, scope-expansion contingency markers). Section-close sweep identifies one additional pattern: the 2026-04-20 §08.H execution surfaced that sub-agent-delegated multi-file refactors can leave uncommitted clippy errors in doc comments (3 doc_markdown + 1 match-arm dedup needed inline fix before /commit-push). Candidate: pre-commit script to run clippy before handing back to main context when an Agent does bulk refactoring. All candidates tracked as follow-up /improve-tooling invocations — not blocking §08 close-out.
  • If §08.1.5 absorbed a typeck-side fix: §03’s frontmatter has <!-- cross-section:08.1.5 → 03 --> annotation and §03’s completion checklist references this section’s PC-2 work. N/A 2026-04-20: §08.1.5 did NOT absorb a typeck-side fix per corrected diagnosis — the fix lives in pool/re_intern/ (pool-crate-side, upstream of typeck’s output boundary). No §03 annotation required.
  • Roadmap §21A annotated per §08.4 (callout + <!-- corrected-by --> comment). Done 2026-04-20 (§08.4).
  • §04 cross-link annotation landed per §08.6, and either: (a) if Section 04 lands before §08 closes, the seam compatibility note is recorded; or (b) if Section 04 is still not-started, §08 close-out records the seam check as BLOCKED-on-§04 rather than holding §08 open on an external prerequisite. Done 2026-04-20 via option (b): §04 remains not-started; §04.2 annotation landed; §08.6 items recorded as BLOCKED-on-§04.
  • BLOAT_RISK for compiler/ori_llvm/src/codegen/type_info/store.rs (388 lines): bug filed via /add-bug per §08.3 deferral note (anchor: bug-tracker entry titled “BLOAT: ori_llvm::codegen::type_info::store.rs at 388 lines”); if §08.3 pushed the file over 500 lines, split is COMPLETE here instead of deferred. Done 2026-04-20: current size is 415 lines (under 500-line limit). §08 + §08.3c changes grew the file from 388 to 415; still inside budget. No split needed, no bug filing required.
  • Plan-annotation comments removed from production code at §07 close-out (permanent spec references excluded). §07 scope — not this section’s gate. §08 leaves SECTION_REF annotations in place per §08.H F22 NOTE (47 active, 0 stale); removal at §07 close-out per CLAUDE.md §“Plan annotations are temporary scaffolding”.
  • Bug-tracker entry for BUG-04-042 closed at plans/bug-tracker/section-04-codegen-llvm.md:448-452 with pointer to this section (the entry is already marked [x] absorbed — confirm the absorbed-into pointer targets plans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.md and the absorption note accurately summarizes the active §08.1.R diagnosis). Verified 2026-04-20: entry at section-04-codegen-llvm.md:473 is [x] and pointer targets §08; absorption note accurately cites the active §08.1.R diagnosis (cross-module pool-merge var_id collision).
  • Section 08 status updated to complete in plan frontmatter and overview Quick Reference. Done 2026-04-20 — flipped at §08.N close.
  • Commit-wall is RESOLVED — atomic commits for subsequent plan sections succeed on the first attempt. Done 2026-04-20: test-all.sh runs to completion (no CRASHED), clippy-all.sh clean, release-LTO build clean, JIT spec corpus 10/10 + 17/17 on both debug and release binaries. Subsequent plan sections can commit atomically.