fix: 18d.4 Iter A — gate arm-close pattern-binder dec on scrutinee mode
Iter B (Own-param dec at fn return) gates on param_modes[i] ==
ParamMode::Own and skips Implicit/Borrow because those modes carry
no caller-handed-off-ownership signal. Iter A (arm-close pattern-
binder dec) is the same shape — pattern-binders loaded out of a
scrutinee owe their drop-validity to the scrutinee's ownership —
but Iter A had no such gate and fired on every ptr-typed binder
with consume_count == 0.
Concrete failure (regression test): pin (params t) is Implicit,
caller loop holds t and re-passes it to itself. pin's (TNode v l r)
arm dec'd l and r, fragmenting the tree the caller still references.
Next recursion tripped ailang_rc_dec underflow.
Fix: thread current_param_modes onto the emitter, set it in
emit_fn from the fn type's param_modes (and reset/save it across
lambda thunk emission). In lower_match, derive scrutinee_is_owned
from the scrutinee's mode (Own only; let-binders and temps are
treated as owned) and skip Iter A when not owned.
Carve-out: a let-alias of a non-Own param is not yet detected;
the regression doesn't trigger it and a propagation pass belongs
in its own iter. Recorded in JOURNAL.
Red: alloc_rc_pattern_bind_in_implicit_fn_does_not_dec_borrowed_children.
Pre-fix the rc binary aborted with refcount underflow; post-fix
matches alloc=gc ("0").
This commit is contained in:
@@ -587,6 +587,21 @@ struct Emitter<'a> {
|
||||
/// particular binder are removed when that binder leaves scope
|
||||
/// (on `Term::Let` body close, on match-arm body close).
|
||||
moved_slots: BTreeMap<String, BTreeSet<usize>>,
|
||||
/// Iter 18d.4 fix: per-fn-body parameter modes, keyed by parameter
|
||||
/// name. Set once at the top of `emit_fn` (and at lambda thunk
|
||||
/// entry) from the fn type's `param_modes`. Consulted by
|
||||
/// `lower_match`'s arm-close pattern-binder dec emission (Iter A) to
|
||||
/// decide whether the scrutinee was statically owned: if the
|
||||
/// scrutinee resolves to a fn-param whose mode is `Borrow` or
|
||||
/// `Implicit`, the pattern-binder dec must NOT fire — the caller
|
||||
/// still holds a reference and dec'ing the pattern-binder would
|
||||
/// fragment the caller's structure.
|
||||
///
|
||||
/// Symmetric with the Iter B gate at fn return (`emit_fn`'s Own-
|
||||
/// param dec): both sites must check the param-mode signal before
|
||||
/// dec'ing, because Implicit and Borrow do not carry the "caller
|
||||
/// handed off ownership" signal that makes the dec safe.
|
||||
current_param_modes: BTreeMap<String, ParamMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -701,6 +716,7 @@ impl<'a> Emitter<'a> {
|
||||
uniqueness,
|
||||
closure_drops: BTreeMap::new(),
|
||||
moved_slots: BTreeMap::new(),
|
||||
current_param_modes: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1302,6 +1318,15 @@ impl<'a> Emitter<'a> {
|
||||
self.lam_counter = 0;
|
||||
// Iter 18d.3: move tracking is per-fn-body.
|
||||
self.moved_slots.clear();
|
||||
// Iter 18d.4 fix: param-mode lookup is per-fn-body. Built from
|
||||
// the fn type's `param_modes` (already destructured above) and
|
||||
// consulted by `lower_match`'s Iter A gate to skip arm-close
|
||||
// pattern-binder dec when the scrutinee is a non-Own param.
|
||||
self.current_param_modes.clear();
|
||||
for (i, pname) in f.params.iter().enumerate() {
|
||||
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
|
||||
self.current_param_modes.insert(pname.clone(), mode);
|
||||
}
|
||||
// Iter 17a: run escape analysis over the fn body. The result
|
||||
// is queried at every `Term::Ctor` / `Term::Lam` lowering site
|
||||
// to decide between `alloca` (non-escaping) and `@GC_malloc`
|
||||
@@ -2732,7 +2757,35 @@ impl<'a> Emitter<'a> {
|
||||
// 18d.3's `moved_slots` interrupts the `xs`-cascade
|
||||
// through the tail slot. Now the arm itself dec's `t`,
|
||||
// freeing the 4-cell tail.
|
||||
if matches!(self.alloc, AllocStrategy::Rc) && !self.block_terminated {
|
||||
//
|
||||
// Iter 18d.4 fix (regression
|
||||
// `alloc_rc_pattern_bind_in_implicit_fn_does_not_dec_borrowed_children`):
|
||||
// Iter A is symmetric to Iter B (Own-param dec at fn
|
||||
// return) and must share Iter B's mode gate. If the
|
||||
// scrutinee resolves to a fn-param whose mode is
|
||||
// `Borrow` or `Implicit`, the caller still holds a
|
||||
// reference to the heap value the pattern-binders were
|
||||
// loaded from. Dec'ing those binders fragments the
|
||||
// caller's structure (refcount underflow on subsequent
|
||||
// accesses; segfault under deeper recursion).
|
||||
//
|
||||
// Only `Own` carries the static "caller handed off
|
||||
// ownership" signal that makes the children-dec safe.
|
||||
// Non-fn-param scrutinees (let-binders, temp
|
||||
// expressions) are treated as owned — the let-binder
|
||||
// owns its RC ref, and a temp scrutinee was just
|
||||
// produced by the local frame.
|
||||
let scrutinee_is_owned: bool = match &scrutinee_binder {
|
||||
Some(sb) => match self.current_param_modes.get(sb) {
|
||||
Some(mode) => matches!(mode, ParamMode::Own),
|
||||
None => true,
|
||||
},
|
||||
None => true,
|
||||
};
|
||||
if matches!(self.alloc, AllocStrategy::Rc)
|
||||
&& !self.block_terminated
|
||||
&& scrutinee_is_owned
|
||||
{
|
||||
for (bname, b_ssa, b_lty, b_ail) in &arm_pushed_binders {
|
||||
if b_lty != "ptr" {
|
||||
continue;
|
||||
@@ -3327,6 +3380,18 @@ impl<'a> Emitter<'a> {
|
||||
// tracking — the outer fn's binders are not in scope inside
|
||||
// the thunk body (only its captures + params). Save and reset.
|
||||
let saved_moved = std::mem::take(&mut self.moved_slots);
|
||||
// Iter 18d.4 fix: a lambda thunk is its own fn frame for
|
||||
// param-mode lookup. The outer fn's params are not in scope
|
||||
// inside the thunk; the thunk's own params are pushed below
|
||||
// and (currently) carry no mode annotation, so they default
|
||||
// to `Implicit` — `lower_match`'s Iter A gate will skip arm-
|
||||
// close pattern-binder dec for matches on lambda params,
|
||||
// mirroring the fn-level Implicit-param treatment.
|
||||
let saved_param_modes = std::mem::take(&mut self.current_param_modes);
|
||||
for pname in lam_params.iter() {
|
||||
self.current_param_modes
|
||||
.insert(pname.clone(), ParamMode::Implicit);
|
||||
}
|
||||
// Lambdas inside lambdas are fine: they get their own counter
|
||||
// namespace within the enclosing thunk. They share the
|
||||
// `deferred_thunks` queue (top-level body owns it).
|
||||
@@ -3409,6 +3474,7 @@ impl<'a> Emitter<'a> {
|
||||
self.ssa_fn_sigs = saved_sigs;
|
||||
self.non_escape = saved_non_escape;
|
||||
self.moved_slots = saved_moved;
|
||||
self.current_param_modes = saved_param_modes;
|
||||
|
||||
// 3. Emit allocation + capture filling + closure-pair packing
|
||||
// in the OUTER body. Captures use 8 bytes each; closure-pair
|
||||
|
||||
Reference in New Issue
Block a user