Files
Aura/crates/aura-std/src/sub.rs
T
Brummel dd5c3fad96 feat(engine): the deterministic single-source sim loop
Cycle 0003: aura-engine's first real content — a Sim that runs a wired DAG of
nodes deterministically, cycle by cycle. First point in the project where
authored nodes execute against data, not just under a hand-fed Ctx.

- `Sim` (aura-engine) — a bootstrapped, frozen root graph: a flat Vec<NodeBox>
  (each node owns its input columns, the cycle-0002 shape) + an index edge
  table, topologically ordered (Kahn). `bootstrap` sizes every input column from
  its node's schema, kind-checks each edge and source target, and rejects
  directed cycles — C7's "type check paid once at wiring" generalized to the
  whole topology (`BootstrapError::{KindMismatch, BadIndex, Cycle}`).
- `run` — the deterministic loop: per record, forward the source value into its
  target slots, evaluate nodes in topo order, capture the observed node's output
  at eval time, and forward each `Some` output into its consumers' input columns
  (a `None` forwards nothing — the structural seed of sample-and-hold). The loop
  destructures `&mut self` into disjoint field borrows and allocates nothing on
  the per-cycle path. This is the flat, monomorphized sharpening of RustAst's
  reference-counted, interior-mutable observer push graph (C1/C7).
- `Edge` / `Target` (aura-engine) — producer->consumer and source->consumer wiring.
- `Sub` (aura-std) — a 2-input f64-difference node, so the loop is proven on a
  real fan-out + join DAG (source -> {SMA(2), SMA(4)} -> Sub), with a determinism
  assertion (C1: a second identical run is bit-identical).

Engine library depends only on aura-core; a test-only dev-dependency on aura-std
lets the integration test wire real Sma/Sub nodes. Sim carries a hand-written,
node-opaque `Debug` impl (Box<dyn Node> is not Debug; the impl prints
nodes.len() + the index/topology fields, needed by the bootstrap-rejection tests'
`unwrap_err`).

Deliberately deferred (recorded as decisions): freshness-gated recompute /
sample-and-hold (C5 -> cycle 0004, with the second source that makes it testable);
the explicit monotonic cycle_id counter (its first reader is freshness, 0004);
the Source trait + data-server ingestion + k-way merge (C3/C11); the builder API
(C19); the real sink + run registry (C18/C22).

Gates green: cargo build/test (26: 18 aura-core + 3 aura-std + 5 aura-engine)/
clippy -D warnings clean; surface-purity grep (no dyn-Any / Rc / RefCell) clean.

refs walking-skeleton
2026-06-03 13:02:47 +02:00

63 lines
1.7 KiB
Rust

//! `Sub` — two-input f64 difference (input 0 minus input 1), e.g. a fast/slow
//! spread. The walking skeleton's second worked node: it gives the sim loop a
//! real fan-out + join to run (two SMAs joining into one node), exercising
//! multi-input `Ctx` access inside a running graph.
use aura_core::{Ctx, InputSpec, Node, NodeSchema, Scalar, ScalarKind};
/// Two-input f64 difference: input 0 minus input 1. Emits `None` until both
/// inputs have a value.
#[derive(Default)]
pub struct Sub;
impl Sub {
/// Build a `Sub` node.
pub fn new() -> Self {
Self
}
}
impl Node for Sub {
fn schema(&self) -> NodeSchema {
NodeSchema {
inputs: vec![
InputSpec { kind: ScalarKind::F64, lookback: 1 },
InputSpec { kind: ScalarKind::F64, lookback: 1 },
],
output: ScalarKind::F64,
}
}
fn eval(&mut self, ctx: Ctx<'_>) -> Option<Scalar> {
let a = ctx.f64_in(0);
let b = ctx.f64_in(1);
if a.is_empty() || b.is_empty() {
return None;
}
Some(Scalar::F64(a[0] - b[0]))
}
}
#[cfg(test)]
mod tests {
use super::*;
use aura_core::AnyColumn;
#[test]
fn sub_is_difference_once_both_inputs_present() {
let mut sub = Sub::new();
let mut inputs = vec![
AnyColumn::with_capacity(ScalarKind::F64, 1),
AnyColumn::with_capacity(ScalarKind::F64, 1),
];
// only input 0 present -> None
inputs[0].push(Scalar::F64(10.0)).unwrap();
assert_eq!(sub.eval(Ctx::new(&inputs)), None);
// both present -> a - b
inputs[1].push(Scalar::F64(4.0)).unwrap();
assert_eq!(sub.eval(Ctx::new(&inputs)), Some(Scalar::F64(6.0)));
}
}