Skip to main content

sui_eval/
eval.rs

1//! Tree-walking Nix evaluator using rnix's typed AST.
2//!
3//! Implements Tvix-style lazy evaluation with thunks: let-bindings and
4//! rec-attrset values are wrapped in `Value::Thunk` and only evaluated
5//! when their value is actually needed (call-by-need with memoization).
6
7use std::cell::{Cell, RefCell};
8use std::collections::{HashSet, HashMap, VecDeque};
9use std::path::PathBuf;
10
11use rnix::ast::{self, AstToken, HasEntry, InterpolPart};
12use rowan::ast::AstNode;
13
14use crate::builtins;
15use crate::value::*;
16
17thread_local! { static EVAL_DEPTH: Cell<usize> = const { Cell::new(0) }; }
18
19
20// ── Source ID for identifier symbol cache ─────────────────────
21//
22// Each call to `rnix::Root::parse` produces a distinct AST tree.
23// Identifiers from different trees may share the same byte offset,
24// so we pair offset with a source ID to form a unique cache key.
25// The ID is stored in a thread-local so `eval_expr` can access it
26// without an extra parameter threaded through every call.
27
28thread_local! {
29    static CURRENT_SOURCE_ID: Cell<u32> = const { Cell::new(0) };
30}
31
32// ── Currently-evaluating-file stack ────────────────────────────
33//
34// Real Nix resolves relative path literals (`./foo.nix`) against the
35// directory of the file that *contains* the literal, not against the
36// process cwd. Track the stack of files we're currently evaluating
37// so the `PathRel` handler and `import` builtin can resolve correctly.
38
39thread_local! {
40    static EVAL_FILE_STACK: RefCell<Vec<PathBuf>> = const { RefCell::new(Vec::new()) };
41    /// Nix-level error context stack — captures source positions for --show-trace.
42    /// Each entry: (file, expression_snippet). Pushed on function calls, select,
43    /// force, and popped on return. Attached to errors for structured diagnostics.
44    static NIX_TRACE_STACK: RefCell<Vec<NixTraceFrame>> = const { RefCell::new(Vec::new()) };
45}
46
47/// A single frame in the Nix-level error trace.
48///
49/// The frame is only ever *observed* on the cold error path (via
50/// `attach_trace`). To keep the hot lambda-call path allocation-free,
51/// the per-call lambda frame stores the raw ingredients (a cheap
52/// `Rc`-clone of the closure env + the raw current-eval-file `PathBuf`)
53/// and defers the `format!` / path-strip work into `attach_trace`. The
54/// rendered `(description, file)` pair is byte-identical to the eager
55/// form either way (see the `description()` / `file()` accessors).
56#[derive(Debug, Clone)]
57pub enum NixTraceFrame {
58    /// Pre-formatted frame (the builtin-call path — kept eager because
59    /// the builtin name is already a `&'static str`, so there is no
60    /// per-call heap-`String` to defer).
61    Eager {
62        file: Option<String>,
63        description: String,
64    },
65    /// Lazy per-lambda-call frame. The `description` string and the
66    /// stripped `file` string are built on demand in `attach_trace`.
67    ///
68    /// - `closure_env` provides the *description*'s file (from
69    ///   `closure.env.eval_file()`) — an O(1) `Rc` refcount bump.
70    /// - `current_file` is the raw `current_eval_file()` snapshot taken
71    ///   at push time (the stack top after the file guard pushed the
72    ///   closure's file), used verbatim for the frame's `file` field so
73    ///   the rendered `loc` matches the eager form byte-for-byte.
74    Lambda {
75        closure_env: Env,
76        current_file: Option<PathBuf>,
77    },
78}
79
80/// Strip the `-source/` store-path prefix from a rendered path exactly
81/// as the eager trace path did (`p.display()...rsplit_once("-source/")`).
82fn strip_source_prefix(p: &std::path::Path) -> String {
83    let s = p.display().to_string();
84    s.rsplit_once("-source/")
85        .map_or_else(|| p.display().to_string(), |(_, tail)| tail.to_string())
86}
87
88impl NixTraceFrame {
89    /// The frame's `file` field (for the trace `loc`), matching the
90    /// eager `frame.file` byte-for-byte.
91    fn file(&self) -> Option<String> {
92        match self {
93            NixTraceFrame::Eager { file, .. } => file.clone(),
94            NixTraceFrame::Lambda { current_file, .. } => {
95                current_file.as_deref().map(strip_source_prefix)
96            }
97        }
98    }
99
100    /// The frame's `description`, matching the eager `frame.description`
101    /// byte-for-byte. Rendered through the `Display` impl (a `write!`
102    /// surface — the description is the frame's canonical serialization,
103    /// per the fleet TYPED-EMISSION rule; no `format!()`).
104    fn description(&self) -> String {
105        self.to_string()
106    }
107}
108
109/// The frame's rendered description IS its `Display` — the typed emission
110/// surface for the trace message (`write!`, never `format!()`). The
111/// `Lambda` arm defers the path-strip to this cold error-path render.
112impl std::fmt::Display for NixTraceFrame {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        match self {
115            NixTraceFrame::Eager { description, .. } => f.write_str(description),
116            NixTraceFrame::Lambda { closure_env, .. } => {
117                let file = closure_env.eval_file().map(|p| strip_source_prefix(p));
118                write!(
119                    f,
120                    "while calling function defined in {}",
121                    file.as_deref().unwrap_or("<eval>")
122                )
123            }
124        }
125    }
126}
127
128/// Push a Nix-level trace frame. Returns a guard that pops on drop.
129fn push_nix_trace(desc: impl Into<String>) -> NixTraceGuard {
130    let frame = NixTraceFrame::Eager {
131        file: current_eval_file().map(|p| {
132            p.display().to_string()
133                .rsplit_once("-source/")
134                .map_or_else(|| p.display().to_string(), |(_, s)| s.to_string())
135        }),
136        description: desc.into(),
137    };
138    NIX_TRACE_STACK.with(|s| s.borrow_mut().push(frame));
139    NixTraceGuard
140}
141
142/// Push a *lazy* Nix-level trace frame for a lambda call. Stores only the
143/// raw ingredients (an O(1) `Rc`-clone of the closure env + the raw
144/// `current_eval_file()` snapshot) — the `format!`/path-strip work is
145/// deferred to the cold `attach_trace` path. Returns a guard that pops on
146/// drop. The rendered frame is byte-identical to the eager form.
147fn push_nix_trace_lambda(closure_env: &Env) -> NixTraceGuard {
148    let frame = NixTraceFrame::Lambda {
149        closure_env: closure_env.clone(),
150        current_file: current_eval_file(),
151    };
152    NIX_TRACE_STACK.with(|s| s.borrow_mut().push(frame));
153    NixTraceGuard
154}
155
156struct NixTraceGuard;
157impl Drop for NixTraceGuard {
158    fn drop(&mut self) {
159        NIX_TRACE_STACK.with(|s| s.borrow_mut().pop());
160    }
161}
162
163/// Capture the current Nix trace and attach it to an error.
164pub fn attach_trace(err: EvalError) -> EvalError {
165    NIX_TRACE_STACK.with(|s| {
166        let stack = s.borrow();
167        if stack.is_empty() {
168            return err;
169        }
170        let max_frames = std::env::var("SUI_M26_MAXFRAMES").ok()
171            .and_then(|s| s.parse::<usize>().ok()).unwrap_or(15);
172        let mut trace = format!("{err}");
173        for (i, frame) in stack.iter().rev().take(max_frames).enumerate() {
174            let file = frame.file();
175            let loc = file.as_deref().unwrap_or("<eval>");
176            trace.push_str(&format!("\n  {} ({loc})", frame.description()));
177            if i + 1 >= max_frames && stack.len() > max_frames {
178                trace.push_str(&format!("\n  ... ({} more frames)", stack.len() - max_frames));
179            }
180        }
181        // CRITICAL: preserve Throw/AssertionFailed variants so tryEval can catch them.
182        // Converting to TypeError would make tryEval miss them.
183        match err {
184            EvalError::Throw(_) => EvalError::Throw(trace),
185            EvalError::AssertionFailed(_) => EvalError::AssertionFailed(trace),
186            _ => EvalError::TypeError(trace),
187        }
188    })
189}
190
191/// Return the directory of the file currently being evaluated, if any.
192/// Used by the `PathRel` AST handler to resolve relative path literals.
193#[must_use]
194pub fn current_eval_dir() -> Option<PathBuf> {
195    EVAL_FILE_STACK.with(|s| s.borrow().last().and_then(|p| p.parent().map(PathBuf::from)))
196}
197
198/// Push a file onto the eval stack. Returns an RAII guard that pops
199/// it on drop. Use when entering an `import <file>` so subsequent
200/// relative path literals resolve against the right directory.
201pub fn push_eval_file(file: PathBuf) -> EvalFileGuard {
202    EVAL_FILE_STACK.with(|s| s.borrow_mut().push(file));
203    EvalFileGuard
204}
205
206/// Return the file currently being evaluated, if any.
207/// Used by error sites to attach source location context.
208#[must_use]
209pub fn current_eval_file() -> Option<PathBuf> {
210    EVAL_FILE_STACK.with(|s| s.borrow().last().cloned())
211}
212
213
214/// Snapshot the entire eval file stack (debug).
215pub fn eval_file_stack_snapshot() -> Vec<String> {
216    EVAL_FILE_STACK.with(|s| {
217        s.borrow().iter().map(|p| {
218            let s = p.display().to_string();
219            s.rsplit_once("-source/").map_or(s.clone(), |(_, r)| r.to_string())
220        }).collect()
221    })
222}
223
224/// Format the current eval file for error context strings.
225/// Returns e.g. `", in '/nix/store/.../default.nix'"` or empty string.
226pub(crate) fn eval_file_ctx() -> String {
227    current_eval_file()
228        .map(|p| format!(", in '{}'", p.display()))
229        .unwrap_or_default()
230}
231
232/// RAII guard that pops the top of the eval-file stack on drop.
233pub struct EvalFileGuard;
234
235impl Drop for EvalFileGuard {
236    fn drop(&mut self) {
237        EVAL_FILE_STACK.with(|s| {
238            s.borrow_mut().pop();
239        });
240    }
241}
242
243/// Set `CURRENT_SOURCE_ID` to `id`, returning an RAII guard that restores
244/// the previous id on drop. Used at thunk force so a cross-file thunk's
245/// idents key the `(source_id, offset)` symbol cache against the file where
246/// the thunk was DEFINED, not the ambient source at force time — the sibling
247/// of the eval-file guard, closing the `parse.nix` cross-file collision.
248pub fn push_source_id(id: u32) -> SourceIdGuard {
249    let prev = CURRENT_SOURCE_ID.with(|s| {
250        let old = s.get();
251        s.set(id);
252        old
253    });
254    SourceIdGuard(prev)
255}
256
257/// RAII guard that restores the previous `CURRENT_SOURCE_ID` on drop.
258pub struct SourceIdGuard(u32);
259
260impl Drop for SourceIdGuard {
261    fn drop(&mut self) {
262        CURRENT_SOURCE_ID.with(|s| s.set(self.0));
263    }
264}
265
266// ── Path normalization ────────────────────────────────────────
267//
268// Normalize a path by removing `.` components and resolving `..`
269// components.  Unlike `canonicalize()`, this doesn't require the
270// path to exist on disk — critical for flake evaluation where
271// files may not be materialized yet.
272
273/// Normalize a path by removing `.` and resolving `..` components
274/// without touching the filesystem.
275///
276/// Delegates to [`crate::path::normalize`] — kept as a public re-export
277/// so existing call-sites continue to compile without changes.
278pub fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
279    crate::path::normalize(path)
280}
281
282// ── Pure (hermetic) evaluation mode ────────────────────────────
283//
284// When pure mode is enabled, impure builtins (`storePath`, `fetchurl`/`fetchTarball`
285// without an explicit hash, `currentTime`, `getEnv`, etc.) should refuse to
286// produce non-deterministic results. The flag is thread-local so each evaluator
287// thread can opt in independently.
288
289thread_local! {
290    static PURE_MODE: Cell<bool> = const { Cell::new(false) };
291}
292
293/// Enable or disable hermetic (pure) evaluation mode for the current thread.
294pub fn set_pure_mode(pure: bool) {
295    PURE_MODE.with(|p| p.set(pure));
296}
297
298/// Whether the current thread is in hermetic (pure) evaluation mode.
299#[must_use]
300pub fn is_pure_mode() -> bool {
301    PURE_MODE.with(Cell::get)
302}
303
304/// Maximum evaluation depth before we report infinite recursion.
305///
306/// With `stacker` dynamically growing the call stack, we are no longer
307/// limited by the default 8 MB thread stack.
308///
309/// **Test builds** keep a low limit (2 048) so that infinite-recursion
310/// tests fail quickly instead of spinning for minutes.
311///
312/// **Non-test builds** disable the depth guard entirely (`usize::MAX`).
313/// nixpkgs uses deeply nested fixpoints (50+ overlay applications, each
314/// creating cascading chains of millions of `eval_expr` calls when
315/// attributes are forced). CppNix has no explicit depth limit — it
316/// relies on the OS stack, which `stacker` now emulates for us. True
317/// infinite recursion is caught by the thunk blackhole detector in
318/// `Thunk::force`, not by this counter.
319#[cfg(test)]
320const MAX_EVAL_DEPTH: usize = 2_048;
321#[cfg(not(test))]
322const MAX_EVAL_DEPTH: usize = usize::MAX;
323
324/// Lightweight depth guard.
325///
326/// In non-test builds where `MAX_EVAL_DEPTH == usize::MAX`, the guard
327/// is effectively a no-op (the overflow check never fires). The
328/// compiler should be able to elide most of the overhead.
329struct DepthGuard;
330
331/// Release-active runaway backstop for the overlay-fixpoint promotion.
332///
333/// Release builds set `MAX_EVAL_DEPTH = usize::MAX` (no eval-depth guard)
334/// so nixpkgs' legitimately-deep fixpoints evaluate.  But a promoted
335/// empty-attrs partial that corrupts a downstream `makeOverridable` /
336/// `commonAttrs` fixpoint (the cross-system Darwin `apple-sdk` path `hello`
337/// hits under `builtins.currentSystem = macOS`) recurses through
338/// `eval_expr` without bound — and that recursion does NOT climb the force
339/// stack, so only an `eval_expr`-level bound catches it before the OS stack
340/// aborts.  Armed ONLY once a promotion has fired (`promotion_occurred()`),
341/// so ordinary deep evaluation (never after a promotion) is untouched.  The
342/// converging native-system fixpoint (`libxcrypt`) peaks well under this
343/// bound and is unaffected; the non-converging cross-system runaway is
344/// caught here, converting a hard native-stack abort into a recoverable
345/// `InfiniteRecursion` that `x.y or default` recovers exactly like nix
346/// (`hello` returns to a clean value-diverge instead of aborting).
347const PROMOTION_RUNAWAY_EVAL_DEPTH: usize = 500;
348
349impl DepthGuard {
350    #[inline(always)]
351    fn enter() -> Result<Self, EvalError> {
352        EVAL_DEPTH.with(|d| {
353            let depth = d.get();
354            if MAX_EVAL_DEPTH != usize::MAX && depth > MAX_EVAL_DEPTH {
355                return Err(EvalError::InfiniteRecursion(
356                    "eval depth exceeded".into(),
357                ));
358            }
359            if depth > PROMOTION_RUNAWAY_EVAL_DEPTH
360                && crate::value::promotion_occurred()
361            {
362                return Err(EvalError::InfiniteRecursion(
363                    "overlay-fixpoint promotion runaway (eval depth exceeded)".into(),
364                ));
365            }
366            d.set(depth + 1);
367            Ok(DepthGuard)
368        })
369    }
370}
371
372impl Drop for DepthGuard {
373    #[inline(always)]
374    fn drop(&mut self) {
375        EVAL_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
376    }
377}
378
379/// Collect ALL identifier names referenced in an AST expression.
380///
381/// Walks the full expression tree (including inside `with` bodies)
382/// and collects every `Ident` node. This is an OVER-APPROXIMATION:
383/// it includes shadowed names and names inside `with` bodies.
384///
385/// Over-approximation is SAFE for dead binding elimination — we may
386/// keep a binding that's unused (waste) but never skip a binding
387/// that IS used (correctness).
388///
389/// Previous versions bailed out on `with` expressions, disabling
390/// dead binding elimination entirely. The fix: collect idents even
391/// inside `with` bodies. If a binding name doesn't appear as ANY
392/// identifier ANYWHERE in the expression, it's provably dead
393/// regardless of `with` scopes — `with` makes names from the
394/// namespace reachable, not names from the enclosing let-scope.
395fn collect_referenced_names(expr: &ast::Expr) -> HashSet<String> {
396    let mut names = HashSet::new();
397    for node in expr.syntax().descendants() {
398        if let Some(ident) = ast::Ident::cast(node) {
399            names.insert(ident_text(&ident));
400        }
401    }
402    names
403}
404
405/// Compute the set of binding names that are transitively needed
406/// by the body expression in a recursive scope (let-in or rec attrset).
407///
408/// Algorithm:
409/// 1. Collect all ident references from the body → root set
410/// 2. Collect all ident references from each binding's value expression
411/// 3. BFS from root set through binding dependencies
412/// 4. Return the set of reachable binding names
413///
414/// Bindings NOT in the returned set are provably dead and can be skipped.
415/// This is correct even for recursive scopes because the BFS follows
416/// transitive dependencies: if A is needed and A references B, then B
417/// is added to the needed set.
418fn compute_needed_bindings(
419    body: &ast::Expr,
420    binding_info: &[(String, Option<ast::Expr>)], // (name, value_expr) — None for plain inherit
421) -> HashSet<String> {
422    // Step 1: Collect idents from the body
423    let body_refs = collect_referenced_names(body);
424
425    // Build the set of all binding names and their dependencies
426    let mut all_names: HashSet<String> = HashSet::with_capacity(binding_info.len());
427    let mut deps: HashMap<String, HashSet<String>> = HashMap::with_capacity(binding_info.len());
428
429    for (name, value_expr) in binding_info {
430        all_names.insert(name.clone());
431        if let Some(expr) = value_expr {
432            deps.insert(name.clone(), collect_referenced_names(expr));
433        }
434    }
435
436    // Step 2: BFS from body refs through binding dependencies
437    let mut needed: HashSet<String> = body_refs.intersection(&all_names).cloned().collect();
438    let mut queue: VecDeque<String> = needed.iter().cloned().collect();
439
440    while let Some(name) = queue.pop_front() {
441        if let Some(name_deps) = deps.get(&name) {
442            for dep in name_deps {
443                if all_names.contains(dep) && needed.insert(dep.clone()) {
444                    queue.push_back(dep.clone());
445                }
446            }
447        }
448    }
449
450    needed
451}
452
453/// Evaluate a Nix expression string.
454#[must_use = "evaluation result should be used"]
455pub fn eval(input: &str) -> Result<Value, EvalError> {
456    eval_with_file(input, None)
457}
458
459// Whether we are inside a top-level eval (used to avoid nested perf reports).
460thread_local! {
461    static EVAL_NESTING: Cell<usize> = const { Cell::new(0) };
462}
463
464/// Evaluate a Nix expression string, optionally tagged with the
465/// path of the source file. The file is stored on the root `Env`
466/// so that any closure created during evaluation captures it and
467/// can resolve relative path literals (`./foo.nix`) in function
468/// defaults that fire after control has left the file's scope.
469
470pub fn eval_with_file(input: &str, file: Option<std::path::PathBuf>) -> Result<Value, EvalError> {
471    let nesting = EVAL_NESTING.with(|n| {
472        let v = n.get();
473        n.set(v + 1);
474        v
475    });
476    if nesting == 0 {
477        crate::perf::init();
478        crate::perf::start();
479        crate::trace::init_trace();
480        // Clear the identifier symbol cache so that offsets from
481        // previous top-level evaluations don't persist.
482        clear_ident_cache();
483        // ENV-RESOLVE M0 (no-op unless `SUI_RESOLVE=1`): clear the per-source
484        // resolution side-table for the same reason — its `(source_id,
485        // offset)` keys must not survive across independent top-level evals.
486        crate::resolve_env::clear();
487        // SOURCE_TEXTS is deliberately NOT cleared here — it is append-only
488        // for the life of the process. Clearing it on a `nesting == 0`
489        // re-entry was a shared-mutable-cell bug: the top-level
490        // `eval_with_file` RETURNS (nesting → 0) BEFORE its caller
491        // deep-forces the result (e.g. `value.to_json()` at the CLI), and
492        // that deep force triggers lazy `import`s which re-enter
493        // `eval_with_file` at nesting == 0 — so clearing here wiped every
494        // registered file's text mid-force. Any `unsafeGetAttrPos` resolved
495        // after the first deep-force import then failed its `text_for()`
496        // existence check and returned null (the cid `options.json` attrTag
497        // `declarations = []` divergence). SOURCE_TEXTS is keyed by canonical
498        // path and `register_source` stores each path's text only once
499        // (identical on re-parse), so append-only is correct — a path always
500        // maps to its own text — and matches CppNix, which never clears its
501        // source registry. The only cost is bounded growth within one process
502        // (a non-issue for a per-invocation CLI). Removing the clearable cell
503        // makes the whole "absent/wrong source text at resolve time" class
504        // unrepresentable rather than merely guarded.
505    }
506    let parse = rnix::Root::parse(input);
507    if !parse.errors().is_empty() {
508        let msgs: Vec<String> = parse.errors().iter().map(|e| e.to_string()).collect();
509        EVAL_NESTING.with(|n| n.set(n.get().saturating_sub(1)));
510        return Err(EvalError::ParseError(msgs.join("; ")));
511    }
512
513    // Each parse tree gets a unique source ID so that identifiers
514    // at the same byte offset in different files don't collide in
515    // the symbol cache.
516    let src_id = next_source_id();
517    // ENV-RESOLVE M0 (no-op unless `SUI_RESOLVE=1`): run the parse-time
518    // variable resolver over THIS parse tree and merge its `Lexical`
519    // resolutions into the per-source table under `src_id`. Pure + fail-safe
520    // (any uncertainty is left `Dynamic`), so the eval below is byte-identical
521    // — the `Lexical` fast path only shortcuts a lexical-bindings hit, which
522    // `lookup_fast` returns first anyway.
523    if crate::resolve_env::enabled() {
524        let table = sui_resolve::resolve(&parse.tree());
525        crate::resolve_env::populate(src_id, &table);
526    }
527    // Register this parse tree's file + text so a static key's byte offset
528    // (recorded by `eval_attrset`) resolves to a file/line/column for
529    // `builtins.unsafeGetAttrPos`. The file flows through the eval-file
530    // stack (store-path prefixed for imported inputs); the position resolver
531    // lifts a cache-dir path to its `/nix/store/<h>-source` store path.
532    crate::pos::register_source(file.as_deref(), input);
533    let prev_src_id = CURRENT_SOURCE_ID.with(|s| {
534        let old = s.get();
535        s.set(src_id);
536        old
537    });
538
539    let root = parse.tree();
540    let expr = match root.expr() {
541        Some(e) => e,
542        None => {
543            CURRENT_SOURCE_ID.with(|s| s.set(prev_src_id));
544            EVAL_NESTING.with(|n| n.set(n.get().saturating_sub(1)));
545            return Err(EvalError::ParseError("empty expression".to_string()));
546        }
547    };
548    let mut env = Env::new();
549    env.set_eval_file(file);
550    // Tag the env with THIS parse tree's source_id so a thunk created here
551    // and forced later (cross-file) restores this id on force (see the
552    // source-id guard in `Thunk::force`), keying `IDENT_CACHE` against the
553    // file where the thunk was defined.
554    env.set_source_id(src_id);
555    builtins::register(&mut env);
556    let result = eval_expr(&expr, &env).map_err(|e| attach_trace(e))?;
557    // Force the top-level result so callers always see a concrete value.
558    let final_result = force_value(&result).map_err(|e| attach_trace(e));
559    // Restore the previous source ID (matters for nested imports).
560    CURRENT_SOURCE_ID.with(|s| s.set(prev_src_id));
561    EVAL_NESTING.with(|n| n.set(n.get().saturating_sub(1)));
562    if nesting == 0 {
563        crate::perf::report();
564    }
565    final_result
566}
567
568/// Force a value: if it is a thunk, evaluate and memoize the result.
569/// Concrete values are returned unchanged.
570/// Force a value: if it is a thunk, evaluate and memoize the result.
571/// Concrete values are returned unchanged.
572///
573/// Inlined aggressively so the non-thunk fast path compiles to a
574/// simple clone without a function-call boundary.
575#[inline(always)]
576/// Force a value and return a type-safe `Concrete` (guaranteed non-Thunk).
577///
578/// This is the preferred forcing API. The `Concrete` return type makes it
579/// impossible to accidentally use an unforced thunk — the compiler rejects it.
580pub fn force_concrete(value: &Value) -> Result<Concrete, EvalError> {
581    value.demand()
582}
583
584/// Force a value (legacy API — returns `Value` for backward compatibility).
585///
586/// Prefer `force_concrete()` or `Value::demand()` for new code.
587pub fn force_value(value: &Value) -> Result<Value, EvalError> {
588    crate::perf::inc(crate::perf::Counter::ForceValue);
589    // Fast path: non-thunk values are returned immediately (no clone needed
590    // until we actually have work to do).
591    if !matches!(value, Value::Thunk(_)) {
592        return Ok(value.clone());
593    }
594    // Slow path: chase thunk chains.
595    //
596    // A legitimate chain is typically 1–3 links deep (result of lazy
597    // evaluation wrapping an intermediate value in another thunk).
598    // Reaching 100 means either (a) a self-referential cycle like
599    // `let x = x; in x` that bypassed per-thunk Blackhole detection,
600    // or (b) pathological Thunk(Thunk(...)) nesting. Both are errors.
601    //
602    // Previous behavior silently returned `Ok(last_thunk)` at depth
603    // 100, which hid infinite-recursion bugs — the blackhole tests
604    // in the lib suite failed because `result.is_ok()` instead of
605    // `is_err()`. Returning `Err` here makes the silent-bail visible
606    // at the CppNix-compatible call site (real Nix raises "infinite
607    // recursion encountered").
608    let mut v = value.clone();
609    let mut depth = 0u32;
610    loop {
611        match v {
612            Value::Thunk(ref thunk) => {
613                v = force_thunk(thunk)?;
614                depth += 1;
615                if depth > 100 {
616                    return Err(EvalError::InfiniteRecursion(
617                        "force_value: thunk chain exceeded depth 100 (cycle or runaway lazy wrap)".into(),
618                    ));
619                }
620            }
621            _ => return Ok(v),
622        }
623    }
624}
625
626/// Force with call-site tracking (legacy API).
627pub fn force_value_tracked(value: &Value, site: &str) -> Result<Value, EvalError> {
628    crate::perf::inc(crate::perf::Counter::ForceValue);
629    if let Value::Thunk(thunk) = value {
630        FORCE_SITES.with(|sites| {
631            *sites.borrow_mut().entry(site.to_string()).or_insert(0) += 1;
632        });
633        force_thunk(thunk)
634    } else {
635        Ok(value.clone())
636    }
637}
638
639thread_local! {
640    static FORCE_SITES: std::cell::RefCell<std::collections::HashMap<String, u64>> =
641        std::cell::RefCell::new(std::collections::HashMap::new());
642    static APPLY_SITES: std::cell::RefCell<std::collections::HashMap<String, u64>> =
643        std::cell::RefCell::new(std::collections::HashMap::new());
644}
645
646/// Dump force-site counters (call from perf reporting).
647pub fn dump_force_sites() {
648    FORCE_SITES.with(|sites| {
649        let sites = sites.borrow();
650        let mut sorted: Vec<_> = sites.iter().collect();
651        sorted.sort_by(|a, b| b.1.cmp(a.1));
652        eprintln!("[force-sites] top thunk force call sites:");
653        for (site, count) in sorted.iter().take(10) {
654            eprintln!("  {count:>8} {site}");
655        }
656    });
657    APPLY_SITES.with(|sites| {
658        let sites = sites.borrow();
659        let mut sorted: Vec<_> = sites.iter().collect();
660        sorted.sort_by(|a, b| b.1.cmp(a.1));
661        eprintln!("[apply-sites] top lambda call sites by source file:");
662        for (site, count) in sorted.iter().take(15) {
663            // Strip nix store prefix for readability
664            let short = site.rsplit_once("-source/").map_or(site.as_str(), |(_,s)| s);
665            eprintln!("  {count:>8} {short}");
666        }
667    });
668}
669
670/// Force a thunk — split out from [`force_value`] so the fast path
671/// (non-thunk clone) stays fully inlined while this cold path can
672/// be a regular function call with stacker protection.
673fn force_thunk(thunk: &Thunk) -> Result<Value, EvalError> {
674    // Ultra-fast path: if the thunk is already cached, skip stacker overhead.
675    if let Some(cached) = thunk.peek() {
676        crate::perf::inc(crate::perf::Counter::ThunkHit);
677        return Ok(cached.clone().into_value());
678    }
679    stacker::maybe_grow(64 * 1024, 2 * 1024 * 1024, || {
680        // Force ONE level only — matches CppNix's forceValue which does
681        // not transitively chase thunk-in-thunk chains. The caller will
682        // force again when the value is actually needed. This is the key
683        // optimization: CppNix forces 71 thunks for lib.version while
684        // sui was forcing 180K due to transitive forcing.
685        thunk.force(&|expr, env| eval_expr(expr, env))
686    })
687}
688
689/// Decide whether to thunk an expression or evaluate it directly.
690///
691/// Trivial expressions (literals, paths) are evaluated immediately --
692/// no thunk allocation. For non-recursive scopes, variable lookups
693/// (Ident) and lambdas are also evaluated eagerly. This matches
694/// CppNix's `maybeThunk` optimization which avoids a large fraction
695/// of thunk creations on nixpkgs.
696///
697/// For recursive scopes (let-in, rec attrsets), set `is_rec = true` to
698/// prevent eager evaluation of `Ident` and `Lambda` expressions:
699/// - Ident: sibling bindings may not be defined yet (forward refs).
700/// - Lambda: the closure must capture the *final* env (set in Phase 2)
701///   so that the lambda body can reference sibling bindings.
702///
703/// `defined_so_far`: In recursive scopes, names that have already been
704/// bound in this scope (i.e. earlier bindings). Idents referencing these
705/// are backward references and can be resolved directly without thunking.
706/// Forward references (names not yet defined) must still be thunked.
707/// Detect whether `value_expr`'s source structurally references
708/// the identifier `name` — the signal that this let-binding is a
709/// self-recursive fix-point (`let x = f x; in x` or
710/// `let x = { a = 1; b = x.a; }; in x`).  Used at let-binding
711/// thunking time to pick `Thunk::new_suspended_recursive` over the
712/// classic `Thunk::new_suspended`, so inner re-entrance during
713/// force returns the partial value via `ThunkRepr::Promise`
714/// instead of erroring with `InfiniteRecursion`.
715///
716/// Implementation walks the value-expr's rnix syntax tree looking
717/// for `TOKEN_IDENT` whose text equals `name`.  This is a
718/// conservative over-approximation:
719/// - shadowing (e.g. `let x = let x = 1; in x; in x`) marks the
720///   outer thunk recursive even though no real cycle exists;
721/// - the resulting Promise behaviour is a strict superset of
722///   Blackhole for non-cyclic forces (the body runs to completion
723///   and the cell gets the final value), so false positives are
724///   semantically safe — they cost only the extra `Rc<RefCell>`
725///   allocation per recursive let-binding.
726///
727/// False negatives (e.g. the bound name appears only inside an
728/// inherit-from-source clause) leave the existing
729/// `InfiniteRecursion` behaviour intact, which is the conservative
730/// fallback.
731/// The set of variable-reference ident names in `value_expr`'s subtree
732/// (`NODE_IDENT` whose parent is NOT a `NODE_ATTRPATH` — i.e. genuine
733/// variable references, not attribute names/keys). ONE subtree walk.
734///
735/// Kills the O(N²) re-walk storm (Storm A) at the call sites: previously
736/// `is_self_recursive_binding` did a full subtree walk once per
737/// `(binding × sibling-name)` in every `let`/`rec` scope; now each RHS is
738/// walked ONCE to build this set, then every name is an O(1) set lookup.
739/// Byte-neutral: the recursion verdict is unchanged (a name is self/mutually
740/// recursive iff it is in the set).
741///
742/// NOT cross-call memoized: a process-lifetime memo keyed on ephemeral AST
743/// node identity `(source-id, range)` collides when nodes are parsed/dropped
744/// without a per-eval clear (the standalone-predicate case). The call-site
745/// single-walk is the byte-safe win; `ContentMemo` (sui-intern) is reserved
746/// for sites with a STABLE content key (the NAR-hash memo's `(dir,name)`, the
747/// overlay-flatten per-node cache).
748///
749/// The attrpath exclusion matters: without it, `placeholder = if
750/// lhs.placeholder == …` in nixpkgs `lib/types.nix` would be falsely flagged
751/// self-recursive (its RHS mentions the *attribute* `.placeholder`), routing
752/// the binding through the `Promise` fix-point path whose env handling drops
753/// the let-scope — surfacing as a force-order-dependent `null` in the module
754/// system (`concatLists: expected list, got null`).
755fn referenced_idents(value_expr: &ast::Expr) -> HashSet<SmolStr> {
756    use rnix::SyntaxKind;
757    // Storm A instrumentation (byte-neutral, gated on perf::enabled()): count
758    // this walk + the rnix descendants it visits + its walltime, so the
759    // residual per-fixpoint-iteration self/mutual-recursion detection cost is
760    // VISIBLE in the SUI_EVAL_PERF report — symmetric with sorted_entries /
761    // overlay-flatten. The counter reads add zero output-relevant work.
762    let perf_on = crate::perf::enabled();
763    let t0 = if perf_on {
764        Some(std::time::Instant::now())
765    } else {
766        None
767    };
768    crate::perf::inc(crate::perf::Counter::SelfRecWalkCalls);
769    let mut nodes_walked: u64 = 0;
770    let mut set: HashSet<SmolStr> = HashSet::new();
771    for node in value_expr.syntax().descendants() {
772        nodes_walked += 1;
773        if node.kind() == SyntaxKind::NODE_IDENT
774            && node
775                .parent()
776                .is_none_or(|p| p.kind() != SyntaxKind::NODE_ATTRPATH)
777            && let Some(i) = ast::Ident::cast(node)
778        {
779            set.insert(SmolStr::from(ident_text(&i).as_str()));
780        }
781    }
782    crate::perf::add(crate::perf::Counter::SelfRecWalkNodes, nodes_walked);
783    if let Some(t0) = t0 {
784        crate::trace::add_self_rec_walk_nanos(t0.elapsed().as_nanos());
785    }
786    set
787}
788
789/// True iff `value_expr` references `name` as a variable. Now a set lookup
790/// over one subtree walk (see `referenced_idents`). Byte-neutral vs the prior
791/// per-name-walk implementation.
792fn is_self_recursive_binding(value_expr: &ast::Expr, name: &str) -> bool {
793    referenced_idents(value_expr).contains(name)
794}
795
796fn maybe_thunk(
797    expr: &ast::Expr,
798    env: &Env,
799    is_rec: bool,
800    defined_so_far: Option<&HashSet<String>>,
801) -> Value {
802    match expr {
803        // Literals: evaluate directly (no allocation needed).
804        ast::Expr::Literal(lit) => eval_literal(lit).unwrap_or_else(|_| {
805            Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
806        }),
807        // Ident resolution: try full lookup (lexical + with-scope cache + force).
808        // On successful lookup → return value directly (most common case).
809        // On blackhole (fixpoint being constructed) → env.lookup returns None
810        // → create WithIdent thunk for deferred O(1) cache-based resolution.
811        // This approach: (1) is fast for resolved with-scopes (no thunk overhead),
812        // (2) handles blackhole fixpoints correctly via WithIdent deferral.
813        ast::Expr::Ident(ident) if !is_rec => {
814            // Cache the interned Symbol by (source_id, text_offset) — same
815            // zero-alloc steady-state path as the strict Ident arm in
816            // `eval_expr`. The ident text is materialized only on the
817            // once-per-offset cold miss and on the (rare) blackhole deferral.
818            // Same cross-file aliasing fix as the strict `eval_expr` Ident arm —
819            // key on the env's source id, not the unmaintained thread-local.
820            // This twin had NO stale-symbol guard at all (the one commit
821            // 2d93e77 added sits only on the strict arm's lookup-MISS path,
822            // after the keyword check), so it was the more exposed of the two.
823            let sym = {
824                let src_id = env.source_id();
825                let offset = u32::from(ident.syntax().text_range().start());
826                crate::value::intern_cached_with(src_id, offset, || {
827                    crate::value::intern(&ident_text(ident))
828                })
829            };
830            // Zero-copy keyword check on the resolved Symbol.
831            if let Some(kw) = crate::value::with_resolved(sym, |s| match s {
832                "true" => Some(Value::Bool(true)),
833                "false" => Some(Value::Bool(false)),
834                "null" => Some(Value::Null),
835                _ => None,
836            }) {
837                return kw;
838            }
839            {
840                {
841                    // `name` arg to `lookup_fast` is unused (lookup is by
842                    // Symbol) — pass "" to skip materializing the ident text on
843                    // the hot HIT path.
844                    if let Some(v) = env.lookup_fast(sym, "") {
845                        return v;
846                    }
847                    // Failed — either blackhole or missing. Create WithIdent
848                    // thunk for deferred resolution (only for the blackhole case).
849                    if let Some((scope_cache, scope_value)) = env.innermost_with_scope() {
850                        return Value::Thunk(Thunk::new_with_ident(
851                            SmolStr::from(ident_text(ident).as_str()),
852                            scope_cache,
853                            scope_value,
854                            env.clone(),
855                        ));
856                    }
857                    crate::perf::inc(crate::perf::Counter::ThunkSiteMaybeIdent);
858                    Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
859                }
860            }
861        }
862        // Identifiers in rec scope: check if it's a backward reference
863        // (name already defined earlier in the same scope). If so, we
864        // can resolve it directly instead of creating a wasteful thunk.
865        ast::Expr::Ident(ident) if is_rec => {
866            let name = ident_text(ident);
867            match name.as_str() {
868                "true" => Value::Bool(true),
869                "false" => Value::Bool(false),
870                "null" => Value::Null,
871                _ => {
872                    // If this name was already defined earlier in the
873                    // scope, it's a backward reference — resolve directly.
874                    if defined_so_far.map_or(false, |d| d.contains(&name)) {
875                        env.lookup(&name).unwrap_or_else(|| {
876                            crate::perf::inc(crate::perf::Counter::ThunkSiteMaybeIdent);
877                            Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
878                        })
879                    } else {
880                        // Forward reference — must thunk
881                        crate::perf::inc(crate::perf::Counter::ThunkSiteMaybeIdent);
882                        Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
883                    }
884                }
885            }
886        }
887        // Absolute and home paths: trivial text extraction — but ONLY
888        // for the non-interpolated case. An interpolated path (`/a/${e}`,
889        // `~/${e}`) must be thunked so its `${…}` parts are evaluated in
890        // `eval_expr_inner`, never spliced as literal text.
891        ast::Expr::PathAbs(p) if !parts_have_interpolation(&p.parts()) => {
892            // CppNix canonicalizes every absolute path literal on eval
893            // (`/.` → `/`, `/a/./b` → `/a/b`, `/a/../b` → `/b`, `..`
894            // clamped at root). A path VALUE carries the canonical form —
895            // the marquee cid root threw in `lib.path.hasStorePathPrefix`
896            // precisely because sui kept the raw `/.` text.
897            let text = crate::path::canon_abs(&p.syntax().text().to_string());
898            Value::Path(Box::new(SmolStr::from(text.as_str())))
899        }
900        ast::Expr::PathHome(p) if !parts_have_interpolation(&p.parts()) => {
901            let text = p.syntax().text().to_string();
902            Value::Path(Box::new(SmolStr::from(text.as_str())))
903        }
904        // Non-interpolated string literal: a constant value with no
905        // interpolation, so `eval_str` runs no `${…}` force/coerce — it is
906        // pure, non-throwing, side-effect-free, and produces a
907        // `String(NixString::with_context(text, EMPTY))`. Evaluating it here is
908        // therefore byte-identical to forcing a suspended thunk of it (M2
909        // thunk-waste: a constant Str thunk is always pure overhead — it can
910        // never observably change eval order because it cannot throw or
911        // diverge). Only the NON-interpolated case is direct; an interpolated
912        // `"${e}"` must stay thunked so its parts force lazily in the right
913        // env/order. `eval_str` on the empty-interpolation input cannot fail,
914        // but fall back to a thunk on the (unreachable) error to preserve
915        // exact prior behavior.
916        ast::Expr::Str(st) if !str_has_interpolation(st) => {
917            eval_str(st, env).unwrap_or_else(|_| {
918                Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
919            })
920        }
921        // Lambda: capture env directly (no computation needed).
922        // But NOT in recursive scopes -- the closure must capture the
923        // final env with all sibling bindings (set in Phase 2).
924        ast::Expr::Lambda(lam) if !is_rec => {
925            if let (Some(param), Some(body)) = (lam.param(), lam.body()) {
926                Value::Lambda(Rc::new(Closure {
927                    param,
928                    body,
929                    env: env.clone(),
930                }))
931            } else {
932                Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
933            }
934        }
935        // Select on a variable: CppNix's maybeThunk evaluates these eagerly
936        // when the base is a simple ident. However, this breaks fixpoints
937        // where the base (e.g., `config`) is a thunk being computed — eagerly
938        // evaluating `config.x` during attrset construction triggers blackhole.
939        //
940        // The nixpkgs module system relies on `{ ...; default = config.x; }`
941        // being lazy. Wrap selects in thunks unconditionally.
942        // The performance cost is minimal (thunk allocation + deferred eval)
943        // and correctness is critical for fixpoint patterns.
944        // Everything else: wrap in a thunk for lazy evaluation.
945        _ => {
946            crate::perf::inc(crate::perf::Counter::ThunkSiteMaybeOther);
947            if crate::perf::enabled() {
948                let kind = match expr {
949                    ast::Expr::Select(_) => "Select",
950                    ast::Expr::Apply(_) => "Apply",
951                    ast::Expr::BinOp(_) => "BinOp",
952                    ast::Expr::IfElse(_) => "IfElse",
953                    ast::Expr::Str(_) => "Str",
954                    ast::Expr::List(_) => "List",
955                    ast::Expr::With(_) => "With",
956                    ast::Expr::Assert(_) => "Assert",
957                    ast::Expr::HasAttr(_) => "HasAttr",
958                    ast::Expr::UnaryOp(_) => "UnaryOp",
959                    ast::Expr::Paren(_) => "Paren",
960                    ast::Expr::LetIn(_) => "LetIn",
961                    ast::Expr::AttrSet(_) => "AttrSet",
962                    ast::Expr::Ident(_) => "Ident(rec)",
963                    ast::Expr::Lambda(_) => "Lambda(rec)",
964                    ast::Expr::LegacyLet(_) => "LegacyLet",
965                    ast::Expr::PathAbs(_)
966                    | ast::Expr::PathHome(_)
967                    | ast::Expr::PathRel(_)
968                    | ast::Expr::PathSearch(_) => "Path(interp)",
969                    _ => "Other",
970                };
971                crate::trace::inc_maybe_other_kind(kind);
972            }
973            Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
974        }
975    }
976}
977
978/// Evaluate an rnix expression in an environment.
979///
980/// Uses `stacker::maybe_grow` to dynamically extend the call stack when
981/// it is close to exhaustion.  This prevents stack overflow on deeply
982/// nested nixpkgs fixpoints (50+ overlay applications each creating
983/// multiple recursive `eval_expr` / `force_value` frames).
984///
985/// **Fast path:** Ident (~32% of all evals), Literal, Paren, and Root
986/// expressions don't recurse and are handled directly, skipping the
987/// `stacker::maybe_grow` overhead for ~40% of all `eval_expr` calls.
988#[inline(always)]
989pub fn eval_expr(expr: &ast::Expr, env: &Env) -> Result<Value, EvalError> {
990    // Fast path: trivial expressions that don't recurse.
991    // Skip stacker overhead for ~40% of all eval_expr calls.
992    match expr {
993        ast::Expr::Ident(ident) => {
994            crate::perf::inc(crate::perf::Counter::EvalExpr);
995            if crate::perf::enabled() {
996                crate::perf::inc(crate::perf::Counter::ExprIdent);
997            }
998            // ── ENV-RESOLVE M0 fast path (no-op unless `SUI_RESOLVE=1`) ──
999            // A parse-time-`Lexical` reference carries its precomputed
1000            // Symbol; probe the lexical bindings map DIRECTLY, skipping the
1001            // per-lookup `ident_text().to_string()` + `intern()`. This is
1002            // parity-by-construction: `lookup_fast` probes the SAME lexical
1003            // map by the SAME Symbol FIRST, so a hit here is byte-identical
1004            // to what the unchanged path below returns. Any miss (a
1005            // mid-fixpoint blackhole where the binding isn't in scope yet, an
1006            // unrecorded ident, or `Dynamic`) falls through to the EXACT
1007            // unchanged path — including the whole with-chain + WithIdent
1008            // deferral. The resolver never records keywords, so the
1009            // true/false/null handling below is untouched on this path.
1010            if crate::resolve_env::enabled() {
1011                let src_id = CURRENT_SOURCE_ID.with(std::cell::Cell::get);
1012                let offset = u32::from(ident.syntax().text_range().start());
1013                if let sui_resolve::Resolution::Lexical { sym } =
1014                    crate::resolve_env::resolution_for(src_id, offset)
1015                {
1016                    if let Some(v) = env.lookup_lexical_sym(sym) {
1017                        return Ok(v);
1018                    }
1019                }
1020                // Miss / Dynamic → fall through to the unchanged path.
1021            }
1022            // Cache the interned Symbol by (source_id, text_offset) so the
1023            // steady-state identifier lookup pays neither a per-lookup
1024            // `ident_text().to_string()` heap alloc nor a string re-hash — the
1025            // ident's text is materialized only on the once-per-offset cold
1026            // miss. The keyword check + the common `lookup_fast` HIT then run
1027            // fully allocation-free; `name` is materialized lazily only on the
1028            // miss/error branches, which need the string anyway.
1029            // KEY ON `env.source_id()`, NOT the thread-local (fixed 2026-07-20).
1030            //
1031            // `CURRENT_SOURCE_ID` is pushed at exactly ONE site —
1032            // `value.rs`'s `ThunkRepr::Suspended` force branch. Lambda
1033            // application and the Native/WithIdent/InheritSelect/Promise force
1034            // branches never push it, so while a callee's body was being
1035            // evaluated the thread-local still named the CALLER's file. The
1036            // `(source_id, offset)` cache key then aliased across files: an
1037            // identifier at byte N in file A could resolve to the Symbol
1038            // interned for a `null`/`true`/`false` token at byte N in file B —
1039            // and the zero-copy keyword check below turned that into a literal
1040            // `Value::Null` for a perfectly well-defined identifier, before any
1041            // environment lookup.
1042            //
1043            // That is what stopped sui evaluating nixpkgs: `hostSuffix` in
1044            // `make-derivation.nix` resolved to `null`, so `attrs.name +
1045            // hostSuffix` raised "cannot add string and null" — observed
1046            // directly as `STALE-KEYWORD ident="hostSuffix" resolvedAs="null"`.
1047            // It is not darwin-specific and has nothing to do with the module
1048            // system; `import <nixpkgs> {}` fails identically on x86_64-linux.
1049            //
1050            // `Env` already carries the correct value: `eval_with_file` sets it
1051            // and `child()` inherits it, and a lambda's `call_env` is
1052            // `closure.env.child()` — so a body's env names its DEFINING file.
1053            // Keying on it fixes every cross-file path at the cause, rather than
1054            // adding a fifth push/pop guard that a sixth path can forget.
1055            let sym = {
1056                let src_id = env.source_id();
1057                let offset = u32::from(ident.syntax().text_range().start());
1058                crate::value::intern_cached_with(src_id, offset, || {
1059                    crate::value::intern(&ident_text(ident))
1060                })
1061            };
1062            // Zero-copy keyword check on the resolved Symbol — the resolver
1063            // never records keywords, so this matches the prior `name.as_str()`
1064            // arm exactly.
1065            if let Some(kw) = crate::value::with_resolved(sym, |s| match s {
1066                "true" => Some(Value::Bool(true)),
1067                "false" => Some(Value::Bool(false)),
1068                "null" => Some(Value::Null),
1069                _ => None,
1070            }) {
1071                return Ok(kw);
1072            }
1073            return {
1074                {
1075                    // `lookup_fast`'s `name` argument is unused (lookup is by
1076                    // Symbol); pass "" to avoid materializing the ident text on
1077                    // the hot HIT path.
1078                    if let Some(v) = env.lookup_fast(sym, "") {
1079                        Ok(v)
1080                    } else {
1081                        let name = ident_text(ident);
1082                        // The `(src_id, text_offset)` identifier-symbol cache
1083                        // (`intern_cached_with`) can hand back a STALE Symbol when
1084                        // a lazily-forced thunk's identifier is resolved under a
1085                        // force-time `CURRENT_SOURCE_ID` that differs from the
1086                        // identifier's PARSE-time src_id — a thunk from file A can
1087                        // be forced while B is the current source, so
1088                        // `(B_src_id, offset)` aliases B's parse tree's identifier
1089                        // at that same byte offset and returns ITS Symbol. (Proven
1090                        // root: nixpkgs `lib/systems/parse.nix` `mkOptionType` — the
1091                        // binding IS present in the env, but the cache returned
1092                        // `Symbol(566)` while the binding was interned under
1093                        // `Symbol(506)`, so `lookup_fast(566)` missed a defined
1094                        // var.) `intern` is deterministic + append-only, so on a
1095                        // miss re-intern the name from its text (the authoritative
1096                        // Symbol) and retry the lexical lookup BEFORE considering
1097                        // with-scopes or undefined. A genuinely undefined variable
1098                        // is unaffected — its fresh lookup also misses and falls
1099                        // through unchanged.
1100                        let fresh = crate::value::intern(name.as_str());
1101                        if fresh != sym {
1102                            if let Some(v) = env.lookup_fast(fresh, name.as_str()) {
1103                                return Ok(v);
1104                            }
1105                        }
1106                        if env.with_scope_count() > 0 {
1107                        // With-scope lookup failed (likely blackhole from fixpoint).
1108                        // Return a WithIdent thunk for deferred resolution.
1109                        // This is the eval_expr equivalent of maybe_thunk's deferral.
1110                        if let Some((scope_cache, scope_value)) = env.innermost_with_scope() {
1111                            Ok(Value::Thunk(Thunk::new_with_ident(
1112                                SmolStr::from(name.as_str()),
1113                                scope_cache,
1114                                scope_value,
1115                                env.clone(),
1116                            )))
1117                        } else if crate::value::in_promise_eval() {
1118                            // M2.6 Promise softening: an undefined
1119                            // identifier inside Promise body evaluation
1120                            // typically means a `with` block sourced
1121                            // from the empty-attrset sentinel didn't
1122                            // populate the with-scope.  Returning null
1123                            // lets the eval proceed; the result is
1124                            // wrong-but-bounded (no further forces
1125                            // happen on null until something downstream
1126                            // demands a real value).
1127                            Ok(Value::Null)
1128                        } else {
1129                            Err(EvalError::UndefinedVar(
1130                                format!("'{name}'{}", eval_file_ctx()),
1131                            ))
1132                        }
1133                    } else {
1134                        if let Ok(dbg_var) = std::env::var("SUI_DEBUG_VAR") {
1135                            if dbg_var == name || dbg_var == "*" {
1136                                eprintln!(
1137                                    "[sui-debug] UndefinedVar '{name}' in {}\n\
1138                                     [sui-debug]   env bindings ({} total): {:?}\n\
1139                                     [sui-debug]   with_scopes: {}",
1140                                    eval_file_ctx(),
1141                                    env.binding_count(),
1142                                    env.binding_names_preview(20),
1143                                    env.with_scope_count(),
1144                                );
1145                            }
1146                        }
1147                        if crate::value::in_promise_eval() {
1148                            // Same Promise softening as the with-scope
1149                            // branch above.
1150                            return Ok(Value::Null);
1151                        }
1152                        Err(EvalError::UndefinedVar(
1153                            format!("'{name}'{}", eval_file_ctx()),
1154                        ))
1155                        }
1156                    }
1157                }
1158            };
1159        }
1160        ast::Expr::Literal(lit) => {
1161            crate::perf::inc(crate::perf::Counter::EvalExpr);
1162            if crate::perf::enabled() {
1163                crate::perf::inc(crate::perf::Counter::ExprLiteral);
1164            }
1165            return eval_literal(lit);
1166        }
1167        ast::Expr::Paren(p) => {
1168            if let Some(inner) = p.expr() {
1169                return eval_expr(&inner, env);
1170            }
1171        }
1172        ast::Expr::Root(r) => {
1173            if let Some(inner) = r.expr() {
1174                return eval_expr(&inner, env);
1175            }
1176        }
1177        // Lambda: no recursion — just captures env into a closure.
1178        ast::Expr::Lambda(lam) => {
1179            crate::perf::inc(crate::perf::Counter::EvalExpr);
1180            if crate::perf::enabled() {
1181                crate::perf::inc(crate::perf::Counter::ExprLambda);
1182            }
1183            if let (Some(param), Some(body)) = (lam.param(), lam.body()) {
1184                return Ok(Value::Lambda(Rc::new(Closure {
1185                    param,
1186                    body,
1187                    env: env.clone(),
1188                })));
1189            }
1190        }
1191        _ => {}
1192    }
1193    // Complex expressions: need stacker for recursion safety
1194    stacker::maybe_grow(64 * 1024, 2 * 1024 * 1024, || {
1195        eval_expr_inner(expr, env)
1196    })
1197}
1198
1199/// Inner implementation of [`eval_expr`] — called from the `stacker`
1200/// trampoline so that the stack is guaranteed to have headroom.
1201///
1202/// Uses a tail-call loop: for expressions in tail position (`if/else`,
1203/// `let..in`, `with`, `assert`, `paren`, `root`), we update the local
1204/// `expr` and `env` variables and loop instead of recursing. This
1205/// eliminates millions of stack frames in nixpkgs evaluation.
1206fn eval_expr_inner(expr: &ast::Expr, env: &Env) -> Result<Value, EvalError> {
1207    // Tail-call trampoline: expressions in tail position update these
1208    // and `continue` instead of recursing into eval_expr.
1209    let mut cur_expr = expr.clone();
1210    let mut cur_env = env.clone();
1211
1212    loop {
1213    crate::perf::inc(crate::perf::Counter::EvalExpr);
1214    // Track expression type distribution when profiling
1215    if crate::perf::enabled() {
1216        use crate::perf::Counter;
1217        let c = match &cur_expr {
1218            ast::Expr::Ident(_) => Counter::ExprIdent,
1219            ast::Expr::Literal(_) => Counter::ExprLiteral,
1220            ast::Expr::Str(_) => Counter::ExprStr,
1221            ast::Expr::List(_) => Counter::ExprList,
1222            ast::Expr::AttrSet(_) => Counter::ExprAttrs,
1223            ast::Expr::Select(_) => Counter::ExprSelect,
1224            ast::Expr::Apply(_) => Counter::ExprApply,
1225            ast::Expr::LetIn(_) => Counter::ExprLetIn,
1226            ast::Expr::IfElse(_) => Counter::ExprIfElse,
1227            ast::Expr::With(_) => Counter::ExprWith,
1228            ast::Expr::Lambda(_) => Counter::ExprLambda,
1229            ast::Expr::BinOp(_) => Counter::ExprBinOp,
1230            ast::Expr::HasAttr(_) => Counter::ExprHasAttr,
1231            ast::Expr::UnaryOp(_) => Counter::ExprUnaryOp,
1232            ast::Expr::Assert(_) => Counter::ExprAssert,
1233            ast::Expr::PathAbs(_) | ast::Expr::PathRel(_)
1234            | ast::Expr::PathHome(_) | ast::Expr::PathSearch(_) => Counter::ExprPath,
1235            _ => Counter::ExprOther,
1236        };
1237        crate::perf::inc(c);
1238    }
1239    let _guard = DepthGuard::enter()?;
1240    let env = &cur_env;
1241    match &cur_expr {
1242        ast::Expr::Literal(lit) => return eval_literal(lit),
1243
1244        ast::Expr::Str(s) => return eval_str(s, env),
1245
1246        ast::Expr::PathAbs(p) => {
1247            // An interpolated absolute path (`/a/${e}`) splices its
1248            // `${…}` parts; a plain one takes the raw-text shortcut.
1249            let parts = p.parts();
1250            if parts_have_interpolation(&parts) {
1251                return eval_interpol_path_parts(&parts, PathKind::Abs, env);
1252            }
1253            // Canonicalize like CppNix (`/.` → `/`, `.`/`..` collapse,
1254            // `..` clamps at root) — see the WHNF fast-path above.
1255            let text = crate::path::canon_abs(&p.syntax().text().to_string());
1256            return Ok(Value::Path(Box::new(SmolStr::from(text.as_str()))));
1257        }
1258        ast::Expr::PathRel(p) => {
1259            // Real Nix resolves `./foo.nix` against the directory
1260            // of the file that *contains* the literal, not the
1261            // process cwd. Use the current eval-file stack; fall
1262            // back to cwd when no file is being evaluated (e.g.,
1263            // top-level `sui eval`).
1264            //
1265            // An interpolated relative path (`./${x}.nix`) first splices
1266            // its `${…}` parts, then resolves the concatenated text the
1267            // same way — the interpolation is evaluated + string-coerced,
1268            // NOT treated as literal `${x}` text.
1269            let parts = p.parts();
1270            if parts_have_interpolation(&parts) {
1271                return eval_interpol_path_parts(&parts, PathKind::Rel, env);
1272            }
1273            let text = p.syntax().text().to_string();
1274            let resolved = if let Some(dir) = current_eval_dir() {
1275                let joined = dir.join(&text);
1276                // Use normalize_path instead of canonicalize so that
1277                // paths with ./  and .. are cleaned without requiring
1278                // the path to exist on disk.
1279                let norm = normalize_path(&joined);
1280                // A relative path literal (`./x`, `../..`) resolves against the
1281                // eval-dir, which for a fetched flake input is the sui fetcher
1282                // CACHE dir. CppNix resolves it against the input's
1283                // `/nix/store/<h>-source` STORE path, so the resulting path
1284                // VALUE must carry the store prefix (this is the value half of
1285                // the store↔cache seam — `materialize`/`dematerialize`). Lift
1286                // the cache path back to the store path so `toString ../..`
1287                // matches CppNix — the options.json `hasPrefix
1288                // <nix-darwin>.outPath decl` rewrite root (`prefix = ../..`).
1289                crate::path::dematerialize(&norm)
1290                    .to_string_lossy()
1291                    .into_owned()
1292            } else {
1293                text.clone()
1294            };
1295            return Ok(Value::Path(Box::new(SmolStr::from(resolved.as_str()))));
1296        }
1297        ast::Expr::PathHome(p) => {
1298            let parts = p.parts();
1299            if parts_have_interpolation(&parts) {
1300                return eval_interpol_path_parts(&parts, PathKind::Home, env);
1301            }
1302            let text = p.syntax().text().to_string();
1303            return Ok(Value::Path(Box::new(SmolStr::from(text.as_str()))));
1304        }
1305        ast::Expr::PathSearch(p) => {
1306            // `<name>` or `<name/sub/path>` — resolve via NIX_PATH
1307            // entries (parsed from the env var). If no NIX_PATH entry
1308            // matches, fall through to the literal text so the error
1309            // message points at the name the user wrote.
1310            let text = p.syntax().text().to_string();
1311            let inner = text
1312                .strip_prefix('<')
1313                .and_then(|s| s.strip_suffix('>'))
1314                .unwrap_or(&text);
1315            if let Some(resolved) = crate::builtins::resolve_search_path(inner) {
1316                return Ok(Value::Path(Box::new(SmolStr::from(resolved.as_str()))));
1317            }
1318            // CppNix: search path resolution failure is a throw
1319            // (catchable by tryEval). Used by nixpkgs impure-overlays.nix
1320            // which tries `import <nixpkgs-overlays>` inside tryEval.
1321            return Err(EvalError::Throw(
1322                format!("search path '{text}' not in NIX_PATH"),
1323            ));
1324        }
1325
1326        ast::Expr::Ident(ident) => {
1327            let name = ident_text(ident);
1328            return match name.as_str() {
1329                "true" => Ok(Value::Bool(true)),
1330                "false" => Ok(Value::Bool(false)),
1331                "null" => Ok(Value::Null),
1332                _ => {
1333                    env.lookup(&name)
1334                        .ok_or_else(|| EvalError::UndefinedVar(
1335                            format!("'{name}'{}", eval_file_ctx()),
1336                        ))
1337                }
1338            };
1339        }
1340
1341        ast::Expr::List(list) => {
1342            // Wrap list elements in thunks for maximum laziness.
1343            // CppNix wraps list elements — only forced when accessed.
1344            // This prevents eager evaluation of unused list elements
1345            // (e.g., nixpkgs overlay lists with thousands of entries).
1346            let values: Vec<Value> = list.items()
1347                .map(|e| maybe_thunk(&e, env, false, None))
1348                .collect();
1349            return Ok(Value::list(values));
1350        }
1351
1352        ast::Expr::AttrSet(set) => return eval_attrset(set, env),
1353
1354        ast::Expr::Select(sel) => return eval_select(sel, env),
1355
1356        ast::Expr::HasAttr(ha) => return eval_has_attr(ha, env),
1357
1358        ast::Expr::UnaryOp(op) => return eval_unary_op(op, env),
1359
1360        ast::Expr::BinOp(binop) => {
1361            let lhs_expr = binop
1362                .lhs()
1363                .ok_or_else(|| EvalError::ParseError("binop missing lhs".to_string()))?;
1364            let rhs_expr = binop
1365                .rhs()
1366                .ok_or_else(|| EvalError::ParseError("binop missing rhs".to_string()))?;
1367            let kind = binop
1368                .operator()
1369                .ok_or_else(|| EvalError::ParseError("binop missing operator".to_string()))?;
1370            return eval_binop(kind, &lhs_expr, &rhs_expr, env);
1371        }
1372
1373        ast::Expr::Apply(app) => return eval_apply(app, env),
1374
1375        ast::Expr::IfElse(ie) => {
1376            let cond = ie
1377                .condition()
1378                .ok_or_else(|| EvalError::ParseError("if missing condition".to_string()))?;
1379            let body = ie
1380                .body()
1381                .ok_or_else(|| EvalError::ParseError("if missing then body".to_string()))?;
1382            let else_body = ie
1383                .else_body()
1384                .ok_or_else(|| EvalError::ParseError("if missing else body".to_string()))?;
1385            if force_concrete(&eval_expr(&cond, env)?)?.as_bool()? {
1386                cur_expr = body;
1387            } else {
1388                cur_expr = else_body;
1389            }
1390            // env stays the same — tail call
1391            continue;
1392        }
1393
1394        ast::Expr::Assert(assert) => {
1395            let cond = assert
1396                .condition()
1397                .ok_or_else(|| EvalError::ParseError("assert missing condition".to_string()))?;
1398            let body = assert
1399                .body()
1400                .ok_or_else(|| EvalError::ParseError("assert missing body".to_string()))?;
1401            if !force_concrete(&eval_expr(&cond, env)?)?.as_bool()? {
1402                return Err(EvalError::AssertionFailed(eval_file_ctx()));
1403            }
1404            cur_expr = body;
1405            continue;
1406        }
1407
1408        ast::Expr::With(with) => {
1409            let ns = with
1410                .namespace()
1411                .ok_or_else(|| EvalError::ParseError("with missing namespace".to_string()))?;
1412            let body = with
1413                .body()
1414                .ok_or_else(|| EvalError::ParseError("with missing body".to_string()))?;
1415            // Don't force the namespace yet — store as a lazy value.
1416            // CppNix evaluates with-scopes lazily: the namespace is only
1417            // forced when a name lookup actually falls through lexical scope.
1418            // This is critical for `fix (self: with self; { … })` patterns
1419            // used throughout nixpkgs.
1420            //
1421            // M2.6 ROOT #4a (byte-verified): `eval_expr(&ns, env)?` was NOT
1422            // lazy — it EVALUATED the namespace expression eagerly at
1423            // `with`-entry.  For `with (throw "X"); body` that runs the
1424            // throw; for `with config.services.borgbackup; { … }` (nixpkgs'
1425            // module `config` shape) it forces `config.services.borgbackup`
1426            // the instant the `with`-body's WHNF/keys are demanded (during
1427            // module collection's `pushDownProperties`), re-entering the
1428            // mid-force `config` fixpoint → the empty-Promise partial →
1429            // `null` softening → `concatLists null`.  cppnix stores the
1430            // namespace as a thunk and forces it ONLY when a bare-ident
1431            // lookup actually falls through lexical scope into the `with`.
1432            // Reduced repro (no module system, iterates in ms):
1433            //   `builtins.attrNames (with (throw "X"); { a = 1; })`
1434            //   nix → [ "a" ] ; sui (before) → throws "X".
1435            // `maybe_thunk` keeps the fast-path for an already-resolved
1436            // ident namespace (no thunk overhead) while deferring any
1437            // non-trivial namespace (Select / Apply / throw) into a lazy
1438            // thunk the scope-lookup path (`Env::lookup_fast`) forces only
1439            // on fallthrough.
1440            let scope_val = maybe_thunk(&ns, env, false, None);
1441            let new_env = env.child().with_scope(scope_val);
1442            cur_expr = body;
1443            cur_env = new_env;
1444            continue;
1445        }
1446
1447        ast::Expr::LetIn(letin) => {
1448            let mut new_env = env.child();
1449
1450            // Phase 1: Create thunks with a dummy env and bind them.
1451            // Collect (key, thunk) pairs so we can update envs later.
1452            let mut thunks: Vec<(String, Thunk)> = Vec::new();
1453
1454            // Track which names have been defined so far in this scope.
1455            // Used by maybe_thunk to resolve backward references directly
1456            // instead of creating wasteful thunks.
1457            let mut defined_so_far: HashSet<String> = HashSet::new();
1458
1459            // Accumulator for dotted-path bindings (`let a.b = 1; a.c = 2; ...`).
1460            // Leaf values are wrapped in thunks so they can reference
1461            // sibling let-bindings (the let scope is recursive in Nix).
1462            let mut dotted_attrs: NixAttrs = NixAttrs::new();
1463
1464            // Pre-pass: collect every binding name in this let-scope
1465            // (single-key bindings + top-level keys of dotted paths +
1466            // names from inherit clauses).  Used by the recursive-thunk
1467            // detector below — a binding is part of the mutual fix-point
1468            // if its RHS references ANY of these names.
1469            let let_scope_names: HashSet<String> = {
1470                let mut s = HashSet::new();
1471                for entry in letin.entries() {
1472                    match entry {
1473                        ast::Entry::AttrpathValue(apv) => {
1474                            if let Some(attrpath) = apv.attrpath() {
1475                                if let Some(first) = attrpath.attrs().next() {
1476                                    if let Ok(name) = eval_attr(&first, env) {
1477                                        s.insert(name);
1478                                    }
1479                                }
1480                            }
1481                        }
1482                        ast::Entry::Inherit(inherit) => {
1483                            for attr in inherit.attrs() {
1484                                if let Ok(name) = eval_attr(&attr, env) {
1485                                    s.insert(name);
1486                                }
1487                            }
1488                        }
1489                    }
1490                }
1491                s
1492            };
1493
1494            for entry in letin.entries() {
1495                match entry {
1496                    ast::Entry::AttrpathValue(ref apv) => {
1497                        let attrpath = apv.attrpath().ok_or_else(|| {
1498                            EvalError::ParseError("binding missing attrpath".to_string())
1499                        })?;
1500                        let value_expr = apv.value().ok_or_else(|| {
1501                            EvalError::ParseError("binding missing value".to_string())
1502                        })?;
1503                        let mut path_keys: Vec<String> = attrpath
1504                            .attrs()
1505                            .map(|a| eval_attr(&a, env))
1506                            .collect::<Result<_, _>>()?;
1507                        if path_keys.len() == 1 {
1508                            let key = path_keys.pop().unwrap();
1509                            // Self/mutual-recursive detection: any binding
1510                            // whose RHS references its own name OR any
1511                            // SIBLING let-scope name is part of the let's
1512                            // mutual fix-point.  Mark as recursive so
1513                            // inner re-entrance during force returns a
1514                            // Promise sentinel instead of erroring with
1515                            // InfiniteRecursion.  This is the M2.6
1516                            // module-system fix path (cppnix's
1517                            // lib/modules.nix uses a deep let-scope with
1518                            // declaredConfig / options / matchedOptions /
1519                            // resultsByName / modules all transitively
1520                            // cycling through each other).
1521                            //
1522                            // `let_scope_names` is collected upfront in a
1523                            // pre-pass so each binding sees every other
1524                            // binding name (not just earlier ones).
1525                            // O(N) not O(N²): compute the RHS's referenced-name
1526                            // set ONCE (memoized), then intersect with the
1527                            // let-scope names. Byte-identical to the prior
1528                            // `references(key) OR references(any sibling)`:
1529                            // chaining `key` covers the self-reference case
1530                            // regardless of whether `key ∈ let_scope_names`.
1531                            let referenced = referenced_idents(&value_expr);
1532                            let in_mutual_cycle = std::iter::once(&key)
1533                                .chain(let_scope_names.iter())
1534                                .any(|n| referenced.contains(n.as_str()));
1535                            let value = if in_mutual_cycle {
1536                                Value::Thunk(Thunk::new_suspended_recursive(
1537                                    value_expr.clone(),
1538                                    env.clone(),
1539                                ))
1540                            } else {
1541                                maybe_thunk(&value_expr, env, true, Some(&defined_so_far))
1542                            };
1543                            new_env.bind(key.clone(), value.clone());
1544                            if let Value::Thunk(t) = &value {
1545                                thunks.push((key.clone(), t.clone()));
1546                            }
1547                            defined_so_far.insert(key);
1548                        } else if path_keys.len() > 1 {
1549                            // Multi-segment dotted path: build a nested
1550                            // attrset with thunks at the leaves so the
1551                            // value expression can reference sibling
1552                            // let-bindings.
1553                            let key = path_keys[0].clone();
1554                            let value = build_nested_attr_thunk(
1555                                &path_keys[1..],
1556                                &value_expr,
1557                                env,
1558                                &mut thunks,
1559                            );
1560                            merge_nested_insert(&mut dotted_attrs, key, value);
1561                        }
1562                    }
1563                    ast::Entry::Inherit(ref inherit) => {
1564                        if let Some(from) = inherit.from() {
1565                            let source_expr = from.expr().ok_or_else(|| {
1566                                EvalError::ParseError(
1567                                    "inherit from missing expr".to_string(),
1568                                )
1569                            })?;
1570                            // Create ONE shared source thunk per
1571                            // `inherit (source)` clause. All inherited
1572                            // names share it via Rc clone — the source
1573                            // is evaluated at most once.
1574                            let source_thunk = Thunk::new_suspended(
1575                                source_expr, env.clone(),
1576                            );
1577                            for attr in inherit.attrs() {
1578                                let name = eval_attr(&attr, env)?;
1579                                let thunk = Thunk::new_inherit_select(
1580                                    source_thunk.clone(),
1581                                    name.clone(),
1582                                );
1583                                new_env.bind(name.clone(), Value::Thunk(thunk.clone()));
1584                                thunks.push((name, thunk));
1585                            }
1586                        } else {
1587                            // `inherit name1 name2 ...` from the
1588                            // enclosing lexical scope. This stays
1589                            // eager because the names already exist
1590                            // in `env` — no fixpoint involved.
1591                            for attr in inherit.attrs() {
1592                                let name = eval_attr(&attr, env)?;
1593                                let value = env.lookup(&name).ok_or_else(|| {
1594                                    EvalError::UndefinedVar(
1595                                        format!("'{name}'{}", eval_file_ctx()),
1596                                    )
1597                                })?;
1598                                new_env.bind(name, value);
1599                            }
1600                        }
1601                    }
1602                }
1603            }
1604
1605            // Phase 1b: Bind accumulated dotted-path attrs into new_env.
1606            // Note: CppNix rejects `inherit (src) x; x.y = ...;` as a
1607            // duplicate definition, so we do not attempt to merge with
1608            // existing inherit thunks — just bind directly.
1609            for (key, value) in dotted_attrs.iter() {
1610                new_env.bind(key.clone(), value.clone());
1611            }
1612
1613            // Phase 2: Update all thunks to capture the final env
1614            // (which now has all names bound).
1615            for (_key, thunk) in &thunks {
1616                thunk.update_env(&new_env);
1617            }
1618
1619            let body = letin
1620                .body()
1621                .ok_or_else(|| EvalError::ParseError("let missing body".to_string()))?;
1622            cur_expr = body;
1623            cur_env = new_env;
1624            continue;
1625        }
1626
1627        ast::Expr::Lambda(lam) => {
1628            let param = lam
1629                .param()
1630                .ok_or_else(|| EvalError::ParseError("lambda missing param".to_string()))?;
1631            let body = lam
1632                .body()
1633                .ok_or_else(|| EvalError::ParseError("lambda missing body".to_string()))?;
1634            return Ok(Value::Lambda(Rc::new(Closure {
1635                param,
1636                body,
1637                env: env.clone(),
1638            })));
1639        }
1640
1641        ast::Expr::Paren(p) => {
1642            let inner = p
1643                .expr()
1644                .ok_or_else(|| EvalError::ParseError("paren missing expr".to_string()))?;
1645            cur_expr = inner;
1646            continue;
1647        }
1648
1649        ast::Expr::Root(r) => {
1650            let inner = r
1651                .expr()
1652                .ok_or_else(|| EvalError::ParseError("root missing expr".to_string()))?;
1653            cur_expr = inner;
1654            continue;
1655        }
1656
1657        ast::Expr::LegacyLet(ll) => {
1658            let mut new_env = env.child();
1659            eval_entries(ll, &mut new_env)?;
1660            // legacy let returns the `body` attr from its bindings
1661            return new_env
1662                .lookup("body")
1663                .ok_or_else(|| EvalError::AttrNotFound(
1664                    format!("'body' in legacy let{}", eval_file_ctx()),
1665                ));
1666        }
1667
1668        ast::Expr::CurPos(_) => return Err(EvalError::NotImplemented("__curPos".to_string())),
1669        ast::Expr::Error(_) => return Err(EvalError::ParseError("parse error node".to_string())),
1670    } // match
1671    } // loop — unreachable, all arms either return or continue
1672}
1673
1674fn eval_literal(lit: &ast::Literal) -> Result<Value, EvalError> {
1675    use ast::LiteralKind;
1676    match lit.kind() {
1677        LiteralKind::Integer(tok) => {
1678            let n = tok
1679                .value()
1680                .map_err(|e| EvalError::ParseError(format!("invalid integer: {e}")))?;
1681            Ok(Value::Int(n))
1682        }
1683        LiteralKind::Float(tok) => {
1684            let f = tok
1685                .value()
1686                .map_err(|e| EvalError::ParseError(format!("invalid float: {e}")))?;
1687            Ok(Value::Float(f))
1688        }
1689        LiteralKind::Uri(tok) => Ok(Value::string(tok.syntax().text().to_string())),
1690    }
1691}
1692
1693/// Result of walking an attrpath on a base value.
1694enum TraverseResult {
1695    /// All keys found; contains the leaf value.
1696    Found(Value),
1697    /// A key was missing; contains the missing key name.
1698    Missing(String),
1699    /// A non-attrset value was encountered during traversal.
1700    NotAttrs(Value),
1701}
1702
1703/// Walk an attrpath on a base value, forcing at each level.
1704///
1705/// Returns `Found(leaf)` when every key exists, `Missing(key)` when
1706/// a key is absent, or `NotAttrs(v)` when a non-attrset is encountered.
1707fn traverse_attrpath(
1708    base: Value,
1709    attrpath: &rnix::ast::Attrpath,
1710    env: &Env,
1711) -> Result<TraverseResult, EvalError> {
1712    let attrs: Vec<_> = attrpath.attrs().collect();
1713    let mut value = base;
1714    for (i, attr) in attrs.iter().enumerate() {
1715        let key = eval_attr(attr, env)?;
1716        // Force the current value to an attrset to select from it.
1717        let forced = force_value(&value)?;
1718        match forced {
1719            Value::Attrs(ref a) => match a.get(&key) {
1720                Some(v) => {
1721                    if i < attrs.len() - 1 {
1722                        // Intermediate step: force to attrset for next selection.
1723                        value = force_value(v)?;
1724                    } else {
1725                        // Final step: return WITHOUT forcing — let the caller
1726                        // decide when to force. Matches CppNix's lazy attr access.
1727                        value = v.clone();
1728                    }
1729                }
1730                None => return Ok(TraverseResult::Missing(key)),
1731            },
1732            _ => return Ok(TraverseResult::NotAttrs(forced)),
1733        }
1734    }
1735    Ok(TraverseResult::Found(value))
1736}
1737
1738fn eval_select(sel: &ast::Select, env: &Env) -> Result<Value, EvalError> {
1739    crate::perf::inc(crate::perf::Counter::Select);
1740    let base_expr = sel.expr().ok_or_else(|| {
1741        EvalError::ParseError("select missing expression".to_string())
1742    })?;
1743    // M2.6 bridge: in `expr.path or default`, an `InfiniteRecursion`
1744    // hit while forcing the LEFT side falls back to the default —
1745    // operationally matches cppnix, which avoids the cycle entirely
1746    // via lazy attribute access during fix-point evaluation.  Without
1747    // a default, the recursion propagates as a real error.  Other
1748    // error kinds (Throw, TypeError, …) always propagate so user
1749    // bugs aren't masked.  Removed when the underlying fix-point /
1750    // lazy-access semantics land — see docs/M2.6-MODULE-SYSTEM-FIXPOINT.md.
1751    let base_result = eval_expr(&base_expr, env)
1752        .and_then(|v| force_concrete(&v).map(Concrete::into_value));
1753    let base = match base_result {
1754        Ok(v) => v,
1755        Err(EvalError::InfiniteRecursion(_)) if sel.default_expr().is_some() => {
1756            return eval_expr(&sel.default_expr().expect("checked"), env);
1757        }
1758        Err(e) => return Err(e),
1759    };
1760    let base_type = base.type_name();
1761    let attrpath = sel.attrpath().ok_or_else(|| {
1762        EvalError::ParseError("select missing attrpath".to_string())
1763    })?;
1764    // M2.6 bridge: when the blackhole-bridge sentinels are active,
1765    // an attribute lookup that misses (`AttrNotFound`) or hits a
1766    // non-attrset intermediate (`NotAttrs`) on the bridge's empty
1767    // sentinel value gets resolved to `null` instead of erroring.
1768    // cppnix's partial attrset would have CARRIED the keys (with
1769    // their lazy values), so the lookup would succeed; null is the
1770    // cheapest sentinel that propagates through downstream code
1771    // without further type errors.
1772    //
1773    // M2.6 ROOT #4 CLOSED (2026-07-11): the `|| crate::value::in_promise_eval()`
1774    // clause that used to soften a mid-Promise `config.<x>` select-miss to
1775    // `null` is REMOVED.  It was the band-aid masking the two real over-forces
1776    // that ROOT #4a (the `with`-namespace eager eval, above) and ROOT #4b (the
1777    // dropped full-set leaf in `merge_nested_insert`, below) now fix at their
1778    // load-bearing cause.  Verified with the softening gone: both
1779    // `lib.nixosSystem { modules = []; }.config.system.name` → `"nixos"` and
1780    // `attrNames sys.options` → 53 (nix-parity), `sui parity` stays 35 match /
1781    // 0 regressions, 1324 sui-eval lib tests + 30 diff tests pass — nothing
1782    // depended on the sentinel any more.  The two explicit operator-gated
1783    // bridges below stay as opt-in experiments (default-off); only the
1784    // always-on Promise softening is retired.
1785    let bridge_active = std::env::var_os("SUI_BLACKHOLE_AS_EMPTY_ATTRS").is_some()
1786        || std::env::var_os("SUI_BLACKHOLE_AS_NULL").is_some();
1787    let traversal = traverse_attrpath(base, &attrpath, env);
1788    match traversal {
1789        Ok(TraverseResult::Found(v)) => Ok(v),
1790        Ok(TraverseResult::Missing(key)) => {
1791            if let Some(def) = sel.default_expr() {
1792                eval_expr(&def, env)
1793            } else if bridge_active {
1794                if std::env::var_os("SUI_M26_SELTRACE").is_some() {
1795                    let path: Vec<String> = sel.attrpath().map(|ap|
1796                        ap.attrs().map(|a| a.syntax().text().to_string()).collect()
1797                    ).unwrap_or_default();
1798                    eprintln!("[M26 SEL-MISS→null] base_type={base_type} path={path:?} missing-key={key}{}", eval_file_ctx());
1799                }
1800                if let Ok(filt) = std::env::var("SUI_M26_HARDSOFTEN") {
1801                    let path: Vec<String> = sel.attrpath().map(|ap|
1802                        ap.attrs().map(|a| a.syntax().text().to_string()).collect()
1803                    ).unwrap_or_default();
1804                    if path.iter().any(|p| p.contains(&filt)) {
1805                        return Err(EvalError::type_error(format!(
1806                            "M26-HARDSOFTEN path={path:?} key={key}"
1807                        )));
1808                    }
1809                }
1810                Ok(Value::Null)
1811            } else {
1812                Err(EvalError::AttrNotFound(
1813                    format!("'{key}'{}", eval_file_ctx()),
1814                ))
1815            }
1816        }
1817        Ok(TraverseResult::NotAttrs(forced)) => {
1818            // CppNix: `expr.a.b or default` falls back to default for
1819            // ANY error in the path — including intermediate values
1820            // that aren't attrsets (e.g., null). The module system
1821            // relies on this: `x.options.type.name or null` must
1822            // return null when x.options is null, not throw.
1823            if let Some(def) = sel.default_expr() {
1824                eval_expr(&def, env)
1825            } else if bridge_active {
1826                if let Ok(filt) = std::env::var("SUI_M26_HARDSOFTEN") {
1827                    let path: Vec<String> = sel.attrpath().map(|ap|
1828                        ap.attrs().map(|a| a.syntax().text().to_string()).collect()
1829                    ).unwrap_or_default();
1830                    if path.iter().any(|p| p.contains(&filt)) {
1831                        return Err(EvalError::type_error(format!(
1832                            "M26-HARDSOFTEN-NOTATTRS path={path:?} base_type={base_type}"
1833                        )));
1834                    }
1835                }
1836                return Ok(Value::Null);
1837            } else {
1838                if std::env::var("SUI_DEBUG_SELECT").is_ok() {
1839                    let path: Vec<String> = sel.attrpath().map(|ap|
1840                        ap.attrs().filter_map(|a| match a {
1841                            ast::Attr::Ident(i) => Some(i.to_string()),
1842                            ast::Attr::Str(s) => Some(format!("\"{}\"", s.syntax().text())),
1843                            ast::Attr::Dynamic(_) => Some("<dyn>".into()),
1844                        }).collect()
1845                    ).unwrap_or_default();
1846                    let dbg = format!("{:?}", forced);
1847                    let truncated = if dbg.len() > 200 { format!("{}…", &dbg[..200]) } else { dbg };
1848                    eprintln!("[SUI_DEBUG_SELECT] base_type={base_type} path={path:?} base={truncated}{}", eval_file_ctx());
1849                }
1850                Err(attach_trace(EvalError::type_error(
1851                    format!("cannot select from {base_type}"),
1852                )))
1853            }
1854        }
1855        // Same M2.6 bridge as on the base force above: if an
1856        // intermediate step in the attrpath traversal raises
1857        // InfiniteRecursion and `or default` was supplied, the
1858        // default is the operationally-correct value.
1859        Err(EvalError::InfiniteRecursion(_)) if sel.default_expr().is_some() => {
1860            eval_expr(&sel.default_expr().expect("checked"), env)
1861        }
1862        Err(e) => Err(e),
1863    }
1864}
1865
1866/// Evaluate `expr ? a.b.c` — check key presence without forcing value thunks.
1867fn eval_has_attr(ha: &ast::HasAttr, env: &Env) -> Result<Value, EvalError> {
1868    let base_expr = ha.expr().ok_or_else(|| {
1869        EvalError::ParseError("hasattr missing expression".to_string())
1870    })?;
1871    let base = force_concrete(&eval_expr(&base_expr, env)?)?.into_value();
1872    let attrpath = ha.attrpath().ok_or_else(|| {
1873        EvalError::ParseError("hasattr missing attrpath".to_string())
1874    })?;
1875    match traverse_attrpath(base, &attrpath, env)? {
1876        TraverseResult::Found(_) => Ok(Value::Bool(true)),
1877        TraverseResult::Missing(_) | TraverseResult::NotAttrs(_) => Ok(Value::Bool(false)),
1878    }
1879}
1880
1881fn eval_unary_op(op: &ast::UnaryOp, env: &Env) -> Result<Value, EvalError> {
1882    let inner = op
1883        .expr()
1884        .ok_or_else(|| EvalError::ParseError("unary op missing expr".to_string()))?;
1885    let val = force_value(&eval_expr(&inner, env)?)?;
1886    let kind = op
1887        .operator()
1888        .ok_or_else(|| EvalError::ParseError("unary op missing operator".to_string()))?;
1889    match kind {
1890        ast::UnaryOpKind::Negate => match val {
1891            Value::Int(n) => Ok(Value::Int(-n)),
1892            Value::Float(f) => Ok(Value::Float(-f)),
1893            _ => Err(EvalError::type_error(
1894                format!("cannot negate {}", val.type_name()),
1895            )),
1896        },
1897        ast::UnaryOpKind::Invert => Ok(Value::Bool(!val.as_bool()?)),
1898    }
1899}
1900
1901/// Builtins that must receive their argument UNFORCED (call-by-need). This is the
1902/// SINGLE source of truth consumed by BOTH `eval_apply` (which must THUNK the arg
1903/// instead of eager-evaluating it) AND the builtin apply arm (which must SKIP the
1904/// arg force). The two sites MUST agree: if `eval_apply` eager-evaluates the arg,
1905/// the apply-arm's force-skip is dead (the arg is already forced — or already
1906/// threw) upstream. They were previously inconsistent (only `tryEval` was thunked
1907/// in `eval_apply`), so `seq`/`deepSeq`/`addErrorContext`/`foldl'` silently got
1908/// eager args despite their apply-time exemption — the bug behind
1909/// `builtins.foldl' (_: x: x) (throw "…") […]` throwing instead of returning the
1910/// last element (nix's foldl' is NOT strict in the nul accumulator).
1911#[inline]
1912pub(crate) fn builtin_takes_lazy_arg(name: &str) -> bool {
1913    matches!(
1914        name,
1915        "tryEval" | "addErrorContext<partial>" | "seq<partial>" | "deepSeq<partial>" | "foldl'<p1>"
1916    )
1917}
1918
1919fn eval_apply(app: &ast::Apply, env: &Env) -> Result<Value, EvalError> {
1920    let func_expr = app
1921        .lambda()
1922        .ok_or_else(|| EvalError::ParseError("apply missing function".to_string()))?;
1923    let arg_expr = app
1924        .argument()
1925        .ok_or_else(|| EvalError::ParseError("apply missing argument".to_string()))?;
1926    let func = force_value(&eval_expr(&func_expr, env)?)?;
1927    // Lambda arguments are wrapped in a thunk for call-by-need semantics.
1928    // Thunk strategy depends on function type:
1929    // - Lambda: ALWAYS thunk (call-by-need, enables fixpoints)
1930    // - tryEval: ALWAYS thunk (must catch errors during force)
1931    // - Builtin: evaluate eagerly (builtins always force args anyway;
1932    //   thunking wastes Rc + OnceCell allocation per call)
1933    // - __functor: evaluate eagerly (will be applied immediately)
1934    let arg = match &func {
1935        Value::Lambda(_) => {
1936            // Call-by-need: the arg is thunked so it forces lazily. But a
1937            // PURE-CONSTANT arg (a literal, a non-interpolated string, or a
1938            // non-interpolated path) can never throw or diverge, so producing
1939            // its value directly is byte-neutral whether or not the lambda ever
1940            // forces it — identical eval-order-observable behavior, one fewer
1941            // never-forced thunk. This is `arg_pure_constant` ONLY: any arg that
1942            // could throw/diverge/observe a fixpoint (Ident with-scope, Select,
1943            // Apply, BinOp, …) stays fully thunked to preserve laziness.
1944            if let Some(v) = eval_pure_constant_arg(&arg_expr) {
1945                v
1946            } else {
1947                crate::perf::inc(crate::perf::Counter::ThunkSiteApplyArg);
1948                Value::Thunk(Thunk::new_suspended(arg_expr.clone(), env.clone()))
1949            }
1950        }
1951        Value::Builtin(b) if builtin_takes_lazy_arg(&b.name) => {
1952            // Call-by-need for the laziness-exempt builtins (tryEval / seq /
1953            // deepSeq / addErrorContext / foldl'<p1>): the arg MUST be thunked,
1954            // not eager-evaluated, so it forces only if/when the builtin demands
1955            // it. Kept in lockstep with the apply-arm skip via `builtin_takes_lazy_arg`.
1956            crate::perf::inc(crate::perf::Counter::ThunkSiteApplyArg);
1957            Value::Thunk(Thunk::new_suspended(arg_expr.clone(), env.clone()))
1958        }
1959        _ => eval_expr(&arg_expr, env)?,
1960    };
1961    apply(func, arg)
1962}
1963
1964/// If `arg_expr` is a PURE CONSTANT — a literal, a non-interpolated string, or
1965/// a non-interpolated absolute/home path — return its value directly (no thunk).
1966///
1967/// A pure constant has no free variables, cannot throw, cannot diverge, and has
1968/// no fixpoint/laziness interaction: `eval_expr(arg)` is total and produces the
1969/// exact value a suspended thunk of it would yield on force. Producing it
1970/// eagerly in a call-by-need arg position is therefore byte-neutral (the
1971/// lambda that never forces the arg observes no difference — the value is inert).
1972///
1973/// Returns `None` for EVERYTHING else (Ident — may hit a with-scope force;
1974/// Select/Apply/BinOp/If/… — may throw or diverge; interpolated Str/Path —
1975/// must force `${…}` lazily), which keeps those args fully thunked. `env` is
1976/// NOT threaded in because a pure constant needs no environment; if a match
1977/// arm ever needed `env`, it would not be a pure constant.
1978fn eval_pure_constant_arg(arg_expr: &ast::Expr) -> Option<Value> {
1979    match arg_expr {
1980        ast::Expr::Literal(lit) => eval_literal(lit).ok(),
1981        ast::Expr::Str(st) if !str_has_interpolation(st) => {
1982            // No interpolation ⇒ `eval_str` runs no force/coerce; env is unused.
1983            eval_str(st, &Env::new()).ok()
1984        }
1985        ast::Expr::PathAbs(p) if !parts_have_interpolation(&p.parts()) => {
1986            let text = crate::path::canon_abs(&p.syntax().text().to_string());
1987            Some(Value::Path(Box::new(SmolStr::from(text.as_str()))))
1988        }
1989        ast::Expr::PathHome(p) if !parts_have_interpolation(&p.parts()) => {
1990            let text = p.syntax().text().to_string();
1991            Some(Value::Path(Box::new(SmolStr::from(text.as_str()))))
1992        }
1993        _ => None,
1994    }
1995}
1996
1997fn eval_str(s: &ast::Str, env: &Env) -> Result<Value, EvalError> {
1998    let mut result = String::new();
1999    let mut ctx = StringContext::new();
2000    for part in s.normalized_parts() {
2001        match part {
2002            InterpolPart::Literal(text) => result.push_str(&text),
2003            InterpolPart::Interpolation(interpol) => {
2004                let expr = interpol.expr().ok_or_else(|| {
2005                    EvalError::ParseError("interpolation missing expr".to_string())
2006                })?;
2007                let val = force_value(&eval_expr(&expr, env)?)?;
2008                // CppNix string interpolation is copy-to-store coercion: an
2009                // interpolated source path (`"${./foo}"`) is NAR-copied into
2010                // the store and the store path is spliced in (with context),
2011                // never the raw filesystem path.
2012                let (s, c) = val.coerce_to_string_copy_to_store()?;
2013                result.push_str(&s);
2014                ctx.merge(&c);
2015            }
2016        }
2017    }
2018    Ok(Value::String(Rc::new(NixString::with_context(result, ctx))))
2019}
2020
2021/// Whether a list of path parts contains a `${…}` interpolation. When
2022/// it does not, the raw `.syntax().text()` shortcut is byte-identical
2023/// and cheaper, so the trivial fast paths stay on that shortcut.
2024fn parts_have_interpolation(parts: &[InterpolPart<rnix::ast::PathContent>]) -> bool {
2025    parts
2026        .iter()
2027        .any(|p| matches!(p, InterpolPart::Interpolation(_)))
2028}
2029
2030/// Whether a string literal contains any `${…}` interpolation part. A `false`
2031/// result means the string is a pure constant (`eval_str` runs no force/coerce
2032/// and cannot throw), so `maybe_thunk` may evaluate it eagerly byte-neutrally.
2033fn str_has_interpolation(s: &ast::Str) -> bool {
2034    s.normalized_parts()
2035        .iter()
2036        .any(|p| matches!(p, InterpolPart::Interpolation(_)))
2037}
2038
2039/// Evaluate an interpolatable path literal that contains `${…}` parts.
2040///
2041/// CppNix path interpolation (`./${x}.nix`, `/a/${e}`, `~/x/${e}`):
2042///   * each literal segment is spliced verbatim,
2043///   * each `${e}` is **plain**-coerced to a string with context
2044///     (NOT copy-to-store — path-typed interpolations splice the raw
2045///     store/filesystem path, e.g. `/bar/${./foo}` → `/bar/tmp/foo`),
2046///   * the concatenated text is then resolved exactly like the plain
2047///     path literal of the same kind (relative → joined + normalized
2048///     against the defining file's directory; absolute/home → verbatim),
2049///   * the result is a `path` value.
2050///
2051/// Parts come from rnix's `<PathKind>::parts()` which splits the path
2052/// token stream into `Literal(PathContent)` / `Interpolation(Interpol)`.
2053fn eval_interpol_path_parts(
2054    parts: &[InterpolPart<rnix::ast::PathContent>],
2055    kind: PathKind,
2056    env: &Env,
2057) -> Result<Value, EvalError> {
2058    let mut text = String::new();
2059    for part in parts {
2060        match part {
2061            InterpolPart::Literal(content) => text.push_str(content.text()),
2062            InterpolPart::Interpolation(interpol) => {
2063                let expr = interpol.expr().ok_or_else(|| {
2064                    EvalError::ParseError("path interpolation missing expr".to_string())
2065                })?;
2066                let val = force_value(&eval_expr(&expr, env)?)?;
2067                // Plain coercion (coerceMore = false): a path-typed
2068                // interpolation splices the raw path string, never a
2069                // copied-to-store hash path.
2070                let (s, _ctx) = val.coerce_to_string()?;
2071                text.push_str(&s);
2072            }
2073        }
2074    }
2075    let resolved = match kind {
2076        // Relative path: resolve against the defining file's directory,
2077        // mirroring the plain `PathRel` branch.
2078        PathKind::Rel => {
2079            if let Some(dir) = current_eval_dir() {
2080                let norm = normalize_path(&dir.join(&text));
2081                // Lift cache→store exactly like the plain `PathRel` branch (the
2082                // store↔cache seam value-half). Without this, an interpolated
2083                // relative-path literal (`./${x}`, `./modules/${name}.nix`)
2084                // inside a fetched flake input yielded a Value::Path holding the
2085                // fetcher CACHE dir instead of the input's `/nix/store/<h>-source`
2086                // path — so its `toString`/copy-to-store/inputSrc diverged from
2087                // CppNix (the plain `./x` sibling already dematerializes; the two
2088                // must agree).
2089                crate::path::dematerialize(&norm).to_string_lossy().into_owned()
2090            } else {
2091                // No eval-file context (top-level `sui eval -E`): the
2092                // plain branch keeps the raw text, so match it — but the
2093                // interpolation is still spliced.
2094                text
2095            }
2096        }
2097        // Absolute paths: canonicalize the concatenated text CppNix's way.
2098        // The `${e}` splice routinely introduces a `//` seam (`/bar/` +
2099        // `/tmp/foo`) or a `.`/`..` component that must collapse
2100        // (`/bar//tmp/foo` → `/bar/tmp/foo`), and `..` must clamp at root.
2101        // `canon_abs` is filesystem-free (works on not-yet-materialized
2102        // flake paths) and root-aware (unlike `normalize_path`, which pops
2103        // past root — the marquee-root divergence).
2104        PathKind::Abs => crate::path::canon_abs(&text),
2105        // Home paths (`~/…`) carry a leading `~` component, so they are
2106        // not absolute-rooted; keep the pre-existing normalization.
2107        PathKind::Home => normalize_path(std::path::Path::new(&text))
2108            .to_string_lossy()
2109            .into_owned(),
2110    };
2111    Ok(Value::Path(Box::new(SmolStr::from(resolved.as_str()))))
2112}
2113
2114/// Which kind of interpolatable path literal — governs how the
2115/// concatenated text is finally resolved.
2116#[derive(Clone, Copy)]
2117enum PathKind {
2118    Abs,
2119    Rel,
2120    Home,
2121}
2122
2123/// Evaluate an attribute name, requiring non-null.
2124/// Use `eval_attr_maybe_null` when null dynamic attrs should be skipped.
2125fn eval_attr(attr: &ast::Attr, env: &Env) -> Result<String, EvalError> {
2126    eval_attr_maybe_null(attr, env)?
2127        .ok_or_else(|| EvalError::TypeError("null dynamic attribute name".into()))
2128}
2129
2130/// Evaluate an attribute name. Returns `None` for null dynamic attrs
2131/// (CppNix silently omits attributes with null names).
2132fn eval_attr_maybe_null(attr: &ast::Attr, env: &Env) -> Result<Option<String>, EvalError> {
2133    match attr {
2134        ast::Attr::Ident(ident) => Ok(Some(ident_text(ident))),
2135        ast::Attr::Dynamic(dyn_) => {
2136            let expr = dyn_
2137                .expr()
2138                .ok_or_else(|| EvalError::ParseError("dynamic attr missing expr".to_string()))?;
2139            let val = force_value(&eval_expr(&expr, env)?)?;
2140            // CppNix: null dynamic attr name → skip the attribute entirely.
2141            // Used by nixpkgs module system: `${if cond then null else "name"} = value;`
2142            if val == Value::Null {
2143                return Ok(None);
2144            }
2145            Ok(Some(val.as_string()?.to_string()))
2146        }
2147        ast::Attr::Str(s) => {
2148            let val = eval_str(s, env)?;
2149            Ok(Some(val.as_string()?.to_string()))
2150        }
2151    }
2152}
2153
2154/// Get the text of an rnix Ident node.
2155fn ident_text(ident: &ast::Ident) -> String {
2156    // Fast path: a `NODE_IDENT` holds a single `TOKEN_IDENT`, whose `text()`
2157    // borrows the source `&str` directly from the green node — no
2158    // `PreorderWithTokens` cursor tree-walk and none of the `NodeData::new`
2159    // allocations that `syntax().text()` (a `SyntaxText` over the node's whole
2160    // descendant span) pays. Byte-identical fallback: the identifier `or` is
2161    // lexed as a nested `TOKEN_OR` (rnix quirk), so `ident_token()` is `None`
2162    // there — walk the full node text in that case, exactly as before.
2163    match ident.ident_token() {
2164        Some(tok) => tok.text().to_string(),
2165        None => ident.syntax().text().to_string(),
2166    }
2167}
2168
2169/// Byte offset of a STATIC attr key (`Ident` or `Str`) in its source text —
2170/// the position `builtins.unsafeGetAttrPos` reports for that key. Returns
2171/// `None` for a dynamic key (`${e}`), which has no fixed source position.
2172///
2173/// CppNix points a binding's position at the KEY token's start; rnix exposes
2174/// it via the syntax node's `text_range().start()`.
2175fn static_attr_offset(attr: &ast::Attr) -> Option<u32> {
2176    let node = match attr {
2177        ast::Attr::Ident(i) => i.syntax(),
2178        ast::Attr::Str(s) => s.syntax(),
2179        ast::Attr::Dynamic(_) => return None,
2180    };
2181    Some(u32::from(node.text_range().start()))
2182}
2183
2184/// Collect a literal attrset's static top-level KEY offsets into an
2185/// [`crate::pos::AttrPositions`] and attach it to `attrs` (behind the value's
2186/// `Rc<AttrPositions>` slot). Records only single-key static bindings — the
2187/// shape `attrTag`'s `tags_` (`{ app = …; file = …; }`) is built from and the
2188/// only shape `builtins.unsafeGetAttrPos` reads in nixpkgs. `None`-costs a
2189/// pointer when the set has no such keys (attaches nothing).
2190fn attach_attrset_positions(set: &ast::AttrSet, attrs: &mut NixAttrs, env: &Env) {
2191    // The FILE is the one the literal is being built in — from the eval-file
2192    // stack, which a thunk restores to its captured file when it forces. This
2193    // is correct under laziness: a `dock.nix` attrset literal forced later
2194    // records `dock.nix`, not whatever file is top-of-stack at force time.
2195    // (`current_source_id`/`CURRENT_SOURCE_ID` is per-`eval_with_file`, NOT
2196    // per-env, so it would mis-attribute a lazily-forced literal.)
2197    let mut table = crate::pos::AttrPositions::new(current_eval_file());
2198    for entry in set.entries() {
2199        if let ast::Entry::AttrpathValue(apv) = entry {
2200            let Some(attrpath) = apv.attrpath() else { continue };
2201            let path_attrs: Vec<ast::Attr> = attrpath.attrs().collect();
2202            // Only a single-segment static key gets a position (a dotted path
2203            // `a.b = …` desugars to a nested set; CppNix points the position
2204            // at the head, and nixpkgs never `unsafeGetAttrPos`es a dotted
2205            // tag). Skip anything else.
2206            if path_attrs.len() != 1 {
2207                continue;
2208            }
2209            let Some(offset) = static_attr_offset(&path_attrs[0]) else { continue };
2210            // Resolve the static key name (Ident/Str) — never forces (a
2211            // dynamic key already returned None above).
2212            if let Ok(Some(name)) = eval_attr_maybe_null(&path_attrs[0], env) {
2213                table.insert(intern(&name), offset);
2214            }
2215        }
2216    }
2217    if !table.is_empty() {
2218        attrs.set_positions(std::rc::Rc::new(table));
2219    }
2220}
2221
2222fn eval_attrset(set: &ast::AttrSet, env: &Env) -> Result<Value, EvalError> {
2223    crate::perf::inc(crate::perf::Counter::Attrset);
2224    let mut attrs = NixAttrs::new();
2225    let is_rec = set.rec_token().is_some();
2226
2227    if is_rec {
2228        let mut rec_env = env.child();
2229        let mut thunks: Vec<(String, Thunk)> = Vec::new();
2230
2231        // Track which names have been defined so far in this scope.
2232        // Used by maybe_thunk to resolve backward references directly
2233        // instead of creating wasteful thunks.
2234        let mut defined_so_far: HashSet<String> = HashSet::new();
2235
2236        // Accumulator for dotted-path bindings (`rec { a.b = 1; a.c = 2; ... }`).
2237        // Leaf values are wrapped in thunks so they participate in the
2238        // recursive env fixpoint, matching CppNix semantics where
2239        // `rec { types.a = f 1; f = x: x + 1; }` allows `f` to be a
2240        // sibling binding.
2241        let mut dotted_attrs: NixAttrs = NixAttrs::new();
2242
2243        // Phase 1: Create thunks with placeholder env and bind them.
2244        for entry in set.entries() {
2245            match entry {
2246                ast::Entry::AttrpathValue(apv) => {
2247                    let attrpath = apv.attrpath().ok_or_else(|| {
2248                        EvalError::ParseError("binding missing attrpath".to_string())
2249                    })?;
2250                    let value_expr = apv.value().ok_or_else(|| {
2251                        EvalError::ParseError("binding missing value".to_string())
2252                    })?;
2253                    let mut path_keys: Vec<String> = attrpath
2254                        .attrs()
2255                        .filter_map(|a| eval_attr_maybe_null(&a, env).transpose())
2256                        .collect::<Result<_, _>>()?;
2257                    // Null dynamic attr name → skip entire binding (CppNix compat)
2258                    if path_keys.is_empty() { continue; }
2259                    if path_keys.len() == 1 {
2260                        let key = path_keys.pop().unwrap();
2261                        // Self-recursive detection in a `rec { … }` scope:
2262                        // any binding whose value-expr references the
2263                        // bound name OR any sibling key declared in this
2264                        // rec scope is potentially self-recursive (the
2265                        // siblings' thunks share the rec_env via Phase 2).
2266                        // Mark as recursive so inner re-entrance during
2267                        // force returns a Promise sentinel instead of
2268                        // erroring with InfiniteRecursion.
2269                        //
2270                        // For simplicity we check `key` and all already-
2271                        // defined siblings; siblings defined later are
2272                        // covered when THEIR thunks force (they reference
2273                        // back into this rec scope via Phase 2's env update).
2274                        // O(N) not O(N²): one memoized referenced-name set,
2275                        // intersected with key + already-defined siblings.
2276                        // Byte-identical to the prior per-name walks.
2277                        let referenced = referenced_idents(&value_expr);
2278                        let is_recursive_binding = referenced.contains(key.as_str())
2279                            || defined_so_far
2280                                .iter()
2281                                .any(|n| referenced.contains(n.as_str()));
2282                        let value = if is_recursive_binding {
2283                            Value::Thunk(Thunk::new_suspended_recursive(
2284                                value_expr.clone(),
2285                                env.clone(),
2286                            ))
2287                        } else {
2288                            // maybeThunk: skip thunk for trivial exprs.
2289                            // is_rec=true because rec attrset bindings
2290                            // can reference each other.
2291                            // Pass defined_so_far so backward refs
2292                            // resolve directly.
2293                            maybe_thunk(&value_expr, env, true, Some(&defined_so_far))
2294                        };
2295                        rec_env.bind(key.clone(), value.clone());
2296                        attrs.insert(key.clone(), value.clone());
2297                        if let Value::Thunk(t) = &value {
2298                            thunks.push((key.clone(), t.clone()));
2299                        }
2300                        defined_so_far.insert(key);
2301                    } else {
2302                        // Multi-segment dotted path: build a nested attrset
2303                        // with a thunk at the leaf so the value expression
2304                        // can reference sibling rec-bindings.
2305                        let key = path_keys[0].clone();
2306                        let value =
2307                            build_nested_attr_thunk(&path_keys[1..], &value_expr, env, &mut thunks);
2308                        merge_nested_insert(&mut dotted_attrs, key, value);
2309                    }
2310                }
2311                ast::Entry::Inherit(inherit) => {
2312                    eval_inherit(&inherit, env, &mut attrs, Some(&mut rec_env), Some(&mut thunks))?;
2313                }
2314            }
2315        }
2316
2317        // Phase 1b: Bind accumulated dotted-path attrs into attrs and rec_env.
2318        // Note: CppNix rejects `inherit (src) x; x.y = ...;` as a
2319        // duplicate definition, so we do not attempt to merge with
2320        // existing inherit thunks — just bind directly.
2321        for (key, value) in dotted_attrs.iter() {
2322            attrs.insert(key.clone(), value.clone());
2323            rec_env.bind(key.clone(), value.clone());
2324        }
2325
2326        // Phase 2: Update all thunks (both Suspended and InheritSelect)
2327        // to capture the final rec_env (which now has all names bound).
2328        for (_key, thunk) in &thunks {
2329            thunk.update_env(&rec_env);
2330        }
2331    } else {
2332        for entry in set.entries() {
2333            match entry {
2334                ast::Entry::AttrpathValue(apv) => {
2335                    let attrpath = apv.attrpath().ok_or_else(|| {
2336                        EvalError::ParseError("binding missing attrpath".to_string())
2337                    })?;
2338                    let value_expr = apv.value().ok_or_else(|| {
2339                        EvalError::ParseError("binding missing value".to_string())
2340                    })?;
2341                    let path_attrs: Vec<ast::Attr> = attrpath.attrs().collect();
2342                    // CppNix defers a dynamic key that is NOT at the HEAD of the
2343                    // attrpath: `{ a.${e} = v; }` builds `{ a = <thunk {${e}=v}>; }`,
2344                    // so `e` never forces until `.a` is demanded. Evaluating the
2345                    // whole path eagerly would force `e` at construction and — in
2346                    // the module-system fixpoint — read `config.<x>` while `config`
2347                    // is mid-force (the M2.6 divergence: `homes.null` instead of
2348                    // `homes.<name>`). Only the head is eager; a lone dynamic tail
2349                    // becomes a deferred thunk. A rarer collision under the same
2350                    // head stays eager (forced) so static deep-merge still works.
2351                    let tail_is_dynamic =
2352                        path_attrs.len() > 1 && attrs_have_dynamic(&path_attrs[1..]);
2353                    let head_key = match eval_attr_maybe_null(&path_attrs[0], env)? {
2354                        Some(k) => k,
2355                        // Null dynamic HEAD attr name → skip entire binding.
2356                        None => continue,
2357                    };
2358                    if tail_is_dynamic && attrs.get(&head_key).is_none() {
2359                        let value =
2360                            build_deferred_tail_attr(&path_attrs[1..], &value_expr, env);
2361                        attrs.insert(head_key, value);
2362                        continue;
2363                    }
2364                    // M2.6 ROOT #3 (collision case): the tail has a dynamic key
2365                    // AND the head already exists (a sibling binding wrote it,
2366                    // e.g. osquery's `systemd.services.… = …` then
2367                    // `systemd.tmpfiles.settings."10-osquery".${dirname …}.d`).
2368                    // The plain deferral above bails (head present), and the
2369                    // eager path below would force the dynamic key at
2370                    // construction — re-reading `config.<x>` mid-fixpoint →
2371                    // the empty-Promise partial. Instead, descend the existing
2372                    // head along the tail's STATIC prefix and splice a DEFERRED
2373                    // thunk at the first dynamic level, so the dynamic key
2374                    // stays lazy exactly as CppNix's nested-literal desugaring
2375                    // does — while preserving the static deep-merge with the
2376                    // sibling binding.
2377                    if tail_is_dynamic {
2378                        if let Some(existing) = attrs.get(&head_key).cloned() {
2379                            let merged = merge_deferred_dynamic_tail(
2380                                existing,
2381                                &path_attrs[1..],
2382                                &value_expr,
2383                                env,
2384                            )?;
2385                            attrs.insert(head_key, merged);
2386                            continue;
2387                        }
2388                    }
2389                    // Eager path: evaluate the remaining (static, or collision)
2390                    // keys now. A null dynamic tail key skips the binding.
2391                    let mut path_keys: Vec<String> = {
2392                        let mut v = Vec::with_capacity(path_attrs.len());
2393                        v.push(head_key);
2394                        let mut skip = false;
2395                        for a in &path_attrs[1..] {
2396                            match eval_attr_maybe_null(a, env)? {
2397                                Some(k) => v.push(k),
2398                                None => { skip = true; break; }
2399                            }
2400                        }
2401                        if skip { v.clear(); }
2402                        v
2403                    };
2404                    // Null dynamic attr name → skip entire binding (CppNix compat)
2405                    if path_keys.is_empty() { continue; }
2406                    if path_keys.len() == 1 {
2407                        let key = path_keys.pop().unwrap();
2408                        // maybeThunk: skip thunk for trivial exprs.
2409                        // is_rec=false — Ident lookups are safe.
2410                        let value = maybe_thunk(&value_expr, env, false, None);
2411                        // CppNix desugars `a.b = x; a = { c = y; };` into a single
2412                        // merged `a = { b = x; c = y; }` at parse time. rnix keeps
2413                        // the two bindings separate, so when a single-key binding
2414                        // collides with an already-built (dotted) attrs for the
2415                        // same key, deep-MERGE instead of overwrite. Force the RHS
2416                        // to WHNF so merge_nested_insert (which needs concrete
2417                        // Value::Attrs on both sides) can merge — forcing an
2418                        // attrset to WHNF does NOT force its fields, so leaf values
2419                        // stay lazy. Only fires on collision; non-colliding
2420                        // single-key bindings keep the plain fast insert.
2421                        // (This is the pkg-config-wrapper `env.addFlags` drop:
2422                        // `env.addFlags = …` then `env = { wrapperName = …; … }`.)
2423                        // If the earlier binding for this key is still a lazy
2424                        // Thunk (an attrset literal inserted via maybe_thunk), force
2425                        // it to WHNF FIRST so a `key = {..}; key = {..}` collision is
2426                        // seen as attrs-vs-attrs and MERGES, matching nix
2427                        // (`{ s = {a=1;}; s = {b=2;}; }` → `{ s = {a=1; b=2;}; }`).
2428                        // Without this the `Some(Value::Attrs(_))` test below is false
2429                        // on a Thunk and the second binding overwrites, dropping the
2430                        // first's keys. The dotted branch below already does this; R3
2431                        // (eval-okay-merge-dynamic-attrs set1/set2) needs it here too.
2432                        // WHNF force does not force fields → leaf laziness preserved.
2433                        // (A non-attrs dup like `s = 1; s = 2` still overwrites here,
2434                        // unchanged — nix errors there, an eval-FAIL case out of scope.)
2435                        if matches!(attrs.get(&key), Some(Value::Thunk(_))) {
2436                            let existing = attrs.get(&key).cloned().unwrap();
2437                            let forced_existing = force_value(&existing)?;
2438                            attrs.insert(key.clone(), forced_existing);
2439                        }
2440                        if matches!(attrs.get(&key), Some(Value::Attrs(_))) {
2441                            let forced = force_value(&value)?;
2442                            merge_nested_insert(&mut attrs, key, forced);
2443                        } else {
2444                            attrs.insert(key, value);
2445                        }
2446                    } else {
2447                        let key = path_keys[0].clone();
2448                        let value = build_nested_attr(&path_keys[1..], &value_expr, env)?;
2449                        // CppNix desugars `a = { x = …; }; a.y = …;` into a
2450                        // single merged `a = { x = …; y = …; }`. When the
2451                        // full-set binding for `a` was inserted FIRST it is a
2452                        // lazy Thunk (attrset literals go through maybe_thunk),
2453                        // so merge_nested_insert — which only merges when the
2454                        // existing value is a concrete Value::Attrs — would
2455                        // NOT see the earlier keys and would overwrite `a`
2456                        // with just `{ y = … }`, silently dropping `x`. Force
2457                        // the existing entry to WHNF on collision so the merge
2458                        // sees the concrete attrs (forcing to WHNF does not
2459                        // force the fields, so leaf laziness is preserved).
2460                        // (This is the gst-plugins-base `passthru.waylandEnabled`
2461                        // drop: `passthru = { … }; passthru.tests.x = …;`.)
2462                        if matches!(attrs.get(&key), Some(Value::Thunk(_))) {
2463                            let existing = attrs.get(&key).cloned().unwrap();
2464                            let forced = force_value(&existing)?;
2465                            attrs.insert(key.clone(), forced);
2466                        }
2467                        merge_nested_insert(&mut attrs, key, value);
2468                    }
2469                }
2470                ast::Entry::Inherit(inherit) => {
2471                    eval_inherit(&inherit, env, &mut attrs, None, None)?;
2472                }
2473            }
2474        }
2475    }
2476
2477    // Record the literal's static-key source positions for
2478    // `builtins.unsafeGetAttrPos` (the `attrTag` `declarations` — options.json
2479    // dock root). Cheap: one entry walk over static Ident/Str keys, no
2480    // forcing; attaches nothing (a pointer-sized `None`) when the set has no
2481    // single-static-key bindings.
2482    attach_attrset_positions(set, &mut attrs, env);
2483
2484    Ok(Value::Attrs(Rc::new(attrs)))
2485}
2486
2487fn eval_inherit(
2488    inherit: &ast::Inherit,
2489    env: &Env,
2490    attrs: &mut NixAttrs,
2491    bind_env: Option<&mut Env>,
2492    mut thunks: Option<&mut Vec<(String, Thunk)>>,
2493) -> Result<(), EvalError> {
2494    if let Some(from) = inherit.from() {
2495        // inherit (expr) a b c;
2496        //
2497        // The source expression must NOT be eagerly evaluated. nixpkgs
2498        // `lib/trivial.nix` has `inherit (lib.trivial) isFunction ...`
2499        // at the top of a file that itself defines `lib.trivial`. If
2500        // we eagerly force `lib.trivial`, we hit a self-referential
2501        // thunk blackhole. Instead: build a thunk per inherited
2502        // name that, when forced, evaluates the source and pulls
2503        // out that one attribute. This is what real Nix does.
2504        //
2505        // For `rec { inherit (X) name; ...; foo = name; }` we ALSO
2506        // need to bind the name in the enclosing rec env so the
2507        // sibling `foo = name` can reference it. The caller passes
2508        // its rec env in `bind_env`.
2509        //
2510        // When `thunks` is provided (rec attrsets), InheritSelect
2511        // thunks are collected so Phase 2 can update their captured
2512        // env to the full recursive scope. Without this, the source
2513        // expression cannot reference sibling bindings.
2514        let source_expr = from
2515            .expr()
2516            .ok_or_else(|| EvalError::ParseError("inherit from missing expr".to_string()))?;
2517        // Shared source thunk — all inherited names share one source
2518        // evaluation (the source thunk's own memoization ensures at
2519        // most one evaluation).
2520        let source_thunk = Thunk::new_suspended(source_expr, env.clone());
2521        let mut be = bind_env;
2522        for attr in inherit.attrs() {
2523            let name = eval_attr(&attr, env)?;
2524            let thunk = Thunk::new_inherit_select(source_thunk.clone(), name.clone());
2525            let value = Value::Thunk(thunk.clone());
2526            attrs.insert(name.clone(), value.clone());
2527            if let Some(ref mut e) = be {
2528                e.bind(name.clone(), value);
2529            }
2530            if let Some(ref mut t) = thunks {
2531                t.push((name, thunk));
2532            }
2533        }
2534    } else {
2535        // inherit a b c;
2536        //
2537        // CppNix resolves a bare `inherit x;` LAZILY, exactly like a plain
2538        // reference to `x` — it does NOT eagerly force the enclosing scope.
2539        // This matters when `x` is provided only by an enclosing `with`
2540        // scope whose value is a fixpoint still being constructed (a
2541        // blackhole): eager `env.lookup` returns None → spurious
2542        // `UndefinedVar`. nixpkgs `all-packages.nix` is
2543        // `… with pkgs; { nettle = import … { inherit callPackage; }; }`,
2544        // so `inherit callPackage` must resolve `callPackage` from the
2545        // `with pkgs` scope AT FORCE TIME, not eagerly at attrset
2546        // construction. Mirror `maybe_thunk`'s Ident path: try the fast
2547        // lookup, and on a miss defer to a WithIdent thunk (or a suspended
2548        // env lookup) so the resolution happens lazily against the settled
2549        // scope. (This was the `nettle` UndefinedVar('callPackage') drop.)
2550        let mut be = bind_env;
2551        for attr in inherit.attrs() {
2552            let name = eval_attr(&attr, env)?;
2553            let sym = crate::value::intern(&name);
2554            let value = if let Some(v) = env.lookup_fast(sym, &name) {
2555                v
2556            } else if let Some((scope_cache, scope_value)) =
2557                env.innermost_with_scope()
2558            {
2559                Value::Thunk(Thunk::new_with_ident(
2560                    SmolStr::from(name.as_str()),
2561                    scope_cache,
2562                    scope_value,
2563                    env.clone(),
2564                ))
2565            } else {
2566                return Err(EvalError::UndefinedVar(format!(
2567                    "'{name}'{}",
2568                    eval_file_ctx()
2569                )));
2570            };
2571            attrs.insert(name.clone(), value.clone());
2572            if let Some(ref mut e) = be {
2573                e.bind(name, value);
2574            }
2575        }
2576    }
2577    Ok(())
2578}
2579
2580fn build_nested_attr(
2581    path: &[String],
2582    expr: &ast::Expr,
2583    env: &Env,
2584) -> Result<Value, EvalError> {
2585    if path.is_empty() {
2586        // CRITICAL: Wrap leaf in a thunk instead of eagerly evaluating.
2587        // For dotted paths like `config.warnings = optionals config.x [...]`,
2588        // the leaf expression must be lazy — eagerly evaluating it during
2589        // attrset construction forces fixpoint thunks prematurely.
2590        return Ok(maybe_thunk(expr, env, false, None));
2591    }
2592    let key = path[0].clone();
2593    let inner = build_nested_attr(&path[1..], expr, env)?;
2594    let mut attrs = NixAttrs::new();
2595    attrs.insert(key, inner);
2596    Ok(Value::Attrs(Rc::new(attrs)))
2597}
2598
2599/// True if a single attr is a DYNAMIC key — one whose resolution runs
2600/// arbitrary expression code and therefore must not be forced at
2601/// attrset-construction time.
2602///
2603/// Two forms are dynamic:
2604///   * `ast::Attr::Dynamic` — a bare `${e}` antiquotation.
2605///   * `ast::Attr::Str` **containing an interpolation** — an interpolated
2606///     string key like `"iwd/${nm}"`.  A `Str` with NO interpolation
2607///     (`"foo bar"`) is a plain static string literal and is NOT dynamic.
2608///
2609/// M2.6 ROOT #3: `attrs_have_dynamic` previously matched ONLY
2610/// `Attr::Dynamic`, so an interpolated-string tail key (`config.a."p${e}"`)
2611/// fell to the eager path and forced `e` at construction.  In the module
2612/// system that forces a `config.<x>` read while `config` is mid-fixpoint
2613/// (`environment.etc."iwd/${configFile.name}"`, where `configFile` reads
2614/// `with config.networking.networkmanager`), yielding the empty-Promise
2615/// partial → the `set/null` softening.  Treating an interpolated `Str` as
2616/// dynamic routes it through the same per-level deferral as `${e}`
2617/// (ROOT #1/#2), so `e` forces only when the enclosing head is demanded —
2618/// exactly CppNix's nested-attrset-literal desugaring.
2619fn attr_is_dynamic(attr: &ast::Attr) -> bool {
2620    match attr {
2621        ast::Attr::Dynamic(_) => true,
2622        // A string attr key is dynamic iff it has ≥1 interpolation part;
2623        // a purely-literal string key forces nothing and stays eager.
2624        ast::Attr::Str(s) => s
2625            .normalized_parts()
2626            .iter()
2627            .any(|p| matches!(p, InterpolPart::Interpolation(_))),
2628        ast::Attr::Ident(_) => false,
2629    }
2630}
2631
2632/// True if any attr in the slice is a dynamic (interpolated) key.
2633///
2634/// A dynamic key beyond the HEAD of an attrpath must NOT be evaluated at
2635/// attrset-construction time — CppNix defers it inside the head's lazy
2636/// value, so `{ a.${e} = v; }` never forces `e` until `.a` is demanded.
2637/// Static string/ident keys are cheap and force nothing, so they don't
2638/// need deferral.
2639fn attrs_have_dynamic(attrs: &[ast::Attr]) -> bool {
2640    attrs.iter().any(attr_is_dynamic)
2641}
2642
2643/// Build the nested attrset for the TAIL of an attrpath, deferring
2644/// evaluation of dynamic tail keys until the value is forced.
2645///
2646/// Given tail attrs `[b, ${e}, c]` and a value expr, produce a lazy
2647/// `Value::Thunk` that, when forced, evaluates each tail key (including
2648/// the dynamic `${e}`) against `env` and builds `{ b = { ${e} = { c =
2649/// <leaf-thunk> }; }; }`. This mirrors CppNix: the inner attrset (and
2650/// thus its dynamic keys) is constructed only when the enclosing head
2651/// attribute is demanded — never at construction of the outer attrset.
2652///
2653/// A dynamic key that evaluates to `null` skips the whole binding
2654/// (returns an empty attrset), matching CppNix's null-dynamic-attr rule.
2655fn build_deferred_tail_attr(
2656    tail: &[ast::Attr],
2657    value_expr: &ast::Expr,
2658    env: &Env,
2659) -> Value {
2660    let tail: Vec<ast::Attr> = tail.to_vec();
2661    let value_expr = value_expr.clone();
2662    let env = env.clone();
2663    Value::Thunk(Thunk::new_native(move || {
2664        build_tail_attrs_now(&tail, &value_expr, &env)
2665    }))
2666}
2667
2668/// Resolve ONE level of the deferred attrpath tail — used from inside
2669/// the deferred thunk above once the enclosing head is demanded.
2670///
2671/// M2.6 ROOT #2 (the OVER-FORCE fix): this resolves *only* `tail[0]`'s
2672/// key and wraps the remaining tail `tail[1..]` in another DEFERRED
2673/// thunk — it does NOT recurse eagerly through the whole tail. This is
2674/// exactly CppNix's desugaring of `a.b.c = v` into nested attrset
2675/// literals `a = { b = { c = v; }; }`, where forcing `a` to WHNF yields
2676/// `{ b = <thunk {c=v}> }` — the inner level (`b`, and any dynamic key
2677/// under it) stays lazy until `.b` is demanded.
2678///
2679/// Forcing the enclosing head therefore resolves ONE tail key, never
2680/// the whole chain: `config.homes.${cfg.pleme.userName} = 7` demanded
2681/// as `config` yields `{ homes = <deferred> }` WITHOUT forcing the
2682/// `${cfg.pleme.userName}` key. The prior implementation recursed the
2683/// whole tail eagerly, forcing that dynamic key while only `.config`
2684/// (or its `._type`) was demanded — the over-force cppnix never does.
2685///
2686/// A dynamic key that evaluates to `null` skips the whole binding
2687/// (returns an empty attrset), matching CppNix's null-dynamic-attr rule.
2688fn build_tail_attrs_now(
2689    tail: &[ast::Attr],
2690    value_expr: &ast::Expr,
2691    env: &Env,
2692) -> Result<Value, EvalError> {
2693    if tail.is_empty() {
2694        return Ok(maybe_thunk(value_expr, env, false, None));
2695    }
2696    if std::env::var_os("SUI_M26_TAILTRACE").is_some() {
2697        let t: String = tail[0].syntax().text().to_string().chars().take(40).collect();
2698        eprintln!("[M26 TAIL-RESOLVE] forcing dynamic tail key `{t}`");
2699        if attrs_have_dynamic(&tail[..1]) {
2700            crate::trace::dump_force_stack_ids();
2701        }
2702    }
2703    let key = match eval_attr_maybe_null(&tail[0], env)? {
2704        Some(k) => k,
2705        // Null dynamic key → the whole binding is skipped; an empty
2706        // attrset is the identity for merge_nested_insert.
2707        None => return Ok(Value::Attrs(Rc::new(NixAttrs::new()))),
2708    };
2709    // Resolve ONE level: if more tail remains, defer it (a new lazy
2710    // thunk) rather than recursing eagerly. Only the leaf (empty tail)
2711    // is built here. This keeps each nested level lazy, exactly like
2712    // CppNix's nested-attrset-literal desugaring — so forcing this
2713    // level does NOT force the next level's (possibly dynamic) key.
2714    let inner = if tail.len() == 1 {
2715        maybe_thunk(value_expr, env, false, None)
2716    } else {
2717        build_deferred_tail_attr(&tail[1..], value_expr, env)
2718    };
2719    let mut attrs = NixAttrs::new();
2720    attrs.insert(key, inner);
2721    Ok(Value::Attrs(Rc::new(attrs)))
2722}
2723
2724/// M2.6 ROOT #3 (collision case): splice a DEFERRED dynamic-tail binding
2725/// into an ALREADY-PRESENT head value without forcing the dynamic key.
2726///
2727/// `existing` is the value already stored at the attrpath's head (written
2728/// by a sibling binding — e.g. `systemd.services.… = …`). `tail` is the
2729/// remaining attrpath (`path_attrs[1..]`) of the new binding, which
2730/// contains ≥1 dynamic attr (`systemd.tmpfiles.….${dirname …}.d`).
2731///
2732/// We descend `existing` along the LONGEST STATIC PREFIX of `tail`
2733/// (`tmpfiles`, `settings`, `"10-osquery"` — all static, forced-free
2734/// keys), forcing each already-present sub-attrset to WHNF so the merge
2735/// sees concrete keys (forcing to WHNF never forces leaf VALUES, so leaf
2736/// laziness is preserved), and at the first DYNAMIC level splice a
2737/// `build_deferred_tail_attr` thunk. The dynamic key therefore forces
2738/// only when that exact nested path is later demanded — CppNix's
2739/// nested-attrset-literal desugaring, now honoured through a sibling
2740/// collision too.
2741fn merge_deferred_dynamic_tail(
2742    existing: Value,
2743    tail: &[ast::Attr],
2744    value_expr: &ast::Expr,
2745    env: &Env,
2746) -> Result<Value, EvalError> {
2747    // `tail` is non-empty and contains a dynamic attr somewhere (the
2748    // caller guarantees `attrs_have_dynamic(tail)`).
2749    debug_assert!(!tail.is_empty());
2750
2751    // If the FIRST tail attr is itself dynamic, there is no static prefix
2752    // to descend — the whole tail is deferred and merged as a lazy
2753    // overlay onto the existing head (a `//`-style right-merge; the
2754    // deferred attrset only materialises its dynamic key on demand).
2755    if attr_is_dynamic(&tail[0]) {
2756        let deferred = build_deferred_tail_attr(tail, value_expr, env);
2757        return Ok(lazy_overlay_merge(existing, deferred));
2758    }
2759
2760    // The head static key of `tail`. Resolve it (static → forces nothing
2761    // relevant; a null dynamic can't occur here since tail[0] is static).
2762    let key = match eval_attr_maybe_null(&tail[0], env)? {
2763        Some(k) => k,
2764        None => return Ok(existing),
2765    };
2766
2767    // Force the existing head to a concrete attrset so we can descend +
2768    // merge on the resolved static key. Forcing to WHNF does NOT force
2769    // its field VALUES, so leaf laziness is preserved.
2770    let existing_forced = force_value(&existing)?;
2771    let mut base = match existing_forced {
2772        Value::Attrs(a) => (*a).clone(),
2773        // The existing head is not an attrset (a sibling wrote a leaf
2774        // here); CppNix would error on the merge, but to stay lazy we
2775        // defer the tail and let a later demand surface the real merge
2776        // conflict. Build the deferred tail as a fresh attrset.
2777        _ => {
2778            let deferred = build_deferred_tail_attr(tail, value_expr, env);
2779            return Ok(deferred);
2780        }
2781    };
2782
2783    // Recurse: merge the REMAINING tail (`tail[1..]`) under `key`.
2784    let child_existing = base.get(&key).cloned();
2785    let new_child = match child_existing {
2786        Some(child) if tail.len() > 1 => {
2787            // Deeper static/dynamic prefix under an existing sub-attrset.
2788            merge_deferred_dynamic_tail(child, &tail[1..], value_expr, env)?
2789        }
2790        Some(child) => {
2791            // tail == [key]; the leaf collides with an existing value.
2792            // Static leaf collision — build the leaf and lazy-merge.
2793            let leaf = maybe_thunk(value_expr, env, false, None);
2794            lazy_overlay_merge(child, leaf)
2795        }
2796        None if tail.len() > 1 => {
2797            // No existing child; the remaining tail may itself start with
2798            // a dynamic key — defer it whole (build_deferred_tail_attr
2799            // handles the static/dynamic split per-level).
2800            build_deferred_tail_attr(&tail[1..], value_expr, env)
2801        }
2802        None => maybe_thunk(value_expr, env, false, None),
2803    };
2804    base.insert(key, new_child);
2805    Ok(Value::Attrs(Rc::new(base)))
2806}
2807
2808/// Lazy right-merge of two values that are (or will force to) attrsets,
2809/// preserving leaf laziness. Used by [`merge_deferred_dynamic_tail`] to
2810/// combine a deferred dynamic-tail attrset with an existing value without
2811/// forcing either's dynamic keys eagerly. When both are concrete attrs we
2812/// deep-merge in place (reusing [`merge_nested_insert`]); otherwise we
2813/// build a lazy overlay thunk that merges on demand.
2814fn lazy_overlay_merge(left: Value, right: Value) -> Value {
2815    match (&left, &right) {
2816        (Value::Attrs(la), Value::Attrs(_)) => {
2817            crate::perf::inc(crate::perf::Counter::SlashDeferredTailClone);
2818            let mut merged = (**la).clone();
2819            if let Value::Attrs(ra) = &right {
2820                // Merging distinct override keys into `merged` is order-
2821                // independent (per-key right-wins), and the result map is
2822                // unordered storage — the sorted `iter()` was dead work.
2823                for (k, v) in ra.iter_unsorted() {
2824                    merge_nested_insert(&mut merged, k.clone(), v.clone());
2825                }
2826            }
2827            Value::Attrs(Rc::new(merged))
2828        }
2829        _ => {
2830            // At least one side is a thunk (a deferred dynamic tail).
2831            // Defer the merge behind a Native thunk so neither side's
2832            // dynamic key forces until the merged attrset is demanded.
2833            Value::Thunk(Thunk::new_native(move || {
2834                let lf = force_value(&left)?;
2835                let rf = force_value(&right)?;
2836                let la = lf.as_attrs()?;
2837                let ra = rf.as_attrs()?;
2838                crate::perf::inc(crate::perf::Counter::SlashDeferredTailClone);
2839                let mut merged = (*la).clone();
2840                for (k, v) in ra.iter_unsorted() {
2841                    merge_nested_insert(&mut merged, k.clone(), v.clone());
2842                }
2843                Ok(Value::Attrs(Rc::new(merged)))
2844            }))
2845        }
2846    }
2847}
2848
2849/// Like [`build_nested_attr`] but wraps the leaf in a [`Thunk`] instead of
2850/// eagerly evaluating it. Used inside `rec { ... }` and `let ... in` so
2851/// that dotted-path leaf expressions can reference sibling bindings
2852/// through the recursive env (which is finalised in Phase 2).
2853///
2854/// Every thunk created is appended to `thunks` so Phase 2 can update
2855/// its captured environment.
2856fn build_nested_attr_thunk(
2857    path: &[String],
2858    expr: &ast::Expr,
2859    env: &Env,
2860    thunks: &mut Vec<(String, Thunk)>,
2861) -> Value {
2862    if path.is_empty() {
2863        let thunk = Thunk::new_suspended(expr.clone(), env.clone());
2864        let val = Value::Thunk(thunk.clone());
2865        thunks.push((String::new(), thunk));
2866        return val;
2867    }
2868    let key = path[0].clone();
2869    let inner = build_nested_attr_thunk(&path[1..], expr, env, thunks);
2870    let mut attrs = NixAttrs::new();
2871    attrs.insert(key, inner);
2872    Value::Attrs(Rc::new(attrs))
2873}
2874
2875/// Insert `value` at `key` in `target`. If `target` already has a
2876/// concrete `Value::Attrs` at that key AND `value` is also a
2877/// concrete `Value::Attrs`, deep-merge them rather than overwriting.
2878/// This is what makes `{ a.b.c = 1; a.b.d = 2; a.e = 3; }` produce
2879/// `{ a = { b = { c = 1; d = 2; }; e = 3; }; }` instead of
2880/// dropping siblings — every nixpkgs module relies on this.
2881fn merge_nested_insert(target: &mut NixAttrs, key: String, value: Value) {
2882    // Fast path: no existing entry at this key → plain insert, keeping the
2883    // value lazy (the overwhelmingly common non-colliding case, so we never
2884    // force a thunk here).
2885    let existing = match target.get(&key) {
2886        Some(e) => e.clone(),
2887        None => {
2888            target.insert(key, value);
2889            return;
2890        }
2891    };
2892    // A collision exists.  A deep merge is warranted only when BOTH the
2893    // existing entry AND the new value are attrset-shaped.  M2.6 ROOT #4b
2894    // (byte-verified): either side may be a lazy `Thunk` wrapping a
2895    // full-set leaf — both dotted-path orderings hit this:
2896    //   forward  `o.a = { x = 1; }; o.a.y = 2;` → EXISTING `a` is a thunk
2897    //            (`build_nested_attr` puts the `{x=1}` leaf through
2898    //            `maybe_thunk`), NEW `a` is `{ y = … }`;
2899    //   reverse  `o.a.y = 2; o.a = { x = 1; };` → EXISTING `a` is `{y}`,
2900    //            NEW `a` is the `<thunk {x=1}>`.
2901    // The old `should_merge` required BOTH sides to already be concrete
2902    // `Value::Attrs`, so a Thunk-vs-Attrs collision fell to the overwrite
2903    // path and silently dropped the earlier leaf's keys.  cppnix desugars
2904    // BOTH orderings into one merged `o.a = { x = 1; y = 2; }`.  Force each
2905    // side's thunk to WHNF ON COLLISION ONLY (forcing an attrset to WHNF
2906    // does NOT force its fields, so leaf laziness is preserved); a thunk
2907    // that forces to a non-attrset (or errors) makes the merge a plain
2908    // overwrite (leaf last-write-wins).
2909    // Symptom this closes: nixpkgs' alsa module declares
2910    // `options.hardware.alsa = { enable = …; cardAliases = …; … }` AND
2911    // `options.hardware.alsa.enablePersistence = …`; sui merged them to
2912    // only `{enablePersistence}`, so `hardware.alsa.cardAliases` "does not
2913    // exist" — the M2.6 frontier once the `with`-namespace over-force (#4a)
2914    // was fixed.
2915    let value = match value {
2916        Value::Thunk(_) => match force_value(&value) {
2917            Ok(v @ Value::Attrs(_)) => v,
2918            _ => value,
2919        },
2920        other => other,
2921    };
2922    if !matches!(value, Value::Attrs(_)) {
2923        target.insert(key, value);
2924        return;
2925    }
2926    // Normalize the existing side to concrete attrs too (forcing a thunk
2927    // to WHNF if needed); if it isn't attrset-shaped, the new attrs wins.
2928    let existing_concrete = match &existing {
2929        Value::Attrs(_) => existing.clone(),
2930        Value::Thunk(_) => match force_value(&existing) {
2931            Ok(v @ Value::Attrs(_)) => v,
2932            _ => {
2933                target.insert(key, value);
2934                return;
2935            }
2936        },
2937        _ => {
2938            target.insert(key, value);
2939            return;
2940        }
2941    };
2942    // Both sides are concrete attrs — merge in place. We pop the
2943    // existing entry, then walk the new attrs and recursively
2944    // merge each child onto it.
2945    let mut existing_attrs = match existing_concrete {
2946        Value::Attrs(a) => (*a).clone(),
2947        _ => unreachable!(),
2948    };
2949    let new_attrs = match value {
2950        Value::Attrs(ref a) => a,
2951        _ => unreachable!(),
2952    };
2953    for (k, v) in new_attrs.iter_unsorted() {
2954        merge_nested_insert(&mut existing_attrs, k.clone(), v.clone());
2955    }
2956    target.insert(key, Value::Attrs(Rc::new(existing_attrs)));
2957}
2958
2959/// Evaluate entries from any HasEntry node (LegacyLet).
2960fn eval_entries<N: HasEntry + AstNode>(node: &N, env: &mut Env) -> Result<(), EvalError> {
2961    for entry in node.entries() {
2962        match entry {
2963            ast::Entry::AttrpathValue(apv) => {
2964                let attrpath = apv.attrpath().ok_or_else(|| {
2965                    EvalError::ParseError("binding missing attrpath".to_string())
2966                })?;
2967                let value_expr = apv.value().ok_or_else(|| {
2968                    EvalError::ParseError("binding missing value".to_string())
2969                })?;
2970                let mut path_keys: Vec<String> = attrpath
2971                    .attrs()
2972                    .map(|a| eval_attr(&a, env))
2973                    .collect::<Result<_, _>>()?;
2974                if path_keys.len() == 1 {
2975                    let key = path_keys.pop().unwrap();
2976                    let value = eval_expr(&value_expr, env)?;
2977                    env.bind(key, value);
2978                }
2979                // Multi-key paths in let are not standard; skip for now.
2980            }
2981            ast::Entry::Inherit(inherit) => {
2982                if let Some(from) = inherit.from() {
2983                    let source_expr = from.expr().ok_or_else(|| {
2984                        EvalError::ParseError("inherit from missing expr".to_string())
2985                    })?;
2986                    let source = force_value(&eval_expr(&source_expr, env)?)?;
2987                    let source_attrs = source.as_attrs()?;
2988                    for attr in inherit.attrs() {
2989                        let name = eval_attr(&attr, env)?;
2990                        let value = source_attrs
2991                            .get(&name)
2992                            .cloned()
2993                            .ok_or_else(|| EvalError::AttrNotFound(
2994                                format!("'{name}' in inherit{}", eval_file_ctx()),
2995                            ))?;
2996                        env.bind(name, value);
2997                    }
2998                } else {
2999                    for attr in inherit.attrs() {
3000                        let name = eval_attr(&attr, env)?;
3001                        let value = env
3002                            .lookup(&name)
3003                            .ok_or_else(|| EvalError::UndefinedVar(
3004                                format!("'{name}'{}", eval_file_ctx()),
3005                            ))?;
3006                        env.bind(name, value);
3007                    }
3008                }
3009            }
3010        }
3011    }
3012    Ok(())
3013}
3014
3015fn eval_binop(
3016    op: ast::BinOpKind,
3017    lhs: &ast::Expr,
3018    rhs: &ast::Expr,
3019    env: &Env,
3020) -> Result<Value, EvalError> {
3021    // Short-circuit for && and ||
3022    match op {
3023        ast::BinOpKind::And => {
3024            let l = force_value(&eval_expr(lhs, env)?)?.as_bool()?;
3025            if !l {
3026                return Ok(Value::Bool(false));
3027            }
3028            return eval_expr(rhs, env);
3029        }
3030        ast::BinOpKind::Or => {
3031            let l = force_value(&eval_expr(lhs, env)?)?.as_bool()?;
3032            if l {
3033                return Ok(Value::Bool(true));
3034            }
3035            return eval_expr(rhs, env);
3036        }
3037        ast::BinOpKind::Implication => {
3038            let l = force_value(&eval_expr(lhs, env)?)?.as_bool()?;
3039            if !l {
3040                return Ok(Value::Bool(true));
3041            }
3042            return eval_expr(rhs, env);
3043        }
3044        _ => {}
3045    }
3046
3047    let lc = force_concrete(&eval_expr(lhs, env)?)?;
3048    let rc = force_concrete(&eval_expr(rhs, env)?)?;
3049    // Consume the Concretes (move, don't clone) so `l`/`r` hold the sole Rc to
3050    // any heap payload. This is byte-neutral — `into_value` yields the identical
3051    // `Value` as `to_value` — but it drops `lc`/`rc`, which is what lets the
3052    // `Concat` arm's structural-share fast path see a uniquely-owned left list
3053    // for a fresh `++` temporary (`Rc::try_unwrap` → append in place). Keeping
3054    // `lc` alive via `to_value` pinned the refcount at ≥2 and defeated reuse.
3055    let l = lc.into_value();
3056    let r = rc.into_value();
3057
3058    match op {
3059        ast::BinOpKind::Add => match (&l, &r) {
3060            (Value::Int(a), Value::Int(b)) => a
3061                .checked_add(*b)
3062                .map(Value::Int)
3063                .ok_or_else(|| int_overflow("adding", *a, '+', *b)),
3064            (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)),
3065            (Value::Int(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)),
3066            (Value::Float(a), Value::Int(b)) => Ok(Value::Float(a + *b as f64)),
3067            (Value::String(a), Value::String(b)) => {
3068                let mut ctx = a.context.clone();
3069                ctx.merge(&b.context);
3070                // Byte-identical to `format!("{}{}", a.chars, b.chars)` but
3071                // routes around the `core::fmt` runtime (its dispatch was the
3072                // #1 self-time frame on the string-concat hot path): a single
3073                // exact-capacity `String` + two `push_str` reserves the final
3074                // size once, so the left operand is copied exactly once instead
3075                // of copied-then-regrown. Result string + context unchanged →
3076                // ByteSufficient. (Also removes a `format!` — TYPED EMISSION.)
3077                let mut s = String::with_capacity(a.chars.len() + b.chars.len());
3078                s.push_str(&a.chars);
3079                s.push_str(&b.chars);
3080                Ok(Value::String(Rc::new(NixString::with_context(s, ctx))))
3081            }
3082            (Value::Path(a), Value::String(b)) => Ok(Value::Path(Box::new(SmolStr::from(format!("{a}{}", b.chars).as_str())))),
3083            (Value::Path(a), Value::Path(b)) => Ok(Value::Path(Box::new(SmolStr::from(format!("{a}/{b}").as_str())))),
3084            // CppNix coerces attrsets with outPath when used with +
3085            (Value::Attrs(_), _) | (_, Value::Attrs(_)) => {
3086                let (ls, lctx) = l.coerce_to_string()?;
3087                let (rs, rctx) = r.coerce_to_string()?;
3088                let mut ctx = lctx;
3089                ctx.merge(&rctx);
3090                Ok(Value::String(Rc::new(NixString::with_context(
3091                    format!("{ls}{rs}"),
3092                    ctx,
3093                ))))
3094            }
3095            _ => Err(EvalError::op_type("add", l.type_name(), r.type_name())),
3096        },
3097        ast::BinOpKind::Sub => num_op(
3098            &l,
3099            &r,
3100            |a, b| a.checked_sub(b),
3101            |a, b| a - b,
3102            |a, b| int_overflow("subtracting", a, '-', b),
3103        ),
3104        ast::BinOpKind::Mul => num_op(
3105            &l,
3106            &r,
3107            |a, b| a.checked_mul(b),
3108            |a, b| a * b,
3109            |a, b| int_overflow("multiplying", a, '*', b),
3110        ),
3111        ast::BinOpKind::Div => {
3112            // CppNix rejects division by zero for both int and float
3113            // operands; Rust's native int-div-by-0 panics (we handle
3114            // that below) but float-div-by-0 silently returns `inf`
3115            // or `NaN`, which sui was then serializing as `null` —
3116            // an invisible silent-Ok bug surfaced by the error-case
3117            // differential corpus.
3118            //
3119            // Cover every zero-denominator case explicitly.
3120            let rhs_is_zero = match &r {
3121                Value::Int(0) => true,
3122                Value::Float(f) => *f == 0.0,
3123                _ => false,
3124            };
3125            if rhs_is_zero {
3126                return Err(EvalError::DivisionByZero);
3127            }
3128            num_op(
3129                &l,
3130                &r,
3131                |a, b| a.checked_div(b),
3132                |a, b| a / b,
3133                |a, b| int_overflow("dividing", a, '/', b),
3134            )
3135        }
3136        ast::BinOpKind::Equal => Ok(Value::Bool(l == r)),
3137        ast::BinOpKind::NotEqual => Ok(Value::Bool(l != r)),
3138        ast::BinOpKind::Less => compare(&l, &r, |o| o == std::cmp::Ordering::Less),
3139        ast::BinOpKind::LessOrEq => compare(&l, &r, |o| o != std::cmp::Ordering::Greater),
3140        ast::BinOpKind::More => compare(&l, &r, |o| o == std::cmp::Ordering::Greater),
3141        ast::BinOpKind::MoreOrEq => compare(&l, &r, |o| o != std::cmp::Ordering::Less),
3142        ast::BinOpKind::Update => {
3143            let la = l.to_attrs()?;
3144            let ra = r.to_attrs()?;
3145            // O(1) lazy overlay — defers merge until attribute access.
3146            Ok(Value::Attrs(Rc::new(la.overlay(ra))))
3147        }
3148        ast::BinOpKind::Concat => {
3149            // Structural-share fast path: when the left operand's `Rc<Vec>` is
3150            // uniquely owned (a fresh temporary, as in a left-associative `++`
3151            // fold `acc ++ [x]`), append the right elements IN PLACE instead of
3152            // cloning the whole accumulator. This turns an O(n) copy per concat
3153            // into amortized O(1), byte-identically — the result is the same
3154            // ordered sequence of the same Rc-shared lazy thunks (no forcing,
3155            // no reordering, no identity change). When the Rc is shared (the
3156            // left came from a still-live binding/thunk) we fall back to the
3157            // clone-extend path, preserving the shared list unchanged.
3158            crate::value::concat_lists(l, r.as_list()?)
3159        }
3160        ast::BinOpKind::And | ast::BinOpKind::Or | ast::BinOpKind::Implication => {
3161            unreachable!("handled above")
3162        }
3163        ast::BinOpKind::PipeRight | ast::BinOpKind::PipeLeft => {
3164            Err(EvalError::NotImplemented("pipe operators".to_string()))
3165        }
3166    }
3167}
3168
3169/// CppNix aborts (uncatchably) on i64 arithmetic overflow, e.g.
3170/// `integer overflow in adding 9223372036854775807 + 1`. `EvalError::Abort` is
3171/// the uncatchable variant (`tryEval` catches only `Throw`/`AssertionFailed`),
3172/// matching nix — a wrapping result would silently produce a wrong drvPath.
3173#[inline]
3174fn int_overflow(verb: &str, a: i64, sym: char, b: i64) -> EvalError {
3175    EvalError::Abort(format!("integer overflow in {verb} {a} {sym} {b}"))
3176}
3177
3178fn num_op(
3179    l: &Value,
3180    r: &Value,
3181    int_op: impl Fn(i64, i64) -> Option<i64>,
3182    float_op: impl Fn(f64, f64) -> f64,
3183    overflow: impl Fn(i64, i64) -> EvalError,
3184) -> Result<Value, EvalError> {
3185    match (l, r) {
3186        (Value::Int(a), Value::Int(b)) => {
3187            int_op(*a, *b).map(Value::Int).ok_or_else(|| overflow(*a, *b))
3188        }
3189        (Value::Float(a), Value::Float(b)) => Ok(Value::Float(float_op(*a, *b))),
3190        (Value::Int(a), Value::Float(b)) => Ok(Value::Float(float_op(*a as f64, *b))),
3191        (Value::Float(a), Value::Int(b)) => Ok(Value::Float(float_op(*a, *b as f64))),
3192        _ => Err(EvalError::op_type("perform arithmetic on", l.type_name(), r.type_name())),
3193    }
3194}
3195
3196fn compare(
3197    l: &Value,
3198    r: &Value,
3199    pred: impl Fn(std::cmp::Ordering) -> bool,
3200) -> Result<Value, EvalError> {
3201    let ord = match (l, r) {
3202        (Value::Int(a), Value::Int(b)) => a.cmp(b),
3203        (Value::Float(a), Value::Float(b)) => {
3204            a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
3205        }
3206        (Value::Int(a), Value::Float(b)) => (*a as f64)
3207            .partial_cmp(b)
3208            .unwrap_or(std::cmp::Ordering::Equal),
3209        (Value::Float(a), Value::Int(b)) => a
3210            .partial_cmp(&(*b as f64))
3211            .unwrap_or(std::cmp::Ordering::Equal),
3212        (Value::String(a), Value::String(b)) => a.chars.cmp(&b.chars),
3213        _ => {
3214            return Err(EvalError::op_type("compare", l.type_name(), r.type_name()));
3215        }
3216    };
3217    Ok(Value::Bool(pred(ord)))
3218}
3219
3220/// Apply a function to an argument.
3221///
3222/// Supports `__functor`: if `func` is an attrset with a `__functor` key,
3223/// calls `__functor self arg` (the Nix `__functor` protocol).
3224///
3225/// For lambda with a simple ident parameter, the argument is NOT forced
3226/// before binding -- this enables fixpoint combinators (`lib.fix`) where
3227/// the argument is a self-referential thunk.
3228/// Apply a function and force the result.
3229///
3230/// Builtins that inspect the return value (via `as_list`, `as_bool`, etc.)
3231/// must use this instead of bare `apply` — otherwise a thunk-wrapped result
3232/// will cause "thunk in as_list: force first" errors.
3233pub fn apply_and_force(func: Value, arg: Value) -> Result<Value, EvalError> {
3234    force_value(&apply(func, arg)?)
3235}
3236
3237pub fn apply(func: Value, arg: Value) -> Result<Value, EvalError> {
3238    stacker::maybe_grow(64 * 1024, 2 * 1024 * 1024, || apply_inner(func, arg))
3239}
3240
3241fn apply_inner(func: Value, arg: Value) -> Result<Value, EvalError> {
3242    crate::perf::inc(crate::perf::Counter::Apply);
3243    let func = force_concrete(&func)?.into_value();
3244    match func {
3245        Value::Lambda(closure) => {
3246            // Hot function tracker: log source file + param name for each lambda call
3247            if crate::perf::enabled() {
3248                APPLY_SITES.with(|sites| {
3249                    let file = closure.env.eval_file()
3250                        .map(|p| p.display().to_string())
3251                        .unwrap_or_else(|| "<eval>".into());
3252                    // Include param info for identification
3253                    let param_name = match &closure.param {
3254                        rnix::ast::Param::IdentParam(ip) => ip.ident().map(|i| ident_text(&i)).unwrap_or_default(),
3255                        rnix::ast::Param::Pattern(pat) => {
3256                            let mut names: Vec<String> = pat.pat_entries()
3257                                .filter_map(|e| e.ident().map(|i| ident_text(&i)))
3258                                .take(3)
3259                                .collect();
3260                            if pat.pat_entries().count() > 3 { names.push("...".to_string()); }
3261                            format!("{{{}}}", names.join(","))
3262                        }
3263                    };
3264                    let key = format!("{}:{}", file.rsplit_once("-source/").map_or(file.as_str(), |(_,s)| s), param_name);
3265                    *sites.borrow_mut().entry(key).or_insert(0u64) += 1;
3266                });
3267            }
3268            let mut call_env = closure.env.child();
3269            let _file_guard = closure
3270                .env
3271                .eval_file()
3272                .cloned()
3273                .map(push_eval_file);
3274            // Push Nix-level trace frame for function calls. Lazy: stores
3275            // only the raw ingredients (O(1) Rc-clone of the closure env +
3276            // the current-eval-file snapshot) and defers the format!/strip
3277            // work to the cold `attach_trace` path. Renders byte-identical
3278            // to the eager form.
3279            let _trace = push_nix_trace_lambda(&closure.env);
3280            match &closure.param {
3281                rnix::ast::Param::IdentParam(_) => {
3282                    // Simple ident param: bind argument WITHOUT forcing.
3283                    // This is critical for fixpoint / call-by-need semantics.
3284                    bind_param(&closure.param, &arg, &mut call_env)?;
3285                }
3286                rnix::ast::Param::Pattern(_) => {
3287                    // Pattern param needs the arg to be an attrset, so force.
3288                    let forced_arg = force_concrete(&arg)?.into_value();
3289                    bind_param(&closure.param, &forced_arg, &mut call_env)?;
3290                }
3291            }
3292            eval_expr(&closure.body, &call_env)
3293        }
3294        Value::Builtin(b) => {
3295            let _trace = push_nix_trace(format!("while calling the '{}' builtin", b.name));
3296            // Special builtins that must receive UNFORCED arguments:
3297            // - tryEval: must catch throw/abort during its own forcing
3298            // - addErrorContext<partial>: wraps value with error context
3299            //   without forcing (the value is the fixpoint `config` which
3300            //   causes infinite recursion if forced during collectModules)
3301            // - seq<partial>: forces first arg but returns second UNFORCED
3302            // Same lazy-arg set as `eval_apply` (single source of truth) — these
3303            // builtins receive the arg UNFORCED. foldl'<p1> is the nul accumulator
3304            // (nix's foldl' is strict in each op RESULT, NOT in the nul).
3305            if builtin_takes_lazy_arg(&b.name) {
3306                (b.func)(&[arg])
3307            } else {
3308                let forced_arg = force_value(&arg)?;
3309                (b.func)(&[forced_arg])
3310            }
3311        }
3312        Value::Attrs(ref attrs) => {
3313            if let Some(functor) = attrs.get("__functor") {
3314                let functor = force_value(functor)?;
3315                // __functor protocol: (functor self) arg
3316                let partial = apply(functor, func.clone())?;
3317                apply(partial, arg)
3318            } else if crate::value::in_promise_eval() {
3319                // M2.6 Promise softening: an attrset without __functor
3320                // being called as a function — typically the empty-
3321                // attrset sentinel inside a fix-point body.  Return
3322                // null so eval can proceed.
3323                Ok(Value::Null)
3324            } else {
3325                Err(EvalError::type_error(
3326                    format!("cannot call {} (missing __functor){}", func.type_name(), eval_file_ctx()),
3327                ))
3328            }
3329        }
3330        _ if crate::value::in_promise_eval() => {
3331            // M2.6 Promise softening: calling null / int / string / list
3332            // as a function inside a Promise body is the sentinel
3333            // cascade landing somewhere it doesn't belong.  Return null
3334            // so the fix-point continues instead of erroring.
3335            Ok(Value::Null)
3336        }
3337        _ => Err(EvalError::type_error(
3338            format!("cannot call {}{}", func.type_name(), eval_file_ctx()),
3339        )),
3340    }
3341}
3342
3343fn bind_param(param: &ast::Param, arg: &Value, env: &mut Env) -> Result<(), EvalError> {
3344    match param {
3345        ast::Param::IdentParam(ip) => {
3346            let ident = ip
3347                .ident()
3348                .ok_or_else(|| EvalError::ParseError("ident param missing ident".to_string()))?;
3349            let name = ident_text(&ident);
3350            env.bind(name, arg.clone());
3351        }
3352        ast::Param::Pattern(pat) => {
3353            let attrs = arg.as_attrs()?;
3354
3355            // @-binding (either `args @ { ... }` or `{ ... } @ args`)
3356            if let Some(pat_bind) = pat.pat_bind()
3357                && let Some(ident) = pat_bind.ident()
3358            {
3359                let name = ident_text(&ident);
3360                env.bind(name, arg.clone());
3361            }
3362
3363            let has_ellipsis = pat.ellipsis_token().is_some();
3364            let entries: Vec<ast::PatEntry> = pat.pat_entries().collect();
3365
3366            // Two-phase binding (matching CppNix semantics):
3367            // Phase 1: Bind all formals. Defaults get thunks with a
3368            //   preliminary env. We collect thunks for Phase 2 update.
3369            // Phase 2: Update default thunks to capture the final env
3370            //   (which now has ALL formals bound). This allows defaults
3371            //   to reference any other formal — including forward refs.
3372            let mut default_thunks: Vec<Thunk> = Vec::new();
3373
3374            for entry in &entries {
3375                let ident = entry.ident().ok_or_else(|| {
3376                    EvalError::ParseError("pat entry missing ident".to_string())
3377                })?;
3378                let name = ident_text(&ident);
3379                let value = if let Some(v) = attrs.get(&name) {
3380                    v.clone()
3381                } else if let Some(default_expr) = entry.default() {
3382                    // Default values in pattern parameters must be lazy
3383                    // (wrapped in thunks), matching CppNix semantics.
3384                    // Patterns like `vendor ? assert false; null` rely on
3385                    // the default never being forced when the body checks
3386                    // `args ? vendor` instead of using `vendor` directly.
3387                    let thunk = Thunk::new_suspended(
3388                        ast::Expr::cast(default_expr.syntax().clone()).unwrap(),
3389                        env.clone(),
3390                    );
3391                    default_thunks.push(thunk.clone());
3392                    Value::Thunk(thunk)
3393                } else {
3394                    return Err(EvalError::type_error(
3395                        format!("missing argument '{name}'{}", eval_file_ctx()),
3396                    ));
3397                };
3398                env.bind(name, value);
3399            }
3400
3401            // Phase 2: Update default thunks to see ALL formals.
3402            for thunk in &default_thunks {
3403                thunk.update_env(env);
3404            }
3405
3406            if !has_ellipsis {
3407                let entry_names: std::collections::HashSet<String> = entries
3408                    .iter()
3409                    .filter_map(|e| e.ident().map(|i| ident_text(&i)))
3410                    .collect();
3411                for key in attrs.keys() {
3412                    if !entry_names.contains(key.as_str()) {
3413                        return Err(EvalError::type_error(
3414                            format!("unexpected argument '{key}'{}", eval_file_ctx()),
3415                        ));
3416                    }
3417                }
3418            }
3419        }
3420    }
3421    Ok(())
3422}
3423
3424#[cfg(test)]
3425mod tests {
3426    use super::*;
3427
3428    fn ev(input: &str) -> Value {
3429        eval(input).unwrap()
3430    }
3431
3432    // Regression (2026-07-10): the let-scope fix-point detector must count
3433    // only GENUINE variable references, not attribute names / attrset keys
3434    // (which sit under a `NODE_ATTRPATH`).  nixpkgs `lib/types.nix` has
3435    // `placeholder = if lhs.placeholder == …` whose RHS mentions the
3436    // *attribute* `.placeholder`; the old raw-token match falsely flagged
3437    // the binding self-recursive and routed it through the Promise path.
3438    #[test]
3439    fn is_self_recursive_binding_ignores_attribute_names() {
3440        fn expr(s: &str) -> ast::Expr {
3441            rnix::Root::parse(s).tree().expr().expect("parse")
3442        }
3443        // attribute names / keys are NOT references to the binding
3444        assert!(!is_self_recursive_binding(&expr("lhs.placeholder"), "placeholder"));
3445        assert!(!is_self_recursive_binding(&expr("{ placeholder = 1; }"), "placeholder"));
3446        assert!(!is_self_recursive_binding(
3447            &expr("if lhs.placeholder == rhs.placeholder then lhs.placeholder else null"),
3448            "placeholder",
3449        ));
3450        // genuine variable references ARE detected
3451        assert!(is_self_recursive_binding(&expr("placeholder + 1"), "placeholder"));
3452        assert!(is_self_recursive_binding(
3453            &expr("if placeholder then 1 else 2"),
3454            "placeholder"
3455        ));
3456    }
3457
3458    // M2 thunk-waste (byte-safe eager constant): a NON-interpolated string in a
3459    // maybe_thunk site is evaluated directly (no suspended thunk). The value +
3460    // its (empty) context must be byte-identical to forcing a thunk of it.
3461    #[test]
3462    fn maybe_thunk_eager_constant_str_is_byte_identical() {
3463        fn expr(s: &str) -> ast::Expr {
3464            rnix::Root::parse(s).tree().expr().expect("parse")
3465        }
3466        let env = Env::new();
3467        // Constant string → returned as a concrete String, NOT a Thunk.
3468        let v = maybe_thunk(&expr(r#""abc""#), &env, false, None);
3469        assert!(matches!(v, Value::String(_)), "constant str should be eager, got {v:?}");
3470        assert_eq!(force_value(&v).unwrap(), Value::string("abc"));
3471        // Interpolated string → MUST stay a thunk (lazy `${…}` force).
3472        let vi = maybe_thunk(&expr(r#""a${b}c""#), &env, false, None);
3473        assert!(matches!(vi, Value::Thunk(_)), "interpolated str must stay thunked");
3474    }
3475
3476    // The pure-constant arg classifier admits ONLY literals + non-interpolated
3477    // strings/paths, and rejects everything that could throw/diverge/observe a
3478    // fixpoint — the laziness safety boundary of the apply-arg optimization.
3479    #[test]
3480    fn eval_pure_constant_arg_classification() {
3481        fn expr(s: &str) -> ast::Expr {
3482            rnix::Root::parse(s).tree().expr().expect("parse")
3483        }
3484        // ADMIT: pure constants (byte-safe to eval eagerly in an arg position).
3485        assert!(eval_pure_constant_arg(&expr("42")).is_some());
3486        assert!(eval_pure_constant_arg(&expr("3.14")).is_some());
3487        assert!(eval_pure_constant_arg(&expr(r#""const""#)).is_some());
3488        assert!(eval_pure_constant_arg(&expr("/abs/path")).is_some());
3489        // REJECT: anything that could throw / diverge / observe laziness.
3490        assert!(eval_pure_constant_arg(&expr(r#""a${b}c""#)).is_none(), "interpolated str");
3491        // `true`/`false`/`null` are IDENTS in nix (shadowable), not literals —
3492        // rejected to avoid a with-scope force, correctly conservative.
3493        assert!(eval_pure_constant_arg(&expr("true")).is_none(), "bool is an ident");
3494        assert!(eval_pure_constant_arg(&expr("x")).is_none(), "ident (with-scope force)");
3495        assert!(eval_pure_constant_arg(&expr("a.b")).is_none(), "select (fixpoint)");
3496        assert!(eval_pure_constant_arg(&expr("f x")).is_none(), "apply (may throw)");
3497        assert!(eval_pure_constant_arg(&expr("1 + 1")).is_none(), "binop (may throw)");
3498        assert!(eval_pure_constant_arg(&expr("throw \"x\"")).is_none(), "throw stays lazy");
3499    }
3500
3501    // LAZINESS GUARD: a lambda that IGNORES its arg must NOT force it — even a
3502    // throwing arg. The pure-constant optimization only touches inert constants,
3503    // so a `throw`-ing arg stays fully thunked and the ignoring lambda succeeds.
3504    #[test]
3505    fn ignored_throwing_arg_stays_lazy() {
3506        assert_eq!(ev(r#"(x: 7) (throw "boom")"#), Value::Int(7));
3507        // And an ignored constant arg is equally invisible.
3508        assert_eq!(ev(r#"(x: 7) "const""#), Value::Int(7));
3509        // A USED constant arg produces the right value.
3510        assert_eq!(ev(r#"(x: x) "used""#), Value::string("used"));
3511    }
3512
3513    #[test]
3514    fn eval_int() { assert_eq!(ev("42"), Value::Int(42)); }
3515
3516    #[test]
3517    fn eval_float() { assert_eq!(ev("3.14"), Value::Float(3.14)); }
3518
3519    #[test]
3520    fn eval_string() { assert_eq!(ev(r#""hello""#), Value::string("hello")); }
3521
3522    #[test]
3523    fn eval_bool() { assert_eq!(ev("true"), Value::Bool(true)); }
3524
3525    #[test]
3526    fn eval_null() { assert_eq!(ev("null"), Value::Null); }
3527
3528    #[test]
3529    fn eval_arithmetic() {
3530        assert_eq!(ev("1 + 2"), Value::Int(3));
3531        assert_eq!(ev("10 - 3"), Value::Int(7));
3532        assert_eq!(ev("2 * 3"), Value::Int(6));
3533        assert_eq!(ev("10 / 3"), Value::Int(3));
3534    }
3535
3536    #[test]
3537    fn eval_precedence() {
3538        assert_eq!(ev("1 + 2 * 3"), Value::Int(7));
3539        assert_eq!(ev("(1 + 2) * 3"), Value::Int(9));
3540    }
3541
3542    #[test]
3543    fn eval_comparison() {
3544        assert_eq!(ev("1 == 1"), Value::Bool(true));
3545        assert_eq!(ev("1 == 2"), Value::Bool(false));
3546        assert_eq!(ev("1 < 2"), Value::Bool(true));
3547        assert_eq!(ev("2 <= 2"), Value::Bool(true));
3548    }
3549
3550    #[test]
3551    fn eval_logic() {
3552        assert_eq!(ev("true && false"), Value::Bool(false));
3553        assert_eq!(ev("true || false"), Value::Bool(true));
3554        assert_eq!(ev("!true"), Value::Bool(false));
3555    }
3556
3557    #[test]
3558    fn eval_string_concat() {
3559        assert_eq!(ev(r#""hello" + " " + "world""#), Value::string("hello world"));
3560    }
3561
3562    #[test]
3563    fn eval_if() {
3564        assert_eq!(ev("if true then 1 else 2"), Value::Int(1));
3565        assert_eq!(ev("if false then 1 else 2"), Value::Int(2));
3566    }
3567
3568    #[test]
3569    fn eval_let() {
3570        assert_eq!(ev("let x = 1; in x"), Value::Int(1));
3571        assert_eq!(ev("let x = 1; y = 2; in x + y"), Value::Int(3));
3572    }
3573
3574    #[test]
3575    fn eval_let_dotted_simple() {
3576        // Two dotted bindings sharing the top-level key `a`.
3577        assert_eq!(ev("let a.b = 1; a.c = 2; in a.b + a.c"), Value::Int(3));
3578    }
3579
3580    #[test]
3581    fn eval_let_dotted_deep() {
3582        // Deeply nested dotted path.
3583        assert_eq!(ev("let a.b.c = 1; in a.b.c"), Value::Int(1));
3584    }
3585
3586    #[test]
3587    fn eval_let_dotted_mixed() {
3588        // Mix of simple and dotted bindings.
3589        assert_eq!(
3590            ev("let a.x = 1; b = 2; a.y = 3; in a.x + a.y + b"),
3591            Value::Int(6),
3592        );
3593    }
3594
3595    #[test]
3596    fn eval_let_dotted_produces_attrset() {
3597        // Dotted let bindings produce a real attrset.
3598        let v = ev("let a.b = 1; a.c = 2; in a");
3599        if let Value::Attrs(attrs) = v {
3600            assert_eq!(attrs.get("b"), Some(&Value::Int(1)));
3601            assert_eq!(attrs.get("c"), Some(&Value::Int(2)));
3602        } else {
3603            panic!("expected Attrs, got {v:?}");
3604        }
3605    }
3606
3607    // ── Inner dynamic attrpath key laziness ──────────────────
3608    // CppNix defers a dynamic key that is NOT at the head of an attrpath:
3609    // `{ a.${e} = v; }` builds `{ a = <thunk {${e}=v}>; }`, so `e` never
3610    // forces until `.a` is demanded. Reading a sibling must not force the
3611    // inner dynamic key. Root fix: `build_deferred_tail_attr` in eval.rs.
3612    // This is the pure-builtins reduction of the NixOS module-system
3613    // `config.homes.${cfg.userName}` fixpoint divergence.
3614    #[test]
3615    fn dynamic_inner_attr_key_is_lazy_on_sibling_read() {
3616        // The dynamic key throws; reading the SIBLING must NOT force it.
3617        assert_eq!(
3618            ev(r#"let s = { a.${throw "KEYFORCED"} = 7; other = 9; }; in s.other"#),
3619            Value::Int(9),
3620        );
3621    }
3622
3623    #[test]
3624    fn dynamic_inner_attr_key_resolves_on_head_demand() {
3625        // Demanding the head DOES resolve the deferred dynamic key.
3626        let v = ev(r#"let u = "bob"; s = { homes.${u} = 7; }; in s.homes"#);
3627        if let Value::Attrs(attrs) = force_value(&v).unwrap() {
3628            assert_eq!(attrs.get("bob"), Some(&Value::Int(7)));
3629        } else {
3630            panic!("expected Attrs");
3631        }
3632    }
3633
3634    #[test]
3635    fn dynamic_inner_attr_key_merges_with_static_sibling() {
3636        // Collision under one head still deep-merges (static + dynamic).
3637        let v = ev(r#"let u = "x"; s = { a.${u} = 1; a.b = 2; }; in s.a"#);
3638        if let Value::Attrs(attrs) = force_value(&v).unwrap() {
3639            assert_eq!(attrs.get("x"), Some(&Value::Int(1)));
3640            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
3641        } else {
3642            panic!("expected Attrs");
3643        }
3644    }
3645
3646    #[test]
3647    fn dynamic_inner_attr_key_null_skips_binding() {
3648        // A null dynamic inner key skips the definition (CppNix rule):
3649        // `a` becomes an empty attrset, the sibling stays.
3650        let v = ev(
3651            r#"let c = true; s = { a.${if c then null else "n"} = 5; b = 1; }; in s.b"#,
3652        );
3653        assert_eq!(v, Value::Int(1));
3654    }
3655
3656    // ── M2.6 ROOT #3: interpolated-STRING tail keys are dynamic too ──────
3657    // `{ a."p${e}" = v; }` must build `{ a = <thunk {"p${e}"=v}>; }` — an
3658    // interpolated-string attr key references `e` and so must defer like a
3659    // bare `${e}`, never force at construction. Reading a sibling must NOT
3660    // force it (the KEYFORCE discriminator, now for a `Str` key).
3661    #[test]
3662    fn interpolated_string_attr_key_is_lazy_on_sibling_read() {
3663        assert_eq!(
3664            ev(r#"let s = { a."p/${throw "KEYFORCED"}" = 7; other = 9; }; in s.other"#),
3665            Value::Int(9),
3666        );
3667    }
3668
3669    #[test]
3670    fn interpolated_string_attr_key_resolves_on_head_demand() {
3671        // Demanding the head DOES resolve the deferred interpolated key.
3672        let v = ev(r#"let u = "bob"; s = { homes."u/${u}" = 7; }; in s.homes"#);
3673        if let Value::Attrs(attrs) = force_value(&v).unwrap() {
3674            assert_eq!(attrs.get("u/bob"), Some(&Value::Int(7)));
3675        } else {
3676            panic!("expected Attrs");
3677        }
3678    }
3679
3680    #[test]
3681    fn purely_literal_string_attr_key_stays_eager_static() {
3682        // A `Str` key with NO interpolation is a plain static key and must
3683        // NOT be treated as dynamic (it forces nothing, deep-merges).
3684        let v = ev(r#"let s = { a."foo bar" = 1; a.b = 2; }; in s.a"#);
3685        if let Value::Attrs(attrs) = force_value(&v).unwrap() {
3686            assert_eq!(attrs.get("foo bar"), Some(&Value::Int(1)));
3687            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
3688        } else {
3689            panic!("expected Attrs");
3690        }
3691    }
3692
3693    // ── M2.6 ROOT #3 (collision case): dynamic tail key under a head that
3694    // a sibling binding already wrote must stay lazy AND deep-merge.
3695    #[test]
3696    fn dynamic_tail_key_under_colliding_head_is_lazy() {
3697        // `sd.services.x` writes head `sd`; the second binding's dynamic
3698        // key must NOT force when a SIBLING (`sd.services`) is read.
3699        let v = ev(
3700            r#"let s = { sd.services.x = 1; sd.tmpfiles.${throw "KEYFORCED"}.d = 2; }; in s.sd.services.x"#,
3701        );
3702        assert_eq!(v, Value::Int(1));
3703    }
3704
3705    #[test]
3706    fn dynamic_tail_key_under_colliding_head_resolves_and_merges() {
3707        // Demanding the dynamic branch resolves the key; the sibling
3708        // static branch (`sd.services`) survives the merge intact.
3709        let v = ev(
3710            r#"let k = "z"; s = { sd.services.x = 1; sd.tmpfiles.${k}.d = 2; }; in s.sd"#,
3711        );
3712        let sd = force_value(&v).unwrap();
3713        if let Value::Attrs(sd_attrs) = &sd {
3714            // static sibling intact
3715            let services = force_value(sd_attrs.get("services").unwrap()).unwrap();
3716            if let Value::Attrs(a) = &services {
3717                assert_eq!(force_value(a.get("x").unwrap()).unwrap(), Value::Int(1));
3718            } else { panic!("expected services attrs"); }
3719            // dynamic branch resolved to key "z"
3720            let tmpfiles = force_value(sd_attrs.get("tmpfiles").unwrap()).unwrap();
3721            if let Value::Attrs(a) = &tmpfiles {
3722                let z = force_value(a.get("z").unwrap()).unwrap();
3723                if let Value::Attrs(zd) = &z {
3724                    assert_eq!(force_value(zd.get("d").unwrap()).unwrap(), Value::Int(2));
3725                } else { panic!("expected z attrs"); }
3726            } else { panic!("expected tmpfiles attrs"); }
3727        } else {
3728            panic!("expected sd attrs");
3729        }
3730    }
3731
3732    // ── M2.6 ROOT #4a — `with` namespace must be LAZY ─────────────────
3733    // `with X; body` stores the namespace as a thunk forced only on a
3734    // bare-ident fallthrough lookup; demanding only the body's WHNF/keys
3735    // must NOT force X.  cppnix: `attrNames (with (throw "X"); {a=1;})`
3736    // → ["a"].  Before the fix, sui EVALUATED the namespace at `with`-entry
3737    // and threw.  This is the load-bearing over-force behind the M2.6
3738    // `concatLists null` (nixpkgs' `config = mkIf … (with config.services.X;
3739    // { … })` module shape forced `config.services.X` during collection).
3740    #[test]
3741    fn with_namespace_is_lazy_on_body_whnf() {
3742        let v = ev(r#"builtins.attrNames (with (throw "WITH-FORCED"); { a = 1; b = 2; })"#);
3743        if let Value::List(items) = force_value(&v).unwrap() {
3744            let names: Vec<String> = items
3745                .iter()
3746                .map(|i| match force_value(i).unwrap() {
3747                    Value::String(s) => s.as_str().to_string(),
3748                    other => panic!("expected string, got {}", other.type_name()),
3749                })
3750                .collect();
3751            assert_eq!(names, vec!["a".to_string(), "b".to_string()]);
3752        } else {
3753            panic!("expected list");
3754        }
3755    }
3756
3757    #[test]
3758    fn with_namespace_forces_only_on_fallthrough() {
3759        // A bare ident that falls through lexical scope DOES resolve via
3760        // the namespace (correct cppnix semantics) — proves the deferred
3761        // thunk is real and gets forced on demand, not an accidental no-op.
3762        assert_eq!(ev(r#"with { x = 42; }; x"#), Value::Int(42));
3763        // A lexical binding shadows the with-scope, so the (throwing)
3764        // namespace is never forced — the laziness we rely on for M2.6.
3765        assert_eq!(ev(r#"let x = 7; in with (throw "NS"); x"#), Value::Int(7));
3766    }
3767
3768    // ── M2.6 ROOT #4b — depth-≥2 dotted full-set leaf must deep-merge ──
3769    // `o.a = { x = 1; }` inserts `o = { a = <thunk {x=1}> }` (leaf goes
3770    // through maybe_thunk); a deeper sibling `o.a.y = 2` recurses
3771    // merge_nested_insert down to key `a` where the existing value is that
3772    // thunk.  Before the fix, merge_nested_insert required BOTH sides to be
3773    // concrete Attrs, so the Thunk-vs-Attrs collision OVERWROTE — dropping
3774    // `x`.  cppnix desugars both orderings into `o.a = { x = 1; y = 2; }`.
3775    // This is the M2.6 post-`with`-fix frontier (nixpkgs alsa's
3776    // `options.hardware.alsa = { … }` + `options.hardware.alsa.enablePersistence
3777    // = …` merged to only {enablePersistence} → `cardAliases` "does not exist").
3778    #[test]
3779    fn dotted_fullset_leaf_deep_merges_with_deeper_sibling() {
3780        let v = ev(r#"{ o.a = { x = 1; }; o.a.y = 2; }.o.a"#);
3781        if let Value::Attrs(a) = force_value(&v).unwrap() {
3782            assert_eq!(force_value(a.get("x").unwrap()).unwrap(), Value::Int(1));
3783            assert_eq!(force_value(a.get("y").unwrap()).unwrap(), Value::Int(2));
3784        } else {
3785            panic!("expected attrs");
3786        }
3787    }
3788
3789    #[test]
3790    fn dotted_fullset_leaf_deep_merge_reverse_order() {
3791        // Deeper sibling FIRST, full-set leaf SECOND — the NEW value is the
3792        // `<thunk {x=1}>`; must still merge (the collision forces it).
3793        let v = ev(r#"{ o.a.y = 2; o.a = { x = 1; }; }.o.a"#);
3794        if let Value::Attrs(a) = force_value(&v).unwrap() {
3795            assert_eq!(force_value(a.get("x").unwrap()).unwrap(), Value::Int(1));
3796            assert_eq!(force_value(a.get("y").unwrap()).unwrap(), Value::Int(2));
3797        } else {
3798            panic!("expected attrs");
3799        }
3800    }
3801
3802    #[test]
3803    fn dotted_fullset_leaf_merge_preserves_leaf_laziness() {
3804        // The merge forces the existing/new leaf to WHNF (keys) but MUST
3805        // NOT force the leaf VALUES — a throwing sibling value that is never
3806        // demanded stays lazy.
3807        assert_eq!(ev(r#"{ o.a = { x = throw "X-NEVER"; }; o.a.y = 2; }.o.a.y"#), Value::Int(2));
3808    }
3809
3810    #[test]
3811    fn eval_nested_let() {
3812        assert_eq!(ev("let a = 1; b = let c = 2; in c; in a + b"), Value::Int(3));
3813    }
3814
3815    #[test]
3816    fn eval_lambda() {
3817        assert_eq!(ev("(x: x + 1) 41"), Value::Int(42));
3818    }
3819
3820    #[test]
3821    fn eval_lambda_multi_arg() {
3822        assert_eq!(ev("(x: y: x + y) 1 2"), Value::Int(3));
3823    }
3824
3825    #[test]
3826    fn eval_list() {
3827        let v = ev("[1 2 3]");
3828        assert_eq!(v, Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]));
3829    }
3830
3831    #[test]
3832    fn eval_list_concat() {
3833        let v = ev("[1 2] ++ [3 4]");
3834        assert_eq!(v, Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(4)]));
3835    }
3836
3837    #[test]
3838    fn eval_attrset() {
3839        let v = ev("{ a = 1; b = 2; }");
3840        if let Value::Attrs(attrs) = v {
3841            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
3842            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
3843        } else {
3844            panic!("expected attrset");
3845        }
3846    }
3847
3848    #[test]
3849    fn eval_select() {
3850        assert_eq!(ev("{ a = 42; }.a"), Value::Int(42));
3851    }
3852
3853    #[test]
3854    fn eval_select_or() {
3855        assert_eq!(ev("{ a = 42; }.b or 0"), Value::Int(0));
3856    }
3857
3858    #[test]
3859    fn eval_has_attr() {
3860        assert_eq!(ev("{ a = 1; } ? a"), Value::Bool(true));
3861        assert_eq!(ev("{ a = 1; } ? b"), Value::Bool(false));
3862    }
3863
3864    #[test]
3865    fn eval_update() {
3866        let v = ev("{ a = 1; b = 2; } // { b = 3; c = 4; }");
3867        if let Value::Attrs(attrs) = v {
3868            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
3869            assert_eq!(attrs.get("b"), Some(&Value::Int(3)));
3870            assert_eq!(attrs.get("c"), Some(&Value::Int(4)));
3871        } else {
3872            panic!("expected attrset");
3873        }
3874    }
3875
3876    #[test]
3877    fn eval_with() {
3878        assert_eq!(ev("with { x = 42; }; x"), Value::Int(42));
3879    }
3880
3881    #[test]
3882    fn eval_assert() {
3883        assert_eq!(ev("assert true; 42"), Value::Int(42));
3884        assert!(eval("assert false; 42").is_err());
3885    }
3886
3887    #[test]
3888    fn eval_formals() {
3889        assert_eq!(ev("({ a, b }: a + b) { a = 1; b = 2; }"), Value::Int(3));
3890    }
3891
3892    #[test]
3893    fn eval_formals_default() {
3894        assert_eq!(ev("({ a, b ? 10 }: a + b) { a = 1; }"), Value::Int(11));
3895    }
3896
3897    #[test]
3898    fn eval_formals_ellipsis() {
3899        assert_eq!(ev("({ a, ... }: a) { a = 1; b = 2; }"), Value::Int(1));
3900    }
3901
3902    #[test]
3903    fn eval_named_formals() {
3904        assert_eq!(ev("(args @ { a }: args.a) { a = 42; }"), Value::Int(42));
3905    }
3906
3907    #[test]
3908    fn eval_rec_attrset() {
3909        assert_eq!(ev("(rec { a = 1; b = a + 1; }).b"), Value::Int(2));
3910    }
3911
3912    #[test]
3913    fn eval_negation() {
3914        assert_eq!(ev("-42"), Value::Int(-42));
3915    }
3916
3917    #[test]
3918    fn eval_float_arithmetic() {
3919        assert_eq!(ev("1.5 + 2.5"), Value::Float(4.0));
3920        assert_eq!(ev("1 + 1.5"), Value::Float(2.5));
3921    }
3922
3923    #[test]
3924    fn eval_division_by_zero() {
3925        assert!(eval("1 / 0").is_err());
3926    }
3927
3928    #[test]
3929    fn eval_builtins_available() {
3930        assert_eq!(ev("builtins.typeOf 42"), Value::string("int"));
3931        assert_eq!(ev("builtins.typeOf true"), Value::string("bool"));
3932    }
3933
3934    #[test]
3935    fn eval_builtins_length() {
3936        assert_eq!(ev("builtins.length [1 2 3]"), Value::Int(3));
3937    }
3938
3939    #[test]
3940    fn eval_builtins_head_tail() {
3941        assert_eq!(ev("builtins.head [1 2 3]"), Value::Int(1));
3942        assert_eq!(ev("builtins.length (builtins.tail [1 2 3])"), Value::Int(2));
3943    }
3944
3945    #[test]
3946    fn eval_builtins_add() {
3947        assert_eq!(ev("builtins.add 1 2"), Value::Int(3));
3948    }
3949
3950    #[test]
3951    fn eval_builtins_to_string() {
3952        assert_eq!(ev("builtins.toString 42"), Value::string("42"));
3953    }
3954
3955    #[test]
3956    fn eval_implication() {
3957        assert_eq!(ev("false -> true"), Value::Bool(true));
3958        assert_eq!(ev("true -> false"), Value::Bool(false));
3959        assert_eq!(ev("true -> true"), Value::Bool(true));
3960    }
3961
3962    // ── New tests ────────────────────────────────────────
3963
3964    #[test]
3965    fn eval_error_undefined_variable() {
3966        let result = eval("nonexistent");
3967        assert!(result.is_err());
3968        let msg = format!("{}", result.unwrap_err());
3969        assert!(msg.contains("undefined variable"));
3970    }
3971
3972    #[test]
3973    fn eval_error_type_mismatch_arithmetic() {
3974        let result = eval(r#"1 + "hello""#);
3975        assert!(result.is_err());
3976        let msg = format!("{}", result.unwrap_err());
3977        assert!(msg.contains("cannot add") || msg.contains("type"));
3978    }
3979
3980    #[test]
3981    fn eval_error_unexpected_argument() {
3982        let result = eval("({ a }: a) { a = 1; b = 2; }");
3983        assert!(result.is_err());
3984        let msg = format!("{}", result.unwrap_err());
3985        assert!(msg.contains("unexpected argument"));
3986    }
3987
3988    #[test]
3989    fn eval_error_missing_required_argument() {
3990        let result = eval("({ a, b }: a + b) { a = 1; }");
3991        assert!(result.is_err());
3992        let msg = format!("{}", result.unwrap_err());
3993        assert!(msg.contains("missing argument"));
3994    }
3995
3996    #[test]
3997    fn eval_builtins_attr_names_sorted() {
3998        let v = ev("builtins.attrNames { z = 1; a = 2; m = 3; }");
3999        // BTreeMap keys are already sorted
4000        assert_eq!(
4001            v,
4002            Value::list(vec![
4003                Value::string("a"),
4004                Value::string("m"),
4005                Value::string("z"),
4006            ]),
4007        );
4008    }
4009
4010    #[test]
4011    fn eval_builtins_attr_values() {
4012        let v = ev("builtins.attrValues { a = 1; b = 2; }");
4013        // BTreeMap iteration is sorted by key, so a=1 first, b=2 second
4014        assert_eq!(v, Value::list(vec![Value::Int(1), Value::Int(2)]));
4015    }
4016
4017    #[test]
4018    fn eval_builtins_is_null() {
4019        assert_eq!(ev("builtins.isNull null"), Value::Bool(true));
4020        assert_eq!(ev("builtins.isNull 1"), Value::Bool(false));
4021    }
4022
4023    #[test]
4024    fn eval_builtins_is_int() {
4025        assert_eq!(ev("builtins.isInt 42"), Value::Bool(true));
4026        assert_eq!(ev("builtins.isInt 3.14"), Value::Bool(false));
4027    }
4028
4029    #[test]
4030    fn eval_builtins_is_bool() {
4031        assert_eq!(ev("builtins.isBool true"), Value::Bool(true));
4032        assert_eq!(ev("builtins.isBool 0"), Value::Bool(false));
4033    }
4034
4035    #[test]
4036    fn eval_builtins_is_string() {
4037        assert_eq!(ev(r#"builtins.isString "hi""#), Value::Bool(true));
4038        assert_eq!(ev("builtins.isString 1"), Value::Bool(false));
4039    }
4040
4041    #[test]
4042    fn eval_builtins_is_list() {
4043        assert_eq!(ev("builtins.isList [1 2]"), Value::Bool(true));
4044        assert_eq!(ev("builtins.isList {}"), Value::Bool(false));
4045    }
4046
4047    #[test]
4048    fn eval_builtins_is_attrs() {
4049        assert_eq!(ev("builtins.isAttrs {}"), Value::Bool(true));
4050        assert_eq!(ev("builtins.isAttrs []"), Value::Bool(false));
4051    }
4052
4053    #[test]
4054    fn eval_builtins_string_length() {
4055        assert_eq!(ev(r#"builtins.stringLength "hello""#), Value::Int(5));
4056        assert_eq!(ev(r#"builtins.stringLength """#), Value::Int(0));
4057    }
4058
4059    #[test]
4060    fn eval_builtins_to_json_roundtrip() {
4061        // toJSON produces a JSON string; fromJSON parses it back
4062        assert_eq!(
4063            ev(r#"builtins.fromJSON (builtins.toJSON 42)"#),
4064            Value::Int(42),
4065        );
4066        assert_eq!(
4067            ev(r#"builtins.fromJSON (builtins.toJSON [1 2 3])"#),
4068            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
4069        );
4070    }
4071
4072    #[test]
4073    fn eval_builtins_from_json() {
4074        assert_eq!(
4075            ev(r#"builtins.fromJSON "{\"a\": 1}""#),
4076            {
4077                let mut attrs = NixAttrs::new();
4078                attrs.insert("a".to_string(), Value::Int(1));
4079                Value::Attrs(Rc::new(attrs))
4080            },
4081        );
4082        assert_eq!(ev(r#"builtins.fromJSON "null""#), Value::Null);
4083        assert_eq!(ev(r#"builtins.fromJSON "true""#), Value::Bool(true));
4084    }
4085
4086    #[test]
4087    fn eval_nested_function_application() {
4088        // (f 1) 2 where f = x: y: x + y
4089        assert_eq!(ev("(x: y: x + y) 1 2"), Value::Int(3));
4090        // equivalent parenthesized form
4091        assert_eq!(ev("((x: y: x + y) 1) 2"), Value::Int(3));
4092    }
4093
4094    #[test]
4095    fn eval_recursive_let() {
4096        assert_eq!(ev("let a = 1; b = a + 1; in b"), Value::Int(2));
4097        assert_eq!(ev("let a = 1; b = a + 1; c = b + 1; in c"), Value::Int(3));
4098    }
4099
4100    #[test]
4101    fn eval_string_comparison() {
4102        assert_eq!(ev(r#""a" < "b""#), Value::Bool(true));
4103        assert_eq!(ev(r#""b" < "a""#), Value::Bool(false));
4104        assert_eq!(ev(r#""abc" == "abc""#), Value::Bool(true));
4105        assert_eq!(ev(r#""abc" != "def""#), Value::Bool(true));
4106    }
4107
4108    #[test]
4109    fn eval_list_in_attrset() {
4110        let v = ev("{ x = [1 2 3]; }.x");
4111        assert_eq!(
4112            v,
4113            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
4114        );
4115    }
4116
4117    #[test]
4118    fn eval_nested_attrset_select() {
4119        assert_eq!(ev("{ a = { b = 42; }; }.a.b"), Value::Int(42));
4120    }
4121
4122    #[test]
4123    fn eval_let_shadows_outer() {
4124        assert_eq!(
4125            ev("let x = 1; in let x = 2; in x"),
4126            Value::Int(2),
4127        );
4128    }
4129
4130    #[test]
4131    fn eval_with_provides_scope() {
4132        // `with` scope is available for name resolution
4133        assert_eq!(
4134            ev("with { x = 42; y = 10; }; x + y"),
4135            Value::Int(52),
4136        );
4137    }
4138
4139    #[test]
4140    fn eval_list_equality() {
4141        assert_eq!(ev("[1 2] == [1 2]"), Value::Bool(true));
4142        assert_eq!(ev("[1 2] == [1 3]"), Value::Bool(false));
4143    }
4144
4145    #[test]
4146    fn eval_attrset_equality() {
4147        assert_eq!(ev("{ a = 1; } == { a = 1; }"), Value::Bool(true));
4148        assert_eq!(ev("{ a = 1; } == { a = 2; }"), Value::Bool(false));
4149    }
4150
4151    // ═══════════════════════════════════════════════════════════
4152    // 1. LITERAL TYPES
4153    // ═══════════════════════════════════════════════════════════
4154
4155    #[test]
4156    fn literal_int_large_zero_negative() {
4157        // Large positive integer (within i64 range)
4158        assert_eq!(ev("9223372036854775807"), Value::Int(i64::MAX));
4159        // Zero
4160        assert_eq!(ev("0"), Value::Int(0));
4161        // Negative via unary negate
4162        assert_eq!(ev("-1"), Value::Int(-1));
4163        assert_eq!(ev("-999999"), Value::Int(-999999));
4164    }
4165
4166    #[test]
4167    fn literal_float_small_large() {
4168        assert_eq!(ev("0.001"), Value::Float(0.001));
4169        assert_eq!(ev("999999.999"), Value::Float(999999.999));
4170        // Float with scientific notation via expression (1e6 parsed by rnix)
4171        assert_eq!(ev("1.0e3"), Value::Float(1000.0));
4172        assert_eq!(ev("1.5e2"), Value::Float(150.0));
4173    }
4174
4175    #[test]
4176    fn literal_string_empty_and_escapes() {
4177        assert_eq!(ev(r#""""#), Value::string(""));
4178        // Escape sequences within strings
4179        assert_eq!(ev(r#""hello\nworld""#), Value::string("hello\nworld"));
4180        assert_eq!(ev(r#""tab\there""#), Value::string("tab\there"));
4181    }
4182
4183    #[test]
4184    fn literal_multiline_string() {
4185        // Indented string ('' ... '')
4186        assert_eq!(
4187            ev("''hello''"),
4188            Value::string("hello"),
4189        );
4190        // Multiline indented string strips common indentation
4191        assert_eq!(
4192            ev("''\n  line1\n  line2\n''"),
4193            Value::string("line1\nline2\n"),
4194        );
4195    }
4196
4197    #[test]
4198    fn literal_paths() {
4199        // Relative path
4200        assert_eq!(ev("./foo"), Value::Path(Box::new(SmolStr::from("./foo"))));
4201        // Absolute path
4202        assert_eq!(ev("/nix/store/abc"), Value::Path(Box::new(SmolStr::from("/nix/store/abc"))));
4203        // Home path
4204        assert_eq!(ev("~/myfile"), Value::Path(Box::new(SmolStr::from("~/myfile"))));
4205    }
4206
4207    // ── Interpolated path literals (cid-marquee root, 2026-07-12) ──
4208    //
4209    // CppNix path literals may contain `${e}` antiquotations: `./${x}.nix`,
4210    // `/a/${e}`, `~/${e}`. sui previously flattened the whole path token to
4211    // raw text and dropped the interpolation (`import ./${x}.nix` →
4212    // `No such file or directory`). The `${e}` must be evaluated,
4213    // string-coerced (plain, no copy-to-store), spliced, and the result is
4214    // still a `path` value. Oracles taken from cppnix.
4215
4216    #[test]
4217    fn interp_path_abs_splices_and_types_path() {
4218        // /a/${x}/b with x="foo" → /a/foo/b, type path (nix oracle).
4219        let v = ev(r#"let x = "foo"; in /a/${x}/b"#);
4220        assert_eq!(v, Value::Path(Box::new(SmolStr::from("/a/foo/b"))));
4221    }
4222
4223    #[test]
4224    fn interp_path_abs_multi_and_slash_in_value() {
4225        // Multiple interpolations + a slash inside the spliced value.
4226        assert_eq!(
4227            ev(r#"let a = "x"; b = "y/z"; in /p/${a}/${b}.nix"#),
4228            Value::Path(Box::new(SmolStr::from("/p/x/y/z.nix"))),
4229        );
4230    }
4231
4232    #[test]
4233    fn interp_path_abs_normalizes_double_slash_seam() {
4234        // A path-typed interpolation splices the raw path (no copy-to-store)
4235        // and the `/` seam is normalized: `/bar/` + `/tmp/foo` → /bar/tmp/foo.
4236        assert_eq!(
4237            ev(r#"/bar/${/tmp/foo}"#),
4238            Value::Path(Box::new(SmolStr::from("/bar/tmp/foo"))),
4239        );
4240    }
4241
4242    #[test]
4243    fn interp_path_rel_resolves_against_eval_dir() {
4244        // The spicetify `map (x: ./${x}.nix) [...]` root: a relative
4245        // interpolated path resolves against the defining file's directory,
4246        // exactly like a plain `./foo.nix` literal.
4247        let _g = push_eval_file(std::path::PathBuf::from("/tmp/example/default.nix"));
4248        assert_eq!(
4249            ev(r#"let x = "foo"; in ./${x}.nix"#),
4250            Value::Path(Box::new(SmolStr::from("/tmp/example/foo.nix"))),
4251        );
4252    }
4253
4254    #[test]
4255    fn interp_path_rel_no_eval_dir_keeps_relative_text() {
4256        // With no eval-file context the plain branch keeps the raw relative
4257        // text; the interpolated branch splices then does the same.
4258        assert_eq!(
4259            ev(r#"let x = "foo"; in ./${x}.nix"#),
4260            Value::Path(Box::new(SmolStr::from("./foo.nix"))),
4261        );
4262    }
4263
4264    #[test]
4265    fn interp_path_home_splices_leading_tilde_preserved() {
4266        // Home paths splice their `${e}`; the leading `~` is carried as-is
4267        // (matching sui's plain `~/foo` behavior — `~`-expansion is a
4268        // separate, pre-existing concern, not introduced here).
4269        assert_eq!(
4270            ev(r#"let x = "foo"; in ~/${x}/bar"#),
4271            Value::Path(Box::new(SmolStr::from("~/foo/bar"))),
4272        );
4273    }
4274
4275    #[test]
4276    fn interp_path_non_interpolated_still_raw() {
4277        // A path with no `${…}` must keep the trivial raw-text shortcut
4278        // (byte-for-byte identical to the plain branch).
4279        assert_eq!(ev("/a/b/c"), Value::Path(Box::new(SmolStr::from("/a/b/c"))));
4280        assert_eq!(ev("~/plain"), Value::Path(Box::new(SmolStr::from("~/plain"))));
4281    }
4282
4283    #[test]
4284    fn literal_null_true_false_standalone() {
4285        assert_eq!(ev("null"), Value::Null);
4286        assert_eq!(ev("true"), Value::Bool(true));
4287        assert_eq!(ev("false"), Value::Bool(false));
4288    }
4289
4290    // ═══════════════════════════════════════════════════════════
4291    // 2. OPERATORS — COMPLETE COVERAGE
4292    // ═══════════════════════════════════════════════════════════
4293
4294    #[test]
4295    fn op_arithmetic_int() {
4296        assert_eq!(ev("100 + 200"), Value::Int(300));
4297        assert_eq!(ev("50 - 30"), Value::Int(20));
4298        assert_eq!(ev("7 * 8"), Value::Int(56));
4299        assert_eq!(ev("17 / 3"), Value::Int(5)); // integer division
4300    }
4301
4302    #[test]
4303    fn op_arithmetic_float() {
4304        assert_eq!(ev("1.5 + 2.5"), Value::Float(4.0));
4305        assert_eq!(ev("5.0 - 1.5"), Value::Float(3.5));
4306        assert_eq!(ev("2.0 * 3.0"), Value::Float(6.0));
4307        assert_eq!(ev("7.0 / 2.0"), Value::Float(3.5));
4308    }
4309
4310    #[test]
4311    fn op_arithmetic_mixed_int_float() {
4312        // int + float => float
4313        assert_eq!(ev("1 + 2.5"), Value::Float(3.5));
4314        assert_eq!(ev("2.5 + 1"), Value::Float(3.5));
4315        // int * float => float
4316        assert_eq!(ev("2 * 1.5"), Value::Float(3.0));
4317        // float - int => float
4318        assert_eq!(ev("5.5 - 2"), Value::Float(3.5));
4319    }
4320
4321    #[test]
4322    fn op_string_concat() {
4323        assert_eq!(ev(r#""foo" + "bar""#), Value::string("foobar"));
4324        assert_eq!(ev(r#""" + "x""#), Value::string("x"));
4325        assert_eq!(ev(r#""a" + "" + "b""#), Value::string("ab"));
4326    }
4327
4328    #[test]
4329    fn op_path_concat() {
4330        // path + string
4331        assert_eq!(ev(r#"./foo + "/bar""#), Value::Path(Box::new(SmolStr::from("./foo/bar"))));
4332        // path + path (should join with /)
4333        assert_eq!(ev("./a + ./b"), Value::Path(Box::new(SmolStr::from("./a/./b"))));
4334    }
4335
4336    #[test]
4337    fn op_comparison_ints() {
4338        assert_eq!(ev("1 < 2"), Value::Bool(true));
4339        assert_eq!(ev("2 < 1"), Value::Bool(false));
4340        assert_eq!(ev("2 > 1"), Value::Bool(true));
4341        assert_eq!(ev("1 > 2"), Value::Bool(false));
4342        assert_eq!(ev("2 <= 2"), Value::Bool(true));
4343        assert_eq!(ev("3 <= 2"), Value::Bool(false));
4344        assert_eq!(ev("2 >= 2"), Value::Bool(true));
4345        assert_eq!(ev("1 >= 2"), Value::Bool(false));
4346    }
4347
4348    #[test]
4349    fn op_comparison_floats() {
4350        assert_eq!(ev("1.5 < 2.5"), Value::Bool(true));
4351        assert_eq!(ev("2.5 > 1.5"), Value::Bool(true));
4352        assert_eq!(ev("1.5 <= 1.5"), Value::Bool(true));
4353        assert_eq!(ev("1.5 >= 1.5"), Value::Bool(true));
4354    }
4355
4356    #[test]
4357    fn op_comparison_strings() {
4358        assert_eq!(ev(r#""apple" < "banana""#), Value::Bool(true));
4359        assert_eq!(ev(r#""banana" > "apple""#), Value::Bool(true));
4360        assert_eq!(ev(r#""abc" == "abc""#), Value::Bool(true));
4361        assert_eq!(ev(r#""abc" != "xyz""#), Value::Bool(true));
4362        assert_eq!(ev(r#""abc" <= "abd""#), Value::Bool(true));
4363        assert_eq!(ev(r#""abc" >= "abb""#), Value::Bool(true));
4364    }
4365
4366    #[test]
4367    fn op_equality_various_types() {
4368        assert_eq!(ev("null == null"), Value::Bool(true));
4369        assert_eq!(ev("true == true"), Value::Bool(true));
4370        assert_eq!(ev("false == false"), Value::Bool(true));
4371        assert_eq!(ev("true == false"), Value::Bool(false));
4372        assert_eq!(ev("1 == 1"), Value::Bool(true));
4373        assert_eq!(ev("1 != 2"), Value::Bool(true));
4374        // Different types are not equal
4375        assert_eq!(ev(r#"1 == "1""#), Value::Bool(false));
4376        assert_eq!(ev("null == false"), Value::Bool(false));
4377    }
4378
4379    #[test]
4380    fn op_logic_short_circuit() {
4381        // false && <error> should NOT evaluate the RHS
4382        assert_eq!(ev("false && (1 / 0 == 0)"), Value::Bool(false));
4383        // true || <error> should NOT evaluate the RHS
4384        assert_eq!(ev("true || (1 / 0 == 0)"), Value::Bool(true));
4385    }
4386
4387    #[test]
4388    fn op_logic_full() {
4389        assert_eq!(ev("true && true"), Value::Bool(true));
4390        assert_eq!(ev("true && false"), Value::Bool(false));
4391        assert_eq!(ev("false && true"), Value::Bool(false));
4392        assert_eq!(ev("false && false"), Value::Bool(false));
4393        assert_eq!(ev("true || true"), Value::Bool(true));
4394        assert_eq!(ev("true || false"), Value::Bool(true));
4395        assert_eq!(ev("false || true"), Value::Bool(true));
4396        assert_eq!(ev("false || false"), Value::Bool(false));
4397        assert_eq!(ev("!true"), Value::Bool(false));
4398        assert_eq!(ev("!false"), Value::Bool(true));
4399    }
4400
4401    #[test]
4402    fn op_implication_truth_table() {
4403        // false -> anything = true
4404        assert_eq!(ev("false -> false"), Value::Bool(true));
4405        assert_eq!(ev("false -> true"), Value::Bool(true));
4406        // true -> x = x
4407        assert_eq!(ev("true -> true"), Value::Bool(true));
4408        assert_eq!(ev("true -> false"), Value::Bool(false));
4409    }
4410
4411    #[test]
4412    fn op_implication_short_circuit() {
4413        // false -> <error> should NOT evaluate the RHS
4414        assert_eq!(ev("false -> (1 / 0 == 0)"), Value::Bool(true));
4415    }
4416
4417    #[test]
4418    fn op_update_merge() {
4419        let v = ev("{ a = 1; } // { b = 2; }");
4420        if let Value::Attrs(attrs) = v {
4421            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
4422            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
4423        } else {
4424            panic!("expected attrs");
4425        }
4426    }
4427
4428    #[test]
4429    fn op_update_right_wins() {
4430        assert_eq!(ev("({ a = 1; } // { a = 2; }).a"), Value::Int(2));
4431    }
4432
4433    #[test]
4434    fn op_list_concat() {
4435        assert_eq!(
4436            ev("[1 2] ++ [3 4]"),
4437            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(4)]),
4438        );
4439        // Empty list concat
4440        assert_eq!(ev("[] ++ [1]"), Value::list(vec![Value::Int(1)]));
4441        assert_eq!(ev("[1] ++ []"), Value::list(vec![Value::Int(1)]));
4442    }
4443
4444    #[test]
4445    fn op_has_attr_present_and_absent() {
4446        assert_eq!(ev("{ x = 1; y = 2; } ? x"), Value::Bool(true));
4447        assert_eq!(ev("{ x = 1; } ? z"), Value::Bool(false));
4448        assert_eq!(ev("{} ? anything"), Value::Bool(false));
4449    }
4450
4451    #[test]
4452    fn op_unary_negate() {
4453        assert_eq!(ev("-42"), Value::Int(-42));
4454        assert_eq!(ev("-3.14"), Value::Float(-3.14));
4455        // Double negate
4456        assert_eq!(ev("- -5"), Value::Int(5));
4457    }
4458
4459    // ═══════════════════════════════════════════════════════════
4460    // 3. CONTROL FLOW
4461    // ═══════════════════════════════════════════════════════════
4462
4463    #[test]
4464    fn control_if_true_branch() {
4465        assert_eq!(ev("if true then 42 else 0"), Value::Int(42));
4466    }
4467
4468    #[test]
4469    fn control_if_false_branch() {
4470        assert_eq!(ev("if false then 42 else 0"), Value::Int(0));
4471    }
4472
4473    #[test]
4474    fn control_if_nested() {
4475        assert_eq!(
4476            ev("if true then (if false then 1 else 2) else 3"),
4477            Value::Int(2),
4478        );
4479        assert_eq!(
4480            ev("if false then 1 else (if true then 2 else 3)"),
4481            Value::Int(2),
4482        );
4483    }
4484
4485    #[test]
4486    fn control_assert_passing() {
4487        assert_eq!(ev("assert 1 == 1; 42"), Value::Int(42));
4488        assert_eq!(ev("assert true; true"), Value::Bool(true));
4489    }
4490
4491    #[test]
4492    fn control_assert_failing() {
4493        assert!(eval("assert false; 42").is_err());
4494        assert!(eval("assert 1 == 2; 42").is_err());
4495    }
4496
4497    #[test]
4498    fn control_with_basic_scope() {
4499        assert_eq!(ev("with { a = 1; b = 2; }; a + b"), Value::Int(3));
4500    }
4501
4502    #[test]
4503    fn control_with_lexical_precedence() {
4504        // let binding takes precedence over with scope
4505        assert_eq!(
4506            ev("let x = 10; in with { x = 99; }; x"),
4507            Value::Int(10),
4508        );
4509    }
4510
4511    #[test]
4512    fn control_with_nested() {
4513        assert_eq!(
4514            ev("with { a = 1; }; with { b = 2; }; a + b"),
4515            Value::Int(3),
4516        );
4517    }
4518
4519    #[test]
4520    fn control_with_lazy_fix_self() {
4521        // THE critical pattern that nixpkgs requires:
4522        // fix (self: with self; { a = 1; b = a + 1; })
4523        // Before the lazy-with fix, this would hit the blackhole detector
4524        // because `with` eagerly forced `self`.
4525        let result = eval(
4526            "let fix = f: let x = f x; in x; in fix (self: with self; { a = 1; b = a + 1; })"
4527        );
4528        assert!(result.is_ok(), "fix with self should work: {:?}", result);
4529        if let Ok(Value::Attrs(attrs)) = result {
4530            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
4531            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
4532        } else {
4533            panic!("expected Attrs, got {:?}", result);
4534        }
4535    }
4536
4537    #[test]
4538    fn control_with_lazy_fix_self_lib_pattern() {
4539        // The nixpkgs pattern: self-referential package set with lib.
4540        // Access via select to force through the thunk layer.
4541        let result = eval(r#"
4542            let fix = f: let x = f x; in x;
4543            in (fix (self: with self; {
4544                lib = { version = "1.0"; };
4545                hello = "hello ${lib.version}";
4546            })).hello
4547        "#);
4548        assert!(result.is_ok(), "nixpkgs-style lib pattern: {:?}", result);
4549        assert_eq!(
4550            result.unwrap(),
4551            Value::String(Rc::new(NixString::plain("hello 1.0"))),
4552        );
4553    }
4554
4555    #[test]
4556    fn control_with_non_attrset_errors() {
4557        // CppNix errors when with-scope is not an attrset and a lookup hits it
4558        let result = eval("with 42; 1");
4559        // The body `1` is a literal and doesn't look up anything in the
4560        // with-scope, so this should succeed (the scope is never forced).
4561        assert_eq!(result.unwrap(), Value::Int(1));
4562    }
4563
4564    #[test]
4565    fn control_with_non_attrset_lookup_falls_through() {
4566        // If the with scope is not an attrset, lookups should fall through
4567        // to outer scopes rather than crashing.
4568        let result = eval("let x = 1; in with 42; x");
4569        assert_eq!(result.unwrap(), Value::Int(1));
4570    }
4571
4572    #[test]
4573    fn control_let_simple_and_multiple() {
4574        assert_eq!(ev("let x = 5; in x"), Value::Int(5));
4575        assert_eq!(ev("let x = 1; y = 2; z = 3; in x + y + z"), Value::Int(6));
4576    }
4577
4578    #[test]
4579    fn control_let_shadow_outer() {
4580        assert_eq!(
4581            ev("let x = 1; in let x = 2; in x"),
4582            Value::Int(2),
4583        );
4584    }
4585
4586    #[test]
4587    fn control_let_recursive_reference() {
4588        assert_eq!(ev("let a = 1; b = a + 1; in b"), Value::Int(2));
4589        assert_eq!(ev("let a = 1; b = a + 1; c = b + 1; in c"), Value::Int(3));
4590    }
4591
4592    #[test]
4593    fn control_nested_let_expression() {
4594        assert_eq!(
4595            ev("let a = let b = 1; in b; in a"),
4596            Value::Int(1),
4597        );
4598        assert_eq!(
4599            ev("let a = let b = 10; in b + 5; in a * 2"),
4600            Value::Int(30),
4601        );
4602    }
4603
4604    // ═══════════════════════════════════════════════════════════
4605    // 4. FUNCTIONS — COMPLETE COVERAGE
4606    // ═══════════════════════════════════════════════════════════
4607
4608    #[test]
4609    fn func_identity_lambda() {
4610        assert_eq!(ev("(x: x) 42"), Value::Int(42));
4611        assert_eq!(ev(r#"(x: x) "hello""#), Value::string("hello"));
4612    }
4613
4614    #[test]
4615    fn func_curried_two_args() {
4616        assert_eq!(ev("(x: y: x + y) 3 4"), Value::Int(7));
4617    }
4618
4619    #[test]
4620    fn func_curried_three_args() {
4621        assert_eq!(ev("(a: b: c: a + b + c) 1 2 3"), Value::Int(6));
4622    }
4623
4624    #[test]
4625    fn func_formals_basic() {
4626        assert_eq!(ev("({ a, b }: a + b) { a = 3; b = 7; }"), Value::Int(10));
4627    }
4628
4629    #[test]
4630    fn func_formals_with_defaults() {
4631        assert_eq!(ev("({ a, b ? 10 }: a + b) { a = 5; }"), Value::Int(15));
4632        // Providing the default-able argument overrides the default
4633        assert_eq!(ev("({ a, b ? 10 }: a + b) { a = 5; b = 20; }"), Value::Int(25));
4634    }
4635
4636    #[test]
4637    fn func_formals_with_ellipsis() {
4638        assert_eq!(ev("({ a, ... }: a) { a = 1; b = 2; c = 3; }"), Value::Int(1));
4639    }
4640
4641    #[test]
4642    fn func_named_formals_at_before() {
4643        // args @ { a, b }: ...
4644        assert_eq!(
4645            ev("(args @ { a, b }: args.a + args.b) { a = 3; b = 4; }"),
4646            Value::Int(7),
4647        );
4648    }
4649
4650    #[test]
4651    fn func_named_formals_at_after() {
4652        // { a, b } @ args: ...
4653        assert_eq!(
4654            ev("({ a, b } @ args: args.a + args.b) { a = 10; b = 20; }"),
4655            Value::Int(30),
4656        );
4657    }
4658
4659    #[test]
4660    fn func_nested_application() {
4661        // Explicit parenthesized application
4662        assert_eq!(ev("((x: y: x * y) 3) 4"), Value::Int(12));
4663    }
4664
4665    #[test]
4666    fn func_higher_order_map() {
4667        assert_eq!(
4668            ev("builtins.map (x: x * 2) [1 2 3]"),
4669            Value::list(vec![Value::Int(2), Value::Int(4), Value::Int(6)]),
4670        );
4671    }
4672
4673    #[test]
4674    fn func_higher_order_filter() {
4675        assert_eq!(
4676            ev("builtins.filter (x: x > 2) [1 2 3 4 5]"),
4677            Value::list(vec![Value::Int(3), Value::Int(4), Value::Int(5)]),
4678        );
4679    }
4680
4681    #[test]
4682    fn func_higher_order_foldl() {
4683        // Sum of list via foldl'
4684        assert_eq!(
4685            ev("builtins.foldl' (acc: x: acc + x) 0 [1 2 3 4]"),
4686            Value::Int(10),
4687        );
4688    }
4689
4690    #[test]
4691    fn func_as_attrset_value() {
4692        assert_eq!(
4693            ev("let s = { f = x: x + 1; }; in s.f 5"),
4694            Value::Int(6),
4695        );
4696    }
4697
4698    #[test]
4699    fn func_immediate_application() {
4700        assert_eq!(ev("(x: x * x) 7"), Value::Int(49));
4701    }
4702
4703    #[test]
4704    fn func_in_let_binding() {
4705        assert_eq!(
4706            ev("let double = x: x * 2; in double 21"),
4707            Value::Int(42),
4708        );
4709    }
4710
4711    // ═══════════════════════════════════════════════════════════
4712    // 5. ATTRIBUTE SETS — COMPLETE COVERAGE
4713    // ═══════════════════════════════════════════════════════════
4714
4715    #[test]
4716    fn attrs_empty_set() {
4717        let v = ev("{}");
4718        if let Value::Attrs(attrs) = v {
4719            assert!(attrs.is_empty());
4720        } else {
4721            panic!("expected attrs");
4722        }
4723    }
4724
4725    #[test]
4726    fn attrs_simple() {
4727        assert_eq!(ev("{ a = 1; }.a"), Value::Int(1));
4728    }
4729
4730    #[test]
4731    fn attrs_nested_access() {
4732        assert_eq!(ev("{ a = { b = { c = 42; }; }; }.a.b.c"), Value::Int(42));
4733    }
4734
4735    #[test]
4736    fn attrs_recursive_set() {
4737        assert_eq!(ev("(rec { a = 1; b = a + 1; c = b + 1; }).c"), Value::Int(3));
4738    }
4739
4740    #[test]
4741    fn attrs_update_disjoint() {
4742        let v = ev("{ a = 1; } // { b = 2; }");
4743        if let Value::Attrs(attrs) = v {
4744            assert_eq!(attrs.len(), 2);
4745            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
4746            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
4747        } else {
4748            panic!("expected attrs");
4749        }
4750    }
4751
4752    #[test]
4753    fn attrs_update_override() {
4754        assert_eq!(ev("({ a = 1; } // { a = 2; }).a"), Value::Int(2));
4755    }
4756
4757    #[test]
4758    fn attrs_has_attr_operator() {
4759        assert_eq!(ev("{ a = 1; } ? a"), Value::Bool(true));
4760        assert_eq!(ev("{ a = 1; } ? b"), Value::Bool(false));
4761    }
4762
4763    #[test]
4764    fn attrs_select_with_default() {
4765        assert_eq!(ev("{ a = 1; }.a or 99"), Value::Int(1));
4766        assert_eq!(ev("{}.missing or 99"), Value::Int(99));
4767        assert_eq!(ev("{ a = 1; }.b or 42"), Value::Int(42));
4768    }
4769
4770    #[test]
4771    fn attrs_nested_attr_path_in_binding() {
4772        // { a.b = 1; } creates { a = { b = 1; }; }
4773        assert_eq!(ev("{ a.b = 1; }.a.b"), Value::Int(1));
4774    }
4775
4776    #[test]
4777    fn attrs_inherit_from_scope() {
4778        assert_eq!(ev("let x = 1; y = 2; in { inherit x y; }.x"), Value::Int(1));
4779        assert_eq!(ev("let x = 1; y = 2; in { inherit x y; }.y"), Value::Int(2));
4780    }
4781
4782    #[test]
4783    fn attrs_inherit_from_expr() {
4784        assert_eq!(
4785            ev("{ inherit ({ a = 42; b = 10; }) a; }.a"),
4786            Value::Int(42),
4787        );
4788    }
4789
4790    #[test]
4791    fn attrs_dynamic_attr_name() {
4792        assert_eq!(
4793            ev(r#"let name = "x"; in { ${name} = 42; }.x"#),
4794            Value::Int(42),
4795        );
4796    }
4797
4798    #[test]
4799    fn attrs_attr_names_sorted() {
4800        assert_eq!(
4801            ev("builtins.attrNames { z = 1; m = 2; a = 3; }"),
4802            Value::list(vec![
4803                Value::string("a"),
4804                Value::string("m"),
4805                Value::string("z"),
4806            ]),
4807        );
4808    }
4809
4810    #[test]
4811    fn attrs_attr_values_follow_key_order() {
4812        // BTreeMap iteration order: a=1, b=2, c=3
4813        assert_eq!(
4814            ev("builtins.attrValues { c = 3; a = 1; b = 2; }"),
4815            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
4816        );
4817    }
4818
4819    #[test]
4820    fn attrs_update_is_shallow() {
4821        // // is a shallow merge; nested attrs are replaced, not merged
4822        assert_eq!(
4823            ev("({ a = { x = 1; }; } // { a = { y = 2; }; }).a ? x"),
4824            Value::Bool(false),
4825        );
4826        assert_eq!(
4827            ev("({ a = { x = 1; }; } // { a = { y = 2; }; }).a.y"),
4828            Value::Int(2),
4829        );
4830    }
4831
4832    // ═══════════════════════════════════════════════════════════
4833    // 6. LISTS — COMPLETE COVERAGE
4834    // ═══════════════════════════════════════════════════════════
4835
4836    #[test]
4837    fn list_empty() {
4838        assert_eq!(ev("[]"), Value::list(vec![]));
4839    }
4840
4841    #[test]
4842    fn list_single_element() {
4843        assert_eq!(ev("[1]"), Value::list(vec![Value::Int(1)]));
4844    }
4845
4846    #[test]
4847    fn list_mixed_types() {
4848        assert_eq!(
4849            ev(r#"[1 "two" true null]"#),
4850            Value::list(vec![
4851                Value::Int(1),
4852                Value::string("two"),
4853                Value::Bool(true),
4854                Value::Null,
4855            ]),
4856        );
4857    }
4858
4859    #[test]
4860    fn list_nested() {
4861        assert_eq!(
4862            ev("[[1 2] [3 4]]"),
4863            Value::list(vec![
4864                Value::list(vec![Value::Int(1), Value::Int(2)]),
4865                Value::list(vec![Value::Int(3), Value::Int(4)]),
4866            ]),
4867        );
4868    }
4869
4870    #[test]
4871    fn list_concat_operator() {
4872        assert_eq!(
4873            ev("[1] ++ [2] ++ [3]"),
4874            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
4875        );
4876    }
4877
4878    #[test]
4879    fn list_builtins_length() {
4880        assert_eq!(ev("builtins.length [1 2 3]"), Value::Int(3));
4881        assert_eq!(ev("builtins.length []"), Value::Int(0));
4882    }
4883
4884    #[test]
4885    fn list_builtins_elem_at() {
4886        assert_eq!(ev("builtins.elemAt [10 20 30] 0"), Value::Int(10));
4887        assert_eq!(ev("builtins.elemAt [10 20 30] 1"), Value::Int(20));
4888        assert_eq!(ev("builtins.elemAt [10 20 30] 2"), Value::Int(30));
4889    }
4890
4891    #[test]
4892    fn list_equality() {
4893        assert_eq!(ev("[1 2 3] == [1 2 3]"), Value::Bool(true));
4894        assert_eq!(ev("[1 2] == [1 2 3]"), Value::Bool(false));
4895        assert_eq!(ev("[] == []"), Value::Bool(true));
4896    }
4897
4898    // ═══════════════════════════════════════════════════════════
4899    // 7. STRING INTERPOLATION
4900    // ═══════════════════════════════════════════════════════════
4901
4902    #[test]
4903    fn interp_simple_variable() {
4904        assert_eq!(
4905            ev(r#"let name = "world"; in "hello ${name}""#),
4906            Value::string("hello world"),
4907        );
4908    }
4909
4910    #[test]
4911    fn interp_nested_expression() {
4912        assert_eq!(
4913            ev(r#""result: ${builtins.toString (1 + 2)}""#),
4914            Value::string("result: 3"),
4915        );
4916    }
4917
4918    #[test]
4919    fn interp_int_coercion() {
4920        // Ints are coerced to string in interpolation
4921        assert_eq!(
4922            ev(r#"let x = 42; in "count: ${builtins.toString x}""#),
4923            Value::string("count: 42"),
4924        );
4925    }
4926
4927    #[test]
4928    fn interp_multiple() {
4929        assert_eq!(
4930            ev(r#"let a = "foo"; b = "bar"; in "${a} and ${b}""#),
4931            Value::string("foo and bar"),
4932        );
4933    }
4934
4935    #[test]
4936    fn interp_in_let() {
4937        assert_eq!(
4938            ev(r#"let x = "world"; in "hello ${x}""#),
4939            Value::string("hello world"),
4940        );
4941    }
4942
4943    #[test]
4944    fn interp_empty_result() {
4945        assert_eq!(
4946            ev(r#"let x = ""; in "a${x}b""#),
4947            Value::string("ab"),
4948        );
4949    }
4950
4951    #[test]
4952    fn interp_path_in_string_context() {
4953        // CppNix string interpolation is copy-to-store coercion: a nonexistent
4954        // path errors "path '…' does not exist" (previously sui spliced the raw
4955        // relative path "./foo" verbatim, diverging from nix). The positive
4956        // copy-to-store case is byte-verified in
4957        // interp_path_copies_to_store_byte_matches_cppnix below.
4958        assert!(eval(r#""path: ${./foo-nonexistent-xyz}""#).is_err());
4959    }
4960
4961    #[test]
4962    fn interp_adjacent_interpolations() {
4963        assert_eq!(
4964            ev(r#"let a = "x"; b = "y"; in "${a}${b}""#),
4965            Value::string("xy"),
4966        );
4967    }
4968
4969    // ═══════════════════════════════════════════════════════════
4970    // 8. BUILTINS — VERIFY ALL MAJOR ONES
4971    // ═══════════════════════════════════════════════════════════
4972
4973    #[test]
4974    fn builtins_map_filter_foldl() {
4975        // map
4976        assert_eq!(
4977            ev("builtins.map (x: x + 10) [1 2 3]"),
4978            Value::list(vec![Value::Int(11), Value::Int(12), Value::Int(13)]),
4979        );
4980        // filter
4981        assert_eq!(
4982            ev("builtins.filter (x: x > 1) [1 2 3]"),
4983            Value::list(vec![Value::Int(2), Value::Int(3)]),
4984        );
4985        // foldl' — product
4986        assert_eq!(
4987            ev("builtins.foldl' (a: b: a * b) 1 [2 3 4]"),
4988            Value::Int(24),
4989        );
4990    }
4991
4992    #[test]
4993    fn builtins_map_attrs() {
4994        assert_eq!(
4995            ev("(builtins.mapAttrs (name: value: value * 2) { a = 1; b = 2; }).a"),
4996            Value::Int(2),
4997        );
4998        assert_eq!(
4999            ev("(builtins.mapAttrs (name: value: value * 2) { a = 1; b = 2; }).b"),
5000            Value::Int(4),
5001        );
5002    }
5003
5004    #[test]
5005    fn builtins_list_to_attrs() {
5006        assert_eq!(
5007            ev(r#"(builtins.listToAttrs [{ name = "x"; value = 1; } { name = "y"; value = 2; }]).x"#),
5008            Value::Int(1),
5009        );
5010    }
5011
5012    #[test]
5013    fn builtins_list_to_attrs_duplicate_key_first_wins() {
5014        // Nix `listToAttrs` keeps the FIRST occurrence of a duplicate `name`
5015        // (later duplicates are ignored). cppnix returns 1 here, not 2.
5016        // Byte-parity root (cid darwin): a Cargo.lock listing a crate twice
5017        // (registry entry then git entry of the same name+version) must
5018        // resolve to the FIRST source, so `substrate/lockfile-delta.nix`'s
5019        // `lockByKey` picks the registry crate exactly as nix does. Last-wins
5020        // silently switched the source to git and produced a structurally
5021        // different `rust_<crate>` derivation.
5022        assert_eq!(
5023            ev(r#"(builtins.listToAttrs [{ name = "k"; value = 1; } { name = "k"; value = 2; }]).k"#),
5024            Value::Int(1),
5025        );
5026    }
5027
5028    #[test]
5029    fn builtins_concat_map() {
5030        assert_eq!(
5031            ev("builtins.concatMap (x: [x (x * 2)]) [1 2 3]"),
5032            Value::list(vec![
5033                Value::Int(1), Value::Int(2),
5034                Value::Int(2), Value::Int(4),
5035                Value::Int(3), Value::Int(6),
5036            ]),
5037        );
5038    }
5039
5040    #[test]
5041    fn builtins_concat_lists() {
5042        assert_eq!(
5043            ev("builtins.concatLists [[1 2] [3] [4 5]]"),
5044            Value::list(vec![
5045                Value::Int(1), Value::Int(2), Value::Int(3),
5046                Value::Int(4), Value::Int(5),
5047            ]),
5048        );
5049    }
5050
5051    #[test]
5052    fn builtins_concat_strings_sep() {
5053        assert_eq!(
5054            ev(r#"builtins.concatStringsSep ", " ["a" "b" "c"]"#),
5055            Value::string("a, b, c"),
5056        );
5057        assert_eq!(
5058            ev(r#"builtins.concatStringsSep "" ["x" "y"]"#),
5059            Value::string("xy"),
5060        );
5061    }
5062
5063    #[test]
5064    fn builtins_replace_strings() {
5065        assert_eq!(
5066            ev(r#"builtins.replaceStrings ["o"] ["0"] "foobar""#),
5067            Value::string("f00bar"),
5068        );
5069        assert_eq!(
5070            ev(r#"builtins.replaceStrings ["hello"] ["goodbye"] "hello world""#),
5071            Value::string("goodbye world"),
5072        );
5073    }
5074
5075    #[test]
5076    fn builtins_has_prefix_has_suffix() {
5077        assert_eq!(ev(r#"builtins.hasPrefix "he" "hello""#), Value::Bool(true));
5078        assert_eq!(ev(r#"builtins.hasPrefix "xx" "hello""#), Value::Bool(false));
5079        assert_eq!(ev(r#"builtins.hasSuffix "lo" "hello""#), Value::Bool(true));
5080        assert_eq!(ev(r#"builtins.hasSuffix "xx" "hello""#), Value::Bool(false));
5081    }
5082
5083    #[test]
5084    fn builtins_all_any() {
5085        assert_eq!(ev("builtins.all (x: x > 0) [1 2 3]"), Value::Bool(true));
5086        assert_eq!(ev("builtins.all (x: x > 1) [1 2 3]"), Value::Bool(false));
5087        assert_eq!(ev("builtins.any (x: x > 2) [1 2 3]"), Value::Bool(true));
5088        assert_eq!(ev("builtins.any (x: x > 5) [1 2 3]"), Value::Bool(false));
5089    }
5090
5091    #[test]
5092    fn builtins_sort() {
5093        assert_eq!(
5094            ev("builtins.sort (a: b: a < b) [3 1 2]"),
5095            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
5096        );
5097    }
5098
5099    #[test]
5100    fn builtins_remove_attrs() {
5101        let v = ev(r#"builtins.removeAttrs { a = 1; b = 2; c = 3; } ["b" "c"]"#);
5102        if let Value::Attrs(attrs) = v {
5103            assert_eq!(attrs.len(), 1);
5104            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
5105            assert!(attrs.get("b").is_none());
5106        } else {
5107            panic!("expected attrs");
5108        }
5109    }
5110
5111    #[test]
5112    fn builtins_intersect_attrs() {
5113        let v = ev("builtins.intersectAttrs { a = 1; b = 2; } { b = 20; c = 30; }");
5114        if let Value::Attrs(attrs) = v {
5115            assert_eq!(attrs.len(), 1);
5116            // intersectAttrs returns values from the second set
5117            assert_eq!(attrs.get("b"), Some(&Value::Int(20)));
5118        } else {
5119            panic!("expected attrs");
5120        }
5121    }
5122
5123    #[test]
5124    fn builtins_type_of_all_types() {
5125        assert_eq!(ev("builtins.typeOf null"), Value::string("null"));
5126        assert_eq!(ev("builtins.typeOf true"), Value::string("bool"));
5127        assert_eq!(ev("builtins.typeOf 42"), Value::string("int"));
5128        assert_eq!(ev("builtins.typeOf 3.14"), Value::string("float"));
5129        assert_eq!(ev(r#"builtins.typeOf "hi""#), Value::string("string"));
5130        assert_eq!(ev("builtins.typeOf [1]"), Value::string("list"));
5131        assert_eq!(ev("builtins.typeOf {}"), Value::string("set"));
5132        assert_eq!(ev("builtins.typeOf (x: x)"), Value::string("lambda"));
5133    }
5134
5135    #[test]
5136    fn builtins_is_type_checks() {
5137        assert_eq!(ev("builtins.isNull null"), Value::Bool(true));
5138        assert_eq!(ev("builtins.isNull 0"), Value::Bool(false));
5139        assert_eq!(ev("builtins.isInt 42"), Value::Bool(true));
5140        assert_eq!(ev("builtins.isInt 3.14"), Value::Bool(false));
5141        assert_eq!(ev("builtins.isBool true"), Value::Bool(true));
5142        assert_eq!(ev("builtins.isBool 1"), Value::Bool(false));
5143        assert_eq!(ev(r#"builtins.isString "x""#), Value::Bool(true));
5144        assert_eq!(ev("builtins.isString 1"), Value::Bool(false));
5145        assert_eq!(ev("builtins.isList []"), Value::Bool(true));
5146        assert_eq!(ev("builtins.isList {}"), Value::Bool(false));
5147        assert_eq!(ev("builtins.isAttrs {}"), Value::Bool(true));
5148        assert_eq!(ev("builtins.isAttrs []"), Value::Bool(false));
5149        assert_eq!(ev("builtins.isFunction (x: x)"), Value::Bool(true));
5150        assert_eq!(ev("builtins.isFunction 1"), Value::Bool(false));
5151        assert_eq!(ev("builtins.isFloat 3.14"), Value::Bool(true));
5152        assert_eq!(ev("builtins.isFloat 1"), Value::Bool(false));
5153    }
5154
5155    #[test]
5156    fn builtins_to_json_from_json_roundtrip() {
5157        // int roundtrip
5158        assert_eq!(ev("builtins.fromJSON (builtins.toJSON 42)"), Value::Int(42));
5159        // string roundtrip
5160        assert_eq!(
5161            ev(r#"builtins.fromJSON (builtins.toJSON "hello")"#),
5162            Value::string("hello"),
5163        );
5164        // list roundtrip
5165        assert_eq!(
5166            ev("builtins.fromJSON (builtins.toJSON [1 2 3])"),
5167            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
5168        );
5169        // null roundtrip
5170        assert_eq!(ev("builtins.fromJSON (builtins.toJSON null)"), Value::Null);
5171        // bool roundtrip
5172        assert_eq!(ev("builtins.fromJSON (builtins.toJSON true)"), Value::Bool(true));
5173    }
5174
5175    #[test]
5176    fn builtins_to_string_various() {
5177        assert_eq!(ev("builtins.toString 42"), Value::string("42"));
5178        assert_eq!(ev("builtins.toString true"), Value::string("1"));
5179        assert_eq!(ev("builtins.toString false"), Value::string(""));
5180        assert_eq!(ev("builtins.toString null"), Value::string(""));
5181        assert_eq!(ev(r#"builtins.toString "hello""#), Value::string("hello"));
5182    }
5183
5184    #[test]
5185    fn builtins_function_args() {
5186        let v = ev("builtins.functionArgs ({ a, b ? 1 }: a)");
5187        if let Value::Attrs(attrs) = v {
5188            assert_eq!(attrs.get("a"), Some(&Value::Bool(false))); // no default
5189            assert_eq!(attrs.get("b"), Some(&Value::Bool(true)));  // has default
5190        } else {
5191            panic!("expected attrs");
5192        }
5193    }
5194
5195    #[test]
5196    fn builtins_gen_list() {
5197        assert_eq!(
5198            ev("builtins.genList (x: x * x) 5"),
5199            Value::list(vec![
5200                Value::Int(0), Value::Int(1), Value::Int(4),
5201                Value::Int(9), Value::Int(16),
5202            ]),
5203        );
5204        assert_eq!(ev("builtins.genList (x: x) 0"), Value::list(vec![]));
5205    }
5206
5207    #[test]
5208    fn builtins_elem() {
5209        assert_eq!(ev("builtins.elem 2 [1 2 3]"), Value::Bool(true));
5210        assert_eq!(ev("builtins.elem 5 [1 2 3]"), Value::Bool(false));
5211        assert_eq!(ev("builtins.elem 1 []"), Value::Bool(false));
5212    }
5213
5214    #[test]
5215    fn builtins_head_tail() {
5216        assert_eq!(ev("builtins.head [10 20 30]"), Value::Int(10));
5217        assert_eq!(
5218            ev("builtins.tail [10 20 30]"),
5219            Value::list(vec![Value::Int(20), Value::Int(30)]),
5220        );
5221    }
5222
5223    #[test]
5224    fn builtins_string_length() {
5225        assert_eq!(ev(r#"builtins.stringLength "hello""#), Value::Int(5));
5226        assert_eq!(ev(r#"builtins.stringLength """#), Value::Int(0));
5227        assert_eq!(ev(r#"builtins.stringLength "abc def""#), Value::Int(7));
5228    }
5229
5230    #[test]
5231    fn builtins_ceil_floor() {
5232        assert_eq!(ev("builtins.ceil 2.3"), Value::Int(3));
5233        assert_eq!(ev("builtins.ceil 2.0"), Value::Int(2));
5234        assert_eq!(ev("builtins.floor 2.9"), Value::Int(2));
5235        assert_eq!(ev("builtins.floor 2.0"), Value::Int(2));
5236        // Int coercion: ceil/floor on int should work via to_float()
5237        assert_eq!(ev("builtins.ceil 5"), Value::Int(5));
5238        assert_eq!(ev("builtins.floor 5"), Value::Int(5));
5239    }
5240
5241    #[test]
5242    fn builtins_try_eval() {
5243        let v = ev("builtins.tryEval 42");
5244        if let Value::Attrs(attrs) = v {
5245            assert_eq!(attrs.get("success"), Some(&Value::Bool(true)));
5246            assert_eq!(attrs.get("value"), Some(&Value::Int(42)));
5247        } else {
5248            panic!("expected attrs");
5249        }
5250    }
5251
5252    #[test]
5253    fn builtins_throw() {
5254        let result = eval(r#"builtins.throw "oops""#);
5255        assert!(result.is_err());
5256        let msg = format!("{}", result.unwrap_err());
5257        assert!(msg.contains("oops"));
5258    }
5259
5260    #[test]
5261    fn builtins_seq_deep_seq() {
5262        // seq forces first arg, returns second
5263        assert_eq!(ev("builtins.seq 1 42"), Value::Int(42));
5264        // deepSeq similarly
5265        assert_eq!(ev("builtins.deepSeq [1 2 3] 99"), Value::Int(99));
5266    }
5267
5268    #[test]
5269    fn builtins_current_system() {
5270        let v = ev("builtins.currentSystem");
5271        if let Value::String(ns) = v {
5272            let s = &ns.chars;
5273            // Should be a valid system string
5274            assert!(
5275                s == "aarch64-darwin"
5276                    || s == "x86_64-darwin"
5277                    || s == "aarch64-linux"
5278                    || s == "x86_64-linux",
5279                "unexpected system: {s}",
5280            );
5281        } else {
5282            panic!("expected string");
5283        }
5284    }
5285
5286    // ═══════════════════════════════════════════════════════════
5287    // 9. REAL-WORLD NIXPKGS PATTERNS
5288    // ═══════════════════════════════════════════════════════════
5289
5290    #[test]
5291    fn pattern_mkif_like() {
5292        // lib.mkIf pattern: if condition then { key = value; } else {}
5293        assert_eq!(
5294            ev("(if true then { x = 1; } else {}).x"),
5295            Value::Int(1),
5296        );
5297        let v = ev("if false then { x = 1; } else {}");
5298        if let Value::Attrs(attrs) = v {
5299            assert!(attrs.is_empty());
5300        } else {
5301            panic!("expected attrs");
5302        }
5303    }
5304
5305    #[test]
5306    fn pattern_optional_attrs() {
5307        // lib.optionalAttrs pattern
5308        assert_eq!(
5309            ev("let optionalAttrs = cond: attrs: if cond then attrs else {}; in (optionalAttrs true { a = 1; }).a"),
5310            Value::Int(1),
5311        );
5312        let v = ev("let optionalAttrs = cond: attrs: if cond then attrs else {}; in optionalAttrs false { a = 1; }");
5313        if let Value::Attrs(attrs) = v {
5314            assert!(attrs.is_empty());
5315        } else {
5316            panic!("expected attrs");
5317        }
5318    }
5319
5320    #[test]
5321    fn pattern_filter_attrs_via_remove() {
5322        // lib.filterAttrs pattern via removeAttrs
5323        assert_eq!(
5324            ev(r#"(builtins.removeAttrs { a = 1; b = 2; c = 3; } ["b"]).a"#),
5325            Value::Int(1),
5326        );
5327        assert_eq!(
5328            ev(r#"(builtins.removeAttrs { a = 1; b = 2; c = 3; } ["b"]) ? b"#),
5329            Value::Bool(false),
5330        );
5331    }
5332
5333    #[test]
5334    fn pattern_override() {
5335        // default // overrides pattern
5336        let v = ev(r#"
5337            let
5338                defaults = { debug = false; port = 8080; host = "localhost"; };
5339                overrides = { debug = true; port = 9090; };
5340            in defaults // overrides
5341        "#);
5342        if let Value::Attrs(attrs) = v {
5343            assert_eq!(attrs.get("debug"), Some(&Value::Bool(true)));
5344            assert_eq!(attrs.get("port"), Some(&Value::Int(9090)));
5345            assert_eq!(attrs.get("host"), Some(&Value::string("localhost")));
5346        } else {
5347            panic!("expected attrs");
5348        }
5349    }
5350
5351    #[test]
5352    fn pattern_functor() {
5353        // { __functor = self: x: self.value + x; value = 10; } 5
5354        assert_eq!(
5355            ev("let s = { __functor = self: x: self.value + x; value = 10; }; in s 5"),
5356            Value::Int(15),
5357        );
5358    }
5359
5360    #[test]
5361    fn pattern_platform_check() {
5362        // Check pattern: if builtins.currentSystem == "..." then ... else ...
5363        let v = ev(r#"if builtins.currentSystem == "aarch64-darwin" then "arm" else "other""#);
5364        // We just verify it evaluates without error and produces a string
5365        if let Value::String(_) = v {
5366            // ok
5367        } else {
5368            panic!("expected string");
5369        }
5370    }
5371
5372    #[test]
5373    fn pattern_recursive_overlay_lambda_structure() {
5374        // Test the lambda structure of an overlay (self: super: { ... })
5375        let v = ev("let overlay = self: super: { pkg = 42; }; in overlay {} {}");
5376        if let Value::Attrs(attrs) = v {
5377            assert_eq!(attrs.get("pkg"), Some(&Value::Int(42)));
5378        } else {
5379            panic!("expected attrs");
5380        }
5381    }
5382
5383    #[test]
5384    fn pattern_call_package_simplified() {
5385        // Simplified callPackage: f: f { inherit lib; }
5386        assert_eq!(
5387            ev("let callPkg = f: f { lib = { id = x: x; }; }; lib = { id = x: x; }; in callPkg ({ lib }: lib.id 42)"),
5388            Value::Int(42),
5389        );
5390    }
5391
5392    #[test]
5393    fn pattern_derivation_like_attrset() {
5394        let v = ev(r#"{ type = "derivation"; name = "hello"; system = builtins.currentSystem; builder = "/bin/sh"; }"#);
5395        if let Value::Attrs(attrs) = v {
5396            assert_eq!(attrs.get("type"), Some(&Value::string("derivation")));
5397            assert_eq!(attrs.get("name"), Some(&Value::string("hello")));
5398            assert_eq!(attrs.get("builder"), Some(&Value::string("/bin/sh")));
5399            // system should be a string (may be a thunk that forces to string)
5400            let system = force_value(attrs.get("system").unwrap()).unwrap();
5401            assert!(matches!(system, Value::String(_)), "expected string, got {system:?}");
5402        } else {
5403            panic!("expected attrs");
5404        }
5405    }
5406
5407    #[test]
5408    fn pattern_module_system_simplified() {
5409        // Simplified NixOS module evaluation
5410        assert_eq!(
5411            ev(r#"
5412                let
5413                    eval = m: m { config = {}; lib = { mkDefault = x: x; }; };
5414                in eval ({ config, lib }: { result = lib.mkDefault 42; })
5415            "#),
5416            {
5417                let mut attrs = NixAttrs::new();
5418                attrs.insert("result".to_string(), Value::Int(42));
5419                Value::Attrs(Rc::new(attrs))
5420            },
5421        );
5422    }
5423
5424    // ═══════════════════════════════════════════════════════════
5425    // 10. ERROR HANDLING
5426    // ═══════════════════════════════════════════════════════════
5427
5428    #[test]
5429    fn error_undefined_variable() {
5430        let result = eval("nonexistent_var");
5431        assert!(result.is_err());
5432        let msg = format!("{}", result.unwrap_err());
5433        assert!(msg.contains("undefined variable") || msg.contains("nonexistent_var"));
5434    }
5435
5436    #[test]
5437    fn error_type_mismatch_arithmetic() {
5438        let result = eval(r#"1 + "hello""#);
5439        assert!(result.is_err());
5440    }
5441
5442    #[test]
5443    fn error_missing_attribute() {
5444        let result = eval("{}.nonexistent");
5445        assert!(result.is_err());
5446        let msg = format!("{}", result.unwrap_err());
5447        assert!(msg.contains("nonexistent") || msg.contains("not found"));
5448    }
5449
5450    #[test]
5451    fn error_division_by_zero() {
5452        assert!(eval("1 / 0").is_err());
5453        assert!(eval("100 / 0").is_err());
5454    }
5455
5456    #[test]
5457    fn error_missing_required_function_arg() {
5458        let result = eval("({ a, b }: a + b) { a = 1; }");
5459        assert!(result.is_err());
5460        let msg = format!("{}", result.unwrap_err());
5461        assert!(msg.contains("missing argument"));
5462    }
5463
5464    #[test]
5465    fn error_unexpected_function_arg() {
5466        let result = eval("({ a }: a) { a = 1; b = 2; }");
5467        assert!(result.is_err());
5468        let msg = format!("{}", result.unwrap_err());
5469        assert!(msg.contains("unexpected argument"));
5470    }
5471
5472    #[test]
5473    fn error_assertion_failure() {
5474        assert!(eval("assert false; 1").is_err());
5475        assert!(eval("assert 1 == 2; 1").is_err());
5476    }
5477
5478    #[test]
5479    fn error_infinite_recursion() {
5480        // `let x = x; in x` should either hit the depth guard or fail on
5481        // undefined variable (since sequential let can't see its own binding).
5482        let result = eval("let x = x; in x");
5483        assert!(result.is_err());
5484    }
5485
5486    #[test]
5487    fn error_infinite_recursion_via_lambda() {
5488        // A true infinite recursion via self-application -- depth guard catches this.
5489        let result = eval("let f = x: f x; in f 1");
5490        assert!(result.is_err());
5491        let msg = format!("{}", result.unwrap_err());
5492        assert!(
5493            msg.contains("infinite recursion") || msg.contains("eval depth") || msg.contains("undefined"),
5494        );
5495    }
5496
5497    // ═══════════════════════════════════════════════════════════
5498    // ADDITIONAL COVERAGE: edge cases and integration
5499    // ═══════════════════════════════════════════════════════════
5500
5501    #[test]
5502    fn integration_let_with_function_returning_attrset() {
5503        assert_eq!(
5504            ev("let mkPkg = name: { inherit name; version = 1; }; in (mkPkg \"hello\").name"),
5505            Value::string("hello"),
5506        );
5507    }
5508
5509    #[test]
5510    fn integration_chained_updates() {
5511        assert_eq!(
5512            ev("({ a = 1; } // { b = 2; } // { c = 3; }).c"),
5513            Value::Int(3),
5514        );
5515    }
5516
5517    #[test]
5518    fn integration_map_over_attrnames() {
5519        // Common nixpkgs pattern: map over attrNames
5520        assert_eq!(
5521            ev(r#"
5522                let
5523                    set = { a = 1; b = 2; };
5524                    names = builtins.attrNames set;
5525                in builtins.length names
5526            "#),
5527            Value::Int(2),
5528        );
5529    }
5530
5531    #[test]
5532    fn integration_compose_functions() {
5533        // Function composition
5534        assert_eq!(
5535            ev("let compose = f: g: x: f (g x); double = x: x * 2; inc = x: x + 1; in compose double inc 5"),
5536            Value::Int(12), // (5 + 1) * 2
5537        );
5538    }
5539
5540    #[test]
5541    fn integration_recursive_list_building() {
5542        // Build a list using genList and map
5543        assert_eq!(
5544            ev("builtins.map (x: x * x) (builtins.genList (x: x + 1) 4)"),
5545            Value::list(vec![Value::Int(1), Value::Int(4), Value::Int(9), Value::Int(16)]),
5546        );
5547    }
5548
5549    #[test]
5550    fn integration_attrset_from_list() {
5551        // Convert list to attrset via listToAttrs + map
5552        let v = ev(r#"
5553            builtins.listToAttrs (builtins.map (x: { name = x; value = true; }) ["a" "b" "c"])
5554        "#);
5555        if let Value::Attrs(attrs) = v {
5556            assert_eq!(attrs.get("a"), Some(&Value::Bool(true)));
5557            assert_eq!(attrs.get("b"), Some(&Value::Bool(true)));
5558            assert_eq!(attrs.get("c"), Some(&Value::Bool(true)));
5559        } else {
5560            panic!("expected attrs");
5561        }
5562    }
5563
5564    #[test]
5565    fn integration_nested_with_and_let() {
5566        assert_eq!(
5567            ev("let x = 10; in with { y = 20; }; x + y"),
5568            Value::Int(30),
5569        );
5570    }
5571
5572    #[test]
5573    fn integration_complex_pattern_match() {
5574        // Complex function with defaults, ellipsis, and @ pattern
5575        assert_eq!(
5576            ev("(args @ { a, b ? 5, ... }: a + b + (if args ? c then args.c else 0)) { a = 1; c = 10; }"),
5577            Value::Int(16), // 1 + 5 + 10
5578        );
5579    }
5580
5581    #[test]
5582    fn integration_substring() {
5583        assert_eq!(
5584            ev(r#"builtins.substring 0 5 "hello world""#),
5585            Value::string("hello"),
5586        );
5587        assert_eq!(
5588            ev(r#"builtins.substring 6 5 "hello world""#),
5589            Value::string("world"),
5590        );
5591    }
5592
5593    #[test]
5594    fn integration_has_attr_on_nested() {
5595        // ? on nested attr paths
5596        assert_eq!(ev("{ a = { b = 1; }; } ? a"), Value::Bool(true));
5597        assert_eq!(
5598            ev("({ a = { b = 1; }; }.a) ? b"),
5599            Value::Bool(true),
5600        );
5601    }
5602
5603    #[test]
5604    fn integration_cat_attrs() {
5605        assert_eq!(
5606            ev(r#"builtins.catAttrs "x" [{ x = 1; } { y = 2; } { x = 3; }]"#),
5607            Value::list(vec![Value::Int(1), Value::Int(3)]),
5608        );
5609    }
5610
5611    #[test]
5612    fn integration_get_attr_builtin() {
5613        assert_eq!(
5614            ev(r#"builtins.getAttr "a" { a = 42; b = 10; }"#),
5615            Value::Int(42),
5616        );
5617    }
5618
5619    #[test]
5620    fn integration_has_attr_builtin() {
5621        assert_eq!(
5622            ev(r#"builtins.hasAttr "a" { a = 1; }"#),
5623            Value::Bool(true),
5624        );
5625        assert_eq!(
5626            ev(r#"builtins.hasAttr "z" { a = 1; }"#),
5627            Value::Bool(false),
5628        );
5629    }
5630
5631    #[test]
5632    fn integration_is_path() {
5633        assert_eq!(ev("builtins.isPath ./foo"), Value::Bool(true));
5634        assert_eq!(ev("builtins.isPath 42"), Value::Bool(false));
5635    }
5636
5637    #[test]
5638    fn integration_builtins_trace() {
5639        // trace prints the first arg (as debug) and returns the second
5640        assert_eq!(ev(r#"builtins.trace "debug msg" 42"#), Value::Int(42));
5641    }
5642
5643    #[test]
5644    fn integration_builtins_split() {
5645        // Nix spec: split returns alternating non-match strings and match group lists.
5646        // When the regex has no capture groups, separator positions get empty lists.
5647        // split "/" "a/b/c" => ["a" [] "b" [] "c"]
5648        assert_eq!(
5649            ev(r#"builtins.split "/" "a/b/c""#),
5650            Value::list(vec![
5651                Value::string("a"),
5652                Value::list(vec![]),
5653                Value::string("b"),
5654                Value::list(vec![]),
5655                Value::string("c"),
5656            ]),
5657        );
5658        // With a capture group, the captured text appears in the list.
5659        // split "(/)" "a/b/c" => ["a" ["/"] "b" ["/"] "c"]
5660        assert_eq!(
5661            ev(r#"builtins.split "(/)" "a/b/c""#),
5662            Value::list(vec![
5663                Value::string("a"),
5664                Value::list(vec![Value::string("/")]),
5665                Value::string("b"),
5666                Value::list(vec![Value::string("/")]),
5667                Value::string("c"),
5668            ]),
5669        );
5670    }
5671
5672    #[test]
5673    fn integration_builtins_split_no_capture_groups() {
5674        // builtins.split with no capture groups returns empty lists
5675        // at separator positions — matches CppNix behavior.
5676        // This is critical for nixpkgs lib.splitString which uses
5677        // builtins.filter builtins.isString on the result.
5678        assert_eq!(
5679            ev(r#"builtins.split "-" "aarch64-darwin""#),
5680            Value::list(vec![
5681                Value::string("aarch64"),
5682                Value::list(vec![]),
5683                Value::string("darwin"),
5684            ]),
5685        );
5686    }
5687
5688    #[test]
5689    fn integration_builtins_split_system_string_filter() {
5690        // Simulates nixpkgs lib.splitString: filter isString (split pattern string)
5691        // This is the exact pattern that parses system strings like "aarch64-darwin".
5692        assert_eq!(
5693            ev(r#"builtins.filter builtins.isString (builtins.split "-" "aarch64-darwin")"#),
5694            Value::list(vec![
5695                Value::string("aarch64"),
5696                Value::string("darwin"),
5697            ]),
5698        );
5699    }
5700
5701    #[test]
5702    fn integration_deeply_nested_let() {
5703        // Deeply nested let-in expressions
5704        assert_eq!(
5705            ev("let a = let b = let c = 10; in c * 2; in b + 1; in a"),
5706            Value::Int(21),
5707        );
5708    }
5709
5710    #[test]
5711    fn integration_if_in_attrset_value() {
5712        assert_eq!(
5713            ev("{ x = if true then 1 else 2; }.x"),
5714            Value::Int(1),
5715        );
5716    }
5717
5718    #[test]
5719    fn integration_lambda_in_list() {
5720        // Store lambdas in a list and apply them
5721        assert_eq!(
5722            ev("let fs = [(x: x + 1) (x: x * 2)]; in (builtins.elemAt fs 0) 5"),
5723            Value::Int(6),
5724        );
5725        assert_eq!(
5726            ev("let fs = [(x: x + 1) (x: x * 2)]; in (builtins.elemAt fs 1) 5"),
5727            Value::Int(10),
5728        );
5729    }
5730
5731    #[test]
5732    fn integration_nixpkgs_lib_id() {
5733        // lib.id = x: x
5734        assert_eq!(
5735            ev("let lib = { id = x: x; const = a: b: a; }; in lib.id 42"),
5736            Value::Int(42),
5737        );
5738        assert_eq!(
5739            ev("let lib = { id = x: x; const = a: b: a; }; in lib.const 1 2"),
5740            Value::Int(1),
5741        );
5742    }
5743
5744    #[test]
5745    fn integration_multiple_inherit() {
5746        assert_eq!(
5747            ev("let a = 1; b = 2; c = 3; in { inherit a b c; }.b"),
5748            Value::Int(2),
5749        );
5750    }
5751
5752    #[test]
5753    fn integration_rec_set_with_builtins() {
5754        assert_eq!(
5755            ev(r#"(rec { a = "hello"; b = builtins.stringLength a; }).b"#),
5756            Value::Int(5),
5757        );
5758    }
5759
5760    // ═══════════════════════════════════════════════════════════
5761    // 11. __FUNCTOR PROTOCOL
5762    // ═══════════════════════════════════════════════════════════
5763
5764    #[test]
5765    fn functor_simple_callable_attrset() {
5766        assert_eq!(
5767            ev("let s = { __functor = self: x: x + 1; }; in s 41"),
5768            Value::Int(42),
5769        );
5770    }
5771
5772    #[test]
5773    fn functor_with_self_reference() {
5774        assert_eq!(
5775            ev("let s = { __functor = self: x: self.base + x; base = 100; }; in s 23"),
5776            Value::Int(123),
5777        );
5778    }
5779
5780    #[test]
5781    fn functor_updated_attrset() {
5782        // Override a field in the attrset, functor still works
5783        assert_eq!(
5784            ev(r#"
5785                let
5786                    mk = { __functor = self: x: self.n + x; n = 0; };
5787                    s = mk // { n = 50; };
5788                in s 7
5789            "#),
5790            Value::Int(57),
5791        );
5792    }
5793
5794    #[test]
5795    fn functor_error_on_non_callable_attrset() {
5796        // Attrset without __functor should produce error when called
5797        let result = eval("let s = { a = 1; }; in s 5");
5798        assert!(result.is_err());
5799    }
5800
5801    // ═══════════════════════════════════════════════════════════
5802    // 12. __TOSTRING PROTOCOL
5803    // ═══════════════════════════════════════════════════════════
5804
5805    #[test]
5806    fn to_string_protocol_in_interpolation() {
5807        assert_eq!(
5808            ev(r#"let s = { __toString = self: "world"; }; in "hello ${s}""#),
5809            Value::string("hello world"),
5810        );
5811    }
5812
5813    #[test]
5814    fn to_string_protocol_accesses_self() {
5815        assert_eq!(
5816            ev(r#"let s = { __toString = self: self.val; val = "abc"; }; in "${s}""#),
5817            Value::string("abc"),
5818        );
5819    }
5820
5821    #[test]
5822    fn to_string_protocol_via_builtin_to_string() {
5823        assert_eq!(
5824            ev(r#"builtins.toString { __toString = self: "via-builtin"; }"#),
5825            Value::string("via-builtin"),
5826        );
5827    }
5828
5829    #[test]
5830    fn to_string_protocol_attrset_without_toString_fails() {
5831        // An attrset without __toString should fail in string context
5832        let result = eval(r#""${{}}"#);
5833        assert!(result.is_err());
5834    }
5835
5836    // ═══════════════════════════════════════════════════════════
5837    // 13. NEWLY IMPLEMENTED BUILTINS (eval-level tests)
5838    // ═══════════════════════════════════════════════════════════
5839
5840    #[test]
5841    fn eval_builtins_concat_strings() {
5842        assert_eq!(
5843            ev(r#"builtins.concatStrings ["a" "b" "c"]"#),
5844            Value::string("abc"),
5845        );
5846        assert_eq!(
5847            ev(r#"builtins.concatStrings []"#),
5848            Value::string(""),
5849        );
5850    }
5851
5852    #[test]
5853    fn eval_builtins_partition() {
5854        let v = ev("builtins.partition (x: x > 3) [1 2 3 4 5]");
5855        if let Value::Attrs(a) = v {
5856            assert_eq!(a.get("right"), Some(&Value::list(vec![Value::Int(4), Value::Int(5)])));
5857            assert_eq!(a.get("wrong"), Some(&Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)])));
5858        } else {
5859            panic!("expected attrs");
5860        }
5861    }
5862
5863    #[test]
5864    fn eval_builtins_group_by() {
5865        let v = ev(r#"builtins.groupBy (x: if x > 0 then "pos" else "neg") [1 (0 - 2) 3 (0 - 4)]"#);
5866        if let Value::Attrs(a) = v {
5867            assert_eq!(a.get("pos"), Some(&Value::list(vec![Value::Int(1), Value::Int(3)])));
5868            assert_eq!(a.get("neg"), Some(&Value::list(vec![Value::Int(-2), Value::Int(-4)])));
5869        } else {
5870            panic!("expected attrs");
5871        }
5872    }
5873
5874    #[test]
5875    fn eval_builtins_zip_attrs_with() {
5876        let v = ev("builtins.zipAttrsWith (n: vs: builtins.head vs) [{ a = 1; } { a = 2; b = 3; }]");
5877        if let Value::Attrs(a) = v {
5878            assert_eq!(a.get("a"), Some(&Value::Int(1)));
5879            assert_eq!(a.get("b"), Some(&Value::Int(3)));
5880        } else {
5881            panic!("expected attrs");
5882        }
5883    }
5884
5885    #[test]
5886    fn eval_builtins_compare_versions() {
5887        assert_eq!(ev(r#"builtins.compareVersions "2.0" "1.0""#), Value::Int(1));
5888        assert_eq!(ev(r#"builtins.compareVersions "1.0" "2.0""#), Value::Int(-1));
5889        assert_eq!(ev(r#"builtins.compareVersions "1.0" "1.0""#), Value::Int(0));
5890    }
5891
5892    #[test]
5893    fn eval_builtins_parse_drv_name() {
5894        let v = ev(r#"builtins.parseDrvName "nix-2.3.4""#);
5895        if let Value::Attrs(a) = v {
5896            assert_eq!(a.get("name"), Some(&Value::string("nix")));
5897            assert_eq!(a.get("version"), Some(&Value::string("2.3.4")));
5898        } else {
5899            panic!("expected attrs");
5900        }
5901    }
5902
5903    #[test]
5904    fn eval_builtins_base_name_of() {
5905        assert_eq!(
5906            ev(r#"builtins.baseNameOf "/foo/bar/baz""#),
5907            Value::string("baz"),
5908        );
5909    }
5910
5911    #[test]
5912    fn eval_builtins_dir_of() {
5913        assert_eq!(
5914            ev(r#"builtins.dirOf "/foo/bar/baz""#),
5915            Value::string("/foo/bar"),
5916        );
5917    }
5918
5919    #[test]
5920    fn eval_builtins_add_error_context() {
5921        assert_eq!(
5922            ev(r#"builtins.addErrorContext "some context" 42"#),
5923            Value::Int(42),
5924        );
5925    }
5926
5927    #[test]
5928    fn eval_builtins_abort() {
5929        let result = eval(r#"builtins.abort "fatal error""#);
5930        assert!(result.is_err());
5931        let msg = format!("{}", result.unwrap_err());
5932        assert!(msg.contains("fatal error"));
5933    }
5934
5935    // ═══════════════════════════════════════════════════════════
5936    // 14. INDENTED STRINGS ('' ... '')
5937    // ═══════════════════════════════════════════════════════════
5938
5939    #[test]
5940    fn indented_string_simple() {
5941        assert_eq!(ev("''hello''"), Value::string("hello"));
5942    }
5943
5944    #[test]
5945    fn indented_string_multiline_strips_indent() {
5946        assert_eq!(
5947            ev("''\n  line1\n  line2\n''"),
5948            Value::string("line1\nline2\n"),
5949        );
5950    }
5951
5952    #[test]
5953    fn indented_string_with_interpolation() {
5954        let code = "let x = \"world\"; in ''hello ${x}''";
5955        assert_eq!(
5956            ev(code),
5957            Value::string("hello world"),
5958        );
5959    }
5960
5961    #[test]
5962    fn indented_string_deeper_indent_preserved() {
5963        // Common indent is 2 spaces; the 4-space line keeps 2 extra
5964        assert_eq!(
5965            ev("''\n  a\n    b\n''"),
5966            Value::string("a\n  b\n"),
5967        );
5968    }
5969
5970    // ═══════════════════════════════════════════════════════════
5971    // 15. DYNAMIC ATTRIBUTE NAMES
5972    // ═══════════════════════════════════════════════════════════
5973
5974    #[test]
5975    fn dynamic_attr_name_in_set() {
5976        assert_eq!(
5977            ev(r#"let key = "mykey"; in { ${key} = 42; }.mykey"#),
5978            Value::Int(42),
5979        );
5980    }
5981
5982    #[test]
5983    fn dynamic_attr_name_with_expression() {
5984        assert_eq!(
5985            ev(r#"let prefix = "foo"; in { ${"${prefix}bar"} = 1; }.foobar"#),
5986            Value::Int(1),
5987        );
5988    }
5989
5990    // ═══════════════════════════════════════════════════════════
5991    // 16. IGNORED TESTS — features needing major infrastructure
5992    // ═══════════════════════════════════════════════════════════
5993
5994    #[test]
5995    fn eval_builtins_match() {
5996        assert_eq!(
5997            ev(r#"builtins.match "([0-9]+)" "42""#),
5998            Value::list(vec![Value::string("42")]),
5999        );
6000    }
6001
6002    #[test]
6003    fn eval_builtins_hash_string() {
6004        let v = ev(r#"builtins.hashString "sha256" "hello""#);
6005        if let Value::String(ns) = v {
6006            assert_eq!(ns.chars.len(), 64);
6007        } else {
6008            panic!("expected string");
6009        }
6010    }
6011
6012    #[test]
6013    fn eval_builtins_import() {
6014        let dir = std::env::temp_dir();
6015        let path = dir.join("sui_eval_test_import_eval.nix");
6016        std::fs::write(&path, "42").unwrap();
6017        let expr = format!(r#"import "{}""#, path.display());
6018        let v = eval(&expr).unwrap();
6019        assert_eq!(v, Value::Int(42));
6020        std::fs::remove_file(&path).ok();
6021    }
6022
6023    #[test]
6024    fn eval_builtins_derivation() {
6025        let v = eval(r#"builtins.derivation { name = "test"; system = "x86_64-linux"; builder = "/bin/sh"; }"#).unwrap();
6026        if let Value::Attrs(a) = v {
6027            assert_eq!(a.get("type"), Some(&Value::string("derivation")));
6028        } else {
6029            panic!("expected attrs");
6030        }
6031    }
6032
6033    #[test]
6034    fn eval_mutual_recursive_let() {
6035        // Multi-pass evaluation allows forward references in let bindings.
6036        // After 3 passes (placeholder + eval + re-eval), `a.x` resolves to
6037        // the value of `b` from the previous pass, and `a.x.y` is an attrset.
6038        // Full semantic equivalence with Nix (a.x.y == a) requires lazy
6039        // thunks, but the multi-pass approach is sufficient for common
6040        // patterns like mutual module references.
6041        let v = eval("let a = { x = b; }; b = { y = a; }; in a.x.y");
6042        assert!(v.is_ok(), "mutual recursive let should not error: {v:?}");
6043        // a.x.y should be an attrset (it's a's value from a prior pass)
6044        let val = v.unwrap();
6045        assert!(
6046            matches!(val, Value::Attrs(_)),
6047            "a.x.y should be an attrset, got: {val:?}",
6048        );
6049    }
6050
6051    #[test]
6052    fn eval_mutual_recursive_let_simple() {
6053        // Simpler case: forward reference in sequential let bindings
6054        let v = eval("let a = b; b = 42; in a");
6055        assert!(v.is_ok());
6056        // After multi-pass: pass 2 sets a=Null (b not yet bound), b=42
6057        // pass 3 sets a=42, b=42
6058        assert_eq!(v.unwrap(), Value::Int(42));
6059    }
6060
6061    #[test]
6062    fn eval_builtins_read_dir() {
6063        let dir = std::env::temp_dir().join("sui_eval_test_readdir_eval");
6064        let _ = std::fs::remove_dir_all(&dir);
6065        std::fs::create_dir_all(&dir).unwrap();
6066        std::fs::write(dir.join("a.txt"), "").unwrap();
6067        let expr = format!(r#"builtins.readDir "{}""#, dir.display());
6068        let v = eval(&expr).unwrap();
6069        if let Value::Attrs(a) = v {
6070            assert_eq!(a.get("a.txt"), Some(&Value::string("regular")));
6071        } else {
6072            panic!("expected attrs");
6073        }
6074        let _ = std::fs::remove_dir_all(&dir);
6075    }
6076
6077    // ═══════════════════════════════════════════════════════════
6078    // 17. THUNK / LAZY EVALUATION
6079    // ═══════════════════════════════════════════════════════════
6080
6081    #[test]
6082    fn thunk_basic_let() {
6083        // Simple let binding through thunk.
6084        assert_eq!(ev("let x = 1; in x"), Value::Int(1));
6085    }
6086
6087    #[test]
6088    fn thunk_forward_ref() {
6089        // Forward reference: `a` references `b` which is defined later.
6090        assert_eq!(ev("let a = b; b = 1; in a"), Value::Int(1));
6091    }
6092
6093    #[test]
6094    fn thunk_mutual_rec_attrset_in_let() {
6095        // Mutual recursion through attrsets in let bindings.
6096        assert_eq!(ev("let a = { x = b; }; b = { y = 1; }; in a.x.y"), Value::Int(1));
6097    }
6098
6099    #[test]
6100    fn thunk_rec_attrset() {
6101        // rec { a = b; b = 1; } -- forward ref within rec set.
6102        assert_eq!(ev("(rec { a = b; b = 1; }).a"), Value::Int(1));
6103    }
6104
6105    #[test]
6106    fn thunk_rec_attrset_chain() {
6107        // Longer chain: c depends on b depends on a.
6108        assert_eq!(ev("(rec { a = 1; b = a + 1; c = b + 1; }).c"), Value::Int(3));
6109    }
6110
6111    #[test]
6112    fn thunk_fixpoint() {
6113        // Classic fixpoint combinator -- the core of nixpkgs' `lib.fix`.
6114        assert_eq!(
6115            ev("let fix = f: let x = f x; in x; in (fix (self: { a = 1; b = self.a + 1; })).b"),
6116            Value::Int(2),
6117        );
6118    }
6119
6120    #[test]
6121    fn thunk_blackhole_self_reference() {
6122        // `let x = x; in x` is infinite recursion -- blackhole detection.
6123        let result = eval("let x = x; in x");
6124        assert!(result.is_err());
6125        let msg = format!("{}", result.unwrap_err());
6126        assert!(
6127            msg.contains("infinite recursion") || msg.contains("blackhole"),
6128            "expected blackhole error, got: {msg}",
6129        );
6130    }
6131
6132    #[test]
6133    fn thunk_mutual_blackhole() {
6134        // `let a = b; b = a; in a` -- mutual infinite recursion.
6135        let result = eval("let a = b; b = a; in a");
6136        assert!(result.is_err());
6137    }
6138
6139    #[test]
6140    fn thunk_let_body_forces_correctly() {
6141        // The let body should be able to use thunked bindings in arithmetic.
6142        assert_eq!(ev("let a = 10; b = 20; in a + b"), Value::Int(30));
6143    }
6144
6145    #[test]
6146    fn thunk_only_forced_when_needed() {
6147        // The binding `bad` would error if forced, but it is never used.
6148        assert_eq!(ev("let bad = 1 / 0; good = 42; in good"), Value::Int(42));
6149    }
6150
6151    #[test]
6152    fn thunk_forward_ref_in_function_body() {
6153        // Forward reference used inside a function body.
6154        assert_eq!(
6155            ev("let f = x: x + b; b = 10; in f 5"),
6156            Value::Int(15),
6157        );
6158    }
6159
6160    #[test]
6161    fn thunk_rec_set_self_ref_through_self() {
6162        // rec set where `b` references `a` which is in the same set.
6163        assert_eq!(
6164            ev(r#"(rec { a = "hello"; b = builtins.stringLength a; }).b"#),
6165            Value::Int(5),
6166        );
6167    }
6168
6169    #[test]
6170    fn thunk_nested_let_forward_ref() {
6171        // Forward reference in nested let.
6172        assert_eq!(
6173            ev("let a = b + 1; b = 2; in a"),
6174            Value::Int(3),
6175        );
6176    }
6177
6178    #[test]
6179    fn thunk_deep_chain() {
6180        // Chain of forward references: e -> d -> c -> b -> a.
6181        assert_eq!(
6182            ev("let a = 1; b = a; c = b; d = c; e = d; in e"),
6183            Value::Int(1),
6184        );
6185    }
6186
6187    #[test]
6188    fn thunk_rec_set_fixpoint() {
6189        // Fixpoint through rec set -- common nixpkgs pattern.
6190        assert_eq!(
6191            ev("let fix = f: let x = f x; in x; in (fix (self: { a = 1; b = self.a + 1; c = self.b + 1; })).c"),
6192            Value::Int(3),
6193        );
6194    }
6195
6196    #[test]
6197    fn thunk_let_with_inherit() {
6198        // Inherit in let should work alongside thunked bindings.
6199        assert_eq!(
6200            ev("let a = 1; in let inherit a; b = a + 1; in b"),
6201            Value::Int(2),
6202        );
6203    }
6204
6205    #[test]
6206    fn thunk_attrset_value_lazy() {
6207        // Values in non-rec attrsets are evaluated eagerly, but the test
6208        // verifies that thunked let bindings inside attrset values work.
6209        assert_eq!(
6210            ev("let x = 42; in { a = x; }.a"),
6211            Value::Int(42),
6212        );
6213    }
6214
6215    #[test]
6216    fn thunk_unused_error_not_forced() {
6217        // Multiple bindings, only `ok` is used. `bad` throws but is never forced.
6218        assert_eq!(
6219            ev(r#"let bad = builtins.throw "boom"; ok = 1; in ok"#),
6220            Value::Int(1),
6221        );
6222    }
6223
6224    #[test]
6225    fn thunk_rec_set_mutual_reference() {
6226        // Mutual reference within rec set.
6227        let v = ev("rec { a = { val = b.val + 1; }; b = { val = 10; }; }");
6228        if let Value::Attrs(attrs) = v {
6229            let a = attrs.get("a").unwrap();
6230            let a_forced = force_value(a).unwrap();
6231            if let Value::Attrs(a_attrs) = a_forced {
6232                assert_eq!(a_attrs.get("val"), Some(&Value::Int(11)));
6233            } else {
6234                panic!("expected attrs for a");
6235            }
6236        } else {
6237            panic!("expected attrs");
6238        }
6239    }
6240
6241    // ── let-rec self-reference corner cases ───────────────
6242
6243    #[test]
6244    fn let_rec_self_reference_simple() {
6245        assert_eq!(
6246            ev("let x = 1; y = x + 1; in y"),
6247            Value::Int(2),
6248        );
6249    }
6250
6251    #[test]
6252    fn let_rec_self_reference_chain() {
6253        assert_eq!(
6254            ev("let a = 1; b = a + 1; c = b + 1; in c"),
6255            Value::Int(3),
6256        );
6257    }
6258
6259    #[test]
6260    fn let_rec_self_reference_with_function() {
6261        assert_eq!(
6262            ev("let f = x: x + 1; y = f 10; in y"),
6263            Value::Int(11),
6264        );
6265    }
6266
6267    #[test]
6268    fn let_rec_mutual_recursion_via_if() {
6269        assert_eq!(
6270            ev("let isEven = n: if n == 0 then true else isOdd (n - 1); isOdd = n: if n == 0 then false else isEven (n - 1); in isEven 4"),
6271            Value::Bool(true),
6272        );
6273    }
6274
6275    #[test]
6276    fn let_rec_forward_ref_in_list() {
6277        assert_eq!(
6278            ev("let xs = [a b]; a = 1; b = 2; in builtins.length xs"),
6279            Value::Int(2),
6280        );
6281    }
6282
6283    // ── with-shadowing corner cases ───────────────────────
6284
6285    #[test]
6286    fn with_shadowing_let_wins_over_with() {
6287        assert_eq!(
6288            ev("let x = 1; in with { x = 2; }; x"),
6289            Value::Int(1),
6290        );
6291    }
6292
6293    #[test]
6294    fn with_shadowing_inner_with_wins() {
6295        assert_eq!(
6296            ev("with { x = 1; }; with { x = 2; }; x"),
6297            Value::Int(2),
6298        );
6299    }
6300
6301    #[test]
6302    fn with_shadowing_outer_provides_missing() {
6303        assert_eq!(
6304            ev("with { x = 1; y = 10; }; with { x = 2; }; x + y"),
6305            Value::Int(12),
6306        );
6307    }
6308
6309    #[test]
6310    fn with_shadowing_lambda_arg_wins() {
6311        assert_eq!(
6312            ev("(x: with { x = 99; }; x) 42"),
6313            Value::Int(42),
6314        );
6315    }
6316
6317    #[test]
6318    fn with_shadowing_nested_let_wins_over_with() {
6319        assert_eq!(
6320            ev("with { x = 1; }; let x = 2; in x"),
6321            Value::Int(2),
6322        );
6323    }
6324
6325    #[test]
6326    fn with_scope_dynamic_attrs() {
6327        assert_eq!(
6328            ev(r#"with { x = 1; y = 2; z = 3; }; x + y + z"#),
6329            Value::Int(6),
6330        );
6331    }
6332
6333    #[test]
6334    fn with_scope_over_lazy_thunk_chain_resolves() {
6335        // A `with`-head that resolves through a NESTED thunk chain
6336        // (`Thunk(Thunk(Attrs))`) must still be searched: the lookup
6337        // has to FULLY force the head (chase the chain), not take a
6338        // single force step. A single step leaves a `Value::Thunk`
6339        // that `type_name()` reports as "set" but the `Value::Attrs`
6340        // match rejects — the scope is skipped and a bare ident
6341        // through it fails with a spurious UndefinedVar. This corners
6342        // the nixpkgs `platforms = with lib.platforms; unix;` shape.
6343        assert_eq!(
6344            ev(r#"let outer = if true then (if true then { unix = 42; } else {}) else {};
6345                      # force a two-deep lazy wrap of the with-head
6346                      head = (x: x) ((y: y) outer);
6347                  in with head; unix"#),
6348            Value::Int(42),
6349        );
6350    }
6351
6352    #[test]
6353    fn with_scope_head_from_deep_select_resolves() {
6354        // `with a.b.c; key` where a.b.c is a lazily-selected attrset —
6355        // the bare-ident body must find `key` through the forced head.
6356        assert_eq!(
6357            ev(r#"let a = { b = { c = { key = 7; }; }; }; in with a.b.c; key"#),
6358            Value::Int(7),
6359        );
6360    }
6361
6362    // ── attrset deep merge ────────────────────────────────
6363
6364    #[test]
6365    fn attrset_deep_merge_simple() {
6366        let v = ev("{ a.b = 1; a.c = 2; }");
6367        if let Value::Attrs(attrs) = v {
6368            let a = force_value(attrs.get("a").unwrap()).unwrap();
6369            if let Value::Attrs(inner) = a {
6370                assert_eq!(force_value(inner.get("b").unwrap()).unwrap(), Value::Int(1));
6371                assert_eq!(force_value(inner.get("c").unwrap()).unwrap(), Value::Int(2));
6372            } else {
6373                panic!("expected nested attrs");
6374            }
6375        } else {
6376            panic!("expected attrs");
6377        }
6378    }
6379
6380    #[test]
6381    fn attrset_deep_merge_three_levels() {
6382        let v = ev("{ a.b.c = 1; a.b.d = 2; a.e = 3; }");
6383        if let Value::Attrs(attrs) = v {
6384            let a = force_value(attrs.get("a").unwrap()).unwrap();
6385            if let Value::Attrs(a_inner) = a {
6386                let e = force_value(a_inner.get("e").unwrap()).unwrap();
6387                assert_eq!(e, Value::Int(3));
6388                let b = force_value(a_inner.get("b").unwrap()).unwrap();
6389                if let Value::Attrs(b_inner) = b {
6390                    assert_eq!(force_value(b_inner.get("c").unwrap()).unwrap(), Value::Int(1));
6391                    assert_eq!(force_value(b_inner.get("d").unwrap()).unwrap(), Value::Int(2));
6392                } else {
6393                    panic!("expected nested attrs for b");
6394                }
6395            } else {
6396                panic!("expected nested attrs for a");
6397            }
6398        } else {
6399            panic!("expected attrs");
6400        }
6401    }
6402
6403    #[test]
6404    fn attrset_deep_merge_preserves_siblings() {
6405        assert_eq!(
6406            ev("{ a.x = 1; b = 2; a.y = 3; }.b"),
6407            Value::Int(2),
6408        );
6409    }
6410
6411    #[test]
6412    fn attrset_deep_merge_in_let() {
6413        let v = ev("let s = { a.b = 1; a.c = 2; }; in s.a.b + s.a.c");
6414        assert_eq!(v, Value::Int(3));
6415    }
6416
6417    #[test]
6418    fn attrset_deep_merge_fullset_then_dotted() {
6419        // General root (gst-plugins-base `passthru.waylandEnabled` drop):
6420        // `a = { x = 1; }; a.y = 2;` — the full-set binding is a lazy
6421        // Thunk (attrset literals go through maybe_thunk), so a naive
6422        // merge_nested_insert (which only merges concrete Value::Attrs)
6423        // overwrote `a` with `{ y = 2 }`, silently dropping `x`. The
6424        // collision must force the existing thunk to WHNF first.
6425        let v = ev("let s = { a = { x = 1; }; a.y = 2; }; in s.a.x + s.a.y");
6426        assert_eq!(v, Value::Int(3));
6427        // both keys must survive (not just their sum)
6428        let both = ev("let s = { a = { x = 1; }; a.y = 2; }; in [ s.a.x s.a.y ]");
6429        if let Value::List(items) = both {
6430            assert_eq!(force_value(&items[0]).unwrap(), Value::Int(1));
6431            assert_eq!(force_value(&items[1]).unwrap(), Value::Int(2));
6432        } else {
6433            panic!("expected list");
6434        }
6435    }
6436
6437    // ── inherit-from patterns ─────────────────────────────
6438
6439    #[test]
6440    fn inherit_from_basic() {
6441        assert_eq!(
6442            ev("let s = { x = 1; y = 2; }; in let inherit (s) x y; in x + y"),
6443            Value::Int(3),
6444        );
6445    }
6446
6447    #[test]
6448    fn inherit_from_with_shadowing() {
6449        assert_eq!(
6450            ev("let x = 10; in let inherit ({ x = 20; }) x; in x"),
6451            Value::Int(20),
6452        );
6453    }
6454
6455    #[test]
6456    fn inherit_from_in_attrset() {
6457        let v = ev(r#"let s = { a = 1; b = 2; }; in { inherit (s) a b; c = 3; }"#);
6458        if let Value::Attrs(attrs) = v {
6459            assert_eq!(force_value(attrs.get("a").unwrap()).unwrap(), Value::Int(1));
6460            assert_eq!(force_value(attrs.get("b").unwrap()).unwrap(), Value::Int(2));
6461            assert_eq!(force_value(attrs.get("c").unwrap()).unwrap(), Value::Int(3));
6462        } else {
6463            panic!("expected attrs");
6464        }
6465    }
6466
6467    #[test]
6468    fn inherit_from_rec_set() {
6469        assert_eq!(
6470            ev("rec { inherit ({ x = 42; }) x; y = x; }.y"),
6471            Value::Int(42),
6472        );
6473    }
6474
6475    #[test]
6476    fn inherit_plain_from_scope() {
6477        assert_eq!(
6478            ev("let x = 1; in { inherit x; }.x"),
6479            Value::Int(1),
6480        );
6481    }
6482
6483    // Regression (2026-07-11): a bare `inherit x;` must resolve LAZILY, like
6484    // a plain reference to `x` — not eagerly at attrset construction. When
6485    // `x` is provided only by an enclosing `with` scope whose value is a
6486    // fixpoint still being constructed, eager resolution spuriously threw
6487    // `UndefinedVar`. nixpkgs `all-packages.nix` is
6488    // `with pkgs; { nettle = import … { inherit callPackage; }; }`, so
6489    // `inherit callPackage` must resolve from the `with pkgs` scope at force
6490    // time. (This was the nettle UndefinedVar('callPackage') drop.)
6491    #[test]
6492    fn inherit_plain_from_with_scope_lazy() {
6493        // `inherit cp` reads `cp` from a `with self` fixpoint scope; the
6494        // attr forcing it (`a`) must resolve `cp` lazily against the settled
6495        // scope, not eagerly during attrset construction.
6496        assert_eq!(
6497            ev("let fix = f: let x = f x; in x;
6498                    self = fix (self: with self; {
6499                      a = use { inherit cp; };
6500                      use = { cp }: cp 5;
6501                      cp = x: x + 100;
6502                    });
6503                in self.a"),
6504            Value::Int(105),
6505        );
6506        // Simpler: bare inherit from a plain (non-blackhole) with scope.
6507        assert_eq!(
6508            ev("with { y = 7; }; { inherit y; }.y"),
6509            Value::Int(7),
6510        );
6511    }
6512
6513    #[test]
6514    fn inherit_multiple_from_expr() {
6515        assert_eq!(
6516            ev("let s = { a = 10; b = 20; c = 30; }; in let inherit (s) a b c; in a + b + c"),
6517            Value::Int(60),
6518        );
6519    }
6520
6521    // ── string interpolation edge cases ───────────────────
6522
6523    #[test]
6524    fn interp_nested_attrset_access() {
6525        assert_eq!(
6526            ev(r#"let x = { a = "hello"; }; in "${x.a} world""#),
6527            Value::string("hello world"),
6528        );
6529    }
6530
6531    #[test]
6532    fn interp_with_let_expression() {
6533        assert_eq!(
6534            ev(r#""${let x = "inner"; in x}""#),
6535            Value::string("inner"),
6536        );
6537    }
6538
6539    #[test]
6540    fn interp_float_coercion() {
6541        // CppNix %f-format: always 6 decimal places.
6542        assert_eq!(
6543            ev(r#""${toString 3.14}""#),
6544            Value::string("3.140000"),
6545        );
6546    }
6547
6548    // ── comparison edge cases ─────────────────────────────
6549
6550    #[test]
6551    fn compare_mixed_int_float() {
6552        assert_eq!(ev("1 < 1.5"), Value::Bool(true));
6553        assert_eq!(ev("1.5 > 1"), Value::Bool(true));
6554        assert_eq!(ev("2.0 == 2"), Value::Bool(true));
6555    }
6556
6557    #[test]
6558    fn compare_string_lexicographic() {
6559        assert_eq!(ev(r#""abc" < "abd""#), Value::Bool(true));
6560        assert_eq!(ev(r#""abc" < "abc""#), Value::Bool(false));
6561        assert_eq!(ev(r#""abc" <= "abc""#), Value::Bool(true));
6562    }
6563
6564    // ── update operator edge cases ────────────────────────
6565
6566    #[test]
6567    fn update_empty_sets() {
6568        let v = ev("{} // {}");
6569        if let Value::Attrs(a) = v { assert!(a.is_empty()); } else { panic!(); }
6570    }
6571
6572    #[test]
6573    fn update_right_overrides_completely() {
6574        assert_eq!(
6575            ev("{ a = 1; b = 2; } // { a = 10; c = 30; }"),
6576            ev("{ a = 10; b = 2; c = 30; }"),
6577        );
6578    }
6579
6580    #[test]
6581    fn update_chained() {
6582        assert_eq!(
6583            ev("{ a = 1; } // { b = 2; } // { c = 3; }"),
6584            ev("{ a = 1; b = 2; c = 3; }"),
6585        );
6586    }
6587
6588    // ── force_value edge cases ────────────────────────────
6589
6590    #[test]
6591    fn force_value_concrete_unchanged() {
6592        let v = Value::Int(42);
6593        assert_eq!(force_value(&v).unwrap(), Value::Int(42));
6594    }
6595
6596    #[test]
6597    fn force_value_null() {
6598        assert_eq!(force_value(&Value::Null).unwrap(), Value::Null);
6599    }
6600
6601    // ── eval_with_file ────────────────────────────────────
6602
6603    #[test]
6604    fn eval_with_file_none() {
6605        let result = eval_with_file("1 + 2", None).unwrap();
6606        assert_eq!(result, Value::Int(3));
6607    }
6608
6609    // ── error messages ────────────────────────────────────
6610
6611    #[test]
6612    fn error_type_mismatch_in_comparison() {
6613        let result = eval(r#"1 < "a""#);
6614        assert!(result.is_err());
6615    }
6616
6617    #[test]
6618    fn error_select_from_non_set() {
6619        let result = eval("42.x");
6620        assert!(result.is_err());
6621    }
6622
6623    #[test]
6624    fn error_call_non_function() {
6625        let result = eval("42 1");
6626        assert!(result.is_err());
6627    }
6628
6629    #[test]
6630    fn error_negate_string() {
6631        let result = eval(r#"-"hello""#);
6632        assert!(result.is_err());
6633    }
6634
6635    // ── multiline string edge cases ───────────────────────
6636
6637    #[test]
6638    fn multiline_string_empty() {
6639        assert_eq!(ev("''''"), Value::string(""));
6640    }
6641
6642    #[test]
6643    fn multiline_string_with_trailing_newline() {
6644        let v = ev("''\n  hello\n''");
6645        assert_eq!(v, Value::string("hello\n"));
6646    }
6647
6648    // ── list operations ───────────────────────────────────
6649
6650    #[test]
6651    fn list_concat_empty_left() {
6652        assert_eq!(ev("[] ++ [1 2]"), Value::list(vec![Value::Int(1), Value::Int(2)]));
6653    }
6654
6655    #[test]
6656    fn list_concat_empty_right() {
6657        assert_eq!(ev("[1 2] ++ []"), Value::list(vec![Value::Int(1), Value::Int(2)]));
6658    }
6659
6660    #[test]
6661    fn list_concat_both_empty() {
6662        assert_eq!(ev("[] ++ []"), Value::list(vec![]));
6663    }
6664
6665    // ── pattern matching / formals edge cases ─────────────
6666
6667    #[test]
6668    fn formals_at_pattern_accessible() {
6669        assert_eq!(
6670            ev("({ x, ... } @ args: builtins.length (builtins.attrNames args)) { x = 1; y = 2; z = 3; }"),
6671            Value::Int(3),
6672        );
6673    }
6674
6675    #[test]
6676    fn formals_default_uses_other_arg() {
6677        assert_eq!(
6678            ev("({ x, y ? x + 1 }: y) { x = 10; }"),
6679            Value::Int(11),
6680        );
6681    }
6682
6683    #[test]
6684    fn formals_default_lazy_assert_false() {
6685        // nixpkgs parse.nix pattern: default is `assert false; null` but
6686        // the body checks `args ? vendor` instead of using `vendor`
6687        // directly, so the default must never be forced.
6688        assert_eq!(
6689            ev("({ cpu, vendor ? assert false; null, kernel } @ args: if args ? vendor then vendor else \"inferred\") { cpu = \"x86_64\"; kernel = \"linux\"; }"),
6690            Value::String(Rc::new(NixString::plain("inferred"))),
6691        );
6692    }
6693
6694    #[test]
6695    fn formals_default_lazy_only_forced_when_accessed() {
6696        // When the default IS accessed, it should still evaluate correctly.
6697        assert_eq!(
6698            ev("({ a, b ? 42 }: b) { a = 1; }"),
6699            Value::Int(42),
6700        );
6701    }
6702
6703    #[test]
6704    fn formals_ellipsis_ignores_extra() {
6705        assert_eq!(
6706            ev("({ x, ... }: x) { x = 1; y = 2; z = 3; }"),
6707            Value::Int(1),
6708        );
6709    }
6710
6711    // ── pure mode ─────────────────────────────────────────
6712
6713    #[test]
6714    fn pure_mode_roundtrip() {
6715        let was_pure = is_pure_mode();
6716        set_pure_mode(true);
6717        assert!(is_pure_mode());
6718        set_pure_mode(false);
6719        assert!(!is_pure_mode());
6720        set_pure_mode(was_pure);
6721    }
6722
6723    // ── path operations ───────────────────────────────────
6724
6725    #[test]
6726    fn path_concat_with_string() {
6727        assert_eq!(
6728            ev(r#"/foo + "bar""#),
6729            Value::Path(Box::new(SmolStr::from("/foobar"))),
6730        );
6731    }
6732
6733    #[test]
6734    fn path_concat_with_path() {
6735        assert_eq!(
6736            ev("/foo + /bar"),
6737            Value::Path(Box::new(SmolStr::from("/foo//bar"))),
6738        );
6739    }
6740
6741    // ── EvalFileGuard / current_eval_dir ───────────────────
6742
6743    #[test]
6744    fn current_eval_dir_empty_when_no_file_pushed() {
6745        // Without a push, current_eval_dir should yield None.
6746        // (Note: this test is order-dependent; we accept whatever the
6747        // top of the stack happens to be when called.)
6748        let snapshot = current_eval_dir();
6749        // At minimum the API doesn't panic and returns Option.
6750        let _ = snapshot;
6751    }
6752
6753    #[test]
6754    fn push_eval_file_sets_current_dir() {
6755        let p = std::path::PathBuf::from("/tmp/example/file.nix");
6756        {
6757            let _g = push_eval_file(p.clone());
6758            assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/tmp/example")));
6759        }
6760        // Guard dropped, stack popped — current dir is whatever was below.
6761        // We can't assert exact value without snapshotting first, but the
6762        // value before push should be restored.
6763    }
6764
6765    #[test]
6766    fn push_eval_file_nested_stack() {
6767        let outer = std::path::PathBuf::from("/a/x.nix");
6768        let inner = std::path::PathBuf::from("/b/y.nix");
6769        {
6770            let _g_outer = push_eval_file(outer.clone());
6771            assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/a")));
6772            {
6773                let _g_inner = push_eval_file(inner.clone());
6774                assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/b")));
6775            }
6776            // Inner dropped — outer is back on top.
6777            assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/a")));
6778        }
6779    }
6780
6781    // ── Source-mapped error context ────────────────────────
6782
6783    #[test]
6784    fn error_undefined_var_includes_file_context() {
6785        let p = std::path::PathBuf::from("/nix/store/abc-default.nix");
6786        let _g = push_eval_file(p);
6787        let result = eval("nonexistent_xyz");
6788        let msg = format!("{}", result.unwrap_err());
6789        assert!(msg.contains("undefined variable"), "msg: {msg}");
6790        assert!(msg.contains("nonexistent_xyz"), "msg: {msg}");
6791        assert!(msg.contains("abc-default.nix"), "msg: {msg}");
6792    }
6793
6794    #[test]
6795    fn error_attr_not_found_includes_file_context() {
6796        let p = std::path::PathBuf::from("/nix/store/xyz-module.nix");
6797        let _g = push_eval_file(p);
6798        let result = eval("{}.missing_key");
6799        let msg = format!("{}", result.unwrap_err());
6800        assert!(msg.contains("not found") || msg.contains("missing_key"), "msg: {msg}");
6801        assert!(msg.contains("xyz-module.nix"), "msg: {msg}");
6802    }
6803
6804    #[test]
6805    fn error_assertion_failed_includes_file_context() {
6806        let p = std::path::PathBuf::from("/nix/store/test-assert.nix");
6807        let _g = push_eval_file(p);
6808        let result = eval("assert false; 1");
6809        let msg = format!("{}", result.unwrap_err());
6810        assert!(msg.contains("assertion failed"), "msg: {msg}");
6811        assert!(msg.contains("test-assert.nix"), "msg: {msg}");
6812    }
6813
6814    #[test]
6815    fn error_missing_argument_includes_file_context() {
6816        let p = std::path::PathBuf::from("/nix/store/func.nix");
6817        let _g = push_eval_file(p);
6818        let result = eval("({ a, b }: a) { a = 1; }");
6819        let msg = format!("{}", result.unwrap_err());
6820        assert!(msg.contains("missing argument"), "msg: {msg}");
6821        assert!(msg.contains("func.nix"), "msg: {msg}");
6822    }
6823
6824    #[test]
6825    fn error_cannot_call_includes_file_context() {
6826        let p = std::path::PathBuf::from("/nix/store/call.nix");
6827        let _g = push_eval_file(p);
6828        let result = eval("42 99");
6829        let msg = format!("{}", result.unwrap_err());
6830        assert!(msg.contains("cannot call"), "msg: {msg}");
6831        assert!(msg.contains("call.nix"), "msg: {msg}");
6832    }
6833
6834    #[test]
6835    fn error_without_file_has_no_in_prefix() {
6836        // When no file is on the eval stack, error messages should
6837        // not contain ", in" context.
6838        let result = eval("nonexistent_xyz");
6839        let msg = format!("{}", result.unwrap_err());
6840        assert!(msg.contains("undefined variable"), "msg: {msg}");
6841        assert!(!msg.contains(", in"), "msg should not contain file context: {msg}");
6842    }
6843
6844    // ── pure mode getter/setter independence ───────────────
6845
6846    #[test]
6847    fn pure_mode_set_get_independence() {
6848        let was = is_pure_mode();
6849        set_pure_mode(true);
6850        assert!(is_pure_mode());
6851        set_pure_mode(false);
6852        assert!(!is_pure_mode());
6853        set_pure_mode(was);
6854    }
6855
6856    // ── eval_with_file with file path ──────────────────────
6857
6858    #[test]
6859    fn eval_with_file_some_path_arithmetic() {
6860        let p = std::path::PathBuf::from("/tmp/imaginary.nix");
6861        let result = eval_with_file("1 + 2", Some(p)).unwrap();
6862        assert_eq!(result, Value::Int(3));
6863    }
6864
6865    // ── unsafeGetAttrPos — the options.json `attrTag` declarations root ──
6866    //
6867    // Seals the CppNix-matching behavior: for a literal attrset built in a
6868    // FILE, `builtins.unsafeGetAttrPos <key> <set>` returns
6869    // `{ file; line=1; column=<key byte offset>+1; }`; for a `<string>` eval
6870    // (no file) it returns `null`. Byte-verified against `nix eval`.
6871
6872    #[test]
6873    fn unsafe_get_attr_pos_reports_file_and_offset_column() {
6874        // The real `attrTag` path: a literal attrset built in an IMPORTED file.
6875        // `import` registers the file's source text + pushes it on the eval
6876        // stack, so `eval_attrset` captures the key positions against that file
6877        // and `unsafeGetAttrPos` resolves them. CppNix reports the file, line 1,
6878        // and column = the KEY's 1-based byte offset (verified against `nix eval`).
6879        let dir = tempfile::tempdir().unwrap();
6880        // The literal's `b` key sits at a known byte offset in this file.
6881        let file_body = "{ a = 1;\n  b = 2; }\n";
6882        let f = dir.path().join("lit.nix");
6883        std::fs::write(&f, file_body).unwrap();
6884        let src = format!("builtins.unsafeGetAttrPos \"b\" (import {})", f.display());
6885        let v = eval(&src).unwrap();
6886        let attrs = match v { Value::Attrs(a) => a, other => panic!("expected attrs, got {other:?}") };
6887        assert_eq!(
6888            attrs.get("file").unwrap().as_string().unwrap(),
6889            f.to_string_lossy(),
6890        );
6891        assert_eq!(*attrs.get("line").unwrap(), Value::Int(1));
6892        // Column = the `b` KEY token's 1-based byte offset in file_body.
6893        let expected_col = (file_body.find("b = 2").unwrap() as i64) + 1;
6894        let col = match attrs.get("column").unwrap() { Value::Int(n) => *n, o => panic!("{o:?}") };
6895        assert_eq!(col, expected_col, "column must be the 1-based byte offset");
6896    }
6897
6898    #[test]
6899    fn unsafe_get_attr_pos_null_for_string_origin() {
6900        // A `<string>`-eval'd literal (no file on the stack) has no position → null.
6901        let v = eval("builtins.unsafeGetAttrPos \"a\" { a = 1; }").unwrap();
6902        assert_eq!(v, Value::Null);
6903    }
6904
6905    #[test]
6906    fn unsafe_get_attr_pos_null_for_missing_key() {
6907        // A key absent from an imported set → null.
6908        let dir = tempfile::tempdir().unwrap();
6909        let f = dir.path().join("lit.nix");
6910        std::fs::write(&f, "{ a = 1; }\n").unwrap();
6911        let src = format!("builtins.unsafeGetAttrPos \"zzz\" (import {})", f.display());
6912        let v = eval(&src).unwrap();
6913        assert_eq!(v, Value::Null);
6914    }
6915
6916    // ── String interpolation primitive coercions ───────────
6917
6918    #[test]
6919    fn interp_int_into_string() {
6920        // Integer interpolated into a string is coerced to its decimal repr.
6921        assert_eq!(ev(r#""val=${toString 42}""#), Value::string("val=42"));
6922    }
6923
6924    #[test]
6925    fn interp_bool_true_becomes_one() {
6926        // Per eval_str: Bool(true) → "1", Bool(false) → "" (empty)
6927        let v = ev(r#"let x = true; in "${builtins.toString x}""#);
6928        assert_eq!(v, Value::string("1"));
6929    }
6930
6931    #[test]
6932    fn interp_null_becomes_empty() {
6933        // Null in interpolation is empty.
6934        let v = ev(r#"let x = null; in "${builtins.toString x}""#);
6935        assert_eq!(v, Value::string(""));
6936    }
6937
6938    #[test]
6939    fn interp_attrset_without_to_string_errors() {
6940        // An attrset interpolated without __toString is a type error.
6941        let result = eval(r#"let s = { x = 1; }; in "${s}""#);
6942        assert!(result.is_err());
6943    }
6944
6945    #[test]
6946    fn interp_attrset_with_to_string_protocol() {
6947        // __toString protocol returns a string when called with self.
6948        let v = ev(r#""${{ __toString = self: "ok"; }}""#);
6949        assert_eq!(v, Value::string("ok"));
6950    }
6951
6952    // ── Path PathRel / PathHome / PathAbs ─────────────────
6953
6954    #[test]
6955    fn eval_path_absolute_literal() {
6956        let v = ev("/tmp/foo");
6957        match v {
6958            Value::Path(p) => assert!(p.contains("/tmp/foo")),
6959            _ => panic!("expected Path"),
6960        }
6961    }
6962
6963    #[test]
6964    fn eval_path_home_literal() {
6965        let v = ev("~/foo.nix");
6966        match v {
6967            Value::Path(p) => assert!(p.contains("~/foo.nix") || p.ends_with("foo.nix")),
6968            _ => panic!("expected Path"),
6969        }
6970    }
6971
6972    // ── search path miss ──────────────────────────────────
6973
6974    #[test]
6975    fn path_search_unmatched_errors() {
6976        // Without NIX_PATH entries matching, <nonexistent> errors out.
6977        // We unset NIX_PATH locally to ensure no entries match.
6978        let saved = std::env::var("NIX_PATH").ok();
6979        // SAFETY: tests run sequentially in single-threaded mode by
6980        // default? The thread_local NIX_PATH is per-thread but std::env
6981        // is process-global. We restore it after.
6982        unsafe {
6983            std::env::remove_var("NIX_PATH");
6984        }
6985        let result = eval("<this_should_not_resolve>");
6986        if let Some(v) = saved {
6987            unsafe {
6988                std::env::set_var("NIX_PATH", v);
6989            }
6990        }
6991        assert!(result.is_err());
6992    }
6993
6994    // ── Unary operators ────────────────────────────────────
6995
6996    #[test]
6997    fn unary_negate_int() {
6998        assert_eq!(ev("-7"), Value::Int(-7));
6999    }
7000
7001    #[test]
7002    fn unary_negate_float() {
7003        assert_eq!(ev("-2.5"), Value::Float(-2.5));
7004    }
7005
7006    #[test]
7007    fn unary_invert_true() {
7008        assert_eq!(ev("!true"), Value::Bool(false));
7009    }
7010
7011    #[test]
7012    fn unary_invert_false() {
7013        assert_eq!(ev("!false"), Value::Bool(true));
7014    }
7015
7016    #[test]
7017    fn unary_negate_bool_errors() {
7018        let result = eval("-true");
7019        assert!(result.is_err());
7020    }
7021
7022    #[test]
7023    fn unary_invert_int_errors() {
7024        let result = eval("!42");
7025        assert!(result.is_err());
7026    }
7027
7028    // ── Binary op type errors ──────────────────────────────
7029
7030    #[test]
7031    fn binop_add_attrs_errors() {
7032        let result = eval("{a=1;} + {b=2;}");
7033        assert!(result.is_err());
7034    }
7035
7036    #[test]
7037    fn binop_sub_string_errors() {
7038        let result = eval(r#""a" - "b""#);
7039        assert!(result.is_err());
7040    }
7041
7042    #[test]
7043    fn binop_mul_string_errors() {
7044        let result = eval(r#""a" * "b""#);
7045        assert!(result.is_err());
7046    }
7047
7048    #[test]
7049    fn binop_div_string_errors() {
7050        let result = eval(r#""a" / "b""#);
7051        assert!(result.is_err());
7052    }
7053
7054    #[test]
7055    fn binop_compare_attrs_errors() {
7056        let result = eval("{a=1;} < {b=2;}");
7057        assert!(result.is_err());
7058    }
7059
7060    #[test]
7061    fn binop_div_float_by_zero_int() {
7062        // Float / int(0) is NOT a DivisionByZero error in this evaluator —
7063        // only int/int matches the DivisionByZero branch. This documents
7064        // that branch.
7065        let result = eval("1.0 / 0");
7066        // Either inf or error is acceptable; the documented branch is
7067        // the int/int(0) → DivisionByZero one.
7068        let _ = result;
7069    }
7070
7071    #[test]
7072    fn binop_int_div_zero_is_division_by_zero() {
7073        let result = eval("5 / 0");
7074        match result {
7075            Err(EvalError::DivisionByZero) => {}
7076            other => panic!("expected DivisionByZero, got {other:?}"),
7077        }
7078    }
7079
7080    // ── if/then/else laziness ──────────────────────────────
7081
7082    #[test]
7083    fn if_else_only_chosen_branch_evaluated_then() {
7084        // The else branch contains a divide-by-zero that would error
7085        // if eagerly evaluated. Choosing the then branch must skip it.
7086        assert_eq!(ev("if true then 42 else 1 / 0"), Value::Int(42));
7087    }
7088
7089    #[test]
7090    fn if_else_only_chosen_branch_evaluated_else() {
7091        assert_eq!(ev("if false then 1 / 0 else 99"), Value::Int(99));
7092    }
7093
7094    #[test]
7095    fn if_condition_must_be_bool() {
7096        let result = eval("if 1 then 1 else 2");
7097        assert!(result.is_err());
7098    }
7099
7100    #[test]
7101    fn if_condition_lazy_does_not_force_unused() {
7102        // Lazy `let` ensures that `bad` is only forced if the chosen
7103        // branch references it.
7104        assert_eq!(
7105            ev("let bad = 1 / 0; in if true then 42 else bad"),
7106            Value::Int(42),
7107        );
7108    }
7109
7110    // ── Logic short-circuit laziness ───────────────────────
7111
7112    #[test]
7113    fn and_short_circuits_on_false() {
7114        // RHS contains an error; should never run.
7115        assert_eq!(ev("false && (1 / 0 == 0)"), Value::Bool(false));
7116    }
7117
7118    #[test]
7119    fn or_short_circuits_on_true() {
7120        assert_eq!(ev("true || (1 / 0 == 0)"), Value::Bool(true));
7121    }
7122
7123    #[test]
7124    fn implication_short_circuits_on_false_lhs() {
7125        // false -> anything is true; RHS not evaluated.
7126        assert_eq!(ev("false -> (1 / 0 == 0)"), Value::Bool(true));
7127    }
7128
7129    // ── Lambda fixpoint via let ────────────────────────────
7130
7131    #[test]
7132    fn lambda_fix_combinator_returns_attrset() {
7133        // The classic `fix = f: let x = f x; in x` shape.
7134        let v = ev(
7135            "let fix = f: let x = f x; in x; in
7136              (fix (self: { val = 1; double = self.val * 2; })).double",
7137        );
7138        assert_eq!(v, Value::Int(2));
7139    }
7140
7141    // ── eval_attrset rec scope details ─────────────────────
7142
7143    #[test]
7144    fn rec_attrset_self_reference() {
7145        // rec set with simple forward reference.
7146        let v = ev("(rec { a = b; b = 1; }).a");
7147        assert_eq!(v, Value::Int(1));
7148    }
7149
7150    #[test]
7151    fn rec_attrset_inherit_from_uses_outer_scope() {
7152        // inherit-from in rec uses the OUTER (lexical) scope to evaluate
7153        // the source expression, not the rec scope. We bind `src` in
7154        // an outer let so the inherit can find it.
7155        let v = ev(
7156            "let src = { a = 10; }; in
7157              rec {
7158                inherit (src) a;
7159                b = a + 1;
7160              }",
7161        );
7162        if let Value::Attrs(attrs) = v {
7163            let b = attrs.get("b").unwrap();
7164            let b_forced = force_value(b).unwrap();
7165            assert_eq!(b_forced, Value::Int(11));
7166        } else {
7167            panic!("expected attrs");
7168        }
7169    }
7170
7171    #[test]
7172    fn nonrec_attrset_no_self_reference() {
7173        // In a non-rec set, a name doesn't see its sibling. The error
7174        // surfaces as an UndefinedVar when the thunk is forced.
7175        let result = eval("({ a = 1; b = a + 1; }).b");
7176        assert!(result.is_err());
7177    }
7178
7179    // ── eval_attrset deep merge edge cases ─────────────────
7180
7181    #[test]
7182    fn dotted_binding_three_segments_then_sibling() {
7183        let v = ev("{ a.b.c = 1; a.b.d = 2; a.e = 3; }");
7184        if let Value::Attrs(attrs) = v {
7185            let a = attrs.get("a").unwrap();
7186            let a_forced = force_value(a).unwrap();
7187            if let Value::Attrs(a_attrs) = a_forced {
7188                let b = a_attrs.get("b").unwrap();
7189                let b_forced = force_value(b).unwrap();
7190                if let Value::Attrs(b_attrs) = b_forced {
7191                    assert_eq!(force_value(b_attrs.get("c").unwrap()).unwrap(), Value::Int(1));
7192                    assert_eq!(force_value(b_attrs.get("d").unwrap()).unwrap(), Value::Int(2));
7193                } else {
7194                    panic!("expected b to be attrs");
7195                }
7196                assert_eq!(force_value(a_attrs.get("e").unwrap()).unwrap(), Value::Int(3));
7197            } else {
7198                panic!("expected a to be attrs");
7199            }
7200        } else {
7201            panic!("expected outer attrs");
7202        }
7203    }
7204
7205    // ── rec/let dotted bindings in recursive scope ────────
7206
7207    #[test]
7208    fn rec_dotted_bindings_visible_to_siblings() {
7209        // Dotted bindings in rec blocks must be visible to sibling
7210        // bindings -- this is the nixpkgs lib/systems/parse.nix pattern.
7211        let v = ev("rec { types.openSB = 1; types.openCpu = 2; foo = types.openSB; }.foo");
7212        assert_eq!(v, Value::Int(1));
7213    }
7214
7215    #[test]
7216    fn rec_dotted_leaf_uses_rec_scope() {
7217        // Leaf expressions in dotted bindings must see sibling
7218        // rec-bindings, not just the parent scope.
7219        let v = ev("rec { types.a = f 1; f = x: x + 1; }.types.a");
7220        assert_eq!(v, Value::Int(2));
7221    }
7222
7223    #[test]
7224    fn rec_dotted_multiple_keys_merge() {
7225        // Multiple dotted bindings sharing a top-level key must merge.
7226        let v = ev("rec { types.a = 1; types.b = 2; x = types; }.x");
7227        if let Value::Attrs(attrs) = v {
7228            assert_eq!(force_value(attrs.get("a").unwrap()).unwrap(), Value::Int(1));
7229            assert_eq!(force_value(attrs.get("b").unwrap()).unwrap(), Value::Int(2));
7230        } else {
7231            panic!("expected attrs");
7232        }
7233    }
7234
7235    #[test]
7236    fn rec_nixpkgs_parse_pattern() {
7237        // Simplified nixpkgs lib/systems/parse.nix pattern:
7238        // rec block with dotted types.xxx bindings that reference
7239        // each other through the rec scope.
7240        let v = ev(r#"
7241            let
7242              mkOptionType = x: x;
7243              mergeOneOption = "merge";
7244              attrValues = builtins.attrValues;
7245              setType = name: value: { __type = name; } // value;
7246              mapAttrs = builtins.mapAttrs;
7247              enum = xs: mkOptionType { name = "enum"; check = x: builtins.elem x xs; };
7248              setTypes = type: mapAttrs (name: value: setType type.name ({ inherit name; } // value));
7249            in
7250            rec {
7251              types.openSB = mkOptionType { name = "sb"; merge = mergeOneOption; };
7252              types.significantByte = enum (attrValues significantBytes);
7253              significantBytes = setTypes types.openSB { bigEndian = {}; littleEndian = {}; };
7254              types.openCpuType = mkOptionType { name = "cpu-type"; };
7255              types.cpuType = enum (attrValues cpuTypes);
7256              cpuTypes = setTypes types.openCpuType { arm = { bits = 32; }; };
7257            }.types.openCpuType
7258        "#);
7259        if let Value::Attrs(attrs) = v {
7260            assert_eq!(
7261                force_value(attrs.get("name").unwrap()).unwrap(),
7262                Value::string("cpu-type")
7263            );
7264        } else {
7265            panic!("expected attrs");
7266        }
7267    }
7268
7269    #[test]
7270    fn let_dotted_leaf_uses_let_scope() {
7271        // Dotted binding leaf in a let block sees sibling let-bindings.
7272        let v = ev("let a.x = f 1; f = x: x + 1; in a.x");
7273        assert_eq!(v, Value::Int(2));
7274    }
7275
7276    #[test]
7277    fn let_inherit_from_plus_dotted_overrides() {
7278        // inherit-from and dotted bindings for the same key in a let
7279        // block: CppNix rejects this as a duplicate definition.  Sui
7280        // currently lets the dotted binding win (last-write-wins).
7281        // This test documents the current behaviour -- when we add
7282        // duplicate detection it should change to assert an error.
7283        let v = ev(r#"
7284            let
7285              src = { types = { existing = true; }; };
7286              inherit (src) types;
7287              types.added = true;
7288            in types
7289        "#);
7290        if let Value::Attrs(attrs) = v {
7291            // Dotted binding overwrites the inherited value
7292            assert_eq!(
7293                force_value(attrs.get("added").unwrap()).unwrap(),
7294                Value::Bool(true)
7295            );
7296            // Inherited 'existing' is lost because dotted replaced it
7297            assert!(attrs.get("existing").is_none());
7298        } else {
7299            panic!("expected attrs");
7300        }
7301    }
7302
7303    // ── Function pattern variations ────────────────────────
7304
7305    #[test]
7306    fn pattern_empty_no_args_no_ellipsis() {
7307        // {} pattern accepts only an empty attrset.
7308        assert_eq!(ev("({}: 1) {}"), Value::Int(1));
7309    }
7310
7311    #[test]
7312    fn pattern_empty_with_ellipsis_accepts_extra() {
7313        assert_eq!(ev("({...}: 1) { a = 1; b = 2; }"), Value::Int(1));
7314    }
7315
7316    #[test]
7317    fn pattern_all_defaults() {
7318        assert_eq!(
7319            ev("({a ? 1, b ? 2}: a + b) {}"),
7320            Value::Int(3),
7321        );
7322    }
7323
7324    #[test]
7325    fn pattern_at_bind_before() {
7326        // args @ { x }: args.x — bind name comes before pattern.
7327        assert_eq!(ev("(args @ { x }: args.x) { x = 7; }"), Value::Int(7));
7328    }
7329
7330    #[test]
7331    fn pattern_at_bind_after() {
7332        // { x } @ args: args.x — bind name comes after pattern.
7333        assert_eq!(ev("({ x } @ args: args.x) { x = 7; }"), Value::Int(7));
7334    }
7335
7336    #[test]
7337    fn pattern_default_references_other_arg() {
7338        // The default for `b` references `a` (which exists).
7339        assert_eq!(ev("({a, b ? a + 1}: b) {a = 10;}"), Value::Int(11));
7340    }
7341
7342    #[test]
7343    fn pattern_required_missing_errors() {
7344        let result = eval("({ a, b }: a) { a = 1; }");
7345        assert!(result.is_err());
7346    }
7347
7348    #[test]
7349    fn pattern_unexpected_errors_without_ellipsis() {
7350        let result = eval("({ a }: a) { a = 1; b = 2; }");
7351        assert!(result.is_err());
7352    }
7353
7354    // ── apply: error on non-callable ───────────────────────
7355
7356    #[test]
7357    fn apply_int_errors() {
7358        let result = eval("42 5");
7359        assert!(result.is_err());
7360    }
7361
7362    #[test]
7363    fn apply_string_errors() {
7364        let result = eval(r#""hi" 5"#);
7365        assert!(result.is_err());
7366    }
7367
7368    #[test]
7369    fn apply_attrset_without_functor_errors() {
7370        let result = eval("{ x = 1; } 5");
7371        assert!(result.is_err());
7372        let msg = format!("{}", result.unwrap_err());
7373        assert!(msg.contains("__functor") || msg.contains("cannot call"));
7374    }
7375
7376    // ── Select with multi-segment + default ────────────────
7377
7378    #[test]
7379    fn select_multi_segment_with_default() {
7380        // a.b.missing or 99 -- the missing segment yields the default.
7381        assert_eq!(ev("{ a = { b = 1; }; }.a.c or 99"), Value::Int(99));
7382    }
7383
7384    #[test]
7385    fn select_from_int_errors() {
7386        let result = eval("(1).x");
7387        assert!(result.is_err());
7388    }
7389
7390    // ── HasAttr edge cases ─────────────────────────────────
7391
7392    #[test]
7393    fn has_attr_on_non_set_returns_false() {
7394        // `expr ? a` where expr is not a set returns false (not error).
7395        assert_eq!(ev("1 ? x"), Value::Bool(false));
7396    }
7397
7398    #[test]
7399    fn has_attr_nested_path_present() {
7400        assert_eq!(ev("{ a = { b = 1; }; } ? a.b"), Value::Bool(true));
7401    }
7402
7403    #[test]
7404    fn has_attr_nested_path_missing() {
7405        assert_eq!(ev("{ a = { b = 1; }; } ? a.c"), Value::Bool(false));
7406    }
7407
7408    #[test]
7409    fn has_attr_intermediate_missing_returns_false() {
7410        assert_eq!(ev("{} ? a.b.c"), Value::Bool(false));
7411    }
7412
7413    // ── List eval edge cases ───────────────────────────────
7414
7415    #[test]
7416    fn list_with_function_value() {
7417        let v = ev("[(x: x + 1)]");
7418        if let Value::List(items) = v {
7419            assert_eq!(items.len(), 1);
7420            // List elements are now lazy (thunked). Force to check type.
7421            let forced = force_value(&items[0]).unwrap();
7422            assert!(matches!(forced, Value::Lambda(_)));
7423        } else {
7424            panic!("expected list");
7425        }
7426    }
7427
7428    // ── eval_inherit edge: inherit from missing var ────────
7429
7430    #[test]
7431    fn inherit_unknown_name_errors() {
7432        let result = eval("let x = 1; in let inherit nonexistent; in nonexistent");
7433        assert!(result.is_err());
7434    }
7435
7436    // ── String op: string concat preserves context ─────────
7437
7438    #[test]
7439    fn string_concat_no_context_when_both_plain() {
7440        let v = ev(r#""abc" + "def""#);
7441        if let Value::String(ns) = v {
7442            assert_eq!(ns.chars, "abcdef");
7443            assert!(!ns.has_context());
7444        } else {
7445            panic!("expected string");
7446        }
7447    }
7448
7449    // ── Parens / Root ──────────────────────────────────────
7450
7451    #[test]
7452    fn parens_around_expression() {
7453        assert_eq!(ev("(1 + 2)"), Value::Int(3));
7454    }
7455
7456    #[test]
7457    fn nested_parens() {
7458        assert_eq!(ev("(((42)))"), Value::Int(42));
7459    }
7460
7461    // ── Throw via builtins ─────────────────────────────────
7462
7463    #[test]
7464    fn throw_propagates_as_error() {
7465        let result = eval(r#"builtins.throw "kaboom""#);
7466        match result {
7467            Err(EvalError::Throw(s)) => assert!(s.contains("kaboom")),
7468            other => panic!("expected Throw, got {other:?}"),
7469        }
7470    }
7471
7472    #[test]
7473    fn assert_failed_propagates_as_error() {
7474        let result = eval("assert false; 1");
7475        match result {
7476            Err(EvalError::AssertionFailed(_)) => {}
7477            other => panic!("expected AssertionFailed, got {other:?}"),
7478        }
7479    }
7480
7481    // ── eval_str InterpolPart::Literal only ────────────────
7482
7483    #[test]
7484    fn string_no_interp_yields_no_context() {
7485        let v = ev(r#""just literal""#);
7486        if let Value::String(ns) = v {
7487            assert!(!ns.has_context());
7488        } else {
7489            panic!("expected string");
7490        }
7491    }
7492
7493    // ── Path interpolation adds context ───────────────────
7494
7495    // Byte-parity root #5: interpolating a source path is CppNix copy-to-store
7496    // coercion — the path is NAR-copied into /nix/store/<hash>-<name> and the
7497    // store path (with store-path context) is spliced in, not the raw path.
7498    // NAR of a single regular file is content+basename only (location-
7499    // independent), so a temp <dir>/data.txt of "hello\n" yields the exact
7500    // store path nix 2.34 produced: /nix/store/y9dmv…-data.txt.
7501    #[test]
7502    fn interp_path_copies_to_store_byte_matches_cppnix() {
7503        let dir = std::env::temp_dir().join(format!("sui-r5-interp-{}", std::process::id()));
7504        let _ = std::fs::remove_dir_all(&dir);
7505        std::fs::create_dir_all(&dir).unwrap();
7506        let f = dir.join("data.txt");
7507        std::fs::write(&f, b"hello\n").unwrap();
7508        let expr = format!(r#""${{{}}}""#, f.display());
7509        let v = eval(&expr).unwrap();
7510        if let Value::String(ns) = v {
7511            assert_eq!(
7512                ns.chars.to_string(),
7513                "/nix/store/y9dmvfhip31hg8ia4njwjz9vfa3ndphr-data.txt",
7514            );
7515            assert!(ns.has_context());
7516        } else {
7517            panic!("expected string");
7518        }
7519        let _ = std::fs::remove_dir_all(&dir);
7520    }
7521
7522    // ── pipe operators (NotImplemented) ────────────────────
7523    // Pipe operators (|>, <|) are parsed as PipeRight/PipeLeft and
7524    // currently return NotImplemented. We can't easily evaluate them
7525    // here because rnix may not even parse them, so we just rely on
7526    // the binop branch existing.
7527
7528    // ── ParseError surface ─────────────────────────────────
7529
7530    #[test]
7531    fn parse_error_unbalanced_braces() {
7532        let result = eval("{ a = 1");
7533        assert!(result.is_err());
7534        let err = result.unwrap_err();
7535        assert!(matches!(err, EvalError::ParseError(_)));
7536    }
7537
7538    #[test]
7539    fn parse_error_dangling_let() {
7540        let result = eval("let in");
7541        assert!(result.is_err());
7542    }
7543
7544    #[test]
7545    fn parse_error_empty_input() {
7546        let result = eval("");
7547        assert!(result.is_err());
7548    }
7549
7550    // ── num_op coverage via float ops ──────────────────────
7551
7552    #[test]
7553    fn float_int_subtraction() {
7554        assert_eq!(ev("3.5 - 1"), Value::Float(2.5));
7555    }
7556
7557    #[test]
7558    fn int_float_subtraction() {
7559        assert_eq!(ev("3 - 0.5"), Value::Float(2.5));
7560    }
7561
7562    #[test]
7563    fn float_float_division() {
7564        assert_eq!(ev("6.0 / 2.0"), Value::Float(3.0));
7565    }
7566
7567    #[test]
7568    fn int_float_multiplication() {
7569        assert_eq!(ev("3 * 2.5"), Value::Float(7.5));
7570    }
7571
7572    // ── compare with mixed numerics ────────────────────────
7573
7574    #[test]
7575    fn compare_int_float_less() {
7576        assert_eq!(ev("1 < 1.5"), Value::Bool(true));
7577    }
7578
7579    #[test]
7580    fn compare_float_int_more() {
7581        assert_eq!(ev("3.5 > 3"), Value::Bool(true));
7582    }
7583
7584    #[test]
7585    fn compare_equal_int_float() {
7586        assert_eq!(ev("3 <= 3.0"), Value::Bool(true));
7587    }
7588
7589    // ── Equality ──────────────────────────────────────────
7590
7591    #[test]
7592    fn equal_lists_same() {
7593        assert_eq!(ev("[1 2 3] == [1 2 3]"), Value::Bool(true));
7594    }
7595
7596    #[test]
7597    fn equal_lists_diff_length() {
7598        assert_eq!(ev("[1 2] == [1 2 3]"), Value::Bool(false));
7599    }
7600
7601    #[test]
7602    fn not_equal_lists() {
7603        assert_eq!(ev("[1] != [2]"), Value::Bool(true));
7604    }
7605
7606    #[test]
7607    fn equal_attrsets_same() {
7608        assert_eq!(ev("{a = 1; b = 2;} == {b = 2; a = 1;}"), Value::Bool(true));
7609    }
7610
7611    // ── Lambda identity equality (Rc ptr_eq) ────────────────
7612    // Regression test: same lambda via Rc must compare equal.
7613    // Without this, nixpkgs stdenv evaluation enters an infinite loop
7614    // because `crossSystem != localSystem` returns true even when both
7615    // are the same elaborate result (containing shared function attrs).
7616
7617    #[test]
7618    fn lambda_self_equality_in_attrset() {
7619        // Same closure shared via let → inherit must be equal
7620        assert_eq!(
7621            ev("let f = x: x; in { a = 1; inherit f; } == { a = 1; inherit f; }"),
7622            Value::Bool(true),
7623        );
7624    }
7625
7626    #[test]
7627    fn lambda_self_reference_attrset_equality() {
7628        // Attrset with function attr: x == x must be true
7629        assert_eq!(
7630            ev("let x = { a = 1; f = y: y; }; in x == x"),
7631            Value::Bool(true),
7632        );
7633    }
7634
7635    #[test]
7636    fn lambda_different_closures_not_equal() {
7637        // Different lambda closures (even structurally identical) must be false
7638        assert_eq!(
7639            ev("{ f = x: x; } == { f = x: x; }"),
7640            Value::Bool(false),
7641        );
7642    }
7643
7644    #[test]
7645    fn lambda_ne_does_not_force_unused_branch() {
7646        // If crossSystem == localSystem (same obj), != returns false,
7647        // and the then-branch (with throw) is never forced.
7648        assert_eq!(
7649            ev("let ls = { a = 1; f = x: x; }; in if ls != ls then builtins.throw \"bug\" else 42"),
7650            Value::Int(42),
7651        );
7652    }
7653
7654    // ── force_value chains thunks ──────────────────────────
7655
7656    #[test]
7657    fn force_value_through_thunk() {
7658        let root = rnix::Root::parse("1 + 2");
7659        let expr = root.tree().expr().unwrap();
7660        let thunk = Thunk::new_suspended(expr, Env::new());
7661        let val = Value::Thunk(thunk);
7662        assert_eq!(force_value(&val).unwrap(), Value::Int(3));
7663    }
7664
7665    // ── Builtin name "tryEval" lazy arg path ──────────────
7666
7667    #[test]
7668    fn try_eval_catches_thrown_error() {
7669        // tryEval wraps the thunk and catches throws inside.
7670        let v = ev(r#"(builtins.tryEval (builtins.throw "oops")).success"#);
7671        assert_eq!(v, Value::Bool(false));
7672    }
7673
7674    #[test]
7675    fn try_eval_returns_value_on_success() {
7676        let v = ev("(builtins.tryEval 42).value");
7677        assert_eq!(v, Value::Int(42));
7678    }
7679
7680    // ── LegacyLet (`let { body = ...; ...}`) ───────────────
7681
7682    #[test]
7683    fn legacy_let_returns_body_attr() {
7684        // `let { x = 1; body = x + 41; }` is the legacy let form: it
7685        // is desugared as a recursive set whose `body` attr is the
7686        // result.
7687        assert_eq!(ev("let { x = 1; body = x + 41; }"), Value::Int(42));
7688    }
7689
7690    #[test]
7691    fn legacy_let_missing_body_errors() {
7692        let result = eval("let { x = 1; }");
7693        assert!(result.is_err());
7694    }
7695
7696    #[test]
7697    fn legacy_let_with_inherit_from_scope() {
7698        assert_eq!(
7699            ev("let outer = 5; in let { inherit outer; body = outer * 2; }"),
7700            Value::Int(10),
7701        );
7702    }
7703
7704    // ── eval_str interpolation more cases ──────────────────
7705
7706    #[test]
7707    fn interp_with_string_concat_preserves_order() {
7708        assert_eq!(
7709            ev(r#"let a = "x"; b = "y"; in "${a}-${b}""#),
7710            Value::string("x-y"),
7711        );
7712    }
7713
7714    #[test]
7715    fn interp_only_literal_part() {
7716        assert_eq!(ev(r#""no interp here""#), Value::string("no interp here"));
7717    }
7718
7719    // ── eval_attr dynamic / string keys ────────────────────
7720
7721    #[test]
7722    fn dynamic_attr_via_string_key_in_set() {
7723        // `{ "a" = 1; }.a` works because attr keys can be string literals.
7724        assert_eq!(ev(r#"{ "a" = 1; }.a"#), Value::Int(1));
7725    }
7726
7727    #[test]
7728    fn dynamic_attr_via_interpolated_key() {
7729        let v = ev(r#"let k = "foo"; in { ${k} = 99; }.foo"#);
7730        assert_eq!(v, Value::Int(99));
7731    }
7732
7733    // ── String key access via select with dynamic ──────────
7734
7735    #[test]
7736    fn select_with_string_key() {
7737        let v = ev(r#"{ a = 42; }."a""#);
7738        assert_eq!(v, Value::Int(42));
7739    }
7740
7741    // ── Apply via __functor on attrset ─────────────────────
7742
7743    #[test]
7744    fn apply_attrset_with_functor_works() {
7745        let v = ev("let s = { __functor = self: x: x + 1; }; in s 5");
7746        assert_eq!(v, Value::Int(6));
7747    }
7748
7749    // ── Negation of negative ───────────────────────────────
7750
7751    #[test]
7752    fn double_negate_int() {
7753        assert_eq!(ev("- (-5)"), Value::Int(5));
7754    }
7755
7756    // ── Inherit from rec scope binding visibility ──────────
7757
7758    #[test]
7759    fn inherit_in_let_makes_name_available() {
7760        assert_eq!(
7761            ev("let src = { a = 7; }; in let inherit (src) a; in a"),
7762            Value::Int(7),
7763        );
7764    }
7765
7766    // ── String + path ──────────────────────────────────────
7767
7768    #[test]
7769    fn path_plus_string_yields_path() {
7770        let v = ev(r#"/foo + "/bar""#);
7771        match v {
7772            Value::Path(p) => assert_eq!(&*p, "/foo/bar"),
7773            _ => panic!("expected path"),
7774        }
7775    }
7776
7777    // ── Lazy attrset value not forced unless selected ──────
7778
7779    #[test]
7780    fn attrset_value_not_forced_unless_selected() {
7781        // `bad` is an attr whose value would error if forced, but we
7782        // only ever select `good`, so it's never touched.
7783        assert_eq!(
7784            ev(r#"{ bad = builtins.throw "boom"; good = 42; }.good"#),
7785            Value::Int(42),
7786        );
7787    }
7788
7789    // ── Lambda calling itself via let ──────────────────────
7790
7791    #[test]
7792    fn lambda_recursive_via_let() {
7793        // factorial via let-bound recursive function
7794        assert_eq!(
7795            ev("let fact = n: if n == 0 then 1 else n * fact (n - 1); in fact 5"),
7796            Value::Int(120),
7797        );
7798    }
7799
7800    // ── Dynamic key in select ──────────────────────────────
7801
7802    #[test]
7803    fn select_with_dynamic_key_via_var() {
7804        // ${k} interpolation in select position is not standard Nix
7805        // syntax, but a string-literal key works for select.
7806        assert_eq!(ev(r#"let k = { x = 1; }; in k.x"#), Value::Int(1));
7807    }
7808
7809    // ── Compare strings ────────────────────────────────────
7810
7811    #[test]
7812    fn compare_string_lex_greater_or_equal() {
7813        assert_eq!(ev(r#""b" >= "a""#), Value::Bool(true));
7814        assert_eq!(ev(r#""a" >= "a""#), Value::Bool(true));
7815        assert_eq!(ev(r#""a" >= "b""#), Value::Bool(false));
7816    }
7817
7818    // ── PartialEq across types ─────────────────────────────
7819
7820    #[test]
7821    fn equal_int_string_false() {
7822        assert_eq!(ev(r#"1 == "1""#), Value::Bool(false));
7823    }
7824
7825    #[test]
7826    fn equal_null_int_false() {
7827        assert_eq!(ev("null == 0"), Value::Bool(false));
7828    }
7829
7830    // ── Update operator on thunked operands ────────────────
7831
7832    #[test]
7833    fn update_with_let_bound_operands() {
7834        assert_eq!(
7835            ev("let a = { x = 1; }; b = { y = 2; }; in (a // b).y"),
7836            Value::Int(2),
7837        );
7838    }
7839
7840    // ── Concat on let-bound lists ──────────────────────────
7841
7842    #[test]
7843    fn concat_lists_from_let() {
7844        assert_eq!(
7845            ev("let a = [1 2]; b = [3 4]; in builtins.length (a ++ b)"),
7846            Value::Int(4),
7847        );
7848    }
7849
7850    // ── String interpolation: list coercion ─────────────────
7851
7852    #[test]
7853    fn interp_list_coerces_with_spaces() {
7854        // Lists in interpolation are now coerced via coerce_to_string
7855        // (space-joined elements).
7856        assert_eq!(
7857            ev(r#""${toString [1 2 3]}""#),
7858            Value::string("1 2 3"),
7859        );
7860    }
7861
7862    #[test]
7863    fn interp_list_directly_coerces() {
7864        // Direct list interpolation space-joins elements via coerce_to_string.
7865        assert_eq!(
7866            ev(r#""${[1 2]}""#),
7867            Value::string("1 2"),
7868        );
7869    }
7870
7871    // ── String interpolation: outPath ─────────────────────
7872
7873    #[test]
7874    fn interp_outpath_attrset() {
7875        assert_eq!(
7876            ev(r#"let x = { outPath = "/nix/store/abc"; }; in "${x}""#),
7877            Value::string("/nix/store/abc"),
7878        );
7879    }
7880
7881    #[test]
7882    fn interp_tostring_takes_priority_over_outpath() {
7883        assert_eq!(
7884            ev(r#"let x = { __toString = self: "custom"; outPath = "/ignored"; }; in "${x}""#),
7885            Value::string("custom"),
7886        );
7887    }
7888
7889    #[test]
7890    fn interp_derivation_coerces_to_outpath() {
7891        // derivation produces an attrset with outPath
7892        let result = eval(r#"
7893            let drv = builtins.derivation {
7894                name = "test";
7895                system = "x86_64-linux";
7896                builder = "/bin/sh";
7897            };
7898            in "${drv}"
7899        "#).unwrap();
7900        if let Value::String(s) = result {
7901            assert!(s.chars.starts_with("/nix/store/"), "got: {}", s.chars);
7902        } else {
7903            panic!("expected string");
7904        }
7905    }
7906
7907    // ── String interpolation: lambda error ─────────────────
7908
7909    #[test]
7910    fn interp_lambda_errors() {
7911        let result = eval(r#""${x: x}""#);
7912        assert!(result.is_err());
7913    }
7914
7915    // ── force_value tests ────────────────────────────────────
7916
7917    #[test]
7918    fn force_value_int_returns_same() {
7919        let v = Value::Int(42);
7920        assert_eq!(force_value(&v).unwrap(), Value::Int(42));
7921    }
7922
7923    #[test]
7924    fn force_value_bool_returns_same() {
7925        let v = Value::Bool(true);
7926        assert_eq!(force_value(&v).unwrap(), Value::Bool(true));
7927    }
7928
7929    #[test]
7930    fn force_value_string_returns_same() {
7931        let v = Value::string("hello");
7932        assert_eq!(force_value(&v).unwrap(), Value::string("hello"));
7933    }
7934
7935    #[test]
7936    fn force_value_attrs_returns_same() {
7937        let mut a = NixAttrs::new();
7938        a.insert("x".to_string(), Value::Int(1));
7939        let v = Value::Attrs(Rc::new(a.clone()));
7940        assert_eq!(force_value(&v).unwrap(), Value::Attrs(Rc::new(a)));
7941    }
7942
7943    #[test]
7944    fn force_value_list_returns_same() {
7945        let v = Value::list(vec![Value::Int(1), Value::Int(2)]);
7946        assert_eq!(
7947            force_value(&v).unwrap(),
7948            Value::list(vec![Value::Int(1), Value::Int(2)]),
7949        );
7950    }
7951
7952    #[test]
7953    fn force_value_null_returns_null() {
7954        let v = Value::Null;
7955        assert_eq!(force_value(&v).unwrap(), Value::Null);
7956    }
7957
7958    #[test]
7959    fn force_value_evaluated_thunk_returns_cached() {
7960        // Thunk wrapping a simple expression should evaluate and cache
7961        let v = ev("let x = 1 + 2; in x");
7962        assert_eq!(v, Value::Int(3));
7963        // Force again — should return the cached value
7964        assert_eq!(force_value(&v).unwrap(), Value::Int(3));
7965    }
7966
7967    // ── Tail-call loop tests ─────────────────────────────────
7968
7969    #[test]
7970    fn tco_if_true_condition() {
7971        assert_eq!(ev("if true then 42 else 0"), Value::Int(42));
7972    }
7973
7974    #[test]
7975    fn tco_if_false_condition() {
7976        assert_eq!(ev("if false then 42 else 0"), Value::Int(0));
7977    }
7978
7979    #[test]
7980    fn tco_deeply_nested_if_else_chain() {
7981        // Build a chain: if false then 1 else if false then 2 else ... else 150
7982        // All conditions are false except the final else, which produces 150.
7983        let mut expr = String::from("150");
7984        for i in (1..150).rev() {
7985            expr = format!("if false then {} else {}", i, expr);
7986        }
7987        let v = ev(&expr);
7988        assert_eq!(v, Value::Int(150));
7989    }
7990
7991    #[test]
7992    fn tco_assert_true_passes_through() {
7993        assert_eq!(ev("assert true; 42"), Value::Int(42));
7994    }
7995
7996    #[test]
7997    fn tco_assert_false_throws_assertion_failed() {
7998        let result = eval("assert false; 42");
7999        assert!(result.is_err());
8000        let err = result.unwrap_err();
8001        assert!(
8002            matches!(err, EvalError::AssertionFailed(_)),
8003            "expected AssertionFailed, got: {err}",
8004        );
8005    }
8006
8007    #[test]
8008    fn tco_with_makes_scope_available() {
8009        assert_eq!(ev("with { x = 10; y = 20; }; x + y"), Value::Int(30));
8010    }
8011
8012    #[test]
8013    fn tco_let_in_creates_bindings() {
8014        assert_eq!(ev("let a = 5; in a"), Value::Int(5));
8015    }
8016
8017    #[test]
8018    fn tco_let_in_multiple_bindings() {
8019        assert_eq!(ev("let a = 1; b = 2; c = 3; in a + b + c"), Value::Int(6));
8020    }
8021
8022    // ── eval_attrset tests ───────────────────────────────────
8023
8024    #[test]
8025    fn eval_attrset_empty() {
8026        let v = ev("{}");
8027        if let Value::Attrs(attrs) = v {
8028            assert!(attrs.is_empty(), "expected empty attrset");
8029        } else {
8030            panic!("expected attrset, got {v:?}");
8031        }
8032    }
8033
8034    #[test]
8035    fn eval_attrset_simple_kv() {
8036        let v = ev("{ a = 1; b = 2; }");
8037        if let Value::Attrs(attrs) = v {
8038            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
8039            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
8040        } else {
8041            panic!("expected attrset, got {v:?}");
8042        }
8043    }
8044
8045    #[test]
8046    fn eval_attrset_recursive() {
8047        assert_eq!(ev("(rec { a = 1; b = a + 1; }).b"), Value::Int(2));
8048        assert_eq!(ev("(rec { a = 1; b = a + 1; }).a"), Value::Int(1));
8049    }
8050
8051    #[test]
8052    fn eval_attrset_inherit_from_scope() {
8053        assert_eq!(ev("let x = 1; in { inherit x; }.x"), Value::Int(1));
8054    }
8055
8056    #[test]
8057    fn eval_attrset_inherit_from_expr() {
8058        assert_eq!(
8059            ev("{ inherit (builtins) true; }.true"),
8060            Value::Bool(true),
8061        );
8062    }
8063
8064    #[test]
8065    fn eval_attrset_dotted_path() {
8066        assert_eq!(ev("{ a.b.c = 1; }.a.b.c"), Value::Int(1));
8067    }
8068
8069    #[test]
8070    fn eval_attrset_update_merge() {
8071        let v = ev("{ a = 1; } // { b = 2; }");
8072        if let Value::Attrs(attrs) = v {
8073            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
8074            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
8075        } else {
8076            panic!("expected attrset, got {v:?}");
8077        }
8078    }
8079
8080    // ── eval_apply tests ─────────────────────────────────────
8081
8082    #[test]
8083    fn eval_apply_simple_function() {
8084        assert_eq!(ev("(x: x + 1) 2"), Value::Int(3));
8085    }
8086
8087    #[test]
8088    fn eval_apply_pattern_destructuring() {
8089        assert_eq!(ev("({a, b}: a + b) { a = 1; b = 2; }"), Value::Int(3));
8090    }
8091
8092    #[test]
8093    fn eval_apply_default_arguments() {
8094        assert_eq!(ev("({a, b ? 0}: a + b) { a = 1; }"), Value::Int(1));
8095    }
8096
8097    #[test]
8098    fn eval_apply_ellipsis() {
8099        assert_eq!(ev("({a, ...}: a) { a = 1; b = 2; }"), Value::Int(1));
8100    }
8101
8102    // ── eval_select tests ────────────────────────────────────
8103
8104    #[test]
8105    fn eval_select_single_key() {
8106        assert_eq!(ev("{ a = 1; }.a"), Value::Int(1));
8107    }
8108
8109    #[test]
8110    fn eval_select_multi_level() {
8111        assert_eq!(ev("{ a.b = 1; }.a.b"), Value::Int(1));
8112    }
8113
8114    #[test]
8115    fn eval_select_with_or_default() {
8116        assert_eq!(ev("{}.a or 42"), Value::Int(42));
8117    }
8118
8119    #[test]
8120    fn eval_select_missing_key_without_default_throws() {
8121        let result = eval("{}.a");
8122        assert!(result.is_err());
8123    }
8124
8125    // ── BinOp tests ──────────────────────────────────────────
8126
8127    #[test]
8128    fn binop_add_ints() {
8129        assert_eq!(ev("1 + 2"), Value::Int(3));
8130    }
8131
8132    #[test]
8133    fn binop_sub_ints() {
8134        assert_eq!(ev("3 - 1"), Value::Int(2));
8135    }
8136
8137    #[test]
8138    fn binop_mul_ints() {
8139        assert_eq!(ev("2 * 3"), Value::Int(6));
8140    }
8141
8142    #[test]
8143    fn binop_div_ints() {
8144        assert_eq!(ev("6 / 2"), Value::Int(3));
8145    }
8146
8147    #[test]
8148    fn binop_float_arithmetic() {
8149        assert_eq!(ev("1.5 + 2.5"), Value::Float(4.0));
8150    }
8151
8152    #[test]
8153    fn binop_string_concat() {
8154        assert_eq!(
8155            ev(r#""hello" + " " + "world""#),
8156            Value::string("hello world"),
8157        );
8158    }
8159
8160    #[test]
8161    fn binop_list_concat() {
8162        assert_eq!(
8163            ev("[1 2] ++ [3 4]"),
8164            Value::list(vec![
8165                Value::Int(1),
8166                Value::Int(2),
8167                Value::Int(3),
8168                Value::Int(4),
8169            ]),
8170        );
8171    }
8172
8173    #[test]
8174    fn binop_attrset_update() {
8175        let v = ev("{ a = 1; } // { b = 2; }");
8176        if let Value::Attrs(attrs) = v {
8177            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
8178            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
8179        } else {
8180            panic!("expected attrset, got {v:?}");
8181        }
8182    }
8183
8184    #[test]
8185    fn binop_less_than() {
8186        assert_eq!(ev("1 < 2"), Value::Bool(true));
8187        assert_eq!(ev("2 < 1"), Value::Bool(false));
8188    }
8189
8190    #[test]
8191    fn binop_greater_than() {
8192        assert_eq!(ev("2 > 1"), Value::Bool(true));
8193        assert_eq!(ev("1 > 2"), Value::Bool(false));
8194    }
8195
8196    #[test]
8197    fn binop_equal() {
8198        assert_eq!(ev("1 == 1"), Value::Bool(true));
8199        assert_eq!(ev("1 == 2"), Value::Bool(false));
8200    }
8201
8202    #[test]
8203    fn binop_not_equal() {
8204        assert_eq!(ev("1 != 2"), Value::Bool(true));
8205        assert_eq!(ev("1 != 1"), Value::Bool(false));
8206    }
8207
8208    #[test]
8209    fn binop_logical_and() {
8210        assert_eq!(ev("true && false"), Value::Bool(false));
8211        assert_eq!(ev("true && true"), Value::Bool(true));
8212    }
8213
8214    #[test]
8215    fn binop_logical_or() {
8216        assert_eq!(ev("true || false"), Value::Bool(true));
8217        assert_eq!(ev("false || false"), Value::Bool(false));
8218    }
8219
8220    #[test]
8221    fn binop_logical_not() {
8222        assert_eq!(ev("!true"), Value::Bool(false));
8223        assert_eq!(ev("!false"), Value::Bool(true));
8224    }
8225
8226    #[test]
8227    fn binop_implication() {
8228        assert_eq!(ev("false -> true"), Value::Bool(true));
8229        assert_eq!(ev("false -> false"), Value::Bool(true));
8230        assert_eq!(ev("true -> true"), Value::Bool(true));
8231        assert_eq!(ev("true -> false"), Value::Bool(false));
8232    }
8233}