Skip to main content

logicaffeine_compile/vm/
value.rs

1//! The VM value type.
2//!
3//! `Value` is the register-file cell. It has two representations, selected at
4//! compile time:
5//!
6//! * **default** — `Value(RuntimeValue)`, a newtype over the tree-walker's fat
7//!   16-byte enum. Every operation delegates to the shared semantics kernel
8//!   (`crate::semantics`) — the same functions the tree-walker calls — so the
9//!   two engines cannot diverge on value semantics.
10//! * **`feature = "narrow-value"`** — `Value(Narrow)`, the 8-byte NaN-boxed
11//!   value from [`super::nanbox`]. Inline scalars (`Int` in ±2^47, `Float`,
12//!   `Bool`, `Nothing`) live in one machine word; everything else (large ints,
13//!   collections, strings, structs, temporals) boxes behind a heap tag. The
14//!   register file (`Vec<Value>`) is then half the width, and every Move/clone
15//!   in the dispatch loop copies 8 bytes instead of 16.
16//!
17//! Both representations expose the SAME public API and route non-inline operands
18//! through `crate::semantics` exactly the same way, so the VM↔tree-walker
19//! differential gate holds under either build. This is the seam work/VM_PLAN.md calls
20//! for: the representation swap touches only this file (plus the `as_runtime`
21//! borrow sites in `machine.rs`, which use [`RuntimeRef`] uniformly).
22
23use std::cell::RefCell;
24use std::rc::Rc;
25
26use crate::ast::stmt::BinaryOpKind;
27use crate::interpreter::RuntimeValue;
28use crate::semantics::{arith, collections, compare};
29
30/// A borrow of a [`Value`] as a `RuntimeValue`, usable identically under both
31/// representations.
32///
33/// * default: `RuntimeRef<'a>` borrows the `Value`'s own `RuntimeValue` — zero
34///   cost, exactly the old `&RuntimeValue` return.
35/// * narrow: a heap `Value` borrows the boxed pointee directly; an inline scalar
36///   materialises a fresh `RuntimeValue` that the `RuntimeRef` owns.
37///
38/// `Deref<Target = RuntimeValue>` makes `r.method()`, `&*r`, and `(*r).clone()`
39/// behave the same in either build. Match scrutinees do not auto-deref, so the
40/// `machine.rs` sites match on `&*value.as_runtime()`.
41pub struct RuntimeRef<'a>(RuntimeRefInner<'a>);
42
43enum RuntimeRefInner<'a> {
44    Borrowed(&'a RuntimeValue),
45    #[cfg(feature = "narrow-value")]
46    Owned(RuntimeValue),
47}
48
49impl std::ops::Deref for RuntimeRef<'_> {
50    type Target = RuntimeValue;
51    #[inline]
52    fn deref(&self) -> &RuntimeValue {
53        match &self.0 {
54            RuntimeRefInner::Borrowed(r) => r,
55            #[cfg(feature = "narrow-value")]
56            RuntimeRefInner::Owned(v) => v,
57        }
58    }
59}
60
61impl AsRef<RuntimeValue> for RuntimeRef<'_> {
62    #[inline]
63    fn as_ref(&self) -> &RuntimeValue {
64        self
65    }
66}
67
68impl std::fmt::Debug for RuntimeRef<'_> {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        (**self).fmt(f)
71    }
72}
73
74// ===========================================================================
75// Default representation: Value(RuntimeValue)
76// ===========================================================================
77
78#[cfg(not(feature = "narrow-value"))]
79mod repr {
80    use super::*;
81
82    /// A VM value (fat 16-byte representation).
83    #[derive(Clone, Debug)]
84    pub struct Value(pub(super) RuntimeValue);
85
86    impl Value {
87        #[inline]
88        pub fn int(n: i64) -> Self {
89            Value(RuntimeValue::Int(n))
90        }
91        #[inline]
92        pub fn float(f: f64) -> Self {
93            Value(RuntimeValue::Float(f))
94        }
95        #[inline]
96        pub fn bool(b: bool) -> Self {
97            Value(RuntimeValue::Bool(b))
98        }
99        #[inline]
100        pub fn nothing() -> Self {
101            Value(RuntimeValue::Nothing)
102        }
103
104        #[inline]
105        pub fn from_runtime(rv: RuntimeValue) -> Self {
106            Value(rv)
107        }
108        #[inline]
109        pub fn into_runtime(self) -> RuntimeValue {
110            self.0
111        }
112
113        /// Borrow as a `RuntimeValue`. Zero cost: the `Value` IS a
114        /// `RuntimeValue`.
115        #[inline]
116        pub fn as_runtime(&self) -> RuntimeRef<'_> {
117            RuntimeRef(RuntimeRefInner::Borrowed(&self.0))
118        }
119
120        /// Borrow the `RuntimeValue` DIRECTLY (lifetime tied to `&self`, no
121        /// `RuntimeRef` temporary) when one already exists in place. Default:
122        /// always `Some`. The seam for the `machine.rs` sites that destructure a
123        /// heap arm and hold a borrow (an `Rc`/`RefMut`) across statements — see
124        /// the narrow impl, where an inline scalar returns `None`.
125        #[inline]
126        pub fn as_runtime_ref(&self) -> Option<&RuntimeValue> {
127            Some(&self.0)
128        }
129
130        /// Mutably borrow as a `RuntimeValue`.
131        #[inline]
132        pub fn as_runtime_mut(&mut self) -> &mut RuntimeValue {
133            &mut self.0
134        }
135
136        #[inline]
137        pub fn as_int(&self) -> Option<i64> {
138            match &self.0 {
139                RuntimeValue::Int(n) => Some(*n),
140                _ => None,
141            }
142        }
143        #[inline]
144        pub fn as_bool(&self) -> Option<bool> {
145            match &self.0 {
146                RuntimeValue::Bool(b) => Some(*b),
147                _ => None,
148            }
149        }
150        #[inline]
151        pub fn as_float(&self) -> Option<f64> {
152            match &self.0 {
153                RuntimeValue::Float(f) => Some(*f),
154                _ => None,
155            }
156        }
157        #[inline]
158        pub fn is_int(&self) -> bool {
159            matches!(self.0, RuntimeValue::Int(_))
160        }
161
162        // ---- Arithmetic (Int×Int inlined; EXACT — overflow promotes to BigInt) ---
163        //
164        // The fast path uses `checked_*` and, only on i64 overflow, falls through to
165        // the shared `arith` operators (which promote to BigInt). This keeps the VM
166        // byte-identical to the tree-walker at the overflow boundary — integer math
167        // is exact on every tier, never silently wrapping.
168
169        #[inline]
170        pub fn add(&self, rhs: &Value) -> Result<Value, String> {
171            if let (RuntimeValue::Int(a), RuntimeValue::Int(b)) = (&self.0, &rhs.0) {
172                if let Some(s) = a.checked_add(*b) {
173                    return Ok(Value::int(s));
174                }
175            }
176            arith::add(self.0.clone(), rhs.0.clone()).map(Value)
177        }
178        #[inline]
179        pub fn sub(&self, rhs: &Value) -> Result<Value, String> {
180            if let (RuntimeValue::Int(a), RuntimeValue::Int(b)) = (&self.0, &rhs.0) {
181                if let Some(s) = a.checked_sub(*b) {
182                    return Ok(Value::int(s));
183                }
184            }
185            arith::subtract(self.0.clone(), rhs.0.clone()).map(Value)
186        }
187        #[inline]
188        pub fn mul(&self, rhs: &Value) -> Result<Value, String> {
189            if let (RuntimeValue::Int(a), RuntimeValue::Int(b)) = (&self.0, &rhs.0) {
190                if let Some(p) = a.checked_mul(*b) {
191                    return Ok(Value::int(p));
192                }
193            }
194            arith::multiply(self.0.clone(), rhs.0.clone()).map(Value)
195        }
196
197        // ---- Comparison (Int×Int inlined — same proof) -----------------------
198
199        #[inline]
200        pub fn lt(&self, rhs: &Value) -> Result<Value, String> {
201            if let (RuntimeValue::Int(a), RuntimeValue::Int(b)) = (&self.0, &rhs.0) {
202                return Ok(Value::bool(a < b));
203            }
204            compare::compare(BinaryOpKind::Lt, &self.0, &rhs.0).map(Value)
205        }
206        #[inline]
207        pub fn gt(&self, rhs: &Value) -> Result<Value, String> {
208            if let (RuntimeValue::Int(a), RuntimeValue::Int(b)) = (&self.0, &rhs.0) {
209                return Ok(Value::bool(a > b));
210            }
211            compare::compare(BinaryOpKind::Gt, &self.0, &rhs.0).map(Value)
212        }
213        #[inline]
214        pub fn lte(&self, rhs: &Value) -> Result<Value, String> {
215            if let (RuntimeValue::Int(a), RuntimeValue::Int(b)) = (&self.0, &rhs.0) {
216                return Ok(Value::bool(a <= b));
217            }
218            compare::compare(BinaryOpKind::LtEq, &self.0, &rhs.0).map(Value)
219        }
220        #[inline]
221        pub fn gte(&self, rhs: &Value) -> Result<Value, String> {
222            if let (RuntimeValue::Int(a), RuntimeValue::Int(b)) = (&self.0, &rhs.0) {
223                return Ok(Value::bool(a >= b));
224            }
225            compare::compare(BinaryOpKind::GtEq, &self.0, &rhs.0).map(Value)
226        }
227        #[inline]
228        pub fn eq_op(&self, rhs: &Value) -> Value {
229            if let (RuntimeValue::Int(a), RuntimeValue::Int(b)) = (&self.0, &rhs.0) {
230                return Value::bool(a == b);
231            }
232            Value::bool(self.values_equal(rhs))
233        }
234        #[inline]
235        pub fn neq_op(&self, rhs: &Value) -> Value {
236            if let (RuntimeValue::Int(a), RuntimeValue::Int(b)) = (&self.0, &rhs.0) {
237                return Value::bool(a != b);
238            }
239            Value::bool(!self.values_equal(rhs))
240        }
241    }
242}
243
244// ===========================================================================
245// Narrow representation: Value(Narrow)
246// ===========================================================================
247
248#[cfg(feature = "narrow-value")]
249mod repr {
250    use super::*;
251    use crate::vm::nanbox::Narrow;
252
253    /// A VM value (8-byte NaN-boxed representation).
254    #[derive(Clone, Debug)]
255    pub struct Value(pub(super) Narrow);
256
257    impl Value {
258        #[inline]
259        pub fn int(n: i64) -> Self {
260            Value(Narrow::int(n))
261        }
262        #[inline]
263        pub fn float(f: f64) -> Self {
264            Value(Narrow::float(f))
265        }
266        #[inline]
267        pub fn bool(b: bool) -> Self {
268            Value(Narrow::bool(b))
269        }
270        #[inline]
271        pub fn nothing() -> Self {
272            Value(Narrow::nothing())
273        }
274
275        #[inline]
276        pub fn from_runtime(rv: RuntimeValue) -> Self {
277            Value(Narrow::from_runtime(rv))
278        }
279        #[inline]
280        pub fn into_runtime(self) -> RuntimeValue {
281            self.0.into_runtime()
282        }
283
284        /// Borrow as a `RuntimeValue`. A heap value borrows the boxed pointee
285        /// directly (zero copy); an inline scalar materialises a fresh
286        /// `RuntimeValue` the [`RuntimeRef`] owns. Both deref to `&RuntimeValue`.
287        #[inline]
288        pub fn as_runtime(&self) -> RuntimeRef<'_> {
289            match self.0.as_heap() {
290                Some(rv) => RuntimeRef(RuntimeRefInner::Borrowed(rv)),
291                None => RuntimeRef(RuntimeRefInner::Owned(self.0.to_runtime())),
292            }
293        }
294
295        /// Borrow the boxed `RuntimeValue` DIRECTLY (lifetime tied to `&self`)
296        /// for the heap arms; `None` for an inline scalar (which is never a List,
297        /// Map, Struct, or Text — the only shapes the borrow-holding `machine.rs`
298        /// sites destructure, so a `None` falls into their `else`/error arm
299        /// exactly as a non-matching variant would). This is the seam that keeps
300        /// an `Rc`/`RefMut` borrow alive across statements without a `RuntimeRef`
301        /// temporary in the way.
302        #[inline]
303        pub fn as_runtime_ref(&self) -> Option<&RuntimeValue> {
304            self.0.as_heap()
305        }
306
307        /// Mutably borrow as a `RuntimeValue`. Promotes an inline scalar to the
308        /// heap form first so a stable `&mut RuntimeValue` always exists (the VM
309        /// only mutates heap arms — Struct/Text — through this; a scalar
310        /// promotion is lossless and the mutation, if any, lands in the box).
311        #[inline]
312        pub fn as_runtime_mut(&mut self) -> &mut RuntimeValue {
313            self.0.make_heap_mut()
314        }
315
316        #[inline]
317        pub fn as_int(&self) -> Option<i64> {
318            self.0.as_int()
319        }
320        #[inline]
321        pub fn as_bool(&self) -> Option<bool> {
322            self.0.as_bool()
323        }
324        #[inline]
325        pub fn as_float(&self) -> Option<f64> {
326            self.0.as_float()
327        }
328        #[inline]
329        pub fn is_int(&self) -> bool {
330            self.0.as_int().is_some()
331        }
332
333        // ---- Arithmetic --------------------------------------------------------
334        // Inline Int×Int rides the nanbox fast path (bit-identical to the kernel,
335        // proven by nanbox's arith-parity test); every other shape (floats, large
336        // boxed ints, mixed operands) routes through `crate::semantics` exactly as
337        // the default representation does, via a single materialised round-trip.
338
339        #[inline]
340        pub fn add(&self, rhs: &Value) -> Result<Value, String> {
341            if let Some(n) = self.0.int_add(&rhs.0) {
342                return Ok(Value(n));
343            }
344            arith::add(self.0.to_runtime(), rhs.0.to_runtime()).map(Value::from_runtime)
345        }
346        #[inline]
347        pub fn sub(&self, rhs: &Value) -> Result<Value, String> {
348            if let Some(n) = self.0.int_sub(&rhs.0) {
349                return Ok(Value(n));
350            }
351            arith::subtract(self.0.to_runtime(), rhs.0.to_runtime()).map(Value::from_runtime)
352        }
353        #[inline]
354        pub fn mul(&self, rhs: &Value) -> Result<Value, String> {
355            if let Some(n) = self.0.int_mul(&rhs.0) {
356                return Ok(Value(n));
357            }
358            arith::multiply(self.0.to_runtime(), rhs.0.to_runtime()).map(Value::from_runtime)
359        }
360
361        // ---- Comparison --------------------------------------------------------
362
363        #[inline]
364        pub fn lt(&self, rhs: &Value) -> Result<Value, String> {
365            if let Some(b) = self.0.int_lt(&rhs.0) {
366                return Ok(Value::bool(b));
367            }
368            compare::compare(BinaryOpKind::Lt, &self.0.to_runtime(), &rhs.0.to_runtime())
369                .map(Value::from_runtime)
370        }
371        #[inline]
372        pub fn gt(&self, rhs: &Value) -> Result<Value, String> {
373            if let (Some(a), Some(b)) = (self.0.as_inline_int(), rhs.0.as_inline_int()) {
374                return Ok(Value::bool(a > b));
375            }
376            compare::compare(BinaryOpKind::Gt, &self.0.to_runtime(), &rhs.0.to_runtime())
377                .map(Value::from_runtime)
378        }
379        #[inline]
380        pub fn lte(&self, rhs: &Value) -> Result<Value, String> {
381            if let (Some(a), Some(b)) = (self.0.as_inline_int(), rhs.0.as_inline_int()) {
382                return Ok(Value::bool(a <= b));
383            }
384            compare::compare(BinaryOpKind::LtEq, &self.0.to_runtime(), &rhs.0.to_runtime())
385                .map(Value::from_runtime)
386        }
387        #[inline]
388        pub fn gte(&self, rhs: &Value) -> Result<Value, String> {
389            if let (Some(a), Some(b)) = (self.0.as_inline_int(), rhs.0.as_inline_int()) {
390                return Ok(Value::bool(a >= b));
391            }
392            compare::compare(BinaryOpKind::GtEq, &self.0.to_runtime(), &rhs.0.to_runtime())
393                .map(Value::from_runtime)
394        }
395        #[inline]
396        pub fn eq_op(&self, rhs: &Value) -> Value {
397            if let Some(b) = self.0.int_eq(&rhs.0) {
398                return Value::bool(b);
399            }
400            Value::bool(self.values_equal(rhs))
401        }
402        #[inline]
403        pub fn neq_op(&self, rhs: &Value) -> Value {
404            if let Some(b) = self.0.int_eq(&rhs.0) {
405                return Value::bool(!b);
406            }
407            Value::bool(!self.values_equal(rhs))
408        }
409    }
410}
411
412pub use repr::Value;
413
414// ===========================================================================
415// Representation-independent API (delegates through the kernel)
416// ===========================================================================
417
418impl Value {
419    #[inline]
420    pub fn text(s: String) -> Self {
421        Value::from_runtime(RuntimeValue::Text(Rc::new(s)))
422    }
423
424    #[inline]
425    pub fn is_truthy(&self) -> bool {
426        // Inline scalar fast paths avoid materialising; heap arms defer to the
427        // kernel's truthiness (which only the boxed types reach).
428        if let Some(b) = self.as_bool() {
429            return b;
430        }
431        if let Some(n) = self.as_int() {
432            return n != 0;
433        }
434        self.as_runtime().is_truthy()
435    }
436    #[inline]
437    pub fn to_display_string(&self) -> String {
438        self.as_runtime().to_display_string()
439    }
440    /// The value's type name. Borrows `&self` (Struct/Inductive names live in the
441    /// value; the scalar names are `'static` literals) — never a temporary, so it
442    /// is valid under the narrow representation where `as_runtime()` would
443    /// materialise an inline scalar into a short-lived `RuntimeValue`.
444    #[inline]
445    pub fn type_name(&self) -> &str {
446        // Inline scalars have `'static` names; the rest borrow the in-place
447        // (heap, under narrow) `RuntimeValue` directly.
448        if self.as_bool().is_some() {
449            return "Bool";
450        }
451        if self.as_int().is_some() {
452            return "Int";
453        }
454        if self.as_float().is_some() {
455            return "Float";
456        }
457        match self.as_runtime_ref() {
458            Some(rv) => rv.type_name(),
459            None => "Nothing",
460        }
461    }
462
463    /// Value equality for the `equals`/`==` operator and for set/list
464    /// membership (epsilon floats, structural inductives — the kernel's rules).
465    #[inline]
466    pub fn values_equal(&self, other: &Value) -> bool {
467        compare::values_equal(&self.as_runtime(), &other.as_runtime())
468    }
469
470    // ---- Collections (shared kernel) ------------------------------------------
471
472    pub fn list(items: Vec<Value>) -> Self {
473        let values: Vec<RuntimeValue> = items.into_iter().map(Value::into_runtime).collect();
474        Value::from_runtime(RuntimeValue::List(Rc::new(RefCell::new(
475            crate::interpreter::ListRepr::from_values(values),
476        ))))
477    }
478    /// Wrap a raw Int vector as a list value (the native tier's fresh
479    /// allocations re-box through here).
480    pub fn int_list(items: Vec<i64>) -> Self {
481        Value::from_runtime(RuntimeValue::List(Rc::new(RefCell::new(
482            crate::interpreter::ListRepr::Ints(items),
483        ))))
484    }
485
486    pub fn empty_list() -> Self {
487        Value::from_runtime(RuntimeValue::List(Rc::new(RefCell::new(
488            crate::interpreter::ListRepr::Ints(Vec::new()),
489        ))))
490    }
491    /// A fresh empty half-width Int sequence (`ListRepr::IntsI32`), backing a
492    /// narrowing-proven `new Seq of Int` under `LOGOS_NARROW_VM`.
493    pub fn empty_list_i32() -> Self {
494        Value::from_runtime(RuntimeValue::List(Rc::new(RefCell::new(
495            crate::interpreter::ListRepr::IntsI32(Vec::new()),
496        ))))
497    }
498    pub fn empty_set() -> Self {
499        Value::from_runtime(RuntimeValue::Set(Rc::new(RefCell::new(Vec::new()))))
500    }
501    pub fn empty_map() -> Self {
502        Value::from_runtime(RuntimeValue::Map(Rc::new(RefCell::new(
503            crate::interpreter::MapStorage::default(),
504        ))))
505    }
506    pub fn int_range(lo: i64, hi: i64) -> Self {
507        Value::from_runtime(RuntimeValue::List(Rc::new(RefCell::new(
508            crate::interpreter::ListRepr::Ints((lo..=hi).collect()),
509        ))))
510    }
511
512    pub fn list_push(&self, value: Value) -> Result<(), String> {
513        collections::list_push(&self.as_runtime(), value.into_runtime())
514    }
515
516    pub fn len(&self) -> Result<i64, String> {
517        match collections::length_of(&self.as_runtime())? {
518            RuntimeValue::Int(n) => Ok(n),
519            _ => unreachable!("length_of always returns Int"),
520        }
521    }
522
523    pub fn index_get(&self, idx: &Value) -> Result<Value, String> {
524        collections::index_get(&self.as_runtime(), &idx.as_runtime()).map(Value::from_runtime)
525    }
526
527    pub fn set_add(&self, value: Value) -> Result<(), String> {
528        collections::set_add(&self.as_runtime(), value.into_runtime())
529    }
530
531    pub fn remove_from(&self, value: &Value) -> Result<(), String> {
532        collections::remove_from(&self.as_runtime(), &value.as_runtime())
533    }
534
535    pub fn index_set(&self, idx: &Value, value: Value) -> Result<(), String> {
536        collections::index_set(&self.as_runtime(), &idx.as_runtime(), value.into_runtime())
537    }
538
539    pub fn contains(&self, value: &Value) -> Result<bool, String> {
540        match collections::contains(&self.as_runtime(), &value.as_runtime())? {
541            RuntimeValue::Bool(b) => Ok(b),
542            _ => unreachable!("contains always returns Bool"),
543        }
544    }
545
546    // ---- General arithmetic (always through the kernel; no inline fast path) ---
547
548    pub fn div(&self, rhs: &Value) -> Result<Value, String> {
549        arith::divide(self.runtime_owned(), rhs.runtime_owned()).map(Value::from_runtime)
550    }
551
552    /// EXACT division (`Op::ExactDiv`) — the type-directed sibling of [`Value::div`]:
553    /// `7 / 2 → 7/2` (a Rational), never the floored `3`. Routes through the shared
554    /// kernel exactly like `div`, so the VM stays bit-identical to the tree-walker.
555    pub fn exact_div(&self, rhs: &Value) -> Result<Value, String> {
556        arith::exact_divide(self.runtime_owned(), rhs.runtime_owned()).map(Value::from_runtime)
557    }
558
559    /// FLOOR division (`Op::FloorDiv`, `a // b`) — the quotient rounded toward negative
560    /// infinity (`-7 // 2 → -4`), distinct from the truncating [`Value::div`]. Routes
561    /// through the shared kernel, so the VM stays bit-identical to the tree-walker.
562    pub fn floor_div(&self, rhs: &Value) -> Result<Value, String> {
563        arith::floor_divide(self.runtime_owned(), rhs.runtime_owned()).map(Value::from_runtime)
564    }
565
566    pub fn modulo(&self, rhs: &Value) -> Result<Value, String> {
567        arith::modulo(self.runtime_owned(), rhs.runtime_owned()).map(Value::from_runtime)
568    }
569
570    pub fn concat(&self, rhs: &Value) -> Result<Value, String> {
571        arith::concat(self.runtime_owned(), rhs.runtime_owned()).map(Value::from_runtime)
572    }
573
574    /// `a followed by b` — merge two sequences into one (reuses the tree-walker semantics).
575    pub fn seq_concat(&self, rhs: &Value) -> Result<Value, String> {
576        arith::seq_concat(self.runtime_owned(), rhs.runtime_owned()).map(Value::from_runtime)
577    }
578
579    pub fn pow(&self, rhs: &Value) -> Result<Value, String> {
580        arith::binary_op(BinaryOpKind::Pow, self.runtime_owned(), rhs.runtime_owned())
581            .map(Value::from_runtime)
582    }
583
584    pub fn bitxor(&self, rhs: &Value) -> Result<Value, String> {
585        arith::binary_op(BinaryOpKind::BitXor, self.runtime_owned(), rhs.runtime_owned())
586            .map(Value::from_runtime)
587    }
588
589    pub fn bitand(&self, rhs: &Value) -> Result<Value, String> {
590        arith::binary_op(BinaryOpKind::BitAnd, self.runtime_owned(), rhs.runtime_owned())
591            .map(Value::from_runtime)
592    }
593
594    pub fn bitor(&self, rhs: &Value) -> Result<Value, String> {
595        arith::binary_op(BinaryOpKind::BitOr, self.runtime_owned(), rhs.runtime_owned())
596            .map(Value::from_runtime)
597    }
598
599    pub fn shl(&self, rhs: &Value) -> Result<Value, String> {
600        arith::binary_op(BinaryOpKind::Shl, self.runtime_owned(), rhs.runtime_owned())
601            .map(Value::from_runtime)
602    }
603
604    pub fn shr(&self, rhs: &Value) -> Result<Value, String> {
605        arith::binary_op(BinaryOpKind::Shr, self.runtime_owned(), rhs.runtime_owned())
606            .map(Value::from_runtime)
607    }
608
609    /// `not x` — logical for Bool, bitwise for Int, error otherwise.
610    pub fn not_op(&self) -> Result<Value, String> {
611        arith::not_value(self.runtime_owned()).map(Value::from_runtime)
612    }
613
614    /// Materialise an owned `RuntimeValue` (a clone in the default repr; a
615    /// decode in the narrow repr). The kernel's `arith`/`binary_op` entry points
616    /// take owned operands, so the general (non-inlined) ops funnel through here.
617    #[inline]
618    fn runtime_owned(&self) -> RuntimeValue {
619        self.as_runtime().clone()
620    }
621}
622
623#[cfg(test)]
624mod fast_path_kernel_differential {
625    //! Every Value op with an Int×Int fast path must be BIT-IDENTICAL to the
626    //! kernel over the full edge grid — the fast path is a transcription of
627    //! the locked wrapping-i64 spec, and this test is the proof. It runs under
628    //! both representations (the narrow inline path re-boxes a wrap past ±2^47,
629    //! still landing on the kernel's wrapping-i64 answer).
630    use super::*;
631
632    const EDGES: [i64; 12] = [
633        i64::MIN,
634        i64::MIN + 1,
635        -4611686018427387904, // i64::MIN / 2
636        -1000003,
637        -2,
638        -1,
639        0,
640        1,
641        2,
642        1000003,
643        4611686018427387903, // i64::MAX / 2
644        i64::MAX,
645    ];
646
647    fn assert_same(op_name: &str, a: i64, b: i64, fast: Result<Value, String>, kernel: Result<RuntimeValue, String>) {
648        match (fast, kernel) {
649            (Ok(f), Ok(k)) => assert_eq!(
650                f.into_runtime(),
651                k,
652                "{op_name}({a}, {b}) fast path diverged from kernel"
653            ),
654            (Err(f), Err(k)) => assert_eq!(f, k, "{op_name}({a}, {b}) error strings diverged"),
655            (f, k) => panic!("{op_name}({a}, {b}): fast {f:?} vs kernel {k:?}"),
656        }
657    }
658
659    #[test]
660    fn int_arith_matches_kernel_over_edge_grid() {
661        for &a in &EDGES {
662            for &b in &EDGES {
663                let (va, vb) = (Value::int(a), Value::int(b));
664                assert_same("add", a, b, va.add(&vb), arith::add(RuntimeValue::Int(a), RuntimeValue::Int(b)));
665                assert_same("sub", a, b, va.sub(&vb), arith::subtract(RuntimeValue::Int(a), RuntimeValue::Int(b)));
666                assert_same("mul", a, b, va.mul(&vb), arith::multiply(RuntimeValue::Int(a), RuntimeValue::Int(b)));
667            }
668        }
669    }
670
671    #[test]
672    fn int_comparisons_match_kernel_over_edge_grid() {
673        for &a in &EDGES {
674            for &b in &EDGES {
675                let (va, vb) = (Value::int(a), Value::int(b));
676                assert_same("lt", a, b, va.lt(&vb), compare::compare(BinaryOpKind::Lt, &RuntimeValue::Int(a), &RuntimeValue::Int(b)));
677                assert_same("gt", a, b, va.gt(&vb), compare::compare(BinaryOpKind::Gt, &RuntimeValue::Int(a), &RuntimeValue::Int(b)));
678                assert_same("lte", a, b, va.lte(&vb), compare::compare(BinaryOpKind::LtEq, &RuntimeValue::Int(a), &RuntimeValue::Int(b)));
679                assert_same("gte", a, b, va.gte(&vb), compare::compare(BinaryOpKind::GtEq, &RuntimeValue::Int(a), &RuntimeValue::Int(b)));
680                assert_eq!(
681                    va.eq_op(&vb).is_truthy(),
682                    compare::values_equal(&RuntimeValue::Int(a), &RuntimeValue::Int(b)),
683                    "eq({a}, {b}) diverged"
684                );
685                assert_eq!(
686                    va.neq_op(&vb).is_truthy(),
687                    !compare::values_equal(&RuntimeValue::Int(a), &RuntimeValue::Int(b)),
688                    "neq({a}, {b}) diverged"
689                );
690            }
691        }
692    }
693
694    #[test]
695    fn value_width_matches_the_active_representation() {
696        // The headline of WS-F: under `narrow-value` the register-file cell is
697        // ONE machine word (the NaN-boxed `u64`); the default cell is the fat
698        // 16-byte `RuntimeValue` newtype. This pins the size each build promises.
699        let w = std::mem::size_of::<Value>();
700        #[cfg(feature = "narrow-value")]
701        assert_eq!(w, 8, "narrow Value must be 8 bytes (NaN-boxed u64)");
702        #[cfg(not(feature = "narrow-value"))]
703        assert_eq!(w, 16, "default Value must be 16 bytes (RuntimeValue newtype)");
704    }
705
706    #[test]
707    fn mixed_operands_still_route_through_the_kernel() {
708        // Int×Float promotion is kernel territory — the fast path must not
709        // capture it.
710        let sum = Value::int(2).add(&Value::float(0.5)).unwrap();
711        assert_eq!(sum.into_runtime(), RuntimeValue::Float(2.5));
712        assert!(Value::int(2).lt(&Value::float(2.5)).unwrap().is_truthy());
713        // Int×Text: whatever the kernel says (it concatenates), bit-for-bit.
714        let fast = Value::int(2).add(&Value::text("x".into()));
715        let kernel = arith::add(RuntimeValue::Int(2), RuntimeValue::Text(Rc::new("x".into())));
716        assert_eq!(fast.map(Value::into_runtime), kernel);
717        // Int×Bool arithmetic is a kernel ERROR — the fast path must not mask it.
718        let fast = Value::int(2).mul(&Value::bool(true));
719        let kernel = arith::multiply(RuntimeValue::Int(2), RuntimeValue::Bool(true));
720        assert_eq!(fast.map(Value::into_runtime), kernel);
721        assert!(matches!(Value::int(2).mul(&Value::bool(true)), Err(_)));
722    }
723}
724
725#[cfg(test)]
726mod value_comparison_tests {
727    use super::*;
728
729    #[test]
730    fn float_relational_is_ieee() {
731        // -0.0 == 0.0 under IEEE 754.
732        assert!(!Value::float(-0.0).lt(&Value::float(0.0)).unwrap().is_truthy());
733        assert!(Value::float(-0.0).lte(&Value::float(0.0)).unwrap().is_truthy());
734        // NaN is unordered: every relational comparison is false (never an error).
735        let nan = Value::float(f64::NAN);
736        assert!(!nan.lt(&Value::float(1.0)).unwrap().is_truthy());
737        assert!(!nan.gt(&Value::float(1.0)).unwrap().is_truthy());
738        assert!(!nan.lte(&nan).unwrap().is_truthy());
739        assert!(!Value::float(1.0).gte(&nan).unwrap().is_truthy());
740        // Ordinary comparisons still hold.
741        assert!(Value::float(1.5).lt(&Value::float(2.5)).unwrap().is_truthy());
742        assert!(Value::int(2).lt(&Value::float(2.5)).unwrap().is_truthy());
743        assert!(Value::int(5).gte(&Value::int(5)).unwrap().is_truthy());
744    }
745
746    #[test]
747    fn float_equality_matches_treewalker() {
748        // NaN is never equal to itself (IEEE, both engines).
749        assert!(!Value::float(f64::NAN).eq_op(&Value::float(f64::NAN)).is_truthy());
750        assert!(Value::float(f64::NAN).neq_op(&Value::float(f64::NAN)).is_truthy());
751        // Exact equal floats are equal.
752        assert!(Value::float(1.5).eq_op(&Value::float(1.5)).is_truthy());
753        // IEEE equality: 0.1 + 0.2 is NOT 0.3 — matching the tree-walker
754        // (and the compiled backend; `is approximately` is the tolerant form).
755        let sum = Value::float(0.1).add(&Value::float(0.2)).unwrap(); // 0.30000000000000004
756        assert!(!sum.eq_op(&Value::float(0.3)).is_truthy());
757        assert!(sum.eq_op(&Value::float(0.30000000000000004)).is_truthy());
758        // Distinct floats are not equal.
759        assert!(!Value::float(1.0).eq_op(&Value::float(2.0)).is_truthy());
760        // Other types unaffected.
761        assert!(Value::int(5).eq_op(&Value::int(5)).is_truthy());
762        assert!(Value::text("hi".to_string()).eq_op(&Value::text("hi".to_string())).is_truthy());
763        assert!(!Value::int(5).eq_op(&Value::int(6)).is_truthy());
764    }
765}