nova-nix 0.1.9.0 → 0.2.0.0
raw patch · 14 files changed
+1030/−235 lines, 14 files
Files
- CHANGELOG.md +10/−1
- README.md +55/−222
- cbits/nn_arena.h +33/−0
- cbits/nn_assert.h +24/−0
- cbits/nn_attrset.h +108/−0
- cbits/nn_bytecode.h +150/−0
- cbits/nn_ctxstr.h +91/−0
- cbits/nn_env.h +106/−0
- cbits/nn_lambda.h +93/−0
- cbits/nn_list.h +61/−0
- cbits/nn_symbol.h +71/−0
- cbits/nn_thunk.h +200/−0
- nova-nix.cabal +16/−9
- src/Nix/Expr/Resolve.hs +12/−3
CHANGELOG.md view
@@ -1,10 +1,19 @@ # Changelog +## 0.2.0.0 — 2026-06-05++### Milestone: `import <nixpkgs> {}` Evaluates++- **`import <nixpkgs> {}` now evaluates to WHNF** — the top-level package set resolves and enumerates ~23,000 attributes, and a package evaluates through the full stdenv bootstrap down to a derivation: `(import <nixpkgs> {}).hello.drvPath` yields a `/nix/store/…-hello-2.12.1.drv` path. (Derivation-hash parity with upstream Nix and on-Windows building are the next fronts — this milestone is evaluation, not yet building.)+- **Fix: `inherit <name>;` de Bruijn level in positional let/rec blocks** — `inherit x;` (without `from`) in a positional `let`/`rec` block desugars to `x = x;` with the right-hand side resolved against the *outer* scope, so it refers to the enclosing `x` rather than the binding being defined. But the desugared thunk is evaluated at runtime in the *inner* (let/rec) env, which adds one parent-chain level the outer resolution did not count — so every resolved level was short by one. This produced `nn_env_lookup_resolved: idx out of bounds` whenever an outer *lexical* variable was inherited inside a positional block — first triggered by perl's `inherit version;` (argument-set slot 7) nested in a 4-binding `let`. Fixed by shifting the desugared `inherit` RHS up one de Bruijn level in `Nix.Expr.Resolve`. (`inherit (from) …` was already correct: its RHS resolves against the inner scope.)+- **Fix: Hackage build — ship C headers in the sdist** — the `cbits/*.h` headers are `#include`d by the C sources (via `include-dirs: cbits`) but were never listed in the cabal file, so `cabal sdist` omitted them and the Hackage build failed with `nn_*.h: No such file or directory`. Added `extra-source-files: cbits/*.h`. A regression introduced in 0.1.9.0 (the first release to ship the C data layer); CI never caught it because CI builds the working tree, not the sdist. Verified by building from a freshly-extracted sdist tarball in isolation.+- 593 tests, `-Werror` clean, ormolu clean, hlint clean+ ## 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.+- **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 stdenv bootstrap now progresses through derivation construction instead of looping (next blocker: a variable-slot bug while evaluating perl's `@`-pattern). ### Technical Audit + C Data Layer Polish
README.md view
@@ -1,273 +1,106 @@ <div align="center"> <h1>nova-nix</h1>-<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>+<p><strong>A Windows-native Nix, from scratch.</strong></p>+<p>Parser, lazy evaluator, content-addressed store, derivation builder, and binary-cache substituter — in Haskell and C99. Runs natively on Windows, macOS, and Linux. No WSL, no Cygwin.</p> [](https://github.com/Novavero-AI/nova-nix/actions/workflows/ci.yml) [](https://hackage.haskell.org/package/nova-nix)-+  -</p> </div> --- -## What is nova-nix?+## Status -| 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). |+nova-nix evaluates real nixpkgs. `import <nixpkgs> {}` resolves the top-level package set (~23,000 attributes) to weak head normal form, and a package evaluates through the stdenv bootstrap down to a derivation: -Every module is pure by default. IO at the boundaries only.+```console+$ NIX_PATH=nixpkgs=/path/to/nixpkgs \+ nova-nix eval --expr '(import <nixpkgs> { system = "x86_64-linux"; }).hello.drvPath'+"/nix/store/azg0gjls29w3sii2kjp4c1v85ka5alnp-hello-2.12.1.drv"+``` ----+It targets the Nix 2.24 language. Evaluation is the milestone reached so far; confirming derivation-hash parity with upstream Nix, and *building* on Windows, are the work ahead. -## Try It+## Quickstart ```bash git clone https://github.com/Novavero-AI/nova-nix.git cd nova-nix-cabal run 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"; }; }-```--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-nova-nix eval --expr 'EXPR' # Evaluate an inline expression-nova-nix build FILE.nix # Build a derivation-nova-nix --nix-path nixpkgs=/path eval FILE # Add search paths (repeatable)+cabal build ``` -```bash+```console $ nova-nix eval --expr '1 + 2' 3 -$ nova-nix eval --expr 'builtins.map (x: x * x) [1 2 3 4 5]'+$ nova-nix eval --expr 'builtins.map (x: x * x) [ 1 2 3 4 5 ]' [ 1 4 9 16 25 ] -$ NIX_PATH=nixpkgs=/path/to/nixpkgs nova-nix eval --expr '(import <nixpkgs/lib>).trivial.version'-"24.11pre-git"-```--```bash-$ nova-nix build hello.nix-/nix/store/abc...-hello-```--The `build` command evaluates, extracts the derivation, builds the dependency graph, checks binary caches, builds locally, and registers outputs.-------## Architecture--```- 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 |- +------------------------------------------------++$ nova-nix eval FILE.nix # evaluate a file+$ nova-nix build FILE.nix # build a derivation+$ nova-nix --nix-path nixpkgs=/path eval FILE.nix ``` -**Key design decisions:**--- **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`.-------## Performance--The C99 data layer moves all evaluation data off the GHC heap:--| 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%** |--Measured on a stress test with 100k attribute sets, recursive computations, list operations, and overlay patterns.--**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.-------## Windows Native--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--### Parser--| Module | Purpose |-|--------|---------|-| `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` | 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 |+## How it works -### C99 Data Layer+Six layers — Haskell for logic, C99 for data: -| Module | Purpose |-|--------|---------|-| `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 |+1. **Parser** (`Nix.Parser`, `Nix.Expr`) — hand-rolled recursive descent; the full Nix grammar to a 19-constructor AST.+2. **Evaluator** (`Nix.Eval`) — the AST compiles to a flat 24-opcode bytecode that a lazy, thunk-memoizing evaluator runs. Recursive `let`/`rec` are knot-tied; reference cycles are caught by blackhole detection. The evaluator is polymorphic over its effect via `MonadEval` — `PureEval` for tests, `EvalIO` for the real thing.+3. **Data layer** (`cbits/nn_*.c`) — nine arena-allocated C99 modules (interned symbols, sorted attrsets, thunks, environments, lists, context strings, bytecode, lambdas) hold evaluation data off the GHC heap. Haskell calls C to create and query it; C never calls back.+4. **Store** (`Nix.Store`) — content-addressed `/nix/store` (`C:\nix\store` on Windows) with SQLite metadata and reference scanning.+5. **Builder** (`Nix.Builder`) — dependency graph, topological sort, and binary-cache substitution before local builds.+6. **Substituter** (`Nix.Substituter`) — the HTTP binary-cache protocol: narinfo parsing, Ed25519 verification, NAR download and unpack. Built on [nova-cache](https://github.com/Novavero-AI/nova-cache). -### Store + Builder+Two decisions shape the rest. **Haskell owns evaluation, C owns data layout** — bulk eval state lives off the GHC heap, so large evaluations don't thrash the collector. And **`derivation` is a lazy wrapper over the eager `derivationStrict` primop** (as in Nix's `corepkgs/derivation.nix`), so referencing a package never forces its build closure. -| 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 |+## Windows-native ----+| Unix assumption | How nova-nix handles it |+|---|---|+| `/nix/store` | `C:\nix\store` — every path is parameterized, never hardcoded |+| `fork`/`exec` | `CreateProcess` via `System.Process` |+| Symlinks | Developer-mode symlinks, with junction / copy fallback |+| 260-char path limit | `\\?\` extended-length prefixes |+| A system `bash` | the builder ships `bash.exe` in the store (MSYS2) | ## Roadmap -### Done+**Done** — parser, lazy bytecode evaluator, the Nix `builtins` set, the C99 data layer, content-addressed store, derivation builder, binary-cache substituter, and `import <nixpkgs> {}` evaluation through to `hello.drvPath`. -- [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** -### Next+- Derivation-hash parity — confirm computed `drvPath`/`outPath` byte-match upstream Nix across nixpkgs.+- Windows stdenv — MinGW GCC + MSYS2 coreutils bootstrap for native builds.+- Substituter — XZ decompression and real NAR hashing (currently uncompressed-only, with a placeholder hash). -- [ ] 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+## Library usage -### Long-Term+```haskell+import Nix.Parser (parseNix)+import Nix.Eval (PureEval (..), eval)+import Nix.Builtins (builtinEnv) -- [ ] `nova-nix shell` — enter a development shell-- [ ] `nova-nix repl` — interactive evaluator-- [ ] Nix daemon protocol compatibility-- [ ] XZ decompression for binary cache downloads+main :: IO ()+main = case parseNix "<expr>" "let x = 5; in x * 2 + 1" of+ Left err -> print err+ Right expr -> print (runPureEval (eval (builtinEnv 0 []) expr)) -- Right (VInt 11)+``` ----+The evaluator is polymorphic over `MonadEval`: `PureEval` for deterministic tests, `EvalIO` for filesystem access. -## Build & Test+## Build & test ```bash-cabal build # Build library + CLI-cabal test # Run all 593 tests-cabal build --ghc-options="-Werror" # Warnings as errors (CI default)+cabal build+cabal test ``` 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.-------<p align="center">- <sub>BSD-3-Clause · <a href="https://github.com/Novavero-AI">Novavero AI</a></sub>-</p>+<p align="center"><sub>BSD-3-Clause · <a href="https://github.com/Novavero-AI">Novavero AI</a></sub></p>
+ cbits/nn_arena.h view
@@ -0,0 +1,33 @@+/*+ * nn_arena.h — Unified arena lifecycle and StablePtr cleanup.+ *+ * Provides batch collection of StablePtr payloads from the thunk arena+ * for efficient cleanup. After M6 bytecode integration, PENDING thunks+ * hold (bc_idx, StablePtr Env) — the Expr is eliminated, replaced by+ * a bytecode index. COMPUTED/NN_VALUE_PTR thunks hold StablePtr NixValue.+ *+ * Inline scalar/C-pointer payloads (INT, FLOAT, BOOL, NULL, STR,+ * PATH, LIST, ATTRS, CTXSTR) are skipped.+ *+ * Used by Haskell-side Arena.hs to free all StablePtrs before+ * destroying the sub-arenas (thunk, env, symbol).+ */++#ifndef NN_ARENA_H+#define NN_ARENA_H++#include <stdint.h>++/* Bulk-free all tracked attribute sets (called during arena teardown). */+void nn_attrset_free_all(void);++/* Count how many thunk payloads are StablePtrs (need freeing).+ * Iterates all thunks, checks state + val_tag. */+uint32_t nn_arena_stableptr_count(void);++/* Collect StablePtr payloads into `output`. Writes at most `max_count`+ * pointers. Returns the number actually written.+ * Caller must allocate the output buffer. */+uint32_t nn_arena_collect_stableptrs(void **output, uint32_t max_count);++#endif /* NN_ARENA_H */
+ cbits/nn_assert.h view
@@ -0,0 +1,24 @@+#ifndef NN_ASSERT_H+#define NN_ASSERT_H++#include <stdio.h>+#include <stdlib.h>++/*+ * Debug-mode bounds checking. Compiles to nothing under NDEBUG.+ * Use for array accesses, index validation, and invariant checks.+ */+#ifdef NDEBUG+#define NN_ASSERT(cond, msg) ((void)0)+#else+#define NN_ASSERT(cond, msg) \+ do { \+ if (!(cond)) { \+ fprintf(stderr, "NN_ASSERT failed: %s\n at %s:%d\n", \+ (msg), __FILE__, __LINE__); \+ abort(); \+ } \+ } while (0)+#endif++#endif /* NN_ASSERT_H */
+ cbits/nn_attrset.h view
@@ -0,0 +1,108 @@+/*+ * nn_attrset.h — Sorted symbol-keyed attribute set for nova-nix.+ *+ * Replaces Haskell's Data.Map.Strict (48 bytes per Bin node, pointer-+ * chasing balanced tree) with a contiguous sorted array of (symbol, slot)+ * pairs. Binary search for O(log n) lookup; linear scan for iteration.+ *+ * Construction is two-phase:+ * 1. nn_attrset_new(cap) + nn_attrset_insert() — append unsorted+ * 2. nn_attrset_freeze() — sort by symbol ID, enable binary search+ *+ * After freeze, the set is immutable (values can be updated in-place+ * but keys cannot change). This matches Nix semantics: attribute sets+ * are constructed once and never gain/lose keys.+ *+ * Values are opaque void* — Haskell passes StablePtr Thunk. The C+ * side never dereferences them.+ *+ * Lifecycle: caller owns the nn_attrset_t* and must call nn_attrset_free.+ * Values (StablePtrs) are NOT freed by nn_attrset_free — Haskell owns them.+ */++#ifndef NN_ATTRSET_H+#define NN_ATTRSET_H++#include "nn_symbol.h"+#include <stddef.h>+#include <stdint.h>++/* --- Types --- */++/* Opaque attribute set handle. */+typedef struct nn_attrset nn_attrset_t;++/* --- Lifecycle --- */++/* Allocate a new empty attribute set with space for `capacity` entries.+ * Returns NULL on allocation failure. */+nn_attrset_t *nn_attrset_new(uint32_t capacity);++/* Free the attribute set structure. Does NOT free the value pointers+ * (Haskell owns those via StablePtr). */+void nn_attrset_free(nn_attrset_t *set);++/* --- Construction (before freeze) --- */++/* Append a key-value pair. Keys need not be sorted or unique at this+ * stage — duplicates are resolved at freeze time (last insert wins,+ * matching Nix // merge semantics).+ * Grows the internal arrays if capacity is exceeded. */+void nn_attrset_insert(nn_attrset_t *set, nn_symbol_t key, void *value);++/* Sort entries by symbol ID and deduplicate (last writer wins).+ * Must be called exactly once before any lookup/keys/values access.+ * After freeze, no more inserts are allowed. */+void nn_attrset_freeze(nn_attrset_t *set);++/* --- Query (after freeze) --- */++/* Look up a key by symbol. Returns the value pointer, or NULL if+ * the key is not present. O(log n) binary search. */+void *nn_attrset_lookup(const nn_attrset_t *set, nn_symbol_t key);++/* Return the index of a key, or -1 if not found.+ * Useful for updating value slots in place. */+int32_t nn_attrset_index(const nn_attrset_t *set, nn_symbol_t key);++/* Update the value at a known index. Index must be valid (0 <= idx < count).+ * Used for lazy materialization: slot starts NULL, gets filled on first force. */+void nn_attrset_set_value(nn_attrset_t *set, uint32_t idx, void *value);++/* Return the value at a known index. */+void *nn_attrset_get_value(const nn_attrset_t *set, uint32_t idx);++/* Return the key at a known index. */+nn_symbol_t nn_attrset_get_key(const nn_attrset_t *set, uint32_t idx);++/* --- Bulk access --- */++/* Number of entries (after freeze: unique keys). */+uint32_t nn_attrset_size(const nn_attrset_t *set);++/* Direct pointer to the sorted keys array (count elements).+ * Valid until the set is freed. */+const nn_symbol_t *nn_attrset_keys_ptr(const nn_attrset_t *set);++/* --- Lifecycle: bulk cleanup --- */++/* Free all tracked attribute sets at once (arena-style cleanup).+ * Every set created via nn_attrset_new is automatically tracked.+ * Call this once at evaluation end, before destroying sub-arenas. */+void nn_attrset_free_all(void);++/* --- Set operations --- */++/* Create a new set that is the right-biased union of `a` and `b`+ * (keys in `b` override `a`). Both inputs must be frozen.+ * The result is returned frozen. Caller owns the result. */+nn_attrset_t *nn_attrset_union(const nn_attrset_t *a, const nn_attrset_t *b);++/* Create a new set with the given keys removed.+ * Input must be frozen. Result is returned frozen. */+nn_attrset_t *nn_attrset_remove_keys(+ const nn_attrset_t *set,+ const nn_symbol_t *keys,+ uint32_t key_count);++#endif /* NN_ATTRSET_H */
+ cbits/nn_bytecode.h view
@@ -0,0 +1,150 @@+/*+ * nn_bytecode.h -- Flat bytecode representation for Nix expressions.+ *+ * Compiles the 19-constructor Expr AST into a contiguous instruction+ * array (nn_op_t[]) plus a variable-length data buffer (uint32_t[]).+ * Each Expr node maps to exactly one instruction. Children are+ * referenced by their index in the instruction array (post-order+ * traversal guarantees children < parent). Variable-length data+ * (string parts, bindings, formals, capture info, attr paths) is+ * stored in the data buffer, referenced by offset from instructions.+ *+ * Goal: eliminate StablePtr (Expr, Env) in pending thunks. After+ * compilation, pending thunks hold (bc_idx, nn_env_t*) -- both+ * C-native, zero GHC heap pressure.+ *+ * Lifecycle: nn_bytecode_init() before evaluation, nn_bytecode_destroy()+ * after. Not thread-safe -- single-threaded evaluation only.+ */++#ifndef NN_BYTECODE_H+#define NN_BYTECODE_H++#include <stdint.h>++/* --- Opcodes (one per Expr constructor) --- */++#define NN_OP_LIT_INT 0+#define NN_OP_LIT_FLOAT 1+#define NN_OP_LIT_BOOL 2+#define NN_OP_LIT_NULL 3+#define NN_OP_LIT_URI 4+#define NN_OP_LIT_PATH 5+#define NN_OP_STR 6+#define NN_OP_IND_STR 7+#define NN_OP_VAR 8+#define NN_OP_WITH_VAR 9+#define NN_OP_RESOLVED_VAR 10+#define NN_OP_ATTRS 11+#define NN_OP_LIST 12+#define NN_OP_SELECT 13+#define NN_OP_HAS_ATTR 14+#define NN_OP_APP 15+#define NN_OP_LAMBDA 16+#define NN_OP_LET 17+#define NN_OP_IF 18+#define NN_OP_WITH 19+#define NN_OP_ASSERT 20+#define NN_OP_UNARY 21+#define NN_OP_BINARY 22+#define NN_OP_SEARCH_PATH 23++/* --- UnaryOp flags (NN_OP_UNARY) --- */++#define NN_UNARY_NOT 0+#define NN_UNARY_NEGATE 1++/* --- BinaryOp flags (NN_OP_BINARY) --- */++#define NN_BINARY_ADD 0+#define NN_BINARY_SUB 1+#define NN_BINARY_MUL 2+#define NN_BINARY_DIV 3+#define NN_BINARY_AND 4+#define NN_BINARY_OR 5+#define NN_BINARY_IMPL 6+#define NN_BINARY_EQ 7+#define NN_BINARY_NEQ 8+#define NN_BINARY_LT 9+#define NN_BINARY_LTE 10+#define NN_BINARY_GT 11+#define NN_BINARY_GTE 12+#define NN_BINARY_CONCAT 13+#define NN_BINARY_UPDATE 14++/* --- Formal type flags (NN_OP_LAMBDA) --- */++#define NN_FORMAL_NAME 0+#define NN_FORMAL_SET 1+#define NN_FORMAL_NAMED_SET 2++/* --- String part tags (data buffer) --- */++#define NN_STRPART_LIT 0+#define NN_STRPART_INTERP 1++/* --- Binding type tags (data buffer) --- */++#define NN_BIND_NAMED 0+#define NN_BIND_INHERIT 1++/* --- CaptureInfo type tags (data buffer) --- */++#define NN_CAPTURE_NONE 0+#define NN_CAPTURE_SLOTS 1+#define NN_CAPTURE_WITH_SCOPES 2++/* --- Attr path element tags (data buffer) --- */++#define NN_ATTRKEY_STATIC 0+#define NN_ATTRKEY_DYNAMIC 1++/* --- Instruction (16 bytes, naturally aligned) --- */++typedef struct nn_op {+ uint8_t opcode; /* Which Expr constructor */+ uint8_t flags; /* Sub-type: BinaryOp, UnaryOp, formal type, etc. */+ uint16_t short_arg; /* Small immediate: count, bool flag, etc. */+ uint32_t arg1; /* Child index, symbol, data offset, lo32, etc. */+ uint32_t arg2; /* Child index, symbol, hi32, etc. */+ uint32_t arg3; /* Child index, data offset, etc. */+} nn_op_t;++/* --- Lifecycle --- */++/* Initialize the global bytecode store. op_capacity is initial+ * instruction slots (0 = default 65536). data_capacity is initial+ * data buffer slots (0 = default 131072). Both grow automatically. */+void nn_bytecode_init(uint32_t op_capacity, uint32_t data_capacity);++/* Destroy the global bytecode store, freeing all memory. */+void nn_bytecode_destroy(void);++/* --- Emit --- */++/* Append one instruction. Returns the instruction index. */+uint32_t nn_bc_emit(uint8_t opcode, uint8_t flags, uint16_t short_arg,+ uint32_t arg1, uint32_t arg2, uint32_t arg3);++/* Append one uint32 to the data buffer. Returns the data offset. */+uint32_t nn_bc_emit_data(uint32_t value);++/* --- Read instructions --- */++uint8_t nn_bc_opcode(uint32_t idx);+uint8_t nn_bc_flags(uint32_t idx);+uint16_t nn_bc_short_arg(uint32_t idx);+uint32_t nn_bc_arg1(uint32_t idx);+uint32_t nn_bc_arg2(uint32_t idx);+uint32_t nn_bc_arg3(uint32_t idx);++/* --- Read data --- */++uint32_t nn_bc_data(uint32_t offset);++/* --- Diagnostics --- */++uint32_t nn_bc_op_count(void);+uint32_t nn_bc_data_count(void);++#endif /* NN_BYTECODE_H */
+ cbits/nn_ctxstr.h view
@@ -0,0 +1,91 @@+/*+ * nn_ctxstr.h — Context-bearing strings for nova-nix.+ *+ * Nix strings carry a StringContext: a set of store path references+ * that track derivation dependencies. Context-free strings use+ * nn_thunk tag 4 (bare nn_symbol_t). Context-bearing strings use+ * tag 8 (nn_ctxstr_t pointer).+ *+ * Each context element is one of:+ * SCPlain — plain store path reference (inputSrcs)+ * SCDrvOutput — derivation output (.drv path + output name)+ * SCAllOutputs — all outputs of a derivation (drvPath itself)+ *+ * StorePaths are represented as two interned symbols (hash + name).+ *+ * Memory: each nn_ctxstr_t is a single contiguous allocation via+ * C99 flexible array member. Headers are tracked globally for+ * bulk cleanup via nn_ctxstr_free_all().+ */++#ifndef NN_CTXSTR_H+#define NN_CTXSTR_H++#include <stdint.h>++/* --- Context element tags --- */++#define NN_SCE_PLAIN 0+#define NN_SCE_DRV_OUTPUT 1+#define NN_SCE_ALL_OUTPUTS 2++/* --- Types --- */++/* A single string context element: store path reference with kind tag.+ * sp_hash and sp_name are interned nn_symbol_t values representing+ * the StorePath's hash and name fields. output is an interned symbol+ * for the derivation output name (only valid when tag == DRV_OUTPUT,+ * 0 otherwise). */+typedef struct nn_sce {+ uint8_t tag;+ uint32_t sp_hash;+ uint32_t sp_name;+ uint32_t output;+} nn_sce_t;++/* A string with context. text is an interned nn_symbol_t for the+ * string content. ctx_count is the number of context elements.+ * ctx[] is a C99 flexible array of context elements, sorted by+ * (tag, sp_hash, sp_name, output) for deterministic comparison.+ *+ * Single contiguous allocation: header + elements. */+typedef struct nn_ctxstr {+ uint32_t text;+ uint16_t ctx_count;+ nn_sce_t ctx[];+} nn_ctxstr_t;++/* --- Lifecycle --- */++/* Allocate a new nn_ctxstr_t with space for ctx_count elements.+ * Elements are uninitialized — caller must fill via nn_ctxstr_set_*.+ * Tracked globally for bulk cleanup. */+nn_ctxstr_t *nn_ctxstr_new(uint32_t text, uint16_t ctx_count);++/* Free all tracked nn_ctxstr_t allocations. Called during arena+ * teardown, after thunk iteration but before env/symbol cleanup. */+void nn_ctxstr_free_all(void);++/* --- Element setters --- */++void nn_ctxstr_set_plain(nn_ctxstr_t *s, uint16_t idx,+ uint32_t sp_hash, uint32_t sp_name);++void nn_ctxstr_set_drv_output(nn_ctxstr_t *s, uint16_t idx,+ uint32_t sp_hash, uint32_t sp_name,+ uint32_t output);++void nn_ctxstr_set_all_outputs(nn_ctxstr_t *s, uint16_t idx,+ uint32_t sp_hash, uint32_t sp_name);++/* --- Accessors --- */++uint32_t nn_ctxstr_text(const nn_ctxstr_t *s);+uint16_t nn_ctxstr_ctx_count(const nn_ctxstr_t *s);++uint8_t nn_ctxstr_elem_tag(const nn_ctxstr_t *s, uint16_t idx);+uint32_t nn_ctxstr_elem_hash(const nn_ctxstr_t *s, uint16_t idx);+uint32_t nn_ctxstr_elem_name(const nn_ctxstr_t *s, uint16_t idx);+uint32_t nn_ctxstr_elem_output(const nn_ctxstr_t *s, uint16_t idx);++#endif /* NN_CTXSTR_H */
+ cbits/nn_env.h view
@@ -0,0 +1,106 @@+/*+ * nn_env.h — C-native evaluation environments.+ *+ * Nix evaluation creates millions of environments, each with a+ * variable-size array of slot pointers, optional lazy scope (attr set),+ * optional parent pointer, and a with-scopes chain.+ *+ * All nn_env_t structs and their backing arrays are arena-allocated+ * from a page-based bump allocator. Nothing is individually freed —+ * nn_env_destroy() tears down everything at eval end.+ *+ * Lifecycle: nn_env_init() before evaluation, nn_env_destroy() after.+ * Not thread-safe — single-threaded evaluation only.+ */++#ifndef NN_ENV_H+#define NN_ENV_H++#include <stdint.h>++/* --- Env struct --- */++/* 48 bytes on 64-bit (with padding after slot_count and with_count).+ * Could be reduced to 40 bytes by reordering fields, but the current+ * layout groups related fields logically and matches access patterns. */+typedef struct nn_env {+ void **slots; /* nn_thunk_t* array (or NULL for 0 slots) */+ uint32_t slot_count; /* 4 bytes + 4 pad to align lazy_scope */+ void *lazy_scope; /* nn_attrset_t* or NULL */+ struct nn_env *parent; /* nn_env_t* or NULL */+ void **with_scopes; /* array of nn_attrset_t* (or NULL) */+ uint16_t with_count; /* 2 bytes + 6 pad to struct alignment */+} nn_env_t;++/* --- Lifecycle --- */++/* Initialize the global env allocator. Must be called once before+ * any nn_env_* calls (except nn_env_destroy). */+void nn_env_init(void);++/* Destroy the global env allocator, freeing all page memory.+ * All pointers returned by any nn_env_* function become invalid. */+void nn_env_destroy(void);++/* --- Slot allocation (unchanged from original API) --- */++/* Allocate an array of `count` void* pointers. O(1) amortized.+ * All slots initialized to NULL. Returns NULL if count is 0. */+void **nn_env_alloc_slots(uint32_t count);++/* --- Env constructors (all arena-allocated) --- */++/* Return a pointer to the global empty env (all fields zero/NULL).+ * Valid until nn_env_destroy(). Do not modify the returned struct. */+nn_env_t *nn_env_empty(void);++/* Full constructor: set all fields explicitly. */+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);++/* Child env with positional slots, inheriting parent's with-scopes.+ * lazy_scope = NULL. */+nn_env_t *nn_env_from_slots(void **slots, uint32_t slot_count,+ nn_env_t *parent);++/* Copy base env with a new with-scope prepended (innermost position).+ * Allocates a new with-scopes array of size (base->with_count + 1). */+nn_env_t *nn_env_push_with(nn_env_t *base, void *scope);++/* Minimal env: slots only, no parent, no with-scopes, no lazy scope. */+nn_env_t *nn_env_new_minimal(void **slots, uint32_t slot_count);++/* --- Accessors --- */++void **nn_env_slots(const nn_env_t *env);+uint32_t nn_env_slot_count(const nn_env_t *env);+void *nn_env_lazy_scope(const nn_env_t *env);+nn_env_t *nn_env_parent(const nn_env_t *env);+void **nn_env_with_scopes(const nn_env_t *env);+uint16_t nn_env_with_count(const nn_env_t *env);++/* --- Lookup --- */++/* Resolved variable lookup: walk `level` parent hops, then read+ * slots[idx]. Returns the nn_thunk_t* at that position.+ * Caller must ensure level/idx are in bounds (guaranteed by+ * the resolution pass). */+void *nn_env_lookup_resolved(const nn_env_t *env, int level, int idx);++/* Walk parent chain to the root env (parent == NULL), return its+ * lazy_scope. Returns NULL if the root has no lazy scope. */+void *nn_env_root_scope(const nn_env_t *env);++/* --- With-scopes array allocation --- */++/* Allocate an arena array of `count` void* pointers for with-scopes.+ * All entries initialized to NULL. Returns NULL if count is 0. */+void **nn_env_alloc_with_scopes(uint16_t count);++/* --- Diagnostics --- */++/* Total bytes allocated across all pages (for memory tracking). */+uint64_t nn_env_bytes_allocated(void);++#endif /* NN_ENV_H */
+ cbits/nn_lambda.h view
@@ -0,0 +1,93 @@+/*+ * nn_lambda.h — Lambda closure structs for nova-nix.+ *+ * Stores lambda closures entirely in C: captured environment (nn_env_t*),+ * body bytecode index, and formal parameter specification. Replaces+ * StablePtr NixValue for VLambda on the GHC heap (~25% of remaining+ * heap after M6) with a C-native tag 9 in the thunk system.+ *+ * Formals are stored as a type tag (Name/Set/NamedSet) plus an array+ * of (symbol, has_default, default_bc_idx) entries for set patterns.+ * Interned symbols avoid any string allocation — pointer equality+ * for name comparison.+ *+ * Lambda structs are malloc'd and tracked for bulk cleanup via+ * nn_lambda_free_all(). Formal entry arrays use malloc (small, typ.+ * 1-20 entries). All freed at evaluation end.+ *+ * Lifecycle: constructed during evaluation, freed via+ * nn_lambda_free_all() at arena teardown. Not thread-safe.+ */++#ifndef NN_LAMBDA_H+#define NN_LAMBDA_H++#include <stdint.h>++/* Forward declarations */+struct nn_env;++/* --- Formals type tags --- */++#define NN_FORMALS_NAME 0 /* x: body */+#define NN_FORMALS_SET 1 /* { a, b, ... }: body */+#define NN_FORMALS_NAMED_SET 2 /* x@{ a, b, ... }: body */++/* --- Types --- */++/* A single formal entry: name + optional default bytecode index. */+typedef struct nn_formal_entry {+ uint32_t name_sym; /* interned symbol ID */+ uint32_t has_default; /* 0 = required, 1 = has default */+ uint32_t default_bc_idx; /* bytecode index of default (valid if has_default) */+} nn_formal_entry_t;++/* Lambda closure: env + body + formals specification.+ * Packed for minimal size (pointers first, then uint32s, then small fields).+ * 28 bytes on 64-bit (no padding). */+typedef struct nn_lambda {+ struct nn_env *env; /* captured closure environment */+ nn_formal_entry_t *entries; /* formal entries (malloc'd, or NULL) */+ uint32_t body_bc_idx; /* bytecode index of lambda body */+ uint32_t name_sym; /* symbol for Name/NamedSet binding */+ uint16_t formal_count; /* number of formal entries */+ uint8_t formals_type; /* NN_FORMALS_NAME/SET/NAMED_SET */+ uint8_t allow_extra; /* 1 if ellipsis (...) present */+} nn_lambda_t;++/* --- Lifecycle --- */++/* Allocate a new lambda closure. The entries array is allocated+ * internally (formal_count entries). Fill entries via+ * nn_lambda_set_entry() after construction.+ * Returns NULL on allocation failure. */+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);++/* Set a formal entry at the given index (for construction).+ * Index must be < formal_count. */+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);++/* Free all tracked lambda structs (arena-style cleanup).+ * Also frees all entries arrays.+ * Call once at evaluation end. */+void nn_lambda_free_all(void);++/* --- Accessors --- */++struct nn_env *nn_lambda_env(const nn_lambda_t *lam);+uint32_t nn_lambda_body(const nn_lambda_t *lam);+uint8_t nn_lambda_formals_type(const nn_lambda_t *lam);+uint32_t nn_lambda_name_sym(const nn_lambda_t *lam);+uint8_t nn_lambda_allow_extra(const nn_lambda_t *lam);+uint16_t nn_lambda_formal_count(const nn_lambda_t *lam);++/* Read formal entry fields by index (must be < formal_count). */+uint32_t nn_lambda_entry_name(const nn_lambda_t *lam, uint16_t idx);+uint32_t nn_lambda_entry_has_default(const nn_lambda_t *lam, uint16_t idx);+uint32_t nn_lambda_entry_default(const nn_lambda_t *lam, uint16_t idx);++#endif /* NN_LAMBDA_H */
+ cbits/nn_list.h view
@@ -0,0 +1,61 @@+/*+ * nn_list.h — Contiguous array of thunk pointers for nova-nix lists.+ *+ * Replaces Haskell's [Thunk] (linked-list cons cells, ~48 bytes per+ * element on the GHC heap) with a flat C array of nn_thunk_t* pointers+ * (8 bytes per element, cache-friendly layout).+ *+ * The items array is allocated via nn_env_alloc_slots (page-based bump+ * allocator). The nn_list_t header is malloc'd and tracked for bulk+ * cleanup via nn_list_free_all(). Both are freed at evaluation end.+ *+ * Lists are immutable after construction: allocate with nn_list_new,+ * fill via nn_list_set, then read with nn_list_get/nn_list_count.+ *+ * Lifecycle: constructed during evaluation, freed via nn_list_free_all()+ * at arena teardown. Not thread-safe — single-threaded evaluation only.+ */++#ifndef NN_LIST_H+#define NN_LIST_H++#include <stdint.h>++/* Forward declaration — nn_thunk_t is defined in nn_thunk.h */+struct nn_thunk;++/* --- Types --- */++/* A list: contiguous array of thunk pointers with a count. */+typedef struct nn_list {+ struct nn_thunk **items; /* contiguous array (page-allocated) */+ uint32_t count; /* number of elements */+} nn_list_t;++/* --- Lifecycle --- */++/* Allocate a new list with space for `count` thunk pointers.+ * The items array is allocated via the env page allocator (O(1)).+ * The header is malloc'd and tracked for bulk cleanup.+ * Returns NULL on allocation failure or if count is 0. */+nn_list_t *nn_list_new(uint32_t count);++/* Free all tracked list headers at once (arena-style cleanup).+ * Items arrays are freed by nn_env_destroy (page allocator).+ * Call this once at evaluation end. */+void nn_list_free_all(void);++/* --- Access --- */++/* Number of elements in the list. */+uint32_t nn_list_count(const nn_list_t *list);++/* Get the thunk pointer at the given index.+ * Index must be < nn_list_count(list). */+struct nn_thunk *nn_list_get(const nn_list_t *list, uint32_t index);++/* Set the thunk pointer at the given index (for construction).+ * Index must be < nn_list_count(list). */+void nn_list_set(nn_list_t *list, uint32_t index, struct nn_thunk *thunk);++#endif /* NN_LIST_H */
+ cbits/nn_symbol.h view
@@ -0,0 +1,71 @@+/*+ * nn_symbol.h — Interned string symbols for nova-nix.+ *+ * Attribute names are the most repeated data in Nix evaluation.+ * nixpkgs uses ~8k unique names across 30k+ packages, but each name+ * appears hundreds of times. Interning deduplicates storage and+ * replaces O(n) string comparison with O(1) integer comparison.+ *+ * Symbols are uint32_t indices into a global table. The table owns+ * all string data in a contiguous arena (no per-string malloc).+ * Lookup uses open-addressing with FNV-1a hashing.+ *+ * Lifecycle: nn_symbol_init() before evaluation, nn_symbol_destroy()+ * after. Not thread-safe — single-threaded evaluation only.+ */++#ifndef NN_SYMBOL_H+#define NN_SYMBOL_H++#include <stddef.h>+#include <stdint.h>++/* --- Types --- */++/* An interned symbol: index into the global symbol table.+ * 0 is reserved as the invalid/empty sentinel. */+typedef uint32_t nn_symbol_t;++/* Invalid symbol constant. */+#define NN_SYMBOL_INVALID ((nn_symbol_t)0)++/* --- Lifecycle --- */++/* Initialize the global symbol table. Must be called once before+ * any nn_symbol_intern() calls. initial_capacity is the expected+ * number of unique symbols (hint for pre-allocation; 0 uses default). */+void nn_symbol_init(uint32_t initial_capacity);++/* Destroy the global symbol table, freeing all memory.+ * All nn_symbol_t values become invalid after this call. */+void nn_symbol_destroy(void);++/* --- Core API --- */++/* Intern a string, returning its symbol. If the string was already+ * interned, returns the existing symbol (O(1) amortized).+ * The input string is copied — the caller may free it after this call.+ * str must not be NULL. len is the byte length (not null-terminated). */+nn_symbol_t nn_symbol_intern(const char *str, size_t len);++/* Return the string data for a symbol. The pointer is valid until+ * nn_symbol_destroy(). Returns NULL for NN_SYMBOL_INVALID. */+const char *nn_symbol_text(nn_symbol_t sym);++/* Return the byte length of a symbol's string.+ * Returns 0 for NN_SYMBOL_INVALID. */+size_t nn_symbol_len(nn_symbol_t sym);++/* --- Comparison --- */++/* Symbols are equal iff their uint32_t values are equal.+ * No function call needed — just use == directly.+ * This macro exists for documentation purposes. */+#define nn_symbol_eq(a, b) ((a) == (b))++/* --- Diagnostics --- */++/* Return the number of unique symbols currently interned. */+uint32_t nn_symbol_count(void);++#endif /* NN_SYMBOL_H */
+ cbits/nn_thunk.h view
@@ -0,0 +1,200 @@+/*+ * nn_thunk.h — Arena-allocated thunk memoization cells for nova-nix.+ *+ * Thunks are the core memoization mechanism in Nix evaluation. Each+ * thunk starts as PENDING (holding a bytecode index + C environment+ * pointer) and transitions to COMPUTED (holding the result value) on+ * first force. A BLACKHOLE state detects infinite recursion.+ *+ * 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).+ * For 8M thunks: ~450 MB less GHC heap pressure.+ *+ * Arena: chunked bump allocator. Thunks are never individually freed —+ * the entire arena is destroyed at evaluation end. Pointers into the+ * arena remain valid until nn_thunk_destroy().+ *+ * PENDING payloads are StablePtr Env (~16 bytes on GHC heap). The+ * StablePtr is necessary for knot-tying: deferred env construction in+ * recursive lets/attrs requires Haskell laziness. Freed on force.+ * COMPUTED payloads are either inline scalars (int/float/bool/null via+ * val_tag 0-3), C-native pointers (str/path/list/attrs/ctxstr/lambda+ * via val_tag 4-9), or StablePtrs (for VBuiltin/VDrv via tag 255).+ *+ * Lifecycle: nn_thunk_init() before evaluation, nn_thunk_destroy()+ * after. Not thread-safe — single-threaded evaluation only.+ */++#ifndef NN_THUNK_H+#define NN_THUNK_H++#include <stdint.h>++/* --- Thunk states --- */++#define NN_THUNK_PENDING 0+#define NN_THUNK_COMPUTED 1+#define NN_THUNK_BLACKHOLE 2++/* --- Value tags (valid when state == COMPUTED) --- */++#define NN_VALUE_INT 0 /* payload = (void*)(intptr_t)int64_value */+#define NN_VALUE_FLOAT 1 /* payload = memcpy'd double */+#define NN_VALUE_BOOL 2 /* payload = (void*)(intptr_t)(0 or 1) */+#define NN_VALUE_NULL 3 /* payload = NULL */+#define NN_VALUE_STR 4 /* payload = (void*)(intptr_t)nn_symbol_t (no context) */+#define NN_VALUE_PATH 5 /* payload = (void*)(intptr_t)nn_symbol_t */+#define NN_VALUE_LIST 6 /* payload = nn_list_t* */+#define NN_VALUE_ATTRS 7 /* payload = nn_attrset_t* */+#define NN_VALUE_CTXSTR 8 /* payload = nn_ctxstr_t* (string with context) */+#define NN_VALUE_LAMBDA 9 /* payload = nn_lambda_t* (lambda closure) */+#define NN_VALUE_PTR 255 /* payload = StablePtr NixValue (complex types) */++/* --- Types --- */++/* A thunk: mutable cell holding either a pending expression or a+ * computed value. 16 bytes: state(1) + val_tag(1) + _pad(2) ++ * bc_idx(4) + payload(8). val_tag is valid only when state == COMPUTED.+ *+ * PENDING: bc_idx = bytecode instruction index+ * payload = StablePtr Env (Haskell-side, for knot-tying)+ * COMPUTED: val_tag selects interpretation of payload:+ * INT/BOOL: (intptr_t) cast of scalar+ * FLOAT: memcpy'd double+ * NULL: ignored+ * PTR: StablePtr to Haskell NixValue (complex types)+ * BLACKHOLE: bc_idx/payload = stale from PENDING (do not dereference) */+typedef struct nn_thunk {+ uint8_t state;+ uint8_t val_tag;+ uint16_t _pad;+ uint32_t bc_idx; /* PENDING: bytecode index; COMPUTED: 0 */+ void *payload;+} nn_thunk_t;++/* Nix integers are int64_t, stored inline via (intptr_t) cast into payload.+ * This requires pointers to be at least 64 bits. 32-bit targets would+ * silently truncate large integers. C99-compatible compile-time check: */+typedef char nn_thunk_ptr_size_check_[(sizeof(void *) >= sizeof(int64_t)) ? 1 : -1];++/* --- Lifecycle --- */++/* Initialize the global thunk arena. initial_capacity is the number+ * of thunks to pre-allocate per block (0 uses default: 65536 = 1 MB).+ * Must be called once before any nn_thunk_new() calls. */+void nn_thunk_init(uint32_t initial_capacity);++/* Destroy the global thunk arena, freeing all block memory.+ * Does NOT free payload StablePtrs — caller must iterate and free+ * them first (via nn_thunk_count/nn_thunk_get).+ * All nn_thunk_t pointers become invalid after this call. */+void nn_thunk_destroy(void);++/* --- Allocation --- */++/* Allocate a new PENDING thunk from the arena (legacy StablePtr path).+ * pending_data is an opaque pointer (StablePtr to Haskell value).+ * Sets bc_idx = 0 to distinguish from bytecode thunks.+ * Returns a pointer into arena memory, valid until nn_thunk_destroy(). */+nn_thunk_t *nn_thunk_new(void *pending_data);++/* Allocate a new PENDING thunk with a bytecode index + env payload.+ * bc_idx is the root instruction index in the bytecode store.+ * env_ptr is a StablePtr Env from Haskell (required for knot-tying;+ * raw Ptr would force the env at allocation time, breaking laziness). */+nn_thunk_t *nn_thunk_new_bc(uint32_t bc_idx, void *env_ptr);++/* Read the bytecode index from a PENDING thunk. */+uint32_t nn_thunk_get_bc_idx(const nn_thunk_t *thunk);++/* Set the payload of a thunk. Used for deferred env fixup in+ * knot-tying (rec attrs, let, formal defaults): allocate the thunk+ * with NULL payload first, then fill the env pointer once the+ * knot-tied env is constructed. */+void nn_thunk_set_payload(nn_thunk_t *thunk, void *payload);++/* Allocate a new pre-COMPUTED thunk from the arena (StablePtr payload).+ * value is an opaque pointer (StablePtr to Haskell NixValue).+ * val_tag is set to NN_VALUE_PTR. */+nn_thunk_t *nn_thunk_new_computed(void *value);++/* Allocate pre-COMPUTED thunks with inline scalar values (no StablePtr). */+nn_thunk_t *nn_thunk_new_computed_int(int64_t value);+nn_thunk_t *nn_thunk_new_computed_float(double value);+nn_thunk_t *nn_thunk_new_computed_bool(uint8_t value);+nn_thunk_t *nn_thunk_new_computed_null(void);++/* Allocate pre-COMPUTED thunks with C-native complex values (no StablePtr). */+nn_thunk_t *nn_thunk_new_computed_str(uint32_t symbol);+nn_thunk_t *nn_thunk_new_computed_path(uint32_t symbol);+nn_thunk_t *nn_thunk_new_computed_list(void *list);+nn_thunk_t *nn_thunk_new_computed_attrs(void *attrset);+nn_thunk_t *nn_thunk_new_computed_ctxstr(void *ctxstr);+nn_thunk_t *nn_thunk_new_computed_lambda(void *lambda);++/* --- State queries --- */++/* Read the current state (NN_THUNK_PENDING/COMPUTED/BLACKHOLE). */+uint8_t nn_thunk_state(const nn_thunk_t *thunk);++/* Read the payload pointer. Meaning depends on state + val_tag. */+void *nn_thunk_payload(const nn_thunk_t *thunk);++/* Read the value tag (valid when state == COMPUTED). */+uint8_t nn_thunk_value_tag(const nn_thunk_t *thunk);++/* Read inline scalar values from a COMPUTED thunk. */+int64_t nn_thunk_get_int(const nn_thunk_t *thunk);+double nn_thunk_get_float(const nn_thunk_t *thunk);+uint8_t nn_thunk_get_bool(const nn_thunk_t *thunk);++/* Read C-native complex values from a COMPUTED thunk. */+uint32_t nn_thunk_get_str(const nn_thunk_t *thunk);+uint32_t nn_thunk_get_path(const nn_thunk_t *thunk);+void *nn_thunk_get_list(const nn_thunk_t *thunk);+void *nn_thunk_get_attrs(const nn_thunk_t *thunk);+void *nn_thunk_get_ctxstr(const nn_thunk_t *thunk);+void *nn_thunk_get_lambda(const nn_thunk_t *thunk);++/* --- State transitions --- */++/* Transition PENDING -> BLACKHOLE (thunk is being evaluated).+ * Payload remains unchanged (caller reads it before this call).+ * Returns 1 on success, 0 if thunk is not PENDING. */+int nn_thunk_mark_blackhole(nn_thunk_t *thunk);++/* Set a non-COMPUTED thunk to COMPUTED with a StablePtr value (NN_VALUE_PTR).+ * Accepts PENDING or BLACKHOLE state (skips blackhole for direct memoization).+ * Returns the old payload (pending StablePtr for caller to free).+ * Returns NULL if thunk is already COMPUTED. */+void *nn_thunk_set_computed(nn_thunk_t *thunk, void *value);++/* Set a non-COMPUTED thunk to COMPUTED with inline scalar values.+ * Returns the old payload (pending StablePtr for caller to free).+ * Returns NULL if thunk is already COMPUTED. */+void *nn_thunk_set_computed_int(nn_thunk_t *thunk, int64_t value);+void *nn_thunk_set_computed_float(nn_thunk_t *thunk, double value);+void *nn_thunk_set_computed_bool(nn_thunk_t *thunk, uint8_t value);+void *nn_thunk_set_computed_null(nn_thunk_t *thunk);++/* Set a non-COMPUTED thunk to COMPUTED with C-native complex values.+ * Returns the old payload (pending StablePtr for caller to free).+ * Returns NULL if thunk is already COMPUTED. */+void *nn_thunk_set_computed_str(nn_thunk_t *thunk, uint32_t symbol);+void *nn_thunk_set_computed_path(nn_thunk_t *thunk, uint32_t symbol);+void *nn_thunk_set_computed_list(nn_thunk_t *thunk, void *list);+void *nn_thunk_set_computed_attrs(nn_thunk_t *thunk, void *attrset);+void *nn_thunk_set_computed_ctxstr(nn_thunk_t *thunk, void *ctxstr);+void *nn_thunk_set_computed_lambda(nn_thunk_t *thunk, void *lambda);++/* --- Arena diagnostics / cleanup iteration --- */++/* Total thunks allocated across all blocks. */+uint32_t nn_thunk_count(void);++/* Get a thunk by global index (0-based, across all blocks).+ * Used for StablePtr cleanup iteration before nn_thunk_destroy().+ * Index must be < nn_thunk_count(). */+nn_thunk_t *nn_thunk_get(uint32_t index);++#endif /* NN_THUNK_H */
nova-nix.cabal view
@@ -1,17 +1,18 @@ cabal-version: 3.0 name: nova-nix-version: 0.1.9.0+version: 0.2.0.0 synopsis: Windows-native Nix implementation in Haskell and C99 description:- 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).+ A from-scratch implementation of the Nix package manager that runs+ natively on Windows, macOS, and Linux — no WSL or Cygwin. A+ Haskell evaluator handles parsing and lazy evaluation, backed by a C99+ data layer that keeps evaluation data off the GHC heap. It evaluates+ Nix expressions (including real nixpkgs @lib@), computes derivations and+ content-addressed store paths, and includes a derivation builder and+ binary-cache substituter. - Built on top of @nova-cache@ for NAR serialization, narinfo handling,- Ed25519 signing, and binary substitution.+ Built on @nova-cache@ for NAR serialization, narinfo handling, and+ Ed25519-signed binary substitution. license: BSD-3-Clause license-file: LICENSE@@ -26,6 +27,12 @@ extra-doc-files: CHANGELOG.md README.md+-- C data-layer headers. These are #included by the cbits/*.c sources+-- (via include-dirs: cbits) but are not auto-added to the sdist by the+-- c-sources field, so they must be listed here or the Hackage build+-- fails with "nn_*.h: No such file or directory".+extra-source-files:+ cbits/*.h data-files: nix/fetchurl.nix data-dir: data
src/Nix/Expr/Resolve.hs view
@@ -221,9 +221,18 @@ resolveLetBinding _ innerStack (Inherit (Just fromExpr) names) = [Inherit (Just (resolve innerStack fromExpr)) names] resolveLetBinding outerStack _ (Inherit Nothing names) =- -- Desugar: inherit x y; → x = x; y = y;- -- RHS resolves against outer scope (before the let bindings).- [NamedBinding [StaticKey name] (resolve outerStack (EVar name)) | name <- names]+ -- Desugar @inherit x y;@ → @x = x; y = y;@, resolving each RHS against the+ -- outer scope so it names the enclosing binding, not the one defined here.+ -- 'shiftInheritLevel' corrects for the inner env the resulting thunk runs in.+ [NamedBinding [StaticKey name] (shiftInheritLevel (resolve outerStack (EVar name))) | name <- names]++-- | A desugared positional @inherit@ RHS is resolved against the outer scope+-- but evaluated in the inner (let\/rec) env — one extra parent-chain hop — so+-- its de Bruijn level is one too shallow. Bump it. 'EVar'\/'EWithVar' are+-- name-based and need no adjustment.+shiftInheritLevel :: Expr -> Expr+shiftInheritLevel (EResolvedVar level idx) = EResolvedVar (level + 1) idx+shiftInheritLevel other = other -- | Resolve variables inside formal default expressions. resolveFormalsDefaults :: [ScopeEntry] -> Formals -> Formals