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 float)
4//! must be preserved. `TypedScalar` carries an explicit `ScalarKind` discriminator
5//! alongside the raw payload bits, eliminating the ambiguity between NaN-boxed I48
6//! integers and plain f64 numbers.
7
8use crate::slot::ValueSlot;
9use crate::value_word::{NanTag, ValueWord};
10
11/// Scalar type discriminator.
12///
13/// Covers all width-specific numeric types plus bool/none/unit sentinels.
14/// Discriminant values are part of the ABI — do not reorder.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[repr(u8)]
17pub enum ScalarKind {
18    I8 = 0,
19    U8 = 1,
20    I16 = 2,
21    U16 = 3,
22    I32 = 4,
23    U32 = 5,
24    I64 = 6,
25    U64 = 7,
26    I128 = 8,
27    U128 = 9,
28    F32 = 10,
29    F64 = 11,
30    Bool = 12,
31    None = 13,
32    Unit = 14,
33}
34
35impl ScalarKind {
36    /// True if this kind represents an integer type (signed or unsigned).
37    #[inline]
38    pub fn is_integer(self) -> bool {
39        matches!(
40            self,
41            ScalarKind::I8
42                | ScalarKind::U8
43                | ScalarKind::I16
44                | ScalarKind::U16
45                | ScalarKind::I32
46                | ScalarKind::U32
47                | ScalarKind::I64
48                | ScalarKind::U64
49                | ScalarKind::I128
50                | ScalarKind::U128
51        )
52    }
53
54    /// True if this kind represents a floating-point type.
55    #[inline]
56    pub fn is_float(self) -> bool {
57        matches!(self, ScalarKind::F32 | ScalarKind::F64)
58    }
59
60    /// True if this kind represents a numeric type (integer or float).
61    #[inline]
62    pub fn is_numeric(self) -> bool {
63        self.is_integer() || self.is_float()
64    }
65
66    /// True if this kind represents an unsigned integer type.
67    #[inline]
68    pub fn is_unsigned_integer(self) -> bool {
69        matches!(
70            self,
71            ScalarKind::U8 | ScalarKind::U16 | ScalarKind::U32 | ScalarKind::U64 | ScalarKind::U128
72        )
73    }
74
75    /// True if this kind represents a signed integer type.
76    #[inline]
77    pub fn is_signed_integer(self) -> bool {
78        matches!(
79            self,
80            ScalarKind::I8 | ScalarKind::I16 | ScalarKind::I32 | ScalarKind::I64 | ScalarKind::I128
81        )
82    }
83}
84
85/// Type-preserving scalar value for VM↔JIT boundary exchange.
86///
87/// Carries an explicit type discriminator (`kind`) so that integer 42 and
88/// float 42.0 are distinguishable even when their f64 bit patterns would
89/// be identical.
90///
91/// `payload_hi` is zero for all types smaller than 128 bits.
92#[derive(Debug, Clone, Copy, PartialEq)]
93#[repr(C)]
94pub struct TypedScalar {
95    pub kind: ScalarKind,
96    pub payload_lo: u64,
97    /// Second 64-bit word — only used for I128/U128. Zero otherwise.
98    pub payload_hi: u64,
99}
100
101impl TypedScalar {
102    /// Create an I64 scalar.
103    #[inline]
104    pub fn i64(v: i64) -> Self {
105        Self {
106            kind: ScalarKind::I64,
107            payload_lo: v as u64,
108            payload_hi: 0,
109        }
110    }
111
112    /// Create an F64 scalar from a value.
113    #[inline]
114    pub fn f64(v: f64) -> Self {
115        Self {
116            kind: ScalarKind::F64,
117            payload_lo: v.to_bits(),
118            payload_hi: 0,
119        }
120    }
121
122    /// Create an F64 scalar from raw bits.
123    #[inline]
124    pub fn f64_from_bits(bits: u64) -> Self {
125        Self {
126            kind: ScalarKind::F64,
127            payload_lo: bits,
128            payload_hi: 0,
129        }
130    }
131
132    /// Create a Bool scalar.
133    #[inline]
134    pub fn bool(v: bool) -> Self {
135        Self {
136            kind: ScalarKind::Bool,
137            payload_lo: v as u64,
138            payload_hi: 0,
139        }
140    }
141
142    /// Create a None scalar.
143    #[inline]
144    pub fn none() -> Self {
145        Self {
146            kind: ScalarKind::None,
147            payload_lo: 0,
148            payload_hi: 0,
149        }
150    }
151
152    /// Create a Unit scalar.
153    #[inline]
154    pub fn unit() -> Self {
155        Self {
156            kind: ScalarKind::Unit,
157            payload_lo: 0,
158            payload_hi: 0,
159        }
160    }
161
162    /// Create an I8 scalar.
163    #[inline]
164    pub fn i8(v: i8) -> Self {
165        Self {
166            kind: ScalarKind::I8,
167            payload_lo: v as i64 as u64,
168            payload_hi: 0,
169        }
170    }
171
172    /// Create a U8 scalar.
173    #[inline]
174    pub fn u8(v: u8) -> Self {
175        Self {
176            kind: ScalarKind::U8,
177            payload_lo: v as u64,
178            payload_hi: 0,
179        }
180    }
181
182    /// Create an I16 scalar.
183    #[inline]
184    pub fn i16(v: i16) -> Self {
185        Self {
186            kind: ScalarKind::I16,
187            payload_lo: v as i64 as u64,
188            payload_hi: 0,
189        }
190    }
191
192    /// Create a U16 scalar.
193    #[inline]
194    pub fn u16(v: u16) -> Self {
195        Self {
196            kind: ScalarKind::U16,
197            payload_lo: v as u64,
198            payload_hi: 0,
199        }
200    }
201
202    /// Create an I32 scalar.
203    #[inline]
204    pub fn i32(v: i32) -> Self {
205        Self {
206            kind: ScalarKind::I32,
207            payload_lo: v as i64 as u64,
208            payload_hi: 0,
209        }
210    }
211
212    /// Create a U32 scalar.
213    #[inline]
214    pub fn u32(v: u32) -> Self {
215        Self {
216            kind: ScalarKind::U32,
217            payload_lo: v as u64,
218            payload_hi: 0,
219        }
220    }
221
222    /// Create a U64 scalar.
223    #[inline]
224    pub fn u64(v: u64) -> Self {
225        Self {
226            kind: ScalarKind::U64,
227            payload_lo: v,
228            payload_hi: 0,
229        }
230    }
231
232    /// Create an F32 scalar.
233    #[inline]
234    pub fn f32(v: f32) -> Self {
235        Self {
236            kind: ScalarKind::F32,
237            payload_lo: f64::from(v).to_bits(),
238            payload_hi: 0,
239        }
240    }
241
242    /// Extract as i64 (only valid if kind is an integer type).
243    /// Returns None for U64 values > i64::MAX (use `as_u64()` instead).
244    #[inline]
245    pub fn as_i64(&self) -> Option<i64> {
246        if self.kind == ScalarKind::U64 {
247            i64::try_from(self.payload_lo).ok()
248        } else if self.kind.is_integer() {
249            Some(self.payload_lo as i64)
250        } else {
251            Option::None
252        }
253    }
254
255    /// Extract as u64 (only valid if kind is an unsigned integer type).
256    #[inline]
257    pub fn as_u64(&self) -> Option<u64> {
258        if self.kind.is_unsigned_integer() {
259            Some(self.payload_lo)
260        } else if self.kind.is_signed_integer() {
261            // Signed → u64: return raw bits (caller interprets)
262            Some(self.payload_lo)
263        } else {
264            Option::None
265        }
266    }
267
268    /// Extract as f64 (only valid if kind is F64 or F32).
269    #[inline]
270    pub fn as_f64(&self) -> Option<f64> {
271        match self.kind {
272            ScalarKind::F64 => Some(f64::from_bits(self.payload_lo)),
273            ScalarKind::F32 => Some(f64::from_bits(self.payload_lo)),
274            _ => Option::None,
275        }
276    }
277
278    /// Extract as bool (only valid if kind is Bool).
279    #[inline]
280    pub fn as_bool(&self) -> Option<bool> {
281        if self.kind == ScalarKind::Bool {
282            Some(self.payload_lo != 0)
283        } else {
284            Option::None
285        }
286    }
287
288    /// Interpret this scalar as an f64 regardless of kind (for numeric comparison).
289    /// Integer kinds are cast; float kinds use their stored value; non-numeric returns None.
290    #[inline]
291    pub fn to_f64_lossy(&self) -> Option<f64> {
292        match self.kind {
293            ScalarKind::F64 | ScalarKind::F32 => Some(f64::from_bits(self.payload_lo)),
294            k if k.is_unsigned_integer() => Some(self.payload_lo as f64),
295            k if k.is_signed_integer() => Some(self.payload_lo as i64 as f64),
296            _ => Option::None,
297        }
298    }
299}
300
301// ============================================================================
302// NumericWidth mapping
303// ============================================================================
304
305// Note: `From<NumericWidth> for ScalarKind` is implemented in shape-vm
306// (where NumericWidth is defined) since shape-value cannot depend on shape-vm.
307
308// ============================================================================
309// ValueWord <-> TypedScalar conversions
310// ============================================================================
311
312impl ValueWord {
313    /// Convert this ValueWord to a TypedScalar, preserving type identity.
314    ///
315    /// Returns `None` for heap-allocated values (strings, arrays, objects, etc.)
316    /// which are not representable as scalars.
317    #[inline]
318    pub fn to_typed_scalar(&self) -> Option<TypedScalar> {
319        match self.tag() {
320            NanTag::F64 => {
321                let bits = self.raw_bits();
322                Some(TypedScalar {
323                    kind: ScalarKind::F64,
324                    payload_lo: bits,
325                    payload_hi: 0,
326                })
327            }
328            NanTag::I48 => {
329                let i = unsafe { self.as_i64_unchecked() };
330                Some(TypedScalar::i64(i))
331            }
332            NanTag::Bool => {
333                let b = unsafe { self.as_bool_unchecked() };
334                Some(TypedScalar::bool(b))
335            }
336            NanTag::None => Some(TypedScalar::none()),
337            NanTag::Unit => Some(TypedScalar::unit()),
338            NanTag::Heap | NanTag::Function | NanTag::ModuleFunction | NanTag::Ref => Option::None,
339        }
340    }
341
342    /// Create a ValueWord from a TypedScalar.
343    ///
344    /// Integer kinds are stored as I48 (clamped to the 48-bit range; values
345    /// outside [-2^47, 2^47-1] are heap-boxed as BigInt via `from_i64`).
346    /// Float kinds are stored as plain f64. Bool/None/Unit use their direct
347    /// ValueWord constructors.
348    #[inline]
349    pub fn from_typed_scalar(ts: TypedScalar) -> Self {
350        match ts.kind {
351            ScalarKind::I64 => ValueWord::from_i64(ts.payload_lo as i64),
352            ScalarKind::I8 | ScalarKind::I16 | ScalarKind::I32 => {
353                // Sign-extend to i64, then use from_i64
354                ValueWord::from_i64(ts.payload_lo as i64)
355            }
356            ScalarKind::U8 | ScalarKind::U16 | ScalarKind::U32 => {
357                // Unsigned sub-64: always fits in i64
358                ValueWord::from_i64(ts.payload_lo as i64)
359            }
360            ScalarKind::U64 => {
361                if ts.payload_lo <= i64::MAX as u64 {
362                    ValueWord::from_i64(ts.payload_lo as i64)
363                } else {
364                    ValueWord::from_native_u64(ts.payload_lo)
365                }
366            }
367            ScalarKind::I128 | ScalarKind::U128 => {
368                // Truncate to i64 (best effort for 128-bit)
369                ValueWord::from_i64(ts.payload_lo as i64)
370            }
371            ScalarKind::F64 => ValueWord::from_f64(f64::from_bits(ts.payload_lo)),
372            ScalarKind::F32 => ValueWord::from_f64(f64::from_bits(ts.payload_lo)),
373            ScalarKind::Bool => ValueWord::from_bool(ts.payload_lo != 0),
374            ScalarKind::None => ValueWord::none(),
375            ScalarKind::Unit => ValueWord::unit(),
376        }
377    }
378}
379
380// ============================================================================
381// ValueSlot <- TypedScalar conversion
382// ============================================================================
383
384impl ValueSlot {
385    /// Create a ValueSlot from a TypedScalar.
386    ///
387    /// Returns `(slot, is_heap)` — `is_heap` is always false for scalars
388    /// since all scalar values fit in 8 bytes without heap allocation.
389    #[inline]
390    pub fn from_typed_scalar(ts: TypedScalar) -> (Self, bool) {
391        match ts.kind {
392            ScalarKind::I8 | ScalarKind::I16 | ScalarKind::I32 | ScalarKind::I64 => {
393                (ValueSlot::from_int(ts.payload_lo as i64), false)
394            }
395            ScalarKind::U8 | ScalarKind::U16 | ScalarKind::U32 => {
396                (ValueSlot::from_int(ts.payload_lo as i64), false)
397            }
398            ScalarKind::U64 => (ValueSlot::from_u64(ts.payload_lo), false),
399            ScalarKind::I128 | ScalarKind::U128 => {
400                (ValueSlot::from_int(ts.payload_lo as i64), false)
401            }
402            ScalarKind::F64 | ScalarKind::F32 => {
403                (ValueSlot::from_number(f64::from_bits(ts.payload_lo)), false)
404            }
405            ScalarKind::Bool => (ValueSlot::from_bool(ts.payload_lo != 0), false),
406            ScalarKind::None | ScalarKind::Unit => (ValueSlot::none(), false),
407        }
408    }
409}
410
411// ============================================================================
412// Tests
413// ============================================================================
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use crate::tags::{I48_MAX, I48_MIN};
419
420    #[test]
421    fn round_trip_i64() {
422        let vw = ValueWord::from_i64(42);
423        let ts = vw.to_typed_scalar().unwrap();
424        assert_eq!(ts.kind, ScalarKind::I64);
425        assert_eq!(ts.payload_lo, 42u64);
426        assert_eq!(ts.payload_hi, 0);
427        let vw2 = ValueWord::from_typed_scalar(ts);
428        assert_eq!(vw.raw_bits(), vw2.raw_bits());
429    }
430
431    #[test]
432    fn round_trip_negative_i64() {
433        let vw = ValueWord::from_i64(-99);
434        let ts = vw.to_typed_scalar().unwrap();
435        assert_eq!(ts.kind, ScalarKind::I64);
436        assert_eq!(ts.payload_lo as i64, -99);
437        let vw2 = ValueWord::from_typed_scalar(ts);
438        assert_eq!(vw.raw_bits(), vw2.raw_bits());
439    }
440
441    #[test]
442    fn round_trip_i48_max() {
443        let vw = ValueWord::from_i64(I48_MAX);
444        let ts = vw.to_typed_scalar().unwrap();
445        assert_eq!(ts.kind, ScalarKind::I64);
446        assert_eq!(ts.payload_lo as i64, I48_MAX);
447        let vw2 = ValueWord::from_typed_scalar(ts);
448        assert_eq!(vw.raw_bits(), vw2.raw_bits());
449    }
450
451    #[test]
452    fn round_trip_i48_min() {
453        let vw = ValueWord::from_i64(I48_MIN);
454        let ts = vw.to_typed_scalar().unwrap();
455        assert_eq!(ts.kind, ScalarKind::I64);
456        assert_eq!(ts.payload_lo as i64, I48_MIN);
457        let vw2 = ValueWord::from_typed_scalar(ts);
458        assert_eq!(vw.raw_bits(), vw2.raw_bits());
459    }
460
461    #[test]
462    fn round_trip_f64() {
463        let vw = ValueWord::from_f64(3.14);
464        let ts = vw.to_typed_scalar().unwrap();
465        assert_eq!(ts.kind, ScalarKind::F64);
466        assert_eq!(f64::from_bits(ts.payload_lo), 3.14);
467        let vw2 = ValueWord::from_typed_scalar(ts);
468        assert_eq!(vw.raw_bits(), vw2.raw_bits());
469    }
470
471    #[test]
472    fn round_trip_f64_nan() {
473        let vw = ValueWord::from_f64(f64::NAN);
474        let ts = vw.to_typed_scalar().unwrap();
475        assert_eq!(ts.kind, ScalarKind::F64);
476        assert!(f64::from_bits(ts.payload_lo).is_nan());
477        let vw2 = ValueWord::from_typed_scalar(ts);
478        // Both should be canonical NaN
479        assert_eq!(vw.raw_bits(), vw2.raw_bits());
480    }
481
482    #[test]
483    fn round_trip_f64_infinity() {
484        let vw = ValueWord::from_f64(f64::INFINITY);
485        let ts = vw.to_typed_scalar().unwrap();
486        assert_eq!(ts.kind, ScalarKind::F64);
487        assert_eq!(f64::from_bits(ts.payload_lo), f64::INFINITY);
488        let vw2 = ValueWord::from_typed_scalar(ts);
489        assert_eq!(vw.raw_bits(), vw2.raw_bits());
490    }
491
492    #[test]
493    fn round_trip_f64_neg_zero() {
494        let vw = ValueWord::from_f64(-0.0);
495        let ts = vw.to_typed_scalar().unwrap();
496        assert_eq!(ts.kind, ScalarKind::F64);
497        // -0.0 has specific bit pattern (sign bit set)
498        assert_eq!(ts.payload_lo, (-0.0f64).to_bits());
499        let vw2 = ValueWord::from_typed_scalar(ts);
500        assert_eq!(vw.raw_bits(), vw2.raw_bits());
501    }
502
503    #[test]
504    fn round_trip_bool_true() {
505        let vw = ValueWord::from_bool(true);
506        let ts = vw.to_typed_scalar().unwrap();
507        assert_eq!(ts.kind, ScalarKind::Bool);
508        assert_eq!(ts.payload_lo, 1);
509        let vw2 = ValueWord::from_typed_scalar(ts);
510        assert_eq!(vw.raw_bits(), vw2.raw_bits());
511    }
512
513    #[test]
514    fn round_trip_bool_false() {
515        let vw = ValueWord::from_bool(false);
516        let ts = vw.to_typed_scalar().unwrap();
517        assert_eq!(ts.kind, ScalarKind::Bool);
518        assert_eq!(ts.payload_lo, 0);
519        let vw2 = ValueWord::from_typed_scalar(ts);
520        assert_eq!(vw.raw_bits(), vw2.raw_bits());
521    }
522
523    #[test]
524    fn round_trip_none() {
525        let vw = ValueWord::none();
526        let ts = vw.to_typed_scalar().unwrap();
527        assert_eq!(ts.kind, ScalarKind::None);
528        let vw2 = ValueWord::from_typed_scalar(ts);
529        assert_eq!(vw.raw_bits(), vw2.raw_bits());
530    }
531
532    #[test]
533    fn round_trip_unit() {
534        let vw = ValueWord::unit();
535        let ts = vw.to_typed_scalar().unwrap();
536        assert_eq!(ts.kind, ScalarKind::Unit);
537        let vw2 = ValueWord::from_typed_scalar(ts);
538        assert_eq!(vw.raw_bits(), vw2.raw_bits());
539    }
540
541    #[test]
542    fn heap_value_returns_none() {
543        let vw = ValueWord::from_string(std::sync::Arc::new("hello".to_string()));
544        assert!(vw.to_typed_scalar().is_none());
545    }
546
547    #[test]
548    fn typed_scalar_convenience_constructors() {
549        assert_eq!(TypedScalar::i64(42).kind, ScalarKind::I64);
550        assert_eq!(TypedScalar::i64(42).payload_lo, 42);
551        assert_eq!(TypedScalar::f64(1.5).kind, ScalarKind::F64);
552        assert_eq!(TypedScalar::bool(true).payload_lo, 1);
553        assert_eq!(TypedScalar::none().kind, ScalarKind::None);
554        assert_eq!(TypedScalar::unit().kind, ScalarKind::Unit);
555    }
556
557    #[test]
558    fn scalar_kind_classification() {
559        assert!(ScalarKind::I64.is_integer());
560        assert!(ScalarKind::U32.is_integer());
561        assert!(!ScalarKind::F64.is_integer());
562        assert!(!ScalarKind::Bool.is_integer());
563
564        assert!(ScalarKind::F64.is_float());
565        assert!(ScalarKind::F32.is_float());
566        assert!(!ScalarKind::I64.is_float());
567
568        assert!(ScalarKind::I64.is_numeric());
569        assert!(ScalarKind::F64.is_numeric());
570        assert!(!ScalarKind::Bool.is_numeric());
571        assert!(!ScalarKind::None.is_numeric());
572    }
573
574    #[test]
575    fn value_slot_from_typed_scalar() {
576        // Integer
577        let (slot, is_heap) = ValueSlot::from_typed_scalar(TypedScalar::i64(-42));
578        assert!(!is_heap);
579        assert_eq!(slot.as_i64(), -42);
580
581        // Float
582        let (slot, is_heap) = ValueSlot::from_typed_scalar(TypedScalar::f64(3.14));
583        assert!(!is_heap);
584        assert_eq!(slot.as_f64(), 3.14);
585
586        // Bool
587        let (slot, is_heap) = ValueSlot::from_typed_scalar(TypedScalar::bool(true));
588        assert!(!is_heap);
589        assert!(slot.as_bool());
590
591        // None
592        let (slot, is_heap) = ValueSlot::from_typed_scalar(TypedScalar::none());
593        assert!(!is_heap);
594        assert_eq!(slot.raw(), 0);
595    }
596
597    #[test]
598    fn to_f64_lossy_works() {
599        assert_eq!(TypedScalar::f64(3.14).to_f64_lossy(), Some(3.14));
600        assert_eq!(TypedScalar::i64(42).to_f64_lossy(), Some(42.0));
601        assert_eq!(TypedScalar::bool(true).to_f64_lossy(), Option::None);
602        assert_eq!(TypedScalar::none().to_f64_lossy(), Option::None);
603    }
604
605    #[test]
606    fn typed_scalar_extraction_methods() {
607        assert_eq!(TypedScalar::i64(42).as_i64(), Some(42));
608        assert_eq!(TypedScalar::f64(3.14).as_i64(), Option::None);
609        assert_eq!(TypedScalar::f64(3.14).as_f64(), Some(3.14));
610        assert_eq!(TypedScalar::i64(42).as_f64(), Option::None);
611        assert_eq!(TypedScalar::bool(true).as_bool(), Some(true));
612        assert_eq!(TypedScalar::i64(1).as_bool(), Option::None);
613    }
614}