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