nova-nix
Windows-Native Nix
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.
Try It · CLI · Architecture · Performance · Modules · Roadmap

What is nova-nix?
| 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. |
Every module is pure by default. IO at the boundaries only.
Try It
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
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)
$ 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 ]
$ NIX_PATH=nixpkgs=/path/to/nixpkgs nova-nix eval --expr '(import <nixpkgs/lib>).trivial.version'
"24.11pre-git"
$ 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 |
+------------------------------------------------+
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.
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 |
C99 Data Layer
| 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 |
Store + Builder
| 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 |
Roadmap
Done
Next
Long-Term
Build & Test
cabal build # Build library + CLI
cabal test # Run all 593 tests
cabal build --ghc-options="-Werror" # Warnings as errors (CI default)
Requires GHC 9.8+ and cabal-install 3.10+.
Library Usage
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.
BSD-3-Clause · Novavero AI