diff --git a/crates/ailang-codegen/src/match_lower.rs b/crates/ailang-codegen/src/match_lower.rs index e872138..7ec413f 100644 --- a/crates/ailang-codegen/src/match_lower.rs +++ b/crates/ailang-codegen/src/match_lower.rs @@ -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