Files
AILang/ail-embed/tests/rt_archive_freshness.rs
Brummel 483117d39b test(ail-embed): RED — build.rs links a stale libailang_rt.a (missing rerun-if-changed on runtime sources)
bugfix-swarm-rc-alloc-undercount, RED stage (audit trail; GREEN
follows separately via implement mini-mode).

Root cause of the m5.2-resume-attempt residual (the M5 swarm
leak-proof still non-deterministic after the atomic-counter fix
7bfa11e): ail-embed/build.rs declares cargo:rerun-if-changed only
for the kernel .ail + build.rs itself — NOT runtime/rc.c. So Cargo
never re-ran build.rs after 7bfa11e; the swarm keeps linking a
stale pre-7bfa11e libailang_rt.a whose g_rc_*count++ is the old
non-atomic racing increment. 7bfa11e was correct and is unchanged;
it just never reached the ail-embed binary. This explains why the
isolated crates/ail RED (rebuilds the staticlib fresh per run) is
green while the ail-embed swarm is not, and the
frees-stable/allocs-jitter asymmetry (few TLS-NULL frees = no
contention; millions of TLS-NULL host allocs = race on the stale
non-atomic counter).

Framing: a genuine defect (build-dependency completeness gap), NOT
test-methodology unsoundness — with a fresh archive the swarm
Σ-over-stat-lines balances deterministically (Σallocs==Σfrees==
12000003, 5/5 per the debugger). runtime/rc.c at HEAD is correct;
the GREEN fix is in the build script, not the runtime.

RED pins the CAUSE deterministically (the symptom is
non-deterministic by construction): objdump the libailang_rt.a that
build.rs emitted for this crate; assert >=2 lock-prefixed
instructions (the atomic lock incq for g_rc_alloc_count +
g_rc_free_count from current rc.c) and that it matches a
freshly-built archive. Boss-verified RED: linked archive has 0 lock
insns ("STALE pre-7bfa11e archive linked").

Does NOT touch the committed #[ignore] of symbol_fan_swarm_leak_free
nor the isolated embed_rc_global_stats_race RED (both correct,
unchanged).
2026-05-19 02:14:12 +02:00

