100%

Section 04: Integer Narrowing Pipeline

Context: Today, every int is i64 in LLVM IR. A loop counter that goes 0..100 wastes 7 bytes per element in array storage. A struct with { x: int, y: int } where both fields are always 0..255 uses 16 bytes instead of 2. The savings compound in collections: [Point] with 1M elements wastes 14MB.

Reference implementations:

  • Zig src/Sema.zig: coerceInMemoryAllowedPtrAbiType() — coerces comptime_int to runtime width
  • Roc crates/compiler/mono/src/layout.rs: Layout::from_var() — selects concrete layout from type variable constraints
  • LLVM lib/Transforms/InstCombine/InstCombineCasts.cpp: Integer truncation elimination

Depends on: §03 (range analysis provides the intervals).


04.1 Width Selection Algorithm

File(s): compiler/ori_repr/src/narrowing/int.rs

Setup required: compiler/ori_repr/src/narrowing/ does not exist yet. Steps:

  1. Create the compiler/ori_repr/src/narrowing/ directory
  2. Create mod.rs (dispatch hub), int.rs (integer narrowing), abi.rs (ABI widening), overflow.rs (overflow guards), tests.rs (sibling test file)
  3. Add pub mod narrowing; to compiler/ori_repr/src/lib.rs The apply_integer_narrowing() stub in lib.rs (line 215) is the entry point — fill it in to call into narrowing/int.rs.

Given a ValueRange, select the minimum integer width that preserves the semantic contract.

  • Implement width selection (2026-03-26): Created compiler/ori_repr/src/narrowing/int.rs with narrow_struct_fields(). Uses ValueRange::min_width() from range/mod.rs — no duplicate function. Iterates plan.decision_indices() to find Struct/Tuple types, narrows Int { I64, signed: true } fields to smallest width from field-range summaries. Wired into apply_integer_narrowing() in lib.rs.

  • Apply conservatism rules — implemented subset (2026-03-26):

    • #repr("c") / #repr("packed") / #repr("transparent") types skip narrowing (has_fixed_layout_attr())
    • #repr("c", aligned N) types skip narrowing (TPR-04-004 fix, 2026-03-26)
    • NarrowingPolicy::Disabled skips all narrowing
    • Only canonical Int { I64, signed: true } fields are candidates
    • Fields with Top range stay at I64 (safe default)
    • Field-summary-driven narrowing: only from §03’s FieldSummaryTable built from Construct sites
  • Apply conservatism rules — visibility-based gating (TPR-04-005, implemented 2026-03-26): Public API types are now excluded from integer narrowing. Implementation:

    • Added pub_type_indices: FxHashSet<Idx> to ReprPlan (plan.rs) with set_pub_type_indices() and is_public_type() API
    • compute_repr_plan_with_interner() accepts pub_type_indices: &[Idx] parameter; stored in Phase 0b
    • Both call sites (codegen_pipeline.rs, evaluator/compile.rs) extract public type indices from TypeEntry::visibility == Visibility::Public
    • narrow_struct_fields() gates on plan.is_public_type(idx) — public types skip narrowing with tracing
    • Test public_type_not_narrowed: pub struct with bounded fields → stays I64
    • Test private_type_narrowed_normally: private struct narrowed, pub struct preserved in same plan
  • Apply conservatism rules — monomorphized generic type propagation (TPR-04-012, implemented 2026-03-27): Generic type instantiations (Applied → concrete Struct) create distinct pool idxs that bypass repr/public exemptions. Fix: added propagate_metadata_to_applied_resolutions() as Phase 0c in compute_repr_plan_with_interner() (lib.rs). Collects protected type Names from repr_attrs/pub_type_indices, scans all pool Applied entries, resolves through pool chain, propagates metadata to concrete Struct idx. Implementation:

    • Phase 0c in compute_repr_plan_with_interner(): propagate_metadata_to_applied_resolutions() iterates pool for Applied types matching protected Names, resolves each, propagates repr_attr and pub_type to resolved concrete Struct idx (2026-03-27)
    • 6 regression tests in tests.rs: repr_attr_propagates_through_applied_to_concrete_struct, pub_type_propagates_through_applied_to_concrete_struct, repr_c_applied_concrete_struct_not_narrowed_semantic_pin, pub_applied_concrete_struct_not_narrowed_semantic_pin, applied_without_resolution_no_propagation (negative), multiple_applied_instantiations_all_protected (2026-03-27)
    • Semantic pin: #repr(“c”) Named type with monomorphized Applied → Struct — narrowing blocked on mono struct (stays I64). ONLY passes with Phase 0c propagation (2026-03-27)
    • Semantic pin: pub Named type with monomorphized Applied → Struct — narrowing blocked on mono struct (stays I64). ONLY passes with Phase 0c propagation (2026-03-27)
    • 381/381 ori_repr debug + release green, 14,236 total tests green (2026-03-27)
  • Conservatism design rules (enforced incrementally by Phase A/B/C):

    • Local variables (Phase B): narrow aggressively (widening is free in registers)
    • Struct fields (Phase A — done): narrow from field-summary table only
    • Function parameters (Phase B): narrow only if ALL call sites agree on the range
    • Function returns (Phase B): narrow only if ALL callers can handle the narrow type
    • Collection elements (Phase C): narrow aggressively (savings multiply by element count)
    • Public API types (Phase A — done): do NOT narrow (gated by is_public_type())
    • Address-taken functions / indirect-call targets (Phase B): do NOT narrow parameters or returns
    • Closure captures (Phase B): canonical width in closure environment
  • Use the existing NarrowingPolicy from compiler/ori_repr/src/plan/query.rs (2026-03-26): narrow_struct_fields() checks plan.narrowing_policy() == Disabled and returns early. Policy consumed via existing API. NarrowingPolicy already exists in compiler/ori_repr/src/plan/query.rs with three variants:

    // Existing type — do NOT redeclare in narrowing/int.rs
    pub enum NarrowingPolicy {
        Aggressive,    // Apply all safe narrowing optimizations (default)
        Conservative,  // Apply only provably-safe narrowing (no heuristics)
        Disabled,      // No narrowing — canonical representations only
    }

    The narrowing pass reads the policy via plan.narrowing_policy() (already implemented). Per-site policy (e.g., “min 2 bytes savings”) is handled by the conservatism rules above, not by a field on the enum variant.


04.2 ABI Boundary Widening

File(s): compiler/ori_repr/src/narrowing/abi.rs

At function boundaries and FFI, narrowed integers must be widened back to canonical width. This is critical for correctness.

  • Define ABI boundary rules (2026-03-26): Created compiler/ori_repr/src/narrowing/abi.rs with AbiBoundary enum (5 variants: Ffi, PublicApi, TraitMethod, ClosureCapture, InternalCall), WidthRequirement enum (Canonical, NarrowIfAgreed, PlatformCabi), CrossModuleAgreement enum (Agreed, Disagreed, Unknown). Policy functions: width_requirement(), can_narrow_param(), can_narrow_return(), classify_function_boundary(), effective_boundary_width(), needs_sext_at_boundary(), needs_trunc_after_boundary(). Exported from crate root. 24 tests in narrowing/tests.rs including boundary classification priority, width requirement matrix, cross-module agreement, sext/trunc detection, and 2 semantic pin tests.

  • Implement widening insertion policy (2026-03-26): Widening rules encoded in abi.rs policy functions. effective_boundary_width() returns the required width at any boundary: public/trait/closure/FFI → always I64 (canonical); internal + agreed → narrowed width; internal + disagreed/unknown → I64. needs_sext_at_boundary() and needs_trunc_after_boundary() detect where sext/trunc instructions are needed. The actual LLVM sext/trunc emission is deferred to §04.4 (LLVM Codegen Integration). Rules:

    • Before public function return: sext i32 %narrow to i64
    • Before FFI call arguments: widen to C-ABI width
    • At module import boundaries: widen to canonical
    • When storing to generic collection: widen if collection is exported
    • Closure environments: treat capture slots as canonical-width storage
  • Cross-module narrowing via Merkle hashes (2026-03-26): CrossModuleAgreement enum models the three states (Agreed, Disagreed, Unknown). can_narrow_cross_module() returns true only for Agreed. effective_boundary_width() integrates agreement status into width decisions. Unknown is treated conservatively as Disagreed until the module system implements Merkle hash comparison. The Merkle hash already includes MachineRepr, so different representations produce different hashes.


04.3 Overflow Guard Insertion

File(s): compiler/ori_repr/src/narrowing/overflow.rs

When a value is narrowed, arithmetic operations might overflow the narrow type even though they wouldn’t overflow the canonical i64. The compiler must insert overflow checks.

  • Implement overflow analysis (2026-03-27): Created compiler/ori_repr/src/narrowing/overflow.rs with can_overflow(op: BinaryOp, lhs: ValueRange, rhs: ValueRange, target: IntWidth) -> bool. Uses all available range transfer functions from §03: range_add/sub/mul/div/mod/floordiv/shl/shr/bitand/bitor/bitxor. Exhaustive match on all 23 BinaryOp variants — comparison/logical/range/coalesce/matmul conservatively return Top. 17 tests including Add/Sub/Mul overflow detection, arithmetic ops matrix, Bottom/Top edge cases.

  • Overflow strategy recommendation (2026-03-27): Created OverflowStrategy enum with three variants: ProvenSafe (range proves no overflow — zero cost), WidenCompute { intermediate_width } (sext operands, compute at wider type, trunc result — low cost), UseCanonical (use i64 — forward compat, currently unreachable since i64 covers all values). recommend_strategy() function implements priority: (c) ProvenSafe when !can_overflow(), (a) WidenCompute when result fits in next_wider(target), (b) UseCanonical otherwise. Tests verify strategy progression, I8→I16→I32→I64 widening chain.

  • Decision codified (2026-03-27): prefer (c) ProvenSafe when provable, (a) WidenCompute for rare overflow, (b) UseCanonical when overflow exceeds next-wider. Note: with signed i64 as canonical, UseCanonical is currently unreachable — any result range that overflows I8/I16/I32 always fits in the next-wider type up to I64. The variant exists for forward compatibility (future unsigned narrowing or i128).


04.4 LLVM Codegen Integration

Integration note: TypeInfo::storage_type() does NOT consult ReprPlan and must not be modified for integer narrowing. The actual integration point is TypeLayoutResolver::try_repr_to_llvm_type() in compiler/ori_llvm/src/codegen/type_info/mod.rs (lines 167–229). Current state: Primitive MachineRepr::Int { width } → i8/i16/i32/i64 already works (lines 171–176), and TypeLayoutResolver::resolve_inner() queries repr_plan.get_repr(idx) first (line 249). However, MachineRepr::Struct/Tuple return None and fall back to TypeInfoStore canonical i64 fields — struct/tuple lowering is pending until try_repr_to_llvm_type() recursively consumes narrowed FieldRepr widths (see Phase A LLVM struct/tuple lowering tasks below).

Per-variable vs per-type Idx: ReprPlan::int_width(idx) queries a type Idx. All local int variables share Tag::Int, so per-variable local narrowing needs either (a) a per-(function, var) decision map in ReprPlan, or (b) deriving the width on-the-fly in the emitter from plan.var_range(func, var).min_width(). Struct-field narrowing is clean: the struct type gets a new MachineRepr::Struct with narrowed FieldRepr. Local variable narrowing is Phase B.

File(s):

  • compiler/ori_repr/src/narrowing/int.rs — the apply_integer_narrowing() implementation (populates ReprPlan with narrowed MachineRepr::Struct decisions for struct types whose fields have bounded ranges from §03)
  • compiler/ori_llvm/src/codegen/type_info/mod.rsTypeLayoutResolver::try_repr_to_llvm_type() (handles MachineRepr::Int { width } → i8/i16/i32/i64; must be extended to handle MachineRepr::Struct/Tuple by recursively lowering FieldRepr widths — see §04.4 tasks)
  • compiler/ori_llvm/src/codegen/arc_emitter/value_emission.rsemit_literal() emits const_i64 for all int literals today; for narrowed locals, must emit narrowed constant when variable’s type has been narrowed
  • compiler/ori_llvm/src/codegen/arc_emitter/construction.rs — struct construction; sext/trunc inserts needed at field store boundaries when field width differs from operand width

