test: RED shadow-rebind RawBuf drop-leak on shipped fixtures (refs #43)
raw_buf_{int,float,bool}_shadow_rebind_drop_balances_rc_stats guard
the three shipped milestone fixtures (the LLM-natural shadow-rebind
idiom `(let buf (new...) (let buf (set buf...) ...(get buf)))`) with a
live==0 RC-stats assertion alongside their existing stdout check.
This is a SECOND, distinct leak mechanism (A2), separate from the
anonymous-temp path fixed in 62cd4b4 (A1). The A1 call-site drop does
not cover it: here `get`'s arg is the BOUND `buf` (a Term::Var), not an
anonymous temp, so the A1 rule (which excludes Var args) correctly does
not fire, and the leak persists — allocs=2 frees=1 live=1.
Cause (A2). The uniqueness pass runs post-monomorphisation
(infer_module called from codegen lib.rs:993). It registers the
intrinsic monomorph signatures in `globals` under their bare local
name `RawBuf_get__Int` (uniqueness.rs:128), but callee_arg_modes
(uniqueness.rs:390-410) looks them up by the cross-module-qualified
callee-Var name `raw_buf.RawBuf_get__Int` -> MISS -> vec![] -> every arg
defaults to Position::Consume (walk arm uniqueness.rs:240-242). The
borrow-mode RawBuf param of `get`/`size` is thus mis-counted as a
consume, inflating the final `buf` binder's consume_count from 0 to 1,
which suppresses the Term::Let drop gate at codegen lib.rs:1795. The
param_modes in the registered Type::Fn are correct ([Borrow, Implicit]);
the only defect is the name-key mismatch that hides them from the
lookup.
NOT issue #42's "shadow-name collapse": #42's own evidence shows the
distinct-name (b0/b1/...) variant leaks identically, so the defect is
the borrow-counted-as-consume default, not name-keyed ownership.
RED until callee_arg_modes resolves the qualified intrinsic name to the
registered signature. The A1 test (raw_buf_owned_drop_balances_rc_stats)
and the four stdout _e2e tests stay green.
This commit is contained in:
@@ -2262,6 +2262,74 @@ fn raw_buf_owned_drop_balances_rc_stats() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (A2, refs #42 / #43): the shadow-rebind RawBuf idiom —
|
||||
/// `(let buf (new …) (let buf (set buf …) … (get buf)))` — must drop
|
||||
/// its owned slab at scope close. This is the LLM-natural shipped
|
||||
/// form (`examples/raw_buf_int.ail`, the actual #42 symptom), not a
|
||||
/// reduced repro: a 3-slot Int buffer filled then summed via three
|
||||
/// borrowing `get`s. The final `buf` binder is dead after the last
|
||||
/// `get` and must be dec'd once.
|
||||
///
|
||||
/// This is a SEPARATE defect from the A1 anonymous-temp leak fixed in
|
||||
/// 62cd4b4. Here the value reaching `get` is a bound `Term::Var`, not
|
||||
/// an anonymous call temp, so A1's call-site rule correctly does not
|
||||
/// fire. The leak is in uniqueness inference: `RawBuf.get`/`.size`
|
||||
/// take their RawBuf param `borrow`, but their post-mono callee Var
|
||||
/// (`raw_buf.RawBuf_get__Int`) is absent from the uniqueness pass's
|
||||
/// `globals` map (registered only under the bare local name
|
||||
/// `RawBuf_get__Int`), so `callee_arg_modes` misses, every arg
|
||||
/// defaults to `Position::Consume`, and the borrowed `buf` is counted
|
||||
/// as a consume. That inflates the binder's `consume_count` to 1,
|
||||
/// suppressing the `Term::Let` scope-close drop gate
|
||||
/// (`crates/ailang-codegen/src/lib.rs:1795`, `consume_count == 0`).
|
||||
///
|
||||
/// NOT issue #42's "shadow-name collapse": #42's own evidence shows
|
||||
/// the distinct-name variant (`b0`/`b1`/…) leaks identically, so the
|
||||
/// defect is the borrow-counted-as-consume default, not name-keyed
|
||||
/// ownership. RED until the borrow modes of the intrinsic ops are
|
||||
/// visible to the consume-counter.
|
||||
#[test]
|
||||
fn raw_buf_int_shadow_rebind_drop_balances_rc_stats() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("raw_buf_int.ail");
|
||||
assert_eq!(stdout.trim(), "60", "fixture must still sum to 60");
|
||||
assert_eq!(
|
||||
live, 0,
|
||||
"shadow-rebind RawBuf leaks {live} slab(s) (allocs={allocs} frees={frees}); \
|
||||
the final `buf` binder must be dropped at end of main"
|
||||
);
|
||||
}
|
||||
|
||||
/// A2 Float sibling: the shadow-rebind drop must balance for the
|
||||
/// `double`-element shipped fixture too (`examples/raw_buf_float.ail`,
|
||||
/// prints `4.0`). Same mechanism and RED state as the Int case.
|
||||
#[test]
|
||||
fn raw_buf_float_shadow_rebind_drop_balances_rc_stats() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("raw_buf_float.ail");
|
||||
assert_eq!(stdout.trim(), "4.0", "fixture must still print 4.0");
|
||||
assert_eq!(
|
||||
live, 0,
|
||||
"shadow-rebind RawBuf leaks {live} slab(s) (allocs={allocs} frees={frees}); \
|
||||
the final `buf` binder must be dropped at end of main"
|
||||
);
|
||||
}
|
||||
|
||||
/// A2 Bool sibling: the shadow-rebind drop must balance for the
|
||||
/// `i1`-element shipped fixture too (`examples/raw_buf_bool.ail`,
|
||||
/// prints `42`). Same mechanism and RED state as the Int case.
|
||||
#[test]
|
||||
fn raw_buf_bool_shadow_rebind_drop_balances_rc_stats() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("raw_buf_bool.ail");
|
||||
assert_eq!(stdout.trim(), "42", "fixture must still print 42");
|
||||
assert_eq!(
|
||||
live, 0,
|
||||
"shadow-rebind RawBuf leaks {live} slab(s) (allocs={allocs} frees={frees}); \
|
||||
the final `buf` binder must be dropped 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).
|
||||
|
||||
Reference in New Issue
Block a user