test: RED owned-RawBuf drop-leak repro (refs #43)

raw_buf_owned_drop_balances_rc_stats + examples/raw_buf_drop_min.ail:
a single owned 1-slot Int RawBuf, written once and read once. Prints
10 correctly but under AILANG_RC_STATS shows allocs=2 frees=1 live=1 —
the slab leaks. Asserts stdout=="10" (green) and live==0 (RED until
the owned-temp drop call is emitted).

Root cause (verified, supersedes #42's disproven diagnosis). The
leaked value is the anonymous owned temporary produced by RawBuf.set
(own-ret, in-place: intercepts.rs emit_rawbuf_set_int does `ret ptr
%b`, same slab as its input), then passed to RawBuf.get whose RawBuf
param is `borrow`. After get borrows and returns an i64 the temp is
dead, but no one drops it: codegen's only two scope-close drop
emitters cover Term::Let binders (lib.rs:1785-1822) and Own fn-params
(lib.rs:1426-1502); the general call lowerer emit_call (lib.rs:2552)
splices args without inspecting per-arg own/borrow modes and emits no
post-call drop for an owned arg landing in a borrow slot.

Binder-independent, contra #42: the fully inline form with no `let`
at all produces byte-identical leaking IR (no Term::Let exists, so the
1785 gate is never reached). The defect is the drop-CALL-site emission
raw-buf.4 deferred to raw-buf.5; uniqueness.rs is not implicated.
This commit is contained in:
2026-05-30 10:41:52 +02:00
parent 8ac8756682
commit 47efbdb5c0
2 changed files with 37 additions and 0 deletions
+28
View File
@@ -2234,6 +2234,34 @@ fn raw_buf_int_e2e() {
assert_eq!(out.trim(), "60");
}
/// Property (refs #42): an owned RawBuf that is no longer referenced
/// at the end of `main` must be dropped — every slab alloc is matched
/// by a free, so `live == 0` at exit. The flat drop function
/// `@drop_raw_buf_RawBuf` is emitted (raw-buf.4), but no call to it is
/// emitted in `main`'s body, so the slab leaks (`allocs=2 frees=1`,
/// the lone free being the `int_to_str` string from `print`). RED
/// until the drop call fires.
///
/// NOTE: the leak is independent of binder names. The fully inline
/// form — `(app RawBuf.get (app RawBuf.set (new RawBuf (con Int) 1)
/// 0 10) 0)`, no `let` at all — produces byte-identical leaking IR,
/// as does a distinct-name (`b0`/`b1`) variant. This contradicts the
/// root-cause stated in issue #42 (uniqueness-table binder-name
/// collapse under shadow-rebind): with no binder there is nothing to
/// collapse, yet the drop call is still absent. The defect is in the
/// owned-RawBuf drop-emission decision, not in name-keyed ownership.
#[test]
fn raw_buf_owned_drop_balances_rc_stats() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("raw_buf_drop_min.ail");
assert_eq!(stdout.trim(), "10", "fixture must still read back the stored value");
assert_eq!(
live, 0,
"owned RawBuf leaks {live} slab(s) (allocs={allocs} frees={frees}); \
the drop call for the owned buffer must fire at end of main"
);
}
/// raw-buf.4 Float variant: a 2-slot Float RawBuf (1.5 + 2.5),
/// exercising the `double` load/store emits. Prints `4.0` (the `%g`
/// whole-double `.0` fallback).
+9
View File
@@ -0,0 +1,9 @@
(module raw_buf_drop_min
(fn main
(doc "Minimal RawBuf drop-leak reproducer (refs #42): a single owned 1-slot Int RawBuf, written once and read once, then dropped at the end of main. Prints 10. Under AILANG_RC_STATS the owned buffer's drop call must fire, so allocs are balanced by frees (live == 0). The leak is independent of binder names — the fully inline form (app RawBuf.get (app RawBuf.set (new RawBuf (con Int) 1) 0 10) 0) with no let-binding at all leaks identically.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(app print
(let buf (new RawBuf (con Int) 1)
(app RawBuf.get (app RawBuf.set buf 0 10) 0))))))