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
+38
View File
@@ -2201,3 +2201,41 @@ fn alloc_rc_let_binder_for_implicit_returning_app_does_not_drop() {
means the unjustified dec triggered the underflow guard."
);
}
/// Iter 18g tidy follow-up: let-alias-aware mode propagation.
///
/// 18d.4 Iter A's fix gated arm-close pattern-binder dec on the
/// scrutinee's `current_param_modes` lookup, but only fn-params
/// register there. A let-binder whose value is `Term::Var` aliasing
/// a non-Own fn-param defeats the gate: the lookup misses, the
/// default treats it as owned, and the arm-close drop fires on
/// pattern-binders whose underlying memory is still owned by the
/// caller. The fixture's shape:
///
/// (fn pin_aliased (params t) ; t: Implicit-mode (default)
/// (let a t ; a aliases t
/// (match a ; matched-on alias
/// (case (TLeaf) 0)
/// (case (TNode v l r) 1))))
///
/// Pre-fix on a recursive caller passing the same heap value: the
/// arm-close drop fragments the caller's tree, the recursive call
/// re-loads now-freed cells, and the binary aborts (SIGSEGV / RC
/// underflow).
///
/// Post-fix: `current_param_modes` propagates through `Term::Let
/// { value: Term::Var(p), ... }` for the duration of the let body,
/// so a let-aliased Implicit / Borrow scrutinee correctly skips
/// the arm-close drop. Output matches `--alloc=gc` (`0`).
#[test]
fn alloc_rc_let_alias_of_implicit_param_does_not_dec_borrowed_children() {
let example = "rc_let_alias_implicit_param.ail.json";
let stdout_gc = build_and_run_with_alloc(example, "gc");
let stdout_rc = build_and_run_with_alloc(example, "rc");
assert_eq!(stdout_gc.trim(), "0");
assert_eq!(stdout_rc.trim(), "0");
assert_eq!(
stdout_gc, stdout_rc,
"alloc=rc must match alloc=gc on rc_let_alias_implicit_param"
);
}