//! RED-pin for the loop-valued let-binder scope-close drop gap //! (surfaced by the raw-buf fieldtest, refs #7). //! //! Property protected: under `--alloc=rc`, an owned `(RawBuf Int)` //! whose `let`-binding value is a `(loop ...)` result, and which is //! afterwards only borrow-read (`RawBuf.get`) — never consumed by an //! `own`-taking callee — MUST be dropped at let-scope-close. The //! runtime RC stats line at exit must report `live == 0` //! (equivalently `allocs == frees`). //! //! Root cause: codegen's `is_rc_heap_allocated` //! (`crates/ailang-codegen/src/drop.rs`) — the predicate the //! `Term::Let` lowering uses to decide whether a let-binder owns a //! fresh RC-heap allocation to `dec` at scope close — has no //! `Term::Loop` arm. A loop-valued let-value falls through to //! `_ => false`, so the binder is never flagged trackable and //! `drop_symbol_for_binder` is never called for it. Compare the //! direct-`(new ...)`-value path (`examples/raw_buf_drop_min.ail`) //! which IS dropped, and the Own-returning `Term::App` arm which is. //! //! As of HEAD this test fails: stderr reports //! `ailang_rc_stats: allocs=2 frees=1 live=1`. The owned RawBuf slab //! threaded through the loop binder leaks. //! //! Scope note: this pins ONLY the scope-close drop gap. A distinct, //! broader per-iteration recur-dec leak (superseded heap loop-binder //! values not RC-dec'd inside the loop body) is filed separately and //! is NOT covered here. RawBuf manifests only the scope-close gap //! because `RawBuf.set` mutates in place (no per-iteration alloc). use std::path::Path; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } #[test] fn alloc_rc_loop_valued_rawbuf_binder_drops_at_scope_close() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src = workspace .join("examples") .join("raw_buf_loop_no_leak_pin.ail"); let tmp = std::env::temp_dir().join(format!( "ailang_raw_buf_loop_no_leak_pin_{}", std::process::id() )); std::fs::create_dir_all(&tmp).unwrap(); let out = tmp.join("bin"); let status = Command::new(ail_bin()) .args(["build", src.to_str().unwrap(), "--alloc=rc", "-o"]) .arg(&out) .status() .expect("ail build failed to run"); assert!( status.success(), "ail build --alloc=rc failed for raw_buf_loop_no_leak_pin.ail" ); let output = Command::new(&out) .env("AILANG_RC_STATS", "1") .output() .expect("execute binary"); assert!( output.status.success(), "binary exited non-zero: status {:?}", output.status ); let stderr = String::from_utf8(output.stderr).expect("stderr utf8"); let stats_line = stderr .lines() .find(|l| l.starts_with("ailang_rc_stats:")) .unwrap_or_else(|| { panic!("missing ailang_rc_stats line in stderr; stderr was:\n{stderr}") }); let mut allocs: Option = None; let mut frees: Option = None; let mut live: Option = None; for tok in stats_line.split_whitespace() { if let Some(v) = tok.strip_prefix("allocs=") { allocs = v.parse().ok(); } else if let Some(v) = tok.strip_prefix("frees=") { frees = v.parse().ok(); } else if let Some(v) = tok.strip_prefix("live=") { live = v.parse().ok(); } } let allocs = allocs.expect("missing allocs= field"); let frees = frees.expect("missing frees= field"); let live = live.expect("missing live= field"); assert_eq!( live, 0, "an owned (RawBuf Int) bound to a (loop ...) result and only \ borrow-read leaks {live} slab cell(s) (allocs={allocs} \ frees={frees}); the loop-valued let-binder must drop at \ scope close. `is_rc_heap_allocated` in \ crates/ailang-codegen/src/drop.rs has no Term::Loop arm, so \ the binder is never flagged trackable." ); assert_eq!( allocs, frees, "alloc/free mismatch (allocs={allocs} frees={frees} live={live})" ); }