fix(codegen): drop owned params on tail-call-terminated match arms (#63 leg 1)
An owned heap ADT param that a tail-recursive match arm neither forwards into the tail call nor consumes had no drop site: the fn-return Own-param dec in lib.rs is gated behind `!block_terminated` and so fires only on the fall-through `ret` path, while the per-arm drop in match_lower.rs handled only pattern binders. A `musttail call`-terminated arm therefore leaked one slab per recursion step (#63, leg 1). match_lower.rs now emits a pre-tail-call Own-param dec, gated exactly like the fn-return path: Rc strategy, `arm_body_is_tail_call`, ParamMode::Own, ptr-typed, and uniqueness `consume_count == 0`. For an Own param consume_count==0 also implies "not forwarded into the tail call" — a forwarded Own arg is walked Position::Consume and carries consume_count>=1 — so the gate cannot drop a param the callee took ownership of. The drop routes through the same per-type / partial-drop machinery (honouring moved_slots) as the fn-return path. The match scrutinee param is explicitly skipped: its ownership is already discharged by the pre-tail-call scrutinee-husk shallow-dec plus the moved-out field binders, so dropping it here would dec the same outer cell twice (a double-handling the first cut of this fix introduced and the quality review caught — the pin read live=0 but the IR showed two decs on the scrutinee cell). Verified: the committed RED pin is green (live=0, allocs==frees); full `cargo test --workspace` 0 failures; the fix introduces no under-free (loop_recur_heap_binder / loop_recur_str_binder / loop_str_recur_literal / rc_pin_recurse_implicit / lit_pat_ctor_tail_drop all stay live=0, no underflow to depth 5). No INTERCEPTS/(intrinsic) or pre_desugar_validation change; no IR snapshot refresh needed. Leg 1 only. Leg 2 — a `let`/`seq`-wrapped tail call makes the arm body an MTerm::Let so `arm_body_is_tail_call` is false, skipping both the husk-dec and this new param-dec — still leaks (examples/series_sma.ail stays live=8). It gets its own RED pin and fix next. refs #63
This commit is contained in:
@@ -756,6 +756,90 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// pre-tail-call Own-param dec. The fn-return Own-param dec
|
||||
// (lib.rs, gated behind `!block_terminated`) fires only on
|
||||
// the fall-through `ret` path. A tail-call-terminated arm
|
||||
// emits `musttail call ... ret` and sets
|
||||
// `block_terminated = true`, so that block is skipped — an
|
||||
// owned param the arm neither forwards nor consumes then has
|
||||
// no drop site and leaks one slab per recursion step (#63).
|
||||
//
|
||||
// The gate mirrors the fn-return path: Rc strategy, the
|
||||
// param is `ParamMode::Own`, ptr-typed, and uniqueness
|
||||
// recorded `consume_count == 0`. For an Own param,
|
||||
// `consume_count == 0` also means it was not forwarded into
|
||||
// the tail call: a forwarded Own arg is walked in
|
||||
// Position::Consume by the uniqueness pass and would carry
|
||||
// `consume_count >= 1`, so this gate cannot drop a param the
|
||||
// callee took ownership of. Borrow params are excluded by
|
||||
// the mode check. The drop routes through the same per-type
|
||||
// / partial-drop machinery the fn-return path uses, honouring
|
||||
// `moved_slots` so a param whose fields were moved out is not
|
||||
// double-freed.
|
||||
if matches!(self.alloc, AllocStrategy::Rc) && arm_body_is_tail_call {
|
||||
let owned_params: Vec<(String, ParamMode)> = self
|
||||
.current_param_modes
|
||||
.iter()
|
||||
.map(|(n, m)| (n.clone(), *m))
|
||||
.collect();
|
||||
for (pname, mode) in &owned_params {
|
||||
if !matches!(mode, ParamMode::Own) {
|
||||
continue;
|
||||
}
|
||||
// Skip the param that IS this match's scrutinee: its
|
||||
// ownership is already discharged by the pre-tail-call
|
||||
// scrutinee-husk shallow-dec above (plus the moved-out
|
||||
// field binders). Dropping it again here would dec the
|
||||
// same outer cell twice.
|
||||
if scrutinee_binder.as_deref() == Some(pname.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let p_ssa = format!("%arg_{}", pname);
|
||||
let p_ail = self
|
||||
.locals
|
||||
.iter()
|
||||
.find(|(_, ssa, _, _)| ssa == &p_ssa)
|
||||
.map(|(_, _, lty, ail)| (lty.clone(), ail.clone()));
|
||||
let (p_lty, p_ail) = match p_ail {
|
||||
Some(t) => t,
|
||||
None => continue,
|
||||
};
|
||||
if p_lty != "ptr" {
|
||||
continue;
|
||||
}
|
||||
let consume_count = self
|
||||
.consume
|
||||
.get(&(self.current_def.clone(), pname.clone()))
|
||||
.copied()
|
||||
.unwrap_or(u32::MAX);
|
||||
if consume_count != 0 {
|
||||
continue;
|
||||
}
|
||||
let moves = self
|
||||
.moved_slots
|
||||
.get(pname)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if moves.is_empty() {
|
||||
let drop_call = self.field_drop_call(&p_ail);
|
||||
self.body.push_str(&format!(
|
||||
" call void @{drop_call}(ptr {p_ssa})\n"
|
||||
));
|
||||
} else {
|
||||
let sym = self.partial_drop_symbol_for_type(&p_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"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let (val, vty) = self.lower_term(&arm.body)?;
|
||||
// arm-close pattern-binder dec. Symmetric to
|
||||
// 18c.3/18c.4's `Term::Let`-scope-close drop emission, but
|
||||
|
||||
Reference in New Issue
Block a user