Files
Aura/crates/aura-core/src/scalar.rs
T
Brummel e97906a0e1 test(aura-core): pin bare-literal Into<Scalar> kind inference
Authoring layers that lower raw literals via `impl Into<Scalar>` (the
named-param-binding surface, spec 0030 / #35) rely on a bare integer
literal inferring as i64 and a bare float as f64 — because i64/f64/bool
are the only `From<…> for Scalar` impls, each literal class resolves to
exactly one. This was true but untested: `from_widenings` uses suffixed
literals (`7i64`), so a regression in the inference path would survive it.

Behaviour-preserving pin of existing aura-core behaviour. Closes the one
grounding gap the grounding-check flagged on spec 0030; also guards the
property independently — adding a second integer `From` impl would now
break loudly here rather than silently at a `.with("knob", 2)` call site.

refs #35
2026-06-10 23:23:27 +02:00

138 lines
4.5 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, serde::Serialize, serde::Deserialize)]
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 bare_literal_infers_kind_through_into_scalar() {
// Authoring layers that lower raw literals via `impl Into<Scalar>` rely on
// a bare (unsuffixed) integer literal inferring as `i64` and a bare float
// literal as `f64` — because `i64`/`f64`/`bool` are the only `From` impls,
// each literal class resolves to exactly one of them. Adding a second
// integer `From` impl (e.g. `From<i32>`) would make `lower(2)` ambiguous
// and break this, loudly, here rather than silently at the call site.
fn lower(v: impl Into<Scalar>) -> Scalar {
v.into()
}
assert_eq!(lower(2), Scalar::I64(2));
assert_eq!(lower(0.5), Scalar::F64(0.5));
}
#[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);
}
#[test]
fn timestamp_serde_round_trips() {
let ts = Timestamp(1_700_000_000_000_000_000);
let json = serde_json::to_string(&ts).expect("serialize Timestamp");
// a newtype struct serializes transparently as its inner i64 — this is
// what lets RunManifest's `window` render as a [from, to] integer array
assert_eq!(json, "1700000000000000000");
let back: Timestamp = serde_json::from_str(&json).expect("deserialize Timestamp");
assert_eq!(back, ts);
}
}