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