fix(codegen): drop owned values on let/seq-wrapped tail-call arms (#63 leg 2)
After leg 1, a tail call wrapped in a trailing `(let ...)` / `(seq ...)`
still leaked: `arm_body_is_tail_call` matched only a direct
App/Do{tail:true} arm body, so for an MTerm::Let/Seq body the gate was
false and both pre-tail-call drops (the scrutinee-husk shallow-dec and
the leg-1 owned-param dec) were skipped — while lower_term still
descended through the wrapper and emitted the inner call as musttail.
The fix replaces the one-level match with a local recursive
`tail_position_is_tail_call` that peels through a trailing Let-body /
Seq-rhs chain to its tail-position sub-term. This is a contained fix:
the predicate feeds only the two drop-emission gates, never the
musttail-emission decision (which is independent, in lower_term off
App{tail:true}), so widening it restores the drops without changing
which calls are tail calls. The let-bound value's scope-close drop is
gated on `!block_terminated` and stays correctly skipped, so the
forwarded let binding is not double-freed.
Verified: the leg-2 pin goes green (live=0, was live=4); the leg-1 pin
stays green; full `cargo test --workspace` 0 failures, no double-free /
underflow / IR-snapshot delta; lockstep pairs untouched.
examples/series_sma.ail drops from live=8 to live=2 with unchanged
stdout (3.0 / 5.33333 / 5.66667 / 5.33333).
Not yet leak-clean: the residual live=2 is a THIRD, distinct leg — an
(own) heap param consumed on one match arm but live on a sibling
base-case (non-tail fall-through) arm is not dropped there, because the
fn-return Own-param dec gates on the per-fn AGGREGATE consume_count
(>=1 here, from the recursive arm) rather than per-arm liveness. Routed
as its own RED-first bugfix.
refs #63
This commit is contained in:
@@ -714,10 +714,23 @@ impl<'a> Emitter<'a> {
|
|||||||
// values that are now owned by downstream frames —
|
// values that are now owned by downstream frames —
|
||||||
// that's exactly the bug 18d.3's `moved_slots` was set
|
// that's exactly the bug 18d.3's `moved_slots` was set
|
||||||
// up to prevent.
|
// up to prevent.
|
||||||
let arm_body_is_tail_call = matches!(
|
// A tail call may be the arm body directly, or wrapped in a
|
||||||
&arm.body,
|
// trailing `(let ...)` / `(seq ...)` chain whose tail-position
|
||||||
MTerm::App { tail: true, .. } | MTerm::Do { tail: true, .. }
|
// sub-term (a `Let`'s body, a `Seq`'s rhs) is the tail call.
|
||||||
);
|
// `lower_term` descends through the wrapper and still emits the
|
||||||
|
// inner call as `musttail`, so the two drop-emission gates below
|
||||||
|
// (husk-dec, leg-1 owned-param dec) must fire for the wrapped
|
||||||
|
// form too — otherwise the scrutinee husk and the non-forwarded
|
||||||
|
// owned param each leak one slab per recursion step (#63, leg 2).
|
||||||
|
fn tail_position_is_tail_call(t: &MTerm) -> bool {
|
||||||
|
match t {
|
||||||
|
MTerm::App { tail: true, .. } | MTerm::Do { tail: true, .. } => true,
|
||||||
|
MTerm::Let { body, .. } => tail_position_is_tail_call(body),
|
||||||
|
MTerm::Seq { rhs, .. } => tail_position_is_tail_call(rhs),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let arm_body_is_tail_call = tail_position_is_tail_call(&arm.body);
|
||||||
if matches!(self.alloc, AllocStrategy::Rc)
|
if matches!(self.alloc, AllocStrategy::Rc)
|
||||||
&& arm_body_is_tail_call
|
&& arm_body_is_tail_call
|
||||||
&& scrutinee_is_owned
|
&& scrutinee_is_owned
|
||||||
|
|||||||
Reference in New Issue
Block a user