diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 86df3f6..3257ff8 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -41,7 +41,7 @@ fn synthetic_prices() -> Vec<(Timestamp, Scalar)> { (7, 0.9990), ] .iter() - .map(|&(t, p)| (Timestamp(t), Scalar::F64(p))) + .map(|&(t, p)| (Timestamp(t), Scalar::f64(p))) .collect() } @@ -58,7 +58,7 @@ fn showcase_prices() -> Vec<(Timestamp, Scalar)> { ] .iter() .enumerate() - .map(|(i, &p)| (Timestamp(i as i64 + 1), Scalar::F64(p))) + .map(|(i, &p)| (Timestamp(i as i64 + 1), Scalar::f64(p))) .collect() } @@ -163,9 +163,9 @@ fn run_sample() -> RunReport { RunReport { manifest: sim_optimal_manifest( vec![ - ("sma_fast".to_string(), Scalar::F64(2.0)), - ("sma_slow".to_string(), Scalar::F64(4.0)), - ("exposure_scale".to_string(), Scalar::F64(0.5)), + ("sma_fast".to_string(), Scalar::f64(2.0)), + ("sma_slow".to_string(), Scalar::f64(4.0)), + ("exposure_scale".to_string(), Scalar::f64(0.5)), ], window, 0, @@ -226,9 +226,9 @@ fn run_sample_real(symbol: &str, from_ms: Option, to_ms: Option) -> Ru RunReport { manifest: sim_optimal_manifest( vec![ - ("sma_fast".to_string(), Scalar::F64(2.0)), - ("sma_slow".to_string(), Scalar::F64(4.0)), - ("exposure_scale".to_string(), Scalar::F64(0.5)), + ("sma_fast".to_string(), Scalar::f64(2.0)), + ("sma_slow".to_string(), Scalar::f64(4.0)), + ("exposure_scale".to_string(), Scalar::f64(0.5)), ], window, 0, @@ -295,7 +295,7 @@ fn signals(name: &str) -> Composite { let blend = g.add( LinComb::builder(3) .named("blend") - .bind("weights[2]", Scalar::F64(0.5)), + .bind("weights[2]", Scalar::f64(0.5)), ); let price = g.input_role("price"); g.feed(price, [trend.input("price"), momentum.input("price")]); @@ -351,9 +351,9 @@ fn sample_blueprint() -> Composite { /// Coerce a sweep point's `Scalar` value to the manifest's `f64` param type. /// A tuning grid carries only numeric params (`I64` lengths, `F64` scales). fn scalar_as_param_f64(s: &Scalar) -> f64 { - match s { - Scalar::I64(n) => *n as f64, - Scalar::F64(f) => *f, + match s.kind() { + ScalarKind::I64 => s.as_i64() as f64, + ScalarKind::F64 => s.as_f64(), other => unreachable!("non-numeric sweep param: {other:?}"), } } @@ -600,9 +600,9 @@ fn mc_family() -> McFamily { RunReport { manifest: sim_optimal_manifest( vec![ - ("sma_fast".to_string(), Scalar::F64(2.0)), - ("sma_slow".to_string(), Scalar::F64(4.0)), - ("exposure_scale".to_string(), Scalar::F64(0.5)), + ("sma_fast".to_string(), Scalar::f64(2.0)), + ("sma_slow".to_string(), Scalar::f64(4.0)), + ("exposure_scale".to_string(), Scalar::f64(0.5)), ], window, seed, @@ -813,7 +813,7 @@ fn macd_blueprint() -> Composite { /// windows so the 7-tick synthetic stream still produces a non-trivial trace /// (conventional MACD is 12/26/9, meaningless on 7 points). fn macd_point() -> Vec { - vec![Scalar::I64(2), Scalar::I64(4), Scalar::I64(3), Scalar::F64(0.5)] + vec![Scalar::i64(2), Scalar::i64(4), Scalar::i64(3), Scalar::f64(0.5)] } /// A longer synthetic stream than the SMA sample's 7 ticks: MACD's EMAs each warm @@ -826,7 +826,7 @@ fn macd_prices() -> Vec<(Timestamp, Scalar)> { ] .iter() .enumerate() - .map(|(i, &p)| (Timestamp(i as i64 + 1), Scalar::F64(p))) + .map(|(i, &p)| (Timestamp(i as i64 + 1), Scalar::f64(p))) .collect() } @@ -856,10 +856,10 @@ fn run_macd() -> RunReport { RunReport { manifest: sim_optimal_manifest( vec![ - ("ema_fast".to_string(), Scalar::F64(2.0)), - ("ema_slow".to_string(), Scalar::F64(4.0)), - ("ema_signal".to_string(), Scalar::F64(3.0)), - ("exposure_scale".to_string(), Scalar::F64(0.5)), + ("ema_fast".to_string(), Scalar::f64(2.0)), + ("ema_slow".to_string(), Scalar::f64(4.0)), + ("ema_signal".to_string(), Scalar::f64(3.0)), + ("exposure_scale".to_string(), Scalar::f64(0.5)), ], window, 0, @@ -957,9 +957,9 @@ mod tests { let report = RunReport { manifest: sim_optimal_manifest( vec![ - ("sma_fast".to_string(), Scalar::F64(2.0)), - ("sma_slow".to_string(), Scalar::F64(4.0)), - ("exposure_scale".to_string(), Scalar::F64(0.5)), + ("sma_fast".to_string(), Scalar::f64(2.0)), + ("sma_slow".to_string(), Scalar::f64(4.0)), + ("exposure_scale".to_string(), Scalar::f64(0.5)), ], window, seed, diff --git a/crates/aura-core/src/any.rs b/crates/aura-core/src/any.rs index 84a9d70..adacb7e 100644 --- a/crates/aura-core/src/any.rs +++ b/crates/aura-core/src/any.rs @@ -60,34 +60,34 @@ impl AnyColumn { /// kind; the column is left untouched (C7 guard). The hot path bypasses this /// by taking the concrete `Column` 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); + match (self, v.kind()) { + (AnyColumn::I64(c), ScalarKind::I64) => { + c.push(v.as_i64()); Ok(()) } - (AnyColumn::F64(c), Scalar::F64(x)) => { - c.push(x); + (AnyColumn::F64(c), ScalarKind::F64) => { + c.push(v.as_f64()); Ok(()) } - (AnyColumn::Bool(c), Scalar::Bool(x)) => { - c.push(x); + (AnyColumn::Bool(c), ScalarKind::Bool) => { + c.push(v.as_bool()); Ok(()) } - (AnyColumn::Ts(c), Scalar::Ts(x)) => { - c.push(x); + (AnyColumn::Ts(c), ScalarKind::Timestamp) => { + c.push(v.as_ts()); Ok(()) } - (this, v) => Err(KindMismatch { expected: this.kind(), got: v.kind() }), + (this, got) => Err(KindMismatch { expected: this.kind(), got }), } } /// Read one type-erased value (0 = newest). `None` if cold / out of range. pub fn get(&self, k: usize) -> Option { 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), + 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), } } @@ -160,20 +160,20 @@ mod tests { #[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(); + 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(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 + 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 @@ -188,7 +188,7 @@ mod tests { 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.get(0), Some(Scalar::f64(3.0))); assert_eq!(e.run_count(), 1); } @@ -208,7 +208,7 @@ mod tests { #[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)))); + 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)))); } } diff --git a/crates/aura-core/src/cell.rs b/crates/aura-core/src/cell.rs new file mode 100644 index 0000000..3d89e8b --- /dev/null +++ b/crates/aura-core/src/cell.rs @@ -0,0 +1,60 @@ +//! `Cell` — a type-erased 64-bit word holding one scalar value with its kind +//! stripped away (C7). The type lives in the schema / column / port, never in +//! the value, so a cell is read back only by naming its type via a typed +//! accessor. `Scalar` is built on top of this; `Cell` itself knows nothing of +//! `Scalar` or `ScalarKind`. + +use crate::scalar::Timestamp; + +/// A type-erased 64-bit cell: the raw storage of one scalar value with its kind +/// stripped away. The bits are meaningless on their own — the type lives in the +/// schema/column/port, not in the value (C7), so the word is recovered **only** +/// by naming the type at the call site via a typed accessor ([`Cell::i64`], +/// [`Cell::f64`], [`Cell::bool`], [`Cell::ts`]). Each reinterprets the word with +/// no tag to check and therefore no branch — the caller's choice of accessor +/// *is* the type context the hot path already holds. The inner word is private; +/// there is no kind-free way to read it. +/// +/// All four base types fit one 64-bit word: `i64`/`Timestamp`/`bool` reuse the +/// same integer word, `f64` via its IEEE-754 bit pattern. `Eq`/`Hash` are +/// bit-exact (so `+0.0`/`-0.0` differ and a `NaN` bit pattern equals itself) — +/// the semantics of a raw word, not of a number. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Cell(u64); + +impl Cell { + /// Store an `i64` (identity bit-cast). + pub fn from_i64(v: i64) -> Self { + Cell(v as u64) + } + /// Store an `f64` as its IEEE-754 bit pattern. + pub fn from_f64(v: f64) -> Self { + Cell(v.to_bits()) + } + /// Store a `bool` as `0`/`1`. + pub fn from_bool(v: bool) -> Self { + Cell(v as u64) + } + /// Store a [`Timestamp`] (its `i64` epoch-ns, bit-cast). + pub fn from_ts(v: Timestamp) -> Self { + Cell(v.0 as u64) + } + + /// Read the word as `i64` (identity bit-cast). The caller asserts the type + /// by choosing this accessor; there is no tag to check, hence no branch. + pub fn i64(self) -> i64 { + self.0 as i64 + } + /// Read the word as `f64` from its IEEE-754 bit pattern. Branch-free. + pub fn f64(self) -> f64 { + f64::from_bits(self.0) + } + /// Read the word as `bool` (any non-zero word is `true`). Branch-free. + pub fn bool(self) -> bool { + self.0 != 0 + } + /// Read the word as a [`Timestamp`] (identity bit-cast). Branch-free. + pub fn ts(self) -> Timestamp { + Timestamp(self.0 as i64) + } +} diff --git a/crates/aura-core/src/ctx.rs b/crates/aura-core/src/ctx.rs index d855691..78b8c9b 100644 --- a/crates/aura-core/src/ctx.rs +++ b/crates/aura-core/src/ctx.rs @@ -76,7 +76,7 @@ mod tests { fn ctx_hands_financial_indexed_windows() { let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 4)]; for v in [10.0_f64, 20.0, 30.0] { - inputs[0].push(Scalar::F64(v)).unwrap(); + inputs[0].push(Scalar::f64(v)).unwrap(); } let ctx = Ctx::new(&inputs, Timestamp(0)); let w = ctx.f64_in(0); @@ -91,8 +91,8 @@ mod tests { AnyColumn::with_capacity(ScalarKind::F64, 2), AnyColumn::with_capacity(ScalarKind::I64, 2), ]; - inputs[0].push(Scalar::F64(1.5)).unwrap(); - inputs[1].push(Scalar::I64(42)).unwrap(); + inputs[0].push(Scalar::f64(1.5)).unwrap(); + inputs[1].push(Scalar::i64(42)).unwrap(); let ctx = Ctx::new(&inputs, Timestamp(0)); assert_eq!(ctx.f64_in(0)[0], 1.5); assert_eq!(ctx.i64_in(1)[0], 42); @@ -102,7 +102,7 @@ mod tests { #[should_panic(expected = "engine bug")] fn ctx_panics_on_kind_mismatch() { let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::I64, 2)]; - inputs[0].push(Scalar::I64(7)).unwrap(); + inputs[0].push(Scalar::i64(7)).unwrap(); let ctx = Ctx::new(&inputs, Timestamp(0)); let _ = ctx.f64_in(0); // wrong kind → panic } diff --git a/crates/aura-core/src/lib.rs b/crates/aura-core/src/lib.rs index 1e44e27..2be6bf5 100644 --- a/crates/aura-core/src/lib.rs +++ b/crates/aura-core/src/lib.rs @@ -29,6 +29,7 @@ //! (C18/C22). mod any; +mod cell; mod column; mod ctx; mod error; @@ -36,6 +37,7 @@ mod node; mod scalar; pub use any::AnyColumn; +pub use cell::Cell; pub use column::{Column, Window}; pub use ctx::Ctx; pub use error::KindMismatch; diff --git a/crates/aura-core/src/node.rs b/crates/aura-core/src/node.rs index 4f9c55a..15e02f9 100644 --- a/crates/aura-core/src/node.rs +++ b/crates/aura-core/src/node.rs @@ -435,45 +435,45 @@ mod tests { #[test] fn bind_removes_slot_and_reconstructs_positionally() { // chained bind of b then a (reverse slot order); c stays open - let bound = probe3().bind("b", Scalar::I64(8)).bind("a", Scalar::I64(7)); + let bound = probe3().bind("b", Scalar::i64(8)).bind("a", Scalar::i64(7)); assert_eq!( bound.params().iter().map(|p| p.name.as_str()).collect::>(), ["c"], // only c remains open ); - let built = bound.build(&[Scalar::F64(9.0)]); // inject c + let built = bound.build(&[Scalar::f64(9.0)]); // inject c assert_eq!( built.label(), - format!("{:?}", vec![Scalar::I64(7), Scalar::I64(8), Scalar::F64(9.0)]), + format!("{:?}", vec![Scalar::i64(7), Scalar::i64(8), Scalar::f64(9.0)]), ); // partial: bind only b; a and c stay open and keep their positions let built2 = probe3() - .bind("b", Scalar::I64(8)) - .build(&[Scalar::I64(7), Scalar::F64(9.0)]); // a, c injected in order + .bind("b", Scalar::i64(8)) + .build(&[Scalar::i64(7), Scalar::f64(9.0)]); // a, c injected in order assert_eq!( built2.label(), - format!("{:?}", vec![Scalar::I64(7), Scalar::I64(8), Scalar::F64(9.0)]), + format!("{:?}", vec![Scalar::i64(7), Scalar::i64(8), Scalar::f64(9.0)]), ); } #[test] fn bind_records_bound_param_at_original_position() { // middle-of-three: binding `b` records its ORIGINAL slot position (1). - let mid = probe3().bind("b", Scalar::I64(8)); + let mid = probe3().bind("b", Scalar::i64(8)); assert_eq!( mid.bound_params(), - [BoundParam { pos: 1, name: "b".into(), kind: ScalarKind::I64, value: Scalar::I64(8) }] + [BoundParam { pos: 1, name: "b".into(), kind: ScalarKind::I64, value: Scalar::i64(8) }] .as_slice(), ); // chained reverse-order bind (b then a): each entry carries its ORIGINAL // position regardless of the shrinking array — positions {1, 0} in call order. - let pair = probe3().bind("b", Scalar::I64(8)).bind("a", Scalar::I64(7)); + let pair = probe3().bind("b", Scalar::i64(8)).bind("a", Scalar::i64(7)); assert_eq!( pair.bound_params(), [ - BoundParam { pos: 1, name: "b".into(), kind: ScalarKind::I64, value: Scalar::I64(8) }, - BoundParam { pos: 0, name: "a".into(), kind: ScalarKind::I64, value: Scalar::I64(7) }, + BoundParam { pos: 1, name: "b".into(), kind: ScalarKind::I64, value: Scalar::i64(8) }, + BoundParam { pos: 0, name: "a".into(), kind: ScalarKind::I64, value: Scalar::i64(7) }, ] .as_slice(), ); @@ -494,7 +494,7 @@ mod tests { }, |_| Box::new(Bare), ); - let _ = b.bind("width", Scalar::I64(2)); // "width" is not a declared param + let _ = b.bind("width", Scalar::i64(2)); // "width" is not a declared param } #[test] @@ -512,7 +512,7 @@ mod tests { }, |_| Box::new(Bare), ); - let _ = b.bind("dup", Scalar::I64(1)); // two slots named "dup" + let _ = b.bind("dup", Scalar::i64(1)); // two slots named "dup" } #[test] @@ -527,7 +527,7 @@ mod tests { }, |_| Box::new(Bare), ); - let _ = b.bind("length", Scalar::F64(2.0)); // F64 value for an I64 slot + let _ = b.bind("length", Scalar::f64(2.0)); // F64 value for an I64 slot } } @@ -542,12 +542,12 @@ mod zip_params_tests { ParamSpec { name: "fast".into(), kind: ScalarKind::I64 }, ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }, ]; - let point = vec![Scalar::I64(2), Scalar::F64(0.5)]; + let point = vec![Scalar::i64(2), Scalar::f64(0.5)]; assert_eq!( zip_params(&space, &point), vec![ - ("fast".to_string(), Scalar::I64(2)), - ("scale".to_string(), Scalar::F64(0.5)), + ("fast".to_string(), Scalar::i64(2)), + ("scale".to_string(), Scalar::f64(0.5)), ], ); } @@ -558,7 +558,7 @@ mod zip_params_tests { ParamSpec { name: "a".into(), kind: ScalarKind::I64 }, ParamSpec { name: "b".into(), kind: ScalarKind::I64 }, ]; - let point = vec![Scalar::I64(7), Scalar::I64(9)]; + let point = vec![Scalar::i64(7), Scalar::i64(9)]; let hand: Vec<(String, Scalar)> = space.iter().zip(&point).map(|(ps, v)| (ps.name.clone(), *v)).collect(); assert_eq!(zip_params(&space, &point), hand); diff --git a/crates/aura-core/src/scalar.rs b/crates/aura-core/src/scalar.rs index 5767167..d4b63db 100644 --- a/crates/aura-core/src/scalar.rs +++ b/crates/aura-core/src/scalar.rs @@ -1,6 +1,10 @@ -//! The closed four-type scalar set streamed on aura's hot path (C7): -//! `i64`, `f64`, `bool`, and `Timestamp` (epoch-ns UTC). The carrier `Scalar` -//! is a `Copy` POD enum — no type-erased payloads, no heap, no reference counting. +//! The closed four-type scalar set streamed on aura's hot path (C7): `i64`, +//! `f64`, `bool`, and `Timestamp` (epoch-ns UTC). The raw value is a type-erased +//! [`Cell`] — one 64-bit word, no tag; a [`Scalar`] pairs a `Cell` with its +//! [`ScalarKind`] for the dynamic boundaries. Both are `Copy` PODs — no heap, no +//! reference counting. + +use crate::cell::Cell; /// Canonical engine time: epoch-nanoseconds UTC. Newtype over `i64` (C7). /// `Default` (epoch 0) lets `Column` pre-size its ring. @@ -16,53 +20,110 @@ pub enum ScalarKind { Timestamp, } -/// The four scalar base types, type-erased into one `Copy` carrier (C7). -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Scalar { - I64(i64), - F64(f64), - Bool(bool), - Ts(Timestamp), +/// One scalar value: a type-erased [`Cell`] plus its [`ScalarKind`] tag — the +/// self-describing form for the dynamic boundaries (builder binding, +/// serialization, rendering). The hot path uses the bare [`Cell`]; a `Scalar` +/// is the fatter form that remembers its own type. +/// +/// Reading is the caller's responsibility. Each `as_*` accessor `debug_assert`s +/// that the stored kind matches, then hands back the **native** value — in +/// release the assert is gone, so an accessor *is* the bare cell read +/// (branch-free). Calling the wrong `as_*` for the stored kind is a caller bug +/// (debug-asserted), not a recoverable error. +/// +/// `PartialEq` is **value** equality (hand-written, not derived — the derived +/// one would inherit the `Cell`'s bitwise compare). Two scalars are equal iff +/// their kinds match and their native payloads compare equal, so `f64` keeps +/// IEEE-754 semantics (`NaN != NaN`, `+0.0 == -0.0`) exactly as the former enum +/// did, and a kind mismatch is never equal even when the words coincide +/// (`i64(0) != f64(0.0)`). +#[derive(Clone, Copy, Debug)] +pub struct Scalar { + kind: ScalarKind, + cell: Cell, } impl Scalar { - /// The kind tag of this scalar. + /// Construct an `i64` scalar. + pub fn i64(v: i64) -> Self { + Scalar { kind: ScalarKind::I64, cell: Cell::from_i64(v) } + } + /// Construct an `f64` scalar. + pub fn f64(v: f64) -> Self { + Scalar { kind: ScalarKind::F64, cell: Cell::from_f64(v) } + } + /// Construct a `bool` scalar. + pub fn bool(v: bool) -> Self { + Scalar { kind: ScalarKind::Bool, cell: Cell::from_bool(v) } + } + /// Construct a [`Timestamp`] scalar. + pub fn ts(v: Timestamp) -> Self { + Scalar { kind: ScalarKind::Timestamp, cell: Cell::from_ts(v) } + } + + /// The kind tag of this scalar (a field read, no branch). pub fn kind(self) -> ScalarKind { - match self { - Scalar::I64(_) => ScalarKind::I64, - Scalar::F64(_) => ScalarKind::F64, - Scalar::Bool(_) => ScalarKind::Bool, - Scalar::Ts(_) => ScalarKind::Timestamp, + self.kind + } + + /// Read as `i64`. The caller asserts the kind by choosing this accessor; a + /// mismatch is a caller bug (debug-asserted, free in release). + pub fn as_i64(self) -> i64 { + debug_assert_eq!(self.kind, ScalarKind::I64); + self.cell.i64() + } + /// Read as `f64`. Caller asserts the kind (debug-asserted, free in release). + pub fn as_f64(self) -> f64 { + debug_assert_eq!(self.kind, ScalarKind::F64); + self.cell.f64() + } + /// Read as `bool`. Caller asserts the kind (debug-asserted, free in release). + pub fn as_bool(self) -> bool { + debug_assert_eq!(self.kind, ScalarKind::Bool); + self.cell.bool() + } + /// Read as [`Timestamp`]. Caller asserts the kind (debug-asserted, free in release). + pub fn as_ts(self) -> Timestamp { + debug_assert_eq!(self.kind, ScalarKind::Timestamp); + self.cell.ts() + } +} + +/// Value equality, not the `Cell`'s bitwise one: kinds must match, then the +/// native payloads compare — so `f64` keeps IEEE-754 semantics (`NaN != NaN`, +/// `+0.0 == -0.0`), matching the pre-`Cell` enum's behaviour. +impl PartialEq for Scalar { + fn eq(&self, other: &Self) -> bool { + if self.kind != other.kind { + return false; + } + match self.kind { + ScalarKind::I64 => self.as_i64() == other.as_i64(), + ScalarKind::F64 => self.as_f64() == other.as_f64(), + ScalarKind::Bool => self.as_bool() == other.as_bool(), + ScalarKind::Timestamp => self.as_ts() == other.as_ts(), } - } - /// The `i64` payload, or `None` if this scalar is not an `I64`. - pub fn as_i64(self) -> Option { - if let Scalar::I64(v) = self { Some(v) } else { None } - } - /// The `f64` payload, or `None` if this scalar is not an `F64`. - pub fn as_f64(self) -> Option { - if let Scalar::F64(v) = self { Some(v) } else { None } } } impl From for Scalar { fn from(v: i64) -> Self { - Scalar::I64(v) + Scalar::i64(v) } } impl From for Scalar { fn from(v: f64) -> Self { - Scalar::F64(v) + Scalar::f64(v) } } impl From for Scalar { fn from(v: bool) -> Self { - Scalar::Bool(v) + Scalar::bool(v) } } impl From for Scalar { fn from(v: Timestamp) -> Self { - Scalar::Ts(v) + Scalar::ts(v) } } @@ -72,18 +133,18 @@ mod tests { #[test] fn kind_matches_variant() { - assert_eq!(Scalar::I64(1).kind(), ScalarKind::I64); - assert_eq!(Scalar::F64(1.0).kind(), ScalarKind::F64); - assert_eq!(Scalar::Bool(true).kind(), ScalarKind::Bool); - assert_eq!(Scalar::Ts(Timestamp(1)).kind(), ScalarKind::Timestamp); + assert_eq!(Scalar::i64(1).kind(), ScalarKind::I64); + assert_eq!(Scalar::f64(1.0).kind(), ScalarKind::F64); + assert_eq!(Scalar::bool(true).kind(), ScalarKind::Bool); + assert_eq!(Scalar::ts(Timestamp(1)).kind(), ScalarKind::Timestamp); } #[test] fn from_widenings() { - assert_eq!(Scalar::from(7i64), Scalar::I64(7)); - assert_eq!(Scalar::from(2.5f64), Scalar::F64(2.5)); - assert_eq!(Scalar::from(true), Scalar::Bool(true)); - assert_eq!(Scalar::from(Timestamp(9)), Scalar::Ts(Timestamp(9))); + assert_eq!(Scalar::from(7i64), Scalar::i64(7)); + assert_eq!(Scalar::from(2.5f64), Scalar::f64(2.5)); + assert_eq!(Scalar::from(true), Scalar::bool(true)); + assert_eq!(Scalar::from(Timestamp(9)), Scalar::ts(Timestamp(9))); } #[test] @@ -97,8 +158,8 @@ mod tests { fn lower(v: impl Into) -> Scalar { v.into() } - assert_eq!(lower(2), Scalar::I64(2)); - assert_eq!(lower(0.5), Scalar::F64(0.5)); + assert_eq!(lower(2), Scalar::i64(2)); + assert_eq!(lower(0.5), Scalar::f64(0.5)); } #[test] @@ -108,16 +169,38 @@ mod tests { } #[test] - fn scalar_value_accessors_are_kind_exact() { - assert_eq!(Scalar::I64(3).as_i64(), Some(3)); - assert_eq!(Scalar::I64(3).as_f64(), None); - assert_eq!(Scalar::F64(0.5).as_f64(), Some(0.5)); - assert_eq!(Scalar::F64(0.5).as_i64(), None); + fn scalar_value_accessors_return_native_payload() { + assert_eq!(Scalar::i64(3).as_i64(), 3); + assert_eq!(Scalar::f64(0.5).as_f64(), 0.5); + assert!(Scalar::bool(true).as_bool()); + assert_eq!(Scalar::ts(Timestamp(7)).as_ts(), Timestamp(7)); + } + + #[test] + fn scalar_eq_is_value_not_bitwise() { + // f64 follows IEEE-754 exactly as the former enum did: a NaN is never + // equal to itself, and +0.0 equals -0.0 — value equality, NOT the + // bitwise equality the underlying `Cell` carries (those two cases are + // precisely where bit- and value-equality diverge). + assert_ne!(Scalar::f64(f64::NAN), Scalar::f64(f64::NAN)); + assert_eq!(Scalar::f64(0.0), Scalar::f64(-0.0)); + assert_eq!(Scalar::f64(2.5), Scalar::f64(2.5)); + // i64 / bool / timestamp compare by native value. + assert_eq!(Scalar::i64(7), Scalar::i64(7)); + assert_ne!(Scalar::i64(7), Scalar::i64(8)); + assert_eq!(Scalar::bool(true), Scalar::bool(true)); + assert_ne!(Scalar::bool(true), Scalar::bool(false)); + assert_eq!(Scalar::ts(Timestamp(9)), Scalar::ts(Timestamp(9))); + // A kind mismatch is never equal — even when the words coincide: + // i64(0) and f64(0.0) share the bit pattern 0; bool(true) and i64(1) + // share 1. Value equality still separates them by kind. + assert_ne!(Scalar::i64(0), Scalar::f64(0.0)); + assert_ne!(Scalar::i64(1), Scalar::bool(true)); } #[test] fn scalar_is_copy() { - let s = Scalar::F64(1.0); + let s = Scalar::f64(1.0); let a = s; let b = s; // Copy: `s` still usable after move-by-value assert_eq!(a, b); diff --git a/crates/aura-engine/src/blueprint.rs b/crates/aura-engine/src/blueprint.rs index 9893e64..b646752 100644 --- a/crates/aura-engine/src/blueprint.rs +++ b/crates/aura-engine/src/blueprint.rs @@ -827,16 +827,16 @@ mod tests { ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }, ]; let axes = vec![ - ("scale".to_string(), vec![Scalar::F64(0.5)]), - ("sma_cross.fast".to_string(), vec![Scalar::I64(2), Scalar::I64(3)]), - ("sma_cross.slow".to_string(), vec![Scalar::I64(4), Scalar::I64(5)]), + ("scale".to_string(), vec![Scalar::f64(0.5)]), + ("sma_cross.fast".to_string(), vec![Scalar::i64(2), Scalar::i64(3)]), + ("sma_cross.slow".to_string(), vec![Scalar::i64(4), Scalar::i64(5)]), ]; assert_eq!( resolve_axes(&space, &axes), Ok(vec![ - vec![Scalar::I64(2), Scalar::I64(3)], - vec![Scalar::I64(4), Scalar::I64(5)], - vec![Scalar::F64(0.5)], + vec![Scalar::i64(2), Scalar::i64(3)], + vec![Scalar::i64(4), Scalar::i64(5)], + vec![Scalar::f64(0.5)], ]), ); } @@ -857,7 +857,7 @@ mod tests { ParamSpec { name: "b".into(), kind: ScalarKind::I64 }, ]; assert_eq!( - resolve_axes(&space, &[("a".to_string(), vec![Scalar::I64(1)])]), + resolve_axes(&space, &[("a".to_string(), vec![Scalar::i64(1)])]), Err(BindError::MissingKnob("b".to_string())), ); } @@ -867,7 +867,7 @@ mod tests { // a MIXED-kind axis: the second element mismatches; per-element check must // catch it (a first-element-only check would pass it through to a panic). let space = vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }]; - let axes = vec![("scale".to_string(), vec![Scalar::F64(0.5), Scalar::I64(1)])]; + let axes = vec![("scale".to_string(), vec![Scalar::f64(0.5), Scalar::i64(1)])]; assert_eq!( resolve_axes(&space, &axes), Err(BindError::KindMismatch { @@ -907,16 +907,16 @@ mod tests { let named = resolve_axes( &space, &[ - ("sma_cross.fast.length".to_string(), vec![Scalar::I64(2), Scalar::I64(3)]), - ("sma_cross.slow.length".to_string(), vec![Scalar::I64(4), Scalar::I64(5)]), - ("exposure.scale".to_string(), vec![Scalar::F64(0.5)]), + ("sma_cross.fast.length".to_string(), vec![Scalar::i64(2), Scalar::i64(3)]), + ("sma_cross.slow.length".to_string(), vec![Scalar::i64(4), Scalar::i64(5)]), + ("exposure.scale".to_string(), vec![Scalar::f64(0.5)]), ], ) .expect("named axes resolve"); let positional = vec![ - vec![Scalar::I64(2), Scalar::I64(3)], - vec![Scalar::I64(4), Scalar::I64(5)], - vec![Scalar::F64(0.5)], + vec![Scalar::i64(2), Scalar::i64(3)], + vec![Scalar::i64(4), Scalar::i64(5)], + vec![Scalar::f64(0.5)], ]; let named_pts = GridSpace::new(&space, named).expect("named grid").points(); let pos_pts = GridSpace::new(&space, positional).expect("positional grid").points(); @@ -932,13 +932,13 @@ mod tests { ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }, ]; let bound = vec![ - ("scale".to_string(), Scalar::F64(0.5)), - ("sma_cross.fast".to_string(), Scalar::I64(2)), - ("sma_cross.slow".to_string(), Scalar::I64(4)), + ("scale".to_string(), Scalar::f64(0.5)), + ("sma_cross.fast".to_string(), Scalar::i64(2)), + ("sma_cross.slow".to_string(), Scalar::i64(4)), ]; assert_eq!( resolve(&space, &bound), - Ok(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]), + Ok(vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)]), ); } @@ -946,7 +946,7 @@ mod tests { fn resolve_unknown_knob() { let space = vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }]; assert_eq!( - resolve(&space, &[("nope".to_string(), Scalar::F64(0.5))]), + resolve(&space, &[("nope".to_string(), Scalar::f64(0.5))]), Err(BindError::UnknownKnob("nope".to_string())), ); } @@ -958,7 +958,7 @@ mod tests { ParamSpec { name: "b".into(), kind: ScalarKind::I64 }, ]; assert_eq!( - resolve(&space, &[("a".to_string(), Scalar::I64(1))]), + resolve(&space, &[("a".to_string(), Scalar::i64(1))]), Err(BindError::MissingKnob("b".to_string())), ); } @@ -967,7 +967,7 @@ mod tests { fn resolve_kind_mismatch() { let space = vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }]; assert_eq!( - resolve(&space, &[("scale".to_string(), Scalar::I64(2))]), + resolve(&space, &[("scale".to_string(), Scalar::i64(2))]), Err(BindError::KindMismatch { knob: "scale".to_string(), expected: ScalarKind::F64, @@ -982,7 +982,7 @@ mod tests { assert_eq!( resolve( &space, - &[("a".to_string(), Scalar::I64(1)), ("a".to_string(), Scalar::I64(2))], + &[("a".to_string(), Scalar::i64(1)), ("a".to_string(), Scalar::i64(2))], ), Err(BindError::DuplicateBinding("a".to_string())), ); @@ -993,8 +993,8 @@ mod tests { // Phase-1 (unknown) wins over Phase-2 (kind mismatch) let space = vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }]; let bound = vec![ - ("typo".to_string(), Scalar::I64(9)), - ("scale".to_string(), Scalar::I64(2)), + ("typo".to_string(), Scalar::i64(9)), + ("scale".to_string(), Scalar::i64(2)), ]; assert_eq!(resolve(&space, &bound), Err(BindError::UnknownKnob("typo".to_string()))); } @@ -1004,8 +1004,8 @@ mod tests { // intra-binding: check a (unknown) precedes check d (duplicate) let space = vec![ParamSpec { name: "a".into(), kind: ScalarKind::I64 }]; let bound = vec![ - ("typo".to_string(), Scalar::I64(1)), - ("typo".to_string(), Scalar::I64(2)), + ("typo".to_string(), Scalar::i64(1)), + ("typo".to_string(), Scalar::i64(2)), ]; assert_eq!(resolve(&space, &bound), Err(BindError::UnknownKnob("typo".to_string()))); } @@ -1027,7 +1027,7 @@ mod tests { let (bp2, pos_eq, pos_ex) = composite_sma_cross_harness(); let mut positional = bp2 - .bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]) + .bootstrap_with_params(vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)]) .expect("positional bootstrap"); positional.run(vec![Box::new(VecSource::new(synthetic_prices()))]); let pos_eq_v = pos_eq.try_iter().collect::>(); @@ -1066,7 +1066,7 @@ mod tests { if a.is_empty() || b.is_empty() { return None; } - self.out[0] = Scalar::F64(a[0] + b[0]); + self.out[0] = Scalar::f64(a[0] + b[0]); Some(&self.out) } } @@ -1084,7 +1084,7 @@ mod tests { if w.is_empty() { return None; } - self.out[0] = Scalar::F64(w[0]); + self.out[0] = Scalar::f64(w[0]); Some(&self.out) } } @@ -1116,7 +1116,7 @@ mod tests { PrimitiveBuilder::new( "Pass1", NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![] }, - |_| Box::new(Pass1 { out: [Scalar::F64(0.0)] }), + |_| Box::new(Pass1 { out: [Scalar::f64(0.0)] }), ) .into() } @@ -1124,7 +1124,7 @@ mod tests { PrimitiveBuilder::new( "Join2", NodeSchema { inputs: vec![f64_any(), f64_any()], output: out_v(), params: vec![] }, - |_| Box::new(Join2 { out: [Scalar::F64(0.0)] }), + |_| Box::new(Join2 { out: [Scalar::f64(0.0)] }), ) .into() } @@ -1236,7 +1236,7 @@ mod tests { // distinct node names -> fan-in distinguishable -> compiles (the two SMA // length params are supplied so the only thing under test is the fan-in) let bp = sma_cross_under_root(true); - assert!(bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4)]).is_ok()); + assert!(bp.compile_with_params(&[Scalar::i64(2), Scalar::i64(4)]).is_ok()); } #[test] @@ -1745,7 +1745,7 @@ mod tests { // (b) the same graph authored as a composite blueprint, compiled let (bp, comp_eq, comp_ex) = composite_sma_cross_harness(); let mut composed = bp - .bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]) + .bootstrap_with_params(vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)]) .expect("composite blueprint compiles"); composed.run(vec![Box::new(VecSource::new(prices))]); @@ -1808,7 +1808,7 @@ mod tests { vec![], // output ); let mut h = bp - .bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4)]) + .bootstrap_with_params(vec![Scalar::i64(2), Scalar::i64(4)]) .expect("multi-output composite bootstraps"); h.run(vec![Box::new(VecSource::new(prices))]); @@ -1825,13 +1825,13 @@ mod tests { fn injecting_a_different_vector_changes_the_run() { let prices = synthetic_prices(); let (bp, eq, _ex) = composite_sma_cross_harness(); - let mut a = bp.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]) + let mut a = bp.bootstrap_with_params(vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)]) .expect("compiles"); a.run(vec![Box::new(VecSource::new(prices.clone()))]); let a_eq = eq.try_iter().collect::>(); let (bp2, eq2, _ex2) = composite_sma_cross_harness(); - let mut b = bp2.bootstrap_with_params(vec![Scalar::I64(5), Scalar::I64(20), Scalar::F64(1.0)]) + let mut b = bp2.bootstrap_with_params(vec![Scalar::i64(5), Scalar::i64(20), Scalar::f64(1.0)]) .expect("compiles"); b.run(vec![Box::new(VecSource::new(prices))]); let b_eq = eq2.try_iter().collect::>(); @@ -1844,7 +1844,7 @@ mod tests { fn wrong_kind_is_a_param_kind_mismatch() { let (bp, _eq, _ex) = composite_sma_cross_harness(); // slot 0 is I64 (an SMA length); inject F64 there - let err = bp.bootstrap_with_params(vec![Scalar::F64(2.0), Scalar::I64(4), Scalar::F64(0.5)]) + let err = bp.bootstrap_with_params(vec![Scalar::f64(2.0), Scalar::i64(4), Scalar::f64(0.5)]) .unwrap_err(); assert!(matches!(err, CompileError::ParamKindMismatch { slot: 0, .. })); } @@ -1853,13 +1853,13 @@ mod tests { fn wrong_arity_is_a_param_arity_error() { let (short, _e1, _x1) = composite_sma_cross_harness(); assert!(matches!( - short.bootstrap_with_params(vec![Scalar::I64(2)]).unwrap_err(), + short.bootstrap_with_params(vec![Scalar::i64(2)]).unwrap_err(), CompileError::ParamArity { expected: 3, got: 1 } )); let (long, _e2, _x2) = composite_sma_cross_harness(); assert!(matches!( long.bootstrap_with_params( - vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5), Scalar::F64(0.0)] + vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5), Scalar::f64(0.0)] ).unwrap_err(), CompileError::ParamArity { expected: 3, got: 4 } )); @@ -1869,11 +1869,11 @@ mod tests { fn same_vector_bootstraps_identically() { let prices = synthetic_prices(); let (bp, eq, _ex) = composite_sma_cross_harness(); - let mut a = bp.bootstrap_with_params(vec![Scalar::I64(3), Scalar::I64(9), Scalar::F64(0.7)]) + let mut a = bp.bootstrap_with_params(vec![Scalar::i64(3), Scalar::i64(9), Scalar::f64(0.7)]) .expect("compiles"); a.run(vec![Box::new(VecSource::new(prices.clone()))]); let (bp2, eq2, _ex2) = composite_sma_cross_harness(); - let mut b = bp2.bootstrap_with_params(vec![Scalar::I64(3), Scalar::I64(9), Scalar::F64(0.7)]) + let mut b = bp2.bootstrap_with_params(vec![Scalar::i64(3), Scalar::i64(9), Scalar::f64(0.7)]) .expect("compiles"); b.run(vec![Box::new(VecSource::new(prices))]); assert_eq!(eq.try_iter().collect::>(), eq2.try_iter().collect::>()); @@ -1896,7 +1896,7 @@ mod tests { // the same blueprint, actually compiled to its flat node array; each flat // node's own declared params, concatenated in flat-node order - let flat = bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]).expect("harness compiles"); + let flat = bp.compile_with_params(&[Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)]).expect("harness compiles"); let from_flat: Vec = flat.signatures.iter().flat_map(|s| s.params.clone()).collect(); @@ -1981,7 +1981,7 @@ mod tests { let strat = Composite::new( "sma2_entry", vec![ - Sma::builder().named("bias").bind("length", Scalar::I64(2)).into(), + Sma::builder().named("bias").bind("length", Scalar::i64(2)).into(), Exposure::builder().named("exp").into(), ], vec![], // edges — irrelevant to param_space() @@ -2055,7 +2055,7 @@ mod tests { // the same blueprint, compiled to its flat node array; each flat node's // own declared params, concatenated in flat-node order - let flat = bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4), Scalar::F64(1.0), Scalar::F64(-1.0)]).expect("nested composite compiles"); + let flat = bp.compile_with_params(&[Scalar::i64(2), Scalar::i64(4), Scalar::f64(1.0), Scalar::f64(-1.0)]).expect("nested composite compiles"); let from_flat: Vec = flat.signatures.iter().flat_map(|s| s.params.clone()).collect(); @@ -2190,7 +2190,7 @@ mod tests { vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) }], vec![], ); - let err = root.compile_with_params(&[Scalar::I64(3)]); + let err = root.compile_with_params(&[Scalar::i64(3)]); // kind fault caught pre-build (no panic) — same variant bootstrap would give assert!(matches!( err, @@ -2209,7 +2209,7 @@ mod tests { ); // the Ok arm holds a FlatGraph (not Debug), so assert via the Err arm. assert_eq!( - root.compile_with_params(&[Scalar::I64(3)]).err(), + root.compile_with_params(&[Scalar::i64(3)]).err(), Some(CompileError::UnboundRootRole { role: 0 }) ); } @@ -2262,7 +2262,7 @@ mod tests { // two params declared (one per SMA); supply both so the only failure under // test is the duplicate path (green today, DuplicateParamPath after). assert_eq!( - bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(3)]).err(), + bp.compile_with_params(&[Scalar::i64(2), Scalar::i64(3)]).err(), Some(CompileError::DuplicateParamPath("dup.sma.length".to_string())) ); } @@ -2280,7 +2280,7 @@ mod tests { let paramless_sma = PrimitiveBuilder::new( "Pass1", NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![] }, - |_| Box::new(Pass1 { out: [Scalar::F64(0.0)] }), + |_| Box::new(Pass1 { out: [Scalar::f64(0.0)] }), ) .named("sma"); let asym = Composite::new( @@ -2312,7 +2312,7 @@ mod tests { bp.param_space().into_iter().map(|p| p.name).collect::>(), ["asym.sma.length"] ); - assert!(bp.compile_with_params(&[Scalar::I64(2)]).is_ok()); + assert!(bp.compile_with_params(&[Scalar::i64(2)]).is_ok()); } #[test] diff --git a/crates/aura-engine/src/builder.rs b/crates/aura-engine/src/builder.rs index a87f38b..e7aa224 100644 --- a/crates/aura-engine/src/builder.rs +++ b/crates/aura-engine/src/builder.rs @@ -240,7 +240,7 @@ mod tests { // cancels in the comparison. Params size buffers, not topology (C11), so // edges/sources are param-independent; a fixed vector just satisfies the // arity gate compile() (no-param) trips on this value-empty harness. - let params = [Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]; + let params = [Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)]; // hand-wired reference harness -> FlatGraph let (hand_bp, _rx_eq, _rx_ex) = composite_sma_cross_harness(); @@ -290,7 +290,7 @@ mod tests { let compiled = g .build() .expect("all port/field names resolve") - .compile_with_params(&[Scalar::F64(0.5)]); + .compile_with_params(&[Scalar::f64(0.5)]); assert_eq!( compiled.err(), Some(CompileError::UnconnectedPort { node: 1, slot: 0 }) diff --git a/crates/aura-engine/src/graph_model.rs b/crates/aura-engine/src/graph_model.rs index 4fbaa48..8014c20 100644 --- a/crates/aura-engine/src/graph_model.rs +++ b/crates/aura-engine/src/graph_model.rs @@ -25,12 +25,12 @@ fn kind_str(k: ScalarKind) -> &'static str { /// whole values (`1.0`, not `1`) so a bound f64 is never mistaken for an i64 in /// the render. The value side of `kind_str`. fn scalar_str(s: aura_core::Scalar) -> String { - use aura_core::Scalar; - match s { - Scalar::I64(n) => n.to_string(), - Scalar::F64(f) => format!("{f:?}"), - Scalar::Bool(b) => b.to_string(), - Scalar::Ts(t) => t.0.to_string(), + use aura_core::ScalarKind; + match s.kind() { + ScalarKind::I64 => s.as_i64().to_string(), + ScalarKind::F64 => format!("{:?}", s.as_f64()), + ScalarKind::Bool => s.as_bool().to_string(), + ScalarKind::Timestamp => s.as_ts().0.to_string(), } } @@ -426,13 +426,13 @@ mod tests { #[test] fn prim_record_emits_bound_only_when_bound() { // a bound i64 param → a "bound" field carrying [pos,"name","kind","value"] - let bound = prim_record(&Sma::builder().bind("length", Scalar::I64(5))); + let bound = prim_record(&Sma::builder().bind("length", Scalar::i64(5))); assert!(bound.contains(r#""bound":[[0,"length","i64","5"]]"#), "{bound}"); // the bound slot also leaves the tunable "params" surface (bind's tuning side) assert!(bound.contains(r#""params":[]"#), "{bound}"); // a bound f64 value renders canonically (shortest round-trip), e.g. 0.5 - let f = prim_record(&Exposure::builder().bind("scale", Scalar::F64(0.5))); + let f = prim_record(&Exposure::builder().bind("scale", Scalar::f64(0.5))); assert!(f.contains(r#""bound":[[0,"scale","f64","0.5"]]"#), "{f}"); // an unbound builder → no "bound" field at all diff --git a/crates/aura-engine/src/harness.rs b/crates/aura-engine/src/harness.rs index d158547..e67125f 100644 --- a/crates/aura-engine/src/harness.rs +++ b/crates/aura-engine/src/harness.rs @@ -183,7 +183,7 @@ impl SyntheticSpec { .map(|i| { let shock = (rng.next_f64() - 0.5) * 0.004; // ±0.2% per step price *= 1.0 + shock; - (Timestamp(i as i64 * self.step + 1), Scalar::F64(price)) + (Timestamp(i as i64 * self.step + 1), Scalar::f64(price)) }) .collect(); VecSource::new(stream) @@ -492,7 +492,7 @@ mod tests { /// Build an f64 source stream from (timestamp, value) points. fn f64_stream(points: &[(i64, f64)]) -> Vec<(Timestamp, Scalar)> { - points.iter().map(|&(t, v)| (Timestamp(t), Scalar::F64(v))).collect() + points.iter().map(|&(t, v)| (Timestamp(t), Scalar::f64(v))).collect() } #[test] @@ -527,19 +527,19 @@ mod tests { #[test] fn vec_source_peek_is_non_consuming_and_idempotent() { - let mut s = VecSource::new(vec![(Timestamp(1), Scalar::F64(10.0)), (Timestamp(2), Scalar::F64(20.0))]); + let mut s = VecSource::new(vec![(Timestamp(1), Scalar::f64(10.0)), (Timestamp(2), Scalar::f64(20.0))]); // peek twice: same head, no advance. assert_eq!(s.peek(), Some(Timestamp(1))); assert_eq!(s.peek(), Some(Timestamp(1))); // next pops it; peek now reflects the new head. - assert_eq!(Source::next(&mut s), Some((Timestamp(1), Scalar::F64(10.0)))); + assert_eq!(Source::next(&mut s), Some((Timestamp(1), Scalar::f64(10.0)))); assert_eq!(s.peek(), Some(Timestamp(2))); } #[test] fn vec_source_exhaustion_yields_none() { - let mut s = VecSource::new(vec![(Timestamp(5), Scalar::I64(7))]); - assert_eq!(Source::next(&mut s), Some((Timestamp(5), Scalar::I64(7)))); + let mut s = VecSource::new(vec![(Timestamp(5), Scalar::i64(7))]); + assert_eq!(Source::next(&mut s), Some((Timestamp(5), Scalar::i64(7)))); assert_eq!(s.peek(), None); assert_eq!(Source::next(&mut s), None); } @@ -639,7 +639,7 @@ mod tests { if a.is_empty() || b.is_empty() { return None; } - self.out[0] = Scalar::F64(a[0] + b[0]); + self.out[0] = Scalar::f64(a[0] + b[0]); Some(&self.out) } } @@ -663,7 +663,7 @@ mod tests { vec![1, 1] } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> { - self.out[0] = Scalar::F64(ctx.f64_in(0)[0] + ctx.f64_in(1)[0]); + self.out[0] = Scalar::f64(ctx.f64_in(0)[0] + ctx.f64_in(1)[0]); Some(&self.out) } } @@ -698,7 +698,7 @@ mod tests { if a.is_empty() || b.is_empty() || c.is_empty() { return None; } - self.out[0] = Scalar::F64(a[0] + b[0] + c[0]); + self.out[0] = Scalar::f64(a[0] + b[0] + c[0]); Some(&self.out) } } @@ -735,7 +735,7 @@ mod tests { if w.is_empty() { return None; // not yet warmed } - self.out[i] = Scalar::F64(w[0]); + self.out[i] = Scalar::f64(w[0]); } Some(&self.out) // one 5-field record, all fields co-fresh } @@ -765,8 +765,8 @@ mod tests { vec![1] } fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> { - self.out[0] = Scalar::F64(0.0); - self.out[1] = Scalar::I64(0); + self.out[0] = Scalar::f64(0.0); + self.out[1] = Scalar::i64(0); Some(&self.out) } } @@ -797,8 +797,8 @@ mod tests { return None; } let v = w[0]; - let _ = self.tx.send((ctx.now(), vec![Scalar::F64(v)])); // sink side effect - self.out[0] = Scalar::F64(v); + let _ = self.tx.send((ctx.now(), vec![Scalar::f64(v)])); // sink side effect + self.out[0] = Scalar::f64(v); Some(&self.out) // producer output: engine forwards it } } @@ -827,9 +827,9 @@ mod tests { assert_eq!( got, vec![ - (Timestamp(3), vec![Scalar::F64(2.0)]), - (Timestamp(4), vec![Scalar::F64(3.0)]), - (Timestamp(5), vec![Scalar::F64(4.0)]), + (Timestamp(3), vec![Scalar::f64(2.0)]), + (Timestamp(4), vec![Scalar::f64(3.0)]), + (Timestamp(5), vec![Scalar::f64(4.0)]), ] ); } @@ -874,9 +874,9 @@ mod tests { assert_eq!( out, vec![ - (Timestamp(4), vec![Scalar::F64(2.0)]), - (Timestamp(5), vec![Scalar::F64(2.0)]), - (Timestamp(6), vec![Scalar::F64(2.0)]), + (Timestamp(4), vec![Scalar::f64(2.0)]), + (Timestamp(5), vec![Scalar::f64(2.0)]), + (Timestamp(6), vec![Scalar::f64(2.0)]), ] ); @@ -895,7 +895,7 @@ mod tests { let build = |tx| { boot( vec![ - Box::new(AsOfSum { out: [Scalar::F64(0.0)] }), + Box::new(AsOfSum { out: [Scalar::f64(0.0)] }), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![ @@ -921,10 +921,10 @@ mod tests { assert_eq!( out, vec![ - (Timestamp(2), vec![Scalar::F64(120.0)]), - (Timestamp(3), vec![Scalar::F64(130.0)]), - (Timestamp(4), vec![Scalar::F64(140.0)]), - (Timestamp(4), vec![Scalar::F64(240.0)]), + (Timestamp(2), vec![Scalar::f64(120.0)]), + (Timestamp(3), vec![Scalar::f64(130.0)]), + (Timestamp(4), vec![Scalar::f64(140.0)]), + (Timestamp(4), vec![Scalar::f64(240.0)]), ] ); @@ -941,7 +941,7 @@ mod tests { let build = |tx| { boot( vec![ - Box::new(BarrierSum { out: [Scalar::F64(0.0)] }), + Box::new(BarrierSum { out: [Scalar::f64(0.0)] }), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![ @@ -967,8 +967,8 @@ mod tests { assert_eq!( out, vec![ - (Timestamp(2), vec![Scalar::F64(120.0)]), - (Timestamp(4), vec![Scalar::F64(240.0)]), + (Timestamp(2), vec![Scalar::f64(120.0)]), + (Timestamp(4), vec![Scalar::f64(240.0)]), ] ); @@ -990,7 +990,7 @@ mod tests { vec![ Box::new(Sma::new(2)), Box::new(Sma::new(4)), - Box::new(BarrierSum { out: [Scalar::F64(0.0)] }), + Box::new(BarrierSum { out: [Scalar::f64(0.0)] }), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![ @@ -1022,9 +1022,9 @@ mod tests { assert_eq!( out, vec![ - (Timestamp(4), vec![Scalar::F64(28.0)]), - (Timestamp(5), vec![Scalar::F64(32.0)]), - (Timestamp(6), vec![Scalar::F64(36.0)]), + (Timestamp(4), vec![Scalar::f64(28.0)]), + (Timestamp(5), vec![Scalar::f64(32.0)]), + (Timestamp(6), vec![Scalar::f64(36.0)]), ] ); @@ -1041,7 +1041,7 @@ mod tests { let (tx, rx) = mpsc::channel(); let mut h = boot( vec![ - Box::new(MixedSum { out: [Scalar::F64(0.0)] }), + Box::new(MixedSum { out: [Scalar::f64(0.0)] }), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![ @@ -1067,8 +1067,8 @@ mod tests { assert_eq!( out, vec![ - (Timestamp(2), vec![Scalar::F64(221.0)]), - (Timestamp(3), vec![Scalar::F64(223.0)]), + (Timestamp(2), vec![Scalar::f64(221.0)]), + (Timestamp(3), vec![Scalar::f64(223.0)]), ] ); } @@ -1153,7 +1153,7 @@ mod tests { let (tx, rx) = mpsc::channel(); let mut h = boot( vec![ - Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }), + Box::new(Ohlcv { out: [Scalar::f64(0.0); 5] }), Box::new(Recorder::new( &[ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64], Firing::Any, @@ -1180,18 +1180,18 @@ mod tests { out, vec![ (Timestamp(1), vec![ - Scalar::F64(10.0), - Scalar::F64(15.0), - Scalar::F64(8.0), - Scalar::F64(12.0), - Scalar::F64(100.0), + Scalar::f64(10.0), + Scalar::f64(15.0), + Scalar::f64(8.0), + Scalar::f64(12.0), + Scalar::f64(100.0), ]), (Timestamp(2), vec![ - Scalar::F64(20.0), - Scalar::F64(25.0), - Scalar::F64(19.0), - Scalar::F64(22.0), - Scalar::F64(200.0), + Scalar::f64(20.0), + Scalar::f64(25.0), + Scalar::f64(19.0), + Scalar::f64(22.0), + Scalar::f64(200.0), ]), ] ); @@ -1206,7 +1206,7 @@ mod tests { let build = |tx| { boot( vec![ - Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }), + Box::new(Ohlcv { out: [Scalar::f64(0.0); 5] }), Box::new(Sub::new()), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], @@ -1232,8 +1232,8 @@ mod tests { assert_eq!( out, vec![ - (Timestamp(1), vec![Scalar::F64(7.0)]), - (Timestamp(2), vec![Scalar::F64(6.0)]), + (Timestamp(1), vec![Scalar::f64(7.0)]), + (Timestamp(2), vec![Scalar::f64(6.0)]), ] ); @@ -1252,7 +1252,7 @@ mod tests { let (tx, rx) = mpsc::channel(); let mut h = boot( vec![ - Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }), + Box::new(Ohlcv { out: [Scalar::f64(0.0); 5] }), Box::new(Sub::new()), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], @@ -1275,8 +1275,8 @@ mod tests { assert_eq!( out, vec![ - (Timestamp(1), vec![Scalar::F64(2.0)]), - (Timestamp(2), vec![Scalar::F64(2.0)]), + (Timestamp(1), vec![Scalar::f64(2.0)]), + (Timestamp(2), vec![Scalar::f64(2.0)]), ] ); } @@ -1304,7 +1304,7 @@ mod tests { // Sma(1)'s f64 input slot is a per-field kind mismatch -> KindMismatch. (The // mismatch is field-specific: from_field 0 would have matched.) let err = boot( - vec![Box::new(TwoField { out: [Scalar::F64(0.0), Scalar::I64(0)] }), Box::new(Sma::new(1))], + vec![Box::new(TwoField { out: [Scalar::f64(0.0), Scalar::i64(0)] }), Box::new(Sma::new(1))], vec![ TwoField::sig(), Sma::builder().schema().clone(), @@ -1355,17 +1355,17 @@ mod tests { assert_eq!( fast, vec![ - (Timestamp(2), vec![Scalar::F64(11.0)]), - (Timestamp(3), vec![Scalar::F64(13.0)]), - (Timestamp(4), vec![Scalar::F64(15.0)]), - (Timestamp(5), vec![Scalar::F64(17.0)]), + (Timestamp(2), vec![Scalar::f64(11.0)]), + (Timestamp(3), vec![Scalar::f64(13.0)]), + (Timestamp(4), vec![Scalar::f64(15.0)]), + (Timestamp(5), vec![Scalar::f64(17.0)]), ] ); assert_eq!( slow, vec![ - (Timestamp(4), vec![Scalar::F64(13.0)]), - (Timestamp(5), vec![Scalar::F64(15.0)]), + (Timestamp(4), vec![Scalar::f64(13.0)]), + (Timestamp(5), vec![Scalar::f64(15.0)]), ] ); } @@ -1377,7 +1377,7 @@ mod tests { let (tx, rx) = mpsc::channel(); let mut h = boot( vec![ - Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }), + Box::new(Ohlcv { out: [Scalar::f64(0.0); 5] }), Box::new(Recorder::new( &[ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64], Firing::Any, @@ -1411,11 +1411,11 @@ mod tests { assert_eq!( out, vec![(Timestamp(1), vec![ - Scalar::F64(10.0), - Scalar::F64(15.0), - Scalar::F64(8.0), - Scalar::F64(12.0), - Scalar::F64(100.0), + Scalar::f64(10.0), + Scalar::f64(15.0), + Scalar::f64(8.0), + Scalar::f64(12.0), + Scalar::f64(100.0), ])] ); } @@ -1446,19 +1446,19 @@ mod tests { ) .expect("valid"); h.run(vec![ - Box::new(VecSource::new(vec![(Timestamp(1), Scalar::I64(7))])), - Box::new(VecSource::new(vec![(Timestamp(2), Scalar::F64(1.5))])), - Box::new(VecSource::new(vec![(Timestamp(3), Scalar::Bool(true))])), - Box::new(VecSource::new(vec![(Timestamp(4), Scalar::Ts(Timestamp(99)))])), + Box::new(VecSource::new(vec![(Timestamp(1), Scalar::i64(7))])), + Box::new(VecSource::new(vec![(Timestamp(2), Scalar::f64(1.5))])), + Box::new(VecSource::new(vec![(Timestamp(3), Scalar::bool(true))])), + Box::new(VecSource::new(vec![(Timestamp(4), Scalar::ts(Timestamp(99)))])), ]); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); assert_eq!( out, vec![(Timestamp(4), vec![ - Scalar::I64(7), - Scalar::F64(1.5), - Scalar::Bool(true), - Scalar::Ts(Timestamp(99)), + Scalar::i64(7), + Scalar::f64(1.5), + Scalar::bool(true), + Scalar::ts(Timestamp(99)), ])] ); } @@ -1472,7 +1472,7 @@ mod tests { let (tx_down, rx_down) = mpsc::channel(); let mut h = boot( vec![ - Box::new(TapForward { out: [Scalar::F64(0.0)], tx: tx_tap }), + Box::new(TapForward { out: [Scalar::f64(0.0)], tx: tx_tap }), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_down)), ], vec![ @@ -1487,9 +1487,9 @@ mod tests { let tapped: Vec<(Timestamp, Vec)> = rx_tap.try_iter().collect(); let downstream: Vec<(Timestamp, Vec)> = rx_down.try_iter().collect(); let expected = vec![ - (Timestamp(1), vec![Scalar::F64(10.0)]), - (Timestamp(2), vec![Scalar::F64(20.0)]), - (Timestamp(3), vec![Scalar::F64(30.0)]), + (Timestamp(1), vec![Scalar::f64(10.0)]), + (Timestamp(2), vec![Scalar::f64(20.0)]), + (Timestamp(3), vec![Scalar::f64(30.0)]), ]; assert_eq!(tapped, expected); // it recorded (sink side effect) assert_eq!(downstream, expected); // and forwarded (producer output) @@ -1559,8 +1559,8 @@ mod tests { assert_eq!( out, vec![ - (Timestamp(2), vec![Scalar::F64(20.0), Scalar::F64(100.0)]), - (Timestamp(4), vec![Scalar::F64(40.0), Scalar::F64(200.0)]), + (Timestamp(2), vec![Scalar::f64(20.0), Scalar::f64(100.0)]), + (Timestamp(4), vec![Scalar::f64(40.0), Scalar::f64(200.0)]), ] ); } @@ -1595,10 +1595,10 @@ mod tests { assert_eq!( out, vec![ - (Timestamp(2), vec![Scalar::F64(20.0), Scalar::F64(100.0)]), - (Timestamp(3), vec![Scalar::F64(30.0), Scalar::F64(100.0)]), - (Timestamp(4), vec![Scalar::F64(40.0), Scalar::F64(100.0)]), - (Timestamp(4), vec![Scalar::F64(40.0), Scalar::F64(200.0)]), + (Timestamp(2), vec![Scalar::f64(20.0), Scalar::f64(100.0)]), + (Timestamp(3), vec![Scalar::f64(30.0), Scalar::f64(100.0)]), + (Timestamp(4), vec![Scalar::f64(40.0), Scalar::f64(100.0)]), + (Timestamp(4), vec![Scalar::f64(40.0), Scalar::f64(200.0)]), ] ); } @@ -1611,7 +1611,7 @@ mod tests { let (tx, _rx) = mpsc::channel(); let err = boot( vec![ - Box::new(TwoField { out: [Scalar::F64(0.0), Scalar::I64(0)] }), + Box::new(TwoField { out: [Scalar::f64(0.0), Scalar::i64(0)] }), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![ @@ -1679,9 +1679,9 @@ mod tests { let s3: Vec<(Timestamp, Vec)> = rc.try_iter().collect(); // SMA(3) warms at cycle 3: mean(2,4,6)=4, mean(4,6,8)=6, mean(6,8,10)=8. let expected = vec![ - (Timestamp(3), vec![Scalar::F64(4.0)]), - (Timestamp(4), vec![Scalar::F64(6.0)]), - (Timestamp(5), vec![Scalar::F64(8.0)]), + (Timestamp(3), vec![Scalar::f64(4.0)]), + (Timestamp(4), vec![Scalar::f64(6.0)]), + (Timestamp(5), vec![Scalar::f64(8.0)]), ]; assert_eq!(s1, expected); assert_eq!(s2, expected); // every tap sees the identical shared stream @@ -1743,17 +1743,17 @@ mod tests { let sub: Vec<(Timestamp, Vec)> = rs.try_iter().collect(); // SMA(2): 11,13,15,17,19 at cycles 2..6 (raw tap). let raw_expected = vec![ - (Timestamp(2), vec![Scalar::F64(11.0)]), - (Timestamp(3), vec![Scalar::F64(13.0)]), - (Timestamp(4), vec![Scalar::F64(15.0)]), - (Timestamp(5), vec![Scalar::F64(17.0)]), - (Timestamp(6), vec![Scalar::F64(19.0)]), + (Timestamp(2), vec![Scalar::f64(11.0)]), + (Timestamp(3), vec![Scalar::f64(13.0)]), + (Timestamp(4), vec![Scalar::f64(15.0)]), + (Timestamp(5), vec![Scalar::f64(17.0)]), + (Timestamp(6), vec![Scalar::f64(19.0)]), ]; // Sub warms once SMA(4) is warm (cycle 4): 15-13, 17-15, 19-17 -> 2. let sub_expected = vec![ - (Timestamp(4), vec![Scalar::F64(2.0)]), - (Timestamp(5), vec![Scalar::F64(2.0)]), - (Timestamp(6), vec![Scalar::F64(2.0)]), + (Timestamp(4), vec![Scalar::f64(2.0)]), + (Timestamp(5), vec![Scalar::f64(2.0)]), + (Timestamp(6), vec![Scalar::f64(2.0)]), ]; assert_eq!(raw, raw_expected); assert_eq!(sub, sub_expected); @@ -1778,7 +1778,7 @@ mod tests { vec![ Box::new(Sma::new(2)), // 0: shared producer (src A) Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t_any)), // 1: as-of tap of 0 - Box::new(BarrierSum { out: [Scalar::F64(0.0)] }), // 2: SMA(2) + src B (barrier) + Box::new(BarrierSum { out: [Scalar::f64(0.0)] }), // 2: SMA(2) + src B (barrier) Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t_bar)), // 3: tap of barrier ], vec![ @@ -1810,15 +1810,15 @@ mod tests { let bar: Vec<(Timestamp, Vec)> = rbar.try_iter().collect(); // As-of tap records every SMA(2) fire: 11@t2, 13@t3, 15@t4. let any_expected = vec![ - (Timestamp(2), vec![Scalar::F64(11.0)]), - (Timestamp(3), vec![Scalar::F64(13.0)]), - (Timestamp(4), vec![Scalar::F64(15.0)]), + (Timestamp(2), vec![Scalar::f64(11.0)]), + (Timestamp(3), vec![Scalar::f64(13.0)]), + (Timestamp(4), vec![Scalar::f64(15.0)]), ]; // Barrier fires only where held SMA output and src B share a timestamp: // t=2 (SMA held=11 + B=100 = 111), t=4 (SMA held=15 + B=200 = 215). let bar_expected = vec![ - (Timestamp(2), vec![Scalar::F64(111.0)]), - (Timestamp(4), vec![Scalar::F64(215.0)]), + (Timestamp(2), vec![Scalar::f64(111.0)]), + (Timestamp(4), vec![Scalar::f64(215.0)]), ]; assert_eq!(any, any_expected); assert_eq!(bar, bar_expected); @@ -1870,8 +1870,8 @@ mod tests { let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // stage1 (c2..c5): 3,5,7,9. stage2 (c3..): 4,6,8. stage3 (c4..): 5,7. let expected = vec![ - (Timestamp(4), vec![Scalar::F64(5.0)]), - (Timestamp(5), vec![Scalar::F64(7.0)]), + (Timestamp(4), vec![Scalar::f64(5.0)]), + (Timestamp(5), vec![Scalar::f64(7.0)]), ]; assert_eq!(out, expected); @@ -1932,19 +1932,19 @@ mod tests { let w3: Vec<(Timestamp, Vec)> = rb.try_iter().collect(); let w4: Vec<(Timestamp, Vec)> = rc.try_iter().collect(); let e2 = vec![ - (Timestamp(2), vec![Scalar::F64(11.0)]), - (Timestamp(3), vec![Scalar::F64(13.0)]), - (Timestamp(4), vec![Scalar::F64(15.0)]), - (Timestamp(5), vec![Scalar::F64(17.0)]), + (Timestamp(2), vec![Scalar::f64(11.0)]), + (Timestamp(3), vec![Scalar::f64(13.0)]), + (Timestamp(4), vec![Scalar::f64(15.0)]), + (Timestamp(5), vec![Scalar::f64(17.0)]), ]; let e3 = vec![ - (Timestamp(3), vec![Scalar::F64(12.0)]), - (Timestamp(4), vec![Scalar::F64(14.0)]), - (Timestamp(5), vec![Scalar::F64(16.0)]), + (Timestamp(3), vec![Scalar::f64(12.0)]), + (Timestamp(4), vec![Scalar::f64(14.0)]), + (Timestamp(5), vec![Scalar::f64(16.0)]), ]; let e4 = vec![ - (Timestamp(4), vec![Scalar::F64(13.0)]), - (Timestamp(5), vec![Scalar::F64(15.0)]), + (Timestamp(4), vec![Scalar::f64(13.0)]), + (Timestamp(5), vec![Scalar::f64(15.0)]), ]; assert_eq!(w2, e2); assert_eq!(w3, e3); @@ -2005,9 +2005,9 @@ mod tests { let prices = f64_stream(&[(1, 10.0), (2, 12.0), (3, 14.0), (4, 16.0), (5, 18.0), (6, 20.0)]); let counts: Vec<(Timestamp, Scalar)> = vec![ - (Timestamp(1), Scalar::I64(7)), - (Timestamp(2), Scalar::I64(8)), - (Timestamp(3), Scalar::I64(9)), + (Timestamp(1), Scalar::i64(7)), + (Timestamp(2), Scalar::i64(8)), + (Timestamp(3), Scalar::i64(9)), ]; let (tr, rr) = mpsc::channel(); @@ -2019,21 +2019,21 @@ mod tests { let sub: Vec<(Timestamp, Vec)> = rs.try_iter().collect(); let i64s: Vec<(Timestamp, Vec)> = ri.try_iter().collect(); let raw_expected = vec![ - (Timestamp(2), vec![Scalar::F64(11.0)]), - (Timestamp(3), vec![Scalar::F64(13.0)]), - (Timestamp(4), vec![Scalar::F64(15.0)]), - (Timestamp(5), vec![Scalar::F64(17.0)]), - (Timestamp(6), vec![Scalar::F64(19.0)]), + (Timestamp(2), vec![Scalar::f64(11.0)]), + (Timestamp(3), vec![Scalar::f64(13.0)]), + (Timestamp(4), vec![Scalar::f64(15.0)]), + (Timestamp(5), vec![Scalar::f64(17.0)]), + (Timestamp(6), vec![Scalar::f64(19.0)]), ]; let sub_expected = vec![ - (Timestamp(4), vec![Scalar::F64(2.0)]), - (Timestamp(5), vec![Scalar::F64(2.0)]), - (Timestamp(6), vec![Scalar::F64(2.0)]), + (Timestamp(4), vec![Scalar::f64(2.0)]), + (Timestamp(5), vec![Scalar::f64(2.0)]), + (Timestamp(6), vec![Scalar::f64(2.0)]), ]; let i64_expected = vec![ - (Timestamp(1), vec![Scalar::I64(7)]), - (Timestamp(2), vec![Scalar::I64(8)]), - (Timestamp(3), vec![Scalar::I64(9)]), + (Timestamp(1), vec![Scalar::i64(7)]), + (Timestamp(2), vec![Scalar::i64(8)]), + (Timestamp(3), vec![Scalar::i64(9)]), ]; assert_eq!(raw, raw_expected); assert_eq!(sub, sub_expected); @@ -2105,12 +2105,11 @@ mod tests { // flat until SMA(4) warms (cycle 4) and the held exposure meets the next // price move (cycle 5): the first four equities are exactly 0. for (_, row) in &equity[0..4] { - assert_eq!(row, &vec![Scalar::F64(0.0)]); + assert_eq!(row, &vec![Scalar::f64(0.0)]); } // cycle 5: prev_exposure 0.00175 * (1.0040 - 1.0020) / 0.0001 = 0.035 pips - let Scalar::F64(last) = equity[4].1[0] else { - panic!("equity is f64"); - }; + debug_assert_eq!(equity[4].1[0].kind(), ScalarKind::F64, "equity is f64"); + let last = equity[4].1[0].as_f64(); assert!((last - 0.035).abs() < 1e-9, "final equity = {last}, want ~0.035"); } diff --git a/crates/aura-engine/src/lib.rs b/crates/aura-engine/src/lib.rs index b048c74..9d8b199 100644 --- a/crates/aura-engine/src/lib.rs +++ b/crates/aura-engine/src/lib.rs @@ -86,7 +86,7 @@ mod reexport_tests { use crate::{Firing, Scalar, ScalarKind, Timestamp}; let _k: ScalarKind = ScalarKind::F64; let _f: Firing = Firing::Any; - let _s: Scalar = Scalar::F64(0.0); + let _s: Scalar = Scalar::f64(0.0); let _t: Timestamp = Timestamp(0); } } diff --git a/crates/aura-engine/src/mc.rs b/crates/aura-engine/src/mc.rs index bc315b7..87c0f76 100644 --- a/crates/aura-engine/src/mc.rs +++ b/crates/aura-engine/src/mc.rs @@ -188,7 +188,7 @@ mod tests { fn base_point() -> Vec { // matches the composite_sma_cross param_space order: fast, slow, scale - vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)] + vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)] } fn draws_with_metrics(values: &[f64]) -> Vec { diff --git a/crates/aura-engine/src/report.rs b/crates/aura-engine/src/report.rs index cea3189..256bdd8 100644 --- a/crates/aura-engine/src/report.rs +++ b/crates/aura-engine/src/report.rs @@ -8,7 +8,7 @@ //! cycle 0029) — the same encoder the run registry uses, so a record's stdout //! and on-disk shapes coincide. -use aura_core::{Scalar, Timestamp}; +use aura_core::{Scalar, ScalarKind, Timestamp}; /// Summary metrics reduced from a run's recorded streams — the `-> metrics` /// half of C12's atomic sim unit. Pure function of the recorded streams. @@ -134,10 +134,10 @@ pub fn f64_field(rows: &[(Timestamp, Vec)], field: usize) -> Vec<(Timest let Some(&scalar) = row.get(field) else { panic!("f64_field: row has no field {field} (row width {})", row.len()); }; - let Scalar::F64(v) = scalar else { + if scalar.kind() != ScalarKind::F64 { panic!("f64_field: field {field} is not an f64 scalar: {scalar:?}"); - }; - (*ts, v) + } + (*ts, scalar.as_f64()) }) .collect() } @@ -164,7 +164,7 @@ mod tests { /// harness.rs test helper; the e2e test needs its own copy — the harness /// test module's is private to that module). fn f64_stream(points: &[(i64, f64)]) -> Vec<(Timestamp, Scalar)> { - points.iter().map(|&(t, v)| (Timestamp(t), Scalar::F64(v))).collect() + points.iter().map(|&(t, v)| (Timestamp(t), Scalar::f64(v))).collect() } /// Bootstrap the cycle-0007 signal-quality harness with TWO sinks: one on @@ -316,8 +316,8 @@ mod tests { #[test] fn f64_field_projects_the_named_field() { let rows = vec![ - (Timestamp(1), vec![Scalar::F64(1.5), Scalar::I64(9)]), - (Timestamp(2), vec![Scalar::F64(2.5), Scalar::I64(8)]), + (Timestamp(1), vec![Scalar::f64(1.5), Scalar::i64(9)]), + (Timestamp(2), vec![Scalar::f64(2.5), Scalar::i64(8)]), ]; assert_eq!( f64_field(&rows, 0), @@ -328,7 +328,7 @@ mod tests { #[test] #[should_panic(expected = "not an f64 scalar")] fn f64_field_panics_on_kind_mismatch() { - let rows = vec![(Timestamp(1), vec![Scalar::I64(7)])]; + let rows = vec![(Timestamp(1), vec![Scalar::i64(7)])]; let _ = f64_field(&rows, 0); } diff --git a/crates/aura-engine/src/sweep.rs b/crates/aura-engine/src/sweep.rs index 0e28f51..ae1b53f 100644 --- a/crates/aura-engine/src/sweep.rs +++ b/crates/aura-engine/src/sweep.rs @@ -217,18 +217,18 @@ mod tests { let grid = GridSpace::new( &space, vec![ - vec![Scalar::I64(2), Scalar::I64(3)], - vec![Scalar::I64(4), Scalar::I64(5)], + vec![Scalar::i64(2), Scalar::i64(3)], + vec![Scalar::i64(4), Scalar::i64(5)], ], ) .expect("valid grid"); assert_eq!( grid.points(), vec![ - vec![Scalar::I64(2), Scalar::I64(4)], - vec![Scalar::I64(2), Scalar::I64(5)], - vec![Scalar::I64(3), Scalar::I64(4)], - vec![Scalar::I64(3), Scalar::I64(5)], + vec![Scalar::i64(2), Scalar::i64(4)], + vec![Scalar::i64(2), Scalar::i64(5)], + vec![Scalar::i64(3), Scalar::i64(4)], + vec![Scalar::i64(3), Scalar::i64(5)], ], ); } @@ -243,9 +243,9 @@ mod tests { let grid = GridSpace::new( &space, vec![ - vec![Scalar::I64(2), Scalar::I64(3)], - vec![Scalar::I64(4), Scalar::I64(5)], - vec![Scalar::F64(0.5)], + vec![Scalar::i64(2), Scalar::i64(3)], + vec![Scalar::i64(4), Scalar::i64(5)], + vec![Scalar::f64(0.5)], ], ) .expect("valid grid"); @@ -256,14 +256,14 @@ mod tests { #[test] fn arity_mismatch_is_an_error() { let space = i64_space(2); - let err = GridSpace::new(&space, vec![vec![Scalar::I64(2)]]).unwrap_err(); + let err = GridSpace::new(&space, vec![vec![Scalar::i64(2)]]).unwrap_err(); assert_eq!(err, SweepError::Arity { expected: 2, got: 1 }); } #[test] fn wrong_kind_is_a_kind_mismatch() { let space = i64_space(1); - let err = GridSpace::new(&space, vec![vec![Scalar::F64(1.0)]]).unwrap_err(); + let err = GridSpace::new(&space, vec![vec![Scalar::f64(1.0)]]).unwrap_err(); assert_eq!( err, SweepError::KindMismatch { @@ -312,9 +312,9 @@ mod tests { GridSpace::new( &space, vec![ - vec![Scalar::I64(2), Scalar::I64(3)], // fast ∈ {2, 3} - vec![Scalar::I64(4), Scalar::I64(5)], // slow ∈ {4, 5} - vec![Scalar::F64(0.5)], // scale ∈ {0.5} + vec![Scalar::i64(2), Scalar::i64(3)], // fast ∈ {2, 3} + vec![Scalar::i64(4), Scalar::i64(5)], // slow ∈ {4, 5} + vec![Scalar::f64(0.5)], // scale ∈ {0.5} ], ) .expect("grid matches the sample param-space") @@ -328,11 +328,11 @@ mod tests { // params carried in enumeration (odometer) order, self-describing assert_eq!( family.points[0].params, - vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)], + vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)], ); assert_eq!( family.points[3].params, - vec![Scalar::I64(3), Scalar::I64(5), Scalar::F64(0.5)], + vec![Scalar::i64(3), Scalar::i64(5), Scalar::f64(0.5)], ); // each point's metrics equal a direct, independent run of the same point: // the sweep adds enumeration + execution, never a metrics change (C1). @@ -361,7 +361,7 @@ mod tests { .map(|(ps, v)| (ps.name, v)) .collect(); assert_eq!(family.named_params(0), expected); - assert_eq!(family.named_params(0)[0].1, Scalar::I64(2)); + assert_eq!(family.named_params(0)[0].1, Scalar::i64(2)); } #[test] diff --git a/crates/aura-engine/src/test_fixtures.rs b/crates/aura-engine/src/test_fixtures.rs index f998e1f..6c65011 100644 --- a/crates/aura-engine/src/test_fixtures.rs +++ b/crates/aura-engine/src/test_fixtures.rs @@ -21,7 +21,7 @@ pub(crate) fn synthetic_prices() -> Vec<(Timestamp, Scalar)> { (7, 0.9990), ] .iter() - .map(|&(t, p)| (Timestamp(t), Scalar::F64(p))) + .map(|&(t, p)| (Timestamp(t), Scalar::f64(p))) .collect() } diff --git a/crates/aura-engine/src/walkforward.rs b/crates/aura-engine/src/walkforward.rs index 1ed2c29..4edf646 100644 --- a/crates/aura-engine/src/walkforward.rs +++ b/crates/aura-engine/src/walkforward.rs @@ -248,9 +248,9 @@ pub fn param_stability(result: &WalkForwardResult) -> Vec { /// scalar in a param slot is a wiring bug, surfaced like the engine's other /// "checked at wiring" violations. fn scalar_as_f64(s: Scalar) -> f64 { - match s { - Scalar::I64(n) => n as f64, - Scalar::F64(f) => f, + match s.kind() { + aura_core::ScalarKind::I64 => s.as_i64() as f64, + aura_core::ScalarKind::F64 => s.as_f64(), other => unreachable!("non-numeric param scalar: {other:?}"), } } @@ -292,7 +292,7 @@ mod tests { fn run_window_fixture(w: WindowBounds) -> WindowRun { let Timestamp(k) = w.oos.0; WindowRun { - chosen_params: vec![Scalar::I64(k % 3), Scalar::F64(k as f64 * 0.5)], + chosen_params: vec![Scalar::i64(k % 3), Scalar::f64(k as f64 * 0.5)], oos_equity: vec![(w.oos.0, k as f64 * 0.1), (w.oos.1, k as f64 * 0.1 + 1.0)], oos_report: dummy_report(), } @@ -373,7 +373,7 @@ mod tests { oos: (Timestamp(0), Timestamp(0)), }, run: WindowRun { - chosen_params: vec![Scalar::I64(a), Scalar::F64(b)], + chosen_params: vec![Scalar::i64(a), Scalar::f64(b)], oos_equity: vec![], oos_report: dummy_report(), }, diff --git a/crates/aura-ingest/src/lib.rs b/crates/aura-ingest/src/lib.rs index 196356a..1c6ee3f 100644 --- a/crates/aura-ingest/src/lib.rs +++ b/crates/aura-ingest/src/lib.rs @@ -63,7 +63,7 @@ impl M1Columns { } /// The close column as an engine source stream: each normalized timestamp - /// zipped with its close as a [`Scalar::F64`]. Ascending in timestamp iff + /// zipped with its close as a [`Scalar::f64`]. Ascending in timestamp iff /// the bars were (the C3 ingestion precondition `Harness::run` relies on). /// This is the price input the SMA-cross sample strategy consumes; a project /// that wants another field reads the public columns and zips its own. @@ -71,7 +71,7 @@ impl M1Columns { self.ts .iter() .zip(&self.close) - .map(|(&t, &v)| (t, Scalar::F64(v))) + .map(|(&t, &v)| (t, Scalar::f64(v))) .collect() } } @@ -147,12 +147,12 @@ pub enum M1Field { /// in isolation. Time is normalized ms→epoch-ns at this one seam (C3). fn decode(field: M1Field, bar: &M1Parsed) -> (Timestamp, Scalar) { let value = match field { - M1Field::Open => Scalar::F64(bar.open), - M1Field::High => Scalar::F64(bar.high), - M1Field::Low => Scalar::F64(bar.low), - M1Field::Close => Scalar::F64(bar.close), - M1Field::Spread => Scalar::F64(bar.spread), - M1Field::Volume => Scalar::I64(bar.volume), + M1Field::Open => Scalar::f64(bar.open), + M1Field::High => Scalar::f64(bar.high), + M1Field::Low => Scalar::f64(bar.low), + M1Field::Close => Scalar::f64(bar.close), + M1Field::Spread => Scalar::f64(bar.spread), + M1Field::Volume => Scalar::i64(bar.volume), }; (unix_ms_to_epoch_ns(bar.time_ms), value) } @@ -313,9 +313,9 @@ mod tests { assert_eq!( stream, vec![ - (Timestamp(1_000_000), Scalar::F64(10.0)), - (Timestamp(2_000_000), Scalar::F64(20.0)), - (Timestamp(3_000_000), Scalar::F64(30.0)), + (Timestamp(1_000_000), Scalar::f64(10.0)), + (Timestamp(2_000_000), Scalar::f64(20.0)), + (Timestamp(3_000_000), Scalar::f64(30.0)), ] ); // ascending in timestamp (the merge precondition). @@ -331,12 +331,12 @@ mod tests { fn decode_projects_each_field_to_its_scalar_kind() { // full_bar: open 1.0, high 2.0, low 0.5, close 1.5, spread 0.1, volume 100. let bar = full_bar(1_000); - assert_eq!(decode(M1Field::Open, &bar), (Timestamp(1_000_000_000), Scalar::F64(1.0))); - assert_eq!(decode(M1Field::High, &bar), (Timestamp(1_000_000_000), Scalar::F64(2.0))); - assert_eq!(decode(M1Field::Low, &bar), (Timestamp(1_000_000_000), Scalar::F64(0.5))); - assert_eq!(decode(M1Field::Close, &bar), (Timestamp(1_000_000_000), Scalar::F64(1.5))); - assert_eq!(decode(M1Field::Spread, &bar), (Timestamp(1_000_000_000), Scalar::F64(0.1))); + assert_eq!(decode(M1Field::Open, &bar), (Timestamp(1_000_000_000), Scalar::f64(1.0))); + assert_eq!(decode(M1Field::High, &bar), (Timestamp(1_000_000_000), Scalar::f64(2.0))); + assert_eq!(decode(M1Field::Low, &bar), (Timestamp(1_000_000_000), Scalar::f64(0.5))); + assert_eq!(decode(M1Field::Close, &bar), (Timestamp(1_000_000_000), Scalar::f64(1.5))); + assert_eq!(decode(M1Field::Spread, &bar), (Timestamp(1_000_000_000), Scalar::f64(0.1))); // volume is the one i64 column. - assert_eq!(decode(M1Field::Volume, &bar), (Timestamp(1_000_000_000), Scalar::I64(100))); + assert_eq!(decode(M1Field::Volume, &bar), (Timestamp(1_000_000_000), Scalar::i64(100))); } } diff --git a/crates/aura-ingest/tests/streaming_seam.rs b/crates/aura-ingest/tests/streaming_seam.rs index 58a0595..a612db9 100644 --- a/crates/aura-ingest/tests/streaming_seam.rs +++ b/crates/aura-ingest/tests/streaming_seam.rs @@ -178,12 +178,12 @@ fn two_field_sources_share_one_window() { let mut n_close = 0usize; while let Some((_, v)) = Source::next(&mut close) { - assert!(matches!(v, Scalar::F64(_)), "close is an f64 column"); + assert_eq!(v.kind(), ScalarKind::F64, "close is an f64 column"); n_close += 1; } let mut n_volume = 0usize; while let Some((_, v)) = Source::next(&mut volume) { - assert!(matches!(v, Scalar::I64(_)), "volume is an i64 column"); + assert_eq!(v.kind(), ScalarKind::I64, "volume is an i64 column"); n_volume += 1; } assert_eq!(n_close, n_volume, "both fields share the same ts axis ⇒ same count"); diff --git a/crates/aura-registry/src/lib.rs b/crates/aura-registry/src/lib.rs index 97caf0c..2d8c93e 100644 --- a/crates/aura-registry/src/lib.rs +++ b/crates/aura-registry/src/lib.rs @@ -272,7 +272,7 @@ mod tests { // total_pips (3.0); the EARLIER of the two (index 1, params p=10) must // win, not the later (index 3, params p=30) — that pins the tie rule. let point = |p: f64, pips: f64| SweepPoint { - params: vec![Scalar::F64(p)], + params: vec![Scalar::f64(p)], report: report_with(pips, 0.5, 0), }; let family = SweepFamily { @@ -291,7 +291,7 @@ mod tests { assert_eq!(winner.report.metrics.total_pips, 3.0); // ...and ties resolve to the earliest enumeration-order point: its params // identify point 1, not point 3. - assert_eq!(winner.params, vec![Scalar::F64(10.0)]); + assert_eq!(winner.params, vec![Scalar::f64(10.0)]); // the whole winning point is returned, params AND report together. assert_eq!(winner, family.points[1]); } diff --git a/crates/aura-std/src/add.rs b/crates/aura-std/src/add.rs index d1df682..84d8031 100644 --- a/crates/aura-std/src/add.rs +++ b/crates/aura-std/src/add.rs @@ -23,7 +23,7 @@ pub struct Add { impl Add { /// Build an `Add` node. pub fn new() -> Self { - Self { out: [Scalar::F64(0.0)] } + Self { out: [Scalar::f64(0.0)] } } /// The param-generic recipe for a blueprint primitive: paramless, builds through @@ -61,7 +61,7 @@ impl Node for Add { if a.is_empty() || b.is_empty() { return None; } - self.out[0] = Scalar::F64(a[0] + b[0]); + self.out[0] = Scalar::f64(a[0] + b[0]); Some(&self.out) } @@ -84,12 +84,12 @@ mod tests { ]; // only input 0 present -> None - inputs[0].push(Scalar::F64(10.0)).unwrap(); + inputs[0].push(Scalar::f64(10.0)).unwrap(); assert_eq!(add.eval(Ctx::new(&inputs, Timestamp(0))), None); // both present -> a + b - inputs[1].push(Scalar::F64(4.0)).unwrap(); - assert_eq!(add.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(14.0)].as_slice())); + inputs[1].push(Scalar::f64(4.0)).unwrap(); + assert_eq!(add.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::f64(14.0)].as_slice())); } #[test] diff --git a/crates/aura-std/src/ema.rs b/crates/aura-std/src/ema.rs index a3e1ebc..0d75d97 100644 --- a/crates/aura-std/src/ema.rs +++ b/crates/aura-std/src/ema.rs @@ -49,7 +49,7 @@ impl Ema { warmup_sum: 0.0, count: 0, ema: 0.0, - out: [Scalar::F64(0.0)], + out: [Scalar::f64(0.0)], } } @@ -64,7 +64,7 @@ impl Ema { output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], }, - |p| Box::new(Ema::new(p[0].as_i64().expect("length slot is I64") as usize)), + |p| Box::new(Ema::new(p[0].as_i64() as usize)), ) } } @@ -96,7 +96,7 @@ impl Node for Ema { // O(1) recurrence: one sub, one mul, one add. self.ema += self.alpha * (x - self.ema); } - self.out[0] = Scalar::F64(self.ema); + self.out[0] = Scalar::f64(self.ema); Some(&self.out) } @@ -124,11 +124,11 @@ mod tests { let expect = [None, None, Some(4.0), Some(7.0)]; for (v, want) in feed.iter().zip(expect) { - inputs[0].push(Scalar::F64(*v)).unwrap(); + inputs[0].push(Scalar::f64(*v)).unwrap(); let got = ema.eval(Ctx::new(&inputs, Timestamp(0))); match want { None => assert_eq!(got, None), - Some(m) => assert_eq!(got, Some([Scalar::F64(m)].as_slice())), + Some(m) => assert_eq!(got, Some([Scalar::f64(m)].as_slice())), } } } @@ -140,11 +140,11 @@ mod tests { let mut ema = Ema::new(1); let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)]; - inputs[0].push(Scalar::F64(7.0)).unwrap(); - assert_eq!(ema.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(7.0)].as_slice())); + inputs[0].push(Scalar::f64(7.0)).unwrap(); + assert_eq!(ema.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::f64(7.0)].as_slice())); - inputs[0].push(Scalar::F64(9.0)).unwrap(); - assert_eq!(ema.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(9.0)].as_slice())); + inputs[0].push(Scalar::f64(9.0)).unwrap(); + assert_eq!(ema.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::f64(9.0)].as_slice())); } #[test] diff --git a/crates/aura-std/src/exposure.rs b/crates/aura-std/src/exposure.rs index 0d440cb..d41d709 100644 --- a/crates/aura-std/src/exposure.rs +++ b/crates/aura-std/src/exposure.rs @@ -19,7 +19,7 @@ impl Exposure { /// Build an exposure node with saturation magnitude `scale` (must be > 0). pub fn new(scale: f64) -> Self { assert!(scale > 0.0, "Exposure scale must be > 0"); - Self { scale, out: [Scalar::F64(0.0)] } + Self { scale, out: [Scalar::f64(0.0)] } } /// The param-generic recipe for a blueprint primitive: declares `scale` and builds @@ -33,7 +33,7 @@ impl Exposure { output: vec![FieldSpec { name: "exposure".into(), kind: ScalarKind::F64 }], params: vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }], }, - |p| Box::new(Exposure::new(p[0].as_f64().expect("scale slot is F64"))), + |p| Box::new(Exposure::new(p[0].as_f64())), ) } } @@ -48,7 +48,7 @@ impl Node for Exposure { if w.is_empty() { return None; // not yet warmed up (C8 filter) } - self.out[0] = Scalar::F64((w[0] / self.scale).clamp(-1.0, 1.0)); + self.out[0] = Scalar::f64((w[0] / self.scale).clamp(-1.0, 1.0)); Some(&self.out) } @@ -75,10 +75,10 @@ mod tests { (-1.0, -1.0), // saturates low ]; for (sig, want) in cases { - inputs[0].push(Scalar::F64(sig)).unwrap(); + inputs[0].push(Scalar::f64(sig)).unwrap(); assert_eq!( e.eval(Ctx::new(&inputs, Timestamp(0))), - Some([Scalar::F64(want)].as_slice()) + Some([Scalar::f64(want)].as_slice()) ); } } diff --git a/crates/aura-std/src/lincomb.rs b/crates/aura-std/src/lincomb.rs index e132f70..6ecc3da 100644 --- a/crates/aura-std/src/lincomb.rs +++ b/crates/aura-std/src/lincomb.rs @@ -37,7 +37,7 @@ impl LinComb { /// Panics if `weights` is empty. pub fn new(weights: Vec) -> Self { assert!(!weights.is_empty(), "LinComb needs at least one weight"); - Self { weights, out: [Scalar::F64(0.0)] } + Self { weights, out: [Scalar::f64(0.0)] } } /// The param-generic recipe for a blueprint primitive. The `arity` is topology @@ -54,7 +54,7 @@ impl LinComb { "LinComb", NodeSchema { inputs, output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params }, |p| Box::new(LinComb::new( - p.iter().map(|s| s.as_f64().expect("weight slot is F64")).collect(), + p.iter().map(|s| s.as_f64()).collect(), )), ) } @@ -74,7 +74,7 @@ impl Node for LinComb { } acc += w * w_in[0]; } - self.out[0] = Scalar::F64(acc); + self.out[0] = Scalar::f64(acc); Some(&self.out) } @@ -97,12 +97,12 @@ mod tests { ]; // only input 0 present -> None - inputs[0].push(Scalar::F64(10.0)).unwrap(); + inputs[0].push(Scalar::f64(10.0)).unwrap(); assert_eq!(lc.eval(Ctx::new(&inputs, Timestamp(0))), None); // both present -> 0.5*10 + 2.0*3 = 11.0 - inputs[1].push(Scalar::F64(3.0)).unwrap(); - assert_eq!(lc.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(11.0)].as_slice())); + inputs[1].push(Scalar::f64(3.0)).unwrap(); + assert_eq!(lc.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::f64(11.0)].as_slice())); } #[test] @@ -112,10 +112,10 @@ mod tests { AnyColumn::with_capacity(ScalarKind::F64, 1), AnyColumn::with_capacity(ScalarKind::F64, 1), ]; - inputs[0].push(Scalar::F64(7.0)).unwrap(); - inputs[1].push(Scalar::F64(5.0)).unwrap(); + inputs[0].push(Scalar::f64(7.0)).unwrap(); + inputs[1].push(Scalar::f64(5.0)).unwrap(); // unit weights reproduce Add: 7 + 5 - assert_eq!(lc.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(12.0)].as_slice())); + assert_eq!(lc.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::f64(12.0)].as_slice())); } #[test] @@ -126,14 +126,14 @@ mod tests { AnyColumn::with_capacity(ScalarKind::F64, 1), AnyColumn::with_capacity(ScalarKind::F64, 1), ]; - inputs[0].push(Scalar::F64(1.0)).unwrap(); - inputs[1].push(Scalar::F64(2.0)).unwrap(); + inputs[0].push(Scalar::f64(1.0)).unwrap(); + inputs[1].push(Scalar::f64(2.0)).unwrap(); // third leg still cold -> None (withheld until every leg is present) assert_eq!(lc.eval(Ctx::new(&inputs, Timestamp(0))), None); - inputs[2].push(Scalar::F64(3.0)).unwrap(); + inputs[2].push(Scalar::f64(3.0)).unwrap(); // all warm -> 1 + 2 + 3 - assert_eq!(lc.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(6.0)].as_slice())); + assert_eq!(lc.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::f64(6.0)].as_slice())); } #[test] @@ -153,33 +153,33 @@ mod tests { fn chained_bind_reconstructs_positional_vector() { // bind BOTH weights, in reverse slot order, to DISTINCT values; build empty. let builder = LinComb::builder(2) - .bind("weights[1]", Scalar::F64(2.0)) - .bind("weights[0]", Scalar::F64(0.5)); + .bind("weights[1]", Scalar::f64(2.0)) + .bind("weights[0]", Scalar::f64(0.5)); assert!(builder.params().is_empty()); let mut lc = builder.build(&[]); let mut inputs = vec![ AnyColumn::with_capacity(ScalarKind::F64, 1), AnyColumn::with_capacity(ScalarKind::F64, 1), ]; - inputs[0].push(Scalar::F64(10.0)).unwrap(); - inputs[1].push(Scalar::F64(3.0)).unwrap(); + inputs[0].push(Scalar::f64(10.0)).unwrap(); + inputs[1].push(Scalar::f64(3.0)).unwrap(); // 0.5*10 + 2.0*3 = 11.0 — holds ONLY if each weight landed in its right slot // (a swap would give 2.0*10 + 0.5*3 = 21.5) - assert_eq!(lc.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(11.0)].as_slice())); + assert_eq!(lc.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::f64(11.0)].as_slice())); // partial: bind weights[0], leave weights[1] open → inject it at build - let partial = LinComb::builder(2).bind("weights[0]", Scalar::F64(0.5)); + let partial = LinComb::builder(2).bind("weights[0]", Scalar::f64(0.5)); assert_eq!( partial.params().iter().map(|p| p.name.as_str()).collect::>(), ["weights[1]"], ); - let mut lc2 = partial.build(&[Scalar::F64(2.0)]); // weights[1] = 2.0 injected + let mut lc2 = partial.build(&[Scalar::f64(2.0)]); // weights[1] = 2.0 injected let mut inputs2 = vec![ AnyColumn::with_capacity(ScalarKind::F64, 1), AnyColumn::with_capacity(ScalarKind::F64, 1), ]; - inputs2[0].push(Scalar::F64(10.0)).unwrap(); - inputs2[1].push(Scalar::F64(3.0)).unwrap(); - assert_eq!(lc2.eval(Ctx::new(&inputs2, Timestamp(0))), Some([Scalar::F64(11.0)].as_slice())); + inputs2[0].push(Scalar::f64(10.0)).unwrap(); + inputs2[1].push(Scalar::f64(3.0)).unwrap(); + assert_eq!(lc2.eval(Ctx::new(&inputs2, Timestamp(0))), Some([Scalar::f64(11.0)].as_slice())); } } diff --git a/crates/aura-std/src/recorder.rs b/crates/aura-std/src/recorder.rs index dc95657..36571b7 100644 --- a/crates/aura-std/src/recorder.rs +++ b/crates/aura-std/src/recorder.rs @@ -62,10 +62,10 @@ impl Node for Recorder { for (i, &kind) in self.kinds.iter().enumerate() { // newest of each column by kind; `?` returns None (warm-up) if cold. let scalar = match kind { - ScalarKind::F64 => Scalar::F64(ctx.f64_in(i).get(0)?), - ScalarKind::I64 => Scalar::I64(ctx.i64_in(i).get(0)?), - ScalarKind::Bool => Scalar::Bool(ctx.bool_in(i).get(0)?), - ScalarKind::Timestamp => Scalar::Ts(ctx.ts_in(i).get(0)?), + ScalarKind::F64 => Scalar::f64(ctx.f64_in(i).get(0)?), + ScalarKind::I64 => Scalar::i64(ctx.i64_in(i).get(0)?), + ScalarKind::Bool => Scalar::bool(ctx.bool_in(i).get(0)?), + ScalarKind::Timestamp => Scalar::ts(ctx.ts_in(i).get(0)?), }; row.push(scalar); } @@ -107,16 +107,16 @@ mod tests { // warm: returns None (pure consumer) but records (now, [F64(newest)]). for (t, v) in [(2_i64, 10.0_f64), (3, 20.0), (4, 30.0)] { - inputs[0].push(Scalar::F64(v)).unwrap(); + inputs[0].push(Scalar::f64(v)).unwrap(); assert_eq!(rec.eval(Ctx::new(&inputs, Timestamp(t))), None); } let rows: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); assert_eq!( rows, vec![ - (Timestamp(2), vec![Scalar::F64(10.0)]), - (Timestamp(3), vec![Scalar::F64(20.0)]), - (Timestamp(4), vec![Scalar::F64(30.0)]), + (Timestamp(2), vec![Scalar::f64(10.0)]), + (Timestamp(3), vec![Scalar::f64(20.0)]), + (Timestamp(4), vec![Scalar::f64(30.0)]), ] ); } @@ -131,15 +131,15 @@ mod tests { ]; // only column 0 present -> None, nothing recorded. - inputs[0].push(Scalar::F64(1.0)).unwrap(); + inputs[0].push(Scalar::f64(1.0)).unwrap(); assert_eq!(rec.eval(Ctx::new(&inputs, Timestamp(1))), None); assert!(rx.try_recv().is_err()); // both present -> records the full row (still returns None). - inputs[1].push(Scalar::F64(2.0)).unwrap(); + inputs[1].push(Scalar::f64(2.0)).unwrap(); assert_eq!(rec.eval(Ctx::new(&inputs, Timestamp(2))), None); let rows: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); - assert_eq!(rows, vec![(Timestamp(2), vec![Scalar::F64(1.0), Scalar::F64(2.0)])]); + assert_eq!(rows, vec![(Timestamp(2), vec![Scalar::f64(1.0), Scalar::f64(2.0)])]); } #[test] diff --git a/crates/aura-std/src/sim_broker.rs b/crates/aura-std/src/sim_broker.rs index 49dfd44..9931c9d 100644 --- a/crates/aura-std/src/sim_broker.rs +++ b/crates/aura-std/src/sim_broker.rs @@ -51,7 +51,7 @@ impl SimBroker { prev_price: None, prev_exposure: 0.0, cum: 0.0, - out: [Scalar::F64(0.0)], + out: [Scalar::f64(0.0)], } } @@ -91,7 +91,7 @@ impl Node for SimBroker { } self.prev_price = Some(price); self.prev_exposure = expo; // update AFTER taking PnL — no look-ahead (C2) - self.out[0] = Scalar::F64(self.cum); + self.out[0] = Scalar::f64(self.cum); Some(&self.out) } @@ -117,11 +117,11 @@ mod tests { // stays empty) and a price into slot 1, then eval and return the equity. fn step(b: &mut SimBroker, inputs: &mut [AnyColumn], expo: Option, price: f64) -> f64 { if let Some(e) = expo { - inputs[0].push(Scalar::F64(e)).unwrap(); + inputs[0].push(Scalar::f64(e)).unwrap(); } - inputs[1].push(Scalar::F64(price)).unwrap(); + inputs[1].push(Scalar::f64(price)).unwrap(); match b.eval(Ctx::new(inputs, Timestamp(0))) { - Some([Scalar::F64(v)]) => *v, + Some([s]) => s.as_f64(), other => panic!("expected Some([F64]), got {other:?}"), } } diff --git a/crates/aura-std/src/sma.rs b/crates/aura-std/src/sma.rs index 96350de..a76909c 100644 --- a/crates/aura-std/src/sma.rs +++ b/crates/aura-std/src/sma.rs @@ -18,7 +18,7 @@ impl Sma { /// Build an SMA of window `length` (must be >= 1). pub fn new(length: usize) -> Self { assert!(length >= 1, "SMA length must be >= 1"); - Self { length, out: [Scalar::F64(0.0)] } + Self { length, out: [Scalar::f64(0.0)] } } /// The param-generic recipe for a blueprint primitive: declares `length` and builds @@ -32,7 +32,7 @@ impl Sma { output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], }, - |p| Box::new(Sma::new(p[0].as_i64().expect("length slot is I64") as usize)), + |p| Box::new(Sma::new(p[0].as_i64() as usize)), ) } } @@ -51,7 +51,7 @@ impl Node for Sma { for k in 0..self.length { sum += w[k]; // index 0 = newest (financial indexing) } - self.out[0] = Scalar::F64(sum / self.length as f64); + self.out[0] = Scalar::f64(sum / self.length as f64); Some(&self.out) } @@ -79,11 +79,11 @@ mod tests { let expect = [None, None, Some(2.0), Some(3.0), Some(4.0)]; for (v, want) in feed.iter().zip(expect) { - inputs[0].push(Scalar::F64(*v)).unwrap(); + inputs[0].push(Scalar::f64(*v)).unwrap(); let got = sma.eval(Ctx::new(&inputs, Timestamp(0))); match want { None => assert_eq!(got, None), - Some(m) => assert_eq!(got, Some([Scalar::F64(m)].as_slice())), + Some(m) => assert_eq!(got, Some([Scalar::f64(m)].as_slice())), } } } @@ -93,11 +93,11 @@ mod tests { let mut sma = Sma::new(1); let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)]; - inputs[0].push(Scalar::F64(7.0)).unwrap(); - assert_eq!(sma.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(7.0)].as_slice())); + inputs[0].push(Scalar::f64(7.0)).unwrap(); + assert_eq!(sma.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::f64(7.0)].as_slice())); - inputs[0].push(Scalar::F64(9.0)).unwrap(); - assert_eq!(sma.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(9.0)].as_slice())); + inputs[0].push(Scalar::f64(9.0)).unwrap(); + assert_eq!(sma.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::f64(9.0)].as_slice())); } #[test] @@ -160,7 +160,7 @@ mod tests { fn bind_removes_slot_from_param_space() { // a bound param-bearing node reports an empty param surface — parity with the // SimBroker precedent (nodes_declare_expected_params, this file) - let sma2 = Sma::builder().named("bias").bind("length", Scalar::I64(2)); + let sma2 = Sma::builder().named("bias").bind("length", Scalar::i64(2)); assert!(sma2.schema().params.is_empty()); // contrast: the length-generic SMA keeps `length` open assert_eq!(Sma::builder().named("bias").params().len(), 1); @@ -169,7 +169,7 @@ mod tests { #[test] fn bound_node_builds_with_injected_value() { // built with an empty open slice, the bound builder yields SMA(2) - let node = Sma::builder().bind("length", Scalar::I64(2)).build(&[]); + let node = Sma::builder().bind("length", Scalar::i64(2)).build(&[]); assert_eq!(node.label(), "SMA(2)"); } } diff --git a/crates/aura-std/src/sub.rs b/crates/aura-std/src/sub.rs index 4b37d5e..eaae6d1 100644 --- a/crates/aura-std/src/sub.rs +++ b/crates/aura-std/src/sub.rs @@ -14,7 +14,7 @@ pub struct Sub { impl Sub { /// Build a `Sub` node. pub fn new() -> Self { - Self { out: [Scalar::F64(0.0)] } + Self { out: [Scalar::f64(0.0)] } } /// The param-generic recipe for a blueprint primitive: paramless, builds through @@ -52,7 +52,7 @@ impl Node for Sub { if a.is_empty() || b.is_empty() { return None; } - self.out[0] = Scalar::F64(a[0] - b[0]); + self.out[0] = Scalar::f64(a[0] - b[0]); Some(&self.out) } @@ -75,12 +75,12 @@ mod tests { ]; // only input 0 present -> None - inputs[0].push(Scalar::F64(10.0)).unwrap(); + inputs[0].push(Scalar::f64(10.0)).unwrap(); assert_eq!(sub.eval(Ctx::new(&inputs, Timestamp(0))), None); // both present -> a - b - inputs[1].push(Scalar::F64(4.0)).unwrap(); - assert_eq!(sub.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(6.0)].as_slice())); + inputs[1].push(Scalar::f64(4.0)).unwrap(); + assert_eq!(sub.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::f64(6.0)].as_slice())); } #[test]