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 +17/−0
- README.md +18/−12
- app/Main.hs +29/−20
- data/nix/fetchurl.nix +74/−0
- nova-nix.cabal +6/−1
- src/Nix/Eval.hs +81/−20
- src/Nix/Eval/Operator.hs +15/−9
- src/Nix/Eval/StringInterp.hs +3/−3
- src/Nix/Eval/Types.hs +9/−0
- src/Nix/Parser/Lexer.hs +3/−12
- test/Main.hs +2/−2
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