Files
AILang/examples/raw_buf_loop_no_leak_pin.ail
T
Brummel 8bcdae1b53 fix(codegen): drop a let-bound owned loop result at scope close
An owned heap value whose `let`-binding value is a `(loop ...)` result,
afterwards only borrow-read (or unused), was never dropped at scope close
— a memory leak (AILANG_RC_STATS live=1). Newly observable once #47 made
fill-loops build: rawbuf_2_running_max leaked one buffer per run.

Root cause: is_rc_heap_allocated — the predicate the let-lowering uses to
decide a scope-close `dec` — matched only Term::Ctor, Term::Lam and
Own-returning Term::App. A Term::Loop-valued binder fell through to false,
so it was never registered for drop. (Sibling of the #47 synth gap: the
Term::Loop arm under-handled in yet another pass.)

Fix: add a Term::Loop arm that tracks the binder iff the loop's static
result type lowers to llvm `ptr` (boxed/heap) AND is not Str — and widen
drop_symbol_for_binder's App arm to cover Term::Loop so it resolves the
per-type drop symbol. Two load-bearing safety gates:
  - the `ptr` gate excludes unboxed primitives (Int/Bool/Float/Unit), so
    an Int-returning loop is not handed to a drop fn;
  - Str is excluded because a loop can return a *static* Str (a seed
    literal threaded unchanged) and ailang_rc_dec on a static-Str pointer
    is UB; an Own-App can never return a static Str, which is why the App
    arm may track Str but the Loop arm must not.
The loop's seed binders are consumed (moved in), so nothing else tracks
the result; linearity guarantees no alias, so the new drop is single.

Verified leak-clean and double-free-free across the fieldtest RawBuf set,
the consumed case, the Int-gate case, and the escape case (a fn returning
a loop-built owned RawBuf to its caller).

The separate per-iteration leak of superseded heap loop-binder values
(e.g. a Str accumulator threaded through recur) is a distinct, broader
pre-existing bug, filed separately — not addressed here.

RED-first: raw_buf_loop_no_leak_pin.
2026-05-30 17:14:28 +02:00

11 lines
1.0 KiB
Plaintext

(module raw_buf_loop_no_leak_pin
(fn main
(doc "RED-pin fixture for the loop-valued let-binder scope-close drop gap. Under --alloc=rc, an owned `(RawBuf Int)` whose let-value is a `(loop ...)` result and which is afterwards only borrow-read (RawBuf.get) is NEVER dropped at scope close. `is_rc_heap_allocated` (crates/ailang-codegen/src/drop.rs) has no `Term::Loop` arm, so the `Term::Let` lowering never flags the binder `filled` as trackable and emits no drop. AILANG_RC_STATS=1 reports `allocs=2 frees=1 live=1` (the RawBuf slab leaks). Compare examples/raw_buf_drop_min.ail (direct `(new ...)` value, no loop) which is dropped correctly. Expected post-fix: `allocs == frees && live == 0`.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(let buf (new RawBuf (con Int) 4)
(let filled (loop (b (con RawBuf (con Int)) buf) (i (con Int) 0)
(if (app ge i 4) b (recur (app RawBuf.set b i i) (app + i 1))))
(app print (app RawBuf.get filled 0)))))))