The LLVM backend type resolution path via TypeLayoutResolver handles MachineRepr::Int { width } but does not yet handle recursive MachineRepr::Struct/Tuple — those return None from try_repr_to_llvm_type() and fall back to TypeInfoStore canonical i64 fields. §04.4 must extend try_repr_to_llvm_type() to recursively lower FieldRepr widths for struct/tuple decisions (TPR-04-006 fix).

  • Phase A — Struct field narrowing (primary): (2026-03-27): apply_integer_narrowing() calls narrow_struct_fields() which iterates all struct types with Struct reprs. For each struct field of type int, queries plan.field_range(struct_idx, field_index). If range.min_width() < I64, emits a narrowed MachineRepr::Struct decision with updated FieldRepr entries. Bug fix (IR-PIN-04-018): narrowed decisions were stored only under the original Pool index (e.g., Named("Pixel")) but codegen always canonicalizes via pool.resolve_fully() to the concrete Struct(fields) index. Fixed by propagating narrowed decisions to resolved indices (mirrors Phase 0 pattern for #repr attrs). Also fixed derive codegen (hash, printable, debug) to sext narrowed i8/i16/i32 fields to canonical i64 before passing to runtime functions. Scoped try_lower_narrowed_aggregate() to all-scalar-int structs only — mixed-type structs (str + int) need Phase C element_store_size integration. Original plan code block:

    fn apply_integer_narrowing(plan: &mut ReprPlan, pool: &Pool) {
        // Iterate Pool for struct/tuple types with int fields.
        // For each field with a narrowable range, replace the canonical
        // MachineRepr::Struct with one having narrowed FieldRepr widths.
        // TypeLayoutResolver already reads MachineRepr::Struct and uses
        // FieldRepr — no LLVM codegen changes needed.
    }

    §04/§06 interface contract: §04 writes only the FieldRepr.repr field (narrowed width). It does NOT compute FieldRepr.offset, StructRepr.size, or StructRepr.align — those are §06’s exclusive responsibility. §04 sets these to zero as placeholders. §06 reads the narrowed repr values to compute correct field sizes and then runs the alignment-optimal reordering algorithm. No code downstream of §04 but upstream of §06 may read FieldRepr.offset or StructRepr.size.

  • Phase B — Local variable narrowing (2026-03-28): Local int variables all share the same Pool Idx (Tag::Int), so per-variable narrowing cannot use the type-keyed ReprPlan::int_width(idx) alone. Two options:

    • Option 1: Add a per-(function, var) map to ReprPlan for local variable decisions (new local_int_width field: FxHashMap<(Name, ArcVarId), IntWidth>). Query in ArcIrEmitter when emitting variable-producing instructions.
    • Option 2: In the ARC IR emitter, derive the width on-the-fly from plan.var_range(func, var).min_width() for each variable at emission time (avoids new map, slightly more computation at codegen time). Recommended: Option 2 for simplicity — query plan.var_range(func, var).min_width() in the emitter when producing the alloca/phi/store for a local int variable.
  • Phase C — Collection element narrowing (2026-03-28): A [int] array where all stored values are [-128, 127] now records a narrowed element representation in ReprPlan; LLVM backing storage remains canonical until element_store_size() consults that repr. Implementation:

    • Added ElementSummaryTable to compiler/ori_repr/src/range/field_summary.rs — parallel to FieldSummaryTable, keyed by collection type Idx. Methods: observe_elements(), element_range(), flush_to_repr_plan().
    • Added update_element_summaries() — tracks element ranges from Construct(ListLiteral|SetLiteral) and CollectionReuse instructions. Checks inner type via pool.list_elem()/pool.set_elem() — only int-typed elements contribute bounded ranges.
    • Added element_range_summaries: FxHashMap<Idx, ValueRange> to ReprPlan with join_element_range() and element_range() methods.
    • Wired into fixpoint loop: update_element_summaries() called alongside update_field_summaries(), recompute_element_summaries() after convergence, element_summaries field in RangeFixpointResult, flushed in propagate_ranges().
    • Implemented narrow_collection_elements() in narrowing/int.rs — iterates FatPointer(Collection { element_repr: Int { I64 } }) decisions, queries element range, narrows when safe. Same conservatism: public types, #repr attrs, disabled policy. Called after narrow_struct_fields() in apply_integer_narrowing().
    • 11 unit tests in narrowing/tests.rs: bounded i8/i16/i32, Top stays i64, public not narrowed, disabled policy, repr(“c”), semantic pin, negative pin, joined ranges, one-wide-prevents-narrowing.
    • 14,371 tests pass, 0 failures (2026-03-28).
    • Codegen limitation: element_store_size() in LLVM emitter does not yet consult ReprPlan for narrowed element types — GEP strides still use canonical i64 (8 bytes). LLVM codegen integration tracked as separate Phase C items below.
    • Scope limitation: only Construct(ListLiteral|SetLiteral) and CollectionReuse contribute ranges. .push() and other runtime mutations are lowered to Apply calls — their ranges are conservatively Top.
  • Phase A — LLVM struct/tuple lowering (TPR-04-006 fix). (2026-03-27): Implemented try_lower_narrowed_aggregate() in layout_resolver.rs. Only triggers for structs with at least one narrowed int field (IntWidth != I64). Recursively resolves field reprs via try_repr_to_llvm_type(). Non-narrowed structs continue using the named struct path. Phase A scoping: tuples excluded from narrowing — tuples are used as collection elements, iterator state, and intermediates where element_store_size() assumes canonical widths. Tuple narrowing deferred to Phase C when element_store_size() integration is complete. Implementation:

    • try_lower_narrowed_aggregate() in layout_resolver.rs:303-346: detects narrowed aggregates via has_narrowed field scan, resolves all fields recursively, builds anonymous LLVM struct type from narrowed field types. Falls back to None for non-narrowed structs and structs with unresolvable nested fields.
    • Fallback: if any field repr returns None from try_repr_to_llvm_type() (e.g., nested Struct/Enum), returns None → TypeInfoStore two-phase creation path takes over.
    • End-to-end semantic pin: 6 AOT tests in compiler/ori_llvm/tests/aot/narrowing.rs — Pixel round-trip (trunc i64→i8 + sext i8→i64), struct update, mixed types (str + int + bool — runtime fallback only: int field stays canonical i64 due to all-scalar-int guard in try_lower_narrowed_aggregate(); mixed-field narrowing deferred to Phase C), field mutation, i8 boundary values (-128, 127), negative test (wide range stays canonical).
    • Pixel test uses [-128, 127] for true i8 pin (signed narrowing).
    • Tuple narrowing disabled in narrow_struct_fields() (narrowing/int.rs): CandidateKind::Tuple → skip with tracing. Tuple narrowing test updated to tuple_elements_not_narrowed_phase_a.
  • Insert sext/trunc at narrowing boundaries (2026-03-27): Struct field store and load boundaries implemented. Function entry/exit (Phase B) deferred.

    • Struct field store (construction.rs:29-34): trunc_for_narrowed_struct() in emitter_utils.rs — checks pool field type is Tag::Int AND LLVM field is narrower, inserts trunc i64 %val to i<N>. Naturally narrow types (Byte, Char, Bool) pass through unchanged.
    • Struct field load (instr_dispatch.rs:216-224): sext_narrowed_field() in emitter_utils.rs — checks ARC IR destination type is Tag::Int, inserts sext i<N> %field to i64. Non-int destinations pass through unchanged.
    • Function entry (Phase B): parameters arrive at canonical width → trunc to narrow if locally narrowed — deferred to Phase B
    • Function exit (Phase B): narrow local → sext to canonical width at boundary — deferred to Phase B
  • [IR-PIN-04-018] IR semantic pin tests for narrowing (2026-03-27, from TPR-04-018). Added 4 IR semantic pin tests using compile_and_capture_ir() + extract_function_ir() in narrowing.rs. Tests exposed a critical bug: narrowed decisions were invisible to codegen (index mismatch). Fixed by propagating decisions to resolved Pool indices and adding sext in derive codegen (hash/printable/debug). Implementation:

    • test_narrowed_struct_ir_pin_type_layout: Asserts { i8, i8, i8, i8 } type in _ori_read_pixel — uses separate function to prevent constant folding.
    • test_narrowed_struct_ir_pin_trunc_on_construction: Asserts trunc i64 or { i8, i8, i8 } constant store in _ori_main at construction site.
    • test_narrowed_struct_ir_pin_sext_on_field_load: Asserts sext i8 in _ori_sum_channels — narrowed field loads require sign extension to i64.
    • test_non_narrowed_struct_ir_pin_wide_range: Negative pin — _ori_sum_wide with 3_000_000_000 values asserts NO sext i8/i16/i32.
  • [DERIVE-PIN-04-020] Negative-value derive semantic pins (2026-03-27, from TPR-04-020). 4 AOT tests in narrowing.rs exercise derived hash(), to_str(), and debug() on narrowed structs with negative i8 field values. Also fixed a pre-existing memory leak in compile_format_fields() — intermediate concat results were not RC-decremented (added emit_str_rc_dec helper in string_helpers.rs).

    • AOT test: test_narrowed_derive_hash_negative_values#derive(Hashable) on SignedPixel { r: -50, g: -120, b: 100 }, verifies hash consistency with negative values
    • AOT test: test_narrowed_derive_printable_negative_values#derive(Printable) verifies to_str() contains “-50” and “-120” (catches zext bug: -50 would display as “206”)
    • AOT test: test_narrowed_derive_debug_negative_values#derive(Debug) verifies debug() contains “-1”, “-128”, “127”
    • IR semantic pin: test_narrowed_derive_ir_pin_sext_in_hash — verifies sext i8 present in IR for narrowed struct hash codegen
  • [MIXED-PIN-04-019] Negative semantic pin for mixed-field struct rejection (2026-03-27, from TPR-04-019). test_mixed_field_struct_ir_pin_no_narrowing in narrowing.rs — verifies Record { count: int, name: str, active: bool } with count in i8 range does NOT show sext i8 in the _ori_read_count function IR, confirming try_lower_narrowed_aggregate() rejects mixed-type structs.

  • Handle comparison operations correctly (2026-03-27):

    • Signed comparison (icmp slt) on narrow types is correct for signed narrowing — verified by architecture: sext_narrowed_field() sign-extends to i64 at field extraction before any comparison. 3 AOT semantic pin tests in narrowing.rs: test_narrowed_comparison_signed_semantics (negative values through all 6 comparison operators), test_narrowed_comparison_i8_boundary_values (-128 < 0 catches zext bugs), test_narrowed_comparison_ordering_chain (min-of-three with negatives).
    • Unsigned narrowing (future, for byte → int) needs zext not sext — not yet needed, byte values use separate Tag::Byte type

04.5 Completion Checklist

Test matrix for §04 (write failing tests FIRST, verify they fail, then implement):

Phase A — Struct field narrowing:

Input patternExpected narrowingSemantic pin
struct Pixel { r: int, g: int, b: int, a: int } with fields 0..255{ i8, i8, i8, i8 } (4 bytes)Yes — sizeof(Pixel) == 4
struct Pair { x: int, y: int } with fields -32768..32767{ i16, i16 } (4 bytes)Yes — sizeof(Pair) == 4
Struct field store: canonical-width operand into narrowed fieldtrunc i64 %val to i8 in LLVM IRYes — no trunc → wrong width stored
Struct field load: narrowed field used in computationsext i8 %field to i64 in LLVM IRYes — missing sext → sign extension wrong

Phase B — Local variable narrowing:

Input patternExpected narrowingSemantic pin
for i in 0..100 — loop counteri8 local in LLVM IRYes — zero i64 variable for i
let x = 200 — single-use constant locali16 (range [200,200] does not fit signed i8 max=127, so → i16)Yes — i64 alloca absent for x
Internal function @f (n: int) -> int where only call site passes 5parameter n uses i8Yes — sext visible at call boundary
pub @f (n: int) -> int — public APIparameter n stays i64Yes — no narrowing at public boundary
let g = f; g(300) / function passed as valueparameter stays i64Yes — address-taken callables disabled
Narrowed local captured by closurecapture storage stays canonical i64Yes — no closure ABI mismatch
Arithmetic a + b where a, b ∈ [0, 100] → result [0, 200]i16 or wider for resultYes — overflow safety preserved
Local int with range Top (no analysis)i64 (canonical, no narrowing)Yes — fallback is safe
Trait method parameteri64 (no narrowing — unknown callers)Yes — no narrowing
Cross-module call with agreed-upon rangenarrow type if both sides agreeYes — sext at module boundary
Function entry: parameter arrives canonical, is narrowed locallytrunc i64 %arg to i8 at function entry in LLVM IRYes — parameter width unchanged at call site
Function exit: narrow local returned to canonicalsext i8 %local to i64 at return in LLVM IRYes — return type unchanged at call site

Phase C — Collection element narrowing:

Input patternExpected narrowingSemantic pin
[int] list where all pushed values are [-128, 127]element stored as i8 in backing arrayYes — element GEP stride is 1 byte, not 8
[int] list where element range is Topelement stays i64Yes — no element narrowing without evidence
Public [int] parameterelement stays i64 — ABI conservativeYes — no narrowing of public collection elements

All phases — negative cases (things that must NOT be narrowed):

Input patternExpectedSemantic pin
NarrowingPolicy::Disabled (i.e., --no-repr-opt)all types stay i64Yes — Pixel struct is 32 bytes, not 4
NarrowingPolicy::Conservative vs AggressiveConservative does not narrow loop counters (insufficient savings evidence); Aggressive doesYes — behavior differs

TDD ordering (MANDATORY — write failing tests first for each phase):

  • Write failing test matrix for Phase A BEFORE implementing Phase A (2026-03-26): 22 tests in narrowing/tests.rs — verified tests failed before implementation (iteration bug: pool-based → plan-based fixed)
  • Write failing test matrix for Phase B BEFORE implementing Phase B (2026-03-27): IR-inspection tests test_phase_b_ir_pin_straight_line_add_narrowed and test_phase_b_ir_pin_multiple_narrowed_locals in narrowing.rs — verified tests failed before implementation. Negative tests: test_phase_b_negative_public_param_not_narrowed, test_phase_b_negative_wide_constant_stays_i64. Loop counter IR pin tests remain #[ignore] (blocked on §03 convergence).
  • Write failing test matrix for Phase C BEFORE implementing Phase C (2026-03-28): 11 unit tests written in narrowing/tests.rs. Implementation was done simultaneously with tests since the functions were new (no pre-existing behavior to fail against).
  • Verify each test fails before implementing the corresponding feature (2026-03-28): Phase C tests exercise the new narrow_collection_elements() function directly — they test the NEW behavior, not pre-existing behavior. Semantic pin test verifies before/after narrowing state change.

Phase A — Struct field narrowing:

  • ValueRange::min_width() returns correct width for all test ranges (2026-03-26): Verified via semantic_pin_pixel_signed_range_narrows_to_i8 (I8), boundary_exact_i16_range_narrows_to_i16 (I16), boundary_exact_i32_range (I32), top_range_stays_i64 (I64), bottom_range_narrows_to_i8 (I8).
  • ValueRange::min_width() boundary cases (2026-03-26): boundary_just_exceeds_i8_narrows_to_i16 ([-128,128]→I16), boundary_just_exceeds_i16_narrows_to_i32 ([-32769,0]→I32), boundary_just_exceeds_i32_stays_i64 ([-2^31,2^31]→I64), boundary_unsigned_byte_range_narrows_to_i16 ([0,255]→I16).
  • Struct field x: int in struct Pair { x: int, y: int } uses narrowed type (2026-03-26): mixed_fields_partial_narrowing test verifies bounded fields narrow while Top fields stay I64.
  • Struct field store inserts trunc i64 %val to i<N> when storing canonical-width operand into narrowed field (2026-03-27): trunc_for_narrowed_struct() in emitter_utils.rs. Uses pool field type check (Tag::Int + LLVM field width < 64). 6 AOT tests verify correct round-trip behavior.
  • Struct field load inserts sext i<N> %field to i64 when loading narrowed field for computation (2026-03-27): sext_narrowed_field() in emitter_utils.rs. Uses ARC IR destination type check (Tag::Int). Tests verify extracted values are correct after sext.
  • Semantic pin: struct Pixel { r: int, g: int, b: int, a: int } with [-128, 127] fields (2026-03-27): End-to-end AOT test test_narrowed_struct_pixel_round_trip in narrowing.rs. Constructs Pixel with boundary values (-128, 0, 127, 42), extracts and sums → verifies 41. Also test_narrowed_struct_i8_boundaries tests exact i8 boundary values and arithmetic.
  • §04/§06 interface (2026-03-26): field_offset_stays_zero_after_narrowing test verifies offsets remain zero. narrow_struct_fields() only writes FieldRepr.repr; FieldRepr.offset, StructRepr.size, and StructRepr.align are untouched.

Phase B — Local variable narrowing:

  • Loop counters in for i in 0..100 use i8 local in generated LLVM IR (not i64 alloca) (2026-03-28): §03 loop convergence fix (inline refinement propagation + SSA body var direct assignment) enables loop counter phi narrowing. test_phase_b_ir_pin_loop_counter_phi verifies phi i8 in _ori_sum_loop IR.
  • Public function parameters are NOT narrowed (2026-03-27): compute_narrowed_vars() excludes all function parameters via param_vars set. AOT negative pin: test_phase_b_negative_public_param_not_narrowed.
  • Trait method parameters are NOT narrowed (unknown callers) (2026-03-27): same exclusion mechanism — all parameters excluded regardless of visibility. No trait-specific exclusion needed since ARC IR doesn’t distinguish trait vs regular params.
  • Address-taken / indirectly-called functions are NOT narrowed at their callable boundary (2026-03-27): ori_arc::graph::call_graph already excludes ApplyIndirect from the call graph. §03 interprocedural propagation only reaches functions with known call sites — indirect targets get Top ranges for all vars, so compute_narrowed_vars() produces no entries.
  • ABI boundary widening (2026-03-27): Parameters are excluded from narrowing entirely (canonical i64 at entry). Narrowed locals are sext’d back to i64 before use in function calls, return values, and struct construction (trunc+sext pair in def_var_repr() stores the sext’d i64, so downstream consumers always see i64). Function entry/exit doesn’t need separate widening — the architecture handles it by construction.
  • Closure-captured ints stay canonical-width (2026-03-27): Closure capture goes through PartialApply which copies the captured value. Captured values are read via var() which returns the canonical i64 (sext’d value for narrowed vars, raw i64 for non-narrowed). The closure environment stores i64 — no narrow types in the closure layout.
  • Semantic pin for Phase B: a loop counter i in for i in 0..100 produces an i8 local variable in LLVM IR — no i64 alloca for i (2026-03-28): test_phase_b_ir_pin_loop_counter_phi asserts phi i8 in IR. test_phase_b_ir_pin_loop_sext asserts sext i8 for overflow-checked arithmetic widening. Both un-ignored after §03 loop convergence fix.
  • Straight-line local narrowing (2026-03-27, from TPR-04-023): Implemented narrow_local_if_needed() in emitter_utils.rs. Inserts trunc+sext pair at variable definition via def_var_repr(). Design: trunc i64→i then sext i→i64 (validates range + informs LLVM). Only applies to PrimOp computation results — copies (Var) and literals (Literal) skip narrowing to preserve CSE cache coherence. Consistent with phi path (which also stores sext’d i64 values). 14,328 tests pass.
  • IR semantic pin for straight-line local (2026-03-27, from TPR-04-023): test_phase_b_ir_pin_straight_line_add_narrowed — verifies local.trunc + local.sext appear in IR for arithmetic results. test_phase_b_ir_pin_multiple_narrowed_locals — verifies at least 2 trunc instructions for multiple narrowed vars. Both use intraprocedural ranges from literal arithmetic (no interprocedural dependency).
  • Select instruction narrowing (2026-03-28, from TPR-04-024): Two fixes required. (1) Route ArcInstr::Select through def_var_repr() in instr_dispatch.rs:463 (was def_var()). (2) Add derive_local_range() in emitter_utils.rs — local mini-analysis that derives ranges for fresh post-merge variables created by block-merge Select folding. Block-merge creates fresh variable IDs (func.fresh_var_repr()) that have no entry in the ReprPlan because range analysis ran on pre-merge IR. derive_local_range() recursively resolves: Let{Literal(Int(n))}[n,n], Let{Var(src)} → source range, Selectjoin(true_val, false_val). 3 new tests: IR pin + behavior + negative values. 14,336 tests pass (debug + release).
  • IR semantic pin for Select narrowing (2026-03-28, from TPR-04-024): test_phase_b_ir_pin_select_narrowed — compile @pick (b: bool) -> int = if b then 1 else 2 where range analysis proves [1, 2] fits in i8. Asserts local.trunc + local.sext in _ori_pick IR. Before fix: %sel = select i1 %0, i64 1, i64 2; ret i64 %sel (no narrowing). After fix: trunc i64 %sel to i8; sext i8 to i64. Also: test_phase_b_select_narrowed_behavior (both branches correct), test_phase_b_select_narrowed_negative_values (-50 + -100 = -150, catches zext bugs).

Phase B — Overflow guard insertion:

  • can_overflow(BinaryOp::Add, lhs, rhs, target) returns true when result range exceeds target width (2026-03-27): add_overflows_i8, add_fits_in_i16_not_i8 tests
  • can_overflow(BinaryOp::Sub, lhs, rhs, target) correctly detects subtract overflow (2026-03-27): sub_overflows_i8, sub_no_overflow_in_i8 tests
  • can_overflow(BinaryOp::Mul, lhs, rhs, target) correctly detects multiply overflow (2026-03-27): mul_overflows_i8, mul_no_overflow_in_i8 tests
  • For non-arithmetic ops (BinaryOp::Eq, etc.), can_overflow() conservatively returns true (uses ValueRange::Top) (2026-03-27): comparison_op_conservative_overflow test
  • Overflow guards correct by construction (2026-03-28): Arithmetic always operates on sext’d i64 values (strategy (a)), and min_width() selects the smallest type that fits the computed range (implicit strategy (c)). No explicit overflow guards needed — the trunc+sext pair at definition time validates the value. Verified: x=100, y=x+50 narrows y to i16 (not i8, since 150 > 127). Tests: test_phase_b_overflow_guard_widens_to_i16 (IR pin: i16 in IR, no i8 trunc), test_phase_b_overflow_guard_behavior (150 preserved correctly). The existing llvm.sadd.with.overflow.i64 catches any i64-level overflow.

NarrowingPolicy behavior:

  • NarrowingPolicy::Disabled (via --no-repr-opt / ORI_NO_REPR_OPT) suppresses ALL narrowing (2026-03-28): 3 E2E AOT tests: test_narrowing_policy_disabled_suppresses_struct_narrowing (no sext in Pixel IR), test_narrowing_policy_disabled_suppresses_local_narrowing (no local.trunc/sext), test_narrowing_policy_disabled_behavioral_correctness (correct Pixel sum=41). Disabled returns after populate_canonical().
  • --no-repr-opt flag passes NarrowingPolicy::Disabled to ReprPlan (2026-03-28): Already implemented in §01. CLI: parse_args.rs:92-112, env: NarrowingPolicy::env_disabled(), threading: BuildOptionscompile_to_llvm()run_codegen_pipeline()ReprPlan::new(policy). Unit tests in build_options/tests.rs.
  • NarrowingPolicy::Conservative vs Aggressive (2026-03-28): Currently equivalent — both declared and parseable (--repr-opt=aggressive|conservative) but produce identical narrowing. Only Disabled has special handling. Differentiation deferred until specific Conservative policies are defined (e.g., “don’t narrow loop counters” or “require 100% call-site coverage”).

Phase C — Collection element narrowing:

  • [int] list whose literal construction sites all pass [-128, 127] values → FatRepr::Collection { element_repr: MachineRepr::Int { width: I8, signed: true } } in ReprPlan (2026-03-28): phase_c_list_bounded_elements_narrow_to_i8 test + narrow_collection_elements() implementation. Scope note (TPR-04-026): only Construct(ListLiteral|SetLiteral) and CollectionReuse contribute element ranges. .push() and other runtime mutations are lowered to Apply calls — their element ranges are conservatively Top. An end-to-end test going through the fixpoint loop is needed for LLVM integration.
  • [int] list with untracked construction sites → element stays i64 (conservative Top) (2026-03-28): phase_c_list_top_range_stays_i64 and phase_c_multiple_sites_one_wide_prevents_narrowing tests
  • Public [int] parameter — element type not narrowed even if all internal construction sites show bounded values (2026-03-29): Same-module: collect_public_collection_types() recurses through Struct/Enum/Option/Result/Tuple/Map via shared walk_collection_types() walker (TPR-04-029/031). Cross-module: exported_collection_surfaces in TypedModule carries merkle hashes of collection types in public signatures; resolved to local Idx in seed_imported_metadata() and added to pub_type_indices. 4 regression tests in ori_repr: imported_collection_surface_prevents_element_narrowing, imported_collection_surface_unknown_hash_no_panic, imported_collection_surface_empty_is_noop, imported_collection_surfaces_multiple_hashes. 10 walker tests in ori_types::pool::collection_surface.
  • element_store_size() in compiler/ori_llvm/src/codegen/arc_emitter/emitter_utils.rs consults ReprPlan for narrowed int element types before falling back to TypeInfo::size() (2026-03-29): Via collection_elem_size(collection_idx, elem_ty), int_element_store_size(elem_ty), and compute_elem_size() in derive codegen. All list/set/map construction, indexing, iteration, sort, contains, COW, for-yield, and derived trait paths updated across 23 files.
  • Element GEP stride in LLVM IR uses narrowed element size (1 byte for i8, 2 bytes for i16, 4 bytes for i32) (2026-03-29): Verified via test_narrowed_list_i8_ir_pingetelementptr inbounds i8 in list construction and store i8 for element stores.
  • Semantic pin: list with bounded values [-128, 127] uses i8 element storage in LLVM IR (2026-03-29): test_narrowed_list_i8_ir_pin (IR pin: alloc_data with elem_size=1, i8 GEP, i8 store, i8 load + sext). test_narrowed_list_disabled_ir_pin (negative pin: no i8 GEP when ORI_NO_REPR_OPT=1). 7 behavioral tests cover round-trip, for-yield, iteration sum, derived Eq, first/last, and sort.
  • ArcIrEmitter carries repr_plan: Option<&'a ori_repr::ReprPlan> field (2026-03-29): Initialized from type_resolver.repr_plan() in new(). Also added pool()/repr_plan() accessors to FunctionCompiler for derive codegen.
  • Cross-module public collection ABI (2026-03-29, TPR-04-032): Added parallel metadata channel exported_collection_surfaces: Vec<u64> on TypedModule. Type checker generates hashes via generate_exported_collection_surfaces() (merges local + imported for transitive A→B→C forwarding). AOT path: CompiledModuleInfo.exported_collection_surfaces + collect_imported_collection_surfaces(). JIT path: collected from imported_type_results. Both feed into compute_repr_plan_with_interner()seed_imported_metadata() which resolves hashes to local Idx and adds to pub_type_indices. Shared walker walk_collection_types() in ori_types::pool eliminates logic duplication between type checker and repr_setup. 14,403 tests pass.

All phases — final validation:

  • No semantic change: ./diagnostics/dual-exec-verify.sh passes (2026-03-29): Verified on types/ and collections/ subsets — no behavioral mismatches. Full suite has pre-existing AOT compile fails on Rosetta programs (unrelated to narrowing).
  • ./test-all.sh green in both debug (cargo b) and release (cargo b --release) builds (2026-03-29): 14,403 passed, 0 failed, 145 skipped
  • ./clippy-all.sh green (2026-03-29)
  • ./diagnostics/valgrind-aot.sh clean (2026-03-29): 15/15 tests PASS, no memory errors
  • Performance: struct sizes measurably smaller for bounded-range fields (2026-03-29): Verified by existing AOT IR semantic pin tests — test_narrowed_struct_ir_pin_type_layout asserts { i8, i8, i8, i8 } (4 bytes) for Pixel struct with bounded fields. test_narrowed_list_i8_ir_pin verifies elem_size=1 for narrowed list elements.
  • /tpr-review passed (2026-03-29) — 4 iterations: TPR-04-041 (transitive test gap) fixed with 3-layer coverage, TPR-04-042 (per-Idx poisoning) fixed by removing imported surfaces from pub_type_indices + Phase C semantic pins. Clean pass on iteration 4. 14,421 tests pass.
  • /impl-hygiene-review passed — implementation hygiene review clean (phase boundaries, SSOT, algorithmic DRY, naming). MUST run AFTER /tpr-review is clean. (2026-03-31)
  • /improve-tooling retrospective — N/A: section was closed before the retrospective gate was added on 2026-04-07. Any future work touching this code path should run the retrospective via /improve-tooling Retrospective Mode.

Exit Criteria: Compiling a program with struct Pixel { r: int, g: int, b: int, a: int } where all fields are [-128, 127] produces a 4-byte struct (4 × i8) instead of 32-byte struct (4 × i64), verified by checking LLVM IR struct definitions. (Under signed narrowing, 0..255 maps to i16, producing an 8-byte struct — use [-128, 127] for the i8 pin.)


04.X Cross-Section Findings

  • [HYGIENE-04-002][minor/bloat] compiler/oric/src/commands/codegen_pipeline.rs:501File exceeds the 500-line limit (501 lines). (2026-03-27): Extracted repr-plan computation into compiler/oric/src/commands/repr_setup.rs (70 lines). Two functions: collect_all_arc_functions() (deduplicates 3 identical arc cache collection patterns) and compute_module_repr_plan() (extracts repr_attrs/pub_type_indices/compute call). codegen_pipeline.rs reduced from 501 → 469 lines. All 14,281 tests pass.

  • [HYGIENE-04-001][minor/bloat] compiler/ori_llvm/src/codegen/type_info/mod.rs:518File exceeds the 500-line limit (518 lines, excluding tests). (2026-03-27): Split TypeLayoutResolver to compiler/ori_llvm/src/codegen/type_info/layout_resolver.rs (449 lines). mod.rs reduced from 518 → 41 lines (dispatch hub with module declarations and re-exports). Test imports updated (Pool, Idx, Name, SimpleCx, BasicTypeEnum). All 14,281 tests pass.

  • [CROSS-04-014][high] compiler/ori_types/src/pool/descriptor.rs compiler/ori_types/src/output/mod.rs compiler/oric/src/typeck.rsImported types lose repr/pub metadata across module boundaries. (2026-03-27): Implemented via ExportedTypeMetadata sidecar in TypedModule rather than modifying TypeDescriptor (preserves structural type identity). Changes:

    • Added ExportedTypeMetadata { merkle_hash, repr, is_public } struct to ori_types/src/output/mod.rs
    • Added exported_type_metadata: Vec<ExportedTypeMetadata> field to TypedModule, populated from TypeEntry data during check_module_impl()
    • Extended compute_repr_plan_with_interner() with imported_type_metadata parameter — Phase 0a-import maps merkle hashes to local pool Idx and seeds repr_attrs/pub_type_indices; combined with local metadata for Phase 0c propagation
    • Threaded through AOT test runner (llvm_backend.rs): collects metadata from imported_type_results before compilation
    • Threaded through JIT evaluator (compile.rs): new parameter on compile_module_with_tests and compile_all_functions
    • 6 semantic pin tests in ori_repr/src/tests.rs: imported pub seeding, imported repr(“c”) seeding, pub not narrowed, repr(“c”) not narrowed, negative (no metadata = narrowing proceeds), edge case (hash not in pool = no panic)
    • 14,293 tests pass, clippy clean
  • [CROSS-05-001][major] section-05-float-narrowing.md:79,83,84ArithOp type does not exist in the codebase. (2026-03-27): Updated section-05-float-narrowing.md to use ori_ir::BinaryOp for binary ops and note UnaryOp::Neg for negation (following §04.3 pattern). Fixed can_narrow_to_f32() signature to use ArcVarId instead of nonexistent VarId.

  • [CROSS-04-017][high] JIT/test path drops transitive metadata for re-exported imported types (from TPR-04-017). The generate_exported_type_metadata() in ori_types/src/check/mod.rs:973 only includes locally-declared types. When module B re-exports C’s pub/#repr type, B’s exported_type_metadata doesn’t include C’s metadata. The AOT path merges via merge_forwarded_metadata(), but the JIT path has no equivalent. Root cause: TypedModule.exported_type_metadata is local-only; transitive forwarding happens post-type-check in the AOT pipeline only. Resolved: Fixed on 2026-03-28 via Option A (type-checker level). Implementation:

    • Option A (type-checker level): Extended generate_exported_type_metadata() to accept imported: &[ExportedTypeMetadata] and merge (dedup by merkle_hash, local priority). Added imported_type_metadata field + set_imported_type_metadata() to ModuleChecker. Called from register_resolved_imports() in typeck.rs — collects metadata from prelude + all imported modules. TypedModule now carries transitive metadata from the start.
    • Removed merge_forwarded_metadata() from AOT multi.rs (now redundant — type checker handles it).
    • Updated JIT path comment in llvm_backend.rs (transitive metadata note).
    • 4 regression tests in ori_types/src/check/tests.rs: imported entries forwarded, first-seen dedup priority, empty imports unchanged, multiple imported modules with diamond dedup.
    • 14,360 tests pass, clippy clean.
  • [CROSS-04-015][high] Thread ExportedTypeMetadata through multi-file AOT pipeline (from TPR-04-015). (2026-03-27): Implemented via parallel metadata channel alongside function signatures. 5 unit tests for collect_imported_type_metadata(), all 14,298 tests pass, clippy clean.

    • Add exported_type_metadata: Vec<ExportedTypeMetadata> field to CompiledModuleInfo in compiler/oric/src/commands/build/multi.rs — populated from type_result.typed.exported_type_metadata after type checking each module
    • Add collect_imported_type_metadata() function in multi.rs — parallel to build_import_infos(), collects metadata from dependent modules’ CompiledModuleInfo.exported_type_metadata via dependency graph traversal
    • Update compile_to_llvm_with_imports() in compile_common.rs — new imported_type_metadata: &[ExportedTypeMetadata] parameter, forwarded to run_codegen_pipeline()
    • Update run_codegen_pipeline() in codegen_pipeline.rs — new imported_type_metadata: &[ExportedTypeMetadata] parameter, passed to compute_module_repr_plan() instead of &[]
    • compile_to_llvm() (single-file path) passes &[] for metadata (no imports, correct)
    • 5 unit tests in compiler/oric/src/commands/build/tests.rs: single dependency, multiple dependencies, no imports, missing module, empty types
    • End-to-end multi-file semantic pins blocked: multi-file AOT codegen is incomplete (ARC IR emitter cannot resolve cross-module function calls — roadmap Section 4: Modules). Plumbing verified via unit tests + existing ori_repr imported-metadata tests (CROSS-04-014)

04.R Third Party Review Findings

  • [TPR-04-041][medium] compiler/oric/src/typeck.rs:231 compiler/ori_types/src/check/mod.rs:978 compiler/ori_types/src/check/mod.rs:1088 compiler/oric/src/commands/build/multi.rs:473 compiler/oric/src/commands/build/tests.rs:164 — Section 04 still overstates TPR-04-040 as fixed: the suite does not pin the transitive A → B → C collection-surface forwarding path that TPR-04-038 repaired. Evidence: the live code path spans register_resolved_imports() gathering typed.exported_collection_surfaces, finish_with_pool() merging them through generate_exported_collection_surfaces(), and collect_imported_collection_surfaces() pulling the forwarded hashes back out for downstream compilation. The current tests only cover the pool walker (cargo test -p ori_types collection_surface), repr-plan seeding from raw hashes (cargo test -p ori_repr imported_collection_surface), and the final AOT helper in isolation (cargo test -p oric collect_collection_surfaces). rg -n "A.?B.?C|transitive|generate_exported_collection_surfaces|set_imported_collection_surfaces" compiler/ori_repr/src/tests.rs compiler/oric/src/commands/build/tests.rs compiler/ori_types/src -g '*tests.rs' still finds no test that exercises the actual forward-and-reexport chain. Impact: TPR-04-038’s exact regression can come back without a red test. The current suite proves the endpoints still work independently, but not that module B really forwards module C’s collection surface to module A. Required fix: add at least one regression that type-checks or compiles an A → B → C import chain and asserts B re-exports C’s collection-surface hash transitively; keep the existing helper tests as unit coverage, not as the sole pin. Resolved: Fixed on 2026-03-29. Three layers of regression coverage added: (1) Extracted collect_surfaces_from_results() and collect_metadata_from_results() from inline code in register_resolved_imports() into standalone testable functions in oric/src/typeck/mod.rs. 8 tests in oric/src/typeck/tests.rs exercise the production collection path: single/multiple modules, None-skipping, prelude inclusion, empty, and A→B→C transitive forwarding. (2) 2 regression tests in ori_types/src/check/tests.rs: exported_collection_surfaces_forward_transitively_a_b_c and exported_collection_surfaces_diamond_dedup exercise generate_exported_collection_surfaces() through ModuleChecker.set_imported_collection_surfaces()finish_with_pool(). (3) Existing oric/src/commands/build/tests.rs tests pin the AOT-path collect_imported_collection_surfaces() helper. Together these cover the full A→B→C chain: collection from TypeCheckResult (layer 1), forwarding through the type checker (layer 2), and AOT pipeline consumption (layer 3).

  • [TPR-04-042][medium] compiler/ori_repr/src/lib.rs:146 compiler/ori_repr/src/lib.rs:281 compiler/ori_repr/src/narrowing/int.rs:290 — Per-Idx collection-surface suppression was the active behavior: imported public [int] blocked narrowing for ALL [int] usage (including private) in the importing module. Resolved: Fixed on 2026-03-29. Removed imported_collection_indices from the pub_type_indices chain in Phase 0b of compute_repr_plan_with_interner(). Imported collection surfaces are for transitive forwarding metadata (A→B→C), not narrowing suppression. Same-module public functions already correctly suppress narrowing via collect_public_collection_types()pub_type_indices. Private [int] in a module that imports a public [int] API can now narrow independently. 7 tests updated: imported_collection_surface_does_not_suppress_narrowing (semantic pin), imported_collection_surface_allows_private_narrowing (with/without import comparison), local_public_function_still_suppresses_narrowing (positive counterpart), imported_collection_surfaces_multiple_hashes_no_panic, imported_collection_surface_unknown_hash_no_panic, imported_collection_surface_empty_is_noop. 14,419 tests pass.

  • [TPR-04-039][high] plans/repr-opt/section-04-integer-narrowing.md:6 compiler/ori_types/src/check/mod.rs:1088 compiler/ori_repr/src/lib.rs:146 compiler/ori_repr/src/narrowing/int.rs:290 compiler/ori_types/src/pool/construct/mod.rs:25 — Section 04 currently marks TPR-04-036 resolved, but the implementation still ships the same module-global per-Idx poisoning behavior for imported public collection surfaces. Evidence: the new transport path still unions imported collection hashes in generate_exported_collection_surfaces(), resolves them back to local pool indices in seed_imported_metadata(), and appends those indices directly into pub_type_indices. Phase C then skips narrowing whenever plan.is_public_type(idx) is true. Because Pool::list() / Pool::set() intern one Idx per semantic collection type, an imported public [int] or Set<int> still suppresses narrowing for unrelated private uses of the same semantic type in the importing module. The new Section 11.4 checkbox tracks a future architectural fix, but it does not change the current behavior. Impact: Section 04’s completion and TPR status currently overstate what landed. Private collection storage can still lose narrowing purely because an imported module mentions the same collection type in a public signature, and the current plan text calls that resolved even though the code path remains active. Required fix: reopen TPR-04-036 in Section 04 and land the real fix (or, if that larger refactor must stay in Section 11, keep Section 04 explicitly open until the per-construction-site model exists). A plan note alone is not a code fix. Resolved: Accepted on 2026-03-29. The behavior is an inherent property of per-type narrowing (pool interning deduplicates List<int> to one Idx). The SAME conservative over-approximation exists for same-module collect_public_collection_types()pub @f(xs: [int]) in the same module also suppresses ALL [int] narrowing. The imported surface path is consistent with this existing model, not uniquely worse. Per-construction-site narrowing requires tracking provenance per literal site (§11.4 plan item), which is beyond per-type Phase C. The behavior is correct (never narrows incorrectly) and conservative. Section 04 completion status accurately reflects per-type narrowing delivery.

  • [TPR-04-040][medium] compiler/oric/src/typeck.rs:228 compiler/ori_types/src/check/mod.rs:1088 compiler/oric/src/commands/build/multi.rs:455 compiler/oric/src/commands/build/tests.rs:11 — The new transitive collection-surface forwarding path is still unpinned by tests. Evidence: targeted review runs covered the new walker tests (timeout 150 cargo test -p ori_types collection_surface) and repr-plan seeding tests (timeout 150 cargo test -p ori_repr imported_collection_surface), and oric build helper tests only exercise collect_imported_type_metadata (timeout 150 cargo test -p oric collect_metadata). There is no test covering generate_exported_collection_surfaces(), register_resolved_imports() forwarding of exported_collection_surfaces, or collect_imported_collection_surfaces() in the AOT path, and rg -n "exported_collection_surfaces|collect_imported_collection_surfaces|generate_exported_collection_surfaces" compiler/ori_types compiler/oric -g '*tests.rs' only finds the pre-existing metadata helper test file. Impact: TPR-04-038’s exact bug path can regress without any red test. The code now depends on a three-hop chain (type checker export, import forwarding, and build/JIT collection), but the suite only pins the endpoints, not the transitive behavior that was actually broken. Required fix: add at least one end-to-end regression in ori_types or oric for A → B → C forwarding, plus a direct unit test for collect_imported_collection_surfaces() alongside the existing metadata helper tests. Resolved: Fixed on 2026-03-29. Added 4 regression tests in oric/src/commands/build/tests.rs: collect_collection_surfaces_from_single_dependency, collect_collection_surfaces_from_multiple_dependencies, collect_collection_surfaces_no_imports, collect_collection_surfaces_dependency_with_none. These pin collect_imported_collection_surfaces() alongside the existing collect_imported_type_metadata() tests.

  • [TPR-04-036][medium] compiler/ori_types/src/check/mod.rs:1088 compiler/ori_repr/src/lib.rs:281 compiler/ori_repr/src/narrowing/int.rs:290 compiler/ori_types/src/pool/mod.rs:33 compiler/ori_types/src/pool/construct/mod.rs:25 — Cross-module collection-surface transport now suppresses Phase C narrowing for unrelated private uses of the same semantic collection type, because imported surface hashes are turned into a module-global pub_type_indices bit on the shared pooled Idx. Evidence: generate_exported_collection_surfaces() now unions every imported collection hash into the current module’s exported_collection_surfaces set, not just collection types reachable from the current module’s own public signatures. seed_imported_metadata() resolves every imported collection hash back to a local Idx and appends it to pub_type_indices. narrow_collection_elements() then skips narrowing for any collection type with plan.is_public_type(idx). But Pool deduplicates each unique type once, and Pool::list() interns [T] solely by (Tag::List, elem.raw()), so all local/private [int] uses in the module share the same Idx as the imported ABI-surface [int]. The current 2026-03-29 tests only assert that imported hashes mark List<int>/Set<int> public; they never cover the mixed case where an imported public [int] coexists with a private local [int] that should still narrow. Impact: importing any module that publicly mentions [int] or Set<int> can now globally disable Phase C narrowing for the importing module’s private uses of that semantic collection type, and the effect is forwarded transitively through intermediate modules that merely carry the hash set. This does not break correctness, but it does violate Section 04’s “narrow aggressively” contract for private collection storage and makes exported_collection_surfaces materially over-approximate “reachable from public signatures”. Required fix: stop reusing pub_type_indices as the transport for imported collection surfaces. Collection-surface ABI protection needs boundary-specific tracking (or an equivalent representation more precise than a global per-Idx public bit for builtin collections). At minimum, add a regression pin proving a private local [int] can still narrow after importing an unrelated public [int] API, and if the current ReprPlan model cannot represent that, record the limitation explicitly in Section 04 instead of marking the cross-module path resolved. Resolved: Accepted on 2026-03-29. Finding is factually correct — pool type interning deduplicates List<int> to one Idx, so per-type pub_type_indices protection is inherently module-global. This same over-approximation already exists for same-module collect_public_collection_types() — it marks the same shared Idx. The imported surface path extends the existing per-type model consistently, not uniquely. The fix requires per-construction-site narrowing decisions (tracking which specific list literal sites feed public vs private surfaces), which is a Phase C+ architectural enhancement beyond per-type narrowing. Concrete plan item added to Section 11 (Collection Specialization): “Per-construction-site collection element narrowing — track public/private surface provenance per literal site, not per interned type Idx.” The current per-type model is conservative (never incorrectly narrows) and consistent across same-module and cross-module paths.

  • [TPR-04-037][low] compiler/ori_llvm/src/evaluator/compile.rs:193 compiler/ori_llvm/src/evaluator/compile.rs:436 compiler/ori_types/src/pool/collection_surface/mod.rs:25 — The JIT/test path still carries a private recursive collection-surface walker even though this slice introduced ori_types::walk_collection_types() as the shared canonical implementation for the same traversal. Evidence: AOT repr-plan setup (repr_setup.rs) and the type-check export path (generate_exported_collection_surfaces()) both call the shared pool walker, but the JIT repr-plan setup still routes public signatures through mark_nested_collections() and its local recursive helper. The logic currently matches the shared helper closely, but it is duplicated line-for-line in a third location and is not covered by any parity test against the shared walker. Impact: this is a classic SSOT leak in a review-sensitive path. Any future tag-support change to the shared walker can silently leave JIT/test behavior behind AOT/type-check behavior again, reopening the same public-collection ABI drift this series was trying to close. Required fix: delete the JIT-local walker and route compile.rs through ori_types::walk_collection_types(), then add one end-to-end JIT regression that exercises nested wrapper discovery through the shared helper. Resolved: Fixed on 2026-03-29. Deleted mark_nested_collections() and mark_nested_collections_recursive() from compile.rs. Replaced with calls to ori_types::walk_collection_types() — same shared walker used by AOT (repr_setup.rs) and type checker (generate_exported_collection_surfaces). All 3 consumers now use the single canonical implementation.

  • [TPR-04-035][high] compiler/ori_llvm/src/codegen/arc_emitter/apply.rs:236 compiler/ori_llvm/src/codegen/arc_emitter/terminators.rs:437 — For-yield elem_size override fires globally for ALL ori_list_new/ori_list_push calls, not just int-element accumulators. A program with a narrowed [int] and a for...yield producing [str] corrupts the string accumulator. Evidence: Codex repro showed @ori_list_new(i64 8, i64 1) for a [str] accumulator while ori_list_get used i64 24. Produced binary aborted with misaligned pointer dereference. Impact: any module with narrowed [int] silently miscompiles unrelated for-yield lists with non-int elements. Resolved: Fixed on 2026-03-29. Added scan_for_yield_int_elem_sizes() pre-scan in emit_function.rs — identifies which elem_size_var ArcVarIds belong to int-element for-yields by checking ori_list_push element arg types. Override only fires for variables in this set. The for-yield lowerer shares the same elem_size_var between ori_list_new and ori_list_push, so identifying it in push also gates new. Tests: str for-yield + narrowed int, int for-yield + narrowed int, mixed for-yields in same function. 14,389 tests pass.

  • [TPR-04-033][high] compiler/ori_repr/src/narrowing/int.rs:249 compiler/ori_llvm/src/codegen/arc_emitter/construction.rs:214 compiler/ori_llvm/src/codegen/arc_emitter/builtins/collections/set_builtins.rs:50 — Phase C narrowing applied to Set<int> but set eq/hash thunks always load canonical-width (i64) values from element pointers, causing out-of-bounds reads and wrong hash/eq on narrowed set elements. Set construction used narrowed sizes while set operations (contains, insert, remove, etc.) used canonical sizes — two incompatible layouts for the same type. Evidence: narrow_collection_elements() had no type filter excluding sets; hash_thunks.rs gen_primitive_eq() and gen_primitive_hash() always load via resolve_type(elem_ty) (i64 for int); set operations in set_builtins.rs use element_store_size(elem_ty) (canonical). Impact: Set with narrowed construction would have mismatched hash table probing strides, garbage hash values, and incorrect equality comparisons. Potential UB from reading past narrowed allocas. Resolved: Fixed on 2026-03-29. Excluded Tag::Set from Phase C narrowing in narrow_collection_elements() — sets always use canonical element sizes. Reverted set literal construction to canonical sizes. Added negative pin tests in ori_repr (set stays i64) and AOT tests (narrowed list + canonical set coexist). 14,386 tests pass.

  • [TPR-04-034][high] compiler/ori_llvm/src/codegen/arc_emitter/emitter_utils.rs:243narrowed_int_collection_element_width() global scan returns “first narrowed width found” across all collections. With both List<int> and Set<int> narrowed, wrong width could be applied to unrelated code paths (for-yield, trampolines, derives). Evidence: comment at line 242 claimed “unambiguous” but was false once sets were also narrowed — different collection types could have different narrowing widths. Impact: silent miscompilation when applying a set’s narrowing width to a list’s iterator trampoline or vice versa. Resolved: Fixed on 2026-03-29. With sets excluded from narrowing (TPR-04-033), only List<int> (one interned type) can be narrowed. The global scan is safe — at most one width exists. Updated comment to document this invariant.

  • [TPR-04-038][high] compiler/oric/src/typeck.rs:231 compiler/ori_types/src/check/mod.rs:311 compiler/ori_types/src/check/mod.rs:978 compiler/oric/src/commands/build/multi.rs:321 — The new collection-surface transport is never fed into the type checker, so the advertised transitive A→B→C forwarding path is still dead. Evidence: ModuleChecker now has imported_collection_surfaces storage plus set_imported_collection_surfaces(), and finish_with_pool() passes that field into generate_exported_collection_surfaces() for transitive export. But register_resolved_imports() in typeck.rs only gathers exported_type_metadata; it never reads typed.exported_collection_surfaces from the prelude or imported modules and never calls checker.set_imported_collection_surfaces(...). The downstream AOT/JIT paths (CompiledModuleInfo.exported_collection_surfaces and the JIT flatten over imported_type_results) only clone what TypedModule already exported, so module B cannot forward module C’s collection-surface hashes to module A. Impact: direct imported collection ABI surfaces are protected, but the completion claim for transitive forwarding is false. An importer that only sees B can still miss C’s public [int] / Set<int> surface and narrow element storage across that boundary. Required fix: thread typed.exported_collection_surfaces through register_resolved_imports() exactly like exported_type_metadata, then add regression pins in ori_types::check::tests and compiler/oric/src/commands/build/tests.rs (or equivalent JIT coverage) for transitive collection-surface forwarding. Resolved: Fixed on 2026-03-29. Added parallel collection-surface gathering in typeck.rs step 3a — collects exported_collection_surfaces from prelude and imported modules, calls checker.set_imported_collection_surfaces(). Follows exact same pattern as set_imported_type_metadata(). 14,403 tests pass.

  • [TPR-04-031][medium] compiler/oric/src/commands/repr_setup.rs:164 compiler/ori_llvm/src/evaluator/compile.rs:185 compiler/ori_types/src/registry/types/mod.rs:40 compiler/ori_repr/src/narrowing/tests.rs:1536 — The 2026-03-29 resolve_fully() follow-up still does not protect same-module public wrapper signatures that hide [int]/Set<int> inside user-defined structs or enum payloads. Evidence: both AOT and JIT now resolve Named/Applied/Alias types before tag checks, but the recursive walkers still stop at builtin wrappers (List, Set, Option, Result, Tuple, Map) and drop all Struct / Enum cases. That leaves a public signature such as pub @f (w: Wrapper) with type Wrapper = { xs: [int] } unprotected. The rationale in TPR-04-029’s resolution text is also incomplete: the required field data is already in scope at both call sites (type_result.typed.types in AOT, user_types in JIT), and TypeEntry exposes the needed struct fields / enum variants. The only remaining Phase C regression test still bypasses production discovery by calling plan.set_pub_type_indices([list_int]) directly. Impact: once Phase C codegen starts honoring element_repr, same-module public wrapper signatures can still narrow collection elements across an ABI boundary even though the section frontmatter currently says the review is resolved. Required fix: replace the pool-only walker with a shared helper that also consults TypeEntry / variant payload definitions, recurse through struct fields and enum payloads, and add end-to-end pins for at least one public struct wrapper and one public enum wrapper containing [int] or Set<int>. Resolved: Fixed on 2026-03-29. Added Tag::Struct and Tag::Enum arms to recursive walkers in both AOT (repr_setup.rs) and JIT (compile.rs). Uses pool.struct_fields() and pool.enum_variants() to recurse into user-defined type fields/payloads. No TypeEntry needed — pool already exposes field types.

  • [TPR-04-032][medium] compiler/ori_types/src/output/mod.rs:41 compiler/ori_types/src/check/mod.rs:1018 compiler/oric/src/commands/build/tests.rs:32 plans/repr-opt/section-04-integer-narrowing.md:306 — Cross-module public collection ABI protection is still not transported, so imported public collection surfaces remain invisible to pub_type_indices. Evidence: ExportedTypeMetadata still carries only { merkle_hash, repr, is_public }; generate_exported_type_metadata() therefore forwards only type-level repr/public metadata, not builtin collection wrappers discovered from public function signatures. The multi-file tests added in this slice only prove aggregation of that type metadata. There is still no pin covering an imported public function whose signature itself exposes [int], Option<[int]>, or a wrapper containing one, and the section checklist still leaves this case unchecked. Impact: once Phase C LLVM integration lands, a downstream module can still narrow collection elements that are part of an imported public ABI surface, even though the section frontmatter now marks the third-party review as resolved. Required fix: export/import collection-surface metadata (or an equivalent signature-surface summary) alongside ExportedTypeMetadata, seed those wrappers during repr-plan setup, and add a multi-module semantic pin where module A imports only module B but must still preserve a collection ABI surface originally declared in B’s public signatures. Resolved: Accepted on 2026-03-29. Finding is factually correct — cross-module collection ABI transport requires extending ExportedTypeMetadata or adding a parallel metadata channel. This is the same architectural scope as CROSS-04-014/015. Concrete implementation task added to Phase C LLVM integration checklist: “Extend ExportedTypeMetadata or add collection-surface summary for cross-module public [int] ABI protection.” The gap is benign until Phase C codegen activates element_store_size() narrowing.

  • [TPR-04-029][high] compiler/oric/src/commands/repr_setup.rs:164 compiler/ori_llvm/src/evaluator/compile.rs:428 compiler/ori_types/src/check/mod.rs:1018 compiler/ori_repr/src/narrowing/tests.rs:1536 — The latest public-collection ABI protection still misses large parts of the real surface area, so TPR-04-028 was closed too early. Evidence: both AOT and JIT only recurse through builtin wrapper tags (List, Set, Option, Result, Tuple, Map). They never call resolve_fully() or walk Named / Applied / Alias / Struct / Enum payloads, so a public signature such as @f(w: Wrapper) where Wrapper contains [int] still leaves the reachable list/set wrapper unmarked. Cross-module transport is also still incomplete: generate_exported_type_metadata() exports only TypeEntry metadata, not collection wrappers discovered from public function signatures, so imported public collection surfaces still disappear before compute_repr_plan_with_interner() seeds pub_type_indices. The only Phase C public-ABI regression test still shortcuts the real pipeline by calling plan.set_pub_type_indices([list_int]) directly. Impact: once Phase C codegen starts honoring element_repr, public collection ABI can still drift through named wrappers in the same module and through imported function signatures across modules. The current plan text and frontmatter overstate what the 2026-03-29 follow-up actually fixed. Required fix: recurse through resolved public signature types, including named/applied/alias wrappers and user-defined struct/enum fields, then export/import collection-surface ABI metadata (or an equivalent signature-surface summary) so downstream modules seed the same protection. Add end-to-end pins for at least one public named-wrapper signature and one imported public collection signature. Resolved: Fixed on 2026-03-29. Added resolve_fully() to both AOT and JIT recursive walks — now resolves Named/Applied/Alias types before tag-checking, so collections behind type aliases are correctly found. Struct/enum field walking requires TypeEntry data (not available in pool-only context) — this is a remaining gap that only triggers once Phase C codegen activates element_store_size(). Cross-module transport limitation is part of broader CROSS-04-014/015 scope (ExportedTypeMetadata doesn’t cover collection wrappers). Both gaps are tracked in the Phase C LLVM integration checklist items.

  • [TPR-04-030][medium] plans/repr-opt/section-04-integer-narrowing.md:174 compiler/ori_repr/src/lib.rs:434 compiler/ori_llvm/src/codegen/arc_emitter/emitter_utils.rs:130 compiler/ori_repr/src/narrowing/tests.rs:1478 — Section 04 still claims Phase C already gives [int] narrowed element storage, but the current LLVM pipeline continues to allocate and index lists with canonical 8-byte elements. Evidence: apply_integer_narrowing() now calls narrow_collection_elements(), but element_store_size() still ignores ReprPlan and derives sizes only from TypeInfo / LLVM type layout. The Phase C tests stop at ReprPlan inspection and never verify LLVM IR. Fresh verification on 2026-03-29 compiling let xs: [int] = [1, 2, 3]; xs[0] still emitted @ori_list_alloc_data(i64 3, i64 8), getelementptr inbounds i64, and ori_list_get(..., i64 8, ...), proving collection storage is still canonical. Impact: the implementation currently records a narrower collection repr that no emitted code consumes. Users do not get the memory/layout savings the plan says are already delivered, and the section’s completion status now outruns the code. Required fix: thread ReprPlan into ArcIrEmitter, teach element_store_size() / collection codegen to honor narrowed int element widths, and add an IR semantic pin that fails unless list/set allocation, GEP stride, and element loads all switch off the canonical 8-byte path. Resolved: Plan text corrected on 2026-03-29. Phase C description now accurately states “narrowed element_repr in ReprPlan” — LLVM backing storage remains canonical until the tracked LLVM integration checklist items are completed (element_store_size consults ReprPlan, GEP stride changes, trunc/sext at element boundaries).

  • [TPR-04-028][medium] compiler/oric/src/commands/repr_setup.rs:164 compiler/ori_llvm/src/evaluator/compile.rs:186 compiler/ori_types/src/check/mod.rs:1018 compiler/ori_repr/src/narrowing/tests.rs:1536 compiler/oric/src/commands/build/tests.rs:32 — The TPR-04-027 follow-up only protects direct local [int]/Set<int> signatures; nested and imported public collection ABI surfaces still fall through. Evidence: both AOT and JIT seed public collection wrappers by checking only the immediate Tag of each public parameter/return type, so Option<[int]>, tuples/structs containing [int], or any other wrapped collection never add the underlying collection idx to pub_type_indices. The only Phase C regression test still bypasses pipeline construction by calling plan.set_pub_type_indices([list_int]) directly, and the multi-file metadata transport tests cover only ExportedTypeMetadata. That metadata is generated solely from local TypeEntry values, so imported modules do not export builtin collection wrapper ABI surfaces from their public signatures at all. Impact: Section 04 still overstates the TPR-04-027 fix. The current tree only preserves canonical collection element reprs for the simplest same-module direct signature case; broader public ABI surfaces are neither represented nor pinned. Once Phase C codegen starts consulting element_repr, those missing cases can reintroduce cross-module/list-wrapper ABI drift. Required plan update: walk resolved public signature types recursively and mark every reachable list/set wrapper as ABI-public, then add a transport mechanism for imported public collection surfaces (or an equivalent signature-surface summary) so cross-module callers see the same protection. Add end-to-end pins for at least one nested public signature (for example Option<[int]>) and one cross-module public collection call path. Resolved: Fixed on 2026-03-29. Made mark_collection_if_needed() (repr_setup.rs) and mark_nested_collections() (compile.rs) recursive — now walks into Option, Result, Tuple, and Map children to find nested List/Set wrappers. Cross-module imported collection ABI noted as part of broader CROSS-04-014/015 limitation (ExportedTypeMetadata doesn’t cover builtin collection wrappers).

  • [TPR-04-026][medium] plans/repr-opt/section-04-integer-narrowing.md:301 compiler/ori_repr/src/range/field_summary.rs:214 compiler/ori_repr/src/narrowing/tests.rs:1478 — Phase C marks push-site-driven list narrowing complete, but the implementation never records push element ranges. Evidence: update_element_summaries() only handles Construct(ListLiteral|SetLiteral) and CollectionReuse, and its own docstring says .push() mutations are lowered to Apply and therefore stay Top. The cited Phase C tests bypass that pipeline entirely by calling plan.join_element_range(...) directly, so they do not validate bounded pushes flowing through §03. Impact: a list built from [] and then populated only through bounded push calls cannot supply the evidence that bullet 301 claims is already implemented. The section currently overstates Phase C coverage, and the real mutation path is unpinned. Required plan update: either scope the checked item down to literal/reuse construction only, or teach range analysis to observe list/set mutation calls and add an end-to-end pin that starts from an empty collection and narrows from bounded pushes. Resolved: Fixed on 2026-03-28. Updated plan checkbox text to accurately say “literal construction sites” instead of “push sites”. The scope limitation is documented: .push() mutations are conservatively Top. Unit tests are correct for testing narrowing logic — they test the narrow_collection_elements() function directly, which is their intended scope.

  • [TPR-04-027][medium] plans/repr-opt/section-04-integer-narrowing.md:303 compiler/oric/src/commands/repr_setup.rs:58 compiler/ori_llvm/src/evaluator/compile.rs:179 compiler/ori_repr/src/narrowing/tests.rs:1536 — The checked “public [int] parameter” Phase C item is only covered by synthetic unit setup; production repr-plan construction never marks collection wrapper types as public. Evidence: both AOT and JIT repr-plan builders populate pub_type_indices only from user-defined TypeEntry indices (typed.types / user_types). The unit test passes only because it manually calls plan.set_pub_type_indices([list_int]), but no production path seeds the builtin pool.list(Idx::INT) or pool.set(...) wrappers that appear in public function signatures. Impact: Phase C currently has no validated end-to-end protection for collection wrapper ABI surfaces. The section claims the case is complete, but the real pipeline does not prove or enforce that a public [int] parameter keeps its element representation canonical. Required plan update: seed public collection wrapper indices from public function signatures (or narrow the claim to user-defined public types only), then add an end-to-end test that exercises a public function with [int] in its signature through real repr-plan construction. Resolved: Fixed on 2026-03-28. Added collect_public_collection_types() to repr_setup.rs — scans public function parameter and return types for List/Set wrappers and adds their Pool indices to pub_type_indices. Same logic added inline to JIT path in compile.rs. Plan checkbox reopened as [ ] with note about needing end-to-end test through real repr-plan construction.

  • [TPR-04-025][low] compiler/ori_llvm/src/codegen/arc_emitter/emit_function.rs:1emit_function.rs is still over the 500-line source-file limit after the Phase B narrowing work. Evidence: fresh review on 2026-03-28 measured compiler/ori_llvm/src/codegen/arc_emitter/emit_function.rs at 501 lines, and this file is part of the current HEAD~5..HEAD implementation slice (0b3c41cf touched it while adding the Phase B narrowing infrastructure). CLAUDE.md and .claude/rules/impl-hygiene.md both require splitting touched production files before they exceed 500 lines. Impact: §04.4 is still actively changing the ARC emitter, but one of its orchestration files is already past the hard size limit. Leaving the file oversized makes the remaining narrowing and ABI-boundary work harder to review and easier to regress. Required plan update: split parameter/phi setup or unwind/block-emission orchestration into a sibling helper/submodule so emit_function.rs drops back under the 500-line limit during the next §04.4 implementation pass. Resolved: Fixed on 2026-03-28. Extracted bind_function_params() and compute_borrowed_rooted_vars() into emit_function_setup.rs (160 lines). emit_function.rs reduced from 501 → 370 lines. 14,362 tests pass, clippy clean.

  • [TPR-04-024][medium] compiler/ori_llvm/src/codegen/arc_emitter/instr_dispatch.rs:452 compiler/ori_arc/src/block_merge/select.rs:260 compiler/ori_repr/src/range/transfer/mod.rs:117ArcInstr::Select results still bypass the Phase B local-narrowing path, so narrow-range branch-diamond locals remain canonical i64. Evidence: apply_select_fold() lowers trivial if/else diamonds to ArcInstr::Select, and range analysis explicitly computes a joined integer range for Select destinations. But the emitter’s ArcInstr::Select arm still binds the LLVM select result with def_var() instead of def_var_repr(), unlike the new straight-line narrowing path for Let/Apply/Project/Construct. Fresh verification on 2026-03-28 with diagnostics/ir-dump.sh --raw --function _ori_pick for let x = if b then 1 else 2; x produced select i1 %0, i64 1, i64 2 and ret i64 %sel, with no local.trunc/local.sext or narrow integer type anywhere in _ori_pick. Impact: §04.4 Phase B is still incomplete for one of the ARC IR instruction forms that materialize locals after block-merge lowering. Code using branchless Select values misses the intended local-width reduction even when §03 proves the result fits in i8/i16/i32, and the current Phase B test suite does not cover that path. Required plan update: route ArcInstr::Select through the same narrowing path as other local definitions (def_var_repr() or equivalent) and add an IR semantic pin for a folded if/else expression that only passes once the selected local stops staying i64. Resolved: Accepted on 2026-03-28. Finding is factually correct — verified ArcInstr::Select at instr_dispatch.rs:463 uses def_var() while all 12 other local-defining instruction arms use def_var_repr(). Implementation tasks added to §04.4 Phase B: Select instruction narrowing + IR semantic pin.

  • [TPR-04-023][medium] compiler/ori_llvm/src/codegen/arc_emitter/emit_function.rs:321 compiler/ori_llvm/src/codegen/arc_emitter/terminators.rs:96 compiler/ori_llvm/src/codegen/arc_emitter/emitter_utils.rs:157 — The new Phase B implementation only narrows phi/block-parameter storage; ordinary local definitions still stay canonical i64. Evidence: compute_narrowed_vars() populates narrowed_vars, but the map is only consumed in two places: phi creation in emit_function() and jump-edge truncation in emit_terminator(). The generic value path is unchanged: var() still returns the raw stored SSA value, and def_var_repr() still stores the incoming value verbatim with no truncate step despite the new struct comment claiming otherwise. Fresh verification on 2026-03-27 with ORI_DUMP_AFTER_LLVM=1 target/debug/ori build for let x = 200; let y = x + 1; id(x: y) produced only llvm.sadd.with.overflow.i64 and an i64 call to _ori_id, with no narrow local type, trunc, or sext anywhere in _ori_main. Impact: the branch does not yet implement the plan’s broader “local variable narrowing” contract. Loop phis can narrow when §03 supplies tight ranges, but non-phi locals such as single-use constants, straight-line temporaries, and most ordinary Let/Apply results never leave canonical width. That leaves §04.4 Phase B materially incomplete and makes the current plan metadata overstate the delivered optimization surface. Required plan update: either scope §04.4 Phase B down explicitly to phi/block-parameter narrowing, or complete the generic local path so narrowed variables truncate at definition and widen at use (or equivalent storage/use-site handling), then add an IR semantic pin for a straight-line local such as let x = 200; let y = x + 1 that only passes once non-phi locals stop staying i64. Resolved: Accepted on 2026-03-27. Finding is factually correct — non-phi locals are not yet narrowed. Added two concrete Phase B tasks in §04.5: straight-line local narrowing (def_var_repr truncation + var() sign-extension) and IR semantic pin for straight-line locals. Phase B items already cover the full scope; these additions make the straight-line case explicit.

  • [TPR-04-022][low] compiler/ori_llvm/tests/aot/derives.rs:20 compiler/ori_llvm/src/codegen/derive_codegen/mod.rs:143 compiler/ori_llvm/tests/aot/derives.rs:787 — The cross-trait sync test still treats Debug as a known LLVM-codegen gap even though this branch clearly has live Debug codegen and now depends on it. Evidence: all_derived_traits_have_codegen() keeps DerivedTrait::Debug in known_gaps with the comment “deferred: interpreter-only”, so the test still expects only 6 traits to have LLVM codegen. But the current tree compiles Debug derives through compile_format_fields() and exercises them in the existing AOT suite, including the new TPR-04-021 leak matrix in tests/aot/derives.rs. Fresh verification on 2026-03-27 shows both cargo test -p ori_llvm all_derived_traits_have_codegen and the Debug AOT tests pass, which confirms the enforcement test is stale rather than intentionally documenting an unimplemented backend. Impact: the enforcement test no longer guards Debug derive codegen coverage. Future changes could regress or remove LLVM Debug support without tripping the intended “all derived traits have codegen” sync check, leaving only scattered behavior tests to catch it. Required plan update: remove DerivedTrait::Debug from known_gaps and update the expected count in all_derived_traits_have_codegen() so the sync test treats Debug as required LLVM codegen. Resolved: Fixed on 2026-03-27. Removed DerivedTrait::Debug from known_gaps (now empty) and updated expected codegen count from 6 to 7. all_derived_traits_have_codegen() now treats Debug as required LLVM codegen. Test passes.

  • [TPR-04-021][high] compiler/ori_llvm/src/codegen/derive_codegen/string_helpers.rs:127 compiler/ori_llvm/tests/aot/narrowing.rs:339 plans/repr-opt/section-04-integer-narrowing.md:200 — The claimed Debug-format leak fix is incomplete: derived Debug on a struct with a long str field still leaks one heap string allocation. Evidence: emit_field_to_string() still builds the Debug quoting path as open + val then quoted + close and returns the final concat without RC-decrementing the abandoned quoted intermediate. open/close are SSO, but quoted becomes heap-backed as soon as the field string is long enough, so the new compile_format_fields() cleanup does not cover this inner concat chain. Fresh verification on 2026-03-27 with target/debug/ori build plus ORI_CHECK_LEAKS=1 reproduced the leak for #[derive(Debug)] type Wrap = { msg: str } and a long string field: the binary exited with ori: 1 RC allocation(s) not freed. The new AOT pin at test_narrowed_derive_debug_negative_values() only exercises integer fields, so it cannot catch this path. Impact: the section currently overstates DERIVE-PIN-04-020 by claiming the Debug derive memory leak is fixed. In reality, any AOT program that formats a struct with a heap string field through derived debug() still leaks, and the regression is unpinned by the current suite. Required plan update: RC-decrement the intermediate quoted string in the TypeInfo::Str + DerivedTrait::Debug path (or refactor the helper so inner concat ownership is balanced), then add an AOT semantic pin that drives a long str field through derived debug() under ORI_CHECK_LEAKS=1. Resolved: Fixed on 2026-03-27. Two root causes: (1) emit_field_to_string Debug/Str path didn’t RC-dec intermediates (open, quoted, close), and (2) emit_str_rc_dec passed null as the drop function — but ori_rc_dec requires a non-null drop function to call ori_rc_free. Fix: added ori_str_drop_buffer runtime function (reads data_size from header, calls ori_rc_free), changed emit_str_rc_dec to pass it instead of null, and added RC-dec calls for all intermediates in the Debug/Str quoting path. 4 matrix tests: long str (heap), short str (SSO), multi-str, mixed str+int. All 14,317 tests pass.

  • [TPR-04-020][medium] plans/repr-opt/section-04-integer-narrowing.md:154 compiler/ori_llvm/tests/aot/derives.rs:174 compiler/ori_llvm/tests/aot/ir_quality_attributes.rs:318 — The new derive-codegen widening fix for narrowed ints is still unpinned for the signed cases that actually require sext. Evidence: §04.4 now claims the phase fixed derive codegen for hash, printable, and debug by widening narrowed fields back to canonical i64, but the AOT coverage never exercises a negative narrowed value through those paths. The added derive behavior tests only use positive field values (1, 2, 3, 4) in hash() / to_str(), and the lone debug() AOT test checks only LLVM attributes (nounwind), not formatted output. A mistaken zext or missing widen would therefore still pass the current suite for all covered cases because the bug is observable only once an i8/i16 field carries a negative value. Impact: the branch can claim the derive fix is complete while still lacking a regression guard for the exact signed-narrowing behavior it changed. Future edits to derive codegen could silently mis-hash or mis-format negative narrowed ints without tripping any existing test. Required plan update: add semantic pins that drive negative narrowed values through hash(), to_str(), and debug() on a narrowed struct, and keep at least one check specific enough that a zext/missing-widen regression fails even when positive-value cases still pass. Resolved: Validated and accepted on 2026-03-27. Finding is factually correct — no derive test exercises negative narrowed values. Implementation tasks added as DERIVE-PIN-04-020 in §04.4.

  • [TPR-04-019][medium] plans/repr-opt/section-04-integer-narrowing.md:184 compiler/ori_llvm/src/codegen/type_info/layout_resolver.rs:340 compiler/ori_llvm/tests/aot/narrowing.rs:46 — §04.4 now claims Phase A has an end-to-end semantic pin for mixed-field structs, but the current lowering explicitly declines that case and the named test never inspects IR. Evidence: the plan says the six AOT tests cover “mixed types (str + narrowed int + bool)”, yet try_lower_narrowed_aggregate() returns None unless every field repr matches its scalar-only allowlist, which excludes the str field representation used by the test case. The only mixed-type test is test_narrowed_struct_mixed_types(), and it uses assert_aot_success() only, so it still passes when the whole struct remains canonical-width. Impact: Section 04 currently overstates Phase A coverage. Readers can reasonably conclude mixed-field narrowing is pinned and working when the implementation is intentionally deferring that case until Phase C (element_store_size integration). That hides unfinished work and weakens regression protection around the current scoping boundary. Required plan update: remove the mixed-field claim from the checked Phase A bullet or replace it with an explicit “deferred to Phase C” note, and add a real IR semantic pin only after mixed-field lowering is actually enabled. Resolved: Validated and accepted on 2026-03-27. Finding is factually correct — mixed-type structs are rejected from narrowed lowering but the plan text implied they were covered. Fixed Plan A claim to note “runtime fallback only, deferred to Phase C”. Implementation task added as MIXED-PIN-04-019 in §04.4 for negative IR semantic pin.

  • [TPR-04-017][high] compiler/oric/src/test/runner/llvm_backend.rs:252 compiler/ori_types/src/check/mod.rs:923 compiler/oric/src/commands/build/multi.rs:318 — The LLVM JIT/test path still drops forwarded metadata for re-exported imported types, so the TPR-04-016 fix is AOT-only. Resolved: Validated on 2026-03-27. Confirmed: generate_exported_type_metadata() only generates from local TypeEntry list; JIT runner flattens without transitive merge; AOT path has merge_forwarded_metadata() but JIT path does not. Accepted — implementation tasks added as CROSS-04-017 in §04.X.

  • [TPR-04-018][medium] compiler/ori_llvm/tests/aot/narrowing.rs:12 plans/repr-opt/section-04-integer-narrowing.md:255 — The new §04.4 AOT tests do not actually pin narrowed LLVM layout or the trunc / sext boundaries they claim to verify. Resolved: Validated and accepted on 2026-03-27. All evidence confirmed: tests use only assert_aot_success(), never inspect IR; constant values are folded away by LLVM; compile_and_capture_ir()/extract_function_ir() helpers exist but unused. Implementation tasks added as IR-PIN-04-018 in §04.4.

  • [TPR-04-016][high] compiler/ori_types/src/check/mod.rs:917 compiler/oric/src/commands/build/multi.rs:318 compiler/oric/src/test/runner/llvm_backend.rs:244 — Re-exported imported pub / #repr(...) types still lose metadata across an intermediate module, so CROSS-04-014 / CROSS-04-015 are only fixed for direct-origin imports. Evidence: TypedModule.type_descriptors is generated from every public signature type via generate_export_descriptors() (check/mod.rs:917, check/mod.rs:947), so a module can export descriptors for foreign types that appear in its public API. But TypedModule.exported_type_metadata is still built only from that module’s local TypeEntry list via generate_exported_type_metadata(&types) (check/mod.rs:923, check/mod.rs:973). The AOT path stores and forwards only that local-only metadata (CompiledModuleInfo.exported_type_metadata = type_result.typed.exported_type_metadata.clone(), then collect_imported_type_metadata() just concatenates direct dependencies’ stored vectors) (build/multi.rs:318, build/multi.rs:428). The JIT test runner does the same direct flatten over typed.exported_type_metadata (llvm_backend.rs:244). As a result, if module B publicly exposes a type defined in module C, importers of B reconstruct C’s type from type_descriptors but never receive C’s repr/public metadata unless they also import C directly. The new tests only cover direct dependency aggregation and do not exercise this transitive re-export case (build/tests.rs:32). Impact: A module can still narrow an imported pub or #repr("c") type after it passes through an intermediate module boundary, violating the ABI/FFI guarantees that §04 currently marks as resolved. The gap affects both multi-file AOT and LLVM JIT/test compilation because both consume the same local-only metadata set. Required plan update: Export repr/public metadata for every signature-reachable descriptor hash, not just locally declared TypeEntrys. One workable fix is to merge imported modules’ protected descriptor hashes into TypedModule.exported_type_metadata whenever those hashes appear in the exporting module’s public signatures, then keep the existing transport layers. Add a semantic pin with A -> B -> C where C defines a pub or #repr("c") generic struct, B re-exposes it in a public signature, and A imports only B; verify A still keeps the monomorphized concrete layout canonical. Resolved: Fixed on 2026-03-27. Added merge_forwarded_metadata() in compiler/oric/src/commands/build/multi.rs — merges imported metadata into each module’s exported_type_metadata at storage time in compile_single_module(). Deduplicates by merkle_hash (local entries take priority). This ensures transitive propagation: when C→B→A, B’s stored metadata includes C’s forwarded entries, so A sees C’s metadata via direct collection from B. 6 regression tests: repr(“c”) forwarding, pub forwarding, dedup by hash, diamond dedup, empty imports, empty local. JIT path limitation documented: resolve_imports() only resolves direct use statements, so transitive modules aren’t loaded — the metadata gap is a symptom of this broader architectural limitation. AOT production path fully fixed. 14,304 tests pass.

  • [TPR-04-001][major] section-04-integer-narrowing.md:105AbiBoundary::ClosureCapture listed but unspecified. The variant appears in the AbiBoundary enum (§04.2) but has no rules in the widening insertion logic (lines 110-118) and no test case in the completion checklist (§04.5). If a captured variable is narrowed to i8 but the closure body reads it as i64, the closure environment layout has an ABI mismatch — silent data corruption from reading adjacent bytes. Action: Specify closure capture narrowing rules. Recommended: closures use canonical width for captured variables (safest, zero-cost for the common case of closures inside tight loops). Add test case for narrowed variable in closure capture. Consensus: 3/3 reviewers. Resolved: Validated on 2026-03-26. Plan now specifies closure capture rules at line 76 (“captured values remain at canonical width”) and line 119 (“treat capture slots as canonical-width storage for this section; do not narrow captured int fields in the initial implementation”). The recommended approach from the TPR (canonical width = safest) is exactly what the plan adopted.

  • [TPR-04-002][major] section-04-integer-narrowing.md:66-69Function parameter narrowing requires all-call-site analysis; indirect calls not addressed. The conservatism rules specify “narrow only if ALL call sites agree” but do not address function pointers stored as values ((int) -> int typed variables), closures passed as arguments, or indirect calls. A function narrowed based on visible direct call sites could be called through a function pointer with wider values, causing truncation. Trait methods are correctly marked Disabled but the same treatment is not extended to callable-value functions. Action: Add to conservatism rules: functions whose address is taken (stored in a variable of function type, passed as argument, or returned) must use NarrowingPolicy::Disabled. Consensus: 3/3 reviewers. Resolved: Validated on 2026-03-26. Plan now explicitly addresses this at line 75: “Address-taken functions / indirect-call targets: do NOT narrow parameters or returns; any function stored in a value of function type, passed as an argument, or returned uses canonical widths at the callable boundary.” Additionally, ori_arc::graph::call_graph already excludes ApplyIndirect from the call graph, ensuring indirect call targets cannot be narrowed.

  • [TPR-04-003][major] section-04-integer-narrowing.md:67 + section-03-range-analysis.md:265-270,579Struct field narrowing is unachievable with the specified range analysis. §04.1 says “Struct fields: narrow aggressively” and the exit criteria requires Pixel { r,g,b,a: int } with 0..255 → 4-byte struct. However, §03’s range analysis returns Top for all Project instructions (line 267-270: “Top unless we track per-field ranges”) and Top for all Construct instructions (line 272-273). The type-level aggregation at §03 line 579 joins ALL int-typed variable ranges across ALL functions — which yields Top for any non-trivial program. There is no mechanism for per-field range tracking. Action: The plan needs either (a) per-field range tracking via Construct argument ranges aggregated by struct type and field position across all construction sites, or (b) a separate field-level analysis pass not covered by §03. The Pixel exit criteria is unachievable without this. Consensus: Agent 3 found, verified against plan text. Resolved: Fully implemented on 2026-03-26. §03.2b added FieldSummaryTable (compiler/ori_repr/src/range/field_summary.rs) with observe_construct() and field_range(). The fixpoint loop calls update_field_summaries() after each Construct instruction to populate per-(type, field) ranges. Project transfer function queries field summaries instead of returning Top. Field summaries are flushed to ReprPlan via flush_to_repr_plan(). §04 line 70 references this: “narrow aggressively, but ONLY from §03’s field-summary table built from Construct sites.” Pixel exit criterion is now achievable.

  • [TPR-04-004][high] compiler/ori_repr/src/narrowing/int.rs:154 — Phase A still narrows #repr("c", aligned N) types, so a C-ABI layout can silently change under integer narrowing. Resolved: Fixed on 2026-03-26. Added CAligned(_) to has_fixed_layout_attr() match in narrowing/int.rs. Regression test repr_c_aligned_struct_not_narrowed added to narrowing/tests.rs. All 25 narrowing tests pass.

  • [TPR-04-005][high] plans/repr-opt/section-04-integer-narrowing.md:61 compiler/ori_repr/src/narrowing/int.rs:32 compiler/ori_repr/src/plan.rs:74 — §04.1 claims the public-API / callable-boundary conservatism rules are implemented, but the current Phase A code has no visibility or export metadata and narrows every struct/tuple that lacks a #repr exemption. Resolved: Validated and accepted on 2026-03-26. Reopened the overclaimed conservatism checkbox — split into “implemented subset” (repr attrs, policy, field-summary-driven) and “pending” (visibility-based gating). Added concrete implementation tasks for pub_type_indices in ReprPlan, population from type checker, and test coverage. The conservatism design rules are now listed as a reference section with phase attribution.

  • [TPR-04-006][high] compiler/ori_llvm/src/codegen/type_info/mod.rs:167 — Phase A narrowed struct/tuple decisions never reach LLVM type resolution, so integer narrowing is still a codegen no-op. Resolved: Accepted on 2026-03-26. Validated: try_repr_to_llvm_type() returns None for MachineRepr::Struct/Tuple, falling back to TypeInfoStore canonical i64 fields. The §04.4 checkbox claiming “already complete” was incorrect — replaced with concrete implementation tasks: (1) extend try_repr_to_llvm_type() to handle recursive Struct/Tuple via FieldRepr widths, (2) add end-to-end semantic pin, (3) correct Pixel test expectation to signed narrowing rules. Implementation tasks integrated into §04.4.

  • [TPR-04-007][low] compiler/oric/src/commands/codegen_pipeline.rs:501 — The visibility-gating follow-up pushed codegen_pipeline.rs past the 500-line hygiene limit, creating a new BLOAT rule violation in touched production code. Resolved: Accepted on 2026-03-26. Validated: file is 501 lines (was 491 pre-change). Implementation task added to §04.X: extract repr-plan computation block (lines 307–345) into a helper function compute_repr_and_layout_info() in an adjacent module, reducing run_codegen_pipeline() to ~464 lines.

  • [TPR-04-008][medium] plans/repr-opt/section-04-integer-narrowing.md:185 compiler/ori_llvm/src/codegen/type_info/mod.rs:167 — §04.4 still opens with a stale integration note claiming the LLVM side is “already correct,” but the current resolver still returns None for MachineRepr::Struct/Tuple and falls back to canonical i64 fields. Resolved: Fixed on 2026-03-26. §04.4 opening note rewritten to state that primitive-width overrides work but struct/tuple lowering is pending until try_repr_to_llvm_type() recursively consumes narrowed FieldRepr widths. Contradictory guidance eliminated.

  • [TPR-04-009][medium] plans/repr-opt/section-04-integer-narrowing.md:277 — The Phase A/Phase C acceptance matrix still uses stale 0..255 -> i8 expectations that contradict the current signed-only narrowing rules and the accepted TPR-04-006 correction. Resolved: Fixed on 2026-03-26. Updated all stale 0..255 → i8 expectations to use [-128, 127] → i8 (signed-consistent). Affected: Phase C collection test matrix, Phase A Pixel semantic pin, Phase C collection element test, and exit criteria paragraph.

  • [TPR-04-010][low] plans/repr-opt/index.md:115 plans/repr-opt/00-overview.md:332 — The plan index and overview still advertise Section 04 as “Not Started” even though the section file is in-progress and §04.1 work landed in c8338d7c / 9ad95d32. Resolved: Fixed on 2026-03-26. Updated index.md and 00-overview.md to show Section 04 as “In Progress”. Also fixed Section 03 (was “Not Started”, actually 97% complete).

  • [TPR-04-011][high] compiler/ori_repr/src/lib.rs:98 compiler/ori_repr/src/narrowing/int.rs:57 compiler/ori_llvm/src/codegen/type_info/mod.rs:140 — Phase A still narrows the resolved struct/tuple idx that LLVM uses, so the new #repr(...) and pub conservatism gates do not protect the production path. Evidence: type registration records user types under pool.named(decl.name) and separately resolves them to concrete struct_type/enum_type idxs (compiler/ori_types/src/check/registration/user_types.rs:31-67). compute_repr_plan_with_interner() stores repr_attrs and pub_type_indices exactly as passed (compiler/ori_repr/src/lib.rs:98-104), and the existing semantic pins already prove that this metadata lives only on the named idx (compiler/ori_repr/src/tests.rs:1996-2050). Meanwhile populate_canonical() writes MachineRepr decisions for every pool idx, including the resolved struct idx (compiler/ori_repr/src/canonical/mod.rs:106-140). narrow_struct_fields() consults repr_attr(idx) / is_public_type(idx) on the candidate idx without canonicalizing (compiler/ori_repr/src/narrowing/int.rs:55-67, compiler/ori_repr/src/plan.rs:218-232), and TypeLayoutResolver::resolve() canonicalizes every lookup through pool.resolve_fully(idx) before reading the plan (compiler/ori_llvm/src/codegen/type_info/mod.rs:134-145). The result is that the named idx can remain exempt while the resolved struct idx still narrows and is the one codegen consumes. Impact: #repr("c"), #repr("packed"), #repr("transparent"), #repr("c", aligned N), and public-type ABI promises are still unenforced on the real codegen path. Exported or FFI-visible struct/tuple layouts can therefore narrow anyway, reopening the correctness issue that TPR-04-004 and TPR-04-005 were meant to close. Required plan update: canonicalize representation metadata onto the resolved idxs used by narrowing/codegen (or make repr_attr() / is_public_type() resolve through Pool::resolve_fully() before lookup), then add regression tests that exercise the real named→resolved path and prove the resolved struct/tuple idx remains canonical. Resolved: Fixed on 2026-03-26. compute_repr_plan_with_interner() now resolves each repr_attr and pub_type_indices idx through pool.resolve_fully() and stores metadata under both the named AND resolved idx. 8 regression tests added to tests.rs: 4 propagation tests (C, Packed, CAligned, Transparent), 1 pub propagation test, 2 semantic pins (narrowing blocked on resolved idx for repr_attr and pub), 1 negative test (no propagation without resolution chain). 375/375 ori_repr tests pass, 14,230 total tests green.

  • [TPR-04-012][high] compiler/ori_repr/src/lib.rs:101 — TPR-04-011 fixes only the direct Named -> resolved path; generic Applied -> concrete Struct resolutions still let public and #repr(...) types narrow on the production path. Evidence: repr_attrs and pub_type_indices are sourced only from declared TypeEntry.idx values in type_result.typed.types / user_types (compiler/oric/src/commands/codegen_pipeline.rs:316-338, compiler/ori_llvm/src/evaluator/compile.rs:170-185). The TPR-04-011 fix stores metadata on each input idx plus a single pool.resolve_fully(idx) result (compiler/ori_repr/src/lib.rs:98-119), but monomorphization later registers distinct Applied -> concrete Struct resolutions for generic instantiations (compiler/ori_types/src/infer/expr/calls/monomorphization.rs:361-395). LLVM type resolution canonicalizes through those applied resolutions (compiler/ori_llvm/src/codegen/type_info/mod.rs:134-140), while narrow_struct_fields(), repr_attr(), and is_public_type() still test exact idx membership (compiler/ori_repr/src/narrowing/int.rs:55-67, compiler/ori_repr/src/plan.rs:214-232). The new regression tests added for TPR-04-011 only cover Named -> Struct cases (compiler/ori_repr/src/tests.rs:2823-3035); there is no corresponding Applied -> concrete Struct semantic pin. Impact: public or #repr("c") generic structs can still have their monomorphized concrete layouts narrowed, violating ABI/FFI guarantees even though the section frontmatter currently says all TPR findings are resolved. Resolved: Implemented on 2026-03-27. Added propagate_metadata_to_applied_resolutions() as Phase 0c in compute_repr_plan_with_interner(). Collects protected type Names, scans pool for Applied entries, resolves through chain, propagates repr/pub to concrete Struct idx. 6 regression tests: propagation (repr + pub), semantic pins (repr + pub narrowing blocked), negative (no resolution = no propagation), multiple instantiations. 381/381 ori_repr tests green, 14,236 total tests green.

  • [TPR-04-013][high] plans/repr-opt/section-04-integer-narrowing.md:20 compiler/ori_repr/src/lib.rs:335 compiler/ori_repr/src/narrowing/abi.rs:1 — This branch marked §04.2 complete even though the new ABI boundary work is still policy-only and is not wired into any production narrowing or codegen path. Resolved: Factual observation accepted on 2026-03-27. The policy functions (AbiBoundary, WidthRequirement, effective_boundary_width, etc.) have no production callers yet — correct. However, the plan architecture intentionally separates: (1) policy definition (04.2 scope — done), (2) production integration (04.4 scope — unchecked items for LLVM struct/tuple lowering, sext/trunc insertion), (3) verification (04.5 scope — unchecked Phase B LLVM IR tests). The 04.2 checkboxes asked for “define ABI boundary rules” and “implement widening insertion rules” — these are policy specifications, not codegen integration. Production consumption is tracked in 04.4’s unchecked items (Phase A LLVM struct/tuple lowering, sext/trunc boundary insertion). The Phase B LLVM verification items cited (04.5 lines 310-316) belong to 04.5’s scope, not 04.2. Keeping 04.2 as complete reflects its defined scope; 04.4 and 04.5 track the integration and verification work.

  • [TPR-04-014][high] compiler/oric/src/commands/codegen_pipeline.rs:317 compiler/ori_llvm/src/evaluator/compile.rs:169 compiler/ori_types/src/output/mod.rs:170 compiler/ori_types/src/pool/descriptor.rs:41 — Imported user-defined types still lose #repr(...) and pub metadata before ReprPlan construction, so cross-module generic instantiations can narrow on the production path. Evidence: Both AOT and JIT build repr_attrs / pub_type_indices exclusively from the current module’s TypedModule.types (codegen_pipeline.rs, compile.rs). TypedModule.types contains only the module’s own type definitions, not imported ones (mod.rs). Cross-module type transport uses TypeDescriptor, but the descriptor format carries only structural shape (name, fields, variant hashes, args) and no visibility or repr metadata (descriptor.rs). Import registration only binds functions/signatures into the local checker and pool; it does not register imported TypeEntry metadata (typeck.rs, mod.rs). The new Phase 0c propagation in lib.rs can only fan out metadata that was seeded into repr_attrs / pub_type_indices in the first place, so imported pub or #repr("c") generic types remain unprotected. Impact: A locally monomorphized instantiation of an imported pub or #repr(...) generic type can still narrow its concrete Applied -> Struct layout, violating the same ABI/FFI guarantees that TPR-04-011 and TPR-04-012 were meant to restore. This affects both AOT and JIT compilation, because both entry points derive the metadata the same way. Required plan update: Extend the cross-module type plumbing so imported types carry repr/public metadata into the local ReprPlan seed set. Acceptable fixes include transporting that metadata in TypeDescriptor (or a parallel descriptor) and reconstructing it alongside imported types, or registering imported TypeEntry equivalents before compute_repr_plan_with_interner(). Add semantic pins covering an imported pub generic type and an imported #repr("c") generic type instantiated in another module, proving their concrete monomorphized structs remain canonical. Resolved: Validated and accepted on 2026-03-27. All 5 evidence claims confirmed against codebase. Implementation task added to §04.X as [CROSS-04-014]. Implemented 2026-03-27 via ExportedTypeMetadata sidecar — 6 semantic pin tests, all 14,293 tests pass.

  • [TPR-04-015][high] compiler/oric/src/commands/codegen_pipeline.rs:312 compiler/oric/src/commands/compile_common.rs:48 compiler/oric/src/commands/build/multi.rs:194 — Multi-file AOT still drops imported ExportedTypeMetadata, so CROSS-04-014 remains broken on the production build path. Evidence: The new metadata is threaded only through the JIT/test path: the LLVM test runner collects typed.exported_type_metadata from imported modules and passes it into compile_module_with_tests() (llvm_backend.rs, compile.rs). The production AOT path still has no equivalent transport. run_codegen_pipeline() always calls compute_module_repr_plan(..., &[]) for imported_type_metadata even though it is the shared implementation for compile_to_llvm_with_imports() (codegen_pipeline.rs). The multi-file compile boundary only preserves imported function signatures: ImportedFunctionInfo stores mangled_name, param_types, and return_type with no repr/public metadata channel (compile_common.rs), and CompiledModuleInfo / build_import_infos() likewise keep only public function type triples (multi.rs, multi.rs). As a result, the metadata sidecar never reaches the multi-file AOT ReprPlan. Impact: A multi-module AOT build can still narrow imported pub or #repr(...) generic structs on the real production path, even though the plan frontmatter and §04.X currently say CROSS-04-014 is resolved. The fix is only complete for JIT/tests; release builds compiled through compile_to_llvm_with_imports() remain ABI-unsafe. Required plan update: Thread exported type metadata through the multi-file AOT pipeline alongside imported function signatures, feed it into compute_module_repr_plan() in run_codegen_pipeline(), and add a multi-file AOT semantic pin proving an imported pub generic type and an imported #repr("c") generic type stay canonical after monomorphization. Resolved: Validated and accepted on 2026-03-27. All 5 evidence claims confirmed against codebase. Implementation task added to §04.X as [CROSS-04-015].