iter embedding-abi-m5.2 (PARTIAL 2/3): data-server adapter + symbol-fan swarm; leak-proof surfaced a non-atomic global g_rc_* stats race
M5 iteration 2 (specae905de, plan9cc9d9c). Tasks 1+2 clean and committable; Task 3 BLOCKED on a real finding → M5 bounce-back. Shipped: - data-server promoted [dev-dependencies] -> [dependencies] (Invariant-1 sanctioned: ail-embed is the sole meeting point; the AILang workspace graph still has data-server count 0; zero compiler-surface diff — not even root Cargo.toml this iter). - additive `adapter` module (ail-embed/src/adapter.rs): tick_to_px, MidPriceStream (lazy Iterator<f64> over SymbolChunkIter), fold_symbol; RED-first (E0432 -> GREEN). m5.1 core untouched except `pub mod adapter;`. - swarm_runner [[bin]]: one thread per symbol, each owning its Kernel. The clean compile IS the compile-time per-thread-ctx proof (Ctx: !Send => a shared-ctx swarm is E0277). - tests/swarm.rs: real-data symbol-fan E2E. `symbol_fan_swarm_bit_exact` is GREEN and live — per-symbol kernel (acc,n) bit-exact vs an independent same-order host reference (EURUSD/GER40/XAUUSD, ~4s). The Task 3 finding (Boss independently confirmed by reading runtime/rc.c): the global Σallocs==Σfrees leak measurement is an INSTRUMENTATION race, not a memory bug. No box crosses a thread (Ctx: !Send), the real refcount/free is correct (bit-exact GREEN every run); only the global g_rc_* stat counters (rc.c:90-91, 161,212) are non-atomic BY rc.c's own documented single-threaded design, losing ++s when 3 worker threads hit the null-__ail_tls_ctx host-side path. M5 is AILang's first concurrent consumer; rc.c's header (rc.c:44-49) explicitly defers exactly this atomic-vs-non- atomic decision to "when it acquires concurrency primitives". The leak assertion was Boss-split into `symbol_fan_swarm_leak_free` (#[ignore], body preserved VERBATIM — quarantined not weakened; un-ignore = the runtime fix's acceptance criterion) so main stays green and the finding is pinned as a regression marker. The earlier plan/journal claim that embed_swarm_tsan.rs covers this path was wrong and is corrected on the record (that test uses the scalar kernel — zero box allocs — so it never exercised the host-side global-counter path). Escalated as an M5 bounce-back: resolution touches M3-frozen runtime/ and re-frames M5's "zero runtime change" commitment; multiple substantive options, not unilaterally Boss's to pick in frozen-runtime territory. Boss recommendation on the record = Option A (make only the global-fallback g_rc_* counters atomic, as a standalone RED-first runtime micro-iteration; M5 framing amended). Time-shard + friction-harvest remain m5.3. Includes the per-iter journal (with Boss disposition), stats, and the INDEX.md line.
This commit is contained in:
+15
-5
@@ -14,13 +14,23 @@ license = "MIT"
|
|||||||
publish = false
|
publish = false
|
||||||
description = "Lean embedding of an AILang M3-frozen staticlib kernel into a Rust host. Not an AILang [workspace] member (Invariant 1)."
|
description = "Lean embedding of an AILang M3-frozen staticlib kernel into a Rust host. Not an AILang [workspace] member (Invariant 1)."
|
||||||
|
|
||||||
# Iter m5.1: the library itself has ZERO dependencies — the embedding
|
# The embedding core (`src/lib.rs`) touches only the frozen C ABI and
|
||||||
# core only touches the frozen C ABI. `data-server` is dev-only,
|
# carries zero finance knowledge; `data-server` enters solely through
|
||||||
# exercised solely by the hermetic smoke test, so Invariant 1 holds
|
# the additive `adapter` module + the `swarm_runner` bin. `ail-embed`
|
||||||
# in the dependency graph, not just on paper.
|
# is the sole sanctioned data-server↔AILang meeting point — Invariant 1
|
||||||
|
# governs the *compiler* crates (`ailang-*`, `crates/ail`, `runtime/`),
|
||||||
|
# which remain data-server-free, not this workspace-excluded crate.
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# The adapter + swarm_runner bin link data-server (ail-embed is the
|
||||||
|
# sole data-server↔AILang meeting point — Invariant 1 governs the
|
||||||
|
# *compiler* crates, not this crate). Path is manifest-relative
|
||||||
|
# (data-server is at /home/brummel/dev/libs/data-server).
|
||||||
|
data-server = { path = "../../libs/data-server" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
data-server = { path = "../../libs/data-server" }
|
|
||||||
zip = "2"
|
zip = "2"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "swarm_runner"
|
||||||
|
path = "src/bin/swarm_runner.rs"
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
//! The sole `data-server`-knowing layer of `ail-embed`: maps
|
||||||
|
//! `data_server::records::TickParsed` to the single scalar the M3
|
||||||
|
//! `Tick` record carries (mid price), and lazily streams
|
||||||
|
//! `SymbolChunkIter` chunks into the data-server-free `Kernel`
|
||||||
|
//! (host-side chunk unroll — the adapter owns chunk iteration).
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use data_server::records::TickParsed;
|
||||||
|
use data_server::{DataServer, SymbolChunkIter};
|
||||||
|
|
||||||
|
use crate::Kernel;
|
||||||
|
|
||||||
|
/// Mid price — the single scalar the M3 `Tick` record carries.
|
||||||
|
#[inline]
|
||||||
|
pub fn tick_to_px(t: &TickParsed) -> f64 {
|
||||||
|
(t.ask + t.bid) / 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lazy `Iterator<Item = f64>` over a `data-server` tick stream,
|
||||||
|
/// yielding the per-tick mid. Pulls `next_chunk` on demand (the
|
||||||
|
/// adapter owns chunk iteration; chunk boundaries are invisible to
|
||||||
|
/// the fold) so a whole symbol is never materialised.
|
||||||
|
pub struct MidPriceStream {
|
||||||
|
it: SymbolChunkIter<TickParsed>,
|
||||||
|
chunk: Option<Arc<[TickParsed]>>,
|
||||||
|
idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MidPriceStream {
|
||||||
|
pub fn new(it: SymbolChunkIter<TickParsed>) -> Self {
|
||||||
|
Self { it, chunk: None, idx: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for MidPriceStream {
|
||||||
|
type Item = f64;
|
||||||
|
fn next(&mut self) -> Option<f64> {
|
||||||
|
loop {
|
||||||
|
if let Some(c) = &self.chunk {
|
||||||
|
if self.idx < c.len() {
|
||||||
|
let px = tick_to_px(&c[self.idx]);
|
||||||
|
self.idx += 1;
|
||||||
|
return Some(px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match self.it.next_chunk() {
|
||||||
|
Some(c) => {
|
||||||
|
self.chunk = Some(c);
|
||||||
|
self.idx = 0;
|
||||||
|
}
|
||||||
|
None => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fold up to `max_ticks` of `symbol`'s mid-price stream through the
|
||||||
|
/// M3 kernel. `None` if the symbol has no tick stream. Returns
|
||||||
|
/// `(Σ mid, count)` — the kernel's `acc += px; n += 1` over the
|
||||||
|
/// bounded prefix, identical-order to a host reference fold.
|
||||||
|
pub fn fold_symbol(
|
||||||
|
server: &Arc<DataServer>,
|
||||||
|
symbol: &str,
|
||||||
|
max_ticks: usize,
|
||||||
|
) -> Option<(f64, i64)> {
|
||||||
|
let it = server.stream_tick(symbol)?;
|
||||||
|
Some(Kernel::new().run(MidPriceStream::new(it).take(max_ticks)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::tick_to_px;
|
||||||
|
use data_server::records::TickParsed;
|
||||||
|
|
||||||
|
/// mid = (ask + bid) / 2. Exact in f64 for these inputs.
|
||||||
|
#[test]
|
||||||
|
fn tick_to_px_is_mid() {
|
||||||
|
let t = TickParsed { time_ms: 0, ask: 2.0, bid: 4.0 };
|
||||||
|
assert_eq!(tick_to_px(&t), 3.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
//! Symbol-fan swarm harness (spec §3 symbol-fan / Testing §2). Run
|
||||||
|
//! as a subprocess by `tests/swarm.rs` under `AILANG_RC_STATS=1` so
|
||||||
|
//! the C-runtime atexit `ailang_rc_stats:` line is observable.
|
||||||
|
//!
|
||||||
|
//! argv[1] = data dir (defaults to `data_server::DEFAULT_DATA_PATH`).
|
||||||
|
//! Enumerates up to `N_SYMBOLS` symbols that have tick files
|
||||||
|
//! (requires >= 2 for a meaningful swarm), spawns one thread per
|
||||||
|
//! symbol — each thread owns its `Kernel` (`Ctx: !Send` forces
|
||||||
|
//! one ctx per thread) — folds at most `MAX_TICKS` mids, and prints
|
||||||
|
//! one `RESULT <symbol> <acc_bits:016x> <count>` line per symbol to
|
||||||
|
//! stdout. Exits 0 on success, non-zero (with a message) otherwise.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use ail_embed::adapter::fold_symbol;
|
||||||
|
use data_server::records::DataFormat;
|
||||||
|
use data_server::{DataServer, DEFAULT_DATA_PATH};
|
||||||
|
|
||||||
|
/// Deterministic bounded prefix per symbol (chunk-aligned via
|
||||||
|
/// `Iterator::take`): enough real volume to be a genuine proof,
|
||||||
|
/// bounded so the E2E stays ~seconds. m5.3 owns the friction
|
||||||
|
/// timing; here it is only a runtime bound.
|
||||||
|
const MAX_TICKS: usize = 2_000_000;
|
||||||
|
/// Max threads/symbols fanned. Real data has >= 2 tick symbols
|
||||||
|
/// (EURUSD, XAUUSD); take min(N_SYMBOLS, available).
|
||||||
|
const N_SYMBOLS: usize = 4;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let data_dir = std::env::args()
|
||||||
|
.nth(1)
|
||||||
|
.unwrap_or_else(|| DEFAULT_DATA_PATH.to_string());
|
||||||
|
|
||||||
|
let server = Arc::new(DataServer::new(&data_dir));
|
||||||
|
|
||||||
|
let mut tick_syms: Vec<String> = server
|
||||||
|
.symbols()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| {
|
||||||
|
server.file_count(s, DataFormat::Tick).unwrap_or(0) > 0
|
||||||
|
})
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
tick_syms.truncate(N_SYMBOLS);
|
||||||
|
|
||||||
|
if tick_syms.len() < 2 {
|
||||||
|
eprintln!(
|
||||||
|
"swarm_runner: need >= 2 tick symbols under {data_dir}, found {}",
|
||||||
|
tick_syms.len()
|
||||||
|
);
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let handles: Vec<_> = tick_syms
|
||||||
|
.into_iter()
|
||||||
|
.map(|sym| {
|
||||||
|
let srv = Arc::clone(&server);
|
||||||
|
// Each thread builds its own Kernel inside the closure.
|
||||||
|
// `Kernel`/`Ctx` are `!Send`, so this is the ONLY way
|
||||||
|
// this compiles — one ctx per thread, enforced by the
|
||||||
|
// type system, not by convention.
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let (acc, n) = fold_symbol(&srv, &sym, MAX_TICKS)
|
||||||
|
.expect("symbol has a tick stream");
|
||||||
|
(sym, acc, n)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for h in handles {
|
||||||
|
let (sym, acc, n) = h.join().expect("worker thread panicked");
|
||||||
|
println!("RESULT {sym} {:016x} {n}", acc.to_bits());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,10 @@
|
|||||||
|
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
/// The sole `data-server`-knowing layer (spec §2). Additive; the
|
||||||
|
/// embedding core below has zero finance knowledge.
|
||||||
|
pub mod adapter;
|
||||||
|
|
||||||
// The M3-frozen C ABI. Symbol names + signatures mirror
|
// The M3-frozen C ABI. Symbol names + signatures mirror
|
||||||
// `crates/ail/tests/embed/tick_roundtrip.c:31-36`. `backtest_step_tick`
|
// `crates/ail/tests/embed/tick_roundtrip.c:31-36`. `backtest_step_tick`
|
||||||
// is the author-chosen export of `examples/embed_backtest_step_tick.ail`.
|
// is the author-chosen export of `examples/embed_backtest_step_tick.ail`.
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
//! 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` — `#[ignore]`d pending an escalated
|
||||||
|
//! design decision. The global `Σallocs==Σfrees` measurement
|
||||||
|
//! (ported from crates/ail/tests/embed_tick_e2e.rs:94-104) is
|
||||||
|
//! *invalid under a multi-threaded host*: host-side box allocs run
|
||||||
|
//! with `__ail_tls_ctx == NULL` and fall through to the
|
||||||
|
//! **non-atomic** global `g_rc_*` counters (runtime/rc.c:90-91,
|
||||||
|
//! 161,212), which rc.c's own header explicitly documents as
|
||||||
|
//! single-threaded/non-atomic ("when it acquires [concurrency],
|
||||||
|
//! atomic-vs-non-atomic becomes a separate decision"). The swarm is
|
||||||
|
//! *actually* leak-free (no box crosses a thread — `Ctx: !Send`;
|
||||||
|
//! bit-exact is GREEN every run); only the global *counter* races,
|
||||||
|
//! losing increments. The assertion body is preserved verbatim and
|
||||||
|
//! will be re-armed (un-`#[ignore]`d) by the runtime iteration that
|
||||||
|
//! makes the global fallback counters atomic. See
|
||||||
|
//! docs/journals/2026-05-19-iter-embedding-abi-m5.2.md and the M5
|
||||||
|
//! bounce-back. Quarantined, NOT weakened — what it asserts is
|
||||||
|
//! unchanged; it is gated on a decision the Boss escalated.
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use data_server::records::DataFormat;
|
||||||
|
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).
|
||||||
|
///
|
||||||
|
/// `#[ignore]`d: this measurement is invalid under a multi-threaded
|
||||||
|
/// host. Host-side box allocs run with `__ail_tls_ctx == NULL` and
|
||||||
|
/// hit the non-atomic global `g_rc_*` counters (runtime/rc.c:90-91,
|
||||||
|
/// 161,212) — single-threaded/non-atomic *by rc.c's own documented
|
||||||
|
/// design*. 3 worker threads race the counter, losing increments;
|
||||||
|
/// the *counter* is unreliable even though the swarm is actually
|
||||||
|
/// leak-free (no box crosses a thread; `symbol_fan_swarm_bit_exact`
|
||||||
|
/// is GREEN every run). Un-`#[ignore]` this — body unchanged — once
|
||||||
|
/// the runtime iteration makes the global fallback counters atomic
|
||||||
|
/// (the deferred "concurrency arrived" decision rc.c:44-49 names).
|
||||||
|
/// See docs/journals/2026-05-19-iter-embedding-abi-m5.2.md.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "blocked: global g_rc_* counter is non-atomic by rc.c design; \
|
||||||
|
invalid under the multi-thread host. Swarm is actually \
|
||||||
|
leak-free (bit-exact is green; no box crosses a thread). \
|
||||||
|
Un-ignore when the runtime makes the global fallback \
|
||||||
|
counters atomic — see the m5.2 journal + M5 bounce-back."]
|
||||||
|
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)"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"iter_id": "embedding-abi-m5.2",
|
||||||
|
"date": "2026-05-19",
|
||||||
|
"mode": "standard",
|
||||||
|
"outcome": "PARTIAL",
|
||||||
|
"tasks_total": 3,
|
||||||
|
"tasks_completed": 2,
|
||||||
|
"reloops_per_task": { "1": 0, "2": 0, "3": 0 },
|
||||||
|
"review_loops_spec": 0,
|
||||||
|
"review_loops_quality": 1,
|
||||||
|
"blocked_reason": "worker-blocked",
|
||||||
|
"notes": "Tasks 1+2 clean (Task 1 DONE_WITH_CONCERNS: plan Step-6 command ordering defect + quality-phase stale-comment honesty fix, 1 quality re-loop). Task 3 BLOCKED: the plan's verbatim leak-free assertion correctly surfaced a non-atomic global g_rc counter race (host-side ail-embed allocs run with __ail_tls_ctx==NULL, falling through to runtime/rc.c:161,212 raced by 3 worker threads). Not re-loopable (verbatim plan code), not in-scope-fixable (zero runtime/ diff mandated; runtime/ M3-frozen). Bit-exact half of the E2E sound. Invariant 1 clean independently (Step4=0, Step5 empty)."
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# iter embedding-abi-m5.2 — data-server adapter + symbol-fan swarm; leak-free gate surfaced a runtime race
|
||||||
|
|
||||||
|
**Date:** 2026-05-19
|
||||||
|
**Started from:** 9cc9d9c5170eb8d52152dcef071726c7a6ffd05d
|
||||||
|
**Status:** PARTIAL
|
||||||
|
**Tasks completed:** 2 of 3
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Tasks 1 and 2 landed clean and verbatim-to-plan: `data-server` was
|
||||||
|
promoted dev-dep → real dep, the additive `adapter` module
|
||||||
|
(`tick_to_px` / `MidPriceStream` / `fold_symbol`) was added with a
|
||||||
|
genuine compile-error RED → GREEN cycle, and the `swarm_runner` bin
|
||||||
|
compiled — that compile *is* the compile-time per-thread-ctx proof
|
||||||
|
(`Ctx: !Send`; a shared-ctx design is `error[E0277]` and could not
|
||||||
|
have built). Invariant 1 holds independently: the AILang workspace
|
||||||
|
graph has zero `data-server` (Step 4 = `0`) and there is zero
|
||||||
|
compiler-surface diff (Step 5 path filter empty — only `ail-embed/**`
|
||||||
|
touched, not even the root `Cargo.toml`).
|
||||||
|
|
||||||
|
Task 3 is BLOCKED. The plan's verbatim leak-free assertion
|
||||||
|
(`Σallocs == Σfrees` across all `ailang_rc_stats:` lines) is correct
|
||||||
|
and is doing its job: it surfaced a genuine concurrency defect. The
|
||||||
|
swarm E2E passed once in isolation by luck (the racing counter
|
||||||
|
happened to land on the balanced value) and then FAILED in the
|
||||||
|
full-suite run and on 4 of 5 direct re-runs. The host-side box
|
||||||
|
allocations in `ail-embed/src/lib.rs` (`make_state` / `make_tick` /
|
||||||
|
the final `ailang_rc_dec`) run on the Rust worker thread where
|
||||||
|
`__ail_tls_ctx == NULL` — the M3 forwarder only sets the TLS ctx for
|
||||||
|
the synchronous duration of the kernel call, not for the host's own
|
||||||
|
allocs. With `__ail_tls_ctx` null those allocs fall through to the
|
||||||
|
**non-atomic global statics** `g_rc_alloc_count` / `g_rc_free_count`
|
||||||
|
(`runtime/rc.c:161,212`), incremented concurrently by 3 worker
|
||||||
|
threads with no synchronisation. The atexit `g_rc` line's `allocs`
|
||||||
|
is consequently non-deterministic across runs
|
||||||
|
(5,993,321 … 5,999,318 over 5 runs; should be a stable 6,000,000)
|
||||||
|
while `frees` stays at a stable 3. The bit-exactness half of the
|
||||||
|
test is sound and passed every run; only the global leak-accounting
|
||||||
|
is racy.
|
||||||
|
|
||||||
|
This is exactly the class of finding the symbol-fan swarm milestone
|
||||||
|
exists to expose — and it is out of m5.2 scope to fix (see Blocked
|
||||||
|
detail). The Boss decides: extend the spec / m5.3 to cover the
|
||||||
|
host-side-alloc TLS-ctx routing (or atomic global counters), or
|
||||||
|
re-scope the leak-free acceptance.
|
||||||
|
|
||||||
|
## Per-task notes
|
||||||
|
|
||||||
|
- iter embedding-abi-m5.2.1: promote `data-server` dev-dep → real
|
||||||
|
dep + additive `adapter` module. RED (`E0432 unresolved import
|
||||||
|
super::tick_to_px`) → impl `tick_to_px`/`MidPriceStream`/
|
||||||
|
`fold_symbol` → GREEN (`tick_to_px_is_mid`, +
|
||||||
|
`kernel_run_sums_prices` unregressed under `--lib`). Cargo.toml +
|
||||||
|
`lib.rs:7` verbatim-to-plan. Stale m5.1 "ZERO dependencies"
|
||||||
|
manifest comment corrected for present-tense honesty (quality
|
||||||
|
phase).
|
||||||
|
- iter embedding-abi-m5.2.2: `src/bin/swarm_runner.rs` written
|
||||||
|
verbatim; `cargo build --bin swarm_runner` → exit 0. The clean
|
||||||
|
compile IS the compile-time per-thread-ctx proof (`Ctx: !Send`;
|
||||||
|
no `E0277`; per-thread-owned-Kernel structure intact, not
|
||||||
|
collapsed to a shared ctx).
|
||||||
|
- iter embedding-abi-m5.2.3: `tests/swarm.rs` written verbatim. The
|
||||||
|
E2E genuinely executed (NOT the skip path — 3 tick symbols
|
||||||
|
present: EURUSD, GER40, XAUUSD; ~4 s, 3×2M FFI folds). Per-symbol
|
||||||
|
bit-exactness vs independent host reference: sound, passed every
|
||||||
|
run. Global `Σallocs==Σfrees` leak-free assertion: BLOCKED —
|
||||||
|
correctly detects a non-atomic global-counter race (see Summary +
|
||||||
|
Blocked detail). Not re-loopable (the plan's verbatim code is
|
||||||
|
exactly as specified; no re-attempt makes a racy C counter
|
||||||
|
deterministic) and not fixable in-scope (Step 5 mandates zero
|
||||||
|
`runtime/` diff; `runtime/` is M3-frozen).
|
||||||
|
|
||||||
|
## Concerns
|
||||||
|
|
||||||
|
- Plan Task 1 Step 6's literal command
|
||||||
|
(`cargo test --manifest-path ail-embed/Cargo.toml`, unfiltered)
|
||||||
|
is unsatisfiable in Task 1's isolation: the `[[bin]] swarm_runner`
|
||||||
|
declaration added by Step 2's verbatim Cargo.toml replacement
|
||||||
|
references `src/bin/swarm_runner.rs`, which Task 2 Step 1 creates.
|
||||||
|
The plan Self-review point 7 ("No later task is required to make
|
||||||
|
an earlier compile gate pass") is incorrect for this command.
|
||||||
|
Task 1's no-core-regression intent was fully proven via `--lib`
|
||||||
|
(`kernel_run_sums_prices` + `tick_to_px_is_mid` GREEN); the
|
||||||
|
smoke + full-suite gate is covered by Task 3 Step 3 once the bin
|
||||||
|
exists. The verbatim Cargo.toml was NOT split to dodge this
|
||||||
|
(splitting it would be the spec-compliance violation; the
|
||||||
|
`[[bin]]` + dep-promotion is one cohesive manifest change). No
|
||||||
|
code defect — purely a plan command-ordering defect.
|
||||||
|
- Quality phase corrected the pre-existing m5.1 Cargo.toml comment
|
||||||
|
("the library itself has ZERO dependencies … data-server is
|
||||||
|
dev-only") which became false the moment `data-server` moved into
|
||||||
|
`[dependencies]`. The plan's verbatim replacement boundary started
|
||||||
|
*at* `[dependencies]` and did not script the preceding comment's
|
||||||
|
removal; leaving a now-false manifest comment violates the
|
||||||
|
DESIGN.md honesty rule (comments mirror current state). The
|
||||||
|
replacement comment states the present-tense m5.2 framing,
|
||||||
|
consistent with the plan's own new block + Boss decision.
|
||||||
|
|
||||||
|
## Known debt
|
||||||
|
|
||||||
|
- The host-side embedding allocation path (`ail-embed/src/lib.rs`
|
||||||
|
`make_state`/`make_tick`/final `ailang_rc_dec`) executes with
|
||||||
|
`__ail_tls_ctx == NULL` (the M3 `@<sym>` forwarder sets the TLS
|
||||||
|
ctx only for the synchronous kernel-call duration). Under the
|
||||||
|
multi-thread swarm these allocs/frees land on the non-atomic
|
||||||
|
global `g_rc_alloc_count`/`g_rc_free_count` fallback
|
||||||
|
(`runtime/rc.c:161,212`), which races. Not touched here:
|
||||||
|
`runtime/` is M3-frozen and Step 5 mandates zero `runtime/` diff;
|
||||||
|
the host-side-routing alternative is a design change beyond
|
||||||
|
"additive adapter + swarm bin". Reserved for Boss/m5.3 design.
|
||||||
|
|
||||||
|
## Blocked detail
|
||||||
|
|
||||||
|
- **Task:** 3 (Step 2 / Step 3 — the leak-free `Σallocs==Σfrees`
|
||||||
|
gate).
|
||||||
|
- **Reason:** `worker-blocked` — the plan's own verbatim acceptance
|
||||||
|
gate correctly detects a runtime concurrency defect whose fix is
|
||||||
|
out of m5.2 scope and would violate this iter's Invariant-1
|
||||||
|
Step-5 zero-`runtime/`-diff constraint.
|
||||||
|
- **Worker says (verbatim):** Task 3's `tests/swarm.rs` is verbatim
|
||||||
|
to plan and genuinely executes (bit-exact half sound, GREEN every
|
||||||
|
run). The `Σallocs==Σfrees` half is non-deterministic: full-suite
|
||||||
|
run FAILED `left: 11997743, right: 12000003`; 5 direct child runs
|
||||||
|
of the atexit `g_rc` line gave `allocs=` 5993699/5996029/5993321/
|
||||||
|
5999318/5996019 with `frees=3` stable. Root cause: host-side
|
||||||
|
`ailang_rc_alloc`/`ailang_rc_dec` from `lib.rs` run with
|
||||||
|
`__ail_tls_ctx==NULL`, falling through to the non-atomic global
|
||||||
|
`g_rc_*` counters (`rc.c:161,212`) raced by 3 worker threads. Not
|
||||||
|
re-loopable (verbatim plan code; a racy C counter is not made
|
||||||
|
deterministic by re-attempting the same code) and not
|
||||||
|
in-scope-fixable (zero `runtime/` diff mandated; `runtime/` is
|
||||||
|
M3-frozen).
|
||||||
|
- **Suggested next step:** Boss decision — either (a) extend the
|
||||||
|
spec/m5.3 to route host-side `ail-embed` allocs through a
|
||||||
|
per-thread `ailang_ctx_t` so the swarm's leak accounting is
|
||||||
|
per-thread-attributed (no global fallback), or make
|
||||||
|
`g_rc_alloc_count`/`g_rc_free_count` atomic in a *separate*
|
||||||
|
runtime iteration outside m5.2's zero-`runtime/`-diff constraint;
|
||||||
|
or (b) re-scope the m5.2 leak-free acceptance. Tasks 1 + 2 are
|
||||||
|
clean and committable as a PARTIAL; the architect should see the
|
||||||
|
TSan-discharge reasoning below at milestone close.
|
||||||
|
|
||||||
|
## Boss-decision rationale carried forward (mirrored from the plan, for the architect at milestone close)
|
||||||
|
|
||||||
|
- **TSan discharge (compile-time + cited green precedent, no new
|
||||||
|
harness).** The spec's "sanitiser-clean (no data race across
|
||||||
|
per-thread ctxs)" acceptance is discharged by (1) `Ctx`/`Kernel`
|
||||||
|
being `!Send` (raw `*mut c_void`) — the swarm is *structurally
|
||||||
|
forced* one-ctx-per-thread; a shared-ctx design is `error[E0277]`
|
||||||
|
and cannot compile, so Task 2 Step 2's clean build is a
|
||||||
|
compile-time race-freedom proof for the *per-ctx kernel calls*;
|
||||||
|
(2) the C-runtime race-freedom of that exact per-thread-ctx
|
||||||
|
pattern is ratified green by `crates/ail/tests/embed_swarm_tsan.rs`
|
||||||
|
(C host, 8 pthreads, own ctx each, `clang -fsanitize=thread`,
|
||||||
|
exit-clean), cited not re-run. NOTE FOR THE ARCHITECT: this
|
||||||
|
discharge covers the *per-thread-ctx kernel path*. The Task 3
|
||||||
|
finding is a *different* surface — the host-side `ail-embed`
|
||||||
|
allocs that bypass the TLS ctx and hit the non-atomic global
|
||||||
|
`g_rc_*` fallback. The TSan precedent does not cover that path
|
||||||
|
(the C host in `embed_swarm_tsan.rs` does not exercise a Rust
|
||||||
|
host allocating boxes with a null TLS ctx). The compile-time
|
||||||
|
`!Send` proof and the cited green TSan remain valid for what they
|
||||||
|
claim; they simply do not reach the global-fallback race m5.2
|
||||||
|
newly exposed.
|
||||||
|
- **`data-server` dev-dep → real dep is sanctioned.** Invariant 1
|
||||||
|
governs the *compiler* crates (`ailang-*`, `crates/ail`,
|
||||||
|
`runtime/`), which gain nothing — verified clean this iter
|
||||||
|
(Step 4 = `0`, Step 5 empty). `ail-embed` is explicitly the
|
||||||
|
*sole* data-server↔AILang meeting point.
|
||||||
|
- **Deferred to m5.3 (not this plan):** time-shard
|
||||||
|
boundary-invisibility, friction-harvest timing, milestone
|
||||||
|
close-out. Not implemented here (scope discipline held — no
|
||||||
|
time-shard, no perf timing, no close-out crept in).
|
||||||
|
|
||||||
|
## Files touched
|
||||||
|
|
||||||
|
- `ail-embed/Cargo.toml` (modified — dep promotion + `[[bin]]` +
|
||||||
|
stale-comment honesty fix)
|
||||||
|
- `ail-embed/src/lib.rs` (modified — `pub mod adapter;`, additive)
|
||||||
|
- `ail-embed/src/adapter.rs` (new — the sole data-server-knowing layer)
|
||||||
|
- `ail-embed/src/bin/swarm_runner.rs` (new — symbol-fan swarm bin)
|
||||||
|
- `ail-embed/tests/swarm.rs` (new — symbol-fan leak-free + bit-exact
|
||||||
|
E2E; bit-exact half sound, leak-free half surfaces the race)
|
||||||
|
|
||||||
|
## Stats
|
||||||
|
|
||||||
|
bench/orchestrator-stats/2026-05-19-iter-embedding-abi-m5.2.json
|
||||||
|
|
||||||
|
## Boss disposition (2026-05-19)
|
||||||
|
|
||||||
|
Boss independently read `runtime/rc.c` and confirms the diagnosis is
|
||||||
|
**precise and the finding is an instrumentation-measurement race, NOT
|
||||||
|
a memory bug**: the per-object refcount header ops (`*hdr +=/-= 1`,
|
||||||
|
rc.c:180/208) are non-atomic but **no box crosses a thread** in the
|
||||||
|
swarm (`Ctx: !Send` structurally forces alloc/use/free within one
|
||||||
|
worker), so there is no real refcount race, no double-free, no leak —
|
||||||
|
the bit-exact half is GREEN every run, confirming the memory
|
||||||
|
management is actually correct. The *only* raced object is the global
|
||||||
|
`g_rc_*` statistics counter (rc.c:90-91,161,212), **non-atomic by
|
||||||
|
rc.c's own explicit design** (header comment rc.c:44-49,85: "AILang
|
||||||
|
has no concurrency primitives yet; when it acquires them,
|
||||||
|
atomic-vs-non-atomic becomes a separate decision"). M5 is AILang's
|
||||||
|
first concurrent consumer — that deferred decision is now due.
|
||||||
|
|
||||||
|
Committed as a PARTIAL (Tasks 1+2 + the GREEN existence proof) so the
|
||||||
|
sound work + the finding land on the record, main stays green:
|
||||||
|
`tests/swarm.rs` was Boss-split into `symbol_fan_swarm_bit_exact`
|
||||||
|
(live, GREEN — the real-data existence proof) and
|
||||||
|
`symbol_fan_swarm_leak_free` (`#[ignore]`, **body preserved
|
||||||
|
verbatim** — quarantined, not weakened; un-`#[ignore]` is the
|
||||||
|
acceptance criterion for the runtime fix). Earlier plan/journal claim
|
||||||
|
that `embed_swarm_tsan.rs` covers this path was wrong and is
|
||||||
|
corrected here: that test uses the *scalar* kernel (zero box allocs)
|
||||||
|
so it never exercised the host-side global-counter path; the
|
||||||
|
compile-time `!Send` proof + the cited green TSan remain valid only
|
||||||
|
for the per-thread-ctx *kernel* path, which they always claimed.
|
||||||
|
|
||||||
|
Escalated to the user as an M5 **bounce-back** (touches M3-frozen
|
||||||
|
`runtime/`; re-frames M5's "zero runtime change" commitment;
|
||||||
|
multiple substantive options — not unilaterally Boss's to pick in
|
||||||
|
frozen-runtime territory). Boss recommendation on the record:
|
||||||
|
**Option A** — make *only the global fallback* `g_rc_*` counters
|
||||||
|
atomic (leave the per-thread ctx path plain — no contention there),
|
||||||
|
as its own RED-first runtime micro-iteration (`debug` skill), outside
|
||||||
|
M5's "zero runtime change" framing; then resume the M5 leak-proof on
|
||||||
|
the now-valid measurement (un-`#[ignore]` `symbol_fan_swarm_leak_free`
|
||||||
|
as that iter's GREEN). Rationale: it discharges exactly the deferred
|
||||||
|
decision rc.c's own comment names, at the defect's actual location,
|
||||||
|
without touching the frozen value layout / ABI offsets / host-free
|
||||||
|
rule (the literal content of the M3 freeze); Option B (host-side
|
||||||
|
TLS-ctx routing) adds a new runtime API surface; Option C (per-symbol
|
||||||
|
single-thread subprocess leak-proof) is sound but weaker and leaves
|
||||||
|
the runtime's documented single-thread-stats footgun for every future
|
||||||
|
concurrent consumer. User picks the direction.
|
||||||
@@ -107,3 +107,4 @@
|
|||||||
- 2026-05-18 — brainstorm embedding-abi-m4 → RETIRED, never speced (premise collapsed under its own feature-acceptance gate during Step-2/3 Q&A; no spec, no grounding-check, no planner handoff — the "problem mis-framed → don't ratify a known-unneeded shape" brainstorm path): `/boss` picked top-P0 "Embedding ABI — M4: sequence crossing via `List`"; user green-lit a continue-here brainstorm; recon (`ailang-plan-recon`) returned a full fact sheet; two user forks resolved in Q&A (own-only `List` param; `List Record`-only element) and Approach A (structural list-shaped `is_c_abi_type` arm, no name-match; B name-anchored / C `std_list`-SSOT-first rejected on language/scope grounds) recommended — all now moot. Struck on **feature-acceptance clause 2**: the shipped M3 gate `is_c_abi_type` (`crates/ailang-check/src/lib.rs:1934-1953`) is a per-parameter loop accepting a C scalar OR a single-ctor all-scalar record *independently per param*, and the forwarder's `llvm_scalar` maps every non-scalar `Type::Con`→`ptr` (M3-frozen), so `(State, Tick) -> State` (both single-ctor all-scalar records) is **already gate-accepted + forwarder-supported today** — the minimal data-server binding is M3 (shipped) + a host-side per-tick loop; cons-list crossing would *add* a 2N+1-box-per-chunk host builder + the deferred flat-array perf debt and removes no redundancy, with no named consumer (M5's adapter unrolls each chunk host-side — a clean adapter, the sole data-server↔AILang meeting point per Invariant 1; whole-chunk in-kernel visibility is semantically void since `State` threads across calls regardless of chunk boundaries). Honest mid-Q&A correction recorded: I had asserted "M5 cannot wire data-server without M4" — false (M5 wires it on M3 + `for tick in chunk`); re-deriving against the code rather than defending the roadmap I wrote is what surfaced the clause-2 failure ("user suggestions ≠ directives, form own judgment"). Outcome: M4 retired in `docs/roadmap.md` (struck entry kept one cycle, never `[x]`); M5 reconciled (`depends on:` M4→M3 + Tick-coverage todo; adapter unrolls host-side; friction feeds the host-per-tick-FFI-vs-batch P2 perf decision); residual = a new `[todo]` "Tick-coverage on M3" (E2E+fixture pinning the two-record-param per-tick `(State, Tick) -> State` shape — capability present today but only E2E-proven for a single record param `State`; every shipped M3 fixture pushes a scalar `Float` sample, none a record `Tick`; test backfill, no brainstorm — the actual "minimal data-server binding"). Forward note: the P2 flat-array item's "1024 cons-cells/chunk" framing is now partly stale (cons-list path dropped) — reconcile when picked up, not now. → 2026-05-18-brainstorm-embedding-abi-m4-retired.md
|
- 2026-05-18 — brainstorm embedding-abi-m4 → RETIRED, never speced (premise collapsed under its own feature-acceptance gate during Step-2/3 Q&A; no spec, no grounding-check, no planner handoff — the "problem mis-framed → don't ratify a known-unneeded shape" brainstorm path): `/boss` picked top-P0 "Embedding ABI — M4: sequence crossing via `List`"; user green-lit a continue-here brainstorm; recon (`ailang-plan-recon`) returned a full fact sheet; two user forks resolved in Q&A (own-only `List` param; `List Record`-only element) and Approach A (structural list-shaped `is_c_abi_type` arm, no name-match; B name-anchored / C `std_list`-SSOT-first rejected on language/scope grounds) recommended — all now moot. Struck on **feature-acceptance clause 2**: the shipped M3 gate `is_c_abi_type` (`crates/ailang-check/src/lib.rs:1934-1953`) is a per-parameter loop accepting a C scalar OR a single-ctor all-scalar record *independently per param*, and the forwarder's `llvm_scalar` maps every non-scalar `Type::Con`→`ptr` (M3-frozen), so `(State, Tick) -> State` (both single-ctor all-scalar records) is **already gate-accepted + forwarder-supported today** — the minimal data-server binding is M3 (shipped) + a host-side per-tick loop; cons-list crossing would *add* a 2N+1-box-per-chunk host builder + the deferred flat-array perf debt and removes no redundancy, with no named consumer (M5's adapter unrolls each chunk host-side — a clean adapter, the sole data-server↔AILang meeting point per Invariant 1; whole-chunk in-kernel visibility is semantically void since `State` threads across calls regardless of chunk boundaries). Honest mid-Q&A correction recorded: I had asserted "M5 cannot wire data-server without M4" — false (M5 wires it on M3 + `for tick in chunk`); re-deriving against the code rather than defending the roadmap I wrote is what surfaced the clause-2 failure ("user suggestions ≠ directives, form own judgment"). Outcome: M4 retired in `docs/roadmap.md` (struck entry kept one cycle, never `[x]`); M5 reconciled (`depends on:` M4→M3 + Tick-coverage todo; adapter unrolls host-side; friction feeds the host-per-tick-FFI-vs-batch P2 perf decision); residual = a new `[todo]` "Tick-coverage on M3" (E2E+fixture pinning the two-record-param per-tick `(State, Tick) -> State` shape — capability present today but only E2E-proven for a single record param `State`; every shipped M3 fixture pushes a scalar `Float` sample, none a record `Tick`; test backfill, no brainstorm — the actual "minimal data-server binding"). Forward note: the P2 flat-array item's "1024 cons-cells/chunk" framing is now partly stale (cons-list path dropped) — reconcile when picked up, not now. → 2026-05-18-brainstorm-embedding-abi-m4-retired.md
|
||||||
- 2026-05-18 — iter bugfix-over-strict-mode-ctor-rebuild-consume (RED→GREEN, debug→implement mini, DONE 1/1): fixed a conservative `[over-strict-mode]` false-positive surfaced by the Tick-coverage fixtures. The lint's consume-detection (`any_sub_binder_consumed_for`/`pattern_has_consumed_heap_binder`, `crates/ailang-check/src/linearity.rs`) only recognised a consume of an `(own (con T))` param when a *heap-typed* pattern-binder was moved out of `match p`; when `p` was destructured into purely *primitive* fields fed into a `Term::Ctor` rebuilding `p`'s own ctor, that genuine dismantle+rebuild consume was invisible, so the lint spuriously advised `(borrow ...)`. Real harm: an LLM author "fixing" the spurious warning by flipping an export's declared mode `own`→`borrow` would silently invert the ABI ownership contract. RED-first: debugger disproved the carrier's initial nested-`match` hypothesis (the M3 `embed_backtest_step_record.ail` is silent only because its implicit-mode scalar `Float` param disables the lint via the activation gate, linearity.rs:327 — NOT because it handles the rebuild; the defect reproduces single-param, no nesting), wrote the synthetic RED unit `over_strict_mode_silent_when_ctor_rebuilt_from_primitive_fields` committed as its own audit-trail commit `a11cb7c`. GREEN (implement mini): added a 2nd recognition path to `any_sub_binder_consumed_for` — a `match p` arm that destructures binders out of `p`'s ctor and references any of them (primitive or not) inside a `Term::Ctor`'s args in the arm body genuinely consumes `p`; two pure helpers (`ctor_uses_any_binder` + deep `term_mentions_any_binder`, so `(+ acc px)`-mediated flow counts), conservative toward NOT suppressing (a ctor ignoring `p`'s payload still warns — negative-control proven), over-strict-only (by-name shadowing imprecision is extra-silence, never under-strict; recorded as Known debt). Both stale doc comments that mis-attributed the FP to nested `match` corrected for doc-honesty (debugger concern #2, same code region — in-scope, not opportunistic). +166/−20 in linearity.rs only; check-only, zero codegen/runtime/ABI/schema/DESIGN.md change. Boss-verified independently: RED→GREEN, full `cargo test -p ailang-check` 108/0 lib + every binary 0-failed with NO existing test modified, `ail check embed_backtest_step_tick.ail` no longer over-strict on `st`/`tick` (exit 0), `_tick_borrow.ail` + M3 `embed_backtest_step_record.ail` still clean, the already-green `embed_tick_e2e` + bench posture untouched. No audit/fieldtest gate (lint-precision bugfix; the RED test + the green check-suite ARE the regression coverage). RED `a11cb7c` → GREEN this commit. → 2026-05-18-iter-bugfix-over-strict-mode-ctor-rebuild-consume.md
|
- 2026-05-18 — iter bugfix-over-strict-mode-ctor-rebuild-consume (RED→GREEN, debug→implement mini, DONE 1/1): fixed a conservative `[over-strict-mode]` false-positive surfaced by the Tick-coverage fixtures. The lint's consume-detection (`any_sub_binder_consumed_for`/`pattern_has_consumed_heap_binder`, `crates/ailang-check/src/linearity.rs`) only recognised a consume of an `(own (con T))` param when a *heap-typed* pattern-binder was moved out of `match p`; when `p` was destructured into purely *primitive* fields fed into a `Term::Ctor` rebuilding `p`'s own ctor, that genuine dismantle+rebuild consume was invisible, so the lint spuriously advised `(borrow ...)`. Real harm: an LLM author "fixing" the spurious warning by flipping an export's declared mode `own`→`borrow` would silently invert the ABI ownership contract. RED-first: debugger disproved the carrier's initial nested-`match` hypothesis (the M3 `embed_backtest_step_record.ail` is silent only because its implicit-mode scalar `Float` param disables the lint via the activation gate, linearity.rs:327 — NOT because it handles the rebuild; the defect reproduces single-param, no nesting), wrote the synthetic RED unit `over_strict_mode_silent_when_ctor_rebuilt_from_primitive_fields` committed as its own audit-trail commit `a11cb7c`. GREEN (implement mini): added a 2nd recognition path to `any_sub_binder_consumed_for` — a `match p` arm that destructures binders out of `p`'s ctor and references any of them (primitive or not) inside a `Term::Ctor`'s args in the arm body genuinely consumes `p`; two pure helpers (`ctor_uses_any_binder` + deep `term_mentions_any_binder`, so `(+ acc px)`-mediated flow counts), conservative toward NOT suppressing (a ctor ignoring `p`'s payload still warns — negative-control proven), over-strict-only (by-name shadowing imprecision is extra-silence, never under-strict; recorded as Known debt). Both stale doc comments that mis-attributed the FP to nested `match` corrected for doc-honesty (debugger concern #2, same code region — in-scope, not opportunistic). +166/−20 in linearity.rs only; check-only, zero codegen/runtime/ABI/schema/DESIGN.md change. Boss-verified independently: RED→GREEN, full `cargo test -p ailang-check` 108/0 lib + every binary 0-failed with NO existing test modified, `ail check embed_backtest_step_tick.ail` no longer over-strict on `st`/`tick` (exit 0), `_tick_borrow.ail` + M3 `embed_backtest_step_record.ail` still clean, the already-green `embed_tick_e2e` + bench posture untouched. No audit/fieldtest gate (lint-precision bugfix; the RED test + the green check-suite ARE the regression coverage). RED `a11cb7c` → GREEN this commit. → 2026-05-18-iter-bugfix-over-strict-mode-ctor-rebuild-consume.md
|
||||||
- 2026-05-19 — iter embedding-abi-m5.1 (DONE 3/3): M5 iter 1 — stood up the workspace-excluded `ail-embed` crate. A zero-dependency embedding core (Rust port of the audited `crates/ail/tests/embed/tick_roundtrip.c`: `extern "C"` to the M3-frozen ABI + frozen-layout `State`/`Tick` box helpers + a `Kernel` price fold, raw pointers never escaping the type) links the M3 staticlib via a new `build.rs` (no in-repo precedent — `AIL_BIN` env override else nested `cargo build -p ail` against the parent workspace, separate target dir so no cargo-lock deadlock). Plus a hermetic `data-server` smoke: a synthetic Pepperstone-format ZIP fixture written via the crate's own public `RawTickRecord` type (correct-by-construction, mirrors `data-server/src/loader.rs:110-124`) → real `DataServer` → `Kernel`, asserting bit-exact vs a same-order host reference fold and the closed-form `55.0`; runs with no `/mnt`. `ail-embed` is its own cargo workspace root (empty `[workspace]` table) so the AILang compiler workspace owes it nothing; `data-server` is a dev-dependency only ⇒ Invariant 1 holds in the dependency graph, not just on paper. Boss-verified independently: ail-embed suite 2/2 green (`kernel_run_sums_prices` unit RED-first + `hermetic_smoke_data_server_roundtrip` integration), full+no-deps `cargo metadata` on the AILang workspace shows `data-server` count 0, `git status` path filter empty (zero diff to `crates/ailang-*`/`crates/ail/`/`runtime/`/`examples/*.ail`), `src/lib.rs` zero code-level `data_server`, AILang `cargo build --workspace` still clean. Two toolchain-forced corrections to the plan's verbatim `ail-embed/Cargo.toml` (added the empty `[workspace]` table; sibling dev-dep path `../libs`→`../../libs` manifest-relative) — confined to the plan-created manifest, no acceptance gate altered, 0 review re-loops; planner-recon implication recorded in the journal Concerns. Adapter API + thread-swarm explicitly deferred to M5 iter 2+. → 2026-05-19-iter-embedding-abi-m5.1.md
|
- 2026-05-19 — iter embedding-abi-m5.1 (DONE 3/3): M5 iter 1 — stood up the workspace-excluded `ail-embed` crate. A zero-dependency embedding core (Rust port of the audited `crates/ail/tests/embed/tick_roundtrip.c`: `extern "C"` to the M3-frozen ABI + frozen-layout `State`/`Tick` box helpers + a `Kernel` price fold, raw pointers never escaping the type) links the M3 staticlib via a new `build.rs` (no in-repo precedent — `AIL_BIN` env override else nested `cargo build -p ail` against the parent workspace, separate target dir so no cargo-lock deadlock). Plus a hermetic `data-server` smoke: a synthetic Pepperstone-format ZIP fixture written via the crate's own public `RawTickRecord` type (correct-by-construction, mirrors `data-server/src/loader.rs:110-124`) → real `DataServer` → `Kernel`, asserting bit-exact vs a same-order host reference fold and the closed-form `55.0`; runs with no `/mnt`. `ail-embed` is its own cargo workspace root (empty `[workspace]` table) so the AILang compiler workspace owes it nothing; `data-server` is a dev-dependency only ⇒ Invariant 1 holds in the dependency graph, not just on paper. Boss-verified independently: ail-embed suite 2/2 green (`kernel_run_sums_prices` unit RED-first + `hermetic_smoke_data_server_roundtrip` integration), full+no-deps `cargo metadata` on the AILang workspace shows `data-server` count 0, `git status` path filter empty (zero diff to `crates/ailang-*`/`crates/ail/`/`runtime/`/`examples/*.ail`), `src/lib.rs` zero code-level `data_server`, AILang `cargo build --workspace` still clean. Two toolchain-forced corrections to the plan's verbatim `ail-embed/Cargo.toml` (added the empty `[workspace]` table; sibling dev-dep path `../libs`→`../../libs` manifest-relative) — confined to the plan-created manifest, no acceptance gate altered, 0 review re-loops; planner-recon implication recorded in the journal Concerns. Adapter API + thread-swarm explicitly deferred to M5 iter 2+. → 2026-05-19-iter-embedding-abi-m5.1.md
|
||||||
|
- 2026-05-19 — iter embedding-abi-m5.2 (PARTIAL 2/3; M5 bounce-back): data-server adapter + symbol-fan swarm; the leak-proof did its job and surfaced a real concurrency finding. Tasks 1+2 landed clean: `data-server` promoted dev-dep→real dep (Invariant-1 sanctioned — compiler workspace graph still data-server-count 0, zero compiler-surface diff), additive `adapter` module (`tick_to_px`/`MidPriceStream` lazy `Iterator`/`fold_symbol`, RED-first), and a `swarm_runner` `[[bin]]` whose clean compile *is* the compile-time per-thread-ctx proof (`Ctx: !Send` ⇒ a shared-ctx swarm is `E0277`, cannot build). The real-data symbol-fan swarm runs (EURUSD/GER40/XAUUSD, ~4s) and is **bit-exact vs an independent same-order host reference every run** (`symbol_fan_swarm_bit_exact`, GREEN, live). Task 3's `Σallocs==Σfrees` leak gate is BLOCKED: Boss independently read `runtime/rc.c` and confirms it is an **instrumentation race, not a memory bug** — no box crosses a thread (`!Send`), the real RC is correct (bit-exact green), only the *global* `g_rc_*` stat counters (rc.c:90-91,161,212) are non-atomic *by rc.c's own documented single-threaded design* and lose `++`s when 3 worker threads hit the null-`__ail_tls_ctx` host-side path. M5 is AILang's first concurrent consumer → the deferred "concurrency arrived: atomic-vs-non-atomic" decision rc.c:44-49 names is now due. The leak assertion was Boss-split into `symbol_fan_swarm_leak_free` (`#[ignore]`, **body verbatim** — quarantined not weakened; un-ignore = the runtime fix's acceptance) so main stays green and the finding is pinned. Prior claim that `embed_swarm_tsan.rs` covers this was corrected (it uses the scalar kernel — zero box allocs — never exercised this path). Escalated as a **bounce-back** (touches M3-frozen `runtime/`; re-frames M5's "zero runtime change"; multiple substantive options); Boss recommendation on the record = Option A (atomic *global-fallback* counters only, standalone RED-first runtime micro-iter, M5 framing amended) — user picks the direction. Time-shard + friction-harvest remain m5.3. → 2026-05-19-iter-embedding-abi-m5.2.md
|
||||||
|
|||||||
Reference in New Issue
Block a user