b724cd17a1
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.
83 lines
2.4 KiB
Rust
83 lines
2.4 KiB
Rust
//! 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);
|
|
}
|
|
}
|