dbd76e5503
bugfix-swarm-rc-alloc-undercount, GREEN stage. RED is the separate audit-trail commit483117d. Root cause of the m5.2-resume-attempt residual: ail-embed/build.rs declared cargo:rerun-if-changed only for the kernel .ail + build.rs itself — NOT the runtime/ C sources ail build --emit=staticlib compiles into libailang_rt.a. Cargo never re-ran build.rs after the atomic-counter fix7bfa11e, so ail-embed kept linking a stale pre-7bfa11e non-atomic libailang_rt.a; the swarm's concurrent TLS-NULL host allocs raced the stale non-atomic g_rc_alloc_count++ (frees-stable/allocs-jitter). NOT a second runtime bug and NOT test-methodology unsoundness — runtime/rc.c at HEAD is correct; the isolated crates/ail RED was green only because it rebuilds the staticlib fresh per run. Fix (ail-embed/build.rs, +7 lines): directory-level cargo:rerun-if-changed=<repo>/runtime (Cargo recurses mtimes under the dir). Preferred over a per-file list — the implementer inspected crates/ail build_staticlib (libailang_rt.a = ar(rc.o, str.o), both under runtime/) and chose the directory mechanism so any future runtime source cannot silently re-introduce the same staleness. Existing kernel/build.rs/AIL_BIN rerun-if-changed lines kept. Cohesive consequence (in-scope): symbol_fan_swarm_leak_free un-#[ignore]d — the test BODY is byte-for-byte unchanged (it was quarantined, never weakened); the module-doc + fn-doc breadcrumbs rewritten to the resolved build-dep-staleness rationale. It is now the integration-level acceptance of both7bfa11eand this fix. Boss also removed a pre-existing unused `DataFormat` import in swarm.rs inline (trivial; the file is committed this iter regardless; warning-clean after). Boss-verified independently: RED rt_archive_freshness -> GREEN; swarm determinism 5/5 (symbol_fan_swarm_leak_free + symbol_fan_swarm_bit_exact GREEN every run, 0 ignored, jitter gone, Sallocs==Sfrees==12000003); isolated embed_rc_global_stats_race still GREEN (untouched, runtime untouched); Invariant 1 data-server count 0; scope = ail-embed/build.rs + ail-embed/tests/swarm.rs + journal + stats only. The M5 swarm leak-proof bounce-back is fully resolved end-to-end. M5 stays open [~]; m5.3 (time-shard + friction-harvest + close-out) remains. Includes the per-iter journal, stats, and INDEX.md line.
164 lines
6.9 KiB
Rust
164 lines
6.9 KiB
Rust
//! Symbol-fan swarm E2E (spec Testing §2). Runs the `swarm_runner`
|
|
//! bin as a subprocess under `AILANG_RC_STATS=1` (the C-runtime
|
|
//! atexit stat line, runtime/rc.c:124-138, is only observable from a
|
|
//! separate process that exits). Skips when /mnt tick data is absent
|
|
//! — mirroring data-server's own tests/data_server.rs::skip_if_no_data().
|
|
//!
|
|
//! Two assertions, deliberately SPLIT into two tests:
|
|
//!
|
|
//! - `symbol_fan_swarm_bit_exact` — the existence proof. Per symbol,
|
|
//! the kernel `(acc,n)` is BIT-EXACT vs an independent same-order
|
|
//! host reference fold. This is GREEN and live: it proves the
|
|
//! real-data-server → adapter → M3-kernel swarm computes correctly.
|
|
//!
|
|
//! - `symbol_fan_swarm_leak_free` — LIVE and GREEN. The global
|
|
//! `Σallocs==Σfrees` measurement (ported from
|
|
//! crates/ail/tests/embed_tick_e2e.rs:94-104) was non-deterministic
|
|
//! under the multi-threaded host. Root cause was a build-dependency
|
|
//! staleness gap, NOT a residual runtime defect: `ail-embed/build.rs`
|
|
//! did not declare the `runtime/` C sources as `rerun-if-changed`,
|
|
//! so the atomic-counter runtime fix `bugfix-rc-global-stats-race`
|
|
//! (commit `7bfa11e`) never relinked `libailang_rt.a` and the swarm
|
|
//! kept linking the stale pre-`7bfa11e` non-atomic
|
|
//! `g_rc_alloc_count++` whose race produced the jittering
|
|
//! `Σallocs` undercount (`Σfrees` stable-exact). `build.rs` now
|
|
//! tracks `runtime/` (this iter, `bugfix-swarm-rc-alloc-undercount`);
|
|
//! the fresh atomic archive relinks and the leak Σ balances
|
|
//! deterministically (`Σallocs == Σfrees == 12000003`, every run).
|
|
//! This test is the integration-level acceptance of both `7bfa11e`
|
|
//! and the build-dependency fix. Body preserved verbatim — it was
|
|
//! quarantined, never weakened. See
|
|
//! docs/journals/2026-05-19-iter-bugfix-swarm-rc-alloc-undercount.md.
|
|
|
|
use std::process::Command;
|
|
use std::sync::Arc;
|
|
|
|
use data_server::{DataServer, DEFAULT_DATA_PATH};
|
|
|
|
// Must match swarm_runner.rs.
|
|
const MAX_TICKS: usize = 2_000_000;
|
|
const N_SYMBOLS: usize = 4;
|
|
|
|
fn skip_if_no_data() -> bool {
|
|
!std::path::Path::new(DEFAULT_DATA_PATH).exists()
|
|
}
|
|
|
|
/// Independent host reference: same symbols, same order, same cap,
|
|
/// pure-Rust fold (`acc += mid; n += 1`). Bit-exact with the kernel
|
|
/// fold because the addition order is identical.
|
|
fn reference(symbol: &str) -> (f64, i64) {
|
|
let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));
|
|
let mut it = server.stream_tick(symbol).expect("tick stream");
|
|
let (mut acc, mut n) = (0.0_f64, 0_i64);
|
|
'outer: while let Some(chunk) = it.next_chunk() {
|
|
for r in chunk.iter() {
|
|
if n as usize >= MAX_TICKS {
|
|
break 'outer;
|
|
}
|
|
acc += (r.ask + r.bid) / 2.0;
|
|
n += 1;
|
|
}
|
|
}
|
|
(acc, n)
|
|
}
|
|
|
|
/// Spawn `swarm_runner` under `AILANG_RC_STATS=1`. `None` on the
|
|
/// skip-if-absent path (no /mnt data); otherwise `(stdout, stderr)`
|
|
/// after asserting a clean child exit.
|
|
fn run_swarm_or_skip() -> Option<(String, String)> {
|
|
if skip_if_no_data() {
|
|
eprintln!("skipping: {DEFAULT_DATA_PATH} absent (mirrors data-server)");
|
|
return None;
|
|
}
|
|
let out = Command::new(env!("CARGO_BIN_EXE_swarm_runner"))
|
|
.arg(DEFAULT_DATA_PATH)
|
|
.env("AILANG_RC_STATS", "1")
|
|
.output()
|
|
.expect("spawn swarm_runner");
|
|
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
|
|
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
|
|
assert!(
|
|
out.status.success(),
|
|
"swarm_runner exited {:?}\nstdout:\n{stdout}\nstderr:\n{stderr}",
|
|
out.status.code()
|
|
);
|
|
Some((stdout, stderr))
|
|
}
|
|
|
|
/// Existence proof (GREEN, live): each symbol's kernel `(acc,n)` is
|
|
/// bit-exact vs an independent same-order host reference fold. Real
|
|
/// data-server → adapter → M3-frozen kernel, one symbol per thread.
|
|
#[test]
|
|
fn symbol_fan_swarm_bit_exact() {
|
|
let Some((stdout, _stderr)) = run_swarm_or_skip() else { return };
|
|
|
|
let mut symbols_checked = 0usize;
|
|
for line in stdout.lines().filter(|l| l.starts_with("RESULT ")) {
|
|
let mut t = line.split_whitespace();
|
|
let _ = t.next(); // "RESULT"
|
|
let sym = t.next().expect("symbol field");
|
|
let acc_bits = u64::from_str_radix(
|
|
t.next().expect("acc-bits field"), 16,
|
|
).expect("acc-bits hex");
|
|
let n: i64 = t.next().expect("n field").parse().expect("n int");
|
|
|
|
let kernel_acc = f64::from_bits(acc_bits);
|
|
let (ref_acc, ref_n) = reference(sym);
|
|
assert_eq!(
|
|
kernel_acc.to_bits(), ref_acc.to_bits(),
|
|
"{sym}: kernel acc bit-exact vs same-order host reference"
|
|
);
|
|
assert_eq!(n, ref_n, "{sym}: tick count matches reference");
|
|
assert!(ref_n > 0, "{sym}: non-empty stream");
|
|
symbols_checked += 1;
|
|
}
|
|
assert!(
|
|
(2..=N_SYMBOLS).contains(&symbols_checked),
|
|
"expected 2..={N_SYMBOLS} symbols fanned, got {symbols_checked}"
|
|
);
|
|
}
|
|
|
|
/// Global leak-freedom: Σallocs == Σfrees across ALL `ailang_rc_stats:`
|
|
/// lines (the M2 dual-stat-line model, ported from
|
|
/// embed_tick_e2e.rs:94-104). The invariant is the global Σ, not
|
|
/// per-line balance (M2 TLS-ctx cross-attribution).
|
|
///
|
|
/// LIVE and GREEN. The non-determinism was a build-dependency
|
|
/// staleness gap, not a residual runtime defect: `ail-embed/build.rs`
|
|
/// did not track the `runtime/` C sources as `rerun-if-changed`, so
|
|
/// the atomic-counter runtime fix `bugfix-rc-global-stats-race`
|
|
/// (commit `7bfa11e`) never relinked `libailang_rt.a` — the swarm
|
|
/// kept linking the stale pre-`7bfa11e` non-atomic
|
|
/// `g_rc_alloc_count++` whose race undercounted `Σallocs` (`Σfrees`
|
|
/// stable-exact). Fixed in `build.rs` this iter
|
|
/// (`bugfix-swarm-rc-alloc-undercount`): the fresh atomic archive
|
|
/// relinks and the leak Σ now balances deterministically
|
|
/// (`Σallocs == Σfrees == 12000003`, every run). This test is the
|
|
/// integration-level acceptance of both `7bfa11e` and the
|
|
/// build-dependency fix. Body unchanged — it was quarantined, never
|
|
/// weakened. See
|
|
/// docs/journals/2026-05-19-iter-bugfix-swarm-rc-alloc-undercount.md.
|
|
#[test]
|
|
fn symbol_fan_swarm_leak_free() {
|
|
let Some((_stdout, stderr)) = run_swarm_or_skip() else { return };
|
|
|
|
let mut allocs: u64 = 0;
|
|
let mut frees: u64 = 0;
|
|
let mut seen = 0usize;
|
|
for line in stderr.lines().filter(|l| l.starts_with("ailang_rc_stats:")) {
|
|
seen += 1;
|
|
for tok in line.split_whitespace() {
|
|
if let Some(v) = tok.strip_prefix("allocs=") {
|
|
allocs += v.parse::<u64>().expect("allocs= u64");
|
|
} else if let Some(v) = tok.strip_prefix("frees=") {
|
|
frees += v.parse::<u64>().expect("frees= u64");
|
|
}
|
|
}
|
|
}
|
|
assert!(seen > 0, "no ailang_rc_stats line; stderr:\n{stderr}");
|
|
assert_eq!(
|
|
allocs, frees,
|
|
"globally leak-free: Σallocs==Σfrees across {seen} stat line(s)"
|
|
);
|
|
}
|