a4760497ec
Cycle 0001 of the walking-skeleton milestone: the aura-core data substrate
everything streams through.
- scalar.rs: the closed four-type set — Scalar (Copy POD carrier), ScalarKind
(the kind tag), Timestamp (i64 epoch-ns newtype). No boxed Value, no dyn Any,
no heap (C7).
- column.rs: Column<T>, a fixed-capacity bounded-lookback ring pre-sized at
construction (no hot-loop realloc, C8), financial-indexed (index 0 = newest),
carrying a monotonic run_count freshness primitive (C5). Window<'_,T> is its
zero-copy read view with newest-at-0 Index. No RefCell/Rc — a sim is
single-threaded (C1), so push is &mut and read is &.
- any.rs: AnyColumn, the type-erased edge over the four Column kinds. push()
kind-checks at the edge (wiring time) and rejects a wrong-kind value with
KindMismatch, leaving the column untouched; the hot path bypasses the check
via the monomorphic as_*_mut accessors (C7).
- error.rs: KindMismatch { expected, got }, the guard error.
Sharpens the RustAst rtl/ reference (VecDeque + RefCell + boxed Value) into a
fixed ring of Copy scalars with direct dispatch. Node trait, Ctx, firing
policies, and the sim loop are deferred to subsequent cycles.
Verified independently: cargo build --workspace (0 warnings), cargo test
--workspace (14 aura-core tests green incl. ring-wrap, run_count monotonicity,
zero-copy window, and the wrong_kind_push_is_rejected guard), cargo clippy
--workspace --all-targets -D warnings (clean), and a surface-purity grep
confirming no RefCell/Rc/dyn Any on the substrate.
refs #0001-core-streaming-substrate
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
177 lines
5.2 KiB
Rust
177 lines
5.2 KiB
Rust
//! `Column<T>` — a fixed-capacity, bounded-lookback SoA ring with financial
|
|
//! indexing (index 0 = newest), and its zero-copy read view `Window` (C5/C8).
|
|
|
|
/// A fixed-capacity bounded-lookback column. Pre-sized at construction
|
|
/// (C8: no realloc in the hot loop); index 0 = newest; carries a monotonic
|
|
/// `run_count` of total pushes — the freshness primitive (C5).
|
|
pub struct Column<T> {
|
|
buf: Box<[T]>,
|
|
head: usize, // next write slot
|
|
len: usize, // valid elements, saturates at capacity
|
|
run_count: u64, // monotonic total pushes (C5)
|
|
}
|
|
|
|
impl<T: Copy + Default> Column<T> {
|
|
/// Create a column sized to `lookback` (must be >= 1). The engine sizes this
|
|
/// once at wiring; it is never reallocated (C8).
|
|
pub fn with_capacity(lookback: usize) -> Self {
|
|
assert!(lookback >= 1, "Column lookback must be >= 1");
|
|
Column {
|
|
buf: vec![T::default(); lookback].into_boxed_slice(),
|
|
head: 0,
|
|
len: 0,
|
|
run_count: 0,
|
|
}
|
|
}
|
|
|
|
/// Append the newest value. O(1), allocation-free. On overflow the oldest
|
|
/// value is overwritten (true ring). Bumps `run_count`.
|
|
pub fn push(&mut self, v: T) {
|
|
let cap = self.buf.len();
|
|
self.buf[self.head] = v;
|
|
self.head = (self.head + 1) % cap;
|
|
if self.len < cap {
|
|
self.len += 1;
|
|
}
|
|
self.run_count += 1;
|
|
}
|
|
|
|
/// Financial index: 0 = newest, `len()-1` = oldest. `None` if `k >= len`
|
|
/// (C8: not-yet-warmed-up / out of range). Zero-copy.
|
|
pub fn get(&self, k: usize) -> Option<T> {
|
|
if k >= self.len {
|
|
return None;
|
|
}
|
|
let cap = self.buf.len();
|
|
Some(self.buf[(self.head + cap - 1 - k) % cap])
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.len
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.len == 0
|
|
}
|
|
|
|
pub fn capacity(&self) -> usize {
|
|
self.buf.len()
|
|
}
|
|
|
|
/// The monotonic push counter — the freshness primitive (C5). Never moved
|
|
/// by a read.
|
|
pub fn run_count(&self) -> u64 {
|
|
self.run_count
|
|
}
|
|
|
|
/// A zero-copy read view handed to a reading node (C8).
|
|
pub fn window(&self) -> Window<'_, T> {
|
|
Window { col: self }
|
|
}
|
|
}
|
|
|
|
/// Read-only, zero-copy view into a [`Column`] — what a reading node sees (C8).
|
|
/// Carries no storage, only a borrow.
|
|
pub struct Window<'a, T> {
|
|
col: &'a Column<T>,
|
|
}
|
|
|
|
impl<'a, T: Copy + Default> Window<'a, T> {
|
|
/// Financial index, 0 = newest; `None` if cold / out of range.
|
|
pub fn get(&self, k: usize) -> Option<T> {
|
|
self.col.get(k)
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.col.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.col.is_empty()
|
|
}
|
|
|
|
pub fn run_count(&self) -> u64 {
|
|
self.col.run_count()
|
|
}
|
|
}
|
|
|
|
/// `window[k]` — newest-at-0 indexed access; panics on out-of-range like a
|
|
/// slice. Nodes that may be cold use [`Window::get`] instead.
|
|
impl<'a, T: Copy + Default> core::ops::Index<usize> for Window<'a, T> {
|
|
type Output = T;
|
|
|
|
fn index(&self, k: usize) -> &T {
|
|
let col = self.col;
|
|
assert!(k < col.len, "Window index {k} out of range (len {})", col.len);
|
|
let cap = col.buf.len();
|
|
&col.buf[(col.head + cap - 1 - k) % cap]
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn newest_at_zero_oldest_at_len_minus_one() {
|
|
let mut c = Column::<i64>::with_capacity(4);
|
|
c.push(10);
|
|
c.push(20);
|
|
c.push(30);
|
|
assert_eq!(c.len(), 3);
|
|
assert_eq!(c.get(0), Some(30)); // newest
|
|
assert_eq!(c.get(1), Some(20));
|
|
assert_eq!(c.get(2), Some(10)); // oldest
|
|
assert_eq!(c.get(3), None); // beyond len
|
|
}
|
|
|
|
#[test]
|
|
fn wraparound_keeps_newest_capacity_values() {
|
|
let mut c = Column::<i64>::with_capacity(3);
|
|
for v in [1, 2, 3, 4, 5] {
|
|
c.push(v);
|
|
}
|
|
assert_eq!(c.len(), 3); // saturated
|
|
assert_eq!(c.capacity(), 3);
|
|
assert_eq!(c.get(0), Some(5)); // newest
|
|
assert_eq!(c.get(1), Some(4));
|
|
assert_eq!(c.get(2), Some(3)); // oldest retained
|
|
assert_eq!(c.get(3), None); // 1 and 2 were dropped
|
|
}
|
|
|
|
#[test]
|
|
fn run_count_counts_pushes_not_len() {
|
|
let mut c = Column::<i64>::with_capacity(2);
|
|
assert_eq!(c.run_count(), 0);
|
|
for v in [1, 2, 3, 4, 5] {
|
|
c.push(v);
|
|
}
|
|
assert_eq!(c.run_count(), 5); // counts pushes, past the ring wrap
|
|
assert_eq!(c.len(), 2); // while len stays at capacity
|
|
let _ = c.get(0);
|
|
assert_eq!(c.run_count(), 5); // a read never moves it
|
|
}
|
|
|
|
#[test]
|
|
fn window_is_a_view_matching_the_column() {
|
|
let mut c = Column::<f64>::with_capacity(4);
|
|
c.push(1.0);
|
|
c.push(2.0);
|
|
let cap_before = c.capacity();
|
|
let w = c.window();
|
|
assert_eq!(w.len(), 2);
|
|
assert_eq!(w.get(0), Some(2.0));
|
|
assert_eq!(w[1], 1.0); // Index, newest-at-0
|
|
assert_eq!(w.run_count(), 2);
|
|
assert_eq!(c.capacity(), cap_before); // taking a window did not realloc
|
|
}
|
|
|
|
#[test]
|
|
fn empty_column_reads_none() {
|
|
let c = Column::<bool>::with_capacity(2);
|
|
assert!(c.is_empty());
|
|
assert_eq!(c.get(0), None);
|
|
assert_eq!(c.run_count(), 0);
|
|
}
|
|
}
|