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
1901fn eval_apply(app: &ast::Apply, env: &Env) -> Result<Value, EvalError> {
1902    let func_expr = app
1903        .lambda()
1904        .ok_or_else(|| EvalError::ParseError("apply missing function".to_string()))?;
1905    let arg_expr = app
1906        .argument()
1907        .ok_or_else(|| EvalError::ParseError("apply missing argument".to_string()))?;
1908    let func = force_value(&eval_expr(&func_expr, env)?)?;
1909    // Lambda arguments are wrapped in a thunk for call-by-need semantics.
1910    // Thunk strategy depends on function type:
1911    // - Lambda: ALWAYS thunk (call-by-need, enables fixpoints)
1912    // - tryEval: ALWAYS thunk (must catch errors during force)
1913    // - Builtin: evaluate eagerly (builtins always force args anyway;
1914    //   thunking wastes Rc + OnceCell allocation per call)
1915    // - __functor: evaluate eagerly (will be applied immediately)
1916    let arg = match &func {
1917        Value::Lambda(_) => {
1918            // Call-by-need: the arg is thunked so it forces lazily. But a
1919            // PURE-CONSTANT arg (a literal, a non-interpolated string, or a
1920            // non-interpolated path) can never throw or diverge, so producing
1921            // its value directly is byte-neutral whether or not the lambda ever
1922            // forces it — identical eval-order-observable behavior, one fewer
1923            // never-forced thunk. This is `arg_pure_constant` ONLY: any arg that
1924            // could throw/diverge/observe a fixpoint (Ident with-scope, Select,
1925            // Apply, BinOp, …) stays fully thunked to preserve laziness.
1926            if let Some(v) = eval_pure_constant_arg(&arg_expr) {
1927                v
1928            } else {
1929                crate::perf::inc(crate::perf::Counter::ThunkSiteApplyArg);
1930                Value::Thunk(Thunk::new_suspended(arg_expr.clone(), env.clone()))
1931            }
1932        }
1933        Value::Builtin(b) if b.name == "tryEval" => {
1934            crate::perf::inc(crate::perf::Counter::ThunkSiteApplyArg);
1935            Value::Thunk(Thunk::new_suspended(arg_expr.clone(), env.clone()))
1936        }
1937        _ => eval_expr(&arg_expr, env)?,
1938    };
1939    apply(func, arg)
1940}
1941
1942/// If `arg_expr` is a PURE CONSTANT — a literal, a non-interpolated string, or
1943/// a non-interpolated absolute/home path — return its value directly (no thunk).
1944///
1945/// A pure constant has no free variables, cannot throw, cannot diverge, and has
1946/// no fixpoint/laziness interaction: `eval_expr(arg)` is total and produces the
1947/// exact value a suspended thunk of it would yield on force. Producing it
1948/// eagerly in a call-by-need arg position is therefore byte-neutral (the
1949/// lambda that never forces the arg observes no difference — the value is inert).
1950///
1951/// Returns `None` for EVERYTHING else (Ident — may hit a with-scope force;
1952/// Select/Apply/BinOp/If/… — may throw or diverge; interpolated Str/Path —
1953/// must force `${…}` lazily), which keeps those args fully thunked. `env` is
1954/// NOT threaded in because a pure constant needs no environment; if a match
1955/// arm ever needed `env`, it would not be a pure constant.
1956fn eval_pure_constant_arg(arg_expr: &ast::Expr) -> Option<Value> {
1957    match arg_expr {
1958        ast::Expr::Literal(lit) => eval_literal(lit).ok(),
1959        ast::Expr::Str(st) if !str_has_interpolation(st) => {
1960            // No interpolation ⇒ `eval_str` runs no force/coerce; env is unused.
1961            eval_str(st, &Env::new()).ok()
1962        }
1963        ast::Expr::PathAbs(p) if !parts_have_interpolation(&p.parts()) => {
1964            let text = crate::path::canon_abs(&p.syntax().text().to_string());
1965            Some(Value::Path(Box::new(SmolStr::from(text.as_str()))))
1966        }
1967        ast::Expr::PathHome(p) if !parts_have_interpolation(&p.parts()) => {
1968            let text = p.syntax().text().to_string();
1969            Some(Value::Path(Box::new(SmolStr::from(text.as_str()))))
1970        }
1971        _ => None,
1972    }
1973}
1974
1975fn eval_str(s: &ast::Str, env: &Env) -> Result<Value, EvalError> {
1976    let mut result = String::new();
1977    let mut ctx = StringContext::new();
1978    for part in s.normalized_parts() {
1979        match part {
1980            InterpolPart::Literal(text) => result.push_str(&text),
1981            InterpolPart::Interpolation(interpol) => {
1982                let expr = interpol.expr().ok_or_else(|| {
1983                    EvalError::ParseError("interpolation missing expr".to_string())
1984                })?;
1985                let val = force_value(&eval_expr(&expr, env)?)?;
1986                // CppNix string interpolation is copy-to-store coercion: an
1987                // interpolated source path (`"${./foo}"`) is NAR-copied into
1988                // the store and the store path is spliced in (with context),
1989                // never the raw filesystem path.
1990                let (s, c) = val.coerce_to_string_copy_to_store()?;
1991                result.push_str(&s);
1992                ctx.merge(&c);
1993            }
1994        }
1995    }
1996    Ok(Value::String(Rc::new(NixString::with_context(result, ctx))))
1997}
1998
1999/// Whether a list of path parts contains a `${…}` interpolation. When
2000/// it does not, the raw `.syntax().text()` shortcut is byte-identical
2001/// and cheaper, so the trivial fast paths stay on that shortcut.
2002fn parts_have_interpolation(parts: &[InterpolPart<rnix::ast::PathContent>]) -> bool {
2003    parts
2004        .iter()
2005        .any(|p| matches!(p, InterpolPart::Interpolation(_)))
2006}
2007
2008/// Whether a string literal contains any `${…}` interpolation part. A `false`
2009/// result means the string is a pure constant (`eval_str` runs no force/coerce
2010/// and cannot throw), so `maybe_thunk` may evaluate it eagerly byte-neutrally.
2011fn str_has_interpolation(s: &ast::Str) -> bool {
2012    s.normalized_parts()
2013        .iter()
2014        .any(|p| matches!(p, InterpolPart::Interpolation(_)))
2015}
2016
2017/// Evaluate an interpolatable path literal that contains `${…}` parts.
2018///
2019/// CppNix path interpolation (`./${x}.nix`, `/a/${e}`, `~/x/${e}`):
2020///   * each literal segment is spliced verbatim,
2021///   * each `${e}` is **plain**-coerced to a string with context
2022///     (NOT copy-to-store — path-typed interpolations splice the raw
2023///     store/filesystem path, e.g. `/bar/${./foo}` → `/bar/tmp/foo`),
2024///   * the concatenated text is then resolved exactly like the plain
2025///     path literal of the same kind (relative → joined + normalized
2026///     against the defining file's directory; absolute/home → verbatim),
2027///   * the result is a `path` value.
2028///
2029/// Parts come from rnix's `<PathKind>::parts()` which splits the path
2030/// token stream into `Literal(PathContent)` / `Interpolation(Interpol)`.
2031fn eval_interpol_path_parts(
2032    parts: &[InterpolPart<rnix::ast::PathContent>],
2033    kind: PathKind,
2034    env: &Env,
2035) -> Result<Value, EvalError> {
2036    let mut text = String::new();
2037    for part in parts {
2038        match part {
2039            InterpolPart::Literal(content) => text.push_str(content.text()),
2040            InterpolPart::Interpolation(interpol) => {
2041                let expr = interpol.expr().ok_or_else(|| {
2042                    EvalError::ParseError("path interpolation missing expr".to_string())
2043                })?;
2044                let val = force_value(&eval_expr(&expr, env)?)?;
2045                // Plain coercion (coerceMore = false): a path-typed
2046                // interpolation splices the raw path string, never a
2047                // copied-to-store hash path.
2048                let (s, _ctx) = val.coerce_to_string()?;
2049                text.push_str(&s);
2050            }
2051        }
2052    }
2053    let resolved = match kind {
2054        // Relative path: resolve against the defining file's directory,
2055        // mirroring the plain `PathRel` branch.
2056        PathKind::Rel => {
2057            if let Some(dir) = current_eval_dir() {
2058                let norm = normalize_path(&dir.join(&text));
2059                // Lift cache→store exactly like the plain `PathRel` branch (the
2060                // store↔cache seam value-half). Without this, an interpolated
2061                // relative-path literal (`./${x}`, `./modules/${name}.nix`)
2062                // inside a fetched flake input yielded a Value::Path holding the
2063                // fetcher CACHE dir instead of the input's `/nix/store/<h>-source`
2064                // path — so its `toString`/copy-to-store/inputSrc diverged from
2065                // CppNix (the plain `./x` sibling already dematerializes; the two
2066                // must agree).
2067                crate::path::dematerialize(&norm).to_string_lossy().into_owned()
2068            } else {
2069                // No eval-file context (top-level `sui eval -E`): the
2070                // plain branch keeps the raw text, so match it — but the
2071                // interpolation is still spliced.
2072                text
2073            }
2074        }
2075        // Absolute paths: canonicalize the concatenated text CppNix's way.
2076        // The `${e}` splice routinely introduces a `//` seam (`/bar/` +
2077        // `/tmp/foo`) or a `.`/`..` component that must collapse
2078        // (`/bar//tmp/foo` → `/bar/tmp/foo`), and `..` must clamp at root.
2079        // `canon_abs` is filesystem-free (works on not-yet-materialized
2080        // flake paths) and root-aware (unlike `normalize_path`, which pops
2081        // past root — the marquee-root divergence).
2082        PathKind::Abs => crate::path::canon_abs(&text),
2083        // Home paths (`~/…`) carry a leading `~` component, so they are
2084        // not absolute-rooted; keep the pre-existing normalization.
2085        PathKind::Home => normalize_path(std::path::Path::new(&text))
2086            .to_string_lossy()
2087            .into_owned(),
2088    };
2089    Ok(Value::Path(Box::new(SmolStr::from(resolved.as_str()))))
2090}
2091
2092/// Which kind of interpolatable path literal — governs how the
2093/// concatenated text is finally resolved.
2094#[derive(Clone, Copy)]
2095enum PathKind {
2096    Abs,
2097    Rel,
2098    Home,
2099}
2100
2101/// Evaluate an attribute name, requiring non-null.
2102/// Use `eval_attr_maybe_null` when null dynamic attrs should be skipped.
2103fn eval_attr(attr: &ast::Attr, env: &Env) -> Result<String, EvalError> {
2104    eval_attr_maybe_null(attr, env)?
2105        .ok_or_else(|| EvalError::TypeError("null dynamic attribute name".into()))
2106}
2107
2108/// Evaluate an attribute name. Returns `None` for null dynamic attrs
2109/// (CppNix silently omits attributes with null names).
2110fn eval_attr_maybe_null(attr: &ast::Attr, env: &Env) -> Result<Option<String>, EvalError> {
2111    match attr {
2112        ast::Attr::Ident(ident) => Ok(Some(ident_text(ident))),
2113        ast::Attr::Dynamic(dyn_) => {
2114            let expr = dyn_
2115                .expr()
2116                .ok_or_else(|| EvalError::ParseError("dynamic attr missing expr".to_string()))?;
2117            let val = force_value(&eval_expr(&expr, env)?)?;
2118            // CppNix: null dynamic attr name → skip the attribute entirely.
2119            // Used by nixpkgs module system: `${if cond then null else "name"} = value;`
2120            if val == Value::Null {
2121                return Ok(None);
2122            }
2123            Ok(Some(val.as_string()?.to_string()))
2124        }
2125        ast::Attr::Str(s) => {
2126            let val = eval_str(s, env)?;
2127            Ok(Some(val.as_string()?.to_string()))
2128        }
2129    }
2130}
2131
2132/// Get the text of an rnix Ident node.
2133fn ident_text(ident: &ast::Ident) -> String {
2134    // Fast path: a `NODE_IDENT` holds a single `TOKEN_IDENT`, whose `text()`
2135    // borrows the source `&str` directly from the green node — no
2136    // `PreorderWithTokens` cursor tree-walk and none of the `NodeData::new`
2137    // allocations that `syntax().text()` (a `SyntaxText` over the node's whole
2138    // descendant span) pays. Byte-identical fallback: the identifier `or` is
2139    // lexed as a nested `TOKEN_OR` (rnix quirk), so `ident_token()` is `None`
2140    // there — walk the full node text in that case, exactly as before.
2141    match ident.ident_token() {
2142        Some(tok) => tok.text().to_string(),
2143        None => ident.syntax().text().to_string(),
2144    }
2145}
2146
2147/// Byte offset of a STATIC attr key (`Ident` or `Str`) in its source text —
2148/// the position `builtins.unsafeGetAttrPos` reports for that key. Returns
2149/// `None` for a dynamic key (`${e}`), which has no fixed source position.
2150///
2151/// CppNix points a binding's position at the KEY token's start; rnix exposes
2152/// it via the syntax node's `text_range().start()`.
2153fn static_attr_offset(attr: &ast::Attr) -> Option<u32> {
2154    let node = match attr {
2155        ast::Attr::Ident(i) => i.syntax(),
2156        ast::Attr::Str(s) => s.syntax(),
2157        ast::Attr::Dynamic(_) => return None,
2158    };
2159    Some(u32::from(node.text_range().start()))
2160}
2161
2162/// Collect a literal attrset's static top-level KEY offsets into an
2163/// [`crate::pos::AttrPositions`] and attach it to `attrs` (behind the value's
2164/// `Rc<AttrPositions>` slot). Records only single-key static bindings — the
2165/// shape `attrTag`'s `tags_` (`{ app = …; file = …; }`) is built from and the
2166/// only shape `builtins.unsafeGetAttrPos` reads in nixpkgs. `None`-costs a
2167/// pointer when the set has no such keys (attaches nothing).
2168fn attach_attrset_positions(set: &ast::AttrSet, attrs: &mut NixAttrs, env: &Env) {
2169    // The FILE is the one the literal is being built in — from the eval-file
2170    // stack, which a thunk restores to its captured file when it forces. This
2171    // is correct under laziness: a `dock.nix` attrset literal forced later
2172    // records `dock.nix`, not whatever file is top-of-stack at force time.
2173    // (`current_source_id`/`CURRENT_SOURCE_ID` is per-`eval_with_file`, NOT
2174    // per-env, so it would mis-attribute a lazily-forced literal.)
2175    let mut table = crate::pos::AttrPositions::new(current_eval_file());
2176    for entry in set.entries() {
2177        if let ast::Entry::AttrpathValue(apv) = entry {
2178            let Some(attrpath) = apv.attrpath() else { continue };
2179            let path_attrs: Vec<ast::Attr> = attrpath.attrs().collect();
2180            // Only a single-segment static key gets a position (a dotted path
2181            // `a.b = …` desugars to a nested set; CppNix points the position
2182            // at the head, and nixpkgs never `unsafeGetAttrPos`es a dotted
2183            // tag). Skip anything else.
2184            if path_attrs.len() != 1 {
2185                continue;
2186            }
2187            let Some(offset) = static_attr_offset(&path_attrs[0]) else { continue };
2188            // Resolve the static key name (Ident/Str) — never forces (a
2189            // dynamic key already returned None above).
2190            if let Ok(Some(name)) = eval_attr_maybe_null(&path_attrs[0], env) {
2191                table.insert(intern(&name), offset);
2192            }
2193        }
2194    }
2195    if !table.is_empty() {
2196        attrs.set_positions(std::rc::Rc::new(table));
2197    }
2198}
2199
2200fn eval_attrset(set: &ast::AttrSet, env: &Env) -> Result<Value, EvalError> {
2201    crate::perf::inc(crate::perf::Counter::Attrset);
2202    let mut attrs = NixAttrs::new();
2203    let is_rec = set.rec_token().is_some();
2204
2205    if is_rec {
2206        let mut rec_env = env.child();
2207        let mut thunks: Vec<(String, Thunk)> = Vec::new();
2208
2209        // Track which names have been defined so far in this scope.
2210        // Used by maybe_thunk to resolve backward references directly
2211        // instead of creating wasteful thunks.
2212        let mut defined_so_far: HashSet<String> = HashSet::new();
2213
2214        // Accumulator for dotted-path bindings (`rec { a.b = 1; a.c = 2; ... }`).
2215        // Leaf values are wrapped in thunks so they participate in the
2216        // recursive env fixpoint, matching CppNix semantics where
2217        // `rec { types.a = f 1; f = x: x + 1; }` allows `f` to be a
2218        // sibling binding.
2219        let mut dotted_attrs: NixAttrs = NixAttrs::new();
2220
2221        // Phase 1: Create thunks with placeholder env and bind them.
2222        for entry in set.entries() {
2223            match entry {
2224                ast::Entry::AttrpathValue(apv) => {
2225                    let attrpath = apv.attrpath().ok_or_else(|| {
2226                        EvalError::ParseError("binding missing attrpath".to_string())
2227                    })?;
2228                    let value_expr = apv.value().ok_or_else(|| {
2229                        EvalError::ParseError("binding missing value".to_string())
2230                    })?;
2231                    let mut path_keys: Vec<String> = attrpath
2232                        .attrs()
2233                        .filter_map(|a| eval_attr_maybe_null(&a, env).transpose())
2234                        .collect::<Result<_, _>>()?;
2235                    // Null dynamic attr name → skip entire binding (CppNix compat)
2236                    if path_keys.is_empty() { continue; }
2237                    if path_keys.len() == 1 {
2238                        let key = path_keys.pop().unwrap();
2239                        // Self-recursive detection in a `rec { … }` scope:
2240                        // any binding whose value-expr references the
2241                        // bound name OR any sibling key declared in this
2242                        // rec scope is potentially self-recursive (the
2243                        // siblings' thunks share the rec_env via Phase 2).
2244                        // Mark as recursive so inner re-entrance during
2245                        // force returns a Promise sentinel instead of
2246                        // erroring with InfiniteRecursion.
2247                        //
2248                        // For simplicity we check `key` and all already-
2249                        // defined siblings; siblings defined later are
2250                        // covered when THEIR thunks force (they reference
2251                        // back into this rec scope via Phase 2's env update).
2252                        // O(N) not O(N²): one memoized referenced-name set,
2253                        // intersected with key + already-defined siblings.
2254                        // Byte-identical to the prior per-name walks.
2255                        let referenced = referenced_idents(&value_expr);
2256                        let is_recursive_binding = referenced.contains(key.as_str())
2257                            || defined_so_far
2258                                .iter()
2259                                .any(|n| referenced.contains(n.as_str()));
2260                        let value = if is_recursive_binding {
2261                            Value::Thunk(Thunk::new_suspended_recursive(
2262                                value_expr.clone(),
2263                                env.clone(),
2264                            ))
2265                        } else {
2266                            // maybeThunk: skip thunk for trivial exprs.
2267                            // is_rec=true because rec attrset bindings
2268                            // can reference each other.
2269                            // Pass defined_so_far so backward refs
2270                            // resolve directly.
2271                            maybe_thunk(&value_expr, env, true, Some(&defined_so_far))
2272                        };
2273                        rec_env.bind(key.clone(), value.clone());
2274                        attrs.insert(key.clone(), value.clone());
2275                        if let Value::Thunk(t) = &value {
2276                            thunks.push((key.clone(), t.clone()));
2277                        }
2278                        defined_so_far.insert(key);
2279                    } else {
2280                        // Multi-segment dotted path: build a nested attrset
2281                        // with a thunk at the leaf so the value expression
2282                        // can reference sibling rec-bindings.
2283                        let key = path_keys[0].clone();
2284                        let value =
2285                            build_nested_attr_thunk(&path_keys[1..], &value_expr, env, &mut thunks);
2286                        merge_nested_insert(&mut dotted_attrs, key, value);
2287                    }
2288                }
2289                ast::Entry::Inherit(inherit) => {
2290                    eval_inherit(&inherit, env, &mut attrs, Some(&mut rec_env), Some(&mut thunks))?;
2291                }
2292            }
2293        }
2294
2295        // Phase 1b: Bind accumulated dotted-path attrs into attrs and rec_env.
2296        // Note: CppNix rejects `inherit (src) x; x.y = ...;` as a
2297        // duplicate definition, so we do not attempt to merge with
2298        // existing inherit thunks — just bind directly.
2299        for (key, value) in dotted_attrs.iter() {
2300            attrs.insert(key.clone(), value.clone());
2301            rec_env.bind(key.clone(), value.clone());
2302        }
2303
2304        // Phase 2: Update all thunks (both Suspended and InheritSelect)
2305        // to capture the final rec_env (which now has all names bound).
2306        for (_key, thunk) in &thunks {
2307            thunk.update_env(&rec_env);
2308        }
2309    } else {
2310        for entry in set.entries() {
2311            match entry {
2312                ast::Entry::AttrpathValue(apv) => {
2313                    let attrpath = apv.attrpath().ok_or_else(|| {
2314                        EvalError::ParseError("binding missing attrpath".to_string())
2315                    })?;
2316                    let value_expr = apv.value().ok_or_else(|| {
2317                        EvalError::ParseError("binding missing value".to_string())
2318                    })?;
2319                    let path_attrs: Vec<ast::Attr> = attrpath.attrs().collect();
2320                    // CppNix defers a dynamic key that is NOT at the HEAD of the
2321                    // attrpath: `{ a.${e} = v; }` builds `{ a = <thunk {${e}=v}>; }`,
2322                    // so `e` never forces until `.a` is demanded. Evaluating the
2323                    // whole path eagerly would force `e` at construction and — in
2324                    // the module-system fixpoint — read `config.<x>` while `config`
2325                    // is mid-force (the M2.6 divergence: `homes.null` instead of
2326                    // `homes.<name>`). Only the head is eager; a lone dynamic tail
2327                    // becomes a deferred thunk. A rarer collision under the same
2328                    // head stays eager (forced) so static deep-merge still works.
2329                    let tail_is_dynamic =
2330                        path_attrs.len() > 1 && attrs_have_dynamic(&path_attrs[1..]);
2331                    let head_key = match eval_attr_maybe_null(&path_attrs[0], env)? {
2332                        Some(k) => k,
2333                        // Null dynamic HEAD attr name → skip entire binding.
2334                        None => continue,
2335                    };
2336                    if tail_is_dynamic && attrs.get(&head_key).is_none() {
2337                        let value =
2338                            build_deferred_tail_attr(&path_attrs[1..], &value_expr, env);
2339                        attrs.insert(head_key, value);
2340                        continue;
2341                    }
2342                    // M2.6 ROOT #3 (collision case): the tail has a dynamic key
2343                    // AND the head already exists (a sibling binding wrote it,
2344                    // e.g. osquery's `systemd.services.… = …` then
2345                    // `systemd.tmpfiles.settings."10-osquery".${dirname …}.d`).
2346                    // The plain deferral above bails (head present), and the
2347                    // eager path below would force the dynamic key at
2348                    // construction — re-reading `config.<x>` mid-fixpoint →
2349                    // the empty-Promise partial. Instead, descend the existing
2350                    // head along the tail's STATIC prefix and splice a DEFERRED
2351                    // thunk at the first dynamic level, so the dynamic key
2352                    // stays lazy exactly as CppNix's nested-literal desugaring
2353                    // does — while preserving the static deep-merge with the
2354                    // sibling binding.
2355                    if tail_is_dynamic {
2356                        if let Some(existing) = attrs.get(&head_key).cloned() {
2357                            let merged = merge_deferred_dynamic_tail(
2358                                existing,
2359                                &path_attrs[1..],
2360                                &value_expr,
2361                                env,
2362                            )?;
2363                            attrs.insert(head_key, merged);
2364                            continue;
2365                        }
2366                    }
2367                    // Eager path: evaluate the remaining (static, or collision)
2368                    // keys now. A null dynamic tail key skips the binding.
2369                    let mut path_keys: Vec<String> = {
2370                        let mut v = Vec::with_capacity(path_attrs.len());
2371                        v.push(head_key);
2372                        let mut skip = false;
2373                        for a in &path_attrs[1..] {
2374                            match eval_attr_maybe_null(a, env)? {
2375                                Some(k) => v.push(k),
2376                                None => { skip = true; break; }
2377                            }
2378                        }
2379                        if skip { v.clear(); }
2380                        v
2381                    };
2382                    // Null dynamic attr name → skip entire binding (CppNix compat)
2383                    if path_keys.is_empty() { continue; }
2384                    if path_keys.len() == 1 {
2385                        let key = path_keys.pop().unwrap();
2386                        // maybeThunk: skip thunk for trivial exprs.
2387                        // is_rec=false — Ident lookups are safe.
2388                        let value = maybe_thunk(&value_expr, env, false, None);
2389                        // CppNix desugars `a.b = x; a = { c = y; };` into a single
2390                        // merged `a = { b = x; c = y; }` at parse time. rnix keeps
2391                        // the two bindings separate, so when a single-key binding
2392                        // collides with an already-built (dotted) attrs for the
2393                        // same key, deep-MERGE instead of overwrite. Force the RHS
2394                        // to WHNF so merge_nested_insert (which needs concrete
2395                        // Value::Attrs on both sides) can merge — forcing an
2396                        // attrset to WHNF does NOT force its fields, so leaf values
2397                        // stay lazy. Only fires on collision; non-colliding
2398                        // single-key bindings keep the plain fast insert.
2399                        // (This is the pkg-config-wrapper `env.addFlags` drop:
2400                        // `env.addFlags = …` then `env = { wrapperName = …; … }`.)
2401                        if matches!(attrs.get(&key), Some(Value::Attrs(_))) {
2402                            let forced = force_value(&value)?;
2403                            merge_nested_insert(&mut attrs, key, forced);
2404                        } else {
2405                            attrs.insert(key, value);
2406                        }
2407                    } else {
2408                        let key = path_keys[0].clone();
2409                        let value = build_nested_attr(&path_keys[1..], &value_expr, env)?;
2410                        // CppNix desugars `a = { x = …; }; a.y = …;` into a
2411                        // single merged `a = { x = …; y = …; }`. When the
2412                        // full-set binding for `a` was inserted FIRST it is a
2413                        // lazy Thunk (attrset literals go through maybe_thunk),
2414                        // so merge_nested_insert — which only merges when the
2415                        // existing value is a concrete Value::Attrs — would
2416                        // NOT see the earlier keys and would overwrite `a`
2417                        // with just `{ y = … }`, silently dropping `x`. Force
2418                        // the existing entry to WHNF on collision so the merge
2419                        // sees the concrete attrs (forcing to WHNF does not
2420                        // force the fields, so leaf laziness is preserved).
2421                        // (This is the gst-plugins-base `passthru.waylandEnabled`
2422                        // drop: `passthru = { … }; passthru.tests.x = …;`.)
2423                        if matches!(attrs.get(&key), Some(Value::Thunk(_))) {
2424                            let existing = attrs.get(&key).cloned().unwrap();
2425                            let forced = force_value(&existing)?;
2426                            attrs.insert(key.clone(), forced);
2427                        }
2428                        merge_nested_insert(&mut attrs, key, value);
2429                    }
2430                }
2431                ast::Entry::Inherit(inherit) => {
2432                    eval_inherit(&inherit, env, &mut attrs, None, None)?;
2433                }
2434            }
2435        }
2436    }
2437
2438    // Record the literal's static-key source positions for
2439    // `builtins.unsafeGetAttrPos` (the `attrTag` `declarations` — options.json
2440    // dock root). Cheap: one entry walk over static Ident/Str keys, no
2441    // forcing; attaches nothing (a pointer-sized `None`) when the set has no
2442    // single-static-key bindings.
2443    attach_attrset_positions(set, &mut attrs, env);
2444
2445    Ok(Value::Attrs(Rc::new(attrs)))
2446}
2447
2448fn eval_inherit(
2449    inherit: &ast::Inherit,
2450    env: &Env,
2451    attrs: &mut NixAttrs,
2452    bind_env: Option<&mut Env>,
2453    mut thunks: Option<&mut Vec<(String, Thunk)>>,
2454) -> Result<(), EvalError> {
2455    if let Some(from) = inherit.from() {
2456        // inherit (expr) a b c;
2457        //
2458        // The source expression must NOT be eagerly evaluated. nixpkgs
2459        // `lib/trivial.nix` has `inherit (lib.trivial) isFunction ...`
2460        // at the top of a file that itself defines `lib.trivial`. If
2461        // we eagerly force `lib.trivial`, we hit a self-referential
2462        // thunk blackhole. Instead: build a thunk per inherited
2463        // name that, when forced, evaluates the source and pulls
2464        // out that one attribute. This is what real Nix does.
2465        //
2466        // For `rec { inherit (X) name; ...; foo = name; }` we ALSO
2467        // need to bind the name in the enclosing rec env so the
2468        // sibling `foo = name` can reference it. The caller passes
2469        // its rec env in `bind_env`.
2470        //
2471        // When `thunks` is provided (rec attrsets), InheritSelect
2472        // thunks are collected so Phase 2 can update their captured
2473        // env to the full recursive scope. Without this, the source
2474        // expression cannot reference sibling bindings.
2475        let source_expr = from
2476            .expr()
2477            .ok_or_else(|| EvalError::ParseError("inherit from missing expr".to_string()))?;
2478        // Shared source thunk — all inherited names share one source
2479        // evaluation (the source thunk's own memoization ensures at
2480        // most one evaluation).
2481        let source_thunk = Thunk::new_suspended(source_expr, env.clone());
2482        let mut be = bind_env;
2483        for attr in inherit.attrs() {
2484            let name = eval_attr(&attr, env)?;
2485            let thunk = Thunk::new_inherit_select(source_thunk.clone(), name.clone());
2486            let value = Value::Thunk(thunk.clone());
2487            attrs.insert(name.clone(), value.clone());
2488            if let Some(ref mut e) = be {
2489                e.bind(name.clone(), value);
2490            }
2491            if let Some(ref mut t) = thunks {
2492                t.push((name, thunk));
2493            }
2494        }
2495    } else {
2496        // inherit a b c;
2497        //
2498        // CppNix resolves a bare `inherit x;` LAZILY, exactly like a plain
2499        // reference to `x` — it does NOT eagerly force the enclosing scope.
2500        // This matters when `x` is provided only by an enclosing `with`
2501        // scope whose value is a fixpoint still being constructed (a
2502        // blackhole): eager `env.lookup` returns None → spurious
2503        // `UndefinedVar`. nixpkgs `all-packages.nix` is
2504        // `… with pkgs; { nettle = import … { inherit callPackage; }; }`,
2505        // so `inherit callPackage` must resolve `callPackage` from the
2506        // `with pkgs` scope AT FORCE TIME, not eagerly at attrset
2507        // construction. Mirror `maybe_thunk`'s Ident path: try the fast
2508        // lookup, and on a miss defer to a WithIdent thunk (or a suspended
2509        // env lookup) so the resolution happens lazily against the settled
2510        // scope. (This was the `nettle` UndefinedVar('callPackage') drop.)
2511        let mut be = bind_env;
2512        for attr in inherit.attrs() {
2513            let name = eval_attr(&attr, env)?;
2514            let sym = crate::value::intern(&name);
2515            let value = if let Some(v) = env.lookup_fast(sym, &name) {
2516                v
2517            } else if let Some((scope_cache, scope_value)) =
2518                env.innermost_with_scope()
2519            {
2520                Value::Thunk(Thunk::new_with_ident(
2521                    SmolStr::from(name.as_str()),
2522                    scope_cache,
2523                    scope_value,
2524                    env.clone(),
2525                ))
2526            } else {
2527                return Err(EvalError::UndefinedVar(format!(
2528                    "'{name}'{}",
2529                    eval_file_ctx()
2530                )));
2531            };
2532            attrs.insert(name.clone(), value.clone());
2533            if let Some(ref mut e) = be {
2534                e.bind(name, value);
2535            }
2536        }
2537    }
2538    Ok(())
2539}
2540
2541fn build_nested_attr(
2542    path: &[String],
2543    expr: &ast::Expr,
2544    env: &Env,
2545) -> Result<Value, EvalError> {
2546    if path.is_empty() {
2547        // CRITICAL: Wrap leaf in a thunk instead of eagerly evaluating.
2548        // For dotted paths like `config.warnings = optionals config.x [...]`,
2549        // the leaf expression must be lazy — eagerly evaluating it during
2550        // attrset construction forces fixpoint thunks prematurely.
2551        return Ok(maybe_thunk(expr, env, false, None));
2552    }
2553    let key = path[0].clone();
2554    let inner = build_nested_attr(&path[1..], expr, env)?;
2555    let mut attrs = NixAttrs::new();
2556    attrs.insert(key, inner);
2557    Ok(Value::Attrs(Rc::new(attrs)))
2558}
2559
2560/// True if a single attr is a DYNAMIC key — one whose resolution runs
2561/// arbitrary expression code and therefore must not be forced at
2562/// attrset-construction time.
2563///
2564/// Two forms are dynamic:
2565///   * `ast::Attr::Dynamic` — a bare `${e}` antiquotation.
2566///   * `ast::Attr::Str` **containing an interpolation** — an interpolated
2567///     string key like `"iwd/${nm}"`.  A `Str` with NO interpolation
2568///     (`"foo bar"`) is a plain static string literal and is NOT dynamic.
2569///
2570/// M2.6 ROOT #3: `attrs_have_dynamic` previously matched ONLY
2571/// `Attr::Dynamic`, so an interpolated-string tail key (`config.a."p${e}"`)
2572/// fell to the eager path and forced `e` at construction.  In the module
2573/// system that forces a `config.<x>` read while `config` is mid-fixpoint
2574/// (`environment.etc."iwd/${configFile.name}"`, where `configFile` reads
2575/// `with config.networking.networkmanager`), yielding the empty-Promise
2576/// partial → the `set/null` softening.  Treating an interpolated `Str` as
2577/// dynamic routes it through the same per-level deferral as `${e}`
2578/// (ROOT #1/#2), so `e` forces only when the enclosing head is demanded —
2579/// exactly CppNix's nested-attrset-literal desugaring.
2580fn attr_is_dynamic(attr: &ast::Attr) -> bool {
2581    match attr {
2582        ast::Attr::Dynamic(_) => true,
2583        // A string attr key is dynamic iff it has ≥1 interpolation part;
2584        // a purely-literal string key forces nothing and stays eager.
2585        ast::Attr::Str(s) => s
2586            .normalized_parts()
2587            .iter()
2588            .any(|p| matches!(p, InterpolPart::Interpolation(_))),
2589        ast::Attr::Ident(_) => false,
2590    }
2591}
2592
2593/// True if any attr in the slice is a dynamic (interpolated) key.
2594///
2595/// A dynamic key beyond the HEAD of an attrpath must NOT be evaluated at
2596/// attrset-construction time — CppNix defers it inside the head's lazy
2597/// value, so `{ a.${e} = v; }` never forces `e` until `.a` is demanded.
2598/// Static string/ident keys are cheap and force nothing, so they don't
2599/// need deferral.
2600fn attrs_have_dynamic(attrs: &[ast::Attr]) -> bool {
2601    attrs.iter().any(attr_is_dynamic)
2602}
2603
2604/// Build the nested attrset for the TAIL of an attrpath, deferring
2605/// evaluation of dynamic tail keys until the value is forced.
2606///
2607/// Given tail attrs `[b, ${e}, c]` and a value expr, produce a lazy
2608/// `Value::Thunk` that, when forced, evaluates each tail key (including
2609/// the dynamic `${e}`) against `env` and builds `{ b = { ${e} = { c =
2610/// <leaf-thunk> }; }; }`. This mirrors CppNix: the inner attrset (and
2611/// thus its dynamic keys) is constructed only when the enclosing head
2612/// attribute is demanded — never at construction of the outer attrset.
2613///
2614/// A dynamic key that evaluates to `null` skips the whole binding
2615/// (returns an empty attrset), matching CppNix's null-dynamic-attr rule.
2616fn build_deferred_tail_attr(
2617    tail: &[ast::Attr],
2618    value_expr: &ast::Expr,
2619    env: &Env,
2620) -> Value {
2621    let tail: Vec<ast::Attr> = tail.to_vec();
2622    let value_expr = value_expr.clone();
2623    let env = env.clone();
2624    Value::Thunk(Thunk::new_native(move || {
2625        build_tail_attrs_now(&tail, &value_expr, &env)
2626    }))
2627}
2628
2629/// Resolve ONE level of the deferred attrpath tail — used from inside
2630/// the deferred thunk above once the enclosing head is demanded.
2631///
2632/// M2.6 ROOT #2 (the OVER-FORCE fix): this resolves *only* `tail[0]`'s
2633/// key and wraps the remaining tail `tail[1..]` in another DEFERRED
2634/// thunk — it does NOT recurse eagerly through the whole tail. This is
2635/// exactly CppNix's desugaring of `a.b.c = v` into nested attrset
2636/// literals `a = { b = { c = v; }; }`, where forcing `a` to WHNF yields
2637/// `{ b = <thunk {c=v}> }` — the inner level (`b`, and any dynamic key
2638/// under it) stays lazy until `.b` is demanded.
2639///
2640/// Forcing the enclosing head therefore resolves ONE tail key, never
2641/// the whole chain: `config.homes.${cfg.pleme.userName} = 7` demanded
2642/// as `config` yields `{ homes = <deferred> }` WITHOUT forcing the
2643/// `${cfg.pleme.userName}` key. The prior implementation recursed the
2644/// whole tail eagerly, forcing that dynamic key while only `.config`
2645/// (or its `._type`) was demanded — the over-force cppnix never does.
2646///
2647/// A dynamic key that evaluates to `null` skips the whole binding
2648/// (returns an empty attrset), matching CppNix's null-dynamic-attr rule.
2649fn build_tail_attrs_now(
2650    tail: &[ast::Attr],
2651    value_expr: &ast::Expr,
2652    env: &Env,
2653) -> Result<Value, EvalError> {
2654    if tail.is_empty() {
2655        return Ok(maybe_thunk(value_expr, env, false, None));
2656    }
2657    if std::env::var_os("SUI_M26_TAILTRACE").is_some() {
2658        let t: String = tail[0].syntax().text().to_string().chars().take(40).collect();
2659        eprintln!("[M26 TAIL-RESOLVE] forcing dynamic tail key `{t}`");
2660        if attrs_have_dynamic(&tail[..1]) {
2661            crate::trace::dump_force_stack_ids();
2662        }
2663    }
2664    let key = match eval_attr_maybe_null(&tail[0], env)? {
2665        Some(k) => k,
2666        // Null dynamic key → the whole binding is skipped; an empty
2667        // attrset is the identity for merge_nested_insert.
2668        None => return Ok(Value::Attrs(Rc::new(NixAttrs::new()))),
2669    };
2670    // Resolve ONE level: if more tail remains, defer it (a new lazy
2671    // thunk) rather than recursing eagerly. Only the leaf (empty tail)
2672    // is built here. This keeps each nested level lazy, exactly like
2673    // CppNix's nested-attrset-literal desugaring — so forcing this
2674    // level does NOT force the next level's (possibly dynamic) key.
2675    let inner = if tail.len() == 1 {
2676        maybe_thunk(value_expr, env, false, None)
2677    } else {
2678        build_deferred_tail_attr(&tail[1..], value_expr, env)
2679    };
2680    let mut attrs = NixAttrs::new();
2681    attrs.insert(key, inner);
2682    Ok(Value::Attrs(Rc::new(attrs)))
2683}
2684
2685/// M2.6 ROOT #3 (collision case): splice a DEFERRED dynamic-tail binding
2686/// into an ALREADY-PRESENT head value without forcing the dynamic key.
2687///
2688/// `existing` is the value already stored at the attrpath's head (written
2689/// by a sibling binding — e.g. `systemd.services.… = …`). `tail` is the
2690/// remaining attrpath (`path_attrs[1..]`) of the new binding, which
2691/// contains ≥1 dynamic attr (`systemd.tmpfiles.….${dirname …}.d`).
2692///
2693/// We descend `existing` along the LONGEST STATIC PREFIX of `tail`
2694/// (`tmpfiles`, `settings`, `"10-osquery"` — all static, forced-free
2695/// keys), forcing each already-present sub-attrset to WHNF so the merge
2696/// sees concrete keys (forcing to WHNF never forces leaf VALUES, so leaf
2697/// laziness is preserved), and at the first DYNAMIC level splice a
2698/// `build_deferred_tail_attr` thunk. The dynamic key therefore forces
2699/// only when that exact nested path is later demanded — CppNix's
2700/// nested-attrset-literal desugaring, now honoured through a sibling
2701/// collision too.
2702fn merge_deferred_dynamic_tail(
2703    existing: Value,
2704    tail: &[ast::Attr],
2705    value_expr: &ast::Expr,
2706    env: &Env,
2707) -> Result<Value, EvalError> {
2708    // `tail` is non-empty and contains a dynamic attr somewhere (the
2709    // caller guarantees `attrs_have_dynamic(tail)`).
2710    debug_assert!(!tail.is_empty());
2711
2712    // If the FIRST tail attr is itself dynamic, there is no static prefix
2713    // to descend — the whole tail is deferred and merged as a lazy
2714    // overlay onto the existing head (a `//`-style right-merge; the
2715    // deferred attrset only materialises its dynamic key on demand).
2716    if attr_is_dynamic(&tail[0]) {
2717        let deferred = build_deferred_tail_attr(tail, value_expr, env);
2718        return Ok(lazy_overlay_merge(existing, deferred));
2719    }
2720
2721    // The head static key of `tail`. Resolve it (static → forces nothing
2722    // relevant; a null dynamic can't occur here since tail[0] is static).
2723    let key = match eval_attr_maybe_null(&tail[0], env)? {
2724        Some(k) => k,
2725        None => return Ok(existing),
2726    };
2727
2728    // Force the existing head to a concrete attrset so we can descend +
2729    // merge on the resolved static key. Forcing to WHNF does NOT force
2730    // its field VALUES, so leaf laziness is preserved.
2731    let existing_forced = force_value(&existing)?;
2732    let mut base = match existing_forced {
2733        Value::Attrs(a) => (*a).clone(),
2734        // The existing head is not an attrset (a sibling wrote a leaf
2735        // here); CppNix would error on the merge, but to stay lazy we
2736        // defer the tail and let a later demand surface the real merge
2737        // conflict. Build the deferred tail as a fresh attrset.
2738        _ => {
2739            let deferred = build_deferred_tail_attr(tail, value_expr, env);
2740            return Ok(deferred);
2741        }
2742    };
2743
2744    // Recurse: merge the REMAINING tail (`tail[1..]`) under `key`.
2745    let child_existing = base.get(&key).cloned();
2746    let new_child = match child_existing {
2747        Some(child) if tail.len() > 1 => {
2748            // Deeper static/dynamic prefix under an existing sub-attrset.
2749            merge_deferred_dynamic_tail(child, &tail[1..], value_expr, env)?
2750        }
2751        Some(child) => {
2752            // tail == [key]; the leaf collides with an existing value.
2753            // Static leaf collision — build the leaf and lazy-merge.
2754            let leaf = maybe_thunk(value_expr, env, false, None);
2755            lazy_overlay_merge(child, leaf)
2756        }
2757        None if tail.len() > 1 => {
2758            // No existing child; the remaining tail may itself start with
2759            // a dynamic key — defer it whole (build_deferred_tail_attr
2760            // handles the static/dynamic split per-level).
2761            build_deferred_tail_attr(&tail[1..], value_expr, env)
2762        }
2763        None => maybe_thunk(value_expr, env, false, None),
2764    };
2765    base.insert(key, new_child);
2766    Ok(Value::Attrs(Rc::new(base)))
2767}
2768
2769/// Lazy right-merge of two values that are (or will force to) attrsets,
2770/// preserving leaf laziness. Used by [`merge_deferred_dynamic_tail`] to
2771/// combine a deferred dynamic-tail attrset with an existing value without
2772/// forcing either's dynamic keys eagerly. When both are concrete attrs we
2773/// deep-merge in place (reusing [`merge_nested_insert`]); otherwise we
2774/// build a lazy overlay thunk that merges on demand.
2775fn lazy_overlay_merge(left: Value, right: Value) -> Value {
2776    match (&left, &right) {
2777        (Value::Attrs(la), Value::Attrs(_)) => {
2778            crate::perf::inc(crate::perf::Counter::SlashDeferredTailClone);
2779            let mut merged = (**la).clone();
2780            if let Value::Attrs(ra) = &right {
2781                // Merging distinct override keys into `merged` is order-
2782                // independent (per-key right-wins), and the result map is
2783                // unordered storage — the sorted `iter()` was dead work.
2784                for (k, v) in ra.iter_unsorted() {
2785                    merge_nested_insert(&mut merged, k.clone(), v.clone());
2786                }
2787            }
2788            Value::Attrs(Rc::new(merged))
2789        }
2790        _ => {
2791            // At least one side is a thunk (a deferred dynamic tail).
2792            // Defer the merge behind a Native thunk so neither side's
2793            // dynamic key forces until the merged attrset is demanded.
2794            Value::Thunk(Thunk::new_native(move || {
2795                let lf = force_value(&left)?;
2796                let rf = force_value(&right)?;
2797                let la = lf.as_attrs()?;
2798                let ra = rf.as_attrs()?;
2799                crate::perf::inc(crate::perf::Counter::SlashDeferredTailClone);
2800                let mut merged = (*la).clone();
2801                for (k, v) in ra.iter_unsorted() {
2802                    merge_nested_insert(&mut merged, k.clone(), v.clone());
2803                }
2804                Ok(Value::Attrs(Rc::new(merged)))
2805            }))
2806        }
2807    }
2808}
2809
2810/// Like [`build_nested_attr`] but wraps the leaf in a [`Thunk`] instead of
2811/// eagerly evaluating it. Used inside `rec { ... }` and `let ... in` so
2812/// that dotted-path leaf expressions can reference sibling bindings
2813/// through the recursive env (which is finalised in Phase 2).
2814///
2815/// Every thunk created is appended to `thunks` so Phase 2 can update
2816/// its captured environment.
2817fn build_nested_attr_thunk(
2818    path: &[String],
2819    expr: &ast::Expr,
2820    env: &Env,
2821    thunks: &mut Vec<(String, Thunk)>,
2822) -> Value {
2823    if path.is_empty() {
2824        let thunk = Thunk::new_suspended(expr.clone(), env.clone());
2825        let val = Value::Thunk(thunk.clone());
2826        thunks.push((String::new(), thunk));
2827        return val;
2828    }
2829    let key = path[0].clone();
2830    let inner = build_nested_attr_thunk(&path[1..], expr, env, thunks);
2831    let mut attrs = NixAttrs::new();
2832    attrs.insert(key, inner);
2833    Value::Attrs(Rc::new(attrs))
2834}
2835
2836/// Insert `value` at `key` in `target`. If `target` already has a
2837/// concrete `Value::Attrs` at that key AND `value` is also a
2838/// concrete `Value::Attrs`, deep-merge them rather than overwriting.
2839/// This is what makes `{ a.b.c = 1; a.b.d = 2; a.e = 3; }` produce
2840/// `{ a = { b = { c = 1; d = 2; }; e = 3; }; }` instead of
2841/// dropping siblings — every nixpkgs module relies on this.
2842fn merge_nested_insert(target: &mut NixAttrs, key: String, value: Value) {
2843    // Fast path: no existing entry at this key → plain insert, keeping the
2844    // value lazy (the overwhelmingly common non-colliding case, so we never
2845    // force a thunk here).
2846    let existing = match target.get(&key) {
2847        Some(e) => e.clone(),
2848        None => {
2849            target.insert(key, value);
2850            return;
2851        }
2852    };
2853    // A collision exists.  A deep merge is warranted only when BOTH the
2854    // existing entry AND the new value are attrset-shaped.  M2.6 ROOT #4b
2855    // (byte-verified): either side may be a lazy `Thunk` wrapping a
2856    // full-set leaf — both dotted-path orderings hit this:
2857    //   forward  `o.a = { x = 1; }; o.a.y = 2;` → EXISTING `a` is a thunk
2858    //            (`build_nested_attr` puts the `{x=1}` leaf through
2859    //            `maybe_thunk`), NEW `a` is `{ y = … }`;
2860    //   reverse  `o.a.y = 2; o.a = { x = 1; };` → EXISTING `a` is `{y}`,
2861    //            NEW `a` is the `<thunk {x=1}>`.
2862    // The old `should_merge` required BOTH sides to already be concrete
2863    // `Value::Attrs`, so a Thunk-vs-Attrs collision fell to the overwrite
2864    // path and silently dropped the earlier leaf's keys.  cppnix desugars
2865    // BOTH orderings into one merged `o.a = { x = 1; y = 2; }`.  Force each
2866    // side's thunk to WHNF ON COLLISION ONLY (forcing an attrset to WHNF
2867    // does NOT force its fields, so leaf laziness is preserved); a thunk
2868    // that forces to a non-attrset (or errors) makes the merge a plain
2869    // overwrite (leaf last-write-wins).
2870    // Symptom this closes: nixpkgs' alsa module declares
2871    // `options.hardware.alsa = { enable = …; cardAliases = …; … }` AND
2872    // `options.hardware.alsa.enablePersistence = …`; sui merged them to
2873    // only `{enablePersistence}`, so `hardware.alsa.cardAliases` "does not
2874    // exist" — the M2.6 frontier once the `with`-namespace over-force (#4a)
2875    // was fixed.
2876    let value = match value {
2877        Value::Thunk(_) => match force_value(&value) {
2878            Ok(v @ Value::Attrs(_)) => v,
2879            _ => value,
2880        },
2881        other => other,
2882    };
2883    if !matches!(value, Value::Attrs(_)) {
2884        target.insert(key, value);
2885        return;
2886    }
2887    // Normalize the existing side to concrete attrs too (forcing a thunk
2888    // to WHNF if needed); if it isn't attrset-shaped, the new attrs wins.
2889    let existing_concrete = match &existing {
2890        Value::Attrs(_) => existing.clone(),
2891        Value::Thunk(_) => match force_value(&existing) {
2892            Ok(v @ Value::Attrs(_)) => v,
2893            _ => {
2894                target.insert(key, value);
2895                return;
2896            }
2897        },
2898        _ => {
2899            target.insert(key, value);
2900            return;
2901        }
2902    };
2903    // Both sides are concrete attrs — merge in place. We pop the
2904    // existing entry, then walk the new attrs and recursively
2905    // merge each child onto it.
2906    let mut existing_attrs = match existing_concrete {
2907        Value::Attrs(a) => (*a).clone(),
2908        _ => unreachable!(),
2909    };
2910    let new_attrs = match value {
2911        Value::Attrs(ref a) => a,
2912        _ => unreachable!(),
2913    };
2914    for (k, v) in new_attrs.iter_unsorted() {
2915        merge_nested_insert(&mut existing_attrs, k.clone(), v.clone());
2916    }
2917    target.insert(key, Value::Attrs(Rc::new(existing_attrs)));
2918}
2919
2920/// Evaluate entries from any HasEntry node (LegacyLet).
2921fn eval_entries<N: HasEntry + AstNode>(node: &N, env: &mut Env) -> Result<(), EvalError> {
2922    for entry in node.entries() {
2923        match entry {
2924            ast::Entry::AttrpathValue(apv) => {
2925                let attrpath = apv.attrpath().ok_or_else(|| {
2926                    EvalError::ParseError("binding missing attrpath".to_string())
2927                })?;
2928                let value_expr = apv.value().ok_or_else(|| {
2929                    EvalError::ParseError("binding missing value".to_string())
2930                })?;
2931                let mut path_keys: Vec<String> = attrpath
2932                    .attrs()
2933                    .map(|a| eval_attr(&a, env))
2934                    .collect::<Result<_, _>>()?;
2935                if path_keys.len() == 1 {
2936                    let key = path_keys.pop().unwrap();
2937                    let value = eval_expr(&value_expr, env)?;
2938                    env.bind(key, value);
2939                }
2940                // Multi-key paths in let are not standard; skip for now.
2941            }
2942            ast::Entry::Inherit(inherit) => {
2943                if let Some(from) = inherit.from() {
2944                    let source_expr = from.expr().ok_or_else(|| {
2945                        EvalError::ParseError("inherit from missing expr".to_string())
2946                    })?;
2947                    let source = force_value(&eval_expr(&source_expr, env)?)?;
2948                    let source_attrs = source.as_attrs()?;
2949                    for attr in inherit.attrs() {
2950                        let name = eval_attr(&attr, env)?;
2951                        let value = source_attrs
2952                            .get(&name)
2953                            .cloned()
2954                            .ok_or_else(|| EvalError::AttrNotFound(
2955                                format!("'{name}' in inherit{}", eval_file_ctx()),
2956                            ))?;
2957                        env.bind(name, value);
2958                    }
2959                } else {
2960                    for attr in inherit.attrs() {
2961                        let name = eval_attr(&attr, env)?;
2962                        let value = env
2963                            .lookup(&name)
2964                            .ok_or_else(|| EvalError::UndefinedVar(
2965                                format!("'{name}'{}", eval_file_ctx()),
2966                            ))?;
2967                        env.bind(name, value);
2968                    }
2969                }
2970            }
2971        }
2972    }
2973    Ok(())
2974}
2975
2976fn eval_binop(
2977    op: ast::BinOpKind,
2978    lhs: &ast::Expr,
2979    rhs: &ast::Expr,
2980    env: &Env,
2981) -> Result<Value, EvalError> {
2982    // Short-circuit for && and ||
2983    match op {
2984        ast::BinOpKind::And => {
2985            let l = force_value(&eval_expr(lhs, env)?)?.as_bool()?;
2986            if !l {
2987                return Ok(Value::Bool(false));
2988            }
2989            return eval_expr(rhs, env);
2990        }
2991        ast::BinOpKind::Or => {
2992            let l = force_value(&eval_expr(lhs, env)?)?.as_bool()?;
2993            if l {
2994                return Ok(Value::Bool(true));
2995            }
2996            return eval_expr(rhs, env);
2997        }
2998        ast::BinOpKind::Implication => {
2999            let l = force_value(&eval_expr(lhs, env)?)?.as_bool()?;
3000            if !l {
3001                return Ok(Value::Bool(true));
3002            }
3003            return eval_expr(rhs, env);
3004        }
3005        _ => {}
3006    }
3007
3008    let lc = force_concrete(&eval_expr(lhs, env)?)?;
3009    let rc = force_concrete(&eval_expr(rhs, env)?)?;
3010    // Consume the Concretes (move, don't clone) so `l`/`r` hold the sole Rc to
3011    // any heap payload. This is byte-neutral — `into_value` yields the identical
3012    // `Value` as `to_value` — but it drops `lc`/`rc`, which is what lets the
3013    // `Concat` arm's structural-share fast path see a uniquely-owned left list
3014    // for a fresh `++` temporary (`Rc::try_unwrap` → append in place). Keeping
3015    // `lc` alive via `to_value` pinned the refcount at ≥2 and defeated reuse.
3016    let l = lc.into_value();
3017    let r = rc.into_value();
3018
3019    match op {
3020        ast::BinOpKind::Add => match (&l, &r) {
3021            (Value::Int(a), Value::Int(b)) => a
3022                .checked_add(*b)
3023                .map(Value::Int)
3024                .ok_or_else(|| int_overflow("adding", *a, '+', *b)),
3025            (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)),
3026            (Value::Int(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)),
3027            (Value::Float(a), Value::Int(b)) => Ok(Value::Float(a + *b as f64)),
3028            (Value::String(a), Value::String(b)) => {
3029                let mut ctx = a.context.clone();
3030                ctx.merge(&b.context);
3031                // Byte-identical to `format!("{}{}", a.chars, b.chars)` but
3032                // routes around the `core::fmt` runtime (its dispatch was the
3033                // #1 self-time frame on the string-concat hot path): a single
3034                // exact-capacity `String` + two `push_str` reserves the final
3035                // size once, so the left operand is copied exactly once instead
3036                // of copied-then-regrown. Result string + context unchanged →
3037                // ByteSufficient. (Also removes a `format!` — TYPED EMISSION.)
3038                let mut s = String::with_capacity(a.chars.len() + b.chars.len());
3039                s.push_str(&a.chars);
3040                s.push_str(&b.chars);
3041                Ok(Value::String(Rc::new(NixString::with_context(s, ctx))))
3042            }
3043            (Value::Path(a), Value::String(b)) => Ok(Value::Path(Box::new(SmolStr::from(format!("{a}{}", b.chars).as_str())))),
3044            (Value::Path(a), Value::Path(b)) => Ok(Value::Path(Box::new(SmolStr::from(format!("{a}/{b}").as_str())))),
3045            // CppNix coerces attrsets with outPath when used with +
3046            (Value::Attrs(_), _) | (_, Value::Attrs(_)) => {
3047                let (ls, lctx) = l.coerce_to_string()?;
3048                let (rs, rctx) = r.coerce_to_string()?;
3049                let mut ctx = lctx;
3050                ctx.merge(&rctx);
3051                Ok(Value::String(Rc::new(NixString::with_context(
3052                    format!("{ls}{rs}"),
3053                    ctx,
3054                ))))
3055            }
3056            _ => Err(EvalError::op_type("add", l.type_name(), r.type_name())),
3057        },
3058        ast::BinOpKind::Sub => num_op(
3059            &l,
3060            &r,
3061            |a, b| a.checked_sub(b),
3062            |a, b| a - b,
3063            |a, b| int_overflow("subtracting", a, '-', b),
3064        ),
3065        ast::BinOpKind::Mul => num_op(
3066            &l,
3067            &r,
3068            |a, b| a.checked_mul(b),
3069            |a, b| a * b,
3070            |a, b| int_overflow("multiplying", a, '*', b),
3071        ),
3072        ast::BinOpKind::Div => {
3073            // CppNix rejects division by zero for both int and float
3074            // operands; Rust's native int-div-by-0 panics (we handle
3075            // that below) but float-div-by-0 silently returns `inf`
3076            // or `NaN`, which sui was then serializing as `null` —
3077            // an invisible silent-Ok bug surfaced by the error-case
3078            // differential corpus.
3079            //
3080            // Cover every zero-denominator case explicitly.
3081            let rhs_is_zero = match &r {
3082                Value::Int(0) => true,
3083                Value::Float(f) => *f == 0.0,
3084                _ => false,
3085            };
3086            if rhs_is_zero {
3087                return Err(EvalError::DivisionByZero);
3088            }
3089            num_op(
3090                &l,
3091                &r,
3092                |a, b| a.checked_div(b),
3093                |a, b| a / b,
3094                |a, b| int_overflow("dividing", a, '/', b),
3095            )
3096        }
3097        ast::BinOpKind::Equal => Ok(Value::Bool(l == r)),
3098        ast::BinOpKind::NotEqual => Ok(Value::Bool(l != r)),
3099        ast::BinOpKind::Less => compare(&l, &r, |o| o == std::cmp::Ordering::Less),
3100        ast::BinOpKind::LessOrEq => compare(&l, &r, |o| o != std::cmp::Ordering::Greater),
3101        ast::BinOpKind::More => compare(&l, &r, |o| o == std::cmp::Ordering::Greater),
3102        ast::BinOpKind::MoreOrEq => compare(&l, &r, |o| o != std::cmp::Ordering::Less),
3103        ast::BinOpKind::Update => {
3104            let la = l.to_attrs()?;
3105            let ra = r.to_attrs()?;
3106            // O(1) lazy overlay — defers merge until attribute access.
3107            Ok(Value::Attrs(Rc::new(la.overlay(ra))))
3108        }
3109        ast::BinOpKind::Concat => {
3110            // Structural-share fast path: when the left operand's `Rc<Vec>` is
3111            // uniquely owned (a fresh temporary, as in a left-associative `++`
3112            // fold `acc ++ [x]`), append the right elements IN PLACE instead of
3113            // cloning the whole accumulator. This turns an O(n) copy per concat
3114            // into amortized O(1), byte-identically — the result is the same
3115            // ordered sequence of the same Rc-shared lazy thunks (no forcing,
3116            // no reordering, no identity change). When the Rc is shared (the
3117            // left came from a still-live binding/thunk) we fall back to the
3118            // clone-extend path, preserving the shared list unchanged.
3119            crate::value::concat_lists(l, r.as_list()?)
3120        }
3121        ast::BinOpKind::And | ast::BinOpKind::Or | ast::BinOpKind::Implication => {
3122            unreachable!("handled above")
3123        }
3124        ast::BinOpKind::PipeRight | ast::BinOpKind::PipeLeft => {
3125            Err(EvalError::NotImplemented("pipe operators".to_string()))
3126        }
3127    }
3128}
3129
3130/// CppNix aborts (uncatchably) on i64 arithmetic overflow, e.g.
3131/// `integer overflow in adding 9223372036854775807 + 1`. `EvalError::Abort` is
3132/// the uncatchable variant (`tryEval` catches only `Throw`/`AssertionFailed`),
3133/// matching nix — a wrapping result would silently produce a wrong drvPath.
3134#[inline]
3135fn int_overflow(verb: &str, a: i64, sym: char, b: i64) -> EvalError {
3136    EvalError::Abort(format!("integer overflow in {verb} {a} {sym} {b}"))
3137}
3138
3139fn num_op(
3140    l: &Value,
3141    r: &Value,
3142    int_op: impl Fn(i64, i64) -> Option<i64>,
3143    float_op: impl Fn(f64, f64) -> f64,
3144    overflow: impl Fn(i64, i64) -> EvalError,
3145) -> Result<Value, EvalError> {
3146    match (l, r) {
3147        (Value::Int(a), Value::Int(b)) => {
3148            int_op(*a, *b).map(Value::Int).ok_or_else(|| overflow(*a, *b))
3149        }
3150        (Value::Float(a), Value::Float(b)) => Ok(Value::Float(float_op(*a, *b))),
3151        (Value::Int(a), Value::Float(b)) => Ok(Value::Float(float_op(*a as f64, *b))),
3152        (Value::Float(a), Value::Int(b)) => Ok(Value::Float(float_op(*a, *b as f64))),
3153        _ => Err(EvalError::op_type("perform arithmetic on", l.type_name(), r.type_name())),
3154    }
3155}
3156
3157fn compare(
3158    l: &Value,
3159    r: &Value,
3160    pred: impl Fn(std::cmp::Ordering) -> bool,
3161) -> Result<Value, EvalError> {
3162    let ord = match (l, r) {
3163        (Value::Int(a), Value::Int(b)) => a.cmp(b),
3164        (Value::Float(a), Value::Float(b)) => {
3165            a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
3166        }
3167        (Value::Int(a), Value::Float(b)) => (*a as f64)
3168            .partial_cmp(b)
3169            .unwrap_or(std::cmp::Ordering::Equal),
3170        (Value::Float(a), Value::Int(b)) => a
3171            .partial_cmp(&(*b as f64))
3172            .unwrap_or(std::cmp::Ordering::Equal),
3173        (Value::String(a), Value::String(b)) => a.chars.cmp(&b.chars),
3174        _ => {
3175            return Err(EvalError::op_type("compare", l.type_name(), r.type_name()));
3176        }
3177    };
3178    Ok(Value::Bool(pred(ord)))
3179}
3180
3181/// Apply a function to an argument.
3182///
3183/// Supports `__functor`: if `func` is an attrset with a `__functor` key,
3184/// calls `__functor self arg` (the Nix `__functor` protocol).
3185///
3186/// For lambda with a simple ident parameter, the argument is NOT forced
3187/// before binding -- this enables fixpoint combinators (`lib.fix`) where
3188/// the argument is a self-referential thunk.
3189/// Apply a function and force the result.
3190///
3191/// Builtins that inspect the return value (via `as_list`, `as_bool`, etc.)
3192/// must use this instead of bare `apply` — otherwise a thunk-wrapped result
3193/// will cause "thunk in as_list: force first" errors.
3194pub fn apply_and_force(func: Value, arg: Value) -> Result<Value, EvalError> {
3195    force_value(&apply(func, arg)?)
3196}
3197
3198pub fn apply(func: Value, arg: Value) -> Result<Value, EvalError> {
3199    stacker::maybe_grow(64 * 1024, 2 * 1024 * 1024, || apply_inner(func, arg))
3200}
3201
3202fn apply_inner(func: Value, arg: Value) -> Result<Value, EvalError> {
3203    crate::perf::inc(crate::perf::Counter::Apply);
3204    let func = force_concrete(&func)?.into_value();
3205    match func {
3206        Value::Lambda(closure) => {
3207            // Hot function tracker: log source file + param name for each lambda call
3208            if crate::perf::enabled() {
3209                APPLY_SITES.with(|sites| {
3210                    let file = closure.env.eval_file()
3211                        .map(|p| p.display().to_string())
3212                        .unwrap_or_else(|| "<eval>".into());
3213                    // Include param info for identification
3214                    let param_name = match &closure.param {
3215                        rnix::ast::Param::IdentParam(ip) => ip.ident().map(|i| ident_text(&i)).unwrap_or_default(),
3216                        rnix::ast::Param::Pattern(pat) => {
3217                            let mut names: Vec<String> = pat.pat_entries()
3218                                .filter_map(|e| e.ident().map(|i| ident_text(&i)))
3219                                .take(3)
3220                                .collect();
3221                            if pat.pat_entries().count() > 3 { names.push("...".to_string()); }
3222                            format!("{{{}}}", names.join(","))
3223                        }
3224                    };
3225                    let key = format!("{}:{}", file.rsplit_once("-source/").map_or(file.as_str(), |(_,s)| s), param_name);
3226                    *sites.borrow_mut().entry(key).or_insert(0u64) += 1;
3227                });
3228            }
3229            let mut call_env = closure.env.child();
3230            let _file_guard = closure
3231                .env
3232                .eval_file()
3233                .cloned()
3234                .map(push_eval_file);
3235            // Push Nix-level trace frame for function calls. Lazy: stores
3236            // only the raw ingredients (O(1) Rc-clone of the closure env +
3237            // the current-eval-file snapshot) and defers the format!/strip
3238            // work to the cold `attach_trace` path. Renders byte-identical
3239            // to the eager form.
3240            let _trace = push_nix_trace_lambda(&closure.env);
3241            match &closure.param {
3242                rnix::ast::Param::IdentParam(_) => {
3243                    // Simple ident param: bind argument WITHOUT forcing.
3244                    // This is critical for fixpoint / call-by-need semantics.
3245                    bind_param(&closure.param, &arg, &mut call_env)?;
3246                }
3247                rnix::ast::Param::Pattern(_) => {
3248                    // Pattern param needs the arg to be an attrset, so force.
3249                    let forced_arg = force_concrete(&arg)?.into_value();
3250                    bind_param(&closure.param, &forced_arg, &mut call_env)?;
3251                }
3252            }
3253            eval_expr(&closure.body, &call_env)
3254        }
3255        Value::Builtin(b) => {
3256            let _trace = push_nix_trace(format!("while calling the '{}' builtin", b.name));
3257            // Special builtins that must receive UNFORCED arguments:
3258            // - tryEval: must catch throw/abort during its own forcing
3259            // - addErrorContext<partial>: wraps value with error context
3260            //   without forcing (the value is the fixpoint `config` which
3261            //   causes infinite recursion if forced during collectModules)
3262            // - seq<partial>: forces first arg but returns second UNFORCED
3263            if b.name == "tryEval"
3264                || b.name == "addErrorContext<partial>"
3265                || b.name == "seq<partial>"
3266                || b.name == "deepSeq<partial>"
3267            {
3268                (b.func)(&[arg])
3269            } else {
3270                let forced_arg = force_value(&arg)?;
3271                (b.func)(&[forced_arg])
3272            }
3273        }
3274        Value::Attrs(ref attrs) => {
3275            if let Some(functor) = attrs.get("__functor") {
3276                let functor = force_value(functor)?;
3277                // __functor protocol: (functor self) arg
3278                let partial = apply(functor, func.clone())?;
3279                apply(partial, arg)
3280            } else if crate::value::in_promise_eval() {
3281                // M2.6 Promise softening: an attrset without __functor
3282                // being called as a function — typically the empty-
3283                // attrset sentinel inside a fix-point body.  Return
3284                // null so eval can proceed.
3285                Ok(Value::Null)
3286            } else {
3287                Err(EvalError::type_error(
3288                    format!("cannot call {} (missing __functor){}", func.type_name(), eval_file_ctx()),
3289                ))
3290            }
3291        }
3292        _ if crate::value::in_promise_eval() => {
3293            // M2.6 Promise softening: calling null / int / string / list
3294            // as a function inside a Promise body is the sentinel
3295            // cascade landing somewhere it doesn't belong.  Return null
3296            // so the fix-point continues instead of erroring.
3297            Ok(Value::Null)
3298        }
3299        _ => Err(EvalError::type_error(
3300            format!("cannot call {}{}", func.type_name(), eval_file_ctx()),
3301        )),
3302    }
3303}
3304
3305fn bind_param(param: &ast::Param, arg: &Value, env: &mut Env) -> Result<(), EvalError> {
3306    match param {
3307        ast::Param::IdentParam(ip) => {
3308            let ident = ip
3309                .ident()
3310                .ok_or_else(|| EvalError::ParseError("ident param missing ident".to_string()))?;
3311            let name = ident_text(&ident);
3312            env.bind(name, arg.clone());
3313        }
3314        ast::Param::Pattern(pat) => {
3315            let attrs = arg.as_attrs()?;
3316
3317            // @-binding (either `args @ { ... }` or `{ ... } @ args`)
3318            if let Some(pat_bind) = pat.pat_bind()
3319                && let Some(ident) = pat_bind.ident()
3320            {
3321                let name = ident_text(&ident);
3322                env.bind(name, arg.clone());
3323            }
3324
3325            let has_ellipsis = pat.ellipsis_token().is_some();
3326            let entries: Vec<ast::PatEntry> = pat.pat_entries().collect();
3327
3328            // Two-phase binding (matching CppNix semantics):
3329            // Phase 1: Bind all formals. Defaults get thunks with a
3330            //   preliminary env. We collect thunks for Phase 2 update.
3331            // Phase 2: Update default thunks to capture the final env
3332            //   (which now has ALL formals bound). This allows defaults
3333            //   to reference any other formal — including forward refs.
3334            let mut default_thunks: Vec<Thunk> = Vec::new();
3335
3336            for entry in &entries {
3337                let ident = entry.ident().ok_or_else(|| {
3338                    EvalError::ParseError("pat entry missing ident".to_string())
3339                })?;
3340                let name = ident_text(&ident);
3341                let value = if let Some(v) = attrs.get(&name) {
3342                    v.clone()
3343                } else if let Some(default_expr) = entry.default() {
3344                    // Default values in pattern parameters must be lazy
3345                    // (wrapped in thunks), matching CppNix semantics.
3346                    // Patterns like `vendor ? assert false; null` rely on
3347                    // the default never being forced when the body checks
3348                    // `args ? vendor` instead of using `vendor` directly.
3349                    let thunk = Thunk::new_suspended(
3350                        ast::Expr::cast(default_expr.syntax().clone()).unwrap(),
3351                        env.clone(),
3352                    );
3353                    default_thunks.push(thunk.clone());
3354                    Value::Thunk(thunk)
3355                } else {
3356                    return Err(EvalError::type_error(
3357                        format!("missing argument '{name}'{}", eval_file_ctx()),
3358                    ));
3359                };
3360                env.bind(name, value);
3361            }
3362
3363            // Phase 2: Update default thunks to see ALL formals.
3364            for thunk in &default_thunks {
3365                thunk.update_env(env);
3366            }
3367
3368            if !has_ellipsis {
3369                let entry_names: std::collections::HashSet<String> = entries
3370                    .iter()
3371                    .filter_map(|e| e.ident().map(|i| ident_text(&i)))
3372                    .collect();
3373                for key in attrs.keys() {
3374                    if !entry_names.contains(key.as_str()) {
3375                        return Err(EvalError::type_error(
3376                            format!("unexpected argument '{key}'{}", eval_file_ctx()),
3377                        ));
3378                    }
3379                }
3380            }
3381        }
3382    }
3383    Ok(())
3384}
3385
3386#[cfg(test)]
3387mod tests {
3388    use super::*;
3389
3390    fn ev(input: &str) -> Value {
3391        eval(input).unwrap()
3392    }
3393
3394    // Regression (2026-07-10): the let-scope fix-point detector must count
3395    // only GENUINE variable references, not attribute names / attrset keys
3396    // (which sit under a `NODE_ATTRPATH`).  nixpkgs `lib/types.nix` has
3397    // `placeholder = if lhs.placeholder == …` whose RHS mentions the
3398    // *attribute* `.placeholder`; the old raw-token match falsely flagged
3399    // the binding self-recursive and routed it through the Promise path.
3400    #[test]
3401    fn is_self_recursive_binding_ignores_attribute_names() {
3402        fn expr(s: &str) -> ast::Expr {
3403            rnix::Root::parse(s).tree().expr().expect("parse")
3404        }
3405        // attribute names / keys are NOT references to the binding
3406        assert!(!is_self_recursive_binding(&expr("lhs.placeholder"), "placeholder"));
3407        assert!(!is_self_recursive_binding(&expr("{ placeholder = 1; }"), "placeholder"));
3408        assert!(!is_self_recursive_binding(
3409            &expr("if lhs.placeholder == rhs.placeholder then lhs.placeholder else null"),
3410            "placeholder",
3411        ));
3412        // genuine variable references ARE detected
3413        assert!(is_self_recursive_binding(&expr("placeholder + 1"), "placeholder"));
3414        assert!(is_self_recursive_binding(
3415            &expr("if placeholder then 1 else 2"),
3416            "placeholder"
3417        ));
3418    }
3419
3420    // M2 thunk-waste (byte-safe eager constant): a NON-interpolated string in a
3421    // maybe_thunk site is evaluated directly (no suspended thunk). The value +
3422    // its (empty) context must be byte-identical to forcing a thunk of it.
3423    #[test]
3424    fn maybe_thunk_eager_constant_str_is_byte_identical() {
3425        fn expr(s: &str) -> ast::Expr {
3426            rnix::Root::parse(s).tree().expr().expect("parse")
3427        }
3428        let env = Env::new();
3429        // Constant string → returned as a concrete String, NOT a Thunk.
3430        let v = maybe_thunk(&expr(r#""abc""#), &env, false, None);
3431        assert!(matches!(v, Value::String(_)), "constant str should be eager, got {v:?}");
3432        assert_eq!(force_value(&v).unwrap(), Value::string("abc"));
3433        // Interpolated string → MUST stay a thunk (lazy `${…}` force).
3434        let vi = maybe_thunk(&expr(r#""a${b}c""#), &env, false, None);
3435        assert!(matches!(vi, Value::Thunk(_)), "interpolated str must stay thunked");
3436    }
3437
3438    // The pure-constant arg classifier admits ONLY literals + non-interpolated
3439    // strings/paths, and rejects everything that could throw/diverge/observe a
3440    // fixpoint — the laziness safety boundary of the apply-arg optimization.
3441    #[test]
3442    fn eval_pure_constant_arg_classification() {
3443        fn expr(s: &str) -> ast::Expr {
3444            rnix::Root::parse(s).tree().expr().expect("parse")
3445        }
3446        // ADMIT: pure constants (byte-safe to eval eagerly in an arg position).
3447        assert!(eval_pure_constant_arg(&expr("42")).is_some());
3448        assert!(eval_pure_constant_arg(&expr("3.14")).is_some());
3449        assert!(eval_pure_constant_arg(&expr(r#""const""#)).is_some());
3450        assert!(eval_pure_constant_arg(&expr("/abs/path")).is_some());
3451        // REJECT: anything that could throw / diverge / observe laziness.
3452        assert!(eval_pure_constant_arg(&expr(r#""a${b}c""#)).is_none(), "interpolated str");
3453        // `true`/`false`/`null` are IDENTS in nix (shadowable), not literals —
3454        // rejected to avoid a with-scope force, correctly conservative.
3455        assert!(eval_pure_constant_arg(&expr("true")).is_none(), "bool is an ident");
3456        assert!(eval_pure_constant_arg(&expr("x")).is_none(), "ident (with-scope force)");
3457        assert!(eval_pure_constant_arg(&expr("a.b")).is_none(), "select (fixpoint)");
3458        assert!(eval_pure_constant_arg(&expr("f x")).is_none(), "apply (may throw)");
3459        assert!(eval_pure_constant_arg(&expr("1 + 1")).is_none(), "binop (may throw)");
3460        assert!(eval_pure_constant_arg(&expr("throw \"x\"")).is_none(), "throw stays lazy");
3461    }
3462
3463    // LAZINESS GUARD: a lambda that IGNORES its arg must NOT force it — even a
3464    // throwing arg. The pure-constant optimization only touches inert constants,
3465    // so a `throw`-ing arg stays fully thunked and the ignoring lambda succeeds.
3466    #[test]
3467    fn ignored_throwing_arg_stays_lazy() {
3468        assert_eq!(ev(r#"(x: 7) (throw "boom")"#), Value::Int(7));
3469        // And an ignored constant arg is equally invisible.
3470        assert_eq!(ev(r#"(x: 7) "const""#), Value::Int(7));
3471        // A USED constant arg produces the right value.
3472        assert_eq!(ev(r#"(x: x) "used""#), Value::string("used"));
3473    }
3474
3475    #[test]
3476    fn eval_int() { assert_eq!(ev("42"), Value::Int(42)); }
3477
3478    #[test]
3479    fn eval_float() { assert_eq!(ev("3.14"), Value::Float(3.14)); }
3480
3481    #[test]
3482    fn eval_string() { assert_eq!(ev(r#""hello""#), Value::string("hello")); }
3483
3484    #[test]
3485    fn eval_bool() { assert_eq!(ev("true"), Value::Bool(true)); }
3486
3487    #[test]
3488    fn eval_null() { assert_eq!(ev("null"), Value::Null); }
3489
3490    #[test]
3491    fn eval_arithmetic() {
3492        assert_eq!(ev("1 + 2"), Value::Int(3));
3493        assert_eq!(ev("10 - 3"), Value::Int(7));
3494        assert_eq!(ev("2 * 3"), Value::Int(6));
3495        assert_eq!(ev("10 / 3"), Value::Int(3));
3496    }
3497
3498    #[test]
3499    fn eval_precedence() {
3500        assert_eq!(ev("1 + 2 * 3"), Value::Int(7));
3501        assert_eq!(ev("(1 + 2) * 3"), Value::Int(9));
3502    }
3503
3504    #[test]
3505    fn eval_comparison() {
3506        assert_eq!(ev("1 == 1"), Value::Bool(true));
3507        assert_eq!(ev("1 == 2"), Value::Bool(false));
3508        assert_eq!(ev("1 < 2"), Value::Bool(true));
3509        assert_eq!(ev("2 <= 2"), Value::Bool(true));
3510    }
3511
3512    #[test]
3513    fn eval_logic() {
3514        assert_eq!(ev("true && false"), Value::Bool(false));
3515        assert_eq!(ev("true || false"), Value::Bool(true));
3516        assert_eq!(ev("!true"), Value::Bool(false));
3517    }
3518
3519    #[test]
3520    fn eval_string_concat() {
3521        assert_eq!(ev(r#""hello" + " " + "world""#), Value::string("hello world"));
3522    }
3523
3524    #[test]
3525    fn eval_if() {
3526        assert_eq!(ev("if true then 1 else 2"), Value::Int(1));
3527        assert_eq!(ev("if false then 1 else 2"), Value::Int(2));
3528    }
3529
3530    #[test]
3531    fn eval_let() {
3532        assert_eq!(ev("let x = 1; in x"), Value::Int(1));
3533        assert_eq!(ev("let x = 1; y = 2; in x + y"), Value::Int(3));
3534    }
3535
3536    #[test]
3537    fn eval_let_dotted_simple() {
3538        // Two dotted bindings sharing the top-level key `a`.
3539        assert_eq!(ev("let a.b = 1; a.c = 2; in a.b + a.c"), Value::Int(3));
3540    }
3541
3542    #[test]
3543    fn eval_let_dotted_deep() {
3544        // Deeply nested dotted path.
3545        assert_eq!(ev("let a.b.c = 1; in a.b.c"), Value::Int(1));
3546    }
3547
3548    #[test]
3549    fn eval_let_dotted_mixed() {
3550        // Mix of simple and dotted bindings.
3551        assert_eq!(
3552            ev("let a.x = 1; b = 2; a.y = 3; in a.x + a.y + b"),
3553            Value::Int(6),
3554        );
3555    }
3556
3557    #[test]
3558    fn eval_let_dotted_produces_attrset() {
3559        // Dotted let bindings produce a real attrset.
3560        let v = ev("let a.b = 1; a.c = 2; in a");
3561        if let Value::Attrs(attrs) = v {
3562            assert_eq!(attrs.get("b"), Some(&Value::Int(1)));
3563            assert_eq!(attrs.get("c"), Some(&Value::Int(2)));
3564        } else {
3565            panic!("expected Attrs, got {v:?}");
3566        }
3567    }
3568
3569    // ── Inner dynamic attrpath key laziness ──────────────────
3570    // CppNix defers a dynamic key that is NOT at the head of an attrpath:
3571    // `{ a.${e} = v; }` builds `{ a = <thunk {${e}=v}>; }`, so `e` never
3572    // forces until `.a` is demanded. Reading a sibling must not force the
3573    // inner dynamic key. Root fix: `build_deferred_tail_attr` in eval.rs.
3574    // This is the pure-builtins reduction of the NixOS module-system
3575    // `config.homes.${cfg.userName}` fixpoint divergence.
3576    #[test]
3577    fn dynamic_inner_attr_key_is_lazy_on_sibling_read() {
3578        // The dynamic key throws; reading the SIBLING must NOT force it.
3579        assert_eq!(
3580            ev(r#"let s = { a.${throw "KEYFORCED"} = 7; other = 9; }; in s.other"#),
3581            Value::Int(9),
3582        );
3583    }
3584
3585    #[test]
3586    fn dynamic_inner_attr_key_resolves_on_head_demand() {
3587        // Demanding the head DOES resolve the deferred dynamic key.
3588        let v = ev(r#"let u = "bob"; s = { homes.${u} = 7; }; in s.homes"#);
3589        if let Value::Attrs(attrs) = force_value(&v).unwrap() {
3590            assert_eq!(attrs.get("bob"), Some(&Value::Int(7)));
3591        } else {
3592            panic!("expected Attrs");
3593        }
3594    }
3595
3596    #[test]
3597    fn dynamic_inner_attr_key_merges_with_static_sibling() {
3598        // Collision under one head still deep-merges (static + dynamic).
3599        let v = ev(r#"let u = "x"; s = { a.${u} = 1; a.b = 2; }; in s.a"#);
3600        if let Value::Attrs(attrs) = force_value(&v).unwrap() {
3601            assert_eq!(attrs.get("x"), Some(&Value::Int(1)));
3602            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
3603        } else {
3604            panic!("expected Attrs");
3605        }
3606    }
3607
3608    #[test]
3609    fn dynamic_inner_attr_key_null_skips_binding() {
3610        // A null dynamic inner key skips the definition (CppNix rule):
3611        // `a` becomes an empty attrset, the sibling stays.
3612        let v = ev(
3613            r#"let c = true; s = { a.${if c then null else "n"} = 5; b = 1; }; in s.b"#,
3614        );
3615        assert_eq!(v, Value::Int(1));
3616    }
3617
3618    // ── M2.6 ROOT #3: interpolated-STRING tail keys are dynamic too ──────
3619    // `{ a."p${e}" = v; }` must build `{ a = <thunk {"p${e}"=v}>; }` — an
3620    // interpolated-string attr key references `e` and so must defer like a
3621    // bare `${e}`, never force at construction. Reading a sibling must NOT
3622    // force it (the KEYFORCE discriminator, now for a `Str` key).
3623    #[test]
3624    fn interpolated_string_attr_key_is_lazy_on_sibling_read() {
3625        assert_eq!(
3626            ev(r#"let s = { a."p/${throw "KEYFORCED"}" = 7; other = 9; }; in s.other"#),
3627            Value::Int(9),
3628        );
3629    }
3630
3631    #[test]
3632    fn interpolated_string_attr_key_resolves_on_head_demand() {
3633        // Demanding the head DOES resolve the deferred interpolated key.
3634        let v = ev(r#"let u = "bob"; s = { homes."u/${u}" = 7; }; in s.homes"#);
3635        if let Value::Attrs(attrs) = force_value(&v).unwrap() {
3636            assert_eq!(attrs.get("u/bob"), Some(&Value::Int(7)));
3637        } else {
3638            panic!("expected Attrs");
3639        }
3640    }
3641
3642    #[test]
3643    fn purely_literal_string_attr_key_stays_eager_static() {
3644        // A `Str` key with NO interpolation is a plain static key and must
3645        // NOT be treated as dynamic (it forces nothing, deep-merges).
3646        let v = ev(r#"let s = { a."foo bar" = 1; a.b = 2; }; in s.a"#);
3647        if let Value::Attrs(attrs) = force_value(&v).unwrap() {
3648            assert_eq!(attrs.get("foo bar"), Some(&Value::Int(1)));
3649            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
3650        } else {
3651            panic!("expected Attrs");
3652        }
3653    }
3654
3655    // ── M2.6 ROOT #3 (collision case): dynamic tail key under a head that
3656    // a sibling binding already wrote must stay lazy AND deep-merge.
3657    #[test]
3658    fn dynamic_tail_key_under_colliding_head_is_lazy() {
3659        // `sd.services.x` writes head `sd`; the second binding's dynamic
3660        // key must NOT force when a SIBLING (`sd.services`) is read.
3661        let v = ev(
3662            r#"let s = { sd.services.x = 1; sd.tmpfiles.${throw "KEYFORCED"}.d = 2; }; in s.sd.services.x"#,
3663        );
3664        assert_eq!(v, Value::Int(1));
3665    }
3666
3667    #[test]
3668    fn dynamic_tail_key_under_colliding_head_resolves_and_merges() {
3669        // Demanding the dynamic branch resolves the key; the sibling
3670        // static branch (`sd.services`) survives the merge intact.
3671        let v = ev(
3672            r#"let k = "z"; s = { sd.services.x = 1; sd.tmpfiles.${k}.d = 2; }; in s.sd"#,
3673        );
3674        let sd = force_value(&v).unwrap();
3675        if let Value::Attrs(sd_attrs) = &sd {
3676            // static sibling intact
3677            let services = force_value(sd_attrs.get("services").unwrap()).unwrap();
3678            if let Value::Attrs(a) = &services {
3679                assert_eq!(force_value(a.get("x").unwrap()).unwrap(), Value::Int(1));
3680            } else { panic!("expected services attrs"); }
3681            // dynamic branch resolved to key "z"
3682            let tmpfiles = force_value(sd_attrs.get("tmpfiles").unwrap()).unwrap();
3683            if let Value::Attrs(a) = &tmpfiles {
3684                let z = force_value(a.get("z").unwrap()).unwrap();
3685                if let Value::Attrs(zd) = &z {
3686                    assert_eq!(force_value(zd.get("d").unwrap()).unwrap(), Value::Int(2));
3687                } else { panic!("expected z attrs"); }
3688            } else { panic!("expected tmpfiles attrs"); }
3689        } else {
3690            panic!("expected sd attrs");
3691        }
3692    }
3693
3694    // ── M2.6 ROOT #4a — `with` namespace must be LAZY ─────────────────
3695    // `with X; body` stores the namespace as a thunk forced only on a
3696    // bare-ident fallthrough lookup; demanding only the body's WHNF/keys
3697    // must NOT force X.  cppnix: `attrNames (with (throw "X"); {a=1;})`
3698    // → ["a"].  Before the fix, sui EVALUATED the namespace at `with`-entry
3699    // and threw.  This is the load-bearing over-force behind the M2.6
3700    // `concatLists null` (nixpkgs' `config = mkIf … (with config.services.X;
3701    // { … })` module shape forced `config.services.X` during collection).
3702    #[test]
3703    fn with_namespace_is_lazy_on_body_whnf() {
3704        let v = ev(r#"builtins.attrNames (with (throw "WITH-FORCED"); { a = 1; b = 2; })"#);
3705        if let Value::List(items) = force_value(&v).unwrap() {
3706            let names: Vec<String> = items
3707                .iter()
3708                .map(|i| match force_value(i).unwrap() {
3709                    Value::String(s) => s.as_str().to_string(),
3710                    other => panic!("expected string, got {}", other.type_name()),
3711                })
3712                .collect();
3713            assert_eq!(names, vec!["a".to_string(), "b".to_string()]);
3714        } else {
3715            panic!("expected list");
3716        }
3717    }
3718
3719    #[test]
3720    fn with_namespace_forces_only_on_fallthrough() {
3721        // A bare ident that falls through lexical scope DOES resolve via
3722        // the namespace (correct cppnix semantics) — proves the deferred
3723        // thunk is real and gets forced on demand, not an accidental no-op.
3724        assert_eq!(ev(r#"with { x = 42; }; x"#), Value::Int(42));
3725        // A lexical binding shadows the with-scope, so the (throwing)
3726        // namespace is never forced — the laziness we rely on for M2.6.
3727        assert_eq!(ev(r#"let x = 7; in with (throw "NS"); x"#), Value::Int(7));
3728    }
3729
3730    // ── M2.6 ROOT #4b — depth-≥2 dotted full-set leaf must deep-merge ──
3731    // `o.a = { x = 1; }` inserts `o = { a = <thunk {x=1}> }` (leaf goes
3732    // through maybe_thunk); a deeper sibling `o.a.y = 2` recurses
3733    // merge_nested_insert down to key `a` where the existing value is that
3734    // thunk.  Before the fix, merge_nested_insert required BOTH sides to be
3735    // concrete Attrs, so the Thunk-vs-Attrs collision OVERWROTE — dropping
3736    // `x`.  cppnix desugars both orderings into `o.a = { x = 1; y = 2; }`.
3737    // This is the M2.6 post-`with`-fix frontier (nixpkgs alsa's
3738    // `options.hardware.alsa = { … }` + `options.hardware.alsa.enablePersistence
3739    // = …` merged to only {enablePersistence} → `cardAliases` "does not exist").
3740    #[test]
3741    fn dotted_fullset_leaf_deep_merges_with_deeper_sibling() {
3742        let v = ev(r#"{ o.a = { x = 1; }; o.a.y = 2; }.o.a"#);
3743        if let Value::Attrs(a) = force_value(&v).unwrap() {
3744            assert_eq!(force_value(a.get("x").unwrap()).unwrap(), Value::Int(1));
3745            assert_eq!(force_value(a.get("y").unwrap()).unwrap(), Value::Int(2));
3746        } else {
3747            panic!("expected attrs");
3748        }
3749    }
3750
3751    #[test]
3752    fn dotted_fullset_leaf_deep_merge_reverse_order() {
3753        // Deeper sibling FIRST, full-set leaf SECOND — the NEW value is the
3754        // `<thunk {x=1}>`; must still merge (the collision forces it).
3755        let v = ev(r#"{ o.a.y = 2; o.a = { x = 1; }; }.o.a"#);
3756        if let Value::Attrs(a) = force_value(&v).unwrap() {
3757            assert_eq!(force_value(a.get("x").unwrap()).unwrap(), Value::Int(1));
3758            assert_eq!(force_value(a.get("y").unwrap()).unwrap(), Value::Int(2));
3759        } else {
3760            panic!("expected attrs");
3761        }
3762    }
3763
3764    #[test]
3765    fn dotted_fullset_leaf_merge_preserves_leaf_laziness() {
3766        // The merge forces the existing/new leaf to WHNF (keys) but MUST
3767        // NOT force the leaf VALUES — a throwing sibling value that is never
3768        // demanded stays lazy.
3769        assert_eq!(ev(r#"{ o.a = { x = throw "X-NEVER"; }; o.a.y = 2; }.o.a.y"#), Value::Int(2));
3770    }
3771
3772    #[test]
3773    fn eval_nested_let() {
3774        assert_eq!(ev("let a = 1; b = let c = 2; in c; in a + b"), Value::Int(3));
3775    }
3776
3777    #[test]
3778    fn eval_lambda() {
3779        assert_eq!(ev("(x: x + 1) 41"), Value::Int(42));
3780    }
3781
3782    #[test]
3783    fn eval_lambda_multi_arg() {
3784        assert_eq!(ev("(x: y: x + y) 1 2"), Value::Int(3));
3785    }
3786
3787    #[test]
3788    fn eval_list() {
3789        let v = ev("[1 2 3]");
3790        assert_eq!(v, Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]));
3791    }
3792
3793    #[test]
3794    fn eval_list_concat() {
3795        let v = ev("[1 2] ++ [3 4]");
3796        assert_eq!(v, Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(4)]));
3797    }
3798
3799    #[test]
3800    fn eval_attrset() {
3801        let v = ev("{ a = 1; b = 2; }");
3802        if let Value::Attrs(attrs) = v {
3803            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
3804            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
3805        } else {
3806            panic!("expected attrset");
3807        }
3808    }
3809
3810    #[test]
3811    fn eval_select() {
3812        assert_eq!(ev("{ a = 42; }.a"), Value::Int(42));
3813    }
3814
3815    #[test]
3816    fn eval_select_or() {
3817        assert_eq!(ev("{ a = 42; }.b or 0"), Value::Int(0));
3818    }
3819
3820    #[test]
3821    fn eval_has_attr() {
3822        assert_eq!(ev("{ a = 1; } ? a"), Value::Bool(true));
3823        assert_eq!(ev("{ a = 1; } ? b"), Value::Bool(false));
3824    }
3825
3826    #[test]
3827    fn eval_update() {
3828        let v = ev("{ a = 1; b = 2; } // { b = 3; c = 4; }");
3829        if let Value::Attrs(attrs) = v {
3830            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
3831            assert_eq!(attrs.get("b"), Some(&Value::Int(3)));
3832            assert_eq!(attrs.get("c"), Some(&Value::Int(4)));
3833        } else {
3834            panic!("expected attrset");
3835        }
3836    }
3837
3838    #[test]
3839    fn eval_with() {
3840        assert_eq!(ev("with { x = 42; }; x"), Value::Int(42));
3841    }
3842
3843    #[test]
3844    fn eval_assert() {
3845        assert_eq!(ev("assert true; 42"), Value::Int(42));
3846        assert!(eval("assert false; 42").is_err());
3847    }
3848
3849    #[test]
3850    fn eval_formals() {
3851        assert_eq!(ev("({ a, b }: a + b) { a = 1; b = 2; }"), Value::Int(3));
3852    }
3853
3854    #[test]
3855    fn eval_formals_default() {
3856        assert_eq!(ev("({ a, b ? 10 }: a + b) { a = 1; }"), Value::Int(11));
3857    }
3858
3859    #[test]
3860    fn eval_formals_ellipsis() {
3861        assert_eq!(ev("({ a, ... }: a) { a = 1; b = 2; }"), Value::Int(1));
3862    }
3863
3864    #[test]
3865    fn eval_named_formals() {
3866        assert_eq!(ev("(args @ { a }: args.a) { a = 42; }"), Value::Int(42));
3867    }
3868
3869    #[test]
3870    fn eval_rec_attrset() {
3871        assert_eq!(ev("(rec { a = 1; b = a + 1; }).b"), Value::Int(2));
3872    }
3873
3874    #[test]
3875    fn eval_negation() {
3876        assert_eq!(ev("-42"), Value::Int(-42));
3877    }
3878
3879    #[test]
3880    fn eval_float_arithmetic() {
3881        assert_eq!(ev("1.5 + 2.5"), Value::Float(4.0));
3882        assert_eq!(ev("1 + 1.5"), Value::Float(2.5));
3883    }
3884
3885    #[test]
3886    fn eval_division_by_zero() {
3887        assert!(eval("1 / 0").is_err());
3888    }
3889
3890    #[test]
3891    fn eval_builtins_available() {
3892        assert_eq!(ev("builtins.typeOf 42"), Value::string("int"));
3893        assert_eq!(ev("builtins.typeOf true"), Value::string("bool"));
3894    }
3895
3896    #[test]
3897    fn eval_builtins_length() {
3898        assert_eq!(ev("builtins.length [1 2 3]"), Value::Int(3));
3899    }
3900
3901    #[test]
3902    fn eval_builtins_head_tail() {
3903        assert_eq!(ev("builtins.head [1 2 3]"), Value::Int(1));
3904        assert_eq!(ev("builtins.length (builtins.tail [1 2 3])"), Value::Int(2));
3905    }
3906
3907    #[test]
3908    fn eval_builtins_add() {
3909        assert_eq!(ev("builtins.add 1 2"), Value::Int(3));
3910    }
3911
3912    #[test]
3913    fn eval_builtins_to_string() {
3914        assert_eq!(ev("builtins.toString 42"), Value::string("42"));
3915    }
3916
3917    #[test]
3918    fn eval_implication() {
3919        assert_eq!(ev("false -> true"), Value::Bool(true));
3920        assert_eq!(ev("true -> false"), Value::Bool(false));
3921        assert_eq!(ev("true -> true"), Value::Bool(true));
3922    }
3923
3924    // ── New tests ────────────────────────────────────────
3925
3926    #[test]
3927    fn eval_error_undefined_variable() {
3928        let result = eval("nonexistent");
3929        assert!(result.is_err());
3930        let msg = format!("{}", result.unwrap_err());
3931        assert!(msg.contains("undefined variable"));
3932    }
3933
3934    #[test]
3935    fn eval_error_type_mismatch_arithmetic() {
3936        let result = eval(r#"1 + "hello""#);
3937        assert!(result.is_err());
3938        let msg = format!("{}", result.unwrap_err());
3939        assert!(msg.contains("cannot add") || msg.contains("type"));
3940    }
3941
3942    #[test]
3943    fn eval_error_unexpected_argument() {
3944        let result = eval("({ a }: a) { a = 1; b = 2; }");
3945        assert!(result.is_err());
3946        let msg = format!("{}", result.unwrap_err());
3947        assert!(msg.contains("unexpected argument"));
3948    }
3949
3950    #[test]
3951    fn eval_error_missing_required_argument() {
3952        let result = eval("({ a, b }: a + b) { a = 1; }");
3953        assert!(result.is_err());
3954        let msg = format!("{}", result.unwrap_err());
3955        assert!(msg.contains("missing argument"));
3956    }
3957
3958    #[test]
3959    fn eval_builtins_attr_names_sorted() {
3960        let v = ev("builtins.attrNames { z = 1; a = 2; m = 3; }");
3961        // BTreeMap keys are already sorted
3962        assert_eq!(
3963            v,
3964            Value::list(vec![
3965                Value::string("a"),
3966                Value::string("m"),
3967                Value::string("z"),
3968            ]),
3969        );
3970    }
3971
3972    #[test]
3973    fn eval_builtins_attr_values() {
3974        let v = ev("builtins.attrValues { a = 1; b = 2; }");
3975        // BTreeMap iteration is sorted by key, so a=1 first, b=2 second
3976        assert_eq!(v, Value::list(vec![Value::Int(1), Value::Int(2)]));
3977    }
3978
3979    #[test]
3980    fn eval_builtins_is_null() {
3981        assert_eq!(ev("builtins.isNull null"), Value::Bool(true));
3982        assert_eq!(ev("builtins.isNull 1"), Value::Bool(false));
3983    }
3984
3985    #[test]
3986    fn eval_builtins_is_int() {
3987        assert_eq!(ev("builtins.isInt 42"), Value::Bool(true));
3988        assert_eq!(ev("builtins.isInt 3.14"), Value::Bool(false));
3989    }
3990
3991    #[test]
3992    fn eval_builtins_is_bool() {
3993        assert_eq!(ev("builtins.isBool true"), Value::Bool(true));
3994        assert_eq!(ev("builtins.isBool 0"), Value::Bool(false));
3995    }
3996
3997    #[test]
3998    fn eval_builtins_is_string() {
3999        assert_eq!(ev(r#"builtins.isString "hi""#), Value::Bool(true));
4000        assert_eq!(ev("builtins.isString 1"), Value::Bool(false));
4001    }
4002
4003    #[test]
4004    fn eval_builtins_is_list() {
4005        assert_eq!(ev("builtins.isList [1 2]"), Value::Bool(true));
4006        assert_eq!(ev("builtins.isList {}"), Value::Bool(false));
4007    }
4008
4009    #[test]
4010    fn eval_builtins_is_attrs() {
4011        assert_eq!(ev("builtins.isAttrs {}"), Value::Bool(true));
4012        assert_eq!(ev("builtins.isAttrs []"), Value::Bool(false));
4013    }
4014
4015    #[test]
4016    fn eval_builtins_string_length() {
4017        assert_eq!(ev(r#"builtins.stringLength "hello""#), Value::Int(5));
4018        assert_eq!(ev(r#"builtins.stringLength """#), Value::Int(0));
4019    }
4020
4021    #[test]
4022    fn eval_builtins_to_json_roundtrip() {
4023        // toJSON produces a JSON string; fromJSON parses it back
4024        assert_eq!(
4025            ev(r#"builtins.fromJSON (builtins.toJSON 42)"#),
4026            Value::Int(42),
4027        );
4028        assert_eq!(
4029            ev(r#"builtins.fromJSON (builtins.toJSON [1 2 3])"#),
4030            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
4031        );
4032    }
4033
4034    #[test]
4035    fn eval_builtins_from_json() {
4036        assert_eq!(
4037            ev(r#"builtins.fromJSON "{\"a\": 1}""#),
4038            {
4039                let mut attrs = NixAttrs::new();
4040                attrs.insert("a".to_string(), Value::Int(1));
4041                Value::Attrs(Rc::new(attrs))
4042            },
4043        );
4044        assert_eq!(ev(r#"builtins.fromJSON "null""#), Value::Null);
4045        assert_eq!(ev(r#"builtins.fromJSON "true""#), Value::Bool(true));
4046    }
4047
4048    #[test]
4049    fn eval_nested_function_application() {
4050        // (f 1) 2 where f = x: y: x + y
4051        assert_eq!(ev("(x: y: x + y) 1 2"), Value::Int(3));
4052        // equivalent parenthesized form
4053        assert_eq!(ev("((x: y: x + y) 1) 2"), Value::Int(3));
4054    }
4055
4056    #[test]
4057    fn eval_recursive_let() {
4058        assert_eq!(ev("let a = 1; b = a + 1; in b"), Value::Int(2));
4059        assert_eq!(ev("let a = 1; b = a + 1; c = b + 1; in c"), Value::Int(3));
4060    }
4061
4062    #[test]
4063    fn eval_string_comparison() {
4064        assert_eq!(ev(r#""a" < "b""#), Value::Bool(true));
4065        assert_eq!(ev(r#""b" < "a""#), Value::Bool(false));
4066        assert_eq!(ev(r#""abc" == "abc""#), Value::Bool(true));
4067        assert_eq!(ev(r#""abc" != "def""#), Value::Bool(true));
4068    }
4069
4070    #[test]
4071    fn eval_list_in_attrset() {
4072        let v = ev("{ x = [1 2 3]; }.x");
4073        assert_eq!(
4074            v,
4075            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
4076        );
4077    }
4078
4079    #[test]
4080    fn eval_nested_attrset_select() {
4081        assert_eq!(ev("{ a = { b = 42; }; }.a.b"), Value::Int(42));
4082    }
4083
4084    #[test]
4085    fn eval_let_shadows_outer() {
4086        assert_eq!(
4087            ev("let x = 1; in let x = 2; in x"),
4088            Value::Int(2),
4089        );
4090    }
4091
4092    #[test]
4093    fn eval_with_provides_scope() {
4094        // `with` scope is available for name resolution
4095        assert_eq!(
4096            ev("with { x = 42; y = 10; }; x + y"),
4097            Value::Int(52),
4098        );
4099    }
4100
4101    #[test]
4102    fn eval_list_equality() {
4103        assert_eq!(ev("[1 2] == [1 2]"), Value::Bool(true));
4104        assert_eq!(ev("[1 2] == [1 3]"), Value::Bool(false));
4105    }
4106
4107    #[test]
4108    fn eval_attrset_equality() {
4109        assert_eq!(ev("{ a = 1; } == { a = 1; }"), Value::Bool(true));
4110        assert_eq!(ev("{ a = 1; } == { a = 2; }"), Value::Bool(false));
4111    }
4112
4113    // ═══════════════════════════════════════════════════════════
4114    // 1. LITERAL TYPES
4115    // ═══════════════════════════════════════════════════════════
4116
4117    #[test]
4118    fn literal_int_large_zero_negative() {
4119        // Large positive integer (within i64 range)
4120        assert_eq!(ev("9223372036854775807"), Value::Int(i64::MAX));
4121        // Zero
4122        assert_eq!(ev("0"), Value::Int(0));
4123        // Negative via unary negate
4124        assert_eq!(ev("-1"), Value::Int(-1));
4125        assert_eq!(ev("-999999"), Value::Int(-999999));
4126    }
4127
4128    #[test]
4129    fn literal_float_small_large() {
4130        assert_eq!(ev("0.001"), Value::Float(0.001));
4131        assert_eq!(ev("999999.999"), Value::Float(999999.999));
4132        // Float with scientific notation via expression (1e6 parsed by rnix)
4133        assert_eq!(ev("1.0e3"), Value::Float(1000.0));
4134        assert_eq!(ev("1.5e2"), Value::Float(150.0));
4135    }
4136
4137    #[test]
4138    fn literal_string_empty_and_escapes() {
4139        assert_eq!(ev(r#""""#), Value::string(""));
4140        // Escape sequences within strings
4141        assert_eq!(ev(r#""hello\nworld""#), Value::string("hello\nworld"));
4142        assert_eq!(ev(r#""tab\there""#), Value::string("tab\there"));
4143    }
4144
4145    #[test]
4146    fn literal_multiline_string() {
4147        // Indented string ('' ... '')
4148        assert_eq!(
4149            ev("''hello''"),
4150            Value::string("hello"),
4151        );
4152        // Multiline indented string strips common indentation
4153        assert_eq!(
4154            ev("''\n  line1\n  line2\n''"),
4155            Value::string("line1\nline2\n"),
4156        );
4157    }
4158
4159    #[test]
4160    fn literal_paths() {
4161        // Relative path
4162        assert_eq!(ev("./foo"), Value::Path(Box::new(SmolStr::from("./foo"))));
4163        // Absolute path
4164        assert_eq!(ev("/nix/store/abc"), Value::Path(Box::new(SmolStr::from("/nix/store/abc"))));
4165        // Home path
4166        assert_eq!(ev("~/myfile"), Value::Path(Box::new(SmolStr::from("~/myfile"))));
4167    }
4168
4169    // ── Interpolated path literals (cid-marquee root, 2026-07-12) ──
4170    //
4171    // CppNix path literals may contain `${e}` antiquotations: `./${x}.nix`,
4172    // `/a/${e}`, `~/${e}`. sui previously flattened the whole path token to
4173    // raw text and dropped the interpolation (`import ./${x}.nix` →
4174    // `No such file or directory`). The `${e}` must be evaluated,
4175    // string-coerced (plain, no copy-to-store), spliced, and the result is
4176    // still a `path` value. Oracles taken from cppnix.
4177
4178    #[test]
4179    fn interp_path_abs_splices_and_types_path() {
4180        // /a/${x}/b with x="foo" → /a/foo/b, type path (nix oracle).
4181        let v = ev(r#"let x = "foo"; in /a/${x}/b"#);
4182        assert_eq!(v, Value::Path(Box::new(SmolStr::from("/a/foo/b"))));
4183    }
4184
4185    #[test]
4186    fn interp_path_abs_multi_and_slash_in_value() {
4187        // Multiple interpolations + a slash inside the spliced value.
4188        assert_eq!(
4189            ev(r#"let a = "x"; b = "y/z"; in /p/${a}/${b}.nix"#),
4190            Value::Path(Box::new(SmolStr::from("/p/x/y/z.nix"))),
4191        );
4192    }
4193
4194    #[test]
4195    fn interp_path_abs_normalizes_double_slash_seam() {
4196        // A path-typed interpolation splices the raw path (no copy-to-store)
4197        // and the `/` seam is normalized: `/bar/` + `/tmp/foo` → /bar/tmp/foo.
4198        assert_eq!(
4199            ev(r#"/bar/${/tmp/foo}"#),
4200            Value::Path(Box::new(SmolStr::from("/bar/tmp/foo"))),
4201        );
4202    }
4203
4204    #[test]
4205    fn interp_path_rel_resolves_against_eval_dir() {
4206        // The spicetify `map (x: ./${x}.nix) [...]` root: a relative
4207        // interpolated path resolves against the defining file's directory,
4208        // exactly like a plain `./foo.nix` literal.
4209        let _g = push_eval_file(std::path::PathBuf::from("/tmp/example/default.nix"));
4210        assert_eq!(
4211            ev(r#"let x = "foo"; in ./${x}.nix"#),
4212            Value::Path(Box::new(SmolStr::from("/tmp/example/foo.nix"))),
4213        );
4214    }
4215
4216    #[test]
4217    fn interp_path_rel_no_eval_dir_keeps_relative_text() {
4218        // With no eval-file context the plain branch keeps the raw relative
4219        // text; the interpolated branch splices then does the same.
4220        assert_eq!(
4221            ev(r#"let x = "foo"; in ./${x}.nix"#),
4222            Value::Path(Box::new(SmolStr::from("./foo.nix"))),
4223        );
4224    }
4225
4226    #[test]
4227    fn interp_path_home_splices_leading_tilde_preserved() {
4228        // Home paths splice their `${e}`; the leading `~` is carried as-is
4229        // (matching sui's plain `~/foo` behavior — `~`-expansion is a
4230        // separate, pre-existing concern, not introduced here).
4231        assert_eq!(
4232            ev(r#"let x = "foo"; in ~/${x}/bar"#),
4233            Value::Path(Box::new(SmolStr::from("~/foo/bar"))),
4234        );
4235    }
4236
4237    #[test]
4238    fn interp_path_non_interpolated_still_raw() {
4239        // A path with no `${…}` must keep the trivial raw-text shortcut
4240        // (byte-for-byte identical to the plain branch).
4241        assert_eq!(ev("/a/b/c"), Value::Path(Box::new(SmolStr::from("/a/b/c"))));
4242        assert_eq!(ev("~/plain"), Value::Path(Box::new(SmolStr::from("~/plain"))));
4243    }
4244
4245    #[test]
4246    fn literal_null_true_false_standalone() {
4247        assert_eq!(ev("null"), Value::Null);
4248        assert_eq!(ev("true"), Value::Bool(true));
4249        assert_eq!(ev("false"), Value::Bool(false));
4250    }
4251
4252    // ═══════════════════════════════════════════════════════════
4253    // 2. OPERATORS — COMPLETE COVERAGE
4254    // ═══════════════════════════════════════════════════════════
4255
4256    #[test]
4257    fn op_arithmetic_int() {
4258        assert_eq!(ev("100 + 200"), Value::Int(300));
4259        assert_eq!(ev("50 - 30"), Value::Int(20));
4260        assert_eq!(ev("7 * 8"), Value::Int(56));
4261        assert_eq!(ev("17 / 3"), Value::Int(5)); // integer division
4262    }
4263
4264    #[test]
4265    fn op_arithmetic_float() {
4266        assert_eq!(ev("1.5 + 2.5"), Value::Float(4.0));
4267        assert_eq!(ev("5.0 - 1.5"), Value::Float(3.5));
4268        assert_eq!(ev("2.0 * 3.0"), Value::Float(6.0));
4269        assert_eq!(ev("7.0 / 2.0"), Value::Float(3.5));
4270    }
4271
4272    #[test]
4273    fn op_arithmetic_mixed_int_float() {
4274        // int + float => float
4275        assert_eq!(ev("1 + 2.5"), Value::Float(3.5));
4276        assert_eq!(ev("2.5 + 1"), Value::Float(3.5));
4277        // int * float => float
4278        assert_eq!(ev("2 * 1.5"), Value::Float(3.0));
4279        // float - int => float
4280        assert_eq!(ev("5.5 - 2"), Value::Float(3.5));
4281    }
4282
4283    #[test]
4284    fn op_string_concat() {
4285        assert_eq!(ev(r#""foo" + "bar""#), Value::string("foobar"));
4286        assert_eq!(ev(r#""" + "x""#), Value::string("x"));
4287        assert_eq!(ev(r#""a" + "" + "b""#), Value::string("ab"));
4288    }
4289
4290    #[test]
4291    fn op_path_concat() {
4292        // path + string
4293        assert_eq!(ev(r#"./foo + "/bar""#), Value::Path(Box::new(SmolStr::from("./foo/bar"))));
4294        // path + path (should join with /)
4295        assert_eq!(ev("./a + ./b"), Value::Path(Box::new(SmolStr::from("./a/./b"))));
4296    }
4297
4298    #[test]
4299    fn op_comparison_ints() {
4300        assert_eq!(ev("1 < 2"), Value::Bool(true));
4301        assert_eq!(ev("2 < 1"), Value::Bool(false));
4302        assert_eq!(ev("2 > 1"), Value::Bool(true));
4303        assert_eq!(ev("1 > 2"), Value::Bool(false));
4304        assert_eq!(ev("2 <= 2"), Value::Bool(true));
4305        assert_eq!(ev("3 <= 2"), Value::Bool(false));
4306        assert_eq!(ev("2 >= 2"), Value::Bool(true));
4307        assert_eq!(ev("1 >= 2"), Value::Bool(false));
4308    }
4309
4310    #[test]
4311    fn op_comparison_floats() {
4312        assert_eq!(ev("1.5 < 2.5"), Value::Bool(true));
4313        assert_eq!(ev("2.5 > 1.5"), Value::Bool(true));
4314        assert_eq!(ev("1.5 <= 1.5"), Value::Bool(true));
4315        assert_eq!(ev("1.5 >= 1.5"), Value::Bool(true));
4316    }
4317
4318    #[test]
4319    fn op_comparison_strings() {
4320        assert_eq!(ev(r#""apple" < "banana""#), Value::Bool(true));
4321        assert_eq!(ev(r#""banana" > "apple""#), Value::Bool(true));
4322        assert_eq!(ev(r#""abc" == "abc""#), Value::Bool(true));
4323        assert_eq!(ev(r#""abc" != "xyz""#), Value::Bool(true));
4324        assert_eq!(ev(r#""abc" <= "abd""#), Value::Bool(true));
4325        assert_eq!(ev(r#""abc" >= "abb""#), Value::Bool(true));
4326    }
4327
4328    #[test]
4329    fn op_equality_various_types() {
4330        assert_eq!(ev("null == null"), Value::Bool(true));
4331        assert_eq!(ev("true == true"), Value::Bool(true));
4332        assert_eq!(ev("false == false"), Value::Bool(true));
4333        assert_eq!(ev("true == false"), Value::Bool(false));
4334        assert_eq!(ev("1 == 1"), Value::Bool(true));
4335        assert_eq!(ev("1 != 2"), Value::Bool(true));
4336        // Different types are not equal
4337        assert_eq!(ev(r#"1 == "1""#), Value::Bool(false));
4338        assert_eq!(ev("null == false"), Value::Bool(false));
4339    }
4340
4341    #[test]
4342    fn op_logic_short_circuit() {
4343        // false && <error> should NOT evaluate the RHS
4344        assert_eq!(ev("false && (1 / 0 == 0)"), Value::Bool(false));
4345        // true || <error> should NOT evaluate the RHS
4346        assert_eq!(ev("true || (1 / 0 == 0)"), Value::Bool(true));
4347    }
4348
4349    #[test]
4350    fn op_logic_full() {
4351        assert_eq!(ev("true && true"), Value::Bool(true));
4352        assert_eq!(ev("true && false"), Value::Bool(false));
4353        assert_eq!(ev("false && true"), Value::Bool(false));
4354        assert_eq!(ev("false && false"), Value::Bool(false));
4355        assert_eq!(ev("true || true"), Value::Bool(true));
4356        assert_eq!(ev("true || false"), Value::Bool(true));
4357        assert_eq!(ev("false || true"), Value::Bool(true));
4358        assert_eq!(ev("false || false"), Value::Bool(false));
4359        assert_eq!(ev("!true"), Value::Bool(false));
4360        assert_eq!(ev("!false"), Value::Bool(true));
4361    }
4362
4363    #[test]
4364    fn op_implication_truth_table() {
4365        // false -> anything = true
4366        assert_eq!(ev("false -> false"), Value::Bool(true));
4367        assert_eq!(ev("false -> true"), Value::Bool(true));
4368        // true -> x = x
4369        assert_eq!(ev("true -> true"), Value::Bool(true));
4370        assert_eq!(ev("true -> false"), Value::Bool(false));
4371    }
4372
4373    #[test]
4374    fn op_implication_short_circuit() {
4375        // false -> <error> should NOT evaluate the RHS
4376        assert_eq!(ev("false -> (1 / 0 == 0)"), Value::Bool(true));
4377    }
4378
4379    #[test]
4380    fn op_update_merge() {
4381        let v = ev("{ a = 1; } // { b = 2; }");
4382        if let Value::Attrs(attrs) = v {
4383            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
4384            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
4385        } else {
4386            panic!("expected attrs");
4387        }
4388    }
4389
4390    #[test]
4391    fn op_update_right_wins() {
4392        assert_eq!(ev("({ a = 1; } // { a = 2; }).a"), Value::Int(2));
4393    }
4394
4395    #[test]
4396    fn op_list_concat() {
4397        assert_eq!(
4398            ev("[1 2] ++ [3 4]"),
4399            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(4)]),
4400        );
4401        // Empty list concat
4402        assert_eq!(ev("[] ++ [1]"), Value::list(vec![Value::Int(1)]));
4403        assert_eq!(ev("[1] ++ []"), Value::list(vec![Value::Int(1)]));
4404    }
4405
4406    #[test]
4407    fn op_has_attr_present_and_absent() {
4408        assert_eq!(ev("{ x = 1; y = 2; } ? x"), Value::Bool(true));
4409        assert_eq!(ev("{ x = 1; } ? z"), Value::Bool(false));
4410        assert_eq!(ev("{} ? anything"), Value::Bool(false));
4411    }
4412
4413    #[test]
4414    fn op_unary_negate() {
4415        assert_eq!(ev("-42"), Value::Int(-42));
4416        assert_eq!(ev("-3.14"), Value::Float(-3.14));
4417        // Double negate
4418        assert_eq!(ev("- -5"), Value::Int(5));
4419    }
4420
4421    // ═══════════════════════════════════════════════════════════
4422    // 3. CONTROL FLOW
4423    // ═══════════════════════════════════════════════════════════
4424
4425    #[test]
4426    fn control_if_true_branch() {
4427        assert_eq!(ev("if true then 42 else 0"), Value::Int(42));
4428    }
4429
4430    #[test]
4431    fn control_if_false_branch() {
4432        assert_eq!(ev("if false then 42 else 0"), Value::Int(0));
4433    }
4434
4435    #[test]
4436    fn control_if_nested() {
4437        assert_eq!(
4438            ev("if true then (if false then 1 else 2) else 3"),
4439            Value::Int(2),
4440        );
4441        assert_eq!(
4442            ev("if false then 1 else (if true then 2 else 3)"),
4443            Value::Int(2),
4444        );
4445    }
4446
4447    #[test]
4448    fn control_assert_passing() {
4449        assert_eq!(ev("assert 1 == 1; 42"), Value::Int(42));
4450        assert_eq!(ev("assert true; true"), Value::Bool(true));
4451    }
4452
4453    #[test]
4454    fn control_assert_failing() {
4455        assert!(eval("assert false; 42").is_err());
4456        assert!(eval("assert 1 == 2; 42").is_err());
4457    }
4458
4459    #[test]
4460    fn control_with_basic_scope() {
4461        assert_eq!(ev("with { a = 1; b = 2; }; a + b"), Value::Int(3));
4462    }
4463
4464    #[test]
4465    fn control_with_lexical_precedence() {
4466        // let binding takes precedence over with scope
4467        assert_eq!(
4468            ev("let x = 10; in with { x = 99; }; x"),
4469            Value::Int(10),
4470        );
4471    }
4472
4473    #[test]
4474    fn control_with_nested() {
4475        assert_eq!(
4476            ev("with { a = 1; }; with { b = 2; }; a + b"),
4477            Value::Int(3),
4478        );
4479    }
4480
4481    #[test]
4482    fn control_with_lazy_fix_self() {
4483        // THE critical pattern that nixpkgs requires:
4484        // fix (self: with self; { a = 1; b = a + 1; })
4485        // Before the lazy-with fix, this would hit the blackhole detector
4486        // because `with` eagerly forced `self`.
4487        let result = eval(
4488            "let fix = f: let x = f x; in x; in fix (self: with self; { a = 1; b = a + 1; })"
4489        );
4490        assert!(result.is_ok(), "fix with self should work: {:?}", result);
4491        if let Ok(Value::Attrs(attrs)) = result {
4492            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
4493            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
4494        } else {
4495            panic!("expected Attrs, got {:?}", result);
4496        }
4497    }
4498
4499    #[test]
4500    fn control_with_lazy_fix_self_lib_pattern() {
4501        // The nixpkgs pattern: self-referential package set with lib.
4502        // Access via select to force through the thunk layer.
4503        let result = eval(r#"
4504            let fix = f: let x = f x; in x;
4505            in (fix (self: with self; {
4506                lib = { version = "1.0"; };
4507                hello = "hello ${lib.version}";
4508            })).hello
4509        "#);
4510        assert!(result.is_ok(), "nixpkgs-style lib pattern: {:?}", result);
4511        assert_eq!(
4512            result.unwrap(),
4513            Value::String(Rc::new(NixString::plain("hello 1.0"))),
4514        );
4515    }
4516
4517    #[test]
4518    fn control_with_non_attrset_errors() {
4519        // CppNix errors when with-scope is not an attrset and a lookup hits it
4520        let result = eval("with 42; 1");
4521        // The body `1` is a literal and doesn't look up anything in the
4522        // with-scope, so this should succeed (the scope is never forced).
4523        assert_eq!(result.unwrap(), Value::Int(1));
4524    }
4525
4526    #[test]
4527    fn control_with_non_attrset_lookup_falls_through() {
4528        // If the with scope is not an attrset, lookups should fall through
4529        // to outer scopes rather than crashing.
4530        let result = eval("let x = 1; in with 42; x");
4531        assert_eq!(result.unwrap(), Value::Int(1));
4532    }
4533
4534    #[test]
4535    fn control_let_simple_and_multiple() {
4536        assert_eq!(ev("let x = 5; in x"), Value::Int(5));
4537        assert_eq!(ev("let x = 1; y = 2; z = 3; in x + y + z"), Value::Int(6));
4538    }
4539
4540    #[test]
4541    fn control_let_shadow_outer() {
4542        assert_eq!(
4543            ev("let x = 1; in let x = 2; in x"),
4544            Value::Int(2),
4545        );
4546    }
4547
4548    #[test]
4549    fn control_let_recursive_reference() {
4550        assert_eq!(ev("let a = 1; b = a + 1; in b"), Value::Int(2));
4551        assert_eq!(ev("let a = 1; b = a + 1; c = b + 1; in c"), Value::Int(3));
4552    }
4553
4554    #[test]
4555    fn control_nested_let_expression() {
4556        assert_eq!(
4557            ev("let a = let b = 1; in b; in a"),
4558            Value::Int(1),
4559        );
4560        assert_eq!(
4561            ev("let a = let b = 10; in b + 5; in a * 2"),
4562            Value::Int(30),
4563        );
4564    }
4565
4566    // ═══════════════════════════════════════════════════════════
4567    // 4. FUNCTIONS — COMPLETE COVERAGE
4568    // ═══════════════════════════════════════════════════════════
4569
4570    #[test]
4571    fn func_identity_lambda() {
4572        assert_eq!(ev("(x: x) 42"), Value::Int(42));
4573        assert_eq!(ev(r#"(x: x) "hello""#), Value::string("hello"));
4574    }
4575
4576    #[test]
4577    fn func_curried_two_args() {
4578        assert_eq!(ev("(x: y: x + y) 3 4"), Value::Int(7));
4579    }
4580
4581    #[test]
4582    fn func_curried_three_args() {
4583        assert_eq!(ev("(a: b: c: a + b + c) 1 2 3"), Value::Int(6));
4584    }
4585
4586    #[test]
4587    fn func_formals_basic() {
4588        assert_eq!(ev("({ a, b }: a + b) { a = 3; b = 7; }"), Value::Int(10));
4589    }
4590
4591    #[test]
4592    fn func_formals_with_defaults() {
4593        assert_eq!(ev("({ a, b ? 10 }: a + b) { a = 5; }"), Value::Int(15));
4594        // Providing the default-able argument overrides the default
4595        assert_eq!(ev("({ a, b ? 10 }: a + b) { a = 5; b = 20; }"), Value::Int(25));
4596    }
4597
4598    #[test]
4599    fn func_formals_with_ellipsis() {
4600        assert_eq!(ev("({ a, ... }: a) { a = 1; b = 2; c = 3; }"), Value::Int(1));
4601    }
4602
4603    #[test]
4604    fn func_named_formals_at_before() {
4605        // args @ { a, b }: ...
4606        assert_eq!(
4607            ev("(args @ { a, b }: args.a + args.b) { a = 3; b = 4; }"),
4608            Value::Int(7),
4609        );
4610    }
4611
4612    #[test]
4613    fn func_named_formals_at_after() {
4614        // { a, b } @ args: ...
4615        assert_eq!(
4616            ev("({ a, b } @ args: args.a + args.b) { a = 10; b = 20; }"),
4617            Value::Int(30),
4618        );
4619    }
4620
4621    #[test]
4622    fn func_nested_application() {
4623        // Explicit parenthesized application
4624        assert_eq!(ev("((x: y: x * y) 3) 4"), Value::Int(12));
4625    }
4626
4627    #[test]
4628    fn func_higher_order_map() {
4629        assert_eq!(
4630            ev("builtins.map (x: x * 2) [1 2 3]"),
4631            Value::list(vec![Value::Int(2), Value::Int(4), Value::Int(6)]),
4632        );
4633    }
4634
4635    #[test]
4636    fn func_higher_order_filter() {
4637        assert_eq!(
4638            ev("builtins.filter (x: x > 2) [1 2 3 4 5]"),
4639            Value::list(vec![Value::Int(3), Value::Int(4), Value::Int(5)]),
4640        );
4641    }
4642
4643    #[test]
4644    fn func_higher_order_foldl() {
4645        // Sum of list via foldl'
4646        assert_eq!(
4647            ev("builtins.foldl' (acc: x: acc + x) 0 [1 2 3 4]"),
4648            Value::Int(10),
4649        );
4650    }
4651
4652    #[test]
4653    fn func_as_attrset_value() {
4654        assert_eq!(
4655            ev("let s = { f = x: x + 1; }; in s.f 5"),
4656            Value::Int(6),
4657        );
4658    }
4659
4660    #[test]
4661    fn func_immediate_application() {
4662        assert_eq!(ev("(x: x * x) 7"), Value::Int(49));
4663    }
4664
4665    #[test]
4666    fn func_in_let_binding() {
4667        assert_eq!(
4668            ev("let double = x: x * 2; in double 21"),
4669            Value::Int(42),
4670        );
4671    }
4672
4673    // ═══════════════════════════════════════════════════════════
4674    // 5. ATTRIBUTE SETS — COMPLETE COVERAGE
4675    // ═══════════════════════════════════════════════════════════
4676
4677    #[test]
4678    fn attrs_empty_set() {
4679        let v = ev("{}");
4680        if let Value::Attrs(attrs) = v {
4681            assert!(attrs.is_empty());
4682        } else {
4683            panic!("expected attrs");
4684        }
4685    }
4686
4687    #[test]
4688    fn attrs_simple() {
4689        assert_eq!(ev("{ a = 1; }.a"), Value::Int(1));
4690    }
4691
4692    #[test]
4693    fn attrs_nested_access() {
4694        assert_eq!(ev("{ a = { b = { c = 42; }; }; }.a.b.c"), Value::Int(42));
4695    }
4696
4697    #[test]
4698    fn attrs_recursive_set() {
4699        assert_eq!(ev("(rec { a = 1; b = a + 1; c = b + 1; }).c"), Value::Int(3));
4700    }
4701
4702    #[test]
4703    fn attrs_update_disjoint() {
4704        let v = ev("{ a = 1; } // { b = 2; }");
4705        if let Value::Attrs(attrs) = v {
4706            assert_eq!(attrs.len(), 2);
4707            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
4708            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
4709        } else {
4710            panic!("expected attrs");
4711        }
4712    }
4713
4714    #[test]
4715    fn attrs_update_override() {
4716        assert_eq!(ev("({ a = 1; } // { a = 2; }).a"), Value::Int(2));
4717    }
4718
4719    #[test]
4720    fn attrs_has_attr_operator() {
4721        assert_eq!(ev("{ a = 1; } ? a"), Value::Bool(true));
4722        assert_eq!(ev("{ a = 1; } ? b"), Value::Bool(false));
4723    }
4724
4725    #[test]
4726    fn attrs_select_with_default() {
4727        assert_eq!(ev("{ a = 1; }.a or 99"), Value::Int(1));
4728        assert_eq!(ev("{}.missing or 99"), Value::Int(99));
4729        assert_eq!(ev("{ a = 1; }.b or 42"), Value::Int(42));
4730    }
4731
4732    #[test]
4733    fn attrs_nested_attr_path_in_binding() {
4734        // { a.b = 1; } creates { a = { b = 1; }; }
4735        assert_eq!(ev("{ a.b = 1; }.a.b"), Value::Int(1));
4736    }
4737
4738    #[test]
4739    fn attrs_inherit_from_scope() {
4740        assert_eq!(ev("let x = 1; y = 2; in { inherit x y; }.x"), Value::Int(1));
4741        assert_eq!(ev("let x = 1; y = 2; in { inherit x y; }.y"), Value::Int(2));
4742    }
4743
4744    #[test]
4745    fn attrs_inherit_from_expr() {
4746        assert_eq!(
4747            ev("{ inherit ({ a = 42; b = 10; }) a; }.a"),
4748            Value::Int(42),
4749        );
4750    }
4751
4752    #[test]
4753    fn attrs_dynamic_attr_name() {
4754        assert_eq!(
4755            ev(r#"let name = "x"; in { ${name} = 42; }.x"#),
4756            Value::Int(42),
4757        );
4758    }
4759
4760    #[test]
4761    fn attrs_attr_names_sorted() {
4762        assert_eq!(
4763            ev("builtins.attrNames { z = 1; m = 2; a = 3; }"),
4764            Value::list(vec![
4765                Value::string("a"),
4766                Value::string("m"),
4767                Value::string("z"),
4768            ]),
4769        );
4770    }
4771
4772    #[test]
4773    fn attrs_attr_values_follow_key_order() {
4774        // BTreeMap iteration order: a=1, b=2, c=3
4775        assert_eq!(
4776            ev("builtins.attrValues { c = 3; a = 1; b = 2; }"),
4777            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
4778        );
4779    }
4780
4781    #[test]
4782    fn attrs_update_is_shallow() {
4783        // // is a shallow merge; nested attrs are replaced, not merged
4784        assert_eq!(
4785            ev("({ a = { x = 1; }; } // { a = { y = 2; }; }).a ? x"),
4786            Value::Bool(false),
4787        );
4788        assert_eq!(
4789            ev("({ a = { x = 1; }; } // { a = { y = 2; }; }).a.y"),
4790            Value::Int(2),
4791        );
4792    }
4793
4794    // ═══════════════════════════════════════════════════════════
4795    // 6. LISTS — COMPLETE COVERAGE
4796    // ═══════════════════════════════════════════════════════════
4797
4798    #[test]
4799    fn list_empty() {
4800        assert_eq!(ev("[]"), Value::list(vec![]));
4801    }
4802
4803    #[test]
4804    fn list_single_element() {
4805        assert_eq!(ev("[1]"), Value::list(vec![Value::Int(1)]));
4806    }
4807
4808    #[test]
4809    fn list_mixed_types() {
4810        assert_eq!(
4811            ev(r#"[1 "two" true null]"#),
4812            Value::list(vec![
4813                Value::Int(1),
4814                Value::string("two"),
4815                Value::Bool(true),
4816                Value::Null,
4817            ]),
4818        );
4819    }
4820
4821    #[test]
4822    fn list_nested() {
4823        assert_eq!(
4824            ev("[[1 2] [3 4]]"),
4825            Value::list(vec![
4826                Value::list(vec![Value::Int(1), Value::Int(2)]),
4827                Value::list(vec![Value::Int(3), Value::Int(4)]),
4828            ]),
4829        );
4830    }
4831
4832    #[test]
4833    fn list_concat_operator() {
4834        assert_eq!(
4835            ev("[1] ++ [2] ++ [3]"),
4836            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
4837        );
4838    }
4839
4840    #[test]
4841    fn list_builtins_length() {
4842        assert_eq!(ev("builtins.length [1 2 3]"), Value::Int(3));
4843        assert_eq!(ev("builtins.length []"), Value::Int(0));
4844    }
4845
4846    #[test]
4847    fn list_builtins_elem_at() {
4848        assert_eq!(ev("builtins.elemAt [10 20 30] 0"), Value::Int(10));
4849        assert_eq!(ev("builtins.elemAt [10 20 30] 1"), Value::Int(20));
4850        assert_eq!(ev("builtins.elemAt [10 20 30] 2"), Value::Int(30));
4851    }
4852
4853    #[test]
4854    fn list_equality() {
4855        assert_eq!(ev("[1 2 3] == [1 2 3]"), Value::Bool(true));
4856        assert_eq!(ev("[1 2] == [1 2 3]"), Value::Bool(false));
4857        assert_eq!(ev("[] == []"), Value::Bool(true));
4858    }
4859
4860    // ═══════════════════════════════════════════════════════════
4861    // 7. STRING INTERPOLATION
4862    // ═══════════════════════════════════════════════════════════
4863
4864    #[test]
4865    fn interp_simple_variable() {
4866        assert_eq!(
4867            ev(r#"let name = "world"; in "hello ${name}""#),
4868            Value::string("hello world"),
4869        );
4870    }
4871
4872    #[test]
4873    fn interp_nested_expression() {
4874        assert_eq!(
4875            ev(r#""result: ${builtins.toString (1 + 2)}""#),
4876            Value::string("result: 3"),
4877        );
4878    }
4879
4880    #[test]
4881    fn interp_int_coercion() {
4882        // Ints are coerced to string in interpolation
4883        assert_eq!(
4884            ev(r#"let x = 42; in "count: ${builtins.toString x}""#),
4885            Value::string("count: 42"),
4886        );
4887    }
4888
4889    #[test]
4890    fn interp_multiple() {
4891        assert_eq!(
4892            ev(r#"let a = "foo"; b = "bar"; in "${a} and ${b}""#),
4893            Value::string("foo and bar"),
4894        );
4895    }
4896
4897    #[test]
4898    fn interp_in_let() {
4899        assert_eq!(
4900            ev(r#"let x = "world"; in "hello ${x}""#),
4901            Value::string("hello world"),
4902        );
4903    }
4904
4905    #[test]
4906    fn interp_empty_result() {
4907        assert_eq!(
4908            ev(r#"let x = ""; in "a${x}b""#),
4909            Value::string("ab"),
4910        );
4911    }
4912
4913    #[test]
4914    fn interp_path_in_string_context() {
4915        // CppNix string interpolation is copy-to-store coercion: a nonexistent
4916        // path errors "path '…' does not exist" (previously sui spliced the raw
4917        // relative path "./foo" verbatim, diverging from nix). The positive
4918        // copy-to-store case is byte-verified in
4919        // interp_path_copies_to_store_byte_matches_cppnix below.
4920        assert!(eval(r#""path: ${./foo-nonexistent-xyz}""#).is_err());
4921    }
4922
4923    #[test]
4924    fn interp_adjacent_interpolations() {
4925        assert_eq!(
4926            ev(r#"let a = "x"; b = "y"; in "${a}${b}""#),
4927            Value::string("xy"),
4928        );
4929    }
4930
4931    // ═══════════════════════════════════════════════════════════
4932    // 8. BUILTINS — VERIFY ALL MAJOR ONES
4933    // ═══════════════════════════════════════════════════════════
4934
4935    #[test]
4936    fn builtins_map_filter_foldl() {
4937        // map
4938        assert_eq!(
4939            ev("builtins.map (x: x + 10) [1 2 3]"),
4940            Value::list(vec![Value::Int(11), Value::Int(12), Value::Int(13)]),
4941        );
4942        // filter
4943        assert_eq!(
4944            ev("builtins.filter (x: x > 1) [1 2 3]"),
4945            Value::list(vec![Value::Int(2), Value::Int(3)]),
4946        );
4947        // foldl' — product
4948        assert_eq!(
4949            ev("builtins.foldl' (a: b: a * b) 1 [2 3 4]"),
4950            Value::Int(24),
4951        );
4952    }
4953
4954    #[test]
4955    fn builtins_map_attrs() {
4956        assert_eq!(
4957            ev("(builtins.mapAttrs (name: value: value * 2) { a = 1; b = 2; }).a"),
4958            Value::Int(2),
4959        );
4960        assert_eq!(
4961            ev("(builtins.mapAttrs (name: value: value * 2) { a = 1; b = 2; }).b"),
4962            Value::Int(4),
4963        );
4964    }
4965
4966    #[test]
4967    fn builtins_list_to_attrs() {
4968        assert_eq!(
4969            ev(r#"(builtins.listToAttrs [{ name = "x"; value = 1; } { name = "y"; value = 2; }]).x"#),
4970            Value::Int(1),
4971        );
4972    }
4973
4974    #[test]
4975    fn builtins_list_to_attrs_duplicate_key_first_wins() {
4976        // Nix `listToAttrs` keeps the FIRST occurrence of a duplicate `name`
4977        // (later duplicates are ignored). cppnix returns 1 here, not 2.
4978        // Byte-parity root (cid darwin): a Cargo.lock listing a crate twice
4979        // (registry entry then git entry of the same name+version) must
4980        // resolve to the FIRST source, so `substrate/lockfile-delta.nix`'s
4981        // `lockByKey` picks the registry crate exactly as nix does. Last-wins
4982        // silently switched the source to git and produced a structurally
4983        // different `rust_<crate>` derivation.
4984        assert_eq!(
4985            ev(r#"(builtins.listToAttrs [{ name = "k"; value = 1; } { name = "k"; value = 2; }]).k"#),
4986            Value::Int(1),
4987        );
4988    }
4989
4990    #[test]
4991    fn builtins_concat_map() {
4992        assert_eq!(
4993            ev("builtins.concatMap (x: [x (x * 2)]) [1 2 3]"),
4994            Value::list(vec![
4995                Value::Int(1), Value::Int(2),
4996                Value::Int(2), Value::Int(4),
4997                Value::Int(3), Value::Int(6),
4998            ]),
4999        );
5000    }
5001
5002    #[test]
5003    fn builtins_concat_lists() {
5004        assert_eq!(
5005            ev("builtins.concatLists [[1 2] [3] [4 5]]"),
5006            Value::list(vec![
5007                Value::Int(1), Value::Int(2), Value::Int(3),
5008                Value::Int(4), Value::Int(5),
5009            ]),
5010        );
5011    }
5012
5013    #[test]
5014    fn builtins_concat_strings_sep() {
5015        assert_eq!(
5016            ev(r#"builtins.concatStringsSep ", " ["a" "b" "c"]"#),
5017            Value::string("a, b, c"),
5018        );
5019        assert_eq!(
5020            ev(r#"builtins.concatStringsSep "" ["x" "y"]"#),
5021            Value::string("xy"),
5022        );
5023    }
5024
5025    #[test]
5026    fn builtins_replace_strings() {
5027        assert_eq!(
5028            ev(r#"builtins.replaceStrings ["o"] ["0"] "foobar""#),
5029            Value::string("f00bar"),
5030        );
5031        assert_eq!(
5032            ev(r#"builtins.replaceStrings ["hello"] ["goodbye"] "hello world""#),
5033            Value::string("goodbye world"),
5034        );
5035    }
5036
5037    #[test]
5038    fn builtins_has_prefix_has_suffix() {
5039        assert_eq!(ev(r#"builtins.hasPrefix "he" "hello""#), Value::Bool(true));
5040        assert_eq!(ev(r#"builtins.hasPrefix "xx" "hello""#), Value::Bool(false));
5041        assert_eq!(ev(r#"builtins.hasSuffix "lo" "hello""#), Value::Bool(true));
5042        assert_eq!(ev(r#"builtins.hasSuffix "xx" "hello""#), Value::Bool(false));
5043    }
5044
5045    #[test]
5046    fn builtins_all_any() {
5047        assert_eq!(ev("builtins.all (x: x > 0) [1 2 3]"), Value::Bool(true));
5048        assert_eq!(ev("builtins.all (x: x > 1) [1 2 3]"), Value::Bool(false));
5049        assert_eq!(ev("builtins.any (x: x > 2) [1 2 3]"), Value::Bool(true));
5050        assert_eq!(ev("builtins.any (x: x > 5) [1 2 3]"), Value::Bool(false));
5051    }
5052
5053    #[test]
5054    fn builtins_sort() {
5055        assert_eq!(
5056            ev("builtins.sort (a: b: a < b) [3 1 2]"),
5057            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
5058        );
5059    }
5060
5061    #[test]
5062    fn builtins_remove_attrs() {
5063        let v = ev(r#"builtins.removeAttrs { a = 1; b = 2; c = 3; } ["b" "c"]"#);
5064        if let Value::Attrs(attrs) = v {
5065            assert_eq!(attrs.len(), 1);
5066            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
5067            assert!(attrs.get("b").is_none());
5068        } else {
5069            panic!("expected attrs");
5070        }
5071    }
5072
5073    #[test]
5074    fn builtins_intersect_attrs() {
5075        let v = ev("builtins.intersectAttrs { a = 1; b = 2; } { b = 20; c = 30; }");
5076        if let Value::Attrs(attrs) = v {
5077            assert_eq!(attrs.len(), 1);
5078            // intersectAttrs returns values from the second set
5079            assert_eq!(attrs.get("b"), Some(&Value::Int(20)));
5080        } else {
5081            panic!("expected attrs");
5082        }
5083    }
5084
5085    #[test]
5086    fn builtins_type_of_all_types() {
5087        assert_eq!(ev("builtins.typeOf null"), Value::string("null"));
5088        assert_eq!(ev("builtins.typeOf true"), Value::string("bool"));
5089        assert_eq!(ev("builtins.typeOf 42"), Value::string("int"));
5090        assert_eq!(ev("builtins.typeOf 3.14"), Value::string("float"));
5091        assert_eq!(ev(r#"builtins.typeOf "hi""#), Value::string("string"));
5092        assert_eq!(ev("builtins.typeOf [1]"), Value::string("list"));
5093        assert_eq!(ev("builtins.typeOf {}"), Value::string("set"));
5094        assert_eq!(ev("builtins.typeOf (x: x)"), Value::string("lambda"));
5095    }
5096
5097    #[test]
5098    fn builtins_is_type_checks() {
5099        assert_eq!(ev("builtins.isNull null"), Value::Bool(true));
5100        assert_eq!(ev("builtins.isNull 0"), Value::Bool(false));
5101        assert_eq!(ev("builtins.isInt 42"), Value::Bool(true));
5102        assert_eq!(ev("builtins.isInt 3.14"), Value::Bool(false));
5103        assert_eq!(ev("builtins.isBool true"), Value::Bool(true));
5104        assert_eq!(ev("builtins.isBool 1"), Value::Bool(false));
5105        assert_eq!(ev(r#"builtins.isString "x""#), Value::Bool(true));
5106        assert_eq!(ev("builtins.isString 1"), Value::Bool(false));
5107        assert_eq!(ev("builtins.isList []"), Value::Bool(true));
5108        assert_eq!(ev("builtins.isList {}"), Value::Bool(false));
5109        assert_eq!(ev("builtins.isAttrs {}"), Value::Bool(true));
5110        assert_eq!(ev("builtins.isAttrs []"), Value::Bool(false));
5111        assert_eq!(ev("builtins.isFunction (x: x)"), Value::Bool(true));
5112        assert_eq!(ev("builtins.isFunction 1"), Value::Bool(false));
5113        assert_eq!(ev("builtins.isFloat 3.14"), Value::Bool(true));
5114        assert_eq!(ev("builtins.isFloat 1"), Value::Bool(false));
5115    }
5116
5117    #[test]
5118    fn builtins_to_json_from_json_roundtrip() {
5119        // int roundtrip
5120        assert_eq!(ev("builtins.fromJSON (builtins.toJSON 42)"), Value::Int(42));
5121        // string roundtrip
5122        assert_eq!(
5123            ev(r#"builtins.fromJSON (builtins.toJSON "hello")"#),
5124            Value::string("hello"),
5125        );
5126        // list roundtrip
5127        assert_eq!(
5128            ev("builtins.fromJSON (builtins.toJSON [1 2 3])"),
5129            Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
5130        );
5131        // null roundtrip
5132        assert_eq!(ev("builtins.fromJSON (builtins.toJSON null)"), Value::Null);
5133        // bool roundtrip
5134        assert_eq!(ev("builtins.fromJSON (builtins.toJSON true)"), Value::Bool(true));
5135    }
5136
5137    #[test]
5138    fn builtins_to_string_various() {
5139        assert_eq!(ev("builtins.toString 42"), Value::string("42"));
5140        assert_eq!(ev("builtins.toString true"), Value::string("1"));
5141        assert_eq!(ev("builtins.toString false"), Value::string(""));
5142        assert_eq!(ev("builtins.toString null"), Value::string(""));
5143        assert_eq!(ev(r#"builtins.toString "hello""#), Value::string("hello"));
5144    }
5145
5146    #[test]
5147    fn builtins_function_args() {
5148        let v = ev("builtins.functionArgs ({ a, b ? 1 }: a)");
5149        if let Value::Attrs(attrs) = v {
5150            assert_eq!(attrs.get("a"), Some(&Value::Bool(false))); // no default
5151            assert_eq!(attrs.get("b"), Some(&Value::Bool(true)));  // has default
5152        } else {
5153            panic!("expected attrs");
5154        }
5155    }
5156
5157    #[test]
5158    fn builtins_gen_list() {
5159        assert_eq!(
5160            ev("builtins.genList (x: x * x) 5"),
5161            Value::list(vec![
5162                Value::Int(0), Value::Int(1), Value::Int(4),
5163                Value::Int(9), Value::Int(16),
5164            ]),
5165        );
5166        assert_eq!(ev("builtins.genList (x: x) 0"), Value::list(vec![]));
5167    }
5168
5169    #[test]
5170    fn builtins_elem() {
5171        assert_eq!(ev("builtins.elem 2 [1 2 3]"), Value::Bool(true));
5172        assert_eq!(ev("builtins.elem 5 [1 2 3]"), Value::Bool(false));
5173        assert_eq!(ev("builtins.elem 1 []"), Value::Bool(false));
5174    }
5175
5176    #[test]
5177    fn builtins_head_tail() {
5178        assert_eq!(ev("builtins.head [10 20 30]"), Value::Int(10));
5179        assert_eq!(
5180            ev("builtins.tail [10 20 30]"),
5181            Value::list(vec![Value::Int(20), Value::Int(30)]),
5182        );
5183    }
5184
5185    #[test]
5186    fn builtins_string_length() {
5187        assert_eq!(ev(r#"builtins.stringLength "hello""#), Value::Int(5));
5188        assert_eq!(ev(r#"builtins.stringLength """#), Value::Int(0));
5189        assert_eq!(ev(r#"builtins.stringLength "abc def""#), Value::Int(7));
5190    }
5191
5192    #[test]
5193    fn builtins_ceil_floor() {
5194        assert_eq!(ev("builtins.ceil 2.3"), Value::Int(3));
5195        assert_eq!(ev("builtins.ceil 2.0"), Value::Int(2));
5196        assert_eq!(ev("builtins.floor 2.9"), Value::Int(2));
5197        assert_eq!(ev("builtins.floor 2.0"), Value::Int(2));
5198        // Int coercion: ceil/floor on int should work via to_float()
5199        assert_eq!(ev("builtins.ceil 5"), Value::Int(5));
5200        assert_eq!(ev("builtins.floor 5"), Value::Int(5));
5201    }
5202
5203    #[test]
5204    fn builtins_try_eval() {
5205        let v = ev("builtins.tryEval 42");
5206        if let Value::Attrs(attrs) = v {
5207            assert_eq!(attrs.get("success"), Some(&Value::Bool(true)));
5208            assert_eq!(attrs.get("value"), Some(&Value::Int(42)));
5209        } else {
5210            panic!("expected attrs");
5211        }
5212    }
5213
5214    #[test]
5215    fn builtins_throw() {
5216        let result = eval(r#"builtins.throw "oops""#);
5217        assert!(result.is_err());
5218        let msg = format!("{}", result.unwrap_err());
5219        assert!(msg.contains("oops"));
5220    }
5221
5222    #[test]
5223    fn builtins_seq_deep_seq() {
5224        // seq forces first arg, returns second
5225        assert_eq!(ev("builtins.seq 1 42"), Value::Int(42));
5226        // deepSeq similarly
5227        assert_eq!(ev("builtins.deepSeq [1 2 3] 99"), Value::Int(99));
5228    }
5229
5230    #[test]
5231    fn builtins_current_system() {
5232        let v = ev("builtins.currentSystem");
5233        if let Value::String(ns) = v {
5234            let s = &ns.chars;
5235            // Should be a valid system string
5236            assert!(
5237                s == "aarch64-darwin"
5238                    || s == "x86_64-darwin"
5239                    || s == "aarch64-linux"
5240                    || s == "x86_64-linux",
5241                "unexpected system: {s}",
5242            );
5243        } else {
5244            panic!("expected string");
5245        }
5246    }
5247
5248    // ═══════════════════════════════════════════════════════════
5249    // 9. REAL-WORLD NIXPKGS PATTERNS
5250    // ═══════════════════════════════════════════════════════════
5251
5252    #[test]
5253    fn pattern_mkif_like() {
5254        // lib.mkIf pattern: if condition then { key = value; } else {}
5255        assert_eq!(
5256            ev("(if true then { x = 1; } else {}).x"),
5257            Value::Int(1),
5258        );
5259        let v = ev("if false then { x = 1; } else {}");
5260        if let Value::Attrs(attrs) = v {
5261            assert!(attrs.is_empty());
5262        } else {
5263            panic!("expected attrs");
5264        }
5265    }
5266
5267    #[test]
5268    fn pattern_optional_attrs() {
5269        // lib.optionalAttrs pattern
5270        assert_eq!(
5271            ev("let optionalAttrs = cond: attrs: if cond then attrs else {}; in (optionalAttrs true { a = 1; }).a"),
5272            Value::Int(1),
5273        );
5274        let v = ev("let optionalAttrs = cond: attrs: if cond then attrs else {}; in optionalAttrs false { a = 1; }");
5275        if let Value::Attrs(attrs) = v {
5276            assert!(attrs.is_empty());
5277        } else {
5278            panic!("expected attrs");
5279        }
5280    }
5281
5282    #[test]
5283    fn pattern_filter_attrs_via_remove() {
5284        // lib.filterAttrs pattern via removeAttrs
5285        assert_eq!(
5286            ev(r#"(builtins.removeAttrs { a = 1; b = 2; c = 3; } ["b"]).a"#),
5287            Value::Int(1),
5288        );
5289        assert_eq!(
5290            ev(r#"(builtins.removeAttrs { a = 1; b = 2; c = 3; } ["b"]) ? b"#),
5291            Value::Bool(false),
5292        );
5293    }
5294
5295    #[test]
5296    fn pattern_override() {
5297        // default // overrides pattern
5298        let v = ev(r#"
5299            let
5300                defaults = { debug = false; port = 8080; host = "localhost"; };
5301                overrides = { debug = true; port = 9090; };
5302            in defaults // overrides
5303        "#);
5304        if let Value::Attrs(attrs) = v {
5305            assert_eq!(attrs.get("debug"), Some(&Value::Bool(true)));
5306            assert_eq!(attrs.get("port"), Some(&Value::Int(9090)));
5307            assert_eq!(attrs.get("host"), Some(&Value::string("localhost")));
5308        } else {
5309            panic!("expected attrs");
5310        }
5311    }
5312
5313    #[test]
5314    fn pattern_functor() {
5315        // { __functor = self: x: self.value + x; value = 10; } 5
5316        assert_eq!(
5317            ev("let s = { __functor = self: x: self.value + x; value = 10; }; in s 5"),
5318            Value::Int(15),
5319        );
5320    }
5321
5322    #[test]
5323    fn pattern_platform_check() {
5324        // Check pattern: if builtins.currentSystem == "..." then ... else ...
5325        let v = ev(r#"if builtins.currentSystem == "aarch64-darwin" then "arm" else "other""#);
5326        // We just verify it evaluates without error and produces a string
5327        if let Value::String(_) = v {
5328            // ok
5329        } else {
5330            panic!("expected string");
5331        }
5332    }
5333
5334    #[test]
5335    fn pattern_recursive_overlay_lambda_structure() {
5336        // Test the lambda structure of an overlay (self: super: { ... })
5337        let v = ev("let overlay = self: super: { pkg = 42; }; in overlay {} {}");
5338        if let Value::Attrs(attrs) = v {
5339            assert_eq!(attrs.get("pkg"), Some(&Value::Int(42)));
5340        } else {
5341            panic!("expected attrs");
5342        }
5343    }
5344
5345    #[test]
5346    fn pattern_call_package_simplified() {
5347        // Simplified callPackage: f: f { inherit lib; }
5348        assert_eq!(
5349            ev("let callPkg = f: f { lib = { id = x: x; }; }; lib = { id = x: x; }; in callPkg ({ lib }: lib.id 42)"),
5350            Value::Int(42),
5351        );
5352    }
5353
5354    #[test]
5355    fn pattern_derivation_like_attrset() {
5356        let v = ev(r#"{ type = "derivation"; name = "hello"; system = builtins.currentSystem; builder = "/bin/sh"; }"#);
5357        if let Value::Attrs(attrs) = v {
5358            assert_eq!(attrs.get("type"), Some(&Value::string("derivation")));
5359            assert_eq!(attrs.get("name"), Some(&Value::string("hello")));
5360            assert_eq!(attrs.get("builder"), Some(&Value::string("/bin/sh")));
5361            // system should be a string (may be a thunk that forces to string)
5362            let system = force_value(attrs.get("system").unwrap()).unwrap();
5363            assert!(matches!(system, Value::String(_)), "expected string, got {system:?}");
5364        } else {
5365            panic!("expected attrs");
5366        }
5367    }
5368
5369    #[test]
5370    fn pattern_module_system_simplified() {
5371        // Simplified NixOS module evaluation
5372        assert_eq!(
5373            ev(r#"
5374                let
5375                    eval = m: m { config = {}; lib = { mkDefault = x: x; }; };
5376                in eval ({ config, lib }: { result = lib.mkDefault 42; })
5377            "#),
5378            {
5379                let mut attrs = NixAttrs::new();
5380                attrs.insert("result".to_string(), Value::Int(42));
5381                Value::Attrs(Rc::new(attrs))
5382            },
5383        );
5384    }
5385
5386    // ═══════════════════════════════════════════════════════════
5387    // 10. ERROR HANDLING
5388    // ═══════════════════════════════════════════════════════════
5389
5390    #[test]
5391    fn error_undefined_variable() {
5392        let result = eval("nonexistent_var");
5393        assert!(result.is_err());
5394        let msg = format!("{}", result.unwrap_err());
5395        assert!(msg.contains("undefined variable") || msg.contains("nonexistent_var"));
5396    }
5397
5398    #[test]
5399    fn error_type_mismatch_arithmetic() {
5400        let result = eval(r#"1 + "hello""#);
5401        assert!(result.is_err());
5402    }
5403
5404    #[test]
5405    fn error_missing_attribute() {
5406        let result = eval("{}.nonexistent");
5407        assert!(result.is_err());
5408        let msg = format!("{}", result.unwrap_err());
5409        assert!(msg.contains("nonexistent") || msg.contains("not found"));
5410    }
5411
5412    #[test]
5413    fn error_division_by_zero() {
5414        assert!(eval("1 / 0").is_err());
5415        assert!(eval("100 / 0").is_err());
5416    }
5417
5418    #[test]
5419    fn error_missing_required_function_arg() {
5420        let result = eval("({ a, b }: a + b) { a = 1; }");
5421        assert!(result.is_err());
5422        let msg = format!("{}", result.unwrap_err());
5423        assert!(msg.contains("missing argument"));
5424    }
5425
5426    #[test]
5427    fn error_unexpected_function_arg() {
5428        let result = eval("({ a }: a) { a = 1; b = 2; }");
5429        assert!(result.is_err());
5430        let msg = format!("{}", result.unwrap_err());
5431        assert!(msg.contains("unexpected argument"));
5432    }
5433
5434    #[test]
5435    fn error_assertion_failure() {
5436        assert!(eval("assert false; 1").is_err());
5437        assert!(eval("assert 1 == 2; 1").is_err());
5438    }
5439
5440    #[test]
5441    fn error_infinite_recursion() {
5442        // `let x = x; in x` should either hit the depth guard or fail on
5443        // undefined variable (since sequential let can't see its own binding).
5444        let result = eval("let x = x; in x");
5445        assert!(result.is_err());
5446    }
5447
5448    #[test]
5449    fn error_infinite_recursion_via_lambda() {
5450        // A true infinite recursion via self-application -- depth guard catches this.
5451        let result = eval("let f = x: f x; in f 1");
5452        assert!(result.is_err());
5453        let msg = format!("{}", result.unwrap_err());
5454        assert!(
5455            msg.contains("infinite recursion") || msg.contains("eval depth") || msg.contains("undefined"),
5456        );
5457    }
5458
5459    // ═══════════════════════════════════════════════════════════
5460    // ADDITIONAL COVERAGE: edge cases and integration
5461    // ═══════════════════════════════════════════════════════════
5462
5463    #[test]
5464    fn integration_let_with_function_returning_attrset() {
5465        assert_eq!(
5466            ev("let mkPkg = name: { inherit name; version = 1; }; in (mkPkg \"hello\").name"),
5467            Value::string("hello"),
5468        );
5469    }
5470
5471    #[test]
5472    fn integration_chained_updates() {
5473        assert_eq!(
5474            ev("({ a = 1; } // { b = 2; } // { c = 3; }).c"),
5475            Value::Int(3),
5476        );
5477    }
5478
5479    #[test]
5480    fn integration_map_over_attrnames() {
5481        // Common nixpkgs pattern: map over attrNames
5482        assert_eq!(
5483            ev(r#"
5484                let
5485                    set = { a = 1; b = 2; };
5486                    names = builtins.attrNames set;
5487                in builtins.length names
5488            "#),
5489            Value::Int(2),
5490        );
5491    }
5492
5493    #[test]
5494    fn integration_compose_functions() {
5495        // Function composition
5496        assert_eq!(
5497            ev("let compose = f: g: x: f (g x); double = x: x * 2; inc = x: x + 1; in compose double inc 5"),
5498            Value::Int(12), // (5 + 1) * 2
5499        );
5500    }
5501
5502    #[test]
5503    fn integration_recursive_list_building() {
5504        // Build a list using genList and map
5505        assert_eq!(
5506            ev("builtins.map (x: x * x) (builtins.genList (x: x + 1) 4)"),
5507            Value::list(vec![Value::Int(1), Value::Int(4), Value::Int(9), Value::Int(16)]),
5508        );
5509    }
5510
5511    #[test]
5512    fn integration_attrset_from_list() {
5513        // Convert list to attrset via listToAttrs + map
5514        let v = ev(r#"
5515            builtins.listToAttrs (builtins.map (x: { name = x; value = true; }) ["a" "b" "c"])
5516        "#);
5517        if let Value::Attrs(attrs) = v {
5518            assert_eq!(attrs.get("a"), Some(&Value::Bool(true)));
5519            assert_eq!(attrs.get("b"), Some(&Value::Bool(true)));
5520            assert_eq!(attrs.get("c"), Some(&Value::Bool(true)));
5521        } else {
5522            panic!("expected attrs");
5523        }
5524    }
5525
5526    #[test]
5527    fn integration_nested_with_and_let() {
5528        assert_eq!(
5529            ev("let x = 10; in with { y = 20; }; x + y"),
5530            Value::Int(30),
5531        );
5532    }
5533
5534    #[test]
5535    fn integration_complex_pattern_match() {
5536        // Complex function with defaults, ellipsis, and @ pattern
5537        assert_eq!(
5538            ev("(args @ { a, b ? 5, ... }: a + b + (if args ? c then args.c else 0)) { a = 1; c = 10; }"),
5539            Value::Int(16), // 1 + 5 + 10
5540        );
5541    }
5542
5543    #[test]
5544    fn integration_substring() {
5545        assert_eq!(
5546            ev(r#"builtins.substring 0 5 "hello world""#),
5547            Value::string("hello"),
5548        );
5549        assert_eq!(
5550            ev(r#"builtins.substring 6 5 "hello world""#),
5551            Value::string("world"),
5552        );
5553    }
5554
5555    #[test]
5556    fn integration_has_attr_on_nested() {
5557        // ? on nested attr paths
5558        assert_eq!(ev("{ a = { b = 1; }; } ? a"), Value::Bool(true));
5559        assert_eq!(
5560            ev("({ a = { b = 1; }; }.a) ? b"),
5561            Value::Bool(true),
5562        );
5563    }
5564
5565    #[test]
5566    fn integration_cat_attrs() {
5567        assert_eq!(
5568            ev(r#"builtins.catAttrs "x" [{ x = 1; } { y = 2; } { x = 3; }]"#),
5569            Value::list(vec![Value::Int(1), Value::Int(3)]),
5570        );
5571    }
5572
5573    #[test]
5574    fn integration_get_attr_builtin() {
5575        assert_eq!(
5576            ev(r#"builtins.getAttr "a" { a = 42; b = 10; }"#),
5577            Value::Int(42),
5578        );
5579    }
5580
5581    #[test]
5582    fn integration_has_attr_builtin() {
5583        assert_eq!(
5584            ev(r#"builtins.hasAttr "a" { a = 1; }"#),
5585            Value::Bool(true),
5586        );
5587        assert_eq!(
5588            ev(r#"builtins.hasAttr "z" { a = 1; }"#),
5589            Value::Bool(false),
5590        );
5591    }
5592
5593    #[test]
5594    fn integration_is_path() {
5595        assert_eq!(ev("builtins.isPath ./foo"), Value::Bool(true));
5596        assert_eq!(ev("builtins.isPath 42"), Value::Bool(false));
5597    }
5598
5599    #[test]
5600    fn integration_builtins_trace() {
5601        // trace prints the first arg (as debug) and returns the second
5602        assert_eq!(ev(r#"builtins.trace "debug msg" 42"#), Value::Int(42));
5603    }
5604
5605    #[test]
5606    fn integration_builtins_split() {
5607        // Nix spec: split returns alternating non-match strings and match group lists.
5608        // When the regex has no capture groups, separator positions get empty lists.
5609        // split "/" "a/b/c" => ["a" [] "b" [] "c"]
5610        assert_eq!(
5611            ev(r#"builtins.split "/" "a/b/c""#),
5612            Value::list(vec![
5613                Value::string("a"),
5614                Value::list(vec![]),
5615                Value::string("b"),
5616                Value::list(vec![]),
5617                Value::string("c"),
5618            ]),
5619        );
5620        // With a capture group, the captured text appears in the list.
5621        // split "(/)" "a/b/c" => ["a" ["/"] "b" ["/"] "c"]
5622        assert_eq!(
5623            ev(r#"builtins.split "(/)" "a/b/c""#),
5624            Value::list(vec![
5625                Value::string("a"),
5626                Value::list(vec![Value::string("/")]),
5627                Value::string("b"),
5628                Value::list(vec![Value::string("/")]),
5629                Value::string("c"),
5630            ]),
5631        );
5632    }
5633
5634    #[test]
5635    fn integration_builtins_split_no_capture_groups() {
5636        // builtins.split with no capture groups returns empty lists
5637        // at separator positions — matches CppNix behavior.
5638        // This is critical for nixpkgs lib.splitString which uses
5639        // builtins.filter builtins.isString on the result.
5640        assert_eq!(
5641            ev(r#"builtins.split "-" "aarch64-darwin""#),
5642            Value::list(vec![
5643                Value::string("aarch64"),
5644                Value::list(vec![]),
5645                Value::string("darwin"),
5646            ]),
5647        );
5648    }
5649
5650    #[test]
5651    fn integration_builtins_split_system_string_filter() {
5652        // Simulates nixpkgs lib.splitString: filter isString (split pattern string)
5653        // This is the exact pattern that parses system strings like "aarch64-darwin".
5654        assert_eq!(
5655            ev(r#"builtins.filter builtins.isString (builtins.split "-" "aarch64-darwin")"#),
5656            Value::list(vec![
5657                Value::string("aarch64"),
5658                Value::string("darwin"),
5659            ]),
5660        );
5661    }
5662
5663    #[test]
5664    fn integration_deeply_nested_let() {
5665        // Deeply nested let-in expressions
5666        assert_eq!(
5667            ev("let a = let b = let c = 10; in c * 2; in b + 1; in a"),
5668            Value::Int(21),
5669        );
5670    }
5671
5672    #[test]
5673    fn integration_if_in_attrset_value() {
5674        assert_eq!(
5675            ev("{ x = if true then 1 else 2; }.x"),
5676            Value::Int(1),
5677        );
5678    }
5679
5680    #[test]
5681    fn integration_lambda_in_list() {
5682        // Store lambdas in a list and apply them
5683        assert_eq!(
5684            ev("let fs = [(x: x + 1) (x: x * 2)]; in (builtins.elemAt fs 0) 5"),
5685            Value::Int(6),
5686        );
5687        assert_eq!(
5688            ev("let fs = [(x: x + 1) (x: x * 2)]; in (builtins.elemAt fs 1) 5"),
5689            Value::Int(10),
5690        );
5691    }
5692
5693    #[test]
5694    fn integration_nixpkgs_lib_id() {
5695        // lib.id = x: x
5696        assert_eq!(
5697            ev("let lib = { id = x: x; const = a: b: a; }; in lib.id 42"),
5698            Value::Int(42),
5699        );
5700        assert_eq!(
5701            ev("let lib = { id = x: x; const = a: b: a; }; in lib.const 1 2"),
5702            Value::Int(1),
5703        );
5704    }
5705
5706    #[test]
5707    fn integration_multiple_inherit() {
5708        assert_eq!(
5709            ev("let a = 1; b = 2; c = 3; in { inherit a b c; }.b"),
5710            Value::Int(2),
5711        );
5712    }
5713
5714    #[test]
5715    fn integration_rec_set_with_builtins() {
5716        assert_eq!(
5717            ev(r#"(rec { a = "hello"; b = builtins.stringLength a; }).b"#),
5718            Value::Int(5),
5719        );
5720    }
5721
5722    // ═══════════════════════════════════════════════════════════
5723    // 11. __FUNCTOR PROTOCOL
5724    // ═══════════════════════════════════════════════════════════
5725
5726    #[test]
5727    fn functor_simple_callable_attrset() {
5728        assert_eq!(
5729            ev("let s = { __functor = self: x: x + 1; }; in s 41"),
5730            Value::Int(42),
5731        );
5732    }
5733
5734    #[test]
5735    fn functor_with_self_reference() {
5736        assert_eq!(
5737            ev("let s = { __functor = self: x: self.base + x; base = 100; }; in s 23"),
5738            Value::Int(123),
5739        );
5740    }
5741
5742    #[test]
5743    fn functor_updated_attrset() {
5744        // Override a field in the attrset, functor still works
5745        assert_eq!(
5746            ev(r#"
5747                let
5748                    mk = { __functor = self: x: self.n + x; n = 0; };
5749                    s = mk // { n = 50; };
5750                in s 7
5751            "#),
5752            Value::Int(57),
5753        );
5754    }
5755
5756    #[test]
5757    fn functor_error_on_non_callable_attrset() {
5758        // Attrset without __functor should produce error when called
5759        let result = eval("let s = { a = 1; }; in s 5");
5760        assert!(result.is_err());
5761    }
5762
5763    // ═══════════════════════════════════════════════════════════
5764    // 12. __TOSTRING PROTOCOL
5765    // ═══════════════════════════════════════════════════════════
5766
5767    #[test]
5768    fn to_string_protocol_in_interpolation() {
5769        assert_eq!(
5770            ev(r#"let s = { __toString = self: "world"; }; in "hello ${s}""#),
5771            Value::string("hello world"),
5772        );
5773    }
5774
5775    #[test]
5776    fn to_string_protocol_accesses_self() {
5777        assert_eq!(
5778            ev(r#"let s = { __toString = self: self.val; val = "abc"; }; in "${s}""#),
5779            Value::string("abc"),
5780        );
5781    }
5782
5783    #[test]
5784    fn to_string_protocol_via_builtin_to_string() {
5785        assert_eq!(
5786            ev(r#"builtins.toString { __toString = self: "via-builtin"; }"#),
5787            Value::string("via-builtin"),
5788        );
5789    }
5790
5791    #[test]
5792    fn to_string_protocol_attrset_without_toString_fails() {
5793        // An attrset without __toString should fail in string context
5794        let result = eval(r#""${{}}"#);
5795        assert!(result.is_err());
5796    }
5797
5798    // ═══════════════════════════════════════════════════════════
5799    // 13. NEWLY IMPLEMENTED BUILTINS (eval-level tests)
5800    // ═══════════════════════════════════════════════════════════
5801
5802    #[test]
5803    fn eval_builtins_concat_strings() {
5804        assert_eq!(
5805            ev(r#"builtins.concatStrings ["a" "b" "c"]"#),
5806            Value::string("abc"),
5807        );
5808        assert_eq!(
5809            ev(r#"builtins.concatStrings []"#),
5810            Value::string(""),
5811        );
5812    }
5813
5814    #[test]
5815    fn eval_builtins_partition() {
5816        let v = ev("builtins.partition (x: x > 3) [1 2 3 4 5]");
5817        if let Value::Attrs(a) = v {
5818            assert_eq!(a.get("right"), Some(&Value::list(vec![Value::Int(4), Value::Int(5)])));
5819            assert_eq!(a.get("wrong"), Some(&Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)])));
5820        } else {
5821            panic!("expected attrs");
5822        }
5823    }
5824
5825    #[test]
5826    fn eval_builtins_group_by() {
5827        let v = ev(r#"builtins.groupBy (x: if x > 0 then "pos" else "neg") [1 (0 - 2) 3 (0 - 4)]"#);
5828        if let Value::Attrs(a) = v {
5829            assert_eq!(a.get("pos"), Some(&Value::list(vec![Value::Int(1), Value::Int(3)])));
5830            assert_eq!(a.get("neg"), Some(&Value::list(vec![Value::Int(-2), Value::Int(-4)])));
5831        } else {
5832            panic!("expected attrs");
5833        }
5834    }
5835
5836    #[test]
5837    fn eval_builtins_zip_attrs_with() {
5838        let v = ev("builtins.zipAttrsWith (n: vs: builtins.head vs) [{ a = 1; } { a = 2; b = 3; }]");
5839        if let Value::Attrs(a) = v {
5840            assert_eq!(a.get("a"), Some(&Value::Int(1)));
5841            assert_eq!(a.get("b"), Some(&Value::Int(3)));
5842        } else {
5843            panic!("expected attrs");
5844        }
5845    }
5846
5847    #[test]
5848    fn eval_builtins_compare_versions() {
5849        assert_eq!(ev(r#"builtins.compareVersions "2.0" "1.0""#), Value::Int(1));
5850        assert_eq!(ev(r#"builtins.compareVersions "1.0" "2.0""#), Value::Int(-1));
5851        assert_eq!(ev(r#"builtins.compareVersions "1.0" "1.0""#), Value::Int(0));
5852    }
5853
5854    #[test]
5855    fn eval_builtins_parse_drv_name() {
5856        let v = ev(r#"builtins.parseDrvName "nix-2.3.4""#);
5857        if let Value::Attrs(a) = v {
5858            assert_eq!(a.get("name"), Some(&Value::string("nix")));
5859            assert_eq!(a.get("version"), Some(&Value::string("2.3.4")));
5860        } else {
5861            panic!("expected attrs");
5862        }
5863    }
5864
5865    #[test]
5866    fn eval_builtins_base_name_of() {
5867        assert_eq!(
5868            ev(r#"builtins.baseNameOf "/foo/bar/baz""#),
5869            Value::string("baz"),
5870        );
5871    }
5872
5873    #[test]
5874    fn eval_builtins_dir_of() {
5875        assert_eq!(
5876            ev(r#"builtins.dirOf "/foo/bar/baz""#),
5877            Value::string("/foo/bar"),
5878        );
5879    }
5880
5881    #[test]
5882    fn eval_builtins_add_error_context() {
5883        assert_eq!(
5884            ev(r#"builtins.addErrorContext "some context" 42"#),
5885            Value::Int(42),
5886        );
5887    }
5888
5889    #[test]
5890    fn eval_builtins_abort() {
5891        let result = eval(r#"builtins.abort "fatal error""#);
5892        assert!(result.is_err());
5893        let msg = format!("{}", result.unwrap_err());
5894        assert!(msg.contains("fatal error"));
5895    }
5896
5897    // ═══════════════════════════════════════════════════════════
5898    // 14. INDENTED STRINGS ('' ... '')
5899    // ═══════════════════════════════════════════════════════════
5900
5901    #[test]
5902    fn indented_string_simple() {
5903        assert_eq!(ev("''hello''"), Value::string("hello"));
5904    }
5905
5906    #[test]
5907    fn indented_string_multiline_strips_indent() {
5908        assert_eq!(
5909            ev("''\n  line1\n  line2\n''"),
5910            Value::string("line1\nline2\n"),
5911        );
5912    }
5913
5914    #[test]
5915    fn indented_string_with_interpolation() {
5916        let code = "let x = \"world\"; in ''hello ${x}''";
5917        assert_eq!(
5918            ev(code),
5919            Value::string("hello world"),
5920        );
5921    }
5922
5923    #[test]
5924    fn indented_string_deeper_indent_preserved() {
5925        // Common indent is 2 spaces; the 4-space line keeps 2 extra
5926        assert_eq!(
5927            ev("''\n  a\n    b\n''"),
5928            Value::string("a\n  b\n"),
5929        );
5930    }
5931
5932    // ═══════════════════════════════════════════════════════════
5933    // 15. DYNAMIC ATTRIBUTE NAMES
5934    // ═══════════════════════════════════════════════════════════
5935
5936    #[test]
5937    fn dynamic_attr_name_in_set() {
5938        assert_eq!(
5939            ev(r#"let key = "mykey"; in { ${key} = 42; }.mykey"#),
5940            Value::Int(42),
5941        );
5942    }
5943
5944    #[test]
5945    fn dynamic_attr_name_with_expression() {
5946        assert_eq!(
5947            ev(r#"let prefix = "foo"; in { ${"${prefix}bar"} = 1; }.foobar"#),
5948            Value::Int(1),
5949        );
5950    }
5951
5952    // ═══════════════════════════════════════════════════════════
5953    // 16. IGNORED TESTS — features needing major infrastructure
5954    // ═══════════════════════════════════════════════════════════
5955
5956    #[test]
5957    fn eval_builtins_match() {
5958        assert_eq!(
5959            ev(r#"builtins.match "([0-9]+)" "42""#),
5960            Value::list(vec![Value::string("42")]),
5961        );
5962    }
5963
5964    #[test]
5965    fn eval_builtins_hash_string() {
5966        let v = ev(r#"builtins.hashString "sha256" "hello""#);
5967        if let Value::String(ns) = v {
5968            assert_eq!(ns.chars.len(), 64);
5969        } else {
5970            panic!("expected string");
5971        }
5972    }
5973
5974    #[test]
5975    fn eval_builtins_import() {
5976        let dir = std::env::temp_dir();
5977        let path = dir.join("sui_eval_test_import_eval.nix");
5978        std::fs::write(&path, "42").unwrap();
5979        let expr = format!(r#"import "{}""#, path.display());
5980        let v = eval(&expr).unwrap();
5981        assert_eq!(v, Value::Int(42));
5982        std::fs::remove_file(&path).ok();
5983    }
5984
5985    #[test]
5986    fn eval_builtins_derivation() {
5987        let v = eval(r#"builtins.derivation { name = "test"; system = "x86_64-linux"; builder = "/bin/sh"; }"#).unwrap();
5988        if let Value::Attrs(a) = v {
5989            assert_eq!(a.get("type"), Some(&Value::string("derivation")));
5990        } else {
5991            panic!("expected attrs");
5992        }
5993    }
5994
5995    #[test]
5996    fn eval_mutual_recursive_let() {
5997        // Multi-pass evaluation allows forward references in let bindings.
5998        // After 3 passes (placeholder + eval + re-eval), `a.x` resolves to
5999        // the value of `b` from the previous pass, and `a.x.y` is an attrset.
6000        // Full semantic equivalence with Nix (a.x.y == a) requires lazy
6001        // thunks, but the multi-pass approach is sufficient for common
6002        // patterns like mutual module references.
6003        let v = eval("let a = { x = b; }; b = { y = a; }; in a.x.y");
6004        assert!(v.is_ok(), "mutual recursive let should not error: {v:?}");
6005        // a.x.y should be an attrset (it's a's value from a prior pass)
6006        let val = v.unwrap();
6007        assert!(
6008            matches!(val, Value::Attrs(_)),
6009            "a.x.y should be an attrset, got: {val:?}",
6010        );
6011    }
6012
6013    #[test]
6014    fn eval_mutual_recursive_let_simple() {
6015        // Simpler case: forward reference in sequential let bindings
6016        let v = eval("let a = b; b = 42; in a");
6017        assert!(v.is_ok());
6018        // After multi-pass: pass 2 sets a=Null (b not yet bound), b=42
6019        // pass 3 sets a=42, b=42
6020        assert_eq!(v.unwrap(), Value::Int(42));
6021    }
6022
6023    #[test]
6024    fn eval_builtins_read_dir() {
6025        let dir = std::env::temp_dir().join("sui_eval_test_readdir_eval");
6026        let _ = std::fs::remove_dir_all(&dir);
6027        std::fs::create_dir_all(&dir).unwrap();
6028        std::fs::write(dir.join("a.txt"), "").unwrap();
6029        let expr = format!(r#"builtins.readDir "{}""#, dir.display());
6030        let v = eval(&expr).unwrap();
6031        if let Value::Attrs(a) = v {
6032            assert_eq!(a.get("a.txt"), Some(&Value::string("regular")));
6033        } else {
6034            panic!("expected attrs");
6035        }
6036        let _ = std::fs::remove_dir_all(&dir);
6037    }
6038
6039    // ═══════════════════════════════════════════════════════════
6040    // 17. THUNK / LAZY EVALUATION
6041    // ═══════════════════════════════════════════════════════════
6042
6043    #[test]
6044    fn thunk_basic_let() {
6045        // Simple let binding through thunk.
6046        assert_eq!(ev("let x = 1; in x"), Value::Int(1));
6047    }
6048
6049    #[test]
6050    fn thunk_forward_ref() {
6051        // Forward reference: `a` references `b` which is defined later.
6052        assert_eq!(ev("let a = b; b = 1; in a"), Value::Int(1));
6053    }
6054
6055    #[test]
6056    fn thunk_mutual_rec_attrset_in_let() {
6057        // Mutual recursion through attrsets in let bindings.
6058        assert_eq!(ev("let a = { x = b; }; b = { y = 1; }; in a.x.y"), Value::Int(1));
6059    }
6060
6061    #[test]
6062    fn thunk_rec_attrset() {
6063        // rec { a = b; b = 1; } -- forward ref within rec set.
6064        assert_eq!(ev("(rec { a = b; b = 1; }).a"), Value::Int(1));
6065    }
6066
6067    #[test]
6068    fn thunk_rec_attrset_chain() {
6069        // Longer chain: c depends on b depends on a.
6070        assert_eq!(ev("(rec { a = 1; b = a + 1; c = b + 1; }).c"), Value::Int(3));
6071    }
6072
6073    #[test]
6074    fn thunk_fixpoint() {
6075        // Classic fixpoint combinator -- the core of nixpkgs' `lib.fix`.
6076        assert_eq!(
6077            ev("let fix = f: let x = f x; in x; in (fix (self: { a = 1; b = self.a + 1; })).b"),
6078            Value::Int(2),
6079        );
6080    }
6081
6082    #[test]
6083    fn thunk_blackhole_self_reference() {
6084        // `let x = x; in x` is infinite recursion -- blackhole detection.
6085        let result = eval("let x = x; in x");
6086        assert!(result.is_err());
6087        let msg = format!("{}", result.unwrap_err());
6088        assert!(
6089            msg.contains("infinite recursion") || msg.contains("blackhole"),
6090            "expected blackhole error, got: {msg}",
6091        );
6092    }
6093
6094    #[test]
6095    fn thunk_mutual_blackhole() {
6096        // `let a = b; b = a; in a` -- mutual infinite recursion.
6097        let result = eval("let a = b; b = a; in a");
6098        assert!(result.is_err());
6099    }
6100
6101    #[test]
6102    fn thunk_let_body_forces_correctly() {
6103        // The let body should be able to use thunked bindings in arithmetic.
6104        assert_eq!(ev("let a = 10; b = 20; in a + b"), Value::Int(30));
6105    }
6106
6107    #[test]
6108    fn thunk_only_forced_when_needed() {
6109        // The binding `bad` would error if forced, but it is never used.
6110        assert_eq!(ev("let bad = 1 / 0; good = 42; in good"), Value::Int(42));
6111    }
6112
6113    #[test]
6114    fn thunk_forward_ref_in_function_body() {
6115        // Forward reference used inside a function body.
6116        assert_eq!(
6117            ev("let f = x: x + b; b = 10; in f 5"),
6118            Value::Int(15),
6119        );
6120    }
6121
6122    #[test]
6123    fn thunk_rec_set_self_ref_through_self() {
6124        // rec set where `b` references `a` which is in the same set.
6125        assert_eq!(
6126            ev(r#"(rec { a = "hello"; b = builtins.stringLength a; }).b"#),
6127            Value::Int(5),
6128        );
6129    }
6130
6131    #[test]
6132    fn thunk_nested_let_forward_ref() {
6133        // Forward reference in nested let.
6134        assert_eq!(
6135            ev("let a = b + 1; b = 2; in a"),
6136            Value::Int(3),
6137        );
6138    }
6139
6140    #[test]
6141    fn thunk_deep_chain() {
6142        // Chain of forward references: e -> d -> c -> b -> a.
6143        assert_eq!(
6144            ev("let a = 1; b = a; c = b; d = c; e = d; in e"),
6145            Value::Int(1),
6146        );
6147    }
6148
6149    #[test]
6150    fn thunk_rec_set_fixpoint() {
6151        // Fixpoint through rec set -- common nixpkgs pattern.
6152        assert_eq!(
6153            ev("let fix = f: let x = f x; in x; in (fix (self: { a = 1; b = self.a + 1; c = self.b + 1; })).c"),
6154            Value::Int(3),
6155        );
6156    }
6157
6158    #[test]
6159    fn thunk_let_with_inherit() {
6160        // Inherit in let should work alongside thunked bindings.
6161        assert_eq!(
6162            ev("let a = 1; in let inherit a; b = a + 1; in b"),
6163            Value::Int(2),
6164        );
6165    }
6166
6167    #[test]
6168    fn thunk_attrset_value_lazy() {
6169        // Values in non-rec attrsets are evaluated eagerly, but the test
6170        // verifies that thunked let bindings inside attrset values work.
6171        assert_eq!(
6172            ev("let x = 42; in { a = x; }.a"),
6173            Value::Int(42),
6174        );
6175    }
6176
6177    #[test]
6178    fn thunk_unused_error_not_forced() {
6179        // Multiple bindings, only `ok` is used. `bad` throws but is never forced.
6180        assert_eq!(
6181            ev(r#"let bad = builtins.throw "boom"; ok = 1; in ok"#),
6182            Value::Int(1),
6183        );
6184    }
6185
6186    #[test]
6187    fn thunk_rec_set_mutual_reference() {
6188        // Mutual reference within rec set.
6189        let v = ev("rec { a = { val = b.val + 1; }; b = { val = 10; }; }");
6190        if let Value::Attrs(attrs) = v {
6191            let a = attrs.get("a").unwrap();
6192            let a_forced = force_value(a).unwrap();
6193            if let Value::Attrs(a_attrs) = a_forced {
6194                assert_eq!(a_attrs.get("val"), Some(&Value::Int(11)));
6195            } else {
6196                panic!("expected attrs for a");
6197            }
6198        } else {
6199            panic!("expected attrs");
6200        }
6201    }
6202
6203    // ── let-rec self-reference corner cases ───────────────
6204
6205    #[test]
6206    fn let_rec_self_reference_simple() {
6207        assert_eq!(
6208            ev("let x = 1; y = x + 1; in y"),
6209            Value::Int(2),
6210        );
6211    }
6212
6213    #[test]
6214    fn let_rec_self_reference_chain() {
6215        assert_eq!(
6216            ev("let a = 1; b = a + 1; c = b + 1; in c"),
6217            Value::Int(3),
6218        );
6219    }
6220
6221    #[test]
6222    fn let_rec_self_reference_with_function() {
6223        assert_eq!(
6224            ev("let f = x: x + 1; y = f 10; in y"),
6225            Value::Int(11),
6226        );
6227    }
6228
6229    #[test]
6230    fn let_rec_mutual_recursion_via_if() {
6231        assert_eq!(
6232            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"),
6233            Value::Bool(true),
6234        );
6235    }
6236
6237    #[test]
6238    fn let_rec_forward_ref_in_list() {
6239        assert_eq!(
6240            ev("let xs = [a b]; a = 1; b = 2; in builtins.length xs"),
6241            Value::Int(2),
6242        );
6243    }
6244
6245    // ── with-shadowing corner cases ───────────────────────
6246
6247    #[test]
6248    fn with_shadowing_let_wins_over_with() {
6249        assert_eq!(
6250            ev("let x = 1; in with { x = 2; }; x"),
6251            Value::Int(1),
6252        );
6253    }
6254
6255    #[test]
6256    fn with_shadowing_inner_with_wins() {
6257        assert_eq!(
6258            ev("with { x = 1; }; with { x = 2; }; x"),
6259            Value::Int(2),
6260        );
6261    }
6262
6263    #[test]
6264    fn with_shadowing_outer_provides_missing() {
6265        assert_eq!(
6266            ev("with { x = 1; y = 10; }; with { x = 2; }; x + y"),
6267            Value::Int(12),
6268        );
6269    }
6270
6271    #[test]
6272    fn with_shadowing_lambda_arg_wins() {
6273        assert_eq!(
6274            ev("(x: with { x = 99; }; x) 42"),
6275            Value::Int(42),
6276        );
6277    }
6278
6279    #[test]
6280    fn with_shadowing_nested_let_wins_over_with() {
6281        assert_eq!(
6282            ev("with { x = 1; }; let x = 2; in x"),
6283            Value::Int(2),
6284        );
6285    }
6286
6287    #[test]
6288    fn with_scope_dynamic_attrs() {
6289        assert_eq!(
6290            ev(r#"with { x = 1; y = 2; z = 3; }; x + y + z"#),
6291            Value::Int(6),
6292        );
6293    }
6294
6295    #[test]
6296    fn with_scope_over_lazy_thunk_chain_resolves() {
6297        // A `with`-head that resolves through a NESTED thunk chain
6298        // (`Thunk(Thunk(Attrs))`) must still be searched: the lookup
6299        // has to FULLY force the head (chase the chain), not take a
6300        // single force step. A single step leaves a `Value::Thunk`
6301        // that `type_name()` reports as "set" but the `Value::Attrs`
6302        // match rejects — the scope is skipped and a bare ident
6303        // through it fails with a spurious UndefinedVar. This corners
6304        // the nixpkgs `platforms = with lib.platforms; unix;` shape.
6305        assert_eq!(
6306            ev(r#"let outer = if true then (if true then { unix = 42; } else {}) else {};
6307                      # force a two-deep lazy wrap of the with-head
6308                      head = (x: x) ((y: y) outer);
6309                  in with head; unix"#),
6310            Value::Int(42),
6311        );
6312    }
6313
6314    #[test]
6315    fn with_scope_head_from_deep_select_resolves() {
6316        // `with a.b.c; key` where a.b.c is a lazily-selected attrset —
6317        // the bare-ident body must find `key` through the forced head.
6318        assert_eq!(
6319            ev(r#"let a = { b = { c = { key = 7; }; }; }; in with a.b.c; key"#),
6320            Value::Int(7),
6321        );
6322    }
6323
6324    // ── attrset deep merge ────────────────────────────────
6325
6326    #[test]
6327    fn attrset_deep_merge_simple() {
6328        let v = ev("{ a.b = 1; a.c = 2; }");
6329        if let Value::Attrs(attrs) = v {
6330            let a = force_value(attrs.get("a").unwrap()).unwrap();
6331            if let Value::Attrs(inner) = a {
6332                assert_eq!(force_value(inner.get("b").unwrap()).unwrap(), Value::Int(1));
6333                assert_eq!(force_value(inner.get("c").unwrap()).unwrap(), Value::Int(2));
6334            } else {
6335                panic!("expected nested attrs");
6336            }
6337        } else {
6338            panic!("expected attrs");
6339        }
6340    }
6341
6342    #[test]
6343    fn attrset_deep_merge_three_levels() {
6344        let v = ev("{ a.b.c = 1; a.b.d = 2; a.e = 3; }");
6345        if let Value::Attrs(attrs) = v {
6346            let a = force_value(attrs.get("a").unwrap()).unwrap();
6347            if let Value::Attrs(a_inner) = a {
6348                let e = force_value(a_inner.get("e").unwrap()).unwrap();
6349                assert_eq!(e, Value::Int(3));
6350                let b = force_value(a_inner.get("b").unwrap()).unwrap();
6351                if let Value::Attrs(b_inner) = b {
6352                    assert_eq!(force_value(b_inner.get("c").unwrap()).unwrap(), Value::Int(1));
6353                    assert_eq!(force_value(b_inner.get("d").unwrap()).unwrap(), Value::Int(2));
6354                } else {
6355                    panic!("expected nested attrs for b");
6356                }
6357            } else {
6358                panic!("expected nested attrs for a");
6359            }
6360        } else {
6361            panic!("expected attrs");
6362        }
6363    }
6364
6365    #[test]
6366    fn attrset_deep_merge_preserves_siblings() {
6367        assert_eq!(
6368            ev("{ a.x = 1; b = 2; a.y = 3; }.b"),
6369            Value::Int(2),
6370        );
6371    }
6372
6373    #[test]
6374    fn attrset_deep_merge_in_let() {
6375        let v = ev("let s = { a.b = 1; a.c = 2; }; in s.a.b + s.a.c");
6376        assert_eq!(v, Value::Int(3));
6377    }
6378
6379    #[test]
6380    fn attrset_deep_merge_fullset_then_dotted() {
6381        // General root (gst-plugins-base `passthru.waylandEnabled` drop):
6382        // `a = { x = 1; }; a.y = 2;` — the full-set binding is a lazy
6383        // Thunk (attrset literals go through maybe_thunk), so a naive
6384        // merge_nested_insert (which only merges concrete Value::Attrs)
6385        // overwrote `a` with `{ y = 2 }`, silently dropping `x`. The
6386        // collision must force the existing thunk to WHNF first.
6387        let v = ev("let s = { a = { x = 1; }; a.y = 2; }; in s.a.x + s.a.y");
6388        assert_eq!(v, Value::Int(3));
6389        // both keys must survive (not just their sum)
6390        let both = ev("let s = { a = { x = 1; }; a.y = 2; }; in [ s.a.x s.a.y ]");
6391        if let Value::List(items) = both {
6392            assert_eq!(force_value(&items[0]).unwrap(), Value::Int(1));
6393            assert_eq!(force_value(&items[1]).unwrap(), Value::Int(2));
6394        } else {
6395            panic!("expected list");
6396        }
6397    }
6398
6399    // ── inherit-from patterns ─────────────────────────────
6400
6401    #[test]
6402    fn inherit_from_basic() {
6403        assert_eq!(
6404            ev("let s = { x = 1; y = 2; }; in let inherit (s) x y; in x + y"),
6405            Value::Int(3),
6406        );
6407    }
6408
6409    #[test]
6410    fn inherit_from_with_shadowing() {
6411        assert_eq!(
6412            ev("let x = 10; in let inherit ({ x = 20; }) x; in x"),
6413            Value::Int(20),
6414        );
6415    }
6416
6417    #[test]
6418    fn inherit_from_in_attrset() {
6419        let v = ev(r#"let s = { a = 1; b = 2; }; in { inherit (s) a b; c = 3; }"#);
6420        if let Value::Attrs(attrs) = v {
6421            assert_eq!(force_value(attrs.get("a").unwrap()).unwrap(), Value::Int(1));
6422            assert_eq!(force_value(attrs.get("b").unwrap()).unwrap(), Value::Int(2));
6423            assert_eq!(force_value(attrs.get("c").unwrap()).unwrap(), Value::Int(3));
6424        } else {
6425            panic!("expected attrs");
6426        }
6427    }
6428
6429    #[test]
6430    fn inherit_from_rec_set() {
6431        assert_eq!(
6432            ev("rec { inherit ({ x = 42; }) x; y = x; }.y"),
6433            Value::Int(42),
6434        );
6435    }
6436
6437    #[test]
6438    fn inherit_plain_from_scope() {
6439        assert_eq!(
6440            ev("let x = 1; in { inherit x; }.x"),
6441            Value::Int(1),
6442        );
6443    }
6444
6445    // Regression (2026-07-11): a bare `inherit x;` must resolve LAZILY, like
6446    // a plain reference to `x` — not eagerly at attrset construction. When
6447    // `x` is provided only by an enclosing `with` scope whose value is a
6448    // fixpoint still being constructed, eager resolution spuriously threw
6449    // `UndefinedVar`. nixpkgs `all-packages.nix` is
6450    // `with pkgs; { nettle = import … { inherit callPackage; }; }`, so
6451    // `inherit callPackage` must resolve from the `with pkgs` scope at force
6452    // time. (This was the nettle UndefinedVar('callPackage') drop.)
6453    #[test]
6454    fn inherit_plain_from_with_scope_lazy() {
6455        // `inherit cp` reads `cp` from a `with self` fixpoint scope; the
6456        // attr forcing it (`a`) must resolve `cp` lazily against the settled
6457        // scope, not eagerly during attrset construction.
6458        assert_eq!(
6459            ev("let fix = f: let x = f x; in x;
6460                    self = fix (self: with self; {
6461                      a = use { inherit cp; };
6462                      use = { cp }: cp 5;
6463                      cp = x: x + 100;
6464                    });
6465                in self.a"),
6466            Value::Int(105),
6467        );
6468        // Simpler: bare inherit from a plain (non-blackhole) with scope.
6469        assert_eq!(
6470            ev("with { y = 7; }; { inherit y; }.y"),
6471            Value::Int(7),
6472        );
6473    }
6474
6475    #[test]
6476    fn inherit_multiple_from_expr() {
6477        assert_eq!(
6478            ev("let s = { a = 10; b = 20; c = 30; }; in let inherit (s) a b c; in a + b + c"),
6479            Value::Int(60),
6480        );
6481    }
6482
6483    // ── string interpolation edge cases ───────────────────
6484
6485    #[test]
6486    fn interp_nested_attrset_access() {
6487        assert_eq!(
6488            ev(r#"let x = { a = "hello"; }; in "${x.a} world""#),
6489            Value::string("hello world"),
6490        );
6491    }
6492
6493    #[test]
6494    fn interp_with_let_expression() {
6495        assert_eq!(
6496            ev(r#""${let x = "inner"; in x}""#),
6497            Value::string("inner"),
6498        );
6499    }
6500
6501    #[test]
6502    fn interp_float_coercion() {
6503        // CppNix %f-format: always 6 decimal places.
6504        assert_eq!(
6505            ev(r#""${toString 3.14}""#),
6506            Value::string("3.140000"),
6507        );
6508    }
6509
6510    // ── comparison edge cases ─────────────────────────────
6511
6512    #[test]
6513    fn compare_mixed_int_float() {
6514        assert_eq!(ev("1 < 1.5"), Value::Bool(true));
6515        assert_eq!(ev("1.5 > 1"), Value::Bool(true));
6516        assert_eq!(ev("2.0 == 2"), Value::Bool(true));
6517    }
6518
6519    #[test]
6520    fn compare_string_lexicographic() {
6521        assert_eq!(ev(r#""abc" < "abd""#), Value::Bool(true));
6522        assert_eq!(ev(r#""abc" < "abc""#), Value::Bool(false));
6523        assert_eq!(ev(r#""abc" <= "abc""#), Value::Bool(true));
6524    }
6525
6526    // ── update operator edge cases ────────────────────────
6527
6528    #[test]
6529    fn update_empty_sets() {
6530        let v = ev("{} // {}");
6531        if let Value::Attrs(a) = v { assert!(a.is_empty()); } else { panic!(); }
6532    }
6533
6534    #[test]
6535    fn update_right_overrides_completely() {
6536        assert_eq!(
6537            ev("{ a = 1; b = 2; } // { a = 10; c = 30; }"),
6538            ev("{ a = 10; b = 2; c = 30; }"),
6539        );
6540    }
6541
6542    #[test]
6543    fn update_chained() {
6544        assert_eq!(
6545            ev("{ a = 1; } // { b = 2; } // { c = 3; }"),
6546            ev("{ a = 1; b = 2; c = 3; }"),
6547        );
6548    }
6549
6550    // ── force_value edge cases ────────────────────────────
6551
6552    #[test]
6553    fn force_value_concrete_unchanged() {
6554        let v = Value::Int(42);
6555        assert_eq!(force_value(&v).unwrap(), Value::Int(42));
6556    }
6557
6558    #[test]
6559    fn force_value_null() {
6560        assert_eq!(force_value(&Value::Null).unwrap(), Value::Null);
6561    }
6562
6563    // ── eval_with_file ────────────────────────────────────
6564
6565    #[test]
6566    fn eval_with_file_none() {
6567        let result = eval_with_file("1 + 2", None).unwrap();
6568        assert_eq!(result, Value::Int(3));
6569    }
6570
6571    // ── error messages ────────────────────────────────────
6572
6573    #[test]
6574    fn error_type_mismatch_in_comparison() {
6575        let result = eval(r#"1 < "a""#);
6576        assert!(result.is_err());
6577    }
6578
6579    #[test]
6580    fn error_select_from_non_set() {
6581        let result = eval("42.x");
6582        assert!(result.is_err());
6583    }
6584
6585    #[test]
6586    fn error_call_non_function() {
6587        let result = eval("42 1");
6588        assert!(result.is_err());
6589    }
6590
6591    #[test]
6592    fn error_negate_string() {
6593        let result = eval(r#"-"hello""#);
6594        assert!(result.is_err());
6595    }
6596
6597    // ── multiline string edge cases ───────────────────────
6598
6599    #[test]
6600    fn multiline_string_empty() {
6601        assert_eq!(ev("''''"), Value::string(""));
6602    }
6603
6604    #[test]
6605    fn multiline_string_with_trailing_newline() {
6606        let v = ev("''\n  hello\n''");
6607        assert_eq!(v, Value::string("hello\n"));
6608    }
6609
6610    // ── list operations ───────────────────────────────────
6611
6612    #[test]
6613    fn list_concat_empty_left() {
6614        assert_eq!(ev("[] ++ [1 2]"), Value::list(vec![Value::Int(1), Value::Int(2)]));
6615    }
6616
6617    #[test]
6618    fn list_concat_empty_right() {
6619        assert_eq!(ev("[1 2] ++ []"), Value::list(vec![Value::Int(1), Value::Int(2)]));
6620    }
6621
6622    #[test]
6623    fn list_concat_both_empty() {
6624        assert_eq!(ev("[] ++ []"), Value::list(vec![]));
6625    }
6626
6627    // ── pattern matching / formals edge cases ─────────────
6628
6629    #[test]
6630    fn formals_at_pattern_accessible() {
6631        assert_eq!(
6632            ev("({ x, ... } @ args: builtins.length (builtins.attrNames args)) { x = 1; y = 2; z = 3; }"),
6633            Value::Int(3),
6634        );
6635    }
6636
6637    #[test]
6638    fn formals_default_uses_other_arg() {
6639        assert_eq!(
6640            ev("({ x, y ? x + 1 }: y) { x = 10; }"),
6641            Value::Int(11),
6642        );
6643    }
6644
6645    #[test]
6646    fn formals_default_lazy_assert_false() {
6647        // nixpkgs parse.nix pattern: default is `assert false; null` but
6648        // the body checks `args ? vendor` instead of using `vendor`
6649        // directly, so the default must never be forced.
6650        assert_eq!(
6651            ev("({ cpu, vendor ? assert false; null, kernel } @ args: if args ? vendor then vendor else \"inferred\") { cpu = \"x86_64\"; kernel = \"linux\"; }"),
6652            Value::String(Rc::new(NixString::plain("inferred"))),
6653        );
6654    }
6655
6656    #[test]
6657    fn formals_default_lazy_only_forced_when_accessed() {
6658        // When the default IS accessed, it should still evaluate correctly.
6659        assert_eq!(
6660            ev("({ a, b ? 42 }: b) { a = 1; }"),
6661            Value::Int(42),
6662        );
6663    }
6664
6665    #[test]
6666    fn formals_ellipsis_ignores_extra() {
6667        assert_eq!(
6668            ev("({ x, ... }: x) { x = 1; y = 2; z = 3; }"),
6669            Value::Int(1),
6670        );
6671    }
6672
6673    // ── pure mode ─────────────────────────────────────────
6674
6675    #[test]
6676    fn pure_mode_roundtrip() {
6677        let was_pure = is_pure_mode();
6678        set_pure_mode(true);
6679        assert!(is_pure_mode());
6680        set_pure_mode(false);
6681        assert!(!is_pure_mode());
6682        set_pure_mode(was_pure);
6683    }
6684
6685    // ── path operations ───────────────────────────────────
6686
6687    #[test]
6688    fn path_concat_with_string() {
6689        assert_eq!(
6690            ev(r#"/foo + "bar""#),
6691            Value::Path(Box::new(SmolStr::from("/foobar"))),
6692        );
6693    }
6694
6695    #[test]
6696    fn path_concat_with_path() {
6697        assert_eq!(
6698            ev("/foo + /bar"),
6699            Value::Path(Box::new(SmolStr::from("/foo//bar"))),
6700        );
6701    }
6702
6703    // ── EvalFileGuard / current_eval_dir ───────────────────
6704
6705    #[test]
6706    fn current_eval_dir_empty_when_no_file_pushed() {
6707        // Without a push, current_eval_dir should yield None.
6708        // (Note: this test is order-dependent; we accept whatever the
6709        // top of the stack happens to be when called.)
6710        let snapshot = current_eval_dir();
6711        // At minimum the API doesn't panic and returns Option.
6712        let _ = snapshot;
6713    }
6714
6715    #[test]
6716    fn push_eval_file_sets_current_dir() {
6717        let p = std::path::PathBuf::from("/tmp/example/file.nix");
6718        {
6719            let _g = push_eval_file(p.clone());
6720            assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/tmp/example")));
6721        }
6722        // Guard dropped, stack popped — current dir is whatever was below.
6723        // We can't assert exact value without snapshotting first, but the
6724        // value before push should be restored.
6725    }
6726
6727    #[test]
6728    fn push_eval_file_nested_stack() {
6729        let outer = std::path::PathBuf::from("/a/x.nix");
6730        let inner = std::path::PathBuf::from("/b/y.nix");
6731        {
6732            let _g_outer = push_eval_file(outer.clone());
6733            assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/a")));
6734            {
6735                let _g_inner = push_eval_file(inner.clone());
6736                assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/b")));
6737            }
6738            // Inner dropped — outer is back on top.
6739            assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/a")));
6740        }
6741    }
6742
6743    // ── Source-mapped error context ────────────────────────
6744
6745    #[test]
6746    fn error_undefined_var_includes_file_context() {
6747        let p = std::path::PathBuf::from("/nix/store/abc-default.nix");
6748        let _g = push_eval_file(p);
6749        let result = eval("nonexistent_xyz");
6750        let msg = format!("{}", result.unwrap_err());
6751        assert!(msg.contains("undefined variable"), "msg: {msg}");
6752        assert!(msg.contains("nonexistent_xyz"), "msg: {msg}");
6753        assert!(msg.contains("abc-default.nix"), "msg: {msg}");
6754    }
6755
6756    #[test]
6757    fn error_attr_not_found_includes_file_context() {
6758        let p = std::path::PathBuf::from("/nix/store/xyz-module.nix");
6759        let _g = push_eval_file(p);
6760        let result = eval("{}.missing_key");
6761        let msg = format!("{}", result.unwrap_err());
6762        assert!(msg.contains("not found") || msg.contains("missing_key"), "msg: {msg}");
6763        assert!(msg.contains("xyz-module.nix"), "msg: {msg}");
6764    }
6765
6766    #[test]
6767    fn error_assertion_failed_includes_file_context() {
6768        let p = std::path::PathBuf::from("/nix/store/test-assert.nix");
6769        let _g = push_eval_file(p);
6770        let result = eval("assert false; 1");
6771        let msg = format!("{}", result.unwrap_err());
6772        assert!(msg.contains("assertion failed"), "msg: {msg}");
6773        assert!(msg.contains("test-assert.nix"), "msg: {msg}");
6774    }
6775
6776    #[test]
6777    fn error_missing_argument_includes_file_context() {
6778        let p = std::path::PathBuf::from("/nix/store/func.nix");
6779        let _g = push_eval_file(p);
6780        let result = eval("({ a, b }: a) { a = 1; }");
6781        let msg = format!("{}", result.unwrap_err());
6782        assert!(msg.contains("missing argument"), "msg: {msg}");
6783        assert!(msg.contains("func.nix"), "msg: {msg}");
6784    }
6785
6786    #[test]
6787    fn error_cannot_call_includes_file_context() {
6788        let p = std::path::PathBuf::from("/nix/store/call.nix");
6789        let _g = push_eval_file(p);
6790        let result = eval("42 99");
6791        let msg = format!("{}", result.unwrap_err());
6792        assert!(msg.contains("cannot call"), "msg: {msg}");
6793        assert!(msg.contains("call.nix"), "msg: {msg}");
6794    }
6795
6796    #[test]
6797    fn error_without_file_has_no_in_prefix() {
6798        // When no file is on the eval stack, error messages should
6799        // not contain ", in" context.
6800        let result = eval("nonexistent_xyz");
6801        let msg = format!("{}", result.unwrap_err());
6802        assert!(msg.contains("undefined variable"), "msg: {msg}");
6803        assert!(!msg.contains(", in"), "msg should not contain file context: {msg}");
6804    }
6805
6806    // ── pure mode getter/setter independence ───────────────
6807
6808    #[test]
6809    fn pure_mode_set_get_independence() {
6810        let was = is_pure_mode();
6811        set_pure_mode(true);
6812        assert!(is_pure_mode());
6813        set_pure_mode(false);
6814        assert!(!is_pure_mode());
6815        set_pure_mode(was);
6816    }
6817
6818    // ── eval_with_file with file path ──────────────────────
6819
6820    #[test]
6821    fn eval_with_file_some_path_arithmetic() {
6822        let p = std::path::PathBuf::from("/tmp/imaginary.nix");
6823        let result = eval_with_file("1 + 2", Some(p)).unwrap();
6824        assert_eq!(result, Value::Int(3));
6825    }
6826
6827    // ── unsafeGetAttrPos — the options.json `attrTag` declarations root ──
6828    //
6829    // Seals the CppNix-matching behavior: for a literal attrset built in a
6830    // FILE, `builtins.unsafeGetAttrPos <key> <set>` returns
6831    // `{ file; line=1; column=<key byte offset>+1; }`; for a `<string>` eval
6832    // (no file) it returns `null`. Byte-verified against `nix eval`.
6833
6834    #[test]
6835    fn unsafe_get_attr_pos_reports_file_and_offset_column() {
6836        // The real `attrTag` path: a literal attrset built in an IMPORTED file.
6837        // `import` registers the file's source text + pushes it on the eval
6838        // stack, so `eval_attrset` captures the key positions against that file
6839        // and `unsafeGetAttrPos` resolves them. CppNix reports the file, line 1,
6840        // and column = the KEY's 1-based byte offset (verified against `nix eval`).
6841        let dir = tempfile::tempdir().unwrap();
6842        // The literal's `b` key sits at a known byte offset in this file.
6843        let file_body = "{ a = 1;\n  b = 2; }\n";
6844        let f = dir.path().join("lit.nix");
6845        std::fs::write(&f, file_body).unwrap();
6846        let src = format!("builtins.unsafeGetAttrPos \"b\" (import {})", f.display());
6847        let v = eval(&src).unwrap();
6848        let attrs = match v { Value::Attrs(a) => a, other => panic!("expected attrs, got {other:?}") };
6849        assert_eq!(
6850            attrs.get("file").unwrap().as_string().unwrap(),
6851            f.to_string_lossy(),
6852        );
6853        assert_eq!(*attrs.get("line").unwrap(), Value::Int(1));
6854        // Column = the `b` KEY token's 1-based byte offset in file_body.
6855        let expected_col = (file_body.find("b = 2").unwrap() as i64) + 1;
6856        let col = match attrs.get("column").unwrap() { Value::Int(n) => *n, o => panic!("{o:?}") };
6857        assert_eq!(col, expected_col, "column must be the 1-based byte offset");
6858    }
6859
6860    #[test]
6861    fn unsafe_get_attr_pos_null_for_string_origin() {
6862        // A `<string>`-eval'd literal (no file on the stack) has no position → null.
6863        let v = eval("builtins.unsafeGetAttrPos \"a\" { a = 1; }").unwrap();
6864        assert_eq!(v, Value::Null);
6865    }
6866
6867    #[test]
6868    fn unsafe_get_attr_pos_null_for_missing_key() {
6869        // A key absent from an imported set → null.
6870        let dir = tempfile::tempdir().unwrap();
6871        let f = dir.path().join("lit.nix");
6872        std::fs::write(&f, "{ a = 1; }\n").unwrap();
6873        let src = format!("builtins.unsafeGetAttrPos \"zzz\" (import {})", f.display());
6874        let v = eval(&src).unwrap();
6875        assert_eq!(v, Value::Null);
6876    }
6877
6878    // ── String interpolation primitive coercions ───────────
6879
6880    #[test]
6881    fn interp_int_into_string() {
6882        // Integer interpolated into a string is coerced to its decimal repr.
6883        assert_eq!(ev(r#""val=${toString 42}""#), Value::string("val=42"));
6884    }
6885
6886    #[test]
6887    fn interp_bool_true_becomes_one() {
6888        // Per eval_str: Bool(true) → "1", Bool(false) → "" (empty)
6889        let v = ev(r#"let x = true; in "${builtins.toString x}""#);
6890        assert_eq!(v, Value::string("1"));
6891    }
6892
6893    #[test]
6894    fn interp_null_becomes_empty() {
6895        // Null in interpolation is empty.
6896        let v = ev(r#"let x = null; in "${builtins.toString x}""#);
6897        assert_eq!(v, Value::string(""));
6898    }
6899
6900    #[test]
6901    fn interp_attrset_without_to_string_errors() {
6902        // An attrset interpolated without __toString is a type error.
6903        let result = eval(r#"let s = { x = 1; }; in "${s}""#);
6904        assert!(result.is_err());
6905    }
6906
6907    #[test]
6908    fn interp_attrset_with_to_string_protocol() {
6909        // __toString protocol returns a string when called with self.
6910        let v = ev(r#""${{ __toString = self: "ok"; }}""#);
6911        assert_eq!(v, Value::string("ok"));
6912    }
6913
6914    // ── Path PathRel / PathHome / PathAbs ─────────────────
6915
6916    #[test]
6917    fn eval_path_absolute_literal() {
6918        let v = ev("/tmp/foo");
6919        match v {
6920            Value::Path(p) => assert!(p.contains("/tmp/foo")),
6921            _ => panic!("expected Path"),
6922        }
6923    }
6924
6925    #[test]
6926    fn eval_path_home_literal() {
6927        let v = ev("~/foo.nix");
6928        match v {
6929            Value::Path(p) => assert!(p.contains("~/foo.nix") || p.ends_with("foo.nix")),
6930            _ => panic!("expected Path"),
6931        }
6932    }
6933
6934    // ── search path miss ──────────────────────────────────
6935
6936    #[test]
6937    fn path_search_unmatched_errors() {
6938        // Without NIX_PATH entries matching, <nonexistent> errors out.
6939        // We unset NIX_PATH locally to ensure no entries match.
6940        let saved = std::env::var("NIX_PATH").ok();
6941        // SAFETY: tests run sequentially in single-threaded mode by
6942        // default? The thread_local NIX_PATH is per-thread but std::env
6943        // is process-global. We restore it after.
6944        unsafe {
6945            std::env::remove_var("NIX_PATH");
6946        }
6947        let result = eval("<this_should_not_resolve>");
6948        if let Some(v) = saved {
6949            unsafe {
6950                std::env::set_var("NIX_PATH", v);
6951            }
6952        }
6953        assert!(result.is_err());
6954    }
6955
6956    // ── Unary operators ────────────────────────────────────
6957
6958    #[test]
6959    fn unary_negate_int() {
6960        assert_eq!(ev("-7"), Value::Int(-7));
6961    }
6962
6963    #[test]
6964    fn unary_negate_float() {
6965        assert_eq!(ev("-2.5"), Value::Float(-2.5));
6966    }
6967
6968    #[test]
6969    fn unary_invert_true() {
6970        assert_eq!(ev("!true"), Value::Bool(false));
6971    }
6972
6973    #[test]
6974    fn unary_invert_false() {
6975        assert_eq!(ev("!false"), Value::Bool(true));
6976    }
6977
6978    #[test]
6979    fn unary_negate_bool_errors() {
6980        let result = eval("-true");
6981        assert!(result.is_err());
6982    }
6983
6984    #[test]
6985    fn unary_invert_int_errors() {
6986        let result = eval("!42");
6987        assert!(result.is_err());
6988    }
6989
6990    // ── Binary op type errors ──────────────────────────────
6991
6992    #[test]
6993    fn binop_add_attrs_errors() {
6994        let result = eval("{a=1;} + {b=2;}");
6995        assert!(result.is_err());
6996    }
6997
6998    #[test]
6999    fn binop_sub_string_errors() {
7000        let result = eval(r#""a" - "b""#);
7001        assert!(result.is_err());
7002    }
7003
7004    #[test]
7005    fn binop_mul_string_errors() {
7006        let result = eval(r#""a" * "b""#);
7007        assert!(result.is_err());
7008    }
7009
7010    #[test]
7011    fn binop_div_string_errors() {
7012        let result = eval(r#""a" / "b""#);
7013        assert!(result.is_err());
7014    }
7015
7016    #[test]
7017    fn binop_compare_attrs_errors() {
7018        let result = eval("{a=1;} < {b=2;}");
7019        assert!(result.is_err());
7020    }
7021
7022    #[test]
7023    fn binop_div_float_by_zero_int() {
7024        // Float / int(0) is NOT a DivisionByZero error in this evaluator —
7025        // only int/int matches the DivisionByZero branch. This documents
7026        // that branch.
7027        let result = eval("1.0 / 0");
7028        // Either inf or error is acceptable; the documented branch is
7029        // the int/int(0) → DivisionByZero one.
7030        let _ = result;
7031    }
7032
7033    #[test]
7034    fn binop_int_div_zero_is_division_by_zero() {
7035        let result = eval("5 / 0");
7036        match result {
7037            Err(EvalError::DivisionByZero) => {}
7038            other => panic!("expected DivisionByZero, got {other:?}"),
7039        }
7040    }
7041
7042    // ── if/then/else laziness ──────────────────────────────
7043
7044    #[test]
7045    fn if_else_only_chosen_branch_evaluated_then() {
7046        // The else branch contains a divide-by-zero that would error
7047        // if eagerly evaluated. Choosing the then branch must skip it.
7048        assert_eq!(ev("if true then 42 else 1 / 0"), Value::Int(42));
7049    }
7050
7051    #[test]
7052    fn if_else_only_chosen_branch_evaluated_else() {
7053        assert_eq!(ev("if false then 1 / 0 else 99"), Value::Int(99));
7054    }
7055
7056    #[test]
7057    fn if_condition_must_be_bool() {
7058        let result = eval("if 1 then 1 else 2");
7059        assert!(result.is_err());
7060    }
7061
7062    #[test]
7063    fn if_condition_lazy_does_not_force_unused() {
7064        // Lazy `let` ensures that `bad` is only forced if the chosen
7065        // branch references it.
7066        assert_eq!(
7067            ev("let bad = 1 / 0; in if true then 42 else bad"),
7068            Value::Int(42),
7069        );
7070    }
7071
7072    // ── Logic short-circuit laziness ───────────────────────
7073
7074    #[test]
7075    fn and_short_circuits_on_false() {
7076        // RHS contains an error; should never run.
7077        assert_eq!(ev("false && (1 / 0 == 0)"), Value::Bool(false));
7078    }
7079
7080    #[test]
7081    fn or_short_circuits_on_true() {
7082        assert_eq!(ev("true || (1 / 0 == 0)"), Value::Bool(true));
7083    }
7084
7085    #[test]
7086    fn implication_short_circuits_on_false_lhs() {
7087        // false -> anything is true; RHS not evaluated.
7088        assert_eq!(ev("false -> (1 / 0 == 0)"), Value::Bool(true));
7089    }
7090
7091    // ── Lambda fixpoint via let ────────────────────────────
7092
7093    #[test]
7094    fn lambda_fix_combinator_returns_attrset() {
7095        // The classic `fix = f: let x = f x; in x` shape.
7096        let v = ev(
7097            "let fix = f: let x = f x; in x; in
7098              (fix (self: { val = 1; double = self.val * 2; })).double",
7099        );
7100        assert_eq!(v, Value::Int(2));
7101    }
7102
7103    // ── eval_attrset rec scope details ─────────────────────
7104
7105    #[test]
7106    fn rec_attrset_self_reference() {
7107        // rec set with simple forward reference.
7108        let v = ev("(rec { a = b; b = 1; }).a");
7109        assert_eq!(v, Value::Int(1));
7110    }
7111
7112    #[test]
7113    fn rec_attrset_inherit_from_uses_outer_scope() {
7114        // inherit-from in rec uses the OUTER (lexical) scope to evaluate
7115        // the source expression, not the rec scope. We bind `src` in
7116        // an outer let so the inherit can find it.
7117        let v = ev(
7118            "let src = { a = 10; }; in
7119              rec {
7120                inherit (src) a;
7121                b = a + 1;
7122              }",
7123        );
7124        if let Value::Attrs(attrs) = v {
7125            let b = attrs.get("b").unwrap();
7126            let b_forced = force_value(b).unwrap();
7127            assert_eq!(b_forced, Value::Int(11));
7128        } else {
7129            panic!("expected attrs");
7130        }
7131    }
7132
7133    #[test]
7134    fn nonrec_attrset_no_self_reference() {
7135        // In a non-rec set, a name doesn't see its sibling. The error
7136        // surfaces as an UndefinedVar when the thunk is forced.
7137        let result = eval("({ a = 1; b = a + 1; }).b");
7138        assert!(result.is_err());
7139    }
7140
7141    // ── eval_attrset deep merge edge cases ─────────────────
7142
7143    #[test]
7144    fn dotted_binding_three_segments_then_sibling() {
7145        let v = ev("{ a.b.c = 1; a.b.d = 2; a.e = 3; }");
7146        if let Value::Attrs(attrs) = v {
7147            let a = attrs.get("a").unwrap();
7148            let a_forced = force_value(a).unwrap();
7149            if let Value::Attrs(a_attrs) = a_forced {
7150                let b = a_attrs.get("b").unwrap();
7151                let b_forced = force_value(b).unwrap();
7152                if let Value::Attrs(b_attrs) = b_forced {
7153                    assert_eq!(force_value(b_attrs.get("c").unwrap()).unwrap(), Value::Int(1));
7154                    assert_eq!(force_value(b_attrs.get("d").unwrap()).unwrap(), Value::Int(2));
7155                } else {
7156                    panic!("expected b to be attrs");
7157                }
7158                assert_eq!(force_value(a_attrs.get("e").unwrap()).unwrap(), Value::Int(3));
7159            } else {
7160                panic!("expected a to be attrs");
7161            }
7162        } else {
7163            panic!("expected outer attrs");
7164        }
7165    }
7166
7167    // ── rec/let dotted bindings in recursive scope ────────
7168
7169    #[test]
7170    fn rec_dotted_bindings_visible_to_siblings() {
7171        // Dotted bindings in rec blocks must be visible to sibling
7172        // bindings -- this is the nixpkgs lib/systems/parse.nix pattern.
7173        let v = ev("rec { types.openSB = 1; types.openCpu = 2; foo = types.openSB; }.foo");
7174        assert_eq!(v, Value::Int(1));
7175    }
7176
7177    #[test]
7178    fn rec_dotted_leaf_uses_rec_scope() {
7179        // Leaf expressions in dotted bindings must see sibling
7180        // rec-bindings, not just the parent scope.
7181        let v = ev("rec { types.a = f 1; f = x: x + 1; }.types.a");
7182        assert_eq!(v, Value::Int(2));
7183    }
7184
7185    #[test]
7186    fn rec_dotted_multiple_keys_merge() {
7187        // Multiple dotted bindings sharing a top-level key must merge.
7188        let v = ev("rec { types.a = 1; types.b = 2; x = types; }.x");
7189        if let Value::Attrs(attrs) = v {
7190            assert_eq!(force_value(attrs.get("a").unwrap()).unwrap(), Value::Int(1));
7191            assert_eq!(force_value(attrs.get("b").unwrap()).unwrap(), Value::Int(2));
7192        } else {
7193            panic!("expected attrs");
7194        }
7195    }
7196
7197    #[test]
7198    fn rec_nixpkgs_parse_pattern() {
7199        // Simplified nixpkgs lib/systems/parse.nix pattern:
7200        // rec block with dotted types.xxx bindings that reference
7201        // each other through the rec scope.
7202        let v = ev(r#"
7203            let
7204              mkOptionType = x: x;
7205              mergeOneOption = "merge";
7206              attrValues = builtins.attrValues;
7207              setType = name: value: { __type = name; } // value;
7208              mapAttrs = builtins.mapAttrs;
7209              enum = xs: mkOptionType { name = "enum"; check = x: builtins.elem x xs; };
7210              setTypes = type: mapAttrs (name: value: setType type.name ({ inherit name; } // value));
7211            in
7212            rec {
7213              types.openSB = mkOptionType { name = "sb"; merge = mergeOneOption; };
7214              types.significantByte = enum (attrValues significantBytes);
7215              significantBytes = setTypes types.openSB { bigEndian = {}; littleEndian = {}; };
7216              types.openCpuType = mkOptionType { name = "cpu-type"; };
7217              types.cpuType = enum (attrValues cpuTypes);
7218              cpuTypes = setTypes types.openCpuType { arm = { bits = 32; }; };
7219            }.types.openCpuType
7220        "#);
7221        if let Value::Attrs(attrs) = v {
7222            assert_eq!(
7223                force_value(attrs.get("name").unwrap()).unwrap(),
7224                Value::string("cpu-type")
7225            );
7226        } else {
7227            panic!("expected attrs");
7228        }
7229    }
7230
7231    #[test]
7232    fn let_dotted_leaf_uses_let_scope() {
7233        // Dotted binding leaf in a let block sees sibling let-bindings.
7234        let v = ev("let a.x = f 1; f = x: x + 1; in a.x");
7235        assert_eq!(v, Value::Int(2));
7236    }
7237
7238    #[test]
7239    fn let_inherit_from_plus_dotted_overrides() {
7240        // inherit-from and dotted bindings for the same key in a let
7241        // block: CppNix rejects this as a duplicate definition.  Sui
7242        // currently lets the dotted binding win (last-write-wins).
7243        // This test documents the current behaviour -- when we add
7244        // duplicate detection it should change to assert an error.
7245        let v = ev(r#"
7246            let
7247              src = { types = { existing = true; }; };
7248              inherit (src) types;
7249              types.added = true;
7250            in types
7251        "#);
7252        if let Value::Attrs(attrs) = v {
7253            // Dotted binding overwrites the inherited value
7254            assert_eq!(
7255                force_value(attrs.get("added").unwrap()).unwrap(),
7256                Value::Bool(true)
7257            );
7258            // Inherited 'existing' is lost because dotted replaced it
7259            assert!(attrs.get("existing").is_none());
7260        } else {
7261            panic!("expected attrs");
7262        }
7263    }
7264
7265    // ── Function pattern variations ────────────────────────
7266
7267    #[test]
7268    fn pattern_empty_no_args_no_ellipsis() {
7269        // {} pattern accepts only an empty attrset.
7270        assert_eq!(ev("({}: 1) {}"), Value::Int(1));
7271    }
7272
7273    #[test]
7274    fn pattern_empty_with_ellipsis_accepts_extra() {
7275        assert_eq!(ev("({...}: 1) { a = 1; b = 2; }"), Value::Int(1));
7276    }
7277
7278    #[test]
7279    fn pattern_all_defaults() {
7280        assert_eq!(
7281            ev("({a ? 1, b ? 2}: a + b) {}"),
7282            Value::Int(3),
7283        );
7284    }
7285
7286    #[test]
7287    fn pattern_at_bind_before() {
7288        // args @ { x }: args.x — bind name comes before pattern.
7289        assert_eq!(ev("(args @ { x }: args.x) { x = 7; }"), Value::Int(7));
7290    }
7291
7292    #[test]
7293    fn pattern_at_bind_after() {
7294        // { x } @ args: args.x — bind name comes after pattern.
7295        assert_eq!(ev("({ x } @ args: args.x) { x = 7; }"), Value::Int(7));
7296    }
7297
7298    #[test]
7299    fn pattern_default_references_other_arg() {
7300        // The default for `b` references `a` (which exists).
7301        assert_eq!(ev("({a, b ? a + 1}: b) {a = 10;}"), Value::Int(11));
7302    }
7303
7304    #[test]
7305    fn pattern_required_missing_errors() {
7306        let result = eval("({ a, b }: a) { a = 1; }");
7307        assert!(result.is_err());
7308    }
7309
7310    #[test]
7311    fn pattern_unexpected_errors_without_ellipsis() {
7312        let result = eval("({ a }: a) { a = 1; b = 2; }");
7313        assert!(result.is_err());
7314    }
7315
7316    // ── apply: error on non-callable ───────────────────────
7317
7318    #[test]
7319    fn apply_int_errors() {
7320        let result = eval("42 5");
7321        assert!(result.is_err());
7322    }
7323
7324    #[test]
7325    fn apply_string_errors() {
7326        let result = eval(r#""hi" 5"#);
7327        assert!(result.is_err());
7328    }
7329
7330    #[test]
7331    fn apply_attrset_without_functor_errors() {
7332        let result = eval("{ x = 1; } 5");
7333        assert!(result.is_err());
7334        let msg = format!("{}", result.unwrap_err());
7335        assert!(msg.contains("__functor") || msg.contains("cannot call"));
7336    }
7337
7338    // ── Select with multi-segment + default ────────────────
7339
7340    #[test]
7341    fn select_multi_segment_with_default() {
7342        // a.b.missing or 99 -- the missing segment yields the default.
7343        assert_eq!(ev("{ a = { b = 1; }; }.a.c or 99"), Value::Int(99));
7344    }
7345
7346    #[test]
7347    fn select_from_int_errors() {
7348        let result = eval("(1).x");
7349        assert!(result.is_err());
7350    }
7351
7352    // ── HasAttr edge cases ─────────────────────────────────
7353
7354    #[test]
7355    fn has_attr_on_non_set_returns_false() {
7356        // `expr ? a` where expr is not a set returns false (not error).
7357        assert_eq!(ev("1 ? x"), Value::Bool(false));
7358    }
7359
7360    #[test]
7361    fn has_attr_nested_path_present() {
7362        assert_eq!(ev("{ a = { b = 1; }; } ? a.b"), Value::Bool(true));
7363    }
7364
7365    #[test]
7366    fn has_attr_nested_path_missing() {
7367        assert_eq!(ev("{ a = { b = 1; }; } ? a.c"), Value::Bool(false));
7368    }
7369
7370    #[test]
7371    fn has_attr_intermediate_missing_returns_false() {
7372        assert_eq!(ev("{} ? a.b.c"), Value::Bool(false));
7373    }
7374
7375    // ── List eval edge cases ───────────────────────────────
7376
7377    #[test]
7378    fn list_with_function_value() {
7379        let v = ev("[(x: x + 1)]");
7380        if let Value::List(items) = v {
7381            assert_eq!(items.len(), 1);
7382            // List elements are now lazy (thunked). Force to check type.
7383            let forced = force_value(&items[0]).unwrap();
7384            assert!(matches!(forced, Value::Lambda(_)));
7385        } else {
7386            panic!("expected list");
7387        }
7388    }
7389
7390    // ── eval_inherit edge: inherit from missing var ────────
7391
7392    #[test]
7393    fn inherit_unknown_name_errors() {
7394        let result = eval("let x = 1; in let inherit nonexistent; in nonexistent");
7395        assert!(result.is_err());
7396    }
7397
7398    // ── String op: string concat preserves context ─────────
7399
7400    #[test]
7401    fn string_concat_no_context_when_both_plain() {
7402        let v = ev(r#""abc" + "def""#);
7403        if let Value::String(ns) = v {
7404            assert_eq!(ns.chars, "abcdef");
7405            assert!(!ns.has_context());
7406        } else {
7407            panic!("expected string");
7408        }
7409    }
7410
7411    // ── Parens / Root ──────────────────────────────────────
7412
7413    #[test]
7414    fn parens_around_expression() {
7415        assert_eq!(ev("(1 + 2)"), Value::Int(3));
7416    }
7417
7418    #[test]
7419    fn nested_parens() {
7420        assert_eq!(ev("(((42)))"), Value::Int(42));
7421    }
7422
7423    // ── Throw via builtins ─────────────────────────────────
7424
7425    #[test]
7426    fn throw_propagates_as_error() {
7427        let result = eval(r#"builtins.throw "kaboom""#);
7428        match result {
7429            Err(EvalError::Throw(s)) => assert!(s.contains("kaboom")),
7430            other => panic!("expected Throw, got {other:?}"),
7431        }
7432    }
7433
7434    #[test]
7435    fn assert_failed_propagates_as_error() {
7436        let result = eval("assert false; 1");
7437        match result {
7438            Err(EvalError::AssertionFailed(_)) => {}
7439            other => panic!("expected AssertionFailed, got {other:?}"),
7440        }
7441    }
7442
7443    // ── eval_str InterpolPart::Literal only ────────────────
7444
7445    #[test]
7446    fn string_no_interp_yields_no_context() {
7447        let v = ev(r#""just literal""#);
7448        if let Value::String(ns) = v {
7449            assert!(!ns.has_context());
7450        } else {
7451            panic!("expected string");
7452        }
7453    }
7454
7455    // ── Path interpolation adds context ───────────────────
7456
7457    // Byte-parity root #5: interpolating a source path is CppNix copy-to-store
7458    // coercion — the path is NAR-copied into /nix/store/<hash>-<name> and the
7459    // store path (with store-path context) is spliced in, not the raw path.
7460    // NAR of a single regular file is content+basename only (location-
7461    // independent), so a temp <dir>/data.txt of "hello\n" yields the exact
7462    // store path nix 2.34 produced: /nix/store/y9dmv…-data.txt.
7463    #[test]
7464    fn interp_path_copies_to_store_byte_matches_cppnix() {
7465        let dir = std::env::temp_dir().join(format!("sui-r5-interp-{}", std::process::id()));
7466        let _ = std::fs::remove_dir_all(&dir);
7467        std::fs::create_dir_all(&dir).unwrap();
7468        let f = dir.join("data.txt");
7469        std::fs::write(&f, b"hello\n").unwrap();
7470        let expr = format!(r#""${{{}}}""#, f.display());
7471        let v = eval(&expr).unwrap();
7472        if let Value::String(ns) = v {
7473            assert_eq!(
7474                ns.chars.to_string(),
7475                "/nix/store/y9dmvfhip31hg8ia4njwjz9vfa3ndphr-data.txt",
7476            );
7477            assert!(ns.has_context());
7478        } else {
7479            panic!("expected string");
7480        }
7481        let _ = std::fs::remove_dir_all(&dir);
7482    }
7483
7484    // ── pipe operators (NotImplemented) ────────────────────
7485    // Pipe operators (|>, <|) are parsed as PipeRight/PipeLeft and
7486    // currently return NotImplemented. We can't easily evaluate them
7487    // here because rnix may not even parse them, so we just rely on
7488    // the binop branch existing.
7489
7490    // ── ParseError surface ─────────────────────────────────
7491
7492    #[test]
7493    fn parse_error_unbalanced_braces() {
7494        let result = eval("{ a = 1");
7495        assert!(result.is_err());
7496        let err = result.unwrap_err();
7497        assert!(matches!(err, EvalError::ParseError(_)));
7498    }
7499
7500    #[test]
7501    fn parse_error_dangling_let() {
7502        let result = eval("let in");
7503        assert!(result.is_err());
7504    }
7505
7506    #[test]
7507    fn parse_error_empty_input() {
7508        let result = eval("");
7509        assert!(result.is_err());
7510    }
7511
7512    // ── num_op coverage via float ops ──────────────────────
7513
7514    #[test]
7515    fn float_int_subtraction() {
7516        assert_eq!(ev("3.5 - 1"), Value::Float(2.5));
7517    }
7518
7519    #[test]
7520    fn int_float_subtraction() {
7521        assert_eq!(ev("3 - 0.5"), Value::Float(2.5));
7522    }
7523
7524    #[test]
7525    fn float_float_division() {
7526        assert_eq!(ev("6.0 / 2.0"), Value::Float(3.0));
7527    }
7528
7529    #[test]
7530    fn int_float_multiplication() {
7531        assert_eq!(ev("3 * 2.5"), Value::Float(7.5));
7532    }
7533
7534    // ── compare with mixed numerics ────────────────────────
7535
7536    #[test]
7537    fn compare_int_float_less() {
7538        assert_eq!(ev("1 < 1.5"), Value::Bool(true));
7539    }
7540
7541    #[test]
7542    fn compare_float_int_more() {
7543        assert_eq!(ev("3.5 > 3"), Value::Bool(true));
7544    }
7545
7546    #[test]
7547    fn compare_equal_int_float() {
7548        assert_eq!(ev("3 <= 3.0"), Value::Bool(true));
7549    }
7550
7551    // ── Equality ──────────────────────────────────────────
7552
7553    #[test]
7554    fn equal_lists_same() {
7555        assert_eq!(ev("[1 2 3] == [1 2 3]"), Value::Bool(true));
7556    }
7557
7558    #[test]
7559    fn equal_lists_diff_length() {
7560        assert_eq!(ev("[1 2] == [1 2 3]"), Value::Bool(false));
7561    }
7562
7563    #[test]
7564    fn not_equal_lists() {
7565        assert_eq!(ev("[1] != [2]"), Value::Bool(true));
7566    }
7567
7568    #[test]
7569    fn equal_attrsets_same() {
7570        assert_eq!(ev("{a = 1; b = 2;} == {b = 2; a = 1;}"), Value::Bool(true));
7571    }
7572
7573    // ── Lambda identity equality (Rc ptr_eq) ────────────────
7574    // Regression test: same lambda via Rc must compare equal.
7575    // Without this, nixpkgs stdenv evaluation enters an infinite loop
7576    // because `crossSystem != localSystem` returns true even when both
7577    // are the same elaborate result (containing shared function attrs).
7578
7579    #[test]
7580    fn lambda_self_equality_in_attrset() {
7581        // Same closure shared via let → inherit must be equal
7582        assert_eq!(
7583            ev("let f = x: x; in { a = 1; inherit f; } == { a = 1; inherit f; }"),
7584            Value::Bool(true),
7585        );
7586    }
7587
7588    #[test]
7589    fn lambda_self_reference_attrset_equality() {
7590        // Attrset with function attr: x == x must be true
7591        assert_eq!(
7592            ev("let x = { a = 1; f = y: y; }; in x == x"),
7593            Value::Bool(true),
7594        );
7595    }
7596
7597    #[test]
7598    fn lambda_different_closures_not_equal() {
7599        // Different lambda closures (even structurally identical) must be false
7600        assert_eq!(
7601            ev("{ f = x: x; } == { f = x: x; }"),
7602            Value::Bool(false),
7603        );
7604    }
7605
7606    #[test]
7607    fn lambda_ne_does_not_force_unused_branch() {
7608        // If crossSystem == localSystem (same obj), != returns false,
7609        // and the then-branch (with throw) is never forced.
7610        assert_eq!(
7611            ev("let ls = { a = 1; f = x: x; }; in if ls != ls then builtins.throw \"bug\" else 42"),
7612            Value::Int(42),
7613        );
7614    }
7615
7616    // ── force_value chains thunks ──────────────────────────
7617
7618    #[test]
7619    fn force_value_through_thunk() {
7620        let root = rnix::Root::parse("1 + 2");
7621        let expr = root.tree().expr().unwrap();
7622        let thunk = Thunk::new_suspended(expr, Env::new());
7623        let val = Value::Thunk(thunk);
7624        assert_eq!(force_value(&val).unwrap(), Value::Int(3));
7625    }
7626
7627    // ── Builtin name "tryEval" lazy arg path ──────────────
7628
7629    #[test]
7630    fn try_eval_catches_thrown_error() {
7631        // tryEval wraps the thunk and catches throws inside.
7632        let v = ev(r#"(builtins.tryEval (builtins.throw "oops")).success"#);
7633        assert_eq!(v, Value::Bool(false));
7634    }
7635
7636    #[test]
7637    fn try_eval_returns_value_on_success() {
7638        let v = ev("(builtins.tryEval 42).value");
7639        assert_eq!(v, Value::Int(42));
7640    }
7641
7642    // ── LegacyLet (`let { body = ...; ...}`) ───────────────
7643
7644    #[test]
7645    fn legacy_let_returns_body_attr() {
7646        // `let { x = 1; body = x + 41; }` is the legacy let form: it
7647        // is desugared as a recursive set whose `body` attr is the
7648        // result.
7649        assert_eq!(ev("let { x = 1; body = x + 41; }"), Value::Int(42));
7650    }
7651
7652    #[test]
7653    fn legacy_let_missing_body_errors() {
7654        let result = eval("let { x = 1; }");
7655        assert!(result.is_err());
7656    }
7657
7658    #[test]
7659    fn legacy_let_with_inherit_from_scope() {
7660        assert_eq!(
7661            ev("let outer = 5; in let { inherit outer; body = outer * 2; }"),
7662            Value::Int(10),
7663        );
7664    }
7665
7666    // ── eval_str interpolation more cases ──────────────────
7667
7668    #[test]
7669    fn interp_with_string_concat_preserves_order() {
7670        assert_eq!(
7671            ev(r#"let a = "x"; b = "y"; in "${a}-${b}""#),
7672            Value::string("x-y"),
7673        );
7674    }
7675
7676    #[test]
7677    fn interp_only_literal_part() {
7678        assert_eq!(ev(r#""no interp here""#), Value::string("no interp here"));
7679    }
7680
7681    // ── eval_attr dynamic / string keys ────────────────────
7682
7683    #[test]
7684    fn dynamic_attr_via_string_key_in_set() {
7685        // `{ "a" = 1; }.a` works because attr keys can be string literals.
7686        assert_eq!(ev(r#"{ "a" = 1; }.a"#), Value::Int(1));
7687    }
7688
7689    #[test]
7690    fn dynamic_attr_via_interpolated_key() {
7691        let v = ev(r#"let k = "foo"; in { ${k} = 99; }.foo"#);
7692        assert_eq!(v, Value::Int(99));
7693    }
7694
7695    // ── String key access via select with dynamic ──────────
7696
7697    #[test]
7698    fn select_with_string_key() {
7699        let v = ev(r#"{ a = 42; }."a""#);
7700        assert_eq!(v, Value::Int(42));
7701    }
7702
7703    // ── Apply via __functor on attrset ─────────────────────
7704
7705    #[test]
7706    fn apply_attrset_with_functor_works() {
7707        let v = ev("let s = { __functor = self: x: x + 1; }; in s 5");
7708        assert_eq!(v, Value::Int(6));
7709    }
7710
7711    // ── Negation of negative ───────────────────────────────
7712
7713    #[test]
7714    fn double_negate_int() {
7715        assert_eq!(ev("- (-5)"), Value::Int(5));
7716    }
7717
7718    // ── Inherit from rec scope binding visibility ──────────
7719
7720    #[test]
7721    fn inherit_in_let_makes_name_available() {
7722        assert_eq!(
7723            ev("let src = { a = 7; }; in let inherit (src) a; in a"),
7724            Value::Int(7),
7725        );
7726    }
7727
7728    // ── String + path ──────────────────────────────────────
7729
7730    #[test]
7731    fn path_plus_string_yields_path() {
7732        let v = ev(r#"/foo + "/bar""#);
7733        match v {
7734            Value::Path(p) => assert_eq!(&*p, "/foo/bar"),
7735            _ => panic!("expected path"),
7736        }
7737    }
7738
7739    // ── Lazy attrset value not forced unless selected ──────
7740
7741    #[test]
7742    fn attrset_value_not_forced_unless_selected() {
7743        // `bad` is an attr whose value would error if forced, but we
7744        // only ever select `good`, so it's never touched.
7745        assert_eq!(
7746            ev(r#"{ bad = builtins.throw "boom"; good = 42; }.good"#),
7747            Value::Int(42),
7748        );
7749    }
7750
7751    // ── Lambda calling itself via let ──────────────────────
7752
7753    #[test]
7754    fn lambda_recursive_via_let() {
7755        // factorial via let-bound recursive function
7756        assert_eq!(
7757            ev("let fact = n: if n == 0 then 1 else n * fact (n - 1); in fact 5"),
7758            Value::Int(120),
7759        );
7760    }
7761
7762    // ── Dynamic key in select ──────────────────────────────
7763
7764    #[test]
7765    fn select_with_dynamic_key_via_var() {
7766        // ${k} interpolation in select position is not standard Nix
7767        // syntax, but a string-literal key works for select.
7768        assert_eq!(ev(r#"let k = { x = 1; }; in k.x"#), Value::Int(1));
7769    }
7770
7771    // ── Compare strings ────────────────────────────────────
7772
7773    #[test]
7774    fn compare_string_lex_greater_or_equal() {
7775        assert_eq!(ev(r#""b" >= "a""#), Value::Bool(true));
7776        assert_eq!(ev(r#""a" >= "a""#), Value::Bool(true));
7777        assert_eq!(ev(r#""a" >= "b""#), Value::Bool(false));
7778    }
7779
7780    // ── PartialEq across types ─────────────────────────────
7781
7782    #[test]
7783    fn equal_int_string_false() {
7784        assert_eq!(ev(r#"1 == "1""#), Value::Bool(false));
7785    }
7786
7787    #[test]
7788    fn equal_null_int_false() {
7789        assert_eq!(ev("null == 0"), Value::Bool(false));
7790    }
7791
7792    // ── Update operator on thunked operands ────────────────
7793
7794    #[test]
7795    fn update_with_let_bound_operands() {
7796        assert_eq!(
7797            ev("let a = { x = 1; }; b = { y = 2; }; in (a // b).y"),
7798            Value::Int(2),
7799        );
7800    }
7801
7802    // ── Concat on let-bound lists ──────────────────────────
7803
7804    #[test]
7805    fn concat_lists_from_let() {
7806        assert_eq!(
7807            ev("let a = [1 2]; b = [3 4]; in builtins.length (a ++ b)"),
7808            Value::Int(4),
7809        );
7810    }
7811
7812    // ── String interpolation: list coercion ─────────────────
7813
7814    #[test]
7815    fn interp_list_coerces_with_spaces() {
7816        // Lists in interpolation are now coerced via coerce_to_string
7817        // (space-joined elements).
7818        assert_eq!(
7819            ev(r#""${toString [1 2 3]}""#),
7820            Value::string("1 2 3"),
7821        );
7822    }
7823
7824    #[test]
7825    fn interp_list_directly_coerces() {
7826        // Direct list interpolation space-joins elements via coerce_to_string.
7827        assert_eq!(
7828            ev(r#""${[1 2]}""#),
7829            Value::string("1 2"),
7830        );
7831    }
7832
7833    // ── String interpolation: outPath ─────────────────────
7834
7835    #[test]
7836    fn interp_outpath_attrset() {
7837        assert_eq!(
7838            ev(r#"let x = { outPath = "/nix/store/abc"; }; in "${x}""#),
7839            Value::string("/nix/store/abc"),
7840        );
7841    }
7842
7843    #[test]
7844    fn interp_tostring_takes_priority_over_outpath() {
7845        assert_eq!(
7846            ev(r#"let x = { __toString = self: "custom"; outPath = "/ignored"; }; in "${x}""#),
7847            Value::string("custom"),
7848        );
7849    }
7850
7851    #[test]
7852    fn interp_derivation_coerces_to_outpath() {
7853        // derivation produces an attrset with outPath
7854        let result = eval(r#"
7855            let drv = builtins.derivation {
7856                name = "test";
7857                system = "x86_64-linux";
7858                builder = "/bin/sh";
7859            };
7860            in "${drv}"
7861        "#).unwrap();
7862        if let Value::String(s) = result {
7863            assert!(s.chars.starts_with("/nix/store/"), "got: {}", s.chars);
7864        } else {
7865            panic!("expected string");
7866        }
7867    }
7868
7869    // ── String interpolation: lambda error ─────────────────
7870
7871    #[test]
7872    fn interp_lambda_errors() {
7873        let result = eval(r#""${x: x}""#);
7874        assert!(result.is_err());
7875    }
7876
7877    // ── force_value tests ────────────────────────────────────
7878
7879    #[test]
7880    fn force_value_int_returns_same() {
7881        let v = Value::Int(42);
7882        assert_eq!(force_value(&v).unwrap(), Value::Int(42));
7883    }
7884
7885    #[test]
7886    fn force_value_bool_returns_same() {
7887        let v = Value::Bool(true);
7888        assert_eq!(force_value(&v).unwrap(), Value::Bool(true));
7889    }
7890
7891    #[test]
7892    fn force_value_string_returns_same() {
7893        let v = Value::string("hello");
7894        assert_eq!(force_value(&v).unwrap(), Value::string("hello"));
7895    }
7896
7897    #[test]
7898    fn force_value_attrs_returns_same() {
7899        let mut a = NixAttrs::new();
7900        a.insert("x".to_string(), Value::Int(1));
7901        let v = Value::Attrs(Rc::new(a.clone()));
7902        assert_eq!(force_value(&v).unwrap(), Value::Attrs(Rc::new(a)));
7903    }
7904
7905    #[test]
7906    fn force_value_list_returns_same() {
7907        let v = Value::list(vec![Value::Int(1), Value::Int(2)]);
7908        assert_eq!(
7909            force_value(&v).unwrap(),
7910            Value::list(vec![Value::Int(1), Value::Int(2)]),
7911        );
7912    }
7913
7914    #[test]
7915    fn force_value_null_returns_null() {
7916        let v = Value::Null;
7917        assert_eq!(force_value(&v).unwrap(), Value::Null);
7918    }
7919
7920    #[test]
7921    fn force_value_evaluated_thunk_returns_cached() {
7922        // Thunk wrapping a simple expression should evaluate and cache
7923        let v = ev("let x = 1 + 2; in x");
7924        assert_eq!(v, Value::Int(3));
7925        // Force again — should return the cached value
7926        assert_eq!(force_value(&v).unwrap(), Value::Int(3));
7927    }
7928
7929    // ── Tail-call loop tests ─────────────────────────────────
7930
7931    #[test]
7932    fn tco_if_true_condition() {
7933        assert_eq!(ev("if true then 42 else 0"), Value::Int(42));
7934    }
7935
7936    #[test]
7937    fn tco_if_false_condition() {
7938        assert_eq!(ev("if false then 42 else 0"), Value::Int(0));
7939    }
7940
7941    #[test]
7942    fn tco_deeply_nested_if_else_chain() {
7943        // Build a chain: if false then 1 else if false then 2 else ... else 150
7944        // All conditions are false except the final else, which produces 150.
7945        let mut expr = String::from("150");
7946        for i in (1..150).rev() {
7947            expr = format!("if false then {} else {}", i, expr);
7948        }
7949        let v = ev(&expr);
7950        assert_eq!(v, Value::Int(150));
7951    }
7952
7953    #[test]
7954    fn tco_assert_true_passes_through() {
7955        assert_eq!(ev("assert true; 42"), Value::Int(42));
7956    }
7957
7958    #[test]
7959    fn tco_assert_false_throws_assertion_failed() {
7960        let result = eval("assert false; 42");
7961        assert!(result.is_err());
7962        let err = result.unwrap_err();
7963        assert!(
7964            matches!(err, EvalError::AssertionFailed(_)),
7965            "expected AssertionFailed, got: {err}",
7966        );
7967    }
7968
7969    #[test]
7970    fn tco_with_makes_scope_available() {
7971        assert_eq!(ev("with { x = 10; y = 20; }; x + y"), Value::Int(30));
7972    }
7973
7974    #[test]
7975    fn tco_let_in_creates_bindings() {
7976        assert_eq!(ev("let a = 5; in a"), Value::Int(5));
7977    }
7978
7979    #[test]
7980    fn tco_let_in_multiple_bindings() {
7981        assert_eq!(ev("let a = 1; b = 2; c = 3; in a + b + c"), Value::Int(6));
7982    }
7983
7984    // ── eval_attrset tests ───────────────────────────────────
7985
7986    #[test]
7987    fn eval_attrset_empty() {
7988        let v = ev("{}");
7989        if let Value::Attrs(attrs) = v {
7990            assert!(attrs.is_empty(), "expected empty attrset");
7991        } else {
7992            panic!("expected attrset, got {v:?}");
7993        }
7994    }
7995
7996    #[test]
7997    fn eval_attrset_simple_kv() {
7998        let v = ev("{ a = 1; b = 2; }");
7999        if let Value::Attrs(attrs) = v {
8000            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
8001            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
8002        } else {
8003            panic!("expected attrset, got {v:?}");
8004        }
8005    }
8006
8007    #[test]
8008    fn eval_attrset_recursive() {
8009        assert_eq!(ev("(rec { a = 1; b = a + 1; }).b"), Value::Int(2));
8010        assert_eq!(ev("(rec { a = 1; b = a + 1; }).a"), Value::Int(1));
8011    }
8012
8013    #[test]
8014    fn eval_attrset_inherit_from_scope() {
8015        assert_eq!(ev("let x = 1; in { inherit x; }.x"), Value::Int(1));
8016    }
8017
8018    #[test]
8019    fn eval_attrset_inherit_from_expr() {
8020        assert_eq!(
8021            ev("{ inherit (builtins) true; }.true"),
8022            Value::Bool(true),
8023        );
8024    }
8025
8026    #[test]
8027    fn eval_attrset_dotted_path() {
8028        assert_eq!(ev("{ a.b.c = 1; }.a.b.c"), Value::Int(1));
8029    }
8030
8031    #[test]
8032    fn eval_attrset_update_merge() {
8033        let v = ev("{ a = 1; } // { b = 2; }");
8034        if let Value::Attrs(attrs) = v {
8035            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
8036            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
8037        } else {
8038            panic!("expected attrset, got {v:?}");
8039        }
8040    }
8041
8042    // ── eval_apply tests ─────────────────────────────────────
8043
8044    #[test]
8045    fn eval_apply_simple_function() {
8046        assert_eq!(ev("(x: x + 1) 2"), Value::Int(3));
8047    }
8048
8049    #[test]
8050    fn eval_apply_pattern_destructuring() {
8051        assert_eq!(ev("({a, b}: a + b) { a = 1; b = 2; }"), Value::Int(3));
8052    }
8053
8054    #[test]
8055    fn eval_apply_default_arguments() {
8056        assert_eq!(ev("({a, b ? 0}: a + b) { a = 1; }"), Value::Int(1));
8057    }
8058
8059    #[test]
8060    fn eval_apply_ellipsis() {
8061        assert_eq!(ev("({a, ...}: a) { a = 1; b = 2; }"), Value::Int(1));
8062    }
8063
8064    // ── eval_select tests ────────────────────────────────────
8065
8066    #[test]
8067    fn eval_select_single_key() {
8068        assert_eq!(ev("{ a = 1; }.a"), Value::Int(1));
8069    }
8070
8071    #[test]
8072    fn eval_select_multi_level() {
8073        assert_eq!(ev("{ a.b = 1; }.a.b"), Value::Int(1));
8074    }
8075
8076    #[test]
8077    fn eval_select_with_or_default() {
8078        assert_eq!(ev("{}.a or 42"), Value::Int(42));
8079    }
8080
8081    #[test]
8082    fn eval_select_missing_key_without_default_throws() {
8083        let result = eval("{}.a");
8084        assert!(result.is_err());
8085    }
8086
8087    // ── BinOp tests ──────────────────────────────────────────
8088
8089    #[test]
8090    fn binop_add_ints() {
8091        assert_eq!(ev("1 + 2"), Value::Int(3));
8092    }
8093
8094    #[test]
8095    fn binop_sub_ints() {
8096        assert_eq!(ev("3 - 1"), Value::Int(2));
8097    }
8098
8099    #[test]
8100    fn binop_mul_ints() {
8101        assert_eq!(ev("2 * 3"), Value::Int(6));
8102    }
8103
8104    #[test]
8105    fn binop_div_ints() {
8106        assert_eq!(ev("6 / 2"), Value::Int(3));
8107    }
8108
8109    #[test]
8110    fn binop_float_arithmetic() {
8111        assert_eq!(ev("1.5 + 2.5"), Value::Float(4.0));
8112    }
8113
8114    #[test]
8115    fn binop_string_concat() {
8116        assert_eq!(
8117            ev(r#""hello" + " " + "world""#),
8118            Value::string("hello world"),
8119        );
8120    }
8121
8122    #[test]
8123    fn binop_list_concat() {
8124        assert_eq!(
8125            ev("[1 2] ++ [3 4]"),
8126            Value::list(vec![
8127                Value::Int(1),
8128                Value::Int(2),
8129                Value::Int(3),
8130                Value::Int(4),
8131            ]),
8132        );
8133    }
8134
8135    #[test]
8136    fn binop_attrset_update() {
8137        let v = ev("{ a = 1; } // { b = 2; }");
8138        if let Value::Attrs(attrs) = v {
8139            assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
8140            assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
8141        } else {
8142            panic!("expected attrset, got {v:?}");
8143        }
8144    }
8145
8146    #[test]
8147    fn binop_less_than() {
8148        assert_eq!(ev("1 < 2"), Value::Bool(true));
8149        assert_eq!(ev("2 < 1"), Value::Bool(false));
8150    }
8151
8152    #[test]
8153    fn binop_greater_than() {
8154        assert_eq!(ev("2 > 1"), Value::Bool(true));
8155        assert_eq!(ev("1 > 2"), Value::Bool(false));
8156    }
8157
8158    #[test]
8159    fn binop_equal() {
8160        assert_eq!(ev("1 == 1"), Value::Bool(true));
8161        assert_eq!(ev("1 == 2"), Value::Bool(false));
8162    }
8163
8164    #[test]
8165    fn binop_not_equal() {
8166        assert_eq!(ev("1 != 2"), Value::Bool(true));
8167        assert_eq!(ev("1 != 1"), Value::Bool(false));
8168    }
8169
8170    #[test]
8171    fn binop_logical_and() {
8172        assert_eq!(ev("true && false"), Value::Bool(false));
8173        assert_eq!(ev("true && true"), Value::Bool(true));
8174    }
8175
8176    #[test]
8177    fn binop_logical_or() {
8178        assert_eq!(ev("true || false"), Value::Bool(true));
8179        assert_eq!(ev("false || false"), Value::Bool(false));
8180    }
8181
8182    #[test]
8183    fn binop_logical_not() {
8184        assert_eq!(ev("!true"), Value::Bool(false));
8185        assert_eq!(ev("!false"), Value::Bool(true));
8186    }
8187
8188    #[test]
8189    fn binop_implication() {
8190        assert_eq!(ev("false -> true"), Value::Bool(true));
8191        assert_eq!(ev("false -> false"), Value::Bool(true));
8192        assert_eq!(ev("true -> true"), Value::Bool(true));
8193        assert_eq!(ev("true -> false"), Value::Bool(false));
8194    }
8195}