Files
Aura/crates/aura-core/src/scalar.rs
T
Brummel 4b64409036 feat(aura-core,aura-std,aura-engine,aura-cli): inject a param-set vector at bootstrap (#31)
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
2026-06-07 21:04:52 +02:00

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);
}
}