04f75c8b71
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 inf488d31— this is the per-iteration leak of the *intermediate* Str binder values, whichf488d31explicitly 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
34 lines
1.7 KiB
Plaintext
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)))))
|