Skip to main content

sui_eval/
lib.rs

1//! Clean-room Nix language evaluator.
2//!
3//! Architecture:
4//! Nix source → rnix parser (CST) → Evaluator (values)
5//!
6//! Parsing is delegated to the `rnix` crate (MIT).
7
8use std::rc::Rc;
9
10/// Core Nix builtins (90+ functions).
11pub mod builtins;
12/// Bidirectional conversion between bytecode VM values and tree-walker values.
13pub mod convert;
14/// Tree-walking evaluator using rnix's typed AST.
15pub mod eval;
16/// Content-addressed derivation path cache (redb-backed).
17pub mod drv_cache;
18/// Content-addressed evaluation cache (file hash + lock hash → result).
19pub mod eval_cache;
20/// Content-addressed input fetcher for flake.lock resolved inputs.
21pub mod fetcher;
22/// Native flake lock management — update, check, write.
23pub mod flake_lock;
24/// Pure-Rust git operations via gix/gitoxide (no CLI spawning, no C deps).
25pub mod git;
26/// Centralized path resolution (normalize, resolve relative, import).
27pub mod path;
28/// Source positions for `builtins.unsafeGetAttrPos` / `__curPos`.
29pub mod pos;
30/// Lightweight evaluation profiling counters.
31pub mod perf;
32/// ENV-RESOLVE M0 flag + per-source resolution-table plumbing (the
33/// tree-walker's consume side of the `sui-resolve` side-table).
34pub mod resolve_env;
35/// Infinite recursion debugging tools (force chain, trace, depth limit, stats).
36pub mod trace;
37/// Nix value types, environments, thunks, and error types.
38pub mod value;
39/// Lazy evaluation primitives — making accidental eagerness impossible.
40pub mod lazy;
41/// Import-from-derivation: realize a derivation output mid-eval via a
42/// binary-installed hook (the pure evaluator owns no build pipeline).
43pub mod realize;
44
45/// Re-export flake lock types from sui-compat where they canonically live.
46pub mod flake {
47    pub use sui_compat::flake::*;
48}
49
50/// Evaluate a Nix expression string (convenience re-export).
51pub use eval::eval;
52/// Re-exported for ergonomic access from dependent crates.
53pub use value::{EvalError, Value};
54
55/// The evaluator trait — enables swapping evaluation strategies.
56///
57/// Implementations: tree-walking (current), bytecode VM (future),
58/// delegation to external `nix eval` (fallback during transition).
59pub trait Evaluator {
60    /// Evaluate a Nix expression string.
61    fn eval_expr(&self, input: &str) -> Result<Value, EvalError>;
62
63    /// Evaluate a Nix file.
64    fn eval_file(&self, path: &std::path::Path) -> Result<Value, EvalError>;
65}
66
67/// The default tree-walking evaluator.
68pub struct TreeWalkEvaluator;
69
70impl Evaluator for TreeWalkEvaluator {
71    fn eval_expr(&self, input: &str) -> Result<Value, EvalError> {
72        eval(input)
73    }
74
75    fn eval_file(&self, path: &std::path::Path) -> Result<Value, EvalError> {
76        let source = std::fs::read_to_string(path)
77            .map_err(|e| EvalError::IoError {
78                context: format!("eval_file: {}", path.display()),
79                message: e.to_string(),
80            })?;
81        let path_buf = path.to_path_buf();
82        let _guard = eval::push_eval_file(path_buf.clone());
83        eval::eval_with_file(&source, Some(path_buf))
84    }
85}
86
87/// Bytecode VM evaluator — compiles to bytecode and executes on the stack VM.
88///
89/// Installs a flake resolver that delegates `builtins.getFlake` to the
90/// tree-walker's [`builtins::evaluate_flake`], so the VM gets correct
91/// flake input resolution for all input types (GitHub, path, indirect).
92pub struct BytecodeEvaluator;
93
94impl BytecodeEvaluator {
95    /// Run a bytecode evaluation with tree-walker bridges installed.
96    ///
97    /// Installs two bridges before running the VM:
98    /// 1. **Flake resolver** — delegates `builtins.getFlake` to the tree-walker
99    /// 2. **Builtin bridge** — delegates missing builtins (getEnv, match, split,
100    ///    fromTOML, genericClosure, etc.) to tree-walker implementations
101    fn eval_with_flake_resolver(input: &str) -> Result<Value, EvalError> {
102        // Install flake resolver: tree-walker evaluate_flake → StringKeyedValue
103        let _flake_guard = sui_bytecode::set_flake_resolver(Box::new(|flake_ref: &str| {
104            let flake_dir = if flake_ref.starts_with('/') || flake_ref.starts_with('.') {
105                std::path::PathBuf::from(flake_ref)
106            } else if let Some(path) = flake_ref.strip_prefix("path:") {
107                std::path::PathBuf::from(path)
108            } else {
109                return Err(format!("unsupported flake reference: {flake_ref}"));
110            };
111
112            let result = builtins::evaluate_flake(&flake_dir)
113                .map_err(|e| e.to_string())?;
114
115            // Convert tree-walker Value → StringKeyedValue for the VM.
116            Ok(eval_to_string_keyed(&result))
117        }));
118
119        // Install builtin bridge: VM builtins → tree-walker builtins
120        let _bridge_guard = sui_bytecode::set_builtin_bridge(Box::new(
121            |name: &str, args: Vec<sui_bytecode::StringKeyedValue>| {
122                // Special case: __import — the VM compiler couldn't handle
123                // this file, so fall back to the tree-walker evaluator.
124                if name == "__import" {
125                    let path_str = match &args[0] {
126                        sui_bytecode::StringKeyedValue::Path(p)
127                        | sui_bytecode::StringKeyedValue::String(p) => p.clone(),
128                        _ => return Err("__import: expected path or string argument".to_string()),
129                    };
130                    let path = std::path::Path::new(&path_str);
131                    let source = std::fs::read_to_string(path)
132                        .map_err(|e| format!("__import: {}: {e}", path.display()))?;
133                    let path_buf = path.to_path_buf();
134                    let _guard = eval::push_eval_file(path_buf.clone());
135                    let result = eval::eval_with_file(&source, Some(path_buf))
136                        .map_err(|e| e.to_string())?;
137                    // Force the top-level result before converting — if the
138                    // tree-walker returned a thunk, the VM would see
139                    // "expected set, got thunk" when accessing attrs.
140                    let forced = eval::force_value(&result)
141                        .map_err(|e| e.to_string())?;
142                    return Ok(eval_to_string_keyed(&forced));
143                }
144
145                // Convert StringKeyedValue args → tree-walker Value
146                let eval_args: Vec<Value> = args
147                    .iter()
148                    .map(|a| convert::string_keyed_to_eval(a))
149                    .collect();
150
151                // Call the tree-walker builtin
152                let result = builtins::call_builtin_by_name(name, &eval_args)
153                    .map_err(|e| e.to_string())?;
154
155                // Force the result before converting — builtins may return
156                // thunks that the VM cannot handle directly.
157                let forced = eval::force_value(&result)
158                    .map_err(|e| e.to_string())?;
159
160                // Convert tree-walker Value → StringKeyedValue
161                Ok(eval_to_string_keyed(&forced))
162            },
163        ));
164
165        match sui_bytecode::eval_full(input) {
166            Ok(result) => Ok(convert::string_keyed_to_eval(&result.to_string_keyed())),
167            Err(sui_bytecode::EvalError::Compile(c)) => {
168                // Compilation failed — fall back to tree-walker entirely.
169                eprintln!("[sui-vm] top-level compile fallback: {c}");
170                eval::eval(input)
171            }
172            Err(sui_bytecode::EvalError::Runtime(r)) => {
173                // Runtime error — fall back to tree-walker entirely.
174                // This handles VM bugs (GetLocal slot mismatch, etc.)
175                // that don't affect correctness of the tree-walker.
176                eprintln!("[sui-vm] top-level runtime fallback: {r}");
177                eval::eval(input)
178            }
179        }
180    }
181}
182
183/// Convert a tree-walker `Value` to a `StringKeyedValue` (no interner needed).
184///
185/// Used by the flake resolver bridge to convert tree-walker results
186/// into a format the bytecode VM can consume.
187///
188/// **Lazy thunk handling:** Tree-walker thunks are NOT eagerly forced.
189/// Instead, they are wrapped in `StringKeyedValue::Thunk` with a callback
190/// that forces the underlying tree-walker thunk on demand. This is critical
191/// for `getFlake` performance: a typical flake has 100+ transitive input
192/// thunks, and forcing them all would trigger git clones, recursive flake
193/// resolution, and full evaluation of every dependency (10s+). By wrapping
194/// lazily, only the inputs actually accessed by the expression are evaluated.
195pub fn eval_to_string_keyed(val: &Value) -> sui_bytecode::StringKeyedValue {
196    match val {
197        Value::Null => sui_bytecode::StringKeyedValue::Null,
198        Value::Bool(b) => sui_bytecode::StringKeyedValue::Bool(*b),
199        Value::Int(n) => sui_bytecode::StringKeyedValue::Int(*n),
200        Value::Float(f) => sui_bytecode::StringKeyedValue::Float(*f),
201        Value::String(s) => sui_bytecode::StringKeyedValue::String(s.chars.to_string()),
202        Value::Path(p) => sui_bytecode::StringKeyedValue::Path(p.to_string()),
203        Value::List(items) => {
204            sui_bytecode::StringKeyedValue::List(
205                items.iter().map(eval_to_string_keyed).collect(),
206            )
207        }
208        Value::Attrs(attrs) => {
209            let mut map = std::collections::BTreeMap::new();
210            for (k, v) in attrs.iter() {
211                map.insert(k.clone(), eval_to_string_keyed(v));
212            }
213            sui_bytecode::StringKeyedValue::Attrs(map)
214        }
215        Value::Lambda(closure) => {
216            // Wrap in Rc so the Fn closure captures a shared pointer
217            // rather than an owned Closure.  Each invocation clones out
218            // of the Rc (all Closure fields are refcounted, so O(1)),
219            // but the capture itself is just an Rc bump — no redundant
220            // outer+inner double-clone.
221            let closure_rc = std::rc::Rc::new((**closure).clone());
222            sui_bytecode::StringKeyedValue::Callable(std::rc::Rc::new(move |arg| {
223                let eval_arg = convert::string_keyed_to_eval(&arg);
224                let func = Value::Lambda(Rc::new((*closure_rc).clone()));
225                let result = eval::apply(func, eval_arg)
226                    .map_err(|e| e.to_string())?;
227                let forced = eval::force_value(&result)
228                    .map_err(|e| e.to_string())?;
229                Ok(eval_to_string_keyed(&forced))
230            }))
231        }
232        Value::Builtin(bf) => {
233            // Same Rc pattern as Lambda above — BuiltinFn::clone() is
234            // already cheap (static str + Rc bump on func), but the Rc
235            // wrapper avoids the misleading double-clone.
236            let bf_rc = std::rc::Rc::new((**bf).clone());
237            sui_bytecode::StringKeyedValue::Callable(std::rc::Rc::new(move |arg| {
238                let eval_arg = convert::string_keyed_to_eval(&arg);
239                let func = Value::Builtin(Box::new((*bf_rc).clone()));
240                let result = eval::apply(func, eval_arg)
241                    .map_err(|e| e.to_string())?;
242                let forced = eval::force_value(&result)
243                    .map_err(|e| e.to_string())?;
244                Ok(eval_to_string_keyed(&forced))
245            }))
246        }
247        Value::Thunk(t) => {
248            // Fast path: if already evaluated, convert the memoized value
249            // without creating a thunk wrapper.
250            if t.is_evaluated() {
251                match t.force(&|e, env| eval::eval_expr(e, env)) {
252                    Ok(v) => eval_to_string_keyed(&v),
253                    Err(_) => sui_bytecode::StringKeyedValue::Null,
254                }
255            } else {
256                // LAZY: Wrap the tree-walker thunk in a callback that
257                // forces it on demand. The Rc clone is cheap and keeps
258                // the thunk's memoization cell shared, so forcing once
259                // caches the result for all subsequent accesses.
260                let thunk_clone = t.clone();
261                sui_bytecode::StringKeyedValue::Thunk(std::rc::Rc::new(move || {
262                    let forced = thunk_clone
263                        .force(&|e, env| eval::eval_expr(e, env))
264                        .map_err(|e| e.to_string())?;
265                    Ok(eval_to_string_keyed(&forced))
266                }))
267            }
268        }
269    }
270}
271
272impl Evaluator for BytecodeEvaluator {
273    fn eval_expr(&self, input: &str) -> Result<Value, EvalError> {
274        Self::eval_with_flake_resolver(input)
275    }
276
277    fn eval_file(&self, path: &std::path::Path) -> Result<Value, EvalError> {
278        let source = std::fs::read_to_string(path)
279            .map_err(|e| EvalError::IoError {
280                context: format!("eval_file: {}", path.display()),
281                message: e.to_string(),
282            })?;
283        self.eval_expr(&source)
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    struct MockEvaluator(Result<Value, EvalError>);
292    impl Evaluator for MockEvaluator {
293        fn eval_expr(&self, _: &str) -> Result<Value, EvalError> {
294            match &self.0 { Ok(v) => Ok(v.clone()), Err(_) => Err(EvalError::NotImplemented("mock".into())) }
295        }
296        fn eval_file(&self, _: &std::path::Path) -> Result<Value, EvalError> {
297            self.eval_expr("")
298        }
299    }
300
301    #[test]
302    fn mock_evaluator_ok() {
303        let e = MockEvaluator(Ok(Value::Int(42)));
304        assert_eq!(e.eval_expr("anything").unwrap(), Value::Int(42));
305    }
306
307    #[test]
308    fn mock_evaluator_err() {
309        let e = MockEvaluator(Err(EvalError::NotImplemented("x".into())));
310        assert!(e.eval_expr("anything").is_err());
311    }
312
313    #[test]
314    fn tree_walk_evaluator() {
315        let e = TreeWalkEvaluator;
316        assert_eq!(e.eval_expr("1 + 2").unwrap(), Value::Int(3));
317    }
318
319    #[test]
320    fn evaluator_trait_object_safe() {
321        fn _assert(_: &dyn Evaluator) {}
322    }
323
324    // ── TreeWalkEvaluator through Evaluator trait ────────────
325
326    #[test]
327    fn tree_walk_eval_integer_arithmetic() {
328        let e: &dyn Evaluator = &TreeWalkEvaluator;
329        assert_eq!(e.eval_expr("2 + 3").unwrap(), Value::Int(5));
330    }
331
332    #[test]
333    fn tree_walk_eval_string_literal() {
334        let e: &dyn Evaluator = &TreeWalkEvaluator;
335        assert_eq!(
336            e.eval_expr(r#""hello world""#).unwrap(),
337            Value::string("hello world"),
338        );
339    }
340
341    #[test]
342    fn tree_walk_eval_boolean() {
343        let e: &dyn Evaluator = &TreeWalkEvaluator;
344        assert_eq!(e.eval_expr("true && false").unwrap(), Value::Bool(false));
345    }
346
347    #[test]
348    fn tree_walk_eval_if_else() {
349        let e: &dyn Evaluator = &TreeWalkEvaluator;
350        assert_eq!(
351            e.eval_expr("if true then 42 else 0").unwrap(),
352            Value::Int(42),
353        );
354    }
355
356    #[test]
357    fn tree_walk_eval_let_binding() {
358        let e: &dyn Evaluator = &TreeWalkEvaluator;
359        assert_eq!(
360            e.eval_expr("let x = 10; in x * 2").unwrap(),
361            Value::Int(20),
362        );
363    }
364
365    #[test]
366    fn tree_walk_eval_attrset() {
367        let e: &dyn Evaluator = &TreeWalkEvaluator;
368        let val = e.eval_expr("{ a = 1; b = 2; }.a").unwrap();
369        assert_eq!(val, Value::Int(1));
370    }
371
372    #[test]
373    fn tree_walk_eval_list() {
374        let e: &dyn Evaluator = &TreeWalkEvaluator;
375        let val = e.eval_expr("[1 2 3]").unwrap();
376        assert_eq!(
377            val,
378            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
379        );
380    }
381
382    #[test]
383    fn tree_walk_eval_lambda_application() {
384        let e: &dyn Evaluator = &TreeWalkEvaluator;
385        assert_eq!(
386            e.eval_expr("(x: x + 1) 5").unwrap(),
387            Value::Int(6),
388        );
389    }
390
391    #[test]
392    fn tree_walk_eval_builtin_via_trait() {
393        let e: &dyn Evaluator = &TreeWalkEvaluator;
394        assert_eq!(
395            e.eval_expr("builtins.length [1 2 3]").unwrap(),
396            Value::Int(3),
397        );
398    }
399
400    #[test]
401    fn tree_walk_eval_parse_error_via_trait() {
402        let e: &dyn Evaluator = &TreeWalkEvaluator;
403        let result = e.eval_expr("let in");
404        assert!(result.is_err());
405    }
406
407    #[test]
408    fn tree_walk_eval_null_via_trait() {
409        let e: &dyn Evaluator = &TreeWalkEvaluator;
410        assert_eq!(e.eval_expr("null").unwrap(), Value::Null);
411    }
412
413    #[test]
414    fn tree_walk_eval_file_missing() {
415        let e: &dyn Evaluator = &TreeWalkEvaluator;
416        let result = e.eval_file(std::path::Path::new("/nonexistent/file.nix"));
417        assert!(result.is_err());
418    }
419
420    #[test]
421    fn tree_walk_eval_string_interpolation_via_trait() {
422        let e: &dyn Evaluator = &TreeWalkEvaluator;
423        assert_eq!(
424            e.eval_expr(r#"let name = "world"; in "hello ${name}""#).unwrap(),
425            Value::string("hello world"),
426        );
427    }
428
429    #[test]
430    fn tree_walk_eval_comparison_via_trait() {
431        let e: &dyn Evaluator = &TreeWalkEvaluator;
432        assert_eq!(e.eval_expr("3 > 2").unwrap(), Value::Bool(true));
433        assert_eq!(e.eval_expr("1 == 1").unwrap(), Value::Bool(true));
434    }
435
436    #[test]
437    fn tree_walk_eval_recursive_attrset_via_trait() {
438        let e: &dyn Evaluator = &TreeWalkEvaluator;
439        assert_eq!(
440            e.eval_expr("rec { x = 1; y = x + 1; }.y").unwrap(),
441            Value::Int(2),
442        );
443    }
444
445    // ── Re-exports & convenience eval shim ─────────────────
446
447    #[test]
448    fn re_export_eval_function_works() {
449        // The top-level `eval` re-export from `eval::eval` should be
450        // identical to the inner function.
451        assert_eq!(eval("1 + 1").unwrap(), Value::Int(2));
452    }
453
454    #[test]
455    fn re_export_value_and_error_types_constructible() {
456        let v: Value = Value::Int(7);
457        let e: EvalError = EvalError::UndefinedVar("x".into());
458        assert_eq!(v.type_name(), "int");
459        assert!(e.to_string().contains("undefined"));
460    }
461
462    // ── flake re-export from sui-compat ────────────────────
463
464    #[test]
465    fn flake_module_re_exports_compat_types() {
466        // Smoke test that the flake submodule re-export compiles. We
467        // reference a known type from the sui_compat::flake module via
468        // our own re-export. Whatever types live there must be reachable.
469        // We use the path explicitly to force compilation of the import.
470        #[allow(unused_imports)]
471        use crate::flake::*;
472        // The block is intentionally empty: success is "this compiled".
473    }
474
475    // ── TreeWalkEvaluator passes path through ──────────────
476
477    #[test]
478    fn tree_walk_eval_file_with_real_temp_file() {
479        let dir = std::env::temp_dir().join("sui-eval-test-tree-walk");
480        let _ = std::fs::create_dir_all(&dir);
481        let path = dir.join("simple.nix");
482        std::fs::write(&path, "1 + 2").unwrap();
483        let e: &dyn Evaluator = &TreeWalkEvaluator;
484        let result = e.eval_file(&path).unwrap();
485        assert_eq!(result, Value::Int(3));
486        let _ = std::fs::remove_file(&path);
487        let _ = std::fs::remove_dir(&dir);
488    }
489
490    #[test]
491    fn tree_walk_eval_file_propagates_io_error_kind() {
492        let e: &dyn Evaluator = &TreeWalkEvaluator;
493        let result = e.eval_file(std::path::Path::new("/nonexistent/never/exists.nix"));
494        match result {
495            Err(EvalError::IoError { context, .. }) => {
496                assert!(context.contains("eval_file"));
497            }
498            other => panic!("expected IoError, got {other:?}"),
499        }
500    }
501
502    #[test]
503    fn tree_walk_eval_file_parse_error_propagates() {
504        let dir = std::env::temp_dir().join("sui-eval-test-tw-parse");
505        let _ = std::fs::create_dir_all(&dir);
506        let path = dir.join("bad.nix");
507        std::fs::write(&path, "let in").unwrap();
508        let e: &dyn Evaluator = &TreeWalkEvaluator;
509        let result = e.eval_file(&path);
510        assert!(result.is_err());
511        let _ = std::fs::remove_file(&path);
512        let _ = std::fs::remove_dir(&dir);
513    }
514
515    // ── Mock evaluator additional ──────────────────────────
516
517    #[test]
518    fn mock_evaluator_dispatched_via_trait_object() {
519        let m: Box<dyn Evaluator> = Box::new(MockEvaluator(Ok(Value::Bool(true))));
520        let r = m.eval_expr("anything").unwrap();
521        assert_eq!(r, Value::Bool(true));
522    }
523
524    #[test]
525    fn mock_evaluator_eval_file_routes_through_eval_expr() {
526        let m = MockEvaluator(Ok(Value::Int(1)));
527        let r = m.eval_file(std::path::Path::new("/dev/null"));
528        assert_eq!(r.unwrap(), Value::Int(1));
529    }
530
531    // ── Through-trait coverage of more constructs ──────────
532
533    #[test]
534    fn tree_walk_eval_function_with_default_args() {
535        let e: &dyn Evaluator = &TreeWalkEvaluator;
536        assert_eq!(
537            e.eval_expr("({a, b ? 10}: a + b) {a = 5;}").unwrap(),
538            Value::Int(15),
539        );
540    }
541
542    #[test]
543    fn tree_walk_eval_with_throws_propagated() {
544        let e: &dyn Evaluator = &TreeWalkEvaluator;
545        let result = e.eval_expr(r#"builtins.throw "boom""#);
546        assert!(result.is_err());
547        let err = result.unwrap_err();
548        assert!(err.is_throw());
549    }
550
551    #[test]
552    fn tree_walk_eval_assert_failure() {
553        let e: &dyn Evaluator = &TreeWalkEvaluator;
554        let result = e.eval_expr("assert false; 42");
555        assert!(matches!(result, Err(EvalError::AssertionFailed(_))));
556    }
557
558    #[test]
559    fn tree_walk_eval_division_by_zero() {
560        let e: &dyn Evaluator = &TreeWalkEvaluator;
561        let result = e.eval_expr("1 / 0");
562        assert!(matches!(result, Err(EvalError::DivisionByZero)));
563    }
564
565    #[test]
566    fn tree_walk_eval_undefined_variable() {
567        let e: &dyn Evaluator = &TreeWalkEvaluator;
568        let result = e.eval_expr("nonexistent_xyz");
569        assert!(matches!(result, Err(EvalError::UndefinedVar(_))));
570    }
571
572    #[test]
573    fn tree_walk_eval_path_literal() {
574        let e: &dyn Evaluator = &TreeWalkEvaluator;
575        let v = e.eval_expr("/tmp/x").unwrap();
576        assert!(matches!(v, Value::Path(_)));
577    }
578
579    #[test]
580    fn tree_walk_eval_float_literal() {
581        let e: &dyn Evaluator = &TreeWalkEvaluator;
582        assert_eq!(e.eval_expr("3.14").unwrap(), Value::Float(3.14));
583    }
584
585    #[test]
586    fn tree_walk_eval_lambda_returns_lambda() {
587        let e: &dyn Evaluator = &TreeWalkEvaluator;
588        let v = e.eval_expr("x: x").unwrap();
589        assert!(matches!(v, Value::Lambda(_)));
590    }
591}