Skip to main content

sui_bytecode/
builtins.rs

1//! Built-in function registry for the bytecode VM.
2//!
3//! Implements Nix builtins natively in the VM value system. These are
4//! the core builtins needed for nixpkgs evaluation. Each builtin is
5//! registered by name and index, and can be called via the `CallBuiltin`
6//! opcode or through the `builtins` attrset.
7//!
8//! Curried builtins (e.g., `map f list`) return a `VMBuiltin` partial
9//! application on the first call, then complete on the second.
10
11use std::cell::Cell;
12use std::collections::BTreeMap;
13use std::rc::Rc;
14
15use crate::error::VMError;
16use crate::intern::{Interner, Symbol};
17use crate::value::{HigherOrderBuiltin, HigherOrderOp, ThunkState, VMBuiltin, VMValue};
18
19/// Registry of builtin functions accessible from the VM.
20pub struct BuiltinRegistry {
21    /// Builtins indexed by name.
22    entries: Vec<BuiltinEntry>,
23}
24
25struct BuiltinEntry {
26    name: &'static str,
27    func: Rc<dyn Fn(Vec<VMValue>) -> Result<VMValue, VMError>>,
28    arity: u8,
29}
30
31impl BuiltinRegistry {
32    /// Create a new registry with all Nix builtins registered.
33    #[must_use]
34    pub fn new() -> Self {
35        let mut reg = Self {
36            entries: Vec::new(),
37        };
38        reg.register_all();
39        reg
40    }
41
42    /// Look up a builtin by name, returning its index.
43    #[must_use]
44    pub fn lookup(&self, name: &str) -> Option<u16> {
45        self.entries
46            .iter()
47            .position(|e| e.name == name)
48            .map(|i| i as u16)
49    }
50
51    /// Call a builtin by index.
52    pub fn call(&self, index: u16, args: Vec<VMValue>) -> Result<VMValue, VMError> {
53        let entry = self
54            .entries
55            .get(index as usize)
56            .ok_or_else(|| VMError::UnknownBuiltin(format!("index {index}")))?;
57        (entry.func)(args)
58    }
59
60    /// Build the `builtins` attribute set with all registered builtins.
61    pub fn make_builtins_attrset(&self, interner: &mut Interner) -> VMValue {
62        let mut attrs = BTreeMap::new();
63        for (i, entry) in self.entries.iter().enumerate() {
64            let sym = interner.intern(entry.name);
65            let builtin = VMValue::Builtin(VMBuiltin {
66                name: entry.name,
67                func: Rc::clone(&entry.func),
68                arity: entry.arity,
69            });
70            let _ = i;
71            attrs.insert(sym, builtin);
72        }
73
74        // Add builtins.currentSystem
75        let sys_sym = interner.intern("currentSystem");
76        let system = if cfg!(target_arch = "aarch64") {
77            if cfg!(target_os = "macos") {
78                "aarch64-darwin"
79            } else {
80                "aarch64-linux"
81            }
82        } else if cfg!(target_os = "macos") {
83            "x86_64-darwin"
84        } else {
85            "x86_64-linux"
86        };
87        attrs.insert(sys_sym, VMValue::String(system.to_string()));
88
89        // Add builtins.nixVersion.
90        //
91        // This literal was "2.24.0" while the tree-walker and sui-ir both said
92        // "2.34.7" — the VM was the arm left behind when the impersonation
93        // target was corrected. Because nixpkgs feature-gates on
94        // `lib.versionAtLeast builtins.nixVersion X`, the VM took the wrong
95        // branch of every such gate and silently evaluated a different
96        // derivation graph than the walker. Now derived, so it cannot happen
97        // again in either direction.
98        let ver_sym = interner.intern("nixVersion");
99        attrs.insert(
100            ver_sym,
101            VMValue::String(sui_compat::versions::IMPERSONATED_NIX_VERSION.to_string()),
102        );
103
104        // Add builtins.langVersion
105        let lang_sym = interner.intern("langVersion");
106        attrs.insert(lang_sym, VMValue::Int(sui_compat::versions::LANG_VERSION));
107
108        // Add builtins.true / builtins.false / builtins.null
109        let true_sym = interner.intern("true");
110        attrs.insert(true_sym, VMValue::Bool(true));
111        let false_sym = interner.intern("false");
112        attrs.insert(false_sym, VMValue::Bool(false));
113        let null_sym = interner.intern("null");
114        attrs.insert(null_sym, VMValue::Null);
115
116        // Add builtins.storeDir
117        let store_sym = interner.intern("storeDir");
118        attrs.insert(store_sym, VMValue::String("/nix/store".to_string()));
119
120        // Add builtins.nixPath from NIX_PATH environment variable
121        let nixpath_sym = interner.intern("nixPath");
122        let nix_path_list = {
123            let nix_path = std::env::var("NIX_PATH").unwrap_or_default();
124            let entries: Vec<VMValue> = nix_path
125                .split(':')
126                .filter(|s| !s.is_empty())
127                .map(|entry| {
128                    let (prefix, path) = if let Some(idx) = entry.find('=') {
129                        (entry[..idx].to_string(), entry[idx + 1..].to_string())
130                    } else {
131                        (String::new(), entry.to_string())
132                    };
133                    let prefix_sym = interner.intern("prefix");
134                    let path_sym = interner.intern("path");
135                    let mut entry_attrs = BTreeMap::new();
136                    entry_attrs.insert(prefix_sym, VMValue::String(prefix));
137                    entry_attrs.insert(path_sym, VMValue::String(path));
138                    VMValue::Attrs(entry_attrs)
139                })
140                .collect();
141            VMValue::List(entries)
142        };
143        attrs.insert(nixpath_sym, nix_path_list);
144
145        // Add builtins.currentTime (0 in pure eval mode)
146        let time_sym = interner.intern("currentTime");
147        attrs.insert(time_sym, VMValue::Int(0));
148
149        // `builtins.builtins` — nix's `builtins` attrset contains ITSELF (it is
150        // what `scopedImport`'s injected scope is built from, and `lib` probes
151        // it), so `builtins ? builtins` is TRUE on nix and on the tree-walker.
152        // It answered FALSE here.
153        //
154        // Nix's is infinitely self-referential; this is ONE level deep, because
155        // the VM's `builtins` is rebuilt eagerly on every `PushBuiltins` and a
156        // truly cyclic value would not terminate. One level answers every shape
157        // observed in the wild (`builtins ? builtins`, `builtins.builtins.X`);
158        // `builtins.builtins.builtins` is the honest remaining gap.
159        let self_sym = interner.intern("builtins");
160        let inner = attrs.clone();
161        attrs.insert(self_sym, VMValue::Attrs(inner));
162
163        VMValue::Attrs(attrs)
164    }
165
166    /// Get the name of a builtin by index.
167    #[must_use]
168    pub fn name(&self, index: u16) -> Option<&'static str> {
169        self.entries.get(index as usize).map(|e| e.name)
170    }
171
172    fn register(
173        &mut self,
174        name: &'static str,
175        arity: u8,
176        func: impl Fn(Vec<VMValue>) -> Result<VMValue, VMError> + 'static,
177    ) {
178        self.entries.push(BuiltinEntry {
179            name,
180            func: Rc::new(func),
181            arity,
182        });
183    }
184
185    fn register_all(&mut self) {
186        self.register_type_checks();
187        self.register_list_ops();
188        self.register_higher_order_ops();
189        self.register_attrset_ops();
190        self.register_string_ops();
191        self.register_conversion_ops();
192        self.register_control_ops();
193        self.register_arithmetic_ops();
194        self.register_derivation_ops();
195        self.register_missing_builtins();
196    }
197
198    // ── Type checking ─────────────────────────────────────────────
199
200    fn register_type_checks(&mut self) {
201        self.register("typeOf", 1, |args| {
202            let name = match &args[0] {
203                VMValue::Null => "null",
204                VMValue::Bool(_) => "bool",
205                VMValue::Int(_) => "int",
206                VMValue::Float(_) => "float",
207                VMValue::String(_) => "string",
208                VMValue::Path(_) => "path",
209                VMValue::List(_) => "list",
210                VMValue::Attrs(_) => "set",
211                VMValue::Closure(_) | VMValue::Builtin(_) | VMValue::HigherOrderBuiltin(_) => "lambda",
212                VMValue::Thunk(_) => "thunk",
213            };
214            Ok(VMValue::String(name.to_string()))
215        });
216        self.register("isNull", 1, |args| {
217            Ok(VMValue::Bool(matches!(args[0], VMValue::Null)))
218        });
219        self.register("isInt", 1, |args| {
220            Ok(VMValue::Bool(matches!(args[0], VMValue::Int(_))))
221        });
222        self.register("isFloat", 1, |args| {
223            Ok(VMValue::Bool(matches!(args[0], VMValue::Float(_))))
224        });
225        self.register("isBool", 1, |args| {
226            Ok(VMValue::Bool(matches!(args[0], VMValue::Bool(_))))
227        });
228        self.register("isString", 1, |args| {
229            Ok(VMValue::Bool(matches!(args[0], VMValue::String(_))))
230        });
231        self.register("isList", 1, |args| {
232            Ok(VMValue::Bool(matches!(args[0], VMValue::List(_))))
233        });
234        self.register("isAttrs", 1, |args| {
235            Ok(VMValue::Bool(matches!(args[0], VMValue::Attrs(_))))
236        });
237        self.register("isFunction", 1, |args| {
238            Ok(VMValue::Bool(matches!(
239                args[0],
240                VMValue::Closure(_) | VMValue::Builtin(_) | VMValue::HigherOrderBuiltin(_)
241            )))
242        });
243        self.register("isPath", 1, |args| {
244            Ok(VMValue::Bool(matches!(args[0], VMValue::Path(_))))
245        });
246    }
247
248    // ── List operations ───────────────────────────────────────────
249
250    fn register_list_ops(&mut self) {
251        self.register("length", 1, |args| {
252            let list = as_list(&args[0])?;
253            Ok(VMValue::Int(list.len() as i64))
254        });
255
256        self.register("head", 1, |args| {
257            let list = as_list(&args[0])?;
258            list.first()
259                .cloned()
260                .ok_or_else(|| VMError::Throw("head: empty list".to_string()))
261        });
262
263        self.register("tail", 1, |args| {
264            let list = as_list(&args[0])?;
265            if list.is_empty() {
266                return Err(VMError::Throw("tail: empty list".to_string()));
267            }
268            Ok(VMValue::List(list[1..].to_vec()))
269        });
270
271        self.register("elemAt", 1, |args| {
272            let list = as_list(&args[0])?.to_vec();
273            Ok(VMValue::Builtin(VMBuiltin {
274                name: "elemAt<partial>",
275                func: Rc::new(move |args2| {
276                    let idx = as_int(&args2[0])? as usize;
277                    list.get(idx).cloned().ok_or_else(|| {
278                        VMError::Throw(format!("elemAt: index {idx} out of bounds"))
279                    })
280                }),
281                arity: 1,
282            }))
283        });
284
285        self.register("elem", 1, |args| {
286            let needle = args[0].clone();
287            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
288                op: HigherOrderOp::Elem,
289                func: Box::new(needle),
290                extra_args: Vec::new(),
291            }))
292        });
293
294        self.register("genList", 1, |args| {
295            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
296                op: HigherOrderOp::GenList,
297                func: Box::new(args[0].clone()),
298                extra_args: Vec::new(),
299            }))
300        });
301
302        // map: curried, returns partial (VM handles closure calling)
303        self.register("map", 1, |args| {
304            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
305                op: HigherOrderOp::Map,
306                func: Box::new(args[0].clone()),
307                extra_args: Vec::new(),
308            }))
309        });
310
311        // filter: curried, returns partial (VM handles closure calling)
312        self.register("filter", 1, |args| {
313            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
314                op: HigherOrderOp::Filter,
315                func: Box::new(args[0].clone()),
316                extra_args: Vec::new(),
317            }))
318        });
319
320        self.register("concatLists", 1, |args| {
321            let lists = as_list(&args[0])?;
322            let mut result = Vec::new();
323            for v in &lists {
324                let inner = as_list(v)?;
325                result.extend(inner);
326            }
327            Ok(VMValue::List(result))
328        });
329
330        self.register("sort", 1, |args| {
331            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
332                op: HigherOrderOp::Sort,
333                func: Box::new(args[0].clone()),
334                extra_args: Vec::new(),
335            }))
336        });
337    }
338
339
340    // ── Higher-order operations (need VM access) ─────────────────
341
342    fn register_higher_order_ops(&mut self) {
343        self.register("foldl'", 1, |args| {
344            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
345                op: HigherOrderOp::FoldlP1,
346                func: Box::new(args[0].clone()),
347                extra_args: Vec::new(),
348            }))
349        });
350        self.register("concatMap", 1, |args| {
351            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
352                op: HigherOrderOp::ConcatMap,
353                func: Box::new(args[0].clone()),
354                extra_args: Vec::new(),
355            }))
356        });
357        self.register("any", 1, |args| {
358            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
359                op: HigherOrderOp::Any,
360                func: Box::new(args[0].clone()),
361                extra_args: Vec::new(),
362            }))
363        });
364        self.register("all", 1, |args| {
365            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
366                op: HigherOrderOp::All,
367                func: Box::new(args[0].clone()),
368                extra_args: Vec::new(),
369            }))
370        });
371        self.register("partition", 1, |args| {
372            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
373                op: HigherOrderOp::Partition,
374                func: Box::new(args[0].clone()),
375                extra_args: Vec::new(),
376            }))
377        });
378        self.register("groupBy", 1, |args| {
379            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
380                op: HigherOrderOp::GroupBy,
381                func: Box::new(args[0].clone()),
382                extra_args: Vec::new(),
383            }))
384        });
385        self.register("mapAttrs", 1, |args| {
386            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
387                op: HigherOrderOp::MapAttrs,
388                func: Box::new(args[0].clone()),
389                extra_args: Vec::new(),
390            }))
391        });
392        self.register("filterAttrs", 1, |args| {
393            Ok(VMValue::HigherOrderBuiltin(HigherOrderBuiltin {
394                op: HigherOrderOp::FilterAttrs,
395                func: Box::new(args[0].clone()),
396                extra_args: Vec::new(),
397            }))
398        });
399        self.register("functionArgs", 1, |args| {
400            match &args[0] {
401                VMValue::Builtin(_) | VMValue::HigherOrderBuiltin(_) => {
402                    Ok(VMValue::Attrs(BTreeMap::new()))
403                }
404                VMValue::Closure(closure) => {
405                    let mut result = BTreeMap::new();
406                    let mut interner = crate::intern::Interner::new();
407                    for (name, has_default) in &closure.formals {
408                        let sym = interner.intern(name);
409                        result.insert(sym, VMValue::Bool(*has_default));
410                    }
411                    Ok(VMValue::Attrs(result))
412                }
413                other => Err(VMError::TypeError {
414                    expected: "lambda",
415                    got: other.type_name(),
416                    context: "functionArgs".to_string(),
417                }),
418            }
419        });
420        self.register("catAttrs", 1, |args| {
421            let name = as_string(&args[0])?.to_string();
422            Ok(VMValue::Builtin(VMBuiltin {
423                name: "catAttrs<partial>",
424                func: Rc::new(move |args2| {
425                    let list = force_as_list(&args2[0])?;
426                    let mut result: Vec<VMValue> = Vec::new();
427                    for item in &list {
428                        if let VMValue::Attrs(_) = item {
429                            let _ = &name;
430                        }
431                    }
432                    Err(VMError::Throw(
433                        "catAttrs: requires interner access (use VM dispatch)".to_string(),
434                    ))
435                }),
436                arity: 1,
437            }))
438        });
439    }
440
441    // ── Attrset operations ────────────────────────────────────────
442
443    fn register_attrset_ops(&mut self) {
444        self.register("attrNames", 1, |args| {
445            let attrs = as_attrs(&args[0])?;
446            // Note: we don't have the interner here, so we can't resolve
447            // Symbol keys. This builtin must be called through the VM
448            // which resolves symbols. For now, this is a placeholder.
449            let _ = attrs;
450            Err(VMError::Throw(
451                "attrNames: requires interner access (use VM dispatch)".to_string(),
452            ))
453        });
454
455        // attrValues needs the VM's interner to resolve Symbol keys to
456        // their string names for lex-sorting — which is what CppNix
457        // semantics require. Symbol itself is an intern-order u32,
458        // NOT a lex-sorted key, so BTreeMap's native iteration is
459        // intern-order and wrong whenever any transitive eval
460        // (e.g. nixpkgs/lib) has interned a "later" key before an
461        // "earlier" one. Route through VM dispatch the same way
462        // attrNames does. Placeholder error that the VM recognizes.
463        //
464        // Discovered while probing sui against real nixpkgs:
465        // `(import <nixpkgs>/lib).attrsets.mapAttrsToList
466        //    (n: v: "${n}=${toString v}") { a = 1; b = 2; }`
467        // returned `[ "b=2" "a=1" ]` instead of `[ "a=1" "b=2" ]`.
468        self.register("attrValues", 1, |_args| {
469            Err(VMError::Throw(
470                "attrValues: requires interner access (use VM dispatch)".to_string(),
471            ))
472        });
473
474        self.register("hasAttr", 1, |args| {
475            let name = as_string(&args[0])?.to_string();
476            Ok(VMValue::Builtin(VMBuiltin {
477                name: "hasAttr<partial>",
478                func: Rc::new(move |_args2| {
479                    // Needs interner to resolve the name to a Symbol.
480                    let _ = &name;
481                    Err(VMError::Throw(
482                        "hasAttr: requires interner access (use VM dispatch)".to_string(),
483                    ))
484                }),
485                arity: 1,
486            }))
487        });
488
489        self.register("getAttr", 1, |args| {
490            let name = as_string(&args[0])?.to_string();
491            Ok(VMValue::Builtin(VMBuiltin {
492                name: "getAttr<partial>",
493                func: Rc::new(move |_args2| {
494                    let _ = &name;
495                    Err(VMError::Throw(
496                        "getAttr: requires interner access (use VM dispatch)".to_string(),
497                    ))
498                }),
499                arity: 1,
500            }))
501        });
502
503        self.register("intersectAttrs", 1, |args| {
504            let a_attrs = as_attrs(&args[0])?.clone();
505            Ok(VMValue::Builtin(VMBuiltin {
506                name: "intersectAttrs<partial>",
507                func: Rc::new(move |args2| {
508                    let b_attrs = as_attrs(&args2[0])?;
509                    let mut result = BTreeMap::new();
510                    for (k, v) in b_attrs {
511                        if a_attrs.contains_key(k) {
512                            result.insert(*k, v.clone());
513                        }
514                    }
515                    Ok(VMValue::Attrs(result))
516                }),
517                arity: 1,
518            }))
519        });
520
521        self.register("removeAttrs", 1, |args| {
522            let set = as_attrs(&args[0])?.clone();
523            Ok(VMValue::Builtin(VMBuiltin {
524                name: "removeAttrs<partial>",
525                func: Rc::new(move |_args2| {
526                    // Needs interner for name resolution
527                    let _ = &set;
528                    Err(VMError::Throw(
529                        "removeAttrs: requires interner access".to_string(),
530                    ))
531                }),
532                arity: 1,
533            }))
534        });
535
536        self.register("listToAttrs", 1, |_args| {
537            Err(VMError::Throw(
538                "listToAttrs: requires interner access".to_string(),
539            ))
540        });
541    }
542
543    // ── String operations ─────────────────────────────────────────
544
545    fn register_string_ops(&mut self) {
546        self.register("stringLength", 1, |args| {
547            let s = as_string(&args[0])?;
548            Ok(VMValue::Int(s.len() as i64))
549        });
550
551        self.register("substring", 1, |args| {
552            // CppNix semantics (verified against 2.33):
553            //   - negative `len` means "to end of string"
554            //   - negative `start` yields empty string
555            //   - out-of-range start clamps; out-of-range end clamps
556            //
557            // sui's VM was previously casting `i64 as usize` immediately,
558            // which turned `-1` (a common CppNix convention for "rest of
559            // string", used by lib.strings.removePrefix) into usize::MAX
560            // and panicked with "begin <= end" on the arithmetic overflow.
561            // Discovered while probing `(import <nixpkgs>/lib).strings
562            //   .removePrefix "foo-" "foo-bar"` — fifth silent/loud bug
563            // of the session.
564            let start_i = as_int(&args[0])?;
565            Ok(VMValue::Builtin(VMBuiltin {
566                name: "substring<p1>",
567                func: Rc::new(move |args2| {
568                    let len_i = as_int(&args2[0])?;
569                    Ok(VMValue::Builtin(VMBuiltin {
570                        name: "substring<p2>",
571                        func: Rc::new(move |args3| {
572                            let s = as_string(&args3[0])?;
573                            if start_i < 0 {
574                                return Err(VMError::Throw(
575                                    "substring: negative start position".to_string(),
576                                ));
577                            }
578                            let s_len = s.len();
579                            let start = (start_i as usize).min(s_len);
580                            let end = if len_i < 0 {
581                                s_len
582                            } else {
583                                start.saturating_add(len_i as usize).min(s_len)
584                            };
585                            Ok(VMValue::String(s[start..end].to_string()))
586                        }),
587                        arity: 1,
588                    }))
589                }),
590                arity: 1,
591            }))
592        });
593
594        self.register("concatStringsSep", 1, |args| {
595            let sep = as_string(&args[0])?.to_string();
596            Ok(VMValue::Builtin(VMBuiltin {
597                name: "concatStringsSep<partial>",
598                func: Rc::new(move |args2| {
599                    let list = force_as_list(&args2[0])?;
600                    let strings: Result<Vec<String>, _> =
601                        list.iter().map(|v| force_as_string(v)).collect();
602                    Ok(VMValue::String(strings?.join(&sep)))
603                }),
604                arity: 1,
605            }))
606        });
607
608        self.register("replaceStrings", 1, |args| {
609            let from: Vec<String> = force_as_list(&args[0])?
610                .iter()
611                .map(|v| force_as_string(v))
612                .collect::<Result<_, _>>()?;
613            Ok(VMValue::Builtin(VMBuiltin {
614                name: "replaceStrings<p1>",
615                func: Rc::new(move |args2| {
616                    let to: Vec<String> = force_as_list(&args2[0])?
617                        .iter()
618                        .map(|v| force_as_string(v))
619                        .collect::<Result<_, _>>()?;
620                    let from2 = from.clone();
621                    Ok(VMValue::Builtin(VMBuiltin {
622                        name: "replaceStrings<p2>",
623                        func: Rc::new(move |args3| {
624                            let mut s = as_string(&args3[0])?.to_string();
625                            for (f, t) in from2.iter().zip(to.iter()) {
626                                if !f.is_empty() {
627                                    s = s.replace(f.as_str(), t);
628                                }
629                            }
630                            Ok(VMValue::String(s))
631                        }),
632                        arity: 1,
633                    }))
634                }),
635                arity: 1,
636            }))
637        });
638
639        self.register("hasPrefix", 1, |args| {
640            let prefix = as_string(&args[0])?.to_string();
641            Ok(VMValue::Builtin(VMBuiltin {
642                name: "hasPrefix<partial>",
643                func: Rc::new(move |args2| {
644                    let s = as_string(&args2[0])?;
645                    Ok(VMValue::Bool(s.starts_with(&*prefix)))
646                }),
647                arity: 1,
648            }))
649        });
650
651        self.register("hasSuffix", 1, |args| {
652            let suffix = as_string(&args[0])?.to_string();
653            Ok(VMValue::Builtin(VMBuiltin {
654                name: "hasSuffix<partial>",
655                func: Rc::new(move |args2| {
656                    let s = as_string(&args2[0])?;
657                    Ok(VMValue::Bool(s.ends_with(&*suffix)))
658                }),
659                arity: 1,
660            }))
661        });
662
663        self.register("toLower", 1, |args| {
664            let s = as_string(&args[0])?;
665            Ok(VMValue::String(s.to_lowercase()))
666        });
667
668        self.register("toUpper", 1, |args| {
669            let s = as_string(&args[0])?;
670            Ok(VMValue::String(s.to_uppercase()))
671        });
672    }
673
674    // ── Conversion operations ─────────────────────────────────────
675
676    fn register_conversion_ops(&mut self) {
677        self.register("toString", 1, |args| {
678            vm_coerce_to_string(&args[0])
679        });
680
681        self.register("toJSON", 1, |args| {
682            let json = vm_value_to_json(&args[0])?;
683            let s = serde_json::to_string(&json)
684                .unwrap_or_else(|_| "null".to_string());
685            Ok(VMValue::String(s))
686        });
687
688        self.register("fromJSON", 1, |args| {
689            let s = as_string(&args[0])?;
690            let json: serde_json::Value = serde_json::from_str(s).map_err(|e| {
691                VMError::Throw(format!("fromJSON: {e}"))
692            })?;
693            Ok(json_to_vm_value(&json))
694        });
695
696        self.register("toInt", 1, |args| {
697            let s = as_string(&args[0])?;
698            let n: i64 = s.trim().parse().map_err(|e| {
699                VMError::Throw(format!("toInt: {e}"))
700            })?;
701            Ok(VMValue::Int(n))
702        });
703    }
704
705    // ── Control flow ──────────────────────────────────────────────
706
707    fn register_control_ops(&mut self) {
708        self.register("throw", 1, |args| {
709            let msg = as_string(&args[0])?;
710            Err(VMError::Throw(format!("throw: {msg}")))
711        });
712
713        self.register("abort", 1, |args| {
714            let msg = as_string(&args[0])?;
715            Err(VMError::Throw(format!("abort: {msg}")))
716        });
717
718        self.register("seq", 1, |args| {
719            let _forced = args[0].clone();
720            Ok(VMValue::Builtin(VMBuiltin {
721                name: "seq<partial>",
722                func: Rc::new(|args2| Ok(args2[0].clone())),
723                arity: 1,
724            }))
725        });
726
727        self.register("deepSeq", 1, |args| {
728            let _forced = args[0].clone();
729            Ok(VMValue::Builtin(VMBuiltin {
730                name: "deepSeq<partial>",
731                func: Rc::new(|args2| Ok(args2[0].clone())),
732                arity: 1,
733            }))
734        });
735
736        self.register("tryEval", 1, |args| {
737            // In the VM, tryEval just wraps the value since we don't
738            // have thunk forcing here. The VM handles the actual try/catch.
739            let val = args[0].clone();
740            // We can't actually catch throws here without interner access.
741            // Return success with the value for now.
742            // The VM will handle this specially.
743            let _ = val;
744            Err(VMError::Throw(
745                "tryEval: requires VM-level implementation".to_string(),
746            ))
747        });
748
749        self.register("trace", 1, |args| {
750            let msg = args[0].clone();
751            eprintln!("trace: {msg}");
752            Ok(VMValue::Builtin(VMBuiltin {
753                name: "trace<partial>",
754                func: Rc::new(|args2| Ok(args2[0].clone())),
755                arity: 1,
756            }))
757        });
758    }
759
760    // ── Arithmetic ────────────────────────────────────────────────
761
762    fn register_arithmetic_ops(&mut self) {
763        self.register("add", 1, |args| {
764            let a = args[0].clone();
765            Ok(VMValue::Builtin(VMBuiltin {
766                name: "add<partial>",
767                func: Rc::new(move |args2| match (&a, &args2[0]) {
768                    (VMValue::Int(x), VMValue::Int(y)) => Ok(VMValue::Int(x + y)),
769                    (VMValue::Float(x), VMValue::Float(y)) => Ok(VMValue::Float(x + y)),
770                    (VMValue::Int(x), VMValue::Float(y)) => Ok(VMValue::Float(*x as f64 + y)),
771                    (VMValue::Float(x), VMValue::Int(y)) => Ok(VMValue::Float(x + *y as f64)),
772                    _ => Err(VMError::Throw("add: expected numbers".to_string())),
773                }),
774                arity: 1,
775            }))
776        });
777
778        // sub/mul/div accept mixed Int+Float operands (CppNix
779        // semantics).  Previously int-only, which diverged on
780        // `builtins.div 10.0 3.0` and similar mixed expressions.
781        self.register("sub", 1, |args| {
782            let a = args[0].clone();
783            Ok(VMValue::Builtin(VMBuiltin {
784                name: "sub<partial>",
785                func: Rc::new(move |args2| vm_numeric_binop("sub", &a, &args2[0], |x, y| x - y, |x, y| x - y)),
786                arity: 1,
787            }))
788        });
789
790        self.register("mul", 1, |args| {
791            let a = args[0].clone();
792            Ok(VMValue::Builtin(VMBuiltin {
793                name: "mul<partial>",
794                func: Rc::new(move |args2| vm_numeric_binop("mul", &a, &args2[0], |x, y| x * y, |x, y| x * y)),
795                arity: 1,
796            }))
797        });
798
799        self.register("div", 1, |args| {
800            let a = args[0].clone();
801            Ok(VMValue::Builtin(VMBuiltin {
802                name: "div<partial>",
803                func: Rc::new(move |args2| match (&a, &args2[0]) {
804                    (VMValue::Int(_), VMValue::Int(0)) => Err(VMError::DivisionByZero),
805                    (VMValue::Int(x), VMValue::Int(y)) => Ok(VMValue::Int(x / y)),
806                    (VMValue::Float(x), VMValue::Float(y)) => Ok(VMValue::Float(x / y)),
807                    (VMValue::Int(x), VMValue::Float(y)) => Ok(VMValue::Float(*x as f64 / *y)),
808                    (VMValue::Float(x), VMValue::Int(y)) => Ok(VMValue::Float(*x / *y as f64)),
809                    _ => Err(VMError::Throw("div: expected numbers".to_string())),
810                }),
811                arity: 1,
812            }))
813        });
814
815        self.register("ceil", 1, |args| {
816            let f = as_float(&args[0])?;
817            Ok(VMValue::Int(f.ceil() as i64))
818        });
819
820        self.register("floor", 1, |args| {
821            let f = as_float(&args[0])?;
822            Ok(VMValue::Int(f.floor() as i64))
823        });
824
825        self.register("bitAnd", 1, |args| {
826            let a = as_int(&args[0])?;
827            Ok(VMValue::Builtin(VMBuiltin {
828                name: "bitAnd<partial>",
829                func: Rc::new(move |args2| {
830                    let b = as_int(&args2[0])?;
831                    Ok(VMValue::Int(a & b))
832                }),
833                arity: 1,
834            }))
835        });
836
837        self.register("bitOr", 1, |args| {
838            let a = as_int(&args[0])?;
839            Ok(VMValue::Builtin(VMBuiltin {
840                name: "bitOr<partial>",
841                func: Rc::new(move |args2| {
842                    let b = as_int(&args2[0])?;
843                    Ok(VMValue::Int(a | b))
844                }),
845                arity: 1,
846            }))
847        });
848
849        self.register("bitXor", 1, |args| {
850            let a = as_int(&args[0])?;
851            Ok(VMValue::Builtin(VMBuiltin {
852                name: "bitXor<partial>",
853                func: Rc::new(move |args2| {
854                    let b = as_int(&args2[0])?;
855                    Ok(VMValue::Int(a ^ b))
856                }),
857                arity: 1,
858            }))
859        });
860    }
861
862    // ── Derivation ────────────────────────────────────────────────
863
864    fn register_derivation_ops(&mut self) {
865        // Both `derivation` and `derivationStrict` delegate to the same impl.
866        // The actual implementation is at the VM level (vm_build_derivation)
867        // because it needs interner access. These stubs are intercepted by
868        // try_vm_builtin before they execute.
869        self.register("derivation", 1, |_args| {
870            Err(VMError::Throw(
871                "derivation: requires VM-level dispatch".to_string(),
872            ))
873        });
874        self.register("derivationStrict", 1, |_args| {
875            Err(VMError::Throw(
876                "derivationStrict: requires VM-level dispatch".to_string(),
877            ))
878        });
879        // getFlake: VM-level dispatch (needs import mechanism).
880        self.register("getFlake", 1, |_args| {
881            Err(VMError::Throw(
882                "getFlake: requires VM-level dispatch".to_string(),
883            ))
884        });
885        // scopedImport: VM-level dispatch (needs import + interner).
886        self.register("scopedImport", 1, |_args| {
887            Err(VMError::Throw(
888                "scopedImport: requires VM-level dispatch".to_string(),
889            ))
890        });
891
892        // ── Missing builtins needed for nixpkgs lib ─────────────────
893
894        // addErrorContext: in eval mode just returns the value (no-op wrapper)
895        self.register("addErrorContext", 1, |args| {
896            // Curried: addErrorContext context value → value
897            Ok(VMValue::Builtin(VMBuiltin {
898                name: "addErrorContext<partial>",
899                func: Rc::new(move |inner_args: Vec<VMValue>| Ok(inner_args[0].clone())),
900                arity: 1,
901            }))
902        });
903
904        // unsafeGetAttrPos: returns null (position info not tracked in VM)
905        self.register("unsafeGetAttrPos", 1, |_args| {
906            Ok(VMValue::Builtin(VMBuiltin {
907                name: "unsafeGetAttrPos<partial>",
908                func: Rc::new(|_args: Vec<VMValue>| Ok(VMValue::Null)),
909                arity: 1,
910            }))
911        });
912
913        // pathExists: check if a path exists on the filesystem
914        self.register("pathExists", 1, |args| {
915            let path = match &args[0] {
916                VMValue::Path(p) => p.clone(),
917                VMValue::String(s) => s.clone(),
918                other => {
919                    return Err(VMError::TypeError {
920                        expected: "path or string",
921                        got: other.type_name(),
922                        context: "pathExists".to_string(),
923                    })
924                }
925            };
926            // Redirect the READ through the installed path materializer: a
927            // flake input's `/nix/store/<narhash>-source` prefix is never on
928            // disk, so a bare `.exists()` answers NO for every file in a
929            // fetched input — silently, since `false` is a legal answer.
930            let read_path = crate::bridge::materialize(&path);
931            Ok(VMValue::Bool(std::path::Path::new(&read_path).exists()))
932        });
933
934        // readFile: read contents of a file
935        self.register("readFile", 1, |args| {
936            let path = match &args[0] {
937                VMValue::Path(p) => p.clone(),
938                VMValue::String(s) => s.clone(),
939                other => {
940                    return Err(VMError::TypeError {
941                        expected: "path or string",
942                        got: other.type_name(),
943                        context: "readFile".to_string(),
944                    })
945                }
946            };
947            // Same redirect as `pathExists` — the two MUST agree about a path,
948            // or a `if pathExists p then readFile p` guard passes and the read
949            // then ENOENTs.
950            let read_path = crate::bridge::materialize(&path);
951            let content = std::fs::read_to_string(&read_path)
952                .map_err(|e| VMError::Throw(format!("readFile {path}: {e}")))?;
953            Ok(VMValue::String(content))
954        });
955
956        // readDir: list directory entries
957        self.register("readDir", 1, |args| {
958            let path = match &args[0] {
959                VMValue::Path(p) => p.clone(),
960                VMValue::String(s) => s.clone(),
961                other => {
962                    return Err(VMError::TypeError {
963                        expected: "path or string",
964                        got: other.type_name(),
965                        context: "readDir".to_string(),
966                    })
967                }
968            };
969            // Unreachable whenever a bridge is installed: `builtins.readDir`
970            // is bridge-dispatched by name in `VM::try_vm_builtin`, so the
971            // tree-walker answers it — and the tree-walker's `readDir` already
972            // routes through `path::materialize`. This stub is the bridgeless
973            // path only; it cannot succeed, so there is nothing to redirect.
974            let _ = path;
975            Err(VMError::Throw(
976                "readDir: requires the tree-walker bridge (no interner access here)".to_string(),
977            ))
978        });
979
980        // baseNameOf: extract filename from a path
981        self.register("baseNameOf", 1, |args| {
982            let path = match &args[0] {
983                VMValue::Path(p) => p.clone(),
984                VMValue::String(s) => s.clone(),
985                other => {
986                    return Err(VMError::TypeError {
987                        expected: "path or string",
988                        got: other.type_name(),
989                        context: "baseNameOf".to_string(),
990                    })
991                }
992            };
993            let base = std::path::Path::new(&path)
994                .file_name()
995                .map(|f| f.to_string_lossy().to_string())
996                .unwrap_or_default();
997            Ok(VMValue::String(base))
998        });
999
1000        // dirOf: extract directory from a path
1001        self.register("dirOf", 1, |args| {
1002            let path = match &args[0] {
1003                VMValue::Path(p) => p.clone(),
1004                VMValue::String(s) => s.clone(),
1005                other => {
1006                    return Err(VMError::TypeError {
1007                        expected: "path or string",
1008                        got: other.type_name(),
1009                        context: "dirOf".to_string(),
1010                    })
1011                }
1012            };
1013            let dir = std::path::Path::new(&path)
1014                .parent()
1015                .map(|p| p.to_string_lossy().to_string())
1016                .unwrap_or_else(|| ".".to_string());
1017            Ok(VMValue::String(dir))
1018        });
1019
1020        // genericClosure: transitive closure computation
1021        self.register("genericClosure", 1, |_args| {
1022            Err(VMError::Throw(
1023                "genericClosure: requires VM-level dispatch".to_string(),
1024            ))
1025        });
1026
1027        // placeholder: returns placeholder string for derivation outputs
1028        self.register("placeholder", 1, |args| {
1029            let output = match &args[0] {
1030                VMValue::String(s) => s.clone(),
1031                _ => "out".to_string(),
1032            };
1033            Ok(VMValue::String(format!("/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9/{output}")))
1034        });
1035
1036        // split: regex split (requires VM-level dispatch for interner)
1037        self.register("split", 1, |args| {
1038            let _pattern = as_string(&args[0])?;
1039            Ok(VMValue::Builtin(VMBuiltin {
1040                name: "split<partial>",
1041                func: Rc::new(|_inner_args: Vec<VMValue>| {
1042                    Err(VMError::Throw("split: requires VM-level dispatch".to_string()))
1043                }),
1044                arity: 1,
1045            }))
1046        });
1047
1048        // match: regex match (requires VM-level dispatch for interner)
1049        self.register("match", 1, |args| {
1050            let _pattern = as_string(&args[0])?;
1051            Ok(VMValue::Builtin(VMBuiltin {
1052                name: "match<partial>",
1053                func: Rc::new(|_inner_args: Vec<VMValue>| {
1054                    Err(VMError::Throw("match: requires VM-level dispatch".to_string()))
1055                }),
1056                arity: 1,
1057            }))
1058        });
1059
1060        // fromTOML: parse a TOML string
1061        self.register("fromTOML", 1, |args| {
1062            let s = as_string(&args[0])?;
1063            // Simple stub - would need full TOML parser
1064            Err(VMError::Throw(format!("fromTOML: not yet implemented")))
1065        });
1066
1067        // concatStrings: concatenate a list of strings (used by nixpkgs lib)
1068        // Note: This isn't strictly a Nix builtin but is sometimes needed
1069        // In Nix it's actually builtins.concatStringsSep "" (already registered)
1070
1071        // storeDir: the Nix store directory
1072        // This is a constant, added in make_builtins_attrset
1073
1074        // fetchurl, fetchTarball, fetchGit, fetchTree stubs
1075        self.register("fetchurl", 1, |_args| {
1076            Err(VMError::Throw("fetchurl: not supported in eval mode".to_string()))
1077        });
1078        self.register("fetchTarball", 1, |_args| {
1079            Err(VMError::Throw("fetchTarball: not supported in eval mode".to_string()))
1080        });
1081        self.register("fetchGit", 1, |_args| {
1082            Err(VMError::Throw("fetchGit: not supported in eval mode".to_string()))
1083        });
1084        self.register("fetchTree", 1, |_args| {
1085            Err(VMError::Throw("fetchTree: not supported in eval mode".to_string()))
1086        });
1087        self.register("fetchMercurial", 1, |_args| {
1088            Err(VMError::Throw("fetchMercurial: not supported in eval mode".to_string()))
1089        });
1090
1091        // toFile: write a file to the Nix store (stub)
1092        self.register("toFile", 1, |_args| {
1093            Err(VMError::Throw("toFile: not supported in eval mode".to_string()))
1094        });
1095
1096        // toPath: convert string to path (deprecated in Nix, but used)
1097        self.register("toPath", 1, |args| {
1098            let s = as_string(&args[0])?;
1099            Ok(VMValue::Path(s.to_string()))
1100        });
1101
1102        // import: as a builtin value (not a special form)
1103        // Already handled at the compiler level via OpCode::Import
1104
1105        // parseDrvName: parse a derivation name-version string
1106        self.register("parseDrvName", 1, |args| {
1107            let name = as_string(&args[0])?;
1108            // Split at last hyphen followed by a digit
1109            let mut split_pos = None;
1110            let bytes = name.as_bytes();
1111            for i in (0..bytes.len()).rev() {
1112                if bytes[i] == b'-' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() {
1113                    split_pos = Some(i);
1114                    break;
1115                }
1116            }
1117            match split_pos {
1118                Some(pos) => {
1119                    Err(VMError::Throw(
1120                        "parseDrvName: requires VM-level dispatch for interner access".to_string(),
1121                    ))
1122                }
1123                None => {
1124                    Err(VMError::Throw(
1125                        "parseDrvName: requires VM-level dispatch for interner access".to_string(),
1126                    ))
1127                }
1128            }
1129        });
1130
1131        // compareVersions — delegates to sui_compat::versions so the
1132        // tree-walker and VM stay in lock-step.  The previous naive
1133        // local implementation (split on `.` only, no `pre` handling)
1134        // diverged from cppnix on every nixpkgs version probe.
1135        self.register("compareVersions", 1, |args| {
1136            let a = as_string(&args[0])?.to_string();
1137            Ok(VMValue::Builtin(VMBuiltin {
1138                name: "compareVersions<partial>",
1139                func: Rc::new(move |inner_args: Vec<VMValue>| {
1140                    let b = as_string(&inner_args[0])?;
1141                    Ok(VMValue::Int(
1142                        sui_compat::versions::compare_versions(&a, b),
1143                    ))
1144                }),
1145                arity: 1,
1146            }))
1147        });
1148
1149        // splitVersion — delegates to sui_compat::versions for the
1150        // same reason compareVersions does.
1151        self.register("splitVersion", 1, |args| {
1152            let version = as_string(&args[0])?;
1153            let parts: Vec<VMValue> = sui_compat::versions::split_version(version)
1154                .into_iter()
1155                .map(VMValue::String)
1156                .collect();
1157            Ok(VMValue::List(parts))
1158        });
1159
1160        // concatStrings is used internally
1161        self.register("concatStrings", 1, |args| {
1162            let list = as_list(&args[0])?;
1163            let mut result = String::new();
1164            for item in &list {
1165                let item = force_vmvalue(item.clone()).unwrap_or_else(|_| item.clone());
1166                match &item {
1167                    VMValue::String(s) => result.push_str(s),
1168                    _ => {
1169                        return Err(VMError::TypeError {
1170                            expected: "string",
1171                            got: item.type_name(),
1172                            context: "concatStrings element".to_string(),
1173                        })
1174                    }
1175                }
1176            }
1177            Ok(VMValue::String(result))
1178        });
1179
1180        // ── String context builtins (no-ops in eval mode) ────────────
1181        // Nix string contexts track derivation dependencies. In eval-only
1182        // mode, strings have no context, so these are identity/no-ops.
1183        self.register("unsafeDiscardStringContext", 1, |args| {
1184            // Just return the string as-is (no context to discard).
1185            Ok(args[0].clone())
1186        });
1187        self.register("getContext", 1, |_args| {
1188            // No context in eval mode — return empty attrset.
1189            // Need VM dispatch for interner.
1190            Err(VMError::Throw(
1191                "getContext: requires VM-level dispatch for interner access".to_string(),
1192            ))
1193        });
1194        self.register("appendContext", 1, |args| {
1195            // No context to append — return string as-is.
1196            Ok(VMValue::Builtin(VMBuiltin {
1197                name: "appendContext<partial>",
1198                func: Rc::new(move |inner_args: Vec<VMValue>| Ok(inner_args[0].clone())),
1199                arity: 1,
1200            }))
1201        });
1202        self.register("hasContext", 1, |_args| {
1203            Ok(VMValue::Bool(false))
1204        });
1205        self.register("unsafeDiscardOutputDependency", 1, |args| {
1206            Ok(args[0].clone())
1207        });
1208        self.register("addDrvOutputDependencies", 1, |args| {
1209            Ok(args[0].clone())
1210        });
1211
1212        // ── Path/string conversion builtins ──────────────────────────
1213        self.register("storePath", 1, |args| {
1214            Ok(args[0].clone())
1215        });
1216        self.register("isStorePath", 1, |args| {
1217            let s = match &args[0] {
1218                VMValue::String(s) => s.as_str(),
1219                VMValue::Path(p) => p.as_str(),
1220                _ => return Ok(VMValue::Bool(false)),
1221            };
1222            Ok(VMValue::Bool(s.starts_with("/nix/store/")))
1223        });
1224        self.register("hashString", 1, |args| {
1225            let algo = as_string(&args[0])?.to_string();
1226            Ok(VMValue::Builtin(VMBuiltin {
1227                name: "hashString<partial>",
1228                func: Rc::new(move |inner_args: Vec<VMValue>| {
1229                    let s = as_string(&inner_args[0])?;
1230                    match algo.as_str() {
1231                        "sha256" => {
1232                            use sha2::{Sha256, Digest};
1233                            let mut hasher = Sha256::new();
1234                            hasher.update(s.as_bytes());
1235                            let result = hasher.finalize();
1236                            let hex: String = result
1237                                .iter()
1238                                .map(|b| format!("{b:02x}"))
1239                                .collect();
1240                            Ok(VMValue::String(hex))
1241                        }
1242                        _ => Err(VMError::Throw(format!("hashString: unsupported algorithm: {algo}")))
1243                    }
1244                }),
1245                arity: 1,
1246            }))
1247        });
1248        self.register("hashFile", 1, |_args| {
1249            Err(VMError::Throw("hashFile: not supported in eval mode".to_string()))
1250        });
1251
1252        // import as a value (not the special form in Apply).
1253        // When used as `import path`, the compiler handles it via OpCode::Import.
1254        // But when `import` is passed as a function value (e.g., `map import paths`),
1255        // it needs to be callable. The VM dispatches this specially.
1256        self.register("import", 1, |_args| {
1257            Err(VMError::Throw(
1258                "import: requires VM-level dispatch".to_string(),
1259            ))
1260        });
1261
1262        // ── Misc builtins needed by nixpkgs lib ─────────────────────
1263        self.register("zipAttrsWith", 1, |_args| {
1264            Err(VMError::Throw(
1265                "zipAttrsWith: requires VM-level dispatch".to_string(),
1266            ))
1267        });
1268    }
1269
1270    // ── Missing builtins: direct implementations + bridge stubs ────
1271    //
1272    // These are builtins that the tree-walker has but the VM was missing.
1273    // Simple ones are implemented directly; complex ones delegate to the
1274    // builtin bridge (which calls back into the tree-walker).
1275
1276    fn register_missing_builtins(&mut self) {
1277        // ── Direct implementations (simple, no tree-walker state) ────
1278
1279        // getEnv: look up environment variable (returns "" if unset)
1280        self.register("getEnv", 1, |args| {
1281            let name = as_string(&args[0])?;
1282            let val = std::env::var(name).unwrap_or_default();
1283            Ok(VMValue::String(val))
1284        });
1285
1286        // readFileType: return file type as string
1287        self.register("readFileType", 1, |args| {
1288            let path = match &args[0] {
1289                VMValue::Path(p) => p.clone(),
1290                VMValue::String(s) => s.clone(),
1291                other => {
1292                    return Err(VMError::TypeError {
1293                        expected: "path or string",
1294                        got: other.type_name(),
1295                        context: "readFileType".to_string(),
1296                    });
1297                }
1298            };
1299            // Same redirect as `pathExists`/`readFile`.
1300            let read_path = crate::bridge::materialize(&path);
1301            match std::fs::symlink_metadata(&read_path) {
1302                Ok(meta) => {
1303                    let kind = if meta.is_symlink() {
1304                        "symlink"
1305                    } else if meta.is_dir() {
1306                        "directory"
1307                    } else if meta.is_file() {
1308                        "regular"
1309                    } else {
1310                        "unknown"
1311                    };
1312                    Ok(VMValue::String(kind.to_string()))
1313                }
1314                Err(e) => Err(VMError::Throw(format!("readFileType {path}: {e}"))),
1315            }
1316        });
1317
1318        // findFile: curried, search NIX_PATH entries for a file
1319        self.register("findFile", 1, |args| {
1320            let search_path = as_list(&args[0])?.clone();
1321            Ok(VMValue::Builtin(VMBuiltin {
1322                name: "findFile<partial>",
1323                func: Rc::new(move |args2| {
1324                    let name = as_string(&args2[0])?;
1325                    for entry in &search_path {
1326                        if let VMValue::Attrs(a) = entry {
1327                            // We need the interner to look up "prefix" and "path" keys.
1328                            // Since this is a bridge builtin, delegate to the bridge.
1329                            // But first try a string-key lookup on a best-effort basis.
1330                            // The bridge will handle the real implementation.
1331                            let _ = a;
1332                        }
1333                    }
1334                    // Delegate to bridge for proper implementation
1335                    bridge_call("findFile", vec![
1336                        VMValue::List(search_path.clone()),
1337                        VMValue::String(name.to_string()),
1338                    ])
1339                }),
1340                arity: 1,
1341            }))
1342        });
1343
1344        // lessThan: curried comparison (missing from VM arithmetic ops)
1345        self.register("lessThan", 1, |args| {
1346            let a = args[0].clone();
1347            Ok(VMValue::Builtin(VMBuiltin {
1348                name: "lessThan<partial>",
1349                func: Rc::new(move |args2| match (&a, &args2[0]) {
1350                    (VMValue::Int(x), VMValue::Int(y)) => Ok(VMValue::Bool(*x < *y)),
1351                    (VMValue::Float(x), VMValue::Float(y)) => Ok(VMValue::Bool(*x < *y)),
1352                    (VMValue::Int(x), VMValue::Float(y)) => {
1353                        Ok(VMValue::Bool((*x as f64) < *y))
1354                    }
1355                    (VMValue::Float(x), VMValue::Int(y)) => {
1356                        Ok(VMValue::Bool(*x < (*y as f64)))
1357                    }
1358                    (VMValue::String(x), VMValue::String(y)) => Ok(VMValue::Bool(*x < *y)),
1359                    _ => Err(VMError::Throw(
1360                        "lessThan: expected comparable types".to_string(),
1361                    )),
1362                }),
1363                arity: 1,
1364            }))
1365        });
1366
1367        // warn: like trace, prints warning and returns identity
1368        self.register("warn", 1, |args| {
1369            if let Ok(msg) = as_string(&args[0]) {
1370                eprintln!("evaluation warning: {msg}");
1371            }
1372            Ok(VMValue::Builtin(VMBuiltin {
1373                name: "warn<partial>",
1374                func: Rc::new(|args2| Ok(args2[0].clone())),
1375                arity: 1,
1376            }))
1377        });
1378
1379        // traceVerbose: like trace but only when SUI_TRACE_VERBOSE=1
1380        self.register("traceVerbose", 1, |args| {
1381            if std::env::var("SUI_TRACE_VERBOSE").ok().as_deref() == Some("1") {
1382                eprintln!("trace: {}", args[0]);
1383            }
1384            Ok(VMValue::Builtin(VMBuiltin {
1385                name: "traceVerbose<partial>",
1386                func: Rc::new(|args2| Ok(args2[0].clone())),
1387                arity: 1,
1388            }))
1389        });
1390
1391        // break: debug breakpoint, just returns its argument
1392        self.register("break", 1, |args| Ok(args[0].clone()));
1393
1394        // ── Bridge-delegating stubs ─────────────────────────────────
1395        //
1396        // These builtins are complex (need tree-walker state, regex cache,
1397        // TOML parser, hash algorithms, etc.) and are delegated to the
1398        // builtin bridge which calls back into the tree-walker.
1399
1400        // Names of builtins that should be bridged and their arities.
1401        // When called, they convert args to StringKeyedValue, call the
1402        // bridge, and convert back.
1403        //
1404        // Note: Some of these are already registered above as stubs that
1405        // throw "requires VM-level dispatch". The bridge versions below
1406        // replace the error with actual functionality when a bridge is set.
1407        // We register them with unique names to avoid conflicts, and the
1408        // VM's try_vm_builtin handles dispatch.
1409
1410        // Bridge complex builtins to tree-walker.
1411        // These need tree-walker state, complex algorithms, or I/O.
1412        //
1413        // ★ REGISTERING A BRIDGE-DISPATCHED BUILTIN IS NOT OPTIONAL: the name
1414        // must appear HERE even though `VM::try_vm_builtin` dispatches it by
1415        // name, because `make_builtins_attrset` is built from THIS registry
1416        // and it is what `builtins ? <name>` answers from. `path`,
1417        // `parseFlakeRef` and `flakeRefToString` were dispatched but never
1418        // registered, so `builtins ? path` answered FALSE while nix and the
1419        // tree-walker answer TRUE — and nixpkgs `lib` gates on exactly that
1420        // shape, so a false answer silently takes the other branch.
1421        for name in &["convertHash", "toXML", "toFile", "filterSource",
1422                      "fetchClosure", "outputOf", "hashFile", "hashString",
1423                      "path", "parseFlakeRef", "flakeRefToString"]
1424        {
1425            let n = (*name).to_string();
1426            self.register(name, 1, move |args| {
1427                bridge_call(&n, args.to_vec())
1428            });
1429        }
1430    }
1431}
1432
1433/// Helper: delegate a builtin call to the tree-walker bridge.
1434///
1435/// Converts `VMValue` args to `StringKeyedValue`, calls the bridge,
1436/// and converts the result back. Returns an error if no bridge is set.
1437/// Apply a curried numeric binop with CppNix mixed-type semantics:
1438/// Int+Int → Int, Float+Float → Float, mixed → Float.  Used by
1439/// sub / mul (div has its own /0 trap).
1440fn vm_numeric_binop(
1441    name: &'static str,
1442    a: &VMValue,
1443    b: &VMValue,
1444    int_op: impl Fn(i64, i64) -> i64,
1445    float_op: impl Fn(f64, f64) -> f64,
1446) -> Result<VMValue, VMError> {
1447    match (a, b) {
1448        (VMValue::Int(x), VMValue::Int(y)) => Ok(VMValue::Int(int_op(*x, *y))),
1449        (VMValue::Float(x), VMValue::Float(y)) => Ok(VMValue::Float(float_op(*x, *y))),
1450        (VMValue::Int(x), VMValue::Float(y)) => Ok(VMValue::Float(float_op(*x as f64, *y))),
1451        (VMValue::Float(x), VMValue::Int(y)) => Ok(VMValue::Float(float_op(*x, *y as f64))),
1452        _ => Err(VMError::Throw(format!("{name}: expected numbers"))),
1453    }
1454}
1455
1456fn bridge_call(name: &str, args: Vec<VMValue>) -> Result<VMValue, VMError> {
1457    use crate::intern::Interner;
1458    // Convert VMValue args to StringKeyedValue (interner-free).
1459    // For this we need a temporary interner to resolve any Symbol keys.
1460    let tmp_interner = Interner::new();
1461    let sk_args: Vec<crate::value::StringKeyedValue> = args
1462        .iter()
1463        .map(|a| a.to_string_keyed(&tmp_interner))
1464        .collect();
1465
1466    // Counted, never fatal — bridging a builtin is the VM's architecture, not
1467    // a failure (it has no native getEnv/match/split/fromTOML/readDir/…). A
1468    // caller that wants to prove the VM computed something WITHOUT the walker
1469    // asserts `fallback::count(Layer::Builtin) == 0` for itself. Making this
1470    // arm fatal under strict would leave strict mode unable to evaluate
1471    // anything, which is a strict mode nobody can use.
1472    let _ = crate::fallback::record(crate::fallback::Layer::Builtin, name);
1473
1474    match crate::bridge::call_builtin_bridge(name, sk_args) {
1475        Ok(Some(result)) => Ok(string_keyed_to_vmvalue(&result, &mut Interner::new())),
1476        Ok(None) => Err(VMError::Throw(format!(
1477            "builtin '{name}' requires bridge but no bridge is set"
1478        ))),
1479        Err(e) => Err(VMError::Throw(e)),
1480    }
1481}
1482
1483/// Convert a `StringKeyedValue` back to a `VMValue`.
1484///
1485/// Requires an interner to create Symbol keys for attrsets.
1486/// Public so the VM's `try_vm_builtin` can use it for bridge dispatch.
1487pub fn string_keyed_to_vmvalue(
1488    sk: &crate::value::StringKeyedValue,
1489    interner: &mut crate::intern::Interner,
1490) -> VMValue {
1491    use crate::value::StringKeyedValue;
1492    match sk {
1493        StringKeyedValue::Null => VMValue::Null,
1494        StringKeyedValue::Bool(b) => VMValue::Bool(*b),
1495        StringKeyedValue::Int(n) => VMValue::Int(*n),
1496        StringKeyedValue::Float(f) => VMValue::Float(*f),
1497        StringKeyedValue::String(s) => VMValue::String(s.clone()),
1498        StringKeyedValue::Path(p) => VMValue::Path(p.clone()),
1499        StringKeyedValue::List(items) => VMValue::List(
1500            items
1501                .iter()
1502                .map(|v| string_keyed_to_vmvalue(v, interner))
1503                .collect(),
1504        ),
1505        StringKeyedValue::Attrs(map) => {
1506            let mut attrs = BTreeMap::new();
1507            for (k, v) in map {
1508                let sym = interner.intern(k);
1509                attrs.insert(sym, string_keyed_to_vmvalue(v, interner));
1510            }
1511            VMValue::Attrs(attrs)
1512        }
1513        StringKeyedValue::Lambda => VMValue::Null,
1514        StringKeyedValue::Callable(cb) => {
1515            let cb_clone = Rc::clone(cb);
1516            VMValue::Builtin(crate::value::VMBuiltin {
1517                name: "<bridge-fn>",
1518                arity: 1,
1519                func: Rc::new(move |args: Vec<VMValue>| {
1520                    let interner = crate::intern::Interner::new();
1521                    let sk_arg = args.into_iter().next()
1522                        .unwrap_or(VMValue::Null)
1523                        .to_string_keyed(&interner);
1524                    let sk_result = cb_clone(sk_arg)
1525                        .map_err(|e| VMError::Throw(e))?;
1526                    let mut tmp_interner = crate::intern::Interner::new();
1527                    Ok(string_keyed_to_vmvalue(&sk_result, &mut tmp_interner))
1528                }),
1529            })
1530        }
1531        StringKeyedValue::Thunk(cb) => {
1532            // Wrap the StringKeyedValue thunk as a VMThunk.
1533            let cb_clone = Rc::clone(cb);
1534            VMValue::Thunk(crate::value::VMThunk::new_native(move || {
1535                let sk_val = cb_clone().map_err(|e| VMError::Throw(e))?;
1536                // Use a fresh interner for the result conversion.
1537                let mut tmp = crate::intern::Interner::new();
1538                Ok(string_keyed_to_vmvalue(&sk_val, &mut tmp))
1539            }))
1540        }
1541    }
1542}
1543
1544impl Default for BuiltinRegistry {
1545    fn default() -> Self {
1546        Self::new()
1547    }
1548}
1549
1550// ── Helper functions ──────────────────────────────────────────────
1551
1552/// Try to extract a concrete value from a `Done` thunk without VM access.
1553/// Returns the inner value for already-evaluated thunks. For non-thunks,
1554/// returns `None` (use the value directly). For pending thunks, returns
1555/// an error that will cause the VM to fall back to the tree-walker.
1556fn try_unwrap_done_thunk(v: &VMValue) -> Option<Result<VMValue, VMError>> {
1557    match v {
1558        VMValue::Thunk(thunk) => {
1559            let state = thunk.state.take();
1560            match state {
1561                Some(ThunkState::Done(boxed)) => {
1562                    let inner = *boxed.clone();
1563                    thunk.state.set(Some(ThunkState::Done(boxed)));
1564                    // Recursively unwrap in case the result is itself a Done thunk.
1565                    match &inner {
1566                        VMValue::Thunk(_) => Some(try_unwrap_done_thunk(&inner)
1567                            .unwrap_or(Ok(inner))),
1568                        _ => Some(Ok(inner)),
1569                    }
1570                }
1571                other => {
1572                    thunk.state.set(other);
1573                    Some(Err(VMError::TypeError {
1574                        expected: "concrete value",
1575                        got: "thunk (pending)",
1576                        context: "builtin argument (thunk needs VM to force)".to_string(),
1577                    }))
1578                }
1579            }
1580        }
1581        _ => None, // Not a thunk — caller uses value directly
1582    }
1583}
1584
1585/// Extract a list, forcing thunks if needed. Returns an owned Vec
1586/// because thunk forcing may produce a value we can't borrow.
1587fn as_list(v: &VMValue) -> Result<Vec<VMValue>, VMError> {
1588    match v {
1589        VMValue::List(l) => Ok(l.clone()),
1590        VMValue::Thunk(_) => {
1591            let forced = force_vmvalue(v.clone())?;
1592            match forced {
1593                VMValue::List(l) => Ok(l),
1594                other => Err(VMError::TypeError {
1595                    expected: "list",
1596                    got: other.type_name(),
1597                    context: "builtin argument".to_string(),
1598                }),
1599            }
1600        }
1601        other => Err(VMError::TypeError {
1602            expected: "list",
1603            got: other.type_name(),
1604            context: "builtin argument".to_string(),
1605        }),
1606    }
1607}
1608
1609/// Force a VMValue if it's a thunk, returning the resolved value.
1610/// Handles Done thunks directly, NativeCallback via bridge, and
1611/// Pending thunks cause a fallback error.
1612fn force_vmvalue(v: VMValue) -> Result<VMValue, VMError> {
1613    match v {
1614        VMValue::Thunk(ref thunk) => {
1615            let state = thunk.state.take();
1616            match state {
1617                Some(ThunkState::Done(boxed)) => {
1618                    let inner = *boxed.clone();
1619                    thunk.state.set(Some(ThunkState::Done(boxed)));
1620                    force_vmvalue(inner) // Recursively unwrap
1621                }
1622                Some(ThunkState::NativeCallback(cb)) => {
1623                    thunk.state.set(Some(ThunkState::Evaluating));
1624                    match cb() {
1625                        Ok(sk_val) => {
1626                            // Convert StringKeyedValue back to VMValue
1627                            let result = sk_to_vmvalue(&sk_val);
1628                            thunk.state.set(Some(ThunkState::Done(Box::new(result.clone()))));
1629                            force_vmvalue(result)
1630                        }
1631                        Err(e) => {
1632                            thunk.state.set(Some(ThunkState::NativeCallback(cb)));
1633                            Err(VMError::Throw(e))
1634                        }
1635                    }
1636                }
1637                other => {
1638                    thunk.state.set(other);
1639                    // Pending/LazySource/Evaluating — needs VM to force.
1640                    Err(VMError::TypeError {
1641                        expected: "concrete value",
1642                        got: "thunk (pending)",
1643                        context: "builtin argument (thunk needs VM to force)".to_string(),
1644                    })
1645                }
1646            }
1647        }
1648        other => Ok(other),
1649    }
1650}
1651
1652/// Convert StringKeyedValue → VMValue (inverse of to_string_keyed).
1653fn sk_to_vmvalue(sk: &crate::value::StringKeyedValue) -> VMValue {
1654    use crate::value::StringKeyedValue;
1655    match sk {
1656        StringKeyedValue::Null => VMValue::Null,
1657        StringKeyedValue::Bool(b) => VMValue::Bool(*b),
1658        StringKeyedValue::Int(n) => VMValue::Int(*n),
1659        StringKeyedValue::Float(f) => VMValue::Float(*f),
1660        StringKeyedValue::String(s) => VMValue::String(s.clone()),
1661        StringKeyedValue::Path(p) => VMValue::Path(p.clone()),
1662        StringKeyedValue::List(items) => {
1663            VMValue::List(items.iter().map(|i| sk_to_vmvalue(i)).collect())
1664        }
1665        StringKeyedValue::Attrs(map) => {
1666            // Use the global interner for symbol resolution
1667            let mut interner = crate::intern::Interner::new();
1668            VMValue::Attrs(map.iter().map(|(k, v)| {
1669                (interner.intern(k), sk_to_vmvalue(v))
1670            }).collect())
1671        }
1672        StringKeyedValue::Lambda => VMValue::Null, // Can't reconstruct closures
1673        StringKeyedValue::Thunk(cb) => {
1674            // Wrap as a NativeCallback VMThunk for lazy evaluation
1675            let cb = cb.clone();
1676            VMValue::Thunk(crate::value::VMThunk {
1677                state: Rc::new(Cell::new(Some(ThunkState::NativeCallback(cb)))),
1678            })
1679        }
1680        StringKeyedValue::Callable(_) => VMValue::Null, // Can't reconstruct
1681    }
1682}
1683
1684fn as_attrs(v: &VMValue) -> Result<&BTreeMap<Symbol, VMValue>, VMError> {
1685    match v {
1686        VMValue::Attrs(a) => Ok(a),
1687        VMValue::Thunk(_) => Err(VMError::TypeError {
1688            expected: "set",
1689            got: "thunk",
1690            context: "builtin argument".to_string(),
1691        }),
1692        other => Err(VMError::TypeError {
1693            expected: "set",
1694            got: other.type_name(),
1695            context: "builtin argument".to_string(),
1696        }),
1697    }
1698}
1699
1700fn as_string(v: &VMValue) -> Result<&str, VMError> {
1701    match v {
1702        VMValue::String(s) => Ok(s),
1703        other => Err(VMError::TypeError {
1704            expected: "string",
1705            got: other.type_name(),
1706            context: "builtin argument".to_string(),
1707        }),
1708    }
1709}
1710
1711/// Force-aware string extraction: forces thunks before extracting.
1712/// Use this when iterating over list elements that may be thunks.
1713fn force_as_string(v: &VMValue) -> Result<String, VMError> {
1714    match v {
1715        VMValue::String(s) => Ok(s.clone()),
1716        VMValue::Thunk(_) => {
1717            let forced = force_vmvalue(v.clone())?;
1718            match forced {
1719                VMValue::String(s) => Ok(s),
1720                other => Err(VMError::TypeError {
1721                    expected: "string",
1722                    got: other.type_name(),
1723                    context: "builtin argument (after forcing thunk)".to_string(),
1724                }),
1725            }
1726        }
1727        other => Err(VMError::TypeError {
1728            expected: "string",
1729            got: other.type_name(),
1730            context: "builtin argument".to_string(),
1731        }),
1732    }
1733}
1734
1735/// Force-aware list extraction: forces thunks before extracting.
1736fn force_as_list(v: &VMValue) -> Result<Vec<VMValue>, VMError> {
1737    match v {
1738        VMValue::List(l) => Ok(l.clone()),
1739        VMValue::Thunk(_) => {
1740            let forced = force_vmvalue(v.clone())?;
1741            match forced {
1742                VMValue::List(l) => Ok(l),
1743                other => Err(VMError::TypeError {
1744                    expected: "list",
1745                    got: other.type_name(),
1746                    context: "builtin argument (after forcing thunk)".to_string(),
1747                }),
1748            }
1749        }
1750        other => Err(VMError::TypeError {
1751            expected: "list",
1752            got: other.type_name(),
1753            context: "builtin argument".to_string(),
1754        }),
1755    }
1756}
1757
1758fn as_int(v: &VMValue) -> Result<i64, VMError> {
1759    match v {
1760        VMValue::Int(n) => Ok(*n),
1761        other => Err(VMError::TypeError {
1762            expected: "int",
1763            got: other.type_name(),
1764            context: "builtin argument".to_string(),
1765        }),
1766    }
1767}
1768
1769fn as_float(v: &VMValue) -> Result<f64, VMError> {
1770    match v {
1771        VMValue::Float(f) => Ok(*f),
1772        VMValue::Int(n) => Ok(*n as f64),
1773        other => Err(VMError::TypeError {
1774            expected: "float",
1775            got: other.type_name(),
1776            context: "builtin argument".to_string(),
1777        }),
1778    }
1779}
1780
1781/// Coerce a VMValue to string, matching CppNix's `builtins.toString` semantics:
1782/// - Strings, ints, floats, bools, null, paths: straightforward conversion
1783/// - Attrsets with `__toString`: call the function with the attrset as argument
1784///   (handled by VM fallback — here we just check `outPath`)
1785/// - Attrsets with `outPath`: coerce the outPath value
1786/// - Lists: space-join coerced elements
1787fn vm_coerce_to_string(v: &VMValue) -> Result<VMValue, VMError> {
1788    match v {
1789        VMValue::String(s) => Ok(VMValue::String(s.clone())),
1790        VMValue::Int(n) => Ok(VMValue::String(n.to_string())),
1791        // 6-decimal fixed-point to match CppNix's `%f` float coercion.
1792        VMValue::Float(f) => Ok(VMValue::String(format!("{f:.6}"))),
1793        VMValue::Bool(true) => Ok(VMValue::String("1".to_string())),
1794        VMValue::Bool(false) => Ok(VMValue::String(String::new())),
1795        VMValue::Null => Ok(VMValue::String(String::new())),
1796        VMValue::Path(p) => Ok(VMValue::String(p.clone())),
1797        VMValue::Attrs(attrs) => {
1798            // Check __toString first (requires calling a function — if present,
1799            // we fall back to the VM bridge for now)
1800            let to_str_sym = crate::intern::intern("__toString");
1801            if attrs.contains_key(&to_str_sym) {
1802                // __toString requires calling a closure with the attrset.
1803                // This can't be done from a pure builtin — the VM will handle
1804                // this via the bridge fallback.
1805                return Err(VMError::Throw(
1806                    "toString: __toString requires VM bridge".to_string(),
1807                ));
1808            }
1809            let out_path_sym = crate::intern::intern("outPath");
1810            if let Some(out_path) = attrs.get(&out_path_sym) {
1811                vm_coerce_to_string(out_path)
1812            } else {
1813                Err(VMError::Throw(
1814                    "cannot coerce a set to a string, but it has no __toString or outPath".to_string(),
1815                ))
1816            }
1817        }
1818        VMValue::List(items) => {
1819            let mut parts = Vec::with_capacity(items.len());
1820            for item in items {
1821                match vm_coerce_to_string(item)? {
1822                    VMValue::String(s) => parts.push(s),
1823                    _ => unreachable!("vm_coerce_to_string always returns String"),
1824                }
1825            }
1826            Ok(VMValue::String(parts.join(" ")))
1827        }
1828        VMValue::Closure(_) | VMValue::Builtin(_) | VMValue::HigherOrderBuiltin(_) => {
1829            Err(VMError::Throw(
1830                "cannot coerce a function to a string".to_string(),
1831            ))
1832        }
1833        VMValue::Thunk(_) => {
1834            Err(VMError::Throw(
1835                "toString: thunk should be forced first".to_string(),
1836            ))
1837        }
1838    }
1839}
1840
1841/// Convert a VMValue to serde_json::Value for toJSON.
1842fn vm_value_to_json(v: &VMValue) -> Result<serde_json::Value, VMError> {
1843    match v {
1844        VMValue::Null => Ok(serde_json::Value::Null),
1845        VMValue::Bool(b) => Ok(serde_json::Value::Bool(*b)),
1846        VMValue::Int(n) => Ok(serde_json::Value::Number(
1847            serde_json::Number::from(*n),
1848        )),
1849        VMValue::Float(f) => serde_json::Number::from_f64(*f)
1850            .map(serde_json::Value::Number)
1851            .ok_or_else(|| VMError::Throw("toJSON: invalid float".to_string())),
1852        VMValue::String(s) => Ok(serde_json::Value::String(s.clone())),
1853        VMValue::Path(p) => Ok(serde_json::Value::String(p.clone())),
1854        VMValue::List(items) => {
1855            let arr: Result<Vec<_>, _> = items.iter().map(vm_value_to_json).collect();
1856            Ok(serde_json::Value::Array(arr?))
1857        }
1858        VMValue::Attrs(_) => {
1859            // Can't convert attrsets without interner access for key names
1860            Err(VMError::Throw(
1861                "toJSON: attrset conversion requires interner".to_string(),
1862            ))
1863        }
1864        VMValue::Closure(_) | VMValue::Builtin(_) | VMValue::HigherOrderBuiltin(_) => {
1865            Err(VMError::Throw("toJSON: cannot convert function".to_string()))
1866        }
1867        VMValue::Thunk(_) => {
1868            Err(VMError::Throw("toJSON: thunk should be forced first".to_string()))
1869        }
1870    }
1871}
1872
1873/// Convert a serde_json::Value to VMValue for fromJSON.
1874fn json_to_vm_value(v: &serde_json::Value) -> VMValue {
1875    match v {
1876        serde_json::Value::Null => VMValue::Null,
1877        serde_json::Value::Bool(b) => VMValue::Bool(*b),
1878        serde_json::Value::Number(n) => {
1879            if let Some(i) = n.as_i64() {
1880                VMValue::Int(i)
1881            } else {
1882                VMValue::Float(n.as_f64().unwrap_or(0.0))
1883            }
1884        }
1885        serde_json::Value::String(s) => VMValue::String(s.clone()),
1886        serde_json::Value::Array(arr) => {
1887            VMValue::List(arr.iter().map(json_to_vm_value).collect())
1888        }
1889        serde_json::Value::Object(_) => {
1890            // Can't create Symbol-keyed attrsets without an interner.
1891            // Return null as a fallback; real usage goes through VM.
1892            VMValue::Null
1893        }
1894    }
1895}
1896
1897#[cfg(test)]
1898mod tests {
1899    use super::*;
1900
1901    #[test]
1902    fn registry_has_builtins() {
1903        let reg = BuiltinRegistry::new();
1904        assert!(reg.lookup("length").is_some());
1905        assert!(reg.lookup("typeOf").is_some());
1906        assert!(reg.lookup("head").is_some());
1907        assert!(reg.lookup("tail").is_some());
1908        assert!(reg.lookup("throw").is_some());
1909        assert!(reg.lookup("nonexistent").is_none());
1910    }
1911
1912    #[test]
1913    fn call_length() {
1914        let reg = BuiltinRegistry::new();
1915        let idx = reg.lookup("length").unwrap();
1916        let result = reg
1917            .call(idx, vec![VMValue::List(vec![VMValue::Int(1), VMValue::Int(2)])])
1918            .unwrap();
1919        assert_eq!(result, VMValue::Int(2));
1920    }
1921
1922    #[test]
1923    fn call_head() {
1924        let reg = BuiltinRegistry::new();
1925        let idx = reg.lookup("head").unwrap();
1926        let result = reg
1927            .call(idx, vec![VMValue::List(vec![VMValue::Int(10)])])
1928            .unwrap();
1929        assert_eq!(result, VMValue::Int(10));
1930    }
1931
1932    #[test]
1933    fn call_head_empty() {
1934        let reg = BuiltinRegistry::new();
1935        let idx = reg.lookup("head").unwrap();
1936        let result = reg.call(idx, vec![VMValue::List(vec![])]);
1937        assert!(result.is_err());
1938    }
1939
1940    #[test]
1941    fn call_type_of() {
1942        let reg = BuiltinRegistry::new();
1943        let idx = reg.lookup("typeOf").unwrap();
1944        assert_eq!(
1945            reg.call(idx, vec![VMValue::Int(42)]).unwrap(),
1946            VMValue::String("int".to_string())
1947        );
1948        assert_eq!(
1949            reg.call(idx, vec![VMValue::String("hello".to_string())])
1950                .unwrap(),
1951            VMValue::String("string".to_string())
1952        );
1953    }
1954
1955    #[test]
1956    fn call_string_length() {
1957        let reg = BuiltinRegistry::new();
1958        let idx = reg.lookup("stringLength").unwrap();
1959        let result = reg
1960            .call(idx, vec![VMValue::String("hello".to_string())])
1961            .unwrap();
1962        assert_eq!(result, VMValue::Int(5));
1963    }
1964
1965    #[test]
1966    fn call_throw() {
1967        let reg = BuiltinRegistry::new();
1968        let idx = reg.lookup("throw").unwrap();
1969        let result = reg.call(idx, vec![VMValue::String("test error".to_string())]);
1970        assert!(matches!(result, Err(VMError::Throw(_))));
1971    }
1972
1973    #[test]
1974    fn call_to_string() {
1975        let reg = BuiltinRegistry::new();
1976        let idx = reg.lookup("toString").unwrap();
1977        assert_eq!(
1978            reg.call(idx, vec![VMValue::Int(42)]).unwrap(),
1979            VMValue::String("42".to_string())
1980        );
1981        assert_eq!(
1982            reg.call(idx, vec![VMValue::Bool(true)]).unwrap(),
1983            VMValue::String("1".to_string())
1984        );
1985    }
1986
1987    #[test]
1988    fn builtins_attrset() {
1989        let reg = BuiltinRegistry::new();
1990        let mut interner = Interner::new();
1991        let builtins = reg.make_builtins_attrset(&mut interner);
1992        match &builtins {
1993            VMValue::Attrs(attrs) => {
1994                let length_sym = interner.lookup("length").unwrap();
1995                assert!(attrs.contains_key(&length_sym));
1996            }
1997            _ => panic!("expected Attrs"),
1998        }
1999    }
2000}