187 lines
7.7 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! RED for `bugfix-swarm-rc-alloc-undercount`.
//!
//! Named property protected: the `libailang_rt.a` archive that
//! `ail-embed` links (emitted by `build.rs` via
//! `ail build --emit=staticlib`) MUST be regenerated whenever
//! `runtime/rc.c` changes, so the global RC stats counter increment
//! in the linked binary is the atomic (`lock`-prefixed) form that
//! commit `7bfa11e` introduced. If the build does not track
//! `runtime/rc.c` as an input, a stale pre-`7bfa11e` archive is
//! linked whose `g_rc_alloc_count++` is the original non-atomic
//! racing increment — the exact swarm alloc-undercount
//! (`Σfrees` stable-exact, `Σallocs` short by a jittering
//! ~160057000 across runs) re-surfaces, frozen into a stale
//! build artefact even though `runtime/rc.c` itself is correct.
//!
//! Why this shape and not a swarm re-run: the symptom (jittering
//! `Σallocs`) is non-deterministic by construction (it depends on
//! thread interleaving on the racing `incq`). The *cause* is fully
//! deterministic: the linked archive's `ailang_rc_alloc` either has
//! the `lock`-prefixed atomic global increment (correct, from the
//! current `runtime/rc.c`) or a plain non-atomic `incq` (stale
//! pre-`7bfa11e`). This RED pins the cause directly: it asserts the
//! archive `build.rs` produced for THIS crate is built from a
//! `runtime/rc.c` whose `g_rc_alloc_count` increment is atomic.
//!
//! Not a methodology unsoundness: with the correct (current)
//! `runtime/rc.c` linked, the swarm's Σ-over-stat-lines balances
//! deterministically (`Σallocs == Σfrees == 12000003`, every run).
//! The Σ invariant is sound; only the *stale-artefact build gap*
//! breaks it. The GREEN fix is a build-dependency completeness fix
//! (`ail-embed/build.rs` must declare `runtime/rc.c` — and the rest
//! of the staticlib's runtime inputs — as `rerun-if-changed`), NOT
//! a runtime change: `runtime/rc.c` at HEAD is already correct.
use std::path::{Path, PathBuf};
use std::process::Command;
/// `<repo>` — the AILang workspace root, parent of this crate.
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("ail-embed has a parent (the AILang repo root)")
.to_path_buf()
}
/// The `libailang_rt.a` that `build.rs` emitted into this crate's
/// `OUT_DIR` and that `swarm_runner` / `lib.rs` link. There is exactly
/// one under `ail-embed/target/**/build/ail-embed-*/out/`.
fn linked_runtime_archive() -> PathBuf {
let target = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target");
let mut found: Vec<PathBuf> = Vec::new();
fn walk(dir: &Path, acc: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
walk(&p, acc);
} else if p.file_name().and_then(|s| s.to_str())
== Some("libailang_rt.a")
{
acc.push(p);
}
}
}
walk(&target, &mut found);
assert!(
!found.is_empty(),
"no libailang_rt.a under {} — build.rs must have emitted one \
(build the crate first: it links this archive)",
target.display()
);
// Newest by mtime — the one the current build links.
found.sort_by_key(|p| {
std::fs::metadata(p)
.and_then(|m| m.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
});
found.pop().unwrap()
}
/// The linked runtime archive must be built from the current
/// `runtime/rc.c`, in which the global `g_rc_alloc_count` increment is
/// atomic. On x86-64 a relaxed `atomic_fetch_add_explicit` lowers to a
/// `lock`-prefixed RMW; the pre-`7bfa11e` non-atomic `g_rc_*count++`
/// emits none. A stale archive (build did not track `runtime/rc.c`)
/// therefore links zero `lock` instructions in `ailang_rc_alloc` and
/// re-introduces the swarm alloc-undercount race.
#[test]
fn linked_runtime_archive_has_atomic_global_rc_counters() {
let archive = linked_runtime_archive();
// Disassemble; count `lock`-prefixed instructions. The current
// runtime emits exactly two (`lock incq` for g_rc_alloc_count in
// ailang_rc_alloc and for g_rc_free_count in ailang_rc_dec). The
// stale pre-7bfa11e archive emits zero (plain `incq`, or a
// cmov-fused plain `incq`).
let out = Command::new("objdump")
.args(["-d", "--no-show-raw-insn"])
.arg(&archive)
.output()
.expect("run objdump on libailang_rt.a");
assert!(
out.status.success(),
"objdump failed on {}:\n{}",
archive.display(),
String::from_utf8_lossy(&out.stderr)
);
let disasm = String::from_utf8_lossy(&out.stdout);
let lock_insns = disasm
.lines()
.filter(|l| {
// Match the mnemonic column, not the symbol name / path.
l.split_once('\t')
.map(|(_, ins)| ins.trim_start().starts_with("lock "))
.unwrap_or(false)
})
.count();
assert!(
lock_insns >= 2,
"linked runtime archive `{}` has {lock_insns} lock-prefixed \
instruction(s); the current runtime/rc.c emits an atomic \
(lock-prefixed) global RC counter increment in both \
ailang_rc_alloc and ailang_rc_dec (>=2 expected). Zero/one \
means a STALE pre-7bfa11e archive is linked: build.rs did not \
track runtime/rc.c as an input, so the swarm links the old \
non-atomic g_rc_alloc_count++ and the alloc-undercount race \
re-surfaces (Sfrees stable-exact, Sallocs short & jittering).",
archive.display()
);
// Cross-check: a freshly-built archive from the SAME current
// runtime/rc.c, via the same `ail build --emit=staticlib` path
// build.rs uses, is atomic. If this differs from the linked one,
// the linked one is provably stale.
let outdir = std::env::temp_dir().join(format!(
"ail-embed-rt-fresh-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&outdir);
std::fs::create_dir_all(&outdir).unwrap();
let repo = repo_root();
let ail_bin = repo.join("target/debug/ail");
assert!(
ail_bin.exists(),
"expected the `ail` CLI at {} (build.rs builds it; run \
`cargo build -p ail` in the workspace first)",
ail_bin.display()
);
let st = Command::new(&ail_bin)
.arg("build")
.arg(repo.join("examples/embed_backtest_step_tick.ail"))
.args(["--emit=staticlib", "-o"])
.arg(&outdir)
.output()
.expect("ail build --emit=staticlib (fresh runtime archive)");
assert!(
st.status.success(),
"fresh `ail build --emit=staticlib` failed:\n{}",
String::from_utf8_lossy(&st.stderr)
);
let fresh = outdir.join("libailang_rt.a");
let fresh_disasm = Command::new("objdump")
.args(["-d", "--no-show-raw-insn"])
.arg(&fresh)
.output()
.expect("objdump fresh archive");
let fresh_lock = String::from_utf8_lossy(&fresh_disasm.stdout)
.lines()
.filter(|l| {
l.split_once('\t')
.map(|(_, ins)| ins.trim_start().starts_with("lock "))
.unwrap_or(false)
})
.count();
let _ = std::fs::remove_dir_all(&outdir);
assert_eq!(
lock_insns, fresh_lock,
"linked archive ({lock_insns} lock insns) disagrees with a \
freshly-built archive from the current runtime/rc.c \
({fresh_lock} lock insns) — the linked archive is STALE: \
ail-embed/build.rs did not regenerate libailang_rt.a after \
runtime/rc.c changed (missing rerun-if-changed on the \
staticlib runtime inputs)."
);
}