Section 01: ARC IR Shape
Goal: Add arg_ownership fields to ApplyIndirect and InvokeIndirect in the ARC IR, following the exact pattern used by Apply and Invoke.
01.1 Add arg_ownership to ApplyIndirect
- Add field to
ArcInstr::ApplyIndirectincompiler/ori_arc/src/ir/instr.rs:41-46:ApplyIndirect { dst: ArcVarId, ty: Idx, closure: ArcVarId, args: Vec<ArcVarId>, arg_ownership: Vec<ArgOwnership>, // NEW }, - Update match arms that destructure
ApplyIndirectacrossori_arc. Usecargo build -p ori_arc— exhaustive match will flag every site. Sites that must change:ir/instr.rs:is_owned_position()(line 313) — major change, see 01.3lower/builder/emission.rs:emit_apply_indirect()(line 49) — addarg_ownership: Vec::new()to construction (empty vec = pre-annotation state, populated in Section 02)oric/src/arc_dump/instr.rs(line 57-75): dump format — must update to show ownership annotations (see 01.3)
- Sites that use
..binding (verify no change needed —cargo buildconfirms):ir/instr.rs:used_vars()(line 212),defined_var()(line 170),substitute_var()(line 369),uses_var()(line 271) — all use..bindingaims/transfer/mod.rs:transfer_arg_demand()(line 266),transfer_def()(line 80) —..bindingaims/normalize/verify.rs(line 332),borrow/update.rs(line 213),borrow/derived.rs(line 122),graph/call_graph/mod.rs(line 84 wildcard arm)aims/realize/walk_dec.rs(line 145),aims/realize/walk.rs(line 223): consume viais_owned_position()— no direct match onApplyIndirect- Note:
forward_walk.rs,unwind_cleanup.rs,dead_cleanup.rs,realize/mod.rsdo NOT referenceApplyIndirect— no changes needed there
- Default initialization: New
arg_ownershipdefaults to emptyVecat construction (same asApply’s pre-annotation state). The AIMS pipeline populates it in Section 02.
01.2 Add arg_ownership to InvokeIndirect
- Add field to
ArcTerminator::InvokeIndirectincompiler/ori_arc/src/ir/mod.rs:276-283:InvokeIndirect { dst: ArcVarId, ty: Idx, closure: ArcVarId, args: Vec<ArcVarId>, arg_ownership: Vec<ArgOwnership>, // NEW normal: ArcBlockId, unwind: ArcBlockId, }, - Update match arms that destructure
InvokeIndirect— sites that must change:lower/builder/mod.rs:terminate_invoke_indirect()(line 322) — addarg_ownership: Vec::new()toArcTerminator::InvokeIndirectconstruction (line 337-344)oric/src/arc_dump/instr.rs(lines 312-329): dump format — replacefmt_args_simple(out, args, func)withfmt_args_with_ownership(out, args, arg_ownership, func)(see 01.3)aims/realize/emit_unified.rs(484 lines — at limit, do NOT add unrelated code):is_var_defined_in_block()(line 478) — addInvokeIndirectcheck after the existingInvokecheck:
Rationale:if let ArcTerminator::InvokeIndirect { dst, .. } = &block.terminator { if *dst == var { return true; } }InvokeIndirect, likeInvoke, definesdstin its normal successor block. The current code only checksInvoke, causing the pipeline to missInvokeIndirectdefs — this is a latent bug that becomes visible oncearg_ownershipenables proper RC emission for indirect invoke sites.aims/emit_rc/forward_walk.rs:emit_terminator_rc()(lines 31-54) — add a matchingInvokeIndirectbranch after the existingInvokebranch:
Note:if let ArcTerminator::InvokeIndirect { args, arg_ownership, .. } = terminator { for (pos, &var) in args.iter().enumerate() { let is_owned = arg_ownership .get(pos) .is_some_and(|o| *o == ArgOwnership::Owned); if is_owned && ctx.project_borrowed_defs.contains(&var) && ctx.func.var_reprs[var.index()] != ValueRepr::Scalar { if let Some(strategy) = rc_strategy(ctx.func, var, ctx.pool) { new_body.push(ArcInstr::RcInc { var, count: 1, strategy }); } } } }InvokeIndirect’sused_vars()returns[...args, closure](closure LAST). Butarg_ownershipparallelsargsdirectly (no offset), so we iterateargswith direct index. Also update the comment at line 82 to include InvokeIndirect in the list of ownership-transferring terminators.
- Sites that use
..binding (verify no change needed —cargo buildconfirms):ir/mod.rs:used_vars()(line 311),uses_var()(line 330),substitute_var()(line 357) — all use..bindingborrow/update.rs: InvokeIndirect branch (lines 272-274) — currently empty{}. Section 02.3 handles updating this for borrow inference.aims/transfer/mod.rs:transfer_def_terminator()(line 367-372) —..bindingaims/emit_rc/trampoline.rs: lines 89 and 136 —..bindingaims/interprocedural/mod.rs: line 513 —..bindingblock_merge/compact.rs: line 136 —..bindinggraph/mod.rs: lines 73 and 88 —..binding- Note:
unwind_cleanup.rs,dead_cleanup.rs,realize/mod.rsdo NOT referenceInvokeIndirect— no changes needed there
01.3 Update IR queries and utilities
- Fix
is_owned_position()incompiler/ori_arc/src/ir/instr.rs:313-342:- Replace the hardcoded
falseforApplyIndirectwitharg_ownership-based lookup:ArcInstr::ApplyIndirect { args, arg_ownership, .. } => { // pos indexes into used_vars(): pos=0 is closure (always borrowed), // pos 1..=args.len() are user args. arg_ownership parallels args, // so arg_ownership[i] corresponds to used_vars position i+1. if pos == 0 { return false; // closure is always borrowed } let arg_idx = pos - 1; // Empty arg_ownership (pre-annotation) → is_some_and returns false → // all positions NOT owned. This is safe (conservative: caller retains // cleanup). Differs from Apply's `is_none_or` which defaults to Owned. arg_idx < args.len() && arg_ownership .get(arg_idx) .is_some_and(|o| *o == ArgOwnership::Owned) }, - WARNING: This does NOT match the
Applypattern directly.Apply’sused_vars()returns onlyargs(no closure field), soposmaps directly toargs[pos]. ForApplyIndirect,used_vars()prepends the closure at position 0, creating a +1 offset. Existing tests (is_owned_position_apply_indirect_closure_is_borrowedinir/tests.rs:1147) validate this.
- Replace the hardcoded
- Add
is_owned_position()for terminators:ArcTerminatorcurrently has nois_owned_position()method. Add one to handleInvokeIndirect(andInvoke, which hasarg_ownershipalready). CAUTION:InvokeIndirect’sused_vars()returns[...args, closure](closure at END, not beginning — opposite ofApplyIndirect’sused_vars()which puts closure first). The position-to-arg mapping must account for this asymmetry. Note: The realization pass (walk.rs,walk_dec.rs) only callsis_owned_positionon body instructions — terminator RC is handled separately viaterminator_deferredand edge cleanup. The new terminatoris_owned_position()may need to be called from the terminator RC emission path (checkemit_unified.rsand the edge cleanup logic). - Update ARC IR dump (
ORI_DUMP_AFTER_ARC=1): displayarg_ownershipannotations for indirect calls, matching the format used forApply/Invoke. Two files to update incompiler/oric/src/arc_dump/instr.rs(459 lines, under 500 limit):ApplyIndirectdump (lines 57-75): change destructure to includearg_ownership, replacefmt_args_simple(out, args, func)(line 90) withfmt_args_with_ownership(out, args, arg_ownership, func)InvokeIndirectdump (lines 312-329): change destructure to includearg_ownership, replacefmt_args_simple(out, args, func)(line 327) withfmt_args_with_ownership(out, args, arg_ownership, func)
- Update ARC IR verifier (
compiler/ori_arc/src/verify/mod.rs, 369 lines): add a newVerifyErrorvariant (e.g.,ArgOwnershipLenMismatch { block, expected, actual }) and add a check thatarg_ownership.len() == args.len()whenarg_ownershipis non-empty, for bothApplyIndirect(instruction loop) andInvokeIndirect(terminator check). The verifier currently has noarg_ownershipchecks at all — this is a new addition. Note:aims/normalize/verify.rsis the TRMC post-rewrite verifier, NOT the general ARC IR verifier. - Update serialization (if
cachefeature is enabled):arg_ownershipneeds serde derives — already handled by the#[cfg_attr(feature = "cache", derive(...))]onArcInstr. Verify this also coversArcTerminatorforInvokeIndirect.
01.4 Tests and verification
TDD order: Write tests first, verify they fail (compilation errors from missing field are acceptable “failures”), then implement.
- Update existing tests in
compiler/ori_arc/src/ir/tests.rs(1215 lines — test file, no 500-line limit):is_owned_position_apply_indirect_closure_is_borrowed(line 1147): addarg_ownership: vec![]to theApplyIndirectconstruction (line 1151-1156). Test should STILL pass (empty vec = all Borrowed viais_some_and).is_owned_position_apply_indirect_no_args(line 1175): addarg_ownership: vec![]to construction (line 1176-1181). Test should STILL pass.
- Write new Rust unit tests in
compiler/ori_arc/src/ir/tests.rs:test_apply_indirect_is_owned_position_empty: emptyarg_ownership→ all positions returnfalse(conservative default — usesis_some_andnotis_none_or, so empty vec = all Borrowed)test_apply_indirect_is_owned_position_owned:arg_ownership = [Owned, Borrowed]→ pos 0 (closure) is NOT owned, pos 1 (args[0]) IS owned, pos 2 (args[1]) is NOT owned. Note the +1 offset:posindexes intoused_vars()where closure is at position 0.test_apply_indirect_is_owned_position_out_of_bounds: pos beyondargs.len()returnsfalsetest_invoke_indirect_is_owned_position: same pattern forInvokeIndirect— but closure is at the END ofused_vars(), so the offset mapping differs. Test:arg_ownership = [Owned, Borrowed]→ pos 0 (args[0]) IS owned, pos 1 (args[1]) is NOT owned, pos 2 (closure) is NOT owned.- Negative pin:
test_apply_indirect_is_owned_position_not_default_owned— verify thatApplyIndirectwith emptyarg_ownershipdoes NOT default to Owned (unlikeApplywhich usesis_none_or). This pins the conservative-for-indirect semantic.
- Write verifier test in
compiler/ori_arc/src/verify/tests.rs:test_verify_apply_indirect_arg_ownership_len_mismatch: constructApplyIndirectwithargs.len() = 2butarg_ownership.len() = 1(non-empty mismatch) → verifier returnsArgOwnershipLenMismatcherrortest_verify_invoke_indirect_arg_ownership_len_mismatch: same forInvokeIndirectterminatortest_verify_apply_indirect_arg_ownership_empty_ok: emptyarg_ownershipwith non-emptyargs→ NO error (empty = pre-annotation, valid)
- Verify all existing tests pass:
timeout 150 cargo t -p ori_arc— exhaustive match ensures no missed sites - Verify ARC IR dump shows
arg_ownershipannotations:ORI_DUMP_AFTER_ARC=1 cargo run -- build /tmp/test.ori
01.R Third Party Review Findings
-
[TPR-01-001][major]edge_cleanup.rs:73,278,312,428— InvokeIndirect missing from invoke-specific RC edge cleanup. Resolved: Fixed on 2026-04-05. Added InvokeIndirect to collect_invoke_edge_decs with correct is_some_and default (Borrowed for indirect, Owned for direct). Added is_indirect flag to invoke_transfers_ownership helper. -
[TPR-01-002][major]dead_cleanup.rs:174,185,248— InvokeIndirect missing from dead-dst fallback sweep. Resolved: Fixed on 2026-04-05. Added InvokeIndirect to emit_dead_invoke_dsts and is_var_defined_in_block. -
[TPR-01-003][minor]ir/tests.rs:1223,1283,1326— Decorative// --- ... ---banner comments. Resolved: Fixed on 2026-04-05. Replaced with plain section comments. -
[TPR-01-004][major]edge_cleanup.rs:312,398,428— is_none_or ownership default wrong for InvokeIndirect (defaults empty to Owned instead of Borrowed). Resolved: Fixed on 2026-04-05 (2 iterations). Added is_indirect detection: Category 3 uses is_some_and, Category 2 uses is_none_or for borrowed detection, invoke_transfers_ownership parameterized. -
[TPR-01-005][medium]drop_hints.rs:95— InvokeIndirect missing from collect_borrowed_call_args terminator scan. Resolved: Pre-existing gap, already tracked in Section 03.1. No regression from Section 01 changes. -
[TPR-01-006][medium]unwind_cleanup.rs:57— InvokeIndirect missing from unwind iterator cleanup. Resolved: Pre-existing gap, tracked for Section 03. No regression from Section 01 changes.
01.N Completion Checklist
-
ApplyIndirecthasarg_ownershipfield (01.1) -
InvokeIndirecthasarg_ownershipfield (01.2) -
is_owned_position()usesarg_ownershipforApplyIndirectwith +1 offset (01.3) -
is_owned_position()added forArcTerminatorto handleInvokeIndirect(01.3) -
emit_terminator_rc()handlesInvokeIndirectproject-borrowed RcInc (01.2) -
is_var_defined_in_block()handlesInvokeIndirect(01.2) - ARC IR dump shows ownership annotations for both
ApplyIndirectandInvokeIndirect(01.3) - Verifier checks
arg_ownershiplength with new error variant (01.3) - Existing
is_owned_positiontests updated witharg_ownershipfield (01.4) - New unit tests for
is_owned_positionwith indirect calls including negative pin (01.4) - Verifier tests for
arg_ownershiplength mismatch (01.4) -
timeout 150 cargo tpasses — no regressions -
timeout 150 ./test-all.shpasses -
/tpr-reviewpassed — clean on iteration 4 (6 findings fixed across 3 iterations) -
/impl-hygiene-reviewpassed — 3 findings fixed (algorithmic duplication, BLOAT, import ordering)