Files
AILang/examples/loop_recur_heap_binder_no_leak_pin.ail
T
Brummel f488d314e8 fix(codegen): dec superseded heap loop-binder values across recur (ADT leg)
A heap value threaded through a `(loop ...)`/`recur` as a loop binder
leaked: the `Term::Recur` arm stored each new arg into the binder's
loop-carried alloca with a plain `store` and never RC-dec'd the heap
value already in that slot, so every iteration's superseded value was
lost. The scope-close drop path (drop.rs, commit 8bcdae1) only ever
sees the loop's FINAL result, never the intermediates.

The recur store now decs the prior value before overwriting it, gated
by the SAME predicate the scope-close path uses — RC-heap AND lowers
to `ptr` AND not `Str`. The binder slot tuple is widened to carry the
binder's AILang type so the gate and the typed drop-symbol lookup
(`field_drop_call`) are available at the store. A same-pointer guard
(`icmp eq prior, new`) skips the dec when a `recur` threads the
identical pointer back — the own->own case (RawBuf fill loops, where
`RawBuf.set` mutates in place), so a retained buffer is never freed.

Scope: the boxed-ADT leg only. Every ADT value is heap-allocated, so
dec'ing a superseded ADT binder is unambiguously UB-free. A `Str`
accumulator is deliberately NOT covered: `Str` literals lower to
constexpr-GEPs into static globals (no rc_header), so a `Str` loop
seed may be static and dec'ing it would be UB. That is the same
static-vs-heap-`Str` obstacle behind 8bcdae1's deliberate `Str`
exclusion; the `Str` accumulator leak remains open in #49 pending a
runtime representation decision.

RED test in crates/ail/tests/loop_recur_heap_binder_no_leak_pin.rs
threads a boxed `Cell` through three recurs: pre-fix allocs=5 frees=2
live=3, post-fix live=0, stdout 2. Verified: zero-recur ADT control
live=0 (no spurious dec), Int loops (isqrt/collatz/gcd) live=0
(non-ptr, no dec), RawBuf fill loops live=0 out 35/16 (same-pointer
guard), Str accumulator unchanged (live=3, no crash).

refs #49
2026-05-30 17:48:51 +02:00

30 lines
1.5 KiB
Plaintext

(module loop_recur_heap_binder_no_leak_pin
; RED for bug #49: a heap loop binder that `recur` REPLACES with a fresh
; heap value each iteration. Each superseded value is a distinct heap
; allocation; codegen's Term::Recur arm stores the new binder value into
; the loop-carried alloca without RC-dec'ing the prior one, so every
; overwritten value leaks.
;
; A boxed ADT (`Cell`), NOT a `Str`, is the deliberate trigger: every
; ADT value is heap-allocated (there is no static-literal representation
; for an ADT), so the per-iteration dec is unambiguously UB-free. `Str`
; literals lower to constexpr-GEPs into static globals (lib.rs Str-lit
; arm), so a `Str` loop seed is static and must NOT be dec'd — the Str
; case is the separate static-vs-heap-Str representation question
; (the same obstacle behind the deliberate Str exclusion in 8bcdae1).
;
; allocs: seed Cell(0) + three recur Cells (Cell at i=0,1,2) = 4 heap
; slabs. The loop returns acc=Cell(2); that final result drops at scope
; close (the drop.rs path). The three superseded Cells (seed, Cell@i=0,
; Cell@i=1) must drop at the recur store. Pre-fix: live=3. Post-fix:
; live=0. Expected stdout: 2.
(data Cell (ctor Cell (con Int)))
(fn main (type (fn-type (params) (ret (con Unit)) (effects IO))) (params)
(body
(let final
(loop (acc (con Cell) (term-ctor Cell Cell 0)) (i (con Int) 0)
(if (app ge i 3)
acc
(recur (term-ctor Cell Cell i) (app + i 1))))
(match final (case (pat-ctor Cell n) (app print n)))))))