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:
@@ -538,6 +538,193 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 18g.tidy.fu2: tag-conditional partial-drop helper.
|
||||
/// Emits `void @partial_drop_<m>_<T>(ptr %p, i64 %mask)` —
|
||||
/// structurally parallel to [`Self::emit_drop_fn_for_type`] but
|
||||
/// gates each ptr-field dec on a bit of `%mask`. If bit j of the
|
||||
/// mask is set, slot j was moved out of the cell (its content
|
||||
/// belongs to a downstream owner) and the dec is skipped; if the
|
||||
/// bit is clear, the field is loaded and routed through
|
||||
/// `field_drop_call` (same dispatch the recursive cascade uses).
|
||||
/// The outer box is always dec'd at `join`.
|
||||
///
|
||||
/// Closes the three dynamic-tag carve-outs that previously
|
||||
/// fell back to shallow `ailang_rc_dec`:
|
||||
/// 1. `lib.rs` Iter B Own-param dec at fn-return when
|
||||
/// `moved_slots[param]` is non-empty;
|
||||
/// 2. `match_lower.rs` Iter A arm-close pattern-binder dec
|
||||
/// when the binder was itself the scrutinee of an inner
|
||||
/// match that moved out some fields;
|
||||
/// 3. [`Self::emit_inlined_partial_drop`]'s non-Ctor branch
|
||||
/// (let-binder whose value is `Term::App`).
|
||||
/// All three share the same shape: a binder whose runtime ctor
|
||||
/// tag is dynamic (not statically known from a `Term::Ctor`)
|
||||
/// and whose `moved_slots` is a strict subset of its ptr fields.
|
||||
/// Pre-fu2 the unmoved fields leaked silently; fu2 routes them
|
||||
/// through this helper.
|
||||
///
|
||||
/// IR shape (analogous to `emit_drop_fn_for_type`):
|
||||
/// ```text
|
||||
/// define void @partial_drop_<m>_<T>(ptr %p, i64 %mask) {
|
||||
/// %is_null = icmp eq ptr %p, null
|
||||
/// br i1 %is_null, label %ret, label %live
|
||||
/// live:
|
||||
/// %tag = load i64, ptr %p
|
||||
/// switch i64 %tag, label %dflt [...]
|
||||
/// arm_i:
|
||||
/// ; for each ptr field f_j of ctor i:
|
||||
/// %b_j = and i64 %mask, (1 << j)
|
||||
/// %s_j = icmp ne i64 %b_j, 0
|
||||
/// br i1 %s_j, label %after_i_j, label %do_i_j
|
||||
/// do_i_j:
|
||||
/// %a = gep i8, ptr %p, (8 + 8*j)
|
||||
/// %v = load ptr, ptr %a
|
||||
/// call void @<field_drop_call>(ptr %v)
|
||||
/// br label %after_i_j
|
||||
/// after_i_j:
|
||||
/// ; next field, or br label %join
|
||||
/// dflt: unreachable
|
||||
/// join:
|
||||
/// call void @ailang_rc_dec(ptr %p)
|
||||
/// br label %ret
|
||||
/// ret:
|
||||
/// ret void
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// One helper per ADT, emitted alongside `drop_<m>_<T>`. We do
|
||||
/// NOT emit a `(drop-iterative)` partial-drop variant: the
|
||||
/// helper runs once on the binder (carve-out sites are not
|
||||
/// cascade points — the unmoved fields go through their own
|
||||
/// `drop_<m>_<F>` which itself decides recursive vs iterative).
|
||||
pub(crate) fn emit_partial_drop_fn_for_type(&mut self, td: &TypeDef) {
|
||||
let m = self.module_name;
|
||||
let tname = &td.name;
|
||||
let mut out = String::new();
|
||||
out.push_str(&format!(
|
||||
"define void @partial_drop_{m}_{tname}(ptr %p, i64 %mask) {{\n"
|
||||
));
|
||||
out.push_str("entry:\n");
|
||||
out.push_str(" %is_null = icmp eq ptr %p, null\n");
|
||||
out.push_str(" br i1 %is_null, label %ret, label %live\n");
|
||||
out.push_str("live:\n");
|
||||
out.push_str(" %tag = load i64, ptr %p, align 8\n");
|
||||
|
||||
let n_ctors = td.ctors.len();
|
||||
out.push_str(" switch i64 %tag, label %dflt [\n");
|
||||
for (i, _) in td.ctors.iter().enumerate() {
|
||||
out.push_str(&format!(" i64 {i}, label %arm_{i}\n"));
|
||||
}
|
||||
out.push_str(" ]\n");
|
||||
|
||||
let mut local = 0u64;
|
||||
for (i, ctor) in td.ctors.iter().enumerate() {
|
||||
out.push_str(&format!("arm_{i}:\n"));
|
||||
for (j, fty) in ctor.fields.iter().enumerate() {
|
||||
let lty = llvm_type(fty).unwrap_or_else(|_| "ptr".into());
|
||||
if lty != "ptr" {
|
||||
continue;
|
||||
}
|
||||
let bit_id = local;
|
||||
local += 1;
|
||||
let set_id = local;
|
||||
local += 1;
|
||||
let bitmask: u64 = 1u64 << j;
|
||||
out.push_str(&format!(
|
||||
" %b{bit_id} = and i64 %mask, {bitmask}\n"
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" %s{set_id} = icmp ne i64 %b{bit_id}, 0\n"
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" br i1 %s{set_id}, label %after_{i}_{j}, label %do_{i}_{j}\n"
|
||||
));
|
||||
out.push_str(&format!("do_{i}_{j}:\n"));
|
||||
let off = 8 + (j as i64) * 8;
|
||||
let addr_id = local;
|
||||
local += 1;
|
||||
let val_id = local;
|
||||
local += 1;
|
||||
out.push_str(&format!(
|
||||
" %a{addr_id} = getelementptr inbounds i8, ptr %p, i64 {off}\n"
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
|
||||
));
|
||||
let drop_call = self.field_drop_call(fty);
|
||||
out.push_str(&format!(
|
||||
" call void @{drop_call}(ptr %v{val_id})\n"
|
||||
));
|
||||
out.push_str(&format!(" br label %after_{i}_{j}\n"));
|
||||
out.push_str(&format!("after_{i}_{j}:\n"));
|
||||
}
|
||||
out.push_str(" br label %join\n");
|
||||
}
|
||||
|
||||
out.push_str("dflt:\n");
|
||||
if n_ctors == 0 {
|
||||
out.push_str(" br label %join\n");
|
||||
} else {
|
||||
out.push_str(" unreachable\n");
|
||||
}
|
||||
|
||||
out.push_str("join:\n");
|
||||
out.push_str(" call void @ailang_rc_dec(ptr %p)\n");
|
||||
out.push_str(" br label %ret\n");
|
||||
out.push_str("ret:\n");
|
||||
out.push_str(" ret void\n");
|
||||
out.push_str("}\n\n");
|
||||
self.body.push_str(&out);
|
||||
}
|
||||
|
||||
/// Iter 18g.tidy.fu2: resolve the `partial_drop_<owner>_<T>`
|
||||
/// symbol for a type, parallel to the existing per-type drop
|
||||
/// dispatch in `field_drop_call`. Returns `None` for non-ADT
|
||||
/// types (Str, fn-typed, type-vars) — callers fall back to
|
||||
/// shallow `ailang_rc_dec` in those cases (no per-type drop
|
||||
/// fn exists either, and `moved_slots` would not have been
|
||||
/// populated for those shapes anyway).
|
||||
pub(crate) fn partial_drop_symbol_for_type(&self, ty: &Type) -> Option<String> {
|
||||
match ty {
|
||||
Type::Con { name, .. } => {
|
||||
if name == "Str" {
|
||||
return None;
|
||||
}
|
||||
if name.matches('.').count() == 1 {
|
||||
let (prefix, suffix) = name.split_once('.').expect("checked");
|
||||
if let Some(target) = self.import_map.get(prefix) {
|
||||
return Some(format!("partial_drop_{target}_{suffix}"));
|
||||
}
|
||||
return Some(format!("partial_drop_{prefix}_{suffix}"));
|
||||
}
|
||||
Some(format!(
|
||||
"partial_drop_{m}_{name}",
|
||||
m = self.module_name
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 18g.tidy.fu2: build the i64 moved-slots bitmask for a
|
||||
/// `partial_drop_<m>_<T>` call. Bit j is set iff slot j is in
|
||||
/// `moved`. We reject slot indices ≥ 64 with `None` — no
|
||||
/// language-level ADT has that many fields, but the cap is
|
||||
/// load-bearing because the helper's mask is a single i64.
|
||||
/// On overflow, callers fall back to shallow dec (correctness-
|
||||
/// safe; carve-out leak path remains as before, but the cap is
|
||||
/// effectively unreachable).
|
||||
pub(crate) fn build_moved_mask(moved: &BTreeSet<usize>) -> Option<u64> {
|
||||
let mut mask: u64 = 0;
|
||||
for &idx in moved {
|
||||
if idx >= 64 {
|
||||
return None;
|
||||
}
|
||||
mask |= 1u64 << idx;
|
||||
}
|
||||
Some(mask)
|
||||
}
|
||||
|
||||
/// Iter 18d.3: emit a partial-drop sequence inline at a let-close
|
||||
/// site whose binder has moved-out pattern slots. Replaces the
|
||||
/// uniform `drop_<m>_<T>(ptr)` call: load each pointer-typed
|
||||
@@ -548,15 +735,15 @@ impl<'a> Emitter<'a> {
|
||||
/// transferred to a pattern-bound binder that owns the dec
|
||||
/// for them.
|
||||
///
|
||||
/// Iter 18g.2 widens the input set: with `Term::App` (Own-
|
||||
/// returning call) now also trackable, a let-binder whose value is
|
||||
/// `Term::App` may accumulate moved slots if its body pattern-
|
||||
/// matches the binder. The per-field static-ctor emission below
|
||||
/// is not usable in that case (the runtime tag is dynamic, not
|
||||
/// statically known from `value`'s shape); fall back to shallow
|
||||
/// `ailang_rc_dec`. The non-moved field cells leak under that
|
||||
/// path — same dynamic-tag partial-drop debt that 18d.4's match
|
||||
/// arm already documents.
|
||||
/// Iter 18g.2 widened the input set to `Term::App` (Own-
|
||||
/// returning call). The static per-field emission below is keyed
|
||||
/// against a `Term::Ctor`'s known ctor; a `Term::App` binder
|
||||
/// whose body pattern-matches it has a *dynamic* runtime tag.
|
||||
/// Iter 18g.tidy.fu2 closes that path: instead of falling back
|
||||
/// to shallow `ailang_rc_dec` (which leaked the unmoved fields),
|
||||
/// we route through the tag-conditional helper
|
||||
/// [`Self::emit_partial_drop_fn_for_type`], which dispatches on
|
||||
/// the runtime tag and dec's only the unmoved fields.
|
||||
pub(crate) fn emit_inlined_partial_drop(
|
||||
&mut self,
|
||||
value: &Term,
|
||||
@@ -566,21 +753,29 @@ impl<'a> Emitter<'a> {
|
||||
let (type_name, ctor_name) = match value {
|
||||
Term::Ctor { type_name, ctor, .. } => (type_name.as_str(), ctor.as_str()),
|
||||
_ => {
|
||||
// Iter 18g.2 fallback: dynamic-tag partial-drop debt.
|
||||
// `value` is `Term::App` (Own-returning) or `Term::Lam`
|
||||
// — `is_rc_heap_allocated` returns true for both, and a
|
||||
// body that pattern-matches such a binder will populate
|
||||
// `moved_slots`. We don't know the active ctor
|
||||
// statically, so we shallow-dec the outer cell. Non-
|
||||
// moved field cells leak under this path; closing the
|
||||
// leak requires a tag-conditional partial-drop helper
|
||||
// (analogous to the per-type drop fn but parameterised
|
||||
// on the tag). Tracked as future work — same family as
|
||||
// 18d.4's `(case (LCons h t) ...)` carve-out.
|
||||
let _ = (val_ssa, moved); // keep compiler quiet about unused
|
||||
self.body.push_str(&format!(
|
||||
" call void @ailang_rc_dec(ptr {val_ssa})\n"
|
||||
));
|
||||
// Iter 18g.tidy.fu2: dynamic-tag partial-drop via the
|
||||
// per-type helper. `value` is `Term::App` (Own-
|
||||
// returning) — the binder's static type is the App's
|
||||
// ret type, recovered through `synth_arg_type` /
|
||||
// `partial_drop_symbol_for_type`. `Term::Lam` shapes
|
||||
// never reach here with a non-empty `moved` (you can't
|
||||
// pattern-match a closure-pair); the fallback below
|
||||
// handles them defensively.
|
||||
let sym = self
|
||||
.synth_arg_type(value)
|
||||
.ok()
|
||||
.and_then(|ty| self.partial_drop_symbol_for_type(&ty));
|
||||
if let (Some(sym), Some(mask)) =
|
||||
(sym, Self::build_moved_mask(moved))
|
||||
{
|
||||
self.body.push_str(&format!(
|
||||
" call void @{sym}(ptr {val_ssa}, i64 {mask})\n"
|
||||
));
|
||||
} else {
|
||||
self.body.push_str(&format!(
|
||||
" call void @ailang_rc_dec(ptr {val_ssa})\n"
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user