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
99/// RAII bundle of every tree-walker bridge the VM needs, held for the
100/// duration of one VM evaluation.
101///
102/// Dropping this restores whatever was installed before.
103pub struct VmBridgeGuards {
104    _flake: sui_bytecode::FlakeResolverGuard,
105    _bridge: sui_bytecode::BuiltinBridgeGuard,
106    _path: sui_bytecode::PathMaterializerGuard,
107}
108
109/// Install every tree-walker bridge the bytecode VM depends on, returning an
110/// RAII bundle that uninstalls them on drop.
111///
112/// ★ THIS IS THE ONE INSTALL SITE. `BytecodeEvaluator` calls it, and so does
113/// `sui-bytecode`'s bridged-parity test suite — so a test can never drift from
114/// what production wires up (which is precisely how a bridge-dependent
115/// divergence stayed invisible: `sui-bytecode/tests/parity.rs` runs the VM with
116/// NO bridge installed).
117///
118/// Three bridges, all thread-local (`sui-bytecode` cannot depend on `sui-eval`
119/// — see that crate's `Cargo.toml` for the publish cycle):
120/// 1. **Flake resolver** — delegates `builtins.getFlake` to the tree-walker.
121/// 2. **Builtin bridge** — delegates builtins the VM has no native
122///    implementation for (`getEnv`, `match`, `split`, `fromTOML`,
123///    `genericClosure`, `readDir`, `hashFile`, …) to the tree-walker.
124/// 3. **Path materializer** — redirects the VM's filesystem reads through
125///    [`path::materialize`], so a flake input's `/nix/store/<narhash>-source`
126///    path (which sui never writes to disk) resolves to the real fetcher-cache
127///    tree. Without it the VM answers `pathExists = false` for every file in a
128///    fetched input — silently, since `false` is a legal answer and the VM's
129///    per-file fallback only fires on an ERROR.
130#[must_use]
131pub fn install_vm_bridges() -> VmBridgeGuards {
132    // Install flake resolver: tree-walker evaluate_flake → StringKeyedValue
133    let _flake_guard = sui_bytecode::set_flake_resolver(Box::new(|flake_ref: &str| {
134            let flake_dir = if flake_ref.starts_with('/') || flake_ref.starts_with('.') {
135                std::path::PathBuf::from(flake_ref)
136            } else if let Some(path) = flake_ref.strip_prefix("path:") {
137                std::path::PathBuf::from(path)
138            } else {
139                return Err(format!("unsupported flake reference: {flake_ref}"));
140            };
141
142            let result = builtins::evaluate_flake(&flake_dir)
143                .map_err(|e| e.to_string())?;
144
145            // Convert tree-walker Value → StringKeyedValue for the VM.
146            Ok(eval_to_string_keyed(&result))
147        }));
148
149        // Install builtin bridge: VM builtins → tree-walker builtins
150        let _bridge_guard = sui_bytecode::set_builtin_bridge(Box::new(
151            |name: &str, args: Vec<sui_bytecode::StringKeyedValue>| {
152                // Special case: __import — the VM compiler couldn't handle
153                // this file, so fall back to the tree-walker evaluator.
154                if name == "__import" {
155                    let path_str = match &args[0] {
156                        sui_bytecode::StringKeyedValue::Path(p)
157                        | sui_bytecode::StringKeyedValue::String(p) => p.clone(),
158                        _ => return Err("__import: expected path or string argument".to_string()),
159                    };
160                    let path = std::path::Path::new(&path_str);
161                    let source = std::fs::read_to_string(path)
162                        .map_err(|e| format!("__import: {}: {e}", path.display()))?;
163                    let path_buf = path.to_path_buf();
164                    let _guard = eval::push_eval_file(path_buf.clone());
165                    let result = eval::eval_with_file(&source, Some(path_buf))
166                        .map_err(|e| e.to_string())?;
167                    // Force the top-level result before converting — if the
168                    // tree-walker returned a thunk, the VM would see
169                    // "expected set, got thunk" when accessing attrs.
170                    let forced = eval::force_value(&result)
171                        .map_err(|e| e.to_string())?;
172                    return Ok(eval_to_string_keyed(&forced));
173                }
174
175                // Convert StringKeyedValue args → tree-walker Value
176                let eval_args: Vec<Value> = args
177                    .iter()
178                    .map(|a| convert::string_keyed_to_eval(a))
179                    .collect();
180
181                // Call the tree-walker builtin
182                let result = builtins::call_builtin_by_name(name, &eval_args)
183                    .map_err(|e| e.to_string())?;
184
185                // Force the result before converting — builtins may return
186                // thunks that the VM cannot handle directly.
187                let forced = eval::force_value(&result)
188                    .map_err(|e| e.to_string())?;
189
190                // Convert tree-walker Value → StringKeyedValue
191                Ok(eval_to_string_keyed(&forced))
192            },
193        ));
194
195    // Install path materializer: the VM's `std::fs` reads (pathExists,
196    // readFile, readFileType, import, scopedImport) go through the SAME
197    // store-path→cache-dir redirect the tree-walker uses. Identity for any
198    // path that is not under a registered flake-input source, so this is a
199    // no-op for every ordinary path.
200    let _path_guard = sui_bytecode::set_path_materializer(Box::new(|p: &str| {
201        crate::path::materialize_str(p)
202    }));
203
204    VmBridgeGuards {
205        _flake: _flake_guard,
206        _bridge: _bridge_guard,
207        _path: _path_guard,
208    }
209}
210
211impl BytecodeEvaluator {
212    /// Run a bytecode evaluation with tree-walker bridges installed.
213    ///
214    /// The bridges themselves are installed by [`install_vm_bridges`] — the
215    /// single install site, shared with the bridged-parity tests.
216    fn eval_with_flake_resolver(input: &str) -> Result<Value, EvalError> {
217        let _bridges = install_vm_bridges();
218
219        match sui_bytecode::eval_full(input) {
220            Ok(result) => Ok(convert::string_keyed_to_eval(&result.to_string_keyed())),
221            Err(sui_bytecode::EvalError::Compile(c)) => {
222                // Compilation failed — fall back to tree-walker entirely.
223                eprintln!("[sui-vm] top-level compile fallback: {c}");
224                eval::eval(input)
225            }
226            Err(sui_bytecode::EvalError::Runtime(r)) => {
227                // Runtime error — fall back to tree-walker entirely.
228                // This handles VM bugs (GetLocal slot mismatch, etc.)
229                // that don't affect correctness of the tree-walker.
230                eprintln!("[sui-vm] top-level runtime fallback: {r}");
231                eval::eval(input)
232            }
233        }
234    }
235}
236
237/// Convert a tree-walker `Value` to a `StringKeyedValue` (no interner needed).
238///
239/// Used by the flake resolver bridge to convert tree-walker results
240/// into a format the bytecode VM can consume.
241///
242/// **Lazy thunk handling:** Tree-walker thunks are NOT eagerly forced.
243/// Instead, they are wrapped in `StringKeyedValue::Thunk` with a callback
244/// that forces the underlying tree-walker thunk on demand. This is critical
245/// for `getFlake` performance: a typical flake has 100+ transitive input
246/// thunks, and forcing them all would trigger git clones, recursive flake
247/// resolution, and full evaluation of every dependency (10s+). By wrapping
248/// lazily, only the inputs actually accessed by the expression are evaluated.
249pub fn eval_to_string_keyed(val: &Value) -> sui_bytecode::StringKeyedValue {
250    match val {
251        Value::Null => sui_bytecode::StringKeyedValue::Null,
252        Value::Bool(b) => sui_bytecode::StringKeyedValue::Bool(*b),
253        Value::Int(n) => sui_bytecode::StringKeyedValue::Int(*n),
254        Value::Float(f) => sui_bytecode::StringKeyedValue::Float(*f),
255        Value::String(s) => sui_bytecode::StringKeyedValue::String(s.chars.to_string()),
256        Value::Path(p) => sui_bytecode::StringKeyedValue::Path(p.to_string()),
257        Value::List(items) => {
258            sui_bytecode::StringKeyedValue::List(
259                items.iter().map(eval_to_string_keyed).collect(),
260            )
261        }
262        Value::Attrs(attrs) => {
263            let mut map = std::collections::BTreeMap::new();
264            for (k, v) in attrs.iter() {
265                map.insert(k.clone(), eval_to_string_keyed(v));
266            }
267            sui_bytecode::StringKeyedValue::Attrs(map)
268        }
269        Value::Lambda(closure) => {
270            // Wrap in Rc so the Fn closure captures a shared pointer
271            // rather than an owned Closure.  Each invocation clones out
272            // of the Rc (all Closure fields are refcounted, so O(1)),
273            // but the capture itself is just an Rc bump — no redundant
274            // outer+inner double-clone.
275            let closure_rc = std::rc::Rc::new((**closure).clone());
276            sui_bytecode::StringKeyedValue::Callable(std::rc::Rc::new(move |arg| {
277                let eval_arg = convert::string_keyed_to_eval(&arg);
278                let func = Value::Lambda(Rc::new((*closure_rc).clone()));
279                let result = eval::apply(func, eval_arg)
280                    .map_err(|e| e.to_string())?;
281                let forced = eval::force_value(&result)
282                    .map_err(|e| e.to_string())?;
283                Ok(eval_to_string_keyed(&forced))
284            }))
285        }
286        Value::Builtin(bf) => {
287            // Same Rc pattern as Lambda above — BuiltinFn::clone() is
288            // already cheap (static str + Rc bump on func), but the Rc
289            // wrapper avoids the misleading double-clone.
290            let bf_rc = std::rc::Rc::new((**bf).clone());
291            sui_bytecode::StringKeyedValue::Callable(std::rc::Rc::new(move |arg| {
292                let eval_arg = convert::string_keyed_to_eval(&arg);
293                let func = Value::Builtin(Box::new((*bf_rc).clone()));
294                let result = eval::apply(func, eval_arg)
295                    .map_err(|e| e.to_string())?;
296                let forced = eval::force_value(&result)
297                    .map_err(|e| e.to_string())?;
298                Ok(eval_to_string_keyed(&forced))
299            }))
300        }
301        Value::Thunk(t) => {
302            // Fast path: if already evaluated, convert the memoized value
303            // without creating a thunk wrapper.
304            if t.is_evaluated() {
305                match t.force(&|e, env| eval::eval_expr(e, env)) {
306                    Ok(v) => eval_to_string_keyed(&v),
307                    Err(_) => sui_bytecode::StringKeyedValue::Null,
308                }
309            } else {
310                // LAZY: Wrap the tree-walker thunk in a callback that
311                // forces it on demand. The Rc clone is cheap and keeps
312                // the thunk's memoization cell shared, so forcing once
313                // caches the result for all subsequent accesses.
314                let thunk_clone = t.clone();
315                sui_bytecode::StringKeyedValue::Thunk(std::rc::Rc::new(move || {
316                    let forced = thunk_clone
317                        .force(&|e, env| eval::eval_expr(e, env))
318                        .map_err(|e| e.to_string())?;
319                    Ok(eval_to_string_keyed(&forced))
320                }))
321            }
322        }
323    }
324}
325
326impl Evaluator for BytecodeEvaluator {
327    fn eval_expr(&self, input: &str) -> Result<Value, EvalError> {
328        Self::eval_with_flake_resolver(input)
329    }
330
331    fn eval_file(&self, path: &std::path::Path) -> Result<Value, EvalError> {
332        let source = std::fs::read_to_string(path)
333            .map_err(|e| EvalError::IoError {
334                context: format!("eval_file: {}", path.display()),
335                message: e.to_string(),
336            })?;
337        self.eval_expr(&source)
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    struct MockEvaluator(Result<Value, EvalError>);
346    impl Evaluator for MockEvaluator {
347        fn eval_expr(&self, _: &str) -> Result<Value, EvalError> {
348            match &self.0 { Ok(v) => Ok(v.clone()), Err(_) => Err(EvalError::NotImplemented("mock".into())) }
349        }
350        fn eval_file(&self, _: &std::path::Path) -> Result<Value, EvalError> {
351            self.eval_expr("")
352        }
353    }
354
355    #[test]
356    fn mock_evaluator_ok() {
357        let e = MockEvaluator(Ok(Value::Int(42)));
358        assert_eq!(e.eval_expr("anything").unwrap(), Value::Int(42));
359    }
360
361    #[test]
362    fn mock_evaluator_err() {
363        let e = MockEvaluator(Err(EvalError::NotImplemented("x".into())));
364        assert!(e.eval_expr("anything").is_err());
365    }
366
367    #[test]
368    fn tree_walk_evaluator() {
369        let e = TreeWalkEvaluator;
370        assert_eq!(e.eval_expr("1 + 2").unwrap(), Value::Int(3));
371    }
372
373    #[test]
374    fn evaluator_trait_object_safe() {
375        fn _assert(_: &dyn Evaluator) {}
376    }
377
378    // ── TreeWalkEvaluator through Evaluator trait ────────────
379
380    #[test]
381    fn tree_walk_eval_integer_arithmetic() {
382        let e: &dyn Evaluator = &TreeWalkEvaluator;
383        assert_eq!(e.eval_expr("2 + 3").unwrap(), Value::Int(5));
384    }
385
386    #[test]
387    fn tree_walk_eval_string_literal() {
388        let e: &dyn Evaluator = &TreeWalkEvaluator;
389        assert_eq!(
390            e.eval_expr(r#""hello world""#).unwrap(),
391            Value::string("hello world"),
392        );
393    }
394
395    #[test]
396    fn tree_walk_eval_boolean() {
397        let e: &dyn Evaluator = &TreeWalkEvaluator;
398        assert_eq!(e.eval_expr("true && false").unwrap(), Value::Bool(false));
399    }
400
401    #[test]
402    fn tree_walk_eval_if_else() {
403        let e: &dyn Evaluator = &TreeWalkEvaluator;
404        assert_eq!(
405            e.eval_expr("if true then 42 else 0").unwrap(),
406            Value::Int(42),
407        );
408    }
409
410    #[test]
411    fn tree_walk_eval_let_binding() {
412        let e: &dyn Evaluator = &TreeWalkEvaluator;
413        assert_eq!(
414            e.eval_expr("let x = 10; in x * 2").unwrap(),
415            Value::Int(20),
416        );
417    }
418
419    #[test]
420    fn tree_walk_eval_attrset() {
421        let e: &dyn Evaluator = &TreeWalkEvaluator;
422        let val = e.eval_expr("{ a = 1; b = 2; }.a").unwrap();
423        assert_eq!(val, Value::Int(1));
424    }
425
426    #[test]
427    fn tree_walk_eval_list() {
428        let e: &dyn Evaluator = &TreeWalkEvaluator;
429        let val = e.eval_expr("[1 2 3]").unwrap();
430        assert_eq!(
431            val,
432            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
433        );
434    }
435
436    #[test]
437    fn tree_walk_eval_lambda_application() {
438        let e: &dyn Evaluator = &TreeWalkEvaluator;
439        assert_eq!(
440            e.eval_expr("(x: x + 1) 5").unwrap(),
441            Value::Int(6),
442        );
443    }
444
445    #[test]
446    fn tree_walk_eval_builtin_via_trait() {
447        let e: &dyn Evaluator = &TreeWalkEvaluator;
448        assert_eq!(
449            e.eval_expr("builtins.length [1 2 3]").unwrap(),
450            Value::Int(3),
451        );
452    }
453
454    #[test]
455    fn tree_walk_eval_parse_error_via_trait() {
456        let e: &dyn Evaluator = &TreeWalkEvaluator;
457        let result = e.eval_expr("let in");
458        assert!(result.is_err());
459    }
460
461    #[test]
462    fn tree_walk_eval_null_via_trait() {
463        let e: &dyn Evaluator = &TreeWalkEvaluator;
464        assert_eq!(e.eval_expr("null").unwrap(), Value::Null);
465    }
466
467    #[test]
468    fn tree_walk_eval_file_missing() {
469        let e: &dyn Evaluator = &TreeWalkEvaluator;
470        let result = e.eval_file(std::path::Path::new("/nonexistent/file.nix"));
471        assert!(result.is_err());
472    }
473
474    #[test]
475    fn tree_walk_eval_string_interpolation_via_trait() {
476        let e: &dyn Evaluator = &TreeWalkEvaluator;
477        assert_eq!(
478            e.eval_expr(r#"let name = "world"; in "hello ${name}""#).unwrap(),
479            Value::string("hello world"),
480        );
481    }
482
483    #[test]
484    fn tree_walk_eval_comparison_via_trait() {
485        let e: &dyn Evaluator = &TreeWalkEvaluator;
486        assert_eq!(e.eval_expr("3 > 2").unwrap(), Value::Bool(true));
487        assert_eq!(e.eval_expr("1 == 1").unwrap(), Value::Bool(true));
488    }
489
490    #[test]
491    fn tree_walk_eval_recursive_attrset_via_trait() {
492        let e: &dyn Evaluator = &TreeWalkEvaluator;
493        assert_eq!(
494            e.eval_expr("rec { x = 1; y = x + 1; }.y").unwrap(),
495            Value::Int(2),
496        );
497    }
498
499    // ── Re-exports & convenience eval shim ─────────────────
500
501    #[test]
502    fn re_export_eval_function_works() {
503        // The top-level `eval` re-export from `eval::eval` should be
504        // identical to the inner function.
505        assert_eq!(eval("1 + 1").unwrap(), Value::Int(2));
506    }
507
508    #[test]
509    fn re_export_value_and_error_types_constructible() {
510        let v: Value = Value::Int(7);
511        let e: EvalError = EvalError::UndefinedVar("x".into());
512        assert_eq!(v.type_name(), "int");
513        assert!(e.to_string().contains("undefined"));
514    }
515
516    // ── flake re-export from sui-compat ────────────────────
517
518    #[test]
519    fn flake_module_re_exports_compat_types() {
520        // Smoke test that the flake submodule re-export compiles. We
521        // reference a known type from the sui_compat::flake module via
522        // our own re-export. Whatever types live there must be reachable.
523        // We use the path explicitly to force compilation of the import.
524        #[allow(unused_imports)]
525        use crate::flake::*;
526        // The block is intentionally empty: success is "this compiled".
527    }
528
529    // ── TreeWalkEvaluator passes path through ──────────────
530
531    #[test]
532    fn tree_walk_eval_file_with_real_temp_file() {
533        let dir = std::env::temp_dir().join("sui-eval-test-tree-walk");
534        let _ = std::fs::create_dir_all(&dir);
535        let path = dir.join("simple.nix");
536        std::fs::write(&path, "1 + 2").unwrap();
537        let e: &dyn Evaluator = &TreeWalkEvaluator;
538        let result = e.eval_file(&path).unwrap();
539        assert_eq!(result, Value::Int(3));
540        let _ = std::fs::remove_file(&path);
541        let _ = std::fs::remove_dir(&dir);
542    }
543
544    #[test]
545    fn tree_walk_eval_file_propagates_io_error_kind() {
546        let e: &dyn Evaluator = &TreeWalkEvaluator;
547        let result = e.eval_file(std::path::Path::new("/nonexistent/never/exists.nix"));
548        match result {
549            Err(EvalError::IoError { context, .. }) => {
550                assert!(context.contains("eval_file"));
551            }
552            other => panic!("expected IoError, got {other:?}"),
553        }
554    }
555
556    #[test]
557    fn tree_walk_eval_file_parse_error_propagates() {
558        let dir = std::env::temp_dir().join("sui-eval-test-tw-parse");
559        let _ = std::fs::create_dir_all(&dir);
560        let path = dir.join("bad.nix");
561        std::fs::write(&path, "let in").unwrap();
562        let e: &dyn Evaluator = &TreeWalkEvaluator;
563        let result = e.eval_file(&path);
564        assert!(result.is_err());
565        let _ = std::fs::remove_file(&path);
566        let _ = std::fs::remove_dir(&dir);
567    }
568
569    // ── Mock evaluator additional ──────────────────────────
570
571    #[test]
572    fn mock_evaluator_dispatched_via_trait_object() {
573        let m: Box<dyn Evaluator> = Box::new(MockEvaluator(Ok(Value::Bool(true))));
574        let r = m.eval_expr("anything").unwrap();
575        assert_eq!(r, Value::Bool(true));
576    }
577
578    #[test]
579    fn mock_evaluator_eval_file_routes_through_eval_expr() {
580        let m = MockEvaluator(Ok(Value::Int(1)));
581        let r = m.eval_file(std::path::Path::new("/dev/null"));
582        assert_eq!(r.unwrap(), Value::Int(1));
583    }
584
585    // ── Through-trait coverage of more constructs ──────────
586
587    #[test]
588    fn tree_walk_eval_function_with_default_args() {
589        let e: &dyn Evaluator = &TreeWalkEvaluator;
590        assert_eq!(
591            e.eval_expr("({a, b ? 10}: a + b) {a = 5;}").unwrap(),
592            Value::Int(15),
593        );
594    }
595
596    #[test]
597    fn tree_walk_eval_with_throws_propagated() {
598        let e: &dyn Evaluator = &TreeWalkEvaluator;
599        let result = e.eval_expr(r#"builtins.throw "boom""#);
600        assert!(result.is_err());
601        let err = result.unwrap_err();
602        assert!(err.is_throw());
603    }
604
605    #[test]
606    fn tree_walk_eval_assert_failure() {
607        let e: &dyn Evaluator = &TreeWalkEvaluator;
608        let result = e.eval_expr("assert false; 42");
609        assert!(matches!(result, Err(EvalError::AssertionFailed(_))));
610    }
611
612    #[test]
613    fn tree_walk_eval_division_by_zero() {
614        let e: &dyn Evaluator = &TreeWalkEvaluator;
615        let result = e.eval_expr("1 / 0");
616        assert!(matches!(result, Err(EvalError::DivisionByZero)));
617    }
618
619    #[test]
620    fn tree_walk_eval_undefined_variable() {
621        let e: &dyn Evaluator = &TreeWalkEvaluator;
622        let result = e.eval_expr("nonexistent_xyz");
623        assert!(matches!(result, Err(EvalError::UndefinedVar(_))));
624    }
625
626    #[test]
627    fn tree_walk_eval_path_literal() {
628        let e: &dyn Evaluator = &TreeWalkEvaluator;
629        let v = e.eval_expr("/tmp/x").unwrap();
630        assert!(matches!(v, Value::Path(_)));
631    }
632
633    #[test]
634    fn tree_walk_eval_float_literal() {
635        let e: &dyn Evaluator = &TreeWalkEvaluator;
636        assert_eq!(e.eval_expr("3.14").unwrap(), Value::Float(3.14));
637    }
638
639    #[test]
640    fn tree_walk_eval_lambda_returns_lambda() {
641        let e: &dyn Evaluator = &TreeWalkEvaluator;
642        let v = e.eval_expr("x: x").unwrap();
643        assert!(matches!(v, Value::Lambda(_)));
644    }
645}