packages feed

nova-nix 0.1.2.0 → 0.1.3.0

raw patch · 11 files changed

+257/−79 lines, 11 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Nix.Eval.Types: thunkSameRef :: Thunk -> Thunk -> Bool

Files

CHANGELOG.md view
@@ -1,5 +1,22 @@ # Changelog +## 0.1.3.0 — 2026-02-25++### nixpkgs Compatibility: Module System, Callable Sets, Parser Fixes++- **`__functor` support** — Attribute sets with `__functor` are now callable, matching real Nix. `(set.__functor set) arg` dispatch enables nixpkgs patterns like `lib.makeOverridable` and `lib.setFunctionArgs`.+- **`builtins.setFunctionArgs`** — Wraps a function in a callable attrset with `__functor` and `__functionArgs` metadata. Used by `lib.mirrorFunctionArgs`, `lib.makeOverridable`, and the entire nixpkgs override system.+- **Null dynamic keys** — `{ ${null} = val; }` now skips the binding instead of erroring (matching real Nix). Used extensively by the nixpkgs module system for conditional attributes like `${if cond then "key" else null}`.+- **Indented string interpolation fix** — `''${expr}''` now parses correctly. The lexer had an overzealous guard (`isIndStringEscape`) that blocked `''` from opening an indented string when followed by `$`. This broke `lib.generators` and many nixpkgs files.+- **`splitVersion` fix** — Dots are separators, not components. `splitVersion "1.2.3"` now returns `["1" "2" "3"]` (was `["1" "." "2" "." "3"]`). Fixes `lib.versions.majorMinor` and all version comparison logic.+- **`builtins.functionArgs` on callable sets** — Now inspects `__functionArgs` metadata on attrsets produced by `setFunctionArgs`, enabling `lib.functionArgs` to work on overridable functions.+- **CLI eval sub-parser** — `--strict` and `--nix-path` flags now work after `eval` command (e.g. `nova-nix eval --strict --expr '...'`). Previously only worked before the command.+- **Bundled `<nix/fetchurl.nix>`** — Ships as a Cabal data-file, added to search paths automatically. nixpkgs stdenv bootstrap imports this file; no system Nix install needed.+- **nixpkgs module system working** — `lib.evalModules`, `lib.mkOption`, `lib.mkIf`, `lib.types.*` all evaluate correctly.+- **nixpkgs functional patterns working** — `lib.makeOverridable`, `lib.makeExtensible`, `lib.fix`, `lib.generators.toKeyValue` all correct.+- **`lib.systems.elaborate` instant** — Was taking minutes before lazy builtins; now returns immediately for any system string.+- 101 builtins (up from 91), 511 tests+ ## 0.1.2.0 — 2026-02-24  ### Lazy Builtins + Correctness 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.-- **91 Built-in Functions** — Type checks, arithmetic, bitwise, strings, lists, attribute sets, higher-order (`map`, `filter`, `foldl'`, `sort`, `genList`, `concatMap`), JSON (`toJSON`/`fromJSON`), hashing (SHA-256/SHA-512/SHA-1/MD5), version parsing, `replaceStrings`, `tryEval`, `deepSeq`, `genericClosure`, 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.+- **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. - **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.@@ -187,13 +187,13 @@  | Module | Purpose | Status | |--------|---------|--------|-| `Nix.Eval` | Lazy evaluator — all 17 AST constructors, thunk forcing, env operations, 91-builtin dispatch, search path resolution, dynamic attribute keys. Polymorphic via `MonadEval` | Done |+| `Nix.Eval` | Lazy evaluator — all 17 AST constructors, thunk forcing, env operations, 101-builtin dispatch, `__functor` callable sets, search path resolution, dynamic attribute keys. Polymorphic via `MonadEval` | Done | | `Nix.Eval.Types` | Shared types — `NixValue` (11 constructors), `Thunk` (lazy env for knot-tying), `Env` (lexical + with-scope chain), `StringContext` (store path tracking), `MonadEval` typeclass, `PureEval` runner | Done | | `Nix.Eval.Operator` | Binary/unary operators — arithmetic with float promotion, deep structural equality, division-by-zero checks | Done | | `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 — 91 builtins, search path plumbing (`parseNixPath`), top-level builtin exposure | Done |+| `Nix.Builtins` | Built-in function environment — 101 builtins, search path plumbing (`parseNixPath`), top-level builtin exposure | Done |  ### Store + Builder @@ -256,7 +256,7 @@ **Key numbers:**  - **22 modules** — all implemented-- **508 tests** — hand-rolled harness, no framework dependencies+- **511 tests** — hand-rolled harness, no framework dependencies - **Zero partial functions** — total by construction, `T.uncons` over `T.head`/`T.tail` - **Strict by default** — bang patterns on all data fields (except Thunk's Env, which is lazy for knot-tying) @@ -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] **91 builtins** — Type checks, arithmetic, bitwise, strings, lists, attrsets, higher-order, JSON, hashing, regex (`match`/`split` via `regex-tdfa`), version parsing, tryEval, deepSeq, genericClosure, string context introspection, all IO builtins, derivation+- [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] **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@@ -306,20 +306,26 @@ - [x] **CLI** — `nova-nix eval FILE.nix`, `nova-nix eval --expr 'EXPR'`, `nova-nix build FILE.nix`, `--nix-path NAME=PATH` - [x] **Thunk memoization** — Per-thunk `IORef` memo cells matching real Nix in-place mutation. GC reclaims dead thunks naturally. - [x] **Regex builtins** — `builtins.match` and `builtins.split` (POSIX ERE via `regex-tdfa`, pure Haskell, cross-platform)-- [x] **508 tests** — parser, evaluator, store, builder, substituter, dependency graph, search paths, dynamic keys, directory imports, CLI end-to-end+- [x] **Callable attribute sets** — `__functor` dispatch: sets with `__functor` are callable, enabling `lib.makeOverridable`, `lib.makeExtensible`, and the nixpkgs override system+- [x] **Lazy builtins** — `map`, `genList`, `mapAttrs` return deferred thunks (O(1) until demanded). `concatMap` is semi-lazy. Critical for nixpkgs which maps over 80,000+ packages.+- [x] **Null dynamic keys** — `{ ${null} = val; }` skips the binding, used by the nixpkgs module system for conditional attributes+- [x] **Bundled `<nix/fetchurl.nix>`** — Ships as Cabal data-file for nixpkgs stdenv bootstrap+- [x] **nixpkgs module system** — `lib.evalModules`, `lib.mkOption`, `lib.mkIf`, `lib.types.*` all working+- [x] **nixpkgs lib fully working** — trivial, strings, lists, attrsets, systems, generators, functional patterns (`fix`, `makeOverridable`, `makeExtensible`)+- [x] **511 tests** — parser, evaluator, store, builder, substituter, dependency graph, search paths, dynamic keys, directory imports, laziness, CLI end-to-end  ### Next -- [ ] **nixpkgs evaluation** — The ultimate test: `import <nixpkgs> {}` evaluates correctly and fast-- [ ] **Missing builtins** — Any others nixpkgs demands (discovered iteratively)-- [ ] **Performance** — Target ~2-5 seconds for full nixpkgs eval+- [ ] **Full `import <nixpkgs> {}` performance** — nixpkgs lib layer evaluates correctly; stdenv bootstrap runs but needs performance optimization for the full 80,000+ package set+- [ ] **Remaining builtins** — 13 missing (`fromTOML`, `hashFile`, `readFileType`, `traceVerbose`, `break`, `filterSource`, `outputOf`, and fetch variants)+- [ ] **`nova-nix shell`** — Enter a development shell (like `nix shell`)+- [ ] **`nova-nix repl`** — Interactive evaluator  ### Long-Term -- [ ] **`nova-nix shell`** — Enter a development shell (like `nix shell`)-- [ ] **`nova-nix repl`** — Interactive evaluator - [ ] **Nix daemon protocol compatibility** - [ ] **XZ decompression** — Enable nova-cache compression flag for real binary cache downloads+- [ ] **Store bootstrap** — Ship prebuilt bash + coreutils for Windows builds  --- @@ -327,7 +333,7 @@  ```bash cabal build                              # Build library + CLI-cabal test                               # Run all 508 tests+cabal test                               # Run all 511 tests cabal build --ghc-options="-Werror"      # Warnings as errors (CI default) cabal haddock                            # Generate API docs ```
app/Main.hs view
@@ -28,6 +28,7 @@ import Nix.Parser (parseNix) import Nix.Store (Store, closeStore, openStore, writeDrv) import Nix.Store.Path (StorePath, defaultStoreDir, parseStorePath, storePathToFilePath)+import Paths_nova_nix (getDataDir) import System.Directory (getCurrentDirectory, getTemporaryDirectory) import System.Environment (getArgs) import System.Exit (exitFailure)@@ -59,18 +60,25 @@       go (opts {optNixPaths = optNixPaths opts ++ [T.pack val]}) rest     go opts ("--strict" : rest) =       go (opts {optStrict = True}) rest-    go opts ("eval" : "--expr" : expr : rest) =-      go (opts {optCommand = CmdEvalExpr (T.pack expr)}) rest-    go opts ("eval" : path : rest) =-      go (opts {optCommand = CmdEvalFile path}) rest+    go opts ("eval" : rest) = goEval opts rest     go opts ("build" : path : rest) =       go (opts {optCommand = CmdBuild path}) rest     go opts _ = opts+    -- Sub-parser for eval: handles --strict and --expr interleaved with the file arg.+    goEval opts [] = opts+    goEval opts ("--strict" : rest) = goEval (opts {optStrict = True}) rest+    goEval opts ("--nix-path" : val : rest) =+      goEval (opts {optNixPaths = optNixPaths opts ++ [T.pack val]}) rest+    goEval opts ("--expr" : expr : rest) =+      go (opts {optCommand = CmdEvalExpr (T.pack expr)}) rest+    goEval opts (path : rest) =+      go (opts {optCommand = CmdEvalFile path}) rest --- | Merge --nix-path entries with the search paths from NIX_PATH.-mergeSearchPaths :: [T.Text] -> [Thunk] -> [Thunk]-mergeSearchPaths extraPaths envPaths =-  concatMap parseNixPath extraPaths ++ envPaths+-- | Merge --nix-path entries, bundled data dir, and NIX_PATH search paths.+-- The data dir is appended last so user paths take priority.+mergeSearchPaths :: [T.Text] -> FilePath -> [Thunk] -> [Thunk]+mergeSearchPaths extraPaths dataDir envPaths =+  concatMap parseNixPath extraPaths ++ envPaths ++ parseNixPath (T.pack dataDir)  -- --------------------------------------------------------------------------- -- Main@@ -80,11 +88,12 @@ main = do   hSetBuffering stdout LineBuffering   args <- getArgs+  dataDir <- getDataDir   let opts = parseArgs args   case optCommand opts of-    CmdEvalFile filePath -> evalFile (optStrict opts) (optNixPaths opts) filePath-    CmdEvalExpr expr -> evalExpr (optStrict opts) (optNixPaths opts) expr-    CmdBuild filePath -> buildFile (optNixPaths opts) filePath+    CmdEvalFile filePath -> evalFile (optStrict opts) (optNixPaths opts) dataDir filePath+    CmdEvalExpr expr -> evalExpr (optStrict opts) (optNixPaths opts) dataDir expr+    CmdBuild filePath -> buildFile (optNixPaths opts) dataDir filePath     CmdHelp -> do       hPutStrLn stderr "Usage: nova-nix [--nix-path NAME=PATH] <command>"       hPutStrLn stderr ""@@ -99,8 +108,8 @@       exitFailure  -- | Evaluate a .nix file and print the result.-evalFile :: Bool -> [T.Text] -> FilePath -> IO ()-evalFile strict extraPaths filePath = do+evalFile :: Bool -> [T.Text] -> FilePath -> FilePath -> IO ()+evalFile strict extraPaths dataDir filePath = do   source <- TIO.readFile filePath   case parseNix (T.pack filePath) source of     Left err -> do@@ -108,7 +117,7 @@       exitFailure     Right expr -> do       st <- newEvalState (takeDirectory filePath)-      let searchPaths = mergeSearchPaths extraPaths (esSearchPaths st)+      let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st)       result <-         runEvalIO st $           eval (builtinEnv (esTimestamp st) searchPaths) expr >>= finalize strict@@ -119,8 +128,8 @@         Right forced -> TIO.putStrLn (prettyValue forced)  -- | Evaluate an inline expression and print the result.-evalExpr :: Bool -> [T.Text] -> T.Text -> IO ()-evalExpr strict extraPaths source = do+evalExpr :: Bool -> [T.Text] -> FilePath -> T.Text -> IO ()+evalExpr strict extraPaths dataDir source = do   case parseNix "<expr>" source of     Left err -> do       hPutStrLn stderr ("parse error: " ++ show err)@@ -128,7 +137,7 @@     Right expr -> do       cwd <- getCurrentDirectory       st <- newEvalState cwd-      let searchPaths = mergeSearchPaths extraPaths (esSearchPaths st)+      let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st)       result <-         runEvalIO st $           eval (builtinEnv (esTimestamp st) searchPaths) expr >>= finalize strict@@ -139,8 +148,8 @@         Right forced -> TIO.putStrLn (prettyValue forced)  -- | Parse, evaluate, extract derivation, build, and print result.-buildFile :: [T.Text] -> FilePath -> IO ()-buildFile extraPaths filePath = do+buildFile :: [T.Text] -> FilePath -> FilePath -> IO ()+buildFile extraPaths dataDir filePath = do   source <- TIO.readFile filePath   case parseNix (T.pack filePath) source of     Left err -> do@@ -148,7 +157,7 @@       exitFailure     Right expr -> do       st <- newEvalState (takeDirectory filePath)-      let searchPaths = mergeSearchPaths extraPaths (esSearchPaths st)+      let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st)       result <- runEvalIO st (eval (builtinEnv (esTimestamp st) searchPaths) expr)       case result of         Left err -> do
+ data/nix/fetchurl.nix view
@@ -0,0 +1,74 @@+# Nix built-in fetchurl — creates a fixed-output derivation that downloads a URL.+# This file is normally shipped with the Nix daemon as <nix/fetchurl.nix>.+# nova-nix bundles it so that nixpkgs bootstrap works without a system Nix install.+{+  system ? "", # obsolete+  url,+  hash ? "", # an SRI hash++  # Legacy hash specification+  md5 ? "",+  sha1 ? "",+  sha256 ? "",+  sha512 ? "",+  outputHash ?+    if hash != "" then+      hash+    else if sha512 != "" then+      sha512+    else if sha1 != "" then+      sha1+    else if md5 != "" then+      md5+    else+      sha256,+  outputHashAlgo ?+    if hash != "" then+      ""+    else if sha512 != "" then+      "sha512"+    else if sha1 != "" then+      "sha1"+    else if md5 != "" then+      "md5"+    else+      "sha256",++  executable ? false,+  unpack ? false,+  name ? baseNameOf (toString url),+  impure ? false,+}:++derivation (+  {+    builder = "builtin:fetchurl";++    # New-style output content requirements.+    outputHashMode = if unpack || executable then "recursive" else "flat";++    inherit+      name+      url+      executable+      unpack+      ;++    system = "builtin";++    # No need to double the amount of network traffic+    preferLocalBuild = true;++    impureEnvVars = [+      "http_proxy"+      "https_proxy"+      "ftp_proxy"+      "all_proxy"+      "no_proxy"+    ];++    # To make "nix-prefetch-url" work.+    urls = [ url ];+  }+  // (if impure then { __impure = true; } else { inherit outputHashAlgo outputHash; })+)
nova-nix.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               nova-nix-version:            0.1.2.0+version:            0.1.3.0 synopsis:           Windows-native Nix implementation in pure Haskell description:   A pure Haskell implementation of the Nix package manager that runs natively@@ -24,6 +24,9 @@ extra-doc-files:     CHANGELOG.md     README.md+data-files:+    nix/fetchurl.nix+data-dir: data  -- --------------------------------------------------------------------------- -- Library: core modules (parser, evaluator, store, builder, substituter)@@ -90,6 +93,8 @@ executable nova-nix   main-is:          Main.hs   hs-source-dirs:   app+  autogen-modules:  Paths_nova_nix+  other-modules:    Paths_nova_nix   default-language:  Haskell2010   default-extensions:     OverloadedStrings
src/Nix/Eval.hs view
@@ -75,6 +75,7 @@     Thunk (..),     emptyContext,     emptyEnv,+    envInsert,     envInsertThunk,     envLookup,     evaluated,@@ -266,15 +267,17 @@ -- | 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 = mapM resolveOne+resolveBindingKeys keyEnv = fmap catMaybes . mapM resolveOne   where     resolveOne (NamedBinding path bodyExpr) = do       resolvedPath <- mapM (resolveKey keyEnv) path-      pure (ResolvedNamed resolvedPath bodyExpr)+      -- 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 (ResolvedInherit names)+      pure (Just (ResolvedInherit names))     resolveOne (Inherit (Just fromExpr) names) =-      pure (ResolvedInheritFrom fromExpr names)+      pure (Just (ResolvedInheritFrom fromExpr names))  -- | Build a flat attribute map from pre-resolved bindings (pure). -- Thunks capture @thunkEnv@ — suitable for knot-tying.@@ -324,22 +327,28 @@ buildNestedAttrM _thunkEnv _keyEnv [] _bodyExpr =   throwEvalError "empty attribute path" buildNestedAttrM thunkEnv keyEnv [key] bodyExpr = do-  keyText <- resolveKey keyEnv key-  pure (Map.singleton keyText (mkThunk thunkEnv bodyExpr))+  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-  keyText <- resolveKey keyEnv key-  inner <- buildNestedAttrM thunkEnv keyEnv rest bodyExpr-  pure (Map.singleton keyText (evaluated (VAttrs inner)))+  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 to its text name.--- Static keys return immediately; dynamic keys evaluate the expression--- and coerce the result to a string.-resolveKey :: (MonadEval m) => Env -> AttrKey -> m Text-resolveKey _env (StaticKey name) = pure name+-- | 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 s+    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@@ -376,8 +385,8 @@ walkAttrPath _env [] val = pure (Just val) walkAttrPath env (key : rest) val = case val of   VAttrs attrs -> do-    keyText <- resolveKey env key-    case Map.lookup keyText attrs of+    resolved <- resolveKey env key+    case resolved >>= (`Map.lookup` attrs) of       Just thunk -> do         inner <- force thunk         walkAttrPath env rest inner@@ -428,6 +437,16 @@     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.@@ -617,7 +636,7 @@       builtin1 "head" builtinHead,       builtin1 "tail" builtinTail,       -- String operations (arity 1)-      builtin1 "toString" (fmap (uncurry VStr) . coerceToString),+      builtin1 "toString" (fmap (uncurry VStr) . coerceToStringPermissive),       builtin1 "stringLength" builtinStringLength,       -- Control (arity 1)       builtin1 "throw" builtinThrow,@@ -676,6 +695,7 @@       -- Attr set higher-order       builtin2 "mapAttrs" builtinMapAttrs,       builtin1 "functionArgs" builtinFunctionArgs,+      builtin2 "setFunctionArgs" builtinSetFunctionArgs,       builtin2 "zipAttrsWith" builtinZipAttrsWith,       -- String manipulation       builtin2 "match" builtinMatch,@@ -774,7 +794,7 @@   "head" -> apply1 builtinHead   "tail" -> apply1 builtinTail   -- String operations (arity 1)-  "toString" -> apply1 (fmap (uncurry VStr) . coerceToString)+  "toString" -> apply1 (fmap (uncurry VStr) . coerceToStringPermissive)   "stringLength" -> apply1 builtinStringLength   -- Control (arity 1)   "throw" -> apply1 builtinThrow@@ -833,6 +853,7 @@   -- Attr set higher-order   "mapAttrs" -> apply2 builtinMapAttrs   "functionArgs" -> apply1 builtinFunctionArgs+  "setFunctionArgs" -> apply2 builtinSetFunctionArgs   "zipAttrsWith" -> apply2 builtinZipAttrsWith   -- String manipulation   "match" -> apply2 builtinMatch@@ -1349,6 +1370,23 @@           }    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@@ -1626,6 +1664,9 @@ 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) @@ -1640,6 +1681,26 @@     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@@ -1842,7 +1903,7 @@ splitVersionComponents :: Text -> [Text] splitVersionComponents t = case T.uncons t of   Nothing -> []-  Just ('.', rest) -> "." : splitVersionComponents rest+  Just ('.', rest) -> splitVersionComponents rest   Just (c, _)     | isDigit c ->         let (digits, rest) = T.span isDigit t
src/Nix/Eval/Operator.hs view
@@ -17,6 +17,7 @@   ( MonadEval (..),     NixValue (..),     Thunk,+    thunkSameRef,     typeName,   ) import Nix.Expr.Types (BinaryOp (..), UnaryOp (..))@@ -167,19 +168,24 @@ -- | Pairwise equality of two thunk lists (for list comparison). listEqual :: (MonadEval m) => Force m -> [Thunk] -> [Thunk] -> m Bool listEqual _ [] [] = pure True-listEqual forceFn (a : as) (b : bs) = do-  va <- forceFn a-  vb <- forceFn b-  eq <- nixEqual forceFn va vb-  if eq then listEqual forceFn as bs else pure False+listEqual forceFn (a : as) (b : bs)+  | thunkSameRef a b = listEqual forceFn as bs+  | otherwise = do+      va <- forceFn a+      vb <- forceFn b+      eq <- nixEqual forceFn va vb+      if eq then listEqual forceFn as bs else pure False listEqual _ _ _ = pure False  -- | Compare two thunks for equality by forcing both.+-- Short-circuits on thunk identity (same IORef = same value). thunkPairEqual :: (MonadEval m) => Force m -> (Thunk, Thunk) -> m Bool-thunkPairEqual forceFn (a, b) = do-  va <- forceFn a-  vb <- forceFn b-  nixEqual forceFn va vb+thunkPairEqual forceFn (a, b)+  | thunkSameRef a b = pure True+  | otherwise = do+      va <- forceFn a+      vb <- forceFn b+      nixEqual forceFn va vb  -- --------------------------------------------------------------------------- -- List / attrset operators
src/Nix/Eval/StringInterp.hs view
@@ -46,9 +46,9 @@  -- | Coerce a Nix value to a string for interpolation. ----- Nix coercion rules: strings pass through (with context), integers--- and floats are shown, paths pass through, null becomes the empty--- string.  Other types (lists, sets, functions) cannot be coerced.+-- Strict coercion: strings, ints, floats, paths, null, bools.+-- Lists, sets (without @__toString@/@outPath@), and functions are errors.+-- Used by string interpolation (@"${...}"@). coerceToString :: (MonadEval m) => NixValue -> m (Text, StringContext) coerceToString val = case val of   VStr s ctx -> pure (s, ctx)
src/Nix/Eval/Types.hs view
@@ -28,6 +28,7 @@     mkThunk,     mkSyntheticThunk,     evaluated,+    thunkSameRef,      -- * Display     typeName,@@ -226,6 +227,14 @@ -- | Wrap an already-computed value as a thunk. evaluated :: NixValue -> Thunk evaluated = Evaluated++-- | Check if two thunks share the same memoization cell (IORef pointer+-- equality).  When true, both thunks will always produce the same value,+-- 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 _ _ = False  -- | Human-readable type name for error messages. typeName :: NixValue -> Text
src/Nix/Parser/Lexer.hs view
@@ -161,9 +161,7 @@                   newSt = advanceCol 1 st {lsInput = rest, lsModes = ModeString : lsModes st}                in lexLoop newSt (tok : acc)             '\''-              | Just '\'' <- safeHead rest,-                -- make sure it's not ''$ or ''\ or ''' (those are inside indented strings only)-                not (isIndStringEscape (T.drop 2 (lsInput st))) ->+              | 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)@@ -312,10 +310,10 @@                       newSt = advanceCol 3 st {lsInput = rest2}                    in lexIndStringMode newSt (litTok : acc)                 Just ('$', rest3)-                  | Just ('{', _) <- T.uncons rest3 ->+                  | Just ('{', rest4) <- T.uncons rest3 ->                       -- ''${ → literal ${                       let litTok = Located (lsLine st) (lsCol st) (TokStringLit "${")-                          newSt = advanceCol 4 st {lsInput = rest3}+                          newSt = advanceCol 4 st {lsInput = rest4}                        in lexIndStringMode newSt (litTok : acc)                 Just ('\\', rest3) ->                   -- ''\x → escape sequence@@ -638,13 +636,6 @@ canFollowWithDivision TokFalse = True canFollowWithDivision TokNull = True canFollowWithDivision _ = False--isIndStringEscape :: Text -> Bool-isIndStringEscape t = case T.uncons t of-  Just ('\'', _) -> True-  Just ('$', _) -> True-  Just ('\\', _) -> True-  _ -> False  -- --------------------------------------------------------------------------- -- Emit helpers
test/Main.hs view
@@ -1189,9 +1189,9 @@         assertEvalFail "cmpVer-err" "builtins.compareVersions 1 2",       -- splitVersion       runTest "splitVersion basic" $-        assertEval "splitVer" "builtins.splitVersion \"1.2.3\" == [ \"1\" \".\" \"2\" \".\" \"3\" ]" (VBool True),+        assertEval "splitVer" "builtins.splitVersion \"1.2.3\" == [ \"1\" \"2\" \"3\" ]" (VBool True),       runTest "splitVersion pre" $-        assertEval "splitVer-pre" "builtins.splitVersion \"1.2pre\" == [ \"1\" \".\" \"2\" \"pre\" ]" (VBool True),+        assertEval "splitVer-pre" "builtins.splitVersion \"1.2pre\" == [ \"1\" \"2\" \"pre\" ]" (VBool True),       runTest "splitVersion type error" $         assertEvalFail "splitVer-err" "builtins.splitVersion 42",       -- parseDrvName