Free superseded heap loop-binder values across recur #49
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
A heap value threaded through a
(loop ...)/recuras a loop binderleaks: each iteration's superseded binder value (the old accumulator,
replaced by the
recur) is never RC-dec'd. The leak is observed evenwhen the loop's final result is consumed.
This is the per-iteration recur-dec gap, distinct from the scope-close
drop of a let-bound loop result, which was fixed in commit
8bcdae1(
fix(codegen): drop a let-bound owned loop result at scope close). Thatcommit deliberately excluded
Strfrom the loop-result drop because aloop can return a static
Str; this issue is the separate, broader leakof the intermediate per-iteration values.
Reproduction (verified)
Module (a
Straccumulator built across aloop/recur, resultconsumed by
print):stdout is correct (
xyyy);live=3is the leak — the three supersededaccslabs (the initial"x"and the two intermediate concat results)are never freed.
Scope
RawBuf.setmutates in place (own -> own, noper-iteration allocation), so a RawBuf loop binder allocates exactly
one slab and does not exhibit this leak; the RawBuf fill-loop fixtures
(
examples/fieldtest/rawbuf_1_score_table.ail,rawbuf_2_running_max.ail) are leak-clean underAILANG_RC_STATS.is affected (heap
Str, boxed ADTs). Only theStrcase is verifiedabove.
loop/recurlowering incrates/ailang-codegen/, which does not emit anRC-dec for a loop binder's prior value when
recurrebinds it. Thescope-close path in
crates/ailang-codegen/src/drop.rsis a differentmechanism and is not where this leak lives.
Context
Surfaced during the raw-buf fieldtest follow-up (the broader observation
behind finding B2,
docs/specs/0058-fieldtest-raw-buf.md). Pre-existing:loop accumulators in the shipped fixtures are
Int(isqrt, collatz,gcd), which carry no RC, so the gap had no prior repro.
Acceptance
Straccumulator threaded through aloop/recurreportslive=0underAILANG_RC_STATSlive=0ADT leg landed in
f488d31(fix(codegen): dec superseded heap loop-binder values across recur (ADT leg)).The recur store now RC-dec's the superseded prior binder value, gated by the scope-close predicate (RC-heap AND lowers to
ptrAND notStr), with a same-pointer guard so own->own threading (RawBuf fill loops) is never decremented. Acceptance bullet 2 (boxed-ADT accumulator) is satisfied: aCellthreaded through three recurs reportslive=0(waslive=3).Remaining open: acceptance bullet 1 (heap-
Straccumulator ->live=0). Blocked on a representation question, not a missing dec.Strliterals lower to constexpr-GEPs into static globals (crates/ailang-codegen/src/lib.rs, Str-literal arm) — a staticStrcarries no rc_header, so dec'ing aStrloop binder is UB when it holds a literal (e.g. the seed"x").runtime/rc.chas no static-vs-heap-Strdiscriminator.Same obstacle as the deliberate
Strexclusion in8bcdae1(which left the final-resultStrloop leak in place): both the per-iteration supersededStrand the final-resultStrcannot be safely RC-dec'd until static-Strand heap-Strpointers are distinguishable at runtime. Verified: a 3-iterationStraccumulator reportsallocs=4 frees=1 live=3, stdout correct.Candidate directions for the
Strleg (none chosen; touches content-addressing /Strhashing, so this is a design decision):Strdiscriminator (header sentinel or tagged pointer) soailang_rc_decno-ops on staticStr— general, but interacts withStridentity/hashing.Strloop seed into a heap slab on loop entry so the binder is always heap — narrower, but has a seed-provenance subtlety (the seed may already be a heap value to retain rather than copy).Str-loop leak as-is and document it.