d72fe0c2e6
The last leg of the #63 drop-soundness cluster: an (own ...) heap param consumed on one match-arm / if-branch but live on a sibling was never dropped on the live path, leaking one slab per call. This was the residual live=2 leak in series_sma that kept the series headline from being leak-clean. Root: the per-fn aggregate consume_count (the worst-case max over all branches from uniqueness::merge_states) was codegen's only consume signal, and every owned-param drop site gated on it. A param consumed on SOME branch makes the aggregate >= 1, so every site skipped it — including the branch where it stays live. match and if share this root (if is first-class MTerm::If, not desugared to match). Mechanism (spec 0068): - CAPTURE: uniqueness retains the per-branch consume snapshots it already computes per branch and discarded at the max merge — new BranchConsume + infer_module_with_cross_branches (the aggregate-only infer_module_with_cross now delegates and drops the channel). - CARRY: additive MArm.consume / MTerm::If.{then,else}_consume, attached by lower_to_mir via a traversal-order cursor (post-order pop, kind- and exhaustion-asserts make a desync a loud panic; neutral for const bodies which uniqueness does not walk). The AST carries no node id, so the correspondence is the structural pre-order both walks share — pinned by branch_consume_maps_attach_to_matching_arms. - GATE: emit_leakclass_branch_param_drops fires a fall-through drop only for the leak class (branch_consume==0 AND aggregate>=1), in match arms + both if branches; the pre-tail-call dec switches its gate source from the aggregate to per-arm. The fn-return dec and arm-close pattern-binder dec are UNCHANGED. Safety: - Double-free: the drop sites partition by aggregate (==0 -> existing fn-return/pre-tail-call; >=1 -> new per-branch). Disjoint, so no param is dropped twice — no fn-return disable, no tail-position analysis. - Use-after-free: the checker rejects use-after-consume, so an aggregate>=1 param is provably dead past the construct (path-terminal drop, tail position irrelevant). - Type eligibility (found by the full-suite gate during implement; spec 0068 refined): the new agg>=1 site is the FIRST drop site that can reach a STATIC closure-pair param (a top-level fn ref like inc in Either.either, consumed on one arm, live on the other) — a .rodata constant with no rc-header. field_drop_call routes Type::Fn / static-Str / Type::Var to the bare ailang_rc_dec, which underflowed on it. The helper now drops only params with a real per-type heap-ADT drop fn (field_drop_call != "ailang_rc_dec"). Sound (no underflow) and leak-correct (a static closure/Str allocates nothing). Witnessed green by std_either_demo / std_list_demo / poly_rec_capture_demo. Verification: full workspace suite green (116 binaries); both new leak pins (if + match) and series_sma_no_leak_pin green at live=0; legs 1/2 pins still green; INTERCEPTS<->(intrinsic) bijection intact; no Pattern::Lit reject path added. The leg-3 helper's type precondition was applied inline by the orchestrator (a narrowing guard clause in an already-reviewed Task-4 helper, full context loaded) after the implement-orchestrator correctly surfaced the spec gap rather than papering over the regression. This clears the series_sma leak tail; the series milestone (#61) close stays a separate deliberate step (its end-to-end milestone fieldtest). closes #63
79 lines
2.8 KiB
Rust
79 lines
2.8 KiB
Rust
//! Pins that the #61 headline `series_sma` example is leak-clean under
|
|
//! `--alloc=rc`. The residual `live=2` was #63 leg 3 (branch-consume-
|
|
//! split); this pin closes that leak tail.
|
|
//!
|
|
//! Property protected: `series_sma` runs to completion under
|
|
//! `AILANG_RC_STATS=1` with `live == 0` (equivalently `allocs == frees`).
|
|
//! `series_sma_pin.rs` already pins stdout; this pin is the RC ledger,
|
|
//! distinct. Goes green once the branch-consume-split leak is fixed.
|
|
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
fn ail_bin() -> &'static str {
|
|
env!("CARGO_BIN_EXE_ail")
|
|
}
|
|
|
|
#[test]
|
|
fn series_sma_is_leak_clean() {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
|
|
let src = workspace.join("examples").join("series_sma.ail");
|
|
let tmp = std::env::temp_dir().join(format!(
|
|
"ailang_series_sma_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 series_sma.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<u64> = None;
|
|
let mut frees: Option<u64> = None;
|
|
let mut live: Option<i64> = 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,
|
|
"series_sma leaks {live} slab(s) (allocs={allocs} frees={frees}): the \
|
|
#61 residual live=2 was the #63 leg-3 branch-consume-split leak; if \
|
|
this is non-zero the leak-class drop is not firing on the series path."
|
|
);
|
|
assert_eq!(
|
|
allocs, frees,
|
|
"alloc/free mismatch (allocs={allocs} frees={frees} live={live})"
|
|
);
|
|
}
|