Files
AILang/examples/rc_tail_sum_explicit_leak.ailx
T
Brummel ae2eb2efac fix: 18g.1 — pre-tail-call shallow-dec for moved-from scrutinee outer cell
18d.4 Iter A (arm-close pattern-binder dec) and Iter B (Own-param
dec at fn return) both fire AFTER the arm body lowers; for a tail-
call arm body, lower_term emits 'musttail call ... ret' and sets
block_terminated=true, so neither seam fires. The result: the
scrutinee's outer cell (whose ptr fields were all moved into the
tail-call's args via 18d.3 moved_slots) leaks one cell per
recursion step.

Surfaced by 18f.2's tail-latency bench: explicit-mode RC peaked
at the same 511 MB RSS as implicit-mode RC despite full mode
annotations — the per-op IntList chains were never being freed.
Bencher diagnosed the tail-call drop-elision; this iter implements
the fix.

Fix shape: in lower_match, BEFORE lower_term(arm.body), emit
'call void @ailang_rc_dec(ptr <scrutinee_ssa>)' iff:
  - alloc=Rc,
  - arm.body is structurally Term::App{tail:true} or
    Term::Do{tail:true},
  - scrutinee_is_owned (existing 18d.4 Iter A gate, hoisted),
  - every ptr-typed slot in this ctor's pattern is in
    moved_slots[scrutinee] (i.e. all ptr children are moved into
    binders that own them downstream — a Wild-bound ptr would
    still hold a live ref that the shallow free would strand).

Shallow rc_dec, not field_drop_call: the per-type drop fn would
re-walk ptr fields, dec'ing values now owned by the downstream
frame — exactly the bug 18d.3's moved_slots was set up to prevent.

Hoists scrutinee_is_owned to a single declaration shared by both
the new 18g.1 emission and 18d.4 Iter A.

Red: alloc_rc_explicit_mode_tail_sum_does_not_leak_outer_cells
on examples/rc_tail_sum_explicit_leak — 100 LCons cells leaked
pre-fix, 0 post-fix. Verified end-to-end on
bench_latency_explicit: pre-fix live=11068575,
post-fix live=1048575 (= the persistent tree cache, separate
issue at main-scope-close).

Test infrastructure: build_and_run_with_rc_stats helper +
AILANG_RC_STATS=1 env-var for the runtime atexit summary.
2026-05-08 14:18:30 +02:00

70 lines
1.8 KiB
Plaintext

; Iter 18g.1 RED-test fixture: minimal tail-recursive list-sum
; under explicit-mode + (drop-iterative). Demonstrates the leak
; surfaced by the 18f.2 latency bench: the LCons outer cell, whose
; only ptr field `t` is moved into the tail-app of `sum_acc`, has
; no drop site and leaks once per element.
;
; Built and run with `AILANG_RC_STATS=1` under `--alloc=rc`. The
; stderr summary should show `allocs == frees + small_const`,
; where `small_const` covers the Tree-shaped drop-frames (zero
; here — the program returns Int, no live ADTs at exit).
;
; Pre-fix: `live = N` (one outer LCons cell per consumed element).
; Post-fix: `live = 0`.
(module rc_tail_sum_explicit_leak
(data IntList
(ctor LNil)
(ctor LCons (con Int) (con IntList))
(drop-iterative))
(fn cons_n_acc
(type
(fn-type
(params (con Int) (own (con IntList)))
(ret (own (con IntList)))))
(params n acc)
(body
(if (app == n 0)
acc
(tail-app cons_n_acc
(app - n 1)
(term-ctor IntList LCons (app - n 1) acc)))))
(fn cons_n
(type
(fn-type
(params (con Int))
(ret (own (con IntList)))))
(params n)
(body
(app cons_n_acc n (term-ctor IntList LNil))))
(fn sum_acc
(type
(fn-type
(params (own (con IntList)) (con Int))
(ret (con Int))))
(params xs acc)
(body
(match xs
(case (pat-ctor LNil) acc)
(case (pat-ctor LCons h t)
(tail-app sum_acc t (app + acc h))))))
(fn sum_list
(type
(fn-type
(params (own (con IntList)))
(ret (con Int))))
(params xs)
(body
(app sum_acc xs 0)))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(do io/print_int (app sum_list (app cons_n 100))))))