nova-nix 0.1.1.0 → 0.1.2.0
raw patch · 6 files changed
+88/−24 lines, 6 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Nix.Eval.Types: mkSyntheticThunk :: Env -> Expr -> Thunk
Files
- CHANGELOG.md +13/−0
- README.md +1/−1
- nova-nix.cabal +1/−1
- src/Nix/Eval.hs +48/−22
- src/Nix/Eval/Types.hs +19/−0
- test/Main.hs +6/−0
CHANGELOG.md view
@@ -1,5 +1,18 @@ # Changelog +## 0.1.2.0 — 2026-02-24++### Lazy Builtins + Correctness Fixes++- **Lazy `map`** — returns deferred thunks instead of eagerly forcing all elements. `map f [80000 items]` is now O(1) until elements are demanded.+- **Lazy `genList`** — each element is a deferred `f(i)` thunk, forced only on demand+- **Lazy `mapAttrs`** — each attr value is a deferred `f key val` thunk. Critical for nixpkgs which `mapAttrs` over the entire package set.+- **Semi-lazy `concatMap`** — forces applications to discover list structure for concatenation, but element thunks within sub-lists stay lazy+- **`@`-pattern scoping fix** — `{ system ? args.system or ..., ... }@args:` now correctly binds the `@`-name before evaluating defaults, matching real Nix. Previously, default expressions couldn't reference the `@`-binding.+- **Synthetic thunk memo cell fix** — `mkSyntheticThunk` uses env bindings (not expr) for IORef uniqueness, preventing GHC's full-laziness transform from sharing one memo cell across all deferred thunks. Without this, `map f [a b c]` would return `[f(a) f(a) f(a)]`.+- `deferApply` helper: builds thunks wrapping `EApp (EVar "__fn") (EVar "__arg")` in a self-contained env, reusing existing eval + memoization machinery+- 511 tests (3 new: map laziness, genList laziness, mapAttrs laziness)+ ## 0.1.1.0 — 2026-02-24 ### Phase 4: nixpkgs Compatibility
README.md view
@@ -264,7 +264,7 @@ ## The Hard Problems -Building Nix on Windows means solving problems nobody has fully solved before:+Building Nix on Windows means solving real platform differences: | Problem | Solution | |---------|----------|
nova-nix.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: nova-nix-version: 0.1.1.0+version: 0.1.2.0 synopsis: Windows-native Nix implementation in pure Haskell description: A pure Haskell implementation of the Nix package manager that runs natively
src/Nix/Eval.hs view
@@ -79,6 +79,7 @@ envLookup, evaluated, mkStr,+ mkSyntheticThunk, mkThunk, pushWithScope, runPureEval,@@ -438,8 +439,10 @@ matchFormalSet closureEnv formals allowExtra argVal matchFormals closureEnv (FormalNamedSet name formals allowExtra) argThunk = do argVal <- force argThunk- matched <- matchFormalSet closureEnv formals allowExtra argVal- pure (envInsertThunk name argThunk matched)+ -- Bind the @-pattern name (e.g. "args") BEFORE matching formals so that+ -- default expressions like @system ? args.system or ...@ can see it.+ let envWithAt = envInsertThunk name argThunk closureEnv+ matchFormalSet envWithAt formals allowExtra argVal -- | Match a formal set pattern against a VAttrs argument. matchFormalSet :: (MonadEval m) => Env -> [Formal] -> Bool -> NixValue -> m Env@@ -1080,9 +1083,9 @@ -- --------------------------------------------------------------------------- builtinMap :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinMap func (VList thunks) = do- mapped <- mapM (applyToThunk func) thunks- pure (VList (map evaluated mapped))+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) @@ -1106,9 +1109,12 @@ builtinGenList :: (MonadEval m) => NixValue -> NixValue -> m NixValue builtinGenList func (VInt n) | n < 0 = throwEvalError "builtins.genList: length must be non-negative"- | otherwise = do- vals <- mapM (applyValue func . VInt) [0 .. n - 1]- pure (VList (map evaluated vals))+ | otherwise =+ -- Lazy: each element is a deferred @f i@, forced only on demand.+ let fnThunk = evaluated func+ env = Env {envBindings = Map.fromList [("__fn", fnThunk)], envWithScopes = []}+ mkIndexThunk i = mkThunk env (EApp (EVar "__fn") (ELit (NixInt i)))+ in pure (VList (map mkIndexThunk [0 .. n - 1])) builtinGenList _ other = throwEvalError ("builtins.genList: expected an integer, got " <> typeName other) @@ -1140,7 +1146,10 @@ builtinConcatMap :: (MonadEval m) => NixValue -> NixValue -> m NixValue builtinConcatMap func (VList thunks) = do- results <- mapM (applyToThunk func) thunks+ -- 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 =@@ -1324,11 +1333,21 @@ -- Builtin helpers -- --------------------------------------------------------------------------- --- | Apply a function to a forced thunk.-applyToThunk :: (MonadEval m) => NixValue -> Thunk -> m NixValue-applyToThunk func thunk = do- val <- force thunk- applyValue func val+-- | 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 (EVar "__fn") (EVar "__arg")@ in a self-contained env.+deferApply :: NixValue -> Thunk -> Thunk+deferApply func argThunk =+ let env =+ Env+ { envBindings =+ Map.fromList+ [ ("__fn", evaluated func),+ ("__arg", argThunk)+ ],+ envWithScopes = []+ }+ in mkSyntheticThunk env (EApp (EVar "__fn") (EVar "__arg")) -- | The current system platform string. currentSystemStr :: Text@@ -1585,15 +1604,22 @@ -- --------------------------------------------------------------------------- builtinMapAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue-builtinMapAttrs func (VAttrs attrs) = do- mapped <- mapM (mapAttrEntry func) (Map.toList attrs)- pure (VAttrs (Map.fromList mapped))+builtinMapAttrs func (VAttrs attrs) =+ -- Lazy: each attr value is a deferred @f key val@, forced only on demand.+ pure (VAttrs (Map.mapWithKey deferAttr attrs)) where- mapAttrEntry fn (key, thunk) = do- val <- force thunk- partial <- applyValue fn (mkStr key)- result <- applyValue partial val- pure (key, evaluated result)+ deferAttr key valThunk =+ let env =+ Env+ { envBindings =+ Map.fromList+ [ ("__fn", evaluated func),+ ("__key", evaluated (mkStr key)),+ ("__val", valThunk)+ ],+ envWithScopes = []+ }+ in mkSyntheticThunk env (EApp (EApp (EVar "__fn") (EVar "__key")) (EVar "__val")) builtinMapAttrs _ other = throwEvalError ("builtins.mapAttrs: expected a set, got " <> typeName other)
src/Nix/Eval/Types.hs view
@@ -26,6 +26,7 @@ -- * Thunk operations mkThunk,+ mkSyntheticThunk, evaluated, -- * Display@@ -193,6 +194,18 @@ mkThunk env thunkExpr = Thunk thunkExpr env (newMemoCell thunkExpr) +-- | Like 'mkThunk' but for synthetic thunks that reuse the same 'Expr'+-- (e.g. @EApp (EVar "__fn") (EVar "__arg")@ in 'deferApply'). Uses the+-- 'Env' bindings map 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 bindings map to WHNF.+mkSyntheticThunk :: Env -> Expr -> Thunk+mkSyntheticThunk env thunkExpr =+ Thunk thunkExpr env (newSyntheticCell (envBindings env))+ -- | Allocate a fresh memoization cell for a thunk. -- -- @NOINLINE@ prevents inlining so GHC can't see inside or CSE calls.@@ -203,6 +216,12 @@ {-# NOINLINE newMemoCell #-} newMemoCell :: Expr -> IORef (Maybe NixValue) newMemoCell expr = unsafePerformIO (expr `seq` newIORef Nothing)++-- | Like 'newMemoCell' but keyed on a bindings map instead of an expression.+-- Used by 'mkSyntheticThunk' where multiple thunks share the same expression.+{-# NOINLINE newSyntheticCell #-}+newSyntheticCell :: Map Text Thunk -> IORef (Maybe NixValue)+newSyntheticCell bindings = unsafePerformIO (bindings `seq` newIORef Nothing) -- | Wrap an already-computed value as a thunk. evaluated :: NixValue -> Thunk
test/Main.hs view
@@ -502,6 +502,8 @@ assertEval "map-id" "builtins.map (x: x) [ 1 2 ] == [ 1 2 ]" (VBool True), runTest "map empty" $ assertEval "map-empty" "builtins.map (x: x) [ ]" (VList []),+ 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 runTest "filter match" $ assertEval "filter" "builtins.filter (x: x == 2) [ 1 2 3 ] == [ 2 ]" (VBool True),@@ -519,6 +521,8 @@ 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 []),+ 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 runTest "sort ints" $ assertEval "sort" "builtins.sort (a: b: a < b) [ 3 1 2 ] == [ 1 2 3 ]" (VBool True),@@ -1133,6 +1137,8 @@ assertEval "mapAttrs-name" "(builtins.mapAttrs (name: val: name) { a = 1; }).a" (mkStr "a"), runTest "mapAttrs type error" $ assertEvalFail "mapAttrs-err" "builtins.mapAttrs (n: v: v) [ 1 ]",+ runTest "mapAttrs lazy" $+ assertEval "mapAttrs-lazy" "let s = builtins.mapAttrs (k: v: if k == \"a\" then v else throw \"boom\") { a = 1; b = 2; }; in s.a" (VInt 1), -- functionArgs runTest "functionArgs set pattern" $ assertEval "funcArgs" "(builtins.functionArgs ({ a, b ? 1 }: a)).b" (VBool True),