codegen rc: tag-conditional partial-drop helper closes 3 carve-outs

Three sibling sites previously fell back to shallow `ailang_rc_dec`
when a binder had non-empty `moved_slots` and a dynamic runtime ctor
tag: Iter B Own-param dec at fn-return (lib.rs), Iter A arm-close
pattern-binder dec (match_lower.rs), and emit_inlined_partial_drop's
non-Ctor branch (drop.rs). Shallow freed the outer cell but did not
walk the unmoved ptr fields — silent leak.

Adds `emit_partial_drop_fn_for_type`: a per-ADT helper structurally
parallel to `emit_drop_fn_for_type` but taking an i64 moved-slots
bitmask. Each ptr-field's dec is gated on the corresponding mask
bit; the outer box is dec'd at join. Emitted alongside the per-type
drop fn for every ADT under --alloc=rc.

Three call sites rewritten to resolve the partial_drop symbol from
the binder's static type and build the mask from `moved_slots`.

Three RED-then-GREEN fixtures (rc_own_param_/rc_match_arm_/
rc_app_let_partial_drop_leak) pin the carve-outs in regression
coverage; live cell counts go from 3/1/3 (pre-fix) to 0/0/0.
e2e count: 66 → 69.

The pre-existing `alloc_rc_own_param_dec_at_fn_return` IR-shape
assertion is widened to accept `partial_drop_<m>_<T>(%arg_xs,
i64 mask)` as a third valid drop shape (the canonical fu2 case).

Closes the JOURNAL queue's only remaining iter; the "wait for
organic fixture" stance from the 18g tidy is retracted — known
leaks are bugs, CLAUDE.md's TDD rule covers them autonomously.
This commit is contained in:
2026-05-08 16:25:40 +02:00
parent 7b65c121ed
commit caefcf996a
11 changed files with 763 additions and 57 deletions
+22 -18
View File
@@ -821,24 +821,28 @@ impl<'a> Emitter<'a> {
" call void @{drop_call}(ptr {b_ssa})\n"
));
} else {
// Iter 18d.4 debt: pattern-binder with
// statically-recorded moved slots requires
// tag-conditional partial-drop emission. The
// 18d.3 `emit_inlined_partial_drop` helper
// assumes a static ctor (it's keyed against a
// `Term::Ctor` node), but a pattern-bound
// binder's runtime tag is dynamic. Until a
// dynamic-tag-aware partial-drop lands, fall
// back to a shallow `ailang_rc_dec` of the
// outer cell. The non-moved fields of the
// active ctor (if any) leak under this path —
// the canonical 18d.4 fixtures avoid this
// case by construction (the regression `t`
// is never re-scrutinised inside the arm,
// so its `moved_slots` entry is empty).
self.body.push_str(&format!(
" call void @ailang_rc_dec(ptr {b_ssa})\n"
));
// Iter 18g.tidy.fu2: pattern-binder with
// statically-recorded moved slots routes
// through the tag-conditional helper
// `partial_drop_<owner>_<T>(b, mask)`. The
// binder's runtime tag is dynamic but its
// static type (`b_ail`) selects the per-type
// helper, which dispatches on the runtime tag
// and dec's only the unmoved fields. Closes
// the 18d.4 carve-out: previously the shallow
// `ailang_rc_dec` leaked the unmoved fields of
// whichever ctor `b` happened to be at runtime.
let sym = self.partial_drop_symbol_for_type(b_ail);
let mask = Self::build_moved_mask(&moves);
if let (Some(sym), Some(mask)) = (sym, mask) {
self.body.push_str(&format!(
" call void @{sym}(ptr {b_ssa}, i64 {mask})\n"
));
} else {
self.body.push_str(&format!(
" call void @ailang_rc_dec(ptr {b_ssa})\n"
));
}
}
}
}