Files
AILang/examples/loop_recur_str_binder_no_leak_pin.ail
T
Brummel 04f75c8b71 test(codegen): RED-pin #49 Str leg — heap-Str loop binder must not leak across recur
A heap-Str accumulator threaded through a `(loop …)`/`recur` as a loop
binder leaks: each iteration's superseded `str_concat` slab is never
RC-dec'd, even when the loop's final result is consumed. The fixture
reports `allocs=4 frees=1 live=3` under AILANG_RC_STATS (stdout `xyyy`
is correct).

Root (data-flow traced): the `Term::Recur` arm in
crates/ailang-codegen/src/lib.rs (~2131) gates its superseded-value dec
behind `!is_str`. That exclusion exists because a `Str` loop seed may be
a static-literal Str — a constexpr GEP into a packed-struct global with
no rc_header (lib.rs:1612) — and `ailang_rc_dec` on a static pointer
reads garbage at `payload-8` and aborts on underflow (rc.c:221, no
runtime guard, per design/contracts/0011-str-abi.md). So the Str binder's
prior heap slab is overwritten by a plain `store` with no dec.

Distinct from the loop-RESULT scope-close drop (8bcdae1, which also
excluded Str) and from the ADT leg already fixed in f488d31 — this is
the per-iteration leak of the *intermediate* Str binder values, which
f488d31 explicitly left open pending a representation decision.

RED test crates/ail/tests/loop_recur_str_binder_no_leak_pin.rs with
fixture examples/loop_recur_str_binder_no_leak_pin.ail: the Str leg
asserts live=0 (RED at HEAD, live=3); the ADT leg (f488d31) is re-pinned
as a green guard so a regression on the shared Term::Recur dec path is
caught with it.

GREEN side (chosen direction): promote a static-literal Str seed to heap
via str_clone at loop entry so every Str binder slot holds a heap Str
with a valid rc_header, then lift the `!is_str` recur-dec exclusion. This
UPHOLDS the 0011-str-abi codegen-proof invariant (extends "static Str
never reaches dec" to the loop case) rather than adding a runtime guard.

refs #49
2026-05-31 04:09:44 +02:00

34 lines
1.7 KiB
Plaintext

(module loop_recur_str_binder_no_leak_pin
; RED for bug #49 (Str leg): a heap-Str loop binder that `recur`
; REPLACES with a fresh heap value each iteration. The seed is the
; static literal "x"; each `recur` rebinds `acc` to a FRESH heap slab
; returned by `str_concat`. Codegen's Term::Recur arm
; (crates/ailang-codegen/src/lib.rs) decs the superseded prior binder
; value only under the gate `is_ptr && !is_str`; the `!is_str` clause
; excludes a Str binder entirely, so the plain `store` overwrites the
; prior str_concat slab without RC-dec'ing it, leaking it every
; iteration.
;
; The companion ADT fixture (loop_recur_heap_binder_no_leak_pin.ail,
; commit f488d31) is the SAME leak class WITH the dec emitted; the only
; difference is the `!is_str` clause. The Str exclusion was a
; conservatism about the static seed literal: the seed "x" may be a
; constexpr-GEP into a static global with no rc_header, so an
; unconditional dec on iteration 1 would be UB. But the intermediate
; str_concat results ARE heap slabs that must be dec'd.
;
; allocs: three str_concat results (acc="xy","xyy","xyyy") = 3 heap
; slabs (the seed "x" is a static literal, not counted as an alloc).
; The loop returns acc="xyyy"; that FINAL result is consumed by print.
; The two superseded heap results ("xy", "xyy") plus the final "xyyy"
; after print must all be freed. Pre-fix: live=3. Post-fix: live=0.
; Expected stdout: xyyy.
(fn main (type (fn-type (params) (ret (con Unit)) (effects IO))) (params)
(body
(let s
(loop (acc (con Str) "x") (i (con Int) 0)
(if (app ge i 3)
acc
(recur (app str_concat acc "y") (app + i 1))))
(app print s)))))