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