Skip to main content

sui_bytecode/
value.rs

1//! VM-specific value representation.
2//!
3//! Simpler than `sui_eval::Value` — no thunks, no rnix AST references.
4//! The bytecode VM handles laziness through its own mechanisms; values
5//! here are always fully evaluated.
6//!
7//! # String Interning
8//!
9//! Attribute set keys use [`Symbol`] handles instead of heap-allocated
10//! `String`s. This makes key comparison O(1) (integer equality) instead
11//! of O(n) (byte-by-byte string comparison). The interner is shared
12//! between the compiler and VM via `Rc<RefCell<Interner>>`.
13
14use std::cell::Cell;
15use std::collections::BTreeMap;
16use std::fmt;
17use std::path::PathBuf;
18use std::rc::Rc;
19
20use crate::chunk::Chunk;
21use crate::intern::{Interner, Symbol};
22use crate::nanbox::NanBox;
23
24/// A value in the bytecode VM.
25///
26/// Intentionally simpler than the tree-walker's `Value` type: no thunks
27/// (the VM manages laziness via its call stack), no rnix AST nodes.
28///
29/// Attribute sets use [`Symbol`] keys for O(1) comparisons. Use
30/// [`VMValue::attrs_to_strings`] to convert back to `BTreeMap<String, VMValue>`
31/// for external consumption.
32#[derive(Clone)]
33pub enum VMValue {
34    /// Nix `null`.
35    Null,
36    /// Nix boolean.
37    Bool(bool),
38    /// Nix integer (64-bit signed).
39    Int(i64),
40    /// Nix float (64-bit IEEE 754).
41    Float(f64),
42    /// Nix string (context tracking deferred to Phase 2).
43    String(String),
44    /// Nix path literal.
45    Path(String),
46    /// Nix list.
47    List(Vec<VMValue>),
48    /// Nix attribute set with interned keys.
49    Attrs(BTreeMap<Symbol, VMValue>),
50    /// A closure: compiled function body + captured upvalues.
51    Closure(VMClosure),
52    /// A built-in function (native Rust implementation).
53    Builtin(VMBuiltin),
54    /// A lazy thunk: deferred computation, evaluated on first force.
55    Thunk(VMThunk),
56    /// A higher-order builtin: partially applied operation that needs VM
57    /// access to call closures. The VM intercepts calls to these and
58    /// executes them with full execution context.
59    HigherOrderBuiltin(HigherOrderBuiltin),
60}
61
62/// Tag identifying which higher-order operation a partially-applied
63/// builtin represents.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum HigherOrderOp {
66    /// `map f list` -- apply f to each element
67    Map,
68    /// `filter pred list` -- keep elements where pred returns true
69    Filter,
70    /// `foldl' f init` -- strict left fold (first partial: has f only)
71    FoldlP1,
72    /// `foldl' f init list` -- strict left fold (second partial: has f + init)
73    FoldlP2,
74    /// `sort comparator list` -- sort using comparator function
75    Sort,
76    /// `genList f n` -- generate list by calling f(0)..f(n-1)
77    GenList,
78    /// `concatMap f list` -- map then concatenate results
79    ConcatMap,
80    /// `any pred list` -- true if any element satisfies pred
81    Any,
82    /// `all pred list` -- true if all elements satisfy pred
83    All,
84    /// `partition pred list` -- split into { right, wrong }
85    Partition,
86    /// `groupBy f list` -- group elements by f result
87    GroupBy,
88    /// `mapAttrs f attrs` -- apply f to each attr value
89    MapAttrs,
90    /// `filterAttrs pred attrs` -- keep attrs where pred name value is true
91    FilterAttrs,
92    /// `elem needle list` -- check if needle is in list (needs VM to force elements)
93    Elem,
94}
95
96/// A partially-applied higher-order builtin that needs VM access to
97/// call user closures.
98#[derive(Clone)]
99pub struct HigherOrderBuiltin {
100    /// Which operation this represents.
101    pub op: HigherOrderOp,
102    /// The captured function/predicate/comparator.
103    pub func: Box<VMValue>,
104    /// Additional captured arguments (e.g., `init` for foldl').
105    pub extra_args: Vec<VMValue>,
106}
107
108/// A compiled closure: the function's bytecode chunk plus captured values.
109#[derive(Clone)]
110pub struct VMClosure {
111    /// The function's compiled bytecode.
112    pub chunk: Rc<Chunk>,
113    /// Captured upvalues (values from enclosing scopes).
114    ///
115    /// Stored as `NanBox` (the runtime frame representation) so that closure
116    /// capture and invocation are Rc-refcount clones, not deep VMValue<->NanBox
117    /// round-trips. The runtime `CallFrame` already holds `Vec<NanBox>`.
118    pub upvalues: Vec<NanBox>,
119    /// Number of parameters this closure expects (1 for Nix lambdas,
120    /// but pattern-match destructuring may set multiple locals).
121    pub arity: u16,
122    /// Name hint for error messages (e.g., the parameter name).
123    pub name: Option<String>,
124    /// Formal parameter names and whether they have defaults.
125    /// Populated for pattern-destructuring lambdas (`{ a, b ? 1 }: ...`).
126    /// Empty for simple ident-param lambdas (`x: ...`).
127    /// Used by `builtins.functionArgs`.
128    pub formals: Vec<(String, bool)>,
129}
130
131/// A built-in function callable from the VM.
132#[derive(Clone)]
133pub struct VMBuiltin {
134    /// Name for error messages (e.g., "length", "map<partial>").
135    pub name: &'static str,
136    /// The native implementation. Takes args and returns a result.
137    pub func: Rc<dyn Fn(Vec<VMValue>) -> Result<VMValue, crate::error::VMError>>,
138    /// How many arguments this builtin expects (0 = variadic/partial).
139    pub arity: u8,
140}
141
142impl fmt::Debug for VMBuiltin {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(f, "<builtin {}>", self.name)
145    }
146}
147
148impl fmt::Debug for HigherOrderBuiltin {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "<hof {:?}>", self.op)
151    }
152}
153
154/// State of a thunk's evaluation lifecycle.
155#[derive(Clone)]
156pub enum ThunkState {
157    /// Not yet evaluated. Holds the bytecode chunk to execute and
158    /// captured upvalues for the thunk body.
159    Pending {
160        chunk: Rc<Chunk>,
161        upvalues: Vec<NanBox>,
162    },
163    /// Lazy source: the thunk body has not been compiled yet.
164    /// On first force, the source span is compiled and then executed.
165    /// This avoids compiling thunk bodies that are never forced.
166    LazySource {
167        /// Shared source text of the file containing this thunk.
168        source: Rc<String>,
169        /// Byte offset of the expression within the source.
170        offset: usize,
171        /// Byte length of the expression.
172        length: usize,
173        /// Base directory for resolving relative imports.
174        base_dir: PathBuf,
175        /// Captured upvalues (resolved at thunk creation time).
176        upvalues: Vec<NanBox>,
177    },
178    /// A native Rust callback that produces a value on demand.
179    ///
180    /// Used by the tree-walker bridge to wrap lazy flake input thunks:
181    /// instead of eagerly forcing all inputs during `eval_to_string_keyed`,
182    /// the tree-walker thunk is wrapped in a callback and only evaluated
183    /// when the VM actually accesses the value.
184    NativeCallback(Rc<dyn Fn() -> Result<StringKeyedValue, String>>),
185    /// Currently being evaluated -- detects infinite recursion (blackhole).
186    Evaluating,
187    /// Already evaluated and memoized.
188    Done(Box<VMValue>),
189}
190
191/// A lazy thunk with memoization and blackhole detection.
192#[derive(Clone)]
193pub struct VMThunk {
194    pub state: Rc<Cell<Option<ThunkState>>>,
195}
196
197impl VMThunk {
198    /// Create a new pending thunk.
199    pub fn new(chunk: Rc<Chunk>, upvalues: Vec<NanBox>) -> Self {
200        Self {
201            state: Rc::new(Cell::new(Some(ThunkState::Pending { chunk, upvalues }))),
202        }
203    }
204
205    /// Create a thunk that is already evaluated (optimization).
206    pub fn new_done(value: VMValue) -> Self {
207        Self {
208            state: Rc::new(Cell::new(Some(ThunkState::Done(Box::new(value))))),
209        }
210    }
211
212    /// Create a native callback thunk from a Rust closure.
213    ///
214    /// The callback is invoked lazily the first time the thunk is forced,
215    /// and the result is memoized. Used by the builtin bridge to wrap
216    /// tree-walker computations that should be deferred.
217    pub fn new_native<F>(callback: F) -> Self
218    where
219        F: Fn() -> Result<VMValue, crate::error::VMError> + 'static,
220    {
221        // Wrap the VMValue-returning callback into a StringKeyedValue callback
222        // that the NativeCallback variant expects.
223        let wrapped: Rc<dyn Fn() -> Result<StringKeyedValue, String>> =
224            Rc::new(move || {
225                let val = callback().map_err(|e| e.to_string())?;
226                let interner = crate::intern::Interner::new();
227                Ok(val.to_string_keyed(&interner))
228            });
229        Self {
230            state: Rc::new(Cell::new(Some(ThunkState::NativeCallback(wrapped)))),
231        }
232    }
233}
234
235impl fmt::Debug for VMThunk {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        write!(f, "<thunk>")
238    }
239}
240
241impl fmt::Debug for VMClosure {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        write!(f, "<closure arity={}", self.arity)?;
244        if let Some(ref name) = self.name {
245            write!(f, " name={name}")?;
246        }
247        write!(f, ">")
248    }
249}
250
251impl VMValue {
252    /// Return the Nix type name for this value.
253    #[must_use]
254    pub fn type_name(&self) -> &'static str {
255        match self {
256            VMValue::Null => "null",
257            VMValue::Bool(_) => "bool",
258            VMValue::Int(_) => "int",
259            VMValue::Float(_) => "float",
260            VMValue::String(_) => "string",
261            VMValue::Path(_) => "path",
262            VMValue::List(_) => "list",
263            VMValue::Attrs(_) => "set",
264            VMValue::Closure(_) | VMValue::Builtin(_) | VMValue::HigherOrderBuiltin(_) => "lambda",
265            VMValue::Thunk(_) => "thunk",
266        }
267    }
268
269    /// Check if this value is truthy (for conditionals).
270    pub fn is_truthy(&self) -> Result<bool, crate::error::VMError> {
271        match self {
272            VMValue::Bool(b) => Ok(*b),
273            other => Err(crate::error::VMError::TypeError {
274                expected: "bool",
275                got: other.type_name(),
276                context: "condition".to_string(),
277            }),
278        }
279    }
280
281    /// Convert a `Symbol`-keyed attrset to a `String`-keyed `BTreeMap`
282    /// using the provided interner. Returns `None` if not an `Attrs`.
283    #[must_use]
284    pub fn attrs_to_strings(&self, interner: &Interner) -> Option<BTreeMap<String, VMValue>> {
285        match self {
286            VMValue::Attrs(attrs) => {
287                let map = attrs
288                    .iter()
289                    .map(|(sym, val)| (interner.resolve(*sym).to_string(), val.clone()))
290                    .collect();
291                Some(map)
292            }
293            _ => None,
294        }
295    }
296
297    /// Convert this entire value tree to use string keys (for external API).
298    /// Recursively resolves all `Symbol` keys in nested attrsets and lists.
299    #[must_use]
300    pub fn to_string_keyed(&self, interner: &Interner) -> StringKeyedValue {
301        match self {
302            VMValue::Null => StringKeyedValue::Null,
303            VMValue::Bool(b) => StringKeyedValue::Bool(*b),
304            VMValue::Int(n) => StringKeyedValue::Int(*n),
305            VMValue::Float(f) => StringKeyedValue::Float(*f),
306            VMValue::String(s) => StringKeyedValue::String(s.clone()),
307            VMValue::Path(p) => StringKeyedValue::Path(p.clone()),
308            VMValue::List(items) => {
309                StringKeyedValue::List(items.iter().map(|v| v.to_string_keyed(interner)).collect())
310            }
311            VMValue::Attrs(attrs) => {
312                let map = attrs
313                    .iter()
314                    .map(|(sym, val)| {
315                        (interner.resolve(*sym).to_string(), val.to_string_keyed(interner))
316                    })
317                    .collect();
318                StringKeyedValue::Attrs(map)
319            }
320            VMValue::Closure(_) | VMValue::Builtin(_) | VMValue::HigherOrderBuiltin(_) => {
321                StringKeyedValue::Lambda
322            }
323            VMValue::Thunk(t) => {
324                // If the thunk is already forced, convert the memoized value.
325                // Otherwise fall back to Lambda (the VM should have forced it).
326                let state = t.state.take();
327                match &state {
328                    Some(ThunkState::Done(v)) => {
329                        let result = v.to_string_keyed(interner);
330                        t.state.set(state);
331                        result
332                    }
333                    _ => {
334                        t.state.set(state);
335                        StringKeyedValue::Lambda
336                    }
337                }
338            }
339        }
340    }
341
342    /// Format this value for display using the interner for key resolution.
343    pub fn display_with(&self, interner: &Interner, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344        match self {
345            VMValue::Null => write!(f, "null"),
346            VMValue::Bool(b) => write!(f, "{b}"),
347            VMValue::Int(n) => write!(f, "{n}"),
348            VMValue::Float(n) => {
349                if n.fract() == 0.0 {
350                    write!(f, "{n:.6}")
351                } else {
352                    write!(f, "{n}")
353                }
354            }
355            VMValue::String(s) => write!(f, "\"{s}\""),
356            VMValue::Path(p) => write!(f, "{p}"),
357            VMValue::List(items) => {
358                write!(f, "[ ")?;
359                for item in items {
360                    item.display_with(interner, f)?;
361                    write!(f, " ")?;
362                }
363                write!(f, "]")
364            }
365            VMValue::Attrs(map) => {
366                write!(f, "{{ ")?;
367                for (sym, v) in map {
368                    let key = interner.resolve(*sym);
369                    write!(f, "{key} = ")?;
370                    v.display_with(interner, f)?;
371                    write!(f, "; ")?;
372                }
373                write!(f, "}}")
374            }
375            VMValue::Closure(_) => write!(f, "<<lambda>>"),
376            VMValue::Builtin(b) => write!(f, "<<builtin {}>>", b.name),
377            VMValue::HigherOrderBuiltin(h) => write!(f, "<<builtin {:?}>>", h.op),
378            VMValue::Thunk(_) => write!(f, "<<thunk>>"),
379        }
380    }
381
382    /// Debug this value using the interner for key resolution.
383    pub fn debug_with(&self, interner: &Interner, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        match self {
385            VMValue::Null => write!(f, "null"),
386            VMValue::Bool(b) => write!(f, "{b}"),
387            VMValue::Int(n) => write!(f, "{n}"),
388            VMValue::Float(n) => write!(f, "{}", sui_compat::versions::cppnix_format_float(*n)),
389            VMValue::String(s) => write!(f, "{s:?}"),
390            VMValue::Path(p) => write!(f, "{p}"),
391            VMValue::List(items) => {
392                write!(f, "[ ")?;
393                for item in items {
394                    item.debug_with(interner, f)?;
395                    write!(f, " ")?;
396                }
397                write!(f, "]")
398            }
399            VMValue::Attrs(map) => {
400                write!(f, "{{ ")?;
401                for (sym, v) in map {
402                    let key = interner.resolve(*sym);
403                    write!(f, "{key} = ")?;
404                    v.debug_with(interner, f)?;
405                    write!(f, "; ")?;
406                }
407                write!(f, "}}")
408            }
409            VMValue::Closure(c) => write!(f, "{c:?}"),
410            VMValue::Builtin(b) => write!(f, "{b:?}"),
411            VMValue::HigherOrderBuiltin(h) => write!(f, "{h:?}"),
412            VMValue::Thunk(t) => write!(f, "{t:?}"),
413        }
414    }
415}
416
417/// A string-keyed value for external API consumption.
418///
419/// Produced by [`VMValue::to_string_keyed`]. Uses `BTreeMap<String, _>`
420/// for attrsets so callers don't need access to the interner.
421///
422/// The `Thunk` variant carries a deferred computation from the tree-walker
423/// bridge. When the VM encounters it during `string_keyed_to_nanbox`, it
424/// wraps the callback in a `VMThunk` so the value is only evaluated when
425/// actually accessed. This keeps `getFlake` fast by not eagerly resolving
426/// all transitive flake inputs.
427pub enum StringKeyedValue {
428    Null,
429    Bool(bool),
430    Int(i64),
431    Float(f64),
432    String(String),
433    Path(String),
434    List(Vec<StringKeyedValue>),
435    Attrs(BTreeMap<String, StringKeyedValue>),
436    Lambda,
437    /// A deferred value — evaluated on demand when the VM forces it.
438    ///
439    /// The callback returns a `StringKeyedValue` which is then converted
440    /// to a `NanBox` by the VM. Uses `Rc<dyn Fn()>` for cheap cloning
441    /// and shared memoization.
442    Thunk(Rc<dyn Fn() -> Result<StringKeyedValue, String>>),
443    /// A callable tree-walker function wrapped as a bridge callback.
444    ///
445    /// When the VM needs to call this, it converts args through the
446    /// bridge and delegates to the tree-walker. Created when a Lambda
447    /// or Builtin value crosses the tree-walker → VM boundary.
448    Callable(Rc<dyn Fn(StringKeyedValue) -> Result<StringKeyedValue, String>>),
449}
450
451impl Clone for StringKeyedValue {
452    fn clone(&self) -> Self {
453        match self {
454            Self::Null => Self::Null,
455            Self::Bool(b) => Self::Bool(*b),
456            Self::Int(n) => Self::Int(*n),
457            Self::Float(f) => Self::Float(*f),
458            Self::String(s) => Self::String(s.clone()),
459            Self::Path(p) => Self::Path(p.clone()),
460            Self::List(items) => Self::List(items.clone()),
461            Self::Attrs(map) => Self::Attrs(map.clone()),
462            Self::Lambda => Self::Lambda,
463            Self::Thunk(cb) => Self::Thunk(Rc::clone(cb)),
464            Self::Callable(cb) => Self::Callable(Rc::clone(cb)),
465        }
466    }
467}
468
469impl PartialEq for StringKeyedValue {
470    fn eq(&self, other: &Self) -> bool {
471        match (self, other) {
472            (Self::Null, Self::Null) => true,
473            (Self::Bool(a), Self::Bool(b)) => a == b,
474            (Self::Int(a), Self::Int(b)) => a == b,
475            (Self::Float(a), Self::Float(b)) => a == b,
476            (Self::String(a), Self::String(b)) => a == b,
477            (Self::Path(a), Self::Path(b)) => a == b,
478            (Self::List(a), Self::List(b)) => a == b,
479            (Self::Attrs(a), Self::Attrs(b)) => a == b,
480            (Self::Lambda, Self::Lambda) => true,
481            // Thunks are never structurally equal (identity comparison
482            // would be misleading since they are lazy).
483            (Self::Thunk(_), _) | (_, Self::Thunk(_)) => false,
484            // Callables are function values — identity comparison is misleading.
485            (Self::Callable(_), _) | (_, Self::Callable(_)) => false,
486            _ => false,
487        }
488    }
489}
490
491impl Eq for StringKeyedValue {}
492
493impl fmt::Debug for StringKeyedValue {
494    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495        match self {
496            Self::Null => write!(f, "Null"),
497            Self::Bool(b) => write!(f, "Bool({b})"),
498            Self::Int(n) => write!(f, "Int({n})"),
499            Self::Float(v) => write!(f, "Float({v})"),
500            Self::String(s) => write!(f, "String({s:?})"),
501            Self::Path(p) => write!(f, "Path({p:?})"),
502            Self::List(items) => f.debug_tuple("List").field(items).finish(),
503            Self::Attrs(map) => f.debug_tuple("Attrs").field(map).finish(),
504            Self::Lambda => write!(f, "Lambda"),
505            Self::Thunk(_) => write!(f, "Thunk(<deferred>)"),
506            Self::Callable(_) => write!(f, "Callable(<bridge-fn>)"),
507        }
508    }
509}
510
511impl fmt::Display for StringKeyedValue {
512    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513        match self {
514            StringKeyedValue::Null => write!(f, "null"),
515            StringKeyedValue::Bool(b) => write!(f, "{b}"),
516            StringKeyedValue::Int(n) => write!(f, "{n}"),
517            StringKeyedValue::Float(n) => write!(f, "{}", sui_compat::versions::cppnix_format_float(*n)),
518            StringKeyedValue::String(s) => write!(f, "\"{s}\""),
519            StringKeyedValue::Path(p) => write!(f, "{p}"),
520            StringKeyedValue::List(items) => {
521                write!(f, "[ ")?;
522                for item in items {
523                    write!(f, "{item} ")?;
524                }
525                write!(f, "]")
526            }
527            StringKeyedValue::Attrs(map) => {
528                write!(f, "{{ ")?;
529                for (k, v) in map {
530                    write!(f, "{k} = {v}; ")?;
531                }
532                write!(f, "}}")
533            }
534            StringKeyedValue::Lambda => write!(f, "<<lambda>>"),
535            StringKeyedValue::Thunk(_) => write!(f, "<<thunk>>"),
536            StringKeyedValue::Callable(_) => write!(f, "<<lambda>>"),
537        }
538    }
539}
540
541// -- Debug / Display without interner (best-effort) --------------------
542
543impl fmt::Debug for VMValue {
544    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
545        match self {
546            VMValue::Null => write!(f, "null"),
547            VMValue::Bool(b) => write!(f, "{b}"),
548            VMValue::Int(n) => write!(f, "{n}"),
549            VMValue::Float(n) => write!(f, "{}", sui_compat::versions::cppnix_format_float(*n)),
550            VMValue::String(s) => write!(f, "{s:?}"),
551            VMValue::Path(p) => write!(f, "{p}"),
552            VMValue::List(items) => {
553                write!(f, "[ ")?;
554                for item in items {
555                    write!(f, "{item:?} ")?;
556                }
557                write!(f, "]")
558            }
559            VMValue::Attrs(map) => {
560                write!(f, "{{ ")?;
561                for (sym, v) in map {
562                    write!(f, "#{} = {v:?}; ", sym.index())?;
563                }
564                write!(f, "}}")
565            }
566            VMValue::Closure(c) => write!(f, "{c:?}"),
567            VMValue::Builtin(b) => write!(f, "{b:?}"),
568            VMValue::HigherOrderBuiltin(h) => write!(f, "{h:?}"),
569            VMValue::Thunk(t) => write!(f, "{t:?}"),
570        }
571    }
572}
573
574impl fmt::Display for VMValue {
575    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
576        match self {
577            VMValue::Null => write!(f, "null"),
578            VMValue::Bool(b) => write!(f, "{b}"),
579            VMValue::Int(n) => write!(f, "{n}"),
580            VMValue::Float(n) => {
581                // Nix always prints at least one decimal place for floats.
582                if n.fract() == 0.0 {
583                    write!(f, "{n:.6}")
584                } else {
585                    write!(f, "{n}")
586                }
587            }
588            VMValue::String(s) => write!(f, "\"{s}\""),
589            VMValue::Path(p) => write!(f, "{p}"),
590            VMValue::List(items) => {
591                write!(f, "[ ")?;
592                for item in items {
593                    write!(f, "{item} ")?;
594                }
595                write!(f, "]")
596            }
597            VMValue::Attrs(map) => {
598                write!(f, "{{ ")?;
599                for (sym, v) in map {
600                    write!(f, "#{} = {v}; ", sym.index())?;
601                }
602                write!(f, "}}")
603            }
604            VMValue::Closure(_) => write!(f, "<<lambda>>"),
605            VMValue::Builtin(b) => write!(f, "<<builtin {}>>", b.name),
606            VMValue::HigherOrderBuiltin(h) => write!(f, "<<builtin {:?}>>", h.op),
607            VMValue::Thunk(_) => write!(f, "<<thunk>>"),
608        }
609    }
610}
611
612impl PartialEq for VMValue {
613    fn eq(&self, other: &Self) -> bool {
614        match (self, other) {
615            (VMValue::Null, VMValue::Null) => true,
616            (VMValue::Bool(a), VMValue::Bool(b)) => a == b,
617            (VMValue::Int(a), VMValue::Int(b)) => a == b,
618            (VMValue::Float(a), VMValue::Float(b)) => a == b,
619            (VMValue::Int(a), VMValue::Float(b)) | (VMValue::Float(b), VMValue::Int(a)) => {
620                (*a as f64) == *b
621            }
622            (VMValue::String(a), VMValue::String(b)) => a == b,
623            (VMValue::Path(a), VMValue::Path(b)) => a == b,
624            (VMValue::List(a), VMValue::List(b)) => a == b,
625            (VMValue::Attrs(a), VMValue::Attrs(b)) => a == b,
626            _ => false,
627        }
628    }
629}
630
631impl Eq for VMValue {}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    #[test]
638    fn type_names() {
639        assert_eq!(VMValue::Null.type_name(), "null");
640        assert_eq!(VMValue::Bool(true).type_name(), "bool");
641        assert_eq!(VMValue::Int(0).type_name(), "int");
642        assert_eq!(VMValue::Float(0.0).type_name(), "float");
643        assert_eq!(VMValue::String("".to_string()).type_name(), "string");
644        assert_eq!(VMValue::Path("/tmp".to_string()).type_name(), "path");
645        assert_eq!(VMValue::List(vec![]).type_name(), "list");
646        assert_eq!(VMValue::Attrs(BTreeMap::new()).type_name(), "set");
647    }
648
649    #[test]
650    fn equality_int_float_coercion() {
651        assert_eq!(VMValue::Int(1), VMValue::Float(1.0));
652        assert_eq!(VMValue::Float(1.0), VMValue::Int(1));
653        assert_ne!(VMValue::Int(1), VMValue::Float(1.5));
654    }
655
656    #[test]
657    fn equality_same_types() {
658        assert_eq!(VMValue::Null, VMValue::Null);
659        assert_eq!(VMValue::Bool(true), VMValue::Bool(true));
660        assert_ne!(VMValue::Bool(true), VMValue::Bool(false));
661        assert_eq!(VMValue::Int(42), VMValue::Int(42));
662        assert_eq!(
663            VMValue::String("hello".to_string()),
664            VMValue::String("hello".to_string())
665        );
666    }
667
668    #[test]
669    fn equality_different_types() {
670        assert_ne!(VMValue::Null, VMValue::Bool(false));
671        assert_ne!(VMValue::Int(0), VMValue::Bool(false));
672        assert_ne!(VMValue::String("1".to_string()), VMValue::Int(1));
673    }
674
675    #[test]
676    fn is_truthy_bool() {
677        assert!(VMValue::Bool(true).is_truthy().unwrap());
678        assert!(!VMValue::Bool(false).is_truthy().unwrap());
679    }
680
681    #[test]
682    fn is_truthy_non_bool_errors() {
683        assert!(VMValue::Int(1).is_truthy().is_err());
684        assert!(VMValue::Null.is_truthy().is_err());
685    }
686
687    #[test]
688    fn attrs_to_strings_conversion() {
689        let mut interner = Interner::new();
690        let key = interner.intern("hello");
691        let mut attrs = BTreeMap::new();
692        attrs.insert(key, VMValue::Int(42));
693        let val = VMValue::Attrs(attrs);
694        let string_map = val.attrs_to_strings(&interner).unwrap();
695        assert_eq!(string_map.get("hello"), Some(&VMValue::Int(42)));
696    }
697
698    #[test]
699    fn to_string_keyed_roundtrip() {
700        let mut interner = Interner::new();
701        let key = interner.intern("x");
702        let mut attrs = BTreeMap::new();
703        attrs.insert(key, VMValue::Int(1));
704        let val = VMValue::Attrs(attrs);
705        let sk = val.to_string_keyed(&interner);
706        match sk {
707            StringKeyedValue::Attrs(map) => {
708                assert_eq!(map.get("x"), Some(&StringKeyedValue::Int(1)));
709            }
710            _ => panic!("expected Attrs"),
711        }
712    }
713
714    #[test]
715    fn symbol_keyed_attrs_equality() {
716        let mut interner = Interner::new();
717        let k1 = interner.intern("a");
718        let k2 = interner.intern("a");
719        let mut a1 = BTreeMap::new();
720        a1.insert(k1, VMValue::Int(1));
721        let mut a2 = BTreeMap::new();
722        a2.insert(k2, VMValue::Int(1));
723        assert_eq!(VMValue::Attrs(a1), VMValue::Attrs(a2));
724    }
725}