77a1d26017
Cycle 0002, the second walking-skeleton slice on top of the 0001 substrate:
the interface every node forever implements (C8), proven end-to-end by a real
node with no engine present.
- `Node` (aura-core) — `schema() -> NodeSchema` declares inputs (kind +
lookback) and the single output kind; `eval(&mut self, Ctx) -> Option<Scalar>`
computes one cycle's output (`None` = filter / not-yet-warmed-up). `&mut self`
so a node may keep its own derived state.
- `Ctx<'a>` (aura-core) — a `Copy` borrow-wrapper handing `eval` zero-copy,
financial-indexed `Window`s per input (`ctx.f64_in(i)[k]`, index 0 = newest).
A kind mismatch panics ("engine bug") — wiring guarantees the kind, so a
mismatch can only mean the wiring layer is broken; this keeps node-author code
clean (`w[k]`, not `w?[k]`).
- `AnyColumn::as_f64/as_i64/as_bool/as_ts` (aura-core) — the read-side mirror of
the existing `as_*_mut`, the mechanism `Ctx` uses to get a typed window from a
type-erased edge. Closes the read-side gap the cycle-0001 audit recorded.
- `Sma` (aura-std) — the skeleton's first real block: a producer node computing
the moving mean of one f64 input, emitting `None` until warmed up. Authored in
a downstream crate (proving aura-core's contract is usable across the crate
boundary) and driven by a hand-written test that mimics exactly what the sim
loop will later generalize: push fresh input, eval, collect.
Deliberately deferred (spec 0002 "Out of scope", recorded as decisions): the
firing policies (C6 — InputSpec carries no firing field yet), the sim loop (C4)
and freshness gating (C5), schema-level tunable params (C12/C19), and the
no-output sink refinement (C8 consumer side).
Gates green: cargo build/test (20: 18 aura-core + 2 aura-std)/clippy -D warnings
all clean; surface-purity grep (no dyn-Any / Rc / RefCell) clean.
refs walking-skeleton
215 lines
6.8 KiB
Rust
215 lines
6.8 KiB
Rust
//! `AnyColumn` — the type-erased edge: the four concrete `Column<T>` behind one
|
|
//! enum. The kind check is paid at the edge (wiring time); the hot loop grabs
|
|
//! the concrete column once (`as_*_mut`) and pushes monomorphically (C7).
|
|
|
|
use crate::column::Column;
|
|
use crate::error::KindMismatch;
|
|
use crate::scalar::{Scalar, ScalarKind, Timestamp};
|
|
|
|
/// A type-erased edge over the four scalar columns (C7).
|
|
pub enum AnyColumn {
|
|
I64(Column<i64>),
|
|
F64(Column<f64>),
|
|
Bool(Column<bool>),
|
|
Ts(Column<Timestamp>),
|
|
}
|
|
|
|
impl AnyColumn {
|
|
/// Create a type-erased column of `kind`, sized to `lookback` (C8).
|
|
pub fn with_capacity(kind: ScalarKind, lookback: usize) -> Self {
|
|
match kind {
|
|
ScalarKind::I64 => AnyColumn::I64(Column::with_capacity(lookback)),
|
|
ScalarKind::F64 => AnyColumn::F64(Column::with_capacity(lookback)),
|
|
ScalarKind::Bool => AnyColumn::Bool(Column::with_capacity(lookback)),
|
|
ScalarKind::Timestamp => AnyColumn::Ts(Column::with_capacity(lookback)),
|
|
}
|
|
}
|
|
|
|
pub fn kind(&self) -> ScalarKind {
|
|
match self {
|
|
AnyColumn::I64(_) => ScalarKind::I64,
|
|
AnyColumn::F64(_) => ScalarKind::F64,
|
|
AnyColumn::Bool(_) => ScalarKind::Bool,
|
|
AnyColumn::Ts(_) => ScalarKind::Timestamp,
|
|
}
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
match self {
|
|
AnyColumn::I64(c) => c.len(),
|
|
AnyColumn::F64(c) => c.len(),
|
|
AnyColumn::Bool(c) => c.len(),
|
|
AnyColumn::Ts(c) => c.len(),
|
|
}
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.len() == 0
|
|
}
|
|
|
|
pub fn run_count(&self) -> u64 {
|
|
match self {
|
|
AnyColumn::I64(c) => c.run_count(),
|
|
AnyColumn::F64(c) => c.run_count(),
|
|
AnyColumn::Bool(c) => c.run_count(),
|
|
AnyColumn::Ts(c) => c.run_count(),
|
|
}
|
|
}
|
|
|
|
/// Edge-time typed push. `Err(KindMismatch)` if `v`'s kind != this column's
|
|
/// kind; the column is left untouched (C7 guard). The hot path bypasses this
|
|
/// by taking the concrete `Column<T>` once via `as_*_mut`.
|
|
pub fn push(&mut self, v: Scalar) -> Result<(), KindMismatch> {
|
|
match (self, v) {
|
|
(AnyColumn::I64(c), Scalar::I64(x)) => {
|
|
c.push(x);
|
|
Ok(())
|
|
}
|
|
(AnyColumn::F64(c), Scalar::F64(x)) => {
|
|
c.push(x);
|
|
Ok(())
|
|
}
|
|
(AnyColumn::Bool(c), Scalar::Bool(x)) => {
|
|
c.push(x);
|
|
Ok(())
|
|
}
|
|
(AnyColumn::Ts(c), Scalar::Ts(x)) => {
|
|
c.push(x);
|
|
Ok(())
|
|
}
|
|
(this, v) => Err(KindMismatch { expected: this.kind(), got: v.kind() }),
|
|
}
|
|
}
|
|
|
|
/// Read one type-erased value (0 = newest). `None` if cold / out of range.
|
|
pub fn get(&self, k: usize) -> Option<Scalar> {
|
|
match self {
|
|
AnyColumn::I64(c) => c.get(k).map(Scalar::I64),
|
|
AnyColumn::F64(c) => c.get(k).map(Scalar::F64),
|
|
AnyColumn::Bool(c) => c.get(k).map(Scalar::Bool),
|
|
AnyColumn::Ts(c) => c.get(k).map(Scalar::Ts),
|
|
}
|
|
}
|
|
|
|
/// Concrete-column accessor for the monomorphic hot path; `Some` only for
|
|
/// the matching kind.
|
|
pub fn as_i64_mut(&mut self) -> Option<&mut Column<i64>> {
|
|
match self {
|
|
AnyColumn::I64(c) => Some(c),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn as_f64_mut(&mut self) -> Option<&mut Column<f64>> {
|
|
match self {
|
|
AnyColumn::F64(c) => Some(c),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn as_bool_mut(&mut self) -> Option<&mut Column<bool>> {
|
|
match self {
|
|
AnyColumn::Bool(c) => Some(c),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn as_ts_mut(&mut self) -> Option<&mut Column<Timestamp>> {
|
|
match self {
|
|
AnyColumn::Ts(c) => Some(c),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Read-side concrete-column accessor (the symmetric partner of
|
|
/// `as_*_mut`); `Some` only for the matching kind. `Ctx` uses these to hand
|
|
/// a typed window from a type-erased edge.
|
|
pub fn as_i64(&self) -> Option<&Column<i64>> {
|
|
match self {
|
|
AnyColumn::I64(c) => Some(c),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn as_f64(&self) -> Option<&Column<f64>> {
|
|
match self {
|
|
AnyColumn::F64(c) => Some(c),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn as_bool(&self) -> Option<&Column<bool>> {
|
|
match self {
|
|
AnyColumn::Bool(c) => Some(c),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn as_ts(&self) -> Option<&Column<Timestamp>> {
|
|
match self {
|
|
AnyColumn::Ts(c) => Some(c),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn same_kind_push_round_trips() {
|
|
let mut e = AnyColumn::with_capacity(ScalarKind::F64, 4);
|
|
e.push(Scalar::F64(1.5)).unwrap();
|
|
e.push(Scalar::F64(2.5)).unwrap();
|
|
assert_eq!(e.kind(), ScalarKind::F64);
|
|
assert_eq!(e.len(), 2);
|
|
assert_eq!(e.run_count(), 2);
|
|
assert_eq!(e.get(0), Some(Scalar::F64(2.5))); // newest
|
|
assert_eq!(e.get(1), Some(Scalar::F64(1.5)));
|
|
assert_eq!(e.get(2), None);
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_kind_push_is_rejected() {
|
|
let mut edge = AnyColumn::with_capacity(ScalarKind::I64, 8);
|
|
let err = edge.push(Scalar::F64(1.5)).unwrap_err(); // f64 into an i64 edge
|
|
assert_eq!(err, KindMismatch { expected: ScalarKind::I64, got: ScalarKind::F64 });
|
|
assert_eq!(edge.len(), 0); // nothing was stored
|
|
assert_eq!(edge.run_count(), 0); // and the counter did not move
|
|
}
|
|
|
|
#[test]
|
|
fn concrete_accessor_matches_only_its_kind() {
|
|
let mut e = AnyColumn::with_capacity(ScalarKind::F64, 2);
|
|
assert!(e.as_f64_mut().is_some());
|
|
assert!(e.as_i64_mut().is_none());
|
|
assert!(e.as_bool_mut().is_none());
|
|
assert!(e.as_ts_mut().is_none());
|
|
// hot-path push through the concrete column, monomorphic:
|
|
e.as_f64_mut().unwrap().push(3.0);
|
|
assert_eq!(e.get(0), Some(Scalar::F64(3.0)));
|
|
assert_eq!(e.run_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn read_accessor_matches_only_its_kind() {
|
|
let f = AnyColumn::with_capacity(ScalarKind::F64, 2);
|
|
assert!(f.as_f64().is_some());
|
|
assert!(f.as_i64().is_none());
|
|
assert!(f.as_bool().is_none());
|
|
assert!(f.as_ts().is_none());
|
|
|
|
let i = AnyColumn::with_capacity(ScalarKind::I64, 2);
|
|
assert!(i.as_i64().is_some());
|
|
assert!(i.as_f64().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn timestamp_edge_round_trips() {
|
|
let mut e = AnyColumn::with_capacity(ScalarKind::Timestamp, 2);
|
|
e.push(Scalar::Ts(Timestamp(1_700_000_000_000_000_000))).unwrap();
|
|
assert_eq!(e.get(0), Some(Scalar::Ts(Timestamp(1_700_000_000_000_000_000))));
|
|
}
|
|
}
|