//! RED for `bugfix-rc-global-stats-race`. //! //! Named property protected: under a multi-threaded host that uses the //! NULL-ctx GLOBAL RC-stats fallback path (`__ail_tls_ctx == NULL` -> //! `g_rc_alloc_count` / `g_rc_free_count`, runtime/rc.c:90-91,161,212), //! the `AILANG_RC_STATS` atexit Σ is EXACT: it counts every //! `ailang_rc_alloc` and every to-zero `ailang_rc_dec`, with no lost //! increments. The two global counters are plain `static uint64_t` //! incremented by a non-atomic `++`; concurrent workers race on the //! read-modify-write and silently drop updates, so the printed total //! under-counts non-deterministically (M5 iter 2's symbol-fan swarm: //! allocs jitter 5993321..5999318, never the true 6_000_000). //! //! This is NOT a memory bug: no box crosses a thread (alloc + dec in //! the same loop body), the per-object refcount header op is correct, //! and the program is bit-exact every run. The ONLY defect is the //! under-counted statistics Σ — exactly the deferred atomic-counter //! decision rc.c's own header (rc.c:44-49,85) names, with M5 the first //! concurrent consumer. //! //! The existing per-ctx swarm/accounting harnesses //! (embed_swarm_tsan.rs, embed_rc_accounting_tsan.rs) always set //! `__ail_tls_ctx = ctx`, so they exercise the per-thread (zero- //! contention) counters and never the global fallback — that is why //! the suite is green while this bug ships. //! //! Build/link mechanism mirrors `embed_swarm_tsan.rs`: a staticlib //! build of any kernel emits `libailang_rt.a`; the C host links the //! runtime archive directly and never calls a kernel. use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } /// 8 threads * 2_000_000 cycles — must match the `#define`s in /// `crates/ail/tests/embed/rc_global_stats_race.c`. const NTHREADS: u64 = 8; const NCYCLES: u64 = 2_000_000; const EXPECTED: u64 = NTHREADS * NCYCLES; #[test] fn global_rc_stats_counters_are_exact_under_thread_contention() { let outdir = std::env::temp_dir().join(format!( "ail-rc-global-stats-race-{}", std::process::id())); std::fs::create_dir_all(&outdir).unwrap(); let root = ailang_test_support::canonical_workspace_root(); // Any kernel staticlib build emits libailang_rt.a alongside it. let st = Command::new(ail_bin()) .args(["build", "examples/embed_backtest_step_tick.ail", "--emit=staticlib", "--alloc=rc", "-o", outdir.to_str().unwrap()]) .current_dir(&root).status().expect("ail build"); assert!(st.success(), "ail build --emit=staticlib failed"); let rt_archive = outdir.join("libailang_rt.a"); assert!(rt_archive.exists(), "libailang_rt.a not emitted"); let host_c = root.join("crates/ail/tests/embed/rc_global_stats_race.c"); let bin = outdir.join("rc_global_stats_race"); let st = Command::new("cc") .args([ "-O2", "-pthread", host_c.to_str().unwrap(), rt_archive.to_str().unwrap(), "-o", bin.to_str().unwrap(), ]) .status().expect("cc"); assert!(st.success(), "linking rc_global_stats_race.c failed"); let out = Command::new(&bin) .env("AILANG_RC_STATS", "1") .output().expect("run rc_global_stats_race"); assert!(out.status.success(), "host must exit 0; stderr:\n{}", String::from_utf8_lossy(&out.stderr)); let stderr = String::from_utf8_lossy(&out.stderr); let line = stderr.lines() .find(|l| l.starts_with("ailang_rc_stats:")) .unwrap_or_else(|| panic!( "missing ailang_rc_stats line; stderr was:\n{stderr}")); let mut allocs: Option = None; let mut frees: Option = None; for tok in line.split_whitespace() { if let Some(v) = tok.strip_prefix("allocs=") { allocs = Some(v.parse().expect("allocs= u64")); } else if let Some(v) = tok.strip_prefix("frees=") { frees = Some(v.parse().expect("frees= u64")); } } let allocs = allocs.expect("allocs= present"); let frees = frees.expect("frees= present"); // Pre-fix: the non-atomic `g_rc_*count++` loses updates under // contention, so allocs/frees are < EXPECTED and jitter run-to-run. // Post-fix (atomic global counters): exact equality, deterministic. assert_eq!(allocs, EXPECTED, "global RC alloc counter must count every ailang_rc_alloc on \ the NULL-ctx fallback path with no lost increments \ (allocs={allocs}, expected {EXPECTED}); line: {line:?}"); assert_eq!(frees, EXPECTED, "global RC free counter must count every to-zero ailang_rc_dec \ on the NULL-ctx fallback path with no lost increments \ (frees={frees}, expected {EXPECTED}); line: {line:?}"); }