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