packages feed

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 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),