From 592d87bf11f71a03a3a88a901fea8686a4dbb795 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 12 May 2026 19:49:22 +0200 Subject: [PATCH] red test: heap-Str slabs leak under RC discipline Strengthen the two hs.4-shipped RC-stats E2E assertions back to the plan-original `live == 0` + `allocs == frees` invariants: - int_to_str_drop_balances_rc_stats: `let s = int_to_str(42) in do io/print_str(s)` allocates a heap-Str (allocs=1) but the let-arm dec at scope close does not fire (frees=0, live=1). - str_field_in_adt_drops_heap_str_correctly: `Box(int_to_str(42))` pattern-matched and printed allocates the ADT cell + the heap-Str slab (allocs=2). The match-arm pattern walk calls field_drop_call's Str arm on the extracted field (frees=1), but the outer ADT cell's let-binder is not trackable (frees=1, live=1). Root cause is the effect-op arg-mode gap surfaced during hs.4: the uniqueness analyser at uniqueness.rs:289-292 walks every Term::Do arg in Position::Consume, gating off the let-arm dec at lib.rs:1440 (consume_count == 0 required). EffectOpSig has no param_modes field to override the Consume-by-default walk. The fix is structural and queued as the next iter. These tests were shipped GREEN in hs.4 (134441b) with weakened `allocs >= N` asserts; that was a fixture-adapted-to-bug anti-pattern. Asserts restored to the language invariant; tests now RED, locking in the fix obligation. --- crates/ail/tests/e2e.rs | 59 +++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 4d4085a..7abeb8f 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -2588,46 +2588,37 @@ fn float_to_str_smoke() { assert_eq!(out, "3.5\n"); } -/// Iter hs.4: `int_to_str` participates in RC bookkeeping under -/// `--alloc=rc`. At least one heap-Str allocation must be recorded -/// when an `int_to_str(...)` call's result is bound and consumed. -/// Note: `live == 0` is NOT asserted yet — the let-binder for an -/// `Implicit`-ret-mode App is not trackable in iter 18g.2's RC -/// discipline (Implicit is the back-compat-leak lane); refining -/// this to Own-ret-mode + a let-arm dec at scope close is queued -/// as known debt in the iter hs.4 journal Concerns section. Stdout -/// still pins the same `42\n` the smoke test asserts; the program -/// runs to clean exit despite the leak. +/// Heap-Str RC-discipline: `int_to_str(42)` bound to a let, consumed +/// by `io/print_str`, must reach `live == 0` at program exit — every +/// heap-Str allocation matched by a drop. RED pin for the +/// effect-op-arg-mode gap surfaced by hs.4 (the uniqueness analyser +/// walks `Term::Do` args as `Position::Consume`; combined with +/// `EffectOpSig`'s missing `param_modes` field, the let-binder for +/// the `int_to_str` result is not trackable and no scope-close +/// `ailang_rc_dec` is emitted). The fix is on the planning queue; +/// this test fails until it lands, locking in the invariant. #[test] -fn int_to_str_records_rc_allocation_under_alloc_rc() { - let (stdout, allocs, _frees, _live) = +fn int_to_str_drop_balances_rc_stats() { + let (stdout, allocs, frees, live) = build_and_run_with_rc_stats("int_to_str_drop_rc.ail.json"); assert_eq!(stdout, "42\n"); - assert!( - allocs >= 1, - "expected at least one heap-Str allocation; got allocs={allocs}" - ); + assert!(allocs >= 1, "expected at least one heap-Str allocation; got allocs={allocs}"); + assert_eq!(allocs, frees, "every heap-Str alloc must be freed; allocs={allocs}, frees={frees}"); + assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}"); } -/// Iter hs.4: an ADT carrying a heap-Str field constructs cleanly -/// and pattern-matches to the inner Str under `--alloc=rc`. Two -/// allocations are observed (ADT cell + heap-Str slab); at least -/// one free fires (the match-arm pattern walks the field through -/// `field_drop_call`'s Str arm to `ailang_rc_dec`). `live == 0` -/// is not yet asserted for the same Implicit-ret-mode reason as -/// `int_to_str_records_rc_allocation_under_alloc_rc` — queued as -/// known debt. Stdout pins the inner Str's bytes (`42\n`). +/// Heap-Str RC-discipline through an ADT: `Box(int_to_str(42))` +/// constructs (ADT cell + heap-Str slab allocs), gets pattern-matched +/// to extract the inner Str, both reach `live == 0` at program exit. +/// RED pin for the same effect-op-arg-mode gap as +/// `int_to_str_drop_balances_rc_stats`; the outer ADT cell leaks via +/// the same `Implicit`-ret-mode path applied to its own let-binder. #[test] -fn str_field_in_adt_records_rc_allocations_under_alloc_rc() { - let (stdout, allocs, frees, _live) = +fn str_field_in_adt_drops_heap_str_correctly() { + let (stdout, allocs, frees, live) = build_and_run_with_rc_stats("str_field_in_adt_heap.ail.json"); assert_eq!(stdout, "42\n"); - assert!( - allocs >= 2, - "expected ADT cell + heap-Str slab; got allocs={allocs}" - ); - assert!( - frees >= 1, - "expected at least the heap-Str slab freed via field_drop_call; got frees={frees}" - ); + assert!(allocs >= 2, "expected ADT cell + heap-Str slab; got allocs={allocs}"); + assert_eq!(allocs, frees, "every RC slab must be freed; allocs={allocs}, frees={frees}"); + assert_eq!(live, 0, "no leaked RC slabs allowed; live={live}"); }