Files
Aura/crates/aura-std/src/when.rs
T
claude a32dc38d18 feat(std): meaning lines for all 27 shipped node schemas
C29 compile/unit seam, task 3 of the self-description plan: every
aura-std NodeSchema literal threads its one-line doc. Four texts were
corrected against the actual eval/finalize semantics rather than taken
from the plan table verbatim: Delay is a lag-N register (not one-step),
GatedRecorder flushes an ungated final row at finalize, Latch is a
level-sensitive set/reset register (captures no input), SeriesReducer
emits its single summary row at finalize (not per cycle).

Gate: cargo build -p aura-std --lib clean; full std test run follows at
the all-crates gate once strategy/backtest/engine thread their sites
(the earlier per-crate test gate was unsatisfiable in isolation --
std's dev-deps pull crates whose sites belong to later tasks).

refs #316
2026-07-23 15:25:16 +02:00

141 lines
5.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `When` — the clock-enable driver `f64 × bool -> f64`: forwards `value`
//! verbatim when `gate` is true, and is **quiet** (`eval -> None`, the
//! `Resample` precedent) when it is false.
//!
//! The single clock-enable standard cell of the RTL metaphor (#281): gating
//! belongs in the *stream*, not in per-node gated variants. Because the run
//! loop is freshness-gated, every existing stateful reducer composes gated,
//! unmodified: `SMA(When(x, gate), N)` IS the gated SMA — under
//! `Firing::Any` a consumer holds its last forwarded value across quiet
//! cycles and its state advances per *firing*, not per cycle. A quiet
//! `When` feeding a `Firing::Barrier(N)` group means the group does not
//! fire that cycle — the quiet member is not at the current timestamp,
//! which is the correct reading of the barrier contract (deliberately
//! pinned by an engine test).
//!
//! Two inputs (`value: F64`, `gate: Bool`, each `Firing::Any` — the enable
//! is level-sensitive: the gate *level* is sampled whenever the cell fires,
//! however long ago it was driven). One `f64` output, no params, stateless.
//! Emits `None` while either leg is cold (warm-up), and on every gate-false
//! cycle (the quiet cycle that IS the semantics).
use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};
/// Clock-enable forwarder: `value` iff `gate`, else quiet (`None`). A false
/// gate is a quiet cycle, not a held or zero output — downstream freshness
/// does not advance.
pub struct When {
out: [Cell; 1],
}
impl When {
/// Build a `When` node.
pub fn new() -> Self {
Self { out: [Cell::from_f64(0.0)] }
}
/// The param-generic recipe for a blueprint primitive: paramless, builds
/// through `When::new`.
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"When",
NodeSchema {
inputs: vec![
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "value".into() },
PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "gate".into() },
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "emits the input only on cycles where the condition input is true",
},
|_| Box::new(When::new()),
)
}
}
impl Default for When {
fn default() -> Self {
Self::new()
}
}
impl Node for When {
fn lookbacks(&self) -> Vec<usize> {
vec![1, 1]
}
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
let value = ctx.f64_in(0);
let gate = ctx.bool_in(1);
if value.is_empty() || gate.is_empty() {
return None; // cold warm-up (C8) — both legs required
}
if !gate[0] {
return None; // gate low -> QUIET cycle (the semantics, not warm-up)
}
self.out[0] = Cell::from_f64(value[0]);
Some(&self.out)
}
fn label(&self) -> String {
"When".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use aura_core::{AnyColumn, Scalar, Timestamp};
fn gate_inputs() -> Vec<AnyColumn> {
vec![
AnyColumn::with_capacity(ScalarKind::F64, 1),
AnyColumn::with_capacity(ScalarKind::Bool, 1),
]
}
#[test]
fn when_forwards_on_true_and_is_quiet_on_false() {
// PROPERTY: gate true -> the value passes through verbatim (bit-equal);
// gate false -> a QUIET cycle (None), never a held or zero output.
let mut node = When::new();
let mut inputs = gate_inputs();
inputs[0].push(Scalar::f64(42.5)).unwrap();
inputs[1].push(Scalar::bool(true)).unwrap();
assert_eq!(
node.eval(Ctx::new(&inputs, Timestamp(0))),
Some([Cell::from_f64(42.5)].as_slice())
);
inputs[0].push(Scalar::f64(7.0)).unwrap();
inputs[1].push(Scalar::bool(false)).unwrap();
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None);
inputs[0].push(Scalar::f64(-3.25)).unwrap();
inputs[1].push(Scalar::bool(true)).unwrap();
assert_eq!(
node.eval(Ctx::new(&inputs, Timestamp(0))),
Some([Cell::from_f64(-3.25)].as_slice())
);
}
#[test]
fn when_is_none_while_either_leg_is_cold() {
// Cold warm-up is distinct from the quiet cycle: before both legs have
// seen a value the cell is cold (C8), regardless of the gate.
let mut node = When::new();
let mut inputs = gate_inputs();
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); // both cold
inputs[0].push(Scalar::f64(1.0)).unwrap();
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); // gate cold
}
#[test]
fn input_slots_are_named_value_gate() {
let names: Vec<String> =
When::builder().schema().inputs.iter().map(|p| p.name.clone()).collect();
assert_eq!(names, ["value", "gate"]);
}
}