Skip to main content

shape_value/
scalar.rs

1//! TypedScalar: type-preserving scalar boundary contract for VM↔JIT exchange.
2//!
3//! When scalar values cross the VM/JIT boundary, their type identity (int vs
4//! float) must be preserved. `TypedScalar` carries an explicit `ScalarKind`
5//! discriminator alongside the raw payload bits.
6
7use crate::slot::ValueSlot;
8
9/// Scalar type discriminator.
10///
11/// Covers all width-specific numeric types plus bool/none/unit sentinels.
12/// Discriminant values are part of the ABI — do not reorder.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[repr(u8)]
15pub enum ScalarKind {
16    I8 = 0,
17    U8 = 1,
18    I16 = 2,
19    U16 = 3,
20    I32 = 4,
21    U32 = 5,
22    I64 = 6,
23    U64 = 7,
24    I128 = 8,
25    U128 = 9,
26    F32 = 10,
27    F64 = 11,
28    Bool = 12,
29    None = 13,
30    Unit = 14,
31}
32
33impl ScalarKind {
34    /// True if this kind represents an integer type (signed or unsigned).
35    #[inline]
36    pub fn is_integer(self) -> bool {
37        matches!(
38            self,
39            ScalarKind::I8
40                | ScalarKind::U8
41                | ScalarKind::I16
42                | ScalarKind::U16
43                | ScalarKind::I32
44                | ScalarKind::U32
45                | ScalarKind::I64
46                | ScalarKind::U64
47                | ScalarKind::I128
48                | ScalarKind::U128
49        )
50    }
51
52    /// True if this kind represents a floating-point type.
53    #[inline]
54    pub fn is_float(self) -> bool {
55        matches!(self, ScalarKind::F32 | ScalarKind::F64)
56    }
57
58    /// True if this kind represents a numeric type (integer or float).
59    #[inline]
60    pub fn is_numeric(self) -> bool {
61        self.is_integer() || self.is_float()
62    }
63
64    /// True if this kind represents an unsigned integer type.
65    #[inline]
66    pub fn is_unsigned_integer(self) -> bool {
67        matches!(
68            self,
69            ScalarKind::U8 | ScalarKind::U16 | ScalarKind::U32 | ScalarKind::U64 | ScalarKind::U128
70        )
71    }
72
73    /// True if this kind represents a signed integer type.
74    #[inline]
75    pub fn is_signed_integer(self) -> bool {
76        matches!(
77            self,
78            ScalarKind::I8 | ScalarKind::I16 | ScalarKind::I32 | ScalarKind::I64 | ScalarKind::I128
79        )
80    }
81}
82
83/// Type-preserving scalar value for VM↔JIT boundary exchange.
84///
85/// Carries an explicit type discriminator (`kind`) so that integer 42 and
86/// float 42.0 are distinguishable even when their f64 bit patterns would
87/// be identical.
88///
89/// `payload_hi` is zero for all types smaller than 128 bits.
90#[derive(Debug, Clone, Copy, PartialEq)]
91#[repr(C)]
92pub struct TypedScalar {
93    pub kind: ScalarKind,
94    pub payload_lo: u64,
95    /// Second 64-bit word — only used for I128/U128. Zero otherwise.
96    pub payload_hi: u64,
97}
98
99impl TypedScalar {
100    /// Create an I64 scalar.
101    #[inline]
102    pub fn i64(v: i64) -> Self {
103        Self {
104            kind: ScalarKind::I64,
105            payload_lo: v as u64,
106            payload_hi: 0,
107        }
108    }
109
110    /// Create an F64 scalar from a value.
111    #[inline]
112    pub fn f64(v: f64) -> Self {
113        Self {
114            kind: ScalarKind::F64,
115            payload_lo: v.to_bits(),
116            payload_hi: 0,
117        }
118    }
119
120    /// Create an F64 scalar from raw bits.
121    #[inline]
122    pub fn f64_from_bits(bits: u64) -> Self {
123        Self {
124            kind: ScalarKind::F64,
125            payload_lo: bits,
126            payload_hi: 0,
127        }
128    }
129
130    /// Create a Bool scalar.
131    #[inline]
132    pub fn bool(v: bool) -> Self {
133        Self {
134            kind: ScalarKind::Bool,
135            payload_lo: v as u64,
136            payload_hi: 0,
137        }
138    }
139
140    /// Create a None scalar.
141    #[inline]
142    pub fn none() -> Self {
143        Self {
144            kind: ScalarKind::None,
145            payload_lo: 0,
146            payload_hi: 0,
147        }
148    }
149
150    /// Create a Unit scalar.
151    #[inline]
152    pub fn unit() -> Self {
153        Self {
154            kind: ScalarKind::Unit,
155            payload_lo: 0,
156            payload_hi: 0,
157        }
158    }
159
160    /// Create an I8 scalar.
161    #[inline]
162    pub fn i8(v: i8) -> Self {
163        Self {
164            kind: ScalarKind::I8,
165            payload_lo: v as i64 as u64,
166            payload_hi: 0,
167        }
168    }
169
170    /// Create a U8 scalar.
171    #[inline]
172    pub fn u8(v: u8) -> Self {
173        Self {
174            kind: ScalarKind::U8,
175            payload_lo: v as u64,
176            payload_hi: 0,
177        }
178    }
179
180    /// Create an I16 scalar.
181    #[inline]
182    pub fn i16(v: i16) -> Self {
183        Self {
184            kind: ScalarKind::I16,
185            payload_lo: v as i64 as u64,
186            payload_hi: 0,
187        }
188    }
189
190    /// Create a U16 scalar.
191    #[inline]
192    pub fn u16(v: u16) -> Self {
193        Self {
194            kind: ScalarKind::U16,
195            payload_lo: v as u64,
196            payload_hi: 0,
197        }
198    }
199
200    /// Create an I32 scalar.
201    #[inline]
202    pub fn i32(v: i32) -> Self {
203        Self {
204            kind: ScalarKind::I32,
205            payload_lo: v as i64 as u64,
206            payload_hi: 0,
207        }
208    }
209
210    /// Create a U32 scalar.
211    #[inline]
212    pub fn u32(v: u32) -> Self {
213        Self {
214            kind: ScalarKind::U32,
215            payload_lo: v as u64,
216            payload_hi: 0,
217        }
218    }
219
220    /// Create a U64 scalar.
221    #[inline]
222    pub fn u64(v: u64) -> Self {
223        Self {
224            kind: ScalarKind::U64,
225            payload_lo: v,
226            payload_hi: 0,
227        }
228    }
229
230    /// Create an F32 scalar.
231    #[inline]
232    pub fn f32(v: f32) -> Self {
233        Self {
234            kind: ScalarKind::F32,
235            payload_lo: f64::from(v).to_bits(),
236            payload_hi: 0,
237        }
238    }
239
240    /// Extract as i64 (only valid if kind is an integer type).
241    /// Returns None for U64 values > i64::MAX (use `as_u64()` instead).
242    #[inline]
243    pub fn as_i64(&self) -> Option<i64> {
244        if self.kind == ScalarKind::U64 {
245            i64::try_from(self.payload_lo).ok()
246        } else if self.kind.is_integer() {
247            Some(self.payload_lo as i64)
248        } else {
249            Option::None
250        }
251    }
252
253    /// Extract as u64 (only valid if kind is an unsigned integer type).
254    #[inline]
255    pub fn as_u64(&self) -> Option<u64> {
256        if self.kind.is_unsigned_integer() {
257            Some(self.payload_lo)
258        } else if self.kind.is_signed_integer() {
259            // Signed → u64: return raw bits (caller interprets)
260            Some(self.payload_lo)
261        } else {
262            Option::None
263        }
264    }
265
266    /// Extract as f64 (only valid if kind is F64 or F32).
267    #[inline]
268    pub fn as_f64(&self) -> Option<f64> {
269        match self.kind {
270            ScalarKind::F64 => Some(f64::from_bits(self.payload_lo)),
271            ScalarKind::F32 => Some(f64::from_bits(self.payload_lo)),
272            _ => Option::None,
273        }
274    }
275
276    /// Extract as bool (only valid if kind is Bool).
277    #[inline]
278    pub fn as_bool(&self) -> Option<bool> {
279        if self.kind == ScalarKind::Bool {
280            Some(self.payload_lo != 0)
281        } else {
282            Option::None
283        }
284    }
285
286    /// Interpret this scalar as an f64 regardless of kind (for numeric comparison).
287    /// Integer kinds are cast; float kinds use their stored value; non-numeric returns None.
288    #[inline]
289    pub fn to_f64_lossy(&self) -> Option<f64> {
290        match self.kind {
291            ScalarKind::F64 | ScalarKind::F32 => Some(f64::from_bits(self.payload_lo)),
292            k if k.is_unsigned_integer() => Some(self.payload_lo as f64),
293            k if k.is_signed_integer() => Some(self.payload_lo as i64 as f64),
294            _ => Option::None,
295        }
296    }
297}
298
299// ============================================================================
300// NumericWidth mapping
301// ============================================================================
302
303// Note: `From<NumericWidth> for ScalarKind` is implemented in shape-vm
304// (where NumericWidth is defined) since shape-value cannot depend on shape-vm.
305
306// ============================================================================
307// ValueSlot <- TypedScalar conversion
308// ============================================================================
309
310impl ValueSlot {
311    /// Create a ValueSlot from a TypedScalar.
312    ///
313    /// Returns `(slot, is_heap)` — `is_heap` is always false for scalars
314    /// since all scalar values fit in 8 bytes without heap allocation.
315    #[inline]
316    pub fn from_typed_scalar(ts: TypedScalar) -> (Self, bool) {
317        match ts.kind {
318            ScalarKind::I8 | ScalarKind::I16 | ScalarKind::I32 | ScalarKind::I64 => {
319                (ValueSlot::from_int(ts.payload_lo as i64), false)
320            }
321            ScalarKind::U8 | ScalarKind::U16 | ScalarKind::U32 => {
322                (ValueSlot::from_int(ts.payload_lo as i64), false)
323            }
324            ScalarKind::U64 => (ValueSlot::from_u64(ts.payload_lo), false),
325            ScalarKind::I128 | ScalarKind::U128 => {
326                (ValueSlot::from_int(ts.payload_lo as i64), false)
327            }
328            ScalarKind::F64 | ScalarKind::F32 => {
329                (ValueSlot::from_number(f64::from_bits(ts.payload_lo)), false)
330            }
331            ScalarKind::Bool => (ValueSlot::from_bool(ts.payload_lo != 0), false),
332            ScalarKind::None | ScalarKind::Unit => (ValueSlot::none(), false),
333        }
334    }
335}
336
337// ============================================================================
338// Tests
339// ============================================================================
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn typed_scalar_convenience_constructors() {
347        assert_eq!(TypedScalar::i64(42).kind, ScalarKind::I64);
348        assert_eq!(TypedScalar::i64(42).payload_lo, 42);
349        assert_eq!(TypedScalar::f64(1.5).kind, ScalarKind::F64);
350        assert_eq!(TypedScalar::bool(true).payload_lo, 1);
351        assert_eq!(TypedScalar::none().kind, ScalarKind::None);
352        assert_eq!(TypedScalar::unit().kind, ScalarKind::Unit);
353    }
354
355    #[test]
356    fn scalar_kind_classification() {
357        assert!(ScalarKind::I64.is_integer());
358        assert!(ScalarKind::U32.is_integer());
359        assert!(!ScalarKind::F64.is_integer());
360        assert!(!ScalarKind::Bool.is_integer());
361
362        assert!(ScalarKind::F64.is_float());
363        assert!(ScalarKind::F32.is_float());
364        assert!(!ScalarKind::I64.is_float());
365
366        assert!(ScalarKind::I64.is_numeric());
367        assert!(ScalarKind::F64.is_numeric());
368        assert!(!ScalarKind::Bool.is_numeric());
369        assert!(!ScalarKind::None.is_numeric());
370    }
371
372    #[test]
373    fn value_slot_from_typed_scalar() {
374        // Integer
375        let (slot, is_heap) = ValueSlot::from_typed_scalar(TypedScalar::i64(-42));
376        assert!(!is_heap);
377        assert_eq!(slot.as_i64(), -42);
378
379        // Float
380        let (slot, is_heap) = ValueSlot::from_typed_scalar(TypedScalar::f64(3.14));
381        assert!(!is_heap);
382        assert_eq!(slot.as_f64(), 3.14);
383
384        // Bool
385        let (slot, is_heap) = ValueSlot::from_typed_scalar(TypedScalar::bool(true));
386        assert!(!is_heap);
387        assert!(slot.as_bool());
388
389        // None
390        let (slot, is_heap) = ValueSlot::from_typed_scalar(TypedScalar::none());
391        assert!(!is_heap);
392        assert_eq!(slot.raw(), 0);
393    }
394
395    #[test]
396    fn to_f64_lossy_works() {
397        assert_eq!(TypedScalar::f64(3.14).to_f64_lossy(), Some(3.14));
398        assert_eq!(TypedScalar::i64(42).to_f64_lossy(), Some(42.0));
399        assert_eq!(TypedScalar::bool(true).to_f64_lossy(), Option::None);
400        assert_eq!(TypedScalar::none().to_f64_lossy(), Option::None);
401    }
402
403    #[test]
404    fn typed_scalar_extraction_methods() {
405        assert_eq!(TypedScalar::i64(42).as_i64(), Some(42));
406        assert_eq!(TypedScalar::f64(3.14).as_i64(), Option::None);
407        assert_eq!(TypedScalar::f64(3.14).as_f64(), Some(3.14));
408        assert_eq!(TypedScalar::i64(42).as_f64(), Option::None);
409        assert_eq!(TypedScalar::bool(true).as_bool(), Some(true));
410        assert_eq!(TypedScalar::i64(1).as_bool(), Option::None);
411    }
412}