//! RED-pin for the 2026-05-14 Cat-A heap-Str leak in `print` //! (uncovered during the per-type-print-op retirement). //! //! Property protected: under `--alloc=rc`, evaluating the trivial //! program `(body (app print 42))` does NOT leak the heap-Str //! allocated inside `print`'s body. `print`'s body is the explicit- //! let `let s = show x in do io/print_str s` (see iter 24.3, pinned //! structurally by `print_mono_body_shape.rs`); `show` at primitive //! type calls `int_to_str` which returns a fresh heap-Str (slab in //! `runtime/str.c`). `io/print_str` borrows the Str. At let-scope- //! close, the binder `s` is the only owner — codegen must emit //! `ailang_rc_dec(s)`. The runtime stats line at exit must report //! `live == 0` (equivalently `allocs == frees`). //! //! Codegen's `is_rc_heap_allocated` App-arm reads the callee's //! `Type::Fn.ret_mode` via `synth_callee_ret_mode` and tracks the //! let-binder iff it is `Own`. The mono'ed `show__Int` inherits its //! `Type::Fn` from the `Show` class method declaration in //! `examples/prelude.ail.json`, whose `show` carries `ret_mode: own` //! (every fn-type slot is moded — spec 0062). So `show__Int` returns //! `Own`, the let-binder for the show-result is trackable, and //! `drop_symbol_for_binder` emits the scope-close dec — `live == 0`. use std::path::Path; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } #[test] fn alloc_rc_print_int_does_not_leak_show_result_str() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src = workspace .join("examples") .join("print_int_no_leak_pin.ail"); let tmp = std::env::temp_dir().join(format!( "ailang_print_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 print_int_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, "`(app print 42)` under --alloc=rc leaks {live} heap-Str cell(s) \ (allocs={allocs} frees={frees}); print's let-binder for the \ show-result must drop at let-scope-close. The Show class method \ `show` carries `ret_mode: own`, so mono-synthesised `show__Int` \ is `Own`-returning and `is_rc_heap_allocated` tracks the binder." ); assert_eq!( allocs, frees, "alloc/free mismatch (allocs={allocs} frees={frees} live={live})" ); }