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)?;
|
let (val, vty) = self.lower_term(&arm.body)?;
|
||||||
// arm-close pattern-binder dec. Symmetric to
|
// arm-close pattern-binder dec. Symmetric to
|
||||||
// 18c.3/18c.4's `Term::Let`-scope-close drop emission, but
|
// 18c.3/18c.4's `Term::Let`-scope-close drop emission, but
|
||||||
|
|||||||
Reference in New Issue
Block a user