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
+34 -8
View File
@@ -800,6 +800,16 @@ impl<'a> Emitter<'a> {
} else {
self.emit_drop_fn_for_type(td);
}
// Iter 18g.tidy.fu2: tag-conditional partial-drop
// helper alongside the recursive/iterative drop fn.
// Used at carve-out sites where a binder's runtime
// tag is dynamic and `moved_slots` is a strict
// subset of its ptr fields. Same shape regardless
// of `(drop-iterative)` (the helper is single-shot
// on the binder; field cascades go through their
// own drop fns which themselves choose recursive
// vs iterative).
self.emit_partial_drop_fn_for_type(td);
}
}
}
@@ -1085,14 +1095,30 @@ impl<'a> Emitter<'a> {
" call void @{drop_call}(ptr {p_ssa})\n"
));
} else {
// Iter 18d.4 debt: dynamic-tag partial-drop
// see the symmetric arm-close branch in
// `lower_match` for the rationale. Falls back
// to shallow `ailang_rc_dec` of the outer
// cell.
self.body.push_str(&format!(
" call void @ailang_rc_dec(ptr {p_ssa})\n"
));
// Iter 18g.tidy.fu2: dynamic-tag partial-drop
// via the per-type helper. The param's runtime
// tag is dynamic but its static type is known
// (`pty_ail`), so we route through
// `partial_drop_<owner>_<T>(p, mask)` which
// dispatches on the runtime tag and dec's only
// the unmoved fields. The fallback to shallow
// `ailang_rc_dec` only fires for non-ADT param
// types (Str, fn-typed, vars) — none of which
// can populate `moved_slots` in practice, so
// the fallback is dead under the typechecker.
let sym = Self::partial_drop_symbol_for_type(
self, pty_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 {p_ssa}, i64 {mask})\n"
));
} else {
self.body.push_str(&format!(
" call void @ailang_rc_dec(ptr {p_ssa})\n"
));
}
}
}
}