Intelligence Reconnaissance
Queries run 2026-04-17:
scripts/intel-query.sh --human file-symbols "ori_llvm/src/codegen/type_info" --repo ori— inventorytype_infomodule symbols before investigatingBoundVarresidue in the shared Pool at MonoInstance compilation.scripts/intel-query.sh --human callers "lambda_mono" --repo ori— blast radius of thelambda_monofunction 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 consumeMonoInstanceto 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 (Rustrustc_codegen_ssaMonoItem, 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_mapsubstitution surface is split across TWO scopes — local mono atcompiler/ori_types/src/infer/expr/calls/monomorphization.rs:94-107and imported mono atcompiler/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_lambdaatcompiler/ori_llvm/src/codegen/function_compiler/lambda_mono/type_resolve.rs:55-73only inspects BoundVar/Scheme on return types — lambdas whose return staysTag::Var(Generalized)bypass mono handling entirely. Separate failure mode from the three §08 hypotheses.apply_bound_var_mapatlambda_mono/type_resolve.rs:142only fixes top-level vars; nested generics inside containers (List<T>) remain unsubstituted.fallback_bound_vars_to_intatlambda_mono/type_resolve.rs:392silently converts unresolved-type bugs into ABI/RC bugs — leaving it enabled during §08.3 will misclassify the root cause.resolve_all_lambda_bound_vars(alambda_monohelper, NOT §04’s seam) runs at TWO callsites:compiler/ori_llvm/src/codegen/function_compiler/define_phase.rs:134(insideemit_arc_function) andcompiler/ori_llvm/src/codegen/function_compiler/nounwind/prepare.rs:173. §04’sassert_no_unresolved_type_varsseam — distinct from this helper — sits atdefine_phase.rs:315(process_arc_function) anddefine_phase.rs:375(declare_and_process_lambda) per the parent plan’s overview Architecture diagram. §04’s seam choice MUST land AFTER bothresolve_all_lambda_bound_varscallsites have run; otherwise the assertion fires on legitimate pre-resolutionBoundVarstate. See §08.6 for the coordination protocol.prepare_mono_cachedatnounwind/prepare.rs:95-120has 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:
- Pool contamination (hypothesis 1): polymorphic lambda registrations leave
BoundVarresidue in the shared Pool. Status: CLOSE, but not quite — the collision is in the TEST-RUNNER’smerged_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). - type_info store leak (hypothesis 2):
Idx(241)observed atTypeInfoStore. Status: SURFACE SYMPTOM —TypeInfoStoreis where the collision manifests as an observable error, not where it originates. Fixing the store was ruled out during 2026-04-18 investigation. - 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.
body_type_map/arc_cachecache poisoning (hypothesis 4): shared cache state across mono contexts. Status: REFUTED —TypeInfoStoreis per-codegen-context (documented atcompiler/ori_llvm/src/codegen/type_info/store.rs:37-65); cross-context poisoning via this cache is not a candidate mechanism.- 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 correctlyVarState::Generalizedpost-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_idsexempt 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→ capturedIdx(241)unresolved atori_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 — theOri spec (LLVM backend) CRASHEDsignal 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 incompiler/ori_llvm/tests/aot/poly_lambda_mono.rsis 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_fullyatcompiler/ori_types/src/pool/accessors.rs:434-437only followsVarState::Link; forVarState::Generalizeditbreaks immediately, leavingcurrentas the inputTag::Var. The comment ataccessors.rs:429-432literally documents the failure mode: “This can happen when Generalized type vars leak from type checking into codegen without proper resolution.” TheTag::Vararm atori_llvm/src/codegen/type_info/store.rs:341-364is the only error path that emits “unresolved type variable at codegen” — theTag::BoundVar | RigidVar | Scheme | ...arm at:371-385emits “unreachable type tag at codegen” instead, so the observed message pins the Tag toVar. - 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
TypeInfoStorecache state at the failure point: NOT RUN — runtime inspection denied. Static replacement: the store’sTag::Vararm callsself.pool.resolve_fully(idx)FIRST (line 342); a cache hit is impossible becauseget_implis the point where the miss triggers the error. Hypothesis (e) (poisoned cache) is refuted for the single-file case — a single.orifile with oneassert_eq<int>mono target cannot produce a cross-context poisoned entry withinTypeInfoStorebecauseTypeInfoStoreis 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.rsconsumesTypeInfoStore::get()for arc-IR types; it reports the sameTypeInfo::Errorthe 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 sameget_implerror 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.rsstay 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):
-
Collision site:
compiler/oric/src/test/runner/llvm_backend.rs:334-343—merged_pool.ensure_var_capacity(max_id + 1)after copying imported types’ var_ids unchanged. The inline comment at:329-333documents: “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. -
Re-intern preserves source var_ids unchanged:
compiler/ori_types/src/pool/re_intern/mod.rs:192-193—Tag::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, becausevar_statesis indexed byvar_idand is pool-specific. -
Re-intern hash fast path is invalid for var-bearing subtrees:
compiler/ori_types/src/pool/re_intern/mod.rs:56-60— ifsource.hash(idx)collides with an existing target entry, it returns the target entry as-is. LeafTag::Varhashes include the rawvar_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. -
var_subst build reads the pre-remap scheme_var_ids:
llvm_backend.rs:321-327—var_substis keyed bygeneric_sig.scheme_var_ids, whichre_intern_sigatpool/re_intern/mod.rs:83-97CLONES unchanged (never re-numbers). If §08.3 remaps imported leaf var_ids in the type tree but does NOT remapscheme_var_idsin the sig, the substitution map stops matching and the fix silently regresses. -
Tag::Scheme extra payload stores raw binder var_ids:
compiler/ori_types/src/pool/construct/mod.rs:161-174writes the binder var_id list intoextraat intern time;compiler/ori_types/src/pool/re_intern/mod.rs:185-193preserves them unchanged viasource.scheme_vars(idx).to_vec()+target.scheme(&vars, body). Binders must remap together with leaves, not independently. -
VarState::Generalized branch reads the collided slot:
compiler/ori_types/src/unify/substitute.rs:78-83andcompiler/ori_types/src/pool/substitute/mod.rs:82-88both branch onVarState::Generalized. A remap that allocates fresh var_ids but blanks them toUnbound(instead of cloning the source’s VarState) destroys the semantic state and distorts downstream generic behavior — the source’sVarState::Generalizedmust be cloned into the new destination id. -
resolutionsmap is NOT in the blast radius:compiler/ori_types/src/pool/accessors.rs:358-359—resolutions: FxHashMap<Idx, Idx>. Keys areIdx, NOTvar_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. -
Production codegen does NOT cross-pool-merge:
compiler/ori_llvm/src/codegen/function_compiler/define_phase.rs:115-165andcompiler/ori_llvm/src/codegen/function_compiler/nounwind/prepare.rs:95-149operate 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 intopool/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_returnspass walking everyExprKind::Lambdaat end-of-body was implemented AND narrowed to just unbound-without-constraint returns. Both variants were reverted becausetypeck.md §GN-3generalizes 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 alreadyVarState::Generalized, andbuild_exempt_var_idsputs everyVarState::Generalizedvar in the exempt set via itsFunctionSig.scheme_var_idspath. 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 markedcompletewith option (ii) selected (sibling pass); §08.3 was framed around the typeck fix. - 2026-04-19
/tp-helpconsensus (Gemini HIGH trust + codex Step 4 blind-spots) identified the cross-module pool-merge var_id collision atllvm_backend.rs:320-360as 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. PerCLAUDE.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 tonot-startedto 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 — perCLAUDE.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_literalsto poly-lambda returns — rejected. The vars are alreadyVarState::Generalizedby 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::Generalizedexemption fromvalidate_body_types— rejected. FiresE2005on every polymorphic let-binding, breaks let-polymorphism entirely;CLAUDE.md §INVERTED-TDDflags 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):
-
Lift the remap logic into
compiler/ori_types/src/pool/re_intern/as a reusable abstraction. Do NOT bury it inllvm_backend.rs:320-360even 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, WASMori_compilerfacade, other test harnesses) gets correct semantics by default. Thellvm_backend.rs:320-360call site becomes the first consumer, not the owner. -
Build a
src_var_id → dst_var_idremap map during re-interning of imported types. For every importedTag::Var/Tag::BoundVar/Tag::RigidVar/Tag::Schemebinder encountered, allocate a freshvar_idviamerged_pool.next_var_idand record the mapping. -
Rebuild the imported type tree with the remapped var_ids via full re-intern (NOT
Item.datarewrite-in-place — the pool is append-only pertypes.md §TY-6; rewriting payloads in place would corrupt the intern map and thehashescolumn). -
Rewrite
FunctionSig.scheme_var_ids(compiler/ori_types/src/output/mod.rs:423-428, consumed byllvm_backend.rs:321-327to buildvar_subst) from the same remap map.re_intern_sigatpool/re_intern/mod.rs:83-97currently clones these unchanged — that is the hidden coherence bug that would silently regress §08.3 if missed. -
Rewrite the
Tag::Schemebinder list inextra(stored as raw var_ids atpool/construct/mod.rs:161-174, preserved unchanged atpool/re_intern/mod.rs:185-193) from the same remap map. -
For each remapped var_id, REBUILD a destination-local
VarStatefrom the source’s variant — do NOT blank-init toUnbound, and do NOT perform a whole-enum literal byte-level clone (a literal clone preserves pool-local identities inside variant payloads —idonUnbound/Generalized,targetonLink— 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 }:idMUST be the newly-allocateddst_var_id(pool-local identity — NOTsource.id);rankandnameare pool-independent and clone verbatim. Copyingsource.idliterally reintroduces exactly the var_id collision this fix eliminates.Generalized { id, name }:idMUST be the newly-allocateddst_var_id(pool-local identity — NOTsource.id);nameclones verbatim. Same reasoning asUnbound. Note:Generalizedhas NOrankfield in the shipped enum.Rigid { name }:nameis a globalNameintern (pool-independent); literal clone is correct. Noidfield exists onRigid; norankfield either.Link { target }:target: Idxis source-pool-local; rebuild asVarState::Link { target: re_intern_type(source, source.target, target_pool, cache, var_remap) }— recursively re-intern the Link target on demand. Do NOT rely oncache.get(&source.target)with anexpect(...)assertion; the cache is only populated for types already visited by the traversal, and aTag::VarwithVarState::Link(T)whereTis reachable ONLY via this Link (not transitively via any other branch of the tree being re-interned) will panic theexpect. Recursivere_intern_typehandles cache hits and on-demand construction uniformly, matching the recursion pattern used for child types in every otherre_intern_by_tagarm (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_withinstallsVarState::Linkonly after the occurs check (compiler/ori_types/src/unify/mod.rs:271-291), andresolve/resolve_readonlyassume acyclic link chains (:125-170), so the source pool presented to re-intern is expected to contain finiteLinkchains rather than arbitrary cycles.
unify/substitute.rs:78-83andpool/substitute/mod.rs:82-88branch onVarState::Generalized; wiping it toUnbounddistorts generic behavior. Preserving the variant with a variant-aware rebuild — and critically remappingidonUnbound/GeneralizedandtargetonLink(via recursive re-intern, not cache lookup) — fixes the aliasing without introducing a new source-pool-reference leak or a latent traversal-order panic. -
The
re_intern_typehash fast path atpool/re_intern/mod.rs:56-60is INVALID for var-bearing subtrees once var_ids are remapped (leafTag::Varhashes include the raw var_id perpool/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_VARall clear). -
Out-of-scope (explicitly not part of §08.3’s blast radius): the
resolutionsmap atpool/accessors.rs:358-359is keyed byIdx, NOT byvar_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-6append-only invariant: re-readtypes.md §TY-6and§TF-3Merkle-hash propagation; confirm that fresh-var-id allocation + full re-intern (notItem.datarewrite-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-6mandates pool append-only during a checking session withpool/re_intern/as the documented cross-pool migration exception that constructs freshIdxin the destination and never mutates the source;Item.datarewrite-in-place would corruptintern_map(§TY-2), thehashescolumn, andflags.§TF-3PROPAGATE_MASK plus the var-id-bearing leaf hash (pool/mod.rs:395-413) makes the fast-path skip atpool/re_intern/mod.rs:56-60invalid for var-bearing subtrees once leaves are remapped — §08.1.5 step 7 captures the unconditionalTag::Schemeskip 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-165andnounwind/prepare.rs:95-149— both operate on a single pool and never invokere_intern_*. Document that the fix site (test-runner pool merge) is the ONLY current caller; lifting the remap abstraction intopool/re_intern/is defensive, not reactive. Confirmed (2026-04-19):define_phase.rs:115-165(emit_arc_function) usesself.poolthroughout, callssuper::lambda_mono::resolve_all_lambda_bound_vars,compile_lambda_arc,purity_analysis::remap_partial_apply_names, andprocess_arc_function— nore_intern_*calls anywhere on the production path.nounwind/prepare.rs:95-149(prepare_mono_cached) likewise usesself.pooland dispatches tolower_function_canwithmono_fn.body_type_mapfor type substitution — nore_intern_*calls. The single current consumer ofpool/re_intern/remainscompiler/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 intopool/re_intern/is defensive: it makes the abstraction available to any future cross-pool-merge call site (production codegen, WASMori_compilerfacade, 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.Rso future readers don’t mistake them for the fix. Done (2026-04-19): Pointer doc-comments added to all three tests atcompiler/ori_types/src/check/validators/tests.rslines 580, 612, 657 —polylambda_return_type_with_boundvar_emits_no_diagnostic(T17),polylambda_return_type_with_generalized_var_emits_no_diagnostic(T18), andpolylambda_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 theVarState::Generalizedexemption (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.mdMUST be reduced to a one-line pointer to §08.1.R after §08.3 lands (perCLAUDE.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 (lsreturns 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 perCLAUDE.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 importedassert_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_MASKregression 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 withunresolved type variable at codegen — type inference bug idx=Idx(238)+Idx(241)— matching §08.1’s documentedIdx(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 theori_llvmcrate that exercises LLVM codegen end-to-end via the AOT pipeline. A separate lower-level test that constructs aPoolmanually and callsTypeInfoStore::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): addedcompiler/ori_llvm/tests/aot/poly_lambda_mono.rswith twoassert_aot_success-based tests (test_poly_lambda_with_imported_assert_eq_intand..._str) drivingori build→ linked binary → runtime exit-0 on fixturesfixtures/poly_lambda_mono/poly_lambda_with_imported_assert_eq_{int,str}.ori. Registered incompiler/ori_llvm/tests/aot/main.rs. Verified TDD signal (2026-04-19, Round 5 resolution of TPR-08-R4-01): both tests FAIL today withE5001 unresolved function 'assert_eq' in apply/invoke — missing mono instance?— this is NOT theIdx(241)unresolved-type-variable symptom §08.1.R diagnoses. Direct code inspection (Round 5) confirmsori buildgoes throughcompiler/oric/src/commands/compile_common.rs:70-110,184-240+compiler/ori_types/src/check/mod.rs:341-407with NOre_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_functionsdoes not traverseimport_sigsfor 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): bothtest_poly_lambda_with_imported_assert_eq_intandtest_poly_lambda_with_imported_assert_eq_strcarry#[ignore = "BUG-04-AOT-MONO"]attributes (compiler/ori_llvm/tests/aot/poly_lambda_mono.rs:46,59), socargo test -p ori_llvm --test aotandtest-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 ofassert_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.orialready 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 ontests/spec/expressions/lambda_mono.oriandtests/spec/traits/iterator/AFTER §08.3 closes the producer-side leak, which is when cell (d)‘s intent (verifyis_polymorphic_lambda’scontains_bound_vargate 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.
- (a) poly-lambda defined but unused ✓ (
- 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_MASKregression pin ✓ (propagate_mask_nested_list—let $wrap = x -> [x, x, x]exercisesTag::Scheme HAS_VARpropagation through[T]lambda body) prepare_mono_cachedcache-miss fallback negative pin: covered implicitly — every test runs from a fresh compilation context, so everyassert_eq<T>mono call lowers through the cache-miss path atnounwind/prepare.rs:119-139. The plan’s original framing required a scenario “where the imported metadata is unavailable” via metadata stripping atllvm_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_param—let $first_of = xs -> xs[0]withxs: [T]nested generic parameter)
- Type dimension:
- [~] 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 stashthat 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.patchto 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 E5001missing mono instance. TDD discipline pertests.md §TDD for Bugssatisfied: 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 tocompiler/ori_types/src/pool/re_intern/tests.rsfor EACH tag, re-interning a standaloneTag::<variant>(id=N)from a source pool into a target pool whosevar_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 onlyTag::Varleaves 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_remapallocates a freshdst_idviatarget.next_var_idANDvar_remap.get(&N) == Some(dst_id)ANDtarget.var_states[dst_id]is a variant-aware rebuild ofsource.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_typepath (without var_remap) reproduces the collision — asserts thattargetnow contains aTag::<variant>(N)whosevar_statesslot was cloned from the target pool (demonstrating why the remap-aware variant is load-bearing for every variable-carrying tag).
- Positive pin (per 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 pinlegacy_re_intern_scheme_preserves_source_binder_ids_unchangedGREEN + positive pinremap_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 everyTag::Varleaf 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 currentsource.scheme_vars(idx).to_vec()pattern IS the bug when the enclosing pool merges var-id spaces.
- Positive pin: the re-interned scheme’s binder list matches the remapped leaves (
- (e3) FunctionSig.scheme_var_ids coherence with remapped type tree — add test cells in
pool/re_intern/tests.rs(or a newsig_remap_tests.rssibling iftests.rsgrows past the §BLOAT threshold) exercisingre_intern_sigon a sig whosescheme_var_ids = [7]and whoseparam_types/return_typereferenceTag::Var(7). Landed 2026-04-19: negative pinlegacy_re_intern_sig_preserves_source_scheme_var_ids_unchangedGREEN + positive pinremap_aware_re_intern_sig_remaps_scheme_var_ids_coherently_with_leaves#[cfg(any())]’d out for §08.3. Tests co-located inpool/re_intern/tests.rs(now ~870 lines; authored-.md500-line limit does not apply to test files perimpl-hygiene.md §File Organization).- Positive pin: after
re_intern_sig,sig.scheme_var_ids == [remap[7]]AND every leafTag::Varinparam_types/return_typeusesremap[7]; a testvar_subst = HashMap::from([(sig.scheme_var_ids[0], concrete)])+substitute_in_pool(target, leaf, &var_subst)resolves correctly. - Negative pin: run
re_intern_sigin its pre-fix form (cloningscheme_var_idsunchanged); assert thatvar_substbuilt from the cloned ids does NOT substitute any leaves (because leaf and sig ids drifted) — proves the hidden coherence bug is exercised.
- Positive pin: after
- (e4) VarState variant-aware remap semantic preservation — add test cells verifying the
VarStaterebuild 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 criticalid-remap rule forUnbound/Generalized(pool-local identity) and thetarget-remap rule forLink(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 thesubstitute_in_poolbranch documented inline in each positive-pin body —re_intern_type_with_var_remapis 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.idandGeneralized.idholddst_id(NOT source’s id);Link.targetholds the result of recursivere_intern_type(source, source.target, target_pool, cache, var_remap)(NOTsource.target, and NOTcache.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, andUnbound.rankpreserve source values verbatim. - Negative pin (per variant): (i) a whole-enum literal clone of
Unbound { id: 7 }leavestarget.var_states[dst_id].id == 7— source-pool identity leaks through; assert this and prove the collision reappears atsubstitute_in_pool’sVarState::Generalizedbranch when a subsequent Generalized slot holds the colliding id. (ii) a variant that blanks the destination toVarState::Unboundmakessubstitute_in_pool(target, leaf, &var_subst)take theUnboundbranch atpool/substitute/mod.rs:82-88(different from theGeneralizedbranch atunify/substitute.rs:78-83), distorting dispatch — proves why variant-aware rebuild is load-bearing rather than cosmetic.
- Positive pin (per variant): after
- (e5) Scheme with var-bearing binders AND var-free body —
PROPAGATE_MASKleaves the parent’s var-bearing flags clear — add test cells forTag::Scheme([7], body: Tag::Int)re-interned across pools. Pertypes.md §TF-3,PROPAGATE_MASKpropagates flags from body children only; a scheme’s raw binder list inextrais NOT a PROPAGATE_MASK source. Sosource.flags(scheme).intersects(HAS_VAR | HAS_BOUND_VAR | HAS_RIGID_VAR) == falseeven though bindervar_id=7is pool-local and MUST be remapped. Pins the interaction between step 5 (binder walk inextra) and step 7 (unconditionalTag::Schemefast-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 pinscheme_with_var_bearing_binders_and_var_free_body_has_no_propagated_var_flagsGREEN — TF-3 invariant itself; (ii) negative pinlegacy_re_intern_scheme_with_var_free_body_preserves_source_binder_idGREEN — legacy-path leak; positive pinremap_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 intargetdiffers from its hash insource(scheme hashing is extra-backed pertypes.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-60onHAS_VAR | HAS_BOUND_VAR | HAS_RIGID_VARalone (instead of step 7’s unconditionalTag::Schemeskip) 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 subsequenttarget.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 unconditionalTag::Schemefast-path skip re-interns the body (a no-op forTag::Int) but produces a scheme withscheme_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.
- Positive pin: after
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.jsonblind_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_MASKdoes not flag on the parent scheme. Distinct from e2 (which pins schemes whose body carries vars, soHAS_VARpropagates from body children and is set on the parent). Driven by gemini Round 3 F1 — identified the gap between step 5 (binder walk inextra) and step 7 (unconditionalTag::Schemefast-path skip): without e5, a regression to aHAS_VAR-gated fast-path guard onTag::Schemewould silently hash-hit through schemes likeScheme([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 legacyre_intern_type/re_intern_sigpath (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_internreports25 passed; 0 failed; 0 ignored. §08.3 simultaneously (a) addsre_intern_type_with_var_remap/re_intern_sig_with_var_remapinpool/re_intern/mod.rsAND (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 introducedtodo!()stubs so positive pins could compile +#[ignore]with §08.3 pointers. The pre-commit hook rejected onclippy::todo— project-wide denied perimpl-hygiene.md §Lint Disciplinewith no existing#[expect(clippy::todo)]precedent. Re-grounded on#[cfg(any())]gating perimpl-hygiene.md §Conditional Compilation: preserves the cell-authored TDD scaffolding without introducing denied-lint exposure, and self-retires when §08.3 un-gates (the removedcfgline + 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_remapAPI inpool/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 asrc_var_id → dst_var_idmap alongside the existing type cache. The existingre_intern_typestays 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 emptyvar_remapfor backward compatibility. Document the distinction: the var-remap variant is mandatory for cross-pool-merge contexts where the target pool’svar_stateswas 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_idduring re-intern: for every importedTag::Var,Tag::BoundVar,Tag::RigidVar, andTag::Schemebinder encountered during re-intern, allocate a freshvar_idviatarget.next_var_id(extending the existingvar_statesvector) and recordvar_remap.insert(src_var_id, dst_var_id). Replace the currentTag::Var | Tag::BoundVar | Tag::RigidVar => target.intern(tag, source.data(idx))arm atpool/re_intern/mod.rs:192-193with a remap-aware arm that readsvar_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 existingre_intern_typetraversal already appends new entries totargetrather than rewriting in place — preserve this. Do NOT introduce any code path that mutatestarget.items[i].dataafter interning; append-only is load-bearing forintern_mapcoherence and for the parallelhashescolumn (types.md §TY-2). -
4. Rewrite
FunctionSig.scheme_var_idsinre_intern_sig: extendre_intern_sigatpool/re_intern/mod.rs:83-97to take the sharedvar_remapmap and rewrite every entry ofresult.scheme_var_idsthrough it (panic viaexpectif a scheme_var_id is not in the remap — that’s a soundness violation, not a recoverable case). This matches thevar_substbuild loop atllvm_backend.rs:321-327so everygeneric_sig.scheme_var_ids[i]still resolves to the intendedinstance.generic_args[i]after remap. -
5. Rewrite the
Tag::Schemebinder list in extra: modify theTag::Schemearm atpool/re_intern/mod.rs:185-193— instead oflet vars = source.scheme_vars(idx).to_vec();followed bytarget.scheme(&vars, body), walk eachsrc_var_idthroughvar_remapto produce the destination binder list, then calltarget.scheme(&remapped_vars, body). The binders MUST be allocated BEFORE the body is re-interned (so the body’s leafTag::BoundVar/Tag::Varreferences to these binders can find them in the remap during the recursive descent). -
6. Rebuild destination-local
VarStatefrom source variant with variant-aware remapping (do NOT whole-enum literal-clone, do NOT blank-init toUnbound): after allocating the newdst_var_idviatarget.next_var_id, constructtarget.var_states[dst_var_id]variant-by-variant fromsource.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() }.idMUST bedst_var_id(NOTsource.id) — the whole point of the remap is to give the destination a fresh pool-local id. Copyingsource.idliterally reintroduces the collision.Generalized { id, name }→Generalized { id: dst_var_id, name: source.name.clone() }. Same reasoning asUnbound:idMUST be the newdst_var_id. Note:Generalizedhas NOrankfield in the shipped enum.Rigid { name }→Rigid { name: source.name }(literal clone;Nameis a global intern, pool-independent). Noidorrankfield exists onRigid.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 usecache.get(&source.target).expect(...). The cache is populated only for types already visited in the traversal, and aTag::VarwithVarState::Link(T)whereTis reachable ONLY via this Link will panic the assertion. Recursivere_intern_typehandles cache hits AND on-demand construction uniformly, matching the pattern used by every otherre_intern_by_tagarm (pool/re_intern/mod.rs:120-122for List children,:127-128for Map key,:133-134for Result ok, etc.).
A whole-enum literal clone leaks source-pool identities via
Unbound.id/Generalized.id/Link.targetand reintroduces the collision the remap is meant to eliminate; blanking toUnbounddestroysGeneralized/Rigidsemantic state and distorts generic dispatch at every downstream substitution site (unify/substitute.rs:78-83,pool/substitute/mod.rs:82-88). Acache.get(&source.target).expect(...)that assumes Link targets are visited before their referencing Link panics on any isolated Link branch. Add unit tests inpool/re_intern/tests.rsexercising all four variants: (a)Unbound { id=src }source producesUnbound { id=dst, rank=src.rank, name=src.name }in target; (b)Generalized { id=src, name }source producesGeneralized { id=dst, name=src.name }in target; (c)Rigid { name }source producesRigid { name }in target (verbatim); (d)Link { target=src_idx }source producesLink { target=dst_idx }wheredst_idxcame from recursive re-intern (NOT cache lookup); (e) Link-target-isolated cell:Tag::Var(id=N) with VarState::Link(T)whereTis 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 theexpect; (f) negative pin: a whole-enum literal clone ofUnbound { id=7 }intotarget.var_states[42]producestarget.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::Schemebinder-only cases: modify the fast path atpool/re_intern/mod.rs:56-60. Beforetarget.lookup_by_hash(source.hash(idx))returns a targetIdx, apply TWO guards: (a) checksource.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) Forsource.tag(idx) == Tag::Scheme, unconditionally SKIP the fast path — pertypes.md §TF-3PROPAGATE_MASKpropagates flags from body children only, NOT from the scheme’s raw binder list inextra. A scheme with a var-free body but var-bearing binders (e.g.,Scheme([7], int)) would have noHAS_VAR/HAS_BOUND_VAR/HAS_RIGID_VARflag yet still embeds pool-local var_ids in its hash (scheme hashing includes the extra payload pertypes.md §TI-3extra-backed class). LeafTag::Varhashes include the rawvar_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.datain place leaves intern_map + hashes column stale: test thatre_intern_with_remapNEVER mutates an existingtargetentry’s payload. Positive pin: after re-intern, everytarget.items[i]that was already present before the call SHALL be==to its pre-call value. Negative pin: a test that deliberately mutatestarget.items[i].dataand assertstarget.lookup_by_hash(target.hash(i))now returnsNone(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_remapon a[Tag::Var(id=7)]list-type whentargetalready contains a structurally-identical[Tag::Var(id=7)]from an unrelated source. Positive pin: the remap allocates a freshdst_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 leafTag::Varreference 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_sigpath on aFunctionSigwhosescheme_var_idscontains[7]and whoseparam_types/return_typereferenceTag::Var(7). Positive pin: after remap,scheme_var_idsand the leaf var references resolve to the SAME fresh dst_var_id, sovar_subst = HashMap::from([(scheme_var_ids[0], concrete)])atllvm_backend.rs:321-328correctly substitutes every leaf. Negative pin: a test that remaps leaves but clonesscheme_var_idsunchanged (step 4 omitted) and assertssubstitute_in_poolleaves the leaves untouched (proving the silent regression path). - (e) VarState variant-aware remap — cloning preserves Generalized semantics with pool-local
idremapped: test re-intern of aTag::Varpointing atVarState::Generalized { id: src_id, name }across pools. Positive pin: the destination slot carriesVarState::Generalized { id: dst_id, name }wheredst_idis the newly-allocated var_id fromtarget.next_var_id(NOTsrc_id);namepreserves source value. Negative pin (two shapes): (i) a test that whole-enum literal-clones the source leavestarget.var_states[dst_id].id == src_id— proves the collision reappears. (ii) a test that blank-inits the destination toVarState::Unboundand assertssubstitute_in_poolnow takes theUnboundbranch instead of theGeneralizedbranch, distorting generic dispatch.
Downstream verification:
-
Run
timeout 150 cargo test -p ori_types pool::re_intern— the new unit tests inpool/re_intern/tests.rspass (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.ori17/0/0,integer_safety.ori30/0/0, fullori_typeslib 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 residualTag::Var(Generalized)leak at codegen requires thetypes.md §SC-1scheme-body migration in §08.3b (Generalized→BoundVar at scheme construction incompiler/ori_types/src/unify/generalization.rs). Corrected diagnosis: codex’s round-0 note —lambda_mono.ori:87-103already contains an unconstrained identity lambda that passes, so the differentiator is “scheme-shape survives to mono” (multi-instantiation atpoly_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 inpool::re_intern),lambda_mono.ori(17/17), andinteger_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 withE5001 missing mono instance(they trackBUG-04-AOT-MONOinplans/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 HEAD9eae468d: 2 tests FAILED withE5001: LLVM module verification failed+unresolved function 'assert_eq' in invoke/apply — missing mono instance?ontest_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 perCLAUDE.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.patchto capture ONLY §08.3’s edits; (2)git apply -R /tmp/section-08-3.patchto reverse-apply; (3) re-runcargo 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 forBUG-04-AOT-MONO); (4)git apply /tmp/section-08-3.patchto 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 commit9eae468d(“chore: ori_types pool re-intern + skills/tooling sweep”); the working-treegit diff <paths>form above returns empty post-commit, so the semantically equivalent formgit diff HEAD~1 HEAD -- <paths>was used. Captured patch: 1037 lines coveringre_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 atcompiler/ori_types/src/pool/mod.rs:27— the re-export line referencesre_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. Thepool/mod.rsre-export edit was bundled into9eae468dbut 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 commit127531c2). (4) Restored viagit apply /tmp/section-08-3.patch; post-restorationcargo 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()atcompiler/ori_types/src/unify/generalization.rs:29-58mutates unbound vars toVarState::Generalizedand wraps the original body unchanged inTag::Scheme. Body-leafTag::Var(Generalized)entries survive to codegen.validate_body_types+build_exempt_var_idsatcompiler/ori_types/src/check/validators/mod.rs:100-165exempt everyVarState::Generalizedvar from PC-2 enforcement pertypes.md §SC-1shipped-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_implatcompiler/ori_llvm/src/codegen/type_info/store.rs:341-364trips on the leak;pool.resolve_fully()atcompiler/ori_types/src/pool/accessors.rs:418-437only followsVarState::Linkand leaves Generalized unresolved.- Target form per
types.md §SC-1: scheme bodies containTag::BoundVarleaves bound by the scheme’s declared binders, NOTTag::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-103already contains an unconstrained identity lambda (let $inner = b -> ...) with importedassert_eqAND 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.oripasses becausea + bconstrains 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_impl—canon.md §7.1AIMS 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.rs—generalize()scheme-construction site; rewrite body leaves toTag::BoundVarwith scheme-binder-indexed var_ids.compiler/ori_types/src/unify/substitute.rs,compiler/ori_types/src/pool/substitute/mod.rs— instantiation paths: substituteTag::BoundVarleaves against freshTag::Varper call site; removeVarState::Generalizedcompensation.compiler/ori_types/src/check/validators/mod.rs— strip unreachableVarState::Generalizedarms frombuild_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.rs—apply_bound_var_map+fallback_bound_vars_to_intverify they handle BoundVar-based schemes;fallback_bound_vars_to_intmay 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 hasTag::BoundVar(notTag::Var(Generalized)); instantiation unifies withintat call site; post-typeck IR has noTag::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 freshTag::Varper call; no sharedVarState::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 containsTag::BoundVar, neverTag::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 hasTag::BoundVarfor bothxandy. 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 withT=intmonomorphizes correctly; scheme body hasOption<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 firesE2005viavalidate_body_types; exemption arm removal MUST NOT regress the Unbound detection path. Covered by existingcompiler/ori_types/src/check/validators/tests.rs::body_expr_types_with_unbound_var_emits_one_e2005(T1) — a minimalPool::fresh_var()inexpr_typeswith emptyscheme_var_idsasserts exactly oneE2005. 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.oricompiles 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 helperrewrite_generalized_to_bound_varthat builds a substitution map{var_id → BoundVar(var_id)}and delegates to the canonicalsubstitute_in_poolmachinery. Reuses existing structural recursion (impl-hygiene.md §Algorithmic DRY); no parallel walker needed. Design adjustment from plan: helper name shortened fromrewrite_body_generalized_to_bound_var; uses substitute-map dispatch rather than panic-on-missing because the substitute machinery already returnstyunchanged for non-matching var_ids — no soundness loss since all generalized vars by construction ARE inscheme_var_ids. - 2. Update
generalize()(line 29-58): afterVarState::Generalizedmutation, callrewrite_generalized_to_bound_var(self.pool, ty, &vars)and pass the rewritten body toTag::Schemeconstruction. Cells A-E now pass GREEN. - 3. Update
pool/substitute/mod.rs(line 28-90): addedTag::BoundVararm viasubstitute_bound_varhelper; widened fast-path gate tointersects(HAS_VAR | HAS_BOUND_VAR)(post-migration scheme bodies haveHAS_BOUND_VAR=true, HAS_VAR=false— the old!HAS_VARgate would skip them). Trap removal (replaces step 9): theVarState::Generalizedarm insubstitute_varwas REMOVED entirely (not retained with a trap as plan originally stipulated) becausemaybe_record_mono_instancewalks the WHOLE pool by raw index and routinely encounters orphanTag::Var(Generalized)entries from unrelated polymorphic functions — the legitimate fall-through (returntyunchanged) handles them. The old Generalized arm’svar_subst.get(&id)fallback was redundant:var_id == idbecause both come from the samefresh_varallocation, so the directvar_subst.get(&var_id)lookup at the top covered every legitimate substitution path. - 4. Update
unify/substitute.rs(line 28-49): addedTag::BoundVararm in thesubstitute()match; widened fast-path gate to includeHAS_BOUND_VAR. TheVarState::Generalizedarm in theTag::Varbranch 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— NO-OP: re-reading the source,VarState::Generalizedarm frombuild_exempt_var_idsbuild_exempt_var_ids(lines 161-173) has no Generalized arm; it builds an exempt set fromscheme_var_idsdirectly. 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::Generalizedarm fromcollect_first_unbound_var— DONE in §08.3b.1 (step 4). TheGeneralizedarm now emitsE2005unless exempted byscheme_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 followsVarState::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_types868/0,cargo test -p ori_llvm --lib637/0,cargo test -p ori_llvm --test aot2161/0,cargo st3622/843/33 (baseline match),cargo run --bin ori -- test --backend=llvm tests/spec/expressions/poly_lambda_with_imported_generic.ori10/0/0. Fulltest-all.shOri 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_intatlambda_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.shlast_run_sha=58c26963reported 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.shon poly-lambda corpus — to run at/commit-pushtime. Done 2026-04-20 (§08.5 verification):lambda_mono.ori17/17 both backends,poly_lambda_with_imported_generic.ori10/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 applyto restore. Run at/commit-pushtime. Deferred to §08.N section-close gate (see08.Nline 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, commit4ff5ca59): §SC-1 now documentsrewrite_generalized_to_bound_var+normalize_body_generalized_to_bound_var_sigas shipped; residualTag::Var(Generalized)at PC-2 is a leak-alarm emittingE2005. 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, commit4ff5ca59): §PC-2 now states the validator treatsVarState::Generalizedidentically toVarState::Unbound(emitsE2005unless in scheme-var exempt set); onlyRigidstays 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_vardoc comment — DONE inline with §08.3b.1 step 4. - Update
plans/empty-container-typeck-phase-contract/00-overview.mdsuccess criteria + section list — §08.3b.1 delivered. Handled at/commit-pushtime. 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_intremains reachable load-bearing safety net). - Downstream verification green — all four passes GREEN post-§08.3b.1.
-
/tpr-reviewpassed on §08.3b diff — Round 0 clean (codex + gemini bothstatus: clean, 0 verified findings). Scratch dir/tmp/tpr-round-ori_lang-joiY7bb1. Exit reason:clean. 2026-04-19. -
/impl-hygiene-reviewpassed 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-toolingretrospective (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-clauderun — 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 commit4ff5ca59; grep-verified againstrewrite_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 commitde135723(fix(ori_arc): §08.3c newtype lowering + §08.3b scheme-body migration). §08.3b.1 shipped in991b17d5. - 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) toTag::BoundVarleaves correctly (§08.3b shipped).- BUT
expr_types[lambda_body_sub_expr]for let-polymorphic lambdas still references the ORIGINALTag::Varleaves whosevar_statewas mutated toGeneralizedin place — the rewrite produces NEWTag::BoundVarIdxs but does NOT update existingexpr_typesentries. - Similarly,
FunctionSig.param_types/FunctionSig.return_typefor 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) callspool.resolve_fully(idx)which only followsVarState::Link—Tag::Var(Generalized)andTag::BoundVarboth fall through unresolved, producing theIdx(246)/Idx(251) unresolvedcodegen error.
Primary surface (target):
compiler/ori_types/src/check/bodies/— add a post-generalize normalization pass, driven from ModuleChecker at end-of-body, that walksInferOutput.expr_types+ the body’sFunctionSig.param_types/return_typeand substitutesTag::Var(Generalized)→Tag::BoundVarfor each scheme’svar_ids. Reusesubstitute_in_poolwith the{var_id → BoundVar(var_id)}substitution map (same shape as §08.3b’srewrite_generalized_to_bound_varhelper).InferEngine::generalize()records the generalized binder ids; Bodies runs the normalization pass beforevalidate_body_types— mirrors the existingdefault_unbound_vars_from_empty_literalspattern.compiler/ori_llvm/src/codegen/type_info/store.rscompute_type_info_inner— Tag::BoundVar arm is a defensive ICE signal per Round-0 Option B reconciliation: returnsTypeInfo::Errorwith a WARN trace when aTag::BoundVarreaches codegen. Substitution itself happens UPSTREAM inlower_function_canviatype_substthreaded fromMonoFunction.body_type_mapatfunction_compiler/nounwind/prepare.rs:133. By codegen time a correctly-substituted mono body has noTag::BoundVarleaves; any survivor signals an upstream substitution gap percanon.md §7.1AIMS Invariant 2 (no masking).TypeInfoStorestays context-free perimpl-hygiene.md §SSOT.compiler/ori_types/src/pool/accessors.rs::resolve_fully— REMAINS UNCHANGED (link-only). Do NOT add aTag::BoundVararm here;MonoInstance.body_type_mapis 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 callresolve_fully).compiler/ori_types/src/check/validators/mod.rs::collect_first_unbound_var— STRIP theVarState::Generalizedexemption 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):
| Call | Decision | Rationale |
|---|---|---|
| Pass insertion point | Option 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 resolution | Option B (RECONCILED Round 0 TPR) — substitute UPSTREAM in lower_function_can via type_subst; TypeInfoStore Tag::BoundVar arm is a defensive ICE signal | Original 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 reachingcompute_type_info_innersurfaces asTypeInfo::Errorpercanon.md §7.1AIMS Invariant 2 (upstreamlower_function_cantype_substis the substitution mechanism; store stays context-free perimpl-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 BEFOREvalidate_body_types.InferEngine::generalize()records the generalized binder ids; Bodies orchestrates the substitute walk. Mirrorsdefault_unbound_vars_from_empty_literalspattern. 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 methodnormalize_body_generalized_to_bound_var_sig(mod.rs:812-898) walksexpr_types+FunctionSig.param_types+return_type, builds{var_id → pool.bound_var(var_id)}substitution map, applies viasubstitute_in_pool. Wired intocheck/bodies/functions.rs:134-148, 254-258andcheck/bodies/impls.rs:213-222, 347-356. Runs BEFORErun_validator(...). - 3. Decide
Tag::BoundVarresolution boundary: RESOLVED — Option B reconciliation (Round-0 TPR). Substitution happens UPSTREAM inlower_function_canviatype_subst(threaded fromMonoFunction.body_type_mapatfunction_compiler/nounwind/prepare.rs:133);TypeInfoStore::compute_type_info_innerTag::BoundVar arm is a defensive ICE signal returningTypeInfo::Errorpercanon.md §7.1AIMS Invariant 2.pool/accessors.rs::resolve_fullystays link-only (monomorphization state must NOT contaminate the context-free pool SSOT).TypeInfoStoreis context-free perimpl-hygiene.md §SSOT. See “Design consensus” table above for full Option B rationale. - 4. Strip the
VarState::Generalizedexemption arm incheck/validators/mod.rs::collect_first_unbound_var— DONE.validators/mod.rs:256-276. TheVarState::Generalized | VarState::Rigid => falsearm was split:Generalizednow emitsE2005unless the var_id is in theexemptset (scheme-var polymorphism preservation);Rigidremains unconditionally exempt pertypeck.md §UN-6. Partial-migration doc comment retired; replaced by §08.3b.1 leak-alarm docs. - 5. Update
validators/tests.rsgeneralized-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_types868/0,cargo test -p ori_llvm637/0 lib + 2161/0 aot,cargo st3622/843/33 (baseline match),poly_lambda_with_imported_generic.ori10/0/0. Fulltest-all.shOri 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_intatlambda_mono/type_resolve.rs:403— REACHABLE (called fromlambda_mono/mod.rs:121and:436). Phase 3 tightened its return-type guard from over-eagercontains_bound_varwalk to top-levelTag::BoundVar | Tag::Varcheck, preventing container-return collapse toIdx::INT. Function is load-bearing safety net for paths where body_type_map resolution misses — no/add-bugneeded. - 8. Cross-plan sync (partial → complete):
validators/mod.rs::collect_first_unbound_vardoc comment cleaned (Phase 3, inline with step 4). Done 2026-04-20:00-overview.mdcarries §08.4 cross-link and JIT-only scope notes at lines 35/304.types.md §SC-1target-only note retirement +typeck.md §PC-2defaulting-pass doc update flow through/sync-claudeat §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-outchecklist unblocks and §08.3b can markcomplete. Done 2026-04-20. -
/tpr-reviewpassed on §08.3b.1 diff — 3 rounds, exit_reasonuser_accepted_at_meta_cap_reached2026-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 percanon.md §7.1AIMS 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-reviewpassed 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 viabuild_mono_body_type_map+BodyTypeMapSinkextraction atpool/substitute/. F3-doc ghost reference RESOLVED. F11-F16 filed in §08.H with concrete close actions (drift watch + BLOAT with concrete anchors perimpl-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-toolingretrospective — 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 batchhygiene-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 isolatetest_expect_none.ori; script asdiagnostics/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-toolingcandidates — 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 commit991b17d5(refactor(ori_types,ori_llvm): §08.3b.1 Generalized→BoundVar + Option B). Follow-on §08.H BLOAT refactors shipped in4ff5ca59.
§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_mapbuilder + 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_typesstill HAS_VAR-only after Phase 2’smaybe_record_mono_instancewidening; (b)lambda_monoreturn-type resolution gated oncontains_varalone, plusresolve_lambda_return_typesmissingPartialApply.tysubstitution +fallback_bound_vars_to_intcollapsing container-return types toIdx::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.1Invariant 2. - Phase 4 (LLVM-backend spec harness crash):
test-all.shshowed Ori spec LLVM backend CRASHED. Bisect →tests/spec/types/option/expect.ori. Root cause: latent nounwind-analyzer over-approximation inlambda_mono/context.rs::is_callee_interceptedtreating ALL intercepted builtins as nounwind, including may-unwindOption.expect/Option.unwrap/Result.expect/Result.unwrap/etc. AddedMAY_UNWIND_INTERCEPTED_METHODSlist +intercepted_is_nounwindhelper; gated Apply/Invoke analyzer paths. Updated AOTwrapper_rc_retainnegative pins (exit code 1 viaori_run_mainis 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_mapfield +with_body_type_mapbuilder present but never wired at the per-MonoFunction codegen boundary. Verified against code: substitution happens UPSTREAM inlower_function_canviatype_subst(threaded fromMonoFunction.body_type_mapatfunction_compiler/nounwind/prepare.rs:133); by codegen time, a correctly-substituted mono body has noTag::BoundVarleaves. 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_mapfield +with_body_type_mapbuilder dropped fromTypeInfoStore;compute_type_info_innerTag::BoundVar arm is now an Error-only defensive ICE signal percanon.md §7.1AIMS Invariant 2 (upstream substitution gaps surface asTypeInfo::Error, not masked). Cell J renamedtype_info_store_bound_var_surfaces_as_error_ice_signaland 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(waspassed 1851, failed 1, skipped 21, lc_fail 2615at state.sh baseline HEAD58c26963).AOT integration tests: 0 passed, 2 failed(2 failures expected and tracked underBUG-04-AOT-MONO; NEW: AOT suite also shows errors ontest_different_newtype_values,test_newtype_parameter,test_newtype_computation).LLVM IR verification failed after codegen (emit_arc_function)atcompiler/ori_llvm/src/codegen/function_compiler/define_phase.rsfor newtype tests.emit_partial_apply: callee not found name="UserId"/"Score"warnings incompiler/ori_llvm/src/codegen/arc_emitter/closures.rs.Call parameter type does not match function signatureLLVM assertions on newtype constructor call sites.ori panic: must be setfollowed byori: 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 incompiler/ori_types/src/pool/re_intern/tests.rs(16 functions) andcompiler/oric/benches/pool_interning.rs(2 benchmark functions). Zero production callers.compiler/ori_llvm/src/evaluator/mod.rs:47is 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 usere_intern_type_with_var_remap/re_intern_sig_with_var_remapwith shared per-modulevar_remapmaps (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 byNameviaself.ctx.functions.get(&callee).Nameis global-interned and pool-independent (perpool/re_intern/mod.rs:511“Nameis 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 — onlyassert_eqandassertare imported (fromstd.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:149aslet mut merged_pool = pool.clone();— the host file’s pool is the seed. Imports are re-interned on top via_with_var_remap. UnderPool: Clone(compiler/ori_types/src/pool/mod.rs:44), theresolutionsmap clones with the rest of the pool, so localTag::Named→ struct/enum entries should survive. TheIdx(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:
- Newtype constructor lowering produces
PartialApply { callee: Name("UserId"), ... }instead ofConstruct— the IR-level lowering ofUserId("user-123")is treating the type-name as a function-pointer reference. The downstreamemit_partial_applythen looks the name up inctx.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 inori_canonorori_arc::lower::expr(Construct vs Call disambiguation forTag::Named-typed call targets). - Newtype
.unwrap()is treated as a mono-instance method requiring resolution — theunresolved 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. Perori-syntax.md §Newtypesthe.innerprojection (andunwrapwhen 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_typeat all. - New fix-shape question is: whether the local newtype constructor +
.unwrap()resolution path needs a dedicated handler inori_arc::lowerorori_canonthat preventsTag::Namedconstructor calls from being lowered toPartialApply. Determining this requires readingori_canon::lower::exprand/orori_arc::lower::callfor the Construct-vs-Call dispatch on aTag::Namedcallee — 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):
UserId("user-123")is parsed and canonicalized asCanExpr::Call { func: <UserId-ref>, args: ["user-123"] }. The canonicalizer typically emits the func subexpression asCanExpr::TypeRef("UserId")(perlower/expr/mod.rs:237CanExpr::Ident(name) | CanExpr::Const(name) | CanExpr::TypeRef(name) => self.lower_ident(...)) — newtype names resolve through the same path as type names.lower_callmatchesfunc_kind.CanExpr::TypeRefis NOT in theFunctionRef | SelfRef | Identarms — 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) }self.lower_expr(func)→ dispatches tolower_ident("UserId", ty=<UserId-Tag::Function-signature>).- Inside
lower_ident: name not in scope (UserIdis a type, not a variable); not invariant_ctors(newtypes are not enum variants); but the type tag ISTag::Function(the type checker assigned(str) -> UserIdto the constructor reference, pertypeck.mdnewtype constructor signature handling). - The
else if Tag::Functionarm fires (lines 354-360) and emitsPartialApply { callee: Name("UserId"), args: vec![] }. - Later in
emit_partial_apply(compiler/ori_llvm/src/codegen/arc_emitter/closures.rs:50), the lookupself.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:
- 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 aTypeRegistrynewtype entry (analogous to howtry_emit_variant_ctorchecks thevariant_ctorsmap for enum variants). The detection requires populating an analogousnewtype_ctors: FxHashMap<Name, NewtypeInfo>(or extendingvariant_ctorsif the type allows) whenArcLowereris constructed. For unary newtype calls with matching arg type, emit a transparentLet { 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). - Handle newtype
.unwrap()and.innerinlower_method_call(and theFieldaccessor path for.inner): detect when the receiver type is a newtype and the method isunwrap(or the field isinner), and emit a transparentLet { Var(receiver_var) }— the unwrap is purely type-level erasure of the newtype tag. - Update
lower_ident’sTag::Functionarm (lower/expr/mod.rs:354-360): add a guard to exclude newtype constructor name references — those should NOT emit aPartialApply(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). - Confirm
repr.md §RP-24is honored at the LLVM emission boundary: codegen should already treat newtype-typed values identically to their inner-typed values perCG:TR-1(the canonical type mapping). If steps 1–3 produce transparent IR, no further codegen change is needed; if they do not, theTag::Named-resolving-to-newtype case inTypeLayoutResolvermay need explicit transparent-pass-through handling.
Scope estimate for the fix (informal, no commitment):
lower_call: ~1 new dispatch case + helper to populatenewtype_ctorsfrom the registry.lower_method_call: ~1 new dispatch case forunwrapon newtype receivers.lower_ident: ~1 guard line in theTag::Functionarm.- Newtype info plumbing: thread through
ArcLowererconstructor (similar tovariant_ctors). - Tests: matrix in
tests/spec/types/newtypes.orialready exists and reproduces the failure cleanly — those will green when the fix lands. Add a Rust unit test incompiler/ori_arc/src/lower/calls/tests.rspinning 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:348onlytracing::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):
-
compiler/ori_types/src/pool/mod.rs+accessors.rs: addednewtype_ctors: FxHashMap<Name, Idx>onPool(clones with the rest of the pool for cross-module merge perllvm_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::lowerconsults it directly without TypeRegistry access (preserving theori_arccrate boundary, percompiler.md §Architecture). -
compiler/ori_types/src/check/registration/user_types.rs§TypeDeclKind::Newtypearm: added two registration calls after the existingregister_newtype(TypeRegistry side):pool.register_newtype_ctor(decl.name, underlying_ty)— populates thenewtype_ctorsmap forori_arclookup.pool.set_resolution(idx, underlying_ty)— links the newtype’sTag::NamedIdx to its underlying type soori_llvm::codegen::type_info::store::resolve_fullyproduces correct LLVM type for newtype-typed values (perrepr.md §RP-24layout-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.
-
compiler/ori_arc/src/lower/calls/mod.rs:try_emit_newtype_ctor(name, ty, arg_vars, span)helper: returnsSome(transparent_let)iffpool.is_newtype_ctor(name)ANDarg_vars.len() == 1. Wired intolower_call’sFunctionRef,Ident(out-of-scope), and a new explicitTypeRefarm — fires before falling through to the wildcard indirect-call path that misroutes throughlower_ident’sTag::Functionarm.try_lower_newtype_unwrap(receiver, method, ty, span)helper: returnsSome(transparent_let)iff method isunwrapAND the receiver’s UNRESOLVED type chain (chasingTag::Varlinks but stopping atTag::NamedBEFORE crossing the newtype→underlying resolution boundary) ends atTag::NamedANDpool.is_newtype_ctor(named_name). Wired intolower_method_callafter the tag-check builtin pre-check. The chase-but-don’t-cross-resolution loop is load-bearing: because step 2 above registersset_resolution(named, underlying), naïvepool.resolve_fullywould 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.ori→ 9 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.ori→ 9 passed, 0 failed, 0 skipped (interpreter was always passing; confirms no regression). - Unit tests:
cargo test -p ori_types→ 865 passed, 0 failed;cargo test -p ori_arc→ 1211 passed, 0 failed. - Clippy: workspace clean (no warnings).
Out of scope for this fix (deferred follow-ups, not blockers):
lower_ident’sTag::Functionarm atlower/expr/mod.rs:354-360still emitsPartialApplyfor 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 apool.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..innerfield-access path on newtypes: not exercised bytests/spec/types/newtypes.ori(which uses.unwrap()exclusively). Same fix shape would apply inlower_fieldif exercised.- Cross-module re-intern of the
newtype_ctorsmap: when imports flow intomerged_poolviare_intern_type_with_var_remap, the imported newtype constructors are NOT propagated intomerged_pool.newtype_ctors. Local newtypes work becausemerged_pool = pool.clone()(line 149) clones the host’s newtype map. Imported newtypes (e.g., a hypotheticalstd.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 set→ori: 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 inori_arc::lower::calls. - Pool API for newtype detection (
Pool::register_newtype_ctor/newtype_underlying/is_newtype_ctor). -
set_resolutionfor newtypes wired intocheck::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) andcargo test -p ori_arc(1211/1211): no regressions. -
cargo clippy: clean. -
test-all.shOri 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-reviewclean 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-reviewclean 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-pushvia 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). Runtimeout 150 ./test-all.shthere. CompareOri 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 inpool/re_intern/tests.rs(16 fns) andoric/benches/pool_interning.rs(2 fns). Zero production callers; doc-only mention atori_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 oncompiler/ori_llvm/confirmsemit_partial_applykeys lookup byNameviaself.ctx.functions.get(&callee)— NOT by var_id.Nameis global-interned and pool-independent (pool/re_intern/mod.rs:511). var_id remap cannot directly cause Name-keyed lookup miss. -
Readcompiler/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 byName(line 50). On miss, emits null closure + warns “callee not found” (line 53). Newtype constructors should NOT route throughemit_partial_applyat all — they should lower toConstruct. The presence ofPartialApply { callee: Name("UserId") }in the ARC IR is the upstream defect. -
Readcompiler/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 atdefine_phase.rs:204(if self.verify_arc && !fn_val.verify(true)). Currently gated behindORI_VERIFY_ARC=1percodegen-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 withintest-all.shcontext? Done 2026-04-20: rantimeout 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 failin 33ms — failure reproduces in isolation, not just intest-all.sh. Symptom log captured in §08.3c re-diagnosis block above. - [~]
diagnostics/codegen-audit.shon 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.shwould 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_typethat broke. MOOT: corrected diagnosis shows the fault was NOT inre_intern_type— zero production callers. The fault was inori_arc::lower::expr::lower_ident’sTag::Functionarm misrouting newtype constructors throughemit_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_unwrapinori_arc::lower::calls, plusPool::{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.shshowsOri 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-reviewclean on §08.3c diff. Deferred to §08.N combined TPR (combined with §08.H BLOAT refactors for scope efficiency). -
/impl-hygiene-reviewclean on §08.3c diff. Deferred to §08.N combined hygiene review. - state.sh refreshed post-commit —
ori_spec_llvmstatus 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 carryingstatus: in-progress; the “verified 2026-03-29” marker islast_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-407plus 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.mdreturned 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.mdto 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 incompiler/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.
- “The Import Resolution note plus subsection 21.7 / 21.11 monomorphization behavior was CORRECTED by
- 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
AskUserQuestionto 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.mdMission 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 at00-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.shon 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) CRASHEDline;assert_eq$m$intcompiles; LLVM IR verification passes. Done 2026-04-20: LLVM backend suite runs to completion;_Unwind_RaiseExceptioncrash from pre-§08.3c is gone;poly_lambda_with_imported_generic.ori10/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.rsstay in-tree as TDD pins for siblingBUG-04-AOT-MONO(AOTcollect_mono_functionsdoes not traverseimport_sigs; different root cause from §08.1.R’s JIT pool-merge collision). §08.5 verifies JIT parity viaori test --backend=llvm; AOT release-binary verification moves toBUG-04-AOT-MONO’s close-out when that sibling bug lands.ori build-produced AOT binaries andori test --backend=llvmJIT 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 atplans/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.shon 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.oriand anytests/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.shremediation.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 ofprocess_arc_function. By the time §04’sassert_no_unresolved_type_varsseam fires atdefine_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-substitutionTag::Var(Generalized)orTag::BoundVarstate will be observable at §04’s seam that wouldn’t have been observable under the prior (incorrect) diagnosis. resolve_all_lambda_bound_varsalready runs at its two callsites (define_phase.rs:134insideemit_arc_function;nounwind/prepare.rs:173insideprepare_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 howresolve_all_lambda_bound_varsruns.- §04’s planned seam at
:315+:375will see POST-lambda-mono-substitution state (becauseemit_arc_function→ … →process_arc_functionputs line 134’s substitution before line 315’s planned assertion). Under the corrected diagnosis, the planned seam will ALSO see post-remap state (becausepool/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_pipelineper-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_pipelineper-lambda hook. Target line; same caveat.
lambda_monohelper —resolve_all_lambda_bound_vars(ALREADY in code) — runs at two callsites:compiler/ori_llvm/src/codegen/function_compiler/define_phase.rs:134(insideemit_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_idsparameter (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’s04.2subsection (thePRIMARY seam: process_arc_function + declare_and_process_lambda hooksitem insection-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.2header insection-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_varfn length (111 lines, limit 100) atcompiler/ori_types/src/check/validators/mod.rs:188. Self-heals when §08.3b.1 strips theVarState::Generalizedexemption arm (lines 241-274 collapse). Done pre-session 2026-04-20: extractedcheck_var_tag+emit_ambiguous_if_not_exempthelpers;collect_first_unbound_varnow 67 lines. - F2 —
collect_first_unbound_varnesting depth (5, limit 4) atcompiler/ori_types/src/check/validators/mod.rs:241. Self-heals when §08.3b.1 simplifies theVarStatedispatch (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.rsfile length (691 lines, limit 500, over by 191). Split into submodules:pool/accessors/mod.rsas facade,pool/accessors/resolution.rsforresolve_fully+ link-chase,pool/accessors/queries.rsfor tag/data/flags/hashes getters,pool/accessors/var_state.rsforVarStateops. Cross-reference:plans/hygiene-full-2/section-08-file-size.md:59tracks the pre-§08.3b baseline (624 lines) — this plan becomes the resolving owner; updatehygiene-full-2 §08to point here. Done 2026-04-20: split intopool/accessors/{mod,resolution,nominal}.rs— before: 706 lines (single file). After:mod.rs364 /resolution.rs229 /nominal.rs149 (all < 500).mod.rsholds compound-type accessors (function, tuple, map/result, borrowed, simple containers, scheme/generic, applied, named);resolution.rsholdsresolve,resolve_fully,chase_var_links,resolve_applied_via_matching_args,var_idx_for_id+ newtype registration;nominal.rsholds struct + enum accessors. 868 tests green. - F4 —
fn resolve_fullynesting depth 5 (limit 4) atcompiler/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 extractedpool/accessors/resolution.rsis the natural home). Done pre-session 2026-04-20:resolve_fullydecomposed intochase_var_links+resolve_applied_via_matching_argshelpers (pre-work already landed); post-F3 split these live inpool/accessors/resolution.rs. - F5 —
fn merkle_hash_extralength (107 lines, limit 100) atcompiler/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) pertypes.md §TI-3Merkle Hash Classification. Resolve together with F7’s file split. Done 2026-04-20:merkle_hash_extramoved topool/hashing.rsand decomposed intomerkle_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_flagslength (139 lines, limit 100) atcompiler/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_flagsmoved topool/flags_compute.rsand decomposed intocompute_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.rsfile length (721 lines, limit 500, over by 221). Split: keepmod.rsas the public facade (Poolstruct +intern+ top-level queries), move interning intopool/interning.rs, flag computation intopool/flags_compute.rs(absorbs F6), Merkle hashing intopool/hashing.rs(absorbs F5). Cross-reference:plans/hygiene-full-2/section-08-file-size.md:57tracks the pre-§08.3b baseline (661 lines) — this plan becomes the resolving owner; updatehygiene-full-2 §08to point here. Done 2026-04-20: split landed. Before: 723 lines. After:mod.rs343 /interning.rs86 /hashing.rs187 /flags_compute.rs198 (all < 500). 868 tests green. - F8 —
fn extract_var_from_typeslength (103 lines, limit 100) atcompiler/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 intoextract_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 substitutelength (229 lines, limit 100) atcompiler/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 intosubstitute_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.rsreturns 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_typesgreen after all splits (no behavior change). Done 2026-04-20: 868/0. -
timeout 150 ./test-all.shstill 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,59entries forpool/mod.rs+pool/accessors.rsupdated to cross-reference this plan as the resolving owner (avoids double-tracking perimpl-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 acrosscompiler/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: extractedbuild_mono_body_type_map<Sink: BodyTypeMapSink>+BodyTypeMapSinktrait atcompiler/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:395doc comment named a non-existent functionapply_bound_var_map_deep(grep returned 0 results). RESOLVED 2026-04-20: removed the ghost reference; doc now citesapply_concrete_param_typesandapply_call_site_typesonly (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_varboth consumesig.scheme_var_idsfor 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 extractingSchemeVarSurfaceonly 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.rsgrew to 989 lines (+489 over 500-line limit) after §08.3b.1 addedpending_generalized_vars+normalize_body_generalized_to_bound_var_sig+ surrounding helpers (~160 lines). Extractnormalize_body_generalized_to_bound_var*,default_unbound_vars_*,collect_unbound_reachable_vars,is_empty_collection_literalinto new submoduleinfer/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 intoinfer/body_finalize/mod.rs(267 lines). Additionally extractedinfer/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.rs509 lines (still 9 lines over limit, primarily due toInferEnginestruct 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_llvmgrew to 527 lines (5.27× the 100-line limit) after §08.3b.1 added imported-mono construction. Extractbuild_imported_mono_functionsto new moduleoric/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: extractedbuild_imported_mono_functionstocompiler/oric/src/test/runner/imported_mono.rs(134 lines). Before:llvm_backend.rs576 lines /run_file_llvm527 lines. After:llvm_backend.rs474 lines (< 500) /run_file_llvm410 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.rsat 768 lines,type_resolve.rsat 675 lines,find_call_site_return_typenesting depth 8. Extract three submodules underlambda_mono/:multi_inst/,single_inst/,call_site/. Targetmod.rs<200 lines. Not §08.3b.1-contributed; pre-existing surface. Done 2026-04-20: extracted the three submodules. Before:mod.rs768 lines /type_resolve.rs675 lines. After:mod.rs145 lines (target < 200 MET) /multi_inst.rs267 /single_inst.rs96 /call_site.rs326 /type_resolve.rs674 (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_testshare near-identicalwith_function_scopeclosures (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: addedBodyOutputsstruct +finalize_body_and_exportinbodies/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_function165L /check_impl_method141L /check_def_impl_method123L /check_test~95L. After:check_function158L /check_impl_method137L /check_def_impl_method119L /check_test91L. 868 tests green. - F16 — BLOAT:fn-length (Minor, plan prediction amendment) —
collect_first_unbound_varstill 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_varrequires 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 samecheck_var_tag+emit_ambiguous_if_not_exemptextraction that closed F1 and F2;collect_first_unbound_varnow 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.rs989→509 with 4 new submodules; F13run_file_llvm527→410 +imported_mono.rs; F14lambda_mono/mod.rs768→145 (< 200 target MET) + 3 new submodules; F15BodyOutputsspine extraction; F16collect_first_unbound_var116→67 (pre-session). -
python3 scripts/hygiene-lint.pybaseline 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 AOTori buildpaths. 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 buildgoes throughcompiler/oric/src/commands/compile_common.rs:70-110,184-240andcompiler/ori_types/src/check/mod.rs:341-407with NOre_intern_*call — cross-module pool merge is a JIT-test-runner-only path. Both AOT integration tests fail today withE5001 unresolved function 'assert_eq' — missing mono instance?(unresolved imported-generic function resolution), NOT theIdx(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 (AOTcollect_mono_functionsdoes not traverseimport_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 incompiler/oric/src/test/runner/llvm_backend.rs:167-251viacompiler/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 atplans/bug-tracker/section-04-codegen-llvm.md— BUG-04-AOT-MONO (AOTori buildlacks imported generic monomorphization;collect_mono_functionsincompiler/ori_llvm/src/monomorphize/mod.rsignoresimport_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 of00-overview.md:35shows “AOT scope (2026-04-19 TPR-08-R4-01 resolution, Round 5): §08 is JIT-only —ori builddoes NOT share there_intern_*code path. The AOT integration tests incompiler/ori_llvm/tests/aot/poly_lambda_mono.rsstay in-tree as failing TDD pins for the sibling bug BUG-04-AOT-MONO”; direct read of00-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 of00-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 “blockstest-all.sh… every other section of this plan” to “blockscargo run --bin ori -- test --backend=llvmfor files mixing polymorphic lambdas with imported generics — the JIT LLVM surface this section owns after Round 5’s narrowing”. The broadertest-all.shwall (test_failed=844,known_failing_count=35perdiagnostics/state.sh show --jsonat HEAD=1294282d, dominated by§06.2interpreter remediation) is NOT §08’s scope. Basis: direct verification againstdiagnostics/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 (E5001missing mono instanceno longer fires)” reframed to “AOT integration tests STAY failing withE5001 missing mono instance— they trackBUG-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 atBUG-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 toBUG-04-AOT-MONOclose-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 survivingTag::Var(Generalized)as “legitimate polymorphic state”. RESOLVED (2026-04-19, Round 6). Pertypeck.md §PC-2+canon.md §4.2strict phase contract (“NoTag::Varin any type-bearing IR position”), §04’s seam atdefine_phase.rs:315(process_arc_function) runs POST-lambda-mono-substitution AND POST-remap — EVERY survivingTag::Varat the seam is a bug regardless ofVarState. Rewrote the task to: align §04’s assertion with strict PC-2 + §4.2; acknowledge the shippedtypes.md §SC-1divergence (Generalized vars stored asTag::Var(Generalized)) as a target-only conformance gap exempt at typeck exit (viavalidate_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 againsttypeck.md §PC-2,canon.md §4.2,types.md §SC-1divergence note, andcompiler/ori_types/src/check/validators/mod.rs::validate_body_typesexemption 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.
- 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
-
[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’sexempt_var_idsparameter permits seam-timeVarState::Generalized/Rigidexemption, 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 (removeexempt_var_ids). Direct read of Section 04’svalidate.rsdoc comment (§04 plan lines 239-244) reveals the design is a nuanced two-case seam: (a) monomorphized functions → caller supplies emptyexempt_var_ids, strict PC-2 applies; (b) non-monomorphized generic function bodies (pre-mono JIT path) → caller populates fromFunctionSig.scheme_var_ids, exempting Generalized/Rigid per the SC-1 divergence. Round 6’s F3 rewrite of §08.6 assumedprocess_arc_functiononly 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 — zeroTag::Varat 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 againstplans/empty-container-typeck-phase-contract/section-04-codegen-assertions.md:239-244doc comment +ori_types::check::validators::build_exempt_var_idspattern. Confidence: high. (Note: codex correctly surfaced the drift; orchestrator-side interpretation per/tpr-review §4 Trust tierselected 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 isblocked-on-04per §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.shsatisfies §08’s gate: noOri spec (LLVM backend) CRASHEDline AND no new failures introduced by §08’s scope. NOTE: the suite still exitsFAILEDbecause 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 byplans/bug-tracker/section-04-codegen-llvm.md, not §08. -
timeout 150 ./clippy-all.shis 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.shclean 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-ltothen executetimeout 150 ./target/release-lto/ori test --backend=llvm tests/spec/expressions/poly_lambda_with_imported_generic.oriagainst the release-LTO binary; confirm §08.2’s JIT spec corpus still greens in release mode (FastISel differs from the debug path perCLAUDE.md §Fix Completeness; the §08.3 pool-crate changes must hold under both debug and release-LTO). AOT release-parity fortest_poly_lambda_with_imported_assert_eq_{int,str}is NOT a §08 gate — it belongs toBUG-04-AOT-MONOclose-out. Done 2026-04-20: release-LTO binary built clean;poly_lambda_with_imported_generic.ori10/10 pass under release-LTO LLVM backend;lambda_mono.ori17/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; perCLAUDE.md §NEVER Use Destructive Git Commandsand §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.patchto capture ONLY §08.3’s edits; (2)git apply -R /tmp/section-08-3.patchto reverse-apply them; (3) confirmcargo st tests/spec/expressions/poly_lambda_with_imported_generic.ori+cargo test -p ori_types pool::re_internfail with §08.1.R symptoms (the JIT-path suites §08.3 owns); (4)git apply /tmp/section-08-3.patchto 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 tracksBUG-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 (perCLAUDE.md §Fix Completenesssemantic+negative pin requirement). MADE REDUNDANT 2026-04-20 post-commit: §08.3 is already committed (de135723and earlier). Runninggit diff <files>from a clean working tree shows empty; scoped-patch reversal against commit history would require complexgit 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 incompiler/ori_types/src/pool/re_intern/tests.rs(§08.2 matrix cells e1–e5 — leaf var remap, scheme binder remap,FunctionSig.scheme_var_idscoherence,Tag::Schemebinder list remap,VarStateclone-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, ANDpoly_lambda_with_imported_generic.ori10/10 pass on both debug and release-LTO. -
/tpr-reviewpassed 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 againstrewrite_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-reviewpassed 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-toolingretrospective 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 inpool/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 remainsnot-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-bugper §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-452with pointer to this section (the entry is already marked[x]absorbed — confirm the absorbed-into pointer targetsplans/empty-container-typeck-phase-contract/section-08-codegen-poly-lambda.mdand the absorption note accurately summarizes the active §08.1.R diagnosis). Verified 2026-04-20: entry atsection-04-codegen-llvm.md:473is[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
completein 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.shruns to completion (no CRASHED),clippy-all.shclean, release-LTO build clean, JIT spec corpus 10/10 + 17/17 on both debug and release binaries. Subsequent plan sections can commit atomically.