packages feed

nova-nix 0.1.3.0 → 0.1.4.0

raw patch · 17 files changed

+4162/−3128 lines, 17 filesdep ~nova-cachePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: nova-cache

API changes (from Hackage documentation)

- Nix.Eval: Thunk :: !Expr -> Env -> !IORef (Maybe NixValue) -> Thunk
- Nix.Eval.Types: Thunk :: !Expr -> Env -> !IORef (Maybe NixValue) -> Thunk
- Nix.Expr.Types: SrcPos :: !Int -> !Int -> SrcPos
- Nix.Expr.Types: SrcSpan :: !Text -> !SrcPos -> !SrcPos -> SrcSpan
- Nix.Expr.Types: [spCol] :: SrcPos -> !Int
- Nix.Expr.Types: [spLine] :: SrcPos -> !Int
- Nix.Expr.Types: [ssEnd] :: SrcSpan -> !SrcPos
- Nix.Expr.Types: [ssFile] :: SrcSpan -> !Text
- Nix.Expr.Types: [ssStart] :: SrcSpan -> !SrcPos
- Nix.Expr.Types: data SrcPos
- Nix.Expr.Types: data SrcSpan
- Nix.Expr.Types: instance GHC.Classes.Eq Nix.Expr.Types.SrcPos
- Nix.Expr.Types: instance GHC.Classes.Eq Nix.Expr.Types.SrcSpan
- Nix.Expr.Types: instance GHC.Show.Show Nix.Expr.Types.SrcPos
- Nix.Expr.Types: instance GHC.Show.Show Nix.Expr.Types.SrcSpan
- Nix.Parser.Internal: currentPos :: Parser (Int, Int)
- Nix.Parser.Internal: expectedButGot :: Text -> Token -> Parser a
- Nix.Parser.Internal: pOptional :: Parser a -> Parser (Maybe a)
- Nix.Parser.Internal: pSome :: Parser a -> Parser [a]
- Nix.Parser.Internal: sepBy :: Parser a -> Token -> Parser [a]
- Nix.Parser.Internal: sepBy1 :: Parser a -> Token -> Parser [a]
- Nix.Parser.Internal: unexpectedEOF :: Text -> Parser a
+ Nix.Eval: Computed :: !NixValue -> ThunkCell
+ Nix.Eval: EagerAttrs :: !Map Text Thunk -> AttrSet
+ Nix.Eval: LazyAttrs :: !Map Text LazyBinding -> !IORef (Map Text Thunk) -> AttrSet
+ Nix.Eval: LazyExpr :: Env -> !Expr -> LazyBinding
+ Nix.Eval: LazyInherit :: Env -> LazyBinding
+ Nix.Eval: LazyInheritFrom :: Env -> !Expr -> LazyBinding
+ Nix.Eval: Pending :: !Expr -> Env -> ThunkCell
+ Nix.Eval: PreBuilt :: !Thunk -> LazyBinding
+ Nix.Eval: ThunkRef :: !IORef ThunkCell -> Thunk
+ Nix.Eval: [envParent] :: Env -> !Maybe Env
+ Nix.Eval: attrSetElems :: AttrSet -> [Thunk]
+ Nix.Eval: attrSetFromMap :: Map Text Thunk -> AttrSet
+ Nix.Eval: attrSetKeys :: AttrSet -> [Text]
+ Nix.Eval: attrSetLookup :: Text -> AttrSet -> Maybe Thunk
+ Nix.Eval: attrSetMember :: Text -> AttrSet -> Bool
+ Nix.Eval: attrSetNull :: AttrSet -> Bool
+ Nix.Eval: attrSetSize :: AttrSet -> Int
+ Nix.Eval: attrSetToAscList :: AttrSet -> [(Text, Thunk)]
+ Nix.Eval: attrSetToMap :: AttrSet -> Map Text Thunk
+ Nix.Eval: data AttrSet
+ Nix.Eval: data LazyBinding
+ Nix.Eval: data ThunkCell
+ Nix.Eval: getFileType :: MonadEval m => Text -> m Text
+ Nix.Eval: instance GHC.Show.Show Nix.Eval.TOMLValue
+ Nix.Eval: readFileBytes :: MonadEval m => Text -> m ByteString
+ Nix.Eval.Types: Computed :: !NixValue -> ThunkCell
+ Nix.Eval.Types: EagerAttrs :: !Map Text Thunk -> AttrSet
+ Nix.Eval.Types: LazyAttrs :: !Map Text LazyBinding -> !IORef (Map Text Thunk) -> AttrSet
+ Nix.Eval.Types: LazyExpr :: Env -> !Expr -> LazyBinding
+ Nix.Eval.Types: LazyInherit :: Env -> LazyBinding
+ Nix.Eval.Types: LazyInheritFrom :: Env -> !Expr -> LazyBinding
+ Nix.Eval.Types: Pending :: !Expr -> Env -> ThunkCell
+ Nix.Eval.Types: PreBuilt :: !Thunk -> LazyBinding
+ Nix.Eval.Types: ThunkRef :: !IORef ThunkCell -> Thunk
+ Nix.Eval.Types: [envParent] :: Env -> !Maybe Env
+ Nix.Eval.Types: attrSetElems :: AttrSet -> [Thunk]
+ Nix.Eval.Types: attrSetFromMap :: Map Text Thunk -> AttrSet
+ Nix.Eval.Types: attrSetKeys :: AttrSet -> [Text]
+ Nix.Eval.Types: attrSetLookup :: Text -> AttrSet -> Maybe Thunk
+ Nix.Eval.Types: attrSetMapWithKey :: (Text -> Thunk -> Thunk) -> AttrSet -> AttrSet
+ Nix.Eval.Types: attrSetMember :: Text -> AttrSet -> Bool
+ Nix.Eval.Types: attrSetNull :: AttrSet -> Bool
+ Nix.Eval.Types: attrSetSize :: AttrSet -> Int
+ Nix.Eval.Types: attrSetToAscList :: AttrSet -> [(Text, Thunk)]
+ Nix.Eval.Types: attrSetToMap :: AttrSet -> Map Text Thunk
+ Nix.Eval.Types: attrSetUnionWith :: (Thunk -> Thunk -> Thunk) -> AttrSet -> AttrSet -> AttrSet
+ Nix.Eval.Types: data AttrSet
+ Nix.Eval.Types: data LazyBinding
+ Nix.Eval.Types: data ThunkCell
+ Nix.Eval.Types: envInsertMany :: Map Text Thunk -> Env -> Env
+ Nix.Eval.Types: getFileType :: MonadEval m => Text -> m Text
+ Nix.Eval.Types: instance GHC.Classes.Eq Nix.Eval.Types.AttrSet
+ Nix.Eval.Types: instance GHC.Classes.Eq Nix.Eval.Types.LazyBinding
+ Nix.Eval.Types: instance GHC.Show.Show Nix.Eval.Types.AttrSet
+ Nix.Eval.Types: instance GHC.Show.Show Nix.Eval.Types.LazyBinding
+ Nix.Eval.Types: instance GHC.Show.Show Nix.Eval.Types.ThunkCell
+ Nix.Eval.Types: newLazyAttrCache :: Map Text LazyBinding -> IORef (Map Text Thunk)
+ Nix.Eval.Types: readFileBytes :: MonadEval m => Text -> m ByteString
+ Nix.Parser.Internal: showToken :: Token -> Text
- Nix.Eval: Env :: !Map Text Thunk -> ![Map Text Thunk] -> Env
+ Nix.Eval: Env :: !Map Text Thunk -> !Maybe Env -> ![AttrSet] -> Env
- Nix.Eval: VAttrs :: !Map Text Thunk -> NixValue
+ Nix.Eval: VAttrs :: !AttrSet -> NixValue
- Nix.Eval: [envWithScopes] :: Env -> ![Map Text Thunk]
+ Nix.Eval: [envWithScopes] :: Env -> ![AttrSet]
- Nix.Eval.Types: Env :: !Map Text Thunk -> ![Map Text Thunk] -> Env
+ Nix.Eval.Types: Env :: !Map Text Thunk -> !Maybe Env -> ![AttrSet] -> Env
- Nix.Eval.Types: VAttrs :: !Map Text Thunk -> NixValue
+ Nix.Eval.Types: VAttrs :: !AttrSet -> NixValue
- Nix.Eval.Types: [envWithScopes] :: Env -> ![Map Text Thunk]
+ Nix.Eval.Types: [envWithScopes] :: Env -> ![AttrSet]
- Nix.Eval.Types: pushWithScope :: Map Text Thunk -> Env -> Env
+ Nix.Eval.Types: pushWithScope :: AttrSet -> Env -> Env

Files

CHANGELOG.md view
@@ -1,5 +1,21 @@ # Changelog +## 0.1.4.0 — 2026-02-26++### Memory Optimization + Lazy Non-Rec Attrsets++- **Lazy non-rec attrsets** — `evalNonRecAttrs` now uses `LazyAttrs` to defer thunk+IORef allocation until first access. For `all-packages.nix` (~15k entries per stdenv stage), only accessed packages allocate thunks.+- **Per-binding Env in `LazyBinding`** — Each `LazyExpr`/`LazyInherit`/`LazyInheritFrom` carries its own `Env` instead of sharing one per-attrset. Fixes a bug where `//` merges of `LazyAttrs` from different scopes lost variables — broke `lib.extends` overlay pattern used by `makeExtensibleWithCustomName`.+- **Batched formal set matching** — `matchFormalSet` creates ONE `Env` with all formals instead of N singleton `Env`s. Uses knot-tying so default expressions can reference other formals, including forward references (`{ a ? b, b ? 1 }: a` → `1`).+- **Env scope chain** — `Env` now uses a parent pointer chain instead of `Map.union`. Variable lookup walks the chain: local bindings, then parent, then with-scopes. Avoids O(n) `Map.union` when extending large envs (e.g. 30k-entry nixpkgs rec set). Peak Map.Bin heap: 956MB → ~40MB.+- **ThunkCell release** — On force, `Pending expr env` is overwritten to `Computed val`, dropping all references to `Expr` and `Env` (matching C++ Nix in-place mutation). Previously retained dead closures indefinitely.+- **Lazy `//` operator** — `mergeAttrSets` preserves `LazyAttrs` when possible, merging binding recipe maps instead of materializing all thunks.+- **Lazy with-scopes** — `pushWithScope` accepts `AttrSet` directly so `LazyAttrs` with-scopes stay lazy. `lookupWithScopes` uses `attrSetLookup` to materialize only the accessed key.+- **Key-driven `intersectAttrs`** — Only touches keys present in both sets instead of materializing entire attrsets.+- **New builtins** — `min`, `max`, `mod`, base64 `encode`/`decode` (via nova-cache 0.2.4)+- `nova-cache >= 0.2.4` dependency bump+- 511 tests, -Werror clean, ormolu 0.7.7.0 clean, hlint clean+ ## 0.1.3.0 — 2026-02-25  ### nixpkgs Compatibility: Module System, Callable Sets, Parser Fixes
README.md view
@@ -21,7 +21,7 @@  - **Parser** — Hand-rolled recursive descent parser for the full Nix expression language. 13 precedence levels, 17 AST constructors, all syntax forms including search paths (`<nixpkgs>`) and dynamic attribute keys (`{ ${expr} = val; }`). Direct `Text` consumption for maximum throughput. - **Lazy Evaluator** — Thunk-based evaluation with environment closures, knot-tying for recursive bindings via Haskell laziness. All 17 AST constructors handled: literals, strings with interpolation, attribute sets (recursive and non-recursive), let bindings, lambdas with formal parameters, if/then/else, with, assert, unary/binary operators, function application, list construction, attribute selection, has-attribute checks, and search path resolution.-- **101 Built-in Functions** — Type checks, arithmetic, bitwise, strings, lists, attribute sets, higher-order (`map`, `filter`, `foldl'`, `sort`, `genList`, `concatMap`, `mapAttrs`), JSON (`toJSON`/`fromJSON`), hashing (SHA-256/SHA-512/SHA-1/MD5), version parsing, `replaceStrings`, `tryEval`, `deepSeq`, `genericClosure`, `setFunctionArgs`/`functionArgs`, string context introspection (`hasContext`, `getContext`, `appendContext`), IO builtins (`import`, `readFile`, `pathExists`, `readDir`, `getEnv`, `toPath`, `toFile`, `findFile`, `scopedImport`, `fetchurl`, `fetchTarball`, `fetchGit`), `derivation`, `placeholder`, `storePath`, and more. 16 builtins available at top level without `builtins.` prefix (`toString`, `map`, `throw`, `import`, `derivation`, `abort`, `baseNameOf`, `dirOf`, `isNull`, `removeAttrs`, `placeholder`, `scopedImport`, `fetchTarball`, `fetchGit`, `fetchurl`, `toFile`) — matching the real Nix language spec.+- **106 Built-in Functions** — Type checks, arithmetic (`min`, `max`, `mod`), bitwise, strings, lists, attribute sets, higher-order (`map`, `filter`, `foldl'`, `sort`, `genList`, `concatMap`, `mapAttrs`), JSON (`toJSON`/`fromJSON`), hashing (SHA-256/SHA-512/SHA-1/MD5), base64 (`encode`/`decode`), version parsing, `replaceStrings`, `tryEval`, `deepSeq`, `genericClosure`, `setFunctionArgs`/`functionArgs`, string context introspection (`hasContext`, `getContext`, `appendContext`), IO builtins (`import`, `readFile`, `pathExists`, `readDir`, `getEnv`, `toPath`, `toFile`, `findFile`, `scopedImport`, `fetchurl`, `fetchTarball`, `fetchGit`), `derivation`, `placeholder`, `storePath`, and more. 16 builtins available at top level without `builtins.` prefix (`toString`, `map`, `throw`, `import`, `derivation`, `abort`, `baseNameOf`, `dirOf`, `isNull`, `removeAttrs`, `placeholder`, `scopedImport`, `fetchTarball`, `fetchGit`, `fetchurl`, `toFile`) — matching the real Nix language spec. - **Search Path Resolution** — `<nixpkgs>` desugars to `builtins.findFile builtins.nixPath "nixpkgs"` — matching real Nix semantics. `NIX_PATH` environment variable is parsed at startup, and `--nix-path` CLI flags merge with it. Directory imports (`import ./dir`) resolve to `dir/default.nix` automatically. - **Dynamic Attribute Keys** — `{ ${expr} = val; }` fully supported in all contexts: non-recursive attrs, recursive attrs, let bindings, attribute selection, and has-attribute checks. Key resolution is cleanly separated from value thunk construction to preserve knot-tying in recursive bindings. - **String Context Tracking** — Every string carries invisible metadata tracking which store paths it references. Context propagates through interpolation, concatenation, `replaceStrings`, and all string operations. The `derivation` builtin collects contexts into `drvInputDrvs` and `drvInputSrcs` — matching real Nix semantics.@@ -193,7 +193,7 @@ | `Nix.Eval.StringInterp` | String interpolation — value coercion with context propagation, indented string whitespace stripping | Done | | `Nix.Eval.Context` | String context construction, queries, extraction — pure helpers for building and inspecting store path references | Done | | `Nix.Eval.IO` | IO evaluation monad — real filesystem access, import cache (with directory import), process execution, store writes, NIX_PATH parsing, per-thunk IORef memoization (matching real Nix in-place mutation) | Done |-| `Nix.Builtins` | Built-in function environment — 101 builtins, search path plumbing (`parseNixPath`), top-level builtin exposure | Done |+| `Nix.Builtins` | Built-in function environment — 106 builtins, search path plumbing (`parseNixPath`), top-level builtin exposure | Done |  ### Store + Builder @@ -289,7 +289,7 @@ - [x] **Lexer** — Full Nix tokenization (integers, floats, strings with interpolation, paths, URIs, search paths, operators, keywords) - [x] **Parser** — 13 precedence levels, all Nix syntax, structured error reporting - [x] **Evaluator** — All 17 AST constructors, lazy thunks, recursive let/rec via knot-tying, with-scope chain, dynamic attribute keys-- [x] **101 builtins** — Type checks, arithmetic, bitwise, strings, lists, attrsets, higher-order, JSON, hashing, regex (`match`/`split` via `regex-tdfa`), version parsing, `setFunctionArgs`/`functionArgs`, tryEval, deepSeq, genericClosure, string context introspection, all IO builtins, derivation+- [x] **106 builtins** — Type checks, arithmetic, bitwise, strings, lists, attrsets, higher-order, JSON, hashing, regex (`match`/`split` via `regex-tdfa`), version parsing, `setFunctionArgs`/`functionArgs`, tryEval, deepSeq, genericClosure, string context introspection, all IO builtins, derivation - [x] **MonadEval refactor** — Evaluator polymorphic in effect monad (`PureEval` for tests, `EvalIO` for real evaluation) - [x] **IO builtins** — `import` (with directory import support), `readFile`, `pathExists`, `readDir`, `getEnv`, `toPath`, `toFile`, `findFile`, `scopedImport`, `fetchurl`, `fetchTarball`, `fetchGit`, `currentTime` - [x] **`derivation`** — Attrset to `.drv` build recipe with computed `drvPath` and `outPath`, context-aware input population
app/Main.hs view
@@ -23,7 +23,7 @@ import Nix.Builder (BuildConfig (..), BuildResult (..), buildWithDeps, defaultBuildConfig) import Nix.Builtins (builtinEnv, parseNixPath) import Nix.Derivation (Derivation (..), DerivationOutput (..))-import Nix.Eval (MonadEval, NixValue (..), Thunk (..), eval, force)+import Nix.Eval (MonadEval, NixValue (..), Thunk (..), attrSetFromMap, attrSetLookup, attrSetToAscList, attrSetToMap, eval, force) import Nix.Eval.IO (EvalState (..), newEvalState, runEvalIO) import Nix.Parser (parseNix) import Nix.Store (Store, closeStore, openStore, writeDrv)@@ -181,13 +181,13 @@ extractDerivation :: NixValue -> IO Derivation extractDerivation (VAttrs attrs) = do   -- Check type = "derivation"-  case Map.lookup "type" attrs of+  case attrSetLookup "type" attrs of     Just (Evaluated (VStr "derivation" _)) -> pure ()     _ -> do       hPutStrLn stderr "error: result is not a derivation (no type = \"derivation\")"       exitFailure   -- Try to extract from _derivation key first-  case Map.lookup "_derivation" attrs of+  case attrSetLookup "_derivation" attrs of     Just (Evaluated (VDerivation drv)) -> pure drv     _ -> do       hPutStrLn stderr "error: derivation result missing _derivation field"@@ -255,8 +255,9 @@   forced <- mapM (force >=> deepForceValue) thunks   pure (VList (map Evaluated forced)) deepForceValue (VAttrs attrs) = do-  forced <- mapM (force >=> deepForceValue) attrs-  pure (VAttrs (Map.map Evaluated forced))+  let m = attrSetToMap attrs+  forced <- mapM (force >=> deepForceValue) m+  pure (VAttrs (attrSetFromMap (Map.map Evaluated forced))) deepForceValue val = pure val  -- | Nix-style pretty-printing of a fully forced value.@@ -271,7 +272,7 @@ prettyValue (VList thunks) =   "[ " <> T.intercalate " " (map prettyThunk thunks) <> " ]" prettyValue (VAttrs attrs) =-  let entries = Map.toAscList attrs+  let entries = attrSetToAscList attrs       rendered = map (\(k, t) -> k <> " = " <> prettyThunk t <> ";") entries    in "{ " <> T.intercalate " " rendered <> " }" prettyValue (VLambda {}) = "«lambda»"@@ -285,7 +286,7 @@ -- 'Evaluated'; unevaluated thunks render as a placeholder. prettyThunk :: Thunk -> T.Text prettyThunk (Evaluated val) = prettyValue val-prettyThunk (Thunk {}) = "«thunk»"+prettyThunk (ThunkRef {}) = "«thunk»"  -- | Escape a string for Nix-style output (quotes, backslashes, newlines, tabs, carriage returns). escapeNixString :: T.Text -> T.Text
nova-nix.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               nova-nix-version:            0.1.3.0+version:            0.1.4.0 synopsis:           Windows-native Nix implementation in pure Haskell description:   A pure Haskell implementation of the Nix package manager that runs natively@@ -69,7 +69,7 @@     , http-types          >= 0.12 && < 0.13     , memory              >= 0.18 && < 1     , mtl                 >= 2.2 && < 2.4-    , nova-cache          >= 0.2 && < 0.3+    , nova-cache          >= 0.2.4 && < 0.3     , process             >= 1.6 && < 1.7     , regex-tdfa          >= 1.3 && < 1.4     , sqlite-simple       >= 0.4 && < 0.5
src/Nix/Builtins.hs view
@@ -18,7 +18,7 @@ import qualified Data.Map.Strict as Map import Data.Text (Text) import qualified Data.Text as T-import Nix.Eval (Env (..), NixValue (..), Thunk (..), builtinNames, currentSystemStr, evaluated)+import Nix.Eval (Env (..), NixValue (..), Thunk (..), attrSetFromMap, builtinNames, currentSystemStr, evaluated) import Nix.Eval.Types (mkStr) import Nix.Store.Path (defaultStoreDirText) @@ -46,6 +46,7 @@           ]             -- Top-level builtin functions (available without builtins. prefix)             ++ map topLevelBuiltin topLevelBuiltinNames,+      envParent = Nothing,       envWithScopes = []     } @@ -81,12 +82,12 @@ builtinEnvWithScope timestamp searchPaths scope =   let base = builtinEnv timestamp searchPaths       scopeMap = Map.fromList scope-   in base {envBindings = Map.union scopeMap (envBindings base)}+   in Env {envBindings = scopeMap, envParent = Just base, envWithScopes = []}  -- | The @builtins@ attribute set, derived from the central registry. builtinsAttrSet :: Integer -> [Thunk] -> NixValue builtinsAttrSet timestamp searchPaths =-  VAttrs $ Map.union builtinEntries (standardEntries timestamp searchPaths)+  VAttrs $ attrSetFromMap $ Map.union builtinEntries (standardEntries timestamp searchPaths)   where     builtinEntries =       Map.fromList [(name, evaluated (VBuiltin name [])) | name <- builtinNames]@@ -129,10 +130,11 @@               | otherwise -> (before, T.drop 1 after)        in evaluated             ( VAttrs-                ( Map.fromList-                    [ ("prefix", evaluated (mkStr prefix)),-                      ("path", evaluated (mkStr path))-                    ]+                ( attrSetFromMap $+                    Map.fromList+                      [ ("prefix", evaluated (mkStr prefix)),+                        ("path", evaluated (mkStr path))+                      ]                 )             ) 
src/Nix/DependencyGraph.hs view
@@ -87,10 +87,10 @@                       dnDerivation = drv,                       dnDeps = deps                     }-                visited' = Map.insert sp node visited-             in case resolveNewDeps readDrv visited' deps of+                visitedWithNode = Map.insert sp node visited+             in case resolveNewDeps readDrv visitedWithNode deps of                   Left err -> Left err-                  Right newItems -> go visited' (foldl' (|>) rest newItems)+                  Right newItems -> go visitedWithNode (foldl' (|>) rest newItems)  -- | Resolve unvisited dependencies by reading their .drv files. resolveNewDeps ::@@ -159,9 +159,9 @@     Seq.EmptyL -> TopoSorted (reverse sorted) -- unreachable, guarded above     sp Seq.:< rest ->       let dependents = fromMaybe [] (Map.lookup sp reverseAdj)-          (depCount', newZero) = decrementDependents depCount dependents-          queue' = foldl' (|>) rest newZero-       in kahnLoop reverseAdj depCount' queue' (sp : sorted) (sortedCount + 1) totalNodes+          (updatedDepCount, newZero) = decrementDependents depCount dependents+          extendedQueue = foldl' (|>) rest newZero+       in kahnLoop reverseAdj updatedDepCount extendedQueue (sp : sorted) (sortedCount + 1) totalNodes  -- | Decrement in-degree for each dependent; collect any that reach zero. decrementDependents :: Map StorePath Int -> [StorePath] -> (Map StorePath Int, [StorePath])@@ -169,10 +169,10 @@   where     step (counts, zeros) dep =       let newDeg = maybe 0 (subtract 1) (Map.lookup dep counts)-          counts' = Map.insert dep newDeg counts+          updatedCounts = Map.insert dep newDeg counts        in if newDeg == 0-            then (counts', dep : zeros)-            else (counts', zeros)+            then (updatedCounts, dep : zeros)+            else (updatedCounts, zeros)  -- --------------------------------------------------------------------------- -- Queries@@ -188,8 +188,8 @@         | Set.member sp visited -> go visited rest         | otherwise ->             let deps = maybe [] dnDeps (Map.lookup sp graph)-                visited' = if sp == root then visited else Set.insert sp visited-             in go visited' (foldl' (|>) rest deps)+                visitedWithDep = if sp == root then visited else Set.insert sp visited+             in go visitedWithDep (foldl' (|>) rest deps)  -- | Direct dependencies of a store path. directDeps :: DepGraph -> StorePath -> [StorePath]
src/Nix/Eval.hs view
@@ -14,2601 +14,3436 @@   ( -- * Values (re-exported from Types)     NixValue (..),     Thunk (..),--    -- * String context (re-exported from Types)-    StringContextElement (..),-    StringContext (..),-    emptyContext,-    mkStr,--    -- * Environment (re-exported from Types)-    Env (..),-    emptyEnv,--    -- * Evaluation monad (re-exported from Types)-    MonadEval (..),-    PureEval (..),--    -- * Evaluation-    eval,-    force,--    -- * Helpers (for Builtins)-    typeName,-    evaluated,--    -- * Builtin registry-    BuiltinDef (..),-    builtinRegistry,-    builtinNames,--    -- * Platform-    currentSystemStr,-  )-where--import Control.Monad (when, (>=>))-import qualified Crypto.Hash as CH-import qualified Data.Array as Array-import Data.Bits (xor, (.&.), (.|.))-import qualified Data.ByteArray as BA-import Data.Char (chr, digitToInt, isAlpha, isDigit, isHexDigit, ord)-import Data.List (foldl')-import Data.Map.Strict (Map)-import qualified Data.Map.Strict as Map-import Data.Maybe (catMaybes, isJust)-import qualified Data.Set as Set-import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Encoding as TE-import Nix.Derivation (Derivation (..), DerivationOutput (..), textToPlatform, toATerm)-import Nix.Eval.Context (extractInputDrvs, extractInputSrcs)-import Nix.Eval.Operator (evalBinary, evalUnary, nixCompare, nixEqual)-import Nix.Eval.StringInterp (coerceToString, evalIndStringParts, evalStringParts)-import Nix.Eval.Types-  ( Env (..),-    MonadEval (..),-    NixValue (..),-    PureEval (..),-    StringContext (..),-    StringContextElement (..),-    Thunk (..),-    emptyContext,-    emptyEnv,-    envInsert,-    envInsertThunk,-    envLookup,-    evaluated,-    mkStr,-    mkSyntheticThunk,-    mkThunk,-    pushWithScope,-    runPureEval,-    typeName,-  )-import Nix.Expr.Types-  ( AttrKey (..),-    AttrPath,-    BinaryOp (..),-    Binding (..),-    Expr (..),-    Formal (..),-    Formals (..),-    NixAtom (..),-    StringPart (..),-  )-import Nix.Hash (byteToHex, sha256Hex, truncatedBase32)-import Nix.Store.Path (StorePath (..), defaultStoreDir, defaultStoreDirText, parseStorePath, storePathToFilePath)-import qualified System.Info-import Text.Regex.TDFA (matchAllText)-import qualified Text.Regex.TDFA as RE---- | Evaluate a Nix expression in an environment.-eval :: (MonadEval m) => Env -> Expr -> m NixValue-eval env expr = case expr of-  ELit atom -> evalLit atom-  EStr parts -> uncurry VStr <$> evalStringParts eval env parts-  EIndStr parts -> uncurry VStr <$> evalIndStringParts eval env parts-  EVar name -> evalVar env name-  EAttrs isRec bindings -> evalAttrs env isRec bindings-  EList exprs -> pure (VList (map (mkThunk env) exprs))-  ESelect target path defExpr -> evalSelect env target path defExpr-  EHasAttr target path -> evalHasAttr env target path-  EApp func arg -> evalApp env func arg-  ELambda formals body -> pure (VLambda env formals body)-  ELet bindings body -> evalLet env bindings body-  EIf cond thenExpr elseExpr -> evalIf env cond thenExpr elseExpr-  EWith scope body -> evalWith env scope body-  EAssert cond body -> evalAssert env cond body-  EUnary op operand -> do-    val <- eval env operand-    evalUnary op val-  ESearchPath name -> evalSearchPath env name-  EBinary OpAnd left right -> evalShortCircuitAnd env left right-  EBinary OpOr left right -> evalShortCircuitOr env left right-  EBinary OpImpl left right -> evalShortCircuitImpl env left right-  EBinary op left right -> do-    leftVal <- eval env left-    rightVal <- eval env right-    evalBinary force op leftVal rightVal---- | Force a thunk to a value.------ Delegates to 'forceThunk' which is a 'MonadEval' method — this--- allows IO evaluators to implement memoization (caching the result--- after the first force) while pure evaluators simply re-evaluate.-force :: (MonadEval m) => Thunk -> m NixValue-force = forceThunk eval---- ------------------------------------------------------------------------------ Literal--- -----------------------------------------------------------------------------evalLit :: (MonadEval m) => NixAtom -> m NixValue-evalLit atom = case atom of-  NixInt n -> pure (VInt n)-  NixFloat n -> pure (VFloat n)-  NixBool b -> pure (VBool b)-  NixNull -> pure VNull-  NixUri u -> pure (mkStr u)-  NixPath p -> VPath <$> resolvePathLiteral p---- ------------------------------------------------------------------------------ Search paths (<nixpkgs>, <nixpkgs/lib>)--- ------------------------------------------------------------------------------- | Evaluate a search path expression.--- Desugars to @builtins.findFile builtins.nixPath "name"@ — exactly how--- real Nix handles @\<name\>@ expressions.-evalSearchPath :: (MonadEval m) => Env -> Text -> m NixValue-evalSearchPath env name = do-  builtinsVal <- evalVar env "builtins"-  case builtinsVal of-    VAttrs builtinsAttrs ->-      case Map.lookup "nixPath" builtinsAttrs of-        Just nixPathThunk -> do-          nixPathVal <- force nixPathThunk-          builtinFindFile nixPathVal (mkStr name)-        Nothing ->-          throwEvalError ("file '" <> name <> "' was not found in the Nix search path")-    _ ->-      throwEvalError ("file '" <> name <> "' was not found in the Nix search path")---- ------------------------------------------------------------------------------ Variables--- -----------------------------------------------------------------------------evalVar :: (MonadEval m) => Env -> Text -> m NixValue-evalVar env name =-  case envLookup name env of-    Just thunk -> force thunk-    Nothing -> throwEvalError ("undefined variable '" <> name <> "'")---- ------------------------------------------------------------------------------ Attribute sets--- -----------------------------------------------------------------------------evalAttrs :: (MonadEval m) => Env -> Bool -> [Binding] -> m NixValue-evalAttrs env False bindings = evalNonRecAttrs env bindings-evalAttrs env True bindings = evalRecAttrs env bindings---- | Non-recursive attribute set: thunks capture the outer environment.-evalNonRecAttrs :: (MonadEval m) => Env -> [Binding] -> m NixValue-evalNonRecAttrs env bindings = do-  attrMap <- buildBindingsMapM env env bindings-  pure (VAttrs attrMap)---- | Recursive attribute set: thunks capture the completed environment--- (Haskell's laziness ties the knot — Thunk's Env field is lazy).--- Dynamic keys in recursive attrs evaluate against the outer env--- (the rec env is not yet available during key resolution).-evalRecAttrs :: (MonadEval m) => Env -> [Binding] -> m NixValue-evalRecAttrs env bindings = do-  -- Resolve dynamic keys eagerly against the outer env.-  resolvedBindings <- resolveBindingKeys env bindings-  -- Now knot-tie: thunks capture recEnv, keys are already resolved.-  let recEnv = env {envBindings = Map.union recBindings (envBindings env)}-      recBindings = buildResolvedBindingsMap recEnv resolvedBindings-  pure (VAttrs recBindings)---- | Build a flat attribute map from bindings (monadic — resolves--- dynamic keys via eval, but only creates thunks for values).--- Used by non-rec {}.------ @thunkEnv@ is the environment captured by value thunks.--- @keyEnv@ is used to evaluate dynamic key expressions.------ Handles nested attribute paths (@a.b.c = 1@) by building nested--- 'VAttrs' maps.  Handles @inherit@ by looking up names in the--- environment or a source expression.-buildBindingsMapM :: (MonadEval m) => Env -> Env -> [Binding] -> m (Map Text Thunk)-buildBindingsMapM thunkEnv keyEnv = foldlM' mergeBinding Map.empty-  where-    mergeBinding current binding = do-      new <- processBindingM thunkEnv keyEnv binding-      pure (mergeAttrMaps current new)---- | Strict left fold over a list in a monad.-foldlM' :: (Monad m) => (b -> a -> m b) -> b -> [a] -> m b-foldlM' _ acc [] = pure acc-foldlM' f !acc (x : xs) = do-  acc2 <- f acc x-  foldlM' f acc2 xs---- | Process a single binding into key-value pairs.-processBindingM :: (MonadEval m) => Env -> Env -> Binding -> m (Map Text Thunk)-processBindingM thunkEnv keyEnv (NamedBinding path bodyExpr) =-  buildNestedAttrM thunkEnv keyEnv path bodyExpr-processBindingM _ lookupEnv (Inherit Nothing names) =-  pure $ Map.fromList [(n, inheritLookup lookupEnv n) | n <- names]-processBindingM thunkEnv _ (Inherit (Just fromExpr) names) =-  pure $-    Map.fromList-      [ (n, mkThunk thunkEnv (ESelect fromExpr [StaticKey n] Nothing))-      | n <- names-      ]---- ------------------------------------------------------------------------------ Resolved bindings (for knot-tying in rec {} and let)--- ------------------------------------------------------------------------------- | A binding with all attribute keys pre-resolved to text.--- Used by 'evalRecAttrs' and 'evalLet' to separate key resolution--- (monadic, may evaluate dynamic keys) from thunk construction--- (pure, enables knot-tying).-data ResolvedBinding-  = -- | @path = expr@ with all keys resolved to text.-    ResolvedNamed ![Text] !Expr-  | -- | @inherit attrs@ from the surrounding scope.-    ResolvedInherit ![Text]-  | -- | @inherit (from) attrs@.-    ResolvedInheritFrom !Expr ![Text]---- | Resolve all attribute keys in a list of bindings, evaluating--- dynamic keys against the given environment.-resolveBindingKeys :: (MonadEval m) => Env -> [Binding] -> m [ResolvedBinding]-resolveBindingKeys keyEnv = fmap catMaybes . mapM resolveOne-  where-    resolveOne (NamedBinding path bodyExpr) = do-      resolvedPath <- mapM (resolveKey keyEnv) path-      -- If any key segment is null, skip the entire binding-      -- (matching real Nix; used by the module system for conditional attrs).-      pure (fmap (`ResolvedNamed` bodyExpr) (sequence resolvedPath))-    resolveOne (Inherit Nothing names) =-      pure (Just (ResolvedInherit names))-    resolveOne (Inherit (Just fromExpr) names) =-      pure (Just (ResolvedInheritFrom fromExpr names))---- | Build a flat attribute map from pre-resolved bindings (pure).--- Thunks capture @thunkEnv@ — suitable for knot-tying.-buildResolvedBindingsMap :: Env -> [ResolvedBinding] -> Map Text Thunk-buildResolvedBindingsMap thunkEnv =-  foldl' mergeBinding Map.empty-  where-    mergeBinding current binding =-      mergeAttrMaps current (processResolved thunkEnv binding)---- | Process a single resolved binding into key-value pairs (pure).-processResolved :: Env -> ResolvedBinding -> Map Text Thunk-processResolved thunkEnv (ResolvedNamed path bodyExpr) =-  buildResolvedNestedAttr thunkEnv path bodyExpr-processResolved lookupEnv (ResolvedInherit names) =-  Map.fromList [(n, inheritLookup lookupEnv n) | n <- names]-processResolved thunkEnv (ResolvedInheritFrom fromExpr names) =-  Map.fromList-    [ (n, mkThunk thunkEnv (ESelect fromExpr [StaticKey n] Nothing))-    | n <- names-    ]---- | Build a nested attribute structure from a resolved dotted path (pure).-buildResolvedNestedAttr :: Env -> [Text] -> Expr -> Map Text Thunk-buildResolvedNestedAttr _thunkEnv [] _bodyExpr = Map.empty-buildResolvedNestedAttr thunkEnv [key] bodyExpr =-  Map.singleton key (mkThunk thunkEnv bodyExpr)-buildResolvedNestedAttr thunkEnv (key : rest) bodyExpr =-  Map.singleton key (evaluated (VAttrs (buildResolvedNestedAttr thunkEnv rest bodyExpr)))---- | Look up a name for @inherit@.  If not found, create a thunk--- that will error when forced (matching real Nix behaviour).-inheritLookup :: Env -> Text -> Thunk-inheritLookup env name =-  case envLookup name env of-    Just thunk -> thunk-    Nothing ->-      -- Deferred error: only triggers if this binding is actually demanded.-      -- Constructs: builtins.throw "undefined variable '<name>'"-      let errMsg = "undefined variable '" <> name <> "'"-          throwExpr = EApp (EVar "throw") (EStr [StrLit errMsg])-       in mkThunk env throwExpr---- | Build a nested attribute structure from a dotted path.--- @a.b.c = expr@ becomes @{ a = { b = { c = thunk; }; }; }@.-buildNestedAttrM :: (MonadEval m) => Env -> Env -> AttrPath -> Expr -> m (Map Text Thunk)-buildNestedAttrM _thunkEnv _keyEnv [] _bodyExpr =-  throwEvalError "empty attribute path"-buildNestedAttrM thunkEnv keyEnv [key] bodyExpr = do-  resolved <- resolveKey keyEnv key-  case resolved of-    Nothing -> pure Map.empty-    Just keyText -> pure (Map.singleton keyText (mkThunk thunkEnv bodyExpr))-buildNestedAttrM thunkEnv keyEnv (key : rest) bodyExpr = do-  resolved <- resolveKey keyEnv key-  case resolved of-    Nothing -> pure Map.empty-    Just keyText -> do-      inner <- buildNestedAttrM thunkEnv keyEnv rest bodyExpr-      pure (Map.singleton keyText (evaluated (VAttrs inner)))---- | Resolve an attribute key.  Static keys always return 'Just'.--- Dynamic keys evaluate to a string or 'null'; null means "skip this binding"--- (matching real Nix, used by the module system for conditional attributes).-resolveKey :: (MonadEval m) => Env -> AttrKey -> m (Maybe Text)-resolveKey _env (StaticKey name) = pure (Just name)-resolveKey env (DynamicKey expr) = do-  val <- eval env expr-  case val of-    VStr s _ -> pure (Just s)-    VNull -> pure Nothing-    _ -> throwEvalError ("dynamic attribute key must be a string, got " <> typeName val)---- | Merge two attribute maps.  For overlapping keys where both sides--- are attribute sets, merge recursively (for nested attr paths).-mergeAttrMaps :: Map Text Thunk -> Map Text Thunk -> Map Text Thunk-mergeAttrMaps = Map.unionWith mergeThunks---- | Merge two thunks at the same key.  If both are evaluated VAttrs,--- merge their contents recursively.  Otherwise the right wins.-mergeThunks :: Thunk -> Thunk -> Thunk-mergeThunks (Evaluated (VAttrs a)) (Evaluated (VAttrs b)) =-  Evaluated (VAttrs (mergeAttrMaps a b))-mergeThunks _ new = new---- ------------------------------------------------------------------------------ Select / has-attr--- -----------------------------------------------------------------------------evalSelect :: (MonadEval m) => Env -> Expr -> AttrPath -> Maybe Expr -> m NixValue-evalSelect env target path defExpr = do-  targetVal <- eval env target-  result <- walkAttrPath env path targetVal-  case result of-    Just val -> pure val-    Nothing -> case defExpr of-      Just def -> eval env def-      Nothing -> throwEvalError ("attribute path not found in " <> typeName targetVal)---- | Walk an attribute path through nested attribute sets.--- Returns @Just value@ if the full path resolves, @Nothing@ if any--- key is missing or a non-set is encountered mid-path.--- The @env@ is used only for resolving dynamic keys.-walkAttrPath :: (MonadEval m) => Env -> AttrPath -> NixValue -> m (Maybe NixValue)-walkAttrPath _env [] val = pure (Just val)-walkAttrPath env (key : rest) val = case val of-  VAttrs attrs -> do-    resolved <- resolveKey env key-    case resolved >>= (`Map.lookup` attrs) of-      Just thunk -> do-        inner <- force thunk-        walkAttrPath env rest inner-      Nothing -> pure Nothing-  _ -> pure Nothing--evalHasAttr :: (MonadEval m) => Env -> Expr -> AttrPath -> m NixValue-evalHasAttr env target path = do-  targetVal <- eval env target-  result <- walkAttrPath env path targetVal-  case result of-    Just _ -> pure (VBool True)-    Nothing -> pure (VBool False)---- ------------------------------------------------------------------------------ Application--- -----------------------------------------------------------------------------evalApp :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue-evalApp env funcExpr argExpr = do-  funcVal <- eval env funcExpr-  case funcVal of-    VLambda closureEnv formals body -> do-      let argThunk = mkThunk env argExpr-      extEnv <- matchFormals closureEnv formals argThunk-      eval extEnv body-    VBuiltin "tryEval" [] -> do-      result <- catchEvalError (eval env argExpr)-      case result of-        Right val ->-          pure-            ( VAttrs-                ( Map.fromList-                    [ ("success", evaluated (VBool True)),-                      ("value", evaluated val)-                    ]-                )-            )-        Left _ ->-          pure-            ( VAttrs-                ( Map.fromList-                    [ ("success", evaluated (VBool False)),-                      ("value", evaluated (VBool False))-                    ]-                )-            )-    VBuiltin name accArgs -> do-      argVal <- eval env argExpr-      applyBuiltin name accArgs argVal-    -- __functor support: a set with __functor is callable.-    -- Semantics: (attrs.__functor attrs) arg-    VAttrs attrs-      | Just functorThunk <- Map.lookup "__functor" attrs -> do-          functor <- force functorThunk-          -- Apply __functor to the set itself, yielding a function-          partiallyApplied <- applyValue functor funcVal-          -- Apply the resulting function to the argument-          argVal <- eval env argExpr-          applyValue partiallyApplied argVal-    _ -> throwEvalError ("attempt to call " <> typeName funcVal <> ", which is not a function")---- | Match function formals against an argument thunk.-matchFormals :: (MonadEval m) => Env -> Formals -> Thunk -> m Env-matchFormals closureEnv (FormalName name) argThunk =-  pure (envInsertThunk name argThunk closureEnv)-matchFormals closureEnv (FormalSet formals allowExtra) argThunk = do-  argVal <- force argThunk-  matchFormalSet closureEnv formals allowExtra argVal-matchFormals closureEnv (FormalNamedSet name formals allowExtra) argThunk = do-  argVal <- force argThunk-  -- Bind the @-pattern name (e.g. "args") BEFORE matching formals so that-  -- default expressions like @system ? args.system or ...@ can see it.-  let envWithAt = envInsertThunk name argThunk closureEnv-  matchFormalSet envWithAt formals allowExtra argVal---- | Match a formal set pattern against a VAttrs argument.-matchFormalSet :: (MonadEval m) => Env -> [Formal] -> Bool -> NixValue -> m Env-matchFormalSet closureEnv formals allowExtra argVal =-  case argVal of-    VAttrs attrs -> do-      checkExtraKeys formals allowExtra attrs-      foldl' (bindFormal attrs) (pure closureEnv) formals-    _ -> throwEvalError ("function expects a set argument, got " <> typeName argVal)---- | Verify that no unexpected keys are present (unless @...@ allows them).-checkExtraKeys :: (MonadEval m) => [Formal] -> Bool -> Map Text Thunk -> m ()-checkExtraKeys _ True _ = pure ()-checkExtraKeys formals False attrs =-  let expected = map fName formals-      actual = Map.keys attrs-      extra = filter (`notElem` expected) actual-   in case extra of-        [] -> pure ()-        (k : _) -> throwEvalError ("unexpected attribute '" <> k <> "' in function argument")---- | Bind a single formal parameter from the argument attrset.-bindFormal :: (MonadEval m) => Map Text Thunk -> m Env -> Formal -> m Env-bindFormal attrs acc (Formal name defExpr) = do-  env <- acc-  case Map.lookup name attrs of-    Just thunk -> pure (envInsertThunk name thunk env)-    Nothing -> case defExpr of-      Just def -> pure (envInsertThunk name (mkThunk env def) env)-      Nothing -> throwEvalError ("missing required attribute '" <> name <> "'")---- ------------------------------------------------------------------------------ Let / if / with / assert--- ------------------------------------------------------------------------------- | Let is recursive in Nix: all bindings are visible to each other.--- Knot-tying via Haskell laziness (Thunk's Env field is lazy).--- Dynamic keys in let evaluate against the outer env (the let env--- is not yet available during key resolution).-evalLet :: (MonadEval m) => Env -> [Binding] -> Expr -> m NixValue-evalLet env bindings body = do-  resolvedBindings <- resolveBindingKeys env bindings-  let letEnv = env {envBindings = Map.union letBindings (envBindings env)}-      letBindings = buildResolvedBindingsMap letEnv resolvedBindings-  eval letEnv body--evalIf :: (MonadEval m) => Env -> Expr -> Expr -> Expr -> m NixValue-evalIf env cond thenExpr elseExpr = do-  condVal <- eval env cond-  case condVal of-    VBool True -> eval env thenExpr-    VBool False -> eval env elseExpr-    _ -> throwEvalError ("'if' condition must be a Boolean, got " <> typeName condVal)--evalWith :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue-evalWith env scope body = do-  scopeVal <- eval env scope-  case scopeVal of-    VAttrs attrs -> eval (pushWithScope attrs env) body-    _ -> throwEvalError ("'with' requires a set, got " <> typeName scopeVal)--evalAssert :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue-evalAssert env cond body = do-  condVal <- eval env cond-  case condVal of-    VBool True -> eval env body-    VBool False -> throwEvalError "assertion failed"-    _ -> throwEvalError ("assertion condition must be a Boolean, got " <> typeName condVal)---- ------------------------------------------------------------------------------ Short-circuit Boolean operators--- -----------------------------------------------------------------------------evalShortCircuitAnd :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue-evalShortCircuitAnd env left right = do-  leftVal <- eval env left-  case leftVal of-    VBool False -> pure (VBool False)-    VBool True -> do-      rightVal <- eval env right-      case rightVal of-        VBool _ -> pure rightVal-        _ -> throwEvalError ("second operand of && must be a Boolean, got " <> typeName rightVal)-    _ -> throwEvalError ("first operand of && must be a Boolean, got " <> typeName leftVal)--evalShortCircuitOr :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue-evalShortCircuitOr env left right = do-  leftVal <- eval env left-  case leftVal of-    VBool True -> pure (VBool True)-    VBool False -> do-      rightVal <- eval env right-      case rightVal of-        VBool _ -> pure rightVal-        _ -> throwEvalError ("second operand of || must be a Boolean, got " <> typeName rightVal)-    _ -> throwEvalError ("first operand of || must be a Boolean, got " <> typeName leftVal)--evalShortCircuitImpl :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue-evalShortCircuitImpl env left right = do-  leftVal <- eval env left-  case leftVal of-    VBool False -> pure (VBool True)-    VBool True -> do-      rightVal <- eval env right-      case rightVal of-        VBool _ -> pure rightVal-        _ -> throwEvalError ("second operand of -> must be a Boolean, got " <> typeName rightVal)-    _ -> throwEvalError ("first operand of -> must be a Boolean, got " <> typeName leftVal)---- ------------------------------------------------------------------------------ Builtin registry (single-definition-site for all builtins)--- ------------------------------------------------------------------------------- | A builtin function definition: its arity and implementation.-data BuiltinDef m = BuiltinDef-  { bdArity :: !Int,-    bdApply :: [NixValue] -> m NixValue-  }---- | Define an arity-1 builtin.-builtin1 :: (MonadEval m) => Text -> (NixValue -> m NixValue) -> (Text, BuiltinDef m)-builtin1 name f =-  ( name,-    BuiltinDef 1 $ \case-      [a] -> f a-      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")-  )---- | Define an arity-2 builtin.-builtin2 ::-  (MonadEval m) =>-  Text ->-  (NixValue -> NixValue -> m NixValue) ->-  (Text, BuiltinDef m)-builtin2 name f =-  ( name,-    BuiltinDef 2 $ \case-      [a, b] -> f a b-      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")-  )---- | Define an arity-3 builtin.-builtin3 ::-  (MonadEval m) =>-  Text ->-  (NixValue -> NixValue -> NixValue -> m NixValue) ->-  (Text, BuiltinDef m)-builtin3 name f =-  ( name,-    BuiltinDef 3 $ \case-      [a, b, c] -> f a b c-      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")-  )---- | Central registry of all builtins.  Adding a new builtin is a single--- entry here plus its implementation function — no other files need changes.-builtinRegistry :: (MonadEval m) => Map Text (BuiltinDef m)-builtinRegistry =-  Map.fromList-    [ -- Type checking (arity 1)-      builtin1 "typeOf" (pure . mkStr . typeOfValue),-      builtin1 "isNull" (pure . VBool . isNullVal),-      builtin1 "isInt" (pure . VBool . isIntVal),-      builtin1 "isFloat" (pure . VBool . isFloatVal),-      builtin1 "isBool" (pure . VBool . isBoolVal),-      builtin1 "isString" (pure . VBool . isStringVal),-      builtin1 "isList" (pure . VBool . isListVal),-      builtin1 "isAttrs" (pure . VBool . isAttrsVal),-      builtin1 "isFunction" (pure . VBool . isFunctionVal),-      -- List operations (arity 1)-      builtin1 "length" builtinLength,-      builtin1 "head" builtinHead,-      builtin1 "tail" builtinTail,-      -- String operations (arity 1)-      builtin1 "toString" (fmap (uncurry VStr) . coerceToStringPermissive),-      builtin1 "stringLength" builtinStringLength,-      -- Control (arity 1)-      builtin1 "throw" builtinThrow,-      builtin1 "abort" builtinThrow,-      -- Attr set operations (arity 1)-      builtin1 "attrNames" builtinAttrNames,-      builtin1 "attrValues" builtinAttrValues,-      builtin1 "listToAttrs" builtinListToAttrs,-      -- Attr set operations (arity 2)-      builtin2 "hasAttr" builtinHasAttr,-      builtin2 "getAttr" builtinGetAttr,-      builtin2 "removeAttrs" builtinRemoveAttrs,-      builtin2 "intersectAttrs" builtinIntersectAttrs,-      builtin2 "catAttrs" builtinCatAttrs,-      -- List higher-order (arity 2)-      builtin2 "map" builtinMap,-      builtin2 "filter" builtinFilter,-      builtin2 "genList" builtinGenList,-      builtin2 "sort" builtinSort,-      builtin2 "concatMap" builtinConcatMap,-      builtin2 "any" builtinAny,-      builtin2 "all" builtinAll,-      builtin2 "elem" builtinElem,-      builtin2 "elemAt" builtinElemAt,-      builtin2 "partition" builtinPartition,-      builtin2 "groupBy" builtinGroupBy,-      -- String operations (arity 2)-      builtin2 "concatStringsSep" builtinConcatStringsSep,-      -- Arity 3-      builtin3 "foldl'" builtinFoldl,-      builtin3 "substring" builtinSubstring,-      -- Numeric-      builtin1 "isPath" (pure . VBool . isPathVal),-      builtin1 "ceil" builtinCeil,-      builtin1 "floor" builtinFloor,-      builtin2 "seq" (\_ b -> pure b),-      builtin2 "trace" (\_ b -> pure b),-      builtin1 "unsafeDiscardStringContext" builtinDiscardContext,-      builtin1 "unsafeDiscardOutputDependency" builtinDiscardOutputDep,-      -- String context introspection-      builtin1 "hasContext" builtinHasContext,-      builtin1 "getContext" builtinGetContext,-      builtin2 "appendContext" builtinAppendContext,-      builtin1 "baseNameOf" builtinBaseNameOf,-      builtin1 "dirOf" builtinDirOf,-      builtin1 "concatLists" builtinConcatLists,-      builtin2 "lessThan" builtinLessThan,-      -- Arithmetic + bitwise-      builtin2 "add" builtinAdd,-      builtin2 "sub" builtinSub,-      builtin2 "mul" builtinMul,-      builtin2 "div" builtinDiv,-      builtin2 "bitAnd" builtinBitAnd,-      builtin2 "bitOr" builtinBitOr,-      builtin2 "bitXor" builtinBitXor,-      -- Attr set higher-order-      builtin2 "mapAttrs" builtinMapAttrs,-      builtin1 "functionArgs" builtinFunctionArgs,-      builtin2 "setFunctionArgs" builtinSetFunctionArgs,-      builtin2 "zipAttrsWith" builtinZipAttrsWith,-      -- String manipulation-      builtin2 "match" builtinMatch,-      builtin2 "split" builtinSplit,-      builtin3 "replaceStrings" builtinReplaceStrings,-      builtin2 "compareVersions" builtinCompareVersions,-      builtin1 "splitVersion" builtinSplitVersion,-      builtin1 "parseDrvName" builtinParseDrvName,-      -- Serialization + hashing-      builtin1 "toJSON" builtinToJSON,-      builtin1 "fromJSON" builtinFromJSON,-      builtin2 "hashString" builtinHashString,-      -- Error handling + sequencing-      builtin1 "tryEval" (\_ -> throwEvalError "unreachable: tryEval handled in evalApp"),-      builtin2 "deepSeq" builtinDeepSeq,-      -- Graph traversal-      builtin1 "genericClosure" builtinGenericClosure,-      -- IO builtins (delegate to MonadEval methods)-      builtin1 "import" builtinImport,-      builtin1 "readFile" builtinReadFile,-      builtin1 "pathExists" builtinPathExists,-      builtin1 "readDir" builtinReadDir,-      builtin1 "getEnv" builtinGetEnv,-      builtin1 "toPath" builtinToPath,-      -- Store path operations-      builtin1 "placeholder" builtinPlaceholder,-      builtin1 "storePath" builtinStorePath,-      builtin2 "findFile" builtinFindFile,-      builtin2 "toFile" builtinToFile,-      builtin2 "scopedImport" builtinScopedImport,-      -- Network fetchers-      builtin1 "fetchurl" builtinFetchurl,-      builtin1 "fetchTarball" builtinFetchTarball,-      builtin1 "fetchGit" builtinFetchGit,-      -- Derivation construction-      builtin1 "derivation" builtinDerivation,-      -- Error context (pass-through — context only matters on error)-      builtin2 "addErrorContext" (\_ val -> pure val),-      -- Attr position (return null — nixpkgs handles this gracefully)-      builtin2 "unsafeGetAttrPos" (\_ _ -> pure VNull)-    ]---- | Names of all registered builtins.-builtinNames :: [Text]-builtinNames = Map.keys (builtinRegistry :: Map Text (BuiltinDef PureEval))---- ------------------------------------------------------------------------------ Builtin dispatch (partial application via accumulated args)--- ------------------------------------------------------------------------------- | Arity of a builtin (how many arguments before execution).-builtinArity :: Text -> Int-builtinArity name = maybe 1 bdArity (Map.lookup name (builtinRegistry :: Map Text (BuiltinDef PureEval)))---- | Apply a builtin with accumulated args.  If we have enough args,--- execute; otherwise return a partially applied builtin.-applyBuiltin :: (MonadEval m) => Text -> [NixValue] -> NixValue -> m NixValue-applyBuiltin name accArgs arg =-  let allArgs = accArgs ++ [arg]-      arity = builtinArity name-   in if length allArgs < arity-        then pure (VBuiltin name allArgs)-        else executeBuiltin name allArgs---- | Apply a function value (lambda or builtin) to one argument.--- Used by higher-order builtins to invoke user-supplied functions.-applyValue :: (MonadEval m) => NixValue -> NixValue -> m NixValue-applyValue (VLambda closureEnv formals body) arg = do-  extEnv <- matchFormals closureEnv formals (evaluated arg)-  eval extEnv body-applyValue (VBuiltin name accArgs) arg =-  applyBuiltin name accArgs arg-applyValue other _ =-  throwEvalError ("attempt to call " <> typeName other <> ", which is not a function")---- | Execute a builtin once all arguments are collected.------ Direct case dispatch avoids rebuilding the polymorphic 'builtinRegistry'--- Map on every call.  'builtinRegistry' is polymorphic in @m@ so GHC--- cannot cache it as a CAF — it gets reconstructed on every use.--- Pattern matching on the name is zero-allocation.-executeBuiltin :: (MonadEval m) => Text -> [NixValue] -> m NixValue-executeBuiltin name args = case name of-  -- Type checking (arity 1)-  "typeOf" -> apply1 (pure . mkStr . typeOfValue)-  "isNull" -> apply1 (pure . VBool . isNullVal)-  "isInt" -> apply1 (pure . VBool . isIntVal)-  "isFloat" -> apply1 (pure . VBool . isFloatVal)-  "isBool" -> apply1 (pure . VBool . isBoolVal)-  "isString" -> apply1 (pure . VBool . isStringVal)-  "isList" -> apply1 (pure . VBool . isListVal)-  "isAttrs" -> apply1 (pure . VBool . isAttrsVal)-  "isFunction" -> apply1 (pure . VBool . isFunctionVal)-  -- List operations (arity 1)-  "length" -> apply1 builtinLength-  "head" -> apply1 builtinHead-  "tail" -> apply1 builtinTail-  -- String operations (arity 1)-  "toString" -> apply1 (fmap (uncurry VStr) . coerceToStringPermissive)-  "stringLength" -> apply1 builtinStringLength-  -- Control (arity 1)-  "throw" -> apply1 builtinThrow-  "abort" -> apply1 builtinThrow-  -- Attr set operations (arity 1)-  "attrNames" -> apply1 builtinAttrNames-  "attrValues" -> apply1 builtinAttrValues-  "listToAttrs" -> apply1 builtinListToAttrs-  -- Attr set operations (arity 2)-  "hasAttr" -> apply2 builtinHasAttr-  "getAttr" -> apply2 builtinGetAttr-  "removeAttrs" -> apply2 builtinRemoveAttrs-  "intersectAttrs" -> apply2 builtinIntersectAttrs-  "catAttrs" -> apply2 builtinCatAttrs-  -- List higher-order (arity 2)-  "map" -> apply2 builtinMap-  "filter" -> apply2 builtinFilter-  "genList" -> apply2 builtinGenList-  "sort" -> apply2 builtinSort-  "concatMap" -> apply2 builtinConcatMap-  "any" -> apply2 builtinAny-  "all" -> apply2 builtinAll-  "elem" -> apply2 builtinElem-  "elemAt" -> apply2 builtinElemAt-  "partition" -> apply2 builtinPartition-  "groupBy" -> apply2 builtinGroupBy-  -- String operations (arity 2)-  "concatStringsSep" -> apply2 builtinConcatStringsSep-  -- Arity 3-  "foldl'" -> apply3 builtinFoldl-  "substring" -> apply3 builtinSubstring-  -- Numeric-  "isPath" -> apply1 (pure . VBool . isPathVal)-  "ceil" -> apply1 builtinCeil-  "floor" -> apply1 builtinFloor-  "seq" -> apply2 (\_ b -> pure b)-  "trace" -> apply2 (\_ b -> pure b)-  "unsafeDiscardStringContext" -> apply1 builtinDiscardContext-  "unsafeDiscardOutputDependency" -> apply1 builtinDiscardOutputDep-  -- String context introspection-  "hasContext" -> apply1 builtinHasContext-  "getContext" -> apply1 builtinGetContext-  "appendContext" -> apply2 builtinAppendContext-  "baseNameOf" -> apply1 builtinBaseNameOf-  "dirOf" -> apply1 builtinDirOf-  "concatLists" -> apply1 builtinConcatLists-  "lessThan" -> apply2 builtinLessThan-  -- Arithmetic + bitwise-  "add" -> apply2 builtinAdd-  "sub" -> apply2 builtinSub-  "mul" -> apply2 builtinMul-  "div" -> apply2 builtinDiv-  "bitAnd" -> apply2 builtinBitAnd-  "bitOr" -> apply2 builtinBitOr-  "bitXor" -> apply2 builtinBitXor-  -- Attr set higher-order-  "mapAttrs" -> apply2 builtinMapAttrs-  "functionArgs" -> apply1 builtinFunctionArgs-  "setFunctionArgs" -> apply2 builtinSetFunctionArgs-  "zipAttrsWith" -> apply2 builtinZipAttrsWith-  -- String manipulation-  "match" -> apply2 builtinMatch-  "split" -> apply2 builtinSplit-  "replaceStrings" -> apply3 builtinReplaceStrings-  "compareVersions" -> apply2 builtinCompareVersions-  "splitVersion" -> apply1 builtinSplitVersion-  "parseDrvName" -> apply1 builtinParseDrvName-  -- Serialization + hashing-  "toJSON" -> apply1 builtinToJSON-  "fromJSON" -> apply1 builtinFromJSON-  "hashString" -> apply2 builtinHashString-  -- Error handling + sequencing-  "tryEval" -> apply1 (\_ -> throwEvalError "unreachable: tryEval handled in evalApp")-  "deepSeq" -> apply2 builtinDeepSeq-  -- Graph traversal-  "genericClosure" -> apply1 builtinGenericClosure-  -- IO builtins (delegate to MonadEval methods)-  "import" -> apply1 builtinImport-  "readFile" -> apply1 builtinReadFile-  "pathExists" -> apply1 builtinPathExists-  "readDir" -> apply1 builtinReadDir-  "getEnv" -> apply1 builtinGetEnv-  "toPath" -> apply1 builtinToPath-  -- Store path operations-  "placeholder" -> apply1 builtinPlaceholder-  "storePath" -> apply1 builtinStorePath-  "findFile" -> apply2 builtinFindFile-  "toFile" -> apply2 builtinToFile-  "scopedImport" -> apply2 builtinScopedImport-  -- Network fetchers-  "fetchurl" -> apply1 builtinFetchurl-  "fetchTarball" -> apply1 builtinFetchTarball-  "fetchGit" -> apply1 builtinFetchGit-  -- Derivation construction-  "derivation" -> apply1 builtinDerivation-  -- Error context (pass-through — context only matters on error)-  "addErrorContext" -> apply2 (\_ val -> pure val)-  -- Attr position (return null — nixpkgs handles this gracefully)-  "unsafeGetAttrPos" -> apply2 (\_ _ -> pure VNull)-  _ -> throwEvalError ("unknown builtin '" <> name <> "'")-  where-    apply1 f = case args of-      [a] -> f a-      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")-    apply2 f = case args of-      [a, b] -> f a b-      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")-    apply3 f = case args of-      [a, b, c] -> f a b c-      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")---- ------------------------------------------------------------------------------ Builtin implementations — type checking--- -----------------------------------------------------------------------------typeOfValue :: NixValue -> Text-typeOfValue val = case val of-  VInt _ -> "int"-  VFloat _ -> "float"-  VBool _ -> "bool"-  VNull -> "null"-  VStr _ _ -> "string"-  VPath _ -> "path"-  VList _ -> "list"-  VAttrs _ -> "set"-  VLambda {} -> "lambda"-  VBuiltin _ _ -> "lambda"-  VDerivation _ -> "set"--isNullVal :: NixValue -> Bool-isNullVal VNull = True-isNullVal _ = False--isIntVal :: NixValue -> Bool-isIntVal (VInt _) = True-isIntVal _ = False--isFloatVal :: NixValue -> Bool-isFloatVal (VFloat _) = True-isFloatVal _ = False--isBoolVal :: NixValue -> Bool-isBoolVal (VBool _) = True-isBoolVal _ = False--isStringVal :: NixValue -> Bool-isStringVal (VStr _ _) = True-isStringVal _ = False--isListVal :: NixValue -> Bool-isListVal (VList _) = True-isListVal _ = False--isAttrsVal :: NixValue -> Bool-isAttrsVal (VAttrs _) = True-isAttrsVal _ = False--isFunctionVal :: NixValue -> Bool-isFunctionVal (VLambda {}) = True-isFunctionVal (VBuiltin _ _) = True-isFunctionVal _ = False---- ------------------------------------------------------------------------------ Builtin implementations — list (arity 1)--- -----------------------------------------------------------------------------builtinLength :: (MonadEval m) => NixValue -> m NixValue-builtinLength (VList xs) = pure (VInt (fromIntegral (length xs)))-builtinLength other = throwEvalError ("builtins.length: expected a list, got " <> typeName other)--builtinHead :: (MonadEval m) => NixValue -> m NixValue-builtinHead (VList []) = throwEvalError "builtins.head: empty list"-builtinHead (VList (x : _)) = force x-builtinHead other = throwEvalError ("builtins.head: expected a list, got " <> typeName other)--builtinTail :: (MonadEval m) => NixValue -> m NixValue-builtinTail (VList []) = throwEvalError "builtins.tail: empty list"-builtinTail (VList (_ : xs)) = pure (VList xs)-builtinTail other = throwEvalError ("builtins.tail: expected a list, got " <> typeName other)---- ------------------------------------------------------------------------------ Builtin implementations — string (arity 1)--- -----------------------------------------------------------------------------builtinStringLength :: (MonadEval m) => NixValue -> m NixValue-builtinStringLength (VStr s _) = pure (VInt (fromIntegral (T.length s)))-builtinStringLength other =-  throwEvalError ("builtins.stringLength: expected a string, got " <> typeName other)---- ------------------------------------------------------------------------------ Builtin implementations — control--- -----------------------------------------------------------------------------builtinThrow :: (MonadEval m) => NixValue -> m NixValue-builtinThrow (VStr msg _) = throwEvalError msg-builtinThrow other = throwEvalError ("builtins.throw: expected a string, got " <> typeName other)---- ------------------------------------------------------------------------------ Builtin implementations — attr set (arity 1)--- -----------------------------------------------------------------------------builtinAttrNames :: (MonadEval m) => NixValue -> m NixValue-builtinAttrNames (VAttrs attrs) =-  pure (VList (map (evaluated . mkStr) (Map.keys attrs)))-builtinAttrNames other =-  throwEvalError ("builtins.attrNames: expected a set, got " <> typeName other)--builtinAttrValues :: (MonadEval m) => NixValue -> m NixValue-builtinAttrValues (VAttrs attrs) =-  pure (VList (Map.elems attrs))-builtinAttrValues other =-  throwEvalError ("builtins.attrValues: expected a set, got " <> typeName other)--builtinListToAttrs :: (MonadEval m) => NixValue -> m NixValue-builtinListToAttrs (VList thunks) = do-  pairs <- mapM listToAttrsPair thunks-  pure (VAttrs (Map.fromList pairs))-builtinListToAttrs other =-  throwEvalError ("builtins.listToAttrs: expected a list, got " <> typeName other)---- | Extract { name, value } from a thunk for listToAttrs.-listToAttrsPair :: (MonadEval m) => Thunk -> m (Text, Thunk)-listToAttrsPair thunk = do-  val <- force thunk-  case val of-    VAttrs attrs -> do-      nameThunk <--        maybe (throwEvalError "builtins.listToAttrs: element missing 'name'") pure $-          Map.lookup "name" attrs-      nameVal <- force nameThunk-      case nameVal of-        VStr keyName _ ->-          case Map.lookup "value" attrs of-            Just valueThunk -> pure (keyName, valueThunk)-            Nothing -> throwEvalError "builtins.listToAttrs: element missing 'value'"-        _ -> throwEvalError "builtins.listToAttrs: 'name' must be a string"-    _ -> throwEvalError "builtins.listToAttrs: element must be a set"---- ------------------------------------------------------------------------------ Builtin implementations — attr set (arity 2)--- -----------------------------------------------------------------------------builtinHasAttr :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinHasAttr (VStr key _) (VAttrs attrs) =-  pure (VBool (Map.member key attrs))-builtinHasAttr (VStr _ _) other =-  throwEvalError ("builtins.hasAttr: expected a set, got " <> typeName other)-builtinHasAttr other _ =-  throwEvalError ("builtins.hasAttr: expected a string, got " <> typeName other)--builtinGetAttr :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinGetAttr (VStr key _) (VAttrs attrs) =-  case Map.lookup key attrs of-    Just thunk -> force thunk-    Nothing -> throwEvalError ("builtins.getAttr: attribute '" <> key <> "' not found")-builtinGetAttr (VStr _ _) other =-  throwEvalError ("builtins.getAttr: expected a set, got " <> typeName other)-builtinGetAttr other _ =-  throwEvalError ("builtins.getAttr: expected a string, got " <> typeName other)--builtinRemoveAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinRemoveAttrs (VAttrs attrs) (VList thunks) = do-  keys <- mapM forceToString thunks-  pure (VAttrs (foldl' (flip Map.delete) attrs keys))-  where-    forceToString thunk = do-      val <- force thunk-      case val of-        VStr s _ -> pure s-        _ -> throwEvalError "builtins.removeAttrs: key list must contain strings"-builtinRemoveAttrs (VAttrs _) other =-  throwEvalError ("builtins.removeAttrs: expected a list, got " <> typeName other)-builtinRemoveAttrs other _ =-  throwEvalError ("builtins.removeAttrs: expected a set, got " <> typeName other)--builtinIntersectAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinIntersectAttrs (VAttrs a) (VAttrs b) =-  pure (VAttrs (Map.intersection b a))-builtinIntersectAttrs (VAttrs _) other =-  throwEvalError ("builtins.intersectAttrs: expected a set, got " <> typeName other)-builtinIntersectAttrs other _ =-  throwEvalError ("builtins.intersectAttrs: expected a set, got " <> typeName other)--builtinCatAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinCatAttrs (VStr key _) (VList thunks) = do-  vals <- catAttrsCollect key thunks-  pure (VList vals)-builtinCatAttrs (VStr _ _) other =-  throwEvalError ("builtins.catAttrs: expected a list, got " <> typeName other)-builtinCatAttrs other _ =-  throwEvalError ("builtins.catAttrs: expected a string, got " <> typeName other)---- | Collect values for a given key from a list of attrsets.-catAttrsCollect :: (MonadEval m) => Text -> [Thunk] -> m [Thunk]-catAttrsCollect _ [] = pure []-catAttrsCollect key (thunk : rest) = do-  val <- force thunk-  case val of-    VAttrs attrs ->-      case Map.lookup key attrs of-        Just found -> (found :) <$> catAttrsCollect key rest-        Nothing -> catAttrsCollect key rest-    _ -> throwEvalError "builtins.catAttrs: list element must be a set"---- ------------------------------------------------------------------------------ Builtin implementations — list higher-order (arity 2)--- -----------------------------------------------------------------------------builtinMap :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinMap func (VList thunks) =-  -- Lazy: each element is a deferred application, forced only on demand.-  pure (VList (map (deferApply func) thunks))-builtinMap _ other =-  throwEvalError ("builtins.map: expected a list, got " <> typeName other)--builtinFilter :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinFilter predFn (VList thunks) = do-  filtered <- filterThunks predFn thunks-  pure (VList filtered)-builtinFilter _ other =-  throwEvalError ("builtins.filter: expected a list, got " <> typeName other)--filterThunks :: (MonadEval m) => NixValue -> [Thunk] -> m [Thunk]-filterThunks _ [] = pure []-filterThunks predFn (thunk : rest) = do-  val <- force thunk-  result <- applyValue predFn val-  case result of-    VBool True -> (thunk :) <$> filterThunks predFn rest-    VBool False -> filterThunks predFn rest-    _ -> throwEvalError "builtins.filter: predicate must return a bool"--builtinGenList :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinGenList func (VInt n)-  | n < 0 = throwEvalError "builtins.genList: length must be non-negative"-  | otherwise =-      -- Lazy: each element is a deferred @f i@, forced only on demand.-      let fnThunk = evaluated func-          env = Env {envBindings = Map.fromList [("__fn", fnThunk)], envWithScopes = []}-          mkIndexThunk i = mkThunk env (EApp (EVar "__fn") (ELit (NixInt i)))-       in pure (VList (map mkIndexThunk [0 .. n - 1]))-builtinGenList _ other =-  throwEvalError ("builtins.genList: expected an integer, got " <> typeName other)--builtinSort :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinSort comparator (VList thunks) = do-  vals <- mapM force thunks-  sorted <- insertionSort comparator vals-  pure (VList (map evaluated sorted))-builtinSort _ other =-  throwEvalError ("builtins.sort: expected a list, got " <> typeName other)---- | Stable insertion sort using a user-supplied comparator.--- The comparator takes two args (curried) and returns bool.-insertionSort :: (MonadEval m) => NixValue -> [NixValue] -> m [NixValue]-insertionSort _ [] = pure []-insertionSort cmp (x : xs) = do-  sorted <- insertionSort cmp xs-  insertSorted cmp x sorted--insertSorted :: (MonadEval m) => NixValue -> NixValue -> [NixValue] -> m [NixValue]-insertSorted _ val [] = pure [val]-insertSorted cmp val (y : ys) = do-  partial <- applyValue cmp val-  result <- applyValue partial y-  case result of-    VBool True -> pure (val : y : ys)-    VBool False -> (y :) <$> insertSorted cmp val ys-    _ -> throwEvalError "builtins.sort: comparator must return a bool"--builtinConcatMap :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinConcatMap func (VList thunks) = do-  -- Semi-eager: must force each application to discover list structure for-  -- concatenation, but element thunks within those sub-lists stay lazy.-  let deferredApps = map (deferApply func) thunks-  results <- mapM force deferredApps-  concatted <- mapM extractList results-  pure (VList (concat concatted))-builtinConcatMap _ other =-  throwEvalError ("builtins.concatMap: expected a list, got " <> typeName other)--extractList :: (MonadEval m) => NixValue -> m [Thunk]-extractList (VList xs) = pure xs-extractList other =-  throwEvalError ("builtins.concatMap: function must return a list, got " <> typeName other)--builtinAny :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinAny predFn (VList thunks) = do-  result <- anyThunk predFn thunks-  pure (VBool result)-builtinAny _ other =-  throwEvalError ("builtins.any: expected a list, got " <> typeName other)--anyThunk :: (MonadEval m) => NixValue -> [Thunk] -> m Bool-anyThunk _ [] = pure False-anyThunk predFn (thunk : rest) = do-  val <- force thunk-  result <- applyValue predFn val-  case result of-    VBool True -> pure True-    VBool False -> anyThunk predFn rest-    _ -> throwEvalError "builtins.any: predicate must return a bool"--builtinAll :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinAll predFn (VList thunks) = do-  result <- allThunk predFn thunks-  pure (VBool result)-builtinAll _ other =-  throwEvalError ("builtins.all: expected a list, got " <> typeName other)--allThunk :: (MonadEval m) => NixValue -> [Thunk] -> m Bool-allThunk _ [] = pure True-allThunk predFn (thunk : rest) = do-  val <- force thunk-  result <- applyValue predFn val-  case result of-    VBool True -> allThunk predFn rest-    VBool False -> pure False-    _ -> throwEvalError "builtins.all: predicate must return a bool"--builtinElem :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinElem needle (VList thunks) = do-  found <- elemCheck needle thunks-  pure (VBool found)-builtinElem _ other =-  throwEvalError ("builtins.elem: expected a list, got " <> typeName other)--elemCheck :: (MonadEval m) => NixValue -> [Thunk] -> m Bool-elemCheck _ [] = pure False-elemCheck needle (thunk : rest) = do-  val <- force thunk-  eq <- nixEqual force needle val-  if eq then pure True else elemCheck needle rest--builtinElemAt :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinElemAt (VList thunks) (VInt idx)-  | idx < 0 || idx >= fromIntegral (length thunks) =-      throwEvalError-        ( "builtins.elemAt: index "-            <> T.pack (show idx)-            <> " out of bounds for list of length "-            <> T.pack (show (length thunks))-        )-  | otherwise = case drop (fromIntegral idx) thunks of-      (t : _) -> force t-      [] -> throwEvalError "builtins.elemAt: index out of bounds"-builtinElemAt (VList _) other =-  throwEvalError ("builtins.elemAt: expected an integer, got " <> typeName other)-builtinElemAt other _ =-  throwEvalError ("builtins.elemAt: expected a list, got " <> typeName other)--builtinPartition :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinPartition predFn (VList thunks) = do-  (rightThunks, wrongThunks) <- partitionThunks predFn thunks-  pure-    ( VAttrs-        ( Map.fromList-            [ ("right", evaluated (VList rightThunks)),-              ("wrong", evaluated (VList wrongThunks))-            ]-        )-    )-builtinPartition _ other =-  throwEvalError ("builtins.partition: expected a list, got " <> typeName other)--partitionThunks :: (MonadEval m) => NixValue -> [Thunk] -> m ([Thunk], [Thunk])-partitionThunks _ [] = pure ([], [])-partitionThunks predFn (thunk : rest) = do-  val <- force thunk-  result <- applyValue predFn val-  (rs, ws) <- partitionThunks predFn rest-  case result of-    VBool True -> pure (thunk : rs, ws)-    VBool False -> pure (rs, thunk : ws)-    _ -> throwEvalError "builtins.partition: predicate must return a bool"--builtinGroupBy :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinGroupBy func (VList thunks) = do-  groups <- groupByCollect func thunks Map.empty-  pure (VAttrs (Map.map (evaluated . VList . reverse) groups))-builtinGroupBy _ other =-  throwEvalError ("builtins.groupBy: expected a list, got " <> typeName other)--groupByCollect ::-  (MonadEval m) =>-  NixValue ->-  [Thunk] ->-  Map Text [Thunk] ->-  m (Map Text [Thunk])-groupByCollect _ [] acc = pure acc-groupByCollect func (thunk : rest) acc = do-  val <- force thunk-  result <- applyValue func val-  case result of-    VStr key _ ->-      groupByCollect func rest (Map.insertWith (++) key [thunk] acc)-    _ -> throwEvalError "builtins.groupBy: function must return a string"---- ------------------------------------------------------------------------------ Builtin implementations — string (arity 2)--- -----------------------------------------------------------------------------builtinConcatStringsSep :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinConcatStringsSep (VStr sep sepCtx) (VList thunks) = do-  pairs <- mapM forceToStrCtx thunks-  let texts = map fst pairs-      mergedCtx = sepCtx <> mconcat (map snd pairs)-  pure (VStr (T.intercalate sep texts) mergedCtx)-  where-    forceToStrCtx thunk = do-      val <- force thunk-      case val of-        VStr s ctx -> pure (s, ctx)-        _ -> throwEvalError "builtins.concatStringsSep: list elements must be strings"-builtinConcatStringsSep (VStr _ _) other =-  throwEvalError ("builtins.concatStringsSep: expected a list, got " <> typeName other)-builtinConcatStringsSep other _ =-  throwEvalError ("builtins.concatStringsSep: expected a string, got " <> typeName other)---- ------------------------------------------------------------------------------ Builtin implementations — arity 3--- -----------------------------------------------------------------------------builtinFoldl :: (MonadEval m) => NixValue -> NixValue -> NixValue -> m NixValue-builtinFoldl op initial (VList thunks) =-  foldlStrict op initial thunks-builtinFoldl _ _ other =-  throwEvalError ("builtins.foldl': expected a list, got " <> typeName other)---- | Strict left fold: apply @op acc elem@ for each element.--- @op@ is curried so we call @applyValue op acc@ then @applyValue partial elem@.-foldlStrict :: (MonadEval m) => NixValue -> NixValue -> [Thunk] -> m NixValue-foldlStrict _ acc [] = pure acc-foldlStrict op acc (thunk : rest) = do-  val <- force thunk-  partial <- applyValue op acc-  stepped <- applyValue partial val-  foldlStrict op stepped rest--builtinSubstring :: (MonadEval m) => NixValue -> NixValue -> NixValue -> m NixValue-builtinSubstring (VInt start) (VInt len) (VStr s ctx) =-  let clampedStart = max 0 (fromIntegral start)-      -- Nix clamps len to available length (negative len means rest of string)-      available = T.length s - clampedStart-      clampedLen =-        if len < 0-          then available-          else min (fromIntegral len) available-   in -- Context is preserved through substring (matching real Nix).-      pure (VStr (T.take clampedLen (T.drop clampedStart s)) ctx)-builtinSubstring _ _ (VStr _ _) =-  throwEvalError "builtins.substring: start and length must be integers"-builtinSubstring _ _ other =-  throwEvalError ("builtins.substring: expected a string, got " <> typeName other)---- ------------------------------------------------------------------------------ Builtin helpers--- ------------------------------------------------------------------------------- | Build a thunk that defers @f arg@ — the application only happens when--- the thunk is forced.  Reuses the existing eval machinery via a synthetic--- @EApp (EVar "__fn") (EVar "__arg")@ in a self-contained env.-deferApply :: NixValue -> Thunk -> Thunk-deferApply func argThunk =-  let env =-        Env-          { envBindings =-              Map.fromList-                [ ("__fn", evaluated func),-                  ("__arg", argThunk)-                ],-            envWithScopes = []-          }-   in mkSyntheticThunk env (EApp (EVar "__fn") (EVar "__arg"))---- | Permissive coercion used by @builtins.toString@.------ Like 'coerceToString' but additionally handles lists: elements are--- recursively coerced and joined with spaces, matching real Nix semantics.--- @toString [1 2 3]@ gives @"1 2 3"@.-coerceToStringPermissive :: (MonadEval m) => NixValue -> m (Text, StringContext)-coerceToStringPermissive (VList thunks) = do-  parts <- mapM coerceThunk thunks-  let texts = map fst parts-      ctx = mconcat (map snd parts)-  pure (T.intercalate " " texts, ctx)-  where-    coerceThunk thunk = do-      val <- force thunk-      coerceToStringPermissive val-coerceToStringPermissive other = coerceToString other---- | The current system platform string.-currentSystemStr :: Text-currentSystemStr = case (System.Info.arch, System.Info.os) of-  ("x86_64", "mingw32") -> "x86_64-windows"-  ("x86_64", "darwin") -> "x86_64-darwin"-  ("aarch64", "darwin") -> "aarch64-darwin"-  ("aarch64", "linux") -> "aarch64-linux"-  ("x86_64", "linux") -> "x86_64-linux"-  (arch, os) -> T.pack arch <> "-" <> T.pack os---- | Store dir with trailing slash, for building store paths.-storeDirPrefix :: Text-storeDirPrefix = defaultStoreDirText <> "/"---- ------------------------------------------------------------------------------ Builtin implementations — numeric + context--- -----------------------------------------------------------------------------isPathVal :: NixValue -> Bool-isPathVal (VPath _) = True-isPathVal _ = False--builtinCeil :: (MonadEval m) => NixValue -> m NixValue-builtinCeil (VFloat f) = pure (VInt (ceiling f))-builtinCeil (VInt n) = pure (VInt n)-builtinCeil other = throwEvalError ("builtins.ceil: expected a number, got " <> typeName other)--builtinFloor :: (MonadEval m) => NixValue -> m NixValue-builtinFloor (VFloat f) = pure (VInt (floor f))-builtinFloor (VInt n) = pure (VInt n)-builtinFloor other = throwEvalError ("builtins.floor: expected a number, got " <> typeName other)--builtinDiscardContext :: (MonadEval m) => NixValue -> m NixValue-builtinDiscardContext (VStr s _) = pure (mkStr s)-builtinDiscardContext other =-  throwEvalError ("builtins.unsafeDiscardStringContext: expected a string, got " <> typeName other)---- | Strip only derivation output dependencies (SCDrvOutput, SCAllOutputs),--- keeping plain store path references (SCPlain).-builtinDiscardOutputDep :: (MonadEval m) => NixValue -> m NixValue-builtinDiscardOutputDep (VStr s (StringContext ctx)) =-  let kept = Set.filter isPlain ctx-   in pure (VStr s (StringContext kept))-  where-    isPlain (SCPlain _) = True-    isPlain _ = False-builtinDiscardOutputDep other =-  throwEvalError ("builtins.unsafeDiscardOutputDependency: expected a string, got " <> typeName other)---- | Check whether a string has any context elements.-builtinHasContext :: (MonadEval m) => NixValue -> m NixValue-builtinHasContext (VStr _ ctx) = pure (VBool (ctx /= emptyContext))-builtinHasContext other =-  throwEvalError ("builtins.hasContext: expected a string, got " <> typeName other)---- | Return the context of a string as an attrset.------ Each key is a store path string.  Each value is an attrset with:---   - @path@: true if there's a SCPlain reference---   - @allOutputs@: true if there's a SCAllOutputs reference---   - @outputs@: list of output names from SCDrvOutput references-builtinGetContext :: (MonadEval m) => NixValue -> m NixValue-builtinGetContext (VStr _ (StringContext ctx)) = do-  let grouped = groupContextByPath (Set.toList ctx)-      attrMap = Map.map contextEntryToAttrs grouped-  pure (VAttrs attrMap)-builtinGetContext other =-  throwEvalError ("builtins.getContext: expected a string, got " <> typeName other)---- | Intermediate representation for grouping context elements by store path.-data ContextEntry = ContextEntry-  { cePath :: !Bool,-    ceAllOutputs :: !Bool,-    ceOutputs :: ![Text]-  }---- | Group context elements by their store path.-groupContextByPath :: [StringContextElement] -> Map Text ContextEntry-groupContextByPath = foldl' addElement Map.empty-  where-    addElement acc (SCPlain sp) =-      Map.insertWith mergeEntry (spToText sp) (ContextEntry True False []) acc-    addElement acc (SCDrvOutput sp outName) =-      Map.insertWith mergeEntry (spToText sp) (ContextEntry False False [outName]) acc-    addElement acc (SCAllOutputs sp) =-      Map.insertWith mergeEntry (spToText sp) (ContextEntry False True []) acc-    mergeEntry new old =-      ContextEntry-        (cePath new || cePath old)-        (ceAllOutputs new || ceAllOutputs old)-        (ceOutputs new ++ ceOutputs old)-    spToText sp =-      T.pack (storePathToFilePath defaultStoreDir sp)---- | Convert a ContextEntry to an attrset thunk.-contextEntryToAttrs :: ContextEntry -> Thunk-contextEntryToAttrs entry =-  let fields =-        [("path", evaluated (VBool True)) | cePath entry]-          ++ [("allOutputs", evaluated (VBool True)) | ceAllOutputs entry]-          ++ [("outputs", evaluated (VList [evaluated (mkStr o) | o <- ceOutputs entry])) | not (null (ceOutputs entry))]-   in evaluated (VAttrs (Map.fromList fields))---- | Append context entries to a string from an attrset.------ @builtins.appendContext string contextAttrset@ adds the specified--- context elements to the string.-builtinAppendContext :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinAppendContext (VStr s ctx) (VAttrs contextAttrs) = do-  newCtx <- parseContextAttrs contextAttrs-  pure (VStr s (ctx <> newCtx))-builtinAppendContext (VStr _ _) other =-  throwEvalError ("builtins.appendContext: second argument must be a set, got " <> typeName other)-builtinAppendContext other _ =-  throwEvalError ("builtins.appendContext: first argument must be a string, got " <> typeName other)---- | Parse a context attrset into a StringContext.--- Each key is a store path; each value is an attrset with optional--- @path@, @allOutputs@, and @outputs@ fields.-parseContextAttrs :: (MonadEval m) => Map Text Thunk -> m StringContext-parseContextAttrs attrs = do-  elements <- mapM parseOneCtx (Map.toList attrs)-  pure (StringContext (Set.fromList (concat elements)))-  where-    parseOneCtx (pathText, thunk) = do-      val <- force thunk-      case val of-        VAttrs inner -> do-          let sp = case parseStorePath defaultStoreDir pathText of-                Just parsed -> parsed-                Nothing -> StorePath (T.take 32 (T.drop (T.length storeDirPrefix) pathText)) (T.drop 33 (T.drop (T.length storeDirPrefix) pathText))-          hasPath <- getBoolAttr "path" inner-          hasAllOuts <- getBoolAttr "allOutputs" inner-          outNames <- getOutputsList inner-          let pathElems = [SCPlain sp | hasPath]-              allOutElems = [SCAllOutputs sp | hasAllOuts]-              outElems = [SCDrvOutput sp o | o <- outNames]-          pure (pathElems ++ allOutElems ++ outElems)-        _ -> throwEvalError "builtins.appendContext: context entry must be a set"--    getBoolAttr key attrs' = case Map.lookup key attrs' of-      Nothing -> pure False-      Just thunk -> do-        val <- force thunk-        case val of-          VBool b -> pure b-          _ -> pure False--    getOutputsList attrs' = case Map.lookup "outputs" attrs' of-      Nothing -> pure []-      Just thunk -> do-        val <- force thunk-        case val of-          VList thunks -> mapM forceToOutputName thunks-          _ -> pure []--    forceToOutputName thunk = do-      val <- force thunk-      case val of-        VStr s _ -> pure s-        _ -> throwEvalError "builtins.appendContext: output name must be a string"--builtinBaseNameOf :: (MonadEval m) => NixValue -> m NixValue-builtinBaseNameOf (VStr s ctx) = pure (VStr (lastComponent s) ctx)-builtinBaseNameOf (VPath p) = pure (mkStr (lastComponent p))-builtinBaseNameOf other =-  throwEvalError ("builtins.baseNameOf: expected a string or path, got " <> typeName other)--lastComponent :: Text -> Text-lastComponent t = case T.splitOn "/" t of-  [] -> t-  parts -> case reverse (filter (not . T.null) parts) of-    [] -> ""-    (final : _) -> final--builtinDirOf :: (MonadEval m) => NixValue -> m NixValue-builtinDirOf (VStr s ctx) = pure (VStr (dirComponent s) ctx)-builtinDirOf (VPath p) = pure (VPath (dirComponent p))-builtinDirOf other =-  throwEvalError ("builtins.dirOf: expected a string or path, got " <> typeName other)--dirComponent :: Text -> Text-dirComponent t =-  let idx = T.findIndex (== '/') (T.reverse t)-   in case idx of-        Nothing -> "."-        Just n -> T.take (T.length t - n - 1) t--builtinConcatLists :: (MonadEval m) => NixValue -> m NixValue-builtinConcatLists (VList thunks) = do-  sublists <- mapM forceThenExtractList thunks-  pure (VList (concat sublists))-  where-    forceThenExtractList thunk = do-      val <- force thunk-      case val of-        VList xs -> pure xs-        _ -> throwEvalError "builtins.concatLists: element must be a list"-builtinConcatLists other =-  throwEvalError ("builtins.concatLists: expected a list, got " <> typeName other)--builtinLessThan :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinLessThan a b = VBool <$> nixCompare a b---- ------------------------------------------------------------------------------ Builtin implementations — arithmetic + bitwise--- -----------------------------------------------------------------------------builtinAdd :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinAdd (VInt a) (VInt b) = pure (VInt (a + b))-builtinAdd (VInt a) (VFloat b) = pure (VFloat (fromInteger a + b))-builtinAdd (VFloat a) (VInt b) = pure (VFloat (a + fromInteger b))-builtinAdd (VFloat a) (VFloat b) = pure (VFloat (a + b))-builtinAdd l r = throwEvalError ("builtins.add: expected numbers, got " <> typeName l <> " and " <> typeName r)--builtinSub :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinSub (VInt a) (VInt b) = pure (VInt (a - b))-builtinSub (VInt a) (VFloat b) = pure (VFloat (fromInteger a - b))-builtinSub (VFloat a) (VInt b) = pure (VFloat (a - fromInteger b))-builtinSub (VFloat a) (VFloat b) = pure (VFloat (a - b))-builtinSub l r = throwEvalError ("builtins.sub: expected numbers, got " <> typeName l <> " and " <> typeName r)--builtinMul :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinMul (VInt a) (VInt b) = pure (VInt (a * b))-builtinMul (VInt a) (VFloat b) = pure (VFloat (fromInteger a * b))-builtinMul (VFloat a) (VInt b) = pure (VFloat (a * fromInteger b))-builtinMul (VFloat a) (VFloat b) = pure (VFloat (a * b))-builtinMul l r = throwEvalError ("builtins.mul: expected numbers, got " <> typeName l <> " and " <> typeName r)--builtinDiv :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinDiv _ (VInt 0) = throwEvalError "builtins.div: division by zero"-builtinDiv (VInt a) (VInt b) = pure (VInt (quot a b))-builtinDiv _ (VFloat 0) = throwEvalError "builtins.div: division by zero"-builtinDiv (VInt a) (VFloat b) = pure (VFloat (fromInteger a / b))-builtinDiv (VFloat a) (VInt b) = pure (VFloat (a / fromInteger b))-builtinDiv (VFloat a) (VFloat b) = pure (VFloat (a / b))-builtinDiv l r = throwEvalError ("builtins.div: expected numbers, got " <> typeName l <> " and " <> typeName r)--builtinBitAnd :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinBitAnd (VInt a) (VInt b) = pure (VInt (a .&. b))-builtinBitAnd _ _ = throwEvalError "builtins.bitAnd: expected two integers"--builtinBitOr :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinBitOr (VInt a) (VInt b) = pure (VInt (a .|. b))-builtinBitOr _ _ = throwEvalError "builtins.bitOr: expected two integers"--builtinBitXor :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinBitXor (VInt a) (VInt b) = pure (VInt (xor a b))-builtinBitXor _ _ = throwEvalError "builtins.bitXor: expected two integers"---- ------------------------------------------------------------------------------ Builtin implementations — attr set higher-order--- -----------------------------------------------------------------------------builtinMapAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinMapAttrs func (VAttrs attrs) =-  -- Lazy: each attr value is a deferred @f key val@, forced only on demand.-  pure (VAttrs (Map.mapWithKey deferAttr attrs))-  where-    deferAttr key valThunk =-      let env =-            Env-              { envBindings =-                  Map.fromList-                    [ ("__fn", evaluated func),-                      ("__key", evaluated (mkStr key)),-                      ("__val", valThunk)-                    ],-                envWithScopes = []-              }-       in mkSyntheticThunk env (EApp (EApp (EVar "__fn") (EVar "__key")) (EVar "__val"))-builtinMapAttrs _ other =-  throwEvalError ("builtins.mapAttrs: expected a set, got " <> typeName other)--builtinFunctionArgs :: (MonadEval m) => NixValue -> m NixValue-builtinFunctionArgs (VLambda _ formals _) = pure (formalsToAttrs formals)-builtinFunctionArgs (VBuiltin _ _) = pure (VAttrs Map.empty)--- Callable sets with __functionArgs metadata (from setFunctionArgs).-builtinFunctionArgs (VAttrs attrs)-  | Just faThunk <- Map.lookup "__functionArgs" attrs = force faThunk-builtinFunctionArgs other =-  throwEvalError ("builtins.functionArgs: expected a function, got " <> typeName other)--formalsToAttrs :: Formals -> NixValue-formalsToAttrs (FormalName _) = VAttrs Map.empty-formalsToAttrs (FormalSet formals _) = formalsListToAttrs formals-formalsToAttrs (FormalNamedSet _ formals _) = formalsListToAttrs formals--formalsListToAttrs :: [Formal] -> NixValue-formalsListToAttrs formals =-  VAttrs $-    Map.fromList-      [(fName f, evaluated (VBool (isJust (fDefault f)))) | f <- formals]---- | @builtins.setFunctionArgs f args@ — wraps @f@ in a callable attrset--- with @__functor@ (so it remains callable) and @__functionArgs@ metadata.--- Used by nixpkgs @lib.mirrorFunctionArgs@ / @lib.makeOverridable@.------ @__functor@ is @self: f@ — a lambda that ignores @self@ and returns--- the original function.  The function is captured in the closure env.-builtinSetFunctionArgs :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinSetFunctionArgs func (VAttrs argSpec) =-  let closureEnv = envInsert "__fn" func emptyEnv-      -- __functor = self: __fn  (ignores self, returns the original function)-      functorLambda = VLambda closureEnv (FormalName "_self") (EVar "__fn")-   in pure $-        VAttrs $-          Map.fromList-            [ ("__functor", evaluated functorLambda),-              ("__functionArgs", evaluated (VAttrs argSpec))-            ]-builtinSetFunctionArgs func args =-  throwEvalError ("builtins.setFunctionArgs: expected a set as second argument, got " <> typeName args <> " (func: " <> typeName func <> ")")--builtinZipAttrsWith :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinZipAttrsWith func (VList thunks) = do-  attrSets <- mapM forceToAttrSet thunks-  let merged = mergeAllAttrs attrSets-  resultPairs <- mapM (applyZip func) (Map.toList merged)-  pure (VAttrs (Map.fromList resultPairs))-  where-    forceToAttrSet thunk = do-      val <- force thunk-      case val of-        VAttrs attrs -> pure attrs-        _ -> throwEvalError "builtins.zipAttrsWith: list element must be a set"-    mergeAllAttrs = foldl' (\acc attrs -> Map.unionWith (++) acc (Map.map (: []) attrs)) Map.empty-    applyZip fn (key, thunkList) = do-      partial <- applyValue fn (mkStr key)-      result <- applyValue partial (VList thunkList)-      pure (key, evaluated result)-builtinZipAttrsWith _ other =-  throwEvalError ("builtins.zipAttrsWith: expected a list, got " <> typeName other)---- ------------------------------------------------------------------------------ Builtin implementations — string manipulation--- -----------------------------------------------------------------------------builtinReplaceStrings ::-  (MonadEval m) => NixValue -> NixValue -> NixValue -> m NixValue-builtinReplaceStrings (VList fromThunks) (VList toThunks) (VStr input inputCtx) = do-  froms <- mapM forceStr fromThunks-  toStrs <- mapM forceStr toThunks-  when (length froms /= length toStrs) $-    throwEvalError "builtins.replaceStrings: 'from' and 'to' must have the same length"-  let fromTexts = map fst froms-      toTexts = map fst toStrs-      -- Merge contexts: input context + all 'to' string contexts + all 'from' string contexts-      mergedCtx = inputCtx <> mconcat (map snd froms) <> mconcat (map snd toStrs)-      pairs = zip fromTexts toTexts-  pure (VStr (replaceAll pairs input) mergedCtx)-  where-    forceStr thunk = do-      val <- force thunk-      case val of-        VStr s ctx -> pure (s, ctx)-        _ -> throwEvalError "builtins.replaceStrings: elements must be strings"-builtinReplaceStrings _ _ (VStr _ _) =-  throwEvalError "builtins.replaceStrings: first two arguments must be lists"-builtinReplaceStrings _ _ other =-  throwEvalError ("builtins.replaceStrings: expected a string, got " <> typeName other)--replaceAll :: [(Text, Text)] -> Text -> Text-replaceAll pairs = go-  where-    go remaining-      | T.null remaining =-          -- At end of string, still check for empty-from match-          case findMatch pairs remaining of-            Just (replacement, _, _) -> replacement-            Nothing -> ""-      | otherwise = case findMatch pairs remaining of-          Just (replacement, rest, matched) ->-            if T.null matched-              then case T.uncons remaining of-                -- empty-from: insert replacement then advance 1 char-                Just (ch, after) -> replacement <> T.singleton ch <> go after-                Nothing -> replacement -- unreachable: guarded by T.null above-              else replacement <> go rest-          Nothing -> case T.uncons remaining of-            Just (ch, after) -> T.singleton ch <> go after-            Nothing -> "" -- unreachable: guarded by T.null above-    findMatch [] _ = Nothing-    findMatch ((from, to) : rest) txt-      | T.null from = Just (to, txt, from)-      | Just suffix <- T.stripPrefix from txt = Just (to, suffix, from)-      | otherwise = findMatch rest txt---- ------------------------------------------------------------------------------ Builtin implementations — regex (POSIX ERE via regex-tdfa)--- ------------------------------------------------------------------------------- | @builtins.match regex str@: match a POSIX ERE against a string.--- The regex is implicitly anchored (must match the entire string).--- Returns @null@ if no match, or a list of capture group strings--- (empty string for unmatched optional groups).-builtinMatch :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinMatch (VStr regex _) (VStr str _) = do-  let anchored = "^" <> T.unpack regex <> "$"-  case RE.makeRegexM anchored :: Maybe RE.Regex of-    Nothing ->-      throwEvalError ("builtins.match: invalid regex: " <> regex)-    Just compiled ->-      let matches = matchAllText compiled (T.unpack str)-       in case matches of-            [] -> pure VNull-            (match : _) ->-              -- match is an Array of (String, (offset, len)) pairs.-              -- Index 0 is the full match; indices 1.. are capture groups.-              let groups = Array.elems match-                  -- Skip index 0 (full match) — return only capture groups.-                  captureGroups = drop 1 groups-                  toThunk (s, _) = evaluated (mkStr (T.pack s))-               in pure (VList (map toThunk captureGroups))-builtinMatch (VStr _ _) other =-  throwEvalError ("builtins.match: expected a string, got " <> typeName other)-builtinMatch other _ =-  throwEvalError ("builtins.match: expected a string (regex), got " <> typeName other)---- | @builtins.split regex str@: split a string by a POSIX ERE.--- Returns an alternating list of non-matched strings and match-group lists.--- Example: @split "(x)" "axbxc"@ → @["a" ["x"] "b" ["x"] "c"]@-builtinSplit :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinSplit (VStr regex _) (VStr str _) = do-  case RE.makeRegexM (T.unpack regex) :: Maybe RE.Regex of-    Nothing ->-      throwEvalError ("builtins.split: invalid regex: " <> regex)-    Just compiled ->-      let allMatches = matchAllText compiled (T.unpack str)-          strText = T.unpack str-          result = buildSplitResult strText 0 allMatches-       in pure (VList result)-builtinSplit (VStr _ _) other =-  throwEvalError ("builtins.split: expected a string, got " <> typeName other)-builtinSplit other _ =-  throwEvalError ("builtins.split: expected a string (regex), got " <> typeName other)---- | Build the alternating list for builtins.split.-buildSplitResult :: String -> Int -> [Array.Array Int (String, (Int, Int))] -> [Thunk]-buildSplitResult remaining pos [] =-  -- No more matches — emit the rest of the string.-  [evaluated (mkStr (T.pack (drop pos remaining)))]-buildSplitResult remaining pos (match : rest) =-  let fullMatch = match Array.! 0-      (_, (matchStart, matchLen)) = fullMatch-      -- Text before this match-      before = T.pack (take (matchStart - pos) (drop pos remaining))-      -- Capture groups (indices 1..)-      groups = drop 1 (Array.elems match)-      groupThunks = map (\(s, _) -> evaluated (mkStr (T.pack s))) groups-      -- Continue after this match-      afterPos = matchStart + matchLen-   in evaluated (mkStr before)-        : evaluated (VList groupThunks)-        : buildSplitResult remaining afterPos rest--builtinCompareVersions :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinCompareVersions (VStr a _) (VStr b _) =-  pure (VInt (compareVersionParts (splitVersionStr a) (splitVersionStr b)))-builtinCompareVersions _ _ = throwEvalError "builtins.compareVersions: expected two strings"--compareVersionParts :: [Text] -> [Text] -> Integer-compareVersionParts [] [] = 0-compareVersionParts [] (_ : _) = -1-compareVersionParts (_ : _) [] = 1-compareVersionParts (a : as) (b : bs) =-  case compareComponent a b of-    0 -> compareVersionParts as bs-    n -> n--compareComponent :: Text -> Text -> Integer-compareComponent a b-  | a == b = 0-  | allDigits a && allDigits b = compare' (readInt a) (readInt b)-  | a == "" = -1-  | b == "" = 1-  | otherwise = if a < b then -1 else 1-  where-    allDigits t = not (T.null t) && T.all isDigit t-    readInt :: Text -> Integer-    readInt = T.foldl' (\acc c -> acc * 10 + fromIntegral (digitToInt c)) (0 :: Integer)-    compare' x y-      | x < y = -1-      | x > y = 1-      | otherwise = 0--splitVersionStr :: Text -> [Text]-splitVersionStr t-  | T.null t = []-  | otherwise =-      let (component, rest) = spanComponent t-       in component : splitVersionAfterComponent rest--splitVersionAfterComponent :: Text -> [Text]-splitVersionAfterComponent t = case T.uncons t of-  Nothing -> []-  Just ('.', rest) -> splitVersionStr rest-  Just _ -> splitVersionStr t--spanComponent :: Text -> (Text, Text)-spanComponent t = case T.uncons t of-  Nothing -> ("", "")-  Just (c, rest)-    | isDigit c -> T.span isDigit t-    | isAlpha c -> T.span isAlpha t-    | otherwise -> (T.singleton c, rest)--builtinSplitVersion :: (MonadEval m) => NixValue -> m NixValue-builtinSplitVersion (VStr s _) =-  pure (VList (map (evaluated . mkStr) (splitVersionComponents s)))-builtinSplitVersion other =-  throwEvalError ("builtins.splitVersion: expected a string, got " <> typeName other)--splitVersionComponents :: Text -> [Text]-splitVersionComponents t = case T.uncons t of-  Nothing -> []-  Just ('.', rest) -> splitVersionComponents rest-  Just (c, _)-    | isDigit c ->-        let (digits, rest) = T.span isDigit t-         in digits : splitVersionComponents rest-    | otherwise ->-        let (alpha, rest) = T.span isAlpha t-         in alpha : splitVersionComponents rest--builtinParseDrvName :: (MonadEval m) => NixValue -> m NixValue-builtinParseDrvName (VStr s _) =-  let (name, version) = parseName s-   in pure-        ( VAttrs-            ( Map.fromList-                [ ("name", evaluated (mkStr name)),-                  ("version", evaluated (mkStr version))-                ]-            )-        )-builtinParseDrvName other =-  throwEvalError ("builtins.parseDrvName: expected a string, got " <> typeName other)--parseName :: Text -> (Text, Text)-parseName t =-  case findVersionDash t 0 of-    Nothing -> (t, "")-    Just idx -> (T.take idx t, T.drop (idx + 1) t)--findVersionDash :: Text -> Int -> Maybe Int-findVersionDash t idx-  | idx >= T.length t = Nothing-  | T.index t idx == '-'-      && idx + 1 < T.length t-      && isDigit (T.index t (idx + 1)) =-      Just idx-  | otherwise = findVersionDash t (idx + 1)---- ------------------------------------------------------------------------------ Builtin implementations — serialization + hashing--- -----------------------------------------------------------------------------builtinToJSON :: (MonadEval m) => NixValue -> m NixValue-builtinToJSON val = mkStr <$> valueToJSON val--valueToJSON :: (MonadEval m) => NixValue -> m Text-valueToJSON VNull = pure "null"-valueToJSON (VBool True) = pure "true"-valueToJSON (VBool False) = pure "false"-valueToJSON (VInt n) = pure (T.pack (show n))-valueToJSON (VFloat f) =-  let s = show f-   in -- Nix outputs 1e+40 style, Haskell outputs 1.0e40 style-      -- For simple cases just use show-      pure (T.pack s)-valueToJSON (VStr s _) = pure (jsonEscapeString s)-valueToJSON (VList thunks) = do-  vals <- mapM force thunks-  jsonVals <- mapM valueToJSON vals-  pure ("[" <> T.intercalate "," jsonVals <> "]")-valueToJSON (VAttrs attrs) = do-  let sortedKeys = Map.keys attrs-  pairs <- mapM (jsonPair attrs) sortedKeys-  pure ("{" <> T.intercalate "," pairs <> "}")-  where-    jsonPair attrMap key = case Map.lookup key attrMap of-      Nothing -> pure ""-      Just thunk -> do-        val <- force thunk-        jsonVal <- valueToJSON val-        pure (jsonEscapeString key <> ":" <> jsonVal)-valueToJSON (VPath p) = pure (jsonEscapeString p)-valueToJSON (VLambda {}) = throwEvalError "builtins.toJSON: cannot convert a function to JSON"-valueToJSON (VBuiltin _ _) = throwEvalError "builtins.toJSON: cannot convert a function to JSON"-valueToJSON (VDerivation _) = throwEvalError "builtins.toJSON: cannot convert a derivation to JSON"--jsonEscapeString :: Text -> Text-jsonEscapeString s = "\"" <> T.concatMap escapeChar s <> "\""-  where-    escapeChar '"' = "\\\""-    escapeChar '\\' = "\\\\"-    escapeChar '\n' = "\\n"-    escapeChar '\r' = "\\r"-    escapeChar '\t' = "\\t"-    escapeChar c-      | ord c < 0x20 = "\\u" <> T.pack (padHex 4 (showHex' (ord c)))-      | otherwise = T.singleton c-    padHex n str = replicate (n - length str) '0' ++ str-    showHex' 0 = "0"-    showHex' num = go num ""-      where-        go 0 acc = acc-        go v acc =-          let (q, r) = quotRem v 16-           in go q (hexDigit r : acc)---- | Safe hex digit lookup (total for 0–15).-hexDigit :: Int -> Char-hexDigit n-  | n >= 0 && n <= 9 = chr (ord '0' + n)-  | n >= 10 && n <= 15 = chr (ord 'a' + n - 10)-  | otherwise = '?' -- unreachable for valid hex--builtinFromJSON :: (MonadEval m) => NixValue -> m NixValue-builtinFromJSON (VStr s _) = case parseJSON (T.strip s) of-  Just (val, rest)-    | T.null (T.strip rest) -> pure val-    | otherwise -> throwEvalError "builtins.fromJSON: trailing content after JSON value"-  Nothing -> throwEvalError "builtins.fromJSON: invalid JSON"-builtinFromJSON other =-  throwEvalError ("builtins.fromJSON: expected a string, got " <> typeName other)--parseJSON :: Text -> Maybe (NixValue, Text)-parseJSON t = case T.uncons (T.stripStart t) of-  Nothing -> Nothing-  Just ('n', rest)-    | Just suffix <- T.stripPrefix "ull" rest -> Just (VNull, suffix)-  Just ('t', rest)-    | Just suffix <- T.stripPrefix "rue" rest -> Just (VBool True, suffix)-  Just ('f', rest)-    | Just suffix <- T.stripPrefix "alse" rest -> Just (VBool False, suffix)-  Just ('"', _) -> parseJSONString (T.stripStart t)-  Just ('[', rest) -> parseJSONArray rest-  Just ('{', rest) -> parseJSONObject rest-  Just (c, _)-    | c == '-' || isDigit c -> parseJSONNumber (T.stripStart t)-  _ -> Nothing--parseJSONString :: Text -> Maybe (NixValue, Text)-parseJSONString t = case T.uncons t of-  Just ('"', rest) ->-    let (strVal, remaining) = parseJSONStringContent rest ""-     in Just (mkStr strVal, remaining)-  _ -> Nothing--parseJSONStringContent :: Text -> Text -> (Text, Text)-parseJSONStringContent t acc = case T.uncons t of-  Nothing -> (acc, "")-  Just ('"', rest) -> (acc, rest)-  Just ('\\', rest) -> case T.uncons rest of-    Just ('"', r) -> parseJSONStringContent r (acc <> "\"")-    Just ('\\', r) -> parseJSONStringContent r (acc <> "\\")-    Just ('/', r) -> parseJSONStringContent r (acc <> "/")-    Just ('n', r) -> parseJSONStringContent r (acc <> "\n")-    Just ('r', r) -> parseJSONStringContent r (acc <> "\r")-    Just ('t', r) -> parseJSONStringContent r (acc <> "\t")-    Just ('u', r) -> case parseHex4 r of-      Just (codepoint, r2) ->-        parseJSONStringContent r2 (acc <> T.singleton (chr codepoint))-      Nothing -> parseJSONStringContent r (acc <> "u")-    _ -> (acc, rest)-  Just (c, rest) -> parseJSONStringContent rest (acc <> T.singleton c)--parseHex4 :: Text -> Maybe (Int, Text)-parseHex4 t-  | T.length t >= 4 =-      let hex = T.take 4 t-       in if T.all isHexDigit hex-            then Just (readHex4 hex, T.drop 4 t)-            else Nothing-  | otherwise = Nothing--readHex4 :: Text -> Int-readHex4 = T.foldl' (\acc c -> acc * 16 + digitToInt c) 0--parseJSONNumber :: Text -> Maybe (NixValue, Text)-parseJSONNumber t =-  let (numStr, rest) = T.span (\c -> isDigit c || c == '.' || c == '-' || c == 'e' || c == 'E' || c == '+') t-   in if T.null numStr-        then Nothing-        else-          if T.any (\c -> c == '.' || c == 'e' || c == 'E') numStr-            then case reads (T.unpack numStr) :: [(Double, String)] of-              [(d, "")] -> Just (VFloat d, rest)-              _ -> Nothing-            else case reads (T.unpack numStr) :: [(Integer, String)] of-              [(n, "")] -> Just (VInt n, rest)-              _ -> Nothing--parseJSONArray :: Text -> Maybe (NixValue, Text)-parseJSONArray t = parseJSONArrayElements (T.stripStart t) []--parseJSONArrayElements :: Text -> [Thunk] -> Maybe (NixValue, Text)-parseJSONArrayElements t acc = case T.uncons (T.stripStart t) of-  Just (']', rest) -> Just (VList (reverse acc), rest)-  _ -> case parseJSON t of-    Just (val, rest) ->-      let stripped = T.stripStart rest-       in case T.uncons stripped of-            Just (',', rest2) -> parseJSONArrayElements rest2 (evaluated val : acc)-            Just (']', rest2) -> Just (VList (reverse (evaluated val : acc)), rest2)-            _ -> Nothing-    Nothing -> Nothing--parseJSONObject :: Text -> Maybe (NixValue, Text)-parseJSONObject t = parseJSONObjectEntries (T.stripStart t) Map.empty--parseJSONObjectEntries :: Text -> Map Text Thunk -> Maybe (NixValue, Text)-parseJSONObjectEntries t acc = case T.uncons (T.stripStart t) of-  Just ('}', rest) -> Just (VAttrs acc, rest)-  _ -> case parseJSONString (T.stripStart t) of-    Just (VStr key _, rest) -> case T.uncons (T.stripStart rest) of-      Just (':', rest2) -> case parseJSON rest2 of-        Just (val, rest3) ->-          let stripped = T.stripStart rest3-              updated = Map.insert key (evaluated val) acc-           in case T.uncons stripped of-                Just (',', rest4) -> parseJSONObjectEntries rest4 updated-                Just ('}', rest4) -> Just (VAttrs updated, rest4)-                _ -> Nothing-        Nothing -> Nothing-      _ -> Nothing-    _ -> Nothing--builtinHashString :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinHashString (VStr algo _) (VStr input _) =-  let inputBytes = TE.encodeUtf8 input-   in case algo of-        "sha256" ->-          let digest = CH.hash inputBytes :: CH.Digest CH.SHA256-           in pure (mkStr (digestToHex digest))-        "sha512" ->-          let digest = CH.hash inputBytes :: CH.Digest CH.SHA512-           in pure (mkStr (digestToHex digest))-        "sha1" ->-          let digest = CH.hash inputBytes :: CH.Digest CH.SHA1-           in pure (mkStr (digestToHex digest))-        "md5" ->-          let digest = CH.hash inputBytes :: CH.Digest CH.MD5-           in pure (mkStr (digestToHex digest))-        _ -> throwEvalError ("builtins.hashString: unknown hash algorithm '" <> algo <> "'")-builtinHashString (VStr _ _) other =-  throwEvalError ("builtins.hashString: expected a string, got " <> typeName other)-builtinHashString other _ =-  throwEvalError ("builtins.hashString: expected a string, got " <> typeName other)--digestToHex :: (BA.ByteArrayAccess a) => a -> Text-digestToHex digest =-  let bytes = BA.unpack digest-   in T.pack (concatMap byteToHex bytes)---- ------------------------------------------------------------------------------ Builtin implementations — deep evaluation--- -----------------------------------------------------------------------------builtinDeepSeq :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinDeepSeq first second = do-  deepForce first-  pure second--deepForce :: (MonadEval m) => NixValue -> m ()-deepForce (VList thunks) = mapM_ (force >=> deepForce) thunks-deepForce (VAttrs attrs) = mapM_ (force >=> deepForce) (Map.elems attrs)-deepForce _ = pure ()---- ------------------------------------------------------------------------------ Builtin implementations — graph traversal--- -----------------------------------------------------------------------------builtinGenericClosure :: (MonadEval m) => NixValue -> m NixValue-builtinGenericClosure (VAttrs attrs) = do-  startSetThunk <--    maybe (throwEvalError "builtins.genericClosure: missing 'startSet'") pure $-      Map.lookup "startSet" attrs-  operatorThunk <--    maybe (throwEvalError "builtins.genericClosure: missing 'operator'") pure $-      Map.lookup "operator" attrs-  startSetVal <- force startSetThunk-  operatorVal <- force operatorThunk-  case startSetVal of-    VList items -> do-      result <- closureLoop operatorVal items [] []-      pure (VList (map evaluated result))-    _ -> throwEvalError "builtins.genericClosure: 'startSet' must be a list"-builtinGenericClosure other =-  throwEvalError ("builtins.genericClosure: expected a set, got " <> typeName other)--closureLoop ::-  (MonadEval m) =>-  NixValue ->-  [Thunk] ->-  [NixValue] ->-  [NixValue] ->-  m [NixValue]-closureLoop _ [] _ acc = pure (reverse acc)-closureLoop operator (thunk : rest) seenKeys acc = do-  item <- force thunk-  key <- extractKey item-  alreadySeen <- keyInList key seenKeys-  if alreadySeen-    then closureLoop operator rest seenKeys acc-    else do-      newItems <- applyValue operator item-      case newItems of-        VList newThunks ->-          closureLoop operator (rest ++ newThunks) (key : seenKeys) (item : acc)-        _ -> throwEvalError "builtins.genericClosure: operator must return a list"--extractKey :: (MonadEval m) => NixValue -> m NixValue-extractKey (VAttrs attrs) =-  case Map.lookup "key" attrs of-    Just thunk -> force thunk-    Nothing -> throwEvalError "builtins.genericClosure: item missing 'key' attribute"-extractKey _ = throwEvalError "builtins.genericClosure: item must be a set with 'key'"--keyInList :: (MonadEval m) => NixValue -> [NixValue] -> m Bool-keyInList _ [] = pure False-keyInList key (seen : rest) = do-  eq <- nixEqual force key seen-  if eq then pure True else keyInList key rest---- ------------------------------------------------------------------------------ IO builtins (delegate to MonadEval methods)--- ------------------------------------------------------------------------------- | Coerce a value to a path 'Text'.  Accepts 'VPath' and 'VStr';--- throws a type error for anything else.-coerceToPath :: (MonadEval m) => Text -> NixValue -> m Text-coerceToPath _ (VPath p) = pure p-coerceToPath _ (VStr s _) = pure s-coerceToPath name other =-  throwEvalError ("builtins." <> name <> ": expected a path or string, got " <> typeName other)--builtinImport :: (MonadEval m) => NixValue -> m NixValue-builtinImport (VPath p) = importFile p-builtinImport other =-  throwEvalError ("import: expected a path, got " <> typeName other)--builtinReadFile :: (MonadEval m) => NixValue -> m NixValue-builtinReadFile val = do-  p <- coerceToPath "readFile" val-  mkStr <$> readFileText p--builtinPathExists :: (MonadEval m) => NixValue -> m NixValue-builtinPathExists val = do-  p <- coerceToPath "pathExists" val-  VBool <$> doesPathExist p--builtinReadDir :: (MonadEval m) => NixValue -> m NixValue-builtinReadDir val = do-  p <- coerceToPath "readDir" val-  entries <- listDirectory p-  pure (VAttrs (Map.fromList [(name, evaluated (mkStr fileType)) | (name, fileType) <- entries]))---- ------------------------------------------------------------------------------ Builtin implementations — environment + paths--- -----------------------------------------------------------------------------builtinGetEnv :: (MonadEval m) => NixValue -> m NixValue-builtinGetEnv (VStr name _) = mkStr <$> getEnvVar name-builtinGetEnv other =-  throwEvalError ("builtins.getEnv: expected a string, got " <> typeName other)--builtinToPath :: (MonadEval m) => NixValue -> m NixValue-builtinToPath (VPath p) = pure (VPath p)-builtinToPath (VStr s _) = case T.uncons s of-  Nothing -> throwEvalError "builtins.toPath: empty path"-  Just ('/', _) -> pure (VPath s)-  Just _ -> throwEvalError ("builtins.toPath: path must be absolute, got " <> s)-builtinToPath other =-  throwEvalError ("builtins.toPath: expected a string or path, got " <> typeName other)---- ------------------------------------------------------------------------------ Builtin implementations — store path operations--- -----------------------------------------------------------------------------builtinPlaceholder :: (MonadEval m) => NixValue -> m NixValue-builtinPlaceholder (VStr outputName _) =-  let preimage = "nix-output:" <> outputName-      hashText = truncatedBase32 (TE.encodeUtf8 preimage)-   in pure (VPath (storeDirPrefix <> hashText <> "-" <> outputName))-builtinPlaceholder other =-  throwEvalError ("builtins.placeholder: expected a string, got " <> typeName other)--builtinStorePath :: (MonadEval m) => NixValue -> m NixValue-builtinStorePath (VPath p) = validateStorePath p-builtinStorePath (VStr s _) = validateStorePath s-builtinStorePath other =-  throwEvalError ("builtins.storePath: expected a path or string, got " <> typeName other)--validateStorePath :: (MonadEval m) => Text -> m NixValue-validateStorePath p-  | storeDirPrefix `T.isPrefixOf` p,-    T.length p > T.length storeDirPrefix,-    let basename = T.drop (T.length storeDirPrefix) p,-    T.length basename >= 33,-    T.index basename 32 == '-' =-      pure (VPath p)-  | otherwise =-      throwEvalError ("builtins.storePath: not a valid store path: " <> p)---- ------------------------------------------------------------------------------ Builtin implementations — Nix search path--- -----------------------------------------------------------------------------builtinFindFile :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinFindFile (VList searchPath) (VStr name _) = do-  entries <- mapM forceSearchEntry searchPath-  findFirst entries name-builtinFindFile (VList _) other =-  throwEvalError ("builtins.findFile: expected a string, got " <> typeName other)-builtinFindFile other _ =-  throwEvalError ("builtins.findFile: expected a list, got " <> typeName other)---- | Extract {prefix, path} from a search path entry thunk.-forceSearchEntry :: (MonadEval m) => Thunk -> m (Text, Text)-forceSearchEntry thunk = do-  val <- force thunk-  case val of-    VAttrs attrs -> do-      prefixThunk <--        maybe (throwEvalError "builtins.findFile: entry missing 'prefix'") pure $-          Map.lookup "prefix" attrs-      pathThunk <--        maybe (throwEvalError "builtins.findFile: entry missing 'path'") pure $-          Map.lookup "path" attrs-      prefixVal <- force prefixThunk-      pathVal <- force pathThunk-      prefix <- case prefixVal of-        VStr s _ -> pure s-        _ -> throwEvalError "builtins.findFile: 'prefix' must be a string"-      path <- case pathVal of-        VStr s _ -> pure s-        VPath s -> pure s-        _ -> throwEvalError "builtins.findFile: 'path' must be a string or path"-      pure (prefix, path)-    _ -> throwEvalError "builtins.findFile: search path entry must be a set"---- | Iterate search path entries, checking for a match.-findFirst :: (MonadEval m) => [(Text, Text)] -> Text -> m NixValue-findFirst [] name =-  throwEvalError ("file '" <> name <> "' was not found in the Nix search path")-findFirst ((prefix, path) : rest) name-  | prefix == name || (not (T.null prefix) && (prefix <> "/") `T.isPrefixOf` name) =-      let suffix = if prefix == name then "" else T.drop (T.length prefix + 1) name-          candidate = if T.null suffix then path else path <> "/" <> suffix-       in do-            exists <- doesPathExist candidate-            if exists-              then pure (VPath candidate)-              else findFirst rest name-  | T.null prefix =-      let candidate = path <> "/" <> name-       in do-            exists <- doesPathExist candidate-            if exists-              then pure (VPath candidate)-              else findFirst rest name-  | otherwise = findFirst rest name---- ------------------------------------------------------------------------------ Builtin implementations — store file creation--- -----------------------------------------------------------------------------builtinToFile :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinToFile (VStr name _) (VStr contents _) = do-  storePath <- writeToStore name contents-  pure (VPath storePath)-builtinToFile (VStr _ _) other =-  throwEvalError ("builtins.toFile: expected a string, got " <> typeName other)-builtinToFile other _ =-  throwEvalError ("builtins.toFile: expected a string, got " <> typeName other)---- ------------------------------------------------------------------------------ Builtin implementations — scoped import--- -----------------------------------------------------------------------------builtinScopedImport :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinScopedImport (VAttrs attrs) pathVal = do-  p <- coerceToPath "scopedImport" pathVal-  let scope = Map.toList attrs-  scopedImportFile scope p-builtinScopedImport other _ =-  throwEvalError ("builtins.scopedImport: expected a set, got " <> typeName other)---- ------------------------------------------------------------------------------ Builtin implementations — network fetchers--- -----------------------------------------------------------------------------builtinFetchurl :: (MonadEval m) => NixValue -> m NixValue-builtinFetchurl (VStr url _) = fetchUrlSimple url Nothing-builtinFetchurl (VAttrs attrs) = do-  url <- forceAttrStr "builtins.fetchurl" "url" attrs-  sha256 <- forceOptionalAttrStr attrs "sha256"-  fetchUrlSimple url sha256-builtinFetchurl other =-  throwEvalError ("builtins.fetchurl: expected a string or set, got " <> typeName other)--builtinFetchTarball :: (MonadEval m) => NixValue -> m NixValue-builtinFetchTarball (VStr url _) = fetchUrlSimple url Nothing-builtinFetchTarball (VAttrs attrs) = do-  url <- forceAttrStr "builtins.fetchTarball" "url" attrs-  sha256 <- forceOptionalAttrStr attrs "sha256"-  fetchUrlSimple url sha256-builtinFetchTarball other =-  throwEvalError ("builtins.fetchTarball: expected a string or set, got " <> typeName other)--builtinFetchGit :: (MonadEval m) => NixValue -> m NixValue-builtinFetchGit (VStr url _) = do-  -- Use a content-based temp dir to avoid predictable paths-  let urlHash = sha256Hex (TE.encodeUtf8 url)-  sysTmpRaw <- getEnvVar "TMPDIR"-  -- TMPDIR on Unix, TEMP/TMP on Windows; fall back to /tmp-  sysTmp <--    if T.null sysTmpRaw-      then do-        winTmp <- getEnvVar "TEMP"-        pure (if T.null winTmp then "/tmp" else winTmp)-      else pure sysTmpRaw-  let tmpDir = sysTmp <> "/nova-nix-fetchgit-" <> urlHash-  (code, _stdout, stderr) <- runProcess "git" ["clone", "--depth", "1", "--", url, tmpDir] ""-  if code /= 0-    then throwEvalError ("builtins.fetchGit: git clone failed: " <> stderr)-    else pure (VPath tmpDir)-builtinFetchGit (VAttrs attrs) = do-  url <- forceAttrStr "builtins.fetchGit" "url" attrs-  builtinFetchGit (mkStr url)-builtinFetchGit other =-  throwEvalError ("builtins.fetchGit: expected a string or set, got " <> typeName other)---- | Fetch a URL and optionally verify its hash.-fetchUrlSimple :: (MonadEval m) => Text -> Maybe Text -> m NixValue-fetchUrlSimple url _sha256 = do-  (code, stdout, stderr) <- runProcess "curl" ["-sSfL", "--", url] ""-  if code /= 0-    then throwEvalError ("fetch failed: " <> stderr)-    else do-      storePath <- writeToStore "fetchurl-result" stdout-      pure (VPath storePath)---- | Force a required string attribute from an attrset.-forceAttrStr :: (MonadEval m) => Text -> Text -> Map Text Thunk -> m Text-forceAttrStr builtin key attrs =-  case Map.lookup key attrs of-    Nothing -> throwEvalError (builtin <> ": missing required attribute '" <> key <> "'")-    Just thunk -> do-      val <- force thunk-      case val of-        VStr s _ -> pure s-        VPath p -> pure p-        _ -> throwEvalError (builtin <> ": '" <> key <> "' must be a string")---- | Force an optional string attribute.-forceOptionalAttrStr :: (MonadEval m) => Map Text Thunk -> Text -> m (Maybe Text)-forceOptionalAttrStr attrs key =-  case Map.lookup key attrs of-    Nothing -> pure Nothing-    Just thunk -> do-      val <- force thunk-      case val of-        VStr s _ -> pure (Just s)-        _ -> pure Nothing---- ------------------------------------------------------------------------------ Builtin implementations — derivation construction--- -----------------------------------------------------------------------------builtinDerivation :: (MonadEval m) => NixValue -> m NixValue-builtinDerivation (VAttrs attrs) = do-  -- Extract required attributes-  drvName <- forceAttrStr "derivation" "name" attrs-  system <- forceAttrStr "derivation" "system" attrs-  builder <- forceAttrStr "derivation" "builder" attrs--  -- Extract optional outputs (default ["out"])-  outputNames <- case Map.lookup "outputs" attrs of-    Nothing -> pure ["out"]-    Just thunk -> do-      val <- force thunk-      case val of-        VList thunks -> mapM forceToText thunks-        _ -> throwEvalError "derivation: 'outputs' must be a list of strings"--  -- Extract optional args (default [])-  builderArgs <- case Map.lookup "args" attrs of-    Nothing -> pure []-    Just thunk -> do-      val <- force thunk-      case val of-        VList thunks -> mapM forceToText thunks-        _ -> throwEvalError "derivation: 'args' must be a list of strings"--  -- Collect all string-coercible attrs into env WITH their contexts-  (drvEnvPairs, envContext) <- collectDrvEnvWithContext attrs--  -- Extract input derivations and input sources from the merged context-  let inputDrvs = extractInputDrvs envContext-      inputSrcs = extractInputSrcs envContext--  -- Build the platform-  let platform = textToPlatform system--  -- Build the derivation with populated inputs for hashing-  let envMap = Map.fromList drvEnvPairs-      drv =-        Derivation-          { drvOutputs = [],-            drvInputDrvs = inputDrvs,-            drvInputSrcs = inputSrcs,-            drvPlatform = platform,-            drvBuilder = builder,-            drvArgs = builderArgs,-            drvEnv = envMap-          }--  -- Serialize to ATerm and hash for drvPath-  let aterm = toATerm drv-      storeRef = ":" <> defaultStoreDirText <> ":"-      drvPathHash = truncatedBase32 (TE.encodeUtf8 ("text:sha256:" <> sha256Hex (TE.encodeUtf8 aterm) <> storeRef <> drvName <> ".drv"))-      drvPathText = storeDirPrefix <> drvPathHash <> "-" <> drvName <> ".drv"--  -- Parse drvPath as a StorePath for context-  let drvSP = case parseStorePath defaultStoreDir drvPathText of-        Just sp -> sp-        Nothing -> StorePath (T.take 32 (T.drop (T.length storeDirPrefix) drvPathText)) (drvName <> ".drv")--  -- Compute output paths-  let computeOutPath outName =-        let nameSuffix = if outName == "out" then "" else "-" <> outName-            preimage = "output:" <> outName <> ":sha256:" <> sha256Hex (TE.encodeUtf8 aterm) <> storeRef <> drvName <> nameSuffix-            outHash = truncatedBase32 (TE.encodeUtf8 preimage)-         in storeDirPrefix <> outHash <> "-" <> drvName <> nameSuffix--  let outPaths = [(outName, computeOutPath outName) | outName <- outputNames]-      mainOutPath = case outPaths of-        ((_, p) : _) -> p-        [] -> ""--  -- Build DerivationOutput records for the Derivation value-  let drvOutputsList =-        [ DerivationOutput-            { doName = outName,-              doPath = case parseStorePath defaultStoreDir outP of-                Just sp -> sp-                -- Fallback: construct manually from the path string-                Nothing -> StorePath (T.take 32 (T.drop (T.length storeDirPrefix) outP)) outName,-              doHashAlgo = "",-              doHash = ""-            }-        | (outName, outP) <- outPaths-        ]--  -- Build the complete Derivation with populated outputs and inputs-  let completeDrv = drv {drvOutputs = drvOutputsList}--  -- Context for output paths: each output carries SCDrvOutput context-  -- Context for drvPath: carries SCAllOutputs context-  let drvPathCtx = StringContext (Set.singleton (SCAllOutputs drvSP))-      outPathCtx outName = StringContext (Set.singleton (SCDrvOutput drvSP outName))--  -- Build result attrset: original attrs + drvPath, outPath, type, per-output attrs-  let baseAttrs =-        Map.fromList $-          [ ("type", evaluated (mkStr "derivation")),-            ("drvPath", evaluated (VStr drvPathText drvPathCtx)),-            ("outPath", evaluated (VStr mainOutPath (outPathCtx "out"))),-            ("name", evaluated (mkStr drvName)),-            ("system", evaluated (mkStr system)),-            ("builder", evaluated (mkStr builder)),-            ("_derivation", evaluated (VDerivation completeDrv))-          ]-            ++ [(outName, evaluated (VStr outP (outPathCtx outName))) | (outName, outP) <- outPaths]-      -- Merge original attrs underneath so computed attrs take priority-      resultAttrs = Map.union baseAttrs attrs--  pure (VAttrs resultAttrs)-builtinDerivation other =-  throwEvalError ("derivation: expected a set, got " <> typeName other)---- | Force a thunk to a Text string.-forceToText :: (MonadEval m) => Thunk -> m Text-forceToText thunk = do-  val <- force thunk-  case val of-    VStr s _ -> pure s-    VPath p -> pure p-    _ -> throwEvalError ("expected a string, got " <> typeName val)---- | Collect all string-coercible attributes for the derivation environment,--- along with the merged string context from all collected values.-collectDrvEnvWithContext :: (MonadEval m) => Map Text Thunk -> m ([(Text, Text)], StringContext)-collectDrvEnvWithContext attrs = do-  let pairs = Map.toList attrs-  results <- mapM tryCoerce pairs-  let envPairs = catMaybes [fmap (\(k, v, _) -> (k, v)) r | r <- results]-      mergedCtx = mconcat [ctx | Just (_, _, ctx) <- results]-  pure (envPairs, mergedCtx)-  where-    tryCoerce (key, thunk) = do-      val <- force thunk-      case val of-        VStr s ctx -> pure (Just (key, s, ctx))-        VPath p -> pure (Just (key, p, emptyContext))-        VInt n -> pure (Just (key, T.pack (show n), emptyContext))-        VBool True -> pure (Just (key, "1", emptyContext))-        VBool False -> pure (Just (key, "", emptyContext))-        VNull -> pure Nothing-        VList _ -> pure Nothing-        VAttrs innerAttrs ->-          -- Derivations in env get their outPath-          case Map.lookup "outPath" innerAttrs of-            Just outThunk -> do-              outVal <- force outThunk-              case outVal of-                VPath p -> pure (Just (key, p, emptyContext))-                VStr s ctx -> pure (Just (key, s, ctx))-                _ -> pure Nothing-            Nothing -> pure Nothing-        _ -> pure Nothing+    ThunkCell (..),++    -- * Attribute sets (re-exported from Types)+    AttrSet (..),+    LazyBinding (..),+    attrSetFromMap,+    attrSetLookup,+    attrSetKeys,+    attrSetToMap,+    attrSetToAscList,+    attrSetMember,+    attrSetElems,+    attrSetNull,+    attrSetSize,++    -- * String context (re-exported from Types)+    StringContextElement (..),+    StringContext (..),+    emptyContext,+    mkStr,++    -- * Environment (re-exported from Types)+    Env (..),+    emptyEnv,++    -- * Evaluation monad (re-exported from Types)+    MonadEval (..),+    PureEval (..),++    -- * Evaluation+    eval,+    force,++    -- * Helpers (for Builtins)+    typeName,+    evaluated,++    -- * Builtin registry+    BuiltinDef (..),+    builtinRegistry,+    builtinNames,++    -- * Platform+    currentSystemStr,+  )+where++import Control.Monad (when, (>=>))+import qualified Crypto.Hash as CH+import qualified Data.Array as Array+import Data.Bits (xor, (.&.), (.|.))+import qualified Data.ByteArray as BA+import qualified Data.ByteString as BS+import Data.Char (chr, digitToInt, isAlpha, isDigit, isHexDigit, isOctDigit, ord)+import Data.List (foldl')+import qualified Data.Map.Lazy as MapL+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Maybe (catMaybes, isJust, isNothing)+import Data.Sequence (Seq (..))+import qualified Data.Sequence as Seq+import qualified Data.Set as Set+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Nix.Derivation (Derivation (..), DerivationOutput (..), textToPlatform, toATerm)+import Nix.Eval.Context (extractInputDrvs, extractInputSrcs)+import Nix.Eval.Operator (evalBinary, evalUnary, nixCompare, nixEqual)+import Nix.Eval.StringInterp (coerceToString, evalIndStringParts, evalStringParts)+import Nix.Eval.Types+  ( AttrSet (..),+    Env (..),+    LazyBinding (..),+    MonadEval (..),+    NixValue (..),+    PureEval (..),+    StringContext (..),+    StringContextElement (..),+    Thunk (..),+    ThunkCell (..),+    attrSetElems,+    attrSetFromMap,+    attrSetKeys,+    attrSetLookup,+    attrSetMapWithKey,+    attrSetMember,+    attrSetNull,+    attrSetSize,+    attrSetToAscList,+    attrSetToMap,+    attrSetUnionWith,+    emptyContext,+    emptyEnv,+    envInsert,+    envInsertMany,+    envInsertThunk,+    envLookup,+    evaluated,+    mkStr,+    mkSyntheticThunk,+    mkThunk,+    newLazyAttrCache,+    pushWithScope,+    typeName,+  )+import Nix.Expr.Types+  ( AttrKey (..),+    AttrPath,+    BinaryOp (..),+    Binding (..),+    Expr (..),+    Formal (..),+    Formals (..),+    NixAtom (..),+    StringPart (..),+  )+import Nix.Hash (byteToHex, sha256Hex, truncatedBase32)+import Nix.Store.Path (StorePath (..), defaultStoreDir, defaultStoreDirText, parseStorePath, storePathToFilePath)+import qualified NovaCache.Base32 as Nix32+import qualified NovaCache.Base64 as B64+import qualified System.Info+import Text.Regex.TDFA (matchAllText)+import qualified Text.Regex.TDFA as RE++-- | Evaluate a Nix expression in an environment.+eval :: (MonadEval m) => Env -> Expr -> m NixValue+eval env expr = case expr of+  ELit atom -> evalLit atom+  EStr parts -> uncurry VStr <$> evalStringParts eval env parts+  EIndStr parts -> uncurry VStr <$> evalIndStringParts eval env parts+  EVar name -> evalVar env name+  EAttrs isRec bindings -> evalAttrs env isRec bindings+  EList exprs -> pure (VList (map (mkThunk env) exprs))+  ESelect target path defExpr -> evalSelect env target path defExpr+  EHasAttr target path -> evalHasAttr env target path+  EApp func arg -> evalApp env func arg+  ELambda formals body -> pure (VLambda env formals body)+  ELet bindings body -> evalLet env bindings body+  EIf cond thenExpr elseExpr -> evalIf env cond thenExpr elseExpr+  EWith scope body -> evalWith env scope body+  EAssert cond body -> evalAssert env cond body+  EUnary op operand -> do+    val <- eval env operand+    evalUnary op val+  ESearchPath name -> evalSearchPath env name+  EBinary OpAnd left right -> evalShortCircuitAnd env left right+  EBinary OpOr left right -> evalShortCircuitOr env left right+  EBinary OpImpl left right -> evalShortCircuitImpl env left right+  EBinary op left right -> do+    leftVal <- eval env left+    rightVal <- eval env right+    evalBinary force op leftVal rightVal++-- | Force a thunk to a value.+--+-- Delegates to 'forceThunk' which is a 'MonadEval' method — this+-- allows IO evaluators to implement memoization (caching the result+-- after the first force) while pure evaluators simply re-evaluate.+force :: (MonadEval m) => Thunk -> m NixValue+force = forceThunk eval++-- ---------------------------------------------------------------------------+-- Literal+-- ---------------------------------------------------------------------------++evalLit :: (MonadEval m) => NixAtom -> m NixValue+evalLit atom = case atom of+  NixInt n -> pure (VInt n)+  NixFloat n -> pure (VFloat n)+  NixBool b -> pure (VBool b)+  NixNull -> pure VNull+  NixUri u -> pure (mkStr u)+  NixPath p -> VPath <$> resolvePathLiteral p++-- ---------------------------------------------------------------------------+-- Search paths (<nixpkgs>, <nixpkgs/lib>)+-- ---------------------------------------------------------------------------++-- | Evaluate a search path expression.+-- Desugars to @builtins.findFile builtins.nixPath "name"@ — exactly how+-- real Nix handles @\<name\>@ expressions.+evalSearchPath :: (MonadEval m) => Env -> Text -> m NixValue+evalSearchPath env name = do+  builtinsVal <- evalVar env "builtins"+  case builtinsVal of+    VAttrs builtinsAttrs ->+      case attrSetLookup "nixPath" builtinsAttrs of+        Just nixPathThunk -> do+          nixPathVal <- force nixPathThunk+          builtinFindFile nixPathVal (mkStr name)+        Nothing ->+          throwEvalError ("file '" <> name <> "' was not found in the Nix search path")+    _ ->+      throwEvalError ("file '" <> name <> "' was not found in the Nix search path")++-- ---------------------------------------------------------------------------+-- Variables+-- ---------------------------------------------------------------------------++evalVar :: (MonadEval m) => Env -> Text -> m NixValue+evalVar env name =+  case envLookup name env of+    Just thunk -> force thunk+    Nothing -> throwEvalError ("undefined variable '" <> name <> "'")++-- ---------------------------------------------------------------------------+-- Attribute sets+-- ---------------------------------------------------------------------------++evalAttrs :: (MonadEval m) => Env -> Bool -> [Binding] -> m NixValue+evalAttrs env False bindings = evalNonRecAttrs env bindings+evalAttrs env True bindings = evalRecAttrs env bindings++-- | Non-recursive attribute set: thunks capture the outer environment.+--+-- Uses 'LazyAttrs' to defer thunk + IORef allocation: only keys that+-- are actually accessed get materialized.  For @all-packages.nix@+-- with ~15k entries per stdenv stage, this avoids ~15k IORef+-- allocations when only ~50 packages are touched.  Dynamic keys are+-- resolved eagerly (they may depend on evaluation), but value thunks+-- are deferred.+evalNonRecAttrs :: (MonadEval m) => Env -> [Binding] -> m NixValue+evalNonRecAttrs env bindings = do+  resolvedBindings <- resolveBindingKeys env bindings+  let lazyBindingMap = buildLazyBindingMap env resolvedBindings+      cache = newLazyAttrCache lazyBindingMap+  pure (VAttrs (LazyAttrs lazyBindingMap cache))++-- | Recursive attribute set: thunks capture the completed environment+-- (Haskell's laziness ties the knot — Thunk's Env field is lazy).+-- Dynamic keys in recursive attrs evaluate against the outer env+-- (the rec env is not yet available during key resolution).+--+-- Uses 'LazyAttrs' to defer thunk + IORef allocation: only keys that+-- are actually accessed get materialized.  For nixpkgs' 30k-entry+-- package set, this avoids ~30k IORef allocations when only ~50+-- packages are touched.  The env uses Data.Map.Lazy to similarly+-- defer thunk construction for variable lookups within the rec set.+evalRecAttrs :: (MonadEval m) => Env -> [Binding] -> m NixValue+evalRecAttrs env bindings = do+  -- Resolve dynamic keys eagerly against the outer env.+  resolvedBindings <- resolveBindingKeys env bindings+  -- Knot-tied: recEnv references recBindings which captures recEnv.+  -- Lazy-valued map for env bindings — uses Data.Map.Lazy so thunks+  -- are stored as unevaluated closures, deferring IORef allocation+  -- until the variable is actually looked up.+  -- Scope chain: recEnv has rec bindings locally, outer env as parent.+  -- Avoids O(n) MapL.union on the 30k-entry nixpkgs set — parent+  -- pointer means outer bindings are shared, not copied.+  let recEnv =+        Env+          { envBindings = recBindings,+            envParent = Just env,+            envWithScopes = envWithScopes env+          }+      -- INTENTIONALLY uses Data.Map.Lazy operations to defer thunk+      -- construction.  Each map value is a closure that allocates+      -- a Thunk + IORef only when forced by variable lookup.+      recBindings = buildResolvedBindingsMapLazy recEnv resolvedBindings+      -- Build per-key lazy binding recipes for the LazyAttrs.+      lazyBindingMap = buildLazyBindingMap recEnv resolvedBindings+      cache = newLazyAttrCache lazyBindingMap+  pure (VAttrs (LazyAttrs lazyBindingMap cache))++-- ---------------------------------------------------------------------------+-- Resolved bindings (for knot-tying in rec {} and let)+-- ---------------------------------------------------------------------------++-- | A binding with all attribute keys pre-resolved to text.+-- Used by 'evalRecAttrs' and 'evalLet' to separate key resolution+-- (monadic, may evaluate dynamic keys) from thunk construction+-- (pure, enables knot-tying).+data ResolvedBinding+  = -- | @path = expr@ with all keys resolved to text.+    ResolvedNamed ![Text] !Expr+  | -- | @inherit attrs@ from the surrounding scope.+    ResolvedInherit ![Text]+  | -- | @inherit (from) attrs@.+    ResolvedInheritFrom !Expr ![Text]++-- | Resolve all attribute keys in a list of bindings, evaluating+-- dynamic keys against the given environment.+resolveBindingKeys :: (MonadEval m) => Env -> [Binding] -> m [ResolvedBinding]+resolveBindingKeys keyEnv = fmap catMaybes . mapM resolveOne+  where+    resolveOne (NamedBinding path bodyExpr) = do+      resolvedPath <- mapM (resolveKey keyEnv) path+      -- If any key segment is null, skip the entire binding+      -- (matching real Nix; used by the module system for conditional attrs).+      pure (fmap (`ResolvedNamed` bodyExpr) (sequence resolvedPath))+    resolveOne (Inherit Nothing names) =+      pure (Just (ResolvedInherit names))+    resolveOne (Inherit (Just fromExpr) names) =+      pure (Just (ResolvedInheritFrom fromExpr names))++-- | Like 'buildResolvedBindingsMap' but uses 'Data.Map.Lazy' operations+-- so that thunk values are stored as unevaluated closures.  This defers+-- 'mkThunk' + IORef allocation until the key is actually accessed.+-- Used by 'evalRecAttrs' and 'evalLet' for the env bindings.+buildResolvedBindingsMapLazy :: Env -> [ResolvedBinding] -> Map Text Thunk+buildResolvedBindingsMapLazy thunkEnv =+  foldl' mergeBindingLazy MapL.empty+  where+    mergeBindingLazy current binding =+      MapL.unionWith mergeThunks current (processResolvedLazy thunkEnv binding)++-- | Like 'processResolved' but uses 'Data.Map.Lazy' operations+-- to avoid forcing thunk values on map insertion.+processResolvedLazy :: Env -> ResolvedBinding -> Map Text Thunk+processResolvedLazy thunkEnv (ResolvedNamed path bodyExpr) =+  buildResolvedNestedAttrLazy thunkEnv path bodyExpr+processResolvedLazy lookupEnv (ResolvedInherit names) =+  MapL.fromList [(n, inheritLookup lookupEnv n) | n <- names]+processResolvedLazy thunkEnv (ResolvedInheritFrom fromExpr names) =+  MapL.fromList+    [ (n, mkThunk thunkEnv (ESelect fromExpr [StaticKey n] Nothing))+    | n <- names+    ]++-- | Like 'buildResolvedNestedAttr' but uses 'Data.Map.Lazy' operations.+buildResolvedNestedAttrLazy :: Env -> [Text] -> Expr -> Map Text Thunk+buildResolvedNestedAttrLazy _thunkEnv [] _bodyExpr = Map.empty+buildResolvedNestedAttrLazy thunkEnv [key] bodyExpr =+  MapL.singleton key (mkThunk thunkEnv bodyExpr)+buildResolvedNestedAttrLazy thunkEnv (key : rest) bodyExpr =+  MapL.singleton key (evaluated (VAttrs (attrSetFromMap (buildResolvedNestedAttrLazy thunkEnv rest bodyExpr))))++-- | Build per-key lazy binding recipes from resolved bindings.+-- Simple @key = expr@ bindings become 'LazyExpr' (deferred thunk+-- construction).  Nested paths and inherits fall back to 'PreBuilt'+-- with eagerly-constructed thunks (these are rare in nixpkgs).+buildLazyBindingMap :: Env -> [ResolvedBinding] -> Map Text LazyBinding+buildLazyBindingMap thunkEnv = foldl' addBinding Map.empty+  where+    addBinding acc (ResolvedNamed [key] bodyExpr) =+      case Map.lookup key acc of+        Nothing -> Map.insert key (LazyExpr thunkEnv bodyExpr) acc+        -- Key collision (rare): must merge, so eagerly build both+        Just existing ->+          let existingThunk = materializeLazy key existing+              newThunk = mkThunk thunkEnv bodyExpr+           in Map.insert key (PreBuilt (mergeThunks existingThunk newThunk)) acc+    addBinding acc (ResolvedNamed path@(_ : _ : _) bodyExpr) =+      -- Nested path: build eagerly (rare in large rec sets)+      let nested = buildResolvedNestedAttr thunkEnv path bodyExpr+       in foldl' (\a (k, t) -> insertLazyWithMerge a k (PreBuilt t)) acc (Map.toList nested)+    addBinding acc (ResolvedNamed [] _) = acc+    addBinding acc (ResolvedInherit names) =+      foldl' (\a n -> insertLazyWithMerge a n (LazyInherit thunkEnv)) acc names+    addBinding acc (ResolvedInheritFrom fromExpr names) =+      foldl' (\a n -> insertLazyWithMerge a n (LazyInheritFrom thunkEnv fromExpr)) acc names++    insertLazyWithMerge acc key new =+      case Map.lookup key acc of+        Nothing -> Map.insert key new acc+        Just existing ->+          let existingThunk = materializeLazy key existing+              newThunk = materializeLazy key new+           in Map.insert key (PreBuilt (mergeThunks existingThunk newThunk)) acc++    materializeLazy _key (LazyExpr bindEnv expr) = mkThunk bindEnv expr+    materializeLazy bindKey (LazyInherit bindEnv) = inheritLookup bindEnv bindKey+    materializeLazy bindKey (LazyInheritFrom bindEnv fromExpr) =+      mkThunk bindEnv (ESelect fromExpr [StaticKey bindKey] Nothing)+    materializeLazy _bindKey (PreBuilt thunk) = thunk++-- | Build a nested attribute structure from a resolved dotted path (pure).+buildResolvedNestedAttr :: Env -> [Text] -> Expr -> Map Text Thunk+buildResolvedNestedAttr _thunkEnv [] _bodyExpr = Map.empty+buildResolvedNestedAttr thunkEnv [key] bodyExpr =+  Map.singleton key (mkThunk thunkEnv bodyExpr)+buildResolvedNestedAttr thunkEnv (key : rest) bodyExpr =+  Map.singleton key (evaluated (VAttrs (attrSetFromMap (buildResolvedNestedAttr thunkEnv rest bodyExpr))))++-- | Look up a name for @inherit@.  If not found, create a thunk+-- that will error when forced (matching real Nix behaviour).+inheritLookup :: Env -> Text -> Thunk+inheritLookup env name =+  case envLookup name env of+    Just thunk -> thunk+    Nothing ->+      -- Deferred error: only triggers if this binding is actually demanded.+      -- Constructs: builtins.throw "undefined variable '<name>'"+      let errMsg = "undefined variable '" <> name <> "'"+          throwExpr = EApp (EVar "throw") (EStr [StrLit errMsg])+       in mkThunk env throwExpr++-- | Resolve an attribute key.  Static keys always return 'Just'.+-- Dynamic keys evaluate to a string or 'null'; null means "skip this binding"+-- (matching real Nix, used by the module system for conditional attributes).+resolveKey :: (MonadEval m) => Env -> AttrKey -> m (Maybe Text)+resolveKey _env (StaticKey name) = pure (Just name)+resolveKey env (DynamicKey expr) = do+  val <- eval env expr+  case val of+    VStr s _ -> pure (Just s)+    VNull -> pure Nothing+    _ -> throwEvalError ("dynamic attribute key must be a string, got " <> typeName val)++-- | Merge two thunks at the same key.  If both are evaluated VAttrs,+-- merge their contents recursively.  Otherwise the right wins.+mergeThunks :: Thunk -> Thunk -> Thunk+mergeThunks (Evaluated (VAttrs a)) (Evaluated (VAttrs b)) =+  Evaluated (VAttrs (attrSetUnionWith mergeThunks a b))+mergeThunks _ new = new++-- ---------------------------------------------------------------------------+-- Select / has-attr+-- ---------------------------------------------------------------------------++evalSelect :: (MonadEval m) => Env -> Expr -> AttrPath -> Maybe Expr -> m NixValue+evalSelect env target path defExpr = do+  targetVal <- eval env target+  result <- walkAttrPath env path targetVal+  case result of+    Just val -> pure val+    Nothing -> case defExpr of+      Just def -> eval env def+      Nothing -> throwEvalError ("attribute path not found in " <> typeName targetVal)++-- | Walk an attribute path through nested attribute sets.+-- Returns @Just value@ if the full path resolves, @Nothing@ if any+-- key is missing or a non-set is encountered mid-path.+-- The @env@ is used only for resolving dynamic keys.+walkAttrPath :: (MonadEval m) => Env -> AttrPath -> NixValue -> m (Maybe NixValue)+walkAttrPath _env [] val = pure (Just val)+walkAttrPath env (key : rest) val = case val of+  VAttrs attrs -> do+    resolved <- resolveKey env key+    case resolved >>= (`attrSetLookup` attrs) of+      Just thunk -> do+        inner <- force thunk+        walkAttrPath env rest inner+      Nothing -> pure Nothing+  _ -> pure Nothing++evalHasAttr :: (MonadEval m) => Env -> Expr -> AttrPath -> m NixValue+evalHasAttr env target path = do+  targetVal <- eval env target+  result <- walkAttrPath env path targetVal+  case result of+    Just _ -> pure (VBool True)+    Nothing -> pure (VBool False)++-- ---------------------------------------------------------------------------+-- Application+-- ---------------------------------------------------------------------------++evalApp :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue+evalApp env funcExpr argExpr = do+  funcVal <- eval env funcExpr+  case funcVal of+    VLambda closureEnv formals body -> do+      let argThunk = mkThunk env argExpr+      extEnv <- matchFormals closureEnv formals argThunk+      eval extEnv body+    VBuiltin "tryEval" [] -> do+      result <- catchEvalError (eval env argExpr)+      case result of+        Right val ->+          pure+            ( VAttrs+                ( attrSetFromMap $+                    Map.fromList+                      [ ("success", evaluated (VBool True)),+                        ("value", evaluated val)+                      ]+                )+            )+        Left _ ->+          pure+            ( VAttrs+                ( attrSetFromMap $+                    Map.fromList+                      [ ("success", evaluated (VBool False)),+                        ("value", evaluated (VBool False))+                      ]+                )+            )+    VBuiltin name accArgs -> do+      argVal <- eval env argExpr+      applyBuiltin name accArgs argVal+    -- __functor support: a set with __functor is callable.+    -- Semantics: (attrs.__functor attrs) arg+    VAttrs attrs+      | Just functorThunk <- attrSetLookup "__functor" attrs -> do+          functor <- force functorThunk+          -- Apply __functor to the set itself, yielding a function+          partiallyApplied <- applyValue functor funcVal+          -- Apply the resulting function to the argument+          argVal <- eval env argExpr+          applyValue partiallyApplied argVal+    _ -> throwEvalError ("attempt to call " <> typeName funcVal <> ", which is not a function")++-- | Match function formals against an argument thunk.+matchFormals :: (MonadEval m) => Env -> Formals -> Thunk -> m Env+matchFormals closureEnv (FormalName name) argThunk =+  pure (envInsertThunk name argThunk closureEnv)+matchFormals closureEnv (FormalSet formals allowExtra) argThunk = do+  argVal <- force argThunk+  matchFormalSet closureEnv formals allowExtra argVal+matchFormals closureEnv (FormalNamedSet name formals allowExtra) argThunk = do+  argVal <- force argThunk+  -- Bind the @-pattern name (e.g. "args") BEFORE matching formals so that+  -- default expressions like @system ? args.system or ...@ can see it.+  let envWithAt = envInsertThunk name argThunk closureEnv+  matchFormalSet envWithAt formals allowExtra argVal++-- | Match a formal set pattern against a VAttrs argument.+--+-- All formals are batched into a SINGLE scope level (one Map, one Env)+-- instead of creating a separate singleton Env per formal.  This reduces+-- Env allocations from O(N) to O(1) per function call — critical for+-- nixpkgs where millions of function calls with 5–30 formals each were+-- creating 7M+ Env objects and 600+ MB of Map.Bin nodes.+--+-- Default expressions use formalEnv via knot-tying, so they can+-- reference other formals — matching real Nix semantics where all+-- formals are mutually visible (e.g. @{ a ? b, b ? 1 }: a@ yields 1).+matchFormalSet :: (MonadEval m) => Env -> [Formal] -> Bool -> NixValue -> m Env+matchFormalSet closureEnv formals allowExtra argVal =+  case argVal of+    VAttrs attrs -> do+      checkExtraKeys formals allowExtra attrs+      -- Eagerly check required formals BEFORE building the scope,+      -- so missing-attribute errors are immediate, not deferred.+      checkMissingFormals attrs formals+      -- Batch all formal bindings into ONE scope level.+      -- Knot-tying: formalEnv is used in mkThunk for defaults,+      -- but mkThunk captures Env lazily (Pending !Expr Env).+      let formalEnv = envInsertMany formalBindings closureEnv+          formalBindings =+            Map.fromList+              [ (fName formal, resolveOneFormal formal)+              | formal <- formals+              ]+          resolveOneFormal (Formal name defExpr) =+            case attrSetLookup name attrs of+              Just thunk -> thunk+              Nothing -> case defExpr of+                Just def -> mkThunk formalEnv def+                -- Unreachable: checkMissingFormals verified all+                -- required formals (those with no default) are present.+                Nothing -> error "matchFormalSet: missing required formal (unreachable)"+      pure formalEnv+    _ -> throwEvalError ("function expects a set argument, got " <> typeName argVal)++-- | Verify that no unexpected keys are present (unless @...@ allows them).+checkExtraKeys :: (MonadEval m) => [Formal] -> Bool -> AttrSet -> m ()+checkExtraKeys _ True _ = pure ()+checkExtraKeys formals False attrs =+  let expected = map fName formals+      actual = attrSetKeys attrs+      extra = filter (`notElem` expected) actual+   in case extra of+        [] -> pure ()+        (k : _) -> throwEvalError ("unexpected attribute '" <> k <> "' in function argument")++-- | Check that all required formals (those without defaults) are present.+checkMissingFormals :: (MonadEval m) => AttrSet -> [Formal] -> m ()+checkMissingFormals attrs formals =+  case [fName f | f <- formals, isNothing (fDefault f), not (attrSetMember (fName f) attrs)] of+    [] -> pure ()+    (name : _) -> throwEvalError ("missing required attribute '" <> name <> "'")++-- ---------------------------------------------------------------------------+-- Let / if / with / assert+-- ---------------------------------------------------------------------------++-- | Let is recursive in Nix: all bindings are visible to each other.+-- Knot-tying via Haskell laziness (Thunk's Env field is lazy).+-- Dynamic keys in let evaluate against the outer env (the let env+-- is not yet available during key resolution).+--+-- Uses Data.Map.Lazy for env bindings to defer thunk construction.+evalLet :: (MonadEval m) => Env -> [Binding] -> Expr -> m NixValue+evalLet env bindings body = do+  resolvedBindings <- resolveBindingKeys env bindings+  -- Scope chain: let bindings as local scope, outer env as parent.+  let letEnv =+        Env+          { envBindings = letBindings,+            envParent = Just env,+            envWithScopes = envWithScopes env+          }+      -- INTENTIONALLY uses Data.Map.Lazy operations to defer thunk+      -- construction, matching evalRecAttrs.+      letBindings = buildResolvedBindingsMapLazy letEnv resolvedBindings+  eval letEnv body++evalIf :: (MonadEval m) => Env -> Expr -> Expr -> Expr -> m NixValue+evalIf env cond thenExpr elseExpr = do+  condVal <- eval env cond+  case condVal of+    VBool True -> eval env thenExpr+    VBool False -> eval env elseExpr+    _ -> throwEvalError ("'if' condition must be a Boolean, got " <> typeName condVal)++evalWith :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue+evalWith env scope body = do+  scopeVal <- eval env scope+  case scopeVal of+    VAttrs attrs -> eval (pushWithScope attrs env) body+    _ -> throwEvalError ("'with' requires a set, got " <> typeName scopeVal)++evalAssert :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue+evalAssert env cond body = do+  condVal <- eval env cond+  case condVal of+    VBool True -> eval env body+    VBool False -> throwEvalError "assertion failed"+    _ -> throwEvalError ("assertion condition must be a Boolean, got " <> typeName condVal)++-- ---------------------------------------------------------------------------+-- Short-circuit Boolean operators+-- ---------------------------------------------------------------------------++evalShortCircuitAnd :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue+evalShortCircuitAnd env left right = do+  leftVal <- eval env left+  case leftVal of+    VBool False -> pure (VBool False)+    VBool True -> do+      rightVal <- eval env right+      case rightVal of+        VBool _ -> pure rightVal+        _ -> throwEvalError ("second operand of && must be a Boolean, got " <> typeName rightVal)+    _ -> throwEvalError ("first operand of && must be a Boolean, got " <> typeName leftVal)++evalShortCircuitOr :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue+evalShortCircuitOr env left right = do+  leftVal <- eval env left+  case leftVal of+    VBool True -> pure (VBool True)+    VBool False -> do+      rightVal <- eval env right+      case rightVal of+        VBool _ -> pure rightVal+        _ -> throwEvalError ("second operand of || must be a Boolean, got " <> typeName rightVal)+    _ -> throwEvalError ("first operand of || must be a Boolean, got " <> typeName leftVal)++evalShortCircuitImpl :: (MonadEval m) => Env -> Expr -> Expr -> m NixValue+evalShortCircuitImpl env left right = do+  leftVal <- eval env left+  case leftVal of+    VBool False -> pure (VBool True)+    VBool True -> do+      rightVal <- eval env right+      case rightVal of+        VBool _ -> pure rightVal+        _ -> throwEvalError ("second operand of -> must be a Boolean, got " <> typeName rightVal)+    _ -> throwEvalError ("first operand of -> must be a Boolean, got " <> typeName leftVal)++-- ---------------------------------------------------------------------------+-- Builtin registry (single-definition-site for all builtins)+-- ---------------------------------------------------------------------------++-- | A builtin function definition: its arity and implementation.+data BuiltinDef m = BuiltinDef+  { bdArity :: !Int,+    bdApply :: [NixValue] -> m NixValue+  }++-- | Define an arity-1 builtin.+builtin1 :: (MonadEval m) => Text -> (NixValue -> m NixValue) -> (Text, BuiltinDef m)+builtin1 name f =+  ( name,+    BuiltinDef 1 $ \case+      [a] -> f a+      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")+  )++-- | Define an arity-2 builtin.+builtin2 ::+  (MonadEval m) =>+  Text ->+  (NixValue -> NixValue -> m NixValue) ->+  (Text, BuiltinDef m)+builtin2 name f =+  ( name,+    BuiltinDef 2 $ \case+      [a, b] -> f a b+      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")+  )++-- | Define an arity-3 builtin.+builtin3 ::+  (MonadEval m) =>+  Text ->+  (NixValue -> NixValue -> NixValue -> m NixValue) ->+  (Text, BuiltinDef m)+builtin3 name f =+  ( name,+    BuiltinDef 3 $ \case+      [a, b, c] -> f a b c+      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")+  )++-- | Central registry of all builtins.  Adding a new builtin is a single+-- entry here plus its implementation function — no other files need changes.+builtinRegistry :: (MonadEval m) => Map Text (BuiltinDef m)+builtinRegistry =+  Map.fromList+    [ -- Type checking (arity 1)+      builtin1 "typeOf" (pure . mkStr . typeOfValue),+      builtin1 "isNull" (pure . VBool . isNullVal),+      builtin1 "isInt" (pure . VBool . isIntVal),+      builtin1 "isFloat" (pure . VBool . isFloatVal),+      builtin1 "isBool" (pure . VBool . isBoolVal),+      builtin1 "isString" (pure . VBool . isStringVal),+      builtin1 "isList" (pure . VBool . isListVal),+      builtin1 "isAttrs" (pure . VBool . isAttrsVal),+      builtin1 "isFunction" (pure . VBool . isFunctionVal),+      -- List operations (arity 1)+      builtin1 "length" builtinLength,+      builtin1 "head" builtinHead,+      builtin1 "tail" builtinTail,+      -- String operations (arity 1)+      builtin1 "toString" (fmap (uncurry VStr) . coerceToStringPermissive),+      builtin1 "stringLength" builtinStringLength,+      -- Control (arity 1)+      builtin1 "throw" builtinThrow,+      builtin1 "abort" builtinThrow,+      -- Attr set operations (arity 1)+      builtin1 "attrNames" builtinAttrNames,+      builtin1 "attrValues" builtinAttrValues,+      builtin1 "listToAttrs" builtinListToAttrs,+      -- Attr set operations (arity 2)+      builtin2 "hasAttr" builtinHasAttr,+      builtin2 "getAttr" builtinGetAttr,+      builtin2 "removeAttrs" builtinRemoveAttrs,+      builtin2 "intersectAttrs" builtinIntersectAttrs,+      builtin2 "catAttrs" builtinCatAttrs,+      -- List higher-order (arity 2)+      builtin2 "map" builtinMap,+      builtin2 "filter" builtinFilter,+      builtin2 "genList" builtinGenList,+      builtin2 "sort" builtinSort,+      builtin2 "concatMap" builtinConcatMap,+      builtin2 "any" builtinAny,+      builtin2 "all" builtinAll,+      builtin2 "elem" builtinElem,+      builtin2 "elemAt" builtinElemAt,+      builtin2 "partition" builtinPartition,+      builtin2 "groupBy" builtinGroupBy,+      -- String operations (arity 2)+      builtin2 "concatStringsSep" builtinConcatStringsSep,+      -- Arity 3+      builtin3 "foldl'" builtinFoldl,+      builtin3 "substring" builtinSubstring,+      -- Numeric+      builtin1 "isPath" (pure . VBool . isPathVal),+      builtin1 "ceil" builtinCeil,+      builtin1 "floor" builtinFloor,+      builtin2 "seq" (\_ b -> pure b),+      builtin2 "trace" (\_ b -> pure b),+      builtin1 "unsafeDiscardStringContext" builtinDiscardContext,+      builtin1 "unsafeDiscardOutputDependency" builtinDiscardOutputDep,+      -- String context introspection+      builtin1 "hasContext" builtinHasContext,+      builtin1 "getContext" builtinGetContext,+      builtin2 "appendContext" builtinAppendContext,+      builtin1 "baseNameOf" builtinBaseNameOf,+      builtin1 "dirOf" builtinDirOf,+      builtin1 "concatLists" builtinConcatLists,+      builtin2 "lessThan" builtinLessThan,+      -- Arithmetic + bitwise+      builtin2 "add" builtinAdd,+      builtin2 "sub" builtinSub,+      builtin2 "mul" builtinMul,+      builtin2 "div" builtinDiv,+      builtin2 "mod" builtinMod,+      builtin2 "bitAnd" builtinBitAnd,+      builtin2 "bitOr" builtinBitOr,+      builtin2 "bitXor" builtinBitXor,+      builtin2 "min" builtinMin,+      builtin2 "max" builtinMax,+      -- Attr set higher-order+      builtin2 "mapAttrs" builtinMapAttrs,+      builtin1 "functionArgs" builtinFunctionArgs,+      builtin2 "setFunctionArgs" builtinSetFunctionArgs,+      builtin2 "zipAttrsWith" builtinZipAttrsWith,+      -- String manipulation+      builtin2 "match" builtinMatch,+      builtin2 "split" builtinSplit,+      builtin3 "replaceStrings" builtinReplaceStrings,+      builtin2 "compareVersions" builtinCompareVersions,+      builtin1 "splitVersion" builtinSplitVersion,+      builtin1 "parseDrvName" builtinParseDrvName,+      -- Serialization + hashing+      builtin1 "toJSON" builtinToJSON,+      builtin1 "fromJSON" builtinFromJSON,+      builtin2 "hashString" builtinHashString,+      -- Error handling + sequencing+      builtin1 "tryEval" (\_ -> throwEvalError "unreachable: tryEval handled in evalApp"),+      builtin2 "deepSeq" builtinDeepSeq,+      -- Graph traversal+      builtin1 "genericClosure" builtinGenericClosure,+      -- IO builtins (delegate to MonadEval methods)+      builtin1 "import" builtinImport,+      builtin1 "readFile" builtinReadFile,+      builtin1 "pathExists" builtinPathExists,+      builtin1 "readDir" builtinReadDir,+      builtin1 "getEnv" builtinGetEnv,+      builtin1 "toPath" builtinToPath,+      -- Store path operations+      builtin1 "placeholder" builtinPlaceholder,+      builtin1 "storePath" builtinStorePath,+      builtin2 "findFile" builtinFindFile,+      builtin2 "toFile" builtinToFile,+      builtin2 "scopedImport" builtinScopedImport,+      -- Network fetchers+      builtin1 "fetchurl" builtinFetchurl,+      builtin1 "fetchTarball" builtinFetchTarball,+      builtin1 "fetchGit" builtinFetchGit,+      -- Derivation construction+      builtin1 "derivation" builtinDerivation,+      -- Error context (pass-through — context only matters on error)+      builtin2 "addErrorContext" (\_ val -> pure val),+      -- Attr position (return null — nixpkgs handles this gracefully)+      builtin2 "unsafeGetAttrPos" (\_ _ -> pure VNull),+      -- Debugging (no-ops without --trace-verbose / --debugger)+      builtin2 "traceVerbose" (\_ b -> pure b),+      builtin1 "break" pure,+      -- IO: file hashing + type detection+      builtin2 "hashFile" builtinHashFile,+      builtin1 "readFileType" builtinReadFileType,+      -- Serialization+      builtin1 "fromTOML" builtinFromTOML,+      -- Hash conversion+      builtin1 "convertHash" builtinConvertHash,+      -- XML serialization+      builtin1 "toXML" builtinToXML,+      -- Source filtering+      builtin2 "filterSource" builtinFilterSource,+      -- Experimental feature stubs+      builtin2 "outputOf" builtinOutputOf,+      builtin1 "fetchTree" builtinFetchTree,+      builtin1 "fetchClosure" builtinFetchClosure+    ]++-- | Names of all registered builtins.+builtinNames :: [Text]+builtinNames = Map.keys (builtinRegistry :: Map Text (BuiltinDef PureEval))++-- ---------------------------------------------------------------------------+-- Builtin dispatch (partial application via accumulated args)+-- ---------------------------------------------------------------------------++-- | Arity of a builtin (how many arguments before execution).+builtinArity :: Text -> Int+builtinArity name = maybe 1 bdArity (Map.lookup name (builtinRegistry :: Map Text (BuiltinDef PureEval)))++-- | Apply a builtin with accumulated args.  If we have enough args,+-- execute; otherwise return a partially applied builtin.+applyBuiltin :: (MonadEval m) => Text -> [NixValue] -> NixValue -> m NixValue+applyBuiltin name accArgs arg =+  let allArgs = accArgs ++ [arg]+      arity = builtinArity name+   in if length allArgs < arity+        then pure (VBuiltin name allArgs)+        else executeBuiltin name allArgs++-- | Apply a function value (lambda or builtin) to one argument.+-- Used by higher-order builtins to invoke user-supplied functions.+applyValue :: (MonadEval m) => NixValue -> NixValue -> m NixValue+applyValue (VLambda closureEnv formals body) arg = do+  extEnv <- matchFormals closureEnv formals (evaluated arg)+  eval extEnv body+applyValue (VBuiltin name accArgs) arg =+  applyBuiltin name accArgs arg+applyValue other _ =+  throwEvalError ("attempt to call " <> typeName other <> ", which is not a function")++-- | Execute a builtin once all arguments are collected.+--+-- Direct case dispatch avoids rebuilding the polymorphic 'builtinRegistry'+-- Map on every call.  'builtinRegistry' is polymorphic in @m@ so GHC+-- cannot cache it as a CAF — it gets reconstructed on every use.+-- Pattern matching on the name is zero-allocation.+executeBuiltin :: (MonadEval m) => Text -> [NixValue] -> m NixValue+executeBuiltin name args = case name of+  -- Type checking (arity 1)+  "typeOf" -> apply1 (pure . mkStr . typeOfValue)+  "isNull" -> apply1 (pure . VBool . isNullVal)+  "isInt" -> apply1 (pure . VBool . isIntVal)+  "isFloat" -> apply1 (pure . VBool . isFloatVal)+  "isBool" -> apply1 (pure . VBool . isBoolVal)+  "isString" -> apply1 (pure . VBool . isStringVal)+  "isList" -> apply1 (pure . VBool . isListVal)+  "isAttrs" -> apply1 (pure . VBool . isAttrsVal)+  "isFunction" -> apply1 (pure . VBool . isFunctionVal)+  -- List operations (arity 1)+  "length" -> apply1 builtinLength+  "head" -> apply1 builtinHead+  "tail" -> apply1 builtinTail+  -- String operations (arity 1)+  "toString" -> apply1 (fmap (uncurry VStr) . coerceToStringPermissive)+  "stringLength" -> apply1 builtinStringLength+  -- Control (arity 1)+  "throw" -> apply1 builtinThrow+  "abort" -> apply1 builtinThrow+  -- Attr set operations (arity 1)+  "attrNames" -> apply1 builtinAttrNames+  "attrValues" -> apply1 builtinAttrValues+  "listToAttrs" -> apply1 builtinListToAttrs+  -- Attr set operations (arity 2)+  "hasAttr" -> apply2 builtinHasAttr+  "getAttr" -> apply2 builtinGetAttr+  "removeAttrs" -> apply2 builtinRemoveAttrs+  "intersectAttrs" -> apply2 builtinIntersectAttrs+  "catAttrs" -> apply2 builtinCatAttrs+  -- List higher-order (arity 2)+  "map" -> apply2 builtinMap+  "filter" -> apply2 builtinFilter+  "genList" -> apply2 builtinGenList+  "sort" -> apply2 builtinSort+  "concatMap" -> apply2 builtinConcatMap+  "any" -> apply2 builtinAny+  "all" -> apply2 builtinAll+  "elem" -> apply2 builtinElem+  "elemAt" -> apply2 builtinElemAt+  "partition" -> apply2 builtinPartition+  "groupBy" -> apply2 builtinGroupBy+  -- String operations (arity 2)+  "concatStringsSep" -> apply2 builtinConcatStringsSep+  -- Arity 3+  "foldl'" -> apply3 builtinFoldl+  "substring" -> apply3 builtinSubstring+  -- Numeric+  "isPath" -> apply1 (pure . VBool . isPathVal)+  "ceil" -> apply1 builtinCeil+  "floor" -> apply1 builtinFloor+  "seq" -> apply2 (\_ b -> pure b)+  "trace" -> apply2 (\_ b -> pure b)+  "unsafeDiscardStringContext" -> apply1 builtinDiscardContext+  "unsafeDiscardOutputDependency" -> apply1 builtinDiscardOutputDep+  -- String context introspection+  "hasContext" -> apply1 builtinHasContext+  "getContext" -> apply1 builtinGetContext+  "appendContext" -> apply2 builtinAppendContext+  "baseNameOf" -> apply1 builtinBaseNameOf+  "dirOf" -> apply1 builtinDirOf+  "concatLists" -> apply1 builtinConcatLists+  "lessThan" -> apply2 builtinLessThan+  -- Arithmetic + bitwise+  "add" -> apply2 builtinAdd+  "sub" -> apply2 builtinSub+  "mul" -> apply2 builtinMul+  "div" -> apply2 builtinDiv+  "mod" -> apply2 builtinMod+  "bitAnd" -> apply2 builtinBitAnd+  "bitOr" -> apply2 builtinBitOr+  "bitXor" -> apply2 builtinBitXor+  "min" -> apply2 builtinMin+  "max" -> apply2 builtinMax+  -- Attr set higher-order+  "mapAttrs" -> apply2 builtinMapAttrs+  "functionArgs" -> apply1 builtinFunctionArgs+  "setFunctionArgs" -> apply2 builtinSetFunctionArgs+  "zipAttrsWith" -> apply2 builtinZipAttrsWith+  -- String manipulation+  "match" -> apply2 builtinMatch+  "split" -> apply2 builtinSplit+  "replaceStrings" -> apply3 builtinReplaceStrings+  "compareVersions" -> apply2 builtinCompareVersions+  "splitVersion" -> apply1 builtinSplitVersion+  "parseDrvName" -> apply1 builtinParseDrvName+  -- Serialization + hashing+  "toJSON" -> apply1 builtinToJSON+  "fromJSON" -> apply1 builtinFromJSON+  "hashString" -> apply2 builtinHashString+  -- Error handling + sequencing+  "tryEval" -> apply1 (\_ -> throwEvalError "unreachable: tryEval handled in evalApp")+  "deepSeq" -> apply2 builtinDeepSeq+  -- Graph traversal+  "genericClosure" -> apply1 builtinGenericClosure+  -- IO builtins (delegate to MonadEval methods)+  "import" -> apply1 builtinImport+  "readFile" -> apply1 builtinReadFile+  "pathExists" -> apply1 builtinPathExists+  "readDir" -> apply1 builtinReadDir+  "getEnv" -> apply1 builtinGetEnv+  "toPath" -> apply1 builtinToPath+  -- Store path operations+  "placeholder" -> apply1 builtinPlaceholder+  "storePath" -> apply1 builtinStorePath+  "findFile" -> apply2 builtinFindFile+  "toFile" -> apply2 builtinToFile+  "scopedImport" -> apply2 builtinScopedImport+  -- Network fetchers+  "fetchurl" -> apply1 builtinFetchurl+  "fetchTarball" -> apply1 builtinFetchTarball+  "fetchGit" -> apply1 builtinFetchGit+  -- Derivation construction+  "derivation" -> apply1 builtinDerivation+  -- Error context (pass-through — context only matters on error)+  "addErrorContext" -> apply2 (\_ val -> pure val)+  -- Attr position (return null — nixpkgs handles this gracefully)+  "unsafeGetAttrPos" -> apply2 (\_ _ -> pure VNull)+  -- Debugging (no-ops without --trace-verbose / --debugger)+  "traceVerbose" -> apply2 (\_ b -> pure b)+  "break" -> apply1 pure+  -- IO: file hashing + type detection+  "hashFile" -> apply2 builtinHashFile+  "readFileType" -> apply1 builtinReadFileType+  -- Serialization+  "fromTOML" -> apply1 builtinFromTOML+  -- Hash conversion+  "convertHash" -> apply1 builtinConvertHash+  -- XML serialization+  "toXML" -> apply1 builtinToXML+  -- Source filtering+  "filterSource" -> apply2 builtinFilterSource+  -- Experimental feature stubs+  "outputOf" -> apply2 builtinOutputOf+  "fetchTree" -> apply1 builtinFetchTree+  "fetchClosure" -> apply1 builtinFetchClosure+  _ -> throwEvalError ("unknown builtin '" <> name <> "'")+  where+    apply1 f = case args of+      [a] -> f a+      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")+    apply2 f = case args of+      [a, b] -> f a b+      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")+    apply3 f = case args of+      [a, b, c] -> f a b c+      _ -> throwEvalError ("builtins." <> name <> ": internal arity error")++-- ---------------------------------------------------------------------------+-- Builtin implementations — type checking+-- ---------------------------------------------------------------------------++typeOfValue :: NixValue -> Text+typeOfValue val = case val of+  VInt _ -> "int"+  VFloat _ -> "float"+  VBool _ -> "bool"+  VNull -> "null"+  VStr _ _ -> "string"+  VPath _ -> "path"+  VList _ -> "list"+  VAttrs _ -> "set"+  VLambda {} -> "lambda"+  VBuiltin _ _ -> "lambda"+  VDerivation _ -> "set"++isNullVal :: NixValue -> Bool+isNullVal VNull = True+isNullVal _ = False++isIntVal :: NixValue -> Bool+isIntVal (VInt _) = True+isIntVal _ = False++isFloatVal :: NixValue -> Bool+isFloatVal (VFloat _) = True+isFloatVal _ = False++isBoolVal :: NixValue -> Bool+isBoolVal (VBool _) = True+isBoolVal _ = False++isStringVal :: NixValue -> Bool+isStringVal (VStr _ _) = True+isStringVal _ = False++isListVal :: NixValue -> Bool+isListVal (VList _) = True+isListVal _ = False++isAttrsVal :: NixValue -> Bool+isAttrsVal (VAttrs _) = True+isAttrsVal _ = False++isFunctionVal :: NixValue -> Bool+isFunctionVal (VLambda {}) = True+isFunctionVal (VBuiltin _ _) = True+isFunctionVal _ = False++-- ---------------------------------------------------------------------------+-- Builtin implementations — list (arity 1)+-- ---------------------------------------------------------------------------++builtinLength :: (MonadEval m) => NixValue -> m NixValue+builtinLength (VList xs) = pure (VInt (fromIntegral (length xs)))+builtinLength other = throwEvalError ("builtins.length: expected a list, got " <> typeName other)++builtinHead :: (MonadEval m) => NixValue -> m NixValue+builtinHead (VList []) = throwEvalError "builtins.head: empty list"+builtinHead (VList (x : _)) = force x+builtinHead other = throwEvalError ("builtins.head: expected a list, got " <> typeName other)++builtinTail :: (MonadEval m) => NixValue -> m NixValue+builtinTail (VList []) = throwEvalError "builtins.tail: empty list"+builtinTail (VList (_ : xs)) = pure (VList xs)+builtinTail other = throwEvalError ("builtins.tail: expected a list, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin implementations — string (arity 1)+-- ---------------------------------------------------------------------------++builtinStringLength :: (MonadEval m) => NixValue -> m NixValue+builtinStringLength (VStr s _) = pure (VInt (fromIntegral (T.length s)))+builtinStringLength other =+  throwEvalError ("builtins.stringLength: expected a string, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin implementations — control+-- ---------------------------------------------------------------------------++builtinThrow :: (MonadEval m) => NixValue -> m NixValue+builtinThrow (VStr msg _) = throwEvalError msg+builtinThrow other = throwEvalError ("builtins.throw: expected a string, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin implementations — attr set (arity 1)+-- ---------------------------------------------------------------------------++builtinAttrNames :: (MonadEval m) => NixValue -> m NixValue+builtinAttrNames (VAttrs attrs) =+  -- Zero thunk allocation for LazyAttrs: attrSetKeys reads binding map keys.+  pure (VList (map (evaluated . mkStr) (attrSetKeys attrs)))+builtinAttrNames other =+  throwEvalError ("builtins.attrNames: expected a set, got " <> typeName other)++builtinAttrValues :: (MonadEval m) => NixValue -> m NixValue+builtinAttrValues (VAttrs attrs) =+  pure (VList (attrSetElems attrs))+builtinAttrValues other =+  throwEvalError ("builtins.attrValues: expected a set, got " <> typeName other)++builtinListToAttrs :: (MonadEval m) => NixValue -> m NixValue+builtinListToAttrs (VList thunks) = do+  pairs <- mapM listToAttrsPair thunks+  pure (VAttrs (attrSetFromMap (Map.fromList pairs)))+builtinListToAttrs other =+  throwEvalError ("builtins.listToAttrs: expected a list, got " <> typeName other)++-- | Extract { name, value } from a thunk for listToAttrs.+listToAttrsPair :: (MonadEval m) => Thunk -> m (Text, Thunk)+listToAttrsPair thunk = do+  val <- force thunk+  case val of+    VAttrs attrs -> do+      nameThunk <-+        maybe (throwEvalError "builtins.listToAttrs: element missing 'name'") pure $+          attrSetLookup "name" attrs+      nameVal <- force nameThunk+      case nameVal of+        VStr keyName _ ->+          case attrSetLookup "value" attrs of+            Just valueThunk -> pure (keyName, valueThunk)+            Nothing -> throwEvalError "builtins.listToAttrs: element missing 'value'"+        _ -> throwEvalError "builtins.listToAttrs: 'name' must be a string"+    _ -> throwEvalError "builtins.listToAttrs: element must be a set"++-- ---------------------------------------------------------------------------+-- Builtin implementations — attr set (arity 2)+-- ---------------------------------------------------------------------------++builtinHasAttr :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinHasAttr (VStr key _) (VAttrs attrs) =+  pure (VBool (attrSetMember key attrs))+builtinHasAttr (VStr _ _) other =+  throwEvalError ("builtins.hasAttr: expected a set, got " <> typeName other)+builtinHasAttr other _ =+  throwEvalError ("builtins.hasAttr: expected a string, got " <> typeName other)++builtinGetAttr :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinGetAttr (VStr key _) (VAttrs attrs) =+  case attrSetLookup key attrs of+    Just thunk -> force thunk+    Nothing -> throwEvalError ("builtins.getAttr: attribute '" <> key <> "' not found")+builtinGetAttr (VStr _ _) other =+  throwEvalError ("builtins.getAttr: expected a set, got " <> typeName other)+builtinGetAttr other _ =+  throwEvalError ("builtins.getAttr: expected a string, got " <> typeName other)++builtinRemoveAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinRemoveAttrs (VAttrs attrs) (VList thunks) = do+  keys <- mapM forceToString thunks+  let m = attrSetToMap attrs+  pure (VAttrs (attrSetFromMap (foldl' (flip Map.delete) m keys)))+  where+    forceToString thunk = do+      val <- force thunk+      case val of+        VStr s _ -> pure s+        _ -> throwEvalError "builtins.removeAttrs: key list must contain strings"+builtinRemoveAttrs (VAttrs _) other =+  throwEvalError ("builtins.removeAttrs: expected a list, got " <> typeName other)+builtinRemoveAttrs other _ =+  throwEvalError ("builtins.removeAttrs: expected a set, got " <> typeName other)++builtinIntersectAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinIntersectAttrs (VAttrs a) (VAttrs b) =+  -- Iterate keys of 'a' (typically the smaller set, e.g. functionArgs)+  -- and point-lookup each in 'b' (typically the large set, e.g. nixpkgs).+  -- This avoids materializing all thunks in 'b'.+  let keysA = attrSetKeys a+      result = Map.fromList [(k, thunk) | k <- keysA, Just thunk <- [attrSetLookup k b]]+   in pure (VAttrs (attrSetFromMap result))+builtinIntersectAttrs (VAttrs _) other =+  throwEvalError ("builtins.intersectAttrs: expected a set, got " <> typeName other)+builtinIntersectAttrs other _ =+  throwEvalError ("builtins.intersectAttrs: expected a set, got " <> typeName other)++builtinCatAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinCatAttrs (VStr key _) (VList thunks) = do+  vals <- catAttrsCollect key thunks+  pure (VList vals)+builtinCatAttrs (VStr _ _) other =+  throwEvalError ("builtins.catAttrs: expected a list, got " <> typeName other)+builtinCatAttrs other _ =+  throwEvalError ("builtins.catAttrs: expected a string, got " <> typeName other)++-- | Collect values for a given key from a list of attrsets.+-- Tail-recursive with accumulator to avoid stack overflow on large lists.+catAttrsCollect :: (MonadEval m) => Text -> [Thunk] -> m [Thunk]+catAttrsCollect key = go []+  where+    go !acc [] = pure (reverse acc)+    go !acc (thunk : rest) = do+      val <- force thunk+      case val of+        VAttrs attrs ->+          case attrSetLookup key attrs of+            Just found -> go (found : acc) rest+            Nothing -> go acc rest+        _ -> throwEvalError "builtins.catAttrs: list element must be a set"++-- ---------------------------------------------------------------------------+-- Builtin implementations — list higher-order (arity 2)+-- ---------------------------------------------------------------------------++builtinMap :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinMap func (VList thunks) =+  -- Lazy: each element is a deferred application, forced only on demand.+  pure (VList (map (deferApply func) thunks))+builtinMap _ other =+  throwEvalError ("builtins.map: expected a list, got " <> typeName other)++builtinFilter :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinFilter predFn (VList thunks) = do+  filtered <- filterThunks predFn thunks+  pure (VList filtered)+builtinFilter _ other =+  throwEvalError ("builtins.filter: expected a list, got " <> typeName other)++filterThunks :: (MonadEval m) => NixValue -> [Thunk] -> m [Thunk]+filterThunks _ [] = pure []+filterThunks predFn (thunk : rest) = do+  val <- force thunk+  result <- applyValue predFn val+  case result of+    VBool True -> (thunk :) <$> filterThunks predFn rest+    VBool False -> filterThunks predFn rest+    _ -> throwEvalError "builtins.filter: predicate must return a bool"++builtinGenList :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinGenList func (VInt n)+  | n < 0 = throwEvalError "builtins.genList: length must be non-negative"+  | otherwise =+      -- Lazy: each element is a deferred @f i@, forced only on demand.+      let fnThunk = evaluated func+          env = Env {envBindings = Map.fromList [("__fn", fnThunk)], envParent = Nothing, envWithScopes = []}+          mkIndexThunk i = mkThunk env (EApp (EVar "__fn") (ELit (NixInt i)))+       in pure (VList (map mkIndexThunk [0 .. n - 1]))+builtinGenList _ other =+  throwEvalError ("builtins.genList: expected an integer, got " <> typeName other)++builtinSort :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinSort comparator (VList thunks) = do+  vals <- mapM force thunks+  sorted <- mergeSort comparator vals+  pure (VList (map evaluated sorted))+builtinSort _ other =+  throwEvalError ("builtins.sort: expected a list, got " <> typeName other)++-- | Stable O(n log n) merge sort using a user-supplied comparator.+-- The comparator takes two args (curried) and returns bool.+mergeSort :: (MonadEval m) => NixValue -> [NixValue] -> m [NixValue]+mergeSort _ [] = pure []+mergeSort _ [x] = pure [x]+mergeSort cmp xs = do+  let half = length xs `div` 2+      (left, right) = splitAt half xs+  sortedLeft <- mergeSort cmp left+  sortedRight <- mergeSort cmp right+  mergeSorted cmp sortedLeft sortedRight++mergeSorted :: (MonadEval m) => NixValue -> [NixValue] -> [NixValue] -> m [NixValue]+mergeSorted _ [] ys = pure ys+mergeSorted _ xs [] = pure xs+mergeSorted cmp (x : xs) (y : ys) = do+  partial <- applyValue cmp x+  result <- applyValue partial y+  case result of+    VBool True -> (x :) <$> mergeSorted cmp xs (y : ys)+    VBool False -> (y :) <$> mergeSorted cmp (x : xs) ys+    _ -> throwEvalError "builtins.sort: comparator must return a bool"++builtinConcatMap :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinConcatMap func (VList thunks) = do+  -- Semi-eager: must force each application to discover list structure for+  -- concatenation, but element thunks within those sub-lists stay lazy.+  let deferredApps = map (deferApply func) thunks+  results <- mapM force deferredApps+  concatted <- mapM extractList results+  pure (VList (concat concatted))+builtinConcatMap _ other =+  throwEvalError ("builtins.concatMap: expected a list, got " <> typeName other)++extractList :: (MonadEval m) => NixValue -> m [Thunk]+extractList (VList xs) = pure xs+extractList other =+  throwEvalError ("builtins.concatMap: function must return a list, got " <> typeName other)++builtinAny :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinAny predFn (VList thunks) = do+  result <- anyThunk predFn thunks+  pure (VBool result)+builtinAny _ other =+  throwEvalError ("builtins.any: expected a list, got " <> typeName other)++anyThunk :: (MonadEval m) => NixValue -> [Thunk] -> m Bool+anyThunk _ [] = pure False+anyThunk predFn (thunk : rest) = do+  val <- force thunk+  result <- applyValue predFn val+  case result of+    VBool True -> pure True+    VBool False -> anyThunk predFn rest+    _ -> throwEvalError "builtins.any: predicate must return a bool"++builtinAll :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinAll predFn (VList thunks) = do+  result <- allThunk predFn thunks+  pure (VBool result)+builtinAll _ other =+  throwEvalError ("builtins.all: expected a list, got " <> typeName other)++allThunk :: (MonadEval m) => NixValue -> [Thunk] -> m Bool+allThunk _ [] = pure True+allThunk predFn (thunk : rest) = do+  val <- force thunk+  result <- applyValue predFn val+  case result of+    VBool True -> allThunk predFn rest+    VBool False -> pure False+    _ -> throwEvalError "builtins.all: predicate must return a bool"++builtinElem :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinElem needle (VList thunks) = do+  found <- elemCheck needle thunks+  pure (VBool found)+builtinElem _ other =+  throwEvalError ("builtins.elem: expected a list, got " <> typeName other)++elemCheck :: (MonadEval m) => NixValue -> [Thunk] -> m Bool+elemCheck _ [] = pure False+elemCheck needle (thunk : rest) = do+  val <- force thunk+  eq <- nixEqual force needle val+  if eq then pure True else elemCheck needle rest++builtinElemAt :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinElemAt (VList thunks) (VInt idx)+  | idx < 0 = elemAtOOB idx thunks+  | otherwise = case drop (fromIntegral idx) thunks of+      (t : _) -> force t+      [] -> elemAtOOB idx thunks+  where+    elemAtOOB i ts =+      throwEvalError+        ( "builtins.elemAt: index "+            <> T.pack (show i)+            <> " out of bounds for list of length "+            <> T.pack (show (length ts))+        )+builtinElemAt (VList _) other =+  throwEvalError ("builtins.elemAt: expected an integer, got " <> typeName other)+builtinElemAt other _ =+  throwEvalError ("builtins.elemAt: expected a list, got " <> typeName other)++builtinPartition :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinPartition predFn (VList thunks) = do+  (rightThunks, wrongThunks) <- partitionThunks predFn thunks+  pure+    ( VAttrs+        ( attrSetFromMap $+            Map.fromList+              [ ("right", evaluated (VList rightThunks)),+                ("wrong", evaluated (VList wrongThunks))+              ]+        )+    )+builtinPartition _ other =+  throwEvalError ("builtins.partition: expected a list, got " <> typeName other)++-- | Tail-recursive partition with accumulator to avoid stack overflow.+partitionThunks :: (MonadEval m) => NixValue -> [Thunk] -> m ([Thunk], [Thunk])+partitionThunks predFn = go [] []+  where+    go !rs !ws [] = pure (reverse rs, reverse ws)+    go !rs !ws (thunk : rest) = do+      val <- force thunk+      result <- applyValue predFn val+      case result of+        VBool True -> go (thunk : rs) ws rest+        VBool False -> go rs (thunk : ws) rest+        _ -> throwEvalError "builtins.partition: predicate must return a bool"++builtinGroupBy :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinGroupBy func (VList thunks) = do+  groups <- groupByCollect func thunks Map.empty+  pure (VAttrs (attrSetFromMap (Map.map (evaluated . VList . reverse) groups)))+builtinGroupBy _ other =+  throwEvalError ("builtins.groupBy: expected a list, got " <> typeName other)++groupByCollect ::+  (MonadEval m) =>+  NixValue ->+  [Thunk] ->+  Map Text [Thunk] ->+  m (Map Text [Thunk])+groupByCollect _ [] acc = pure acc+groupByCollect func (thunk : rest) acc = do+  val <- force thunk+  result <- applyValue func val+  case result of+    VStr key _ ->+      groupByCollect func rest (Map.insertWith (++) key [thunk] acc)+    _ -> throwEvalError "builtins.groupBy: function must return a string"++-- ---------------------------------------------------------------------------+-- Builtin implementations — string (arity 2)+-- ---------------------------------------------------------------------------++builtinConcatStringsSep :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinConcatStringsSep (VStr sep sepCtx) (VList thunks) = do+  pairs <- mapM forceToStrCtx thunks+  let texts = map fst pairs+      mergedCtx = sepCtx <> mconcat (map snd pairs)+  pure (VStr (T.intercalate sep texts) mergedCtx)+  where+    forceToStrCtx thunk = do+      val <- force thunk+      case val of+        VStr s ctx -> pure (s, ctx)+        _ -> throwEvalError "builtins.concatStringsSep: list elements must be strings"+builtinConcatStringsSep (VStr _ _) other =+  throwEvalError ("builtins.concatStringsSep: expected a list, got " <> typeName other)+builtinConcatStringsSep other _ =+  throwEvalError ("builtins.concatStringsSep: expected a string, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin implementations — arity 3+-- ---------------------------------------------------------------------------++builtinFoldl :: (MonadEval m) => NixValue -> NixValue -> NixValue -> m NixValue+builtinFoldl op initial (VList thunks) =+  foldlStrict op initial thunks+builtinFoldl _ _ other =+  throwEvalError ("builtins.foldl': expected a list, got " <> typeName other)++-- | Strict left fold: apply @op acc elem@ for each element.+-- @op@ is curried so we call @applyValue op acc@ then @applyValue partial elem@.+foldlStrict :: (MonadEval m) => NixValue -> NixValue -> [Thunk] -> m NixValue+foldlStrict _ acc [] = pure acc+foldlStrict op acc (thunk : rest) = do+  val <- force thunk+  partial <- applyValue op acc+  stepped <- applyValue partial val+  foldlStrict op stepped rest++builtinSubstring :: (MonadEval m) => NixValue -> NixValue -> NixValue -> m NixValue+builtinSubstring (VInt start) (VInt len) (VStr s ctx) =+  let clampedStart = max 0 (fromIntegral start)+      -- Nix clamps len to available length (negative len means rest of string)+      available = T.length s - clampedStart+      clampedLen =+        if len < 0+          then available+          else min (fromIntegral len) available+   in -- Context is preserved through substring (matching real Nix).+      pure (VStr (T.take clampedLen (T.drop clampedStart s)) ctx)+builtinSubstring _ _ (VStr _ _) =+  throwEvalError "builtins.substring: start and length must be integers"+builtinSubstring _ _ other =+  throwEvalError ("builtins.substring: expected a string, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin helpers+-- ---------------------------------------------------------------------------++-- | Build a thunk that defers @f arg@ — the application only happens when+-- the thunk is forced.  Reuses the existing eval machinery via a synthetic+-- @EApp (EVar "__fn") (EVar "__arg")@ in a self-contained env.+deferApply :: NixValue -> Thunk -> Thunk+deferApply func argThunk =+  let env =+        Env+          { envBindings =+              Map.fromList+                [ ("__fn", evaluated func),+                  ("__arg", argThunk)+                ],+            envParent = Nothing,+            envWithScopes = []+          }+   in mkSyntheticThunk env (EApp (EVar "__fn") (EVar "__arg"))++-- | Permissive coercion used by @builtins.toString@.+--+-- Like 'coerceToString' but additionally handles lists: elements are+-- recursively coerced and joined with spaces, matching real Nix semantics.+-- @toString [1 2 3]@ gives @"1 2 3"@.+coerceToStringPermissive :: (MonadEval m) => NixValue -> m (Text, StringContext)+coerceToStringPermissive (VList thunks) = do+  parts <- mapM coerceThunk thunks+  let texts = map fst parts+      ctx = mconcat (map snd parts)+  pure (T.intercalate " " texts, ctx)+  where+    coerceThunk thunk = do+      val <- force thunk+      coerceToStringPermissive val+coerceToStringPermissive other = coerceToString other++-- | The current system platform string.+currentSystemStr :: Text+currentSystemStr = case (System.Info.arch, System.Info.os) of+  ("x86_64", "mingw32") -> "x86_64-windows"+  ("x86_64", "darwin") -> "x86_64-darwin"+  ("aarch64", "darwin") -> "aarch64-darwin"+  ("aarch64", "linux") -> "aarch64-linux"+  ("x86_64", "linux") -> "x86_64-linux"+  (arch, os) -> T.pack arch <> "-" <> T.pack os++-- | Store dir with trailing slash, for building store paths.+storeDirPrefix :: Text+storeDirPrefix = defaultStoreDirText <> "/"++-- ---------------------------------------------------------------------------+-- Builtin implementations — numeric + context+-- ---------------------------------------------------------------------------++isPathVal :: NixValue -> Bool+isPathVal (VPath _) = True+isPathVal _ = False++builtinCeil :: (MonadEval m) => NixValue -> m NixValue+builtinCeil (VFloat f) = pure (VInt (ceiling f))+builtinCeil (VInt n) = pure (VInt n)+builtinCeil other = throwEvalError ("builtins.ceil: expected a number, got " <> typeName other)++builtinFloor :: (MonadEval m) => NixValue -> m NixValue+builtinFloor (VFloat f) = pure (VInt (floor f))+builtinFloor (VInt n) = pure (VInt n)+builtinFloor other = throwEvalError ("builtins.floor: expected a number, got " <> typeName other)++builtinDiscardContext :: (MonadEval m) => NixValue -> m NixValue+builtinDiscardContext (VStr s _) = pure (mkStr s)+builtinDiscardContext other =+  throwEvalError ("builtins.unsafeDiscardStringContext: expected a string, got " <> typeName other)++-- | Strip only derivation output dependencies (SCDrvOutput, SCAllOutputs),+-- keeping plain store path references (SCPlain).+builtinDiscardOutputDep :: (MonadEval m) => NixValue -> m NixValue+builtinDiscardOutputDep (VStr s (StringContext ctx)) =+  let kept = Set.filter isPlain ctx+   in pure (VStr s (StringContext kept))+  where+    isPlain (SCPlain _) = True+    isPlain _ = False+builtinDiscardOutputDep other =+  throwEvalError ("builtins.unsafeDiscardOutputDependency: expected a string, got " <> typeName other)++-- | Check whether a string has any context elements.+builtinHasContext :: (MonadEval m) => NixValue -> m NixValue+builtinHasContext (VStr _ ctx) = pure (VBool (ctx /= emptyContext))+builtinHasContext other =+  throwEvalError ("builtins.hasContext: expected a string, got " <> typeName other)++-- | Return the context of a string as an attrset.+--+-- Each key is a store path string.  Each value is an attrset with:+--   - @path@: true if there's a SCPlain reference+--   - @allOutputs@: true if there's a SCAllOutputs reference+--   - @outputs@: list of output names from SCDrvOutput references+builtinGetContext :: (MonadEval m) => NixValue -> m NixValue+builtinGetContext (VStr _ (StringContext ctx)) = do+  let grouped = groupContextByPath (Set.toList ctx)+      attrMap = Map.map contextEntryToAttrs grouped+  pure (VAttrs (attrSetFromMap attrMap))+builtinGetContext other =+  throwEvalError ("builtins.getContext: expected a string, got " <> typeName other)++-- | Intermediate representation for grouping context elements by store path.+data ContextEntry = ContextEntry+  { cePath :: !Bool,+    ceAllOutputs :: !Bool,+    ceOutputs :: ![Text]+  }++-- | Group context elements by their store path.+groupContextByPath :: [StringContextElement] -> Map Text ContextEntry+groupContextByPath = foldl' addElement Map.empty+  where+    addElement acc (SCPlain sp) =+      Map.insertWith mergeEntry (spToText sp) (ContextEntry True False []) acc+    addElement acc (SCDrvOutput sp outName) =+      Map.insertWith mergeEntry (spToText sp) (ContextEntry False False [outName]) acc+    addElement acc (SCAllOutputs sp) =+      Map.insertWith mergeEntry (spToText sp) (ContextEntry False True []) acc+    mergeEntry new old =+      ContextEntry+        (cePath new || cePath old)+        (ceAllOutputs new || ceAllOutputs old)+        (ceOutputs new ++ ceOutputs old)+    spToText sp =+      T.pack (storePathToFilePath defaultStoreDir sp)++-- | Convert a ContextEntry to an attrset thunk.+contextEntryToAttrs :: ContextEntry -> Thunk+contextEntryToAttrs entry =+  let fields =+        [("path", evaluated (VBool True)) | cePath entry]+          ++ [("allOutputs", evaluated (VBool True)) | ceAllOutputs entry]+          ++ [("outputs", evaluated (VList [evaluated (mkStr o) | o <- ceOutputs entry])) | not (null (ceOutputs entry))]+   in evaluated (VAttrs (attrSetFromMap (Map.fromList fields)))++-- | Append context entries to a string from an attrset.+--+-- @builtins.appendContext string contextAttrset@ adds the specified+-- context elements to the string.+builtinAppendContext :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinAppendContext (VStr s ctx) (VAttrs contextAttrs) = do+  newCtx <- parseContextAttrs (attrSetToMap contextAttrs)+  pure (VStr s (ctx <> newCtx))+builtinAppendContext (VStr _ _) other =+  throwEvalError ("builtins.appendContext: second argument must be a set, got " <> typeName other)+builtinAppendContext other _ =+  throwEvalError ("builtins.appendContext: first argument must be a string, got " <> typeName other)++-- | Parse a context attrset into a StringContext.+-- Each key is a store path; each value is an attrset with optional+-- @path@, @allOutputs@, and @outputs@ fields.+parseContextAttrs :: (MonadEval m) => Map Text Thunk -> m StringContext+parseContextAttrs attrs = do+  elements <- mapM parseOneCtx (Map.toList attrs)+  pure (StringContext (Set.fromList (concat elements)))+  where+    parseOneCtx (pathText, thunk) = do+      val <- force thunk+      case val of+        VAttrs inner -> do+          let sp = case parseStorePath defaultStoreDir pathText of+                Just parsed -> parsed+                Nothing -> StorePath (T.take 32 (T.drop (T.length storeDirPrefix) pathText)) (T.drop 33 (T.drop (T.length storeDirPrefix) pathText))+          hasPath <- getBoolAttr "path" inner+          hasAllOuts <- getBoolAttr "allOutputs" inner+          outNames <- getOutputsList inner+          let pathElems = [SCPlain sp | hasPath]+              allOutElems = [SCAllOutputs sp | hasAllOuts]+              outElems = [SCDrvOutput sp o | o <- outNames]+          pure (pathElems ++ allOutElems ++ outElems)+        _ -> throwEvalError "builtins.appendContext: context entry must be a set"++    getBoolAttr key attrs' = case attrSetLookup key attrs' of+      Nothing -> pure False+      Just thunk -> do+        val <- force thunk+        case val of+          VBool b -> pure b+          _ -> pure False++    getOutputsList attrs' = case attrSetLookup "outputs" attrs' of+      Nothing -> pure []+      Just thunk -> do+        val <- force thunk+        case val of+          VList thunks -> mapM forceToOutputName thunks+          _ -> pure []++    forceToOutputName thunk = do+      val <- force thunk+      case val of+        VStr s _ -> pure s+        _ -> throwEvalError "builtins.appendContext: output name must be a string"++builtinBaseNameOf :: (MonadEval m) => NixValue -> m NixValue+builtinBaseNameOf (VStr s ctx) = pure (VStr (lastComponent s) ctx)+builtinBaseNameOf (VPath p) = pure (mkStr (lastComponent p))+builtinBaseNameOf other =+  throwEvalError ("builtins.baseNameOf: expected a string or path, got " <> typeName other)++lastComponent :: Text -> Text+lastComponent t = case T.splitOn "/" t of+  [] -> t+  parts -> case reverse (filter (not . T.null) parts) of+    [] -> ""+    (final : _) -> final++builtinDirOf :: (MonadEval m) => NixValue -> m NixValue+builtinDirOf (VStr s ctx) = pure (VStr (dirComponent s) ctx)+builtinDirOf (VPath p) = pure (VPath (dirComponent p))+builtinDirOf other =+  throwEvalError ("builtins.dirOf: expected a string or path, got " <> typeName other)++dirComponent :: Text -> Text+dirComponent t =+  let idx = T.findIndex (== '/') (T.reverse t)+   in case idx of+        Nothing -> "."+        Just n -> T.take (T.length t - n - 1) t++builtinConcatLists :: (MonadEval m) => NixValue -> m NixValue+builtinConcatLists (VList thunks) = do+  sublists <- mapM forceThenExtractList thunks+  pure (VList (concat sublists))+  where+    forceThenExtractList thunk = do+      val <- force thunk+      case val of+        VList xs -> pure xs+        _ -> throwEvalError "builtins.concatLists: element must be a list"+builtinConcatLists other =+  throwEvalError ("builtins.concatLists: expected a list, got " <> typeName other)++builtinLessThan :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinLessThan a b = VBool <$> nixCompare a b++-- ---------------------------------------------------------------------------+-- Builtin implementations — arithmetic + bitwise+-- ---------------------------------------------------------------------------++builtinAdd :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinAdd (VInt a) (VInt b) = pure (VInt (a + b))+builtinAdd (VInt a) (VFloat b) = pure (VFloat (fromInteger a + b))+builtinAdd (VFloat a) (VInt b) = pure (VFloat (a + fromInteger b))+builtinAdd (VFloat a) (VFloat b) = pure (VFloat (a + b))+builtinAdd l r = throwEvalError ("builtins.add: expected numbers, got " <> typeName l <> " and " <> typeName r)++builtinSub :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinSub (VInt a) (VInt b) = pure (VInt (a - b))+builtinSub (VInt a) (VFloat b) = pure (VFloat (fromInteger a - b))+builtinSub (VFloat a) (VInt b) = pure (VFloat (a - fromInteger b))+builtinSub (VFloat a) (VFloat b) = pure (VFloat (a - b))+builtinSub l r = throwEvalError ("builtins.sub: expected numbers, got " <> typeName l <> " and " <> typeName r)++builtinMul :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinMul (VInt a) (VInt b) = pure (VInt (a * b))+builtinMul (VInt a) (VFloat b) = pure (VFloat (fromInteger a * b))+builtinMul (VFloat a) (VInt b) = pure (VFloat (a * fromInteger b))+builtinMul (VFloat a) (VFloat b) = pure (VFloat (a * b))+builtinMul l r = throwEvalError ("builtins.mul: expected numbers, got " <> typeName l <> " and " <> typeName r)++builtinDiv :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinDiv _ (VInt 0) = throwEvalError "builtins.div: division by zero"+builtinDiv (VInt a) (VInt b) = pure (VInt (quot a b))+builtinDiv _ (VFloat 0) = throwEvalError "builtins.div: division by zero"+builtinDiv (VInt a) (VFloat b) = pure (VFloat (fromInteger a / b))+builtinDiv (VFloat a) (VInt b) = pure (VFloat (a / fromInteger b))+builtinDiv (VFloat a) (VFloat b) = pure (VFloat (a / b))+builtinDiv l r = throwEvalError ("builtins.div: expected numbers, got " <> typeName l <> " and " <> typeName r)++builtinMod :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinMod _ (VInt 0) = throwEvalError "builtins.mod: division by zero"+builtinMod (VInt a) (VInt b) = pure (VInt (rem a b))+builtinMod l r = throwEvalError ("builtins.mod: expected two integers, got " <> typeName l <> " and " <> typeName r)++builtinMin :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinMin a b = do+  aIsLess <- nixCompare a b+  pure (if aIsLess then a else b)++builtinMax :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinMax a b = do+  aIsLess <- nixCompare a b+  pure (if aIsLess then b else a)++builtinBitAnd :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinBitAnd (VInt a) (VInt b) = pure (VInt (a .&. b))+builtinBitAnd _ _ = throwEvalError "builtins.bitAnd: expected two integers"++builtinBitOr :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinBitOr (VInt a) (VInt b) = pure (VInt (a .|. b))+builtinBitOr _ _ = throwEvalError "builtins.bitOr: expected two integers"++builtinBitXor :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinBitXor (VInt a) (VInt b) = pure (VInt (xor a b))+builtinBitXor _ _ = throwEvalError "builtins.bitXor: expected two integers"++-- ---------------------------------------------------------------------------+-- Builtin implementations — attr set higher-order+-- ---------------------------------------------------------------------------++builtinMapAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinMapAttrs func (VAttrs attrs) =+  -- Lazy: each attr value is a deferred @f key val@, forced only on demand.+  pure (VAttrs (attrSetMapWithKey deferAttr attrs))+  where+    deferAttr key valThunk =+      let env =+            Env+              { envBindings =+                  Map.fromList+                    [ ("__fn", evaluated func),+                      ("__key", evaluated (mkStr key)),+                      ("__val", valThunk)+                    ],+                envParent = Nothing,+                envWithScopes = []+              }+       in mkSyntheticThunk env (EApp (EApp (EVar "__fn") (EVar "__key")) (EVar "__val"))+builtinMapAttrs _ other =+  throwEvalError ("builtins.mapAttrs: expected a set, got " <> typeName other)++builtinFunctionArgs :: (MonadEval m) => NixValue -> m NixValue+builtinFunctionArgs (VLambda _ formals _) = pure (formalsToAttrs formals)+builtinFunctionArgs (VBuiltin _ _) = pure (VAttrs (attrSetFromMap Map.empty))+-- Callable sets with __functionArgs metadata (from setFunctionArgs).+builtinFunctionArgs (VAttrs attrs)+  | Just faThunk <- attrSetLookup "__functionArgs" attrs = force faThunk+builtinFunctionArgs other =+  throwEvalError ("builtins.functionArgs: expected a function, got " <> typeName other)++formalsToAttrs :: Formals -> NixValue+formalsToAttrs (FormalName _) = VAttrs (attrSetFromMap Map.empty)+formalsToAttrs (FormalSet formals _) = formalsListToAttrs formals+formalsToAttrs (FormalNamedSet _ formals _) = formalsListToAttrs formals++formalsListToAttrs :: [Formal] -> NixValue+formalsListToAttrs formals =+  VAttrs $+    attrSetFromMap $+      Map.fromList+        [(fName f, evaluated (VBool (isJust (fDefault f)))) | f <- formals]++-- | @builtins.setFunctionArgs f args@ — wraps @f@ in a callable attrset+-- with @__functor@ (so it remains callable) and @__functionArgs@ metadata.+-- Used by nixpkgs @lib.mirrorFunctionArgs@ / @lib.makeOverridable@.+--+-- @__functor@ is @self: f@ — a lambda that ignores @self@ and returns+-- the original function.  The function is captured in the closure env.+builtinSetFunctionArgs :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinSetFunctionArgs func (VAttrs argSpec) =+  let closureEnv = envInsert "__fn" func emptyEnv+      -- __functor = self: __fn  (ignores self, returns the original function)+      functorLambda = VLambda closureEnv (FormalName "_self") (EVar "__fn")+   in pure $+        VAttrs $+          attrSetFromMap $+            Map.fromList+              [ ("__functor", evaluated functorLambda),+                ("__functionArgs", evaluated (VAttrs argSpec))+              ]+builtinSetFunctionArgs func args =+  throwEvalError ("builtins.setFunctionArgs: expected a set as second argument, got " <> typeName args <> " (func: " <> typeName func <> ")")++builtinZipAttrsWith :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinZipAttrsWith func (VList thunks) = do+  attrSets <- mapM forceToAttrSet thunks+  let merged = mergeAllAttrs attrSets+  resultPairs <- mapM (applyZip func) (Map.toList merged)+  pure (VAttrs (attrSetFromMap (Map.fromList resultPairs)))+  where+    forceToAttrSet thunk = do+      val <- force thunk+      case val of+        VAttrs attrs -> pure (attrSetToMap attrs)+        _ -> throwEvalError "builtins.zipAttrsWith: list element must be a set"+    mergeAllAttrs = foldl' (\acc m -> Map.unionWith (++) acc (Map.map (: []) m)) Map.empty+    applyZip fn (key, thunkList) = do+      partial <- applyValue fn (mkStr key)+      result <- applyValue partial (VList thunkList)+      pure (key, evaluated result)+builtinZipAttrsWith _ other =+  throwEvalError ("builtins.zipAttrsWith: expected a list, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin implementations — string manipulation+-- ---------------------------------------------------------------------------++builtinReplaceStrings ::+  (MonadEval m) => NixValue -> NixValue -> NixValue -> m NixValue+builtinReplaceStrings (VList fromThunks) (VList toThunks) (VStr input inputCtx) = do+  froms <- mapM forceStr fromThunks+  toStrs <- mapM forceStr toThunks+  when (length froms /= length toStrs) $+    throwEvalError "builtins.replaceStrings: 'from' and 'to' must have the same length"+  let fromTexts = map fst froms+      toTexts = map fst toStrs+      -- Merge contexts: input context + all 'to' string contexts + all 'from' string contexts+      mergedCtx = inputCtx <> mconcat (map snd froms) <> mconcat (map snd toStrs)+      pairs = zip fromTexts toTexts+  pure (VStr (replaceAll pairs input) mergedCtx)+  where+    forceStr thunk = do+      val <- force thunk+      case val of+        VStr s ctx -> pure (s, ctx)+        _ -> throwEvalError "builtins.replaceStrings: elements must be strings"+builtinReplaceStrings _ _ (VStr _ _) =+  throwEvalError "builtins.replaceStrings: first two arguments must be lists"+builtinReplaceStrings _ _ other =+  throwEvalError ("builtins.replaceStrings: expected a string, got " <> typeName other)++-- | Replace all occurrences, O(n) via chunk list + T.concat.+replaceAll :: [(Text, Text)] -> Text -> Text+replaceAll pairs input = T.concat (go input)+  where+    go remaining+      | T.null remaining =+          -- At end of string, still check for empty-from match+          case findMatch pairs remaining of+            Just (replacement, _, _) -> [replacement]+            Nothing -> []+      | otherwise = case findMatch pairs remaining of+          Just (replacement, rest, matched) ->+            if T.null matched+              then case T.uncons remaining of+                -- empty-from: insert replacement then advance 1 char+                Just (ch, after) -> replacement : T.singleton ch : go after+                Nothing -> [replacement]+              else replacement : go rest+          Nothing -> case T.uncons remaining of+            Just (ch, after) -> T.singleton ch : go after+            Nothing -> []+    findMatch [] _ = Nothing+    findMatch ((from, to) : rest) txt+      | T.null from = Just (to, txt, from)+      | Just suffix <- T.stripPrefix from txt = Just (to, suffix, from)+      | otherwise = findMatch rest txt++-- ---------------------------------------------------------------------------+-- Builtin implementations — regex (POSIX ERE via regex-tdfa)+-- ---------------------------------------------------------------------------++-- | @builtins.match regex str@: match a POSIX ERE against a string.+-- The regex is implicitly anchored (must match the entire string).+-- Returns @null@ if no match, or a list of capture group strings+-- (empty string for unmatched optional groups).+builtinMatch :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinMatch (VStr regex _) (VStr str _) = do+  let anchored = "^" <> T.unpack regex <> "$"+  case RE.makeRegexM anchored :: Maybe RE.Regex of+    Nothing ->+      throwEvalError ("builtins.match: invalid regex: " <> regex)+    Just compiled ->+      let matches = matchAllText compiled (T.unpack str)+       in case matches of+            [] -> pure VNull+            (match : _) ->+              -- match is an Array of (String, (offset, len)) pairs.+              -- Index 0 is the full match; indices 1.. are capture groups.+              let groups = Array.elems match+                  -- Skip index 0 (full match) — return only capture groups.+                  captureGroups = drop 1 groups+                  toThunk (s, _) = evaluated (mkStr (T.pack s))+               in pure (VList (map toThunk captureGroups))+builtinMatch (VStr _ _) other =+  throwEvalError ("builtins.match: expected a string, got " <> typeName other)+builtinMatch other _ =+  throwEvalError ("builtins.match: expected a string (regex), got " <> typeName other)++-- | @builtins.split regex str@: split a string by a POSIX ERE.+-- Returns an alternating list of non-matched strings and match-group lists.+-- Example: @split "(x)" "axbxc"@ → @["a" ["x"] "b" ["x"] "c"]@+builtinSplit :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinSplit (VStr regex _) (VStr str _) = do+  case RE.makeRegexM (T.unpack regex) :: Maybe RE.Regex of+    Nothing ->+      throwEvalError ("builtins.split: invalid regex: " <> regex)+    Just compiled ->+      let allMatches = matchAllText compiled (T.unpack str)+          strText = T.unpack str+          result = buildSplitResult strText 0 allMatches+       in pure (VList result)+builtinSplit (VStr _ _) other =+  throwEvalError ("builtins.split: expected a string, got " <> typeName other)+builtinSplit other _ =+  throwEvalError ("builtins.split: expected a string (regex), got " <> typeName other)++-- | Build the alternating list for builtins.split.+buildSplitResult :: String -> Int -> [Array.Array Int (String, (Int, Int))] -> [Thunk]+buildSplitResult remaining pos [] =+  -- No more matches — emit the rest of the string.+  [evaluated (mkStr (T.pack (drop pos remaining)))]+buildSplitResult remaining pos (match : rest) =+  let elems = Array.elems match+      (_, (matchStart, matchLen)) = case elems of+        (full : _) -> full+        [] -> ("", (pos, 0)) -- defensive: should not happen from matchAllText+        -- Text before this match+      before = T.pack (take (matchStart - pos) (drop pos remaining))+      -- Capture groups (indices 1..)+      groups = drop 1 (Array.elems match)+      groupThunks = map (\(s, _) -> evaluated (mkStr (T.pack s))) groups+      -- Continue after this match+      afterPos = matchStart + matchLen+   in evaluated (mkStr before)+        : evaluated (VList groupThunks)+        : buildSplitResult remaining afterPos rest++builtinCompareVersions :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinCompareVersions (VStr a _) (VStr b _) =+  pure (VInt (compareVersionParts (splitVersionStr a) (splitVersionStr b)))+builtinCompareVersions _ _ = throwEvalError "builtins.compareVersions: expected two strings"++compareVersionParts :: [Text] -> [Text] -> Integer+compareVersionParts [] [] = 0+compareVersionParts [] (_ : _) = -1+compareVersionParts (_ : _) [] = 1+compareVersionParts (a : as) (b : bs) =+  case compareComponent a b of+    0 -> compareVersionParts as bs+    n -> n++compareComponent :: Text -> Text -> Integer+compareComponent a b+  | a == b = 0+  | allDigits a && allDigits b = compare' (readInt a) (readInt b)+  | a == "" = -1+  | b == "" = 1+  | otherwise = if a < b then -1 else 1+  where+    allDigits t = not (T.null t) && T.all isDigit t+    readInt :: Text -> Integer+    readInt = T.foldl' (\acc c -> acc * 10 + fromIntegral (digitToInt c)) (0 :: Integer)+    compare' x y+      | x < y = -1+      | x > y = 1+      | otherwise = 0++splitVersionStr :: Text -> [Text]+splitVersionStr t+  | T.null t = []+  | otherwise =+      let (component, rest) = spanComponent t+       in component : splitVersionAfterComponent rest++splitVersionAfterComponent :: Text -> [Text]+splitVersionAfterComponent t = case T.uncons t of+  Nothing -> []+  Just ('.', rest) -> splitVersionStr rest+  Just _ -> splitVersionStr t++spanComponent :: Text -> (Text, Text)+spanComponent t = case T.uncons t of+  Nothing -> ("", "")+  Just (c, rest)+    | isDigit c -> T.span isDigit t+    | isAlpha c -> T.span isAlpha t+    | otherwise -> (T.singleton c, rest)++builtinSplitVersion :: (MonadEval m) => NixValue -> m NixValue+builtinSplitVersion (VStr s _) =+  pure (VList (map (evaluated . mkStr) (splitVersionComponents s)))+builtinSplitVersion other =+  throwEvalError ("builtins.splitVersion: expected a string, got " <> typeName other)++splitVersionComponents :: Text -> [Text]+splitVersionComponents t = case T.uncons t of+  Nothing -> []+  Just ('.', rest) -> splitVersionComponents rest+  Just (c, _)+    | isDigit c ->+        let (digits, rest) = T.span isDigit t+         in digits : splitVersionComponents rest+    | otherwise ->+        let (alpha, rest) = T.span isAlpha t+         in alpha : splitVersionComponents rest++builtinParseDrvName :: (MonadEval m) => NixValue -> m NixValue+builtinParseDrvName (VStr s _) =+  let (name, version) = parseName s+   in pure+        ( VAttrs+            ( attrSetFromMap $+                Map.fromList+                  [ ("name", evaluated (mkStr name)),+                    ("version", evaluated (mkStr version))+                  ]+            )+        )+builtinParseDrvName other =+  throwEvalError ("builtins.parseDrvName: expected a string, got " <> typeName other)++parseName :: Text -> (Text, Text)+parseName t =+  case findVersionDash t 0 of+    Nothing -> (t, "")+    Just idx -> (T.take idx t, T.drop (idx + 1) t)++findVersionDash :: Text -> Int -> Maybe Int+findVersionDash t idx = case T.uncons (T.drop idx t) of+  Nothing -> Nothing+  Just ('-', after)+    | Just (d, _) <- T.uncons after,+      isDigit d ->+        Just idx+  Just _ -> findVersionDash t (idx + 1)++-- ---------------------------------------------------------------------------+-- Builtin implementations — serialization + hashing+-- ---------------------------------------------------------------------------++builtinToJSON :: (MonadEval m) => NixValue -> m NixValue+builtinToJSON val = mkStr <$> valueToJSON val++valueToJSON :: (MonadEval m) => NixValue -> m Text+valueToJSON VNull = pure "null"+valueToJSON (VBool True) = pure "true"+valueToJSON (VBool False) = pure "false"+valueToJSON (VInt n) = pure (T.pack (show n))+valueToJSON (VFloat f) =+  let s = show f+   in -- Nix outputs 1e+40 style, Haskell outputs 1.0e40 style+      -- For simple cases just use show+      pure (T.pack s)+valueToJSON (VStr s _) = pure (jsonEscapeString s)+valueToJSON (VList thunks) = do+  vals <- mapM force thunks+  jsonVals <- mapM valueToJSON vals+  pure ("[" <> T.intercalate "," jsonVals <> "]")+valueToJSON (VAttrs attrs) = do+  let m = attrSetToMap attrs+      sortedKeys = Map.keys m+  pairs <- mapM (jsonPair m) sortedKeys+  pure ("{" <> T.intercalate "," pairs <> "}")+  where+    jsonPair attrMap key = case Map.lookup key attrMap of+      Nothing -> pure ""+      Just thunk -> do+        val <- force thunk+        jsonVal <- valueToJSON val+        pure (jsonEscapeString key <> ":" <> jsonVal)+valueToJSON (VPath p) = pure (jsonEscapeString p)+valueToJSON (VLambda {}) = throwEvalError "builtins.toJSON: cannot convert a function to JSON"+valueToJSON (VBuiltin _ _) = throwEvalError "builtins.toJSON: cannot convert a function to JSON"+valueToJSON (VDerivation _) = throwEvalError "builtins.toJSON: cannot convert a derivation to JSON"++jsonEscapeString :: Text -> Text+jsonEscapeString s = "\"" <> T.concatMap escapeChar s <> "\""+  where+    escapeChar '"' = "\\\""+    escapeChar '\\' = "\\\\"+    escapeChar '\n' = "\\n"+    escapeChar '\r' = "\\r"+    escapeChar '\t' = "\\t"+    escapeChar c+      | ord c < 0x20 = "\\u" <> T.pack (padHex 4 (showHex' (ord c)))+      | otherwise = T.singleton c+    padHex n str = replicate (n - length str) '0' ++ str+    showHex' 0 = "0"+    showHex' num = go num ""+      where+        go 0 acc = acc+        go v acc =+          let (q, r) = quotRem v 16+           in go q (hexDigit r : acc)++-- | Safe hex digit lookup (total for 0–15).+hexDigit :: Int -> Char+hexDigit n+  | n >= 0 && n <= 9 = chr (ord '0' + n)+  | n >= 10 && n <= 15 = chr (ord 'a' + n - 10)+  | otherwise = '?' -- unreachable for valid hex++builtinFromJSON :: (MonadEval m) => NixValue -> m NixValue+builtinFromJSON (VStr s _) = case parseJSON (T.strip s) of+  Just (val, rest)+    | T.null (T.strip rest) -> pure val+    | otherwise -> throwEvalError "builtins.fromJSON: trailing content after JSON value"+  Nothing -> throwEvalError "builtins.fromJSON: invalid JSON"+builtinFromJSON other =+  throwEvalError ("builtins.fromJSON: expected a string, got " <> typeName other)++parseJSON :: Text -> Maybe (NixValue, Text)+parseJSON t = case T.uncons (T.stripStart t) of+  Nothing -> Nothing+  Just ('n', rest)+    | Just suffix <- T.stripPrefix "ull" rest -> Just (VNull, suffix)+  Just ('t', rest)+    | Just suffix <- T.stripPrefix "rue" rest -> Just (VBool True, suffix)+  Just ('f', rest)+    | Just suffix <- T.stripPrefix "alse" rest -> Just (VBool False, suffix)+  Just ('"', _) -> parseJSONString (T.stripStart t)+  Just ('[', rest) -> parseJSONArray rest+  Just ('{', rest) -> parseJSONObject rest+  Just (c, _)+    | c == '-' || isDigit c -> parseJSONNumber (T.stripStart t)+  _ -> Nothing++parseJSONString :: Text -> Maybe (NixValue, Text)+parseJSONString t = case T.uncons t of+  Just ('"', rest) ->+    let (strVal, remaining) = parseJSONStringContent rest+     in Just (mkStr strVal, remaining)+  _ -> Nothing++-- | Parse JSON string content, O(n) via chunk list + T.concat.+parseJSONStringContent :: Text -> (Text, Text)+parseJSONStringContent = go []+  where+    go !chunks t = case T.uncons t of+      Nothing -> (T.concat (reverse chunks), "")+      Just ('"', rest) -> (T.concat (reverse chunks), rest)+      Just ('\\', rest) -> case T.uncons rest of+        Just ('"', r) -> go ("\"" : chunks) r+        Just ('\\', r) -> go ("\\" : chunks) r+        Just ('/', r) -> go ("/" : chunks) r+        Just ('n', r) -> go ("\n" : chunks) r+        Just ('r', r) -> go ("\r" : chunks) r+        Just ('t', r) -> go ("\t" : chunks) r+        Just ('u', r) -> case parseHex4 r of+          Just (codepoint, r2) ->+            go (T.singleton (chr codepoint) : chunks) r2+          Nothing -> go ("u" : chunks) r+        _ -> (T.concat (reverse chunks), rest)+      Just (c, rest) -> go (T.singleton c : chunks) rest++parseHex4 :: Text -> Maybe (Int, Text)+parseHex4 t+  | T.length t >= 4 =+      let hex = T.take 4 t+       in if T.all isHexDigit hex+            then Just (readHex4 hex, T.drop 4 t)+            else Nothing+  | otherwise = Nothing++readHex4 :: Text -> Int+readHex4 = T.foldl' (\acc c -> acc * 16 + digitToInt c) 0++parseJSONNumber :: Text -> Maybe (NixValue, Text)+parseJSONNumber t =+  let (numStr, rest) = T.span (\c -> isDigit c || c == '.' || c == '-' || c == 'e' || c == 'E' || c == '+') t+   in if T.null numStr+        then Nothing+        else+          if T.any (\c -> c == '.' || c == 'e' || c == 'E') numStr+            then case reads (T.unpack numStr) :: [(Double, String)] of+              [(d, "")] -> Just (VFloat d, rest)+              _ -> Nothing+            else case reads (T.unpack numStr) :: [(Integer, String)] of+              [(n, "")] -> Just (VInt n, rest)+              _ -> Nothing++parseJSONArray :: Text -> Maybe (NixValue, Text)+parseJSONArray t = parseJSONArrayElements (T.stripStart t) []++parseJSONArrayElements :: Text -> [Thunk] -> Maybe (NixValue, Text)+parseJSONArrayElements t acc = case T.uncons (T.stripStart t) of+  Just (']', rest) -> Just (VList (reverse acc), rest)+  _ -> case parseJSON t of+    Just (val, rest) ->+      let stripped = T.stripStart rest+       in case T.uncons stripped of+            Just (',', rest2) -> parseJSONArrayElements rest2 (evaluated val : acc)+            Just (']', rest2) -> Just (VList (reverse (evaluated val : acc)), rest2)+            _ -> Nothing+    Nothing -> Nothing++parseJSONObject :: Text -> Maybe (NixValue, Text)+parseJSONObject t = parseJSONObjectEntries (T.stripStart t) Map.empty++parseJSONObjectEntries :: Text -> Map Text Thunk -> Maybe (NixValue, Text)+parseJSONObjectEntries t acc = case T.uncons (T.stripStart t) of+  Just ('}', rest) -> Just (VAttrs (attrSetFromMap acc), rest)+  _ -> case parseJSONString (T.stripStart t) of+    Just (VStr key _, rest) -> case T.uncons (T.stripStart rest) of+      Just (':', rest2) -> case parseJSON rest2 of+        Just (val, rest3) ->+          let stripped = T.stripStart rest3+              updated = Map.insert key (evaluated val) acc+           in case T.uncons stripped of+                Just (',', rest4) -> parseJSONObjectEntries rest4 updated+                Just ('}', rest4) -> Just (VAttrs (attrSetFromMap updated), rest4)+                _ -> Nothing+        Nothing -> Nothing+      _ -> Nothing+    _ -> Nothing++builtinHashString :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinHashString (VStr algo _) (VStr input _) =+  hashBytesWithAlgo "hashString" algo (TE.encodeUtf8 input)+builtinHashString (VStr _ _) other =+  throwEvalError ("builtins.hashString: expected a string, got " <> typeName other)+builtinHashString other _ =+  throwEvalError ("builtins.hashString: expected a string, got " <> typeName other)++digestToHex :: (BA.ByteArrayAccess a) => a -> Text+digestToHex digest =+  let bytes = BA.unpack digest+   in T.pack (concatMap byteToHex bytes)++-- ---------------------------------------------------------------------------+-- Builtin implementations — deep evaluation+-- ---------------------------------------------------------------------------++builtinDeepSeq :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinDeepSeq first second = do+  deepForce first+  pure second++deepForce :: (MonadEval m) => NixValue -> m ()+deepForce (VList thunks) = mapM_ (force >=> deepForce) thunks+deepForce (VAttrs attrs) = mapM_ (force >=> deepForce) (attrSetElems attrs)+deepForce _ = pure ()++-- ---------------------------------------------------------------------------+-- Builtin implementations — graph traversal+-- ---------------------------------------------------------------------------++builtinGenericClosure :: (MonadEval m) => NixValue -> m NixValue+builtinGenericClosure (VAttrs attrs) = do+  startSetThunk <-+    maybe (throwEvalError "builtins.genericClosure: missing 'startSet'") pure $+      attrSetLookup "startSet" attrs+  operatorThunk <-+    maybe (throwEvalError "builtins.genericClosure: missing 'operator'") pure $+      attrSetLookup "operator" attrs+  startSetVal <- force startSetThunk+  operatorVal <- force operatorThunk+  case startSetVal of+    VList items -> do+      result <- closureLoop operatorVal (Seq.fromList items) [] []+      pure (VList (map evaluated result))+    _ -> throwEvalError "builtins.genericClosure: 'startSet' must be a list"+builtinGenericClosure other =+  throwEvalError ("builtins.genericClosure: expected a set, got " <> typeName other)++-- | BFS loop for genericClosure.  Uses Data.Sequence for O(1) queue+-- append (the old list-based version was O(n) per operator call).+-- seenKeys is still a linear scan — Nix value equality is monadic so+-- Set/HashMap is not directly applicable without specialising on key type.+closureLoop ::+  (MonadEval m) =>+  NixValue ->+  Seq Thunk ->+  [NixValue] ->+  [NixValue] ->+  m [NixValue]+closureLoop _ Empty _ acc = pure (reverse acc)+closureLoop operator (thunk :<| rest) seenKeys acc = do+  item <- force thunk+  key <- extractKey item+  alreadySeen <- keyInList key seenKeys+  if alreadySeen+    then closureLoop operator rest seenKeys acc+    else do+      newItems <- applyValue operator item+      case newItems of+        VList newThunks ->+          closureLoop operator (rest <> Seq.fromList newThunks) (key : seenKeys) (item : acc)+        _ -> throwEvalError "builtins.genericClosure: operator must return a list"++extractKey :: (MonadEval m) => NixValue -> m NixValue+extractKey (VAttrs attrs) =+  case attrSetLookup "key" attrs of+    Just thunk -> force thunk+    Nothing -> throwEvalError "builtins.genericClosure: item missing 'key' attribute"+extractKey _ = throwEvalError "builtins.genericClosure: item must be a set with 'key'"++keyInList :: (MonadEval m) => NixValue -> [NixValue] -> m Bool+keyInList _ [] = pure False+keyInList key (seen : rest) = do+  eq <- nixEqual force key seen+  if eq then pure True else keyInList key rest++-- ---------------------------------------------------------------------------+-- IO builtins (delegate to MonadEval methods)+-- ---------------------------------------------------------------------------++-- | Coerce a value to a path 'Text'.  Accepts 'VPath' and 'VStr';+-- throws a type error for anything else.+coerceToPath :: (MonadEval m) => Text -> NixValue -> m Text+coerceToPath _ (VPath p) = pure p+coerceToPath _ (VStr s _) = pure s+coerceToPath name other =+  throwEvalError ("builtins." <> name <> ": expected a path or string, got " <> typeName other)++builtinImport :: (MonadEval m) => NixValue -> m NixValue+builtinImport (VPath p) = importFile p+builtinImport other =+  throwEvalError ("import: expected a path, got " <> typeName other)++builtinReadFile :: (MonadEval m) => NixValue -> m NixValue+builtinReadFile val = do+  p <- coerceToPath "readFile" val+  mkStr <$> readFileText p++builtinPathExists :: (MonadEval m) => NixValue -> m NixValue+builtinPathExists val = do+  p <- coerceToPath "pathExists" val+  VBool <$> doesPathExist p++builtinReadDir :: (MonadEval m) => NixValue -> m NixValue+builtinReadDir val = do+  p <- coerceToPath "readDir" val+  entries <- listDirectory p+  pure (VAttrs (attrSetFromMap (Map.fromList [(name, evaluated (mkStr fileType)) | (name, fileType) <- entries])))++-- ---------------------------------------------------------------------------+-- Builtin implementations — environment + paths+-- ---------------------------------------------------------------------------++builtinGetEnv :: (MonadEval m) => NixValue -> m NixValue+builtinGetEnv (VStr name _) = mkStr <$> getEnvVar name+builtinGetEnv other =+  throwEvalError ("builtins.getEnv: expected a string, got " <> typeName other)++builtinToPath :: (MonadEval m) => NixValue -> m NixValue+builtinToPath (VPath p) = pure (VPath p)+builtinToPath (VStr s _) = case T.uncons s of+  Nothing -> throwEvalError "builtins.toPath: empty path"+  Just ('/', _) -> pure (VPath s)+  Just _ -> throwEvalError ("builtins.toPath: path must be absolute, got " <> s)+builtinToPath other =+  throwEvalError ("builtins.toPath: expected a string or path, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin implementations — store path operations+-- ---------------------------------------------------------------------------++builtinPlaceholder :: (MonadEval m) => NixValue -> m NixValue+builtinPlaceholder (VStr outputName _) =+  let preimage = "nix-output:" <> outputName+      hashText = truncatedBase32 (TE.encodeUtf8 preimage)+   in pure (VPath (storeDirPrefix <> hashText <> "-" <> outputName))+builtinPlaceholder other =+  throwEvalError ("builtins.placeholder: expected a string, got " <> typeName other)++builtinStorePath :: (MonadEval m) => NixValue -> m NixValue+builtinStorePath (VPath p) = validateStorePath p+builtinStorePath (VStr s _) = validateStorePath s+builtinStorePath other =+  throwEvalError ("builtins.storePath: expected a path or string, got " <> typeName other)++validateStorePath :: (MonadEval m) => Text -> m NixValue+validateStorePath p+  | storeDirPrefix `T.isPrefixOf` p,+    T.length p > T.length storeDirPrefix,+    let basename = T.drop (T.length storeDirPrefix) p,+    T.length basename >= 33,+    Just ('-', _) <- T.uncons (T.drop 32 basename) =+      pure (VPath p)+  | otherwise =+      throwEvalError ("builtins.storePath: not a valid store path: " <> p)++-- ---------------------------------------------------------------------------+-- Builtin implementations — Nix search path+-- ---------------------------------------------------------------------------++builtinFindFile :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinFindFile (VList searchPath) (VStr name _) = do+  entries <- mapM forceSearchEntry searchPath+  findFirst entries name+builtinFindFile (VList _) other =+  throwEvalError ("builtins.findFile: expected a string, got " <> typeName other)+builtinFindFile other _ =+  throwEvalError ("builtins.findFile: expected a list, got " <> typeName other)++-- | Extract {prefix, path} from a search path entry thunk.+forceSearchEntry :: (MonadEval m) => Thunk -> m (Text, Text)+forceSearchEntry thunk = do+  val <- force thunk+  case val of+    VAttrs attrs -> do+      prefixThunk <-+        maybe (throwEvalError "builtins.findFile: entry missing 'prefix'") pure $+          attrSetLookup "prefix" attrs+      pathThunk <-+        maybe (throwEvalError "builtins.findFile: entry missing 'path'") pure $+          attrSetLookup "path" attrs+      prefixVal <- force prefixThunk+      pathVal <- force pathThunk+      prefix <- case prefixVal of+        VStr s _ -> pure s+        _ -> throwEvalError "builtins.findFile: 'prefix' must be a string"+      path <- case pathVal of+        VStr s _ -> pure s+        VPath s -> pure s+        _ -> throwEvalError "builtins.findFile: 'path' must be a string or path"+      pure (prefix, path)+    _ -> throwEvalError "builtins.findFile: search path entry must be a set"++-- | Iterate search path entries, checking for a match.+findFirst :: (MonadEval m) => [(Text, Text)] -> Text -> m NixValue+findFirst [] name =+  throwEvalError ("file '" <> name <> "' was not found in the Nix search path")+findFirst ((prefix, path) : rest) name+  | prefix == name || (not (T.null prefix) && (prefix <> "/") `T.isPrefixOf` name) =+      let suffix = if prefix == name then "" else T.drop (T.length prefix + 1) name+          candidate = if T.null suffix then path else path <> "/" <> suffix+       in do+            exists <- doesPathExist candidate+            if exists+              then pure (VPath candidate)+              else findFirst rest name+  | T.null prefix =+      let candidate = path <> "/" <> name+       in do+            exists <- doesPathExist candidate+            if exists+              then pure (VPath candidate)+              else findFirst rest name+  | otherwise = findFirst rest name++-- ---------------------------------------------------------------------------+-- Builtin implementations — store file creation+-- ---------------------------------------------------------------------------++builtinToFile :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinToFile (VStr name _) (VStr contents _) = do+  storePath <- writeToStore name contents+  pure (VPath storePath)+builtinToFile (VStr _ _) other =+  throwEvalError ("builtins.toFile: expected a string, got " <> typeName other)+builtinToFile other _ =+  throwEvalError ("builtins.toFile: expected a string, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin implementations — scoped import+-- ---------------------------------------------------------------------------++builtinScopedImport :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinScopedImport (VAttrs attrs) pathVal = do+  p <- coerceToPath "scopedImport" pathVal+  let scope = Map.toList (attrSetToMap attrs)+  scopedImportFile scope p+builtinScopedImport other _ =+  throwEvalError ("builtins.scopedImport: expected a set, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin implementations — network fetchers+-- ---------------------------------------------------------------------------++builtinFetchurl :: (MonadEval m) => NixValue -> m NixValue+builtinFetchurl (VStr url _) = fetchUrlSimple url Nothing+builtinFetchurl (VAttrs attrs) = do+  url <- forceAttrStr "builtins.fetchurl" "url" attrs+  sha256 <- forceOptionalAttrStr attrs "sha256"+  fetchUrlSimple url sha256+builtinFetchurl other =+  throwEvalError ("builtins.fetchurl: expected a string or set, got " <> typeName other)++builtinFetchTarball :: (MonadEval m) => NixValue -> m NixValue+builtinFetchTarball (VStr url _) = fetchUrlSimple url Nothing+builtinFetchTarball (VAttrs attrs) = do+  url <- forceAttrStr "builtins.fetchTarball" "url" attrs+  sha256 <- forceOptionalAttrStr attrs "sha256"+  fetchUrlSimple url sha256+builtinFetchTarball other =+  throwEvalError ("builtins.fetchTarball: expected a string or set, got " <> typeName other)++builtinFetchGit :: (MonadEval m) => NixValue -> m NixValue+builtinFetchGit (VStr url _) = do+  -- Use a content-based temp dir to avoid predictable paths+  let urlHash = sha256Hex (TE.encodeUtf8 url)+  sysTmpRaw <- getEnvVar "TMPDIR"+  -- TMPDIR on Unix, TEMP/TMP on Windows; fall back to /tmp+  sysTmp <-+    if T.null sysTmpRaw+      then do+        winTmp <- getEnvVar "TEMP"+        pure (if T.null winTmp then "/tmp" else winTmp)+      else pure sysTmpRaw+  let tmpDir = sysTmp <> "/nova-nix-fetchgit-" <> urlHash+  (code, _stdout, stderr) <- runProcess "git" ["clone", "--depth", "1", "--", url, tmpDir] ""+  if code /= 0+    then throwEvalError ("builtins.fetchGit: git clone failed: " <> stderr)+    else pure (VPath tmpDir)+builtinFetchGit (VAttrs attrs) = do+  url <- forceAttrStr "builtins.fetchGit" "url" attrs+  builtinFetchGit (mkStr url)+builtinFetchGit other =+  throwEvalError ("builtins.fetchGit: expected a string or set, got " <> typeName other)++-- | Fetch a URL and optionally verify its hash.+fetchUrlSimple :: (MonadEval m) => Text -> Maybe Text -> m NixValue+fetchUrlSimple url _sha256 = do+  (code, stdout, stderr) <- runProcess "curl" ["-sSfL", "--", url] ""+  if code /= 0+    then throwEvalError ("fetch failed: " <> stderr)+    else do+      storePath <- writeToStore "fetchurl-result" stdout+      pure (VPath storePath)++-- | Force a required string attribute from an attrset.+forceAttrStr :: (MonadEval m) => Text -> Text -> AttrSet -> m Text+forceAttrStr builtin key attrs =+  case attrSetLookup key attrs of+    Nothing -> throwEvalError (builtin <> ": missing required attribute '" <> key <> "'")+    Just thunk -> do+      val <- force thunk+      case val of+        VStr s _ -> pure s+        VPath p -> pure p+        _ -> throwEvalError (builtin <> ": '" <> key <> "' must be a string")++-- | Force an optional string attribute.+forceOptionalAttrStr :: (MonadEval m) => AttrSet -> Text -> m (Maybe Text)+forceOptionalAttrStr attrs key =+  case attrSetLookup key attrs of+    Nothing -> pure Nothing+    Just thunk -> do+      val <- force thunk+      case val of+        VStr s _ -> pure (Just s)+        _ -> pure Nothing++-- ---------------------------------------------------------------------------+-- Builtin implementations — derivation construction+-- ---------------------------------------------------------------------------++builtinDerivation :: (MonadEval m) => NixValue -> m NixValue+builtinDerivation (VAttrs attrs) = do+  -- Extract required attributes+  drvName <- forceAttrStr "derivation" "name" attrs+  system <- forceAttrStr "derivation" "system" attrs+  builder <- forceAttrStr "derivation" "builder" attrs++  -- Extract optional outputs (default ["out"])+  outputNames <- case attrSetLookup "outputs" attrs of+    Nothing -> pure ["out"]+    Just thunk -> do+      val <- force thunk+      case val of+        VList thunks -> mapM forceToText thunks+        _ -> throwEvalError "derivation: 'outputs' must be a list of strings"++  -- Extract optional args (default [])+  builderArgs <- case attrSetLookup "args" attrs of+    Nothing -> pure []+    Just thunk -> do+      val <- force thunk+      case val of+        VList thunks -> mapM forceToText thunks+        _ -> throwEvalError "derivation: 'args' must be a list of strings"++  -- Collect all string-coercible attrs into env WITH their contexts+  (drvEnvPairs, envContext) <- collectDrvEnvWithContext (attrSetToMap attrs)++  -- Extract input derivations and input sources from the merged context+  let inputDrvs = extractInputDrvs envContext+      inputSrcs = extractInputSrcs envContext++  -- Build the platform+  let platform = textToPlatform system++  -- Build the derivation with populated inputs for hashing+  let envMap = Map.fromList drvEnvPairs+      drv =+        Derivation+          { drvOutputs = [],+            drvInputDrvs = inputDrvs,+            drvInputSrcs = inputSrcs,+            drvPlatform = platform,+            drvBuilder = builder,+            drvArgs = builderArgs,+            drvEnv = envMap+          }++  -- Serialize to ATerm and hash for drvPath+  let aterm = toATerm drv+      storeRef = ":" <> defaultStoreDirText <> ":"+      drvPathHash = truncatedBase32 (TE.encodeUtf8 ("text:sha256:" <> sha256Hex (TE.encodeUtf8 aterm) <> storeRef <> drvName <> ".drv"))+      drvPathText = storeDirPrefix <> drvPathHash <> "-" <> drvName <> ".drv"++  -- Parse drvPath as a StorePath for context+  let drvSP = case parseStorePath defaultStoreDir drvPathText of+        Just sp -> sp+        Nothing -> StorePath (T.take 32 (T.drop (T.length storeDirPrefix) drvPathText)) (drvName <> ".drv")++  -- Compute output paths+  let computeOutPath outName =+        let nameSuffix = if outName == "out" then "" else "-" <> outName+            preimage = "output:" <> outName <> ":sha256:" <> sha256Hex (TE.encodeUtf8 aterm) <> storeRef <> drvName <> nameSuffix+            outHash = truncatedBase32 (TE.encodeUtf8 preimage)+         in storeDirPrefix <> outHash <> "-" <> drvName <> nameSuffix++  let outPaths = [(outName, computeOutPath outName) | outName <- outputNames]+      mainOutPath = case outPaths of+        ((_, p) : _) -> p+        [] -> ""++  -- Build DerivationOutput records for the Derivation value+  let drvOutputsList =+        [ DerivationOutput+            { doName = outName,+              doPath = case parseStorePath defaultStoreDir outP of+                Just sp -> sp+                -- Fallback: construct manually from the path string+                Nothing -> StorePath (T.take 32 (T.drop (T.length storeDirPrefix) outP)) outName,+              doHashAlgo = "",+              doHash = ""+            }+        | (outName, outP) <- outPaths+        ]++  -- Build the complete Derivation with populated outputs and inputs+  let completeDrv = drv {drvOutputs = drvOutputsList}++  -- Context for output paths: each output carries SCDrvOutput context+  -- Context for drvPath: carries SCAllOutputs context+  let drvPathCtx = StringContext (Set.singleton (SCAllOutputs drvSP))+      outPathCtx outName = StringContext (Set.singleton (SCDrvOutput drvSP outName))++  -- Build result attrset: original attrs + drvPath, outPath, type, per-output attrs+  let baseAttrs =+        Map.fromList $+          [ ("type", evaluated (mkStr "derivation")),+            ("drvPath", evaluated (VStr drvPathText drvPathCtx)),+            ("outPath", evaluated (VStr mainOutPath (outPathCtx "out"))),+            ("name", evaluated (mkStr drvName)),+            ("system", evaluated (mkStr system)),+            ("builder", evaluated (mkStr builder)),+            ("_derivation", evaluated (VDerivation completeDrv))+          ]+            ++ [(outName, evaluated (VStr outP (outPathCtx outName))) | (outName, outP) <- outPaths]+      -- Merge original attrs underneath so computed attrs take priority+      resultAttrs = Map.union baseAttrs (attrSetToMap attrs)++  pure (VAttrs (attrSetFromMap resultAttrs))+builtinDerivation other =+  throwEvalError ("derivation: expected a set, got " <> typeName other)++-- | Force a thunk to a Text string.+forceToText :: (MonadEval m) => Thunk -> m Text+forceToText thunk = do+  val <- force thunk+  case val of+    VStr s _ -> pure s+    VPath p -> pure p+    _ -> throwEvalError ("expected a string, got " <> typeName val)++-- | Collect all string-coercible attributes for the derivation environment,+-- along with the merged string context from all collected values.+collectDrvEnvWithContext :: (MonadEval m) => Map Text Thunk -> m ([(Text, Text)], StringContext)+collectDrvEnvWithContext attrs = do+  let pairs = Map.toList attrs+  results <- mapM tryCoerce pairs+  let envPairs = catMaybes [fmap (\(k, v, _) -> (k, v)) r | r <- results]+      mergedCtx = mconcat [ctx | Just (_, _, ctx) <- results]+  pure (envPairs, mergedCtx)+  where+    tryCoerce (key, thunk) = do+      val <- force thunk+      case val of+        VStr s ctx -> pure (Just (key, s, ctx))+        VPath p -> pure (Just (key, p, emptyContext))+        VInt n -> pure (Just (key, T.pack (show n), emptyContext))+        VBool True -> pure (Just (key, "1", emptyContext))+        VBool False -> pure (Just (key, "", emptyContext))+        VNull -> pure Nothing+        VList _ -> pure Nothing+        VAttrs innerAttrs ->+          -- Derivations in env get their outPath+          case attrSetLookup "outPath" innerAttrs of+            Just outThunk -> do+              outVal <- force outThunk+              case outVal of+                VPath p -> pure (Just (key, p, emptyContext))+                VStr s ctx -> pure (Just (key, s, ctx))+                _ -> pure Nothing+            Nothing -> pure Nothing+        _ -> pure Nothing++-- ---------------------------------------------------------------------------+-- Builtin implementations — hashFile, readFileType+-- ---------------------------------------------------------------------------++-- | @builtins.hashFile algo path@ — hash raw bytes of a file on disk.+-- Returns base-16 hex string, matching @builtins.hashString@ output format.+builtinHashFile :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinHashFile (VStr algo _) (VPath path) = do+  bytes <- readFileBytes path+  hashBytesWithAlgo "hashFile" algo bytes+builtinHashFile (VStr algo _) (VStr path _) = do+  bytes <- readFileBytes path+  hashBytesWithAlgo "hashFile" algo bytes+builtinHashFile (VStr _ _) other =+  throwEvalError ("builtins.hashFile: expected a path, got " <> typeName other)+builtinHashFile other _ =+  throwEvalError ("builtins.hashFile: expected a string, got " <> typeName other)++-- | Shared hash dispatch for raw 'ByteString' input.+hashBytesWithAlgo :: (MonadEval m) => Text -> Text -> BS.ByteString -> m NixValue+hashBytesWithAlgo ctx algo bytes = case algo of+  "sha256" -> pure (mkStr (digestToHex (CH.hash bytes :: CH.Digest CH.SHA256)))+  "sha512" -> pure (mkStr (digestToHex (CH.hash bytes :: CH.Digest CH.SHA512)))+  "sha1" -> pure (mkStr (digestToHex (CH.hash bytes :: CH.Digest CH.SHA1)))+  "md5" -> pure (mkStr (digestToHex (CH.hash bytes :: CH.Digest CH.MD5)))+  _ -> throwEvalError ("builtins." <> ctx <> ": unknown hash algorithm '" <> algo <> "'")++-- | @builtins.readFileType path@ — classify a filesystem entry.+-- Returns @"regular"@, @"directory"@, @"symlink"@, or @"unknown"@.+builtinReadFileType :: (MonadEval m) => NixValue -> m NixValue+builtinReadFileType (VPath path) = mkStr <$> getFileType path+builtinReadFileType (VStr path _) = mkStr <$> getFileType path+builtinReadFileType other =+  throwEvalError ("builtins.readFileType: expected a path, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin implementations — convertHash+-- ---------------------------------------------------------------------------++-- | @builtins.convertHash { hash, hashAlgo?, toHashFormat }@ — convert+-- between hash representations.  Supports base16, nix32, base64, and sri.+builtinConvertHash :: (MonadEval m) => NixValue -> m NixValue+builtinConvertHash (VAttrs attrs) = do+  hashVal <- requireStrAttr "convertHash" "hash" attrs+  toFmt <- requireStrAttr "convertHash" "toHashFormat" attrs+  -- Detect input format and decode to raw bytes + algo+  (algo, rawBytes) <- decodeHashInput attrs hashVal+  -- Encode to target format+  case toFmt of+    "base16" -> pure (mkStr (bytesToHex rawBytes))+    "nix32" -> pure (mkStr (Nix32.encode rawBytes))+    "base32" -> pure (mkStr (Nix32.encode rawBytes)) -- deprecated alias+    "base64" -> pure (mkStr (bytesToBase64 rawBytes))+    "sri" -> pure (mkStr (algo <> "-" <> bytesToBase64 rawBytes))+    _ -> throwEvalError ("builtins.convertHash: unknown toHashFormat '" <> toFmt <> "'")+builtinConvertHash other =+  throwEvalError ("builtins.convertHash: expected a set, got " <> typeName other)++-- | Extract algo + raw bytes from the hash input, handling SRI, prefixed, and plain formats.+decodeHashInput :: (MonadEval m) => AttrSet -> Text -> m (Text, BS.ByteString)+decodeHashInput attrs hashStr+  -- SRI format: algo-base64+  | Just (algo, b64) <- parseSRI hashStr = do+      bytes <- decodeBase64E "convertHash" b64+      pure (algo, bytes)+  -- Prefixed format: algo:hex or algo:nix32+  | Just (algo, rest) <- parseAlgoPrefix hashStr =+      decodeWithAlgo algo rest+  -- Plain hash — need hashAlgo attribute+  | otherwise = do+      algo <- requireStrAttr "convertHash" "hashAlgo" attrs+      decodeWithAlgo algo hashStr++-- | Try to decode as hex, then nix32, then base64.+decodeWithAlgo :: (MonadEval m) => Text -> Text -> m (Text, BS.ByteString)+decodeWithAlgo algo s+  | Just bytes <- hexToBytes s = pure (algo, bytes)+  | Right bytes <- Nix32.decode s = pure (algo, bytes)+  | Right bytes <- decodeBase64Pure s = pure (algo, bytes)+  | otherwise = throwEvalError ("builtins.convertHash: cannot decode hash '" <> s <> "'")++-- | Parse @sha256-base64...@ SRI format.+parseSRI :: Text -> Maybe (Text, Text)+parseSRI t = case T.breakOn "-" t of+  (algo, rest)+    | not (T.null rest) && algo `elem` ["sha256", "sha512", "sha1", "md5"] ->+        Just (algo, T.drop 1 rest)+  _ -> Nothing++-- | Parse @sha256:value@ prefixed format.+parseAlgoPrefix :: Text -> Maybe (Text, Text)+parseAlgoPrefix t = case T.breakOn ":" t of+  (algo, rest)+    | not (T.null rest) && algo `elem` ["sha256", "sha512", "sha1", "md5"] ->+        Just (algo, T.drop 1 rest)+  _ -> Nothing++-- | Require a string attribute from an attrset.+requireStrAttr :: (MonadEval m) => Text -> Text -> AttrSet -> m Text+requireStrAttr ctx key attrs = case attrSetLookup key attrs of+  Just thunk -> do+    val <- force thunk+    case val of+      VStr s _ -> pure s+      _ -> throwEvalError ("builtins." <> ctx <> ": " <> key <> " must be a string")+  Nothing -> throwEvalError ("builtins." <> ctx <> ": missing required attribute '" <> key <> "'")++-- | Encode bytes to base16 hex.+bytesToHex :: BS.ByteString -> Text+bytesToHex = T.pack . concatMap byteToHex . BS.unpack++-- | Decode hex string to bytes.+hexToBytes :: Text -> Maybe BS.ByteString+hexToBytes t+  | T.null t = Just BS.empty+  | odd (T.length t) = Nothing+  | T.all isHexDigit t = Just (BS.pack (go (T.unpack t)))+  | otherwise = Nothing+  where+    go [] = []+    go (hi : lo : rest) = fromIntegral (digitToInt hi * 16 + digitToInt lo) : go rest+    go [_] = [] -- unreachable due to odd check++-- ---------------------------------------------------------------------------+-- Base64 encode/decode — delegates to nova-cache (base64-bytestring under the hood)+-- ---------------------------------------------------------------------------++-- | Encode bytes to base64.+bytesToBase64 :: BS.ByteString -> Text+bytesToBase64 = B64.encode++-- | Decode base64 text to bytes (pure).+decodeBase64Pure :: Text -> Either Text BS.ByteString+decodeBase64Pure t =+  case B64.decode (T.filter (/= '=') (T.filter (/= '\n') (T.filter (/= '\r') t))) of+    Right bytes -> Right bytes+    Left _ -> Left "invalid base64"++-- | Decode base64 with error context for builtins.+decodeBase64E :: (MonadEval m) => Text -> Text -> m BS.ByteString+decodeBase64E ctx t = case decodeBase64Pure t of+  Right bytes -> pure bytes+  Left _ -> throwEvalError ("builtins." <> ctx <> ": invalid base64 encoding")++-- ---------------------------------------------------------------------------+-- Builtin implementations — fromTOML+-- ---------------------------------------------------------------------------++-- | @builtins.fromTOML str@ — parse a TOML document to a Nix value.+-- Hand-rolled parser covering the TOML v1.0 subset used by nixpkgs:+-- bare/quoted keys, dotted keys, basic/literal strings (multiline),+-- integers (dec/hex/oct/bin), floats (inc. inf/nan), booleans,+-- inline tables, arrays, array-of-tables, and standard tables.+-- Datetimes are represented as strings (matching real Nix).+builtinFromTOML :: (MonadEval m) => NixValue -> m NixValue+builtinFromTOML (VStr s _) = case parseTOML s of+  Right val -> pure val+  Left err -> throwEvalError ("builtins.fromTOML: " <> err)+builtinFromTOML other =+  throwEvalError ("builtins.fromTOML: expected a string, got " <> typeName other)++-- | Intermediate TOML value before conversion to NixValue.+data TOMLValue+  = TOMLStr !Text+  | TOMLInt !Integer+  | TOMLFloat !Double+  | TOMLBool !Bool+  | TOMLArray ![TOMLValue]+  | TOMLTable !(Map Text TOMLValue)+  deriving (Show)++-- | Parse a TOML document into a NixValue.+parseTOML :: Text -> Either Text NixValue+parseTOML input = do+  table <- parseTOMLDoc (T.lines input)+  pure (tomlToNix (TOMLTable table))++-- | Convert a TOMLValue to NixValue.+tomlToNix :: TOMLValue -> NixValue+tomlToNix val = case val of+  TOMLStr s -> mkStr s+  TOMLInt n -> VInt n+  TOMLFloat d -> VFloat d+  TOMLBool b -> VBool b+  TOMLArray xs -> VList (map (evaluated . tomlToNix) xs)+  TOMLTable m -> VAttrs (attrSetFromMap (Map.map (evaluated . tomlToNix) m))++-- | Parse all lines of a TOML document into a table.+parseTOMLDoc :: [Text] -> Either Text (Map Text TOMLValue)+parseTOMLDoc lns = go lns [] Map.empty+  where+    go [] _ root = Right root+    go (line : rest) currentPath root+      | T.null stripped || T.isPrefixOf "#" stripped =+          -- Empty line or comment+          go rest currentPath root+      | T.isPrefixOf "[[" stripped && T.isSuffixOf "]]" stripped =+          -- Array of tables: [[key]]+          let keyStr = T.strip (T.drop 2 (T.dropEnd 2 stripped))+              keys = parseDottedKey keyStr+           in go rest keys (insertArrayTable keys root)+      | T.isPrefixOf "[" stripped && T.isSuffixOf "]" stripped =+          -- Standard table: [key]+          let keyStr = T.strip (T.drop 1 (T.dropEnd 1 stripped))+              keys = parseDottedKey keyStr+           in go rest keys (ensureTable keys root)+      | otherwise =+          -- Key = value pair+          case parseKVLine stripped of+            Right (keys, val) ->+              go rest currentPath (insertNested (currentPath ++ keys) val root)+            Left err -> Left err+      where+        stripped = T.strip line++-- | Parse a key = value line.+parseKVLine :: Text -> Either Text ([Text], TOMLValue)+parseKVLine line =+  let (keyPart, afterEq) = splitAtEquals line+   in case T.uncons afterEq of+        Nothing -> Left ("expected '=' in: " <> line)+        Just _ -> do+          let val = T.strip afterEq+          parsed <- parseTOMLValue val+          Right (parseDottedKey (T.strip keyPart), parsed)++-- | Split a line at the first unquoted '=' sign.+splitAtEquals :: Text -> (Text, Text)+splitAtEquals = go T.empty+  where+    go acc t = case T.uncons t of+      Nothing -> (acc, T.empty)+      Just ('=', rest) -> (acc, rest)+      Just ('"', rest) ->+        let (quoted, after) = T.break (== '"') rest+         in case T.uncons after of+              Just ('"', r) -> go (acc <> "\"" <> quoted <> "\"") r+              _ -> go (acc <> "\"" <> quoted) after+      Just ('\'', rest) ->+        let (quoted, after) = T.break (== '\'') rest+         in case T.uncons after of+              Just ('\'', r) -> go (acc <> "'" <> quoted <> "'") r+              _ -> go (acc <> "'" <> quoted) after+      Just (c, rest) -> go (T.snoc acc c) rest++-- | Parse dotted key like @foo.bar."baz qux"@ into @["foo", "bar", "baz qux"]@.+parseDottedKey :: Text -> [Text]+parseDottedKey t+  | T.null t = []+  | otherwise = case T.uncons t of+      Just ('"', rest) ->+        let (key, after) = T.break (== '"') rest+         in key : parseDottedKey (T.drop 1 (T.stripStart (dropDot after)))+      Just ('\'', rest) ->+        let (key, after) = T.break (== '\'') rest+         in key : parseDottedKey (T.drop 1 (T.stripStart (dropDot after)))+      _ ->+        let (key, after) = T.break (\c -> c == '.' || c == '"') t+         in T.strip key : case T.uncons after of+              Nothing -> []+              Just _ -> parseDottedKey (T.drop 1 (T.stripStart after))+  where+    dropDot txt = case T.uncons txt of+      Just ('.', rest) -> rest+      _ -> txt++-- | Parse a TOML value (right side of '=').+parseTOMLValue :: Text -> Either Text TOMLValue+parseTOMLValue t =+  let stripped = T.strip t+      -- Strip inline comments (not inside strings)+      cleaned = stripInlineComment stripped+   in case T.uncons cleaned of+        Nothing -> Left "empty value"+        Just ('"', _)+          | T.isPrefixOf "\"\"\"" cleaned -> parseMultilineBasicStr (T.drop 3 cleaned)+          | otherwise -> parseBasicStr (T.drop 1 cleaned)+        Just ('\'', _)+          | T.isPrefixOf "'''" cleaned -> parseMultilineLiteralStr (T.drop 3 cleaned)+          | otherwise -> parseLiteralStr (T.drop 1 cleaned)+        Just ('{', _) -> parseInlineTable cleaned+        Just ('[', _) -> parseInlineArray cleaned+        Just ('t', _)+          | T.isPrefixOf "true" cleaned -> Right (TOMLBool True)+        Just ('f', _)+          | T.isPrefixOf "false" cleaned -> Right (TOMLBool False)+        Just ('i', _)+          | T.isPrefixOf "inf" cleaned -> Right (TOMLFloat (1 / 0))+        Just ('+', rest)+          | T.isPrefixOf "inf" rest -> Right (TOMLFloat (1 / 0))+          | T.isPrefixOf "nan" rest -> Right (TOMLFloat (0 / 0))+        Just ('-', rest)+          | T.isPrefixOf "inf" rest -> Right (TOMLFloat (negate (1 / 0)))+          | T.isPrefixOf "nan" rest -> Right (TOMLFloat (0 / 0))+        Just ('n', _)+          | T.isPrefixOf "nan" cleaned -> Right (TOMLFloat (0 / 0))+        _ -> parseTOMLNumberOrDatetime cleaned++-- | Strip inline comment from a value (not inside quotes).+stripInlineComment :: Text -> Text+stripInlineComment = go (0 :: Int)+  where+    go _ t | T.null t = T.empty+    go depth t = case T.uncons t of+      Nothing -> T.empty+      Just ('#', _) | depth == (0 :: Int) -> T.empty+      Just ('"', rest)+        | depth == 0 ->+            let (str, after) = T.break (== '"') rest+             in T.cons '"' (str <> T.take 1 after <> go depth (T.drop 1 after))+      Just ('[', rest) -> T.cons '[' (go (depth + 1) rest)+      Just ('{', rest) -> T.cons '{' (go (depth + 1) rest)+      Just (']', rest) -> T.cons ']' (go (max 0 (depth - 1)) rest)+      Just ('}', rest) -> T.cons '}' (go (max 0 (depth - 1)) rest)+      Just (c, rest) -> T.cons c (go depth rest)++-- | Parse a basic (double-quoted) TOML string.+-- O(n) via chunk list + T.concat instead of O(n^2) T.snoc.+parseBasicStr :: Text -> Either Text TOMLValue+parseBasicStr = go []+  where+    go !chunks t = case T.uncons t of+      Nothing -> Left "unterminated basic string"+      Just ('"', _) -> Right (TOMLStr (T.concat (reverse chunks)))+      Just ('\\', rest) -> case T.uncons rest of+        Just ('n', r) -> go ("\n" : chunks) r+        Just ('t', r) -> go ("\t" : chunks) r+        Just ('r', r) -> go ("\r" : chunks) r+        Just ('\\', r) -> go ("\\" : chunks) r+        Just ('"', r) -> go ("\"" : chunks) r+        Just ('b', r) -> go ("\b" : chunks) r+        Just ('f', r) -> go ("\f" : chunks) r+        Just ('u', r) -> case parseHex4 r of+          Just (cp, r2) -> go (T.singleton (chr cp) : chunks) r2+          Nothing -> Left "invalid \\u escape"+        _ -> Left "invalid escape sequence"+      Just (c, rest) -> go (T.singleton c : chunks) rest++-- | Parse a literal (single-quoted) TOML string.+parseLiteralStr :: Text -> Either Text TOMLValue+parseLiteralStr t =+  let (content, rest) = T.break (== '\'') t+   in case T.uncons rest of+        Just ('\'', _) -> Right (TOMLStr content)+        _ -> Left "unterminated literal string"++-- | Parse a multiline basic string.+parseMultilineBasicStr :: Text -> Either Text TOMLValue+parseMultilineBasicStr t =+  case T.breakOn "\"\"\"" t of+    (content, rest)+      | T.isPrefixOf "\"\"\"" rest ->+          Right (TOMLStr (T.replace "\\\n" "" (stripLeadingNewline content)))+      | otherwise -> Left ("unterminated multiline basic string, remaining: " <> T.take 20 rest)++-- | Parse a multiline literal string.+parseMultilineLiteralStr :: Text -> Either Text TOMLValue+parseMultilineLiteralStr t =+  case T.breakOn "'''" t of+    (content, rest)+      | T.isPrefixOf "'''" rest -> Right (TOMLStr (stripLeadingNewline content))+      | otherwise -> Left "unterminated multiline literal string"++-- | Strip a leading newline (TOML spec: first newline after opening quotes is trimmed).+stripLeadingNewline :: Text -> Text+stripLeadingNewline t = case T.uncons t of+  Just ('\n', rest) -> rest+  Just ('\r', rest) -> case T.uncons rest of+    Just ('\n', r) -> r+    _ -> rest+  _ -> t++-- | Parse a TOML number or datetime.+parseTOMLNumberOrDatetime :: Text -> Either Text TOMLValue+parseTOMLNumberOrDatetime s+  -- Hex, octal, binary integers+  | T.isPrefixOf "0x" s || T.isPrefixOf "0X" s = parseHexInt (T.drop 2 s)+  | T.isPrefixOf "0o" s || T.isPrefixOf "0O" s = parseOctInt (T.drop 2 s)+  | T.isPrefixOf "0b" s || T.isPrefixOf "0B" s = parseBinInt (T.drop 2 s)+  -- Contains date separators → treat as datetime string+  | T.any (== 'T') s && T.any (== '-') s = Right (TOMLStr s)+  | T.count "-" s >= 2 && T.any isDigit s = Right (TOMLStr s)+  | T.any (== ':') s && T.any isDigit s = Right (TOMLStr s)+  -- Float (has dot or exponent)+  | T.any (== '.') s || T.any (\c -> c == 'e' || c == 'E') s = parseFloat s+  -- Plain integer+  | otherwise = parseInt s++-- | Parse a plain decimal integer, ignoring underscores.+parseInt :: Text -> Either Text TOMLValue+parseInt t =+  let cleaned = T.filter (/= '_') t+      (sign, digits) = case T.uncons cleaned of+        Just ('+', rest) -> (1, rest)+        Just ('-', rest) -> (-1, rest)+        _ -> (1, cleaned)+   in case readDecimal digits of+        Just n -> Right (TOMLInt (sign * n))+        Nothing -> Left ("invalid integer: " <> t)++parseHexInt :: Text -> Either Text TOMLValue+parseHexInt t =+  let cleaned = T.filter (/= '_') t+   in case readHexT cleaned of+        Just n -> Right (TOMLInt n)+        Nothing -> Left ("invalid hex integer: " <> t)++parseOctInt :: Text -> Either Text TOMLValue+parseOctInt t =+  let cleaned = T.filter (/= '_') t+   in case readOctT cleaned of+        Just n -> Right (TOMLInt n)+        Nothing -> Left ("invalid octal integer: " <> t)++parseBinInt :: Text -> Either Text TOMLValue+parseBinInt t =+  let cleaned = T.filter (/= '_') t+   in case readBinT cleaned of+        Just n -> Right (TOMLInt n)+        Nothing -> Left ("invalid binary integer: " <> t)++parseFloat :: Text -> Either Text TOMLValue+parseFloat t =+  let cleaned = T.filter (/= '_') t+   in case readDouble cleaned of+        Just d -> Right (TOMLFloat d)+        Nothing -> Left ("invalid float: " <> t)++-- | Read a decimal integer from Text.+readDecimal :: Text -> Maybe Integer+readDecimal t+  | T.null t = Nothing+  | T.all isDigit t = Just (T.foldl' (\acc c -> acc * 10 + toInteger (digitToInt c)) 0 t)+  | otherwise = Nothing++readHexT :: Text -> Maybe Integer+readHexT t+  | T.null t = Nothing+  | T.all isHexDigit t = Just (T.foldl' (\acc c -> acc * 16 + toInteger (digitToInt c)) 0 t)+  | otherwise = Nothing++readOctT :: Text -> Maybe Integer+readOctT t+  | T.null t = Nothing+  | T.all isOctDigit t =+      Just (T.foldl' (\acc c -> acc * 8 + toInteger (digitToInt c)) 0 t)+  | otherwise = Nothing++readBinT :: Text -> Maybe Integer+readBinT t+  | T.null t = Nothing+  | T.all (\c -> c == '0' || c == '1') t =+      Just (T.foldl' (\acc c -> acc * 2 + toInteger (digitToInt c)) 0 t)+  | otherwise = Nothing++readDouble :: Text -> Maybe Double+readDouble t = case reads (T.unpack t) of+  [(d, "")] -> Just d+  _ -> Nothing++-- | Parse an inline table: @{ key = val, ... }@.+parseInlineTable :: Text -> Either Text TOMLValue+parseInlineTable t = case T.uncons t of+  Just ('{', rest) ->+    let inner = T.strip (T.dropWhileEnd (== '}') (T.strip rest))+     in if T.null inner+          then Right (TOMLTable Map.empty)+          else do+            pairs <- mapM parseInlineKV (splitCommas inner)+            Right (TOMLTable (Map.fromList (concatMap flattenPair pairs)))+  _ -> Left "expected '{'"+  where+    flattenPair (keys, val) = case keys of+      [] -> []+      [k] -> [(k, val)]+      (k : ks) -> [(k, nestKeys ks val)]+    nestKeys [] v = v+    nestKeys (k : ks) v = TOMLTable (Map.singleton k (nestKeys ks v))++-- | Parse an inline array: @[ val, ... ]@.+parseInlineArray :: Text -> Either Text TOMLValue+parseInlineArray t = case T.uncons t of+  Just ('[', rest) ->+    let inner = T.strip (T.dropWhileEnd (== ']') (T.strip rest))+     in if T.null inner+          then Right (TOMLArray [])+          else do+            vals <- mapM (parseTOMLValue . T.strip) (splitCommas inner)+            Right (TOMLArray vals)+  _ -> Left "expected '['"++-- | Parse a single key=value pair in an inline table.+parseInlineKV :: Text -> Either Text ([Text], TOMLValue)+parseInlineKV t =+  let (keyPart, afterEq) = splitAtEquals (T.strip t)+   in do+        val <- parseTOMLValue (T.strip afterEq)+        Right (parseDottedKey (T.strip keyPart), val)++-- | Split on commas not inside brackets or braces.+-- O(n) via chunk list + T.concat instead of O(n^2) T.snoc.+splitCommas :: Text -> [Text]+splitCommas = go (0 :: Int) []+  where+    finalize chunks =+      let t = T.concat (reverse chunks)+       in [t | not (T.null (T.strip t))]+    go _ !chunks t | T.null t = finalize chunks+    go depth !chunks t = case T.uncons t of+      Nothing -> finalize chunks+      Just (',', rest) | depth == 0 -> T.concat (reverse chunks) : go 0 [] rest+      Just ('[', rest) -> go (depth + 1) ("[" : chunks) rest+      Just ('{', rest) -> go (depth + 1) ("{" : chunks) rest+      Just (']', rest) -> go (max 0 (depth - 1)) ("]" : chunks) rest+      Just ('}', rest) -> go (max 0 (depth - 1)) ("}" : chunks) rest+      Just ('"', rest) ->+        let (str, after) = T.break (== '"') rest+            consumed = "\"" <> str <> T.take 1 after+         in go depth (consumed : chunks) (T.drop 1 after)+      Just (c, rest) -> go depth (T.singleton c : chunks) rest++-- | Insert a value at a nested key path into a table.+insertNested :: [Text] -> TOMLValue -> Map Text TOMLValue -> Map Text TOMLValue+insertNested [] _ m = m+insertNested [k] v m = Map.insert k v m+insertNested (k : ks) v m =+  let sub = case Map.lookup k m of+        Just (TOMLTable inner) -> inner+        _ -> Map.empty+   in Map.insert k (TOMLTable (insertNested ks v sub)) m++-- | Ensure a table path exists (for @[table]@ headers).+ensureTable :: [Text] -> Map Text TOMLValue -> Map Text TOMLValue+ensureTable [] m = m+ensureTable [k] m = case Map.lookup k m of+  Just (TOMLTable _) -> m+  Nothing -> Map.insert k (TOMLTable Map.empty) m+  _ -> m+ensureTable (k : ks) m =+  let sub = case Map.lookup k m of+        Just (TOMLTable inner) -> inner+        _ -> Map.empty+   in Map.insert k (TOMLTable (ensureTable ks sub)) m++-- | Insert an entry into an array-of-tables (@[[table]]@).+insertArrayTable :: [Text] -> Map Text TOMLValue -> Map Text TOMLValue+insertArrayTable [] m = m+insertArrayTable [k] m = case Map.lookup k m of+  Just (TOMLArray xs) -> Map.insert k (TOMLArray (xs ++ [TOMLTable Map.empty])) m+  Nothing -> Map.insert k (TOMLArray [TOMLTable Map.empty]) m+  _ -> Map.insert k (TOMLArray [TOMLTable Map.empty]) m+insertArrayTable (k : ks) m =+  let sub = case Map.lookup k m of+        Just (TOMLTable inner) -> inner+        Just (TOMLArray xs) ->+          -- Descend into the last element of the array+          case reverse xs of+            (TOMLTable inner : _) -> inner+            _ -> Map.empty+        _ -> Map.empty+      updated = insertArrayTable ks sub+   in case Map.lookup k m of+        Just (TOMLArray xs) ->+          case reverse xs of+            (TOMLTable _ : prev) ->+              Map.insert k (TOMLArray (reverse prev ++ [TOMLTable updated])) m+            _ -> Map.insert k (TOMLTable updated) m+        _ -> Map.insert k (TOMLTable updated) m++-- ---------------------------------------------------------------------------+-- Builtin implementations — toXML+-- ---------------------------------------------------------------------------++-- | @builtins.toXML val@ — convert a Nix value to its XML representation.+-- Matches the format defined by the Nix manual: strings, ints, floats,+-- bools, nulls, lists, and attrsets map to their XML counterparts.+builtinToXML :: (MonadEval m) => NixValue -> m NixValue+builtinToXML val = do+  xml <- valueToXML 0 val+  pure (mkStr ("<?xml version='1.0' encoding='utf-8'?>\n<expr>\n" <> xml <> "</expr>\n"))++valueToXML :: (MonadEval m) => Int -> NixValue -> m Text+valueToXML depth val = case val of+  VStr s _ ->+    pure (indent depth <> "<string value=" <> xmlQuote s <> " />\n")+  VInt n ->+    pure (indent depth <> "<int value=\"" <> T.pack (show n) <> "\" />\n")+  VFloat d ->+    pure (indent depth <> "<float value=\"" <> T.pack (show d) <> "\" />\n")+  VBool True ->+    pure (indent depth <> "<bool value=\"true\" />\n")+  VBool False ->+    pure (indent depth <> "<bool value=\"false\" />\n")+  VNull ->+    pure (indent depth <> "<null />\n")+  VPath p ->+    pure (indent depth <> "<path value=" <> xmlQuote p <> " />\n")+  VList thunks -> do+    items <- mapM (force >=> valueToXML (depth + 1)) thunks+    pure (indent depth <> "<list>\n" <> T.concat items <> indent depth <> "</list>\n")+  VAttrs attrs -> do+    let pairs = attrSetToAscList attrs+    items <- mapM (attrToXML (depth + 1)) pairs+    pure (indent depth <> "<attrs>\n" <> T.concat items <> indent depth <> "</attrs>\n")+  VLambda {} ->+    pure (indent depth <> "<function />\n")+  VBuiltin _ _ ->+    pure (indent depth <> "<function />\n")+  VDerivation _ ->+    pure (indent depth <> "<derivation />\n")+  where+    attrToXML d (name, thunk) = do+      v <- force thunk+      inner <- valueToXML d v+      pure (indent d <> "<attr name=" <> xmlQuote name <> ">\n" <> inner <> indent d <> "</attr>\n")++indent :: Int -> Text+indent n = T.replicate (n * 2) " "++xmlQuote :: Text -> Text+xmlQuote s = "\"" <> T.concatMap escapeChar s <> "\""+  where+    escapeChar '<' = "&lt;"+    escapeChar '>' = "&gt;"+    escapeChar '&' = "&amp;"+    escapeChar '"' = "&quot;"+    escapeChar c = T.singleton c++-- ---------------------------------------------------------------------------+-- Builtin implementations — filterSource+-- ---------------------------------------------------------------------------++-- | @builtins.filterSource filter path@ — copy a path to the store,+-- filtering entries via a predicate.  The filter function receives+-- @(path, type)@ where type is @"regular"@, @"directory"@, @"symlink"@,+-- or @"unknown"@.+builtinFilterSource :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinFilterSource _filterFn (VPath _path) =+  throwEvalError "builtins.filterSource: not yet implemented (requires store integration)"+builtinFilterSource _filterFn (VStr _path _) =+  throwEvalError "builtins.filterSource: not yet implemented (requires store integration)"+builtinFilterSource _ other =+  throwEvalError ("builtins.filterSource: expected a path, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin stubs — experimental features+-- ---------------------------------------------------------------------------++builtinOutputOf :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinOutputOf _ _ =+  throwEvalError "builtins.outputOf: requires experimental feature 'dynamic-derivations'"++builtinFetchTree :: (MonadEval m) => NixValue -> m NixValue+builtinFetchTree _ =+  throwEvalError "builtins.fetchTree: requires experimental feature 'fetch-tree'"++builtinFetchClosure :: (MonadEval m) => NixValue -> m NixValue+builtinFetchClosure _ =+  throwEvalError "builtins.fetchClosure: requires experimental feature 'fetch-closure'"
src/Nix/Eval/IO.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE ScopedTypeVariables #-}  -- | IO-based Nix evaluator.@@ -28,7 +29,8 @@ import Control.Exception (Exception, SomeException, displayException, fromException, throwIO, try) import Control.Monad (when) import Control.Monad.IO.Class (liftIO)-import Control.Monad.Reader (ReaderT (..), asks, local)+import Control.Monad.Reader (ReaderT (..), ask, asks, local)+import qualified Data.ByteString as BS import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map@@ -39,7 +41,7 @@ import Data.Time.Clock.POSIX (getPOSIXTime) import Nix.Builtins (builtinEnv, builtinEnvWithScope, parseNixPath) import Nix.Eval (eval)-import Nix.Eval.Types (MonadEval (..), NixValue (..), Thunk (..))+import Nix.Eval.Types (MonadEval (..), NixValue (..), Thunk (..), ThunkCell (..)) import Nix.Expr.Types (AttrKey (..), Binding (..), Expr (..), Formal (..), Formals (..), NixAtom (..), StringPart (..)) import Nix.Hash (sha256Hex, truncatedBase32) import Nix.Parser (parseNix)@@ -87,7 +89,7 @@ newEvalState :: FilePath -> IO EvalState newEvalState baseDir = do   cache <- newIORef Map.empty-  now <- fmap (floor . toRational) getPOSIXTime :: IO Integer+  now <- fmap floor getPOSIXTime :: IO Integer   nixPathStr <- lookupEnvText "NIX_PATH"   let searchPaths = case nixPathStr of         Just val -> parseNixPath (T.pack val)@@ -117,7 +119,7 @@   throwEvalError msg = EvalIO (liftIO (throwIO (NixEvalError msg)))    catchEvalError (EvalIO action) = EvalIO $ do-    st <- asks id+    st <- ask     result <- liftIO (try (runReaderT action st))     pure (case result of Left (NixEvalError msg) -> Left msg; Right val -> Right val) @@ -217,6 +219,10 @@               (unEvalIO (eval scopedEnv expr))           ) +  readFileBytes path = wrapIO (BS.readFile (T.unpack path))++  getFileType path = wrapIO (classifyPath (T.unpack path))+   runProcess cmd cmdArgs stdinText = wrapIO $ do     let cp =           (Proc.proc (T.unpack cmd) (map T.unpack cmdArgs))@@ -239,35 +245,47 @@       else pure path    forceThunk _ (Evaluated val) = pure val-  forceThunk evalFn (Thunk expr env ref) = do-    -- Per-thunk IORef memoization: read once, evaluate at most once,-    -- cache in the thunk's own cell.  Memory is naturally bounded —-    -- when the thunk is GC'd, so is its cached value.-    cached <- wrapIO (readIORef ref)-    case cached of-      Just val -> pure val-      Nothing -> do+  forceThunk evalFn (ThunkRef ref) = do+    -- Per-thunk IORef memoization.  On first force, overwrites+    -- Pending with Computed — dropping the Expr and Env so they+    -- become unreachable and are GC'd.  Matches C++ Nix which+    -- mutates Value structs in place.+    cell <- EvalIO (liftIO (readIORef ref))+    case cell of+      Computed val -> pure val+      Pending expr env -> do         val <- evalFn env expr-        wrapIO (writeIORef ref (Just val))+        EvalIO (liftIO (writeIORef ref (Computed val)))         pure val  -- --------------------------------------------------------------------------- -- Helpers -- --------------------------------------------------------------------------- --- | Classify a directory entry as @"regular"@, @"directory"@, or @"symlink"@--- (matching Nix's @builtins.readDir@ semantics).+-- | Classify a filesystem path as @"regular"@, @"directory"@, @"symlink"@,+-- or @"unknown"@ — matching Nix's @builtins.readDir@ / @readFileType@.+classifyPath :: FilePath -> IO Text+classifyPath fp =+  firstMatch+    "unknown"+    [ (Dir.pathIsSymbolicLink fp, "symlink"),+      (Dir.doesDirectoryExist fp, "directory"),+      (Dir.doesFileExist fp, "regular")+    ]++-- | Return the label of the first predicate that holds, or the default.+firstMatch :: Text -> [(IO Bool, Text)] -> IO Text+firstMatch def [] = pure def+firstMatch def ((test, label) : rest) =+  test >>= \case+    True -> pure label+    False -> firstMatch def rest++-- | Classify a directory entry (name relative to parent). classifyEntry :: FilePath -> FilePath -> IO (Text, Text) classifyEntry parentDir name = do-  let fullPath = parentDir </> name-  isSym <- Dir.pathIsSymbolicLink fullPath-  if isSym-    then pure (T.pack name, "symlink")-    else do-      isDir <- Dir.doesDirectoryExist fullPath-      if isDir-        then pure (T.pack name, "directory")-        else pure (T.pack name, "regular")+  ty <- classifyPath (parentDir </> name)+  pure (T.pack name, ty)  -- | Convert IO exceptions to eval errors. -- Guards against double-wrapping: if the exception is already a
src/Nix/Eval/Operator.hs view
@@ -14,9 +14,14 @@ import qualified Data.Map.Strict as Map import Data.Text (Text) import Nix.Eval.Types-  ( MonadEval (..),+  ( AttrSet (..),+    LazyBinding (..),+    MonadEval (..),     NixValue (..),     Thunk,+    attrSetElems,+    attrSetKeys,+    newLazyAttrCache,     thunkSameRef,     typeName,   )@@ -158,9 +163,9 @@   | length as /= length bs = pure False   | otherwise = listEqual forceFn as bs nixEqual forceFn (VAttrs as) (VAttrs bs)-  | Map.keys as /= Map.keys bs = pure False+  | attrSetKeys as /= attrSetKeys bs = pure False   | otherwise = do-      let pairs = zip (Map.elems as) (Map.elems bs)+      let pairs = zip (attrSetElems as) (attrSetElems bs)       results <- mapM (thunkPairEqual forceFn) pairs       pure (and results) nixEqual _ _ _ = pure False@@ -199,7 +204,38 @@  -- | Attribute set merge (//).  Right-biased: keys in the right -- operand shadow keys in the left.+--+-- When one side is a 'LazyAttrs', avoid full materialization by+-- merging binding recipes directly.  This is critical for nixpkgs+-- where the overlay system does @big_set // small_set@. evalUpdate :: (MonadEval m) => NixValue -> NixValue -> m NixValue-evalUpdate (VAttrs as) (VAttrs bs) = pure (VAttrs (Map.union bs as))+evalUpdate (VAttrs as) (VAttrs bs) = pure (VAttrs (mergeAttrSets as bs)) evalUpdate left right =   throwEvalError ("cannot merge " <> typeName left <> " and " <> typeName right)++-- | Merge two 'AttrSet's, right-biased.  Keeps 'LazyAttrs' lazy+-- when possible — avoids materializing 30k thunks for @big // small@.+-- Each 'LazyBinding' carries its own env, so merging sets from+-- different scopes is safe — no env confusion.+mergeAttrSets :: AttrSet -> AttrSet -> AttrSet+-- LazyAttrs // EagerAttrs: override binding recipes with pre-built thunks+mergeAttrSets (LazyAttrs bindings _cache) (EagerAttrs small) =+  let overrides = Map.map PreBuilt small+      merged = Map.union overrides bindings -- overrides shadow originals+      newCache = newLazyAttrCache merged+   in LazyAttrs merged newCache+-- EagerAttrs // LazyAttrs: lazy set wins on conflicts, eager fills gaps+mergeAttrSets (EagerAttrs small) (LazyAttrs bindings _cache) =+  let fallbacks = Map.map PreBuilt small+      merged = Map.union bindings fallbacks -- LazyAttrs keys win+      newCache = newLazyAttrCache merged+   in LazyAttrs merged newCache+-- LazyAttrs // LazyAttrs: merge binding maps, right wins.+-- Safe because each LazyBinding carries its own env.+mergeAttrSets (LazyAttrs leftBindings _) (LazyAttrs rightBindings _) =+  let merged = Map.union rightBindings leftBindings -- right wins+      newCache = newLazyAttrCache merged+   in LazyAttrs merged newCache+-- EagerAttrs // EagerAttrs: standard Map.union+mergeAttrSets (EagerAttrs as) (EagerAttrs bs) =+  EagerAttrs (Map.union bs as) -- right-biased: bs shadows as
src/Nix/Eval/StringInterp.hs view
@@ -114,7 +114,6 @@  -- | Drop a single trailing newline. stripTrailingNewline :: Text -> Text-stripTrailingNewline t-  | T.null t = t-  | T.last t == '\n' = T.init t-  | otherwise = t+stripTrailingNewline t = case T.unsnoc t of+  Just (prefix, '\n') -> prefix+  _ -> t
src/Nix/Eval/Types.hs view
@@ -9,7 +9,24 @@   ( -- * Values     NixValue (..),     Thunk (..),+    ThunkCell (..), +    -- * Attribute sets (lazy/eager unified abstraction)+    AttrSet (..),+    LazyBinding (..),+    attrSetLookup,+    attrSetKeys,+    attrSetToMap,+    attrSetFromMap,+    attrSetMember,+    attrSetNull,+    attrSetSize,+    attrSetElems,+    attrSetToAscList,+    attrSetMapWithKey,+    attrSetUnionWith,+    newLazyAttrCache,+     -- * String context     StringContextElement (..),     StringContext (..),@@ -22,6 +39,7 @@     envLookup,     envInsert,     envInsertThunk,+    envInsertMany,     pushWithScope,      -- * Thunk operations@@ -39,13 +57,14 @@   ) where -import Data.IORef (IORef, newIORef)+import Data.ByteString (ByteString)+import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Set (Set) import Data.Text (Text) import Nix.Derivation (Derivation)-import Nix.Expr.Types (Expr, Formals)+import Nix.Expr.Types (AttrKey (..), Expr (..), Formals, StringPart (..)) import Nix.Store.Path (StorePath) import System.IO.Unsafe (unsafePerformIO) @@ -80,34 +99,41 @@ -- Thunks -- --------------------------------------------------------------------------- --- | A thunk: either an unevaluated expression paired with its--- capturing environment, or an already-forced value.+-- | Contents of a thunk's memoization cell.  When forced, 'Pending'+-- is overwritten with 'Computed', dropping the Expr and Env — they+-- become unreachable and are GC'd.  This matches C++ Nix, which+-- overwrites Value structs in place to release closures.+data ThunkCell+  = -- | Not yet forced: expression + capturing environment.+    -- Expr is strict; Env is LAZY for knot-tying in rec {} and let.+    Pending !Expr Env+  | -- | Forced: the computed result.  Expr + Env are gone.+    Computed !NixValue+  deriving (Show)++-- | A thunk: either an IORef-backed memoization cell (deferred+-- evaluation), or an already-known value (no IORef needed). ----- Each unevaluated thunk carries an 'IORef' memoization cell that--- caches the result after the first force — matching real Nix, which--- mutates thunks in-place.  The IORef is allocated lazily via--- 'unsafePerformIO' in 'mkThunk' so that knot-tying in recursive--- @let@ and @rec {}@ works unchanged (the IORef doesn't participate--- in the knot).  Memory is naturally bounded: when a thunk is no--- longer reachable, both the thunk and its cached value are GC'd.+-- Each 'ThunkRef' carries an 'IORef ThunkCell'.  On first force,+-- the cell is overwritten from @Pending expr env@ to @Computed val@,+-- which drops all references to the Expr and Env — matching real Nix+-- which mutates thunks in-place.  The IORef is allocated via+-- 'unsafePerformIO' in 'mkThunk' so that knot-tying works unchanged. data Thunk-  = -- | Unevaluated: Expr is strict, Env is LAZY to allow-    -- knot-tying in recursive let and rec { }.  The IORef cell-    -- is written once on first force, then read on subsequent forces.-    Thunk !Expr Env !(IORef (Maybe NixValue))-  | Evaluated !NixValue+  = -- | Deferred thunk with memoization cell.+    ThunkRef !(IORef ThunkCell)+  | -- | Already-known value (no IORef overhead).+    Evaluated !NixValue  instance Show Thunk where   show (Evaluated val) = "Evaluated (" ++ show val ++ ")"-  show (Thunk expr _ _) = "Thunk (" ++ show expr ++ ") <env> <ref>"+  show (ThunkRef _) = "ThunkRef <ref>" --- | Structural equality — compares expressions for unevaluated thunks,--- values for evaluated ones.  Ignores the IORef cell and environment.--- Will diverge on recursive environments; only used in tests on--- non-recursive structures.+-- | Equality: compares values for 'Evaluated', IORef identity for+-- 'ThunkRef'.  Only used in tests on non-recursive structures. instance Eq Thunk where   (Evaluated v1) == (Evaluated v2) = v1 == v2-  (Thunk e1 _ _) == (Thunk e2 _ _) = e1 == e2+  (ThunkRef ref1) == (ThunkRef ref2) = ref1 == ref2   _ == _ = False  -- | A Nix value — the result of evaluating an expression.@@ -126,8 +152,8 @@     VPath !Text   | -- | List of thunks (lazy elements).     VList ![Thunk]-  | -- | Attribute set: name -> thunk (lazy values).-    VAttrs !(Map Text Thunk)+  | -- | Attribute set: unified lazy/eager representation.+    VAttrs !AttrSet   | -- | Lambda closure: captures environment, formals, body.     VLambda !Env !Formals !Expr   | -- | A realized derivation (build recipe).@@ -137,63 +163,294 @@     VBuiltin !Text ![NixValue]   deriving (Eq, Show) --- | Evaluation environment.+-- ---------------------------------------------------------------------------+-- Attribute sets (lazy/eager unified abstraction)+-- ---------------------------------------------------------------------------++-- | Per-key lazy binding recipe.  Used by 'LazyAttrs' to defer+-- thunk construction until first access.  Only ~50 of nixpkgs' 30k+-- packages are touched for a typical eval — storing recipes instead+-- of pre-built thunks avoids ~30k IORef allocations. ----- Variable lookup checks 'envBindings' first (lexical scope always wins),--- then walks 'envWithScopes' innermost-first.  This correctly handles--- nested @with@ expressions where inner scopes shadow outer ones, and--- lexical bindings always take priority.+-- Each binding carries its own 'Env' so that @//@ merges of two+-- 'LazyAttrs' with different environments remain correct — left-side+-- bindings keep their original env instead of being re-parented to the+-- right side's env.  The env is INTENTIONALLY LAZY for knot-tying in+-- @rec {}@.+data LazyBinding+  = -- | Simple @key = expr@ — deferred @mkThunk env expr@.+    -- Env is lazy for knot-tying in rec {}.+    LazyExpr Env !Expr+  | -- | @inherit key@ — deferred @inheritLookup env key@.+    -- Env is lazy for knot-tying in rec {}.+    LazyInherit Env+  | -- | @inherit (from) key@ — deferred @mkThunk env (ESelect from [StaticKey key] Nothing)@.+    -- Env is lazy for knot-tying in rec {}.+    LazyInheritFrom Env !Expr+  | -- | Pre-built thunk (for merged nested paths or other complex cases).+    PreBuilt !Thunk+  deriving (Eq, Show)++-- | Attribute set representation: either an eagerly-materialized map+-- of thunks, or a lazy binding map that defers thunk construction.+--+-- The 'LazyAttrs' constructor is used for @rec {}@, @let@, and+-- non-recursive attribute sets (e.g. nixpkgs' 30k-entry package set).+-- Thunks + IORefs are only allocated for keys that are actually accessed.+-- Each 'LazyBinding' carries its own 'Env', so @//@ merges of sets+-- from different scopes remain correct.+data AttrSet+  = -- | Eagerly-materialized attribute set (small sets, builtins).+    EagerAttrs !(Map Text Thunk)+  | -- | Lazy attribute set: thunks built on demand from binding recipes.+    -- Each 'LazyBinding' carries its own env (lazy for knot-tying).+    LazyAttrs+      -- | Key → binding recipe (O(log n) lookup)+      !(Map Text LazyBinding)+      -- | Materialized thunk cache (written via unsafePerformIO)+      !(IORef (Map Text Thunk))++instance Eq AttrSet where+  EagerAttrs a == EagerAttrs b = a == b+  a == b = attrSetToMap a == attrSetToMap b++instance Show AttrSet where+  show (EagerAttrs m) = show m+  show (LazyAttrs bindings _) =+    "<lazy " ++ show (Map.size bindings) ++ " bindings>"++-- | Look up a single key.  For 'EagerAttrs', pure 'Map.lookup'.+-- For 'LazyAttrs', checks the cache first, then materializes a+-- single thunk from the binding recipe on cache miss.+--+-- Uses 'unsafePerformIO' for cache writes — safe because this is+-- idempotent memoization with no observable side effects beyond+-- caching (same rationale as thunk memoization via 'newMemoCell').+attrSetLookup :: Text -> AttrSet -> Maybe Thunk+attrSetLookup key (EagerAttrs m) = Map.lookup key m+attrSetLookup key (LazyAttrs bindings cacheRef) =+  -- Check cache first, then binding map+  let cached = unsafePerformIO (readIORef cacheRef)+   in case Map.lookup key cached of+        Just thunk -> Just thunk+        Nothing -> case Map.lookup key bindings of+          Nothing -> Nothing+          Just recipe ->+            let thunk = materializeBinding key recipe+             in -- Cache the materialized thunk+                unsafePerformIO $ do+                  atomicModifyIORef' cacheRef (\c -> (Map.insert key thunk c, ()))+                  pure (Just thunk)++-- | Materialize a single 'LazyBinding' into a 'Thunk'.+-- Each binding carries its own env, so no separate env parameter needed.+materializeBinding :: Text -> LazyBinding -> Thunk+materializeBinding _key (LazyExpr env expr) = mkThunk env expr+materializeBinding key (LazyInherit env) = inheritLookupThunk env key+materializeBinding key (LazyInheritFrom env fromExpr) =+  mkThunk env (ESelect fromExpr [StaticKey key] Nothing)+materializeBinding _key (PreBuilt thunk) = thunk++-- | Look up a name for lazy @inherit@.  If not found, create a thunk+-- that will error when forced (matching real Nix behaviour).+inheritLookupThunk :: Env -> Text -> Thunk+inheritLookupThunk env name =+  case envLookup name env of+    Just thunk -> thunk+    Nothing ->+      let errMsg = "undefined variable '" <> name <> "'"+          throwExpr = EApp (EVar "throw") (EStr [StrLit errMsg])+       in mkThunk env throwExpr++-- | All keys in the attribute set.  For 'LazyAttrs', reads keys from+-- the binding map — zero thunk allocation.+attrSetKeys :: AttrSet -> [Text]+attrSetKeys (EagerAttrs m) = Map.keys m+attrSetKeys (LazyAttrs bindings _) = Map.keys bindings++-- | Check key membership without materializing thunks.+attrSetMember :: Text -> AttrSet -> Bool+attrSetMember key (EagerAttrs m) = Map.member key m+attrSetMember key (LazyAttrs bindings _) = Map.member key bindings++-- | Check if the attribute set is empty.+attrSetNull :: AttrSet -> Bool+attrSetNull (EagerAttrs m) = Map.null m+attrSetNull (LazyAttrs bindings _) = Map.null bindings++-- | Number of attributes.+attrSetSize :: AttrSet -> Int+attrSetSize (EagerAttrs m) = Map.size m+attrSetSize (LazyAttrs bindings _) = Map.size bindings++-- | Full materialization: build a 'Map Text Thunk' from all bindings.+-- For 'LazyAttrs', this allocates thunks + IORefs for every key —+-- expensive on large sets, avoid on the hot path.+--+-- Uses 'unsafePerformIO' to update the cache atomically.+attrSetToMap :: AttrSet -> Map Text Thunk+attrSetToMap (EagerAttrs m) = m+attrSetToMap (LazyAttrs bindings cacheRef) =+  unsafePerformIO $ do+    cached <- readIORef cacheRef+    if Map.size cached == Map.size bindings+      then pure cached+      else do+        let full = Map.mapWithKey (materializeOne cached) bindings+        atomicModifyIORef' cacheRef (const (full, ()))+        pure full+  where+    materializeOne cached key recipe =+      case Map.lookup key cached of+        Just thunk -> thunk+        Nothing -> materializeBinding key recipe++-- | All thunk values (materialized).+attrSetElems :: AttrSet -> [Thunk]+attrSetElems = Map.elems . attrSetToMap++-- | Sorted key-value pairs (materialized).+attrSetToAscList :: AttrSet -> [(Text, Thunk)]+attrSetToAscList = Map.toAscList . attrSetToMap++-- | Map a function over all key-value pairs (materializes lazy sets).+attrSetMapWithKey :: (Text -> Thunk -> Thunk) -> AttrSet -> AttrSet+attrSetMapWithKey f attrs = EagerAttrs (Map.mapWithKey f (attrSetToMap attrs))++-- | Union two attribute sets with a combining function (materializes both).+attrSetUnionWith :: (Thunk -> Thunk -> Thunk) -> AttrSet -> AttrSet -> AttrSet+attrSetUnionWith f a b = EagerAttrs (Map.unionWith f (attrSetToMap a) (attrSetToMap b))++-- | Wrap an eager map as an 'AttrSet'.+attrSetFromMap :: Map Text Thunk -> AttrSet+attrSetFromMap = EagerAttrs++-- | Allocate a fresh materialization cache for 'LazyAttrs'.+-- Uses 'unsafePerformIO' with @NOINLINE@ + @seq@ to ensure each+-- call site gets its own IORef (same pattern as 'newMemoCell').+{-# NOINLINE newLazyAttrCache #-}+newLazyAttrCache :: Map Text LazyBinding -> IORef (Map Text Thunk)+newLazyAttrCache bindings = unsafePerformIO (bindings `seq` newIORef Map.empty)++-- | Evaluation environment — scope chain.+--+-- Each Env holds only its LOCAL bindings and a parent pointer.+-- Variable lookup walks the chain: local bindings first, then parent,+-- then grandparent, etc.  With-scopes are checked AFTER exhausting+-- all lexical bindings in the chain (Nix: lexical always wins).+--+-- This avoids O(n) Map.union/insert when extending large envs (e.g.+-- the 30k-entry nixpkgs rec set).  A function application that adds+-- 5 formals creates 5 singleton Maps instead of 5 copies of the+-- 30k-entry map's internal Bin nodes. data Env = Env-  { envBindings :: !(Map Text Thunk),-    envWithScopes :: ![Map Text Thunk]+  { -- | Local lexical bindings at this scope level.+    envBindings :: !(Map Text Thunk),+    -- | Parent scope (Nothing at the root).  LAZY for knot-tying+    -- in rec {} where recEnv's parent is the outer env.+    envParent :: !(Maybe Env),+    -- | With-scopes, innermost first.  Inherited from parent on+    -- env extension; only 'pushWithScope' adds new entries.+    envWithScopes :: ![AttrSet]   }-  deriving (Eq, Show) +-- | Shallow comparison: checks local bindings and with-scopes only.+-- Ignores parent chain to avoid diverging on deep/recursive envs.+-- Only used in tests on non-recursive structures.+instance Eq Env where+  Env b1 _ w1 == Env b2 _ w2 = b1 == b2 && w1 == w2++-- | Compact show: just the size and structure, not the full contents.+instance Show Env where+  show (Env bindings parent withs) =+    "Env{"+      ++ show (Map.size bindings)+      ++ " local"+      ++ maybe "" (const ", parent") parent+      ++ ", "+      ++ show (length withs)+      ++ " withs}"+ -- | Empty environment (no variables in scope). emptyEnv :: Env-emptyEnv = Env Map.empty []+emptyEnv = Env Map.empty Nothing [] --- | Look up a variable: lexical bindings first, then with-scopes.+-- | Look up a variable: walk the parent chain checking lexical+-- bindings at each level; fall back to with-scopes (from the+-- starting env) only after exhausting all lexical scopes. envLookup :: Text -> Env -> Maybe Thunk-envLookup name (Env bindings withs) =-  case Map.lookup name bindings of-    Just val -> Just val-    Nothing -> lookupWithScopes name withs+envLookup name env = lexicalLookup env+  where+    -- With-scopes from the STARTING env: these are the most recent+    -- and subsume all ancestor with-scopes (children inherit them).+    withs = envWithScopes env+    lexicalLookup (Env bindings parent _) =+      case Map.lookup name bindings of+        Just val -> Just val+        Nothing -> case parent of+          Just p -> lexicalLookup p+          Nothing -> lookupWithScopes name withs  -- | Walk with-scopes innermost to outermost.-lookupWithScopes :: Text -> [Map Text Thunk] -> Maybe Thunk+-- Uses 'attrSetLookup' so that 'LazyAttrs' with-scopes only+-- materialize the accessed key, not the entire set.+lookupWithScopes :: Text -> [AttrSet] -> Maybe Thunk lookupWithScopes _ [] = Nothing lookupWithScopes name (scope : rest) =-  case Map.lookup name scope of+  case attrSetLookup name scope of     Just val -> Just val     Nothing -> lookupWithScopes name rest --- | Insert an already-forced value into the lexical bindings.+-- | Insert an already-forced value as a new scope level.+-- Creates a child env with a singleton Map — O(1) instead of+-- O(log n) Map.insert on the parent's (potentially large) Map. envInsert :: Text -> NixValue -> Env -> Env envInsert name val env =-  env {envBindings = Map.insert name (Evaluated val) (envBindings env)}+  Env+    { envBindings = Map.singleton name (Evaluated val),+      envParent = Just env,+      envWithScopes = envWithScopes env+    } --- | Insert a thunk into the lexical bindings.+-- | Insert a thunk as a new scope level. envInsertThunk :: Text -> Thunk -> Env -> Env envInsertThunk name thunk env =-  env {envBindings = Map.insert name thunk (envBindings env)}+  Env+    { envBindings = Map.singleton name thunk,+      envParent = Just env,+      envWithScopes = envWithScopes env+    } +-- | Batch-insert multiple bindings as a single scope level.+-- Creates ONE Env for all bindings instead of N singleton Envs.+-- Used by formal set matching: a function with N formals creates+-- one scope (one Map with N entries) instead of N scopes (N singleton Maps).+envInsertMany :: Map Text Thunk -> Env -> Env+envInsertMany bindings env =+  Env+    { envBindings = bindings,+      envParent = Just env,+      envWithScopes = envWithScopes env+    }+ -- | Push a with-scope onto the scope chain (innermost position).-pushWithScope :: Map Text Thunk -> Env -> Env+-- Accepts 'AttrSet' directly so 'LazyAttrs' with-scopes stay lazy.+-- Does not create a new parent level — just modifies the with-scope list.+pushWithScope :: AttrSet -> Env -> Env pushWithScope scope env =   env {envWithScopes = scope : envWithScopes env}  -- | Create an unevaluated thunk with a fresh memoization cell. ----- Each thunk gets its own 'IORef' for in-place memoization, matching--- real Nix which mutates @Value@ structs on first force.  The cell is--- allocated via @newMemoCell@ which uses 'unsafePerformIO' — safe--- because 'newIORef' is a pure allocation with no observable side--- effects, and the @NOINLINE@ + @seq@ pattern prevents GHC from--- floating the allocation to a shared top-level CAF.+-- Each thunk gets its own 'IORef ThunkCell'.  On first force the+-- cell is overwritten from @Pending@ to @Computed@, dropping the+-- Expr and Env.  The cell is allocated via @newMemoCell@ which uses+-- 'unsafePerformIO' — safe because 'newIORef' is a pure allocation,+-- and the @NOINLINE@ + @seq@ pattern prevents GHC from floating the+-- allocation to a shared top-level CAF. mkThunk :: Env -> Expr -> Thunk mkThunk env thunkExpr =-  Thunk thunkExpr env (newMemoCell thunkExpr)+  ThunkRef (newMemoCell thunkExpr env)  -- | Like 'mkThunk' but for synthetic thunks that reuse the same 'Expr' -- (e.g. @EApp (EVar "__fn") (EVar "__arg")@ in 'deferApply').  Uses the@@ -205,24 +462,27 @@ -- recursive envs), since it forces the bindings map to WHNF. mkSyntheticThunk :: Env -> Expr -> Thunk mkSyntheticThunk env thunkExpr =-  Thunk thunkExpr env (newSyntheticCell (envBindings env))+  ThunkRef (newSyntheticCell (envBindings env) thunkExpr env)  -- | Allocate a fresh memoization cell for a thunk. -- -- @NOINLINE@ prevents inlining so GHC can't see inside or CSE calls.--- @seq@ on the argument creates a data dependency that prevents--- float-out to a top-level CAF — without this, GHC would hoist--- @unsafePerformIO (newIORef Nothing)@ and share ONE cell across--- ALL thunks.+-- @seq@ on the expr creates a data dependency that prevents float-out+-- to a top-level CAF — without this, GHC would hoist the+-- @unsafePerformIO (newIORef ...)@ and share ONE cell across ALL thunks.+--+-- The cell starts as @Pending expr env@, putting both references inside+-- the IORef.  On force, it's overwritten to @Computed val@, dropping+-- the Expr and Env so they become unreachable. {-# NOINLINE newMemoCell #-}-newMemoCell :: Expr -> IORef (Maybe NixValue)-newMemoCell expr = unsafePerformIO (expr `seq` newIORef Nothing)+newMemoCell :: Expr -> Env -> IORef ThunkCell+newMemoCell expr env = unsafePerformIO (expr `seq` newIORef (Pending expr env))  -- | Like 'newMemoCell' but keyed on a bindings map instead of an expression. -- Used by 'mkSyntheticThunk' where multiple thunks share the same expression. {-# NOINLINE newSyntheticCell #-}-newSyntheticCell :: Map Text Thunk -> IORef (Maybe NixValue)-newSyntheticCell bindings = unsafePerformIO (bindings `seq` newIORef Nothing)+newSyntheticCell :: Map Text Thunk -> Expr -> Env -> IORef ThunkCell+newSyntheticCell bindings expr env = unsafePerformIO (bindings `seq` newIORef (Pending expr env))  -- | Wrap an already-computed value as a thunk. evaluated :: NixValue -> Thunk@@ -233,7 +493,7 @@ -- so deep equality can short-circuit to 'True' without forcing. -- Matching real Nix which uses pointer identity for same-thunk comparison. thunkSameRef :: Thunk -> Thunk -> Bool-thunkSameRef (Thunk _ _ ref1) (Thunk _ _ ref2) = ref1 == ref2+thunkSameRef (ThunkRef ref1) (ThunkRef ref2) = ref1 == ref2 thunkSameRef _ _ = False  -- | Human-readable type name for error messages.@@ -283,6 +543,13 @@   -- | Import a file with a custom scope overlaid on builtins.   scopedImportFile :: [(Text, Thunk)] -> Text -> m NixValue +  -- | Read raw bytes from a file.  Used by @builtins.hashFile@.+  readFileBytes :: Text -> m ByteString++  -- | Classify a single filesystem path as @"regular"@, @"directory"@,+  -- @"symlink"@, or @"unknown"@.  Used by @builtins.readFileType@.+  getFileType :: Text -> m Text+   -- | Run an external process: @(command, args, stdin) -> (exitCode, stdout, stderr)@.   runProcess :: Text -> [Text] -> Text -> m (Int, Text, Text) @@ -317,7 +584,14 @@   getCurrentTime = pure 0   writeToStore _ _ = throwEvalError "toFile: not available in pure evaluation"   scopedImportFile _ _ = throwEvalError "scopedImport: not available in pure evaluation"+  readFileBytes _ = throwEvalError "hashFile: not available in pure evaluation"+  getFileType _ = throwEvalError "readFileType: not available in pure evaluation"   runProcess _ _ _ = throwEvalError "runProcess: not available in pure evaluation"   resolvePathLiteral = pure   forceThunk _ (Evaluated val) = pure val-  forceThunk evalFn (Thunk expr env _) = evalFn env expr+  forceThunk evalFn (ThunkRef ref) =+    -- Read the cell via unsafePerformIO — safe because readIORef has no+    -- side effects and PureEval never writes back (no memoization).+    case unsafePerformIO (readIORef ref) of+      Computed val -> pure val+      Pending expr env -> evalFn env expr
src/Nix/Expr/Types.hs view
@@ -27,29 +27,10 @@      -- * String parts (interpolation)     StringPart (..),--    -- * Source locations-    SrcPos (..),-    SrcSpan (..),   ) where  import Data.Text (Text)---- | Source position for error reporting.-data SrcPos = SrcPos-  { spLine :: !Int,-    spCol :: !Int-  }-  deriving (Eq, Show)---- | Source span (start to end).-data SrcSpan = SrcSpan-  { ssFile :: !Text,-    ssStart :: !SrcPos,-    ssEnd :: !SrcPos-  }-  deriving (Eq, Show)  -- | Atomic (literal) values. data NixAtom
src/Nix/Parser/Expr.hs view
@@ -18,7 +18,6 @@ where  import Data.Text (Text)-import qualified Data.Text as T import Nix.Expr.Types import Nix.Parser.Internal import Nix.Parser.Lexer (Token (..))@@ -36,7 +35,7 @@     then pure expr     else do       tok <- peek-      parseError ("unexpected " <> showTok tok <> " after expression")+      parseError ("unexpected " <> showToken tok <> " after expression")  -- --------------------------------------------------------------------------- -- Expression (entry point for precedence climbing)@@ -105,7 +104,7 @@       name <- expectIdent       expect TokColon       ELambda (FormalNamedSet name formals hasEllipsis) <$> parseExpr-    _ -> parseError ("expected ':' or '@' after formals, got " <> showTok tok)+    _ -> parseError ("expected ':' or '@' after formals, got " <> showToken tok)  -- | Parse the inside of @{ ... }@ formals (comma-separated, optional defaults, -- optional ellipsis). Expects the closing @}@.@@ -132,7 +131,7 @@       then Just <$> parseExpr       else pure Nothing   let formal = Formal {fName = name, fDefault = defVal}-      newAcc = acc ++ [formal]+      newAcc = formal : acc   tok <- peek   case tok of     TokComma -> do@@ -143,15 +142,15 @@         TokEllipsis -> do           _ <- advance           expect TokRBrace-          pure (newAcc, True)+          pure (reverse newAcc, True)         TokRBrace -> do           _ <- advance-          pure (newAcc, False)+          pure (reverse newAcc, False)         _ -> parseFormalsList newAcc     TokRBrace -> do       _ <- advance-      pure (newAcc, False)-    _ -> parseError ("expected ',' or '}' in formals, got " <> showTok tok)+      pure (reverse newAcc, False)+    _ -> parseError ("expected ',' or '}' in formals, got " <> showToken tok)  -- --------------------------------------------------------------------------- -- Precedence levels (lowest to highest)@@ -389,7 +388,7 @@     TokIf -> parseIf     TokWith -> parseWith     TokAssert -> parseAssert-    _ -> parseError ("unexpected " <> showTok tok)+    _ -> parseError ("unexpected " <> showToken tok)  -- --------------------------------------------------------------------------- -- Strings@@ -424,7 +423,7 @@           expr <- parseExpr           expect TokInterpClose           go (StrInterp expr : acc)-        _ -> parseError ("unexpected " <> showTok tok <> " in string")+        _ -> parseError ("unexpected " <> showToken tok <> " in string")  -- --------------------------------------------------------------------------- -- Compound expressions@@ -454,7 +453,7 @@         _ | canStartAtom tok -> do           elem_ <- parseSelect           go (elem_ : acc)-        _ -> parseError ("unexpected " <> showTok tok <> " in list")+        _ -> parseError ("unexpected " <> showToken tok <> " in list")  parseAttrSet :: Bool -> Parser Expr parseAttrSet isRec =@@ -509,13 +508,13 @@       case tok of         TokIdent name -> do           _ <- advance-          go (acc ++ [name])+          go (name : acc)         -- Quoted strings for keywords used as attribute names: inherit "or";         TokStringOpen -> do           _ <- advance           name <- parseQuotedInheritName-          go (acc ++ [name])-        _ -> pure acc+          go (name : acc)+        _ -> pure (reverse acc)  -- | Parse a quoted inherit name: a simple string literal (no interpolation). -- Used for keywords like @"or"@ in @inherit (self.trivial) "or";@.@@ -530,7 +529,7 @@     TokStringClose -> do       _ <- advance       pure ""-    _ -> parseError ("expected string literal in inherit, got " <> showTok tok)+    _ -> parseError ("expected string literal in inherit, got " <> showToken tok)  parseAttrPath :: Parser AttrPath parseAttrPath = do@@ -553,7 +552,7 @@       -- mode — } is TokRBrace rather than TokInterpClose.       expect TokRBrace       pure (DynamicKey expr)-    _ -> parseError ("expected attribute key, got " <> showTok tok)+    _ -> parseError ("expected attribute key, got " <> showToken tok)  parseLet :: Parser Expr parseLet = do@@ -621,63 +620,7 @@ canStartAtom TokLBracket = True canStartAtom TokRec = True canStartAtom TokLet = True+canStartAtom TokIf = True+canStartAtom TokWith = True+canStartAtom TokAssert = True canStartAtom _ = False---- | Show a token for error messages (short form).-showTok :: Token -> Text-showTok TokEOF = "end of input"-showTok TokIf = "'if'"-showTok TokThen = "'then'"-showTok TokElse = "'else'"-showTok TokLet = "'let'"-showTok TokIn = "'in'"-showTok TokWith = "'with'"-showTok TokAssert = "'assert'"-showTok TokRec = "'rec'"-showTok TokInherit = "'inherit'"-showTok TokTrue = "'true'"-showTok TokFalse = "'false'"-showTok TokNull = "'null'"-showTok (TokIdent n) = "'" <> n <> "'"-showTok (TokInt n) = T.pack (show n)-showTok (TokFloat d) = T.pack (show d)-showTok (TokUri u) = "URI '" <> u <> "'"-showTok (TokPath p) = "path '" <> p <> "'"-showTok (TokSearchPath p) = "'<" <> p <> ">'"-showTok TokStringOpen = "'\"'"-showTok TokStringClose = "'\"'"-showTok TokIndStringOpen = "'''"-showTok TokIndStringClose = "'''"-showTok (TokStringLit _) = "string literal"-showTok TokInterpOpen = "'${'"-showTok TokInterpClose = "'}'"-showTok TokPlus = "'+'"-showTok TokMinus = "'-'"-showTok TokStar = "'*'"-showTok TokSlash = "'/'"-showTok TokConcat = "'++'"-showTok TokUpdate = "'//'"-showTok TokNot = "'!'"-showTok TokAnd = "'&&'"-showTok TokOr = "'||'"-showTok TokImpl = "'->'"-showTok TokEq = "'=='"-showTok TokNeq = "'!='"-showTok TokLt = "'<'"-showTok TokLte = "'<='"-showTok TokGt = "'>'"-showTok TokGte = "'>='"-showTok TokQuestion = "'?'"-showTok TokDot = "'.'"-showTok TokEllipsis = "'...'"-showTok TokAt = "'@'"-showTok TokColon = "':'"-showTok TokSemicolon = "';'"-showTok TokAssign = "'='"-showTok TokComma = "','"-showTok TokLParen = "'('"-showTok TokRParen = "')'"-showTok TokLBrace = "'{'"-showTok TokRBrace = "'}'"-showTok TokLBracket = "'['"-showTok TokRBracket = "']'"
src/Nix/Parser/Internal.hs view
@@ -18,10 +18,6 @@     match,     tryParser,     pMany,-    pSome,-    sepBy,-    sepBy1,-    pOptional,     atEnd,      -- * Specific expects@@ -29,9 +25,9 @@      -- * Errors     parseError,-    unexpectedEOF,-    expectedButGot,-    currentPos,++    -- * Token display+    showToken,   ) where @@ -73,12 +69,6 @@ runParser :: Parser a -> ParseState -> Either ParseError (a, ParseState) runParser = unParser --- | Get current position for error messages.-currentPos :: Parser (Int, Int)-currentPos = Parser $ \st -> case psTokens st of-  (Located ln col _ : _) -> Right ((ln, col), st)-  [] -> Right ((0, 0), st)- -- | Peek at the next token without consuming it. peek :: Parser Token peek = Parser $ \st -> case psTokens st of@@ -143,34 +133,6 @@         Nothing -> pure (reverse acc)         Just val -> go (val : acc) --- | Parse one or more occurrences.-pSome :: Parser a -> Parser [a]-pSome p = do-  first <- p-  rest <- pMany p-  pure (first : rest)---- | Parse zero or more separated by a delimiter token.-sepBy :: Parser a -> Token -> Parser [a]-sepBy p sep = do-  result <- tryParser p-  case result of-    Nothing -> pure []-    Just first -> do-      rest <- pMany (expect sep >> p)-      pure (first : rest)---- | Parse one or more separated by a delimiter token.-sepBy1 :: Parser a -> Token -> Parser [a]-sepBy1 p sep = do-  first <- p-  rest <- pMany (expect sep >> p)-  pure (first : rest)---- | Optionally parse something.-pOptional :: Parser a -> Parser (Maybe a)-pOptional = tryParser- -- | Check if we've consumed all tokens. atEnd :: Parser Bool atEnd = Parser $ \st -> case psTokens st of@@ -184,37 +146,6 @@   (Located ln col _ : _) ->     Left ParseError {peFile = psFile st, peLine = ln, peCol = col, peMessage = msg}   [] -> Left (makeEOF st)---- | Raise an "unexpected end of input" error.-unexpectedEOF :: Text -> Parser a-unexpectedEOF ctx = Parser $ \st ->-  Left-    ParseError-      { peFile = psFile st,-        peLine = 0,-        peCol = 0,-        peMessage = "unexpected end of input in " <> ctx-      }---- | Raise an "expected X but got Y" error.-expectedButGot :: Text -> Token -> Parser a-expectedButGot expected tok = Parser $ \st -> case psTokens st of-  (Located ln col _ : _) ->-    Left-      ParseError-        { peFile = psFile st,-          peLine = ln,-          peCol = col,-          peMessage = "expected " <> expected <> " but got " <> showToken tok-        }-  [] ->-    Left-      ParseError-        { peFile = psFile st,-          peLine = 0,-          peCol = 0,-          peMessage = "expected " <> expected <> " but got end of input"-        }  -- | Consume and return an identifier, or fail. expectIdent :: Parser Text
src/Nix/Parser/Lexer.hs view
@@ -13,9 +13,10 @@ where  import Data.Char (isAlpha, isAlphaNum, isDigit, isSpace)-import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as TB import Nix.Parser.ParseError (ParseError (..))  -- | A positioned token.@@ -143,151 +144,145 @@   _ -> lexNormalMode st acc  lexNormalMode :: LexState -> [Located] -> Either ParseError [Located]-lexNormalMode st acc-  | T.null (lsInput st) = Right (reverse (Located (lsLine st) (lsCol st) TokEOF : acc))-  | otherwise =-      let c = T.head (lsInput st)-          rest = T.tail (lsInput st)-       in case c of-            _ | isSpace c -> lexNormalMode (skipWhitespace st) acc-            '#' -> lexNormalMode (skipLineComment st) acc-            '/'-              | Just '*' <- safeHead rest ->-                  case skipBlockComment (advanceCol 2 st {lsInput = T.drop 2 (lsInput st)}) of-                    Left err -> Left err-                    Right newSt -> lexNormalMode newSt acc-            '"' ->-              let tok = Located (lsLine st) (lsCol st) TokStringOpen-                  newSt = advanceCol 1 st {lsInput = rest, lsModes = ModeString : lsModes st}+lexNormalMode st acc = case T.uncons (lsInput st) of+  Nothing -> Right (reverse (Located (lsLine st) (lsCol st) TokEOF : acc))+  Just (c, rest) -> case c of+    _ | isSpace c -> lexNormalMode (skipWhitespace st) acc+    '#' -> lexNormalMode (skipLineComment st) acc+    '/'+      | Just '*' <- safeHead rest ->+          case skipBlockComment (advanceCol 2 st {lsInput = T.drop 2 (lsInput st)}) of+            Left err -> Left err+            Right newSt -> lexNormalMode newSt acc+    '"' ->+      let tok = Located (lsLine st) (lsCol st) TokStringOpen+          newSt = advanceCol 1 st {lsInput = rest, lsModes = ModeString : lsModes st}+       in lexLoop newSt (tok : acc)+    '\''+      | Just '\'' <- safeHead rest ->+          let tok = Located (lsLine st) (lsCol st) TokIndStringOpen+              newSt = advanceCol 2 st {lsInput = T.drop 2 (lsInput st), lsModes = ModeIndString : lsModes st}+           in lexLoop newSt (tok : acc)+    '.'+      | Just '.' <- safeHead rest,+        Just '.' <- safeHead (T.drop 1 rest) ->+          emit3 st TokEllipsis acc+    '.'+      | Just '/' <- safeHead rest ->+          lexPath st acc+      | Just '.' <- safeHead rest,+        Just c2 <- safeHead (T.drop 1 rest),+        c2 == '/' ->+          lexPath st acc+    '.' -> emit1 st TokDot acc+    ',' -> emit1 st TokComma acc+    ';' -> emit1 st TokSemicolon acc+    ':' -> emit1 st TokColon acc+    '@' -> emit1 st TokAt acc+    '?' -> emit1 st TokQuestion acc+    '(' -> emit1 st TokLParen acc+    ')' -> emit1 st TokRParen acc+    '[' -> emit1 st TokLBracket acc+    ']' -> emit1 st TokRBracket acc+    '{' ->+      let tok = Located (lsLine st) (lsCol st) TokLBrace+          newSt = advanceCol 1 st {lsInput = rest, lsBraceDepth = lsBraceDepth st + 1}+       in lexNormalMode newSt (tok : acc)+    '}' ->+      case lsModes st of+        -- closing an interpolation: pop back to string/indstring mode+        (ModeNormal : outerMode : restModes)+          | lsBraceDepth st == 0,+            outerMode == ModeString || outerMode == ModeIndString ->+              let tok = Located (lsLine st) (lsCol st) TokInterpClose+                  newSt = advanceCol 1 st {lsInput = rest, lsModes = outerMode : restModes}                in lexLoop newSt (tok : acc)-            '\''-              | Just '\'' <- safeHead rest ->-                  let tok = Located (lsLine st) (lsCol st) TokIndStringOpen-                      newSt = advanceCol 2 st {lsInput = T.drop 2 (lsInput st), lsModes = ModeIndString : lsModes st}-                   in lexLoop newSt (tok : acc)-            '.'-              | Just '.' <- safeHead rest,-                Just '.' <- safeHead (T.drop 1 rest) ->-                  emit3 st TokEllipsis acc-            '.'-              | Just '/' <- safeHead rest ->-                  lexPath st acc-              | Just '.' <- safeHead rest,-                Just c2 <- safeHead (T.drop 1 rest),-                c2 == '/' ->-                  lexPath st acc-            '.' -> emit1 st TokDot acc-            ',' -> emit1 st TokComma acc-            ';' -> emit1 st TokSemicolon acc-            ':' -> emit1 st TokColon acc-            '@' -> emit1 st TokAt acc-            '?' -> emit1 st TokQuestion acc-            '(' -> emit1 st TokLParen acc-            ')' -> emit1 st TokRParen acc-            '[' -> emit1 st TokLBracket acc-            ']' -> emit1 st TokRBracket acc-            '{' ->-              let tok = Located (lsLine st) (lsCol st) TokLBrace-                  newSt = advanceCol 1 st {lsInput = rest, lsBraceDepth = lsBraceDepth st + 1}-               in lexNormalMode newSt (tok : acc)-            '}' ->-              case lsModes st of-                -- closing an interpolation: pop back to string/indstring mode-                (ModeNormal : outerMode : restModes)-                  | lsBraceDepth st == 0,-                    outerMode == ModeString || outerMode == ModeIndString ->-                      let tok = Located (lsLine st) (lsCol st) TokInterpClose-                          newSt = advanceCol 1 st {lsInput = rest, lsModes = outerMode : restModes}-                       in lexLoop newSt (tok : acc)-                _ ->-                  let depth = lsBraceDepth st-                      newDepth = if depth > 0 then depth - 1 else 0-                      tok = Located (lsLine st) (lsCol st) TokRBrace-                      newSt = advanceCol 1 st {lsInput = rest, lsBraceDepth = newDepth}-                   in lexNormalMode newSt (tok : acc)-            '+' | Just '+' <- safeHead rest -> emit2 st TokConcat acc-            '+' -> emit1 st TokPlus acc-            '*' -> emit1 st TokStar acc-            '-' | Just '>' <- safeHead rest -> emit2 st TokImpl acc-            '-' -> emit1 st TokMinus acc-            '!' | Just '=' <- safeHead rest -> emit2 st TokNeq acc-            '!' -> emit1 st TokNot acc-            '&' | Just '&' <- safeHead rest -> emit2 st TokAnd acc-            '|' | Just '|' <- safeHead rest -> emit2 st TokOr acc-            '=' | Just '=' <- safeHead rest -> emit2 st TokEq acc-            '=' -> emit1 st TokAssign acc-            '<' | Just '=' <- safeHead rest -> emit2 st TokLte acc-            '<'-              | isAlpha (fromMaybe ' ' (safeHead rest)) || (safeHead rest == Just '_') ->-                  lexSearchPath st acc-            '<' -> emit1 st TokLt acc-            '>' | Just '=' <- safeHead rest -> emit2 st TokGte acc-            '>' -> emit1 st TokGt acc-            '/' | Just '/' <- safeHead rest -> emit2 st TokUpdate acc-            '/'-              -- After an expression token, / is always division-              | prevCanEndExpr -> emit1 st TokSlash acc-              -- Not after expression: / followed by path char starts an absolute path-              | Just c2 <- safeHead rest, isPathChar c2 -> lexPath st acc-              -- Otherwise, division-              | otherwise -> emit1 st TokSlash acc-              where-                prevCanEndExpr = case acc of-                  (Located _ _ tok : _) -> canFollowWithDivision tok-                  [] -> False-            '$'-              | Just '{' <- safeHead rest ->-                  emit2 st TokInterpOpen acc-            '~'-              | Just '/' <- safeHead rest ->-                  lexPath st acc-            _ | isDigit c -> lexNumber st acc-            _ | isIdentStart c -> lexIdentOrKeyword st acc-            _ ->-              Left-                ParseError-                  { peFile = lsFile st,-                    peLine = lsLine st,-                    peCol = lsCol st,-                    peMessage = "unexpected character: " <> T.singleton c-                  }---- ------------------------------------------------------------------------------ String modes--- -----------------------------------------------------------------------------lexStringMode :: LexState -> [Located] -> Either ParseError [Located]-lexStringMode st acc-  | T.null (lsInput st) =+        _ ->+          let depth = lsBraceDepth st+              newDepth = if depth > 0 then depth - 1 else 0+              tok = Located (lsLine st) (lsCol st) TokRBrace+              newSt = advanceCol 1 st {lsInput = rest, lsBraceDepth = newDepth}+           in lexNormalMode newSt (tok : acc)+    '+' | Just '+' <- safeHead rest -> emit2 st TokConcat acc+    '+' -> emit1 st TokPlus acc+    '*' -> emit1 st TokStar acc+    '-' | Just '>' <- safeHead rest -> emit2 st TokImpl acc+    '-' -> emit1 st TokMinus acc+    '!' | Just '=' <- safeHead rest -> emit2 st TokNeq acc+    '!' -> emit1 st TokNot acc+    '&' | Just '&' <- safeHead rest -> emit2 st TokAnd acc+    '|' | Just '|' <- safeHead rest -> emit2 st TokOr acc+    '=' | Just '=' <- safeHead rest -> emit2 st TokEq acc+    '=' -> emit1 st TokAssign acc+    '<' | Just '=' <- safeHead rest -> emit2 st TokLte acc+    '<'+      | maybe False (\ch -> isAlpha ch || ch == '_') (safeHead rest) ->+          lexSearchPath st acc+    '<' -> emit1 st TokLt acc+    '>' | Just '=' <- safeHead rest -> emit2 st TokGte acc+    '>' -> emit1 st TokGt acc+    '/' | Just '/' <- safeHead rest -> emit2 st TokUpdate acc+    '/'+      -- After an expression token, / is always division+      | prevCanEndExpr -> emit1 st TokSlash acc+      -- Not after expression: / followed by path char starts an absolute path+      | Just c2 <- safeHead rest, isPathChar c2 -> lexPath st acc+      -- Otherwise, division+      | otherwise -> emit1 st TokSlash acc+      where+        prevCanEndExpr = case acc of+          (Located _ _ tok : _) -> canFollowWithDivision tok+          [] -> False+    '$'+      | Just '{' <- safeHead rest ->+          emit2 st TokInterpOpen acc+    '~'+      | Just '/' <- safeHead rest ->+          lexPath st acc+    _ | isDigit c -> lexNumber st acc+    _ | isIdentStart c -> lexIdentOrKeyword st acc+    _ ->       Left         ParseError           { peFile = lsFile st,             peLine = lsLine st,             peCol = lsCol st,-            peMessage = "unterminated string"+            peMessage = "unexpected character: " <> T.singleton c           }-  | otherwise =-      let c = T.head (lsInput st)-          rest = T.tail (lsInput st)-       in case c of-            '"' ->-              let tok = Located (lsLine st) (lsCol st) TokStringClose-                  newSt = advanceCol 1 st {lsInput = rest, lsModes = safeTail (lsModes st)}-               in lexLoop newSt (tok : acc)-            '$'-              | Just '{' <- safeHead rest ->-                  let tok = Located (lsLine st) (lsCol st) TokInterpOpen-                      newSt =-                        advanceCol-                          2-                          st-                            { lsInput = T.drop 1 rest,-                              lsModes = ModeNormal : lsModes st,-                              lsBraceDepth = 0-                            }-                   in lexLoop newSt (tok : acc)-            _ -> lexStringLiteral st acc +-- ---------------------------------------------------------------------------+-- String modes+-- ---------------------------------------------------------------------------++lexStringMode :: LexState -> [Located] -> Either ParseError [Located]+lexStringMode st acc = case T.uncons (lsInput st) of+  Nothing ->+    Left+      ParseError+        { peFile = lsFile st,+          peLine = lsLine st,+          peCol = lsCol st,+          peMessage = "unterminated string"+        }+  Just (c, rest) -> case c of+    '"' ->+      let tok = Located (lsLine st) (lsCol st) TokStringClose+          newSt = advanceCol 1 st {lsInput = rest, lsModes = safeTail (lsModes st)}+       in lexLoop newSt (tok : acc)+    '$'+      | Just '{' <- safeHead rest ->+          let tok = Located (lsLine st) (lsCol st) TokInterpOpen+              newSt =+                advanceCol+                  2+                  st+                    { lsInput = T.drop 1 rest,+                      lsModes = ModeNormal : lsModes st,+                      lsBraceDepth = 0+                    }+           in lexLoop newSt (tok : acc)+    _ -> lexStringLiteral st acc+ lexIndStringMode :: LexState -> [Located] -> Either ParseError [Located] lexIndStringMode st acc   | T.null (lsInput st) =@@ -357,61 +352,66 @@             _ -> lexIndStringLiteral st acc  -- | Lex a literal segment inside a regular string.+-- Uses 'TB.Builder' for O(1) amortized append instead of O(n) 'T.snoc'. lexStringLiteral :: LexState -> [Located] -> Either ParseError [Located]-lexStringLiteral st0 acc = go st0 T.empty+lexStringLiteral st0 acc = go st0 mempty   where-    go st chunk-      | T.null (lsInput st) =-          Left-            ParseError-              { peFile = lsFile st,-                peLine = lsLine st,-                peCol = lsCol st,-                peMessage = "unterminated string"-              }-      | otherwise =-          let c = T.head (lsInput st)-              rest = T.tail (lsInput st)-           in case c of-                '"' -> finishChunk st chunk-                '$' | Just '{' <- safeHead rest -> finishChunk st chunk-                '\\' -> case T.uncons rest of-                  Just (ec, rest2) ->-                    let escaped = case ec of-                          'n' -> "\n"-                          't' -> "\t"-                          'r' -> "\r"-                          '\\' -> "\\"-                          '"' -> "\""-                          '$' -> "$"-                          _ -> T.cons '\\' (T.singleton ec)-                        newSt = advanceBy ec (advanceCol 1 st {lsInput = rest2})-                     in go newSt (chunk <> escaped)-                  Nothing ->-                    Left-                      ParseError-                        { peFile = lsFile st,-                          peLine = lsLine st,-                          peCol = lsCol st,-                          peMessage = "unterminated escape in string"-                        }-                '\n' ->-                  let newSt = st {lsInput = rest, lsLine = lsLine st + 1, lsCol = 1}-                   in go newSt (chunk <> "\n")-                _ ->-                  let newSt = advanceCol 1 st {lsInput = rest}-                   in go newSt (T.snoc chunk c)-    finishChunk st chunk-      | T.null chunk = lexStringMode st acc+    go st !builder = case T.uncons (lsInput st) of+      Nothing ->+        Left+          ParseError+            { peFile = lsFile st,+              peLine = lsLine st,+              peCol = lsCol st,+              peMessage = "unterminated string"+            }+      Just (c, rest) -> case c of+        '"' -> finishChunk st builder+        '$' | Just '{' <- safeHead rest -> finishChunk st builder+        '\\' -> case T.uncons rest of+          Just (ec, rest2) ->+            let escaped = case ec of+                  'n' -> TB.singleton '\n'+                  't' -> TB.singleton '\t'+                  'r' -> TB.singleton '\r'+                  '\\' -> TB.singleton '\\'+                  '"' -> TB.singleton '"'+                  '$' -> TB.singleton '$'+                  _ -> TB.singleton '\\' <> TB.singleton ec+                newSt = advanceBy ec (advanceCol 1 st {lsInput = rest2})+             in go newSt (builder <> escaped)+          Nothing ->+            Left+              ParseError+                { peFile = lsFile st,+                  peLine = lsLine st,+                  peCol = lsCol st,+                  peMessage = "unterminated escape in string"+                }+        '\n' ->+          let newSt = st {lsInput = rest, lsLine = lsLine st + 1, lsCol = 1}+           in go newSt (builder <> TB.singleton '\n')+        _ ->+          let newSt = advanceCol 1 st {lsInput = rest}+           in go newSt (builder <> TB.singleton c)+    finishChunk st builder+      | builderIsEmpty = lexStringMode st acc       | otherwise =-          let tok = Located (lsLine st0) (lsCol st0) (TokStringLit chunk)+          let chunk = TL.toStrict (TB.toLazyText builder)+              tok = Located (lsLine st0) (lsCol st0) (TokStringLit chunk)            in lexStringMode st (tok : acc)+      where+        builderIsEmpty = TL.null (TB.toLazyText builder)  -- | Lex a literal segment inside an indented string.+-- No escape sequences here (those are handled by 'lexIndStringMode'),+-- so the chunk is identical to the source text — count chars, then slice+-- once at the end.  This avoids O(n^2) 'T.snoc' allocation. lexIndStringLiteral :: LexState -> [Located] -> Either ParseError [Located]-lexIndStringLiteral st0 acc = go st0 T.empty+lexIndStringLiteral st0 acc = go st0 0   where-    go st chunk+    startInput = lsInput st0+    go st !consumed       | T.null (lsInput st) =           Left             ParseError@@ -425,16 +425,16 @@            in case T.uncons input of                 Just ('\'', rest1)                   | Just ('\'', _) <- T.uncons rest1 ->-                      finishChunk st chunk+                      finishChunk st consumed                 Just ('$', rest1)                   | Just ('{', _) <- T.uncons rest1 ->-                      finishChunk st chunk+                      finishChunk st consumed                 Just ('\n', rest1) ->                   let newSt = st {lsInput = rest1, lsLine = lsLine st + 1, lsCol = 1}-                   in go newSt (T.snoc chunk '\n')-                Just (c, rest1) ->+                   in go newSt (consumed + 1)+                Just (_c, rest1) ->                   let newSt = advanceCol 1 st {lsInput = rest1}-                   in go newSt (T.snoc chunk c)+                   in go newSt (consumed + 1)                 Nothing ->                   Left                     ParseError@@ -443,10 +443,11 @@                         peCol = lsCol st,                         peMessage = "unterminated indented string"                       }-    finishChunk st chunk-      | T.null chunk = lexIndStringMode st acc+    finishChunk st consumed+      | consumed == 0 = lexIndStringMode st acc       | otherwise =-          let tok = Located (lsLine st0) (lsCol st0) (TokStringLit chunk)+          let chunk = T.take consumed startInput+              tok = Located (lsLine st0) (lsCol st0) (TokStringLit chunk)            in lexIndStringMode st (tok : acc)  -- ---------------------------------------------------------------------------@@ -459,8 +460,8 @@       len = T.length digits    in case T.uncons after of         Just ('.', after2)-          | not (T.null after2),-            isDigit (T.head after2) ->+          | Just (d, _) <- T.uncons after2,+            isDigit d ->               let (decimals, after3) = T.span isDigit after2                   fullLen = T.length digits + 1 + T.length decimals                   val = readDouble digits decimals@@ -550,7 +551,7 @@ lexSearchPath :: LexState -> [Located] -> Either ParseError [Located] lexSearchPath st acc =   -- st is at '<', skip it-  let after = T.tail (lsInput st)+  let after = T.drop 1 (lsInput st)       (name, after2) = T.span isSearchPathChar after    in case T.uncons after2 of         Just ('>', after3) ->@@ -580,24 +581,21 @@         _ -> st {lsInput = after}  skipBlockComment :: LexState -> Either ParseError LexState-skipBlockComment st-  | T.null (lsInput st) =-      Left-        ParseError-          { peFile = lsFile st,-            peLine = lsLine st,-            peCol = lsCol st,-            peMessage = "unterminated block comment"-          }-  | otherwise =-      let c = T.head (lsInput st)-          rest = T.tail (lsInput st)-       in case c of-            '*'-              | Just '/' <- safeHead rest ->-                  Right (advanceCol 2 st {lsInput = T.tail rest})-            '\n' -> skipBlockComment st {lsInput = rest, lsLine = lsLine st + 1, lsCol = 1}-            _ -> skipBlockComment (advanceCol 1 st {lsInput = rest})+skipBlockComment st = case T.uncons (lsInput st) of+  Nothing ->+    Left+      ParseError+        { peFile = lsFile st,+          peLine = lsLine st,+          peCol = lsCol st,+          peMessage = "unterminated block comment"+        }+  Just (c, rest) -> case c of+    '*'+      | Just '/' <- safeHead rest ->+          Right (advanceCol 2 st {lsInput = T.drop 1 rest})+    '\n' -> skipBlockComment st {lsInput = rest, lsLine = lsLine st + 1, lsCol = 1}+    _ -> skipBlockComment (advanceCol 1 st {lsInput = rest})  -- --------------------------------------------------------------------------- -- Character predicates@@ -644,7 +642,7 @@ emit1 :: LexState -> Token -> [Located] -> Either ParseError [Located] emit1 st tok acc =   let located = Located (lsLine st) (lsCol st) tok-      newSt = advanceCol 1 st {lsInput = T.tail (lsInput st)}+      newSt = advanceCol 1 st {lsInput = T.drop 1 (lsInput st)}    in lexNormalMode newSt (located : acc)  emit2 :: LexState -> Token -> [Located] -> Either ParseError [Located]
src/Nix/Store.hs view
@@ -60,6 +60,7 @@ import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as TE+import qualified Data.Text.IO as TIO import Nix.Derivation (Derivation, toATerm) import Nix.Store.DB import Nix.Store.Path@@ -238,4 +239,4 @@   let destPath = storePathToFilePath (stDir store) sp       aterm = toATerm drv   createDirectoryIfMissing True (unStoreDir (stDir store))-  writeFile destPath (T.unpack aterm)+  TIO.writeFile destPath aterm
test/Main.hs view
@@ -15,7 +15,7 @@ import Nix.Builtins (builtinEnv, parseNixPath) import qualified Nix.DependencyGraph as DepGraph import Nix.Derivation (Derivation (..), DerivationOutput (..), Platform (..), currentPlatform, fromATerm, platformToText, toATerm)-import Nix.Eval (Env (..), NixValue (..), StringContext (..), StringContextElement (..), Thunk (..), emptyContext, emptyEnv, eval, mkStr, runPureEval)+import Nix.Eval (Env (..), NixValue (..), StringContext (..), StringContextElement (..), Thunk (..), attrSetFromMap, attrSetLookup, attrSetNull, attrSetSize, emptyContext, emptyEnv, eval, mkStr, runPureEval) import qualified Nix.Eval.Context as Context import Nix.Eval.IO (EvalState (..), newEvalState, runEvalIO) import Nix.Expr.Types@@ -1145,7 +1145,7 @@       runTest "functionArgs no default" $         assertEval "funcArgs-nodef" "(builtins.functionArgs ({ a, b ? 1 }: a)).a" (VBool False),       runTest "functionArgs simple lambda" $-        assertEval "funcArgs-simple" "builtins.functionArgs (x: x)" (VAttrs Map.empty),+        assertEval "funcArgs-simple" "builtins.functionArgs (x: x)" (VAttrs (attrSetFromMap Map.empty)),       runTest "functionArgs type error" $         assertEvalFail "funcArgs-err" "builtins.functionArgs 42",       -- zipAttrsWith@@ -1242,7 +1242,7 @@       runTest "fromJSON object" $         assertEval "fromJSON-obj" "(builtins.fromJSON \"{\\\"a\\\": 1}\").a" (VInt 1),       runTest "fromJSON roundtrip" $-        assertEval "fromJSON-rt" "builtins.fromJSON (builtins.toJSON { a = 1; b = [ 2 3 ]; })" (VAttrs (Map.fromList [("a", Evaluated (VInt 1)), ("b", Evaluated (VList [Evaluated (VInt 2), Evaluated (VInt 3)]))])),+        assertEval "fromJSON-rt" "builtins.fromJSON (builtins.toJSON { a = 1; b = [ 2 3 ]; })" (VAttrs (attrSetFromMap (Map.fromList [("a", Evaluated (VInt 1)), ("b", Evaluated (VList [Evaluated (VInt 2), Evaluated (VInt 3)]))]))),       runTest "fromJSON invalid" $         assertEvalFail "fromJSON-bad" "builtins.fromJSON \"not json\"",       -- hashString@@ -1934,7 +1934,7 @@       runTest "getContext on plain string" $         assertRight "getCtx-plain" (evalNix "builtins.getContext \"hello\"") $ \val ->           case val of-            VAttrs m -> if Map.null m then Pass else Fail "expected empty attrset"+            VAttrs m -> if attrSetNull m then Pass else Fail "expected empty attrset"             _ -> Fail ("expected VAttrs, got " <> T.pack (show val)),       -- getContext: drv outPath has outputs entry       runTest "getContext on drv outPath"@@ -1943,9 +1943,9 @@           (evalNix "builtins.getContext (derivation { name = \"test\"; system = \"x86_64-linux\"; builder = \"/bin/sh\"; }).outPath")         $ \val -> case val of           VAttrs m ->-            if Map.size m == 1+            if attrSetSize m == 1               then Pass-              else Fail ("expected 1 entry, got " <> T.pack (show (Map.size m)))+              else Fail ("expected 1 entry, got " <> T.pack (show (attrSetSize m)))           _ -> Fail ("expected VAttrs, got " <> T.pack (show val)),       -- getContext: drvPath has allOutputs       runTest "getContext on drvPath has allOutputs"@@ -2057,18 +2057,18 @@       runTest "linear chain topo" $ case DepGraph.buildDepGraph readChain drvB spB of         Right graph -> case DepGraph.topoSort graph of           DepGraph.TopoSorted order ->-            if length order == 2 && head order == spC-              then Pass-              else Fail ("bad order: " <> T.pack (show order))+            case order of+              [first, _] | first == spC -> Pass+              _ -> Fail ("bad order: " <> T.pack (show order))           DepGraph.TopoCycle cyc -> Fail ("unexpected cycle: " <> T.pack (show cyc))         Left err -> Fail ("graph build failed: " <> err),       -- Diamond D->B,C; B->C: topoSort should have C first, D last       runTest "diamond topo" $ case DepGraph.buildDepGraph readDiamond drvD spD of         Right graph -> case DepGraph.topoSort graph of           DepGraph.TopoSorted order ->-            if length order == 3 && head order == spC && last order == spD-              then Pass-              else Fail ("bad diamond order: " <> T.pack (show order))+            case order of+              [first, _, lastElem] | first == spC, lastElem == spD -> Pass+              _ -> Fail ("bad diamond order: " <> T.pack (show order))           DepGraph.TopoCycle cyc -> Fail ("unexpected cycle: " <> T.pack (show cyc))         Left err -> Fail ("graph build failed: " <> err),       -- transitiveDeps@@ -2287,7 +2287,7 @@                 ]           )         $ \case-          VAttrs attrs -> case Map.lookup "_derivation" attrs of+          VAttrs attrs -> case attrSetLookup "_derivation" attrs of             Just (Evaluated (VDerivation drv)) ->               if Map.null (drvInputDrvs drv)                 then Fail "expected non-empty drvInputDrvs"@@ -2727,13 +2727,12 @@           (evalNix "let d = derivation { name = \"test\"; system = \"x86_64-linux\"; builder = \"/bin/sh\"; }; in d._derivation")         $ \val -> case val of           VDerivation drv ->-            if null (drvOutputs drv)-              then Fail "drvOutputs is empty"-              else-                let firstOut = head (drvOutputs drv)-                 in if doName firstOut == "out"-                      then Pass-                      else Fail ("first output name: " <> doName firstOut)+            case drvOutputs drv of+              [] -> Fail "drvOutputs is empty"+              (firstOut : _) ->+                if doName firstOut == "out"+                  then Pass+                  else Fail ("first output name: " <> doName firstOut)           _ -> Fail ("expected VDerivation, got " <> T.pack (show val)),       -- builtinDerivation multi-output populates drvOutputs       runTest "builtinDerivation multi-output"@@ -2971,7 +2970,7 @@       case evalResult of         Left err -> pure (Left ("eval error: " <> err))         Right val -> case val of-          VAttrs attrs -> case Map.lookup "_derivation" attrs of+          VAttrs attrs -> case attrSetLookup "_derivation" attrs of             Just (Evaluated (VDerivation drv)) -> do               store <- openStore storeDir               tmpBase <- getTemporaryDirectory@@ -3080,7 +3079,7 @@         let result = parseNixPath "nixpkgs=/home/user/nixpkgs"          in case result of               [Evaluated (VAttrs m)] ->-                case (Map.lookup "prefix" m, Map.lookup "path" m) of+                case (attrSetLookup "prefix" m, attrSetLookup "path" m) of                   (Just (Evaluated (VStr "nixpkgs" _)), Just (Evaluated (VStr "/home/user/nixpkgs" _))) -> Pass                   _ -> Fail "wrong prefix/path"               _ -> Fail ("expected one entry, got " <> T.pack (show (length result))),@@ -3090,7 +3089,7 @@         let result = parseNixPath "/some/path"          in case result of               [Evaluated (VAttrs m)] ->-                case (Map.lookup "prefix" m, Map.lookup "path" m) of+                case (attrSetLookup "prefix" m, attrSetLookup "path" m) of                   (Just (Evaluated (VStr "" _)), Just (Evaluated (VStr "/some/path" _))) -> Pass                   _ -> Fail "wrong prefix/path for plain"               _ -> Fail "expected one entry",