e97906a0e1
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
138 lines
4.5 KiB
Rust
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);
|
|
}
|
|
}
|