Section 03: Take-Project Hardening
Context: The AIMS take-project ownership model has two gaps discovered during the dual-source TPR review:
-
Lineage invariant gap (TPR-07-001-gemini): The
in_classmembership set (computed via bidirectional union-find including Jump edges attake_project/mod.rs:645) is a superset ofvar_to_lineage(computed via forward-only BFS attake_project/mod.rs:389). Predecessor block arguments enterin_classvia the bidirectional Jump union but don’t get lineage entries because the alias graph’s Jump edges are forward-only. These orphaned vars are skipped bydead_cleanup.rs:152(in_class check) andedge_cleanup.rs:212(in_class check), andis_bypass_safe_entry_for_varreturns false without lineage (line 232) → no RC decrement ever emitted → memory leak. -
Iterator-only scope (TPR-07-005-codex, TPR-07-004-gemini):
is_take_project()inborrowed_defs.rs:61-88checksmatches!(dst_tag, Tag::Iterator | Tag::DoubleEndedIterator)— a tag-based whitelist. Future unique-owned types (Box, channels) will silently stay on the borrow path, reintroducing double-free or leak bugs.
Approach: Fix the lineage gap via a separate reconciliation pass in compute_lineage() (NOT backward edges in build_alias_graph() — that would contaminate singleton lineages via phi→arg backward paths). Generalize is_take_project to check MachineRepr for unique ownership instead of tag whitelist.
This section is independent of §01/§02 — take-project hardening doesn’t touch enum layout. Can be developed in parallel with §02.
03.1 Lineage Reconciliation Pass
File(s): compiler/ori_arc/src/aims/emit_rc/take_project/mod.rs (modify compute_lineage() and analyze())
Critical design constraint: Do NOT add backward edges to build_alias_graph(). The alias graph’s Jump edges are intentionally forward-only to prevent phi contamination (established in TPR-07-019 fix, iteration 2). Adding backward edges would allow BFS from source A to reach source B via A→phi→B backward paths, merging what should be separate lineages. Instead, add a post-BFS reconciliation pass.
-
Write failing TDD matrix FIRST — unit tests in
compiler/ori_arc/src/aims/emit_rc/take_project/tests.rs:lineage_reconciliation_predecessor_arg_gets_lineage: Topology with predecessor arg in_class but not forward-reachable → after fix, var has lineage entry. Without fix, var has no lineage (the gap).lineage_reconciliation_preserves_singleton_lineages: Two sources merge at phi → each source retains singleton lineage {0} and {1} respectively, NOT merged. The phi param gets mixed lineage {0,1}.lineage_reconciliation_unrelated_predecessor_gets_default: A predecessor arg that is NOT a take-project source but enters in_class via the union-find gets a default lineage matching its component’s sources.- Negative pin:
lineage_reconciliation_does_not_create_spurious_lineage— vars NOT in in_class do NOT get lineage entries from the reconciliation pass.
-
Add reconciliation pass in
compute_lineage()— after the forward-only BFS completes (around line 420), before sorting/dedup:// Post-BFS reconciliation: assign lineage to in_class vars that // the forward BFS didn't reach. These are predecessor args that // entered in_class via the bidirectional union-find (Jump edges) // but aren't forward-reachable from any source. // // Strategy: for each in_class var missing from var_to_sources, // find its union-find component, collect the source indices of // that component, and assign those as the var's lineage. // This preserves phi-safety: the var inherits the lineage of // the COMPONENT it belongs to, not of any individual source. for &var in &in_class { if !var_to_sources.contains_key(&var) { // Find component root, collect all source indices in that component let component_sources = /* collect source indices for vars in the same union-find component as `var` */; if !component_sources.is_empty() { var_to_sources.insert(var, component_sources); } } }The exact implementation will need to reference the union-find
parentmap (fromcompute_membership) and the existingtp_sourceslist to map component → source indices. -
Thread
in_classandparentintocompute_lineage()— currentlycompute_lineagetakesfunc,tp_sources, and the alias graph. It needsin_classand the union-findparentmap to perform reconciliation. Either pass them as parameters or restructure to access them fromanalyze(). -
Add
debug_assert!inanalyze()— aftercompute_lineage()returns, verify the invariant:debug_assert!( in_class.iter().all(|v| var_to_lineage.contains_key(v)), "in_class variable without lineage entry — reconciliation incomplete" );This assertion fires in debug builds on every test run, catching any future regression.
-
AOT regression test in
compiler/ori_llvm/tests/aot/iterator_drop.rs:take_project_predecessor_arg_no_leak: Fixture with a branch where one arm has a take-project and the other doesn’t, both jumping to a merge block. The non-take-project arm’s arg must still get cleaned up correctly. Verify withORI_CHECK_LEAKS=1.
-
Verify all 16+ existing
iterator_dropAOT tests pass in both debug and release. Zero regressions from the reconciliation pass. -
Subsection close-out (03.1) — Run
/improve-toolingretrospectively. Commit separately. -
/sync-claudesection-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW. -
Repo hygiene check — run
diagnostics/repo-hygiene.sh --checkand clean any detected temp files.
03.2 is_take_project Generalization
File(s): compiler/ori_arc/src/aims/emit_rc/borrowed_defs.rs (modify is_take_project())
-
Write failing TDD test FIRST — currently hard to test since no non-iterator unique-owned types exist. Write a unit test that exercises the predicate directly:
is_take_project_iterator_positive: existing behavior preservedis_take_project_non_iterator_unmanaged_ptr: future-proofing test — if a new UnmanagedPtr type is added, the predicate accepts itis_take_project_rc_pointer_negative: RcPointer payloads are NOT take-projects (they have RC)is_take_project_opaque_ptr_negative: OpaquePtr payloads are NOT take-projects (they may have external ownership)
-
Replace tag whitelist with MachineRepr check in
is_take_project():// Current (iterator-only): // matches!(dst_tag, Tag::Iterator | Tag::DoubleEndedIterator) // New (MachineRepr-driven): // Check if the projected field's MachineRepr represents unique ownership // (UnmanagedPtr that is Box-allocated, no RC header). // This automatically covers Iterator, DoubleEndedIterator, and any // future unique-owned type without predicate changes.The exact check depends on how
MachineReprclassifies unique ownership. Iterator/DoubleEndedIterator map toMachineRepr::UnmanagedPtrincanonical/mod.rs:287. The check becomes: “is the destination’s MachineRepr an UnmanagedPtr?” with an exclusion forOpaquePtr(C FFI pointers that have external ownership). -
Also replace
*field == 0check (line 70 ofborrowed_defs.rs):- Current:
if *field == 0 { return false; }— hardcodes tag at field 0 - New:
if enum_layout_info.field_is_tag(*field) { return false; }— uses §01’sEnumLayoutInfo - This makes the predicate correct for ALL tag encodings (niche, tagged-ptr, none), not just explicit-tag
- Current:
-
Subsection close-out (03.2) — Run
/improve-toolingretrospectively. Commit separately. -
/sync-claudesection-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW. -
Repo hygiene check — run
diagnostics/repo-hygiene.sh --checkand clean any detected temp files.
03.3 Completion Checklist
- Lineage reconciliation pass assigns lineage to ALL in_class vars
-
debug_assert!enforcesin_class ⊆ var_to_lineage.keys()— fires on full test suite in debug - Singleton lineages preserved (no phi contamination from reconciliation)
-
is_take_projectuses MachineRepr ownership classification, not tag whitelist -
field_is_tag()from §01 replaces hardcoded*field == 0check - All existing
iterator_dropAOT tests pass (16+ tests, debug and release) - New AOT test for predecessor arg leak scenario
-
ORI_CHECK_LEAKS=1clean on all iterator-related test programs -
./test-all.shgreen -
./clippy-all.shgreen -
/tpr-reviewpassed — independent review clean -
/impl-hygiene-reviewpassed — MUST run AFTER/tpr-reviewis clean -
/improve-toolingsection-close sweep completed
03.R Third Party Review Findings
- None.