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.
This commit is contained in:
2026-05-12 19:49:22 +02:00
parent 134441b472
commit 592d87bf11
+25 -34
View File
@@ -2588,46 +2588,37 @@ fn float_to_str_smoke() {
assert_eq!(out, "3.5\n"); assert_eq!(out, "3.5\n");
} }
/// Iter hs.4: `int_to_str` participates in RC bookkeeping under /// Heap-Str RC-discipline: `int_to_str(42)` bound to a let, consumed
/// `--alloc=rc`. At least one heap-Str allocation must be recorded /// by `io/print_str`, must reach `live == 0` at program exit — every
/// when an `int_to_str(...)` call's result is bound and consumed. /// heap-Str allocation matched by a drop. RED pin for the
/// Note: `live == 0` is NOT asserted yet — the let-binder for an /// effect-op-arg-mode gap surfaced by hs.4 (the uniqueness analyser
/// `Implicit`-ret-mode App is not trackable in iter 18g.2's RC /// walks `Term::Do` args as `Position::Consume`; combined with
/// discipline (Implicit is the back-compat-leak lane); refining /// `EffectOpSig`'s missing `param_modes` field, the let-binder for
/// this to Own-ret-mode + a let-arm dec at scope close is queued /// the `int_to_str` result is not trackable and no scope-close
/// as known debt in the iter hs.4 journal Concerns section. Stdout /// `ailang_rc_dec` is emitted). The fix is on the planning queue;
/// still pins the same `42\n` the smoke test asserts; the program /// this test fails until it lands, locking in the invariant.
/// runs to clean exit despite the leak.
#[test] #[test]
fn int_to_str_records_rc_allocation_under_alloc_rc() { fn int_to_str_drop_balances_rc_stats() {
let (stdout, allocs, _frees, _live) = let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("int_to_str_drop_rc.ail.json"); build_and_run_with_rc_stats("int_to_str_drop_rc.ail.json");
assert_eq!(stdout, "42\n"); assert_eq!(stdout, "42\n");
assert!( assert!(allocs >= 1, "expected at least one heap-Str allocation; got allocs={allocs}");
allocs >= 1, assert_eq!(allocs, frees, "every heap-Str alloc must be freed; allocs={allocs}, frees={frees}");
"expected at least one heap-Str allocation; got allocs={allocs}" assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
);
} }
/// Iter hs.4: an ADT carrying a heap-Str field constructs cleanly /// Heap-Str RC-discipline through an ADT: `Box(int_to_str(42))`
/// and pattern-matches to the inner Str under `--alloc=rc`. Two /// constructs (ADT cell + heap-Str slab allocs), gets pattern-matched
/// allocations are observed (ADT cell + heap-Str slab); at least /// to extract the inner Str, both reach `live == 0` at program exit.
/// one free fires (the match-arm pattern walks the field through /// RED pin for the same effect-op-arg-mode gap as
/// `field_drop_call`'s Str arm to `ailang_rc_dec`). `live == 0` /// `int_to_str_drop_balances_rc_stats`; the outer ADT cell leaks via
/// is not yet asserted for the same Implicit-ret-mode reason as /// the same `Implicit`-ret-mode path applied to its own let-binder.
/// `int_to_str_records_rc_allocation_under_alloc_rc` — queued as
/// known debt. Stdout pins the inner Str's bytes (`42\n`).
#[test] #[test]
fn str_field_in_adt_records_rc_allocations_under_alloc_rc() { fn str_field_in_adt_drops_heap_str_correctly() {
let (stdout, allocs, frees, _live) = let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("str_field_in_adt_heap.ail.json"); build_and_run_with_rc_stats("str_field_in_adt_heap.ail.json");
assert_eq!(stdout, "42\n"); assert_eq!(stdout, "42\n");
assert!( assert!(allocs >= 2, "expected ADT cell + heap-Str slab; got allocs={allocs}");
allocs >= 2, assert_eq!(allocs, frees, "every RC slab must be freed; allocs={allocs}, frees={frees}");
"expected ADT cell + heap-Str slab; got allocs={allocs}" assert_eq!(live, 0, "no leaked RC slabs allowed; live={live}");
);
assert!(
frees >= 1,
"expected at least the heap-Str slab freed via field_drop_call; got frees={frees}"
);
} }