diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -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 |
 |---------|----------|
diff --git a/nova-nix.cabal b/nova-nix.cabal
--- a/nova-nix.cabal
+++ b/nova-nix.cabal
@@ -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
diff --git a/src/Nix/Eval.hs b/src/Nix/Eval.hs
--- a/src/Nix/Eval.hs
+++ b/src/Nix/Eval.hs
@@ -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)
 
diff --git a/src/Nix/Eval/Types.hs b/src/Nix/Eval/Types.hs
--- a/src/Nix/Eval/Types.hs
+++ b/src/Nix/Eval/Types.hs
@@ -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
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -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),
