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:
2026-05-08 13:56:17 +02:00
parent 7e841ad90e
commit e8c6e99a87
5 changed files with 262 additions and 1 deletions
+37
View File
@@ -1974,3 +1974,40 @@ fn iter18e_no_annotation_keeps_recursive_drop_body() {
"without (drop-iterative), the body must not call worklist runtime helpers. Body was:\n{drop_body}"
);
}
/// Iter 18d.4 regression: pattern-binder dec at arm close (Iter A in
/// the 18d.4 emission seam) was firing on pattern-bound pointer
/// fields whose enclosing fn is Implicit-mode. The dec freed memory
/// the caller still referenced, producing refcount underflow or
/// SIGSEGV.
///
/// The fixture's shape — a heap-allocated `t` shared between a
/// pattern-destructuring callee (`pin`) and a recursive call that
/// re-passes `t` (`loop`) — is the canonical case where the bug
/// fires. `pin` does not consume the pattern-bound children (`l`,
/// `r`); pre-fix codegen emitted `drop_<m>_<Tree>(l)` and
/// `drop_<m>_<Tree>(r)` at arm close, dec'ing pointers that lived
/// inside `t`'s box, which the outer `loop`'s next iteration still
/// expects to be valid.
///
/// Properties guarded:
/// (1) The binary exits cleanly under `--alloc=rc` (no SIGSEGV
/// from a use-after-free, no abort from
/// `ailang_rc_dec: refcount underflow` in `runtime/rc.c`).
/// (2) Stdout matches `--alloc=gc` (`0`).
///
/// Pre-fix this test fails with exit code 139 (SIGSEGV) under rc.
/// Post-fix both arms produce the same clean output. Kept as
/// regression coverage — see CLAUDE.md "Bug fixes — TDD, always".
#[test]
fn alloc_rc_pattern_bind_in_implicit_fn_does_not_dec_borrowed_children() {
let example = "rc_pin_recurse_implicit.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_pin_recurse_implicit"
);
}