Section 02: Ownership Propagation
Goal: Teach the AIMS pipeline to populate arg_ownership on ApplyIndirect/InvokeIndirect instructions. The ownership must be seeded from the resolved closure target’s contract/signature for the USER arguments (not captures) without creating a second ownership-annotation code path that can drift from direct-call semantics.
Context from Section 01: ApplyIndirect and InvokeIndirect now carry arg_ownership fields, initialized to empty Vec. This section populates them.
Empty arg_ownership semantics: Before annotation (pipeline steps 1-4), arg_ownership is empty — this means “not yet annotated.” The is_owned_position() implementation uses is_none_or() which treats missing entries (empty vec) as Owned for Apply. For ApplyIndirect, the pre-annotation behavior should be all-Borrowed (conservative — uses is_some_and, so empty = all not-owned). After annotation, a non-empty arg_ownership vec means “annotated.” An empty vec after annotation would be a bug (missed propagation).
File size budget for annotate.rs: Currently 258 lines. This section adds ~150 lines (def map builder, ResolvedDef enum, resolve_indirect_arg_ownership, new branches). Projected total ~410 lines — under the 500-line limit but close. If implementation exceeds ~450 lines, extract resolve_indirect_arg_ownership and its supporting types into a sibling closure_resolve.rs module and re-export from rc_insert/mod.rs.
TDD execution order: Although 02.4 is listed last for readability, TDD requires writing the unit test stubs (02.4 Rust tests) FIRST, verifying they fail or do not compile, THEN implementing 02.1-02.3, THEN verifying the tests pass unchanged. The subsection ordering is logical grouping, not execution sequence.
- Add
debug_assert!incompiler/ori_arc/src/aims/emit_rc/arg_ownership.rsat the end ofemit_arg_ownership()(after line 124, the call toannotate_arg_ownership): iterate all blocks infunc, check everyArcInstr::ApplyIndirect { args, arg_ownership, .. }and everyArcTerminator::InvokeIndirect { args, arg_ownership, .. }— assertargs.is_empty() || !arg_ownership.is_empty(). This catches missed propagation in debug builds. - Update doc comments: (a)
annotate_arg_ownership()incompiler/ori_arc/src/rc_insert/annotate.rs:107currently says “Populatearg_ownershipon allApplyandInvokeinstructions” — update to includeApplyIndirect/InvokeIndirect. (b) Module doc incompiler/ori_arc/src/aims/emit_rc/arg_ownership.rs:3currently says “Populatesarg_ownershiponApply/Invokeinstructions” — update to includeApplyIndirect/InvokeIndirect.
02.1 Extend annotate_arg_ownership for indirect calls
The existing annotate_arg_ownership() in compiler/ori_arc/src/rc_insert/annotate.rs (258 lines, well under 500 limit) loops over ArcInstr::Apply (body, lines 148-167) and ArcTerminator::Invoke (terminator, lines 170-188) but skips ApplyIndirect and InvokeIndirect. The AIMS pipeline calls it via emit_arg_ownership() in compiler/ori_arc/src/aims/emit_rc/arg_ownership.rs (125 lines). The indirect path must be added without duplicating the monomorphization merge or builtin/type-qualified override rules that the direct path already centralizes.
Approach: Keep indirect-call annotation co-located with Apply/Invoke, but resolve the closure to a PartialApply target/capture list and then reuse the same normalized AnnotatedSig + builtin override machinery that direct calls already use. Do NOT create a second path that reads raw MemoryContracts directly for indirect calls — that would drift from emit_arg_ownership()’s monomorphization merge and from annotate_arg_ownership()’s type-qualified builtin overrides.
- Preserve one normalized ownership source: keep
emit_arg_ownership()responsible for building the normalizedAnnotatedSigmap (including monomorphization merge). Ifannotate_arg_ownership()needs extra context for indirect calls, thread only the minimal additional resolver context needed to find the closure target/captures. Do NOT add a parallel raw-MemoryContractlookup path for indirect calls.
Rationale:// Pseudocode shape: keep `sigs` authoritative, add only resolver context. pub fn annotate_arg_ownership(/* ... */, sigs: &FxHashMap<Name, AnnotatedSig>, /* ... */)emit_arg_ownership()already performs conservative monomorphization merging. Re-implementing that in the indirect path would be drift-prone. - Define
ResolvedDefenum incompiler/ori_arc/src/rc_insert/annotate.rs(place afterConsumingCtxstruct, before free functions). This is a small enum (~15 lines) that captures only the fields needed for closure resolution — NOT a reference to&ArcInstr(which would conflict with the mutable borrow):enum ResolvedDef { PartialApply { func: Name, capture_args: Vec<ArcVarId> }, Alias(ArcVarId), BlockParam { predecessors: Vec<ArcVarId> }, Other, } - Implement
build_closure_def_map()incompiler/ori_arc/src/rc_insert/annotate.rs(place afterResolvedDef). Signature:fn build_closure_def_map(func: &ArcFunction) -> FxHashMap<ArcVarId, ResolvedDef>. Two-pass construction: (1) walk&func.blocksto recordPartialApplytargets/capture-args,Let { value: Var(src) }aliases, and all other instructions asOther; (2) walk allJump { target, args }terminators and for each target block, record each block-param var →BlockParam { predecessors }mapping from the jump args. Note: a similarbuild_definition_mapexists inaims/interprocedural/extract.rs:490(private, returns&ArcInstrreferences) — do NOT reuse it (different return type, would create borrow conflict). Build a specialized version. - Insert def map construction before mutable loop: insert
let def_map = build_closure_def_map(func);BEFORE thefor block in &mut func.blocksloop (afterConsumingCtxcreation at line 144). This precomputes the closure resolution data whilefuncis still immutably borrowable. Borrow compatibility:ConsumingCtxborrowsfunc.var_types(a field separate fromfunc.blocks), sobuild_closure_def_map(&*func)before the loop and&mut func.blocksinside the loop are compatible — Rust’s borrow checker allows this becauseblocksandvar_typesare disjoint fields. - Add
ApplyIndirectbranch to the body instruction loop (afterApplybranch, ~line 166):
Note:if let ArcInstr::ApplyIndirect { closure, args, arg_ownership, .. } = instr { *arg_ownership = resolve_indirect_arg_ownership( *closure, args, &def_map, sigs, &consuming_ctx, builtins, ); }resolve_indirect_arg_ownershipmust resolve the indirect callee to(target, capture_count)via the precomputeddef_mapand then reuse the same ownership computation as direct calls over the FULL logical arg list[captures..., user_args...], slicing off the capture prefix afterward. - Add
InvokeIndirectbranch to the terminator handling (afterInvokebranch, ~line 187):- Same pattern: resolve ownership from the closure’s contract via
resolve_indirect_arg_ownership
- Same pattern: resolve ownership from the closure’s contract via
- Conservative default: When the closure’s contract cannot be resolved → all args
Borrowed. This matches the “caller retains cleanup” model that was already implicitly in use.
02.2 Closure contract resolution
The key challenge: at an ApplyIndirect call site, the closure is a runtime value (ArcVarId). We need to determine which lambda function it points to (if statically known) and retrieve that lambda’s MemoryContract.
-
Implement
resolve_indirect_arg_ownership()— new function incompiler/ori_arc/src/rc_insert/annotate.rs(place afterbuild_closure_def_map, beforeapply_consuming_overrides— i.e., between the def-map builder and the override logic). Signature:fn resolve_indirect_arg_ownership( closure_var: ArcVarId, user_args: &[ArcVarId], def_map: &FxHashMap<ArcVarId, ResolvedDef>, sigs: &FxHashMap<Name, AnnotatedSig>, consuming_ctx: &ConsumingCtx<'_>, builtins: &BuiltinOwnershipSets, ) -> Vec<ArgOwnership>Borrow-safety note:
annotate_arg_ownershipiterates&mut func.blocks, so you cannot pass&ArcFunctioninto the resolver during that loop. Thedef_mapandConsumingCtxare both precomputed before the mutable loop (02.1 items above).ResolvedDefis defined in 02.1.Implementation steps (each is a verifiable sub-step within
resolve_indirect_arg_ownership):- Zero user-arg fast path (FIRST): if
user_args.is_empty(), returnVec::new()immediately — no resolution needed. This handles thunks (zero-arg closures) and avoids unnecessary def-map lookups. - Resolve closure var through SSA defs: implement a local helper
resolve_to_partial_apply(var, def_map, visited) -> Option<(Name, Vec<ArcVarId>)>that followsAlias(src)chains through the def map. Track avisited: &mut FxHashSet<ArcVarId>so loop-carried block params / alias cycles terminate — if a var is re-encountered, returnNone. Do NOT rely on “walk blocks backward” ordering — block order is not a semantic dominance order. - Handle block params / merged closures explicitly: when the resolver hits a
BlockParam { predecessors }, recursively resolve each predecessor. Reuse a merged result ONLY when every incoming value resolves to a compatiblePartialApplyfor this specific call site: same targetName, same capture count, and no disagreement in the post-override user-arg ownership vector after running the full[captures..., user_args...]computation. If incoming edges disagree, or a cycle is hit before proving equivalence, returnNone(which triggers the opaque fallback). - Stop at
PartialApply: once aPartialApply { func: target, capture_args }is found, usecapture_argsdirectly —capture_args.len()is the capture prefix length. - Reuse the direct-call ownership path on the FULL arg list: construct a combined var list
[capture_args..., user_args...]from thePartialApply’s stored capture args and the call site’s user args. Callcompute_arg_ownership(target, combined.len(), sigs, consuming_ctx.interner, &builtins.borrowing, &builtins.consuming_receiver_only, &builtins.protocol)and thenapply_consuming_overrides(target, &combined, &mut ownership, consuming_ctx). Only after that, slice away the capture prefix (ownership[capture_args.len()..].to_vec()) and return ownership for the user args. - Opaque closure fallback: if the closure resolves to
Other,None, or any untraceable origin (function parameter, result of another call,Project,Select, collection element, or ambiguous merge) → returnvec![ArgOwnership::Borrowed; user_args.len()]. - Length validation:
debug_assert_eq!(result.len(), user_args.len())before returning.
- Zero user-arg fast path (FIRST): if
-
Handle capture offset only after shared ownership computation: the
PartialApply’sargsare the first N logical call arguments, but the slice must happen AFTER builtin/type-qualified overrides run on the full[captures..., user_args...]vector. This is required for partially applied builtins likepush,add,concat,remove, andunion, whose contracts rely on the existing direct-call override path. -
No separate
num_captureslookup needed: ThePartialApplyinstruction itself tells us the number of captures —partial_apply.args.len(). Souser_arg_offset = partial_apply.args.len()and we skip that many params in the contract. No need to threadArcFunction.num_captures(line 391 ofir/mod.rs). -
Do not duplicate monomorphized-name fallback: indirect resolution must consume the same already-normalized lookup that
emit_arg_ownership()builds. Do NOT add a second$m$stripping path here.
02.3 Legacy borrow inference parity
Currently, borrow/update.rs:272-274 (309 lines total, under 500 limit) has an empty branch for InvokeIndirect:
// InvokeIndirect: closure call — no named callee to look up
// ownership for, so treat conservatively (no promotion).
| ArcTerminator::InvokeIndirect { .. } => {}
-
Update
InvokeIndirectterminator branch incompiler/ori_arc/src/borrow/update.rs:268-274. Replace the emptyInvokeIndirect { .. } => {}arm (currently grouped withBranch/Switch/Unreachable/Resume) with a dedicated arm that usesarg_ownership:ArcTerminator::InvokeIndirect { args, arg_ownership, .. } => { // Do NOT promote the closure operand itself here. In ARC IR the closure // position is always borrowed; only user arguments transfer ownership. // For args with Owned ownership, mark the corresponding parameter as owned. for (i, &arg) in args.iter().enumerate() { if arg_ownership.get(i).is_some_and(|o| *o == ArgOwnership::Owned) { changed |= try_mark_param_owned(arg, ctx.func, ctx.my_sig, ctx.aliases); } } }This requires importing
ArgOwnershipinborrow/update.rs(currently not imported — adduse crate::ir::ArgOwnership;). -
Update
ApplyIndirectbody instruction branch incompiler/ori_arc/src/borrow/update.rs:213-218. Currently it unconditionally promotes the closure operand and ALL args to owned:// BEFORE (current): ArcInstr::ApplyIndirect { closure, args, .. } => { changed |= try_mark_param_owned(*closure, ctx.func, ctx.my_sig, ctx.aliases); for &arg in args { changed |= try_mark_param_owned(arg, ctx.func, ctx.my_sig, ctx.aliases); } }Replace with
arg_ownership-aware logic — only promote args wherearg_ownership[i] == Owned:// AFTER: ArcInstr::ApplyIndirect { closure, args, arg_ownership, .. } => { // Closure operand: always borrowed in ARC IR — do NOT promote. // User args: promote only where arg_ownership says Owned. for (i, &arg) in args.iter().enumerate() { if arg_ownership.get(i).is_some_and(|o| *o == ArgOwnership::Owned) { changed |= try_mark_param_owned(arg, ctx.func, ctx.my_sig, ctx.aliases); } } }Note the closure operand (
*closure) is no longer promoted — it was incorrectly promoted before because the absence of ownership info forced the conservative “promote everything” approach. Witharg_ownershippopulated, only user args that transfer ownership need promotion. -
Remove stale comment at
borrow/update.rs:272-273(“InvokeIndirect: closure call — no named callee to look up ownership for, so treat conservatively (no promotion).”) — this comment describes the pre-Section-02 behavior and becomes stale after the update.Boundary clarification: this is a consumer-parity task for the legacy SCC borrow-inference pass, not the source of AIMS
arg_ownershipsemantics. The authoritative ownership semantics must still be produced inannotate_arg_ownership()/emit_arg_ownership(). Sequencing note: implement this AFTER 02.1 and 02.2 (it consumes thearg_ownershipthey produce), but it MUST be completed within Section 02 — it is not optional. The active leak fix runs throughcompute_aims_contracts()+realize_rc_reuse(), notborrow/update.rs; this item exists to keep legacy SCC/query tooling semantically aligned. Leaving it unfinished would create a DRIFT between the AIMS path and the legacy path. -
Cross-reference:
collect_borrowed_call_args()update: Section 03.1 handles thedrop_hints.rsrefinement — replacing the conservativeApplyIndirectworkaround witharg_ownership-aware logic, and addingInvokeIndirectterminator handling. This item (02.3) handles the borrow inference update only. -
Hygiene:
borrow/update.rs:241— the statementlet _ = value;in theArcInstr::Letarm is unnecessary (the field is already unused via..on the destructure). Remove this dead binding during the 02.3 edit pass.
02.4 Tests and verification
TDD execution sequence (mandatory — per CLAUDE.md TDD discipline):
- Write test stubs first (all Rust unit tests below) — verify they fail or do not compile before 02.1-02.3 implementation
- Implement 02.1 (def map, branches) → verify resolver-level tests still fail (ownership not yet resolved)
- Implement 02.2 (resolver) → verify resolver-level tests pass
- Implement 02.3 (borrow inference) → verify full test suite passes
- Verify matrix tests (AOT closure tests) pass with zero leaks
Test module wiring (prerequisite — do this FIRST before writing any tests):
- Create
rc_insert/tests.rs: add file atcompiler/ori_arc/src/rc_insert/tests.rs, add#[cfg(test)] mod tests;declaration at the bottom ofcompiler/ori_arc/src/rc_insert/mod.rs. Currentlymod.rsonly hasmod annotate;andpub use— the new line goes after thepub use. - Convert
arg_ownership.rsto directory: renamecompiler/ori_arc/src/aims/emit_rc/arg_ownership.rs→compiler/ori_arc/src/aims/emit_rc/arg_ownership/mod.rs, createcompiler/ori_arc/src/aims/emit_rc/arg_ownership/tests.rs, add#[cfg(test)] mod tests;in the newmod.rs. Update themod arg_ownership;incompiler/ori_arc/src/aims/emit_rc/mod.rs— no change needed there since Rust resolves botharg_ownership.rsandarg_ownership/mod.rsthe same way.
Rust unit tests (in compiler/ori_arc/src/rc_insert/tests.rs):
Resolver-level tests — these cover annotate_arg_ownership() / resolve_indirect_arg_ownership() directly by constructing minimal ArcFunction values with the required instruction patterns:
-
test_annotate_apply_indirect_from_partial_apply: construct anArcFunctionwith aPartialApplycreating a closure and anApplyIndirectcalling it. Provide a matchingAnnotatedSigin thesigsmap (notMemoryContract—annotate_arg_ownershiptakessigs, not contracts). Verifyarg_ownershipis populated with the correct ownership from the sig’s user-arg params. -
test_annotate_apply_indirect_opaque_closure:ApplyIndirectwhere the closure var is a function parameter (not traceable toPartialApply). Verify result is all-Borrowed. -
test_annotate_invoke_indirect: same pattern forInvokeIndirectterminator — verify ownership populated from sig. -
test_annotate_apply_indirect_with_captures_offset:PartialApplywith 2 captures, sig has 4 params. Verifyarg_ownershipfor theApplyIndirect(2 user args) comes from sig params[2..4], NOT[0..2]. -
test_annotate_apply_indirect_builtin_partial_apply: partially apply a builtin with receiver-consuming override (push,add,concat, orremove) and verify the indirect-call user args inherit the same ownership as a direct call after capture slicing. -
test_annotate_apply_indirect_zero_capture_function_ref: zero-capturePartialApply/function-ref closure should annotate exactly like a direct call, including external/runtime and builtin behavior. -
test_annotate_apply_indirect_alias_across_blocks: closure var reaches the call site through a cross-block alias chain or block param; verify same-target merges annotate correctly. -
test_annotate_apply_indirect_merge_conflict_defaults_borrowed: two different closures flow to one block param and are called indirectly; verify the resolver falls back to all-Borrowedinstead of picking one arbitrarily. -
test_annotate_apply_indirect_loop_carried_block_param: loop-carried/block-param closure resolution terminates (no infinite recursion) and either preserves the resolved ownership for equivalent backedges or explicitly falls back to all-Borrowedwhen equivalence cannot be proven. -
test_annotate_apply_indirect_zero_user_args:ApplyIndirectcalling a thunk (zero user args after captures). Verify result is emptyVecand no def-map lookup is attempted. - Negative pin:
test_annotate_apply_indirect_opaque_not_owned— verify opaque closure does NOT produceOwnedfor any arg (pins the conservative default).
AIMS entry-point tests (in compiler/ori_arc/src/aims/emit_rc/arg_ownership/tests.rs):
-
test_emit_arg_ownership_indirect_reuses_monomorphized_merge: populatecontractswith only monomorphized lambda names (or multiple monomorphizations merged conservatively), runemit_arg_ownership(), and verify the indirect call site sees the normalized ownership without a duplicate raw-contract fallback path. -
test_emit_arg_ownership_indirect_debug_assert_invariant: verify the new post-annotationdebug_assert!invariant by checking that non-empty indirect arg lists receive non-emptyarg_ownershipafteremit_arg_ownership().
Soundness proof test (in compiler/ori_arc/src/rc_insert/tests.rs or as AOT test):
- Opaque higher-order wrapper proof: add one end-to-end test where a closure is passed through a function parameter and called indirectly (
ApplyIndirectclosure source = function param, not resolvablePartialApply). Verify the chosen all-Borrowedfallback is actually RC-balanced for the wrapper pattern being relied on here (for example, wrapper forwards into a curried closure or returns a closure built from the borrowed arg). If this test fails, Section 02 scope must expand to model higher-order closure contracts rather than relying on the opaque fallback.
Semantic pin:
- ARC IR dump test showing
ApplyIndirectwith[own, borrow]annotations for a curried closure — useORI_DUMP_AFTER_ARC=1on a test program and verify the output format. Verified:%8 = ApplyIndirect %7(%6 [own])and%9 = ApplyIndirect %8(%2 [borrow])on curried capture list test.
Matrix testing — the fix must be verified across ALL closure patterns (these are existing AOT test files in compiler/ori_llvm/tests/aot/):
| Pattern | Test file (under fixtures/) | What to verify |
|---|---|---|
| Curried capture (list) | arc/arc_curried_closure_capture_list.ori | Zero leaks, exit 0 |
| Curried capture (str) | arc/arc_curried_closure_capture_str.ori | Zero leaks, exit 0 |
| Curried capture (nested) | arc/arc_curried_closure_capture_nested.ori | Zero leaks, exit 0 |
| Nested borrowed param (list) | higher_order/nested_closure_borrowed_list_param.ori | Zero leaks, exit 0 |
| Nested borrowed param (str) | higher_order/nested_closure_borrowed_str_param.ori | Zero leaks, exit 0 |
| Triple nested | higher_order/triple_nested_closure_capture.ori | Zero leaks, exit 0 |
| Closure with user arg (borrowed) | fat_matrix/f05_closure_param/fm_closure_param_str_heap.ori | Zero leaks, exit 0 |
| Scalar capture (negative pin) | arc/arc_curried_closure_scalar_no_inc.ori | No RcInc for scalars |
- Verify RC trace balance:
ORI_TRACE_RC=1on the curried repro shows balanced inc/dec
Build mode verification (per CLAUDE.md Fix Completeness — debug AND release must pass):
- Debug build:
timeout 150 cargo t -p ori_arc— all new unit tests pass in debug - Release build:
timeout 150 cargo test --release -p ori_arc— all new unit tests pass in release (FastISel behavior differs between modes; ownership annotation is IR-level, but release-mode test coverage catches optimizer-sensitive codegen paths downstream)
02.R Third Party Review Findings
These findings were raised during pre-implementation plan review. The plan text (02.1, 02.2) has been rewritten to incorporate all three resolutions. The checkboxes below track whether the implemented code satisfies each finding’s requirement — check them during /tpr-review after implementation.
-
[TPR-02-001][major]plans/closure-ownership/section-02-ownership-propagation.md:56-108— the original draft proposed reading rawMemoryContracts directly for indirect calls, which would drift fromemit_arg_ownership()’s monomorphization merge and fromannotate_arg_ownership()’s builtin/type-qualified override logic. Resolved: Fixed on 2026-04-05.resolve_indirect_arg_ownership()callscompute_arg_ownership()+apply_consuming_overrides()on the full[captures..., user_args...]arg list via the sameAnnotatedSigpath as direct calls. Verified bytest_emit_arg_ownership_indirect_reuses_monomorphized_merge. -
[TPR-02-002][major]plans/closure-ownership/section-02-ownership-propagation.md:97-108—capture_args.len()as an offset is only valid after computing ownership on the full logical arg list. Slicing raw contract params first is wrong for partially applied builtins whose semantics are fixed up byapply_consuming_overrides(). Resolved: Fixed on 2026-04-05. Ownership computed on full arg list first, thenownership.drain(..capture_count)slices off captures. Verified bytest_annotate_apply_indirect_with_captures_offsetandtest_annotate_apply_indirect_builtin_partial_apply. -
[TPR-02-003][major]plans/closure-ownership/section-02-ownership-propagation.md:97-102— “walk blocks backward” is not a robust closure resolver in SSA form and does not define behavior for block params / merged closures. Resolved: Fixed on 2026-04-05. SSA def-map resolver inclosure_resolve.rshandles aliases, block params (with per-predecessor visited sets), and loop-carried cycles. Verified bytest_annotate_apply_indirect_alias_across_blocks,test_annotate_apply_indirect_merge_conflict_defaults_borrowed,test_annotate_apply_indirect_loop_carried_block_param, andtest_annotate_apply_indirect_diamond_cfg_same_origin. -
[TPR-02-004][high]compiler/ori_arc/src/rc_insert/closure_resolve.rs:153— block-param merging currently treats “same callee name + same capture count” as equivalent and keeps the first predecessor’s capture list, but the effective indirect-call ownership can still differ after builtin/type-qualified overrides. Resolved: Fixed on 2026-04-05. Changed merge comparison fromexisting_captures.len() != new_captures.len()to*existing_captures != new_captures(compares actual capture args, not just count). Added regression testtest_annotate_apply_indirect_different_capture_args_defaults_borrowed. -
[TPR-02-005][high]compiler/ori_arc/src/rc_insert/closure_resolve.rs:122— the resolver uses one sharedvisitedset across all predecessor branches, so compatible merges through distinct aliases of the same closure incorrectly collapse to the opaque all-Borrowed fallback. Resolved: Fixed on 2026-04-05. Changed to clonevisitedper-predecessor traversal (let mut pred_visited = visited.clone()) so alias paths don’t contaminate each other. Added regression testtest_annotate_apply_indirect_diamond_cfg_same_origin. -
[TPR-02-006][high]compiler/ori_llvm/tests/aot/fixtures/arc/arc_opaque_closure_wrapper.ori:9— the new “opaque higher-order wrapper proof” does not exercise any RC-managed values, so it cannot validate the all-Borrowed fallback that Section 02 relies on. Resolved: Fixed on 2026-04-05. Replaced int-only fixture with str-capturing closure + str user arg. RC trace confirms 1 alloc / 1 dec / 1 free / live=0.ORI_CHECK_LEAKS=1passes. -
[TPR-02-007][medium]compiler/ori_arc/src/borrow/update.rs:275—InvokeIndirectborrow inference was changed, but the new behavior has no dedicated unit coverage. Resolved: Fixed on 2026-04-05. Addedinvoke_indirect_empty_ownership_all_borrowedandinvoke_indirect_owned_arg_promotedtests toborrow/tests.rs, mirroring the ApplyIndirect coverage. -
[TPR-02-008][high]compiler/ori_arc/src/rc_insert/closure_resolve.rs— block-param merging now compares raw capture-var identity, which is still too strict for type-qualified builtins. Resolved: Fixed on 2026-04-05. Changed merge comparison from var-identity to type-based viacaptures_same_types()helper. Same-type captures merge; different-type captures fall back. Threadedvar_typesthroughresolve_to_partial_apply. Regression:test_annotate_apply_indirect_same_capture_types_merges. -
[TPR-02-009][medium]compiler/ori_arc/src/rc_insert/tests.rs—test_annotate_apply_indirect_different_capture_args_defaults_borroweddoes not exercise the type-qualified ownership hazard. Resolved: Fixed on 2026-04-05. Renamed totest_annotate_apply_indirect_different_capture_types_defaults_borrowed, usesList<int>vsstrcapture types to exercise the actual type-divergent hazard. Added companiontest_annotate_apply_indirect_same_capture_types_mergesfor the same-type merge case. -
[TPR-02-010][medium]compiler/ori_arc/src/rc_insert/tests.rs:693— the replacement TPR-02-009 regression still does not exercise the type-qualified ownership hazard it claims to cover. Resolved: Fixed on 2026-04-05. Updated test to registerconcatinconsuming_receiverandconsuming_second_argbuiltin sets soapply_consuming_overridesactually fires for List captures but not str captures, exercising the real type-qualified divergence path. -
[TPR-02-011][high]compiler/ori_arc/src/rc_insert/closure_resolve.rs—captures_same_types()compared rawIdxequality, falling back onList<int>vsList<str>even though both resolve toTag::List. Resolved: Fixed on 2026-04-05. Renamed tocaptures_same_override_semantics(), now compares resolved type TAG viapool.tag(pool.resolve_fully(idx)). Threaded&Poolthrough resolver. Regression:test_annotate_apply_indirect_cross_instantiation_same_tag_merges(Listvs List merge succeeds). -
[TPR-02-012][medium]plans/closure-ownership/section-02-ownership-propagation.md:317-318— the completion checklist was out of sync with the implementation. Resolved: Fixed on 2026-04-05. Checked off both stale checklist items (tag-based merge + typed builtin regressions) which are implemented and tested.
02.N Completion Checklist
-
ResolvedDefenum defined (02.1) -
build_closure_def_map()implemented with two-pass block-param handling (02.1) -
annotate_arg_ownership()handlesApplyIndirectviaresolve_indirect_arg_ownership(02.1) -
annotate_arg_ownership()handlesInvokeIndirectviaresolve_indirect_arg_ownership(02.1) - Indirect calls reuse the same normalized ownership lookup as direct calls (02.1)
-
resolve_indirect_arg_ownership()traces closure toPartialApplysource through SSA defs (02.2) - Block-param / merged-closure cases handled conservatively (02.2)
- Resolver terminates on loop-carried/block-param cycles and does not recurse indefinitely (02.2)
- Capture offset handled correctly AFTER full-arg ownership computation (02.2)
- No duplicate monomorphized-name resolution path added for indirect calls (02.2)
- Opaque closures default to all-Borrowed (02.2)
-
debug_assert!verifies non-emptyarg_ownershipafter annotation (02 preamble) - Doc comments updated:
annotate_arg_ownership()andarg_ownership.rsmodule doc include ApplyIndirect/InvokeIndirect (02 preamble) - Legacy borrow inference updated for
InvokeIndirect(02.3) - Legacy borrow inference updated for
ApplyIndirectprecision — no longer promotes closure operand (02.3) -
ArgOwnershipimport added toborrow/update.rs(02.3) - Stale comment at
borrow/update.rs:272-273removed (02.3) - Dead binding
let _ = value;atborrow/update.rs:241removed (02.3) - Cross-reference verified:
collect_borrowed_call_args()refinement tracked in Section 03.1 (02.3) - Test module wiring:
rc_insert/tests.rscreated withmod tests;inrc_insert/mod.rs(02.4) - Test module wiring:
arg_ownership.rsconverted toarg_ownership/mod.rs+arg_ownership/tests.rs(02.4) - Unit tests include: partial-apply resolution, opaque closure, InvokeIndirect, capture-offset, builtin partial-apply, zero-capture function-ref, cross-block alias, merge conflict, loop-carried cycle, zero-user-args thunk, negative pin (02.4)
- AIMS entry-point tests: monomorphization merge, debug_assert invariant (02.4)
- Opaque higher-order wrapper proof test passes (02.4)
- All 8 closure pattern tests pass with zero leaks (02.4)
-
annotate.rsstays under 500 lines (if not, extractclosure_resolve.rs) (02 preamble) - Debug AND release
cargo t -p ori_arcpass (02.4) -
timeout 150 cargo tpasses -
timeout 150 ./test-all.shpasses -
/tpr-reviewpassed (clean on iteration 6 after 11 findings fixed) -
/impl-hygiene-reviewpassed (1 LEAK fixed: test helper duplication) - Plan sync: Section 02 frontmatter
status:updated tocomplete - Plan sync:
00-overview.mdfrontmatterstatus:stillin-progress(Section 03 remains) - Plan sync:
index.mdQuick Reference table updated (Section 02 status → Complete) - Plan sync: Section 03
depends_on: ["01", "02"]verified accurate - Block-param merge accepts distinct capture vars when they produce the same effective indirect-call ownership (tag-based comparison via
captures_same_override_semantics) - Typed builtin regression coverage:
different_capture_types(List vs str → fallback),same_capture_types(int vs int → merge),cross_instantiation_same_tag(Listvs List → merge)