nova-nix 0.1.8.0 → 0.1.9.0
raw patch · 41 files changed
+11120/−4752 lines, 41 filesdep −primitivedep ~nova-cache
Dependencies removed: primitive
Dependency ranges changed: nova-cache
Files
- CHANGELOG.md +16/−0
- README.md +154/−201
- app/Main.hs +38/−21
- cbits/nn_arena.c +76/−0
- cbits/nn_attrset.c +384/−0
- cbits/nn_bytecode.c +134/−0
- cbits/nn_ctxstr.c +147/−0
- cbits/nn_env.c +290/−0
- cbits/nn_lambda.c +157/−0
- cbits/nn_list.c +96/−0
- cbits/nn_symbol.c +254/−0
- cbits/nn_thunk.c +532/−0
- nova-nix.cabal +32/−9
- src/Nix/Builder.hs +8/−2
- src/Nix/Builtins.hs +25/−24
- src/Nix/Derivation.hs +9/−3
- src/Nix/Eval.hs +3940/−3625
- src/Nix/Eval/Arena.hs +98/−0
- src/Nix/Eval/CAttrSet.hs +194/−0
- src/Nix/Eval/CBytecode.hs +319/−0
- src/Nix/Eval/CCtxStr.hs +127/−0
- src/Nix/Eval/CEnv.hs +220/−0
- src/Nix/Eval/CLambda.hs +138/−0
- src/Nix/Eval/CList.hs +169/−0
- src/Nix/Eval/CThunk.hs +435/−0
- src/Nix/Eval/Compile.hs +583/−0
- src/Nix/Eval/Context.hs +2/−1
- src/Nix/Eval/EvalFormals.hs +33/−0
- src/Nix/Eval/IO.hs +158/−34
- src/Nix/Eval/Operator.hs +42/−46
- src/Nix/Eval/StringInterp.hs +27/−4
- src/Nix/Eval/Symbol.hs +119/−0
- src/Nix/Eval/Types.hs +1108/−701
- src/Nix/Expr/Resolve.hs +12/−1
- src/Nix/Expr/Types.hs +2/−1
- src/Nix/Hash.hs +19/−3
- src/Nix/Parser/Expr.hs +43/−12
- src/Nix/Parser/Lexer.hs +19/−7
- src/Nix/Store.hs +9/−2
- src/Nix/Substituter.hs +21/−2
- test/Main.hs +931/−53
CHANGELOG.md view
@@ -1,5 +1,21 @@ # Changelog +## 0.1.9.0 — 2026-06-05++### nixpkgs Evaluation: Lazy Derivations++- **Fix: `import <nixpkgs> {}` infinite recursion — lazy `derivation` over `derivationStrict`** — `builtins.derivation` was eager: forcing a derivation to WHNF forced its entire env/input closure. This turned nixpkgs' lazy `perl` ↔ `libxcrypt` dependency cycle into a blackhole — `perl`'s `assert (libxcrypt != null)` forced `libxcrypt`'s build, which forced `perl` mid-construction. Restored Nix's two-tier model: the eager `builtins.derivationStrict` primop (identical content-hashing, renamed from the old `builtins.derivation` body) plus a lazy `derivation` wrapper over it (mirroring `corepkgs/derivation.nix`). Forcing a derivation to WHNF now yields its attribute spine without computing `drvPath`/`outPath` or forcing its inputs, so referencing a package no longer builds its closure. No C changes; `drvPath`/`outPath` hashes unchanged. The full stdenv bootstrap and package construction (perl, libxcrypt, binutils) now evaluate.++### Technical Audit + C Data Layer Polish++- **Fix: Nix integer division semantics** — `builtins.div` and `builtins.mod` now use floored division (`div`/`mod`) instead of truncated (`quot`/`rem`). `-7 / 3` correctly returns `-3` (was `-2`). Affects `builtinDiv`, `builtinMod`, and `evalDiv`.+- **Fix: `inherit (from)` in positional let/rec blocks** — The resolution pass treated `inherit (expr) x y;` as positional, but the bytecode evaluator did not handle `BcInheritFrom` in `allBcPositional`, `bcBindingSlotCount`, `buildBcSlotThunks`, or `buildBcAttrMapFromSlots`. This caused env slot count mismatches and `idx out of bounds` assertions on any nixpkgs expression using `inherit (from)` in a positional let block.+- **C memory safety audit** — `nn_bytecode.c`: `ensure_op_space`/`ensure_data_space` now return error codes instead of failing silently; overflow guard on capacity doubling. `nn_lambda.c`: `NN_ASSERT` bounds checks on entry accessors prevent null dereference when `formal_count == 0`. `nn_attrset.c`: documented partial-realloc-failure behavior.+- **Dead code removal** — Removed unused `cattrsetIntersect` Haskell wrapper, `nn_attrset_intersect`, and `nn_attrset_values_ptr` C functions.+- **New: `nn_assert.h`** — Debug-mode bounds checking macro. Compiles to nothing under `NDEBUG`.+- **Performance** — Stress test: 6.25 MB max residency, 56.3% GC productivity (down from 69.7 MB / 1.6% pre-C-data-layer).+- 109 builtins, 593 tests, `-Werror` clean, ormolu clean, hlint clean+ ## 0.1.8.0 — 2026-03-08 ### Builder Correctness, Windows Store Paths, Hackage Fix
README.md view
@@ -1,8 +1,8 @@ <div align="center"> <h1>nova-nix</h1>-<p><strong>Windows-Native Nix in Pure Haskell</strong></p>-<p>A from-scratch implementation of the Nix package manager — parser, lazy evaluator, content-addressed store, derivation builder, binary substituter — running natively on Windows, macOS, and Linux. No WSL. No Cygwin. No MSYS2.</p>-<p><a href="#quick-start">Quick Start</a> · <a href="#cli">CLI</a> · <a href="#modules">Modules</a> · <a href="#architecture">Architecture</a> · <a href="#the-hard-problems">Hard Problems</a> · <a href="#roadmap">Roadmap</a> · <a href="#build--test">Build & Test</a></p>+<p><strong>Windows-Native Nix</strong></p>+<p>A from-scratch Nix implementation in Haskell + C99. Parser, lazy evaluator, content-addressed store, derivation builder, binary substituter. Runs natively on Windows, macOS, and Linux. No WSL. No Cygwin.</p>+<p><a href="#try-it">Try It</a> · <a href="#cli">CLI</a> · <a href="#architecture">Architecture</a> · <a href="#performance">Performance</a> · <a href="#modules">Modules</a> · <a href="#roadmap">Roadmap</a></p> <p> [](https://github.com/Novavero-AI/nova-nix/actions/workflows/ci.yml)@@ -17,20 +17,17 @@ ## What is nova-nix? -A pure Haskell implementation of Nix that treats Windows as a first-class target:--- **Parser** — Hand-rolled recursive descent parser for the full Nix expression language. 13 precedence levels, 18 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 18 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.-- **108 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.-- **Content-Addressed Store** — `/nix/store` on Unix, `C:\nix\store` on Windows, with real SQLite metadata tracking (ValidPaths + Refs tables, WAL mode)-- **Derivation Builder** — Full build loop with recursive dependency resolution: topological sort via Kahn's algorithm, binary cache substitution before local builds, input validation, reference scanning, output registration-- **Binary Substituter** — HTTP binary cache protocol: narinfo fetch + parse, Ed25519 signature verification, NAR download/decompress/unpack, store registration. Priority-ordered multi-cache support. Built on [nova-cache](https://github.com/Novavero-AI/nova-cache).-- **ATerm Serialization** — Full round-trip `.drv` serialization and parsing with string escape handling+| Layer | What it does |+|-------|-------------|+| **Parser** | Hand-rolled recursive descent. 14 precedence levels, 19 AST constructors, all Nix syntax including `<nixpkgs>`, `${expr}` keys, indented strings. |+| **Evaluator** | Bytecode-compiled lazy evaluation. Thunk memoization with blackhole detection. Knot-tying for recursive `let`/`rec`. Polymorphic via `MonadEval`. |+| **109 Builtins** | Arithmetic, strings, lists, attrsets, higher-order (`map`, `filter`, `foldl'`, `sort`, `genList`, `concatMap`, `mapAttrs`), JSON, hashing, regex, version parsing, `tryEval`, `deepSeq`, `genericClosure`, string contexts, IO (`import`, `readFile`, `pathExists`, `derivation`, `fetchurl`, `fetchTarball`, `fetchGit`), and more. |+| **C99 Data Layer** | 9 arena-allocated C modules for interned symbols, sorted attrsets, thunks, envs, lists, bytecode, lambdas. All eval data off the GHC heap. |+| **Store** | Content-addressed `/nix/store` (or `C:\nix\store`). SQLite metadata, reference scanning, read-only enforcement. |+| **Builder** | Dependency graph via Kahn's toposort. Binary cache substitution before local builds. Multi-output, reference scanning, store registration. |+| **Substituter** | HTTP binary cache protocol. Narinfo parsing, Ed25519 verification, NAR download/unpack. Priority-ordered multi-cache. Built on [nova-cache](https://github.com/Novavero-AI/nova-cache). | -Every module is pure by default. IO lives at the boundaries only.+Every module is pure by default. IO at the boundaries only. --- @@ -42,132 +39,117 @@ cabal run nova-nix -- --strict eval test.nix ``` -Output:- ``` { count = 6; greeting = "Hello, nova-nix!"; items = [ 2 4 6 8 10 ]; nested = { a = 1; b = 2; c = 4; }; types = { attrs = "set"; int = "int"; list = "list"; string = "string"; }; } ``` -That's a Nix expression with `let` bindings, `rec` attrs, lambdas, `builtins.map`, `builtins.typeOf`, and arithmetic — parsed, lazily evaluated, and pretty-printed. On Windows, macOS, or Linux.+That's `let` bindings, `rec` attrs, lambdas, `builtins.map`, `builtins.typeOf`, and arithmetic — parsed, lazily evaluated, and printed. On Windows, macOS, or Linux. --- ## CLI ```bash-nova-nix eval FILE.nix # Evaluate a .nix file, print result+nova-nix eval FILE.nix # Evaluate a .nix file nova-nix eval --expr 'EXPR' # Evaluate an inline expression-nova-nix build FILE.nix # Build a derivation from a .nix file+nova-nix build FILE.nix # Build a derivation nova-nix --nix-path nixpkgs=/path eval FILE # Add search paths (repeatable) ``` -### Evaluate- ```bash-$ nova-nix --strict eval test.nix-{ count = 6; greeting = "Hello, nova-nix!"; items = [ 2 4 6 8 10 ]; nested = { a = 1; b = 2; c = 4; }; types = { attrs = "set"; int = "int"; list = "list"; string = "string"; }; }-```--Inline expressions:--```bash $ nova-nix eval --expr '1 + 2' 3 $ nova-nix eval --expr 'builtins.map (x: x * x) [1 2 3 4 5]' [ 1 4 9 16 25 ] -$ nova-nix eval --expr '{ x = 1; y = 2; }.x + { x = 1; y = 2; }.y'-3+$ NIX_PATH=nixpkgs=/path/to/nixpkgs nova-nix eval --expr '(import <nixpkgs/lib>).trivial.version'+"24.11pre-git" ``` -Search paths:- ```bash-$ nova-nix --nix-path nixpkgs=/path/to/nixpkgs eval --expr 'import <nixpkgs> {}'--$ NIX_PATH=nixpkgs=/path/to/nixpkgs nova-nix eval --expr 'import <nixpkgs> {}'-```--### Build--```bash-$ cat > hello.nix <<'EOF'-derivation {- name = "hello";- system = builtins.currentSystem;- builder = "/bin/sh";- args = [ "-c" "mkdir -p $out && echo 'Hello from nova-nix!' > $out/greeting.txt" ];-}-EOF- $ nova-nix build hello.nix /nix/store/abc...-hello ``` -The `build` command evaluates the `.nix` file, extracts the derivation, builds the full dependency graph, topologically sorts it, checks binary caches for substitutes, builds anything missing locally, and registers all outputs in the store DB.+The `build` command evaluates, extracts the derivation, builds the dependency graph, checks binary caches, builds locally, and registers outputs. --- -## Quick Start--Add to your `.cabal` file:+## Architecture -```cabal-build-depends: nova-nix ```+ Pure Core (no IO)+ +------------------------------------------------++ | |+ | Parser --> Expr.Types --> Eval --> Builtins |+ | | | |+ | Expr.Resolve Eval.Types |+ | Expr.ClosureTrim Eval.Operator |+ | Parser.Lexer Eval.Compile |+ | Parser.Expr Eval.StringInterp |+ | | |+ | Derivation --> Hash |+ | | |+ | Store.Path DependencyGraph |+ | |+ +------------------------------------------------++ |+ IO Boundary (thin)+ +------------------------------------------------++ | Eval.IO Store.DB Store Builder Substituter |+ +------------------------------------------------++ |+ C99 Data Layer (off GHC heap)+ +------------------------------------------------++ | nn_symbol nn_attrset nn_thunk nn_env |+ | nn_list nn_ctxstr nn_bytecode nn_lambda |+ | nn_arena |+ +------------------------------------------------++``` -### Parse a Nix Expression+**Key design decisions:** -```haskell-import Nix.Parser (parseNix)-import Nix.Expr.Types+- **Haskell owns eval logic, C99 owns data layout.** The evaluator stays in Haskell. Data structures (attr sets, thunks, envs, values) live in C. Haskell calls C to create, query, and mutate data. C never calls back into Haskell.+- **Bytecode-compiled evaluation.** The 19-constructor Expr AST compiles to a flat 24-opcode bytecode array. The bytecode evaluator (`evalBytecode`) is the sole dispatch path. The AST is GC'd after compilation.+- **MonadEval typeclass.** `PureEval` (newtype over `Either Text`) for deterministic testing. `EvalIO` for real filesystem access. Same `eval` function, different effects.+- **Arena allocation.** Thunks, attr sets, envs, and lambdas are arena-allocated in C. Bulk free at eval end, zero per-object GC overhead. The GHC heap holds only control flow and `StablePtr` handles.+- **Knot-tying via Haskell laziness.** Recursive `let` and `rec {}` use two-phase construction: allocate slots, create env, fill slots. Thunks capture environments lazily via `StablePtr` so they can reference not-yet-filled arrays.+- **Lazy derivations.** `derivation` is a lazy wrapper over the eager `derivationStrict` primop (mirroring Nix's `corepkgs/derivation.nix`). Forcing a derivation to WHNF yields its attribute spine without computing `drvPath`/`outPath` or forcing its input closure — so referencing a package (`pkg ? out`, `assert (dep != null)`) is free.+- **String context propagation.** Every `VStr` carries a `StringContext` tracking store path references. The `derivationStrict` primop collects all context into `drvInputDrvs`/`drvInputSrcs`. -main :: IO ()-main = do- case parseNix "<stdin>" "let x = 1 + 2; in x" of- Left err -> print err- Right expr -> print expr- -- ELet [NamedBinding [StaticKey "x"]- -- (EBinary OpAdd (ELit (NixInt 1)) (ELit (NixInt 2)))]- -- (EVar "x")-```+--- -### Evaluate an Expression+## Performance -```haskell-import Nix.Parser (parseNix)-import Nix.Eval (eval, PureEval(..), NixValue(..))-import Nix.Builtins (builtinEnv)+The C99 data layer moves all evaluation data off the GHC heap: -main :: IO ()-main = do- case parseNix "<stdin>" "let x = 5; y = x * 2; in y + 1" of- Left err -> print err- Right expr -> case runPureEval (eval (builtinEnv 0 []) expr) of- Left err -> putStrLn ("Error: " ++ show err)- Right val -> print val -- VInt 11-```+| Metric | Pure Haskell | After C Data Layer | Change |+|--------|-------------|-------------------|--------|+| Max residency | 69.7 MB | 6.25 MB | **-91%** |+| GC productivity | 1.6% | 56.3% | **+35x** |+| Total memory | ~200 MB | 26 MB | **-87%** | -The evaluator is polymorphic via `MonadEval` — `PureEval` runs without IO, while `EvalIO` can access the filesystem for `import`, `readFile`, etc.+Measured on a stress test with 100k attribute sets, recursive computations, list operations, and overlay patterns. -### Lazy Evaluation in Action+**nixpkgs status:** `import <nixpkgs/lib>` evaluates correctly (450 attributes). `lib.fix`, `lib.extends`, `lib.makeExtensible`, `lib.evalModules`, and `lib.systems.elaborate` all work. The full stdenv bootstrap and package construction — perl, libxcrypt, binutils — now evaluate: `derivation` is a lazy wrapper over the eager `derivationStrict` primop (matching C++ Nix), so referencing a derivation no longer forces its build closure. Full `import <nixpkgs> {}` is currently blocked on a variable-resolution bug in named-formal-set (`@`-pattern) argument slots. -```haskell--- Nix is lazy: unused bindings are never evaluated--- runPureEval (eval (builtinEnv 0 []) expr) where expr parses:--- "let unused = builtins.throw \"boom\"; x = 42; in x"--- Right (VInt 42) — "boom" is never triggered+--- --- Recursive attribute sets with self-reference--- "rec { a = 1; b = a + 1; c = b * 2; }.c"--- Right (VInt 4)+## Windows Native --- Lambda closures, set patterns with defaults--- "({ name, greeting ? \"Hello\" }: \"${greeting}, ${name}!\") { name = \"Nix\"; }"--- Right (VStr "Hello, Nix!")-```+nova-nix runs natively on Windows — no compatibility layers, no translation. +| Platform Difference | How nova-nix Handles It |+|---------------------|------------------------|+| No `fork`/`exec` | Win32 `CreateProcess` via `System.Process` |+| No symlinks by default | Developer Mode symlinks; junction point / copy fallback |+| No `/nix/store` | `C:\nix\store` — all paths parameterized, never hardcoded |+| Case-insensitive FS | Content-addressed hashes make collisions impossible |+| 260-char path limit | `\\?\` extended-length prefix (32K chars) |+| No bash | Builder ships `bash.exe` in the store (from MSYS2, same as Git for Windows) |+| stdenv bootstrap | Windows stdenv with MinGW GCC + MSYS2 coreutils in `C:\nix\store` |+ --- ## Modules@@ -176,129 +158,81 @@ | Module | Purpose | |--------|---------|-| `Nix.Expr` | Re-exports from `Nix.Expr.Types` |-| `Nix.Expr.Types` | Complete Nix AST — 18 expression constructors (including `ESearchPath`, `EResolvedVar`), atoms, formals, operators, string parts |-| `Nix.Expr.Resolve` | De Bruijn-style variable resolution pass — replaces `EVar` with `EResolvedVar` for lambda-bound variables at parse time |-| `Nix.Expr.ClosureTrim` | Closure trimming — statically determines free variables per lambda/with to minimize captured environment size |-| `Nix.Parser` | Hand-rolled recursive descent parser + lexer. Direct `Text` consumption, source position tracking |-| `Nix.Parser.Lexer` | Tokenizer — integers, floats, strings with interpolation, paths, URIs, search paths, all operators/keywords |-| `Nix.Parser.Expr` | Expression parser — 13 precedence levels, left/right/non-associative operators, application, selection, dynamic keys |-| `Nix.Parser.Internal` | Parser state and combinator internals |-| `Nix.Parser.ParseError` | Structured parse errors with source positions |+| `Nix.Expr.Types` | Complete Nix AST — 18 expression constructors, atoms, formals, operators, string parts |+| `Nix.Expr.Resolve` | De Bruijn-style variable resolution — replaces `EVar` with `EResolvedVar` at parse time |+| `Nix.Expr.ClosureTrim` | Closure trimming — determines free variables per lambda/with to minimize captured env size |+| `Nix.Parser` | Hand-rolled recursive descent parser + lexer, source position tracking |+| `Nix.Parser.Lexer` | Tokenizer — integers, floats, strings with interpolation, paths, URIs, search paths, operators/keywords |+| `Nix.Parser.Expr` | Expression parser — 14 precedence levels, left/right/non-associative operators | ### Evaluator | Module | Purpose | |--------|---------|-| `Nix.Eval` | Lazy evaluator — all 18 AST constructors, thunk forcing, env operations, 108-builtin dispatch, `__functor` callable sets, search path resolution, dynamic attribute keys. Polymorphic via `MonadEval` |-| `Nix.Eval.Types` | Shared types — `NixValue` (11 constructors), `Thunk` (IORef memo cell), `Env` (SmallArray positional slots + scope chain), `AttrSet` (lazy/eager), `StringContext` (store path tracking), `MonadEval` typeclass, `PureEval` runner |-| `Nix.Eval.Operator` | Binary/unary operators — arithmetic with float promotion, deep structural equality, division-by-zero checks |-| `Nix.Eval.StringInterp` | String interpolation — value coercion with context propagation, indented string whitespace stripping |-| `Nix.Eval.Context` | String context construction, queries, extraction — pure helpers for building and inspecting store path references |-| `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) |-| `Nix.Builtins` | Built-in function environment — 108 builtins, search path plumbing (`parseNixPath`), top-level builtin exposure |+| `Nix.Eval` | Bytecode evaluator — 24-opcode dispatch, thunk forcing, env operations, 109-builtin dispatch. Polymorphic via `MonadEval` |+| `Nix.Eval.Types` | `NixValue` (12 constructors), `Thunk` (C arena cell), `Env` (C-native struct), `AttrSet` (C sorted arrays), `MonadEval` typeclass |+| `Nix.Eval.Compile` | Bytecode compiler — Expr AST to flat `nn_bytecode` instruction array |+| `Nix.Eval.Operator` | Arithmetic with float promotion, deep structural equality, floored division |+| `Nix.Eval.StringInterp` | String interpolation, `coerceToString`, indented string stripping, float formatting |+| `Nix.Eval.Context` | String context construction, queries, and extraction for derivation building |+| `Nix.Eval.IO` | IO evaluation monad — filesystem access, import cache, process execution, per-thunk memoization with blackhole detection |+| `Nix.Builtins` | 109 builtins, search path plumbing, top-level builtin exposure | -### Store + Builder+### C99 Data Layer | Module | Purpose | |--------|---------|-| `Nix.Derivation` | Derivation type, ATerm serialization + parsing (`toATerm`/`fromATerm`), platform detection |-| `Nix.Hash` | Derivation hashing, store path computation, shared hex/base-32 utilities |-| `Nix.Store.Path` | Store path types — `StoreDir`, `StorePath`, `parseStorePath`, Windows/Unix support |-| `Nix.Store.DB` | SQLite store database — `ValidPaths` + `Refs` tables, WAL mode, path registration, reference/deriver queries |-| `Nix.Store` | High-level store operations — `addToStore`, `scanReferences`, `setReadOnly`, `writeDrv` |-| `Nix.Builder` | Derivation builder — dependency graph construction, topological sort, binary cache substitution, local build with output registration |-| `Nix.DependencyGraph` | Dependency graph construction (BFS with `Seq` queue) and topological sort (Kahn's algorithm, O(V+E)), cycle detection |-| `Nix.Substituter` | Binary cache substituter — HTTP narinfo fetch, signature verification, NAR download/decompress/unpack, store registration. Multi-cache with priority ordering |-------## Architecture--```- Pure Core (no IO)- +-------------------------------------------------+- | |- | Parser --> Expr.Types --> Eval --> Builtins |- | | | |- | Expr.Resolve Eval.Types |- | Parser.Lexer Eval.Operator |- | Parser.Expr Eval.StringInterp |- | Parser.Internal Eval.Context |- | ParseError |- | | |- | Derivation --> Hash |- | | |- | Store.Path DependencyGraph |- | |- +-------------------------------------------------+- |- IO Boundary (thin)- +-------------------------------------------------+- | Eval.IO Store.DB Store Builder Substituter|- +-------------------------------------------------+-```--**Evaluator design:**--- **MonadEval typeclass** — The evaluator is `eval :: (MonadEval m) => Env -> Expr -> m NixValue`, polymorphic in its effect monad. `PureEval` (newtype over `Either Text`) runs all pure tests with no IO. `EvalIO` provides `readFileText`, `doesPathExist`, `listDirectory`, `getEnvVar`, `getCurrentTime`, `writeToStore`, `scopedImportFile`, `runProcess` for IO builtins.-- **Thunk-based lazy evaluation with memoization** — List elements and attribute set values are stored as unevaluated thunks (`Thunk Expr Env`). Only forced when a value is demanded. `(x: 1) (throw "boom")` returns `1` because `x` is never referenced. In `EvalIO`, each thunk carries a per-thunk `IORef` memo cell — forced once, then cached in place (matching real Nix's in-place mutation). Dead thunks are reclaimed by GC naturally.-- **Knot-tying via Haskell laziness** — Recursive `let` and `rec { }` create self-referential environments. The `Thunk` type has a lazy `Env` field so thunks can capture environments that include themselves. Haskell's own laziness resolves the recursion. Dynamic attribute keys are resolved monadically *before* knot-tying — the two-phase design (`resolveBindingKeys` then `buildResolvedBindingsMap`) cleanly separates key evaluation from value thunk construction.-- **Search path desugaring** — `<nixpkgs>` is its own AST constructor (`ESearchPath`), desugared at eval time to `builtins.findFile builtins.nixPath "nixpkgs"` — exactly how real Nix handles it. `builtins.nixPath` is populated from `NIX_PATH` and `--nix-path` flags.-- **With-scope chain** — `Env` has lexical bindings (always win) plus a stack of with-scopes walked innermost-first. `let a = 1; in with { a = 2; }; a` correctly returns `1` because lexical scope takes priority.-- **Short-circuit operators** — `&&`, `||`, and `->` are handled directly in eval (not delegated to Operator) because they must not evaluate both operands.-- **String context propagation** — Every `VStr` carries a `StringContext` tracking store path references (`SCPlain`, `SCDrvOutput`, `SCAllOutputs`). Context merges through interpolation, concatenation, and string builtins. The `derivation` builtin collects all context into `drvInputDrvs`/`drvInputSrcs`.--**Build pipeline:**--1. Evaluate `.nix` file to extract derivation-2. Build dependency graph by reading `.drv` files from the store (BFS traversal)-3. Topologically sort via Kahn's algorithm — leaves first, cycle detection-4. For each dependency in build order: check store cache, try binary substitution, build locally-5. Build execution: validate inputs, set up environment, run builder process, scan references, register outputs in SQLite DB+| `nn_symbol` | FNV-1a hash table for string interning — O(1) comparison |+| `nn_attrset` | Sorted arrays with binary search — O(log n) lookup, merge-join union |+| `nn_thunk` | 16-byte arena cells — 10 value tags for inline scalars and C-native types, blackhole detection |+| `nn_env` | 40-byte arena structs — slots, lazy scope, parent chain, with-scopes, resolved variable lookup |+| `nn_list` | Contiguous thunk pointer arrays |+| `nn_ctxstr` | Context-bearing strings with interned StorePath fields |+| `nn_bytecode` | Flat instruction array — 24 opcodes, 16-byte `nn_op_t` + data buffer |+| `nn_lambda` | Lambda closures — env ptr, body bytecode index, formals |+| `nn_arena` | Unified lifecycle — batch StablePtr cleanup, coordinated init/destroy | -**Key numbers:**+### Store + Builder -- **24 modules** — all implemented-- **526 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)+| Module | Purpose |+|--------|---------|+| `Nix.Derivation` | Derivation type, ATerm serialization + parsing, platform detection |+| `Nix.Store.Path` | Store path types — `StoreDir`, `StorePath`, Windows/Unix support |+| `Nix.Store.DB` | SQLite store database — WAL mode, path registration, reference queries |+| `Nix.Store` | `addToStore`, `scanReferences`, `setReadOnly`, `writeDrv` |+| `Nix.Builder` | Dependency graph, topological sort, binary cache substitution, local build |+| `Nix.DependencyGraph` | BFS graph construction, Kahn's toposort, cycle detection |+| `Nix.Hash` | Derivation hashing (`DrvHash`), SHA-256, truncated base-32, shared hash utilities |+| `Nix.Substituter` | HTTP binary cache — narinfo, Ed25519 verification, NAR download/unpack | --- -## The Hard Problems--Building Nix on Windows means solving real platform differences:--| Problem | Solution |-|---------|----------|-| **No `fork`/`exec`** | `System.Process.createProcess` maps to Win32 `CreateProcess` natively |-| **No symlinks (sometimes)** | Developer Mode enables symlinks; fallback to junction points / copies |-| **`/nix/store` doesn't exist** | `C:\nix\store` as `StoreDir` — all paths parameterized, never hardcoded |-| **Case-insensitive filesystem** | Nix store paths are case-sensitive by content hash — collisions impossible |-| **260-char path limit** | `\\?\` extended-length prefix (32K chars), already used by cargo/node |-| **No bash** | Ship `bash.exe` from MSYS2 (same as Git for Windows) |-| **Sandboxing** | Unsandboxed initially (macOS did this for years); future: Win32 Job Objects + App Containers |-| **stdenv bootstrap** | Cross-compile from Linux, or bootstrap from MSYS2 MinGW toolchain |-| **Cross-device moves** | `renameDirectory` can fail across devices; fallback to recursive copy + remove |--The biggest challenge isn't any single feature — it's **nixpkgs compatibility**. nixpkgs is 80,000+ packages defined as one massive recursive attrset. It exercises every builtin, every edge case in string context tracking, and every lazy evaluation pattern. The evaluator must handle all of this correctly and fast enough (~2-5 seconds for full nixpkgs eval).+## Roadmap ----+### Done -## Roadmap+- [x] Full Nix parser (14 precedence levels, all syntax forms)+- [x] Lazy bytecode evaluator (24 opcodes, thunk memoization, blackhole detection)+- [x] 109 builtins (matching real Nix spec)+- [x] C99 data layer (9 modules, all eval data off GHC heap)+- [x] Content-addressed store with SQLite metadata+- [x] Derivation builder with dependency resolution+- [x] Binary cache substituter with Ed25519 verification+- [x] String context tracking and propagation+- [x] nixpkgs lib layer evaluation (`lib.fix`, `lib.extends`, `lib.evalModules`) ### Next -- [ ] **Full `import <nixpkgs> {}` performance** — nixpkgs lib layer evaluates correctly; stdenv bootstrap runs but needs further memory optimization for the full 80,000+ package set (de Bruijn indices + SmallArray slots eliminated 1.37 GB of Map.Bin overhead; testing on 16 GB Windows machine in progress)-- [ ] **`nova-nix shell`** — Enter a development shell (like `nix shell`)-- [ ] **`nova-nix repl`** — Interactive evaluator+- [ ] Full `import <nixpkgs> {}` — bootstrap + package construction evaluate (lazy `derivation`); blocked on a named-formal-set (`@`-pattern) variable-slot bug+- [ ] `--system` flag — override `builtins.currentSystem` for cross-platform evaluation+- [ ] Windows stdenv — MinGW GCC + MSYS2 coreutils bootstrap for native Windows builds ### Long-Term -- [ ] **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+- [ ] `nova-nix shell` — enter a development shell+- [ ] `nova-nix repl` — interactive evaluator+- [ ] Nix daemon protocol compatibility+- [ ] XZ decompression for binary cache downloads --- @@ -306,12 +240,31 @@ ```bash cabal build # Build library + CLI-cabal test # Run all 526 tests+cabal test # Run all 593 tests cabal build --ghc-options="-Werror" # Warnings as errors (CI default)-cabal haddock # Generate API docs ``` -Requires GHC 9.8 and cabal-install 3.10+.+Requires GHC 9.8+ and cabal-install 3.10+.++---++## Library Usage++```haskell+import Nix.Parser (parseNix)+import Nix.Eval (eval, NixValue(..), PureEval(..))+import Nix.Builtins (builtinEnv)++main :: IO ()+main = do+ case parseNix "<stdin>" "let x = 5; y = x * 2; in y + 1" of+ Left err -> print err+ Right expr -> case runPureEval (eval (builtinEnv 0 []) expr) of+ Left err -> putStrLn ("Error: " ++ show err)+ Right val -> print val -- VInt 11+```++The evaluator is polymorphic via `MonadEval` — `PureEval` for pure tests, `EvalIO` for filesystem access. ---
app/Main.hs view
@@ -16,15 +16,17 @@ -- @ module Main (main) where -import Control.Monad ((>=>))+import Control.Monad (void, (>=>)) import qualified Data.Map.Strict as Map import qualified Data.Text as T import qualified Data.Text.IO as TIO import Nix.Builder (BuildConfig (..), BuildResult (..), buildWithDeps, defaultBuildConfig) import Nix.Builtins (builtinEnv, parseNixPath) import Nix.Derivation (Derivation (..), DerivationOutput (..))-import Nix.Eval (MonadEval, NixValue (..), Thunk (..), attrSetFromMap, attrSetLookup, attrSetToAscList, attrSetToMap, eval, force)+import Nix.Eval (MonadEval, NixValue (..), Thunk (..), attrSetFromMap, attrSetLookup, attrSetToAscList, attrSetToMap, eval, evaluated, force, readThunkValue)+import Nix.Eval.Arena (arenaInit) import Nix.Eval.IO (EvalState (..), newEvalState, runEvalIO)+import Nix.Eval.Types (clistFromThunks, clistThunks, thunkToCPtr) import Nix.Parser (parseNix, readFileAutoEncoding) import Nix.Store (Store, closeStore, openStore, writeDrv) import Nix.Store.Path (StorePath, defaultStoreDir, parseStorePath, platformStoreDir, storePathToFilePath)@@ -87,6 +89,8 @@ main :: IO () main = do hSetBuffering stdout LineBuffering+ -- Initialize C data layer (symbol interning, thunk arena, env allocator)+ arenaInit args <- getArgs dataDir <- getDataDir let opts = parseArgs args@@ -116,8 +120,9 @@ hPutStrLn stderr ("parse error: " ++ show err) exitFailure Right expr -> do- st <- newEvalState (takeDirectory filePath)- let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st)+ st0 <- newEvalState (takeDirectory filePath)+ let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st0)+ st = st0 {esSearchPaths = searchPaths} result <- runEvalIO st $ eval (builtinEnv (esTimestamp st) searchPaths) expr >>= finalize strict@@ -136,8 +141,9 @@ exitFailure Right expr -> do cwd <- getCurrentDirectory- st <- newEvalState cwd- let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st)+ st0 <- newEvalState cwd+ let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st0)+ st = st0 {esSearchPaths = searchPaths} result <- runEvalIO st $ eval (builtinEnv (esTimestamp st) searchPaths) expr >>= finalize strict@@ -156,9 +162,20 @@ hPutStrLn stderr ("parse error: " ++ show err) exitFailure Right expr -> do- st <- newEvalState (takeDirectory filePath)- let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st)- result <- runEvalIO st (eval (builtinEnv (esTimestamp st) searchPaths) expr)+ st0 <- newEvalState (takeDirectory filePath)+ let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st0)+ st = st0 {esSearchPaths = searchPaths}+ result <- runEvalIO st $ do+ val <- eval (builtinEnv (esTimestamp st) searchPaths) expr+ -- 'derivation' is a lazy wrapper now; force the attrs extractDerivation+ -- reads (a build legitimately needs the drvPath + closure).+ case val of+ VAttrs attrs ->+ mapM_+ (\k -> maybe (pure ()) (void . force) (attrSetLookup k attrs))+ ["_derivation", "drvPath"]+ _ -> pure ()+ pure val case result of Left err -> do TIO.hPutStrLn stderr ("eval error: " <> err)@@ -183,20 +200,20 @@ extractDerivation (VAttrs attrs) = do -- Check type = "derivation" case attrSetLookup "type" attrs of- Just (Evaluated (VStr "derivation" _)) -> pure ()+ Just thunk | Just (VStr "derivation" _) <- readThunkValue thunk -> pure () _ -> do hPutStrLn stderr "error: result is not a derivation (no type = \"derivation\")" exitFailure -- Extract the Derivation struct from _derivation drv <- case attrSetLookup "_derivation" attrs of- Just (Evaluated (VDerivation d)) -> pure d+ Just thunk | Just (VDerivation d) <- readThunkValue thunk -> pure d _ -> do hPutStrLn stderr "error: derivation result missing _derivation field" exitFailure -- Extract drvPath — this is the store path of the .drv file itself, -- computed by hashing the ATerm serialization during evaluation. drvSP <- case attrSetLookup "drvPath" attrs of- Just (Evaluated (VStr path _)) -> case parseStorePath defaultStoreDir path of+ Just thunk | Just (VStr path _) <- readThunkValue thunk -> case parseStorePath defaultStoreDir path of Just sp -> pure sp Nothing -> do TIO.hPutStrLn stderr ("error: invalid drvPath: " <> path)@@ -241,15 +258,16 @@ -- | Recursively force all thunks in a value, returning the fully -- materialized tree. Unlike 'deepForce' (which returns @()@), this--- rebuilds the value with all thunks replaced by 'Evaluated'.+-- rebuilds the value with all thunks replaced by computed C thunks. deepForceValue :: (MonadEval m) => NixValue -> m NixValue-deepForceValue (VList thunks) = do+deepForceValue (VList cl) = do+ let thunks = map Thunk (clistThunks cl) forced <- mapM (force >=> deepForceValue) thunks- pure (VList (map Evaluated forced))+ pure (VList (clistFromThunks (map (thunkToCPtr . evaluated) forced))) deepForceValue (VAttrs attrs) = do let m = attrSetToMap attrs forced <- mapM (force >=> deepForceValue) m- pure (VAttrs (attrSetFromMap (Map.map Evaluated forced)))+ pure (VAttrs (attrSetFromMap (Map.map evaluated forced))) deepForceValue val = pure val -- | Nix-style pretty-printing of a fully forced value.@@ -261,8 +279,8 @@ prettyValue VNull = "null" prettyValue (VStr s _) = "\"" <> escapeNixString s <> "\"" prettyValue (VPath p) = p-prettyValue (VList thunks) =- "[ " <> T.intercalate " " (map prettyThunk thunks) <> " ]"+prettyValue (VList cl) =+ "[ " <> T.intercalate " " (map (prettyThunk . Thunk) (clistThunks cl)) <> " ]" prettyValue (VAttrs attrs) = let entries = attrSetToAscList attrs rendered = map (\(k, t) -> k <> " = " <> prettyThunk t <> ";") entries@@ -276,10 +294,9 @@ [] -> "«derivation»" -- | Pretty-print a thunk. After deep-forcing, all thunks should be--- 'Evaluated'; unevaluated thunks render as a placeholder.+-- computed thunks render their value; pending thunks render as a placeholder. prettyThunk :: Thunk -> T.Text-prettyThunk (Evaluated val) = prettyValue val-prettyThunk (ThunkRef {}) = "«thunk»"+prettyThunk thunk = maybe "«thunk»" prettyValue (readThunkValue thunk) -- | Escape a string for Nix-style output (quotes, backslashes, newlines, tabs, carriage returns). escapeNixString :: T.Text -> T.Text
+ cbits/nn_arena.c view
@@ -0,0 +1,76 @@+/*+ * nn_arena.c — Batch StablePtr collection from the thunk arena.+ *+ * Iterates all thunks via nn_thunk_count/nn_thunk_get and identifies+ * payloads that are Haskell StablePtrs (as opposed to inline scalar+ * values or C pointers).+ *+ * After M6 bytecode integration, PENDING thunks store (bc_idx,+ * StablePtr Env) — the bc_idx replaces the Expr, but the Env stays+ * as a StablePtr for knot-tying laziness.+ *+ * StablePtr sources:+ * 1. PENDING/BLACKHOLE: payload = StablePtr Env (all pending thunks)+ * 2. COMPUTED with val_tag == NN_VALUE_PTR: StablePtr NixValue+ *+ * All other COMPUTED payloads (inline scalars, C pointers) are NOT+ * StablePtrs and must NOT be freed.+ */++#include "nn_arena.h"+#include "nn_attrset.h"+#include "nn_thunk.h"++#include <stddef.h>++uint32_t+nn_arena_stableptr_count(void)+{+ uint32_t total = nn_thunk_count();+ uint32_t count = 0;+ uint32_t i;++ for (i = 0; i < total; i++) {+ nn_thunk_t *t = nn_thunk_get(i);+ if (!t) continue;++ uint8_t state = nn_thunk_state(t);+ if (state == NN_THUNK_PENDING || state == NN_THUNK_BLACKHOLE) {+ void *p = nn_thunk_payload(t);+ if (p) count++;+ } else if (state == NN_THUNK_COMPUTED) {+ uint8_t vtag = nn_thunk_value_tag(t);+ if (vtag == NN_VALUE_PTR) {+ void *p = nn_thunk_payload(t);+ if (p) count++;+ }+ }+ }+ return count;+}++uint32_t+nn_arena_collect_stableptrs(void **output, uint32_t max_count)+{+ uint32_t total = nn_thunk_count();+ uint32_t written = 0;+ uint32_t i;++ for (i = 0; i < total && written < max_count; i++) {+ nn_thunk_t *t = nn_thunk_get(i);+ if (!t) continue;++ uint8_t state = nn_thunk_state(t);+ if (state == NN_THUNK_PENDING || state == NN_THUNK_BLACKHOLE) {+ void *p = nn_thunk_payload(t);+ if (p) output[written++] = p;+ } else if (state == NN_THUNK_COMPUTED) {+ uint8_t vtag = nn_thunk_value_tag(t);+ if (vtag == NN_VALUE_PTR) {+ void *p = nn_thunk_payload(t);+ if (p) output[written++] = p;+ }+ }+ }+ return written;+}
+ cbits/nn_attrset.c view
@@ -0,0 +1,384 @@+/*+ * nn_attrset.c — Sorted symbol-keyed attribute set.+ *+ * Two contiguous arrays: keys (nn_symbol_t) and values (void*),+ * sorted by symbol ID after freeze. Binary search for lookup.+ *+ * Memory layout after freeze (example with 4 entries):+ * keys: [ 3, 7, 12, 45 ] (sorted symbol IDs)+ * values: [ p0, p1, p2, p3 ] (parallel opaque pointers)+ *+ * This replaces Haskell's Map.Bin tree:+ * Map.Bin: ~48 bytes/node (constructor + key + value + size + left + right)+ * nn_attrset: ~12 bytes/entry (4-byte key + 8-byte pointer)+ * For 30k entries: 1.4 MB → 360 KB (4x reduction, plus cache-friendly)+ */++#include "nn_attrset.h"+#include "nn_assert.h"++#include <stdio.h>+#include <stdlib.h>+#include <string.h>++/* --- Global tracking for bulk cleanup --- */++static nn_attrset_t **g_tracked = NULL;+static uint32_t g_tracked_count = 0;+static uint32_t g_tracked_cap = 0;++static void nn_attrset_track(nn_attrset_t *set)+{+ if (g_tracked_count >= g_tracked_cap) {+ uint32_t new_cap = g_tracked_cap ? g_tracked_cap * 2 : 256;+ nn_attrset_t **new_arr = (nn_attrset_t **)realloc(+ g_tracked, (size_t)new_cap * sizeof(nn_attrset_t *));+ if (!new_arr) {+ fprintf(stderr, "nn_attrset_track: realloc failed\n");+ abort();+ }+ g_tracked = new_arr;+ g_tracked_cap = new_cap;+ }+ g_tracked[g_tracked_count++] = set;+}++void nn_attrset_free_all(void)+{+ uint32_t i;+ for (i = 0; i < g_tracked_count; i++) {+ nn_attrset_free(g_tracked[i]);+ }+ free(g_tracked);+ g_tracked = NULL;+ g_tracked_count = 0;+ g_tracked_cap = 0;+}++/* --- Internal representation --- */++struct nn_attrset {+ nn_symbol_t *keys;+ void **values;+ uint32_t count; /* entries used */+ uint32_t capacity; /* entries allocated */+ uint32_t frozen; /* 1 after freeze, 0 during construction */+};++/* --- Internal types --- */++typedef struct {+ nn_symbol_t key;+ uint32_t idx;+ void *value;+} nn_tagged_entry_t;++/* --- Forward declarations --- */++static int cmp_tagged(const void *a, const void *b);++/* --- Helpers --- */++/* Grow arrays to at least new_cap. Returns 0 on success, -1 on failure. */+static int grow(nn_attrset_t *set, uint32_t new_cap)+{+ nn_symbol_t *new_keys = (nn_symbol_t *)realloc(+ set->keys, (size_t)new_cap * sizeof(nn_symbol_t));+ if (!new_keys) return -1;+ set->keys = new_keys;++ void **new_values = (void **)realloc(+ set->values, (size_t)new_cap * sizeof(void *));+ if (!new_values) return -1;+ set->values = new_values;++ set->capacity = new_cap;+ return 0;+}++/* Binary search for a symbol in the sorted keys array.+ * Returns the index if found, or -1. */+static int32_t bsearch_key(const nn_symbol_t *keys, uint32_t count, nn_symbol_t key)+{+ uint32_t lo = 0;+ uint32_t hi = count;+ while (lo < hi) {+ uint32_t mid = lo + (hi - lo) / 2;+ if (keys[mid] < key) {+ lo = mid + 1;+ } else if (keys[mid] > key) {+ hi = mid;+ } else {+ return (int32_t)mid;+ }+ }+ return -1;+}++/* --- Lifecycle --- */++nn_attrset_t *nn_attrset_new(uint32_t capacity)+{+ nn_attrset_t *set = (nn_attrset_t *)malloc(sizeof(nn_attrset_t));+ if (!set) return NULL;++ if (capacity == 0) capacity = 8;++ set->keys = (nn_symbol_t *)malloc((size_t)capacity * sizeof(nn_symbol_t));+ set->values = (void **)malloc((size_t)capacity * sizeof(void *));+ set->count = 0;+ set->capacity = capacity;+ set->frozen = 0;++ if (!set->keys || !set->values) {+ free(set->keys);+ free(set->values);+ free(set);+ return NULL;+ }++ nn_attrset_track(set);+ return set;+}++void nn_attrset_free(nn_attrset_t *set)+{+ if (!set) return;+ free(set->keys);+ free(set->values);+ free(set);+}++/* --- Construction --- */++void nn_attrset_insert(nn_attrset_t *set, nn_symbol_t key, void *value)+{+ if (set->count >= set->capacity) {+ if (grow(set, set->capacity * 2) != 0) {+ fprintf(stderr, "nn_attrset_insert: grow failed (capacity %u)\n",+ (unsigned)set->capacity);+ abort();+ }+ }+ set->keys[set->count] = key;+ set->values[set->count] = value;+ set->count++;+}++void nn_attrset_freeze(nn_attrset_t *set)+{+ if (set->frozen) return;++ if (set->count <= 1) {+ set->frozen = 1;+ return;+ }++ /*+ * Sort by key. We need last-writer-wins for duplicates, but qsort+ * isn't stable. Strategy: tag each entry with its insertion index,+ * sort by (key, index), then dedup keeping the highest index per key.+ *+ * For small sets (<= 64), use a simple insertion sort that IS stable+ * (avoids the malloc overhead of tagging).+ */+ if (set->count <= 64) {+ /* Stable insertion sort. */+ uint32_t i, j;+ for (i = 1; i < set->count; i++) {+ nn_symbol_t tmpk = set->keys[i];+ void *tmpv = set->values[i];+ j = i;+ while (j > 0 && set->keys[j - 1] > tmpk) {+ set->keys[j] = set->keys[j - 1];+ set->values[j] = set->values[j - 1];+ j--;+ }+ set->keys[j] = tmpk;+ set->values[j] = tmpv;+ }+ } else {+ /*+ * Large set: tag entries with insertion index, qsort by (key, index),+ * then strip tags. This gives stable sort semantics.+ */+ nn_tagged_entry_t *tagged = (nn_tagged_entry_t *)malloc(+ (size_t)set->count * sizeof(nn_tagged_entry_t));+ if (!tagged) {+ /* Fallback: in-place insertion sort for large sets when malloc+ * fails. Falls through to the dedup block below. */+ for (uint32_t i = 1; i < set->count; i++) {+ nn_symbol_t key = set->keys[i];+ void *val = set->values[i];+ uint32_t j = i;+ while (j > 0 && set->keys[j-1] > key) {+ set->keys[j] = set->keys[j-1];+ set->values[j] = set->values[j-1];+ j--;+ }+ set->keys[j] = key;+ set->values[j] = val;+ }+ } else {+ uint32_t i;+ for (i = 0; i < set->count; i++) {+ tagged[i].key = set->keys[i];+ tagged[i].idx = i;+ tagged[i].value = set->values[i];+ }++ /* Sort by key, break ties by insertion index (ascending). */+ qsort(tagged, set->count, sizeof(nn_tagged_entry_t), cmp_tagged);++ for (i = 0; i < set->count; i++) {+ set->keys[i] = tagged[i].key;+ set->values[i] = tagged[i].value;+ }+ free(tagged);+ }+ }++ /* Dedup: keep last occurrence of each key (last-writer-wins).+ * After stable sort, duplicates are adjacent with the last insert+ * at the highest index position. */+ {+ uint32_t write = 0;+ uint32_t read;+ for (read = 0; read < set->count; read++) {+ /* If next entry has the same key, skip this one (keep the later one). */+ if (read + 1 < set->count && set->keys[read] == set->keys[read + 1]) {+ continue;+ }+ if (write != read) {+ set->keys[write] = set->keys[read];+ set->values[write] = set->values[read];+ }+ write++;+ }+ set->count = write;+ }++ set->frozen = 1;+}++/* --- Query --- */++void *nn_attrset_lookup(const nn_attrset_t *set, nn_symbol_t key)+{+ int32_t idx = bsearch_key(set->keys, set->count, key);+ if (idx < 0) return NULL;+ return set->values[idx];+}++int32_t nn_attrset_index(const nn_attrset_t *set, nn_symbol_t key)+{+ return bsearch_key(set->keys, set->count, key);+}++void nn_attrset_set_value(nn_attrset_t *set, uint32_t idx, void *value)+{+ NN_ASSERT(idx < set->count, "nn_attrset_set_value: idx out of bounds");+ set->values[idx] = value;+}++void *nn_attrset_get_value(const nn_attrset_t *set, uint32_t idx)+{+ NN_ASSERT(idx < set->count, "nn_attrset_get_value: idx out of bounds");+ return set->values[idx];+}++nn_symbol_t nn_attrset_get_key(const nn_attrset_t *set, uint32_t idx)+{+ NN_ASSERT(idx < set->count, "nn_attrset_get_key: idx out of bounds");+ return set->keys[idx];+}++uint32_t nn_attrset_size(const nn_attrset_t *set)+{+ return set->count;+}++const nn_symbol_t *nn_attrset_keys_ptr(const nn_attrset_t *set)+{+ return set->keys;+}++/* --- Set operations --- */++nn_attrset_t *nn_attrset_union(const nn_attrset_t *a, const nn_attrset_t *b)+{+ /* Merge-join: all keys from both, b wins on conflict. */+ uint32_t cap = a->count + b->count;+ nn_attrset_t *result = nn_attrset_new(cap);+ if (!result) { fprintf(stderr, "nn_attrset_union: alloc failed\n"); abort(); }+ uint32_t ia = 0, ib = 0;++ while (ia < a->count && ib < b->count) {+ if (a->keys[ia] < b->keys[ib]) {+ nn_attrset_insert(result, a->keys[ia], a->values[ia]);+ ia++;+ } else if (a->keys[ia] > b->keys[ib]) {+ nn_attrset_insert(result, b->keys[ib], b->values[ib]);+ ib++;+ } else {+ /* Same key — b wins (right-biased //). */+ nn_attrset_insert(result, b->keys[ib], b->values[ib]);+ ia++;+ ib++;+ }+ }+ while (ia < a->count) {+ nn_attrset_insert(result, a->keys[ia], a->values[ia]);+ ia++;+ }+ while (ib < b->count) {+ nn_attrset_insert(result, b->keys[ib], b->values[ib]);+ ib++;+ }++ result->frozen = 1; /* Merge preserves sorted order. */+ return result;+}++nn_attrset_t *nn_attrset_remove_keys(+ const nn_attrset_t *set,+ const nn_symbol_t *keys,+ uint32_t key_count)+{+ nn_attrset_t *result = nn_attrset_new(set->count);+ if (!result) { fprintf(stderr, "nn_attrset_remove_keys: alloc failed\n"); abort(); }+ uint32_t i;++ for (i = 0; i < set->count; i++) {+ /* Linear scan of removal keys — fine for small removal lists.+ * For large removal lists, sort them and merge-scan instead. */+ uint32_t j;+ int skip = 0;+ for (j = 0; j < key_count; j++) {+ if (set->keys[i] == keys[j]) {+ skip = 1;+ break;+ }+ }+ if (!skip) {+ nn_attrset_insert(result, set->keys[i], set->values[i]);+ }+ }++ result->frozen = 1; /* Source was sorted, no removals change order. */+ return result;+}++/* --- Internal: tagged sort comparator --- */++static int cmp_tagged(const void *a, const void *b)+{+ const nn_tagged_entry_t *ta = (const nn_tagged_entry_t *)a;+ const nn_tagged_entry_t *tb = (const nn_tagged_entry_t *)b;+ if (ta->key < tb->key) return -1;+ if (ta->key > tb->key) return 1;+ /* Same key: sort by insertion index (ascending) so last writer is last. */+ if (ta->idx < tb->idx) return -1;+ if (ta->idx > tb->idx) return 1;+ return 0;+}
+ cbits/nn_bytecode.c view
@@ -0,0 +1,134 @@+/*+ * nn_bytecode.c -- Flat bytecode storage for Nix expressions.+ *+ * Two growable arrays:+ * - g_ops: nn_op_t instructions (16 bytes each), indexed by uint32+ * - g_data: uint32_t values for variable-length operands+ *+ * Both use realloc-doubling. Write-once during compilation, random+ * access during evaluation. Indices remain stable across realloc.+ */++#include "nn_bytecode.h"+#include "nn_assert.h"++#include <stdio.h>+#include <stdlib.h>+#include <string.h>++/* --- Constants --- */++#define NN_BC_DEFAULT_OP_CAPACITY 65536u+#define NN_BC_DEFAULT_DATA_CAPACITY 131072u++/* --- Global state --- */++static nn_op_t *g_ops = NULL;+static uint32_t g_op_count = 0;+static uint32_t g_op_capacity = 0;++static uint32_t *g_data = NULL;+static uint32_t g_data_count = 0;+static uint32_t g_data_capacity = 0;++/* --- Lifecycle --- */++void nn_bytecode_init(uint32_t op_capacity, uint32_t data_capacity)+{+ if (g_ops) nn_bytecode_destroy();++ if (op_capacity == 0) op_capacity = NN_BC_DEFAULT_OP_CAPACITY;+ if (data_capacity == 0) data_capacity = NN_BC_DEFAULT_DATA_CAPACITY;++ g_ops = (nn_op_t *)malloc((size_t)op_capacity * sizeof(nn_op_t));+ if (!g_ops) { fprintf(stderr, "nn_bytecode_init: ops alloc failed\n"); abort(); }+ g_op_count = 0;+ g_op_capacity = op_capacity;++ g_data = (uint32_t *)malloc((size_t)data_capacity * sizeof(uint32_t));+ if (!g_data) { fprintf(stderr, "nn_bytecode_init: data alloc failed\n"); abort(); }+ g_data_count = 0;+ g_data_capacity = data_capacity;+}++void nn_bytecode_destroy(void)+{+ free(g_ops);+ g_ops = NULL;+ g_op_count = 0;+ g_op_capacity = 0;++ free(g_data);+ g_data = NULL;+ g_data_count = 0;+ g_data_capacity = 0;+}++/* --- Internal: grow arrays --- */++static int ensure_op_space(void)+{+ if (g_op_count < g_op_capacity) return 0;+ uint32_t new_cap = g_op_capacity * 2;+ if (new_cap < g_op_capacity) return -1; /* overflow guard */+ nn_op_t *new_ops = (nn_op_t *)realloc(g_ops, (size_t)new_cap * sizeof(nn_op_t));+ if (!new_ops) return -1;+ g_ops = new_ops;+ g_op_capacity = new_cap;+ return 0;+}++static int ensure_data_space(void)+{+ if (g_data_count < g_data_capacity) return 0;+ uint32_t new_cap = g_data_capacity * 2;+ if (new_cap < g_data_capacity) return -1; /* overflow guard */+ uint32_t *new_data = (uint32_t *)realloc(g_data, (size_t)new_cap * sizeof(uint32_t));+ if (!new_data) return -1;+ g_data = new_data;+ g_data_capacity = new_cap;+ return 0;+}++/* --- Emit --- */++uint32_t nn_bc_emit(uint8_t opcode, uint8_t flags, uint16_t short_arg,+ uint32_t arg1, uint32_t arg2, uint32_t arg3)+{+ if (ensure_op_space() != 0) return UINT32_MAX;+ uint32_t idx = g_op_count++;+ nn_op_t *op = &g_ops[idx];+ op->opcode = opcode;+ op->flags = flags;+ op->short_arg = short_arg;+ op->arg1 = arg1;+ op->arg2 = arg2;+ op->arg3 = arg3;+ return idx;+}++uint32_t nn_bc_emit_data(uint32_t value)+{+ if (ensure_data_space() != 0) return UINT32_MAX;+ uint32_t offset = g_data_count++;+ g_data[offset] = value;+ return offset;+}++/* --- Read instructions --- */++uint8_t nn_bc_opcode(uint32_t idx) { NN_ASSERT(idx < g_op_count, "nn_bc_opcode: idx out of bounds"); return g_ops[idx].opcode; }+uint8_t nn_bc_flags(uint32_t idx) { NN_ASSERT(idx < g_op_count, "nn_bc_flags: idx out of bounds"); return g_ops[idx].flags; }+uint16_t nn_bc_short_arg(uint32_t idx) { NN_ASSERT(idx < g_op_count, "nn_bc_short_arg: idx out of bounds"); return g_ops[idx].short_arg; }+uint32_t nn_bc_arg1(uint32_t idx) { NN_ASSERT(idx < g_op_count, "nn_bc_arg1: idx out of bounds"); return g_ops[idx].arg1; }+uint32_t nn_bc_arg2(uint32_t idx) { NN_ASSERT(idx < g_op_count, "nn_bc_arg2: idx out of bounds"); return g_ops[idx].arg2; }+uint32_t nn_bc_arg3(uint32_t idx) { NN_ASSERT(idx < g_op_count, "nn_bc_arg3: idx out of bounds"); return g_ops[idx].arg3; }++/* --- Read data --- */++uint32_t nn_bc_data(uint32_t offset) { NN_ASSERT(offset < g_data_count, "nn_bc_data: offset out of bounds"); return g_data[offset]; }++/* --- Diagnostics --- */++uint32_t nn_bc_op_count(void) { return g_op_count; }+uint32_t nn_bc_data_count(void) { return g_data_count; }
+ cbits/nn_ctxstr.c view
@@ -0,0 +1,147 @@+/*+ * nn_ctxstr.c — Context-bearing string allocation and tracking.+ *+ * Each nn_ctxstr_t is a single contiguous malloc (header + flexible+ * array of nn_sce_t elements). Pointers are tracked in a global+ * dynamic array for bulk cleanup via nn_ctxstr_free_all().+ */++#include "nn_ctxstr.h"+#include "nn_assert.h"++#include <stdio.h>+#include <stdlib.h>+#include <string.h>++/* --- Tracking array --- */++#define NN_CTXSTR_INITIAL_CAPACITY 256u++static nn_ctxstr_t **g_tracked = NULL;+static uint32_t g_tracked_count = 0;+static uint32_t g_tracked_capacity = 0;++static void+track(nn_ctxstr_t *s)+{+ if (g_tracked_count >= g_tracked_capacity) {+ uint32_t new_cap = g_tracked_capacity == 0+ ? NN_CTXSTR_INITIAL_CAPACITY+ : g_tracked_capacity * 2;+ nn_ctxstr_t **new_arr = (nn_ctxstr_t **)realloc(+ g_tracked, (size_t)new_cap * sizeof(nn_ctxstr_t *));+ if (!new_arr) {+ fprintf(stderr, "nn_ctxstr_track: realloc failed\n");+ abort();+ }+ g_tracked = new_arr;+ g_tracked_capacity = new_cap;+ }+ g_tracked[g_tracked_count++] = s;+}++/* --- Lifecycle --- */++nn_ctxstr_t *+nn_ctxstr_new(uint32_t text, uint16_t ctx_count)+{+ size_t size = sizeof(nn_ctxstr_t) + (size_t)ctx_count * sizeof(nn_sce_t);+ nn_ctxstr_t *s = (nn_ctxstr_t *)malloc(size);+ if (!s) return NULL;+ s->text = text;+ s->ctx_count = ctx_count;+ memset(s->ctx, 0, (size_t)ctx_count * sizeof(nn_sce_t));+ track(s);+ return s;+}++void+nn_ctxstr_free_all(void)+{+ uint32_t i;+ for (i = 0; i < g_tracked_count; i++) {+ free(g_tracked[i]);+ }+ free(g_tracked);+ g_tracked = NULL;+ g_tracked_count = 0;+ g_tracked_capacity = 0;+}++/* --- Element setters --- */++void+nn_ctxstr_set_plain(nn_ctxstr_t *s, uint16_t idx,+ uint32_t sp_hash, uint32_t sp_name)+{+ NN_ASSERT(idx < s->ctx_count, "nn_ctxstr_set_plain: idx out of bounds");+ s->ctx[idx].tag = NN_SCE_PLAIN;+ s->ctx[idx].sp_hash = sp_hash;+ s->ctx[idx].sp_name = sp_name;+ s->ctx[idx].output = 0;+}++void+nn_ctxstr_set_drv_output(nn_ctxstr_t *s, uint16_t idx,+ uint32_t sp_hash, uint32_t sp_name,+ uint32_t output)+{+ NN_ASSERT(idx < s->ctx_count, "nn_ctxstr_set_drv_output: idx out of bounds");+ s->ctx[idx].tag = NN_SCE_DRV_OUTPUT;+ s->ctx[idx].sp_hash = sp_hash;+ s->ctx[idx].sp_name = sp_name;+ s->ctx[idx].output = output;+}++void+nn_ctxstr_set_all_outputs(nn_ctxstr_t *s, uint16_t idx,+ uint32_t sp_hash, uint32_t sp_name)+{+ NN_ASSERT(idx < s->ctx_count, "nn_ctxstr_set_all_outputs: idx out of bounds");+ s->ctx[idx].tag = NN_SCE_ALL_OUTPUTS;+ s->ctx[idx].sp_hash = sp_hash;+ s->ctx[idx].sp_name = sp_name;+ s->ctx[idx].output = 0;+}++/* --- Accessors --- */++uint32_t+nn_ctxstr_text(const nn_ctxstr_t *s)+{+ return s->text;+}++uint16_t+nn_ctxstr_ctx_count(const nn_ctxstr_t *s)+{+ return s->ctx_count;+}++uint8_t+nn_ctxstr_elem_tag(const nn_ctxstr_t *s, uint16_t idx)+{+ NN_ASSERT(idx < s->ctx_count, "nn_ctxstr_elem_tag: idx out of bounds");+ return s->ctx[idx].tag;+}++uint32_t+nn_ctxstr_elem_hash(const nn_ctxstr_t *s, uint16_t idx)+{+ NN_ASSERT(idx < s->ctx_count, "nn_ctxstr_elem_hash: idx out of bounds");+ return s->ctx[idx].sp_hash;+}++uint32_t+nn_ctxstr_elem_name(const nn_ctxstr_t *s, uint16_t idx)+{+ NN_ASSERT(idx < s->ctx_count, "nn_ctxstr_elem_name: idx out of bounds");+ return s->ctx[idx].sp_name;+}++uint32_t+nn_ctxstr_elem_output(const nn_ctxstr_t *s, uint16_t idx)+{+ NN_ASSERT(idx < s->ctx_count, "nn_ctxstr_elem_output: idx out of bounds");+ return s->ctx[idx].output;+}
+ cbits/nn_env.c view
@@ -0,0 +1,290 @@+/*+ * nn_env.c — C-native evaluation environments.+ *+ * Arena-allocated nn_env_t structs and backing arrays. Pages are+ * 256 KB each, allocated on demand as a linked list. Each allocation+ * bumps a pointer within the current page.+ *+ * All memory is freed in bulk via nn_env_destroy().+ */++#include "nn_env.h"+#include "nn_assert.h"++#include <stdio.h>+#include <stdlib.h>+#include <string.h>++/* --- Constants --- */++#define NN_ENV_PAGE_SIZE (256u * 1024u) /* 256 KB per page */+#define NN_ENV_ALIGN 8u /* 8-byte alignment */++/* --- Internal types --- */++struct nn_env_page {+ struct nn_env_page *next; /* NULL for newest page */+ uint32_t used; /* bytes used in this page */+ uint32_t capacity; /* usable bytes in this page */+ /* Flexible array member: page data follows */+ char data[];+};++/* --- Global state --- */++static struct nn_env_page *g_first_page = NULL;+static struct nn_env_page *g_current_page = NULL;+static uint64_t g_total_bytes = 0;++/* Global empty env — initialized in nn_env_init, valid until destroy. */+static nn_env_t g_empty_env;++/* --- Internal helpers --- */++static struct nn_env_page *+alloc_page(uint32_t capacity)+{+ struct nn_env_page *page = (struct nn_env_page *)malloc(+ sizeof(struct nn_env_page) + (size_t)capacity);+ if (!page) return NULL;+ page->next = NULL;+ page->used = 0;+ page->capacity = capacity;+ return page;+}++static uint32_t+align_up(uint32_t n, uint32_t alignment)+{+ return (n + alignment - 1) & ~(alignment - 1);+}++/* Allocate raw bytes from the page-based bump allocator.+ * Returns aligned, zero-initialized memory. */+static void *+nn_env_alloc_raw(uint32_t bytes)+{+ bytes = align_up(bytes, NN_ENV_ALIGN);++ if (!g_current_page || g_current_page->used + bytes > g_current_page->capacity) {+ uint32_t page_cap = bytes > NN_ENV_PAGE_SIZE ? bytes : NN_ENV_PAGE_SIZE;+ struct nn_env_page *page = alloc_page(page_cap);+ if (!page) return NULL;+ if (g_current_page) {+ g_current_page->next = page;+ } else {+ g_first_page = page;+ }+ g_current_page = page;+ }++ void *result = g_current_page->data + g_current_page->used;+ memset(result, 0, bytes);+ g_current_page->used += bytes;+ g_total_bytes += bytes;+ return result;+}++/* --- Lifecycle --- */++void+nn_env_init(void)+{+ if (g_first_page) {+ nn_env_destroy();+ }++ g_first_page = alloc_page(NN_ENV_PAGE_SIZE);+ if (!g_first_page) { fprintf(stderr, "nn_env_init: page alloc failed\n"); abort(); }+ g_current_page = g_first_page;+ g_total_bytes = 0;++ /* Initialize global empty env */+ memset(&g_empty_env, 0, sizeof(g_empty_env));+}++void+nn_env_destroy(void)+{+ struct nn_env_page *page = g_first_page;+ while (page) {+ struct nn_env_page *next = page->next;+ free(page);+ page = next;+ }+ g_first_page = NULL;+ g_current_page = NULL;+ g_total_bytes = 0;+}++/* --- Slot allocation --- */++void **+nn_env_alloc_slots(uint32_t count)+{+ if (count == 0) return NULL;+ uint64_t bytes64 = (uint64_t)count * sizeof(void *);+ if (bytes64 > UINT32_MAX) return NULL;+ uint32_t bytes = (uint32_t)bytes64;+ return (void **)nn_env_alloc_raw(bytes);+}++/* --- Env constructors --- */++nn_env_t *+nn_env_empty(void)+{+ return &g_empty_env;+}++nn_env_t *+nn_env_new(void **slots, uint32_t slot_count,+ void *lazy_scope, nn_env_t *parent,+ void **with_scopes, uint16_t with_count)+{+ nn_env_t *env = (nn_env_t *)nn_env_alloc_raw((uint32_t)sizeof(nn_env_t));+ if (!env) return NULL;+ env->slots = slots;+ env->slot_count = slot_count;+ env->lazy_scope = lazy_scope;+ env->parent = parent;+ env->with_scopes = with_scopes;+ env->with_count = with_count;+ return env;+}++nn_env_t *+nn_env_from_slots(void **slots, uint32_t slot_count,+ nn_env_t *parent)+{+ nn_env_t *env = (nn_env_t *)nn_env_alloc_raw((uint32_t)sizeof(nn_env_t));+ if (!env) return NULL;+ env->slots = slots;+ env->slot_count = slot_count;+ env->lazy_scope = NULL;+ env->parent = parent;+ env->with_scopes = parent ? parent->with_scopes : NULL;+ env->with_count = parent ? parent->with_count : 0;+ return env;+}++nn_env_t *+nn_env_push_with(nn_env_t *base, void *scope)+{+ if (base->with_count >= UINT16_MAX) return NULL;+ uint16_t new_count = base->with_count + 1;+ void **new_withs = (void **)nn_env_alloc_raw(+ (uint32_t)new_count * (uint32_t)sizeof(void *));+ if (!new_withs) return NULL;++ /* New scope at index 0 (innermost) */+ new_withs[0] = scope;+ /* Copy existing with-scopes after it */+ if (base->with_count > 0 && base->with_scopes) {+ memcpy(new_withs + 1, base->with_scopes,+ (size_t)base->with_count * sizeof(void *));+ }++ nn_env_t *env = (nn_env_t *)nn_env_alloc_raw((uint32_t)sizeof(nn_env_t));+ if (!env) return NULL;+ env->slots = base->slots;+ env->slot_count = base->slot_count;+ env->lazy_scope = base->lazy_scope;+ env->parent = base->parent;+ env->with_scopes = new_withs;+ env->with_count = new_count;+ return env;+}++nn_env_t *+nn_env_new_minimal(void **slots, uint32_t slot_count)+{+ nn_env_t *env = (nn_env_t *)nn_env_alloc_raw((uint32_t)sizeof(nn_env_t));+ if (!env) return NULL;+ env->slots = slots;+ env->slot_count = slot_count;+ /* lazy_scope, parent, with_scopes, with_count already zero from alloc_raw */+ return env;+}++/* --- Accessors --- */++void **+nn_env_slots(const nn_env_t *env)+{+ return env->slots;+}++uint32_t+nn_env_slot_count(const nn_env_t *env)+{+ return env->slot_count;+}++void *+nn_env_lazy_scope(const nn_env_t *env)+{+ return env->lazy_scope;+}++nn_env_t *+nn_env_parent(const nn_env_t *env)+{+ return env->parent;+}++void **+nn_env_with_scopes(const nn_env_t *env)+{+ return env->with_scopes;+}++uint16_t+nn_env_with_count(const nn_env_t *env)+{+ return env->with_count;+}++/* --- Lookup --- */++void *+nn_env_lookup_resolved(const nn_env_t *env, int level, int idx)+{+ NN_ASSERT(env != NULL, "nn_env_lookup_resolved: NULL env");+ while (level > 0) {+ NN_ASSERT(env->parent != NULL, "nn_env_lookup_resolved: parent chain too short for level");+ env = env->parent;+ level--;+ }+ NN_ASSERT(idx >= 0 && (uint32_t)idx < env->slot_count, "nn_env_lookup_resolved: idx out of bounds");+ return env->slots[idx];+}++void *+nn_env_root_scope(const nn_env_t *env)+{+ while (env->parent) {+ env = env->parent;+ }+ return env->lazy_scope;+}++/* --- With-scopes array allocation --- */++void **+nn_env_alloc_with_scopes(uint16_t count)+{+ if (count == 0) return NULL;+ uint64_t bytes64 = (uint64_t)count * sizeof(void *);+ if (bytes64 > UINT32_MAX) return NULL;+ uint32_t bytes = (uint32_t)bytes64;+ return (void **)nn_env_alloc_raw(bytes);+}++/* --- Diagnostics --- */++uint64_t+nn_env_bytes_allocated(void)+{+ return g_total_bytes;+}
+ cbits/nn_lambda.c view
@@ -0,0 +1,157 @@+/*+ * nn_lambda.c — Lambda closure structs for nova-nix.+ *+ * Each nn_lambda_t is a malloc'd struct holding the closure env,+ * body bytecode index, and formals specification. Tracked in a+ * global array for bulk cleanup at evaluation end.+ */++#include "nn_lambda.h"+#include "nn_assert.h"++#include <stdio.h>+#include <stdlib.h>+#include <string.h>++/* --- Global tracking for bulk cleanup --- */++static nn_lambda_t **g_tracked = NULL;+static uint32_t g_tracked_count = 0;+static uint32_t g_tracked_cap = 0;++static void nn_lambda_track(nn_lambda_t *lam)+{+ if (g_tracked_count >= g_tracked_cap) {+ uint32_t new_cap = g_tracked_cap ? g_tracked_cap * 2 : 256;+ nn_lambda_t **new_arr = (nn_lambda_t **)realloc(+ g_tracked, (size_t)new_cap * sizeof(nn_lambda_t *));+ if (!new_arr) {+ fprintf(stderr, "nn_lambda_track: realloc failed\n");+ abort();+ }+ g_tracked = new_arr;+ g_tracked_cap = new_cap;+ }+ g_tracked[g_tracked_count++] = lam;+}++/* --- Lifecycle --- */++nn_lambda_t *+nn_lambda_new(struct nn_env *env, uint32_t body_bc_idx,+ uint8_t formals_type, uint32_t name_sym,+ uint8_t allow_extra, uint16_t formal_count)+{+ nn_lambda_t *lam = (nn_lambda_t *)malloc(sizeof(nn_lambda_t));+ if (!lam) return NULL;++ lam->env = env;+ lam->body_bc_idx = body_bc_idx;+ lam->formals_type = formals_type;+ lam->name_sym = name_sym;+ lam->allow_extra = allow_extra;+ lam->formal_count = formal_count;++ if (formal_count > 0) {+ lam->entries = (nn_formal_entry_t *)malloc(+ (size_t)formal_count * sizeof(nn_formal_entry_t));+ if (!lam->entries) {+ free(lam);+ return NULL;+ }+ memset(lam->entries, 0,+ (size_t)formal_count * sizeof(nn_formal_entry_t));+ } else {+ lam->entries = NULL;+ }++ nn_lambda_track(lam);+ return lam;+}++void+nn_lambda_set_entry(nn_lambda_t *lam, uint16_t idx,+ uint32_t name_sym, uint32_t has_default,+ uint32_t default_bc_idx)+{+ NN_ASSERT(idx < lam->formal_count, "nn_lambda_set_entry: idx out of bounds");+ lam->entries[idx].name_sym = name_sym;+ lam->entries[idx].has_default = has_default;+ lam->entries[idx].default_bc_idx = default_bc_idx;+}++void+nn_lambda_free_all(void)+{+ uint32_t i;+ for (i = 0; i < g_tracked_count; i++) {+ free(g_tracked[i]->entries);+ free(g_tracked[i]);+ }+ free(g_tracked);+ g_tracked = NULL;+ g_tracked_count = 0;+ g_tracked_cap = 0;+}++/* --- Accessors --- */++struct nn_env *+nn_lambda_env(const nn_lambda_t *lam)+{+ return lam->env;+}++uint32_t+nn_lambda_body(const nn_lambda_t *lam)+{+ return lam->body_bc_idx;+}++uint8_t+nn_lambda_formals_type(const nn_lambda_t *lam)+{+ return lam->formals_type;+}++uint32_t+nn_lambda_name_sym(const nn_lambda_t *lam)+{+ return lam->name_sym;+}++uint8_t+nn_lambda_allow_extra(const nn_lambda_t *lam)+{+ return lam->allow_extra;+}++uint16_t+nn_lambda_formal_count(const nn_lambda_t *lam)+{+ return lam->formal_count;+}++uint32_t+nn_lambda_entry_name(const nn_lambda_t *lam, uint16_t idx)+{+ NN_ASSERT(lam->entries != NULL && idx < lam->formal_count,+ "nn_lambda_entry_name: idx out of bounds or no formals");+ return lam->entries[idx].name_sym;+}++uint32_t+nn_lambda_entry_has_default(const nn_lambda_t *lam, uint16_t idx)+{+ NN_ASSERT(lam->entries != NULL && idx < lam->formal_count,+ "nn_lambda_entry_has_default: idx out of bounds or no formals");+ return lam->entries[idx].has_default;+}++uint32_t+nn_lambda_entry_default(const nn_lambda_t *lam, uint16_t idx)+{+ NN_ASSERT(lam->entries != NULL && idx < lam->formal_count,+ "nn_lambda_entry_default: idx out of bounds or no formals");+ return lam->entries[idx].default_bc_idx;+}
+ cbits/nn_list.c view
@@ -0,0 +1,96 @@+/*+ * nn_list.c — Contiguous array of thunk pointers for lists.+ *+ * Each nn_list_t is a small header (malloc'd, tracked) pointing to+ * a contiguous items array (page-allocated via nn_env_alloc_slots).+ * Bulk cleanup frees all headers; the page allocator frees items.+ */++#include "nn_list.h"+#include "nn_env.h"+#include "nn_assert.h"++#include <stdio.h>+#include <stdlib.h>++/* --- Global tracking for bulk cleanup --- */++static nn_list_t **g_tracked = NULL;+static uint32_t g_tracked_count = 0;+static uint32_t g_tracked_cap = 0;++static void nn_list_track(nn_list_t *list)+{+ if (g_tracked_count >= g_tracked_cap) {+ uint32_t new_cap = g_tracked_cap ? g_tracked_cap * 2 : 256;+ nn_list_t **new_arr = (nn_list_t **)realloc(+ g_tracked, (size_t)new_cap * sizeof(nn_list_t *));+ if (!new_arr) {+ fprintf(stderr, "nn_list_track: realloc failed\n");+ abort();+ }+ g_tracked = new_arr;+ g_tracked_cap = new_cap;+ }+ g_tracked[g_tracked_count++] = list;+}++/* --- Lifecycle --- */++nn_list_t *+nn_list_new(uint32_t count)+{+ if (count == 0) return NULL;++ nn_list_t *list = (nn_list_t *)malloc(sizeof(nn_list_t));+ if (!list) return NULL;++ /* Items array via the env page allocator (O(1) bump allocation).+ * nn_env_alloc_slots returns void**, which we cast to nn_thunk**.+ * All slots are zero-initialized (NULL pointers). */+ list->items = (struct nn_thunk **)nn_env_alloc_slots(count);+ list->count = count;++ if (!list->items) {+ free(list);+ return NULL;+ }++ nn_list_track(list);+ return list;+}++void+nn_list_free_all(void)+{+ uint32_t i;+ for (i = 0; i < g_tracked_count; i++) {+ free(g_tracked[i]);+ }+ free(g_tracked);+ g_tracked = NULL;+ g_tracked_count = 0;+ g_tracked_cap = 0;+}++/* --- Access --- */++uint32_t+nn_list_count(const nn_list_t *list)+{+ return list->count;+}++struct nn_thunk *+nn_list_get(const nn_list_t *list, uint32_t index)+{+ NN_ASSERT(index < list->count, "nn_list_get: index out of bounds");+ return list->items[index];+}++void+nn_list_set(nn_list_t *list, uint32_t index, struct nn_thunk *thunk)+{+ NN_ASSERT(index < list->count, "nn_list_set: index out of bounds");+ list->items[index] = thunk;+}
+ cbits/nn_symbol.c view
@@ -0,0 +1,254 @@+/*+ * nn_symbol.c — Interned string symbol table.+ *+ * Implementation: open-addressed hash table with linear probing.+ * String data is stored in a contiguous arena (bulk allocation,+ * no per-string malloc/free). Symbols are 1-indexed uint32_t+ * values; 0 is the invalid sentinel.+ *+ * The table doubles when load exceeds 75%. Deletion is not+ * supported — symbols live for the entire evaluation lifetime.+ */++#include "nn_symbol.h"+#include "nn_assert.h"++#include <stdio.h>+#include <stdlib.h>+#include <string.h>++/* --- Configuration --- */++#define NN_SYMBOL_DEFAULT_CAPACITY 4096+#define NN_SYMBOL_LOAD_PERCENT 75+#define NN_SYMBOL_ARENA_INITIAL (256 * 1024) /* 256 KB */++/* --- Internal types --- */++/* Per-symbol metadata stored in a flat array indexed by symbol ID. */+typedef struct {+ uint32_t offset; /* byte offset into string arena */+ uint32_t len; /* byte length of the string */+ uint32_t hash; /* cached FNV-1a hash */+} nn_symbol_entry_t;++/* Hash table slot: maps hash → symbol ID. Empty slots have id == 0. */+typedef struct {+ uint32_t hash;+ uint32_t id; /* 1-based symbol ID, 0 = empty */+} nn_slot_t;++/* Global symbol table state. */+static struct {+ /* Symbol metadata array (1-indexed; index 0 unused). */+ nn_symbol_entry_t *entries;+ uint32_t count; /* number of interned symbols */+ uint32_t entries_cap; /* allocated entry slots */++ /* Hash table (open addressing, linear probing). */+ nn_slot_t *slots;+ uint32_t slots_cap; /* always a power of two */+ uint32_t slots_mask; /* slots_cap - 1 */++ /* Contiguous string arena. */+ char *arena;+ size_t arena_used;+ size_t arena_cap;+} g_sym;++/* --- FNV-1a hash --- */++static uint32_t fnv1a(const char *data, size_t len)+{+ uint32_t h = 2166136261u;+ size_t i;+ for (i = 0; i < len; i++) {+ h ^= (uint8_t)data[i];+ h *= 16777619u;+ }+ return h;+}++/* --- Arena --- */++/* Ensure the arena has room for `needed` more bytes. */+static void arena_ensure(size_t needed)+{+ if (g_sym.arena_used + needed <= g_sym.arena_cap) return;++ size_t new_cap = g_sym.arena_cap;+ while (new_cap < g_sym.arena_used + needed) {+ new_cap *= 2;+ }+ char *new_arena = (char *)realloc(g_sym.arena, new_cap);+ if (!new_arena) {+ fprintf(stderr, "nn_symbol: arena realloc failed (requested %zu bytes)\n", new_cap);+ abort();+ }+ g_sym.arena = new_arena;+ g_sym.arena_cap = new_cap;+}++/* Copy `len` bytes into the arena, returning the start offset. */+static uint32_t arena_push(const char *str, size_t len)+{+ arena_ensure(len + 1); /* +1 for null terminator */+ if (g_sym.arena_used > (size_t)UINT32_MAX) {+ fprintf(stderr, "nn_symbol: arena offset exceeds uint32_t range\n");+ abort();+ }+ uint32_t offset = (uint32_t)g_sym.arena_used;+ memcpy(g_sym.arena + offset, str, len);+ g_sym.arena[offset + len] = '\0';+ g_sym.arena_used += len + 1;+ return offset;+}++/* --- Entry array --- */++/* Ensure the entry array has room for one more symbol. */+static void entries_ensure(void)+{+ /* count is 0-based count, entries are 1-indexed, so we need count+1 < cap */+ if (g_sym.count + 1 < g_sym.entries_cap) return;++ uint32_t new_cap = g_sym.entries_cap * 2;+ nn_symbol_entry_t *new_entries = (nn_symbol_entry_t *)realloc(+ g_sym.entries, (size_t)new_cap * sizeof(nn_symbol_entry_t));+ if (!new_entries) {+ fprintf(stderr, "nn_symbol: entries realloc failed\n");+ abort();+ }+ g_sym.entries = new_entries;+ g_sym.entries_cap = new_cap;+}++/* --- Hash table --- */++/* Rebuild the hash table at double capacity. */+static void slots_grow(void)+{+ uint32_t new_cap = g_sym.slots_cap * 2;+ uint32_t new_mask = new_cap - 1;+ nn_slot_t *new_slots = (nn_slot_t *)calloc((size_t)new_cap, sizeof(nn_slot_t));+ if (!new_slots) {+ fprintf(stderr, "nn_symbol: slots calloc failed\n");+ abort();+ }++ uint32_t i;+ for (i = 0; i < g_sym.slots_cap; i++) {+ if (g_sym.slots[i].id == 0) continue;+ uint32_t idx = g_sym.slots[i].hash & new_mask;+ while (new_slots[idx].id != 0) {+ idx = (idx + 1) & new_mask;+ }+ new_slots[idx] = g_sym.slots[i];+ }++ free(g_sym.slots);+ g_sym.slots = new_slots;+ g_sym.slots_cap = new_cap;+ g_sym.slots_mask = new_mask;+}++/* --- Public API --- */++void nn_symbol_init(uint32_t initial_capacity)+{+ uint32_t cap = NN_SYMBOL_DEFAULT_CAPACITY;+ if (initial_capacity > cap) {+ /* Round up to power of two. */+ cap = 1;+ while (cap < initial_capacity) cap *= 2;+ }++ memset(&g_sym, 0, sizeof(g_sym));++ /* Entry array (1-indexed, so allocate cap+1 but we just use cap). */+ g_sym.entries_cap = cap;+ g_sym.entries = (nn_symbol_entry_t *)calloc(+ (size_t)cap, sizeof(nn_symbol_entry_t));+ if (!g_sym.entries) { fprintf(stderr, "nn_symbol_init: entries alloc failed\n"); abort(); }+ g_sym.count = 0;++ /* Hash table at 2x entries for low load factor. */+ g_sym.slots_cap = cap * 2;+ g_sym.slots_mask = g_sym.slots_cap - 1;+ g_sym.slots = (nn_slot_t *)calloc(+ (size_t)g_sym.slots_cap, sizeof(nn_slot_t));+ if (!g_sym.slots) { fprintf(stderr, "nn_symbol_init: slots alloc failed\n"); abort(); }++ /* String arena. */+ g_sym.arena_cap = NN_SYMBOL_ARENA_INITIAL;+ g_sym.arena = (char *)malloc(g_sym.arena_cap);+ if (!g_sym.arena) { fprintf(stderr, "nn_symbol_init: arena alloc failed\n"); abort(); }+ g_sym.arena_used = 0;+}++void nn_symbol_destroy(void)+{+ free(g_sym.entries);+ free(g_sym.slots);+ free(g_sym.arena);+ memset(&g_sym, 0, sizeof(g_sym));+}++nn_symbol_t nn_symbol_intern(const char *str, size_t len)+{+ uint32_t hash = fnv1a(str, len);+ uint32_t idx = hash & g_sym.slots_mask;++ /* Probe for existing entry. */+ for (;;) {+ uint32_t id = g_sym.slots[idx].id;+ if (id == 0) break; /* empty slot — not found */++ if (g_sym.slots[idx].hash == hash) {+ nn_symbol_entry_t *e = &g_sym.entries[id];+ if (e->len == (uint32_t)len &&+ memcmp(g_sym.arena + e->offset, str, len) == 0) {+ return (nn_symbol_t)id;+ }+ }+ idx = (idx + 1) & g_sym.slots_mask;+ }++ /* Not found — insert new symbol. */+ entries_ensure();+ g_sym.count++;+ uint32_t new_id = g_sym.count; /* 1-based */++ uint32_t offset = arena_push(str, len);++ g_sym.entries[new_id].offset = offset;+ g_sym.entries[new_id].len = (uint32_t)len;+ g_sym.entries[new_id].hash = hash;++ g_sym.slots[idx].hash = hash;+ g_sym.slots[idx].id = new_id;++ /* Grow hash table if load exceeds threshold. */+ if ((uint64_t)g_sym.count * 100 >= (uint64_t)g_sym.slots_cap * NN_SYMBOL_LOAD_PERCENT) {+ slots_grow();+ }++ return (nn_symbol_t)new_id;+}++const char *nn_symbol_text(nn_symbol_t sym)+{+ if (sym == NN_SYMBOL_INVALID || sym > g_sym.count) return NULL;+ return g_sym.arena + g_sym.entries[sym].offset;+}++size_t nn_symbol_len(nn_symbol_t sym)+{+ if (sym == NN_SYMBOL_INVALID || sym > g_sym.count) return 0;+ return (size_t)g_sym.entries[sym].len;+}++uint32_t nn_symbol_count(void)+{+ return g_sym.count;+}
+ cbits/nn_thunk.c view
@@ -0,0 +1,532 @@+/*+ * nn_thunk.c — Arena-allocated thunk memoization cells.+ *+ * Chunked bump allocator: linked list of blocks, each containing a+ * contiguous array of nn_thunk_t slots. When the current block is+ * full, a new block is allocated (same capacity). Pointers into any+ * block remain valid until the arena is destroyed.+ *+ * Memory layout per block:+ * [nn_thunk_block header] [slot 0] [slot 1] ... [slot N-1]+ *+ * Block header uses C99 flexible array member for the slots array,+ * so the entire block (header + slots) is one contiguous allocation.+ */++#include "nn_thunk.h"++#include <stdlib.h>+#include <string.h>++/* --- Constants --- */++#define NN_THUNK_DEFAULT_BLOCK_CAPACITY 65536u /* 64k thunks = 1 MB */++/* --- Internal types --- */++struct nn_thunk_block {+ struct nn_thunk_block *next; /* NULL for newest block */+ uint32_t count; /* slots used in this block */+ uint32_t capacity; /* total slots in this block */+ nn_thunk_t slots[]; /* C99 flexible array member */+};++struct nn_thunk_arena {+ struct nn_thunk_block *head; /* first block (for iteration) */+ struct nn_thunk_block *current; /* newest block (for allocation) */+ uint32_t total; /* total thunks across all blocks */+ uint32_t block_capacity; /* slots per new block */+};++/* --- Global arena --- */++static struct nn_thunk_arena *g_arena = NULL;++/* --- Internal helpers --- */++static struct nn_thunk_block *+alloc_block(uint32_t capacity)+{+ struct nn_thunk_block *block = (struct nn_thunk_block *)malloc(+ sizeof(struct nn_thunk_block) + (size_t)capacity * sizeof(nn_thunk_t));+ if (!block) return NULL;+ block->next = NULL;+ block->count = 0;+ block->capacity = capacity;+ return block;+}++static nn_thunk_t *+arena_alloc(struct nn_thunk_arena *arena)+{+ /* Current block full — allocate a new one */+ if (arena->current->count >= arena->current->capacity) {+ struct nn_thunk_block *block = alloc_block(arena->block_capacity);+ if (!block) return NULL;+ arena->current->next = block;+ arena->current = block;+ }+ nn_thunk_t *t = &arena->current->slots[arena->current->count];+ arena->current->count++;+ arena->total++;+ return t;+}++/* --- Lifecycle --- */++void+nn_thunk_init(uint32_t initial_capacity)+{+ /* Destroy existing arena if re-initializing */+ if (g_arena) {+ nn_thunk_destroy();+ }++ uint32_t cap = initial_capacity > 0 ? initial_capacity+ : NN_THUNK_DEFAULT_BLOCK_CAPACITY;++ g_arena = (struct nn_thunk_arena *)malloc(sizeof(struct nn_thunk_arena));+ if (!g_arena) return;++ struct nn_thunk_block *first = alloc_block(cap);+ if (!first) {+ free(g_arena);+ g_arena = NULL;+ return;+ }++ g_arena->head = first;+ g_arena->current = first;+ g_arena->total = 0;+ g_arena->block_capacity = cap;+}++void+nn_thunk_destroy(void)+{+ if (!g_arena) return;++ struct nn_thunk_block *block = g_arena->head;+ while (block) {+ struct nn_thunk_block *next = block->next;+ free(block);+ block = next;+ }++ free(g_arena);+ g_arena = NULL;+}++/* --- Allocation --- */++nn_thunk_t *+nn_thunk_new(void *pending_data)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_PENDING;+ t->val_tag = 0;+ t->_pad = 0;+ t->bc_idx = 0; /* legacy StablePtr thunk marker */+ t->payload = pending_data;+ return t;+}++nn_thunk_t *+nn_thunk_new_bc(uint32_t bc_idx, void *env_ptr)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_PENDING;+ t->val_tag = 0;+ t->_pad = 0;+ t->bc_idx = bc_idx;+ t->payload = env_ptr;+ return t;+}++uint32_t+nn_thunk_get_bc_idx(const nn_thunk_t *thunk)+{+ return thunk->bc_idx;+}++void+nn_thunk_set_payload(nn_thunk_t *thunk, void *payload)+{+ thunk->payload = payload;+}++nn_thunk_t *+nn_thunk_new_computed(void *value)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_COMPUTED;+ t->val_tag = NN_VALUE_PTR;+ t->_pad = 0;+ t->bc_idx = 0;+ t->payload = value;+ return t;+}++nn_thunk_t *+nn_thunk_new_computed_int(int64_t value)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_COMPUTED;+ t->val_tag = NN_VALUE_INT;+ t->_pad = 0;+ t->bc_idx = 0;+ t->payload = (void *)(intptr_t)value;+ return t;+}++nn_thunk_t *+nn_thunk_new_computed_float(double value)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_COMPUTED;+ t->val_tag = NN_VALUE_FLOAT;+ t->_pad = 0;+ t->bc_idx = 0;+ memcpy(&t->payload, &value, sizeof(double));+ return t;+}++nn_thunk_t *+nn_thunk_new_computed_bool(uint8_t value)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_COMPUTED;+ t->val_tag = NN_VALUE_BOOL;+ t->_pad = 0;+ t->bc_idx = 0;+ t->payload = (void *)(intptr_t)value;+ return t;+}++nn_thunk_t *+nn_thunk_new_computed_null(void)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_COMPUTED;+ t->val_tag = NN_VALUE_NULL;+ t->_pad = 0;+ t->bc_idx = 0;+ t->payload = NULL;+ return t;+}++nn_thunk_t *+nn_thunk_new_computed_str(uint32_t symbol)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_COMPUTED;+ t->val_tag = NN_VALUE_STR;+ t->_pad = 0;+ t->bc_idx = 0;+ t->payload = (void *)(intptr_t)symbol;+ return t;+}++nn_thunk_t *+nn_thunk_new_computed_path(uint32_t symbol)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_COMPUTED;+ t->val_tag = NN_VALUE_PATH;+ t->_pad = 0;+ t->bc_idx = 0;+ t->payload = (void *)(intptr_t)symbol;+ return t;+}++nn_thunk_t *+nn_thunk_new_computed_list(void *list)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_COMPUTED;+ t->val_tag = NN_VALUE_LIST;+ t->_pad = 0;+ t->bc_idx = 0;+ t->payload = list;+ return t;+}++nn_thunk_t *+nn_thunk_new_computed_attrs(void *attrset)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_COMPUTED;+ t->val_tag = NN_VALUE_ATTRS;+ t->_pad = 0;+ t->bc_idx = 0;+ t->payload = attrset;+ return t;+}++nn_thunk_t *+nn_thunk_new_computed_ctxstr(void *ctxstr)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_COMPUTED;+ t->val_tag = NN_VALUE_CTXSTR;+ t->_pad = 0;+ t->bc_idx = 0;+ t->payload = ctxstr;+ return t;+}++nn_thunk_t *+nn_thunk_new_computed_lambda(void *lambda)+{+ nn_thunk_t *t = arena_alloc(g_arena);+ if (!t) return NULL;+ t->state = NN_THUNK_COMPUTED;+ t->val_tag = NN_VALUE_LAMBDA;+ t->_pad = 0;+ t->bc_idx = 0;+ t->payload = lambda;+ return t;+}++/* --- State queries --- */++uint8_t+nn_thunk_state(const nn_thunk_t *thunk)+{+ return thunk->state;+}++void *+nn_thunk_payload(const nn_thunk_t *thunk)+{+ return thunk->payload;+}++uint8_t+nn_thunk_value_tag(const nn_thunk_t *thunk)+{+ return thunk->val_tag;+}++int64_t+nn_thunk_get_int(const nn_thunk_t *thunk)+{+ return (int64_t)(intptr_t)thunk->payload;+}++double+nn_thunk_get_float(const nn_thunk_t *thunk)+{+ double result;+ memcpy(&result, &thunk->payload, sizeof(double));+ return result;+}++uint8_t+nn_thunk_get_bool(const nn_thunk_t *thunk)+{+ return (uint8_t)(intptr_t)thunk->payload;+}++uint32_t+nn_thunk_get_str(const nn_thunk_t *thunk)+{+ return (uint32_t)(intptr_t)thunk->payload;+}++uint32_t+nn_thunk_get_path(const nn_thunk_t *thunk)+{+ return (uint32_t)(intptr_t)thunk->payload;+}++void *+nn_thunk_get_list(const nn_thunk_t *thunk)+{+ return thunk->payload;+}++void *+nn_thunk_get_attrs(const nn_thunk_t *thunk)+{+ return thunk->payload;+}++void *+nn_thunk_get_ctxstr(const nn_thunk_t *thunk)+{+ return thunk->payload;+}++void *+nn_thunk_get_lambda(const nn_thunk_t *thunk)+{+ return thunk->payload;+}++/* --- State transitions --- */++int+nn_thunk_mark_blackhole(nn_thunk_t *thunk)+{+ if (thunk->state != NN_THUNK_PENDING) return 0;+ thunk->state = NN_THUNK_BLACKHOLE;+ return 1;+}++void *+nn_thunk_set_computed(nn_thunk_t *thunk, void *value)+{+ if (thunk->state == NN_THUNK_COMPUTED) return NULL;+ void *old = thunk->payload;+ thunk->state = NN_THUNK_COMPUTED;+ thunk->val_tag = NN_VALUE_PTR;+ thunk->payload = value;+ return old;+}++void *+nn_thunk_set_computed_int(nn_thunk_t *thunk, int64_t value)+{+ if (thunk->state == NN_THUNK_COMPUTED) return NULL;+ void *old = thunk->payload;+ thunk->state = NN_THUNK_COMPUTED;+ thunk->val_tag = NN_VALUE_INT;+ thunk->payload = (void *)(intptr_t)value;+ return old;+}++void *+nn_thunk_set_computed_float(nn_thunk_t *thunk, double value)+{+ if (thunk->state == NN_THUNK_COMPUTED) return NULL;+ void *old = thunk->payload;+ thunk->state = NN_THUNK_COMPUTED;+ thunk->val_tag = NN_VALUE_FLOAT;+ memcpy(&thunk->payload, &value, sizeof(double));+ return old;+}++void *+nn_thunk_set_computed_bool(nn_thunk_t *thunk, uint8_t value)+{+ if (thunk->state == NN_THUNK_COMPUTED) return NULL;+ void *old = thunk->payload;+ thunk->state = NN_THUNK_COMPUTED;+ thunk->val_tag = NN_VALUE_BOOL;+ thunk->payload = (void *)(intptr_t)value;+ return old;+}++void *+nn_thunk_set_computed_null(nn_thunk_t *thunk)+{+ if (thunk->state == NN_THUNK_COMPUTED) return NULL;+ void *old = thunk->payload;+ thunk->state = NN_THUNK_COMPUTED;+ thunk->val_tag = NN_VALUE_NULL;+ thunk->payload = NULL;+ return old;+}++void *+nn_thunk_set_computed_str(nn_thunk_t *thunk, uint32_t symbol)+{+ if (thunk->state == NN_THUNK_COMPUTED) return NULL;+ void *old = thunk->payload;+ thunk->state = NN_THUNK_COMPUTED;+ thunk->val_tag = NN_VALUE_STR;+ thunk->payload = (void *)(intptr_t)symbol;+ return old;+}++void *+nn_thunk_set_computed_path(nn_thunk_t *thunk, uint32_t symbol)+{+ if (thunk->state == NN_THUNK_COMPUTED) return NULL;+ void *old = thunk->payload;+ thunk->state = NN_THUNK_COMPUTED;+ thunk->val_tag = NN_VALUE_PATH;+ thunk->payload = (void *)(intptr_t)symbol;+ return old;+}++void *+nn_thunk_set_computed_list(nn_thunk_t *thunk, void *list)+{+ if (thunk->state == NN_THUNK_COMPUTED) return NULL;+ void *old = thunk->payload;+ thunk->state = NN_THUNK_COMPUTED;+ thunk->val_tag = NN_VALUE_LIST;+ thunk->payload = list;+ return old;+}++void *+nn_thunk_set_computed_attrs(nn_thunk_t *thunk, void *attrset)+{+ if (thunk->state == NN_THUNK_COMPUTED) return NULL;+ void *old = thunk->payload;+ thunk->state = NN_THUNK_COMPUTED;+ thunk->val_tag = NN_VALUE_ATTRS;+ thunk->payload = attrset;+ return old;+}++void *+nn_thunk_set_computed_ctxstr(nn_thunk_t *thunk, void *ctxstr)+{+ if (thunk->state == NN_THUNK_COMPUTED) return NULL;+ void *old = thunk->payload;+ thunk->state = NN_THUNK_COMPUTED;+ thunk->val_tag = NN_VALUE_CTXSTR;+ thunk->payload = ctxstr;+ return old;+}++void *+nn_thunk_set_computed_lambda(nn_thunk_t *thunk, void *lambda)+{+ if (thunk->state == NN_THUNK_COMPUTED) return NULL;+ void *old = thunk->payload;+ thunk->state = NN_THUNK_COMPUTED;+ thunk->val_tag = NN_VALUE_LAMBDA;+ thunk->payload = lambda;+ return old;+}++/* --- Arena diagnostics / cleanup iteration --- */++uint32_t+nn_thunk_count(void)+{+ return g_arena ? g_arena->total : 0;+}++nn_thunk_t *+nn_thunk_get(uint32_t index)+{+ if (!g_arena || index >= g_arena->total) return NULL;++ struct nn_thunk_block *block = g_arena->head;+ while (block) {+ if (index < block->count) {+ return &block->slots[index];+ }+ index -= block->count;+ block = block->next;+ }+ return NULL; /* unreachable if index < total */+}
nova-nix.cabal view
@@ -1,12 +1,14 @@ cabal-version: 3.0 name: nova-nix-version: 0.1.8.0-synopsis: Windows-native Nix implementation in pure Haskell+version: 0.1.9.0+synopsis: Windows-native Nix implementation in Haskell and C99 description:- A pure Haskell implementation of the Nix package manager that runs natively- on Windows — no WSL, no MSYS2, no Cygwin required. Evaluates .nix files,- builds derivations, manages a content-addressed store, and substitutes- pre-built binaries from remote caches (nova-cache, cache.nixos.org).+ A Windows-native implementation of the Nix package manager — Haskell for+ evaluation logic, a C99 data layer for off-heap evaluation data. Runs+ natively on Windows, macOS, and Linux — no WSL, no MSYS2, no Cygwin+ required. Evaluates .nix files, builds derivations, manages a+ content-addressed store, and substitutes pre-built binaries from remote+ caches (nova-cache, cache.nixos.org). Built on top of @nova-cache@ for NAR serialization, narinfo handling, Ed25519 signing, and binary substitution.@@ -48,6 +50,17 @@ Nix.Eval.IO Nix.Eval.Operator Nix.Eval.StringInterp+ Nix.Eval.Symbol+ Nix.Eval.CAttrSet+ Nix.Eval.CThunk+ Nix.Eval.CEnv+ Nix.Eval.CLambda+ Nix.Eval.CList+ Nix.Eval.CCtxStr+ Nix.Eval.CBytecode+ Nix.Eval.Compile+ Nix.Eval.EvalFormals+ Nix.Eval.Arena Nix.Store Nix.Store.Path Nix.Store.DB@@ -71,14 +84,25 @@ , http-types >= 0.12 && < 0.13 , memory >= 0.18 && < 1 , mtl >= 2.2 && < 2.4- , nova-cache >= 0.3.0 && < 0.3.1- , primitive >= 0.7 && < 1+ , nova-cache >= 0.3.0 && < 0.4 , process >= 1.6 && < 1.7 , regex-tdfa >= 1.3 && < 1.4 , sqlite-simple >= 0.4 && < 0.5 , text >= 2.0 && < 2.2 , time >= 1.9 && < 1.15 + c-sources:+ cbits/nn_symbol.c+ cbits/nn_attrset.c+ cbits/nn_thunk.c+ cbits/nn_env.c+ cbits/nn_list.c+ cbits/nn_ctxstr.c+ cbits/nn_arena.c+ cbits/nn_bytecode.c+ cbits/nn_lambda.c+ include-dirs: cbits+ cc-options: -std=c99 -Wall -Wextra -pedantic -Werror hs-source-dirs: src default-language: Haskell2010 default-extensions:@@ -130,7 +154,6 @@ , directory >= 1.3 && < 1.4 , filepath >= 1.4 && < 1.6 , nova-nix- , primitive >= 0.7 && < 1 , process >= 1.6 && < 1.7 , text >= 2.0 && < 2.2
src/Nix/Builder.hs view
@@ -109,8 +109,14 @@ defaultBuildConfig dir = BuildConfig { bcStoreDir = dir,- bcTmpDir = "/tmp/nova-nix-build",- bcBashPath = "/bin/bash",+ bcTmpDir =+ if isWindows+ then "C:\\Temp\\nova-nix-build"+ else "/tmp/nova-nix-build",+ bcBashPath =+ if isWindows+ then "bash" -- rely on PATH (MSYS2/Git Bash)+ else "/bin/bash", bcSandbox = False, bcCaches = [] }
src/Nix/Builtins.hs view
@@ -15,11 +15,13 @@ ) where +import Data.Int (Int64) import qualified Data.Map.Strict as Map import Data.Text (Text) import qualified Data.Text as T+import Foreign.Ptr (nullPtr) import Nix.Eval (Env (..), NixValue (..), Thunk (..), attrSetFromMap, builtinNames, currentSystemStr, evaluated)-import Nix.Eval.Types (mkStr)+import Nix.Eval.Types (clistFromThunks, mkStr, newCEnv, thunkToCPtr) import Nix.Store.Path (platformStoreDirText) -- | The initial environment containing all builtins.@@ -33,25 +35,24 @@ -- -- @searchPaths@ populates @builtins.nixPath@. Parsed from @NIX_PATH@ -- by 'parseNixPath'. In tests, pass @[]@.-builtinEnv :: Integer -> [Thunk] -> Env+builtinEnv :: Int64 -> [Thunk] -> Env builtinEnv timestamp searchPaths =- Env- { envSlots = mempty,- envLazyScope =- Just $- attrSetFromMap $- Map.fromList $- -- Values- [ ("true", evaluated (VBool True)),- ("false", evaluated (VBool False)),- ("null", evaluated VNull),- ("builtins", evaluated (builtinsAttrSet timestamp searchPaths))- ]- -- Top-level builtin functions (available without builtins. prefix)- ++ map topLevelBuiltin topLevelBuiltinNames,- envParent = Nothing,- envWithScopes = []- }+ let scope =+ attrSetFromMap $+ Map.fromList $+ -- Values+ [ ("true", evaluated (VBool True)),+ ("false", evaluated (VBool False)),+ ("null", evaluated VNull),+ ("builtins", evaluated (builtinsAttrSet timestamp searchPaths)),+ -- Search path support: <name> desugars to __findFile __nixPath "name"+ -- (matching C++ Nix's parser desugaring).+ ("__findFile", evaluated (VBuiltin "findFile" [])),+ ("__nixPath", evaluated (VList (clistFromThunks (map thunkToCPtr searchPaths))))+ ]+ -- Top-level builtin functions (available without builtins. prefix)+ ++ map topLevelBuiltin topLevelBuiltinNames+ in newCEnv nullPtr 0 (Just scope) Nothing nullPtr 0 -- | Builtins exposed at the top level (without @builtins.@ prefix). -- This matches real Nix — nixpkgs uses these unqualified everywhere.@@ -81,21 +82,21 @@ -- | Like 'builtinEnv' but with additional scope bindings overlaid on -- the top-level environment. Used by @scopedImport@.-builtinEnvWithScope :: Integer -> [Thunk] -> [(Text, Thunk)] -> Env+builtinEnvWithScope :: Int64 -> [Thunk] -> [(Text, Thunk)] -> Env builtinEnvWithScope timestamp searchPaths scope = let base = builtinEnv timestamp searchPaths scopeMap = Map.fromList scope- in Env {envSlots = mempty, envLazyScope = Just (attrSetFromMap scopeMap), envParent = Just base, envWithScopes = []}+ in newCEnv nullPtr 0 (Just (attrSetFromMap scopeMap)) (Just base) nullPtr 0 -- | The @builtins@ attribute set, derived from the central registry.-builtinsAttrSet :: Integer -> [Thunk] -> NixValue+builtinsAttrSet :: Int64 -> [Thunk] -> NixValue builtinsAttrSet timestamp searchPaths = VAttrs $ attrSetFromMap $ Map.union builtinEntries (standardEntries timestamp searchPaths) where builtinEntries = Map.fromList [(name, evaluated (VBuiltin name [])) | name <- builtinNames] -standardEntries :: Integer -> [Thunk] -> Map.Map Text Thunk+standardEntries :: Int64 -> [Thunk] -> Map.Map Text Thunk standardEntries timestamp searchPaths = Map.fromList [ ("true", evaluated (VBool True)),@@ -104,7 +105,7 @@ ("storeDir", evaluated (mkStr platformStoreDirText)), ("nixVersion", evaluated (mkStr "2.24.0")), ("langVersion", evaluated (VInt 6)),- ("nixPath", evaluated (VList searchPaths)),+ ("nixPath", evaluated (VList (clistFromThunks (map thunkToCPtr searchPaths)))), ("currentTime", evaluated (VInt timestamp)), ("currentSystem", evaluated (mkStr currentSystemStr)) ]
src/Nix/Derivation.hs view
@@ -65,6 +65,8 @@ ) where +import Data.Function (on)+import Data.List (sortBy) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Text (Text)@@ -183,9 +185,11 @@ <> ")" -- | Serialize outputs: @[(name,path,hashAlgo,hash)]@+-- Sorted by output name for deterministic ATerm hashing. atermOutputs :: [DerivationOutput] -> Text atermOutputs outs =- "[" <> T.intercalate "," (map atermOutput outs) <> "]"+ let sorted = sortBy (compare `on` doName) outs+ in "[" <> T.intercalate "," (map atermOutput sorted) <> "]" atermOutput :: DerivationOutput -> Text atermOutput out =@@ -215,9 +219,11 @@ <> ")" -- | Serialize input sources: @[path1,path2,...]@+-- Sorted for deterministic ATerm hashing. atermInputSrcs :: [StorePath] -> Text atermInputSrcs srcs =- "[" <> T.intercalate "," (map (atermString . SP.storePathToText SP.defaultStoreDir) srcs) <> "]"+ let sorted = sortBy (compare `on` SP.storePathToText SP.defaultStoreDir) srcs+ in "[" <> T.intercalate "," (map (atermString . SP.storePathToText SP.defaultStoreDir) sorted) <> "]" -- | Serialize a list of strings: @[s1,s2,...]@ atermStringList :: [Text] -> Text@@ -421,7 +427,7 @@ pChar ',' envPairs <- pList pEnvPair pChar ')'- let inputDrvs = Map.fromList inputDrvsList+ let inputDrvs = Map.fromListWith (++) inputDrvsList env = Map.fromList envPairs platform = textToPlatform platformStr pure
src/Nix/Eval.hs view
@@ -15,3631 +15,3946 @@ NixValue (..), CompiledRegex (..), Thunk (..),- ThunkCell (..),-- -- * Attribute sets (re-exported from Types)- AttrSet (..),- LazyBinding (..),- attrSetFromMap,- attrSetLookup,- attrSetKeys,- attrSetToMap,- attrSetToAscList,- attrSetMember,- attrSetElems,- attrSetNull,- attrSetRemoveKeys,- 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.IORef (IORef, atomicModifyIORef', newIORef)-import Data.List (find, foldl')-import Data.Map.Strict (Map)-import qualified Data.Map.Strict as Map-import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)-import Data.Primitive.SmallArray (SmallArray, indexSmallArray, smallArrayFromListN)-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, plainContext)-import Nix.Eval.Operator (evalBinary, evalUnary, nixCompare, nixEqual)-import Nix.Eval.StringInterp (coerceToString, evalIndStringParts, evalStringParts)-import Nix.Eval.Types- ( AttrSet (..),- CompiledRegex (..),- Env (..),- LazyBinding (..),- MonadEval (..),- NixValue (..),- PureEval (..),- StringContext (..),- StringContextElement (..),- Thunk (..),- ThunkCell (..),- attrSetElems,- attrSetFromMap,- attrSetKeys,- attrSetLookup,- attrSetMapWithKeyLazy,- attrSetMember,- attrSetNull,- attrSetRemoveKeys,- attrSetSize,- attrSetToAscList,- attrSetToMap,- attrSetUnionWith,- cheapThunk,- emptyContext,- emptyEnv,- envFromSlots,- envLookup,- envLookupResolved,- evaluated,- lookupWithScopes,- mkStr,- mkSyntheticThunk,- mkThunk,- newLazyAttrCache,- pushWithScope,- typeName,- withScopesForCapture,- )-import Nix.Expr.Types- ( AttrKey (..),- AttrPath,- BinaryOp (..),- Binding (..),- CaptureInfo (..),- 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 System.IO.Unsafe (unsafePerformIO)-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 force applyValue env parts- EIndStr parts -> uncurry VStr <$> evalIndStringParts eval force applyValue env parts- EVar name -> evalVar env name- EWithVar name -> evalWithVar env name- EResolvedVar level idx -> force (envLookupResolved level idx env)- EAttrs isRec bindings captureInfo -> evalAttrs env isRec bindings captureInfo- EList exprs -> pure (VList (map (cheapThunk 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 captureInfo ->- pure (VLambda (buildCaptureEnv env captureInfo) formals body)- ELet bindings body captureInfo -> evalLet env bindings body captureInfo- 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 <> "'")---- | Evaluate a with-scoped variable: check with-scopes first (innermost--- to outermost), then fall back to the standard name-based lookup--- (parent chain to builtins). For trimmed envs ('CapturesWithScopes'),--- the root scope is already appended to 'envWithScopes' so the--- with-scope lookup finds builtins without needing a parent chain.-evalWithVar :: (MonadEval m) => Env -> Text -> m NixValue-evalWithVar env name =- case lookupWithScopes name (envWithScopes env) of- Just thunk -> force thunk- Nothing -> evalVar env name---- ------------------------------------------------------------------------------ Attribute sets--- -----------------------------------------------------------------------------evalAttrs :: (MonadEval m) => Env -> Bool -> [Binding] -> CaptureInfo -> m NixValue-evalAttrs env False bindings _captureInfo = evalNonRecAttrs env bindings-evalAttrs env True bindings captureInfo = evalRecAttrs env bindings captureInfo---- | 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).------ Positional path: when all bindings are single static keys, builds--- 'envSlots' for variable lookup and 'EagerAttrs' for the return value.--- Both reference the SAME thunk objects (no duplication).------ Fallback path: uses 'LazyAttrs' to defer thunk + IORef allocation.--- The env's 'envLazyScope' points to the SAME 'LazyAttrs' that backs--- the resulting attr set — one map instead of two.-evalRecAttrs :: (MonadEval m) => Env -> [Binding] -> CaptureInfo -> m NixValue-evalRecAttrs env bindings captureInfo- | allPositionalBindings bindings =- -- Positional path: slots for variable lookup, EagerAttrs for return.- let slotCount = bindingSlotCount bindings- parentEnv = buildCaptureEnv env captureInfo- recEnv =- Env- { envSlots = slots,- envLazyScope = Nothing,- envParent = Just parentEnv,- envWithScopes = case captureInfo of- NoCaptureInfo -> envWithScopes env- Captures _ -> []- CapturesWithScopes _ -> withScopesForCapture env- }- slots = smallArrayFromListN slotCount (buildSlotThunks recEnv env bindings)- attrMap = buildAttrMapFromSlots bindings slots- in pure (VAttrs (EagerAttrs attrMap))- | otherwise = do- -- Fallback: dynamic/nested keys — use LazyAttrs.- resolvedBindings <- resolveBindingKeys env bindings- let recEnv =- Env- { envSlots = mempty,- envLazyScope = Just attrSet,- envParent = Just env,- envWithScopes = envWithScopes env- }- lazyBindingMap = buildLazyBindingMap recEnv resolvedBindings- cache = newLazyAttrCache lazyBindingMap- attrSet = LazyAttrs lazyBindingMap cache- pure (VAttrs attrSet)---- ------------------------------------------------------------------------------ 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 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 = cheapThunk 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.------ Builds a positional slot list matching the indices assigned by--- 'Nix.Expr.Resolve':------ * @FormalName n@ → @[argThunk]@--- * @FormalSet [a, b, c] _@ → @[aThunk, bThunk, cThunk]@--- * @FormalNamedSet n [a, b, c] _@ → @[argThunk, aThunk, bThunk, cThunk]@-matchFormals :: (MonadEval m) => Env -> Formals -> Thunk -> m Env-matchFormals closureEnv (FormalName _) argThunk =- pure (envFromSlots (smallArrayFromListN 1 [argThunk]) closureEnv)-matchFormals closureEnv (FormalSet formals allowExtra) argThunk = do- argVal <- force argThunk- matchFormalSet closureEnv formals allowExtra argVal Nothing-matchFormals closureEnv (FormalNamedSet _ formals allowExtra) argThunk = do- argVal <- force argThunk- -- Pass the @-pattern thunk so it goes into slot 0 of the combined env.- matchFormalSet closureEnv formals allowExtra argVal (Just argThunk)---- | Match a formal set pattern against a VAttrs argument.------ All formals are batched into a SINGLE scope level (one slot list)--- instead of creating a separate Map per formal. This replaces--- Map.Bin nodes (48 bytes each) with list cons cells (16 bytes each)--- and eliminates Text key storage entirely.------ 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 -> Maybe Thunk -> m Env-matchFormalSet closureEnv formals allowExtra argVal atThunk =- 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 = envFromSlots formalSlots closureEnv- formalSlots = case atThunk of- Nothing ->- smallArrayFromListN (length formals) (map resolveOneFormal formals)- Just at ->- smallArrayFromListN (length formals + 1) (at : map resolveOneFormal 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 <> "'")---- | Build a flat env from capture coordinates, extracting only the--- referenced slots from the parent chain. Returns the original env--- unchanged when untrimmed. Used by lambdas (as closure env), and--- by let\/rec blocks (as trimmed parent env) to break the retention--- chain to the full outer scope.-buildCaptureEnv :: Env -> CaptureInfo -> Env-buildCaptureEnv env NoCaptureInfo = env-buildCaptureEnv env (Captures captureList) =- Env- { envSlots =- smallArrayFromListN- (length captureList)- [envLookupResolved lvl idx env | (lvl, idx) <- captureList],- envLazyScope = Nothing,- envParent = Nothing,- envWithScopes = []- }-buildCaptureEnv env (CapturesWithScopes captureList) =- Env- { envSlots =- smallArrayFromListN- (length captureList)- [envLookupResolved lvl idx env | (lvl, idx) <- captureList],- envLazyScope = Nothing,- envParent = Nothing,- envWithScopes = withScopesForCapture env- }---- ------------------------------------------------------------------------------ Let / if / with / assert--- ------------------------------------------------------------------------------- | Check if all bindings have single static keys (eligible for--- positional slot-based evaluation). Mirrors 'allStaticSingleKey'--- in 'Nix.Expr.Resolve'.-allPositionalBindings :: [Binding] -> Bool-allPositionalBindings = all isEligible- where- isEligible (NamedBinding [StaticKey _] _) = True- isEligible (Inherit _ _) = True- isEligible _ = False---- | Count the number of positional slots needed for a list of bindings.--- Each named binding contributes 1, each inherit contributes its name count.-bindingSlotCount :: [Binding] -> Int-bindingSlotCount = foldl' countOne 0- where- countOne !acc (NamedBinding [StaticKey _] _) = acc + 1- countOne !acc (Inherit _ names) = acc + length names- countOne !acc _ = acc---- | Build thunks for positional let\/rec bindings in declaration order.--- Returns a list of thunks matching the slot indices assigned by--- 'lexicalScopeFromBindings' in the resolve pass.------ Inherits are expanded in-place: @inherit x y@ produces thunks at--- consecutive slots. The @thunkEnv@ is the knot-tied let\/rec env;--- @outerEnv@ is the parent env used for inherit lookups.-buildSlotThunks :: Env -> Env -> [Binding] -> [Thunk]-buildSlotThunks thunkEnv outerEnv = concatMap slotThunk- where- slotThunk (NamedBinding [StaticKey _] bodyExpr) =- [cheapThunk thunkEnv bodyExpr]- slotThunk (Inherit Nothing names) =- -- inherit x → look up x in the OUTER env (before the let scope).- map (inheritLookup outerEnv) names- slotThunk (Inherit (Just fromExpr) names) =- -- inherit (from) x → ESelect from.x, evaluated in the let env.- [mkThunk thunkEnv (ESelect fromExpr [StaticKey name] Nothing) | name <- names]- -- Unreachable: allPositionalBindings guards this path.- slotThunk _ = []---- | Build a 'Map Text Thunk' indexing into a 'SmallArray Thunk' by--- binding name. The thunk pointers are shared (no copies).--- Used by 'evalRecAttrs' to produce the 'EagerAttrs' return value--- while slots serve for variable lookup.-buildAttrMapFromSlots :: [Binding] -> SmallArray Thunk -> Map Text Thunk-buildAttrMapFromSlots bindings slots = snd (foldl' addOne (0, Map.empty) bindings)- where- addOne (!idx, !acc) (NamedBinding [StaticKey name] _) =- (idx + 1, Map.insert name (indexSmallArray slots idx) acc)- addOne (!idx, !acc) (Inherit _ names) =- let acc' = foldl' (\a (offset, name) -> Map.insert name (indexSmallArray slots (idx + offset)) a) acc (zip [0 ..] names)- in (idx + length names, acc')- -- Unreachable: allPositionalBindings guards this path.- addOne (!idx, !acc) _ = (idx, acc)---- | 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).------ Positional path: when all bindings are single static keys, builds--- 'envSlots' for O(1) variable lookup and sets 'envLazyScope = Nothing'.--- This enables closure trimming to fire on lambdas that reference--- let-bound variables.------ Fallback path: dynamic\/nested-key blocks use the existing 'LazyAttrs'--- path with 'envLazyScope'.-evalLet :: (MonadEval m) => Env -> [Binding] -> Expr -> CaptureInfo -> m NixValue-evalLet env bindings body captureInfo- | allPositionalBindings bindings =- -- Positional path: slots for O(1) lookup, no envLazyScope.- let slotCount = bindingSlotCount bindings- parentEnv = buildCaptureEnv env captureInfo- -- Knot-tied: letEnv references slots which reference letEnv lazily.- letEnv =- Env- { envSlots = smallArrayFromListN slotCount (buildSlotThunks letEnv env bindings),- envLazyScope = Nothing,- envParent = Just parentEnv,- envWithScopes = case captureInfo of- NoCaptureInfo -> envWithScopes env- Captures _ -> []- CapturesWithScopes _ -> withScopesForCapture env- }- in eval letEnv body- | otherwise = do- -- Fallback: dynamic/nested keys — use LazyAttrs.- resolvedBindings <- resolveBindingKeys env bindings- let letEnv =- Env- { envSlots = mempty,- envLazyScope = Just (LazyAttrs lazyBindingMap cache),- envParent = Just env,- envWithScopes = envWithScopes env- }- lazyBindingMap = buildLazyBindingMap letEnv resolvedBindings- cache = newLazyAttrCache lazyBindingMap- 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" builtinSeq,- builtin2 "trace" builtinTrace,- builtin2 "warn" builtinWarn,- 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 (traceVerbose: same as trace for now, --trace-verbose not yet gated)- builtin2 "traceVerbose" builtinTrace,- 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 + path import- builtin1 "path" builtinPath,- 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 (precompileArgs name allArgs))- else executeBuiltin name allArgs---- | Pre-compile regex patterns at partial application time.--- When builtins.match or builtins.split receives its first argument--- (the pattern string), compile it immediately and store the compiled--- RE.Regex in a VCompiledRegex, replacing the raw VStr. The compiled--- form is carried in VBuiltin's accumulated args and reused on every--- subsequent application — zero recompilation.-precompileArgs :: Text -> [NixValue] -> [NixValue]-precompileArgs "match" [VStr pat _] =- let anchored = "^" <> pat <> "$"- in case cachedCompileRegex anchored of- Just compiled -> [VCompiledRegex (CompiledRegex pat compiled)]- Nothing -> [VStr pat emptyContext] -- fail later at execute time-precompileArgs "split" [VStr pat _] =- case cachedCompileRegex pat of- Just compiled -> [VCompiledRegex (CompiledRegex pat compiled)]- Nothing -> [VStr pat emptyContext] -- fail later at execute time-precompileArgs _ args = args---- | 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 builtinSeq- "trace" -> apply2 builtinTrace- "warn" -> apply2 builtinWarn- "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 (traceVerbose: same as trace for now)- "traceVerbose" -> apply2 builtinTrace- "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 + path import- "path" -> apply1 builtinPath- "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"- VCompiledRegex _ -> "lambda"--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- pure (VAttrs (attrSetRemoveKeys keys attrs))- 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.- -- Slot 0 = function.- let fnThunk = evaluated func- env = Env {envSlots = smallArrayFromListN 1 [fnThunk], envLazyScope = Nothing, envParent = Nothing, envWithScopes = []}- mkIndexThunk i = mkThunk env (EApp (EResolvedVar 0 0) (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 (EResolvedVar 0 0) (EResolvedVar 0 1)@ in a self-contained env.--- Slot 0 = function, slot 1 = argument.-deferApply :: NixValue -> Thunk -> Thunk-deferApply func argThunk =- let env =- Env- { envSlots = smallArrayFromListN 2 [evaluated func, argThunk],- envLazyScope = Nothing,- envParent = Nothing,- envWithScopes = []- }- in mkSyntheticThunk env deferApplyExpr---- | Shared expression for 'deferApply'. Allocated once as a CAF.-deferApplyExpr :: Expr-deferApplyExpr = EApp (EResolvedVar 0 0) (EResolvedVar 0 1)-{-# NOINLINE deferApplyExpr #-}---- | 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 force applyValue 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.- -- Uses attrSetMapWithKeyLazy to avoid materializing all bindings in- -- LazyAttrs — each entry stays as a PreBuilt recipe until accessed.- -- Slot 0 = function, slot 1 = key, slot 2 = value.- pure (VAttrs (attrSetMapWithKeyLazy deferAttr attrs))- where- deferAttr key valThunk =- let env =- Env- { envSlots = smallArrayFromListN 3 [evaluated func, evaluated (mkStr key), valThunk],- envLazyScope = Nothing,- envParent = Nothing,- envWithScopes = []- }- in mkSyntheticThunk env mapAttrsExpr-builtinMapAttrs _ other =- throwEvalError ("builtins.mapAttrs: expected a set, got " <> typeName other)---- | Shared expression for 'builtinMapAttrs'. Allocated once as a CAF.-mapAttrsExpr :: Expr-mapAttrsExpr = EApp (EApp (EResolvedVar 0 0) (EResolvedVar 0 1)) (EResolvedVar 0 2)-{-# NOINLINE mapAttrsExpr #-}--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) =- -- Closure env holds the function at slot 0. The __functor lambda- -- body is EResolvedVar 1 0: level 1 (past _self's slot), index 0.- let closureEnv =- Env- { envSlots = smallArrayFromListN 1 [evaluated func],- envLazyScope = Nothing,- envParent = Nothing,- envWithScopes = []- }- -- __functor = self: __fn (ignores self, returns the original function)- functorLambda = VLambda closureEnv (FormalName "_self") (EResolvedVar 1 0)- 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)--- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------ Regex compilation cache--- ------------------------------------------------------------------------------- | Global regex compilation cache. Keyed by the raw pattern string--- (including anchoring for match). Idempotent memoization via--- unsafePerformIO — same rationale as thunk memoization.-{-# NOINLINE regexCacheRef #-}-regexCacheRef :: IORef (Map Text RE.Regex)-regexCacheRef = unsafePerformIO (newIORef Map.empty)---- | Compile a regex, using the global cache to avoid recompilation.--- Returns Nothing for invalid patterns. NOINLINE prevents GHC from--- inlining and floating the unsafePerformIO reads.-{-# NOINLINE cachedCompileRegex #-}-cachedCompileRegex :: Text -> Maybe RE.Regex-cachedCompileRegex pat =- unsafePerformIO $ do- cache <- atomicModifyIORef' regexCacheRef (\c -> (c, c))- case Map.lookup pat cache of- Just compiled -> pure (Just compiled)- Nothing -> case RE.makeRegexM (T.unpack pat) :: Maybe RE.Regex of- Nothing -> pure Nothing- Just compiled -> do- atomicModifyIORef' regexCacheRef (\c -> (Map.insert pat compiled c, ()))- pure (Just compiled)---- ------------------------------------------------------------------------------ Regex builtins--- ------------------------------------------------------------------------------- | @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--- Pre-compiled path: regex was compiled at partial-application time.-builtinMatch (VCompiledRegex (CompiledRegex _ compiled)) (VStr str _) =- matchWithCompiled compiled str--- Direct 2-arg call: use global compilation cache.-builtinMatch (VStr regex _) (VStr str _) =- let anchored = "^" <> regex <> "$"- in case cachedCompileRegex anchored of- Nothing -> throwEvalError ("builtins.match: invalid regex: " <> regex)- Just compiled -> matchWithCompiled compiled str-builtinMatch (VStr _ _) other =- throwEvalError ("builtins.match: expected a string, got " <> typeName other)-builtinMatch (VCompiledRegex _) other =- throwEvalError ("builtins.match: expected a string, got " <> typeName other)-builtinMatch other _ =- throwEvalError ("builtins.match: expected a string (regex), got " <> typeName other)---- | Shared match logic for pre-compiled and freshly-compiled regex paths.-matchWithCompiled :: (MonadEval m) => RE.Regex -> Text -> m NixValue-matchWithCompiled compiled str =- 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))---- | @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--- Pre-compiled path: regex was compiled at partial-application time.-builtinSplit (VCompiledRegex (CompiledRegex _ compiled)) (VStr str _) =- splitWithCompiled compiled str--- Direct 2-arg call: use global compilation cache.-builtinSplit (VStr regex _) (VStr str _) =- case cachedCompileRegex regex of- Nothing -> throwEvalError ("builtins.split: invalid regex: " <> regex)- Just compiled -> splitWithCompiled compiled str-builtinSplit (VStr _ _) other =- throwEvalError ("builtins.split: expected a string, got " <> typeName other)-builtinSplit (VCompiledRegex _) other =- throwEvalError ("builtins.split: expected a string, got " <> typeName other)-builtinSplit other _ =- throwEvalError ("builtins.split: expected a string (regex), got " <> typeName other)---- | Shared split logic for pre-compiled and freshly-compiled regex paths.-splitWithCompiled :: (MonadEval m) => RE.Regex -> Text -> m NixValue-splitWithCompiled compiled str =- let allMatches = matchAllText compiled (T.unpack str)- strText = T.unpack str- result = buildSplitResult strText 0 allMatches- in pure (VList result)---- | 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"-valueToJSON (VCompiledRegex _) = throwEvalError "builtins.toJSON: cannot convert a function 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 ()---- | @builtins.seq a b@ — evaluate @a@ to WHNF, then return @b@.-builtinSeq :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinSeq !_first = pure---- | @builtins.trace msg val@ — print @msg@ to stderr, return @val@.-builtinTrace :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinTrace msgVal result = do- msg <- case msgVal of- VStr s _ -> pure s- other -> pure (typeName other)- traceMessage ("trace: " <> msg)- pure result---- | @builtins.warn msg val@ — print warning to stderr, return @val@.-builtinWarn :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinWarn msgVal result = do- msg <- case msgVal of- VStr s _ -> pure s- other -> pure (typeName other)- traceMessage ("warning: " <> msg)- pure result---- ------------------------------------------------------------------------------ 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 _) = fetchAndExtractTarball url-builtinFetchTarball (VAttrs attrs) = do- url <- forceAttrStr "builtins.fetchTarball" "url" attrs- fetchAndExtractTarball url-builtinFetchTarball other =- throwEvalError ("builtins.fetchTarball: expected a string or set, got " <> typeName other)---- | Download a tarball, extract it, and return the path to the extracted--- directory. Uses a content-hashed temp directory. Downloads and extracts--- in a single shell pipeline to avoid binary-as-text encoding issues.-fetchAndExtractTarball :: (MonadEval m) => Text -> m NixValue-fetchAndExtractTarball url = do- sysTmp <- getTempDir- let urlHash = sha256Hex (TE.encodeUtf8 url)- extractDir = sysTmp <> "/nova-nix-tarball-" <> urlHash- -- Single pipeline: mkdir, download, extract with --strip-components=1- -- The -- separator prevents argument injection from the URL.- (code, _, errOut) <-- runProcess- "sh"- [ "-c",- "mkdir -p \"$1\" && curl -sSfL -- \"$2\" | tar -xz -C \"$1\" --strip-components=1",- "--",- extractDir,- url- ]- ""- case code of- 0 -> pure (VPath extractDir)- _ -> throwEvalError ("builtins.fetchTarball: " <> errOut)--builtinFetchGit :: (MonadEval m) => NixValue -> m NixValue-builtinFetchGit (VStr url _) = do- sysTmp <- getTempDir- let urlHash = sha256Hex (TE.encodeUtf8 url)- cloneDir = sysTmp <> "/nova-nix-fetchgit-" <> urlHash- (code, _, errOut) <- runProcess "git" ["clone", "--depth", "1", "--", url, cloneDir] ""- case code of- 0 -> pure (VPath cloneDir)- _ -> throwEvalError ("builtins.fetchGit: git clone failed: " <> errOut)-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)---- | Resolve the system temp directory. Checks @TMPDIR@ (Unix), then--- @TEMP@ (Windows), falls back to @\/tmp@.-getTempDir :: (MonadEval m) => m Text-getTempDir = do- candidates <- mapM getEnvVar ["TMPDIR", "TEMP"]- pure (fromMaybe "/tmp" (find (not . T.null) candidates))---- | 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"-- -- Materialize once, reuse for both env collection and result merge- let materialized = attrSetToMap attrs-- -- Collect all string-coercible attrs into env WITH their contexts- (drvEnvPairs, envContext) <- collectDrvEnvWithContext materialized-- -- 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 env.- -- The hash was computed from the pre-output drv (drvOutputs = []),- -- so adding output paths and drvPath to drvEnv here does not affect- -- the content address. Real Nix .drv files include these in their- -- env section — builders read $out etc. from the environment.- let completeEnv =- Map.union- (Map.fromList (("drvPath", drvPathText) : outPaths))- envMap- completeDrv = drv {drvOutputs = drvOutputsList, drvEnv = completeEnv}-- -- 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 materialized-- 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++ -- * Attribute sets (re-exported from Types)+ AttrSet (..),+ CAttrSet,+ attrSetFromMap,+ attrSetLookup,+ attrSetKeys,+ attrSetToMap,+ attrSetToAscList,+ attrSetMember,+ attrSetElems,+ attrSetNull,+ attrSetRemoveKeys,+ 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,+ evalBytecode,+ force,++ -- * Helpers (for Builtins)+ typeName,+ evaluated,+ readThunkValue,++ -- * Builtin registry+ BuiltinDef (..),+ builtinRegistry,+ builtinNames,++ -- * Platform+ currentSystemStr,+ )+where++import Control.Monad (foldM, when, (>=>))+import qualified Crypto.Hash as CH+import qualified Data.Array as Array+import Data.Bits (complement, 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.IORef (IORef, atomicModifyIORef', newIORef)+import Data.Int (Int64)+import Data.List (find, foldl')+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, maybeToList)+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 Data.Word (Word32, Word8)+import Foreign.Ptr (Ptr, castPtr, nullPtr, ptrToWordPtr, wordPtrToPtr)+import Foreign.Storable (peekElemOff, pokeElemOff)+import Nix.Derivation (Derivation (..), DerivationOutput (..), textToPlatform, toATerm)+import Nix.Eval.CBytecode (cbcArg1, cbcArg2, cbcArg3, cbcData, cbcFlags, cbcOpcode, cbcShortArg)+import Nix.Eval.CEnv (cenvPushWith)+import Nix.Eval.CList (CList (..), clistGet)+import Nix.Eval.CThunk (CThunkPtr)+import Nix.Eval.Compile (BcAttrKey (..), BcBinding (..), compileExpr, decodeBcBindings, decodeBcCaptureInfo, decodeBcFormals, reassembleDouble, reassembleInt64)+import Nix.Eval.Context (extractInputDrvs, extractInputSrcs, plainContext)+import Nix.Eval.Operator (evalBinary, evalUnary, nixCompare, nixEqual)+import Nix.Eval.StringInterp (coerceToString, formatNixFloat, stripIndentation)+import Nix.Eval.Symbol (Symbol (..), symbolText)+import Nix.Eval.Types+ ( AttrSet (..),+ CAttrSet,+ CompiledRegex (..),+ Env (..),+ EvalFormal (..),+ EvalFormals (..),+ MonadEval (..),+ NixValue (..),+ PureEval (..),+ StringContext (..),+ StringContextElement (..),+ Thunk (..),+ allocCSlots,+ attrSetElems,+ attrSetFromMap,+ attrSetKeys,+ attrSetLookup,+ attrSetMapWithKey,+ attrSetMember,+ attrSetNull,+ attrSetRemoveKeys,+ attrSetSize,+ attrSetToAscList,+ attrSetToMap,+ attrSetUnionWith,+ buildCAttrSetKeys,+ buildCSlots,+ cheapThunkBc,+ clistFromThunks,+ clistLen,+ clistThunks,+ emptyContext,+ emptyEnv,+ envFromSlots,+ envLookup,+ envLookupResolved,+ envWithScopesRaw,+ evaluated,+ fillCAttrSetValues,+ fillCSlots,+ mkStr,+ mkSyntheticThunk,+ mkThunk,+ mkThunkBc,+ newCEnv,+ newMinimalEnv,+ readThunkValue,+ thunkToCPtr,+ typeName,+ withScopesForCapture,+ )+import Nix.Expr.Types+ ( AttrKey (..),+ BinaryOp (..),+ CaptureInfo (..),+ Expr (..),+ NixAtom (..),+ UnaryOp (..),+ )+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 System.IO.Unsafe (unsafePerformIO)+import qualified System.Info+import Text.Regex.TDFA (matchAllText)+import qualified Text.Regex.TDFA as RE++-- | Evaluate a Nix expression in an environment.+-- Compiles the expression to bytecode and dispatches to 'evalBytecode'.+eval :: (MonadEval m) => Env -> Expr -> m NixValue+eval env expr =+ let bcIdx = unsafePerformIO (compileExpr expr)+ in evalBytecode env bcIdx++-- | 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 evalBytecode++-- | Evaluate a bytecode instruction by index.+-- This is the primary evaluator — reads opcodes from the C bytecode+-- store and dispatches. All recursive evaluation goes through this+-- function, never through 'eval' directly.+evalBytecode :: (MonadEval m) => Env -> Word32 -> m NixValue+evalBytecode env bcIdx =+ let opcode = unsafePerformIO (cbcOpcode bcIdx)+ in case opcode of+ 0 {- LIT_INT -} ->+ let lo = unsafePerformIO (cbcArg1 bcIdx)+ hi = unsafePerformIO (cbcArg2 bcIdx)+ in pure (VInt (reassembleInt64 lo hi))+ 1 {- LIT_FLOAT -} ->+ let lo = unsafePerformIO (cbcArg1 bcIdx)+ hi = unsafePerformIO (cbcArg2 bcIdx)+ in pure (VFloat (reassembleDouble lo hi))+ 2 {- LIT_BOOL -} ->+ let flag = unsafePerformIO (cbcShortArg bcIdx)+ in pure (VBool (flag /= 0))+ 3 {- LIT_NULL -} -> pure VNull+ 4 {- LIT_URI -} ->+ let sym = unsafePerformIO (cbcArg1 bcIdx)+ in pure (mkStr (symbolText (Symbol sym)))+ 5 {- LIT_PATH -} ->+ let sym = unsafePerformIO (cbcArg1 bcIdx)+ in VPath <$> resolvePathLiteral (symbolText (Symbol sym))+ 6 {- STR -} -> evalBcStr env bcIdx+ 7 {- IND_STR -} -> evalBcIndStr env bcIdx+ 8 {- VAR -} ->+ let sym = unsafePerformIO (cbcArg1 bcIdx)+ in evalVar env (symbolText (Symbol sym))+ 9 {- WITH_VAR -} ->+ let sym = unsafePerformIO (cbcArg1 bcIdx)+ name = symbolText (Symbol sym)+ in evalWithVar env name+ 10 {- RESOLVED_VAR -} ->+ let level = fromIntegral (unsafePerformIO (cbcArg1 bcIdx))+ idx = fromIntegral (unsafePerformIO (cbcArg2 bcIdx))+ in force (envLookupResolved level idx env)+ 11 {- ATTRS -} -> evalBcAttrs env bcIdx+ 12 {- LIST -} -> evalBcList env bcIdx+ 13 {- SELECT -} -> evalBcSelect env bcIdx+ 14 {- HAS_ATTR -} -> evalBcHasAttr env bcIdx+ 15 {- APP -} -> evalBcApp env bcIdx+ 16 {- LAMBDA -} -> evalBcLambda env bcIdx+ 17 {- LET -} -> evalBcLet env bcIdx+ 18 {- IF -} ->+ let condIdx = unsafePerformIO (cbcArg1 bcIdx)+ thenIdx = unsafePerformIO (cbcArg2 bcIdx)+ elseIdx = unsafePerformIO (cbcArg3 bcIdx)+ in do+ condVal <- evalBytecode env condIdx+ case condVal of+ VBool True -> evalBytecode env thenIdx+ VBool False -> evalBytecode env elseIdx+ _ -> throwEvalError ("'if' condition must be a Boolean, got " <> typeName condVal)+ 19 {- WITH -} ->+ let scopeIdx = unsafePerformIO (cbcArg1 bcIdx)+ bodyIdx = unsafePerformIO (cbcArg2 bcIdx)+ -- Lazy with: defer forcing the scope until a WITH_VAR lookup+ -- actually needs it. This is critical for nixpkgs where+ -- all-packages.nix uses `with pkgs;` inside a fixpoint —+ -- eagerly forcing the scope would blackhole.+ scopeThunk = cheapThunkBc env scopeIdx+ in evalBytecode (pushLazyWithScope scopeThunk env) bodyIdx+ 20 {- ASSERT -} ->+ let condIdx = unsafePerformIO (cbcArg1 bcIdx)+ bodyIdx = unsafePerformIO (cbcArg2 bcIdx)+ in do+ condVal <- evalBytecode env condIdx+ case condVal of+ VBool True -> evalBytecode env bodyIdx+ VBool False -> throwEvalError "assertion failed"+ _ -> throwEvalError ("assertion condition must be a Boolean, got " <> typeName condVal)+ 21 {- UNARY -} ->+ let flags_ = unsafePerformIO (cbcFlags bcIdx)+ operandIdx = unsafePerformIO (cbcArg1 bcIdx)+ in do+ val <- evalBytecode env operandIdx+ evalUnary (decodeUnaryOp flags_) val+ 22 {- BINARY -} -> evalBcBinary env bcIdx+ 23 {- SEARCH_PATH -} ->+ let sym = unsafePerformIO (cbcArg1 bcIdx)+ in evalSearchPath env (symbolText (Symbol sym))+ _ -> throwEvalError "evalBytecode: unknown opcode"++-- ---------------------------------------------------------------------------+-- Bytecode helpers+-- ---------------------------------------------------------------------------++-- | Decode a UnaryOp from bytecode flags.+decodeUnaryOp :: Word8 -> UnaryOp+decodeUnaryOp 0 = OpNot+decodeUnaryOp 1 = OpNegate+decodeUnaryOp n = error ("decodeUnaryOp: unknown tag " <> show n)++-- | Decode a BinaryOp from bytecode flags.+decodeBinaryOp :: Word8 -> BinaryOp+decodeBinaryOp 0 = OpAdd+decodeBinaryOp 1 = OpSub+decodeBinaryOp 2 = OpMul+decodeBinaryOp 3 = OpDiv+decodeBinaryOp 4 = OpAnd+decodeBinaryOp 5 = OpOr+decodeBinaryOp 6 = OpImpl+decodeBinaryOp 7 = OpEq+decodeBinaryOp 8 = OpNeq+decodeBinaryOp 9 = OpLt+decodeBinaryOp 10 = OpLte+decodeBinaryOp 11 = OpGt+decodeBinaryOp 12 = OpGte+decodeBinaryOp 13 = OpConcat+decodeBinaryOp 14 = OpUpdate+decodeBinaryOp n = error ("decodeBinaryOp: unknown tag " <> show n)++-- | Evaluate a binary operation from bytecode, with short-circuit+-- support for &&, ||, ->.+evalBcBinary :: (MonadEval m) => Env -> Word32 -> m NixValue+evalBcBinary env bcIdx0 =+ let flags_ = unsafePerformIO (cbcFlags bcIdx0)+ leftIdx = unsafePerformIO (cbcArg1 bcIdx0)+ rightIdx = unsafePerformIO (cbcArg2 bcIdx0)+ op = decodeBinaryOp flags_+ in case op of+ OpAnd -> evalShortCircuitAnd env leftIdx rightIdx+ OpOr -> evalShortCircuitOr env leftIdx rightIdx+ OpImpl -> evalShortCircuitImpl env leftIdx rightIdx+ OpAdd -> do+ leftVal <- evalBytecode env leftIdx+ rightVal <- evalBytecode env rightIdx+ evalAddWithCoercion leftVal rightVal+ _ -> do+ leftVal <- evalBytecode env leftIdx+ rightVal <- evalBytecode env rightIdx+ evalBinary force op leftVal rightVal++-- | Addition with string coercion fallback, matching C++ Nix behavior.+-- C++ Nix's ExprOpAdd falls through to concatStrings when operands+-- are not both numeric and neither is a path. concatStrings calls+-- coerceToString on each part, which handles attrsets via outPath.+evalAddWithCoercion :: (MonadEval m) => NixValue -> NixValue -> m NixValue+evalAddWithCoercion left right = case (left, right) of+ -- Numeric: direct arithmetic (matches C++ Nix priority)+ (VFloat _, _) -> evalBinary force OpAdd left right+ (_, VFloat _) -> evalBinary force OpAdd left right+ (VInt _, VInt _) -> evalBinary force OpAdd left right+ -- Path: direct concat+ (VPath _, _) -> evalBinary force OpAdd left right+ -- String-like: direct concat when both are string/path+ (VStr {}, VStr {}) -> evalBinary force OpAdd left right+ (VStr {}, VPath _) -> evalBinary force OpAdd left right+ -- Otherwise: coerce both to strings via concatStrings (matching C++ Nix)+ _ -> do+ (leftStr, leftCtx) <- coerceToString force applyValue left+ (rightStr, rightCtx) <- coerceToString force applyValue right+ pure (VStr (leftStr <> rightStr) (leftCtx <> rightCtx))++-- | Bytecode short-circuit &&+evalShortCircuitAnd :: (MonadEval m) => Env -> Word32 -> Word32 -> m NixValue+evalShortCircuitAnd env leftIdx rightIdx = do+ leftVal <- evalBytecode env leftIdx+ case leftVal of+ VBool False -> pure (VBool False)+ VBool True -> do+ rightVal <- evalBytecode env rightIdx+ 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)++-- | Bytecode short-circuit ||+evalShortCircuitOr :: (MonadEval m) => Env -> Word32 -> Word32 -> m NixValue+evalShortCircuitOr env leftIdx rightIdx = do+ leftVal <- evalBytecode env leftIdx+ case leftVal of+ VBool True -> pure (VBool True)+ VBool False -> do+ rightVal <- evalBytecode env rightIdx+ 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)++-- | Bytecode short-circuit ->+evalShortCircuitImpl :: (MonadEval m) => Env -> Word32 -> Word32 -> m NixValue+evalShortCircuitImpl env leftIdx rightIdx = do+ leftVal <- evalBytecode env leftIdx+ case leftVal of+ VBool False -> pure (VBool True)+ VBool True -> do+ rightVal <- evalBytecode env rightIdx+ 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)++-- | Evaluate a string literal from bytecode data buffer.+evalBcStr :: (MonadEval m) => Env -> Word32 -> m NixValue+evalBcStr env bcIdx0 = do+ let count = fromIntegral (unsafePerformIO (cbcShortArg bcIdx0))+ dataOff = unsafePerformIO (cbcArg1 bcIdx0)+ chunks <- evalBcStringParts env count dataOff+ let (texts, ctxs) = unzip chunks+ pure (VStr (T.concat texts) (mconcat ctxs))++-- | Evaluate an indented string literal from bytecode data buffer.+evalBcIndStr :: (MonadEval m) => Env -> Word32 -> m NixValue+evalBcIndStr env bcIdx0 = do+ let count = fromIntegral (unsafePerformIO (cbcShortArg bcIdx0))+ dataOff = unsafePerformIO (cbcArg1 bcIdx0)+ chunks <- evalBcStringParts env count dataOff+ let (texts, ctxs) = unzip chunks+ raw = T.concat texts+ pure (VStr (stripIndentation raw) (mconcat ctxs))++-- | Evaluate string parts from the bytecode data buffer.+-- Each part is two words: (tag, value).+-- tag=0 -> StrLit (value = symbol), tag=1 -> StrInterp (value = bc_idx).+evalBcStringParts :: (MonadEval m) => Env -> Int -> Word32 -> m [(Text, StringContext)]+evalBcStringParts _ 0 _ = pure []+evalBcStringParts env n off = do+ let tag = unsafePerformIO (cbcData off)+ val = unsafePerformIO (cbcData (off + 1))+ chunk <- case tag of+ 0 -> pure (symbolText (Symbol val), emptyContext)+ _ -> do+ v <- evalBytecode env val+ coerceToString force applyValue v+ rest <- evalBcStringParts env (n - 1) (off + 2)+ pure (chunk : rest)++-- | Evaluate a list from bytecode data buffer.+evalBcList :: (MonadEval m) => Env -> Word32 -> m NixValue+evalBcList env bcIdx0 =+ let count = fromIntegral (unsafePerformIO (cbcShortArg bcIdx0)) :: Int+ dataOff = unsafePerformIO (cbcArg1 bcIdx0)+ readChildren 0 _ = []+ readChildren n off =+ let childIdx = unsafePerformIO (cbcData off)+ in cheapThunkBc env childIdx : readChildren (n - 1) (off + 1)+ in pure (VList (clistFromThunks (map thunkToCPtr (readChildren count dataOff))))++-- | Evaluate a function application from bytecode.+evalBcApp :: (MonadEval m) => Env -> Word32 -> m NixValue+evalBcApp env bcIdx0 = do+ let funcIdx = unsafePerformIO (cbcArg1 bcIdx0)+ argIdx = unsafePerformIO (cbcArg2 bcIdx0)+ funcVal <- evalBytecode env funcIdx+ case funcVal of+ VLambda closureEnv formals bodyBcIdx -> do+ let argThunk = cheapThunkBc env argIdx+ extEnv <- matchFormals closureEnv formals argThunk+ evalBytecode extEnv bodyBcIdx+ VBuiltin "tryEval" [] -> do+ result <- catchEvalError (evalBytecode env argIdx)+ 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 <- evalBytecode env argIdx+ applyBuiltin name accArgs argVal+ VAttrs attrs+ | Just functorThunk <- attrSetLookup "__functor" attrs -> do+ functor <- force functorThunk+ partiallyApplied <- applyValue functor funcVal+ -- Maintain laziness: thunk the argument for lambdas, like+ -- the normal VLambda path above. Builtins force anyway.+ case partiallyApplied of+ VLambda closureEnv formals bodyBcIdx -> do+ let argThunk = cheapThunkBc env argIdx+ extEnv <- matchFormals closureEnv formals argThunk+ evalBytecode extEnv bodyBcIdx+ _ -> do+ argVal <- evalBytecode env argIdx+ applyValue partiallyApplied argVal+ _ -> throwEvalError ("attempt to call " <> typeName funcVal <> ", which is not a function")++-- | Evaluate a lambda literal from bytecode — returns VLambda.+evalBcLambda :: (MonadEval m) => Env -> Word32 -> m NixValue+evalBcLambda env bcIdx0 =+ let flags_ = unsafePerformIO (cbcFlags bcIdx0)+ formalsOff = unsafePerformIO (cbcArg1 bcIdx0)+ bodyBcIdx = unsafePerformIO (cbcArg2 bcIdx0)+ captureOff = unsafePerformIO (cbcArg3 bcIdx0)+ formals = unsafePerformIO (decodeBcFormals flags_ formalsOff)+ captureInfo = unsafePerformIO (decodeBcCaptureInfo captureOff)+ in pure (VLambda (buildCaptureEnv env captureInfo) formals bodyBcIdx)++-- | Evaluate a select expression from bytecode.+evalBcSelect :: (MonadEval m) => Env -> Word32 -> m NixValue+evalBcSelect env bcIdx0 = do+ let hasDef = unsafePerformIO (cbcFlags bcIdx0) /= 0+ pathLen = fromIntegral (unsafePerformIO (cbcShortArg bcIdx0))+ targetIdx = unsafePerformIO (cbcArg1 bcIdx0)+ pathOff = unsafePerformIO (cbcArg2 bcIdx0)+ defIdx = unsafePerformIO (cbcArg3 bcIdx0)+ targetVal <- evalBytecode env targetIdx+ result <- walkBcAttrPath env pathLen pathOff targetVal+ case result of+ Just val -> pure val+ Nothing+ | hasDef -> evalBytecode env defIdx+ | otherwise -> do+ let pathName = collectBcAttrPathNames pathLen pathOff+ targetKeys = case targetVal of+ VAttrs attrs -> T.intercalate ", " (take 20 (attrSetKeys attrs))+ _ -> ""+ throwEvalError ("attribute '" <> pathName <> "' not found in " <> typeName targetVal <> " {" <> targetKeys <> "}")++-- | Evaluate a hasAttr expression from bytecode.+evalBcHasAttr :: (MonadEval m) => Env -> Word32 -> m NixValue+evalBcHasAttr env bcIdx0 = do+ let pathLen = fromIntegral (unsafePerformIO (cbcShortArg bcIdx0))+ targetIdx = unsafePerformIO (cbcArg1 bcIdx0)+ pathOff = unsafePerformIO (cbcArg2 bcIdx0)+ targetVal <- evalBytecode env targetIdx+ result <- walkBcAttrPath env pathLen pathOff targetVal+ pure (VBool (isJust result))++-- | Collect static attribute path names for error reporting.+collectBcAttrPathNames :: Int -> Word32 -> Text+collectBcAttrPathNames pathLen pathOff = T.intercalate "." (go pathLen pathOff)+ where+ go 0 _ = []+ go n off =+ let isExpr = unsafePerformIO (cbcData off)+ keyVal = unsafePerformIO (cbcData (off + 1))+ name = if isExpr /= 0 then "<expr>" else symbolText (Symbol keyVal)+ in name : go (n - 1) (off + 2)++-- | Walk an attribute path stored in the bytecode data buffer.+-- Each element is two words: (is_expr, key_or_bc_idx).+walkBcAttrPath :: (MonadEval m) => Env -> Int -> Word32 -> NixValue -> m (Maybe NixValue)+walkBcAttrPath _ 0 _ val = pure (Just val)+walkBcAttrPath env n off val = case val of+ VAttrs attrs -> do+ let isExpr = unsafePerformIO (cbcData off)+ keyVal = unsafePerformIO (cbcData (off + 1))+ resolved <-+ if isExpr /= 0+ then do+ keyResult <- evalBytecode env keyVal+ case keyResult of+ VStr s _ -> pure (Just s)+ VNull -> pure Nothing+ _ -> throwEvalError ("dynamic attribute key must be a string, got " <> typeName keyResult)+ else pure (Just (symbolText (Symbol keyVal)))+ case resolved >>= (`attrSetLookup` attrs) of+ Just thunk -> do+ inner <- force thunk+ walkBcAttrPath env (n - 1) (off + 2) inner+ Nothing -> pure Nothing+ -- Non-attrset: attribute path cannot continue. Return Nothing so+ -- that callers with a default (``a.b or def'') or hasAttr (``a ? b'')+ -- can handle it gracefully, matching C++ Nix behaviour.+ _ -> pure Nothing++-- | Evaluate attribute set from bytecode.+evalBcAttrs :: (MonadEval m) => Env -> Word32 -> m NixValue+evalBcAttrs env bcIdx0 = do+ let isRec = unsafePerformIO (cbcFlags bcIdx0) /= 0+ bindCount = unsafePerformIO (cbcShortArg bcIdx0)+ dataOff = unsafePerformIO (cbcArg1 bcIdx0)+ captureOff = unsafePerformIO (cbcArg2 bcIdx0)+ bindings = unsafePerformIO (decodeBcBindings bindCount dataOff)+ if isRec+ then do+ let captureInfo = unsafePerformIO (decodeBcCaptureInfo captureOff)+ evalBcRecAttrs env bindings captureInfo+ else evalBcNonRecAttrs env bindings++-- | Evaluate a non-recursive attr set from bytecode bindings.+evalBcNonRecAttrs :: (MonadEval m) => Env -> [BcBinding] -> m NixValue+evalBcNonRecAttrs env bindings = do+ thunkMap <- buildBcThunkMap env bindings+ pure (VAttrs (attrSetFromMap thunkMap))++-- | Evaluate a recursive attr set from bytecode bindings.+evalBcRecAttrs :: (MonadEval m) => Env -> [BcBinding] -> CaptureInfo -> m NixValue+evalBcRecAttrs env bindings captureInfo+ | allBcPositional bindings =+ -- Positional path: slots for variable lookup, CAttrSet for return.+ let slotCount = bcBindingSlotCount bindings+ slotsPtr = allocCSlots slotCount+ parentEnv = buildCaptureEnv env captureInfo+ (withArr, withCount) = case captureInfo of+ NoCaptureInfo -> envWithScopesRaw env+ Captures _ -> (nullPtr, 0)+ CapturesWithScopes _ -> withScopesForCapture env+ recEnv = newCEnv slotsPtr slotCount Nothing (Just parentEnv) withArr withCount+ thunkList = buildBcSlotThunks recEnv env bindings+ filled = fillCSlots slotsPtr thunkList+ attrMap = buildBcAttrMapFromSlots bindings thunkList+ in filled `seq` pure (VAttrs (attrSetFromMap attrMap))+ | otherwise = do+ -- Fallback: dynamic/nested keys — two-phase CAttrSet.+ allKeys <- bcBindingAllKeys env bindings+ let cset = buildCAttrSetKeys allKeys+ attrSet = AttrSet cset+ parentEnv = buildCaptureEnv env captureInfo+ (withArr, withCount) = case captureInfo of+ NoCaptureInfo -> envWithScopesRaw env+ Captures _ -> (nullPtr, 0)+ CapturesWithScopes _ -> withScopesForCapture env+ recEnv = newCEnv nullPtr 0 (Just attrSet) (Just parentEnv) withArr withCount+ thunkMap <- buildBcThunkMap recEnv bindings+ let filled = fillCAttrSetValues cset thunkMap+ in filled `seq` pure (VAttrs attrSet)++-- | Evaluate a let expression from bytecode.+evalBcLet :: (MonadEval m) => Env -> Word32 -> m NixValue+evalBcLet env bcIdx0 = do+ let bindCount = unsafePerformIO (cbcShortArg bcIdx0)+ dataOff = unsafePerformIO (cbcArg1 bcIdx0)+ bodyIdx = unsafePerformIO (cbcArg2 bcIdx0)+ captureOff = unsafePerformIO (cbcArg3 bcIdx0)+ bindings = unsafePerformIO (decodeBcBindings bindCount dataOff)+ captureInfo = unsafePerformIO (decodeBcCaptureInfo captureOff)+ if allBcPositional bindings+ then do+ -- Positional path: slots for O(1) lookup.+ let slotCount = bcBindingSlotCount bindings+ slotsPtr = allocCSlots slotCount+ parentEnv = buildCaptureEnv env captureInfo+ (withArr, withCount) = case captureInfo of+ NoCaptureInfo -> envWithScopesRaw env+ Captures _ -> (nullPtr, 0)+ CapturesWithScopes _ -> withScopesForCapture env+ letEnv = newCEnv slotsPtr slotCount Nothing (Just parentEnv) withArr withCount+ filled = fillCSlots slotsPtr (buildBcSlotThunks letEnv env bindings)+ in filled `seq` evalBytecode letEnv bodyIdx+ else do+ -- Fallback: dynamic/nested keys — two-phase CAttrSet.+ allKeys <- bcBindingAllKeys env bindings+ let cset = buildCAttrSetKeys allKeys+ parentEnv = buildCaptureEnv env captureInfo+ (withArr, withCount) = case captureInfo of+ NoCaptureInfo -> envWithScopesRaw env+ Captures _ -> (nullPtr, 0)+ CapturesWithScopes _ -> withScopesForCapture env+ letEnv = newCEnv nullPtr 0 (Just (AttrSet cset)) (Just parentEnv) withArr withCount+ thunkMap <- buildBcThunkMap letEnv bindings+ let filled = fillCAttrSetValues cset thunkMap+ in filled `seq` evalBytecode letEnv bodyIdx++-- | Check if all bytecode bindings are single static keys (eligible for positional).+-- Must stay in sync with 'allStaticSingleKey' in 'Nix.Expr.Resolve'.+allBcPositional :: [BcBinding] -> Bool+allBcPositional = all isEligible+ where+ isEligible (BcNamed [BcStaticKey _] _) = True+ isEligible (BcInherit _) = True+ isEligible (BcInheritFrom _ _) = True+ isEligible _ = False++-- | Count positional slots for bytecode bindings.+-- Must stay in sync with 'lexicalScopeFromBindings' in 'Nix.Expr.Resolve'.+bcBindingSlotCount :: [BcBinding] -> Int+bcBindingSlotCount = foldl' countOne 0+ where+ countOne !acc (BcNamed [BcStaticKey _] _) = acc + 1+ countOne !acc (BcInherit syms) = acc + length syms+ countOne !acc (BcInheritFrom _ syms) = acc + length syms+ countOne !acc _ = acc++-- | Build thunks for positional bytecode bindings in declaration order.+buildBcSlotThunks :: Env -> Env -> [BcBinding] -> [Thunk]+buildBcSlotThunks recEnv outerEnv = concatMap slotThunk+ where+ slotThunk (BcNamed [BcStaticKey _] valBcIdx) =+ [mkThunkBc recEnv valBcIdx]+ slotThunk (BcInherit syms) =+ map (inheritLookup outerEnv . symbolText . Symbol) syms+ slotThunk (BcInheritFrom fromBcIdx syms) =+ -- inherit (from) x y z; → one thunk per name that selects from the from-expr.+ -- Each thunk gets a minimal env with the from-value at slot 0.+ let fromThunk = mkThunkBc recEnv fromBcIdx+ mkInheritThunk sym =+ let name = symbolText (Symbol sym)+ selectExpr = ESelect (EResolvedVar 0 0) [StaticKey name] Nothing+ (sp, sc) = buildCSlots [fromThunk]+ fromEnv = newMinimalEnv sp sc+ in mkSyntheticThunk fromEnv selectExpr+ in map mkInheritThunk syms+ -- Unreachable: allBcPositional guards this path.+ slotThunk _ = []++-- | Build attr map from slots for positional bytecode bindings.+buildBcAttrMapFromSlots :: [BcBinding] -> [Thunk] -> Map Text Thunk+buildBcAttrMapFromSlots bindings thunks = go bindings thunks Map.empty+ where+ go [] _ !acc = acc+ go (BcNamed [BcStaticKey sym] _ : bs) (t : ts) !acc =+ go bs ts (Map.insert (symbolText (Symbol sym)) t acc)+ go (BcInherit syms : bs) ts !acc =+ let (used, rest) = splitAt (length syms) ts+ accMerged = foldl' (\a (sym, t0) -> Map.insert (symbolText (Symbol sym)) t0 a) acc (zip syms used)+ in go bs rest accMerged+ go (BcInheritFrom _ syms : bs) ts !acc =+ let (used, rest) = splitAt (length syms) ts+ accMerged = foldl' (\a (sym, t0) -> Map.insert (symbolText (Symbol sym)) t0 a) acc (zip syms used)+ in go bs rest accMerged+ -- Unreachable: allBcPositional guards this path.+ go (_ : bs) ts !acc = go bs ts acc++-- | Build thunk map for bytecode attrs (non-rec or fallback rec path).+buildBcThunkMap :: (MonadEval m) => Env -> [BcBinding] -> m (Map Text Thunk)+buildBcThunkMap thunkEnv = foldM addBinding Map.empty+ where+ addBinding acc (BcNamed keys valBcIdx) = do+ resolvedKeys <- mapM (resolveBcKey thunkEnv) keys+ case sequence resolvedKeys of+ Nothing -> pure acc -- null key -> skip+ Just [key] ->+ pure (insertWithMerge acc key (mkThunkBc thunkEnv valBcIdx))+ Just path ->+ let nested = buildBcNestedAttr thunkEnv path valBcIdx+ in pure (foldl' (\a (k, t0) -> insertWithMerge a k t0) acc (Map.toList nested))+ addBinding acc (BcInherit syms) =+ pure (foldl' (\a sym -> let name = symbolText (Symbol sym) in insertWithMerge a name (inheritLookup thunkEnv name)) acc syms)+ addBinding acc (BcInheritFrom fromBcIdx syms) =+ -- inherit (from) name -> select name from the from-expr.+ -- Create a small env with the from value at slot 0, then a+ -- synthetic expression that selects name from slot 0.+ let addInheritFrom a sym =+ let name = symbolText (Symbol sym)+ selectExpr = ESelect (EResolvedVar 0 0) [StaticKey name] Nothing+ (sp, sc) = buildCSlots [mkThunkBc thunkEnv fromBcIdx]+ fromEnv = newMinimalEnv sp sc+ in insertWithMerge a name (mkSyntheticThunk fromEnv selectExpr)+ in pure (foldl' addInheritFrom acc syms)++ insertWithMerge acc key thunk =+ case Map.lookup key acc of+ Nothing -> Map.insert key thunk acc+ Just existing -> Map.insert key (mergeThunks existing thunk) acc++-- | Resolve a bytecode attr key to text.+resolveBcKey :: (MonadEval m) => Env -> BcAttrKey -> m (Maybe Text)+resolveBcKey _env (BcStaticKey sym) = pure (Just (symbolText (Symbol sym)))+resolveBcKey env (BcDynamicKey bcIdx0) = do+ val <- evalBytecode env bcIdx0+ case val of+ VStr s _ -> pure (Just s)+ VNull -> pure Nothing+ _ -> throwEvalError ("dynamic attribute key must be a string, got " <> typeName val)++-- | Build a nested attribute structure from a resolved dotted path (bytecode).+buildBcNestedAttr :: Env -> [Text] -> Word32 -> Map Text Thunk+buildBcNestedAttr _thunkEnv [] _valBcIdx = Map.empty+buildBcNestedAttr thunkEnv [key] valBcIdx =+ Map.singleton key (mkThunkBc thunkEnv valBcIdx)+buildBcNestedAttr thunkEnv (key : rest) valBcIdx =+ Map.singleton key (evaluated (VAttrs (attrSetFromMap (buildBcNestedAttr thunkEnv rest valBcIdx))))++-- | Extract all top-level keys from bytecode bindings, resolving+-- dynamic keys as needed. Used by the fallback path (two-phase+-- CAttrSet construction) where all keys must be known before thunks.+bcBindingAllKeys :: (MonadEval m) => Env -> [BcBinding] -> m [Text]+bcBindingAllKeys env bindings = fmap concat (mapM oneBinding bindings)+ where+ oneBinding (BcNamed (key : _) _) = do+ resolved <- resolveBcKey env key+ pure (maybeToList resolved)+ oneBinding (BcNamed [] _) = pure []+ oneBinding (BcInherit syms) =+ pure (map (symbolText . Symbol) syms)+ oneBinding (BcInheritFrom _ syms) =+ pure (map (symbolText . Symbol) syms)++-- ---------------------------------------------------------------------------+-- 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 <> "'")++-- | Evaluate a with-scoped variable: check with-scopes first (innermost+-- to outermost), then fall back to the standard name-based lookup+-- (parent chain to builtins). For trimmed envs ('CapturesWithScopes'),+-- the root scope is already appended to 'envWithScopes' so the+-- with-scope lookup finds builtins without needing a parent chain.+-- Supports both resolved (CAttrSet*) and lazy (CThunk*, tagged with bit 0)+-- with-scope entries. Lazy entries are forced on first lookup and the+-- pointer is updated in place so subsequent lookups hit the resolved+-- attrset directly.+evalWithVar :: (MonadEval m) => Env -> Text -> m NixValue+evalWithVar env name =+ let (withArr, withCount) = envWithScopesRaw env+ in evalWithVarScopes env name withArr (fromIntegral withCount) 0++evalWithVarScopes :: (MonadEval m) => Env -> Text -> Ptr (Ptr ()) -> Int -> Int -> m NixValue+evalWithVarScopes env name withArr count idx+ | idx >= count = evalVar env name+ | otherwise = do+ let scopePtr = unsafePerformIO (peekElemOff withArr idx)+ if isLazyWithScope scopePtr+ then do+ -- Lazy with-scope: force the thunk to get the attrset+ let thunkPtr = untagWithScope scopePtr+ scopeVal <- force (Thunk thunkPtr)+ case scopeVal of+ VAttrs (AttrSet cset) -> do+ -- Cache: replace the tagged thunk pointer with the resolved+ -- attrset pointer so future lookups are fast.+ let resolvedPtr = castPtr cset+ seq (unsafePerformIO (pokeElemOff withArr idx resolvedPtr)) (pure ())+ case attrSetLookup name (AttrSet cset) of+ Just thunk -> force thunk+ Nothing -> evalWithVarScopes env name withArr count (idx + 1)+ _ -> throwEvalError ("'with' requires a set, got " <> typeName scopeVal)+ else+ -- Resolved attrset scope (normal path)+ case attrSetLookup name (AttrSet (castPtr scopePtr)) of+ Just thunk -> force thunk+ Nothing -> evalWithVarScopes env name withArr count (idx + 1)++-- | Check if a with-scope pointer is tagged as lazy (bit 0 set).+isLazyWithScope :: Ptr () -> Bool+isLazyWithScope ptr = ptrToWordPtr ptr .&. 1 /= 0++-- | Remove the lazy tag from a with-scope pointer, returning a CThunkPtr.+untagWithScope :: Ptr () -> CThunkPtr+untagWithScope ptr =+ let tagged = ptrToWordPtr ptr+ in wordPtrToPtr (tagged .&. complement 1)++-- | Tag a CThunkPtr as a lazy with-scope (set bit 0).+tagLazyWithScope :: CThunkPtr -> Ptr ()+tagLazyWithScope ptr =+ let raw = ptrToWordPtr (castPtr ptr)+ in wordPtrToPtr (raw .|. 1)++-- | Push a lazy (thunk-based) with-scope onto the environment.+-- The scope is NOT forced until a WITH_VAR lookup actually needs it.+{-# NOINLINE pushLazyWithScope #-}+pushLazyWithScope :: Thunk -> Env -> Env+pushLazyWithScope (Thunk thunkPtr) =+ pushWithScopeRaw (tagLazyWithScope thunkPtr)++-- | Push a raw pointer as a with-scope.+{-# NOINLINE pushWithScopeRaw #-}+pushWithScopeRaw :: Ptr () -> Env -> Env+pushWithScopeRaw ptr (Env envPtr) =+ Env (unsafePerformIO (cenvPushWith envPtr ptr))++-- ---------------------------------------------------------------------------+-- Formals matching + env helpers (used by evalBcApp, applyValue, etc.)+-- ---------------------------------------------------------------------------++-- | Match a lambda's formals against an argument thunk.+matchFormals :: (MonadEval m) => Env -> EvalFormals -> Thunk -> m Env+matchFormals closureEnv (EFName _) argThunk =+ let (sp, sc) = buildCSlots [argThunk]+ in pure (envFromSlots sp sc closureEnv)+matchFormals closureEnv (EFSet formals allowExtra) argThunk = do+ argVal <- force argThunk+ matchFormalSet closureEnv formals allowExtra argVal Nothing+matchFormals closureEnv (EFNamedSet _ formals allowExtra) argThunk = do+ argVal <- force argThunk+ matchFormalSet closureEnv formals allowExtra argVal (Just argThunk)++-- | Match destructuring set pattern formals against an attrset value.+matchFormalSet :: (MonadEval m) => Env -> [EvalFormal] -> Bool -> NixValue -> Maybe Thunk -> m Env+matchFormalSet closureEnv formals allowExtra argVal atThunk =+ case argVal of+ VAttrs attrs -> do+ checkExtraKeys formals allowExtra attrs+ checkMissingFormals attrs formals+ let formalEnv = envFromSlots formalSlotsPtr formalSlotCount closureEnv+ (formalSlotsPtr, formalSlotCount) = buildCSlots formalThunks+ formalThunks = case atThunk of+ Nothing -> map resolveOneFormal formals+ Just at -> at : map resolveOneFormal formals+ resolveOneFormal (EvalFormal name defBcIdx) =+ case attrSetLookup name attrs of+ Just thunk -> thunk+ Nothing -> case defBcIdx of+ Just bcIdx -> mkThunkBc formalEnv bcIdx+ Nothing -> error "matchFormalSet: missing required formal (unreachable)"+ pure formalEnv+ _ -> throwEvalError ("function expects a set argument, got " <> typeName argVal)++checkExtraKeys :: (MonadEval m) => [EvalFormal] -> Bool -> AttrSet -> m ()+checkExtraKeys _ True _ = pure ()+checkExtraKeys formals False attrs =+ let expected = map efName formals+ actual = attrSetKeys attrs+ extra = filter (`notElem` expected) actual+ in case extra of+ [] -> pure ()+ (k : _) -> throwEvalError ("unexpected attribute '" <> k <> "' in function argument")++checkMissingFormals :: (MonadEval m) => AttrSet -> [EvalFormal] -> m ()+checkMissingFormals attrs formals =+ let allMissing = [efName f | f <- formals, isNothing (efDefault f), not (attrSetMember (efName f) attrs)]+ in case allMissing of+ [] -> pure ()+ (name : _) ->+ let provided = attrSetKeys attrs+ provSnippet = T.intercalate ", " (take 20 provided)+ missSnippet = T.intercalate ", " allMissing+ in throwEvalError ("missing required attribute '" <> name <> "'; all missing: [" <> missSnippet <> "]; provided keys (" <> T.pack (show (length provided)) <> "): [" <> provSnippet <> "]")++-- | Build capture environment from capture info.+-- NoCaptureInfo: no trimming, use env as-is.+-- Captures: build minimal env from captured slots.+-- CapturesWithScopes: build minimal env + copy with-scopes.+buildCaptureEnv :: Env -> CaptureInfo -> Env+buildCaptureEnv env NoCaptureInfo = env+buildCaptureEnv env (Captures captureList) =+ let (slotsPtr, slotCount) = buildCSlots [envLookupResolved lvl idx env | (lvl, idx) <- captureList]+ in newMinimalEnv slotsPtr slotCount+buildCaptureEnv env (CapturesWithScopes captureList) =+ let (slotsPtr, slotCount) = buildCSlots [envLookupResolved lvl idx env | (lvl, idx) <- captureList]+ (withArr, withCount) = withScopesForCapture env+ in newCEnv slotsPtr slotCount Nothing Nothing withArr withCount++-- | Look up a name in the environment and return its thunk.+-- Used by @inherit@ bindings (both bytecode and Expr paths).+inheritLookup :: Env -> Text -> Thunk+inheritLookup env name =+ case envLookup name env of+ Just thunk -> thunk+ Nothing -> error ("inheritLookup: undefined variable '" <> T.unpack name <> "' (unreachable)")++-- | Merge two thunks for nested attribute update.+-- If both are computed attrsets, recursively merge with attrSetUnionWith.+-- Otherwise the new value wins.+mergeThunks :: Thunk -> Thunk -> Thunk+mergeThunks a b =+ case (readThunkValue a, readThunkValue b) of+ (Just (VAttrs aa), Just (VAttrs bb)) ->+ evaluated (VAttrs (attrSetUnionWith mergeThunks aa bb))+ _ -> b++-- ---------------------------------------------------------------------------+-- 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" builtinAbort,+ -- 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" builtinSeq,+ builtin2 "trace" builtinTrace,+ builtin2 "warn" builtinWarn,+ 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: lazy 'derivation' wrapper over the eager+ -- 'derivationStrict' primop (matches C++ Nix corepkgs/derivation.nix).+ builtin1 "derivation" builtinDerivationLazy,+ builtin1 "derivationStrict" builtinDerivationStrict,+ -- 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 (traceVerbose: same as trace for now, --trace-verbose not yet gated)+ builtin2 "traceVerbose" builtinTrace,+ 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 + path import+ builtin1 "path" builtinPath,+ 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 (precompileArgs name allArgs))+ else executeBuiltin name allArgs++-- | Pre-compile regex patterns at partial application time.+-- When builtins.match or builtins.split receives its first argument+-- (the pattern string), compile it immediately and store the compiled+-- RE.Regex in a VCompiledRegex, replacing the raw VStr. The compiled+-- form is carried in VBuiltin's accumulated args and reused on every+-- subsequent application — zero recompilation.+precompileArgs :: Text -> [NixValue] -> [NixValue]+precompileArgs "match" [VStr pat _] =+ let anchored = "^" <> pat <> "$"+ in case cachedCompileRegex anchored of+ Just compiled -> [VCompiledRegex (CompiledRegex pat compiled)]+ Nothing -> [VStr pat emptyContext] -- fail later at execute time+precompileArgs "split" [VStr pat _] =+ case cachedCompileRegex pat of+ Just compiled -> [VCompiledRegex (CompiledRegex pat compiled)]+ Nothing -> [VStr pat emptyContext] -- fail later at execute time+precompileArgs _ args = args++-- | 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 bodyBcIdx) arg = do+ extEnv <- matchFormals closureEnv formals (evaluated arg)+ evalBytecode extEnv bodyBcIdx+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 builtinAbort+ -- 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 builtinSeq+ "trace" -> apply2 builtinTrace+ "warn" -> apply2 builtinWarn+ "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: lazy 'derivation' over eager 'derivationStrict'+ "derivation" -> apply1 builtinDerivationLazy+ "derivationStrict" -> apply1 builtinDerivationStrict+ -- 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 (traceVerbose: same as trace for now)+ "traceVerbose" -> apply2 builtinTrace+ "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 + path import+ "path" -> apply1 builtinPath+ "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"+ VCompiledRegex _ -> "lambda"++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 cl) = pure (VInt (fromIntegral (clistLen cl)))+builtinLength other = throwEvalError ("builtins.length: expected a list, got " <> typeName other)++builtinHead :: (MonadEval m) => NixValue -> m NixValue+builtinHead (VList cl)+ | clistLen cl == 0 = throwEvalError "builtins.head: empty list"+ | otherwise = case clistThunks cl of+ (p : _) -> force (Thunk p)+ [] -> throwEvalError "builtins.head: empty list" -- unreachable: clistLen > 0+builtinHead other = throwEvalError ("builtins.head: expected a list, got " <> typeName other)++builtinTail :: (MonadEval m) => NixValue -> m NixValue+builtinTail (VList cl)+ | clistLen cl == 0 = throwEvalError "builtins.tail: empty list"+ | otherwise = pure (VList (clistFromThunks (drop 1 (clistThunks cl))))+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)++builtinAbort :: (MonadEval m) => NixValue -> m NixValue+builtinAbort (VStr msg _) = abortEvaluation msg+builtinAbort other = abortEvaluation ("builtins.abort: expected a string, got " <> typeName other)++-- ---------------------------------------------------------------------------+-- Builtin implementations — attr set (arity 1)+-- ---------------------------------------------------------------------------++builtinAttrNames :: (MonadEval m) => NixValue -> m NixValue+builtinAttrNames (VAttrs attrs) =+ -- Zero thunk allocation: attrSetKeys reads symbol names from C arrays.+ let thunks = map (evaluated . mkStr) (attrSetKeys attrs)+ in pure (VList (clistFromThunks (map thunkToCPtr thunks)))+builtinAttrNames other =+ throwEvalError ("builtins.attrNames: expected a set, got " <> typeName other)++builtinAttrValues :: (MonadEval m) => NixValue -> m NixValue+builtinAttrValues (VAttrs attrs) =+ pure (VList (clistFromThunks (map thunkToCPtr (attrSetElems attrs))))+builtinAttrValues other =+ throwEvalError ("builtins.attrValues: expected a set, got " <> typeName other)++builtinListToAttrs :: (MonadEval m) => NixValue -> m NixValue+builtinListToAttrs (VList cl) = do+ let thunks = map Thunk (clistThunks cl)+ pairs <- mapM listToAttrsPair thunks+ -- Nix listToAttrs uses first-wins: if duplicate name, first element wins.+ let firstWins = Map.fromListWith (\_ kept -> kept) pairs+ pure (VAttrs (attrSetFromMap firstWins))+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 cl) = do+ let thunks = map Thunk (clistThunks cl)+ keys <- mapM forceToString thunks+ pure (VAttrs (attrSetRemoveKeys keys attrs))+ 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 cl) = do+ let thunks = map Thunk (clistThunks cl)+ vals <- catAttrsCollect key thunks+ pure (VList (clistFromThunks (map thunkToCPtr 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 cl) =+ -- Lazy: each element is a deferred application, forced only on demand.+ let thunks = map Thunk (clistThunks cl)+ in pure (VList (clistFromThunks (map (thunkToCPtr . deferApply func) thunks)))+builtinMap _ other =+ throwEvalError ("builtins.map: expected a list, got " <> typeName other)++builtinFilter :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinFilter predFn (VList cl) = do+ let thunks = map Thunk (clistThunks cl)+ filtered <- filterThunks predFn thunks+ pure (VList (clistFromThunks (map thunkToCPtr 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.+ -- Slot 0 = function.+ let fnThunk = evaluated func+ (sp, sc) = buildCSlots [fnThunk]+ env = newMinimalEnv sp sc+ mkIndexThunk i = mkThunk env (EApp (EResolvedVar 0 0) (ELit (NixInt i)))+ in pure (VList (clistFromThunks (map (thunkToCPtr . 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 cl) = do+ let thunks = map Thunk (clistThunks cl)+ vals <- mapM force thunks+ sorted <- mergeSort comparator vals+ pure (VList (clistFromThunks (map (thunkToCPtr . 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 cl) = do+ -- Semi-eager: must force each application to discover list structure for+ -- concatenation, but element thunks within those sub-lists stay lazy.+ let thunks = map Thunk (clistThunks cl)+ deferredApps = map (deferApply func) thunks+ results <- mapM force deferredApps+ concatted <- mapM extractList results+ pure (VList (clistFromThunks (map thunkToCPtr (concat concatted))))+builtinConcatMap _ other =+ throwEvalError ("builtins.concatMap: expected a list, got " <> typeName other)++extractList :: (MonadEval m) => NixValue -> m [Thunk]+extractList (VList cl) = pure (map Thunk (clistThunks cl))+extractList other =+ throwEvalError ("builtins.concatMap: function must return a list, got " <> typeName other)++builtinAny :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinAny predFn (VList cl) = do+ let thunks = map Thunk (clistThunks cl)+ 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 cl) = do+ let thunks = map Thunk (clistThunks cl)+ 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 cl) = do+ let thunks = map Thunk (clistThunks cl)+ 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 cl) (VInt idx)+ | idx < 0 || fromIntegral idx >= clistLen cl = elemAtOOB idx cl+ | otherwise =+ -- O(1) direct C array access instead of materializing the whole list+ let ptr = unsafePerformIO (clistGet (unCList cl) (fromIntegral idx))+ in force (Thunk ptr)+ where+ elemAtOOB i c =+ throwEvalError+ ( "builtins.elemAt: index "+ <> T.pack (show i)+ <> " out of bounds for list of length "+ <> T.pack (show (clistLen c))+ )+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 cl) = do+ let thunks = map Thunk (clistThunks cl)+ (rightThunks, wrongThunks) <- partitionThunks predFn thunks+ pure+ ( VAttrs+ ( attrSetFromMap $+ Map.fromList+ [ ("right", evaluated (VList (clistFromThunks (map thunkToCPtr rightThunks)))),+ ("wrong", evaluated (VList (clistFromThunks (map thunkToCPtr 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 cl) = do+ let thunks = map Thunk (clistThunks cl)+ groups <- groupByCollect func thunks Map.empty+ pure (VAttrs (attrSetFromMap (Map.map (evaluated . VList . clistFromThunks . map thunkToCPtr . 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 cl) = do+ let thunks = map Thunk (clistThunks cl)+ 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+ -- Nix coerces list elements to strings (paths, derivations, etc.)+ coerceToString force applyValue val+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 cl) =+ foldlStrict op initial (map Thunk (clistThunks cl))+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 (EResolvedVar 0 0) (EResolvedVar 0 1)@ in a self-contained env.+-- Slot 0 = function, slot 1 = argument.+deferApply :: NixValue -> Thunk -> Thunk+deferApply func argThunk =+ let (sp, sc) = buildCSlots [evaluated func, argThunk]+ env = newMinimalEnv sp sc+ in mkSyntheticThunk env deferApplyExpr++-- | Shared expression for 'deferApply'. Allocated once as a CAF.+deferApplyExpr :: Expr+deferApplyExpr = EApp (EResolvedVar 0 0) (EResolvedVar 0 1)+{-# NOINLINE deferApplyExpr #-}++-- | 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 cl) = do+ let thunks = map Thunk (clistThunks cl)+ 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 force applyValue 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 (clistFromThunks [thunkToCPtr (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 cl -> mapM (forceToOutputName . Thunk) (clistThunks cl)+ _ -> 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 cl) = do+ let thunks = map Thunk (clistThunks cl)+ sublists <- mapM forceThenExtractList thunks+ pure (VList (clistFromThunks (concat sublists)))+ where+ forceThenExtractList thunk = do+ val <- force thunk+ case val of+ VList innerCl -> pure (clistThunks innerCl)+ _ -> 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 (fromIntegral a + b))+builtinAdd (VFloat a) (VInt b) = pure (VFloat (a + fromIntegral 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 (fromIntegral a - b))+builtinSub (VFloat a) (VInt b) = pure (VFloat (a - fromIntegral 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 (fromIntegral a * b))+builtinMul (VFloat a) (VInt b) = pure (VFloat (a * fromIntegral 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)+ | a == minBound && b == -1 = pure (VInt minBound)+ | otherwise = pure (VInt (quot a b))+builtinDiv _ (VFloat 0) = throwEvalError "builtins.div: division by zero"+builtinDiv (VInt a) (VFloat b) = pure (VFloat (fromIntegral a / b))+builtinDiv (VFloat a) (VInt b) = pure (VFloat (a / fromIntegral 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)+ | a == minBound && b == -1 = pure (VInt 0)+ | otherwise = 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) =+ -- Each attr value is a deferred @f key val@, forced only on demand.+ -- Eagerly builds all thunks via attrSetMapWithKey — with C arena thunks+ -- (~16 bytes each), this is cheaper than the former MappedAttrs overhead+ -- and keeps all data off the GHC heap.+ -- Slot 0 = function, slot 1 = key, slot 2 = value.+ pure (VAttrs (attrSetMapWithKey deferAttr attrs))+ where+ deferAttr key valThunk =+ let (sp, sc) = buildCSlots [evaluated func, evaluated (mkStr key), valThunk]+ env = newMinimalEnv sp sc+ in mkSyntheticThunk env mapAttrsExpr+builtinMapAttrs _ other =+ throwEvalError ("builtins.mapAttrs: expected a set, got " <> typeName other)++-- | Shared expression for 'builtinMapAttrs'. Allocated once as a CAF.+mapAttrsExpr :: Expr+mapAttrsExpr = EApp (EApp (EResolvedVar 0 0) (EResolvedVar 0 1)) (EResolvedVar 0 2)+{-# NOINLINE mapAttrsExpr #-}++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 :: EvalFormals -> NixValue+formalsToAttrs (EFName _) = VAttrs (attrSetFromMap Map.empty)+formalsToAttrs (EFSet formals _) = formalsListToAttrs formals+formalsToAttrs (EFNamedSet _ formals _) = formalsListToAttrs formals++formalsListToAttrs :: [EvalFormal] -> NixValue+formalsListToAttrs formals =+ VAttrs $+ attrSetFromMap $+ Map.fromList+ [(efName f, evaluated (VBool (isJust (efDefault 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) =+ -- Closure env holds the function at slot 0. The __functor lambda+ -- body is EResolvedVar 1 0: level 1 (past _self's slot), index 0.+ let (sp, sc) = buildCSlots [evaluated func]+ closureEnv = newMinimalEnv sp sc+ bodyBcIdx = unsafePerformIO (compileExpr (EResolvedVar 1 0))+ -- __functor = self: __fn (ignores self, returns the original function)+ functorLambda = VLambda closureEnv (EFName "_self") bodyBcIdx+ 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 cl) = do+ let thunks = map Thunk (clistThunks cl)+ attrSets <- mapM forceToAttrSet thunks+ let merged = mergeAllAttrs attrSets+ -- Lazy: each result is a deferred f(name)(values) thunk, not eagerly+ -- evaluated. Critical for nixpkgs evalModules fixpoint — config is a+ -- self-referencing lazy attrset that must be COMPUTED (holding the lazy+ -- result) before any individual attribute thunks are forced.+ resultPairs = map (deferZip 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+ -- Slot 0 = function, slot 1 = name, slot 2 = values list.+ deferZip fn (key, thunkList) =+ let valueList = VList (clistFromThunks (map thunkToCPtr thunkList))+ (slots, slotCount) = buildCSlots [evaluated fn, evaluated (mkStr key), evaluated valueList]+ env = newMinimalEnv slots slotCount+ in (key, mkSyntheticThunk env mapAttrsExpr)+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 fromCl) (VList toCl) (VStr input inputCtx) = do+ let fromThunks = map Thunk (clistThunks fromCl)+ toThunks = map Thunk (clistThunks toCl)+ 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+ -- Nix semantics: input context + 'to' string contexts (not 'from').+ -- Ideally only 'to' contexts for patterns that matched, but including+ -- all 'to' contexts is the common implementation.+ mergedCtx = inputCtx <> 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)+-- ---------------------------------------------------------------------------++-- ---------------------------------------------------------------------------+-- Regex compilation cache+-- ---------------------------------------------------------------------------++-- | Global regex compilation cache. Keyed by the raw pattern string+-- (including anchoring for match). Idempotent memoization via+-- unsafePerformIO — same rationale as thunk memoization.+{-# NOINLINE regexCacheRef #-}+regexCacheRef :: IORef (Map Text RE.Regex)+regexCacheRef = unsafePerformIO (newIORef Map.empty)++-- | Compile a regex, using the global cache to avoid recompilation.+-- Returns Nothing for invalid patterns. NOINLINE prevents GHC from+-- inlining and floating the unsafePerformIO reads.+{-# NOINLINE cachedCompileRegex #-}+cachedCompileRegex :: Text -> Maybe RE.Regex+cachedCompileRegex pat =+ unsafePerformIO $ do+ cache <- atomicModifyIORef' regexCacheRef (\c -> (c, c))+ case Map.lookup pat cache of+ Just compiled -> pure (Just compiled)+ Nothing -> case RE.makeRegexM (T.unpack pat) :: Maybe RE.Regex of+ Nothing -> pure Nothing+ Just compiled -> do+ atomicModifyIORef' regexCacheRef (\c -> (Map.insert pat compiled c, ()))+ pure (Just compiled)++-- ---------------------------------------------------------------------------+-- Regex builtins+-- ---------------------------------------------------------------------------++-- | @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+-- Pre-compiled path: regex was compiled at partial-application time.+builtinMatch (VCompiledRegex (CompiledRegex _ compiled)) (VStr str _) =+ matchWithCompiled compiled str+-- Direct 2-arg call: use global compilation cache.+builtinMatch (VStr regex _) (VStr str _) =+ let anchored = "^" <> regex <> "$"+ in case cachedCompileRegex anchored of+ Nothing -> throwEvalError ("builtins.match: invalid regex: " <> regex)+ Just compiled -> matchWithCompiled compiled str+builtinMatch (VStr _ _) other =+ throwEvalError ("builtins.match: expected a string, got " <> typeName other)+builtinMatch (VCompiledRegex _) other =+ throwEvalError ("builtins.match: expected a string, got " <> typeName other)+builtinMatch other _ =+ throwEvalError ("builtins.match: expected a string (regex), got " <> typeName other)++-- | Shared match logic for pre-compiled and freshly-compiled regex paths.+matchWithCompiled :: (MonadEval m) => RE.Regex -> Text -> m NixValue+matchWithCompiled compiled str =+ 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 (clistFromThunks (map (thunkToCPtr . toThunk) captureGroups)))++-- | @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+-- Pre-compiled path: regex was compiled at partial-application time.+builtinSplit (VCompiledRegex (CompiledRegex _ compiled)) (VStr str _) =+ splitWithCompiled compiled str+-- Direct 2-arg call: use global compilation cache.+builtinSplit (VStr regex _) (VStr str _) =+ case cachedCompileRegex regex of+ Nothing -> throwEvalError ("builtins.split: invalid regex: " <> regex)+ Just compiled -> splitWithCompiled compiled str+builtinSplit (VStr _ _) other =+ throwEvalError ("builtins.split: expected a string, got " <> typeName other)+builtinSplit (VCompiledRegex _) other =+ throwEvalError ("builtins.split: expected a string, got " <> typeName other)+builtinSplit other _ =+ throwEvalError ("builtins.split: expected a string (regex), got " <> typeName other)++-- | Shared split logic for pre-compiled and freshly-compiled regex paths.+splitWithCompiled :: (MonadEval m) => RE.Regex -> Text -> m NixValue+splitWithCompiled compiled str =+ let allMatches = matchAllText compiled (T.unpack str)+ strText = T.unpack str+ result = buildSplitResult strText 0 allMatches+ in pure (VList (clistFromThunks (map thunkToCPtr result)))++-- | 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 (clistFromThunks (map thunkToCPtr 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"++-- | Convert 'Ordering' to the Nix compareVersions convention: -1, 0, 1.+ordToNix :: Ordering -> Int64+ordToNix LT = -1+ordToNix EQ = 0+ordToNix GT = 1++compareVersionParts :: [Text] -> [Text] -> Int64+compareVersionParts [] [] = 0+compareVersionParts [] (_ : _) = -1+compareVersionParts (_ : _) [] = 1+compareVersionParts (a : as) (b : bs) =+ case compareComponent a b of+ EQ -> compareVersionParts as bs+ cmp -> ordToNix cmp++compareComponent :: Text -> Text -> Ordering+compareComponent a b+ | a == b = EQ+ | allDigits a && allDigits b = compare (readInt a) (readInt b)+ -- "pre" sorts before everything else (Nix pre-release convention)+ | a == "pre" = LT+ | b == "pre" = GT+ | a == "" = LT+ | b == "" = GT+ -- Alphabetic components sort before numeric in Nix+ | isAlphaComp a && allDigits b = LT+ | allDigits a && isAlphaComp b = GT+ | otherwise = compare a b+ where+ allDigits t = not (T.null t) && T.all isDigit t+ isAlphaComp t = case T.uncons t of+ Just (c, _) -> isAlpha c+ Nothing -> False+ readInt :: Text -> Int64+ readInt = T.foldl' (\acc c -> acc * 10 + fromIntegral (digitToInt c)) (0 :: Int64)++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 (clistFromThunks (map (thunkToCPtr . 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+ | isAlpha c ->+ let (alpha, rest) = T.span isAlpha t+ in alpha : splitVersionComponents rest+ | otherwise ->+ -- Non-alphanumeric separator (e.g. '-', '_'): consume as single char+ let (_, rest) = T.splitAt 1 t+ in 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 = do+ (json, ctx) <- valueToJSON val+ pure (VStr json ctx)++valueToJSON :: (MonadEval m) => NixValue -> m (Text, StringContext)+valueToJSON VNull = pure ("null", emptyContext)+valueToJSON (VBool True) = pure ("true", emptyContext)+valueToJSON (VBool False) = pure ("false", emptyContext)+valueToJSON (VInt n) = pure (T.pack (show n), emptyContext)+valueToJSON (VFloat f) = pure (formatNixFloat f, emptyContext)+valueToJSON (VStr s ctx) = pure (jsonEscapeString s, ctx)+valueToJSON (VList cl) = do+ let thunks = map Thunk (clistThunks cl)+ vals <- mapM force thunks+ results <- mapM valueToJSON vals+ let jsonVals = map fst results+ ctx = mconcat (map snd results)+ pure ("[" <> T.intercalate "," jsonVals <> "]", ctx)+valueToJSON (VAttrs attrs) =+ -- Derivation-like attrsets (with outPath) coerce to string in toJSON,+ -- matching C++ Nix behavior.+ case attrSetLookup "outPath" attrs of+ Just outThunk -> do+ outVal <- force outThunk+ (s, ctx) <- coerceToString force applyValue outVal+ pure (jsonEscapeString s, ctx)+ Nothing -> do+ let m = attrSetToMap attrs+ sortedKeys = Map.keys m+ results <- mapM (jsonPair m) sortedKeys+ let pairs = map fst results+ ctx = mconcat (map snd results)+ pure ("{" <> T.intercalate "," pairs <> "}", ctx)+ where+ jsonPair attrMap key = case Map.lookup key attrMap of+ Nothing -> pure ("", emptyContext)+ Just thunk -> do+ val <- force thunk+ (jsonVal, ctx) <- valueToJSON val+ pure (jsonEscapeString key <> ":" <> jsonVal, ctx)+valueToJSON (VPath p) = pure (jsonEscapeString p, emptyContext)+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"+valueToJSON (VCompiledRegex _) = throwEvalError "builtins.toJSON: cannot convert a function 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 (hi, r2)+ -- UTF-16 surrogate pair: high surrogate followed by \uXXXX low+ | hi >= 0xD800 && hi <= 0xDBFF ->+ case T.stripPrefix "\\u" r2 of+ Just r3 -> case parseHex4 r3 of+ Just (lo, r4)+ | lo >= 0xDC00 && lo <= 0xDFFF ->+ let combined = 0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00)+ in go (T.singleton (chr combined) : chunks) r4+ _ -> go (T.singleton (chr hi) : chunks) r2+ Nothing -> go (T.singleton (chr hi) : chunks) r2+ 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) :: [(Int64, 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 (clistFromThunks (map thunkToCPtr (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 (clistFromThunks (map thunkToCPtr (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 cl) = mapM_ ((force >=> deepForce) . Thunk) (clistThunks cl)+deepForce (VAttrs attrs) = mapM_ (force >=> deepForce) (attrSetElems attrs)+deepForce _ = pure ()++-- | @builtins.seq a b@ — evaluate @a@ to WHNF, then return @b@.+builtinSeq :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinSeq !_first = pure++-- | @builtins.trace msg val@ — print @msg@ to stderr, return @val@.+builtinTrace :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinTrace msgVal result = do+ msg <- case msgVal of+ VStr s _ -> pure s+ other -> pure (showValueForTrace other)+ traceMessage ("trace: " <> msg)+ pure result++-- | @builtins.warn msg val@ — print warning to stderr, return @val@.+builtinWarn :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinWarn msgVal result = do+ msg <- case msgVal of+ VStr s _ -> pure s+ other -> pure (showValueForTrace other)+ traceMessage ("warning: " <> msg)+ pure result++-- | Pretty-print a value for trace/warn, matching C++ Nix's printValue.+showValueForTrace :: NixValue -> Text+showValueForTrace (VInt n) = T.pack (show n)+showValueForTrace (VFloat f) = formatNixFloat f+showValueForTrace (VBool True) = "true"+showValueForTrace (VBool False) = "false"+showValueForTrace VNull = "null"+showValueForTrace (VPath p) = p+showValueForTrace other = "<<" <> typeName other <> ">>"++-- ---------------------------------------------------------------------------+-- 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 cl -> do+ let items = map Thunk (clistThunks cl)+ result <- closureLoop operatorVal (Seq.fromList items) [] []+ pure (VList (clistFromThunks (map (thunkToCPtr . 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 newCl ->+ closureLoop operator (rest <> Seq.fromList (map Thunk (clistThunks newCl))) (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 (VStr s _) = importFile s+builtinImport other =+ throwEvalError ("import: expected a path or string, 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 (mkStr (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 cl) (VStr name _) = do+ let searchPath = map Thunk (clistThunks cl)+ 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 _) = fetchAndExtractTarball url+builtinFetchTarball (VAttrs attrs) = do+ url <- forceAttrStr "builtins.fetchTarball" "url" attrs+ fetchAndExtractTarball url+builtinFetchTarball other =+ throwEvalError ("builtins.fetchTarball: expected a string or set, got " <> typeName other)++-- | Download a tarball, extract it, and return the path to the extracted+-- directory. Uses a content-hashed temp directory. Downloads and extracts+-- in a single shell pipeline to avoid binary-as-text encoding issues.+fetchAndExtractTarball :: (MonadEval m) => Text -> m NixValue+fetchAndExtractTarball url = do+ sysTmp <- getTempDir+ let urlHash = sha256Hex (TE.encodeUtf8 url)+ extractDir = sysTmp <> "/nova-nix-tarball-" <> urlHash+ -- Single pipeline: mkdir, download, extract with --strip-components=1+ -- The -- separator prevents argument injection from the URL.+ (code, _, errOut) <-+ runProcess+ "sh"+ [ "-c",+ "mkdir -p \"$1\" && curl -sSfL -- \"$2\" | tar -xz -C \"$1\" --strip-components=1",+ "--",+ extractDir,+ url+ ]+ ""+ case code of+ 0 -> pure (VPath extractDir)+ _ -> throwEvalError ("builtins.fetchTarball: " <> errOut)++builtinFetchGit :: (MonadEval m) => NixValue -> m NixValue+builtinFetchGit (VStr url _) = do+ sysTmp <- getTempDir+ let urlHash = sha256Hex (TE.encodeUtf8 url)+ cloneDir = sysTmp <> "/nova-nix-fetchgit-" <> urlHash+ (code, _, errOut) <- runProcess "git" ["clone", "--depth", "1", "--", url, cloneDir] ""+ case code of+ 0 -> pure (VPath cloneDir)+ _ -> throwEvalError ("builtins.fetchGit: git clone failed: " <> errOut)+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)++-- | Resolve the system temp directory. Checks @TMPDIR@ (Unix), then+-- @TEMP@ (Windows), falls back to @\/tmp@.+getTempDir :: (MonadEval m) => m Text+getTempDir = do+ candidates <- mapM getEnvVar ["TMPDIR", "TEMP"]+ pure (fromMaybe "/tmp" (find (not . T.null) candidates))++-- | 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, using full Nix string+-- coercion (VStr, VPath, VInt, VBool, VAttrs via __toString/outPath).+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+ result <- catchEvalError (coerceToString force applyValue val)+ case result of+ Right (s, _ctx) -> pure s+ Left _ -> throwEvalError (builtin <> ": '" <> key <> "' must be a string")++-- | Force an optional string attribute via full Nix coercion.+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+ result <- catchEvalError (coerceToString force applyValue val)+ case result of+ Right (s, _ctx) -> pure (Just s)+ Left _ -> pure Nothing++-- ---------------------------------------------------------------------------+-- Builtin implementations — derivation construction+-- ---------------------------------------------------------------------------++-- | Eager derivation computation — @builtins.derivationStrict@. Forces all+-- input attrs into env vars, content-hashes, and returns the full derivation+-- attrset (drvPath, outPath, per-output, _derivation). Called LAZILY by the+-- @derivation@ wrapper ('builtinDerivationLazy'), so forcing a derivation to+-- WHNF never forces this — matching C++ Nix's derivationStrict/derivation split.+builtinDerivationStrict :: (MonadEval m) => NixValue -> m NixValue+builtinDerivationStrict (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 cl -> mapM (forceToText . Thunk) (clistThunks cl)+ _ -> 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 cl -> mapM (forceToText . Thunk) (clistThunks cl)+ _ -> throwEvalError "derivation: 'args' must be a list of strings"++ -- Materialize once, reuse for both env collection and result merge+ let materialized = attrSetToMap attrs++ -- Collect all string-coercible attrs into env WITH their contexts+ (drvEnvPairs, envContext) <- collectDrvEnvWithContext materialized++ -- 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 env.+ -- The hash was computed from the pre-output drv (drvOutputs = []),+ -- so adding output paths and drvPath to drvEnv here does not affect+ -- the content address. Real Nix .drv files include these in their+ -- env section — builders read $out etc. from the environment.+ let completeEnv =+ Map.union+ (Map.fromList (("drvPath", drvPathText) : outPaths))+ envMap+ completeDrv = drv {drvOutputs = drvOutputsList, drvEnv = completeEnv}++ -- 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 per-output attrsets matching Nix: drv.out = { outPath, drvPath, type }+ let mkOutputAttrs outName outP =+ let outCtx = outPathCtx outName+ outputAttrMap =+ Map.fromList+ [ ("outPath", evaluated (VStr outP outCtx)),+ ("drvPath", evaluated (VStr drvPathText drvPathCtx)),+ ("type", evaluated (mkStr "derivation"))+ ]+ in evaluated (VAttrs (attrSetFromMap outputAttrMap))++ -- 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, mkOutputAttrs outName outP) | (outName, outP) <- outPaths]+ -- Merge original attrs underneath so computed attrs take priority+ resultAttrs = Map.union baseAttrs materialized++ pure (VAttrs (attrSetFromMap resultAttrs))+builtinDerivationStrict other =+ throwEvalError ("derivation: expected a set, got " <> typeName other)++-- | Lazy @derivation@ wrapper — mirrors C++ Nix's @corepkgs/derivation.nix@.+-- Returns a WHNF attrset whose @drvPath@/@outPath@/output-path/@_derivation@+-- attrs are LAZY thunks that defer to 'builtinDerivationStrict'. Forcing a+-- derivation to WHNF therefore does NOT force its input/env closure — which is+-- essential for nixpkgs, where merely referencing a derivation (e.g.+-- @drv != null@, @assert (libxcrypt != null)@) must not build its whole closure.+--+-- The lazy thunks are built with the same synthetic-select pattern used by+-- @inherit (from)@: a single shared @strict@ thunk (so the eager computation+-- runs at most once) selected from via fresh minimal envs.+builtinDerivationLazy :: (MonadEval m) => NixValue -> m NixValue+builtinDerivationLazy (VAttrs attrs) = do+ -- Output names are cheap (matches @drvAttrs @ { outputs ? [ "out" ], ... }@).+ outputNames <- case attrSetLookup "outputs" attrs of+ Nothing -> pure ["out"]+ Just thunk -> do+ val <- force thunk+ case val of+ VList cl -> mapM (forceToText . Thunk) (clistThunks cl)+ _ -> throwEvalError "derivation: 'outputs' must be a list of strings"+ -- One shared thunk computing @derivationStrict attrs@, forced only when an+ -- output path / drvPath is actually read.+ let drvAttrsThunk = evaluated (VAttrs attrs)+ strictBuiltinThunk = evaluated (VBuiltin "derivationStrict" [])+ strictThunk =+ let (sp, sc) = buildCSlots [drvAttrsThunk, strictBuiltinThunk]+ envDS = newMinimalEnv sp sc+ in mkSyntheticThunk envDS (EApp (EResolvedVar 0 1) (EResolvedVar 0 0))+ selectStrict field =+ let (sp, sc) = buildCSlots [strictThunk]+ envF = newMinimalEnv sp sc+ in mkSyntheticThunk envF (ESelect (EResolvedVar 0 0) [StaticKey field] Nothing)+ -- WHNF spine: input attrs (unforced) overlaid with the lazy computed attrs.+ let computedAttrs =+ Map.fromList $+ [ ("type", evaluated (mkStr "derivation")),+ ("drvPath", selectStrict "drvPath"),+ ("outPath", selectStrict "outPath"),+ ("_derivation", selectStrict "_derivation")+ ]+ ++ [(outName, selectStrict outName) | outName <- outputNames]+ resultAttrs = Map.union computedAttrs (attrSetToMap attrs)+ pure (VAttrs (attrSetFromMap resultAttrs))+builtinDerivationLazy other =+ throwEvalError ("derivation: expected a set, got " <> typeName other)++-- | Force a thunk to a Text string via full Nix coercion.+forceToText :: (MonadEval m) => Thunk -> m Text+forceToText thunk = do+ val <- force thunk+ (s, _ctx) <- coerceToString force applyValue val+ pure s++-- | Collect all string-coercible attributes for the derivation environment,+-- along with the merged string context from all collected values.+-- Uses 'coerceToStringPermissive' for full Nix coercion including+-- __toString, outPath, and list-to-space-separated-string.+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+ VNull -> pure Nothing+ _ -> do+ result <- catchEvalError (coerceToStringPermissive val)+ case result of+ Right (s, ctx) -> pure (Just (key, s, ctx))+ Left _ -> 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 !Int64+ | 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 (clistFromThunks (map (thunkToCPtr . 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 Int64+readDecimal t+ | T.null t = Nothing+ | T.all isDigit t = Just (T.foldl' (\acc c -> acc * 10 + fromIntegral (digitToInt c)) 0 t)+ | otherwise = Nothing++readHexT :: Text -> Maybe Int64+readHexT t+ | T.null t = Nothing+ | T.all isHexDigit t = Just (T.foldl' (\acc c -> acc * 16 + fromIntegral (digitToInt c)) 0 t)+ | otherwise = Nothing++readOctT :: Text -> Maybe Int64+readOctT t+ | T.null t = Nothing+ | T.all isOctDigit t =+ Just (T.foldl' (\acc c -> acc * 8 + fromIntegral (digitToInt c)) 0 t)+ | otherwise = Nothing++readBinT :: Text -> Maybe Int64+readBinT t+ | T.null t = Nothing+ | T.all (\c -> c == '0' || c == '1') t =+ Just (T.foldl' (\acc c -> acc * 2 + fromIntegral (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 cl -> do+ let thunks = map Thunk (clistThunks cl) items <- mapM (force >=> valueToXML (depth + 1)) thunks pure (indent depth <> "<list>\n" <> T.concat items <> indent depth <> "</list>\n") VAttrs attrs -> do
+ src/Nix/Eval/Arena.hs view
@@ -0,0 +1,98 @@+-- | Unified arena lifecycle for the C data layer.+--+-- Initializes and destroys all C sub-arenas (symbol table, thunk arena,+-- env slot allocator) with a single pair of calls. Handles StablePtr+-- cleanup on destruction: iterates all thunks, identifies payloads that+-- are Haskell StablePtrs (PENDING + COMPUTED\/PTR), and frees them+-- before tearing down the C memory.+--+-- @+-- bracket_ arenaInit arenaDestroy $ do+-- ... evaluation ...+-- @+module Nix.Eval.Arena+ ( arenaInit,+ arenaDestroy,+ )+where++import Data.Word (Word32)+import Foreign.Marshal.Array (allocaArray, peekArray)+import Foreign.Ptr (Ptr)+import Foreign.StablePtr (castPtrToStablePtr, freeStablePtr)+import Nix.Eval.CBytecode (cbcDestroy, cbcInit)+import Nix.Eval.CCtxStr (cctxstrFreeAll)+import Nix.Eval.CEnv (cenvDestroy, cenvInit)+import Nix.Eval.CLambda (clambdaFreeAll)+import Nix.Eval.CList (clistFreeAll)+import Nix.Eval.CThunk (cthunkDestroy, cthunkInit)+import Nix.Eval.Symbol (symbolDestroy, symbolInit)++-- ---------------------------------------------------------------------------+-- FFI imports for batch StablePtr collection+-- ---------------------------------------------------------------------------++foreign import ccall unsafe "nn_arena_stableptr_count"+ c_nn_arena_stableptr_count :: IO Word32++foreign import ccall unsafe "nn_arena_collect_stableptrs"+ c_nn_arena_collect_stableptrs :: Ptr (Ptr ()) -> Word32 -> IO Word32++-- ---------------------------------------------------------------------------+-- FFI import for bulk CAttrSet cleanup+-- ---------------------------------------------------------------------------++foreign import ccall unsafe "nn_attrset_free_all"+ cattrsetFreeAll :: IO ()++-- ---------------------------------------------------------------------------+-- Lifecycle+-- ---------------------------------------------------------------------------++-- | Initialize all C data layer arenas. Call once before evaluation.+-- Initializes: symbol table, thunk arena, env slot allocator, bytecode store.+arenaInit :: IO ()+arenaInit = do+ symbolInit 0+ cthunkInit 0+ cenvInit+ cbcInit 0 0++-- | Destroy all C arenas, properly freeing StablePtrs first.+--+-- 1. Collects all StablePtr payloads from thunks (batch C call)+-- 2. Frees each StablePtr from Haskell+-- 3. Frees all tracked CLists (bulk cleanup)+-- 4. Frees all tracked CAttrSets (bulk cleanup)+-- 5. Destroys bytecode store+-- 6. Destroys env slot pages+-- 7. Destroys thunk arena blocks+-- 8. Destroys symbol table + string arena+arenaDestroy :: IO ()+arenaDestroy = do+ cleanupStablePtrs+ cctxstrFreeAll+ clambdaFreeAll+ clistFreeAll+ cattrsetFreeAll+ cbcDestroy+ cenvDestroy+ cthunkDestroy+ symbolDestroy++-- | Free all StablePtrs held by thunk payloads.+-- Uses batch C-side collection to avoid per-thunk FFI crossing.+-- Frees payloads from three thunk states:+-- PENDING: StablePtr (Expr, Env)+-- BLACKHOLE: original PENDING StablePtr (eval failed/in-progress)+-- COMPUTED/NN_VALUE_PTR: StablePtr NixValue+-- Inline scalar/C-pointer payloads (INT..CTXSTR) are skipped.+cleanupStablePtrs :: IO ()+cleanupStablePtrs = do+ count <- c_nn_arena_stableptr_count+ if count == 0+ then pure ()+ else allocaArray (fromIntegral count) $ \buf -> do+ written <- c_nn_arena_collect_stableptrs buf count+ ptrs <- peekArray (fromIntegral written) buf+ mapM_ (freeStablePtr . castPtrToStablePtr) ptrs
+ src/Nix/Eval/CAttrSet.hs view
@@ -0,0 +1,194 @@+-- | C-backed sorted attribute set via FFI.+--+-- Wraps @cbits/nn_attrset.c@ — a contiguous sorted array of+-- @(nn_symbol_t, void*)@ pairs. Replaces Haskell's @Data.Map.Strict@+-- for attribute sets, cutting per-entry overhead from ~48 bytes+-- (Map.Bin node) to ~12 bytes (4-byte symbol + 8-byte pointer).+--+-- Construction is two-phase: insert entries, then freeze (sort + dedup).+-- After freeze, lookup is O(log n) binary search over contiguous memory.+--+-- Values are opaque @CThunkPtr@ handles stored as @void*@ on the C side.+-- The C side never dereferences them. Thunk lifetimes are managed by+-- the thunk arena; attribute set cleanup only frees the key\/value arrays.+module Nix.Eval.CAttrSet+ ( -- * Opaque handle+ CAttrSet,++ -- * Lifecycle+ cattrsetNew,+ cattrsetFree,+ withCAttrSet,++ -- * Construction+ cattrsetInsert,+ cattrsetFreeze,++ -- * Query+ cattrsetLookup,+ cattrsetIndex,+ cattrsetGetValue,+ cattrsetSetValue,+ cattrsetGetKey,++ -- * Bulk access+ cattrsetSize,+ cattrsetKeys,++ -- * Set operations+ cattrsetUnion,+ cattrsetRemoveKeys,+ )+where++import Data.Word (Word32)+import Foreign.C.Types (CInt (..))+import Foreign.Marshal.Array (peekArray, withArray)+import Foreign.Ptr (Ptr, castPtr, nullPtr)+import Nix.Eval.CThunk (CThunkPtr)+import Nix.Eval.Symbol (Symbol (..))++-- | Opaque handle to a C-allocated attribute set.+-- The Haskell side holds a raw pointer; the C side owns the memory.+data NnAttrSet++-- | Convenience alias.+type CAttrSet = Ptr NnAttrSet++-- ---------------------------------------------------------------------------+-- FFI imports (all unsafe — no callbacks, fast data access)+-- ---------------------------------------------------------------------------++foreign import ccall unsafe "nn_attrset_new"+ c_nn_attrset_new :: Word32 -> IO CAttrSet++foreign import ccall unsafe "nn_attrset_free"+ c_nn_attrset_free :: CAttrSet -> IO ()++foreign import ccall unsafe "nn_attrset_insert"+ c_nn_attrset_insert :: CAttrSet -> Word32 -> Ptr () -> IO ()++foreign import ccall unsafe "nn_attrset_freeze"+ c_nn_attrset_freeze :: CAttrSet -> IO ()++foreign import ccall unsafe "nn_attrset_lookup"+ c_nn_attrset_lookup :: CAttrSet -> Word32 -> IO (Ptr ())++foreign import ccall unsafe "nn_attrset_index"+ c_nn_attrset_index :: CAttrSet -> Word32 -> IO CInt++foreign import ccall unsafe "nn_attrset_set_value"+ c_nn_attrset_set_value :: CAttrSet -> Word32 -> Ptr () -> IO ()++foreign import ccall unsafe "nn_attrset_get_value"+ c_nn_attrset_get_value :: CAttrSet -> Word32 -> IO (Ptr ())++foreign import ccall unsafe "nn_attrset_get_key"+ c_nn_attrset_get_key :: CAttrSet -> Word32 -> IO Word32++foreign import ccall unsafe "nn_attrset_size"+ c_nn_attrset_size :: CAttrSet -> IO Word32++foreign import ccall unsafe "nn_attrset_keys_ptr"+ c_nn_attrset_keys_ptr :: CAttrSet -> IO (Ptr Word32)++foreign import ccall unsafe "nn_attrset_union"+ c_nn_attrset_union :: CAttrSet -> CAttrSet -> IO CAttrSet++foreign import ccall unsafe "nn_attrset_remove_keys"+ c_nn_attrset_remove_keys :: CAttrSet -> Ptr Word32 -> Word32 -> IO CAttrSet++-- ---------------------------------------------------------------------------+-- Lifecycle+-- ---------------------------------------------------------------------------++-- | Allocate a new empty attribute set with the given capacity hint.+cattrsetNew :: Word32 -> IO CAttrSet+cattrsetNew = c_nn_attrset_new++-- | Free a C-allocated attribute set. Does NOT free thunk values.+cattrsetFree :: CAttrSet -> IO ()+cattrsetFree = c_nn_attrset_free++-- | Bracket pattern: allocate, use, free.+withCAttrSet :: Word32 -> (CAttrSet -> IO a) -> IO a+withCAttrSet cap action = do+ set <- cattrsetNew cap+ result <- action set+ cattrsetFree set+ pure result++-- ---------------------------------------------------------------------------+-- Construction+-- ---------------------------------------------------------------------------++-- | Insert a key-value pair. Call before freeze.+-- The value is a CThunkPtr cast to Ptr () — C never dereferences it.+cattrsetInsert :: CAttrSet -> Symbol -> CThunkPtr -> IO ()+cattrsetInsert set (Symbol sym) ptr =+ c_nn_attrset_insert set sym (castPtr ptr)++-- | Sort and deduplicate. Must be called once before any queries.+cattrsetFreeze :: CAttrSet -> IO ()+cattrsetFreeze = c_nn_attrset_freeze++-- ---------------------------------------------------------------------------+-- Query+-- ---------------------------------------------------------------------------++-- | Look up a key. Returns 'Nothing' if not found.+-- The returned 'CThunkPtr' is borrowed — the thunk arena owns it.+cattrsetLookup :: CAttrSet -> Symbol -> IO (Maybe CThunkPtr)+cattrsetLookup set (Symbol sym) = do+ ptr <- c_nn_attrset_lookup set sym+ pure (if ptr == nullPtr then Nothing else Just (castPtr ptr))++-- | Find the index of a key, or 'Nothing' if not found.+cattrsetIndex :: CAttrSet -> Symbol -> IO (Maybe Word32)+cattrsetIndex set (Symbol sym) = do+ idx <- c_nn_attrset_index set sym+ pure (if idx < 0 then Nothing else Just (fromIntegral idx))++-- | Get the value at a known index.+cattrsetGetValue :: CAttrSet -> Word32 -> IO CThunkPtr+cattrsetGetValue set idx = castPtr <$> c_nn_attrset_get_value set idx++-- | Update the value at a known index (for two-phase construction).+cattrsetSetValue :: CAttrSet -> Word32 -> CThunkPtr -> IO ()+cattrsetSetValue set idx ptr = c_nn_attrset_set_value set idx (castPtr ptr)++-- | Get the key symbol at a known index.+cattrsetGetKey :: CAttrSet -> Word32 -> IO Symbol+cattrsetGetKey set idx = Symbol <$> c_nn_attrset_get_key set idx++-- ---------------------------------------------------------------------------+-- Bulk access+-- ---------------------------------------------------------------------------++-- | Number of entries (unique keys after freeze).+cattrsetSize :: CAttrSet -> IO Word32+cattrsetSize = c_nn_attrset_size++-- | Read all keys as a list of symbols (sorted).+cattrsetKeys :: CAttrSet -> IO [Symbol]+cattrsetKeys set = do+ n <- cattrsetSize set+ ptr <- c_nn_attrset_keys_ptr set+ raw <- peekArray (fromIntegral n) ptr+ pure (map Symbol raw)++-- ---------------------------------------------------------------------------+-- Set operations+-- ---------------------------------------------------------------------------++-- | Right-biased union (// semantics). Result is frozen.+cattrsetUnion :: CAttrSet -> CAttrSet -> IO CAttrSet+cattrsetUnion = c_nn_attrset_union++-- | Remove a list of keys. Result is frozen.+cattrsetRemoveKeys :: CAttrSet -> [Symbol] -> IO CAttrSet+cattrsetRemoveKeys set syms = do+ let raw = map unSymbol syms+ n = fromIntegral (length raw)+ withArray raw $ \ptr ->+ c_nn_attrset_remove_keys set ptr n
+ src/Nix/Eval/CBytecode.hs view
@@ -0,0 +1,319 @@+-- | C-backed bytecode storage via FFI.+--+-- Wraps @cbits/nn_bytecode.c@ — two growable arrays storing flat Nix+-- expression bytecode: an @nn_op_t[]@ instruction array (16 bytes each)+-- and a @uint32_t[]@ data buffer for variable-length operands.+--+-- @+-- cbcInit 0 0+-- idx <- cbcEmit opLitInt 0 0 42 0 0+-- cbcOpcode idx -- NN_OP_LIT_INT (0)+-- cbcArg1 idx -- 42+-- cbcDestroy+-- @+module Nix.Eval.CBytecode+ ( -- * Lifecycle+ cbcInit,+ cbcDestroy,++ -- * Emit+ cbcEmit,+ cbcEmitData,++ -- * Read instructions+ cbcOpcode,+ cbcFlags,+ cbcShortArg,+ cbcArg1,+ cbcArg2,+ cbcArg3,++ -- * Read data+ cbcData,++ -- * Diagnostics+ cbcOpCount,+ cbcDataCount,++ -- * Opcodes+ opLitInt,+ opLitFloat,+ opLitBool,+ opLitNull,+ opLitUri,+ opLitPath,+ opStr,+ opIndStr,+ opVar,+ opWithVar,+ opResolvedVar,+ opAttrs,+ opList,+ opSelect,+ opHasAttr,+ opApp,+ opLambda,+ opLet,+ opIf,+ opWith,+ opAssert,+ opUnary,+ opBinary,+ opSearchPath,++ -- * UnaryOp flags+ unaryNot,+ unaryNegate,++ -- * BinaryOp flags+ binaryAdd,+ binarySub,+ binaryMul,+ binaryDiv,+ binaryAnd,+ binaryOr,+ binaryImpl,+ binaryEq,+ binaryNeq,+ binaryLt,+ binaryLte,+ binaryGt,+ binaryGte,+ binaryConcat,+ binaryUpdate,++ -- * Formal type flags+ formalName,+ formalSet,+ formalNamedSet,++ -- * String part tags+ strpartLit,+ strpartInterp,++ -- * Binding type tags+ bindNamed,+ bindInherit,++ -- * CaptureInfo type tags+ captureNone,+ captureSlots,+ captureWithScopes,++ -- * Attr key tags+ attrkeyStatic,+ attrkeyDynamic,+ )+where++import Data.Word (Word16, Word32, Word8)++-- ---------------------------------------------------------------------------+-- FFI imports (all unsafe — no callbacks, fast data access)+-- ---------------------------------------------------------------------------++foreign import ccall unsafe "nn_bytecode_init"+ c_nn_bytecode_init :: Word32 -> Word32 -> IO ()++foreign import ccall unsafe "nn_bytecode_destroy"+ c_nn_bytecode_destroy :: IO ()++foreign import ccall unsafe "nn_bc_emit"+ c_nn_bc_emit :: Word8 -> Word8 -> Word16 -> Word32 -> Word32 -> Word32 -> IO Word32++foreign import ccall unsafe "nn_bc_emit_data"+ c_nn_bc_emit_data :: Word32 -> IO Word32++foreign import ccall unsafe "nn_bc_opcode"+ c_nn_bc_opcode :: Word32 -> IO Word8++foreign import ccall unsafe "nn_bc_flags"+ c_nn_bc_flags :: Word32 -> IO Word8++foreign import ccall unsafe "nn_bc_short_arg"+ c_nn_bc_short_arg :: Word32 -> IO Word16++foreign import ccall unsafe "nn_bc_arg1"+ c_nn_bc_arg1 :: Word32 -> IO Word32++foreign import ccall unsafe "nn_bc_arg2"+ c_nn_bc_arg2 :: Word32 -> IO Word32++foreign import ccall unsafe "nn_bc_arg3"+ c_nn_bc_arg3 :: Word32 -> IO Word32++foreign import ccall unsafe "nn_bc_data"+ c_nn_bc_data :: Word32 -> IO Word32++foreign import ccall unsafe "nn_bc_op_count"+ c_nn_bc_op_count :: IO Word32++foreign import ccall unsafe "nn_bc_data_count"+ c_nn_bc_data_count :: IO Word32++-- ---------------------------------------------------------------------------+-- Lifecycle+-- ---------------------------------------------------------------------------++-- | Initialize the global bytecode store. Call once before evaluation.+-- Arguments are capacity hints (0 = defaults: 65536 ops, 131072 data).+cbcInit :: Word32 -> Word32 -> IO ()+cbcInit = c_nn_bytecode_init++-- | Destroy the global bytecode store, freeing all memory.+cbcDestroy :: IO ()+cbcDestroy = c_nn_bytecode_destroy++-- ---------------------------------------------------------------------------+-- Emit+-- ---------------------------------------------------------------------------++-- | Append one instruction. Returns the instruction index.+cbcEmit :: Word8 -> Word8 -> Word16 -> Word32 -> Word32 -> Word32 -> IO Word32+cbcEmit = c_nn_bc_emit++-- | Append one uint32 to the data buffer. Returns the data offset.+cbcEmitData :: Word32 -> IO Word32+cbcEmitData = c_nn_bc_emit_data++-- ---------------------------------------------------------------------------+-- Read instructions+-- ---------------------------------------------------------------------------++cbcOpcode :: Word32 -> IO Word8+cbcOpcode = c_nn_bc_opcode++cbcFlags :: Word32 -> IO Word8+cbcFlags = c_nn_bc_flags++cbcShortArg :: Word32 -> IO Word16+cbcShortArg = c_nn_bc_short_arg++cbcArg1 :: Word32 -> IO Word32+cbcArg1 = c_nn_bc_arg1++cbcArg2 :: Word32 -> IO Word32+cbcArg2 = c_nn_bc_arg2++cbcArg3 :: Word32 -> IO Word32+cbcArg3 = c_nn_bc_arg3++-- ---------------------------------------------------------------------------+-- Read data+-- ---------------------------------------------------------------------------++cbcData :: Word32 -> IO Word32+cbcData = c_nn_bc_data++-- ---------------------------------------------------------------------------+-- Diagnostics+-- ---------------------------------------------------------------------------++cbcOpCount :: IO Word32+cbcOpCount = c_nn_bc_op_count++cbcDataCount :: IO Word32+cbcDataCount = c_nn_bc_data_count++-- ---------------------------------------------------------------------------+-- Opcode constants+-- ---------------------------------------------------------------------------++opLitInt, opLitFloat, opLitBool, opLitNull :: Word8+opLitInt = 0+opLitFloat = 1+opLitBool = 2+opLitNull = 3++opLitUri, opLitPath :: Word8+opLitUri = 4+opLitPath = 5++opStr, opIndStr :: Word8+opStr = 6+opIndStr = 7++opVar, opWithVar, opResolvedVar :: Word8+opVar = 8+opWithVar = 9+opResolvedVar = 10++opAttrs, opList :: Word8+opAttrs = 11+opList = 12++opSelect, opHasAttr, opApp :: Word8+opSelect = 13+opHasAttr = 14+opApp = 15++opLambda, opLet :: Word8+opLambda = 16+opLet = 17++opIf, opWith, opAssert :: Word8+opIf = 18+opWith = 19+opAssert = 20++opUnary, opBinary :: Word8+opUnary = 21+opBinary = 22++opSearchPath :: Word8+opSearchPath = 23++-- ---------------------------------------------------------------------------+-- Sub-type flags+-- ---------------------------------------------------------------------------++unaryNot, unaryNegate :: Word8+unaryNot = 0+unaryNegate = 1++binaryAdd, binarySub, binaryMul, binaryDiv :: Word8+binaryAdd = 0+binarySub = 1+binaryMul = 2+binaryDiv = 3++binaryAnd, binaryOr, binaryImpl :: Word8+binaryAnd = 4+binaryOr = 5+binaryImpl = 6++binaryEq, binaryNeq :: Word8+binaryEq = 7+binaryNeq = 8++binaryLt, binaryLte, binaryGt, binaryGte :: Word8+binaryLt = 9+binaryLte = 10+binaryGt = 11+binaryGte = 12++binaryConcat, binaryUpdate :: Word8+binaryConcat = 13+binaryUpdate = 14++formalName, formalSet, formalNamedSet :: Word8+formalName = 0+formalSet = 1+formalNamedSet = 2++strpartLit, strpartInterp :: Word32+strpartLit = 0+strpartInterp = 1++bindNamed, bindInherit :: Word32+bindNamed = 0+bindInherit = 1++captureNone, captureSlots, captureWithScopes :: Word32+captureNone = 0+captureSlots = 1+captureWithScopes = 2++attrkeyStatic, attrkeyDynamic :: Word32+attrkeyStatic = 0+attrkeyDynamic = 1
+ src/Nix/Eval/CCtxStr.hs view
@@ -0,0 +1,127 @@+-- | C-backed context-bearing strings via FFI.+--+-- Wraps @cbits/nn_ctxstr.c@ — a string with its StringContext stored+-- as a contiguous C struct. Context-free strings continue to use+-- thunk tag 4 (bare nn_symbol_t); this module handles tag 8 for+-- strings with non-empty context.+--+-- This module provides raw FFI bindings only. The higher-level+-- marshalling between 'StringContext' and C is in 'Nix.Eval.Types'+-- (to avoid circular imports).+module Nix.Eval.CCtxStr+ ( -- * Opaque handle+ NnCtxStr,+ CCtxStrPtr,++ -- * Lifecycle+ cctxstrNew,+ cctxstrFreeAll,++ -- * Element setters+ cctxstrSetPlain,+ cctxstrSetDrvOutput,+ cctxstrSetAllOutputs,++ -- * Accessors+ cctxstrText,+ cctxstrCtxCount,+ cctxstrElemTag,+ cctxstrElemHash,+ cctxstrElemName,+ cctxstrElemOutput,+ )+where++import Data.Word (Word16, Word32, Word8)+import Foreign.Ptr (Ptr)++-- | Phantom type for C-side @nn_ctxstr_t@.+data NnCtxStr++-- | Pointer to a C-allocated context string.+type CCtxStrPtr = Ptr NnCtxStr++-- ---------------------------------------------------------------------------+-- FFI imports (all unsafe — no callbacks, fast data access)+-- ---------------------------------------------------------------------------++foreign import ccall unsafe "nn_ctxstr_new"+ c_nn_ctxstr_new :: Word32 -> Word16 -> IO CCtxStrPtr++foreign import ccall unsafe "nn_ctxstr_free_all"+ c_nn_ctxstr_free_all :: IO ()++foreign import ccall unsafe "nn_ctxstr_set_plain"+ c_nn_ctxstr_set_plain :: CCtxStrPtr -> Word16 -> Word32 -> Word32 -> IO ()++foreign import ccall unsafe "nn_ctxstr_set_drv_output"+ c_nn_ctxstr_set_drv_output :: CCtxStrPtr -> Word16 -> Word32 -> Word32 -> Word32 -> IO ()++foreign import ccall unsafe "nn_ctxstr_set_all_outputs"+ c_nn_ctxstr_set_all_outputs :: CCtxStrPtr -> Word16 -> Word32 -> Word32 -> IO ()++foreign import ccall unsafe "nn_ctxstr_text"+ c_nn_ctxstr_text :: CCtxStrPtr -> IO Word32++foreign import ccall unsafe "nn_ctxstr_ctx_count"+ c_nn_ctxstr_ctx_count :: CCtxStrPtr -> IO Word16++foreign import ccall unsafe "nn_ctxstr_elem_tag"+ c_nn_ctxstr_elem_tag :: CCtxStrPtr -> Word16 -> IO Word8++foreign import ccall unsafe "nn_ctxstr_elem_hash"+ c_nn_ctxstr_elem_hash :: CCtxStrPtr -> Word16 -> IO Word32++foreign import ccall unsafe "nn_ctxstr_elem_name"+ c_nn_ctxstr_elem_name :: CCtxStrPtr -> Word16 -> IO Word32++foreign import ccall unsafe "nn_ctxstr_elem_output"+ c_nn_ctxstr_elem_output :: CCtxStrPtr -> Word16 -> IO Word32++-- ---------------------------------------------------------------------------+-- Lifecycle+-- ---------------------------------------------------------------------------++-- | Allocate a new context string with space for @ctxCount@ elements.+-- Elements are uninitialized — caller must fill via set functions.+cctxstrNew :: Word32 -> Word16 -> IO CCtxStrPtr+cctxstrNew = c_nn_ctxstr_new++-- | Free all tracked nn_ctxstr_t allocations (arena-style cleanup).+cctxstrFreeAll :: IO ()+cctxstrFreeAll = c_nn_ctxstr_free_all++-- ---------------------------------------------------------------------------+-- Element setters+-- ---------------------------------------------------------------------------++cctxstrSetPlain :: CCtxStrPtr -> Word16 -> Word32 -> Word32 -> IO ()+cctxstrSetPlain = c_nn_ctxstr_set_plain++cctxstrSetDrvOutput :: CCtxStrPtr -> Word16 -> Word32 -> Word32 -> Word32 -> IO ()+cctxstrSetDrvOutput = c_nn_ctxstr_set_drv_output++cctxstrSetAllOutputs :: CCtxStrPtr -> Word16 -> Word32 -> Word32 -> IO ()+cctxstrSetAllOutputs = c_nn_ctxstr_set_all_outputs++-- ---------------------------------------------------------------------------+-- Accessors+-- ---------------------------------------------------------------------------++cctxstrText :: CCtxStrPtr -> IO Word32+cctxstrText = c_nn_ctxstr_text++cctxstrCtxCount :: CCtxStrPtr -> IO Word16+cctxstrCtxCount = c_nn_ctxstr_ctx_count++cctxstrElemTag :: CCtxStrPtr -> Word16 -> IO Word8+cctxstrElemTag = c_nn_ctxstr_elem_tag++cctxstrElemHash :: CCtxStrPtr -> Word16 -> IO Word32+cctxstrElemHash = c_nn_ctxstr_elem_hash++cctxstrElemName :: CCtxStrPtr -> Word16 -> IO Word32+cctxstrElemName = c_nn_ctxstr_elem_name++cctxstrElemOutput :: CCtxStrPtr -> Word16 -> IO Word32+cctxstrElemOutput = c_nn_ctxstr_elem_output
+ src/Nix/Eval/CEnv.hs view
@@ -0,0 +1,220 @@+-- | C-backed evaluation environments.+--+-- Wraps @cbits/nn_env.c@ — arena-allocated @nn_env_t@ structs that+-- hold slot arrays, lazy scopes, parent pointers, and with-scopes.+-- All memory is freed in bulk via 'cenvDestroy' at evaluation end.+--+-- This moves Env records off the GHC heap. Each @nn_env_t@ is 40+-- bytes (arena-allocated, zero GC overhead), replacing the former+-- Haskell record (~80-100 bytes with GHC info pointers, Maybe+-- constructors, and list cons cells per env).+module Nix.Eval.CEnv+ ( -- * Opaque C type+ NnEnv,++ -- * Lifecycle+ cenvInit,+ cenvDestroy,++ -- * Slot allocation+ cenvAllocSlots,++ -- * Env constructors+ cenvEmpty,+ cenvNew,+ cenvFromSlots,+ cenvPushWith,+ cenvNewMinimal,++ -- * Accessors+ cenvSlots,+ cenvSlotCount,+ cenvLazyScope,+ cenvParent,+ cenvWithScopes,+ cenvWithCount,++ -- * Lookup+ cenvLookupResolved,+ cenvRootScope,++ -- * With-scopes array+ cenvAllocWithScopes,+ )+where++import Data.Word (Word16, Word32)+import Foreign.Ptr (Ptr)+import Nix.Eval.CThunk (CThunkPtr)++-- ---------------------------------------------------------------------------+-- Opaque C type (phantom — never constructed on the Haskell side)+-- ---------------------------------------------------------------------------++-- | Phantom type representing the C @nn_env_t@ struct.+data NnEnv++-- ---------------------------------------------------------------------------+-- FFI imports (all unsafe — no callbacks, fast operations)+-- ---------------------------------------------------------------------------++foreign import ccall unsafe "nn_env_init"+ c_nn_env_init :: IO ()++foreign import ccall unsafe "nn_env_destroy"+ c_nn_env_destroy :: IO ()++foreign import ccall unsafe "nn_env_alloc_slots"+ c_nn_env_alloc_slots :: Word32 -> IO (Ptr CThunkPtr)++foreign import ccall unsafe "nn_env_empty"+ c_nn_env_empty :: IO (Ptr NnEnv)++foreign import ccall unsafe "nn_env_new"+ c_nn_env_new ::+ Ptr CThunkPtr ->+ Word32 ->+ Ptr () ->+ Ptr NnEnv ->+ Ptr (Ptr ()) ->+ Word16 ->+ IO (Ptr NnEnv)++foreign import ccall unsafe "nn_env_from_slots"+ c_nn_env_from_slots ::+ Ptr CThunkPtr -> Word32 -> Ptr NnEnv -> IO (Ptr NnEnv)++foreign import ccall unsafe "nn_env_push_with"+ c_nn_env_push_with :: Ptr NnEnv -> Ptr () -> IO (Ptr NnEnv)++foreign import ccall unsafe "nn_env_new_minimal"+ c_nn_env_new_minimal :: Ptr CThunkPtr -> Word32 -> IO (Ptr NnEnv)++foreign import ccall unsafe "nn_env_slots"+ c_nn_env_slots :: Ptr NnEnv -> IO (Ptr CThunkPtr)++foreign import ccall unsafe "nn_env_slot_count"+ c_nn_env_slot_count :: Ptr NnEnv -> IO Word32++foreign import ccall unsafe "nn_env_lazy_scope"+ c_nn_env_lazy_scope :: Ptr NnEnv -> IO (Ptr ())++foreign import ccall unsafe "nn_env_parent"+ c_nn_env_parent :: Ptr NnEnv -> IO (Ptr NnEnv)++foreign import ccall unsafe "nn_env_with_scopes"+ c_nn_env_with_scopes :: Ptr NnEnv -> IO (Ptr (Ptr ()))++foreign import ccall unsafe "nn_env_with_count"+ c_nn_env_with_count :: Ptr NnEnv -> IO Word16++foreign import ccall unsafe "nn_env_lookup_resolved"+ c_nn_env_lookup_resolved :: Ptr NnEnv -> Int -> Int -> IO CThunkPtr++foreign import ccall unsafe "nn_env_root_scope"+ c_nn_env_root_scope :: Ptr NnEnv -> IO (Ptr ())++foreign import ccall unsafe "nn_env_alloc_with_scopes"+ c_nn_env_alloc_with_scopes :: Word16 -> IO (Ptr (Ptr ()))++-- ---------------------------------------------------------------------------+-- Lifecycle+-- ---------------------------------------------------------------------------++-- | Initialize the global env allocator. Call once before evaluation.+cenvInit :: IO ()+cenvInit = c_nn_env_init++-- | Destroy the global env allocator, freeing all page memory.+-- All env and slot array pointers become invalid after this call.+cenvDestroy :: IO ()+cenvDestroy = c_nn_env_destroy++-- ---------------------------------------------------------------------------+-- Slot allocation+-- ---------------------------------------------------------------------------++-- | Allocate a C array of @count@ 'CThunkPtr' slots. O(1) amortized.+-- Returns 'nullPtr' if count is 0. All slots are zero-initialized.+cenvAllocSlots :: Word32 -> IO (Ptr CThunkPtr)+cenvAllocSlots = c_nn_env_alloc_slots++-- ---------------------------------------------------------------------------+-- Env constructors+-- ---------------------------------------------------------------------------++-- | Return a pointer to the global empty env (all fields zero/NULL).+cenvEmpty :: IO (Ptr NnEnv)+cenvEmpty = c_nn_env_empty++-- | Full constructor: set all fields explicitly. Arena-allocated.+cenvNew ::+ Ptr CThunkPtr ->+ Word32 ->+ Ptr () ->+ Ptr NnEnv ->+ Ptr (Ptr ()) ->+ Word16 ->+ IO (Ptr NnEnv)+cenvNew = c_nn_env_new++-- | Child env with positional slots, inheriting parent's with-scopes.+cenvFromSlots :: Ptr CThunkPtr -> Word32 -> Ptr NnEnv -> IO (Ptr NnEnv)+cenvFromSlots = c_nn_env_from_slots++-- | Copy base env with a new with-scope prepended.+cenvPushWith :: Ptr NnEnv -> Ptr () -> IO (Ptr NnEnv)+cenvPushWith = c_nn_env_push_with++-- | Minimal env: slots only, no parent, no with-scopes.+cenvNewMinimal :: Ptr CThunkPtr -> Word32 -> IO (Ptr NnEnv)+cenvNewMinimal = c_nn_env_new_minimal++-- ---------------------------------------------------------------------------+-- Accessors+-- ---------------------------------------------------------------------------++-- | Read the slot array pointer from a C env.+cenvSlots :: Ptr NnEnv -> IO (Ptr CThunkPtr)+cenvSlots = c_nn_env_slots++-- | Read the slot count from a C env.+cenvSlotCount :: Ptr NnEnv -> IO Word32+cenvSlotCount = c_nn_env_slot_count++-- | Read the lazy scope pointer from a C env (NULL if none).+cenvLazyScope :: Ptr NnEnv -> IO (Ptr ())+cenvLazyScope = c_nn_env_lazy_scope++-- | Read the parent pointer from a C env (NULL if root).+cenvParent :: Ptr NnEnv -> IO (Ptr NnEnv)+cenvParent = c_nn_env_parent++-- | Read the with-scopes array pointer from a C env.+cenvWithScopes :: Ptr NnEnv -> IO (Ptr (Ptr ()))+cenvWithScopes = c_nn_env_with_scopes++-- | Read the with-scope count from a C env.+cenvWithCount :: Ptr NnEnv -> IO Word16+cenvWithCount = c_nn_env_with_count++-- ---------------------------------------------------------------------------+-- Lookup+-- ---------------------------------------------------------------------------++-- | Resolved variable lookup: walk @level@ parent hops, then read+-- @slots[idx]@. Single FFI call — O(level) in C.+cenvLookupResolved :: Ptr NnEnv -> Int -> Int -> IO CThunkPtr+cenvLookupResolved = c_nn_env_lookup_resolved++-- | Walk parent chain to root, return root's lazy scope (NULL if none).+cenvRootScope :: Ptr NnEnv -> IO (Ptr ())+cenvRootScope = c_nn_env_root_scope++-- ---------------------------------------------------------------------------+-- With-scopes array allocation+-- ---------------------------------------------------------------------------++-- | Allocate an arena array for with-scopes. Zero-initialized.+cenvAllocWithScopes :: Word16 -> IO (Ptr (Ptr ()))+cenvAllocWithScopes = c_nn_env_alloc_with_scopes
+ src/Nix/Eval/CLambda.hs view
@@ -0,0 +1,138 @@+-- | C-backed lambda closure structs via FFI.+--+-- Wraps @cbits/nn_lambda.c@ — a malloc'd struct holding the closure+-- environment (nn_env_t*), body bytecode index, and formal parameter+-- specification. Replaces StablePtr NixValue for VLambda (~25% of+-- remaining GHC heap after M6) with C-native tag 9 in the thunk system.+--+-- Lambda structs are tracked for bulk cleanup via 'clambdaFreeAll'+-- at evaluation end.+module Nix.Eval.CLambda+ ( -- * Opaque handle+ NnLambda,+ CLambdaPtr,++ -- * Lifecycle+ clambdaNew,+ clambdaSetEntry,+ clambdaFreeAll,++ -- * Accessors+ clambdaEnv,+ clambdaBody,+ clambdaFormalsType,+ clambdaNameSym,+ clambdaAllowExtra,+ clambdaFormalCount,+ clambdaEntryName,+ clambdaEntryHasDefault,+ clambdaEntryDefault,+ )+where++import Data.Word (Word16, Word32, Word8)+import Foreign.Ptr (Ptr)+import Nix.Eval.CEnv (NnEnv)++-- | Phantom type for C-side @nn_lambda_t@.+data NnLambda++-- | Pointer to a C-allocated lambda closure.+type CLambdaPtr = Ptr NnLambda++-- ---------------------------------------------------------------------------+-- FFI imports (all unsafe — no callbacks, fast data access)+-- ---------------------------------------------------------------------------++foreign import ccall unsafe "nn_lambda_new"+ c_nn_lambda_new :: Ptr NnEnv -> Word32 -> Word8 -> Word32 -> Word8 -> Word16 -> IO CLambdaPtr++foreign import ccall unsafe "nn_lambda_set_entry"+ c_nn_lambda_set_entry :: CLambdaPtr -> Word16 -> Word32 -> Word32 -> Word32 -> IO ()++foreign import ccall unsafe "nn_lambda_free_all"+ c_nn_lambda_free_all :: IO ()++foreign import ccall unsafe "nn_lambda_env"+ c_nn_lambda_env :: CLambdaPtr -> IO (Ptr NnEnv)++foreign import ccall unsafe "nn_lambda_body"+ c_nn_lambda_body :: CLambdaPtr -> IO Word32++foreign import ccall unsafe "nn_lambda_formals_type"+ c_nn_lambda_formals_type :: CLambdaPtr -> IO Word8++foreign import ccall unsafe "nn_lambda_name_sym"+ c_nn_lambda_name_sym :: CLambdaPtr -> IO Word32++foreign import ccall unsafe "nn_lambda_allow_extra"+ c_nn_lambda_allow_extra :: CLambdaPtr -> IO Word8++foreign import ccall unsafe "nn_lambda_formal_count"+ c_nn_lambda_formal_count :: CLambdaPtr -> IO Word16++foreign import ccall unsafe "nn_lambda_entry_name"+ c_nn_lambda_entry_name :: CLambdaPtr -> Word16 -> IO Word32++foreign import ccall unsafe "nn_lambda_entry_has_default"+ c_nn_lambda_entry_has_default :: CLambdaPtr -> Word16 -> IO Word32++foreign import ccall unsafe "nn_lambda_entry_default"+ c_nn_lambda_entry_default :: CLambdaPtr -> Word16 -> IO Word32++-- ---------------------------------------------------------------------------+-- Lifecycle+-- ---------------------------------------------------------------------------++-- | Allocate a new lambda closure with space for @formalCount@ entries.+-- Fill entries via 'clambdaSetEntry' after construction.+clambdaNew :: Ptr NnEnv -> Word32 -> Word8 -> Word32 -> Word8 -> Word16 -> IO CLambdaPtr+clambdaNew = c_nn_lambda_new++-- | Set a formal entry at the given index.+clambdaSetEntry :: CLambdaPtr -> Word16 -> Word32 -> Word32 -> Word32 -> IO ()+clambdaSetEntry = c_nn_lambda_set_entry++-- | Free all tracked lambda structs (arena-style cleanup).+clambdaFreeAll :: IO ()+clambdaFreeAll = c_nn_lambda_free_all++-- ---------------------------------------------------------------------------+-- Accessors+-- ---------------------------------------------------------------------------++-- | Read the closure environment pointer.+clambdaEnv :: CLambdaPtr -> IO (Ptr NnEnv)+clambdaEnv = c_nn_lambda_env++-- | Read the body bytecode index.+clambdaBody :: CLambdaPtr -> IO Word32+clambdaBody = c_nn_lambda_body++-- | Read the formals type (0=Name, 1=Set, 2=NamedSet).+clambdaFormalsType :: CLambdaPtr -> IO Word8+clambdaFormalsType = c_nn_lambda_formals_type++-- | Read the binding name symbol (for Name/NamedSet).+clambdaNameSym :: CLambdaPtr -> IO Word32+clambdaNameSym = c_nn_lambda_name_sym++-- | Read the ellipsis flag (1 if ... present).+clambdaAllowExtra :: CLambdaPtr -> IO Word8+clambdaAllowExtra = c_nn_lambda_allow_extra++-- | Read the number of formal entries.+clambdaFormalCount :: CLambdaPtr -> IO Word16+clambdaFormalCount = c_nn_lambda_formal_count++-- | Read a formal entry's name symbol.+clambdaEntryName :: CLambdaPtr -> Word16 -> IO Word32+clambdaEntryName = c_nn_lambda_entry_name++-- | Read whether a formal entry has a default.+clambdaEntryHasDefault :: CLambdaPtr -> Word16 -> IO Word32+clambdaEntryHasDefault = c_nn_lambda_entry_has_default++-- | Read a formal entry's default bytecode index.+clambdaEntryDefault :: CLambdaPtr -> Word16 -> IO Word32+clambdaEntryDefault = c_nn_lambda_entry_default
+ src/Nix/Eval/CList.hs view
@@ -0,0 +1,169 @@+-- | C-backed contiguous list of thunk pointers via FFI.+--+-- Wraps @cbits/nn_list.c@ — a flat array of @nn_thunk_t*@ pointers.+-- Replaces Haskell's @[Thunk]@ (linked-list cons cells, ~48 bytes per+-- element) with a contiguous C array (8 bytes per element).+--+-- Lists are immutable after construction: allocate with 'clistNew',+-- fill with 'clistSet', then read with 'clistGet'/'clistCount'.+-- All memory is freed in bulk via 'clistFreeAll' at evaluation end.+module Nix.Eval.CList+ ( -- * Opaque handle+ NnList,+ CListPtr,++ -- * CList newtype+ CList (..),+ emptyCList,+ clistFromThunks,+ clistThunks,+ clistLen,++ -- * Lifecycle+ clistNew,+ clistFreeAll,++ -- * Access+ clistCount,+ clistGet,+ clistSet,++ -- * Conversion+ thunkListToCList,+ clistToThunkList,+ )+where++import Data.Word (Word32)+import Foreign.Ptr (Ptr, nullPtr)+import Nix.Eval.CThunk (CThunkPtr)+import System.IO.Unsafe (unsafePerformIO)++-- | Phantom type for C-side @nn_list_t@.+data NnList++-- | Pointer to a C-allocated list.+type CListPtr = Ptr NnList++-- ---------------------------------------------------------------------------+-- FFI imports (all unsafe — no callbacks, fast data access)+-- ---------------------------------------------------------------------------++foreign import ccall unsafe "nn_list_new"+ c_nn_list_new :: Word32 -> IO CListPtr++foreign import ccall unsafe "nn_list_free_all"+ c_nn_list_free_all :: IO ()++foreign import ccall unsafe "nn_list_count"+ c_nn_list_count :: CListPtr -> IO Word32++foreign import ccall unsafe "nn_list_get"+ c_nn_list_get :: CListPtr -> Word32 -> IO CThunkPtr++foreign import ccall unsafe "nn_list_set"+ c_nn_list_set :: CListPtr -> Word32 -> CThunkPtr -> IO ()++-- ---------------------------------------------------------------------------+-- Lifecycle+-- ---------------------------------------------------------------------------++-- | Allocate a new list with space for @count@ thunk pointers.+-- Returns 'nullPtr' if count is 0.+clistNew :: Word32 -> IO CListPtr+clistNew = c_nn_list_new++-- | Free all tracked list headers (arena-style cleanup).+-- Items arrays are freed by the env page allocator.+clistFreeAll :: IO ()+clistFreeAll = c_nn_list_free_all++-- ---------------------------------------------------------------------------+-- Access+-- ---------------------------------------------------------------------------++-- | Number of elements in the list.+clistCount :: CListPtr -> IO Word32+clistCount = c_nn_list_count++-- | Get the thunk pointer at the given index.+clistGet :: CListPtr -> Word32 -> IO CThunkPtr+clistGet = c_nn_list_get++-- | Set the thunk pointer at the given index (for construction).+clistSet :: CListPtr -> Word32 -> CThunkPtr -> IO ()+clistSet = c_nn_list_set++-- ---------------------------------------------------------------------------+-- Conversion helpers+-- ---------------------------------------------------------------------------++-- | Convert a Haskell list of 'CThunkPtr' to a C list.+-- Allocates a new nn_list_t and fills it with the thunk pointers.+-- Returns 'nullPtr' for empty lists.+thunkListToCList :: [CThunkPtr] -> IO CListPtr+thunkListToCList [] = pure nullPtr+thunkListToCList ptrs = do+ let n = fromIntegral (length ptrs)+ clist <- clistNew n+ if clist == nullPtr+ then pure nullPtr+ else do+ fillList clist 0 ptrs+ pure clist+ where+ fillList _ _ [] = pure ()+ fillList cl !i (p : ps) = do+ clistSet cl i p+ fillList cl (i + 1) ps++-- | Convert a C list back to a Haskell list of 'CThunkPtr'.+-- Returns @[]@ for 'nullPtr'.+clistToThunkList :: CListPtr -> IO [CThunkPtr]+clistToThunkList cl+ | cl == nullPtr = pure []+ | otherwise = do+ n <- clistCount cl+ mapM (clistGet cl) [0 .. n - 1]++-- ---------------------------------------------------------------------------+-- CList newtype (wraps CListPtr for use in NixValue ADT)+-- ---------------------------------------------------------------------------++-- | Opaque wrapper around a C-backed list. Used as the payload of+-- @VList@ in the 'NixValue' ADT. The underlying @nn_list_t*@ is+-- arena-allocated and valid until evaluation end.+newtype CList = CList {unCList :: CListPtr}++-- | Pointer equality — two CLists are the same if they point to the+-- same C struct. Deep equality is handled by the evaluator.+instance Eq CList where+ CList a == CList b = a == b++instance Show CList where+ show (CList p)+ | p == nullPtr = "<list 0>"+ | otherwise =+ let n = unsafePerformIO (clistCount p)+ in "<list " ++ show n ++ ">"++-- | The empty CList (null pointer, zero elements).+emptyCList :: CList+emptyCList = CList nullPtr++-- | Convert a Haskell list of 'CThunkPtr' to a 'CList'.+-- Uses 'unsafePerformIO' — safe because allocation is idempotent+-- per call (no shared mutable state beyond the tracking array).+{-# NOINLINE clistFromThunks #-}+clistFromThunks :: [CThunkPtr] -> CList+clistFromThunks ptrs = CList (unsafePerformIO (thunkListToCList ptrs))++-- | Convert a 'CList' back to a Haskell list of 'CThunkPtr'.+clistThunks :: CList -> [CThunkPtr]+clistThunks (CList p) = unsafePerformIO (clistToThunkList p)++-- | Number of elements in the list.+clistLen :: CList -> Int+clistLen (CList p)+ | p == nullPtr = 0+ | otherwise = fromIntegral (unsafePerformIO (clistCount p))
+ src/Nix/Eval/CThunk.hs view
@@ -0,0 +1,435 @@+-- | C-backed thunk memoization cells via FFI.+--+-- Wraps @cbits/nn_thunk.c@ — an arena-allocated mutable cell that+-- holds either a pending expression (StablePtr to (Expr, Env)) or a+-- computed value (StablePtr to NixValue). Replaces Haskell's+-- @IORef ThunkCell@ (~56 bytes on GHC heap, all GC-traced) with a+-- 16-byte C struct in an arena (invisible to GHC GC).+--+-- Includes blackhole detection: a thunk being forced is marked+-- BLACKHOLE; if forced again before completion, infinite recursion+-- is detected (matching C++ Nix behavior).+--+-- @+-- cthunkInit 0+-- ptr <- cthunkNew pendingStablePtr+-- cthunkState ptr -- 0 (PENDING)+-- cthunkMarkBlackhole ptr -- 1 (success)+-- cthunkSetComputed ptr valueStablePtr -- returns old pending ptr+-- cthunkState ptr -- 1 (COMPUTED)+-- cthunkDestroy+-- @+module Nix.Eval.CThunk+ ( -- * Opaque handle+ NnThunk,+ CThunkPtr,++ -- * Lifecycle+ cthunkInit,+ cthunkDestroy,++ -- * Allocation+ cthunkNew,+ cthunkNewBc,+ cthunkNewComputed,+ cthunkNewComputedInt,+ cthunkNewComputedFloat,+ cthunkNewComputedBool,+ cthunkNewComputedNull,+ cthunkNewComputedStr,+ cthunkNewComputedPath,+ cthunkNewComputedList,+ cthunkNewComputedAttrs,+ cthunkNewComputedCtxStr,+ cthunkNewComputedLambda,++ -- * State queries+ cthunkState,+ cthunkPayload,+ cthunkValueTag,+ cthunkGetBcIdx,+ cthunkSetPayload,+ cthunkGetInt,+ cthunkGetFloat,+ cthunkGetBool,+ cthunkGetStr,+ cthunkGetPath,+ cthunkGetList,+ cthunkGetAttrs,+ cthunkGetCtxStr,+ cthunkGetLambda,++ -- * State transitions+ cthunkMarkBlackhole,+ cthunkSetComputed,+ cthunkSetComputedInt,+ cthunkSetComputedFloat,+ cthunkSetComputedBool,+ cthunkSetComputedNull,+ cthunkSetComputedStr,+ cthunkSetComputedPath,+ cthunkSetComputedList,+ cthunkSetComputedAttrs,+ cthunkSetComputedCtxStr,+ cthunkSetComputedLambda,++ -- * Arena diagnostics / cleanup+ cthunkCount,+ cthunkGet,+ )+where++import Data.Int (Int64)+import Data.Word (Word32, Word8)+import Foreign.C.Types (CDouble (..))+import Foreign.Ptr (Ptr, nullPtr)++-- | Phantom type for C-side @nn_thunk_t@.+data NnThunk++-- | Pointer to a C-allocated thunk (lives in arena).+type CThunkPtr = Ptr NnThunk++-- ---------------------------------------------------------------------------+-- FFI imports (all unsafe — no callbacks, fast data access)+-- ---------------------------------------------------------------------------++foreign import ccall unsafe "nn_thunk_init"+ c_nn_thunk_init :: Word32 -> IO ()++foreign import ccall unsafe "nn_thunk_destroy"+ c_nn_thunk_destroy :: IO ()++foreign import ccall unsafe "nn_thunk_new"+ c_nn_thunk_new :: Ptr () -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_new_bc"+ c_nn_thunk_new_bc :: Word32 -> Ptr () -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_get_bc_idx"+ c_nn_thunk_get_bc_idx :: CThunkPtr -> IO Word32++foreign import ccall unsafe "nn_thunk_set_payload"+ c_nn_thunk_set_payload :: CThunkPtr -> Ptr () -> IO ()++foreign import ccall unsafe "nn_thunk_new_computed"+ c_nn_thunk_new_computed :: Ptr () -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_new_computed_int"+ c_nn_thunk_new_computed_int :: Int64 -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_new_computed_float"+ c_nn_thunk_new_computed_float :: CDouble -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_new_computed_bool"+ c_nn_thunk_new_computed_bool :: Word8 -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_new_computed_null"+ c_nn_thunk_new_computed_null :: IO CThunkPtr++foreign import ccall unsafe "nn_thunk_new_computed_str"+ c_nn_thunk_new_computed_str :: Word32 -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_new_computed_path"+ c_nn_thunk_new_computed_path :: Word32 -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_new_computed_list"+ c_nn_thunk_new_computed_list :: Ptr () -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_new_computed_attrs"+ c_nn_thunk_new_computed_attrs :: Ptr () -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_new_computed_ctxstr"+ c_nn_thunk_new_computed_ctxstr :: Ptr () -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_new_computed_lambda"+ c_nn_thunk_new_computed_lambda :: Ptr () -> IO CThunkPtr++foreign import ccall unsafe "nn_thunk_state"+ c_nn_thunk_state :: CThunkPtr -> IO Word8++foreign import ccall unsafe "nn_thunk_payload"+ c_nn_thunk_payload :: CThunkPtr -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_value_tag"+ c_nn_thunk_value_tag :: CThunkPtr -> IO Word8++foreign import ccall unsafe "nn_thunk_get_int"+ c_nn_thunk_get_int :: CThunkPtr -> IO Int64++foreign import ccall unsafe "nn_thunk_get_float"+ c_nn_thunk_get_float :: CThunkPtr -> IO CDouble++foreign import ccall unsafe "nn_thunk_get_bool"+ c_nn_thunk_get_bool :: CThunkPtr -> IO Word8++foreign import ccall unsafe "nn_thunk_get_str"+ c_nn_thunk_get_str :: CThunkPtr -> IO Word32++foreign import ccall unsafe "nn_thunk_get_path"+ c_nn_thunk_get_path :: CThunkPtr -> IO Word32++foreign import ccall unsafe "nn_thunk_get_list"+ c_nn_thunk_get_list :: CThunkPtr -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_get_attrs"+ c_nn_thunk_get_attrs :: CThunkPtr -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_get_ctxstr"+ c_nn_thunk_get_ctxstr :: CThunkPtr -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_get_lambda"+ c_nn_thunk_get_lambda :: CThunkPtr -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_mark_blackhole"+ c_nn_thunk_mark_blackhole :: CThunkPtr -> IO Int++foreign import ccall unsafe "nn_thunk_set_computed"+ c_nn_thunk_set_computed :: CThunkPtr -> Ptr () -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_set_computed_int"+ c_nn_thunk_set_computed_int :: CThunkPtr -> Int64 -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_set_computed_float"+ c_nn_thunk_set_computed_float :: CThunkPtr -> CDouble -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_set_computed_bool"+ c_nn_thunk_set_computed_bool :: CThunkPtr -> Word8 -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_set_computed_null"+ c_nn_thunk_set_computed_null :: CThunkPtr -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_set_computed_str"+ c_nn_thunk_set_computed_str :: CThunkPtr -> Word32 -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_set_computed_path"+ c_nn_thunk_set_computed_path :: CThunkPtr -> Word32 -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_set_computed_list"+ c_nn_thunk_set_computed_list :: CThunkPtr -> Ptr () -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_set_computed_attrs"+ c_nn_thunk_set_computed_attrs :: CThunkPtr -> Ptr () -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_set_computed_ctxstr"+ c_nn_thunk_set_computed_ctxstr :: CThunkPtr -> Ptr () -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_set_computed_lambda"+ c_nn_thunk_set_computed_lambda :: CThunkPtr -> Ptr () -> IO (Ptr ())++foreign import ccall unsafe "nn_thunk_count"+ c_nn_thunk_count :: IO Word32++foreign import ccall unsafe "nn_thunk_get"+ c_nn_thunk_get :: Word32 -> IO CThunkPtr++-- ---------------------------------------------------------------------------+-- Lifecycle+-- ---------------------------------------------------------------------------++-- | Initialize the global thunk arena. Call once before evaluation.+-- @capacity@ is a hint for thunks per block (0 uses default: 65536).+cthunkInit :: Word32 -> IO ()+cthunkInit = c_nn_thunk_init++-- | Destroy the global thunk arena, freeing all C-side memory.+-- Caller must free all payload StablePtrs first.+-- All 'CThunkPtr' values become invalid after this call.+cthunkDestroy :: IO ()+cthunkDestroy = c_nn_thunk_destroy++-- ---------------------------------------------------------------------------+-- Allocation+-- ---------------------------------------------------------------------------++-- | Allocate a new PENDING thunk from the arena (legacy StablePtr path).+-- The payload is an opaque pointer (StablePtr cast to Ptr ()).+cthunkNew :: Ptr () -> IO CThunkPtr+cthunkNew = c_nn_thunk_new++-- | Allocate a new PENDING thunk with a bytecode index + C env pointer.+-- No Haskell heap references — zero GC pressure for pending thunks.+cthunkNewBc :: Word32 -> Ptr () -> IO CThunkPtr+cthunkNewBc = c_nn_thunk_new_bc++-- | Read the bytecode index from a PENDING thunk.+cthunkGetBcIdx :: CThunkPtr -> IO Word32+cthunkGetBcIdx = c_nn_thunk_get_bc_idx++-- | Set the payload of a thunk (deferred env fixup for knot-tying).+cthunkSetPayload :: CThunkPtr -> Ptr () -> IO ()+cthunkSetPayload = c_nn_thunk_set_payload++-- | Allocate a new pre-COMPUTED thunk with StablePtr payload (complex types).+cthunkNewComputed :: Ptr () -> IO CThunkPtr+cthunkNewComputed = c_nn_thunk_new_computed++-- | Allocate a pre-COMPUTED thunk with an inline int64 (no StablePtr).+cthunkNewComputedInt :: Int64 -> IO CThunkPtr+cthunkNewComputedInt = c_nn_thunk_new_computed_int++-- | Allocate a pre-COMPUTED thunk with an inline double (no StablePtr).+cthunkNewComputedFloat :: Double -> IO CThunkPtr+cthunkNewComputedFloat d = c_nn_thunk_new_computed_float (CDouble d)++-- | Allocate a pre-COMPUTED thunk with an inline bool (no StablePtr).+cthunkNewComputedBool :: Word8 -> IO CThunkPtr+cthunkNewComputedBool = c_nn_thunk_new_computed_bool++-- | Allocate a pre-COMPUTED thunk with null value (no StablePtr).+cthunkNewComputedNull :: IO CThunkPtr+cthunkNewComputedNull = c_nn_thunk_new_computed_null++-- | Allocate a pre-COMPUTED thunk with an interned string symbol (no StablePtr).+-- For context-free strings only (tag 4).+cthunkNewComputedStr :: Word32 -> IO CThunkPtr+cthunkNewComputedStr = c_nn_thunk_new_computed_str++-- | Allocate a pre-COMPUTED thunk with an interned path symbol (no StablePtr).+cthunkNewComputedPath :: Word32 -> IO CThunkPtr+cthunkNewComputedPath = c_nn_thunk_new_computed_path++-- | Allocate a pre-COMPUTED thunk with a CList pointer (no StablePtr).+cthunkNewComputedList :: Ptr () -> IO CThunkPtr+cthunkNewComputedList = c_nn_thunk_new_computed_list++-- | Allocate a pre-COMPUTED thunk with a CAttrSet pointer (no StablePtr).+cthunkNewComputedAttrs :: Ptr () -> IO CThunkPtr+cthunkNewComputedAttrs = c_nn_thunk_new_computed_attrs++-- | Allocate a pre-COMPUTED thunk with a CCtxStr pointer (no StablePtr).+-- For strings with non-empty context (tag 8).+cthunkNewComputedCtxStr :: Ptr () -> IO CThunkPtr+cthunkNewComputedCtxStr = c_nn_thunk_new_computed_ctxstr++-- | Allocate a pre-COMPUTED thunk with a CLambda pointer (no StablePtr).+-- For lambda closures (tag 9).+cthunkNewComputedLambda :: Ptr () -> IO CThunkPtr+cthunkNewComputedLambda = c_nn_thunk_new_computed_lambda++-- ---------------------------------------------------------------------------+-- State queries+-- ---------------------------------------------------------------------------++-- | Read the current state: 0 = PENDING, 1 = COMPUTED, 2 = BLACKHOLE.+cthunkState :: CThunkPtr -> IO Word8+cthunkState = c_nn_thunk_state++-- | Read the payload pointer. Returns 'nullPtr' for NULL payloads.+cthunkPayload :: CThunkPtr -> IO (Ptr ())+cthunkPayload ptr = do+ p <- c_nn_thunk_payload ptr+ pure (if p == nullPtr then nullPtr else p)++-- | Read the value tag (valid when state == COMPUTED).+-- 0=INT, 1=FLOAT, 2=BOOL, 3=NULL, 255=PTR (StablePtr).+cthunkValueTag :: CThunkPtr -> IO Word8+cthunkValueTag = c_nn_thunk_value_tag++-- | Read an inline int64 from a COMPUTED thunk (val_tag == 0).+cthunkGetInt :: CThunkPtr -> IO Int64+cthunkGetInt = c_nn_thunk_get_int++-- | Read an inline double from a COMPUTED thunk (val_tag == 1).+cthunkGetFloat :: CThunkPtr -> IO Double+cthunkGetFloat ptr = do+ CDouble d <- c_nn_thunk_get_float ptr+ pure d++-- | Read an inline bool from a COMPUTED thunk (val_tag == 2).+cthunkGetBool :: CThunkPtr -> IO Word8+cthunkGetBool = c_nn_thunk_get_bool++-- | Read an interned string symbol from a COMPUTED thunk (val_tag == 4).+cthunkGetStr :: CThunkPtr -> IO Word32+cthunkGetStr = c_nn_thunk_get_str++-- | Read an interned path symbol from a COMPUTED thunk (val_tag == 5).+cthunkGetPath :: CThunkPtr -> IO Word32+cthunkGetPath = c_nn_thunk_get_path++-- | Read a CList pointer from a COMPUTED thunk (val_tag == 6).+cthunkGetList :: CThunkPtr -> IO (Ptr ())+cthunkGetList = c_nn_thunk_get_list++-- | Read a CAttrSet pointer from a COMPUTED thunk (val_tag == 7).+cthunkGetAttrs :: CThunkPtr -> IO (Ptr ())+cthunkGetAttrs = c_nn_thunk_get_attrs++-- | Read a CCtxStr pointer from a COMPUTED thunk (val_tag == 8).+cthunkGetCtxStr :: CThunkPtr -> IO (Ptr ())+cthunkGetCtxStr = c_nn_thunk_get_ctxstr++-- | Read a CLambda pointer from a COMPUTED thunk (val_tag == 9).+cthunkGetLambda :: CThunkPtr -> IO (Ptr ())+cthunkGetLambda = c_nn_thunk_get_lambda++-- ---------------------------------------------------------------------------+-- State transitions+-- ---------------------------------------------------------------------------++-- | Mark a PENDING thunk as BLACKHOLE (being evaluated).+-- Returns 'True' on success, 'False' if not PENDING.+cthunkMarkBlackhole :: CThunkPtr -> IO Bool+cthunkMarkBlackhole ptr = do+ result <- c_nn_thunk_mark_blackhole ptr+ pure (result /= 0)++-- | Set a BLACKHOLE thunk to COMPUTED with a StablePtr value (complex types).+-- Returns the old payload (pending StablePtr to free), or 'nullPtr'+-- if the thunk was not in BLACKHOLE state.+cthunkSetComputed :: CThunkPtr -> Ptr () -> IO (Ptr ())+cthunkSetComputed = c_nn_thunk_set_computed++-- | Set a BLACKHOLE thunk to COMPUTED with an inline int64 (no StablePtr).+cthunkSetComputedInt :: CThunkPtr -> Int64 -> IO (Ptr ())+cthunkSetComputedInt = c_nn_thunk_set_computed_int++-- | Set a BLACKHOLE thunk to COMPUTED with an inline double (no StablePtr).+cthunkSetComputedFloat :: CThunkPtr -> Double -> IO (Ptr ())+cthunkSetComputedFloat ptr d = c_nn_thunk_set_computed_float ptr (CDouble d)++-- | Set a BLACKHOLE thunk to COMPUTED with an inline bool (no StablePtr).+cthunkSetComputedBool :: CThunkPtr -> Word8 -> IO (Ptr ())+cthunkSetComputedBool = c_nn_thunk_set_computed_bool++-- | Set a BLACKHOLE thunk to COMPUTED with null value (no StablePtr).+cthunkSetComputedNull :: CThunkPtr -> IO (Ptr ())+cthunkSetComputedNull = c_nn_thunk_set_computed_null++-- | Set a non-COMPUTED thunk to COMPUTED with an interned string symbol.+cthunkSetComputedStr :: CThunkPtr -> Word32 -> IO (Ptr ())+cthunkSetComputedStr = c_nn_thunk_set_computed_str++-- | Set a non-COMPUTED thunk to COMPUTED with an interned path symbol.+cthunkSetComputedPath :: CThunkPtr -> Word32 -> IO (Ptr ())+cthunkSetComputedPath = c_nn_thunk_set_computed_path++-- | Set a non-COMPUTED thunk to COMPUTED with a CList pointer.+cthunkSetComputedList :: CThunkPtr -> Ptr () -> IO (Ptr ())+cthunkSetComputedList = c_nn_thunk_set_computed_list++-- | Set a non-COMPUTED thunk to COMPUTED with a CAttrSet pointer.+cthunkSetComputedAttrs :: CThunkPtr -> Ptr () -> IO (Ptr ())+cthunkSetComputedAttrs = c_nn_thunk_set_computed_attrs++-- | Set a non-COMPUTED thunk to COMPUTED with a CCtxStr pointer.+cthunkSetComputedCtxStr :: CThunkPtr -> Ptr () -> IO (Ptr ())+cthunkSetComputedCtxStr = c_nn_thunk_set_computed_ctxstr++-- | Set a non-COMPUTED thunk to COMPUTED with a CLambda pointer.+cthunkSetComputedLambda :: CThunkPtr -> Ptr () -> IO (Ptr ())+cthunkSetComputedLambda = c_nn_thunk_set_computed_lambda++-- ---------------------------------------------------------------------------+-- Arena diagnostics / cleanup+-- ---------------------------------------------------------------------------++-- | Total thunks allocated in the arena.+cthunkCount :: IO Word32+cthunkCount = c_nn_thunk_count++-- | Get a thunk by global index (for cleanup iteration).+-- Returns 'nullPtr' if index is out of range.+cthunkGet :: Word32 -> IO CThunkPtr+cthunkGet = c_nn_thunk_get
+ src/Nix/Eval/Compile.hs view
@@ -0,0 +1,583 @@+-- | Compile Nix 'Expr' AST trees to flat C bytecode.+--+-- Post-order traversal: children are compiled before parents, so child+-- bytecode indices are always less than parent indices. Variable-length+-- data (string parts, bindings, formals, capture info, attr paths) goes+-- into a separate @uint32_t[]@ data buffer, referenced by offset from+-- instructions.+--+-- Must be called after 'cbcInit' and 'symbolInit'.+module Nix.Eval.Compile+ ( compileExpr,+ compileFormalsToEval,+ decodeBcFormals,+ decodeBcCaptureInfo,+ BcBinding (..),+ BcAttrKey (..),+ decodeBcBindings,+ reassembleInt64,+ reassembleDouble,+ )+where++import Data.Bits (shiftL, shiftR, (.&.), (.|.))+import Data.Int (Int64)+import Data.Text (Text)+import Data.Word (Word16, Word32, Word64, Word8)+import GHC.Float (castDoubleToWord64, castWord64ToDouble)+import Nix.Eval.CBytecode+ ( attrkeyDynamic,+ attrkeyStatic,+ binaryAdd,+ binaryAnd,+ binaryConcat,+ binaryDiv,+ binaryEq,+ binaryGt,+ binaryGte,+ binaryImpl,+ binaryLt,+ binaryLte,+ binaryMul,+ binaryNeq,+ binaryOr,+ binarySub,+ binaryUpdate,+ bindInherit,+ bindNamed,+ captureNone,+ captureSlots,+ captureWithScopes,+ cbcData,+ cbcEmit,+ cbcEmitData,+ formalName,+ formalNamedSet,+ formalSet,+ opApp,+ opAssert,+ opAttrs,+ opBinary,+ opHasAttr,+ opIf,+ opIndStr,+ opLambda,+ opLet,+ opList,+ opLitBool,+ opLitFloat,+ opLitInt,+ opLitNull,+ opLitPath,+ opLitUri,+ opResolvedVar,+ opSearchPath,+ opSelect,+ opStr,+ opUnary,+ opVar,+ opWith,+ opWithVar,+ strpartInterp,+ strpartLit,+ unaryNegate,+ unaryNot,+ )+import Nix.Eval.EvalFormals (EvalFormal (..), EvalFormals (..))+import Nix.Eval.Symbol (Symbol (..), symbolIntern, symbolText)+import Nix.Expr.Types+ ( AttrKey (..),+ BinaryOp (..),+ Binding (..),+ CaptureInfo (..),+ Expr (..),+ Formal (..),+ Formals (..),+ NixAtom (..),+ StringPart (..),+ UnaryOp (..),+ )++-- | Compile an 'Expr' tree to bytecode, returning the root instruction index.+-- The bytecode is stored in the global @nn_bytecode@ arena.+compileExpr :: Expr -> IO Word32+compileExpr = go+ where+ go :: Expr -> IO Word32+ go (ELit atom) = compileLit atom+ go (EStr parts) = compileStringParts opStr parts+ go (EIndStr parts) = compileStringParts opIndStr parts+ go (EVar name) = compileSymbolOp opVar name+ go (EWithVar name) = compileSymbolOp opWithVar name+ go (EResolvedVar level idx) =+ cbcEmit opResolvedVar 0 0 (fi level) (fi idx) 0+ go (EAttrs isRec bindings captureInfo) =+ compileAttrs isRec bindings captureInfo+ go (EList exprs) = compileList exprs+ go (ESelect target path defExpr) =+ compileSelect target path defExpr+ go (EHasAttr target path) = compileHasAttr target path+ go (EApp func arg) = do+ funcIdx <- go func+ argIdx <- go arg+ cbcEmit opApp 0 0 funcIdx argIdx 0+ go (ELambda formals body captureInfo) =+ compileLambda formals body captureInfo+ go (ELet bindings body captureInfo) =+ compileLet bindings body captureInfo+ go (EIf cond thenExpr elseExpr) = do+ condIdx <- go cond+ thenIdx <- go thenExpr+ elseIdx <- go elseExpr+ cbcEmit opIf 0 0 condIdx thenIdx elseIdx+ go (EWith scope body) = do+ scopeIdx <- go scope+ bodyIdx <- go body+ cbcEmit opWith 0 0 scopeIdx bodyIdx 0+ go (EAssert cond body) = do+ condIdx <- go cond+ bodyIdx <- go body+ cbcEmit opAssert 0 0 condIdx bodyIdx 0+ go (EUnary op operand) = do+ operandIdx <- go operand+ cbcEmit opUnary (encodeUnaryOp op) 0 operandIdx 0 0+ go (EBinary op left right) = do+ leftIdx <- go left+ rightIdx <- go right+ cbcEmit opBinary (encodeBinaryOp op) 0 leftIdx rightIdx 0+ go (ESearchPath name) = compileSymbolOp opSearchPath name++ -- -----------------------------------------------------------------+ -- Literals+ -- -----------------------------------------------------------------++ compileLit :: NixAtom -> IO Word32+ compileLit (NixInt n) =+ let (lo, hi) = splitInt64 n+ in cbcEmit opLitInt 0 0 lo hi 0+ compileLit (NixFloat d) =+ let (lo, hi) = splitDouble d+ in cbcEmit opLitFloat 0 0 lo hi 0+ compileLit (NixBool b) =+ cbcEmit opLitBool 0 (if b then 1 else 0) 0 0 0+ compileLit NixNull =+ cbcEmit opLitNull 0 0 0 0 0+ compileLit (NixUri u) = compileSymbolOp opLitUri u+ compileLit (NixPath p) = compileSymbolOp opLitPath p++ -- \| Emit an instruction whose only operand is an interned symbol.+ compileSymbolOp :: Word8 -> Text -> IO Word32+ compileSymbolOp op name = do+ Symbol sym <- symbolIntern name+ cbcEmit op 0 0 sym 0 0++ -- -----------------------------------------------------------------+ -- Strings (EStr / EIndStr)+ -- -----------------------------------------------------------------++ compileStringParts :: Word8 -> [StringPart] -> IO Word32+ compileStringParts op parts = do+ compiled <- mapM compileOnePart parts+ dataOff <- emitPairs compiled+ cbcEmit op 0 (fi (length parts)) dataOff 0 0++ compileOnePart :: StringPart -> IO (Word32, Word32)+ compileOnePart (StrLit t) = do+ Symbol sym <- symbolIntern t+ pure (strpartLit, sym)+ compileOnePart (StrInterp expr) = do+ idx <- go expr+ pure (strpartInterp, idx)++ -- -----------------------------------------------------------------+ -- Attribute sets (EAttrs)+ -- -----------------------------------------------------------------++ compileAttrs :: Bool -> [Binding] -> CaptureInfo -> IO Word32+ compileAttrs isRec bindings captureInfo = do+ dataOff <- compileBindings bindings+ capOff <- compileCaptureInfo captureInfo+ cbcEmit opAttrs (if isRec then 1 else 0) (fi (length bindings)) dataOff capOff 0++ -- -----------------------------------------------------------------+ -- Lists (EList)+ -- -----------------------------------------------------------------++ compileList :: [Expr] -> IO Word32+ compileList exprs = do+ childIndices <- mapM go exprs+ dataOff <- emitWordList childIndices+ cbcEmit opList 0 (fi (length exprs)) dataOff 0 0++ -- -----------------------------------------------------------------+ -- Select / HasAttr+ -- -----------------------------------------------------------------++ compileSelect :: Expr -> [AttrKey] -> Maybe Expr -> IO Word32+ compileSelect target path defExpr = do+ targetIdx <- go target+ defIdx <- case defExpr of+ Nothing -> pure 0+ Just d -> go d+ (pathOff, pathLen) <- compileAttrPath path+ let hasDef = case defExpr of Nothing -> 0; Just _ -> 1+ cbcEmit opSelect hasDef pathLen targetIdx pathOff defIdx++ compileHasAttr :: Expr -> [AttrKey] -> IO Word32+ compileHasAttr target path = do+ targetIdx <- go target+ (pathOff, pathLen) <- compileAttrPath path+ cbcEmit opHasAttr 0 pathLen targetIdx pathOff 0++ compileAttrPath :: [AttrKey] -> IO (Word32, Word16)+ compileAttrPath path = do+ compiled <- mapM compileAttrKey path+ off <- emitPairs compiled+ pure (off, fi (length path))++ compileAttrKey :: AttrKey -> IO (Word32, Word32)+ compileAttrKey (StaticKey name) = do+ Symbol sym <- symbolIntern name+ pure (attrkeyStatic, sym)+ compileAttrKey (DynamicKey expr) = do+ idx <- go expr+ pure (attrkeyDynamic, idx)++ -- -----------------------------------------------------------------+ -- Lambda (ELambda)+ -- -----------------------------------------------------------------++ compileLambda :: Formals -> Expr -> CaptureInfo -> IO Word32+ compileLambda formals body captureInfo = do+ bodyIdx <- go body+ formalsOff <- compileFormals formals+ capOff <- compileCaptureInfo captureInfo+ cbcEmit opLambda (encodeFormalType formals) 0 formalsOff bodyIdx capOff++ compileFormals :: Formals -> IO Word32+ compileFormals (FormalName name) = do+ Symbol sym <- symbolIntern name+ emitWordList [sym]+ compileFormals (FormalSet formals ellipsis) = do+ formalWords <- compileFormalEntries formals+ emitWordList $+ [fi (length formals), if ellipsis then 1 else 0]+ ++ formalWords+ compileFormals (FormalNamedSet name formals ellipsis) = do+ Symbol nameSym <- symbolIntern name+ formalWords <- compileFormalEntries formals+ emitWordList $+ [nameSym, fi (length formals), if ellipsis then 1 else 0]+ ++ formalWords++ compileFormalEntries :: [Formal] -> IO [Word32]+ compileFormalEntries = fmap concat . mapM compileFormalEntry++ compileFormalEntry :: Formal -> IO [Word32]+ compileFormalEntry (Formal name defExpr) = do+ Symbol nameSym <- symbolIntern name+ case defExpr of+ Nothing -> pure [nameSym, 0, 0]+ Just d -> do+ defIdx <- go d+ pure [nameSym, 1, defIdx]++ -- -----------------------------------------------------------------+ -- Let (ELet)+ -- -----------------------------------------------------------------++ compileLet :: [Binding] -> Expr -> CaptureInfo -> IO Word32+ compileLet bindings body captureInfo = do+ bodyIdx <- go body+ dataOff <- compileBindings bindings+ capOff <- compileCaptureInfo captureInfo+ cbcEmit opLet 0 (fi (length bindings)) dataOff bodyIdx capOff++ -- -----------------------------------------------------------------+ -- Bindings (shared by EAttrs and ELet)+ -- -----------------------------------------------------------------++ compileBindings :: [Binding] -> IO Word32+ compileBindings bindings = do+ allWords <- mapM compileOneBinding bindings+ emitWordList (concat allWords)++ compileOneBinding :: Binding -> IO [Word32]+ compileOneBinding (NamedBinding path expr) = do+ compiledKeys <- mapM compileAttrKey path+ valIdx <- go expr+ pure $+ [bindNamed, fi (length path)]+ ++ concatMap (\(t, v) -> [t, v]) compiledKeys+ ++ [valIdx]+ compileOneBinding (Inherit maybeFrom names) = do+ (hasFrom, fromIdx) <- case maybeFrom of+ Nothing -> pure (0 :: Word32, 0 :: Word32)+ Just fromExpr -> do+ idx <- go fromExpr+ pure (1, idx)+ syms <- mapM internName names+ pure $+ [bindInherit, hasFrom, fromIdx, fi (length names)]+ ++ syms++ internName :: Text -> IO Word32+ internName name = do+ Symbol sym <- symbolIntern name+ pure sym++ -- -----------------------------------------------------------------+ -- CaptureInfo (shared by EAttrs, ELet, ELambda)+ -- -----------------------------------------------------------------++ compileCaptureInfo :: CaptureInfo -> IO Word32+ compileCaptureInfo NoCaptureInfo =+ emitWordList [captureNone]+ compileCaptureInfo (Captures pairs) =+ emitWordList $+ [captureSlots, fi (length pairs)]+ ++ concatMap (\(level, idx) -> [fi level, fi idx]) pairs+ compileCaptureInfo (CapturesWithScopes pairs) =+ emitWordList $+ [captureWithScopes, fi (length pairs)]+ ++ concatMap (\(level, idx) -> [fi level, fi idx]) pairs++ -- -----------------------------------------------------------------+ -- Operator encoding+ -- -----------------------------------------------------------------++ encodeUnaryOp :: UnaryOp -> Word8+ encodeUnaryOp OpNot = unaryNot+ encodeUnaryOp OpNegate = unaryNegate++ encodeBinaryOp :: BinaryOp -> Word8+ encodeBinaryOp OpAdd = binaryAdd+ encodeBinaryOp OpSub = binarySub+ encodeBinaryOp OpMul = binaryMul+ encodeBinaryOp OpDiv = binaryDiv+ encodeBinaryOp OpAnd = binaryAnd+ encodeBinaryOp OpOr = binaryOr+ encodeBinaryOp OpImpl = binaryImpl+ encodeBinaryOp OpEq = binaryEq+ encodeBinaryOp OpNeq = binaryNeq+ encodeBinaryOp OpLt = binaryLt+ encodeBinaryOp OpLte = binaryLte+ encodeBinaryOp OpGt = binaryGt+ encodeBinaryOp OpGte = binaryGte+ encodeBinaryOp OpConcat = binaryConcat+ encodeBinaryOp OpUpdate = binaryUpdate++ encodeFormalType :: Formals -> Word8+ encodeFormalType (FormalName {}) = formalName+ encodeFormalType (FormalSet {}) = formalSet+ encodeFormalType (FormalNamedSet {}) = formalNamedSet++ -- -----------------------------------------------------------------+ -- Numeric encoding+ -- -----------------------------------------------------------------++ splitInt64 :: Int64 -> (Word32, Word32)+ splitInt64 n =+ let w64 = fromIntegral n :: Word64+ in (fromIntegral (w64 .&. 0xFFFFFFFF), fromIntegral (shiftR w64 32))++ splitDouble :: Double -> (Word32, Word32)+ splitDouble d =+ let w64 = castDoubleToWord64 d+ in (fromIntegral (w64 .&. 0xFFFFFFFF), fromIntegral (shiftR w64 32))++ -- -----------------------------------------------------------------+ -- Data buffer helpers+ -- -----------------------------------------------------------------++ -- \| Emit pairs of (tag, value) to the data buffer.+ -- Returns the offset of the first emitted word, or 0 if empty.+ emitPairs :: [(Word32, Word32)] -> IO Word32+ emitPairs [] = pure 0+ emitPairs ((tag, val) : rest) = do+ off <- cbcEmitData tag+ _ <- cbcEmitData val+ mapM_ (\(t, v) -> cbcEmitData t >> cbcEmitData v) rest+ pure off++ -- \| Emit a list of uint32 values to the data buffer.+ -- Returns the offset of the first emitted word, or 0 if empty.+ emitWordList :: [Word32] -> IO Word32+ emitWordList [] = pure 0+ emitWordList (x : xs) = do+ off <- cbcEmitData x+ mapM_ cbcEmitData xs+ pure off++ -- \| @fromIntegral@ shorthand.+ fi :: (Integral a, Num b) => a -> b+ fi = fromIntegral++-- ---------------------------------------------------------------------------+-- Compile parser Formals to eval EvalFormals+-- ---------------------------------------------------------------------------++-- | Compile parser 'Formals' (with 'Maybe Expr' defaults) to eval-time+-- 'EvalFormals' (with 'Maybe Word32' bc_idx defaults).+compileFormalsToEval :: Formals -> IO EvalFormals+compileFormalsToEval (FormalName name) = pure (EFName name)+compileFormalsToEval (FormalSet formals ellipsis) =+ EFSet <$> mapM compileOneFormal formals <*> pure ellipsis+compileFormalsToEval (FormalNamedSet name formals ellipsis) =+ EFNamedSet name <$> mapM compileOneFormal formals <*> pure ellipsis++compileOneFormal :: Formal -> IO EvalFormal+compileOneFormal (Formal name defExpr) = do+ defBcIdx <- case defExpr of+ Nothing -> pure Nothing+ Just expr -> Just <$> compileExpr expr+ pure (EvalFormal name defBcIdx)++-- ---------------------------------------------------------------------------+-- Bytecode decoding (eval-time: read from C data buffer)+-- ---------------------------------------------------------------------------++-- | Decode formals from the bytecode data buffer.+-- @flags@ is the formal type (0=Name, 1=Set, 2=NamedSet).+-- @dataOff@ is the offset into the data buffer.+decodeBcFormals :: Word8 -> Word32 -> IO EvalFormals+decodeBcFormals flags dataOff = case flags of+ 0 {- FormalName -} -> do+ sym <- cbcData dataOff+ pure (EFName (symbolText (Symbol sym)))+ 1 {- FormalSet -} -> do+ count <- cbcData dataOff+ ellipsis <- cbcData (dataOff + 1)+ formals <- decodeFormalEntries (fromIntegral count) (dataOff + 2)+ pure (EFSet formals (ellipsis /= 0))+ 2 {- FormalNamedSet -} -> do+ nameSym <- cbcData dataOff+ count <- cbcData (dataOff + 1)+ ellipsis <- cbcData (dataOff + 2)+ formals <- decodeFormalEntries (fromIntegral count) (dataOff + 3)+ pure (EFNamedSet (symbolText (Symbol nameSym)) formals (ellipsis /= 0))+ _ -> error "decodeBcFormals: invalid formal type flag"++-- | Decode a list of formal entries from the data buffer.+-- Each entry is 3 words: [name_sym, has_default, default_bc_idx].+decodeFormalEntries :: Int -> Word32 -> IO [EvalFormal]+decodeFormalEntries 0 _ = pure []+decodeFormalEntries n off = do+ nameSym <- cbcData off+ hasDef <- cbcData (off + 1)+ defBcIdx <- cbcData (off + 2)+ let defMaybe = if hasDef /= 0 then Just defBcIdx else Nothing+ rest <- decodeFormalEntries (n - 1) (off + 3)+ pure (EvalFormal (symbolText (Symbol nameSym)) defMaybe : rest)++-- | Decode capture info from the bytecode data buffer.+decodeBcCaptureInfo :: Word32 -> IO CaptureInfo+decodeBcCaptureInfo dataOff = do+ tag <- cbcData dataOff+ case tag of+ 0 {- NoCaptureInfo -} -> pure NoCaptureInfo+ 1 {- Captures -} -> do+ count <- cbcData (dataOff + 1)+ pairs <- decodePairs (fromIntegral count) (dataOff + 2)+ pure (Captures pairs)+ 2 {- CapturesWithScopes -} -> do+ count <- cbcData (dataOff + 1)+ pairs <- decodePairs (fromIntegral count) (dataOff + 2)+ pure (CapturesWithScopes pairs)+ _ -> error "decodeBcCaptureInfo: invalid capture type tag"++-- | Decode (level, idx) pairs from the data buffer.+decodePairs :: Int -> Word32 -> IO [(Int, Int)]+decodePairs 0 _ = pure []+decodePairs n off = do+ level <- cbcData off+ idx <- cbcData (off + 1)+ rest <- decodePairs (n - 1) (off + 2)+ pure ((fromIntegral level, fromIntegral idx) : rest)++-- ---------------------------------------------------------------------------+-- Binding decoding (for evalBcAttrs / evalBcLet)+-- ---------------------------------------------------------------------------++-- | A decoded binding from the bytecode data buffer.+data BcBinding+ = -- | @path = expr@ with attr path keys and value bc_idx+ BcNamed ![BcAttrKey] !Word32+ | -- | @inherit names@ from surrounding scope (symbol ids)+ BcInherit ![Word32]+ | -- | @inherit (from) names@ with from-expr bc_idx and name symbols+ BcInheritFrom !Word32 ![Word32]++-- | An attribute key in a binding path.+data BcAttrKey+ = -- | Static key (interned symbol)+ BcStaticKey !Word32+ | -- | Dynamic key (bc_idx of expression to evaluate)+ BcDynamicKey !Word32++-- | Decode all bindings from the bytecode data buffer.+-- @bindCount@ is the number of bindings, @dataOff@ is the start offset.+decodeBcBindings :: Word16 -> Word32 -> IO [BcBinding]+decodeBcBindings 0 _ = pure []+decodeBcBindings count dataOff = do+ (binding, nextOff) <- decodeOneBinding dataOff+ rest <- decodeBcBindings (count - 1) nextOff+ pure (binding : rest)++-- | Decode a single binding, returning it and the offset past it.+decodeOneBinding :: Word32 -> IO (BcBinding, Word32)+decodeOneBinding off = do+ tag <- cbcData off+ case tag of+ 0 {- NamedBinding -} -> do+ pathLen <- cbcData (off + 1)+ let keyStart = off + 2+ keyWords = fromIntegral pathLen * 2+ valOff = keyStart + keyWords+ keys <- decodeAttrKeys (fromIntegral pathLen) keyStart+ valBcIdx <- cbcData valOff+ pure (BcNamed keys valBcIdx, valOff + 1)+ 1 {- Inherit -} -> do+ hasFrom <- cbcData (off + 1)+ fromBcIdx <- cbcData (off + 2)+ nameCount <- cbcData (off + 3)+ syms <- decodeSymList (fromIntegral nameCount) (off + 4)+ let nextOff = off + 4 + nameCount+ if hasFrom /= 0+ then pure (BcInheritFrom fromBcIdx syms, nextOff)+ else pure (BcInherit syms, nextOff)+ _ -> error "decodeOneBinding: invalid binding type tag"++-- | Decode attr path keys: pairs of (is_expr, key_or_bc_idx).+decodeAttrKeys :: Int -> Word32 -> IO [BcAttrKey]+decodeAttrKeys 0 _ = pure []+decodeAttrKeys n off = do+ isExpr <- cbcData off+ val <- cbcData (off + 1)+ rest <- decodeAttrKeys (n - 1) (off + 2)+ let key = if isExpr /= 0 then BcDynamicKey val else BcStaticKey val+ pure (key : rest)++-- | Decode a list of symbol IDs from the data buffer.+decodeSymList :: Int -> Word32 -> IO [Word32]+decodeSymList 0 _ = pure []+decodeSymList n off = do+ sym <- cbcData off+ rest <- decodeSymList (n - 1) (off + 1)+ pure (sym : rest)++-- ---------------------------------------------------------------------------+-- Numeric reassembly (from two uint32 halves)+-- ---------------------------------------------------------------------------++-- | Reassemble an Int64 from two uint32 halves (lo, hi).+reassembleInt64 :: Word32 -> Word32 -> Int64+reassembleInt64 lo hi =+ let w64 = fromIntegral lo .|. (fromIntegral hi `shiftL` 32) :: Word64+ in fromIntegral w64++-- | Reassemble a Double from two uint32 halves (lo, hi).+reassembleDouble :: Word32 -> Word32 -> Double+reassembleDouble lo hi =+ let w64 = fromIntegral lo .|. (fromIntegral hi `shiftL` 32) :: Word64+ in castWord64ToDouble w64
src/Nix/Eval/Context.hs view
@@ -24,6 +24,7 @@ ) where +import Data.List (foldl') import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import qualified Data.Set as Set@@ -80,6 +81,6 @@ -- | Concatenate multiple strings with contexts, merging all contexts. concatStrings :: [(Text, StringContext)] -> (Text, StringContext)-concatStrings = foldl merge ("", mempty)+concatStrings = foldl' merge ("", mempty) where merge (!accText, !accCtx) (t, ctx) = (accText <> t, accCtx <> ctx)
+ src/Nix/Eval/EvalFormals.hs view
@@ -0,0 +1,33 @@+-- | Eval-time formals for lambda parameters.+--+-- Separates the evaluation representation (defaults are bytecode indices)+-- from the parser representation ('Nix.Expr.Types.Formal', defaults are+-- 'Expr'). This avoids circular dependencies: both 'Nix.Eval.Types'+-- (for 'VLambda') and 'Nix.Eval.Compile' (for encoding\/decoding)+-- import this leaf module.+module Nix.Eval.EvalFormals+ ( EvalFormals (..),+ EvalFormal (..),+ )+where++import Data.Text (Text)+import Data.Word (Word32)++-- | A single formal parameter with an optional default (bytecode index).+data EvalFormal = EvalFormal+ { efName :: !Text,+ efDefault :: !(Maybe Word32)+ }+ deriving (Eq, Show)++-- | Formal parameter specification for a lambda.+-- Mirrors 'Nix.Expr.Types.Formals' but with bytecode indices for defaults.+data EvalFormals+ = -- | @x: body@ — single named parameter+ EFName !Text+ | -- | @{ a, b, ... }: body@ — destructuring set pattern+ EFSet ![EvalFormal] !Bool+ | -- | @args\@{ a, b, ... }: body@ — named set pattern+ EFNamedSet !Text ![EvalFormal] !Bool+ deriving (Eq, Show)
src/Nix/Eval/IO.hs view
@@ -23,15 +23,17 @@ -- * Errors NixEvalError (..),+ NixAbortError (..), ) where -import Control.Exception (Exception, SomeException, displayException, fromException, throwIO, try)+import Control.Exception (Exception, SomeAsyncException, SomeException, displayException, fromException, throwIO, try) import Control.Monad (unless, when) import Control.Monad.IO.Class (liftIO) import Control.Monad.Reader (ReaderT (..), ask, asks, local) import qualified Data.ByteString as BS-import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef)+import Data.IORef (IORef, modifyIORef', newIORef, readIORef)+import Data.Int (Int64) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Text (Text)@@ -39,12 +41,18 @@ import Data.Text.Encoding (encodeUtf8) import qualified Data.Text.IO as TIO import Data.Time.Clock.POSIX (getPOSIXTime)+import Foreign.Ptr (Ptr, castPtr, nullPtr)+import Foreign.StablePtr (castPtrToStablePtr, castStablePtrToPtr, deRefStablePtr, freeStablePtr, newStablePtr) import Nix.Builtins (builtinEnv, builtinEnvWithScope, parseNixPath) import Nix.Eval (eval)-import Nix.Eval.Types (MonadEval (..), NixValue (..), Thunk (..), ThunkCell (..), attrSetSize)+import Nix.Eval.CList (CList (..))+import Nix.Eval.CThunk (CThunkPtr, cthunkGetAttrs, cthunkGetBcIdx, cthunkGetBool, cthunkGetCtxStr, cthunkGetFloat, cthunkGetInt, cthunkGetLambda, cthunkGetList, cthunkGetPath, cthunkGetStr, cthunkMarkBlackhole, cthunkPayload, cthunkSetComputed, cthunkSetComputedAttrs, cthunkSetComputedBool, cthunkSetComputedCtxStr, cthunkSetComputedFloat, cthunkSetComputedInt, cthunkSetComputedLambda, cthunkSetComputedList, cthunkSetComputedNull, cthunkSetComputedPath, cthunkSetComputedStr, cthunkState, cthunkValueTag)+import Nix.Eval.Symbol (Symbol (..), symbolIntern, symbolText)+import Nix.Eval.Types (AttrSet (..), Env (..), MonadEval (..), NixValue (..), Thunk (..), attrSetSize, emptyContext, marshalLambda, marshalStringContext, unmarshalLambdaValue, unmarshalStringContext) import Nix.Expr.Types (AttrKey (..), Binding (..), Expr (..), Formal (..), Formals (..), NixAtom (..), StringPart (..)) import Nix.Hash (sha256Hex, truncatedBase32) import Nix.Parser (parseNix, readFileAutoEncoding)+import qualified Nix.Store.Path as SP import qualified System.Directory as Dir import System.Environment (lookupEnv) import System.Exit (ExitCode (..))@@ -56,12 +64,18 @@ -- Error type -- --------------------------------------------------------------------------- --- | Evaluation error surfaced as an IO exception.+-- | Evaluation error surfaced as an IO exception (catchable by tryEval). newtype NixEvalError = NixEvalError Text deriving (Show) instance Exception NixEvalError +-- | Abort error — NOT catchable by tryEval (matches real Nix semantics).+newtype NixAbortError = NixAbortError Text+ deriving (Show)++instance Exception NixAbortError+ -- --------------------------------------------------------------------------- -- State -- ---------------------------------------------------------------------------@@ -81,7 +95,7 @@ { esImportCache :: !(IORef (Map FilePath NixValue)), esBaseDir :: !FilePath, esStoreDir :: !FilePath,- esTimestamp :: !Integer,+ esTimestamp :: !Int64, esSearchPaths :: ![Thunk] } @@ -90,7 +104,7 @@ newEvalState :: FilePath -> IO EvalState newEvalState baseDir = do cache <- newIORef Map.empty- now <- fmap floor getPOSIXTime :: IO Integer+ now <- floor <$> getPOSIXTime :: IO Int64 nixPathStr <- lookupEnvText "NIX_PATH" let searchPaths = case nixPathStr of Just val -> parseNixPath (T.pack val)@@ -99,7 +113,7 @@ EvalState { esImportCache = cache, esBaseDir = baseDir,- esStoreDir = "/nix/store",+ esStoreDir = T.unpack SP.platformStoreDirText, esTimestamp = now, esSearchPaths = searchPaths }@@ -118,6 +132,7 @@ instance MonadEval EvalIO where throwEvalError msg = EvalIO (liftIO (throwIO (NixEvalError msg)))+ abortEvaluation msg = EvalIO (liftIO (throwIO (NixAbortError msg))) catchEvalError (EvalIO action) = EvalIO $ do st <- ask@@ -190,6 +205,8 @@ -- Validate name to prevent path traversal when (T.any (== '/') name) $ throwEvalError ("writeToStore: name must not contain '/': " <> name)+ when (T.any (== '\\') name) $+ throwEvalError ("writeToStore: name must not contain '\\': " <> name) when (T.isInfixOf ".." name) $ throwEvalError ("writeToStore: name must not contain '..': " <> name) when (T.any (== '\0') name) $@@ -198,8 +215,11 @@ let contentHash = sha256Hex (encodeUtf8 contents) inner = "nix-store:sha256:" <> contentHash <> ":" <> T.pack storeDir <> ":" <> name pathHash = truncatedBase32 (encodeUtf8 inner)- storePath = T.pack storeDir <> "/" <> pathHash <> "-" <> name- filePath = T.unpack storePath+ basename = pathHash <> "-" <> name+ -- File path uses platform separator for I/O+ filePath = storeDir </> T.unpack basename+ -- Store path text always uses forward slash (Nix convention)+ storePath = T.pack storeDir <> "/" <> basename wrapIO $ do Dir.createDirectoryIfMissing True storeDir TIO.writeFile filePath contents@@ -212,13 +232,17 @@ let raw = T.unpack rawPath resolved = if isRelative raw then baseDir </> raw else raw canonical <- wrapIO (Dir.canonicalizePath resolved)- source <- wrapIO (readFileAutoEncoding canonical)- case parseNix (T.pack canonical) source of+ -- Directory import: append /default.nix if target is a directory+ target <- wrapIO $ do+ isDir <- Dir.doesDirectoryExist canonical+ pure (if isDir then canonical </> "default.nix" else canonical)+ source <- wrapIO (readFileAutoEncoding target)+ case parseNix (T.pack target) source of Left err -> throwEvalError- ("scopedImport " <> T.pack canonical <> ": " <> T.pack (show err))+ ("scopedImport " <> T.pack target <> ": " <> T.pack (show err)) Right rawExpr -> do- let fileDir = takeDirectory canonical+ let fileDir = takeDirectory target expr = resolveRelativePaths fileDir rawExpr -- No import cache for scoped imports (different scopes = different results) let scopedEnv = builtinEnvWithScope timestamp searchPaths scope@@ -247,12 +271,25 @@ pure (code, T.pack stdoutStr, T.pack stderrStr) copyPathToStore srcPath name = do+ -- Validate name to prevent path traversal (matches writeToStore)+ when (T.any (== '/') name) $+ throwEvalError ("copyPathToStore: name must not contain '/': " <> name)+ when (T.any (== '\\') name) $+ throwEvalError ("copyPathToStore: name must not contain '\\': " <> name)+ when (T.isInfixOf ".." name) $+ throwEvalError ("copyPathToStore: name must not contain '..': " <> name)+ when (T.any (== '\0') name) $+ throwEvalError ("copyPathToStore: name must not contain null bytes: " <> name) storeDir <- EvalIO (asks esStoreDir) let contentHash = sha256Hex (encodeUtf8 ("source:" <> srcPath <> ":" <> name)) inner = "nix-store:sha256:" <> contentHash <> ":" <> T.pack storeDir <> ":" <> name pathHash = truncatedBase32 (encodeUtf8 inner)- destPath = T.pack storeDir <> "/" <> pathHash <> "-" <> name- wrapIO (copyToStoreIfMissing (T.unpack srcPath) (T.unpack destPath) storeDir)+ basename = pathHash <> "-" <> name+ -- File path uses platform separator for I/O+ destFilePath = storeDir </> T.unpack basename+ -- Store path text always uses forward slash (Nix convention)+ destPath = T.pack storeDir <> "/" <> basename+ wrapIO (copyToStoreIfMissing (T.unpack srcPath) destFilePath storeDir) pure destPath traceMessage msg = EvalIO (liftIO (hPutStrLn stderr (T.unpack msg)))@@ -264,20 +301,100 @@ then pure (T.pack (baseDir </> raw)) else pure path - forceThunk _ (Evaluated val) = pure val- 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- EvalIO (liftIO (writeIORef ref (Computed val)))+ forceThunk evalFn (Thunk ptr) = do+ -- Force protocol: PENDING -> BLACKHOLE -> COMPUTED with memoization.+ -- Scalar values (int/float/bool/null) are stored inline in the+ -- thunk payload (no StablePtr), dispatched via val_tag.+ --+ -- Blackhole detection: PENDING thunks are marked BLACKHOLE before+ -- evaluation begins. If evaluation re-enters the same thunk, it+ -- sees BLACKHOLE and reports infinite recursion. This is safe with+ -- knot-tying (evalRecAttrs, evalLet, matchFormalSet) because those+ -- patterns create distinct thunks sharing an env — no thunk ever+ -- forces itself.+ state <- EvalIO (liftIO (cthunkState ptr))+ case state of+ 1 {- COMPUTED -} ->+ EvalIO (liftIO (readComputed ptr))+ 2 {- BLACKHOLE -} ->+ -- Infinite recursion is non-catchable (like abort), matching C++ Nix.+ -- tryEval must NOT catch blackholes — using abortEvaluation ensures+ -- the error propagates through tryEval/catchEvalError.+ abortEvaluation "infinite recursion encountered"+ _ {- PENDING -} -> do+ -- Bytecode thunks: read bc_idx + StablePtr Env.+ -- The Expr is gone (replaced by bc_idx in the struct).+ -- The Env is still a StablePtr (for knot-tying laziness).+ bcIdx <- EvalIO (liftIO (cthunkGetBcIdx ptr))+ envSp <- EvalIO (liftIO (cthunkPayload ptr))+ let pendingSp = castPtrToStablePtr envSp+ env <- EvalIO (liftIO (deRefStablePtr pendingSp))+ -- Mark blackhole BEFORE evaluation — any re-entry hits the+ -- BLACKHOLE branch above.+ _ <- EvalIO (liftIO (cthunkMarkBlackhole ptr))+ val <- evalFn env bcIdx+ oldPayload <- EvalIO (liftIO (storeComputed ptr val))+ -- Free the pending env StablePtr.+ when (oldPayload /= nullPtr) $+ EvalIO (liftIO (freeStablePtr (castPtrToStablePtr oldPayload))) pure val +-- | Store a computed NixValue in a C thunk.+-- Scalars (int, float, bool, null) are stored inline (no StablePtr).+-- Complex values use StablePtr. Returns old payload for cleanup.+storeComputed :: CThunkPtr -> NixValue -> IO (Ptr ())+storeComputed ptr val = case val of+ VInt n -> cthunkSetComputedInt ptr n+ VFloat d -> cthunkSetComputedFloat ptr d+ VBool b -> cthunkSetComputedBool ptr (if b then 1 else 0)+ VNull -> cthunkSetComputedNull ptr+ VAttrs (AttrSet cset) -> cthunkSetComputedAttrs ptr (castPtr cset)+ VPath p -> do+ Symbol sym <- symbolIntern p+ cthunkSetComputedPath ptr sym+ VStr t ctx+ | ctx == emptyContext -> do+ Symbol sym <- symbolIntern t+ cthunkSetComputedStr ptr sym+ | otherwise -> do+ csptr <- marshalStringContext t ctx+ cthunkSetComputedCtxStr ptr (castPtr csptr)+ VList (CList clistPtr) -> cthunkSetComputedList ptr (castPtr clistPtr)+ VLambda (Env envPtr) formals bodyBcIdx -> do+ lamPtr <- marshalLambda envPtr formals bodyBcIdx+ cthunkSetComputedLambda ptr lamPtr+ _ -> do+ valSp <- newStablePtr val+ cthunkSetComputed ptr (castStablePtrToPtr valSp)++-- | Read a computed NixValue from a C thunk.+-- Dispatches on val_tag: scalars are read inline, complex via StablePtr.+readComputed :: CThunkPtr -> IO NixValue+readComputed ptr = do+ tag <- cthunkValueTag ptr+ case tag of+ 0 {- INT -} -> VInt <$> cthunkGetInt ptr+ 1 {- FLOAT -} -> VFloat <$> cthunkGetFloat ptr+ 2 {- BOOL -} -> (\b -> VBool (b /= 0)) <$> cthunkGetBool ptr+ 3 {- NULL -} -> pure VNull+ 4 {- STR -} -> do+ sym <- cthunkGetStr ptr+ pure (VStr (symbolText (Symbol sym)) emptyContext)+ 5 {- PATH -} -> VPath . symbolText . Symbol <$> cthunkGetPath ptr+ 6 {- LIST -} -> do+ listPtr <- cthunkGetList ptr+ pure (VList (CList (castPtr listPtr)))+ 7 {- ATTRS -} -> VAttrs . AttrSet . castPtr <$> cthunkGetAttrs ptr+ 8 {- CTXSTR -} -> do+ csptr <- cthunkGetCtxStr ptr+ uncurry VStr <$> unmarshalStringContext (castPtr csptr)+ 9 {- LAMBDA -} -> do+ lamPtr <- cthunkGetLambda ptr+ unmarshalLambdaValue lamPtr+ _ {- PTR -} -> do+ payloadPtr <- cthunkPayload ptr+ deRefStablePtr (castPtrToStablePtr payloadPtr)+ -- --------------------------------------------------------------------------- -- Constants -- ---------------------------------------------------------------------------@@ -325,19 +442,26 @@ result <- try action case result of Right val -> pure val- Left (err :: SomeException) ->- case fromException err of- Just nixErr -> throwIO (nixErr :: NixEvalError)- Nothing -> throwIO (NixEvalError (T.pack (displayException err)))+ Left (err :: SomeException)+ | Just (_ :: SomeAsyncException) <- fromException err -> throwIO err+ | Just abortErr <- fromException err -> throwIO (abortErr :: NixAbortError)+ | Just nixErr <- fromException err -> throwIO (nixErr :: NixEvalError)+ | otherwise -> throwIO (NixEvalError (T.pack (displayException err))) -- | Run an IO evaluation, returning @Left@ on error. ----- Note: only catches 'NixEvalError'. Async exceptions--- (@StackOverflow@, @ThreadKilled@, etc.) propagate uncaught.+-- Catches 'NixEvalError' (throw) and 'NixAbortError' (abort).+-- Async exceptions (@StackOverflow@, @ThreadKilled@, etc.) propagate uncaught. runEvalIO :: EvalState -> EvalIO a -> IO (Either Text a) runEvalIO st (EvalIO action) = do result <- try (runReaderT action st)- pure (case result of Left (NixEvalError msg) -> Left msg; Right val -> Right val)+ case result of+ Right val -> pure (Right val)+ Left (err :: SomeException)+ | Just (_ :: SomeAsyncException) <- fromException err -> throwIO err+ | Just (NixEvalError msg) <- fromException err -> pure (Left msg)+ | Just (NixAbortError msg) <- fromException err -> pure (Left msg)+ | otherwise -> pure (Left (T.pack (displayException err))) -- | Look up an environment variable, returning Nothing if unset. lookupEnvText :: String -> IO (Maybe String)
src/Nix/Eval/Operator.hs view
@@ -11,21 +11,22 @@ ) where -import qualified Data.Map.Strict as Map+import Data.Int (Int64) import Data.Text (Text)+import Nix.Eval.CAttrSet (cattrsetUnion)+import Nix.Eval.CList (clistFromThunks, clistLen, clistThunks) import Nix.Eval.Types ( AttrSet (..),- LazyBinding (..), MonadEval (..), NixValue (..),- Thunk,+ Thunk (..), attrSetElems, attrSetKeys,- newLazyAttrCache, thunkSameRef, typeName, ) import Nix.Expr.Types (BinaryOp (..), UnaryOp (..))+import System.IO.Unsafe (unsafePerformIO) -- | Force function passed by the caller to break the import cycle. -- Needed for deep equality on lists and attribute sets.@@ -74,11 +75,13 @@ -- | Addition: int/float arithmetic, string concatenation, path append. evalAdd :: (MonadEval m) => NixValue -> NixValue -> m NixValue evalAdd (VInt a) (VInt b) = pure (VInt (a + b))-evalAdd (VInt a) (VFloat b) = pure (VFloat (fromInteger a + b))-evalAdd (VFloat a) (VInt b) = pure (VFloat (a + fromInteger b))+evalAdd (VInt a) (VFloat b) = pure (VFloat (fromIntegral a + b))+evalAdd (VFloat a) (VInt b) = pure (VFloat (a + fromIntegral b)) evalAdd (VFloat a) (VFloat b) = pure (VFloat (a + b)) evalAdd (VStr a ctxA) (VStr b ctxB) = pure (VStr (a <> b) (ctxA <> ctxB)) evalAdd (VPath a) (VStr b _) = pure (VPath (a <> b))+evalAdd (VPath a) (VPath b) = pure (VPath (a <> b))+evalAdd (VStr a ctxA) (VPath b) = pure (VStr (a <> b) ctxA) evalAdd left right = throwEvalError ("cannot add " <> typeName left <> " and " <> typeName right) @@ -86,15 +89,15 @@ evalArith :: (MonadEval m) => Text ->- (Integer -> Integer -> Integer) ->+ (Int64 -> Int64 -> Int64) -> (Double -> Double -> Double) -> NixValue -> NixValue -> m NixValue evalArith name intOp floatOp left right = case (left, right) of (VInt a, VInt b) -> pure (VInt (intOp a b))- (VInt a, VFloat b) -> pure (VFloat (floatOp (fromInteger a) b))- (VFloat a, VInt b) -> pure (VFloat (floatOp a (fromInteger b)))+ (VInt a, VFloat b) -> pure (VFloat (floatOp (fromIntegral a) b))+ (VFloat a, VInt b) -> pure (VFloat (floatOp a (fromIntegral b))) (VFloat a, VFloat b) -> pure (VFloat (floatOp a b)) _ -> throwEvalError@@ -106,16 +109,21 @@ <> typeName right ) --- | Division with zero check. Integer division uses 'quot'.+-- | Division with zero check. Integer division uses 'quot'+-- (truncation toward zero, matching C++ Nix semantics). evalDiv :: (MonadEval m) => NixValue -> NixValue -> m NixValue evalDiv left right = case (left, right) of (VInt _, VInt 0) -> throwEvalError "division by zero"- (VInt a, VInt b) -> pure (VInt (quot a b))+ (VInt a, VInt b)+ -- quot minBound (-1) throws arithmetic overflow in Haskell;+ -- C++ Nix wraps silently (undefined behavior, wraps to minBound).+ | a == minBound && b == -1 -> pure (VInt minBound)+ | otherwise -> pure (VInt (quot a b)) (VInt a, VFloat b) | b == 0 -> throwEvalError "division by zero"- | otherwise -> pure (VFloat (fromInteger a / b))+ | otherwise -> pure (VFloat (fromIntegral a / b)) (VFloat _, VInt 0) -> throwEvalError "division by zero"- (VFloat a, VInt b) -> pure (VFloat (a / fromInteger b))+ (VFloat a, VInt b) -> pure (VFloat (a / fromIntegral b)) (VFloat a, VFloat b) | b == 0 -> throwEvalError "division by zero" | otherwise -> pure (VFloat (a / b))@@ -134,8 +142,8 @@ -- | Ordering comparison for < (reused for >, <=, >= via argument swap). nixCompare :: (MonadEval m) => NixValue -> NixValue -> m Bool nixCompare (VInt a) (VInt b) = pure (a < b)-nixCompare (VInt a) (VFloat b) = pure (fromInteger a < b)-nixCompare (VFloat a) (VInt b) = pure (a < fromInteger b)+nixCompare (VInt a) (VFloat b) = pure (fromIntegral a < b)+nixCompare (VFloat a) (VInt b) = pure (a < fromIntegral b) nixCompare (VFloat a) (VFloat b) = pure (a < b) -- String comparison ignores context (matching real Nix). nixCompare (VStr a _) (VStr b _) = pure (a < b)@@ -151,17 +159,17 @@ -- attribute sets as needed. nixEqual :: (MonadEval m) => Force m -> NixValue -> NixValue -> m Bool nixEqual _ (VInt a) (VInt b) = pure (a == b)-nixEqual _ (VInt a) (VFloat b) = pure (fromInteger a == b)-nixEqual _ (VFloat a) (VInt b) = pure (a == fromInteger b)+nixEqual _ (VInt a) (VFloat b) = pure (fromIntegral a == b)+nixEqual _ (VFloat a) (VInt b) = pure (a == fromIntegral b) nixEqual _ (VFloat a) (VFloat b) = pure (a == b) nixEqual _ (VBool a) (VBool b) = pure (a == b) nixEqual _ VNull VNull = pure True -- String equality ignores context (matching real Nix). nixEqual _ (VStr a _) (VStr b _) = pure (a == b) nixEqual _ (VPath a) (VPath b) = pure (a == b)-nixEqual forceFn (VList as) (VList bs)- | length as /= length bs = pure False- | otherwise = listEqual forceFn as bs+nixEqual forceFn (VList clA) (VList clB)+ | clistLen clA /= clistLen clB = pure False+ | otherwise = listEqual forceFn (map Thunk (clistThunks clA)) (map Thunk (clistThunks clB)) nixEqual forceFn (VAttrs as) (VAttrs bs) | attrSetKeys as /= attrSetKeys bs = pure False | otherwise = do@@ -198,7 +206,8 @@ -- | List concatenation (++). evalConcat :: (MonadEval m) => NixValue -> NixValue -> m NixValue-evalConcat (VList as) (VList bs) = pure (VList (as ++ bs))+evalConcat (VList clA) (VList clB) =+ pure (VList (clistFromThunks (clistThunks clA ++ clistThunks clB))) evalConcat left right = throwEvalError ("cannot concatenate " <> typeName left <> " and " <> typeName right) @@ -213,29 +222,16 @@ 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.+-- | Merge two 'AttrSet's, right-biased (@//@).+-- Delegates to C-side @nn_attrset_union@ which performs a linear merge+-- of two sorted arrays — O(n+m) on contiguous, cache-friendly memory.+--+-- 'unsafePerformIO' safety: @nn_attrset_union@ is a pure C function+-- that allocates a new result set from its two inputs without side+-- effects, callbacks to Haskell, or dependency on mutable state+-- beyond the C allocator. The NOINLINE pragma prevents float-out+-- from sharing results across distinct call sites.+{-# NOINLINE mergeAttrSets #-} 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+mergeAttrSets (AttrSet a) (AttrSet b) =+ AttrSet (unsafePerformIO (cattrsetUnion a b))
src/Nix/Eval/StringInterp.hs view
@@ -8,6 +8,8 @@ ( evalStringParts, evalIndStringParts, coerceToString,+ formatNixFloat,+ stripIndentation, ) where @@ -16,6 +18,7 @@ import Nix.Eval.Context (concatStrings) import Nix.Eval.Types (Env, MonadEval (..), NixValue (..), StringContext, Thunk, attrSetLookup, emptyContext, typeName) import Nix.Expr.Types (Expr, StringPart (..))+import Numeric (showFFloat) -- | The evaluator function, passed as a parameter to avoid cyclic imports. type Eval m = Env -> Expr -> m NixValue@@ -59,7 +62,7 @@ coerceToString :: (MonadEval m) => Force m -> Apply m -> NixValue -> m (Text, StringContext) coerceToString _ _ (VStr s ctx) = pure (s, ctx) coerceToString _ _ (VInt n) = pure (T.pack (show n), emptyContext)-coerceToString _ _ (VFloat n) = pure (T.pack (show n), emptyContext)+coerceToString _ _ (VFloat n) = pure (formatNixFloat n, emptyContext) coerceToString _ _ (VPath p) = pure (p, emptyContext) coerceToString _ _ VNull = pure ("", emptyContext) coerceToString _ _ (VBool True) = pure ("1", emptyContext)@@ -80,6 +83,23 @@ coerceToString _ _ other = throwEvalError ("cannot coerce " <> typeName other <> " to a string") +-- | Format a float the way C++ Nix does: 6 fixed decimal places+-- (@std::to_string@), then strip trailing zeros and unnecessary+-- decimal point. E.g. @1.0@ → @"1"@, @3.14@ → @"3.14"@.+formatNixFloat :: Double -> Text+formatNixFloat n+ | isNaN n = "nan"+ | isInfinite n = if n > 0 then "inf" else "-inf"+ | otherwise =+ let fixed = showFFloat (Just 6) n ""+ in T.pack (stripZeros fixed)+ where+ stripZeros s+ | '.' `elem` s = reverse (dropDot (dropWhile (== '0') (reverse s)))+ | otherwise = s+ dropDot ('.' : rest) = rest+ dropDot xs = xs+ -- --------------------------------------------------------------------------- -- Indented string whitespace stripping -- ---------------------------------------------------------------------------@@ -108,14 +128,17 @@ countIndent :: Text -> Int countIndent = T.length . T.takeWhile (\c -> c == ' ' || c == '\t') --- | Find the minimum indentation across all non-empty lines.+-- | Find the minimum indentation across all non-blank lines.+-- Whitespace-only lines are treated as blank (infinite indent) per Nix semantics. minimumIndent :: [Text] -> Int minimumIndent lns =- let nonEmpty = filter (not . T.null) lns- indents = map countIndent nonEmpty+ let nonBlank = filter (\t -> not (T.null t) && not (T.all isSpace t)) lns+ indents = map countIndent nonBlank in case indents of [] -> 0 xs -> minimum xs+ where+ isSpace c = c == ' ' || c == '\t' -- | Strip up to @n@ leading whitespace characters from a line. stripPrefix :: Int -> Text -> Text
+ src/Nix/Eval/Symbol.hs view
@@ -0,0 +1,119 @@+-- | Interned string symbols backed by a C hash table.+--+-- Attribute names are the most repeated data in Nix evaluation.+-- This module interns them via a C-side hash table ('cbits/nn_symbol.c'),+-- replacing O(n) string comparison with O(1) integer comparison.+--+-- @+-- symbolInit 8192+-- sym <- symbolIntern "name"+-- symbolText sym -- "name"+-- symbolInit destroys and re-creates; call once per evaluation.+-- @+module Nix.Eval.Symbol+ ( -- * Symbol type+ Symbol (..),++ -- * Lifecycle+ symbolInit,+ symbolDestroy,++ -- * Core operations+ symbolIntern,+ symbolText,+ symbolLen,++ -- * Diagnostics+ symbolCount,+ )+where++import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Foreign as TF+import Data.Word (Word32)+import Foreign.C.Types (CChar, CSize (..))+import Foreign.Ptr (Ptr, nullPtr)+import System.IO.Unsafe (unsafePerformIO)++-- | An interned symbol — a 'Word32' index into the global symbol table.+-- Two symbols are equal iff their indices are equal (O(1) comparison).+-- 0 is the invalid sentinel.+newtype Symbol = Symbol {unSymbol :: Word32}+ deriving (Eq, Ord, Show)++-- ---------------------------------------------------------------------------+-- FFI imports (unsafe — these never call back to Haskell)+-- ---------------------------------------------------------------------------++foreign import ccall unsafe "nn_symbol_init"+ c_nn_symbol_init :: Word32 -> IO ()++foreign import ccall unsafe "nn_symbol_destroy"+ c_nn_symbol_destroy :: IO ()++foreign import ccall unsafe "nn_symbol_intern"+ c_nn_symbol_intern :: Ptr CChar -> CSize -> IO Word32++foreign import ccall unsafe "nn_symbol_text"+ c_nn_symbol_text :: Word32 -> IO (Ptr CChar)++foreign import ccall unsafe "nn_symbol_len"+ c_nn_symbol_len :: Word32 -> IO CSize++foreign import ccall unsafe "nn_symbol_count"+ c_nn_symbol_count :: IO Word32++-- ---------------------------------------------------------------------------+-- Lifecycle+-- ---------------------------------------------------------------------------++-- | Initialize the global symbol table. Call once before evaluation.+-- @capacity@ is a hint for the expected number of unique symbols.+-- Pass 0 to use the default (4096).+symbolInit :: Word32 -> IO ()+symbolInit = c_nn_symbol_init++-- | Destroy the global symbol table, freeing all C-side memory.+-- All 'Symbol' values become invalid after this call.+symbolDestroy :: IO ()+symbolDestroy = c_nn_symbol_destroy++-- ---------------------------------------------------------------------------+-- Core operations+-- ---------------------------------------------------------------------------++-- | Intern a 'Text' value, returning its 'Symbol'.+-- If the string was already interned, returns the existing symbol.+-- This is the canonical entry point for Text → C conversion.+symbolIntern :: Text -> IO Symbol+symbolIntern txt =+ TF.withCStringLen txt $ \(ptr, len) -> do+ sid <- c_nn_symbol_intern ptr (fromIntegral len)+ pure (Symbol sid)++-- | Retrieve the text of an interned symbol.+-- Returns the original string. The result is safe to use — it copies+-- from the C arena into a fresh 'Text'.+symbolText :: Symbol -> Text+symbolText (Symbol sid)+ | sid == 0 = T.empty+ | otherwise = unsafePerformIO $ do+ ptr <- c_nn_symbol_text sid+ if ptr == nullPtr+ then pure T.empty+ else do+ len <- c_nn_symbol_len sid+ TF.peekCStringLen (ptr, fromIntegral len)++-- | Byte length of a symbol's string.+symbolLen :: Symbol -> Int+symbolLen (Symbol sid) = fromIntegral (unsafePerformIO (c_nn_symbol_len sid))++-- ---------------------------------------------------------------------------+-- Diagnostics+-- ---------------------------------------------------------------------------++-- | Number of unique symbols currently interned.+symbolCount :: IO Word32+symbolCount = c_nn_symbol_count
src/Nix/Eval/Types.hs view
@@ -10,704 +10,1111 @@ NixValue (..), CompiledRegex (..), Thunk (..),- ThunkCell (..),-- -- * Attribute sets (lazy/eager unified abstraction)- AttrSet (..),- LazyBinding (..),- attrSetLookup,- attrSetKeys,- attrSetToMap,- attrSetFromMap,- attrSetMember,- attrSetNull,- attrSetSize,- attrSetElems,- attrSetToAscList,- attrSetMapWithKey,- attrSetMapWithKeyLazy,- attrSetRemoveKeys,- attrSetUnionWith,- newLazyAttrCache,-- -- * String context- StringContextElement (..),- StringContext (..),- emptyContext,- mkStr,-- -- * Environment- Env (..),- emptyEnv,- envLookup,- envLookupResolved,- envFromSlots,- pushWithScope,- lookupWithScopes,- withScopesForCapture,-- -- * Thunk operations- mkThunk,- mkSyntheticThunk,- cheapThunk,- evaluated,- thunkSameRef,-- -- * Display- typeName,-- -- * Evaluation monad- MonadEval (..),- PureEval (..),- )-where--import Data.ByteString (ByteString)-import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef)-import Data.List (foldl')-import Data.Map.Strict (Map)-import qualified Data.Map.Strict as Map-import Data.Primitive.SmallArray (SmallArray, indexSmallArray, sizeofSmallArray, smallArrayFromListN)-import Data.Set (Set)-import Data.Text (Text)-import Nix.Derivation (Derivation)-import Nix.Expr.Types (AttrKey (..), CaptureInfo (..), Expr (..), Formals, NixAtom (..), StringPart (..))-import Nix.Store.Path (StorePath)-import System.IO.Unsafe (unsafePerformIO)-import qualified Text.Regex.TDFA as RE---- ------------------------------------------------------------------------------ Compiled regex--- ------------------------------------------------------------------------------- | Compiled regex with Eq/Show based on source pattern text.--- Carries the pre-compiled 'RE.Regex' alongside the original pattern--- so that partial application of @builtins.match@ / @builtins.split@--- avoids recompiling the same pattern on every invocation.-data CompiledRegex = CompiledRegex !Text RE.Regex--instance Eq CompiledRegex where- CompiledRegex a _ == CompiledRegex b _ = a == b--instance Show CompiledRegex where- show (CompiledRegex pat _) = "CompiledRegex " <> show pat---- ------------------------------------------------------------------------------ String context--- ------------------------------------------------------------------------------- | A single element of string context, tracking where a string--- references store paths.-data StringContextElement- = -- | Plain store path reference (inputSrcs).- SCPlain !StorePath- | -- | Derivation output reference (inputDrvs): .drv path + output name.- SCDrvOutput !StorePath !Text- | -- | All outputs of a derivation (for drvPath itself).- SCAllOutputs !StorePath- deriving (Eq, Ord, Show)---- | Context carried by Nix strings, tracking store path dependencies.-newtype StringContext = StringContext {unStringContext :: Set StringContextElement}- deriving (Eq, Ord, Show, Semigroup, Monoid)---- | Empty string context (alias for 'mempty').-emptyContext :: StringContext-emptyContext = mempty---- | Smart constructor for context-free strings.-mkStr :: Text -> NixValue-mkStr t = VStr t emptyContext---- ------------------------------------------------------------------------------ Thunks--- ------------------------------------------------------------------------------- | 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 '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- = -- | 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 (ThunkRef _) = "ThunkRef <ref>"---- | 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- (ThunkRef ref1) == (ThunkRef ref2) = ref1 == ref2- _ == _ = False---- | A Nix value — the result of evaluating an expression.-data NixValue- = -- | Integer.- VInt !Integer- | -- | Floating-point.- VFloat !Double- | -- | Boolean.- VBool !Bool- | -- | The null value.- VNull- | -- | String with dependency context.- VStr !Text !StringContext- | -- | Path.- VPath !Text- | -- | List of thunks (lazy elements).- VList ![Thunk]- | -- | Attribute set: unified lazy/eager representation.- VAttrs !AttrSet- | -- | Lambda closure: captures environment, formals, body.- VLambda !Env !Formals !Expr- | -- | A realized derivation (build recipe).- VDerivation !Derivation- | -- | Built-in function, dispatched by name.- -- Accumulated args support curried partial application.- VBuiltin !Text ![NixValue]- | -- | Pre-compiled regex carried in a partially-applied builtin.- -- Internal only — never exposed to Nix code directly.- VCompiledRegex !CompiledRegex- deriving (Eq, Show)---- ------------------------------------------------------------------------------ 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.------ 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).- -- INTENTIONALLY LAZY: attrSetMapWithKeyLazy wraps deferred computations- -- here; the Thunk is only forced when the key is actually accessed via- -- attrSetLookup (which caches the result). This avoids materializing- -- all 30k entries when mapAttrs is applied to a large set like nixpkgs.- 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))---- | Like 'attrSetMapWithKey' but preserves laziness for 'LazyAttrs'.--- Each binding is wrapped in 'PreBuilt' so that materialization (IORef--- allocation) only happens when a specific key is accessed. This avoids--- ~30k IORef allocations when @mapAttrs@ is applied to a large set (e.g.--- nixpkgs overlays) and only a few keys are actually demanded.-attrSetMapWithKeyLazy :: (Text -> Thunk -> Thunk) -> AttrSet -> AttrSet-attrSetMapWithKeyLazy f (EagerAttrs m) = EagerAttrs (Map.mapWithKey f m)-attrSetMapWithKeyLazy f (LazyAttrs bindings _cache) =- let mapped = Map.mapWithKey (\k recipe -> PreBuilt (f k (materializeBinding k recipe))) bindings- in LazyAttrs mapped (newLazyAttrCache mapped)---- | Remove a list of keys from an attribute set without materializing--- 'LazyAttrs'. For 'EagerAttrs', deletes from the map directly.--- For 'LazyAttrs', deletes from the binding map and creates a fresh cache.-attrSetRemoveKeys :: [Text] -> AttrSet -> AttrSet-attrSetRemoveKeys keys (EagerAttrs m) =- EagerAttrs (foldl' (flip Map.delete) m keys)-attrSetRemoveKeys keys (LazyAttrs bindings _cache) =- let newBindings = foldl' (flip Map.delete) bindings keys- in LazyAttrs newBindings (newLazyAttrCache newBindings)---- | 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 with positional slots.------ Lambda formals are stored in positional 'envSlots' (de Bruijn-style),--- eliminating Map.Bin overhead. Let\/rec bindings and builtins use--- name-based 'envLazyScope'. Variable lookup has two paths:------ * 'envLookupResolved': O(1) array index for 'EResolvedVar'--- * 'envLookup': name-based walk for 'EVar' (envLazyScope + with-scopes)-data Env = Env- { -- | Positional bindings for lambda formals.- -- Indexed by position (0-based), filled by 'matchFormals'.- -- O(1) lookup via 'indexSmallArray'. Empty for let/rec/builtin envs.- envSlots :: !(SmallArray Thunk),- -- | Lazy scope shared with a 'LazyAttrs' attr set.- -- For rec {} and let, this points to the SAME 'LazyAttrs' that- -- backs the resulting attr set — one map instead of two.- -- For builtins, holds the top-level bindings (true, false, etc.).- -- Variable lookup checks this for name-based 'EVar' lookups.- envLazyScope :: !(Maybe AttrSet),- -- | 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.- -- For trimmed envs ('CapturesWithScopes'), the root scope- -- (builtins) is appended as the outermost entry so that- -- 'EWithVar' can fall back to builtins without a parent chain.- envWithScopes :: ![AttrSet]- }---- | Shallow comparison: checks slot count 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 s1 _ _ w1 == Env s2 _ _ w2 =- sizeofSmallArray s1 == sizeofSmallArray s2 && w1 == w2---- | Compact show: just the size and structure, not the full contents.-instance Show Env where- show (Env slots lazyScope parent withs) =- "Env{"- ++ show (sizeofSmallArray slots)- ++ " slots"- ++ maybe "" (const ", lazyScope") lazyScope- ++ maybe "" (const ", parent") parent- ++ ", "- ++ show (length withs)- ++ " withs}"---- | Empty environment (no variables in scope).-emptyEnv :: Env-emptyEnv = Env mempty Nothing Nothing []---- | Look up a resolved variable by level and index. O(level) parent--- hops, then O(1) array index via 'indexSmallArray'.------ Unreachable branch: the resolution pass guarantees valid indices.-envLookupResolved :: Int -> Int -> Env -> Thunk-envLookupResolved 0 idx env = indexSmallArray (envSlots env) idx-envLookupResolved lvl idx env = case envParent env of- Just parent -> envLookupResolved (lvl - 1) idx parent- Nothing -> error "envLookupResolved: level exceeded (unreachable after resolution)"---- | Name-based variable lookup: walk the parent chain checking--- 'envLazyScope' at each level; fall back to with-scopes (from the--- starting env) only after exhausting all lexical scopes.------ 'envSlots' are positional (no names) and are NOT searched here.--- Used for 'EVar' lookups (let\/rec bindings, builtins, with-scopes).-envLookup :: Text -> Env -> Maybe Thunk-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 _slots lazyScope parent _) =- case lazyScope of- Just scope -> case attrSetLookup name scope of- Just val -> Just val- Nothing -> goParent parent- Nothing -> goParent parent- goParent (Just p) = lexicalLookup p- goParent Nothing = lookupWithScopes name withs---- | Walk with-scopes innermost to outermost.--- 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 attrSetLookup name scope of- Just val -> Just val- Nothing -> lookupWithScopes name rest---- | Create a child env with positional slots (for lambda formals).--- Inherits with-scopes from the parent.-envFromSlots :: SmallArray Thunk -> Env -> Env-envFromSlots slots parent =- Env- { envSlots = slots,- envLazyScope = Nothing,- envParent = Just parent,- envWithScopes = envWithScopes parent- }---- | Push a with-scope onto the scope chain (innermost position).--- 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}---- | Find the root env's lazy scope (builtins) by walking the parent chain.--- Returns the 'envLazyScope' of the bottommost env (the one with--- @envParent = Nothing@). This is the builtin env for standard evals.-envRootScope :: Env -> Maybe AttrSet-envRootScope env = case envParent env of- Nothing -> envLazyScope env- Just parent -> envRootScope parent---- | Build the with-scopes list for a trimmed env that needs with-scope--- access ('CapturesWithScopes'). Appends the root scope (builtins)--- as the outermost entry so 'EWithVar' can fall back to builtins--- without retaining the parent chain.-withScopesForCapture :: Env -> [AttrSet]-withScopesForCapture env =- case envRootScope env of- Just rootScope -> envWithScopes env ++ [rootScope]- Nothing -> envWithScopes env---- | Create an unevaluated thunk with a fresh memoization cell.------ 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 =- ThunkRef (newMemoCell thunkExpr env)---- | Like 'mkThunk' but for synthetic thunks that reuse the same 'Expr'--- (e.g. @EApp (EResolvedVar 0 0) (EResolvedVar 0 1)@ in 'deferApply').--- Uses the 'Env' slots list for cell uniqueness instead of the expression,--- since GHC's full-laziness transform would otherwise float the shared--- expr to a CAF and all thunks would get the same IORef.------ Must only be called with freshly-constructed envs (not knot-tied--- recursive envs), since it forces the slots list to WHNF.-mkSyntheticThunk :: Env -> Expr -> Thunk-mkSyntheticThunk env thunkExpr =- ThunkRef (newSyntheticCell (envSlots env) thunkExpr env)---- | Like 'mkThunk' but avoids IORef allocation for trivial expressions.--- Resolved variables reuse the existing thunk from the env (no wrapper).--- Literals and lambdas use 'Evaluated' directly (no IORef needed).--- Everything else falls back to 'mkThunk'.-cheapThunk :: Env -> Expr -> Thunk-cheapThunk env (EResolvedVar level idx) = envLookupResolved level idx env-cheapThunk _ (ELit (NixInt n)) = Evaluated (VInt n)-cheapThunk _ (ELit (NixFloat n)) = Evaluated (VFloat n)-cheapThunk _ (ELit (NixBool b)) = Evaluated (VBool b)-cheapThunk _ (ELit NixNull) = Evaluated VNull-cheapThunk env (ELambda formals body NoCaptureInfo) = Evaluated (VLambda env formals body)-cheapThunk env (ELambda formals body (Captures captureList)) =- let trimmedEnv =- Env- { envSlots = smallArrayFromListN (length captureList) [envLookupResolved lvl idx env | (lvl, idx) <- captureList],- envLazyScope = Nothing,- envParent = Nothing,- envWithScopes = []- }- in Evaluated (VLambda trimmedEnv formals body)-cheapThunk env (ELambda formals body (CapturesWithScopes captureList)) =- let trimmedEnv =- Env- { envSlots = smallArrayFromListN (length captureList) [envLookupResolved lvl idx env | (lvl, idx) <- captureList],- envLazyScope = Nothing,- envParent = Nothing,- envWithScopes = withScopesForCapture env- }- in Evaluated (VLambda trimmedEnv formals body)-cheapThunk env expr = mkThunk env expr---- | Allocate a fresh memoization cell for a thunk.------ @NOINLINE@ prevents inlining so GHC can't see inside or CSE calls.--- @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 -> Env -> IORef ThunkCell-newMemoCell expr env = unsafePerformIO (expr `seq` newIORef (Pending expr env))---- | Like 'newMemoCell' but keyed on a SmallArray instead of an expression.--- Used by 'mkSyntheticThunk' where multiple thunks share the same expression.-{-# NOINLINE newSyntheticCell #-}-newSyntheticCell :: SmallArray Thunk -> Expr -> Env -> IORef ThunkCell-newSyntheticCell slots expr env = unsafePerformIO (slots `seq` newIORef (Pending expr env))---- | 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 (ThunkRef ref1) (ThunkRef ref2) = ref1 == ref2-thunkSameRef _ _ = False---- | Human-readable type name for error messages.-typeName :: NixValue -> Text-typeName val = case val of- VInt _ -> "an integer"- VFloat _ -> "a float"- VBool _ -> "a Boolean"- VNull -> "null"- VStr _ _ -> "a string"- VPath _ -> "a path"- VList _ -> "a list"- VAttrs _ -> "a set"- VLambda {} -> "a function"- VDerivation _ -> "a derivation"- VBuiltin _ _ -> "a built-in function"- VCompiledRegex _ -> "a built-in function"---- ------------------------------------------------------------------------------ Evaluation monad--- ------------------------------------------------------------------------------- | Effect class for Nix evaluation. Core logic is polymorphic in @m@--- so the same evaluator composes into 'PureEval' for tests or @IO@ for--- real file-system access (e.g. @import@, @readFile@).-class (Monad m) => MonadEval m where- throwEvalError :: Text -> m a- catchEvalError :: m a -> m (Either Text a)- readFileText :: Text -> m Text- doesPathExist :: Text -> m Bool-- -- | List a directory, returning @(name, fileType)@ pairs.- -- @fileType@ is one of @"regular"@, @"directory"@, or @"symlink"@- -- (matching Nix's @builtins.readDir@ semantics).- listDirectory :: Text -> m [(Text, Text)]-- importFile :: Text -> m NixValue-- -- | Look up an environment variable. Returns @""@ if unset.- getEnvVar :: Text -> m Text-- -- | Get the current epoch time (seconds since 1970-01-01).- getCurrentTime :: m Integer-- -- | Write a named file to the store, returning the store path.- writeToStore :: Text -> Text -> m Text-- -- | 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)-- -- | Resolve a path literal to an absolute path.- -- In IO evaluation, relative paths are resolved against the current- -- file's directory (@esBaseDir@). In pure evaluation, paths are- -- returned unchanged. This ensures that path values captured in- -- closures remain valid after the import scope ends.- resolvePathLiteral :: Text -> m Text-- -- | Copy a path (file or directory) to the store, returning the store path.- -- First argument is the source path, second is the store name.- copyPathToStore :: Text -> Text -> m Text-- -- | Print a trace/warning message.- -- IO evaluators write to stderr; pure evaluators silently discard.- traceMessage :: Text -> m ()-- -- | Force a thunk to a value, with memoization.- -- IO evaluators should cache results (via per-thunk @IORef@).- -- Pure evaluators re-evaluate each time.- -- The first argument is the evaluation function (to break the- -- Eval.Types → Eval circular dependency).- forceThunk :: (Env -> Expr -> m NixValue) -> Thunk -> m NixValue---- | Pure evaluation monad — wraps 'Either Text'.--- IO builtins ('readFile', 'import') are unavailable;--- everything else evaluates identically to the IO version.-newtype PureEval a = PureEval {runPureEval :: Either Text a}- deriving (Functor, Applicative, Monad)--instance MonadEval PureEval where- throwEvalError msg = PureEval (Left msg)- catchEvalError (PureEval action) = PureEval (Right action)- readFileText _ = throwEvalError "readFile: not available in pure evaluation"- doesPathExist _ = pure False- listDirectory _ = throwEvalError "builtins.readDir: not available in pure evaluation"- importFile _ = throwEvalError "import: not available in pure evaluation"- getEnvVar _ = pure ""- 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"- copyPathToStore _ _ = throwEvalError "builtins.path: not available in pure evaluation"- traceMessage _ = pure ()- resolvePathLiteral = pure- forceThunk _ (Evaluated val) = pure val- 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+ readThunkValue,++ -- * Attribute sets (C-backed sorted arrays)+ AttrSet (..),+ CAttrSet,+ attrSetLookup,+ attrSetKeys,+ attrSetToMap,+ attrSetFromMap,+ attrSetMember,+ attrSetNull,+ attrSetSize,+ attrSetElems,+ attrSetToAscList,+ attrSetMapWithKey,+ attrSetRemoveKeys,+ attrSetUnionWith,+ buildCAttrSetKeys,+ fillCAttrSetValues,++ -- * Lists (C-backed)+ CList (..),+ emptyCList,+ clistFromThunks,+ clistThunks,+ clistLen,++ -- * String context+ StringContextElement (..),+ StringContext (..),+ emptyContext,+ mkStr,+ marshalStringContext,+ unmarshalStringContext,++ -- * Environment+ Env (..),+ NnEnv,+ emptyEnv,+ envLookup,+ envLookupResolved,+ envFromSlots,+ pushWithScope,+ lookupWithScopes,+ withScopesForCapture,+ envWithScopesRaw,+ newCEnv,+ newMinimalEnv,++ -- * Eval-time formals (re-exported from EvalFormals)+ EvalFormals (..),+ EvalFormal (..),++ -- * Thunk operations+ mkThunk,+ mkSyntheticThunk,+ cheapThunk,+ cheapThunkBc,+ mkThunkBc,+ evaluated,+ thunkSameRef,+ thunkToCPtr,+ buildCSlots,+ allocCSlots,+ fillCSlots,++ -- * Lambda marshalling+ marshalLambda,+ unmarshalLambdaValue,++ -- * Display+ typeName,++ -- * Evaluation monad+ MonadEval (..),+ PureEval (..),+ )+where++import Control.Monad (forM_)+import Data.Bits (shiftL, (.&.), (.|.))+import Data.ByteString (ByteString)+import Data.Int (Int64)+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Set (Set)+import qualified Data.Set as Set+import Data.Text (Text)+import Data.Word (Word16, Word32, Word64, Word8)+import Foreign.Ptr (Ptr, castPtr, nullPtr, ptrToWordPtr)+import Foreign.StablePtr (castPtrToStablePtr, castStablePtrToPtr, deRefStablePtr, newStablePtr)+import Foreign.Storable (peekElemOff, pokeElemOff)+import GHC.Float (castWord64ToDouble)+import Nix.Derivation (Derivation)+import Nix.Eval.CAttrSet (CAttrSet, cattrsetFreeze, cattrsetGetKey, cattrsetGetValue, cattrsetIndex, cattrsetInsert, cattrsetKeys, cattrsetLookup, cattrsetNew, cattrsetRemoveKeys, cattrsetSetValue, cattrsetSize)+import Nix.Eval.CBytecode (cbcArg1, cbcArg2, cbcOpcode, cbcShortArg)+import Nix.Eval.CCtxStr (CCtxStrPtr, cctxstrCtxCount, cctxstrElemHash, cctxstrElemName, cctxstrElemOutput, cctxstrElemTag, cctxstrNew, cctxstrSetAllOutputs, cctxstrSetDrvOutput, cctxstrSetPlain, cctxstrText)+import Nix.Eval.CEnv (NnEnv, cenvAllocSlots, cenvAllocWithScopes, cenvEmpty, cenvFromSlots, cenvLazyScope, cenvLookupResolved, cenvNew, cenvNewMinimal, cenvParent, cenvPushWith, cenvRootScope, cenvSlotCount, cenvWithCount, cenvWithScopes)+import Nix.Eval.CLambda (clambdaAllowExtra, clambdaBody, clambdaEntryDefault, clambdaEntryHasDefault, clambdaEntryName, clambdaEnv, clambdaFormalCount, clambdaFormalsType, clambdaNameSym, clambdaNew, clambdaSetEntry)+import Nix.Eval.CList (CList (..), clistFromThunks, clistLen, clistThunks, emptyCList)+import Nix.Eval.CThunk (CThunkPtr, cthunkGetAttrs, cthunkGetBcIdx, cthunkGetBool, cthunkGetCtxStr, cthunkGetFloat, cthunkGetInt, cthunkGetList, cthunkGetPath, cthunkGetStr, cthunkNewBc, cthunkNewComputed, cthunkNewComputedAttrs, cthunkNewComputedBool, cthunkNewComputedCtxStr, cthunkNewComputedFloat, cthunkNewComputedInt, cthunkNewComputedLambda, cthunkNewComputedList, cthunkNewComputedNull, cthunkNewComputedPath, cthunkNewComputedStr, cthunkPayload, cthunkState, cthunkValueTag)+import Nix.Eval.Compile (compileExpr, compileFormalsToEval)+import Nix.Eval.EvalFormals (EvalFormal (..), EvalFormals (..))+import Nix.Eval.Symbol (Symbol (..), symbolIntern, symbolText)+import Nix.Expr.Types (CaptureInfo (..), Expr (..), NixAtom (..))+import Nix.Store.Path (StorePath (..))+import System.IO.Unsafe (unsafePerformIO)+import qualified Text.Regex.TDFA as RE++-- ---------------------------------------------------------------------------+-- Compiled regex+-- ---------------------------------------------------------------------------++-- | Compiled regex with Eq/Show based on source pattern text.+-- Carries the pre-compiled 'RE.Regex' alongside the original pattern+-- so that partial application of @builtins.match@ / @builtins.split@+-- avoids recompiling the same pattern on every invocation.+data CompiledRegex = CompiledRegex !Text RE.Regex++instance Eq CompiledRegex where+ CompiledRegex a _ == CompiledRegex b _ = a == b++instance Show CompiledRegex where+ show (CompiledRegex pat _) = "CompiledRegex " <> show pat++-- ---------------------------------------------------------------------------+-- String context+-- ---------------------------------------------------------------------------++-- | A single element of string context, tracking where a string+-- references store paths.+data StringContextElement+ = -- | Plain store path reference (inputSrcs).+ SCPlain !StorePath+ | -- | Derivation output reference (inputDrvs): .drv path + output name.+ SCDrvOutput !StorePath !Text+ | -- | All outputs of a derivation (for drvPath itself).+ SCAllOutputs !StorePath+ deriving (Eq, Ord, Show)++-- | Context carried by Nix strings, tracking store path dependencies.+newtype StringContext = StringContext {unStringContext :: Set StringContextElement}+ deriving (Eq, Ord, Show, Semigroup, Monoid)++-- | Empty string context (alias for 'mempty').+emptyContext :: StringContext+emptyContext = mempty++-- | Smart constructor for context-free strings.+mkStr :: Text -> NixValue+mkStr t = VStr t emptyContext++-- ---------------------------------------------------------------------------+-- Thunks+-- ---------------------------------------------------------------------------++-- | A thunk: a C arena-allocated memoization cell.+--+-- Each thunk points to an @nn_thunk_t@ in the C arena.+-- On first force, the cell transitions PENDING -> BLACKHOLE -> COMPUTED,+-- which detects infinite recursion (BLACKHOLE) and drops the Expr/Env+-- references — matching real Nix which mutates thunks in-place.+--+-- The C thunk is allocated via 'unsafePerformIO' in 'mkThunk' (same+-- pattern as the former IORef-based approach) so that knot-tying works+-- unchanged. Arena pointers remain valid until 'cthunkDestroy'.+newtype Thunk = Thunk {unThunk :: CThunkPtr}++instance Show Thunk where+ show (Thunk ptr) =+ case unsafePerformIO (cthunkState ptr) of+ 1 {- COMPUTED -} -> case readThunkValue (Thunk ptr) of+ Just val -> "Thunk (" ++ show val ++ ")"+ Nothing -> "Thunk <computed?>"+ 0 -> "Thunk <pending>"+ 2 -> "Thunk <blackhole>"+ _ -> "Thunk <unknown>"++-- | Equality: pointer identity fast path, then value comparison for+-- COMPUTED thunks. Only used in tests on non-recursive structures.+instance Eq Thunk where+ (Thunk p1) == (Thunk p2)+ | p1 == p2 = True+ | otherwise = case (readThunkValue (Thunk p1), readThunkValue (Thunk p2)) of+ (Just v1, Just v2) -> v1 == v2+ _ -> False++-- | Read a COMPUTED thunk's value without forcing.+-- Returns 'Nothing' for PENDING or BLACKHOLE thunks.+-- Uses 'unsafePerformIO' — safe because C reads are idempotent.+readThunkValue :: Thunk -> Maybe NixValue+readThunkValue (Thunk ptr) =+ unsafePerformIO $ do+ state <- cthunkState ptr+ if state /= 1+ then pure Nothing+ else do+ tag <- cthunkValueTag ptr+ fmap Just $ case tag of+ 0 {- INT -} -> VInt <$> cthunkGetInt ptr+ 1 {- FLOAT -} -> VFloat <$> cthunkGetFloat ptr+ 2 {- BOOL -} -> (\b -> VBool (b /= 0)) <$> cthunkGetBool ptr+ 3 {- NULL -} -> pure VNull+ 4 {- STR -} -> (\sym -> VStr (symbolText (Symbol sym)) emptyContext) <$> cthunkGetStr ptr+ 5 {- PATH -} -> VPath . symbolText . Symbol <$> cthunkGetPath ptr+ 6 {- LIST -} -> do+ listPtr <- cthunkGetList ptr+ pure (VList (CList (castPtr listPtr)))+ 7 {- ATTRS -} -> VAttrs . AttrSet . castPtr <$> cthunkGetAttrs ptr+ 8 {- CTXSTR -} -> do+ csptr <- cthunkGetCtxStr ptr+ uncurry VStr <$> unmarshalStringContext (castPtr csptr)+ 9 {- LAMBDA -} -> do+ lamRaw <- cthunkPayload ptr+ unmarshalLambdaValue (castPtr lamRaw)+ _ {- PTR -} -> do+ payload <- cthunkPayload ptr+ deRefStablePtr (castPtrToStablePtr payload)++-- | A Nix value — the result of evaluating an expression.+data NixValue+ = -- | 64-bit signed integer (matching Nix semantics).+ VInt !Int64+ | -- | Floating-point.+ VFloat !Double+ | -- | Boolean.+ VBool !Bool+ | -- | The null value.+ VNull+ | -- | String with dependency context.+ VStr !Text !StringContext+ | -- | Path.+ VPath !Text+ | -- | List of thunks (lazy elements), backed by C array.+ VList !CList+ | -- | Attribute set: unified lazy/eager representation.+ VAttrs !AttrSet+ | -- | Lambda closure: captures environment, formals, body bytecode index.+ VLambda !Env !EvalFormals !Word32+ | -- | A realized derivation (build recipe).+ VDerivation !Derivation+ | -- | Built-in function, dispatched by name.+ -- Accumulated args support curried partial application.+ VBuiltin !Text ![NixValue]+ | -- | Pre-compiled regex carried in a partially-applied builtin.+ -- Internal only — never exposed to Nix code directly.+ VCompiledRegex !CompiledRegex+ deriving (Eq, Show)++-- ---------------------------------------------------------------------------+-- Attribute sets (C-backed sorted arrays)+-- ---------------------------------------------------------------------------++-- | Attribute set backed by a C-allocated sorted key-value array.+--+-- All keys are interned symbols; values are CThunkPtrs in a parallel+-- array. O(log n) binary search on contiguous memory for lookup.+-- Replaces the former EagerAttrs\/LazyAttrs\/MappedAttrs\/CAttrs ADT+-- with a single C-backed representation — all attr set data lives off+-- the GHC heap, dramatically reducing GC pressure for large evaluations.+newtype AttrSet = AttrSet {unAttrSet :: CAttrSet}++instance Eq AttrSet where+ a == b = attrSetToMap a == attrSetToMap b++instance Show AttrSet where+ show (AttrSet cset) =+ let n = unsafePerformIO (cattrsetSize cset)+ in "<attrset " ++ show n ++ " entries>"++-- | Look up a single key via symbol interning + C binary search.+-- Uses 'unsafePerformIO' — safe because symbolIntern and cattrsetLookup+-- are idempotent (same key always yields same symbol and same result).+attrSetLookup :: Text -> AttrSet -> Maybe Thunk+attrSetLookup key (AttrSet cset) =+ unsafePerformIO $ do+ sym <- symbolIntern key+ mptr <- cattrsetLookup cset sym+ pure (fmap Thunk mptr)++-- | All keys in sorted order (symbol text from C).+attrSetKeys :: AttrSet -> [Text]+attrSetKeys (AttrSet cset) =+ map symbolText (unsafePerformIO (cattrsetKeys cset))++-- | Check key membership without materializing thunks.+attrSetMember :: Text -> AttrSet -> Bool+attrSetMember key (AttrSet cset) =+ unsafePerformIO $ do+ sym <- symbolIntern key+ midx <- cattrsetIndex cset sym+ pure (case midx of Nothing -> False; Just _ -> True)++-- | Check if the attribute set is empty.+attrSetNull :: AttrSet -> Bool+attrSetNull (AttrSet cset) =+ unsafePerformIO (cattrsetSize cset) == 0++-- | Number of attributes.+attrSetSize :: AttrSet -> Int+attrSetSize (AttrSet cset) =+ fromIntegral (unsafePerformIO (cattrsetSize cset))++-- | Full materialization: build a 'Map Text Thunk' from all entries.+-- Iterates the C array and builds a Haskell Map. Expensive on large+-- sets — avoid on the hot path.+attrSetToMap :: AttrSet -> Map Text Thunk+attrSetToMap (AttrSet cset) =+ unsafePerformIO $ do+ n <- cattrsetSize cset+ if n == 0+ then pure Map.empty+ else do+ pairs <- mapM materializeEntry [0 .. n - 1]+ pure (Map.fromList pairs)+ where+ materializeEntry i = do+ sym <- cattrsetGetKey cset i+ ptr <- cattrsetGetValue cset i+ pure (symbolText sym, Thunk ptr)++-- | 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, building a new CAttrSet.+-- Materializes the source set, applies @f@ to each entry, and rebuilds.+attrSetMapWithKey :: (Text -> Thunk -> Thunk) -> AttrSet -> AttrSet+attrSetMapWithKey f attrs = attrSetFromMap (Map.mapWithKey f (attrSetToMap attrs))++-- | Remove a list of keys, returning a new CAttrSet.+-- Uses the C-side @nn_attrset_remove_keys@ for O(n) removal.+{-# NOINLINE attrSetRemoveKeys #-}+attrSetRemoveKeys :: [Text] -> AttrSet -> AttrSet+attrSetRemoveKeys keys (AttrSet cset) = unsafePerformIO $ do+ syms <- mapM symbolIntern keys+ newSet <- cattrsetRemoveKeys cset syms+ pure (AttrSet newSet)++-- | Union two attribute sets with a combining function.+-- Materializes both to Maps, applies the combining function, rebuilds.+attrSetUnionWith :: (Thunk -> Thunk -> Thunk) -> AttrSet -> AttrSet -> AttrSet+attrSetUnionWith f a b = attrSetFromMap (Map.unionWith f (attrSetToMap a) (attrSetToMap b))++-- | Build a CAttrSet from a Haskell 'Map'. Interns all keys as symbols,+-- inserts key-value pairs, freezes (sort + dedup). The canonical entry+-- point for all attribute set construction.+--+-- Uses 'unsafePerformIO' with @NOINLINE@ — safe because C allocation+-- is idempotent and the resulting CAttrSet is referentially transparent.+{-# NOINLINE attrSetFromMap #-}+attrSetFromMap :: Map Text Thunk -> AttrSet+attrSetFromMap m = m `seq` unsafePerformIO $ do+ let pairs = Map.toList m+ n = fromIntegral (length pairs)+ cset <- cattrsetNew n+ forM_ pairs $ \(key, Thunk ptr) -> do+ sym <- symbolIntern key+ cattrsetInsert cset sym ptr+ cattrsetFreeze cset+ pure (AttrSet cset)++-- | Allocate a CAttrSet skeleton with keys only (NULL values).+-- Used for two-phase construction in @rec {}@ and @let@ (knot-tying):+-- keys are known before thunks, so the CAttrSet is built first, then+-- values are filled via 'fillCAttrSetValues'.+{-# NOINLINE buildCAttrSetKeys #-}+buildCAttrSetKeys :: [Text] -> CAttrSet+buildCAttrSetKeys keys = unsafePerformIO $ do+ let n = fromIntegral (length keys)+ cset <- cattrsetNew n+ forM_ keys $ \key -> do+ sym <- symbolIntern key+ cattrsetInsert cset sym nullPtr+ cattrsetFreeze cset+ pure cset++-- | Fill values into a pre-allocated CAttrSet (two-phase construction).+-- Looks up each key's index and writes the CThunkPtr at that position.+-- Keys not found in the CAttrSet are silently ignored (should not happen+-- in correct knot-tying code).+{-# NOINLINE fillCAttrSetValues #-}+fillCAttrSetValues :: CAttrSet -> Map Text Thunk -> ()+fillCAttrSetValues cset thunkMap = unsafePerformIO $+ forM_ (Map.toList thunkMap) $ \(key, Thunk ptr) -> do+ sym <- symbolIntern key+ midx <- cattrsetIndex cset sym+ case midx of+ Just idx -> cattrsetSetValue cset idx ptr+ Nothing -> pure ()++-- | Evaluation environment — C-backed scope chain.+--+-- Arena-allocated @nn_env_t@ struct (40 bytes, zero GC overhead).+-- All env data (slots, lazy scope, parent, with-scopes) lives in C.+-- Haskell holds only the pointer. Variable lookup has two paths:+--+-- * 'envLookupResolved': single C call for 'EResolvedVar'+-- * 'envLookup': name-based walk for 'EVar' (lazy scope + with-scopes)+newtype Env = Env (Ptr NnEnv)++-- | Pointer equality: two envs are equal iff they are the same C struct.+instance Eq Env where+ Env p1 == Env p2 = p1 == p2++-- | Compact show via C accessors.+instance Show Env where+ show (Env envPtr) =+ let sc = unsafePerformIO (cenvSlotCount envPtr)+ ls = unsafePerformIO (cenvLazyScope envPtr)+ par = unsafePerformIO (cenvParent envPtr)+ wc = unsafePerformIO (cenvWithCount envPtr)+ in "Env{"+ ++ show sc+ ++ " slots"+ ++ (if ls /= nullPtr then ", lazyScope" else "")+ ++ (if par /= nullPtr then ", parent" else "")+ ++ ", "+ ++ show wc+ ++ " withs}"++-- | Empty environment (no variables in scope).+-- Points to a static global C struct — valid until 'arenaDestroy'.+{-# NOINLINE emptyEnv #-}+emptyEnv :: Env+emptyEnv = Env (unsafePerformIO cenvEmpty)++-- | General C-backed env constructor.+-- Takes Haskell-level types; converts Maybe to nullPtr internally.+{-# NOINLINE newCEnv #-}+newCEnv :: Ptr CThunkPtr -> Int -> Maybe AttrSet -> Maybe Env -> Ptr (Ptr ()) -> Word16 -> Env+newCEnv slots slotCount lazyScope parent withs withCount =+ Env+ ( unsafePerformIO+ ( cenvNew+ slots+ (fromIntegral slotCount)+ (case lazyScope of Nothing -> nullPtr; Just (AttrSet cset) -> castPtr cset)+ (case parent of Nothing -> nullPtr; Just (Env p) -> p)+ withs+ withCount+ )+ )++-- | Minimal env: slots only, no parent, no with-scopes, no lazy scope.+{-# NOINLINE newMinimalEnv #-}+newMinimalEnv :: Ptr CThunkPtr -> Int -> Env+newMinimalEnv slots n =+ Env (unsafePerformIO (cenvNewMinimal slots (fromIntegral n)))++-- | Look up a resolved variable by level and index. Single C call:+-- O(level) parent hops in C, then O(1) array read.+envLookupResolved :: Int -> Int -> Env -> Thunk+envLookupResolved level idx (Env envPtr) =+ Thunk (unsafePerformIO (cenvLookupResolved envPtr level idx))++-- | Name-based variable lookup: walk the parent chain checking+-- lazy scopes; fall back to with-scopes (from the starting env).+--+-- Positional slots are NOT searched here — used only for+-- 'EVar' lookups (let\/rec bindings, builtins, with-scopes).+envLookup :: Text -> Env -> Maybe Thunk+envLookup name (Env envPtr) = lexicalLookup envPtr+ where+ -- With-scopes from the STARTING env (C array)+ startWiths = unsafePerformIO (cenvWithScopes envPtr)+ startWithCount = unsafePerformIO (cenvWithCount envPtr)+ lexicalLookup ep =+ let ls = unsafePerformIO (cenvLazyScope ep)+ scopeResult =+ if ls /= nullPtr+ then attrSetLookup name (AttrSet (castPtr ls))+ else Nothing+ in case scopeResult of+ Just val -> Just val+ Nothing ->+ let par = unsafePerformIO (cenvParent ep)+ in if par /= nullPtr+ then lexicalLookup par+ else lookupWithScopesC name startWiths startWithCount++-- | Walk with-scopes (C array) innermost to outermost.+-- Skips tagged lazy entries (bit 0 set) — those are thunk pointers+-- that can only be resolved by 'evalWithVarScopes' which has monadic+-- 'force'. This is a safety guard: currently unreachable because+-- 'resolveVars' ensures EVar behind a with-scope always becomes+-- EWithVar, but protects against future changes.+lookupWithScopesC :: Text -> Ptr (Ptr ()) -> Word16 -> Maybe Thunk+lookupWithScopesC _ _ 0 = Nothing+lookupWithScopesC name withArr count = unsafePerformIO $ go 0+ where+ go i+ | i >= fromIntegral count = pure Nothing+ | otherwise = do+ scopePtr <- peekElemOff withArr i+ if ptrToWordPtr scopePtr .&. 1 /= 0+ then go (i + 1) -- skip tagged lazy with-scope+ else case attrSetLookup name (AttrSet (castPtr scopePtr)) of+ Just val -> pure (Just val)+ Nothing -> go (i + 1)++-- | Walk with-scopes as a Haskell list (backward-compatible signature).+lookupWithScopes :: Text -> [AttrSet] -> Maybe Thunk+lookupWithScopes _ [] = Nothing+lookupWithScopes name (scope : rest) =+ case attrSetLookup name scope of+ Just val -> Just val+ Nothing -> lookupWithScopes name rest++-- | Create a child env with positional slots (for lambda formals).+-- Inherits with-scopes from the parent. Arena-allocated.+{-# NOINLINE envFromSlots #-}+envFromSlots :: Ptr CThunkPtr -> Int -> Env -> Env+envFromSlots slotsPtr slotCount (Env parentPtr) =+ Env (unsafePerformIO (cenvFromSlots slotsPtr (fromIntegral slotCount) parentPtr))++-- | Push a with-scope onto the scope chain (innermost position).+-- Allocates a new C env struct with extended with-scopes array.+{-# NOINLINE pushWithScope #-}+pushWithScope :: AttrSet -> Env -> Env+pushWithScope (AttrSet cset) (Env envPtr) =+ Env (unsafePerformIO (cenvPushWith envPtr (castPtr cset)))++-- | Read with-scopes array pointer and count from a C env.+{-# NOINLINE envWithScopesRaw #-}+envWithScopesRaw :: Env -> (Ptr (Ptr ()), Word16)+envWithScopesRaw (Env envPtr) = unsafePerformIO $ do+ withs <- cenvWithScopes envPtr+ count <- cenvWithCount envPtr+ pure (withs, count)++-- | Build with-scopes for a trimmed env that needs with-scope access+-- ('CapturesWithScopes'). Appends the root scope (builtins) as the+-- outermost entry so 'EWithVar' can fall back to builtins without+-- retaining the parent chain. Returns C array + count.+{-# NOINLINE withScopesForCapture #-}+withScopesForCapture :: Env -> (Ptr (Ptr ()), Word16)+withScopesForCapture (Env envPtr) = unsafePerformIO $ do+ rootPtr <- cenvRootScope envPtr+ existingWiths <- cenvWithScopes envPtr+ existingCount <- cenvWithCount envPtr+ if rootPtr == nullPtr+ then pure (existingWiths, existingCount)+ else do+ let newCount = existingCount + 1+ arr <- cenvAllocWithScopes newCount+ -- Copy existing with-scopes+ forM_ [0 .. fromIntegral existingCount - 1] $ \i -> do+ val <- peekElemOff existingWiths i+ pokeElemOff arr i val+ -- Append root scope at end (outermost)+ pokeElemOff arr (fromIntegral existingCount) rootPtr+ pure (arr, newCount)++-- | Create an unevaluated thunk with a fresh C arena-allocated cell.+--+-- Compiles the Expr to bytecode and stores (bc_idx, env_ptr) in the+-- C thunk — no StablePtr, zero GHC heap pressure for pending thunks.+-- The cell is allocated via 'unsafePerformIO' — safe because C+-- allocation is a pure side effect, 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 (newBcThunkPtr thunkExpr env)++-- | Like 'mkThunk' but for synthetic thunks that reuse the same 'Expr'+-- (e.g. @EApp (EResolvedVar 0 0) (EResolvedVar 0 1)@ in 'deferApply').+-- Uses the env pointer for cell uniqueness instead of the expression,+-- since GHC's full-laziness transform would otherwise float the shared+-- expr to a CAF and all thunks would get the same cell.+--+-- Must only be called with freshly-constructed envs (not knot-tied+-- recursive envs), since it forces the env pointer to WHNF.+mkSyntheticThunk :: Env -> Expr -> Thunk+mkSyntheticThunk env@(Env envPtr) thunkExpr =+ Thunk (newSyntheticBcThunkPtr envPtr thunkExpr env)++-- | Like 'mkThunk' but avoids C arena allocation for trivial expressions.+-- Resolved variables reuse the existing thunk from the env (no wrapper).+-- Literals use inline C scalars (no StablePtr).+-- Lambdas compile formals+body to bytecode and produce VLambda directly.+-- Everything else falls back to 'mkThunk'.+cheapThunk :: Env -> Expr -> Thunk+cheapThunk env (EResolvedVar level idx) = envLookupResolved level idx env+cheapThunk _ (ELit (NixInt n)) = Thunk (newComputedIntPtr n)+cheapThunk _ (ELit (NixFloat n)) = Thunk (newComputedFloatPtr n)+cheapThunk _ (ELit (NixBool b)) = Thunk (newComputedBoolPtr (if b then 1 else 0))+cheapThunk _ (ELit NixNull) = Thunk newComputedNullPtr+cheapThunk env (ELambda formals body NoCaptureInfo) =+ let bodyBcIdx = unsafePerformIO (compileExpr body)+ evalFormals = unsafePerformIO (compileFormalsToEval formals)+ in evaluated (VLambda env evalFormals bodyBcIdx)+cheapThunk env (ELambda formals body (Captures captureList)) =+ let (slotsPtr, slotCount) = buildCSlots [envLookupResolved lvl idx env | (lvl, idx) <- captureList]+ trimmedEnv = newMinimalEnv slotsPtr slotCount+ bodyBcIdx = unsafePerformIO (compileExpr body)+ evalFormals = unsafePerformIO (compileFormalsToEval formals)+ in evaluated (VLambda trimmedEnv evalFormals bodyBcIdx)+cheapThunk env (ELambda formals body (CapturesWithScopes captureList)) =+ let (slotsPtr, slotCount) = buildCSlots [envLookupResolved lvl idx env | (lvl, idx) <- captureList]+ (withArr, withCount) = withScopesForCapture env+ trimmedEnv = newCEnv slotsPtr slotCount Nothing Nothing withArr withCount+ bodyBcIdx = unsafePerformIO (compileExpr body)+ evalFormals = unsafePerformIO (compileFormalsToEval formals)+ in evaluated (VLambda trimmedEnv evalFormals bodyBcIdx)+cheapThunk env expr = mkThunk env expr++-- | Create a pending thunk from a bytecode index (no compilation needed).+-- Used inside 'evalBytecode' where the bc_idx is already known.+-- The Env is captured LAZILY (for knot-tying in rec attrs / let / matchFormalSet).+mkThunkBc :: Env -> Word32 -> Thunk+mkThunkBc env bcIdx =+ Thunk (newBcThunkPtrLazy bcIdx env)++-- | Like 'cheapThunk' but for bytecode indices (used inside evalBytecode).+-- Short-circuits for resolved vars and literals without creating a+-- pending thunk. Everything else falls back to 'mkThunkBc'.+cheapThunkBc :: Env -> Word32 -> Thunk+cheapThunkBc env bcIdx =+ let opcode = unsafePerformIO (cbcOpcode bcIdx)+ in case opcode of+ 10 {- RESOLVED_VAR -} ->+ let level = fromIntegral (unsafePerformIO (cbcArg1 bcIdx))+ idx = fromIntegral (unsafePerformIO (cbcArg2 bcIdx))+ in envLookupResolved level idx env+ 0 {- LIT_INT -} ->+ let lo = unsafePerformIO (cbcArg1 bcIdx)+ hi = unsafePerformIO (cbcArg2 bcIdx)+ w64 = fromIntegral lo .|. (fromIntegral hi `shiftL` 32) :: Word64+ in Thunk (newComputedIntPtr (fromIntegral w64 :: Int64))+ 1 {- LIT_FLOAT -} ->+ let lo = unsafePerformIO (cbcArg1 bcIdx)+ hi = unsafePerformIO (cbcArg2 bcIdx)+ w64 = fromIntegral lo .|. (fromIntegral hi `shiftL` 32) :: Word64+ in Thunk (newComputedFloatPtr (castWord64ToDouble w64))+ 2 {- LIT_BOOL -} ->+ let flag = unsafePerformIO (cbcShortArg bcIdx)+ in Thunk (newComputedBoolPtr (if flag /= 0 then 1 else 0))+ 3 {- LIT_NULL -} -> Thunk newComputedNullPtr+ _ -> mkThunkBc env bcIdx++-- | Allocate a fresh C arena thunk cell with bytecode.+--+-- @NOINLINE@ prevents inlining so GHC can't see inside or CSE calls.+-- @seq@ on the expr creates a data dependency that prevents float-out+-- to a top-level CAF — without this, GHC would hoist the+-- @unsafePerformIO@ and share ONE cell across ALL thunks.+--+-- Compiles the Expr to bytecode and stores (bc_idx, StablePtr Env)+-- in the C thunk. The Expr tree is eliminated from the GHC heap —+-- only the StablePtr Env (~16 bytes) remains for knot-tying laziness.+{-# NOINLINE newBcThunkPtr #-}+newBcThunkPtr :: Expr -> Env -> CThunkPtr+newBcThunkPtr expr env =+ unsafePerformIO $ expr `seq` do+ bcIdx <- compileExpr expr+ sp <- newStablePtr env+ cthunkNewBc bcIdx (castStablePtrToPtr sp)++-- | Like 'newBcThunkPtr' but keyed on the env's C pointer instead+-- of the expression. Used by 'mkSyntheticThunk' where multiple thunks+-- share the same expression (e.g. 'deferApplyExpr').+{-# NOINLINE newSyntheticBcThunkPtr #-}+newSyntheticBcThunkPtr :: Ptr NnEnv -> Expr -> Env -> CThunkPtr+newSyntheticBcThunkPtr envKey expr env =+ unsafePerformIO $ envKey `seq` do+ bcIdx <- compileExpr expr+ sp <- newStablePtr env+ cthunkNewBc bcIdx (castStablePtrToPtr sp)++-- | Allocate a fresh C arena thunk cell from a known bytecode index.+-- No compilation needed — the bc_idx is already available.+--+-- Uses StablePtr for the Env so it stays lazy — essential for+-- knot-tying in recursive attrs, let bindings, and matchFormalSet.+-- The StablePtr points to an Env (newtype around Ptr NnEnv) — ~16+-- bytes on the GHC heap, negligible compared to the Expr trees we+-- eliminated.+{-# NOINLINE newBcThunkPtrLazy #-}+newBcThunkPtrLazy :: Word32 -> Env -> CThunkPtr+newBcThunkPtrLazy bcIdx env =+ unsafePerformIO $ bcIdx `seq` do+ sp <- newStablePtr env+ cthunkNewBc bcIdx (castStablePtrToPtr sp)++-- | Wrap an already-computed value as a thunk.+-- Scalars (int/float/bool/null) use inline C thunks (no StablePtr).+-- Attrs, paths, and context-free strings use C-native tags (no StablePtr).+-- Other complex values fall back to StablePtr-backed C thunks.+evaluated :: NixValue -> Thunk+evaluated (VInt n) = Thunk (newComputedIntPtr n)+evaluated (VFloat d) = Thunk (newComputedFloatPtr d)+evaluated (VBool b) = Thunk (newComputedBoolPtr (if b then 1 else 0))+evaluated VNull = Thunk newComputedNullPtr+evaluated (VAttrs (AttrSet cset)) = Thunk (newComputedAttrsPtr cset)+evaluated (VPath p) = Thunk (newComputedPathPtr p)+evaluated (VStr t ctx)+ | ctx == emptyContext = Thunk (newComputedStrPtr t)+ | otherwise = Thunk (newComputedCtxStrPtr t ctx)+evaluated (VList cl) = Thunk (newComputedListPtrC cl)+evaluated (VLambda env formals bodyBcIdx) = Thunk (newComputedLambdaPtr env formals bodyBcIdx)+evaluated val = Thunk (newComputedThunkPtr val)++-- | Check if two thunks share the same memoization cell (C pointer+-- equality). When true, both thunks will always produce the same value,+-- so deep equality can short-circuit to 'True' without forcing.+thunkSameRef :: Thunk -> Thunk -> Bool+thunkSameRef (Thunk p1) (Thunk p2) = p1 == p2++-- | Extract the C pointer from a thunk (zero-cost, newtype unwrap).+thunkToCPtr :: Thunk -> CThunkPtr+thunkToCPtr (Thunk ptr) = ptr++-- | Wrap an already-computed 'NixValue' in a C thunk (StablePtr path).+-- Arena-allocated (O(1)). Used for complex values (string, list, attrs, etc.).+{-# NOINLINE newComputedThunkPtr #-}+newComputedThunkPtr :: NixValue -> CThunkPtr+newComputedThunkPtr val =+ unsafePerformIO $ val `seq` do+ sp <- newStablePtr val+ cthunkNewComputed (castStablePtrToPtr sp)++-- | Wrap an int64 in a pre-computed C thunk (inline, no StablePtr).+{-# NOINLINE newComputedIntPtr #-}+newComputedIntPtr :: Int64 -> CThunkPtr+newComputedIntPtr n = unsafePerformIO (cthunkNewComputedInt n)++-- | Wrap a double in a pre-computed C thunk (inline, no StablePtr).+{-# NOINLINE newComputedFloatPtr #-}+newComputedFloatPtr :: Double -> CThunkPtr+newComputedFloatPtr d = unsafePerformIO (cthunkNewComputedFloat d)++-- | Wrap a bool in a pre-computed C thunk (inline, no StablePtr).+{-# NOINLINE newComputedBoolPtr #-}+newComputedBoolPtr :: Word8 -> CThunkPtr+newComputedBoolPtr b = unsafePerformIO (cthunkNewComputedBool b)++-- | Wrap null in a pre-computed C thunk (inline, no StablePtr).+{-# NOINLINE newComputedNullPtr #-}+newComputedNullPtr :: CThunkPtr+newComputedNullPtr = unsafePerformIO cthunkNewComputedNull++-- | Wrap a context-free string in a pre-computed C thunk (interned symbol, no StablePtr).+{-# NOINLINE newComputedStrPtr #-}+newComputedStrPtr :: Text -> CThunkPtr+newComputedStrPtr t =+ unsafePerformIO $ t `seq` do+ Symbol sym <- symbolIntern t+ cthunkNewComputedStr sym++-- | Wrap a path in a pre-computed C thunk (interned symbol, no StablePtr).+{-# NOINLINE newComputedPathPtr #-}+newComputedPathPtr :: Text -> CThunkPtr+newComputedPathPtr p =+ unsafePerformIO $ p `seq` do+ Symbol sym <- symbolIntern p+ cthunkNewComputedPath sym++-- | Wrap a CList in a pre-computed C thunk (no conversion needed).+{-# NOINLINE newComputedListPtrC #-}+newComputedListPtrC :: CList -> CThunkPtr+newComputedListPtrC (CList clistPtr) =+ unsafePerformIO $+ clistPtr `seq`+ cthunkNewComputedList (castPtr clistPtr)++-- | Wrap a string with context in a pre-computed C thunk (no StablePtr).+-- Interns the text and all StorePath fields as symbols, builds nn_ctxstr_t.+{-# NOINLINE newComputedCtxStrPtr #-}+newComputedCtxStrPtr :: Text -> StringContext -> CThunkPtr+newComputedCtxStrPtr t ctx =+ unsafePerformIO $+ t `seq`+ ctx `seq` do+ ptr <- marshalStringContext t ctx+ cthunkNewComputedCtxStr (castPtr ptr)++-- | Wrap a lambda closure in a pre-computed C thunk (no StablePtr).+-- Marshals EvalFormals to nn_lambda_t, stores as tag 9.+{-# NOINLINE newComputedLambdaPtr #-}+newComputedLambdaPtr :: Env -> EvalFormals -> Word32 -> CThunkPtr+newComputedLambdaPtr (Env envPtr) formals bodyBcIdx =+ unsafePerformIO $+ formals `seq` do+ clam <- marshalLambda envPtr formals bodyBcIdx+ cthunkNewComputedLambda clam++-- | Marshal Env + EvalFormals + body bc_idx to a C nn_lambda_t.+marshalLambda :: Ptr NnEnv -> EvalFormals -> Word32 -> IO (Ptr ())+marshalLambda envPtr formals bodyBcIdx = case formals of+ EFName name -> do+ Symbol nameSym <- symbolIntern name+ lam <- clambdaNew envPtr bodyBcIdx 0 nameSym 0 0+ pure (castPtr lam)+ EFSet entries allowExtra -> do+ let count = fromIntegral (length entries) :: Word16+ extraFlag = if allowExtra then 1 else 0 :: Word8+ lam <- clambdaNew envPtr bodyBcIdx 1 0 extraFlag count+ fillEntries lam 0 entries+ pure (castPtr lam)+ EFNamedSet name entries allowExtra -> do+ Symbol nameSym <- symbolIntern name+ let count = fromIntegral (length entries) :: Word16+ extraFlag = if allowExtra then 1 else 0 :: Word8+ lam <- clambdaNew envPtr bodyBcIdx 2 nameSym extraFlag count+ fillEntries lam 0 entries+ pure (castPtr lam)+ where+ fillEntries _ _ [] = pure ()+ fillEntries lam !idx (EvalFormal name defBcIdx : rest) = do+ Symbol nameSym <- symbolIntern name+ let (hasDef, defIdx) = case defBcIdx of+ Nothing -> (0, 0)+ Just di -> (1, di)+ clambdaSetEntry lam idx nameSym hasDef defIdx+ fillEntries lam (idx + 1) rest++-- | Read a C nn_lambda_t back into VLambda.+-- Reconstructs Env (newtype wrap), EvalFormals (from symbol IDs), body bc_idx.+unmarshalLambdaValue :: Ptr () -> IO NixValue+unmarshalLambdaValue rawPtr = do+ let lamPtr = castPtr rawPtr+ envPtr <- clambdaEnv lamPtr+ bodyIdx <- clambdaBody lamPtr+ formalsType <- clambdaFormalsType lamPtr+ formals <- case formalsType of+ 0 {- Name -} -> do+ nameSym <- clambdaNameSym lamPtr+ pure (EFName (symbolText (Symbol nameSym)))+ 1 {- Set -} -> do+ count <- clambdaFormalCount lamPtr+ extra <- clambdaAllowExtra lamPtr+ entries <- readLambdaEntries lamPtr count+ pure (EFSet entries (extra /= 0))+ _ {- NamedSet -} -> do+ nameSym <- clambdaNameSym lamPtr+ count <- clambdaFormalCount lamPtr+ extra <- clambdaAllowExtra lamPtr+ entries <- readLambdaEntries lamPtr count+ pure (EFNamedSet (symbolText (Symbol nameSym)) entries (extra /= 0))+ pure (VLambda (Env envPtr) formals bodyIdx)+ where+ readLambdaEntries lamPtr count = mapM (readOneEntry lamPtr) [0 .. count - 1]+ readOneEntry lamPtr idx = do+ nameSym <- clambdaEntryName lamPtr idx+ hasDef <- clambdaEntryHasDefault lamPtr idx+ defIdx <- clambdaEntryDefault lamPtr idx+ let defMaybe = if hasDef /= 0 then Just defIdx else Nothing+ pure (EvalFormal (symbolText (Symbol nameSym)) defMaybe)++-- | Marshal Text + StringContext to a C nn_ctxstr_t.+marshalStringContext :: Text -> StringContext -> IO CCtxStrPtr+marshalStringContext textVal (StringContext ctxSet) = do+ Symbol textSym <- symbolIntern textVal+ let elems = Set.toAscList ctxSet+ count = fromIntegral (length elems) :: Word16+ ptr <- cctxstrNew textSym count+ fillElems ptr 0 elems+ pure ptr+ where+ fillElems _ _ [] = pure ()+ fillElems ptr !idx (e : es) = do+ marshalElem ptr idx e+ fillElems ptr (idx + 1) es+ marshalElem eptr eidx (SCPlain (StorePath h n)) = do+ Symbol hashSym <- symbolIntern h+ Symbol nameSym <- symbolIntern n+ cctxstrSetPlain eptr eidx hashSym nameSym+ marshalElem eptr eidx (SCDrvOutput (StorePath h n) out) = do+ Symbol hashSym <- symbolIntern h+ Symbol nameSym <- symbolIntern n+ Symbol outSym <- symbolIntern out+ cctxstrSetDrvOutput eptr eidx hashSym nameSym outSym+ marshalElem eptr eidx (SCAllOutputs (StorePath h n)) = do+ Symbol hashSym <- symbolIntern h+ Symbol nameSym <- symbolIntern n+ cctxstrSetAllOutputs eptr eidx hashSym nameSym++-- | Unmarshal a C nn_ctxstr_t back to (Text, StringContext).+unmarshalStringContext :: CCtxStrPtr -> IO (Text, StringContext)+unmarshalStringContext ptr = do+ textSym <- cctxstrText ptr+ count <- cctxstrCtxCount ptr+ let textVal = symbolText (Symbol textSym)+ elems <- mapM (readElem ptr) [0 .. count - 1]+ pure (textVal, StringContext (Set.fromList elems))+ where+ readElem cptr idx = do+ tag <- cctxstrElemTag cptr idx+ hashSym <- cctxstrElemHash cptr idx+ nameSym <- cctxstrElemName cptr idx+ let sp = StorePath (symbolText (Symbol hashSym)) (symbolText (Symbol nameSym))+ case tag of+ 0 -> pure (SCPlain sp)+ 1 -> do+ outSym <- cctxstrElemOutput cptr idx+ pure (SCDrvOutput sp (symbolText (Symbol outSym)))+ _ -> pure (SCAllOutputs sp)++-- | Wrap a CAttrSet in a pre-computed C thunk (pointer, no StablePtr).+{-# NOINLINE newComputedAttrsPtr #-}+newComputedAttrsPtr :: CAttrSet -> CThunkPtr+newComputedAttrsPtr cset =+ unsafePerformIO $+ cset `seq`+ cthunkNewComputedAttrs (castPtr cset)++-- | Build a C-allocated slot array from a list of 'Thunk' values.+-- Each thunk is converted to 'CThunkPtr' via 'thunkToCPtr'.+-- Returns the C array pointer and the slot count.+-- Uses 'unsafePerformIO' — safe because allocation is idempotent.+{-# NOINLINE buildCSlots #-}+buildCSlots :: [Thunk] -> (Ptr CThunkPtr, Int)+buildCSlots thunks = unsafePerformIO $ do+ let n = length thunks+ if n == 0+ then pure (nullPtr, 0)+ else do+ arr <- cenvAllocSlots (fromIntegral n)+ pokeSlots arr 0 thunks+ pure (arr, n)+ where+ pokeSlots _ _ [] = pure ()+ pokeSlots arr i (t : ts) = do+ pokeElemOff arr i (thunkToCPtr t)+ pokeSlots arr (i + 1) ts++-- | Allocate a C slot array of the given size WITHOUT filling it.+-- Returns 'nullPtr' for size 0. Used by knot-tying sites (rec {},+-- let) where the Env must reference the slot pointer before the+-- thunks are materialized. Caller MUST fill all slots via 'fillCSlots'+-- before any slot is read (e.g. before forcing any thunk).+{-# NOINLINE allocCSlots #-}+allocCSlots :: Int -> Ptr CThunkPtr+allocCSlots 0 = nullPtr+allocCSlots n = unsafePerformIO (cenvAllocSlots (fromIntegral n))++-- | Fill a pre-allocated C slot array with thunks. Each thunk is+-- converted to 'CThunkPtr' via 'thunkToCPtr' and poked at its index.+-- The list length MUST equal the allocated array size.+-- Used after 'allocCSlots' in knot-tying contexts.+{-# NOINLINE fillCSlots #-}+fillCSlots :: Ptr CThunkPtr -> [Thunk] -> ()+fillCSlots arr thunks = unsafePerformIO (go 0 thunks)+ where+ go _ [] = pure ()+ go !i (t : ts) = do+ pokeElemOff arr i (thunkToCPtr t)+ go (i + 1) ts++-- | Human-readable type name for error messages.+typeName :: NixValue -> Text+typeName val = case val of+ VInt _ -> "an integer"+ VFloat _ -> "a float"+ VBool _ -> "a Boolean"+ VNull -> "null"+ VStr _ _ -> "a string"+ VPath _ -> "a path"+ VList _ -> "a list"+ VAttrs _ -> "a set"+ VLambda {} -> "a function"+ VDerivation _ -> "a derivation"+ VBuiltin _ _ -> "a built-in function"+ VCompiledRegex _ -> "a built-in function"++-- ---------------------------------------------------------------------------+-- Evaluation monad+-- ---------------------------------------------------------------------------++-- | Effect class for Nix evaluation. Core logic is polymorphic in @m@+-- so the same evaluator composes into 'PureEval' for tests or @IO@ for+-- real file-system access (e.g. @import@, @readFile@).+class (Monad m) => MonadEval m where+ throwEvalError :: Text -> m a++ -- | Abort evaluation (uncatchable by tryEval, matching real Nix).+ abortEvaluation :: Text -> m a++ catchEvalError :: m a -> m (Either Text a)+ readFileText :: Text -> m Text+ doesPathExist :: Text -> m Bool++ -- | List a directory, returning @(name, fileType)@ pairs.+ -- @fileType@ is one of @"regular"@, @"directory"@, or @"symlink"@+ -- (matching Nix's @builtins.readDir@ semantics).+ listDirectory :: Text -> m [(Text, Text)]++ importFile :: Text -> m NixValue++ -- | Look up an environment variable. Returns @""@ if unset.+ getEnvVar :: Text -> m Text++ -- | Get the current epoch time (seconds since 1970-01-01).+ getCurrentTime :: m Int64++ -- | Write a named file to the store, returning the store path.+ writeToStore :: Text -> Text -> m Text++ -- | 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)++ -- | Resolve a path literal to an absolute path.+ -- In IO evaluation, relative paths are resolved against the current+ -- file's directory (@esBaseDir@). In pure evaluation, paths are+ -- returned unchanged. This ensures that path values captured in+ -- closures remain valid after the import scope ends.+ resolvePathLiteral :: Text -> m Text++ -- | Copy a path (file or directory) to the store, returning the store path.+ -- First argument is the source path, second is the store name.+ copyPathToStore :: Text -> Text -> m Text++ -- | Print a trace/warning message.+ -- IO evaluators write to stderr; pure evaluators silently discard.+ traceMessage :: Text -> m ()++ -- | Force a thunk to a value, with memoization.+ -- IO evaluators should cache results (via per-thunk @IORef@).+ -- Pure evaluators re-evaluate each time.+ -- The first argument is the bytecode evaluation function (to break+ -- the Eval.Types → Eval circular dependency).+ forceThunk :: (Env -> Word32 -> m NixValue) -> Thunk -> m NixValue++-- | Pure evaluation monad — wraps 'Either Text'.+-- IO builtins ('readFile', 'import') are unavailable;+-- everything else evaluates identically to the IO version.+newtype PureEval a = PureEval {runPureEval :: Either Text a}+ deriving (Functor, Applicative, Monad)++instance MonadEval PureEval where+ throwEvalError msg = PureEval (Left msg)+ abortEvaluation msg = PureEval (Left ("evaluation aborted: " <> msg))+ catchEvalError (PureEval action) = PureEval (Right action)+ readFileText _ = throwEvalError "readFile: not available in pure evaluation"+ doesPathExist _ = pure False+ listDirectory _ = throwEvalError "builtins.readDir: not available in pure evaluation"+ importFile _ = throwEvalError "import: not available in pure evaluation"+ getEnvVar _ = pure ""+ 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"+ copyPathToStore _ _ = throwEvalError "builtins.path: not available in pure evaluation"+ traceMessage _ = pure ()+ resolvePathLiteral = pure+ forceThunk evalFn (Thunk ptr) =+ -- Read the C thunk via unsafePerformIO — safe because reads are+ -- idempotent and PureEval never writes back (no memoization).+ -- Does NOT mark blackholes — PureEval may re-force the same thunk.+ -- Dispatches on val_tag for computed scalars (no StablePtr deref).+ case unsafePerformIO (cthunkState ptr) of+ 1 {- COMPUTED -} ->+ let tag = unsafePerformIO (cthunkValueTag ptr)+ in case tag of+ 0 {- INT -} -> pure (VInt (unsafePerformIO (cthunkGetInt ptr)))+ 1 {- FLOAT -} -> pure (VFloat (unsafePerformIO (cthunkGetFloat ptr)))+ 2 {- BOOL -} -> pure (VBool (unsafePerformIO (cthunkGetBool ptr) /= 0))+ 3 {- NULL -} -> pure VNull+ 4 {- STR -} ->+ let sym = unsafePerformIO (cthunkGetStr ptr)+ in pure (VStr (symbolText (Symbol sym)) emptyContext)+ 5 {- PATH -} ->+ let sym = unsafePerformIO (cthunkGetPath ptr)+ in pure (VPath (symbolText (Symbol sym)))+ 6 {- LIST -} ->+ let listPtr = unsafePerformIO (cthunkGetList ptr)+ in pure (VList (CList (castPtr listPtr)))+ 7 {- ATTRS -} ->+ let p = unsafePerformIO (cthunkGetAttrs ptr)+ in pure (VAttrs (AttrSet (castPtr p)))+ 8 {- CTXSTR -} ->+ let csptr = unsafePerformIO (cthunkGetCtxStr ptr)+ (t, ctx) = unsafePerformIO (unmarshalStringContext (castPtr csptr))+ in pure (VStr t ctx)+ 9 {- LAMBDA -} ->+ let lamRaw = unsafePerformIO (cthunkPayload ptr)+ val = unsafePerformIO (unmarshalLambdaValue lamRaw)+ in pure val+ _ {- PTR -} ->+ let payload = unsafePerformIO (cthunkPayload ptr)+ val = unsafePerformIO (deRefStablePtr (castPtrToStablePtr payload))+ in pure val+ 2 {- BLACKHOLE -} ->+ -- Non-catchable: must escape tryEval like in C++ Nix+ abortEvaluation "infinite recursion encountered"+ _ {- PENDING -} ->+ let bcIdx = unsafePerformIO (cthunkGetBcIdx ptr)+ envSp = unsafePerformIO (cthunkPayload ptr)+ env = unsafePerformIO (deRefStablePtr (castPtrToStablePtr envSp))+ in evalFn env bcIdx
src/Nix/Expr/Resolve.hs view
@@ -92,7 +92,18 @@ EAssert (resolve stack cond) (resolve stack body) EUnary op operand -> EUnary op (resolve stack operand) EBinary op l r -> EBinary op (resolve stack l) (resolve stack r)- ESearchPath _ -> expr+ -- Desugar: <name> → __findFile __nixPath "name"+ -- Matches C++ Nix's parser desugaring. __findFile and __nixPath are+ -- in the root scope (Builtins.hs), so they resolve via name-based+ -- lookup at runtime. This ensures closure trimming captures the+ -- implicit builtins dependency.+ ESearchPath name ->+ resolve+ stack+ ( EApp+ (EApp (EVar "__findFile") (EVar "__nixPath"))+ (EStr [StrLit name])+ ) -- | Resolve a variable by walking the scope stack. --
src/Nix/Expr/Types.hs view
@@ -33,11 +33,12 @@ ) where +import Data.Int (Int64) import Data.Text (Text) -- | Atomic (literal) values. data NixAtom- = NixInt !Integer+ = NixInt !Int64 | NixFloat !Double | NixBool !Bool | NixNull
src/Nix/Hash.hs view
@@ -46,8 +46,10 @@ where import qualified Crypto.Hash as CH+import Data.Bits (xor) import qualified Data.ByteArray as BA import qualified Data.ByteString as BS+import Data.List (foldl') import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8)@@ -80,12 +82,26 @@ bytes = BA.unpack digest in T.pack (concatMap byteToHex bytes) --- | Truncate a SHA-256 digest to 20 bytes and Nix base-32 encode.+-- | Compress a SHA-256 digest to 20 bytes via XOR-folding and Nix base-32+-- encode. Matches C++ Nix @compressHash@: bytes beyond the target length+-- are XOR'd back onto the prefix, preserving information from the full+-- digest rather than simply truncating. truncatedBase32 :: BS.ByteString -> Text truncatedBase32 bs = let digest = CH.hash bs :: CH.Digest CH.SHA256- bytes20 = BS.pack (take 20 (BA.unpack digest :: [Word8]))- in encode bytes20+ allBytes = BA.unpack digest :: [Word8]+ compressed = compressHash 20 allBytes+ in encode (BS.pack compressed)++-- | XOR-fold a hash to @targetLen@ bytes, matching C++ Nix @compressHash@.+-- Each source byte is XOR'd into position @i mod targetLen@.+compressHash :: Int -> [Word8] -> [Word8]+compressHash targetLen bytes =+ let arr0 = replicate targetLen 0+ fold_ acc (i, b) =+ let pos = i `mod` targetLen+ in zipWith (\j x -> if j == pos then xor x b else x) [0 :: Int ..] acc+ in foldl' fold_ arr0 (zip [0 ..] bytes) -- | Format a single byte as two lowercase hex digits. byteToHex :: Word8 -> String
src/Nix/Parser/Expr.hs view
@@ -1,12 +1,13 @@ -- | Recursive descent expression parser for the Nix language. ----- 13 precedence levels, lowest to highest:+-- 14 precedence levels, lowest (top) to highest (deepest): -- -- @--- lambda / implication / || / && / == != / < > <= >= / //--- ! / ++ / + - / * \\/ / negate / application / selection+-- lambda / -> / || / && / == != / < > <= >= / //+-- ! / + - / * \\/ / ++ / ? / negate / application / selection -- @ --+-- Matches the C++ Nix parser (parser.y) precedence exactly. -- Entirely pure. No IO, no Megaparsec, no Parsec. module Nix.Parser.Expr ( -- * Entry point@@ -307,9 +308,21 @@ TokMinus -> do _ <- advance EUnary OpNegate <$> parseNegate- _ -> parseApp+ _ -> parseHasAttr --- | Level 12: Function application (juxtaposition, left-associative).+-- | Level 12: Has-attribute (@?@, non-associative).+-- Right operand is an attr path, not a full expression.+parseHasAttr :: Parser Expr+parseHasAttr = do+ lhs <- parseApp+ tok <- peekMaybe+ case tok of+ Just TokQuestion -> do+ _ <- advance+ EHasAttr lhs <$> parseAttrPath+ _ -> pure lhs++-- | Level 13: Function application (juxtaposition, left-associative). parseApp :: Parser Expr parseApp = do func <- parseSelect@@ -323,8 +336,7 @@ loopApp (EApp func arg) _ -> pure func --- | Level 13: Attribute selection (@e.a@, @e.a or default@) and--- has-attribute (@e ? a@).+-- | Level 14: Attribute selection (@e.a@, @e.a or default@). parseSelect :: Parser Expr parseSelect = do expr <- parseAtom@@ -340,10 +352,6 @@ -- Check for 'or' default orDefault <- tryParser parseOrDefault selectLoop (ESelect expr path orDefault)- Just TokQuestion -> do- _ <- advance- path <- parseAttrPath- selectLoop (EHasAttr expr path) _ -> pure expr -- | Parse the @or default@ part after @e.path@.@@ -509,6 +517,10 @@ TokIdent name -> do _ <- advance go (name : acc)+ -- Keywords are valid as inherit names: inherit if then else;+ _ | Just name <- keywordToText tok -> do+ _ <- advance+ go (name : acc) -- Quoted strings for keywords used as attribute names: inherit "or"; TokStringOpen -> do _ <- advance@@ -541,8 +553,10 @@ parseAttrKey = do tok <- peek case tok of- -- 'or' can be used as an attr key (handled by TokIdent since 'or' is not a keyword) TokIdent name -> advance >> pure (StaticKey name)+ -- Keywords are valid as attribute names in Nix:+ -- { if = 1; }, a.then, { else = 2; }, etc.+ _ | Just name <- keywordToText tok -> advance >> pure (StaticKey name) TokStringOpen -> DynamicKey <$> parseString TokInterpOpen -> do@@ -553,6 +567,23 @@ expect TokRBrace pure (DynamicKey expr) _ -> parseError ("expected attribute key, got " <> showToken tok)++-- | Convert keyword tokens to their text for use as attribute names.+-- All Nix keywords are valid as attr keys in binding and select position.+keywordToText :: Token -> Maybe Text+keywordToText TokIf = Just "if"+keywordToText TokThen = Just "then"+keywordToText TokElse = Just "else"+keywordToText TokLet = Just "let"+keywordToText TokIn = Just "in"+keywordToText TokWith = Just "with"+keywordToText TokAssert = Just "assert"+keywordToText TokRec = Just "rec"+keywordToText TokInherit = Just "inherit"+keywordToText TokTrue = Just "true"+keywordToText TokFalse = Just "false"+keywordToText TokNull = Just "null"+keywordToText _ = Nothing parseLet :: Parser Expr parseLet = do
src/Nix/Parser/Lexer.hs view
@@ -13,6 +13,7 @@ where import Data.Char (isAlpha, isAlphaNum, isDigit, isSpace)+import Data.Int (Int64) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Lazy as TL@@ -44,7 +45,7 @@ | TokNull | -- Identifiers and literals TokIdent !Text- | TokInt !Integer+ | TokInt !Int64 | TokFloat !Double | TokUri !Text | TokPath !Text@@ -235,7 +236,13 @@ [] -> False '$' | Just '{' <- safeHead rest ->- emit2 st TokInterpOpen acc+ -- Increment brace depth so the closing } is TokRBrace, not+ -- TokInterpClose. Without this, ${name} inside a string+ -- interpolation like "${env.${name}}" prematurely ends the+ -- outer interpolation.+ let tok = Located (lsLine st) (lsCol st) TokInterpOpen+ newSt = advanceCol 2 st {lsInput = T.drop 1 rest, lsBraceDepth = lsBraceDepth st + 1}+ in lexNormalMode newSt (tok : acc) '~' | Just '/' <- safeHead rest -> lexPath st acc@@ -299,10 +306,10 @@ Just ('\'', rest1) | Just ('\'', rest2) <- T.uncons rest1 -> -- Check for escape sequences: ''', ''$, ''\x, ''${ case T.uncons rest2 of- Just ('\'', _) ->- -- ''' → literal single quote+ Just ('\'', rest3) ->+ -- ''' → literal single quote (consume all 3) let litTok = Located (lsLine st) (lsCol st) (TokStringLit "'")- newSt = advanceCol 3 st {lsInput = rest2}+ newSt = advanceCol 3 st {lsInput = rest3} in lexIndStringMode newSt (litTok : acc) Just ('$', rest3) | Just ('{', rest4) <- T.uncons rest3 ->@@ -310,6 +317,11 @@ let litTok = Located (lsLine st) (lsCol st) (TokStringLit "${") newSt = advanceCol 4 st {lsInput = rest4} in lexIndStringMode newSt (litTok : acc)+ Just ('$', rest3) ->+ -- ''$ (without brace) → literal $+ let litTok = Located (lsLine st) (lsCol st) (TokStringLit "$")+ newSt = advanceCol 3 st {lsInput = rest3}+ in lexIndStringMode newSt (litTok : acc) Just ('\\', rest3) -> -- ''\x → escape sequence case T.uncons rest3 of@@ -319,7 +331,7 @@ 't' -> "\t" 'r' -> "\r" '\\' -> "\\"- _ -> T.singleton ec+ _ -> "\\" <> T.singleton ec litTok = Located (lsLine st) (lsCol st) (TokStringLit escaped) newSt = advanceCol 4 st {lsInput = rest4} in lexIndStringMode newSt (litTok : acc)@@ -469,7 +481,7 @@ newSt = advanceCol fullLen st {lsInput = after3} in lexNormalMode newSt (tok : acc) _ ->- let val = readInteger digits+ let val = fromIntegral (readInteger digits) :: Int64 tok = Located (lsLine st) (lsCol st) (TokInt val) newSt = advanceCol len st {lsInput = after} in lexNormalMode newSt (tok : acc)
src/Nix/Store.hs view
@@ -175,13 +175,20 @@ scanReferences :: StoreDir -> [StorePath] -> FilePath -> IO [StorePath] scanReferences storeDir candidates dir = do let candidateSet = Set.fromList [(spHash sp, sp) | sp <- candidates]- prefixBytes = TE.encodeUtf8 (T.pack (unStoreDir storeDir <> "/"))+ storeDirStr = unStoreDir storeDir+ -- Scan for both forward-slash and backslash store prefixes.+ -- On Windows, binaries may contain either separator style.+ prefixFwd = TE.encodeUtf8 (T.pack (storeDirStr <> "/"))+ prefixBwd = TE.encodeUtf8 (T.pack (storeDirStr <> "\\"))+ prefixBytes = prefixFwd prefixLen = BS.length prefixBytes hashLen = 32 files <- collectRegularFiles dir foundHashes <- foldlIO Set.empty files $ \acc filePath -> do contents <- BS.readFile filePath- pure (scanBytes prefixBytes prefixLen hashLen contents acc)+ -- Scan with forward-slash prefix, then backslash (for Windows)+ let acc1 = scanBytes prefixFwd prefixLen hashLen contents acc+ pure (scanBytes prefixBwd prefixLen hashLen contents acc1) pure [sp | (h, sp) <- Set.toList candidateSet, Set.member h foundHashes] -- | Collect all regular files under a path, recursively.
src/Nix/Substituter.hs view
@@ -286,7 +286,26 @@ createDirectoryIfMissing True (takeDirectory path) -- On Windows, symlinks require elevated permissions. -- Fall back to writing the target as a text file.- Dir.createFileLink (T.unpack target) path+ result <- try (Dir.createFileLink (T.unpack target) path)+ case result of+ Right () -> pure ()+ Left (_ :: SomeException) ->+ writeFile path (T.unpack target) NAR.NarDirectory entries -> do createDirectoryIfMissing True path- mapM_ (\(name, child) -> unpackNarEntry (path </> T.unpack name) child) entries+ mapM_+ ( \(name, child) ->+ if isSafeNarName name+ then unpackNarEntry (path </> T.unpack name) child+ else error ("unpackNarEntry: unsafe directory entry name: " <> T.unpack name)+ )+ entries++-- | Validate that a NAR entry name is safe (no path traversal).+isSafeNarName :: Text -> Bool+isSafeNarName name =+ not (T.null name)+ && name /= ".."+ && name /= "."+ && not (T.isInfixOf "/" name)+ && not (T.isInfixOf "\\" name)
test/Main.hs view
@@ -3,22 +3,31 @@ module Main (main) where import Control.Exception (bracket_)-import Control.Monad (filterM, when)+import Control.Monad (filterM, void, when) import qualified Data.ByteString as BS import qualified Data.Map.Strict as Map-import Data.Primitive.SmallArray (sizeofSmallArray)+import Data.Maybe (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 qualified Data.Text.IO as TIO+import Foreign.Ptr (castPtr)+import Foreign.StablePtr (StablePtr, castPtrToStablePtr, castStablePtrToPtr, deRefStablePtr, freeStablePtr, newStablePtr) import Nix.Builder (BuildConfig (..), BuildResult (..), buildDerivation, buildWithDeps, defaultBuildConfig) 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 (..), attrSetFromMap, attrSetLookup, attrSetNull, attrSetSize, emptyContext, emptyEnv, eval, mkStr, runPureEval)+import Nix.Eval (NixValue (..), StringContext (..), StringContextElement (..), attrSetFromMap, attrSetLookup, attrSetNull, attrSetSize, emptyContext, emptyEnv, eval, force, mkStr, readThunkValue, runPureEval)+import Nix.Eval.Arena (arenaDestroy, arenaInit)+import Nix.Eval.CAttrSet (cattrsetFreeze, cattrsetInsert, cattrsetKeys, cattrsetLookup, cattrsetNew, cattrsetSize, cattrsetUnion)+import Nix.Eval.CBytecode (binaryAdd, captureSlots, captureWithScopes, cbcArg1, cbcArg2, cbcArg3, cbcData, cbcFlags, cbcOpCount, cbcOpcode, cbcShortArg, formalName, formalNamedSet, formalSet, opApp, opAssert, opAttrs, opBinary, opHasAttr, opIf, opIndStr, opLambda, opLet, opList, opLitBool, opLitFloat, opLitInt, opLitNull, opLitPath, opLitUri, opResolvedVar, opSelect, opStr, opUnary, opVar, opWith, opWithVar, strpartInterp, strpartLit, unaryNegate)+import Nix.Eval.CThunk (CThunkPtr, cthunkCount, cthunkGet, cthunkMarkBlackhole, cthunkNew, cthunkNewComputed, cthunkPayload, cthunkSetComputed, cthunkState)+import Nix.Eval.Compile (compileExpr) import qualified Nix.Eval.Context as Context import Nix.Eval.IO (EvalState (..), newEvalState, runEvalIO)+import Nix.Eval.Symbol (Symbol (..), symbolCount, symbolIntern, symbolLen, symbolText)+import Nix.Eval.Types (emptyCList) import Nix.Expr.Types import Nix.Parser (parseNix) import Nix.Parser.Lexer (Located (..), Token (..), tokenize)@@ -35,7 +44,7 @@ import qualified System.Process as Proc -- ------------------------------------------------------------------------------ Test harness (same pattern as gbnet-hs, nova-cache)+-- Test harness -- --------------------------------------------------------------------------- data TestResult = Pass | Fail !Text@@ -215,7 +224,7 @@ putStrLn "eval/literals" sequence [ runTest "empty env" $- assertEqual "emptyEnv" 0 (sizeofSmallArray (envSlots emptyEnv)),+ assertEqual "emptyEnv" emptyEnv emptyEnv, runTest "int" $ assertEval "int" "42" (VInt 42), runTest "float" $@@ -531,8 +540,8 @@ testEvalErrors = do putStrLn "eval/errors" sequence- [ runTest "type error in add" $- assertEvalFail "type-add" "1 + true",+ [ runTest "type error in add (set + list)" $+ assertEvalFail "type-add" "{} + []", runTest "call non-function" $ assertEvalFail "call-non" "42 1", runTest "builtins.throw" $@@ -553,7 +562,7 @@ runTest "map identity" $ assertEval "map-id" "builtins.map (x: x) [ 1 2 ] == [ 1 2 ]" (VBool True), runTest "map empty" $- assertEval "map-empty" "builtins.map (x: x) [ ]" (VList []),+ assertEval "map-empty" "builtins.map (x: x) [ ]" (VList emptyCList), runTest "map lazy" $ assertEval "map-lazy" "let xs = builtins.map (x: x * 2) [ 1 (throw \"boom\") 3 ]; in builtins.elemAt xs 0" (VInt 2), -- filter@@ -572,7 +581,7 @@ runTest "genList basic" $ assertEval "genList" "builtins.genList (i: i * 2) 4 == [ 0 2 4 6 ]" (VBool True), runTest "genList zero" $- assertEval "genList-0" "builtins.genList (i: i) 0" (VList []),+ assertEval "genList-0" "builtins.genList (i: i) 0" (VList emptyCList), runTest "genList lazy" $ assertEval "genList-lazy" "let xs = builtins.genList (i: if i == 0 then 42 else throw \"boom\") 5; in builtins.elemAt xs 0" (VInt 42), -- sort@@ -1142,7 +1151,7 @@ runTest "concatLists basic" $ assertEval "concatLists" "builtins.concatLists [ [ 1 2 ] [ 3 ] [ 4 5 ] ] == [ 1 2 3 4 5 ]" (VBool True), runTest "concatLists empty" $- assertEval "concatLists-empty" "builtins.concatLists [ ]" (VList []),+ assertEval "concatLists-empty" "builtins.concatLists [ ]" (VList emptyCList), runTest "concatLists type error" $ assertEvalFail "concatLists-err" "builtins.concatLists [ 1 2 ]", -- lessThan@@ -1160,7 +1169,7 @@ runTest "langVersion" $ assertEval "langVersion" "builtins.langVersion" (VInt 6), runTest "nixPath" $- assertEval "nixPath" "builtins.nixPath" (VList [])+ assertEval "nixPath" "builtins.nixPath" (VList emptyCList) ] -- ---------------------------------------------------------------------------@@ -1328,7 +1337,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 (attrSetFromMap (Map.fromList [("a", Evaluated (VInt 1)), ("b", Evaluated (VList [Evaluated (VInt 2), Evaluated (VInt 3)]))]))),+ assertEval "fromJSON-rt" "let x = builtins.fromJSON (builtins.toJSON { a = 1; b = [ 2 3 ]; }); in x.a == 1 && x.b == [ 2 3 ]" (VBool True), runTest "fromJSON invalid" $ assertEvalFail "fromJSON-bad" "builtins.fromJSON \"not json\"", -- hashString@@ -1362,9 +1371,9 @@ assertEval "tryEval-throw" "(builtins.tryEval (builtins.throw \"boom\")).success" (VBool False), runTest "tryEval failure value" $ assertEval "tryEval-fval" "(builtins.tryEval (builtins.throw \"boom\")).value" (VBool False),- -- tryEval catches type error- runTest "tryEval catches type error" $- assertEval "tryEval-tyerr" "(builtins.tryEval (1 + \"a\")).success" (VBool False),+ -- tryEval catches coercion error (+ on attrset without outPath)+ runTest "tryEval catches coercion error" $+ assertEval "tryEval-tyerr" "(builtins.tryEval ({} + [])).success" (VBool False), -- deepSeq runTest "deepSeq returns second" $ assertEval "deepSeq" "builtins.deepSeq [ 1 2 3 ] 42" (VInt 42),@@ -1485,8 +1494,8 @@ runTestIOFail "import nonexistent -> error" testDir "import ./nonexistent.nix", runTestIO "import attrset + select" testDir "(import ./attrset.nix).x" (VInt 1), runTestIO "import let/lambda" testDir "import ./uses-arg.nix" (VInt 15),- -- import rejects strings (real Nix semantics)- runTestIOFail "import rejects string" testDir "import \"./literal.nix\"",+ -- import accepts strings (real Nix coerces string to path)+ runTestIO "import accepts string" testDir "import \"./literal.nix\"" (VInt 42), -- pathExists runTestIO "pathExists true"@@ -1598,8 +1607,8 @@ runTest "placeholder out starts with /nix/store/" $ assertRight "placeholder-prefix" (evalNix "builtins.placeholder \"out\"") $ \val -> case val of- VPath p -> if "/nix/store/" `T.isPrefixOf` p then Pass else Fail ("bad prefix: " <> p)- _ -> Fail ("expected VPath, got " <> T.pack (show val)),+ VStr p _ -> if "/nix/store/" `T.isPrefixOf` p then Pass else Fail ("bad prefix: " <> p)+ _ -> Fail ("expected VStr, got " <> T.pack (show val)), runTest "placeholder deterministic" $ assertRight "placeholder-det" (evalNix "builtins.placeholder \"out\" == builtins.placeholder \"out\"") $ \val -> assertEqual "deterministic" (VBool True) val,@@ -1667,6 +1676,23 @@ ] -- ---------------------------------------------------------------------------+-- Tests: Blackhole (infinite recursion detection)+-- ---------------------------------------------------------------------------++testBlackhole :: IO [Bool]+testBlackhole = do+ putStrLn "eval/blackhole"+ tmpBase <- getTemporaryDirectory+ sequence+ [ runTestIOFail "let x = x; in x" tmpBase "let x = x; in x",+ runTestIOFail "rec { a = a; }.a" tmpBase "rec { a = a; }.a",+ runTestIOFail "let a = b; b = a; in a" tmpBase "let a = b; b = a; in a",+ -- Non-recursive cases must still work+ runTestIO "rec { a = 1; b = a; }.b" tmpBase "rec { a = 1; b = a; }.b" (VInt 1),+ runTestIO "let a = 1; b = a + 1; in b" tmpBase "let a = 1; b = a + 1; in b" (VInt 2)+ ]++-- --------------------------------------------------------------------------- -- Tests: Batch D — toFile -- --------------------------------------------------------------------------- @@ -1845,12 +1871,14 @@ then Pass else Fail ("bad outPath: " <> p) _ -> Fail ("expected VStr with context, got " <> T.pack (show val)),+ -- 'derivation' is lazy (matches C++ Nix): the missing-required-attribute+ -- error fires when a path is forced (.drvPath), not at construction. runTest "derivation missing name" $- assertEvalFail "drv-noname" "derivation { system = \"x86_64-linux\"; builder = \"/bin/sh\"; }",+ assertEvalFail "drv-noname" "(derivation { system = \"x86_64-linux\"; builder = \"/bin/sh\"; }).drvPath", runTest "derivation missing system" $- assertEvalFail "drv-nosys" "derivation { name = \"hello\"; builder = \"/bin/sh\"; }",+ assertEvalFail "drv-nosys" "(derivation { name = \"hello\"; builder = \"/bin/sh\"; }).drvPath", runTest "derivation missing builder" $- assertEvalFail "drv-nobuilder" "derivation { name = \"hello\"; system = \"x86_64-linux\"; }",+ assertEvalFail "drv-nobuilder" "(derivation { name = \"hello\"; system = \"x86_64-linux\"; }).drvPath", runTest "derivation type error" $ assertEvalFail "drv-tyerr" "derivation 42", runTest "derivation deterministic" $@@ -2310,8 +2338,8 @@ -- Builder will fail (nonexistent) but the graph should resolve correctly pure $ case result of BuildFailure _ _ -> Pass- BuildSuccess _ -> Pass, -- Would pass if builder somehow exists- -- buildWithDeps with cycle detection (mocked through malformed graph)+ BuildSuccess _ -> Fail "expected build failure for nonexistent builder",+ -- buildWithDeps with cycle detection (mocked through malformed graph) runTest "cycle detection returns failure" $ let spA = StorePath "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "a.drv" spB = StorePath "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "b.drv"@@ -2342,9 +2370,9 @@ in case DepGraph.buildDepGraph readFn drvACyc spA of Right graph -> case DepGraph.topoSort graph of DepGraph.TopoCycle _ -> Pass- DepGraph.TopoSorted _ -> Pass -- Partial sort is also acceptable- Left _ -> Pass, -- Build graph failure also acceptable- -- missing .drv -> failure in dep graph+ DepGraph.TopoSorted order -> Fail ("expected cycle, got sorted: " <> T.pack (show order))+ Left err -> Fail ("expected graph to build, got: " <> err),+ -- missing .drv -> failure in dep graph runTest "missing drv in dep graph" $ let sp = StorePath "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "missing.drv" drv =@@ -2369,17 +2397,15 @@ T.concat [ "let dep = derivation { name = \"dep\"; system = \"x86_64-linux\"; builder = \"/bin/sh\"; }; ", "main = derivation { name = \"main\"; system = \"x86_64-linux\"; builder = \"/bin/sh\"; src = dep.outPath; }; ",- "in main"+ "in main._derivation" ] ) $ \case- VAttrs attrs -> case attrSetLookup "_derivation" attrs of- Just (Evaluated (VDerivation drv)) ->- if Map.null (drvInputDrvs drv)- then Fail "expected non-empty drvInputDrvs"- else Pass- _ -> Fail "missing _derivation"- _ -> Fail "expected VAttrs"+ VDerivation drv ->+ if Map.null (drvInputDrvs drv)+ then Fail "expected non-empty drvInputDrvs"+ else Pass+ _ -> Fail "expected VDerivation" ] -- ---------------------------------------------------------------------------@@ -2720,8 +2746,8 @@ complexTestDrv = Derivation { drvOutputs =- [ DerivationOutput "out" (StorePath "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "pkg-2.0") "" "",- DerivationOutput "dev" (StorePath "dddddddddddddddddddddddddddddddd" "pkg-2.0-dev") "" ""+ [ DerivationOutput "dev" (StorePath "dddddddddddddddddddddddddddddddd" "pkg-2.0-dev") "" "",+ DerivationOutput "out" (StorePath "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "pkg-2.0") "" "" ], drvInputDrvs = Map.fromList@@ -3103,12 +3129,19 @@ Left err -> pure (Left ("parse error: " <> T.pack (show err))) Right expr -> do st <- newEvalState "."- evalResult <- runEvalIO st (eval (builtinEnv (esTimestamp st) (esSearchPaths st)) expr)+ evalResult <- runEvalIO st $ do+ val <- eval (builtinEnv (esTimestamp st) (esSearchPaths st)) expr+ -- 'derivation' is lazy now; force _derivation so the peek below sees it+ -- (and so a missing required attr surfaces as an eval error).+ case val of+ VAttrs attrs -> maybe (pure ()) (void . force) (attrSetLookup "_derivation" attrs)+ _ -> pure ()+ pure val case evalResult of Left err -> pure (Left ("eval error: " <> err)) Right val -> case val of- VAttrs attrs -> case attrSetLookup "_derivation" attrs of- Just (Evaluated (VDerivation drv)) -> do+ VAttrs attrs -> case attrSetLookup "_derivation" attrs >>= readThunkValue of+ Just (VDerivation drv) -> do store <- openStore storeDir tmpBase <- getTemporaryDirectory let config = (defaultBuildConfig storeDir) {bcTmpDir = tmpBase </> "nova-nix-e2e-tmp"}@@ -3215,9 +3248,9 @@ runTest "parseNixPath single" $ let result = parseNixPath "nixpkgs=/home/user/nixpkgs" in case result of- [Evaluated (VAttrs m)] ->- case (attrSetLookup "prefix" m, attrSetLookup "path" m) of- (Just (Evaluated (VStr "nixpkgs" _)), Just (Evaluated (VStr "/home/user/nixpkgs" _))) -> Pass+ [thunk] | Just (VAttrs m) <- readThunkValue thunk ->+ case (attrSetLookup "prefix" m >>= readThunkValue, attrSetLookup "path" m >>= readThunkValue) of+ (Just (VStr "nixpkgs" _), Just (VStr "/home/user/nixpkgs" _)) -> Pass _ -> Fail "wrong prefix/path" _ -> Fail ("expected one entry, got " <> T.pack (show (length result))), runTest "parseNixPath multiple" $@@ -3225,16 +3258,16 @@ runTest "parseNixPath plain path" $ let result = parseNixPath "/some/path" in case result of- [Evaluated (VAttrs m)] ->- case (attrSetLookup "prefix" m, attrSetLookup "path" m) of- (Just (Evaluated (VStr "" _)), Just (Evaluated (VStr "/some/path" _))) -> Pass+ [thunk] | Just (VAttrs m) <- readThunkValue thunk ->+ case (attrSetLookup "prefix" m >>= readThunkValue, attrSetLookup "path" m >>= readThunkValue) of+ (Just (VStr "" _), Just (VStr "/some/path" _)) -> Pass _ -> Fail "wrong prefix/path for plain" _ -> Fail "expected one entry",- -- ESearchPath parser test+ -- ESearchPath desugars to __findFile __nixPath "name" during resolution runTest "parse <nixpkgs>" $- assertParse "search path" "<nixpkgs>" (ESearchPath "nixpkgs"),+ assertParse "search path" "<nixpkgs>" (EApp (EApp (EVar "__findFile") (EVar "__nixPath")) (EStr [StrLit "nixpkgs"])), runTest "parse <nixpkgs/lib>" $- assertParse "search path with subpath" "<nixpkgs/lib>" (ESearchPath "nixpkgs/lib"),+ assertParse "search path with subpath" "<nixpkgs/lib>" (EApp (EApp (EVar "__findFile") (EVar "__nixPath")) (EStr [StrLit "nixpkgs/lib"])), -- ESearchPath eval (should fail in pure mode since no search paths) runTest "eval <nixpkgs> fails without path" $ assertEvalFail "search path not found" "<nixpkgs>",@@ -3248,7 +3281,11 @@ runTest "dynamic key in hasAttr" $ assertEval "dynamic key hasAttr" "let s = { x = 10; }; in s ? ${\"x\"}" (VBool True), runTest "dynamic key hasAttr missing" $- assertEval "dynamic key hasAttr missing" "let s = { x = 10; }; in s ? ${\"y\"}" (VBool False)+ assertEval "dynamic key hasAttr missing" "let s = { x = 10; }; in s ? ${\"y\"}" (VBool False),+ -- Dynamic attr inside string interpolation (the ${name} must not+ -- prematurely close the outer interpolation)+ runTest "dynamic key in string interp" $+ assertEval "dynamic key interp" "let s = { x = 10; }; in \"${toString s.${\"x\"}}\"" (VStr "10" mempty) ] testPhase4IO :: IO [Bool]@@ -3275,7 +3312,7 @@ st <- newEvalState testDir let nixPaths = parseNixPath ("mypkg=" <> T.pack subDir) env = builtinEnv (esTimestamp st) nixPaths- result <- runEvalIO st (eval env (ESearchPath "mypkg"))+ result <- runEvalIO st (eval env (EApp (EApp (EVar "__findFile") (EVar "__nixPath")) (EStr [StrLit "mypkg"]))) pure $ case result of Right (VPath _) -> Pass Right other -> Fail ("expected VPath, got " <> T.pack (show other))@@ -3286,11 +3323,847 @@ pure results -- ---------------------------------------------------------------------------+-- Symbol interning (C FFI)+-- ---------------------------------------------------------------------------++testSymbol :: IO [Bool]+testSymbol = do+ putStrLn "symbol"+ -- Symbol table is initialized by arenaInit in main bracket.+ sequence+ [ runTestM "intern returns non-zero" $ do+ sym <- symbolIntern "hello"+ pure (if unSymbol sym /= 0 then Pass else Fail "got symbol 0"),+ runTestM "intern same string returns same symbol" $ do+ sym1 <- symbolIntern "name"+ sym2 <- symbolIntern "name"+ pure (assertEqual "same symbol" sym1 sym2),+ runTestM "intern different strings returns different symbols" $ do+ sym1 <- symbolIntern "foo"+ sym2 <- symbolIntern "bar"+ pure (if sym1 /= sym2 then Pass else Fail "symbols should differ"),+ runTestM "symbolText round-trips" $ do+ sym <- symbolIntern "version"+ let txt = symbolText sym+ pure (assertEqual "text" "version" txt),+ runTestM "symbolLen correct" $ do+ sym <- symbolIntern "outputs"+ pure (assertEqual "len" 7 (symbolLen sym)),+ runTestM "empty string interns" $ do+ sym <- symbolIntern ""+ let txt = symbolText sym+ pure (assertEqual "empty" "" txt),+ runTestM "symbolCount tracks unique entries" $ do+ _ <- symbolIntern "alpha"+ _ <- symbolIntern "beta"+ _ <- symbolIntern "alpha"+ count <- symbolCount+ -- count includes all symbols interned in this bracket,+ -- so at least the ones from prior tests plus alpha + beta+ pure (if count >= 2 then Pass else Fail ("count too low: " <> T.pack (show count))),+ runTestM "many symbols (stress)" $ do+ let names = map (\i -> "pkg_" <> T.pack (show (i :: Int))) [1 .. 1000]+ syms <- mapM symbolIntern names+ -- All unique+ let unique = length (Set.fromList (map unSymbol syms))+ pure (assertEqual "1000 unique" 1000 unique)+ ]++-- ---------------------------------------------------------------------------+-- C attribute set (FFI)+-- ---------------------------------------------------------------------------++-- | Cast a StablePtr to CThunkPtr for CAttrSet tests.+-- CAttrSet stores void* — we use StablePtrs as opaque values in tests.+spToCPtr :: StablePtr a -> CThunkPtr+spToCPtr = castPtr . castStablePtrToPtr++-- | Cast a CThunkPtr back to StablePtr for CAttrSet test verification.+cptrToSp :: CThunkPtr -> StablePtr a+cptrToSp = castPtrToStablePtr . castPtr++testCAttrSet :: IO [Bool]+testCAttrSet = do+ putStrLn "cattrset"+ -- Symbol table is initialized by arenaInit in main bracket.+ sequence+ [ runTestM "new/free" $ do+ _set <- cattrsetNew 16+ -- set freed by arenaDestroy via nn_attrset_free_all+ pure Pass,+ runTestM "insert + freeze + lookup" $ do+ set <- cattrsetNew 4+ kName <- symbolIntern "name"+ kVer <- symbolIntern "version"+ valName <- newStablePtr ("hello" :: Text)+ valVer <- newStablePtr ("1.0" :: Text)+ cattrsetInsert set kName (spToCPtr valName)+ cattrsetInsert set kVer (spToCPtr valVer)+ cattrsetFreeze set+ result <- cattrsetLookup set kName+ case result of+ Nothing -> do+ -- set freed by arenaDestroy via nn_attrset_free_all+ freeStablePtr valName+ freeStablePtr valVer+ pure (Fail "lookup returned Nothing")+ Just cptr -> do+ val <- deRefStablePtr (cptrToSp cptr) :: IO Text+ -- set freed by arenaDestroy via nn_attrset_free_all+ freeStablePtr valName+ freeStablePtr valVer+ pure (assertEqual "lookup name" "hello" val),+ runTestM "lookup missing key returns Nothing" $ do+ set <- cattrsetNew 4+ kFoo <- symbolIntern "foo"+ kBar <- symbolIntern "bar"+ sp <- newStablePtr ("x" :: Text)+ cattrsetInsert set kFoo (spToCPtr sp)+ cattrsetFreeze set+ result <- cattrsetLookup set kBar+ -- set freed by arenaDestroy via nn_attrset_free_all+ freeStablePtr sp+ pure (case result of Nothing -> Pass; Just _ -> Fail "expected Nothing"),+ runTestM "size after freeze" $ do+ set <- cattrsetNew 4+ k1 <- symbolIntern "a"+ k2 <- symbolIntern "b"+ k3 <- symbolIntern "c"+ sp <- newStablePtr (42 :: Int)+ cattrsetInsert set k1 (spToCPtr sp)+ cattrsetInsert set k2 (spToCPtr sp)+ cattrsetInsert set k3 (spToCPtr sp)+ cattrsetFreeze set+ n <- cattrsetSize set+ -- set freed by arenaDestroy via nn_attrset_free_all+ freeStablePtr sp+ pure (assertEqual "size" 3 n),+ runTestM "duplicate keys: last writer wins" $ do+ set <- cattrsetNew 4+ kName <- symbolIntern "name"+ sp1 <- newStablePtr ("first" :: Text)+ sp2 <- newStablePtr ("second" :: Text)+ cattrsetInsert set kName (spToCPtr sp1)+ cattrsetInsert set kName (spToCPtr sp2)+ cattrsetFreeze set+ n <- cattrsetSize set+ result <- cattrsetLookup set kName+ val <- case result of+ Nothing -> pure "MISSING"+ Just cptr -> deRefStablePtr (cptrToSp cptr)+ -- set freed by arenaDestroy via nn_attrset_free_all+ freeStablePtr sp1+ freeStablePtr sp2+ pure+ ( if n == 1 && val == ("second" :: Text)+ then Pass+ else Fail ("size=" <> T.pack (show n) <> " val=" <> val)+ ),+ runTestM "keys returned sorted" $ do+ set <- cattrsetNew 8+ -- Insert in reverse order; after freeze keys should be sorted by symbol ID+ k1 <- symbolIntern "zzz"+ k2 <- symbolIntern "aaa"+ k3 <- symbolIntern "mmm"+ sp <- newStablePtr (0 :: Int)+ cattrsetInsert set k1 (spToCPtr sp)+ cattrsetInsert set k2 (spToCPtr sp)+ cattrsetInsert set k3 (spToCPtr sp)+ cattrsetFreeze set+ keys <- cattrsetKeys set+ -- set freed by arenaDestroy via nn_attrset_free_all+ freeStablePtr sp+ -- Keys should be sorted by symbol ID (ascending)+ let ids = map unSymbol keys+ sorted = ids == foldl (\acc x -> acc ++ [x]) [] (Set.toAscList (Set.fromList ids))+ pure (if sorted then Pass else Fail ("unsorted: " <> T.pack (show ids))),+ runTestM "union right-biased" $ do+ setA <- cattrsetNew 4+ setB <- cattrsetNew 4+ kX <- symbolIntern "x"+ kY <- symbolIntern "y"+ kZ <- symbolIntern "z"+ spA <- newStablePtr ("fromA" :: Text)+ spB <- newStablePtr ("fromB" :: Text)+ spZ <- newStablePtr ("onlyA" :: Text)+ cattrsetInsert setA kX (spToCPtr spA)+ cattrsetInsert setA kZ (spToCPtr spZ)+ cattrsetInsert setB kX (spToCPtr spB)+ cattrsetInsert setB kY (spToCPtr spB)+ cattrsetFreeze setA+ cattrsetFreeze setB+ merged <- cattrsetUnion setA setB+ n <- cattrsetSize merged+ resultX <- cattrsetLookup merged kX+ valX <- case resultX of+ Nothing -> pure "MISSING"+ Just cptr -> deRefStablePtr (cptrToSp cptr)+ -- sets freed by arenaDestroy via nn_attrset_free_all+ freeStablePtr spA+ freeStablePtr spB+ freeStablePtr spZ+ pure+ ( if n == 3 && valX == ("fromB" :: Text)+ then Pass+ else Fail ("size=" <> T.pack (show n) <> " x=" <> valX)+ ),+ runTestM "stress: 10k entries" $ do+ set <- cattrsetNew 1024+ sp <- newStablePtr (0 :: Int)+ syms <- mapM (\i -> symbolIntern ("key_" <> T.pack (show (i :: Int)))) [1 .. 10000]+ mapM_ (\sym -> cattrsetInsert set sym (spToCPtr sp)) syms+ cattrsetFreeze set+ n <- cattrsetSize set+ -- Spot-check a few lookups+ hit <- cattrsetLookup set (syms !! 5000)+ -- set freed by arenaDestroy via nn_attrset_free_all+ freeStablePtr sp+ pure+ ( if n == 10000 && isJust hit+ then Pass+ else Fail ("size=" <> T.pack (show n))+ )+ ]++-- ---------------------------------------------------------------------------+-- C thunk arena (FFI)+-- ---------------------------------------------------------------------------++testCThunk :: IO [Bool]+testCThunk = do+ putStrLn "cthunk"+ -- Arena is already initialized by main's bracket.+ -- Each test uses the shared arena (thunks accumulate — that's fine).+ sequence+ [ runTestM "new pending + state" $ do+ sp <- newStablePtr ("pending" :: Text)+ ptr <- cthunkNew (castStablePtrToPtr sp)+ state <- cthunkState ptr+ pure (assertEqual "state" 0 state),+ runTestM "new computed + state" $ do+ sp <- newStablePtr ("computed" :: Text)+ ptr <- cthunkNewComputed (castStablePtrToPtr sp)+ state <- cthunkState ptr+ pure (assertEqual "state" 1 state),+ runTestM "payload round-trips (pending)" $ do+ sp <- newStablePtr ("hello" :: Text)+ ptr <- cthunkNew (castStablePtrToPtr sp)+ payload <- cthunkPayload ptr+ val <- deRefStablePtr (castPtrToStablePtr payload) :: IO Text+ pure (assertEqual "payload" "hello" val),+ runTestM "payload round-trips (computed)" $ do+ sp <- newStablePtr (42 :: Int)+ ptr <- cthunkNewComputed (castStablePtrToPtr sp)+ payload <- cthunkPayload ptr+ val <- deRefStablePtr (castPtrToStablePtr payload) :: IO Int+ pure (assertEqual "payload" 42 val),+ runTestM "mark_blackhole succeeds on pending" $ do+ sp <- newStablePtr ("x" :: Text)+ ptr <- cthunkNew (castStablePtrToPtr sp)+ ok <- cthunkMarkBlackhole ptr+ state <- cthunkState ptr+ pure+ ( if ok && state == 2+ then Pass+ else Fail ("ok=" <> T.pack (show ok) <> " state=" <> T.pack (show state))+ ),+ runTestM "mark_blackhole fails on computed" $ do+ sp <- newStablePtr ("x" :: Text)+ ptr <- cthunkNewComputed (castStablePtrToPtr sp)+ ok <- cthunkMarkBlackhole ptr+ pure (if not ok then Pass else Fail "should have failed"),+ runTestM "mark_blackhole fails on blackhole" $ do+ sp <- newStablePtr ("x" :: Text)+ ptr <- cthunkNew (castStablePtrToPtr sp)+ _ <- cthunkMarkBlackhole ptr+ ok <- cthunkMarkBlackhole ptr+ pure (if not ok then Pass else Fail "should have failed"),+ runTestM "set_computed returns old payload" $ do+ pendingSp <- newStablePtr ("old" :: Text)+ ptr <- cthunkNew (castStablePtrToPtr pendingSp)+ _ <- cthunkMarkBlackhole ptr+ computedSp <- newStablePtr ("new" :: Text)+ oldPayload <- cthunkSetComputed ptr (castStablePtrToPtr computedSp)+ oldVal <- deRefStablePtr (castPtrToStablePtr oldPayload) :: IO Text+ state <- cthunkState ptr+ newPayload <- cthunkPayload ptr+ newVal <- deRefStablePtr (castPtrToStablePtr newPayload) :: IO Text+ freeStablePtr pendingSp+ pure+ ( if oldVal == "old" && newVal == "new" && state == 1+ then Pass+ else+ Fail+ ( "old="+ <> oldVal+ <> " new="+ <> newVal+ <> " state="+ <> T.pack (show state)+ )+ ),+ runTestM "set_computed on pending returns old payload" $ do+ sp <- newStablePtr ("x" :: Text)+ ptr <- cthunkNew (castStablePtrToPtr sp)+ valSp <- newStablePtr ("v" :: Text)+ oldPayload <- cthunkSetComputed ptr (castStablePtrToPtr valSp)+ -- set_computed accepts PENDING (direct memoization, no blackhole step)+ oldVal <- deRefStablePtr (castPtrToStablePtr oldPayload) :: IO Text+ freeStablePtr sp+ pure (assertEqual "old payload" "x" oldVal),+ runTestM "count tracks allocations" $ do+ countBefore <- cthunkCount+ sp <- newStablePtr (0 :: Int)+ _ <- cthunkNew (castStablePtrToPtr sp)+ _ <- cthunkNew (castStablePtrToPtr sp)+ _ <- cthunkNewComputed (castStablePtrToPtr sp)+ countAfter <- cthunkCount+ let delta = countAfter - countBefore+ pure (assertEqual "delta" 3 delta),+ runTestM "get retrieves by index" $ do+ countBefore <- cthunkCount+ sp <- newStablePtr ("indexed" :: Text)+ ptr <- cthunkNew (castStablePtrToPtr sp)+ retrieved <- cthunkGet countBefore+ stateOrig <- cthunkState ptr+ stateRetrieved <- cthunkState retrieved+ pure+ ( if ptr == retrieved && stateOrig == stateRetrieved+ then Pass+ else Fail "get returned wrong pointer"+ ),+ runTestM "stress: 100k thunks" $ do+ countBefore <- cthunkCount+ sp <- newStablePtr (0 :: Int)+ mapM_ (\_ -> cthunkNew (castStablePtrToPtr sp)) [(1 :: Int) .. 100000]+ countAfter <- cthunkCount+ let delta = countAfter - countBefore+ -- Spot-check: retrieve one from the middle+ midPtr <- cthunkGet (countBefore + 50000)+ midState <- cthunkState midPtr+ pure+ ( if delta == 100000 && midState == 0+ then Pass+ else+ Fail+ ( "delta="+ <> T.pack (show delta)+ <> " midState="+ <> T.pack (show midState)+ )+ )+ ]++-- ---------------------------------------------------------------------------+-- Bytecode compilation tests+-- ---------------------------------------------------------------------------++testBytecodeCompile :: IO [Bool]+testBytecodeCompile = do+ putStrLn "bytecode"+ -- Arena (including bytecode store) already initialized by main's bracket.+ sequence+ [ runTestM "compile ELit NixInt" $ do+ idx <- compileExpr (ELit (NixInt 42))+ op <- cbcOpcode idx+ a1 <- cbcArg1 idx+ a2 <- cbcArg2 idx+ pure+ ( if op == opLitInt && a1 == 42 && a2 == 0+ then Pass+ else Fail ("op=" <> T.pack (show op) <> " a1=" <> T.pack (show a1))+ ),+ runTestM "compile ELit NixInt negative" $ do+ idx <- compileExpr (ELit (NixInt (-1)))+ op <- cbcOpcode idx+ a1 <- cbcArg1 idx+ a2 <- cbcArg2 idx+ -- -1 as uint64 = 0xFFFFFFFFFFFFFFFF, lo=0xFFFFFFFF, hi=0xFFFFFFFF+ pure+ ( if op == opLitInt && a1 == 0xFFFFFFFF && a2 == 0xFFFFFFFF+ then Pass+ else+ Fail+ ( "a1="+ <> T.pack (show a1)+ <> " a2="+ <> T.pack (show a2)+ )+ ),+ runTestM "compile ELit NixBool" $ do+ idxT <- compileExpr (ELit (NixBool True))+ idxF <- compileExpr (ELit (NixBool False))+ opT <- cbcOpcode idxT+ opF <- cbcOpcode idxF+ saT <- cbcShortArg idxT+ saF <- cbcShortArg idxF+ pure+ ( if opT == opLitBool && saT == 1 && opF == opLitBool && saF == 0+ then Pass+ else Fail "bool encoding mismatch"+ ),+ runTestM "compile ELit NixNull" $ do+ idx <- compileExpr (ELit NixNull)+ op <- cbcOpcode idx+ pure (assertEqual "opcode" opLitNull op),+ runTestM "compile EResolvedVar" $ do+ idx <- compileExpr (EResolvedVar 3 7)+ op <- cbcOpcode idx+ a1 <- cbcArg1 idx+ a2 <- cbcArg2 idx+ pure+ ( if op == opResolvedVar && a1 == 3 && a2 == 7+ then Pass+ else+ Fail+ ( "op="+ <> T.pack (show op)+ <> " a1="+ <> T.pack (show a1)+ <> " a2="+ <> T.pack (show a2)+ )+ ),+ runTestM "compile EVar" $ do+ idx <- compileExpr (EVar "hello")+ op <- cbcOpcode idx+ sym <- cbcArg1 idx+ let symText = symbolText (Symbol sym)+ pure+ ( if op == opVar && symText == "hello"+ then Pass+ else Fail ("op=" <> T.pack (show op) <> " sym=" <> symText)+ ),+ runTestM "compile EApp" $ do+ idx <- compileExpr (EApp (EVar "f") (ELit (NixInt 1)))+ op <- cbcOpcode idx+ funcIdx <- cbcArg1 idx+ argIdx <- cbcArg2 idx+ funcOp <- cbcOpcode funcIdx+ argOp <- cbcOpcode argIdx+ pure+ ( if op == opApp && funcOp == opVar && argOp == opLitInt+ then Pass+ else+ Fail+ ( "op="+ <> T.pack (show op)+ <> " funcOp="+ <> T.pack (show funcOp)+ <> " argOp="+ <> T.pack (show argOp)+ )+ ),+ runTestM "compile EIf" $ do+ idx <-+ compileExpr+ (EIf (ELit (NixBool True)) (ELit (NixInt 1)) (ELit (NixInt 2)))+ op <- cbcOpcode idx+ condIdx <- cbcArg1 idx+ thenIdx <- cbcArg2 idx+ elseIdx <- cbcArg3 idx+ condOp <- cbcOpcode condIdx+ thenA1 <- cbcArg1 thenIdx+ elseA1 <- cbcArg1 elseIdx+ pure+ ( if op == opIf && condOp == opLitBool && thenA1 == 1 && elseA1 == 2+ then Pass+ else Fail "if structure mismatch"+ ),+ runTestM "compile EBinary" $ do+ idx <-+ compileExpr+ (EBinary OpAdd (ELit (NixInt 10)) (ELit (NixInt 20)))+ op <- cbcOpcode idx+ fl <- cbcFlags idx+ leftIdx <- cbcArg1 idx+ rightIdx <- cbcArg2 idx+ leftA1 <- cbcArg1 leftIdx+ rightA1 <- cbcArg1 rightIdx+ pure+ ( if op == opBinary && fl == binaryAdd && leftA1 == 10 && rightA1 == 20+ then Pass+ else Fail "binary structure mismatch"+ ),+ runTestM "compile EUnary" $ do+ idx <- compileExpr (EUnary OpNegate (ELit (NixInt 5)))+ op <- cbcOpcode idx+ fl <- cbcFlags idx+ operandIdx <- cbcArg1 idx+ operandA1 <- cbcArg1 operandIdx+ pure+ ( if op == opUnary && fl == unaryNegate && operandA1 == 5+ then Pass+ else Fail "unary structure mismatch"+ ),+ runTestM "compile EList" $ do+ idx <-+ compileExpr+ (EList [ELit (NixInt 1), ELit (NixInt 2), ELit (NixInt 3)])+ op <- cbcOpcode idx+ count <- cbcShortArg idx+ dataOff <- cbcArg1 idx+ c0 <- cbcData dataOff+ c1 <- cbcData (dataOff + 1)+ c2 <- cbcData (dataOff + 2)+ c0a1 <- cbcArg1 c0+ c1a1 <- cbcArg1 c1+ c2a1 <- cbcArg1 c2+ pure+ ( if op == opList && count == 3 && c0a1 == 1 && c1a1 == 2 && c2a1 == 3+ then Pass+ else+ Fail+ ( "op="+ <> T.pack (show op)+ <> " count="+ <> T.pack (show count)+ )+ ),+ runTestM "compile EStr with interpolation" $ do+ idx <-+ compileExpr+ (EStr [StrLit "hello ", StrInterp (EVar "name"), StrLit "!"])+ op <- cbcOpcode idx+ count <- cbcShortArg idx+ dataOff <- cbcArg1 idx+ -- 3 parts × 2 words each = 6 data words+ tag0 <- cbcData dataOff+ _val0 <- cbcData (dataOff + 1)+ tag1 <- cbcData (dataOff + 2)+ val1 <- cbcData (dataOff + 3)+ tag2 <- cbcData (dataOff + 4)+ -- tag0=0(lit), tag1=1(interp), tag2=0(lit)+ interpOp <- cbcOpcode val1+ pure+ ( if op == opStr+ && count == 3+ && tag0 == strpartLit+ && tag1 == strpartInterp+ && tag2 == strpartLit+ && interpOp == opVar+ then Pass+ else+ Fail+ ( "op="+ <> T.pack (show op)+ <> " count="+ <> T.pack (show count)+ <> " tag0="+ <> T.pack (show tag0)+ <> " tag1="+ <> T.pack (show tag1)+ )+ ),+ runTestM "compile EWith" $ do+ idx <- compileExpr (EWith (EVar "lib") (EVar "x"))+ op <- cbcOpcode idx+ scopeIdx <- cbcArg1 idx+ bodyIdx <- cbcArg2 idx+ scopeOp <- cbcOpcode scopeIdx+ bodyOp <- cbcOpcode bodyIdx+ pure+ ( if op == opWith && scopeOp == opVar && bodyOp == opVar+ then Pass+ else Fail "with structure mismatch"+ ),+ runTestM "compile EAssert" $ do+ idx <- compileExpr (EAssert (ELit (NixBool True)) (ELit (NixInt 1)))+ op <- cbcOpcode idx+ condIdx <- cbcArg1 idx+ bodyIdx <- cbcArg2 idx+ condOp <- cbcOpcode condIdx+ bodyOp <- cbcOpcode bodyIdx+ pure+ ( if op == opAssert && condOp == opLitBool && bodyOp == opLitInt+ then Pass+ else Fail "assert structure mismatch"+ ),+ runTestM "compile ELambda (FormalName)" $ do+ idx <-+ compileExpr+ (ELambda (FormalName "x") (EResolvedVar 0 0) NoCaptureInfo)+ op <- cbcOpcode idx+ fl <- cbcFlags idx+ bodyIdx <- cbcArg2 idx+ bodyOp <- cbcOpcode bodyIdx+ pure+ ( if op == opLambda && fl == formalName && bodyOp == opResolvedVar+ then Pass+ else+ Fail+ ( "op="+ <> T.pack (show op)+ <> " flags="+ <> T.pack (show fl)+ )+ ),+ runTestM "compile ELet" $ do+ idx <-+ compileExpr+ ( ELet+ [NamedBinding [StaticKey "x"] (ELit (NixInt 42))]+ (EResolvedVar 0 0)+ NoCaptureInfo+ )+ op <- cbcOpcode idx+ count <- cbcShortArg idx+ bodyIdx <- cbcArg2 idx+ bodyOp <- cbcOpcode bodyIdx+ pure+ ( if op == opLet && count == 1 && bodyOp == opResolvedVar+ then Pass+ else+ Fail+ ( "op="+ <> T.pack (show op)+ <> " count="+ <> T.pack (show count)+ )+ ),+ runTestM "compile EAttrs" $ do+ idx <-+ compileExpr+ ( EAttrs+ False+ [NamedBinding [StaticKey "a"] (ELit (NixInt 1))]+ NoCaptureInfo+ )+ op <- cbcOpcode idx+ fl <- cbcFlags idx+ count <- cbcShortArg idx+ pure+ ( if op == opAttrs && fl == 0 && count == 1+ then Pass+ else Fail "attrs structure mismatch"+ ),+ runTestM "compile EAttrs recursive" $ do+ idx <-+ compileExpr+ ( EAttrs+ True+ [ NamedBinding [StaticKey "x"] (ELit (NixInt 1)),+ NamedBinding [StaticKey "y"] (EResolvedVar 0 0)+ ]+ (Captures [(0, 0)])+ )+ op <- cbcOpcode idx+ fl <- cbcFlags idx+ count <- cbcShortArg idx+ pure+ ( if op == opAttrs && fl == 1 && count == 2+ then Pass+ else+ Fail+ ( "flags="+ <> T.pack (show fl)+ <> " count="+ <> T.pack (show count)+ )+ ),+ runTestM "compile ESelect" $ do+ idx <-+ compileExpr+ (ESelect (EVar "x") [StaticKey "a"] Nothing)+ op <- cbcOpcode idx+ fl <- cbcFlags idx+ pure+ ( if op == opSelect && fl == 0+ then Pass+ else Fail ("flags=" <> T.pack (show fl))+ ),+ runTestM "compile ESelect with default" $ do+ idx <-+ compileExpr+ (ESelect (EVar "x") [StaticKey "a"] (Just (ELit NixNull)))+ op <- cbcOpcode idx+ fl <- cbcFlags idx+ defIdx <- cbcArg3 idx+ defOp <- cbcOpcode defIdx+ pure+ ( if op == opSelect && fl == 1 && defOp == opLitNull+ then Pass+ else Fail ("flags=" <> T.pack (show fl))+ ),+ runTestM "compile EHasAttr" $ do+ idx <-+ compileExpr+ (EHasAttr (EVar "x") [StaticKey "a", StaticKey "b"])+ op <- cbcOpcode idx+ pathLen <- cbcShortArg idx+ pure+ ( if op == opHasAttr && pathLen == 2+ then Pass+ else+ Fail+ ( "op="+ <> T.pack (show op)+ <> " pathLen="+ <> T.pack (show pathLen)+ )+ ),+ -- ESearchPath is desugared by resolver to __findFile __nixPath "name",+ -- so the compiler sees an EApp chain, not a raw ESearchPath.+ runTestM "compile desugared search path" $ do+ idx <- compileExpr (EApp (EApp (EVar "__findFile") (EVar "__nixPath")) (EStr [StrLit "nixpkgs"]))+ op <- cbcOpcode idx+ pure+ ( if op == opApp+ then Pass+ else Fail ("expected opApp, got op=" <> T.pack (show op))+ ),+ runTestM "op_count grows after compilation" $ do+ before <- cbcOpCount+ _ <- compileExpr (EApp (EVar "f") (ELit (NixInt 1)))+ after <- cbcOpCount+ pure+ ( if after > before+ then Pass+ else+ Fail+ ( "before="+ <> T.pack (show before)+ <> " after="+ <> T.pack (show after)+ )+ ),+ runTestM "compile ELit NixFloat" $ do+ idx <- compileExpr (ELit (NixFloat 3.14))+ op <- cbcOpcode idx+ pure (assertEqual "opcode" opLitFloat op),+ runTestM "compile ELit NixUri" $ do+ idx <- compileExpr (ELit (NixUri "https://example.com"))+ op <- cbcOpcode idx+ sym <- cbcArg1 idx+ pure+ ( if op == opLitUri && symbolText (Symbol sym) == "https://example.com"+ then Pass+ else Fail "uri mismatch"+ ),+ runTestM "compile ELit NixPath" $ do+ idx <- compileExpr (ELit (NixPath "/nix/store/foo"))+ op <- cbcOpcode idx+ sym <- cbcArg1 idx+ pure+ ( if op == opLitPath && symbolText (Symbol sym) == "/nix/store/foo"+ then Pass+ else Fail "path mismatch"+ ),+ runTestM "compile Inherit binding" $ do+ idx <-+ compileExpr+ ( EAttrs+ False+ [Inherit Nothing ["x", "y"]]+ NoCaptureInfo+ )+ op <- cbcOpcode idx+ count <- cbcShortArg idx+ pure+ ( if op == opAttrs && count == 1+ then Pass+ else Fail "inherit binding mismatch"+ ),+ runTestM "compile ELambda (FormalSet)" $ do+ idx <-+ compileExpr+ ( ELambda+ (FormalSet [Formal "a" Nothing, Formal "b" (Just (ELit (NixInt 0)))] False)+ (EResolvedVar 0 0)+ NoCaptureInfo+ )+ op <- cbcOpcode idx+ fl <- cbcFlags idx+ pure+ ( if op == opLambda && fl == formalSet+ then Pass+ else Fail ("flags=" <> T.pack (show fl))+ ),+ runTestM "compile ELambda (FormalNamedSet)" $ do+ idx <-+ compileExpr+ ( ELambda+ (FormalNamedSet "args" [Formal "x" Nothing] True)+ (EResolvedVar 0 0)+ NoCaptureInfo+ )+ op <- cbcOpcode idx+ fl <- cbcFlags idx+ pure+ ( if op == opLambda && fl == formalNamedSet+ then Pass+ else Fail ("flags=" <> T.pack (show fl))+ ),+ runTestM "compile CaptureInfo (Captures)" $ do+ idx <-+ compileExpr+ ( ELambda+ (FormalName "x")+ (EResolvedVar 0 0)+ (Captures [(1, 2), (3, 4)])+ )+ op <- cbcOpcode idx+ capOff <- cbcArg3 idx+ capTag <- cbcData capOff+ capCount <- cbcData (capOff + 1)+ capL0 <- cbcData (capOff + 2)+ capI0 <- cbcData (capOff + 3)+ capL1 <- cbcData (capOff + 4)+ capI1 <- cbcData (capOff + 5)+ pure+ ( if op == opLambda+ && capTag == captureSlots+ && capCount == 2+ && capL0 == 1+ && capI0 == 2+ && capL1 == 3+ && capI1 == 4+ then Pass+ else+ Fail+ ( "capTag="+ <> T.pack (show capTag)+ <> " capCount="+ <> T.pack (show capCount)+ )+ ),+ runTestM "compile CaptureInfo (CapturesWithScopes)" $ do+ idx <-+ compileExpr+ ( ELambda+ (FormalName "x")+ (EResolvedVar 0 0)+ (CapturesWithScopes [(0, 0)])+ )+ capOff <- cbcArg3 idx+ capTag <- cbcData capOff+ pure (assertEqual "captureTag" captureWithScopes capTag),+ runTestM "compile EWithVar" $ do+ idx <- compileExpr (EWithVar "dynamic")+ op <- cbcOpcode idx+ sym <- cbcArg1 idx+ pure+ ( if op == opWithVar && symbolText (Symbol sym) == "dynamic"+ then Pass+ else Fail "withvar mismatch"+ ),+ runTestM "compile EIndStr" $ do+ idx <- compileExpr (EIndStr [StrLit "indented"])+ op <- cbcOpcode idx+ count <- cbcShortArg idx+ pure+ ( if op == opIndStr && count == 1+ then Pass+ else Fail "indstr mismatch"+ )+ ]++-- --------------------------------------------------------------------------- -- Main -- --------------------------------------------------------------------------- main :: IO ()-main = do+main = bracket_ arenaInit arenaDestroy $ do hSetBuffering stdout LineBuffering putStrLn "nova-nix test suite" putStrLn "==================="@@ -3334,6 +4207,7 @@ testBatchB, testBatchC, testBatchCIO,+ testBlackhole, testBatchD, testBatchE, testBatchEIO,@@ -3354,7 +4228,11 @@ testBuilder, testE2E, testPhase4,- testPhase4IO+ testPhase4IO,+ testSymbol,+ testCAttrSet,+ testCThunk,+ testBytecodeCompile ] let total = length results passed = length (filter id results)