fix: let-alias-aware mode propagation through Term::Let

Closes the carve-out shared by 18d.4 Iter A and 18g.1's pre-tail-
call seam: a let-binder whose value is Term::Var aliasing a non-
Own fn-param now inherits the param's mode in current_param_modes
for the duration of the let body. Without this, (let a t (match
a ...)) where t is Implicit / Borrow defeated scrutinee_is_owned
(default for non-fn-param scrutinees was 'owned'), and the arm-
close drop fired on pattern-binders whose underlying memory
belongs to the caller — refcount underflow / SIGSEGV on the next
recursion.

Implementation: Term::Let lowering checks if value is Term::Var
referencing a name in current_param_modes; if so, inserts that
mode under the let-binder's name for the body and restores on
let-close (push/pop pattern, symmetric with locals).

Red:
alloc_rc_let_alias_of_implicit_param_does_not_dec_borrowed_children
on examples/rc_let_alias_implicit_param. Pre-fix: SIGSEGV / RC
underflow on the second iteration of loop. Post-fix: matches
alloc=gc ('0').

The Borrow-ret-mode case is already covered by the language-design
rule (typechecker rejects 'consume-while-borrowed' on the
borrow-aliasing shape); the Implicit-mode case is what the carve-
out actually surfaces. Both are now closed at codegen-time.

Two of the three known debts from the 18g tidy now resolved
(stat-of-N earlier in the session, let-alias here). The remaining
debt — tag-conditional partial-drop helper for the dynamic-tag
carve-outs in lower_match and emit_inlined_partial_drop — is the
substantively larger piece and stays queued.
This commit is contained in:
2026-05-08 15:21:40 +02:00
parent c2af5ad3da
commit 2e0006029b
4 changed files with 153 additions and 0 deletions
+35
View File
@@ -1822,7 +1822,42 @@ impl<'a> Emitter<'a> {
let (val_ssa, val_ty) = self.lower_term(value)?;
self.locals
.push((name.clone(), val_ssa.clone(), val_ty.clone(), val_ail));
// Iter 18g.tidy.fu: let-alias-aware mode propagation.
// If `value` is a `Term::Var` referencing a name in
// `current_param_modes`, the let-binder inherits that
// mode for the duration of the body. Without this,
// `(let a t (match a ...))` where `t` is an Implicit
// / Borrow-mode param defeats the
// `scrutinee_is_owned` gate in `lower_match` (the
// gate looks up `a` in `current_param_modes`, misses,
// and defaults to "owned" — Iter A then dec's
// pattern-binders whose underlying memory belongs to
// the caller).
//
// Restored on let-body-close (push/pop pattern).
let inherited_mode: Option<ParamMode> = match value.as_ref() {
Term::Var { name: src } => {
self.current_param_modes.get(src).copied()
}
_ => None,
};
let prior_mode = if let Some(m) = inherited_mode {
let prior = self.current_param_modes.insert(name.clone(), m);
Some(prior)
} else {
None
};
let r = self.lower_term(body);
if let Some(prior) = prior_mode {
match prior {
Some(m) => {
self.current_param_modes.insert(name.clone(), m);
}
None => {
self.current_param_modes.remove(name);
}
}
}
self.locals.pop();
// Iter 18d.3: lift the binder's move set out of the
// side table. The binder is leaving scope here; we