The last leg of the #63 drop-soundness cluster: an (own ...) heap param
consumed on one match-arm / if-branch but live on a sibling was never
dropped on the live path, leaking one slab per call. This was the
residual live=2 leak in series_sma that kept the series headline from
being leak-clean.
Root: the per-fn aggregate consume_count (the worst-case max over all
branches from uniqueness::merge_states) was codegen's only consume
signal, and every owned-param drop site gated on it. A param consumed
on SOME branch makes the aggregate >= 1, so every site skipped it —
including the branch where it stays live. match and if share this root
(if is first-class MTerm::If, not desugared to match).
Mechanism (spec 0068):
- CAPTURE: uniqueness retains the per-branch consume snapshots it
already computes per branch and discarded at the max merge — new
BranchConsume + infer_module_with_cross_branches (the aggregate-only
infer_module_with_cross now delegates and drops the channel).
- CARRY: additive MArm.consume / MTerm::If.{then,else}_consume, attached
by lower_to_mir via a traversal-order cursor (post-order pop, kind- and
exhaustion-asserts make a desync a loud panic; neutral for const bodies
which uniqueness does not walk). The AST carries no node id, so the
correspondence is the structural pre-order both walks share — pinned by
branch_consume_maps_attach_to_matching_arms.
- GATE: emit_leakclass_branch_param_drops fires a fall-through drop only
for the leak class (branch_consume==0 AND aggregate>=1), in match arms
+ both if branches; the pre-tail-call dec switches its gate source from
the aggregate to per-arm. The fn-return dec and arm-close pattern-binder
dec are UNCHANGED.
Safety:
- Double-free: the drop sites partition by aggregate (==0 -> existing
fn-return/pre-tail-call; >=1 -> new per-branch). Disjoint, so no param
is dropped twice — no fn-return disable, no tail-position analysis.
- Use-after-free: the checker rejects use-after-consume, so an
aggregate>=1 param is provably dead past the construct (path-terminal
drop, tail position irrelevant).
- Type eligibility (found by the full-suite gate during implement; spec
0068 refined): the new agg>=1 site is the FIRST drop site that can
reach a STATIC closure-pair param (a top-level fn ref like inc in
Either.either, consumed on one arm, live on the other) — a .rodata
constant with no rc-header. field_drop_call routes Type::Fn / static-Str
/ Type::Var to the bare ailang_rc_dec, which underflowed on it. The
helper now drops only params with a real per-type heap-ADT drop fn
(field_drop_call != "ailang_rc_dec"). Sound (no underflow) and
leak-correct (a static closure/Str allocates nothing). Witnessed green
by std_either_demo / std_list_demo / poly_rec_capture_demo.
Verification: full workspace suite green (116 binaries); both new leak
pins (if + match) and series_sma_no_leak_pin green at live=0; legs 1/2
pins still green; INTERCEPTS<->(intrinsic) bijection intact; no
Pattern::Lit reject path added.
The leg-3 helper's type precondition was applied inline by the
orchestrator (a narrowing guard clause in an already-reviewed Task-4
helper, full context loaded) after the implement-orchestrator correctly
surfaced the spec gap rather than papering over the regression.
This clears the series_sma leak tail; the series milestone (#61) close
stays a separate deliberate step (its end-to-end milestone fieldtest).
closes#63