4b64409036
Cycle B of milestone "The World — parameter-space & sweep". A blueprint is now value-empty: a leaf holds a param-generic recipe, not a built node, and a positional Scalar vector is bound slot-by-slot at bootstrap — so one blueprint bootstraps into many distinct instances under different vectors, with no cdylib rebuild (C12/C19). This is the binding primitive a sweep (#32) drives. Ratified design (brainstorm): value-empty reconstruct-through-new() over mutate-in-place and over a default-bearing variant. The value lives only in the injected vector (no baked default), keeping the blueprint a pure param-generic recipe (C19); every injected value flows through the node's own constructor (the single sizing/validation gate). - aura-core: LeafFactory { name, params, build } — the recipe (params -> sized node through `new`); Scalar::as_i64/as_f64 value accessors. Node trait unchanged. - aura-std: each of the 7 nodes exposes factory() (SMA length:I64, Exposure scale:F64, LinComb arity x weights[i]:F64; Sub/Add/SimBroker/Recorder paramless, capturing their non-param construction args — pip_size, the Recorder channel). - aura-engine: BlueprintNode::Leaf(LeafFactory), From<LeafFactory>; param_space() reads factory.params() pre-build; compile_with_params/bootstrap_with_params build each leaf from its kind-checked slice while lowering (build-then-wire), arity checked up front via param_space().len(); CompileError::{ParamKindMismatch, ParamArity}. compile/inline/edge-rewrite are structurally unchanged, so the compilat stays bit-identical for a given point (the bit-identity and both param_space mirror tests stay green). The vestigial pre-build Composite::schema / BlueprintNode::schema (no live caller — interface resolution is structural on the built flat nodes) are removed. - aura-cli: the blueprint view labels leaves by bare type via LeafFactory::label() (`[SMA]`) — the ascii-dag renderer cannot render wide cluster-sibling labels (see spec); the compiled view still labels valued (`SMA(2)`). The sample supplies its point as a vector; the mis-wiring swap moved to the compiled view. The #34 dual-traversal drift hazard is subsumed: compile_with_params consumes the vector in the same recipe walk param_space() reports, so the two share one traversal. Verified: cargo build/test --workspace green (127 tests, incl. bit-identity, both mirror tests, and 4 new injection tests — different-vector-different-run, kind mismatch, arity, determinism); clippy --workspace --all-targets -D warnings clean; `aura graph` blueprint view renders cleanly. Scope: one vector -> one instance. Deferred: sweep enumeration (#32), domain validation (#32/C20), single-run authoring convenience (#35). closes #31
112 lines
3.2 KiB
Rust
112 lines
3.2 KiB
Rust
//! 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.
|
|
|
|
/// Canonical engine time: epoch-nanoseconds UTC. Newtype over `i64` (C7).
|
|
/// `Default` (epoch 0) lets `Column<Timestamp>` pre-size its ring.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub struct Timestamp(pub i64);
|
|
|
|
/// The kind tag of a scalar / column, used for the edge-time type check (C7).
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum ScalarKind {
|
|
I64,
|
|
F64,
|
|
Bool,
|
|
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),
|
|
}
|
|
|
|
impl Scalar {
|
|
/// The kind tag of this scalar.
|
|
pub fn kind(self) -> ScalarKind {
|
|
match self {
|
|
Scalar::I64(_) => ScalarKind::I64,
|
|
Scalar::F64(_) => ScalarKind::F64,
|
|
Scalar::Bool(_) => ScalarKind::Bool,
|
|
Scalar::Ts(_) => ScalarKind::Timestamp,
|
|
}
|
|
}
|
|
/// The `i64` payload, or `None` if this scalar is not an `I64`.
|
|
pub fn as_i64(self) -> Option<i64> {
|
|
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<f64> {
|
|
if let Scalar::F64(v) = self { Some(v) } else { None }
|
|
}
|
|
}
|
|
|
|
impl From<i64> for Scalar {
|
|
fn from(v: i64) -> Self {
|
|
Scalar::I64(v)
|
|
}
|
|
}
|
|
impl From<f64> for Scalar {
|
|
fn from(v: f64) -> Self {
|
|
Scalar::F64(v)
|
|
}
|
|
}
|
|
impl From<bool> for Scalar {
|
|
fn from(v: bool) -> Self {
|
|
Scalar::Bool(v)
|
|
}
|
|
}
|
|
impl From<Timestamp> for Scalar {
|
|
fn from(v: Timestamp) -> Self {
|
|
Scalar::Ts(v)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[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)));
|
|
}
|
|
|
|
#[test]
|
|
fn timestamp_orders_and_defaults() {
|
|
assert!(Timestamp(1) < Timestamp(2));
|
|
assert_eq!(Timestamp::default(), Timestamp(0));
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_is_copy() {
|
|
let s = Scalar::F64(1.0);
|
|
let a = s;
|
|
let b = s; // Copy: `s` still usable after move-by-value
|
|
assert_eq!(a, b);
|
|
assert_eq!(s, a);
|
|
}
|
|
}
|