//! 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 //! ~1600–57000 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; /// `` — 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 = Vec::new(); fn walk(dir: &Path, acc: &mut Vec) { 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)." ); }