diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
 # Changelog
 
+## 0.1.7.0 — 2026-03-07
+
+### Build Fix: drvPath Resolution + cmd.exe Quoting
+
+- **Fix: `nova-nix build` drvPath resolution** — `builtinDerivation` now writes `drvPath` and output paths (`out`, `dev`, etc.) into `drvEnv` on the `Derivation` struct. Previously these were only present in the eval result attr set, so `buildAndRegister` could not find the `.drv` store path for dependency resolution (error: "no drvPath available for dependency resolution").
+- **Fix: `extractDerivation` returns `StorePath`** — The CLI `build` command now extracts the `drvPath` `StorePath` directly from the eval result alongside the `Derivation`, passing it explicitly to `buildWithDeps`. Eliminates the fragile `Map.lookup "drvPath" drvEnv` fallback.
+- **Fix: cmd.exe builder quoting on Windows** — New `mkBuilderProcess` detects when the builder is `cmd.exe` with `/c` and uses `Proc.shell` instead of `Proc.proc`. GHC's `proc` wraps each argument in double-quotes for `CommandLineToArgvW`, but cmd.exe doesn't use that convention — `args = [ "/c" "echo Hello" ]` would fail because cmd.exe tried to find an executable literally named `"echo Hello"`.
+- **Fix: UTF-16 auto-detection** — New `readFileAutoEncoding` detects UTF-16 LE/BE and UTF-8 BOM at the byte level before decoding. PowerShell's `>` operator writes UTF-16 LE by default — a Windows-first Nix implementation must handle this at the input boundary. Wired into all 5 file-read sites (Parser, Eval/IO, Main).
+- **nova-cache `>= 0.3.0`** — Bumped lower bound to track nova-cache 0.3.x series.
+- **`crypton < 1.1` pin** — `http-client-tls` still uses `memory`'s `ByteArrayAccess`; `crypton >= 1.1` switched to `ram`, causing instance mismatches. Pinned in `cabal.project` until `http-client-tls` migrates.
+- 108 builtins, 524 tests, -Werror clean, ormolu clean, hlint clean
+
 ## 0.1.6.0 — 2026-02-26
 
 ### De Bruijn-Style Positional Env + SmallArray Slots
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -7,8 +7,8 @@
 
 [![CI](https://github.com/Novavero-AI/nova-nix/actions/workflows/ci.yml/badge.svg)](https://github.com/Novavero-AI/nova-nix/actions/workflows/ci.yml)
 [![Hackage](https://img.shields.io/hackage/v/nova-nix.svg)](https://hackage.haskell.org/package/nova-nix)
-![Haskell](https://img.shields.io/badge/haskell-GHC%209.6-purple)
-![License](https://img.shields.io/badge/license-MIT-blue)
+![Haskell](https://img.shields.io/badge/haskell-GHC%209.8-purple)
+![License](https://img.shields.io/badge/license-BSD--3--Clause-blue)
 
 </p>
 </div>
@@ -179,6 +179,7 @@
 | `Nix.Expr` | Re-exports from `Nix.Expr.Types` |
 | `Nix.Expr.Types` | Complete Nix AST — 18 expression constructors (including `ESearchPath`, `EResolvedVar`), atoms, formals, operators, string parts |
 | `Nix.Expr.Resolve` | De Bruijn-style variable resolution pass — replaces `EVar` with `EResolvedVar` for lambda-bound variables at parse time |
+| `Nix.Expr.ClosureTrim` | Closure trimming — statically determines free variables per lambda/with to minimize captured environment size |
 | `Nix.Parser` | Hand-rolled recursive descent parser + lexer. Direct `Text` consumption, source position tracking |
 | `Nix.Parser.Lexer` | Tokenizer — integers, floats, strings with interpolation, paths, URIs, search paths, all operators/keywords |
 | `Nix.Parser.Expr` | Expression parser — 13 precedence levels, left/right/non-associative operators, application, selection, dynamic keys |
@@ -258,8 +259,8 @@
 
 **Key numbers:**
 
-- **23 modules** — all implemented
-- **511 tests** — hand-rolled harness, no framework dependencies
+- **24 modules** — all implemented
+- **524 tests** — hand-rolled harness, no framework dependencies
 - **Zero partial functions** — total by construction, `T.uncons` over `T.head`/`T.tail`
 - **Strict by default** — bang patterns on all data fields (except Thunk's Env, which is lazy for knot-tying)
 
@@ -305,15 +306,15 @@
 
 ```bash
 cabal build                              # Build library + CLI
-cabal test                               # Run all 511 tests
+cabal test                               # Run all 524 tests
 cabal build --ghc-options="-Werror"      # Warnings as errors (CI default)
 cabal haddock                            # Generate API docs
 ```
 
-Requires GHC 9.6 and cabal-install 3.10+.
+Requires GHC 9.8 and cabal-install 3.10+.
 
 ---
 
 <p align="center">
-  <sub>MIT License · <a href="https://github.com/Novavero-AI">Novavero AI</a></sub>
+  <sub>BSD-3-Clause · <a href="https://github.com/Novavero-AI">Novavero AI</a></sub>
 </p>
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -25,7 +25,7 @@
 import Nix.Derivation (Derivation (..), DerivationOutput (..))
 import Nix.Eval (MonadEval, NixValue (..), Thunk (..), attrSetFromMap, attrSetLookup, attrSetToAscList, attrSetToMap, eval, force)
 import Nix.Eval.IO (EvalState (..), newEvalState, runEvalIO)
-import Nix.Parser (parseNix)
+import Nix.Parser (parseNix, readFileAutoEncoding)
 import Nix.Store (Store, closeStore, openStore, writeDrv)
 import Nix.Store.Path (StorePath, defaultStoreDir, parseStorePath, storePathToFilePath)
 import Paths_nova_nix (getDataDir)
@@ -110,7 +110,7 @@
 -- | Evaluate a .nix file and print the result.
 evalFile :: Bool -> [T.Text] -> FilePath -> FilePath -> IO ()
 evalFile strict extraPaths dataDir filePath = do
-  source <- TIO.readFile filePath
+  source <- readFileAutoEncoding filePath
   case parseNix (T.pack filePath) source of
     Left err -> do
       hPutStrLn stderr ("parse error: " ++ show err)
@@ -150,7 +150,7 @@
 -- | Parse, evaluate, extract derivation, build, and print result.
 buildFile :: [T.Text] -> FilePath -> FilePath -> IO ()
 buildFile extraPaths dataDir filePath = do
-  source <- TIO.readFile filePath
+  source <- readFileAutoEncoding filePath
   case parseNix (T.pack filePath) source of
     Left err -> do
       hPutStrLn stderr ("parse error: " ++ show err)
@@ -164,9 +164,9 @@
           TIO.hPutStrLn stderr ("eval error: " <> err)
           exitFailure
         Right val -> do
-          drv <- extractDerivation val
+          (drv, drvSP) <- extractDerivation val
           store <- openStore defaultStoreDir
-          buildResult <- buildAndRegister store drv
+          buildResult <- buildAndRegister store drv drvSP
           closeStore store
           case buildResult of
             BuildSuccess sp ->
@@ -175,10 +175,11 @@
               TIO.hPutStrLn stderr ("build failed (exit " <> T.pack (show code) <> "): " <> msg)
               exitFailure
 
--- | Extract a Derivation from an evaluated value.
--- The value should be a VAttrs with type = "derivation" and a _derivation key.
--- Falls back to reading the .drv file via fromATerm if _derivation is not present.
-extractDerivation :: NixValue -> IO Derivation
+-- | Extract a Derivation and its store path from an evaluated value.
+-- The value must be a VAttrs with type = "derivation", a _derivation key
+-- holding the Derivation struct, and a drvPath key holding the .drv store path.
+-- Both are computed by builtinDerivation during evaluation.
+extractDerivation :: NixValue -> IO (Derivation, StorePath)
 extractDerivation (VAttrs attrs) = do
   -- Check type = "derivation"
   case attrSetLookup "type" attrs of
@@ -186,51 +187,42 @@
     _ -> do
       hPutStrLn stderr "error: result is not a derivation (no type = \"derivation\")"
       exitFailure
-  -- Try to extract from _derivation key first
-  case attrSetLookup "_derivation" attrs of
-    Just (Evaluated (VDerivation drv)) -> pure drv
+  -- Extract the Derivation struct from _derivation
+  drv <- case attrSetLookup "_derivation" attrs of
+    Just (Evaluated (VDerivation d)) -> pure d
     _ -> do
       hPutStrLn stderr "error: derivation result missing _derivation field"
       exitFailure
-extractDerivation (VDerivation drv) = pure drv
+  -- Extract drvPath — this is the store path of the .drv file itself,
+  -- computed by hashing the ATerm serialization during evaluation.
+  drvSP <- case attrSetLookup "drvPath" attrs of
+    Just (Evaluated (VStr path _)) -> case parseStorePath defaultStoreDir path of
+      Just sp -> pure sp
+      Nothing -> do
+        TIO.hPutStrLn stderr ("error: invalid drvPath: " <> path)
+        exitFailure
+    _ -> do
+      hPutStrLn stderr "error: derivation result missing drvPath"
+      exitFailure
+  pure (drv, drvSP)
 extractDerivation _ = do
   hPutStrLn stderr "error: result is not a derivation"
   exitFailure
 
 -- | Write the .drv file to the store and build with dependency resolution.
-buildAndRegister :: Store -> Derivation -> IO BuildResult
-buildAndRegister store drv = do
-  -- Write the .drv file to store
-  let drvSP = extractDrvStorePath drv
-  writeDrvToStore store drv
+-- The drvPath is the store path of the .drv file itself, extracted from
+-- the evaluation result alongside the Derivation struct.
+buildAndRegister :: Store -> Derivation -> StorePath -> IO BuildResult
+buildAndRegister store drv drvSP = do
+  -- Write the .drv file to store at its content-addressed path
+  writeDrv store drv drvSP
   -- Build with dependency resolution
   tmpDir <- getTemporaryDirectory
   let config =
         (defaultBuildConfig defaultStoreDir)
           { bcTmpDir = tmpDir
           }
-  case drvSP of
-    Just sp -> buildWithDeps config store drv sp
-    Nothing -> do
-      -- No drvPath available — fall back to direct build without dep resolution
-      hPutStrLn stderr "warning: no drvPath, building without dependency resolution"
-      -- Import buildDerivation for fallback
-      pure (BuildFailure "no drvPath available for dependency resolution" 1)
-
--- | Extract the .drv store path from a derivation's env.
-extractDrvStorePath :: Derivation -> Maybe StorePath
-extractDrvStorePath drv =
-  Map.lookup "drvPath" (drvEnv drv) >>= parseStorePath defaultStoreDir
-
--- | Write a .drv file to the store at its derived path.
-writeDrvToStore :: Store -> Derivation -> IO ()
-writeDrvToStore store drv =
-  case drvOutputs drv of
-    [] -> pure () -- no outputs, nothing to write
-    _ -> do
-      -- Write .drv to store if a drvPath is available in the env.
-      let envDrvPath = Map.lookup "drvPath" (drvEnv drv)
-      mapM_ (writeDrv store drv) (envDrvPath >>= parseStorePath defaultStoreDir)
+  buildWithDeps config store drv drvSP
 
 -- ---------------------------------------------------------------------------
 -- Output formatting
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.6.0
+version:            0.1.7.0
 synopsis:           Windows-native Nix implementation in pure Haskell
 description:
   A pure Haskell implementation of the Nix package manager that runs natively
@@ -20,7 +20,7 @@
 category:           Nix, Distribution, System
 stability:          experimental
 build-type:         Simple
-tested-with:        GHC == 9.6.7
+tested-with:        GHC == 9.8.4
 extra-doc-files:
     CHANGELOG.md
     README.md
@@ -34,6 +34,7 @@
 library
   exposed-modules:
     Nix.Expr
+    Nix.Expr.ClosureTrim
     Nix.Expr.Resolve
     Nix.Expr.Types
     Nix.Parser
@@ -70,7 +71,7 @@
     , http-types          >= 0.12 && < 0.13
     , memory              >= 0.18 && < 1
     , mtl                 >= 2.2 && < 2.4
-    , nova-cache          >= 0.2.4 && < 0.3
+    , nova-cache          >= 0.3.0 && < 0.4
     , primitive           >= 0.7 && < 1
     , process             >= 1.6 && < 1.7
     , regex-tdfa          >= 1.3 && < 1.4
diff --git a/src/Nix/Builder.hs b/src/Nix/Builder.hs
--- a/src/Nix/Builder.hs
+++ b/src/Nix/Builder.hs
@@ -44,6 +44,7 @@
 
 import Control.Exception (SomeException, try)
 import Control.Monad (when)
+import Data.Char (toLower)
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
 import Data.Text (Text)
@@ -58,7 +59,7 @@
 import System.Directory (createDirectoryIfMissing, doesDirectoryExist, removeDirectoryRecursive)
 import qualified System.Environment
 import System.Exit (ExitCode (..))
-import System.FilePath (takeDirectory, (</>))
+import System.FilePath (takeDirectory, takeFileName, (</>))
 import qualified System.IO
 import qualified System.IO.Unsafe
 import qualified System.Info
@@ -307,7 +308,7 @@
       mergedEnv = Map.union buildEnv systemMap
       envList = [(T.unpack k, T.unpack v) | (k, v) <- Map.toList mergedEnv]
       cp =
-        (Proc.proc builderPath builderArgs)
+        (mkBuilderProcess builderPath builderArgs)
           { Proc.cwd = Just workDir,
             Proc.env = Just envList,
             Proc.std_out = Proc.CreatePipe,
@@ -317,6 +318,46 @@
   case exitCode of
     ExitSuccess -> pure (Right ())
     ExitFailure code -> pure (Left (code, T.pack stderrText))
+
+-- | Create the appropriate process spec for the builder.
+--
+-- On Windows, cmd.exe uses its own command line parser — @\/c@ takes a
+-- raw command string, not individually-quoted arguments.  GHC's 'Proc.proc'
+-- wraps each arg in double quotes for the @CommandLineToArgvW@ convention,
+-- but cmd.exe doesn't use that convention, so a derivation like:
+--
+-- @
+-- derivation { builder = "cmd.exe"; args = [ "\/c" "echo Hello" ]; ... }
+-- @
+--
+-- would fail because GHC quotes @echo Hello@ → @\"echo Hello\"@, and
+-- cmd.exe tries to find an executable literally named @\"echo Hello\"@.
+--
+-- Fix: when the builder is cmd.exe with @\/c@, use 'Proc.shell' which
+-- passes the command string directly to @cmd.exe \/c@ without quoting.
+-- For all other builders, use 'Proc.proc' (standard @CommandLineToArgvW@).
+mkBuilderProcess :: FilePath -> [String] -> Proc.CreateProcess
+mkBuilderProcess builder args
+  | isWindows,
+    isCmdExe builder,
+    Just cmdString <- extractCmdString args =
+      Proc.shell cmdString
+  | otherwise = Proc.proc builder args
+
+-- | Check if the builder is cmd.exe (case-insensitive, handles full paths).
+isCmdExe :: FilePath -> Bool
+isCmdExe path = map toLower (takeFileName path) == "cmd.exe"
+
+-- | Extract the raw command string from cmd.exe args.
+-- Looks for @\/c@ (case-insensitive) and joins everything after it.
+extractCmdString :: [String] -> Maybe String
+extractCmdString (flag : cmdArgs)
+  | map toLower flag == "/c" = Just (unwords cmdArgs)
+extractCmdString _ = Nothing
+
+-- | Whether we are running on Windows (compile-time constant via 'System.Info').
+isWindows :: Bool
+isWindows = System.Info.os == "mingw32"
 
 -- ---------------------------------------------------------------------------
 -- Output registration
diff --git a/src/Nix/Builtins.hs b/src/Nix/Builtins.hs
--- a/src/Nix/Builtins.hs
+++ b/src/Nix/Builtins.hs
@@ -20,7 +20,7 @@
 import qualified Data.Text as T
 import Nix.Eval (Env (..), NixValue (..), Thunk (..), attrSetFromMap, builtinNames, currentSystemStr, evaluated)
 import Nix.Eval.Types (mkStr)
-import Nix.Store.Path (defaultStoreDirText)
+import Nix.Store.Path (platformStoreDirText)
 
 -- | The initial environment containing all builtins.
 --
@@ -101,7 +101,7 @@
     [ ("true", evaluated (VBool True)),
       ("false", evaluated (VBool False)),
       ("null", evaluated VNull),
-      ("storeDir", evaluated (mkStr defaultStoreDirText)),
+      ("storeDir", evaluated (mkStr platformStoreDirText)),
       ("nixVersion", evaluated (mkStr "2.24.0")),
       ("langVersion", evaluated (VInt 6)),
       ("nixPath", evaluated (VList searchPaths)),
diff --git a/src/Nix/Eval.hs b/src/Nix/Eval.hs
--- a/src/Nix/Eval.hs
+++ b/src/Nix/Eval.hs
@@ -75,7 +75,7 @@
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
 import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)
-import Data.Primitive.SmallArray (smallArrayFromListN)
+import Data.Primitive.SmallArray (SmallArray, indexSmallArray, smallArrayFromListN)
 import Data.Sequence (Seq (..))
 import qualified Data.Sequence as Seq
 import qualified Data.Set as Set
@@ -110,24 +110,28 @@
     attrSetToAscList,
     attrSetToMap,
     attrSetUnionWith,
+    cheapThunk,
     emptyContext,
     emptyEnv,
     envFromSlots,
     envLookup,
     envLookupResolved,
     evaluated,
+    lookupWithScopes,
     mkStr,
     mkSyntheticThunk,
     mkThunk,
     newLazyAttrCache,
     pushWithScope,
     typeName,
+    withScopesForCapture,
   )
 import Nix.Expr.Types
   ( AttrKey (..),
     AttrPath,
     BinaryOp (..),
     Binding (..),
+    CaptureInfo (..),
     Expr (..),
     Formal (..),
     Formals (..),
@@ -150,14 +154,16 @@
   EStr parts -> uncurry VStr <$> evalStringParts eval force applyValue env parts
   EIndStr parts -> uncurry VStr <$> evalIndStringParts eval force applyValue env parts
   EVar name -> evalVar env name
+  EWithVar name -> evalWithVar env name
   EResolvedVar level idx -> force (envLookupResolved level idx env)
-  EAttrs isRec bindings -> evalAttrs env isRec bindings
-  EList exprs -> pure (VList (map (mkThunk env) exprs))
+  EAttrs isRec bindings captureInfo -> evalAttrs env isRec bindings captureInfo
+  EList exprs -> pure (VList (map (cheapThunk env) exprs))
   ESelect target path defExpr -> evalSelect env target path defExpr
   EHasAttr target path -> evalHasAttr env target path
   EApp func arg -> evalApp env func arg
-  ELambda formals body -> pure (VLambda env formals body)
-  ELet bindings body -> evalLet env bindings body
+  ELambda formals body captureInfo ->
+    pure (VLambda (buildCaptureEnv env captureInfo) formals body)
+  ELet bindings body captureInfo -> evalLet env bindings body captureInfo
   EIf cond thenExpr elseExpr -> evalIf env cond thenExpr elseExpr
   EWith scope body -> evalWith env scope body
   EAssert cond body -> evalAssert env cond body
@@ -225,13 +231,24 @@
     Just thunk -> force thunk
     Nothing -> throwEvalError ("undefined variable '" <> name <> "'")
 
+-- | Evaluate a with-scoped variable: check with-scopes first (innermost
+-- to outermost), then fall back to the standard name-based lookup
+-- (parent chain to builtins).  For trimmed envs ('CapturesWithScopes'),
+-- the root scope is already appended to 'envWithScopes' so the
+-- with-scope lookup finds builtins without needing a parent chain.
+evalWithVar :: (MonadEval m) => Env -> Text -> m NixValue
+evalWithVar env name =
+  case lookupWithScopes name (envWithScopes env) of
+    Just thunk -> force thunk
+    Nothing -> evalVar env name
+
 -- ---------------------------------------------------------------------------
 -- Attribute sets
 -- ---------------------------------------------------------------------------
 
-evalAttrs :: (MonadEval m) => Env -> Bool -> [Binding] -> m NixValue
-evalAttrs env False bindings = evalNonRecAttrs env bindings
-evalAttrs env True bindings = evalRecAttrs env bindings
+evalAttrs :: (MonadEval m) => Env -> Bool -> [Binding] -> CaptureInfo -> m NixValue
+evalAttrs env False bindings _captureInfo = evalNonRecAttrs env bindings
+evalAttrs env True bindings captureInfo = evalRecAttrs env bindings captureInfo
 
 -- | Non-recursive attribute set: thunks capture the outer environment.
 --
@@ -253,35 +270,46 @@
 -- Dynamic keys in recursive attrs evaluate against the outer env
 -- (the rec env is not yet available during key resolution).
 --
--- Uses 'LazyAttrs' to defer thunk + IORef allocation: only keys that
--- are actually accessed get materialized.  For nixpkgs' 30k-entry
--- package set, this avoids ~30k IORef allocations when only ~50
--- packages are touched.
+-- Positional path: when all bindings are single static keys, builds
+-- 'envSlots' for variable lookup and 'EagerAttrs' for the return value.
+-- Both reference the SAME thunk objects (no duplication).
 --
+-- Fallback path: uses 'LazyAttrs' to defer thunk + IORef allocation.
 -- The env's 'envLazyScope' points to the SAME 'LazyAttrs' that backs
--- the resulting attr set — one map instead of two.  Variable lookup
--- and attr set access share a single materialization cache, eliminating
--- the dual-map space leak where both a lazy Thunk map and a LazyBinding
--- map retained the entire recursive environment.
-evalRecAttrs :: (MonadEval m) => Env -> [Binding] -> m NixValue
-evalRecAttrs env bindings = do
-  -- Resolve dynamic keys eagerly against the outer env.
-  resolvedBindings <- resolveBindingKeys env bindings
-  -- Knot-tied: recEnv references attrSet (via envLazyScope) which
-  -- references lazyBindingMap which captures recEnv lazily inside
-  -- each LazyBinding.  The map structure is built from resolvedBindings
-  -- (already resolved), not from recEnv.
-  let recEnv =
-        Env
-          { envSlots = mempty,
-            envLazyScope = Just attrSet,
-            envParent = Just env,
-            envWithScopes = envWithScopes env
-          }
-      lazyBindingMap = buildLazyBindingMap recEnv resolvedBindings
-      cache = newLazyAttrCache lazyBindingMap
-      attrSet = LazyAttrs lazyBindingMap cache
-  pure (VAttrs attrSet)
+-- the resulting attr set — one map instead of two.
+evalRecAttrs :: (MonadEval m) => Env -> [Binding] -> CaptureInfo -> m NixValue
+evalRecAttrs env bindings captureInfo
+  | allPositionalBindings bindings =
+      -- Positional path: slots for variable lookup, EagerAttrs for return.
+      let slotCount = bindingSlotCount bindings
+          parentEnv = buildCaptureEnv env captureInfo
+          recEnv =
+            Env
+              { envSlots = slots,
+                envLazyScope = Nothing,
+                envParent = Just parentEnv,
+                envWithScopes = case captureInfo of
+                  NoCaptureInfo -> envWithScopes env
+                  Captures _ -> []
+                  CapturesWithScopes _ -> withScopesForCapture env
+              }
+          slots = smallArrayFromListN slotCount (buildSlotThunks recEnv env bindings)
+          attrMap = buildAttrMapFromSlots bindings slots
+       in pure (VAttrs (EagerAttrs attrMap))
+  | otherwise = do
+      -- Fallback: dynamic/nested keys — use LazyAttrs.
+      resolvedBindings <- resolveBindingKeys env bindings
+      let recEnv =
+            Env
+              { envSlots = mempty,
+                envLazyScope = Just attrSet,
+                envParent = Just env,
+                envWithScopes = envWithScopes env
+              }
+          lazyBindingMap = buildLazyBindingMap recEnv resolvedBindings
+          cache = newLazyAttrCache lazyBindingMap
+          attrSet = LazyAttrs lazyBindingMap cache
+      pure (VAttrs attrSet)
 
 -- ---------------------------------------------------------------------------
 -- Resolved bindings (for knot-tying in rec {} and let)
@@ -440,7 +468,7 @@
   funcVal <- eval env funcExpr
   case funcVal of
     VLambda closureEnv formals body -> do
-      let argThunk = mkThunk env argExpr
+      let argThunk = cheapThunk env argExpr
       extEnv <- matchFormals closureEnv formals argThunk
       eval extEnv body
     VBuiltin "tryEval" [] -> do
@@ -556,30 +584,136 @@
     [] -> pure ()
     (name : _) -> throwEvalError ("missing required attribute '" <> name <> "'")
 
+-- | Build a flat env from capture coordinates, extracting only the
+-- referenced slots from the parent chain.  Returns the original env
+-- unchanged when untrimmed.  Used by lambdas (as closure env), and
+-- by let\/rec blocks (as trimmed parent env) to break the retention
+-- chain to the full outer scope.
+buildCaptureEnv :: Env -> CaptureInfo -> Env
+buildCaptureEnv env NoCaptureInfo = env
+buildCaptureEnv env (Captures captureList) =
+  Env
+    { envSlots =
+        smallArrayFromListN
+          (length captureList)
+          [envLookupResolved lvl idx env | (lvl, idx) <- captureList],
+      envLazyScope = Nothing,
+      envParent = Nothing,
+      envWithScopes = []
+    }
+buildCaptureEnv env (CapturesWithScopes captureList) =
+  Env
+    { envSlots =
+        smallArrayFromListN
+          (length captureList)
+          [envLookupResolved lvl idx env | (lvl, idx) <- captureList],
+      envLazyScope = Nothing,
+      envParent = Nothing,
+      envWithScopes = withScopesForCapture env
+    }
+
 -- ---------------------------------------------------------------------------
 -- Let / if / with / assert
 -- ---------------------------------------------------------------------------
 
+-- | Check if all bindings have single static keys (eligible for
+-- positional slot-based evaluation).  Mirrors 'allStaticSingleKey'
+-- in 'Nix.Expr.Resolve'.
+allPositionalBindings :: [Binding] -> Bool
+allPositionalBindings = all isEligible
+  where
+    isEligible (NamedBinding [StaticKey _] _) = True
+    isEligible (Inherit _ _) = True
+    isEligible _ = False
+
+-- | Count the number of positional slots needed for a list of bindings.
+-- Each named binding contributes 1, each inherit contributes its name count.
+bindingSlotCount :: [Binding] -> Int
+bindingSlotCount = foldl' countOne 0
+  where
+    countOne !acc (NamedBinding [StaticKey _] _) = acc + 1
+    countOne !acc (Inherit _ names) = acc + length names
+    countOne !acc _ = acc
+
+-- | Build thunks for positional let\/rec bindings in declaration order.
+-- Returns a list of thunks matching the slot indices assigned by
+-- 'lexicalScopeFromBindings' in the resolve pass.
+--
+-- Inherits are expanded in-place: @inherit x y@ produces thunks at
+-- consecutive slots.  The @thunkEnv@ is the knot-tied let\/rec env;
+-- @outerEnv@ is the parent env used for inherit lookups.
+buildSlotThunks :: Env -> Env -> [Binding] -> [Thunk]
+buildSlotThunks thunkEnv outerEnv = concatMap slotThunk
+  where
+    slotThunk (NamedBinding [StaticKey _] bodyExpr) =
+      [cheapThunk thunkEnv bodyExpr]
+    slotThunk (Inherit Nothing names) =
+      -- inherit x → look up x in the OUTER env (before the let scope).
+      map (inheritLookup outerEnv) names
+    slotThunk (Inherit (Just fromExpr) names) =
+      -- inherit (from) x → ESelect from.x, evaluated in the let env.
+      [mkThunk thunkEnv (ESelect fromExpr [StaticKey name] Nothing) | name <- names]
+    -- Unreachable: allPositionalBindings guards this path.
+    slotThunk _ = []
+
+-- | Build a 'Map Text Thunk' indexing into a 'SmallArray Thunk' by
+-- binding name.  The thunk pointers are shared (no copies).
+-- Used by 'evalRecAttrs' to produce the 'EagerAttrs' return value
+-- while slots serve for variable lookup.
+buildAttrMapFromSlots :: [Binding] -> SmallArray Thunk -> Map Text Thunk
+buildAttrMapFromSlots bindings slots = snd (foldl' addOne (0, Map.empty) bindings)
+  where
+    addOne (!idx, !acc) (NamedBinding [StaticKey name] _) =
+      (idx + 1, Map.insert name (indexSmallArray slots idx) acc)
+    addOne (!idx, !acc) (Inherit _ names) =
+      let acc' = foldl' (\a (offset, name) -> Map.insert name (indexSmallArray slots (idx + offset)) a) acc (zip [0 ..] names)
+       in (idx + length names, acc')
+    -- Unreachable: allPositionalBindings guards this path.
+    addOne (!idx, !acc) _ = (idx, acc)
+
 -- | Let is recursive in Nix: all bindings are visible to each other.
 -- Knot-tying via Haskell laziness (Thunk's Env field is lazy).
 -- Dynamic keys in let evaluate against the outer env (the let env
 -- is not yet available during key resolution).
 --
--- Uses 'envLazyScope' to share the same 'LazyAttrs' as evalRecAttrs,
--- eliminating the dual-map space leak.
-evalLet :: (MonadEval m) => Env -> [Binding] -> Expr -> m NixValue
-evalLet env bindings body = do
-  resolvedBindings <- resolveBindingKeys env bindings
-  let letEnv =
-        Env
-          { envSlots = mempty,
-            envLazyScope = Just (LazyAttrs lazyBindingMap cache),
-            envParent = Just env,
-            envWithScopes = envWithScopes env
-          }
-      lazyBindingMap = buildLazyBindingMap letEnv resolvedBindings
-      cache = newLazyAttrCache lazyBindingMap
-  eval letEnv body
+-- Positional path: when all bindings are single static keys, builds
+-- 'envSlots' for O(1) variable lookup and sets 'envLazyScope = Nothing'.
+-- This enables closure trimming to fire on lambdas that reference
+-- let-bound variables.
+--
+-- Fallback path: dynamic\/nested-key blocks use the existing 'LazyAttrs'
+-- path with 'envLazyScope'.
+evalLet :: (MonadEval m) => Env -> [Binding] -> Expr -> CaptureInfo -> m NixValue
+evalLet env bindings body captureInfo
+  | allPositionalBindings bindings =
+      -- Positional path: slots for O(1) lookup, no envLazyScope.
+      let slotCount = bindingSlotCount bindings
+          parentEnv = buildCaptureEnv env captureInfo
+          -- Knot-tied: letEnv references slots which reference letEnv lazily.
+          letEnv =
+            Env
+              { envSlots = smallArrayFromListN slotCount (buildSlotThunks letEnv env bindings),
+                envLazyScope = Nothing,
+                envParent = Just parentEnv,
+                envWithScopes = case captureInfo of
+                  NoCaptureInfo -> envWithScopes env
+                  Captures _ -> []
+                  CapturesWithScopes _ -> withScopesForCapture env
+              }
+       in eval letEnv body
+  | otherwise = do
+      -- Fallback: dynamic/nested keys — use LazyAttrs.
+      resolvedBindings <- resolveBindingKeys env bindings
+      let letEnv =
+            Env
+              { envSlots = mempty,
+                envLazyScope = Just (LazyAttrs lazyBindingMap cache),
+                envParent = Just env,
+                envWithScopes = envWithScopes env
+              }
+          lazyBindingMap = buildLazyBindingMap letEnv resolvedBindings
+          cache = newLazyAttrCache lazyBindingMap
+      eval letEnv body
 
 evalIf :: (MonadEval m) => Env -> Expr -> Expr -> Expr -> m NixValue
 evalIf env cond thenExpr elseExpr = do
@@ -2832,8 +2966,16 @@
         | (outName, outP) <- outPaths
         ]
 
-  -- Build the complete Derivation with populated outputs and inputs
-  let completeDrv = drv {drvOutputs = drvOutputsList}
+  -- Build the complete Derivation with populated outputs and env.
+  -- The hash was computed from the pre-output drv (drvOutputs = []),
+  -- so adding output paths and drvPath to drvEnv here does not affect
+  -- the content address.  Real Nix .drv files include these in their
+  -- env section — builders read $out etc. from the environment.
+  let completeEnv =
+        Map.union
+          (Map.fromList (("drvPath", drvPathText) : outPaths))
+          envMap
+      completeDrv = drv {drvOutputs = drvOutputsList, drvEnv = completeEnv}
 
   -- Context for output paths: each output carries SCDrvOutput context
   -- Context for drvPath: carries SCAllOutputs context
diff --git a/src/Nix/Eval/IO.hs b/src/Nix/Eval/IO.hs
--- a/src/Nix/Eval/IO.hs
+++ b/src/Nix/Eval/IO.hs
@@ -44,7 +44,7 @@
 import Nix.Eval.Types (MonadEval (..), NixValue (..), Thunk (..), ThunkCell (..), attrSetSize)
 import Nix.Expr.Types (AttrKey (..), Binding (..), Expr (..), Formal (..), Formals (..), NixAtom (..), StringPart (..))
 import Nix.Hash (sha256Hex, truncatedBase32)
-import Nix.Parser (parseNix)
+import Nix.Parser (parseNix, readFileAutoEncoding)
 import qualified System.Directory as Dir
 import System.Environment (lookupEnv)
 import System.Exit (ExitCode (..))
@@ -124,7 +124,7 @@
     result <- liftIO (try (runReaderT action st))
     pure (case result of Left (NixEvalError msg) -> Left msg; Right val -> Right val)
 
-  readFileText path = wrapIO (TIO.readFile (T.unpack path))
+  readFileText path = wrapIO (readFileAutoEncoding (T.unpack path))
 
   doesPathExist path = wrapIO (Dir.doesPathExist (T.unpack path))
 
@@ -150,7 +150,7 @@
     case Map.lookup target cache of
       Just cached -> pure cached
       Nothing -> do
-        source <- wrapIO (TIO.readFile target)
+        source <- wrapIO (readFileAutoEncoding target)
         case parseNix (T.pack target) source of
           Left err ->
             throwEvalError
@@ -212,7 +212,7 @@
     let raw = T.unpack rawPath
         resolved = if isRelative raw then baseDir </> raw else raw
     canonical <- wrapIO (Dir.canonicalizePath resolved)
-    source <- wrapIO (TIO.readFile canonical)
+    source <- wrapIO (readFileAutoEncoding canonical)
     case parseNix (T.pack canonical) source of
       Left err ->
         throwEvalError
@@ -367,15 +367,16 @@
       EStr parts -> EStr (map goPart parts)
       EIndStr parts -> EIndStr (map goPart parts)
       EVar _ -> expr
+      EWithVar _ -> expr
       EResolvedVar _ _ -> expr
-      EAttrs isRec bindings -> EAttrs isRec (map goBinding bindings)
+      EAttrs isRec bindings captureInfo -> EAttrs isRec (map goBinding bindings) captureInfo
       EList elems -> EList (map goExpr elems)
       ESelect target path mDef ->
         ESelect (goExpr target) (map goKey path) (fmap goExpr mDef)
       EHasAttr target path -> EHasAttr (goExpr target) (map goKey path)
       EApp f x -> EApp (goExpr f) (goExpr x)
-      ELambda formals body -> ELambda (goFormals formals) (goExpr body)
-      ELet bindings body -> ELet (map goBinding bindings) (goExpr body)
+      ELambda formals body captures -> ELambda (goFormals formals) (goExpr body) captures
+      ELet bindings body captureInfo -> ELet (map goBinding bindings) (goExpr body) captureInfo
       EIf c t f -> EIf (goExpr c) (goExpr t) (goExpr f)
       EWith scope body -> EWith (goExpr scope) (goExpr body)
       EAssert cond body -> EAssert (goExpr cond) (goExpr body)
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
@@ -43,10 +43,13 @@
     envLookupResolved,
     envFromSlots,
     pushWithScope,
+    lookupWithScopes,
+    withScopesForCapture,
 
     -- * Thunk operations
     mkThunk,
     mkSyntheticThunk,
+    cheapThunk,
     evaluated,
     thunkSameRef,
 
@@ -64,11 +67,11 @@
 import Data.List (foldl')
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
-import Data.Primitive.SmallArray (SmallArray, indexSmallArray, sizeofSmallArray)
+import Data.Primitive.SmallArray (SmallArray, indexSmallArray, sizeofSmallArray, smallArrayFromListN)
 import Data.Set (Set)
 import Data.Text (Text)
 import Nix.Derivation (Derivation)
-import Nix.Expr.Types (AttrKey (..), Expr (..), Formals, StringPart (..))
+import Nix.Expr.Types (AttrKey (..), CaptureInfo (..), Expr (..), Formals, NixAtom (..), StringPart (..))
 import Nix.Store.Path (StorePath)
 import System.IO.Unsafe (unsafePerformIO)
 import qualified Text.Regex.TDFA as RE
@@ -212,7 +215,11 @@
     -- Env is lazy for knot-tying in rec {}.
     LazyInheritFrom Env !Expr
   | -- | Pre-built thunk (for merged nested paths or other complex cases).
-    PreBuilt !Thunk
+    -- INTENTIONALLY LAZY: attrSetMapWithKeyLazy wraps deferred computations
+    -- here; the Thunk is only forced when the key is actually accessed via
+    -- attrSetLookup (which caches the result).  This avoids materializing
+    -- all 30k entries when mapAttrs is applied to a large set like nixpkgs.
+    PreBuilt Thunk
   deriving (Eq, Show)
 
 -- | Attribute set representation: either an eagerly-materialized map
@@ -401,6 +408,9 @@
     envParent :: !(Maybe Env),
     -- | With-scopes, innermost first.  Inherited from parent on
     -- env extension; only 'pushWithScope' adds new entries.
+    -- For trimmed envs ('CapturesWithScopes'), the root scope
+    -- (builtins) is appended as the outermost entry so that
+    -- 'EWithVar' can fall back to builtins without a parent chain.
     envWithScopes :: ![AttrSet]
   }
 
@@ -486,6 +496,24 @@
 pushWithScope scope env =
   env {envWithScopes = scope : envWithScopes env}
 
+-- | Find the root env's lazy scope (builtins) by walking the parent chain.
+-- Returns the 'envLazyScope' of the bottommost env (the one with
+-- @envParent = Nothing@).  This is the builtin env for standard evals.
+envRootScope :: Env -> Maybe AttrSet
+envRootScope env = case envParent env of
+  Nothing -> envLazyScope env
+  Just parent -> envRootScope parent
+
+-- | Build the with-scopes list for a trimmed env that needs with-scope
+-- access ('CapturesWithScopes').  Appends the root scope (builtins)
+-- as the outermost entry so 'EWithVar' can fall back to builtins
+-- without retaining the parent chain.
+withScopesForCapture :: Env -> [AttrSet]
+withScopesForCapture env =
+  case envRootScope env of
+    Just rootScope -> envWithScopes env ++ [rootScope]
+    Nothing -> envWithScopes env
+
 -- | Create an unevaluated thunk with a fresh memoization cell.
 --
 -- Each thunk gets its own 'IORef ThunkCell'.  On first force the
@@ -509,6 +537,37 @@
 mkSyntheticThunk :: Env -> Expr -> Thunk
 mkSyntheticThunk env thunkExpr =
   ThunkRef (newSyntheticCell (envSlots env) thunkExpr env)
+
+-- | Like 'mkThunk' but avoids IORef allocation for trivial expressions.
+-- Resolved variables reuse the existing thunk from the env (no wrapper).
+-- Literals and lambdas use 'Evaluated' directly (no IORef needed).
+-- Everything else falls back to 'mkThunk'.
+cheapThunk :: Env -> Expr -> Thunk
+cheapThunk env (EResolvedVar level idx) = envLookupResolved level idx env
+cheapThunk _ (ELit (NixInt n)) = Evaluated (VInt n)
+cheapThunk _ (ELit (NixFloat n)) = Evaluated (VFloat n)
+cheapThunk _ (ELit (NixBool b)) = Evaluated (VBool b)
+cheapThunk _ (ELit NixNull) = Evaluated VNull
+cheapThunk env (ELambda formals body NoCaptureInfo) = Evaluated (VLambda env formals body)
+cheapThunk env (ELambda formals body (Captures captureList)) =
+  let trimmedEnv =
+        Env
+          { envSlots = smallArrayFromListN (length captureList) [envLookupResolved lvl idx env | (lvl, idx) <- captureList],
+            envLazyScope = Nothing,
+            envParent = Nothing,
+            envWithScopes = []
+          }
+   in Evaluated (VLambda trimmedEnv formals body)
+cheapThunk env (ELambda formals body (CapturesWithScopes captureList)) =
+  let trimmedEnv =
+        Env
+          { envSlots = smallArrayFromListN (length captureList) [envLookupResolved lvl idx env | (lvl, idx) <- captureList],
+            envLazyScope = Nothing,
+            envParent = Nothing,
+            envWithScopes = withScopesForCapture env
+          }
+   in Evaluated (VLambda trimmedEnv formals body)
+cheapThunk env expr = mkThunk env expr
 
 -- | Allocate a fresh memoization cell for a thunk.
 --
diff --git a/src/Nix/Expr/ClosureTrim.hs b/src/Nix/Expr/ClosureTrim.hs
new file mode 100644
--- /dev/null
+++ b/src/Nix/Expr/ClosureTrim.hs
@@ -0,0 +1,501 @@
+-- | Closure trimming pass: rewrites lambdas, let blocks, and recursive
+-- attr sets to capture only the free variables they actually use.
+--
+-- Without trimming, every 'VLambda' and every let\/rec binding thunk
+-- retains the ENTIRE parent 'Env' chain.  A scope using 3 variables
+-- retains a 30k-entry nixpkgs scope.  This pass, run after
+-- 'resolveVars', attaches a 'Captures' list to each trimmable
+-- 'ELambda', 'ELet', and recursive 'EAttrs' so the evaluator can
+-- build a flat env with @envParent = Nothing@, breaking the retention
+-- chain.
+--
+-- A scope is trimmable when its body contains only 'EResolvedVar'
+-- references to outer scopes (no 'EVar' that resolves through the
+-- parent chain).  Scopes with outer 'EVar' references (with-scope
+-- lookups, non-positional bindings) cannot be trimmed because those
+-- require walking the full parent chain at runtime.
+module Nix.Expr.ClosureTrim
+  ( trimClosures,
+  )
+where
+
+import Data.List (foldl', sortBy)
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import Data.Ord (comparing)
+import Data.Set (Set)
+import qualified Data.Set as Set
+import Data.Text (Text)
+import Nix.Expr.Types
+
+-- | Walk the AST and attach 'Captures' info to trimmable scopes
+-- (lambdas, let blocks, and recursive attr sets).
+trimClosures :: Expr -> Expr
+trimClosures = trimExpr Set.empty
+
+-- | Walk expression, tracking names bound in enclosing inner scopes
+-- (let/rec/lambda formals) so we can tell if an 'EVar' escapes to
+-- an outer scope.
+--
+-- @innerNames@ accumulates names from scopes INSIDE the current
+-- lambda being analyzed.  It resets to empty when we enter a new
+-- lambda (since that lambda's body is the new analysis target).
+trimExpr :: Set Text -> Expr -> Expr
+trimExpr innerNames expr = case expr of
+  ELit {} -> expr
+  EStr parts -> EStr (map (trimPart innerNames) parts)
+  EIndStr parts -> EIndStr (map (trimPart innerNames) parts)
+  EVar {} -> expr
+  EWithVar {} -> expr
+  EResolvedVar {} -> expr
+  EAttrs True bindings captureInfo ->
+    let names = collectBindingNames bindings
+        innerNamesRec = Set.union innerNames names
+        trimmedBindings = map (trimBinding innerNamesRec) bindings
+     in case captureInfo of
+          Captures {} -> EAttrs True trimmedBindings captureInfo
+          CapturesWithScopes {} -> EAttrs True trimmedBindings captureInfo
+          NoCaptureInfo ->
+            case trimOneRecAttrs trimmedBindings of
+              Left unchanged -> EAttrs True unchanged NoCaptureInfo
+              Right (captureList, rewrittenBindings, needsWithScopes) ->
+                let ci =
+                      if needsWithScopes
+                        then CapturesWithScopes captureList
+                        else Captures captureList
+                 in EAttrs True rewrittenBindings ci
+  EAttrs False bindings _captureInfo ->
+    EAttrs False (map (trimBinding innerNames) bindings) NoCaptureInfo
+  EList elems -> EList (map (trimExpr innerNames) elems)
+  ESelect target path defExpr ->
+    ESelect
+      (trimExpr innerNames target)
+      (map (trimKey innerNames) path)
+      (fmap (trimExpr innerNames) defExpr)
+  EHasAttr target path ->
+    EHasAttr (trimExpr innerNames target) (map (trimKey innerNames) path)
+  EApp f x -> EApp (trimExpr innerNames f) (trimExpr innerNames x)
+  ELambda formals body captures ->
+    -- First, recursively trim nested lambdas in the body.
+    -- Reset innerNames since we're entering a new lambda scope.
+    let formalNames = formalsNames formals
+        trimmedFormals = trimFormals formals
+        trimmedBody = trimExpr formalNames body
+     in case captures of
+          Captures {} -> ELambda trimmedFormals trimmedBody captures
+          CapturesWithScopes {} -> ELambda trimmedFormals trimmedBody captures
+          NoCaptureInfo ->
+            case trimOneLambda trimmedFormals trimmedBody of
+              Left unchanged -> unchanged
+              Right (captureList, rewrittenFormals, rewrittenBody, needsWithScopes) ->
+                let captureInfo =
+                      if needsWithScopes
+                        then CapturesWithScopes captureList
+                        else Captures captureList
+                 in ELambda rewrittenFormals rewrittenBody captureInfo
+  ELet bindings body captureInfo ->
+    let names = collectBindingNames bindings
+        innerNamesLet = Set.union innerNames names
+        trimmedBindings = map (trimBinding innerNamesLet) bindings
+        trimmedBody = trimExpr innerNamesLet body
+     in case captureInfo of
+          Captures {} -> ELet trimmedBindings trimmedBody captureInfo
+          CapturesWithScopes {} -> ELet trimmedBindings trimmedBody captureInfo
+          NoCaptureInfo ->
+            case trimOneLetBlock trimmedBindings trimmedBody of
+              Left (unchangedBindings, unchangedBody) ->
+                ELet unchangedBindings unchangedBody NoCaptureInfo
+              Right (captureList, rewrittenBindings, rewrittenBody, needsWithScopes) ->
+                let ci =
+                      if needsWithScopes
+                        then CapturesWithScopes captureList
+                        else Captures captureList
+                 in ELet rewrittenBindings rewrittenBody ci
+  EIf c t f ->
+    EIf (trimExpr innerNames c) (trimExpr innerNames t) (trimExpr innerNames f)
+  EWith scope body ->
+    EWith (trimExpr innerNames scope) (trimExpr innerNames body)
+  EAssert cond body ->
+    EAssert (trimExpr innerNames cond) (trimExpr innerNames body)
+  EUnary op operand -> EUnary op (trimExpr innerNames operand)
+  EBinary op l r -> EBinary op (trimExpr innerNames l) (trimExpr innerNames r)
+  ESearchPath {} -> expr
+
+trimPart :: Set Text -> StringPart -> StringPart
+trimPart _ p@(StrLit _) = p
+trimPart innerNames (StrInterp e) = StrInterp (trimExpr innerNames e)
+
+trimKey :: Set Text -> AttrKey -> AttrKey
+trimKey _ k@(StaticKey _) = k
+trimKey innerNames (DynamicKey e) = DynamicKey (trimExpr innerNames e)
+
+trimBinding :: Set Text -> Binding -> Binding
+trimBinding innerNames (NamedBinding path bodyExpr) =
+  NamedBinding (map (trimKey innerNames) path) (trimExpr innerNames bodyExpr)
+trimBinding innerNames (Inherit (Just fromExpr) names) =
+  Inherit (Just (trimExpr innerNames fromExpr)) names
+trimBinding _ b@(Inherit Nothing _) = b
+
+trimFormals :: Formals -> Formals
+trimFormals f@(FormalName _) = f
+trimFormals (FormalSet formals ellipsis) =
+  FormalSet (map trimFormal formals) ellipsis
+trimFormals (FormalNamedSet name formals ellipsis) =
+  FormalNamedSet name (map trimFormal formals) ellipsis
+
+trimFormal :: Formal -> Formal
+trimFormal (Formal name defExpr) =
+  -- Default expressions are evaluated in the lambda's own scope,
+  -- so they get fresh innerNames (just the formals themselves).
+  -- But we already trimmed nested lambdas, and defaults are part
+  -- of the lambda's body — they'll be captured correctly.
+  Formal name (fmap (trimExpr Set.empty) defExpr)
+
+-- | Analyze a single lambda body and formals defaults, produce a capture
+-- list + rewritten body + rewritten formals, or return the original
+-- lambda unchanged if untrimable.
+--
+-- Formals defaults must be included because they are evaluated in the
+-- lambda's env: a default like @{ x ? outerVar }: x@ references the
+-- closure env, which for a trimmed lambda is the flat capture env.
+-- Missing captures for defaults would cause out-of-bounds slot lookups.
+trimOneLambda :: Formals -> Expr -> Either Expr ([(Int, Int)], Formals, Expr, Bool)
+trimOneLambda formals body =
+  let (bodyVars, bodyHasEVar, bodyHasWithVar) = collectFreeVars 0 body
+      (formalVars, formalHasEVar, formalHasWithVar) = foldFormalsDefaults 0 formals
+      freeVars = Set.union bodyVars formalVars
+      hasOuterEVar = bodyHasEVar || formalHasEVar
+      needsWithScopes = bodyHasWithVar || formalHasWithVar
+   in if hasOuterEVar || (Set.null freeVars && not needsWithScopes)
+        then Left (ELambda formals body NoCaptureInfo)
+        else
+          let captureList = sortBy (comparing fst <> comparing snd) (Set.toList freeVars)
+              captureMap = Map.fromList (zip captureList [0 ..])
+              rewrittenBody = rewriteBody 0 captureMap body
+              rewrittenFormals = rewriteFormals 0 captureMap formals
+           in Right (captureList, rewrittenFormals, rewrittenBody, needsWithScopes)
+
+-- | Collect free variable references from a lambda body.
+--
+-- Returns @(freeVars, hasOuterEVar, hasOuterWithVar)@ where:
+-- * @freeVars@ is the set of @(level, index)@ pairs for 'EResolvedVar'
+--   references that point outside the lambda (level > current depth)
+-- * @hasOuterEVar@ is 'True' if there's any 'EVar' that could reference
+--   the outer scope (not shadowed by inner let/rec/lambda scopes)
+-- * @hasOuterWithVar@ is 'True' if there's any 'EWithVar' that needs
+--   with-scope access (doesn't block trimming, but env must preserve
+--   'envWithScopes' with builtins appended)
+--
+-- @depth@ tracks how many scope-creating constructs we've entered
+-- inside the lambda body (nested lambdas, let, rec attrs).
+collectFreeVars :: Int -> Expr -> (Set (Int, Int), Bool, Bool)
+collectFreeVars depth expr = case expr of
+  ELit {} -> (Set.empty, False, False)
+  EStr parts -> foldParts depth parts
+  EIndStr parts -> foldParts depth parts
+  EVar _ ->
+    -- Any EVar in the body means we need the parent chain for name lookup.
+    -- This makes the lambda untrimable.
+    (Set.empty, True, False)
+  EWithVar _ ->
+    -- Needs with-scopes, but doesn't block trimming.
+    (Set.empty, False, True)
+  EResolvedVar level idx
+    | level > depth -> (Set.singleton (level - depth - 1, idx), False, False)
+    | otherwise -> (Set.empty, False, False)
+  EAttrs True bindings captureInfo ->
+    -- Recursive attrs create a scope (depth + 1)
+    let (vs1, h1, w1) = foldBindings (depth + 1) bindings
+        (vs2, innerWithVar) = case captureInfo of
+          NoCaptureInfo -> (Set.empty, False)
+          Captures caps ->
+            (Set.fromList [(lvl - depth - 1, idx) | (lvl, idx) <- caps, lvl > depth], False)
+          CapturesWithScopes caps ->
+            (Set.fromList [(lvl - depth - 1, idx) | (lvl, idx) <- caps, lvl > depth], True)
+     in (Set.union vs1 vs2, h1, w1 || innerWithVar)
+  EAttrs False bindings _captureInfo ->
+    -- Non-recursive attrs: no new scope
+    foldBindings depth bindings
+  EList elems -> foldExprs depth elems
+  ESelect target path defExpr ->
+    let (vs1, h1, w1) = collectFreeVars depth target
+        (vs2, h2, w2) = foldKeys depth path
+        (vs3, h3, w3) = case defExpr of
+          Nothing -> (Set.empty, False, False)
+          Just e -> collectFreeVars depth e
+     in (Set.unions [vs1, vs2, vs3], h1 || h2 || h3, w1 || w2 || w3)
+  EHasAttr target path ->
+    let (vs1, h1, w1) = collectFreeVars depth target
+        (vs2, h2, w2) = foldKeys depth path
+     in (Set.union vs1 vs2, h1 || h2, w1 || w2)
+  EApp f x ->
+    let (vs1, h1, w1) = collectFreeVars depth f
+        (vs2, h2, w2) = collectFreeVars depth x
+     in (Set.union vs1 vs2, h1 || h2, w1 || w2)
+  ELambda formals body_ captures ->
+    -- Nested lambda creates a LexicalScope (depth + 1).
+    -- If the inner lambda was already trimmed (has Captures), its capture
+    -- coordinates are outer references that we must propagate upward —
+    -- otherwise an outer lambda won't know it needs those variables.
+    let (vs1, h1, w1) = foldFormalsDefaults (depth + 1) formals
+        (vs2, h2, w2) = collectFreeVars (depth + 1) body_
+        (vs3, innerWithVar) = case captures of
+          NoCaptureInfo -> (Set.empty, False)
+          Captures caps ->
+            (Set.fromList [(lvl - depth - 1, idx) | (lvl, idx) <- caps, lvl > depth], False)
+          CapturesWithScopes caps ->
+            (Set.fromList [(lvl - depth - 1, idx) | (lvl, idx) <- caps, lvl > depth], True)
+     in (Set.unions [vs1, vs2, vs3], h1 || h2, w1 || w2 || innerWithVar)
+  ELet bindings body_ captureInfo ->
+    -- Let creates a scope (depth + 1)
+    let (vs1, h1, w1) = foldBindings (depth + 1) bindings
+        (vs2, h2, w2) = collectFreeVars (depth + 1) body_
+        (vs3, innerWithVar) = case captureInfo of
+          NoCaptureInfo -> (Set.empty, False)
+          Captures caps ->
+            (Set.fromList [(lvl - depth - 1, idx) | (lvl, idx) <- caps, lvl > depth], False)
+          CapturesWithScopes caps ->
+            (Set.fromList [(lvl - depth - 1, idx) | (lvl, idx) <- caps, lvl > depth], True)
+     in (Set.unions [vs1, vs2, vs3], h1 || h2, w1 || w2 || innerWithVar)
+  EIf c t f ->
+    let (vs1, h1, w1) = collectFreeVars depth c
+        (vs2, h2, w2) = collectFreeVars depth t
+        (vs3, h3, w3) = collectFreeVars depth f
+     in (Set.unions [vs1, vs2, vs3], h1 || h2 || h3, w1 || w2 || w3)
+  EWith scope body_ ->
+    -- With does NOT create a scope frame
+    let (vs1, h1, w1) = collectFreeVars depth scope
+        (vs2, h2, w2) = collectFreeVars depth body_
+     in (Set.union vs1 vs2, h1 || h2, w1 || w2)
+  EAssert cond body_ ->
+    let (vs1, h1, w1) = collectFreeVars depth cond
+        (vs2, h2, w2) = collectFreeVars depth body_
+     in (Set.union vs1 vs2, h1 || h2, w1 || w2)
+  EUnary _ operand -> collectFreeVars depth operand
+  EBinary _ l r ->
+    let (vs1, h1, w1) = collectFreeVars depth l
+        (vs2, h2, w2) = collectFreeVars depth r
+     in (Set.union vs1 vs2, h1 || h2, w1 || w2)
+  ESearchPath {} -> (Set.empty, False, False)
+
+foldExprs :: Int -> [Expr] -> (Set (Int, Int), Bool, Bool)
+foldExprs depth = foldl' combine (Set.empty, False, False)
+  where
+    combine (!acc, !h, !w) e =
+      let (vs, hv, wv) = collectFreeVars depth e
+       in (Set.union acc vs, h || hv, w || wv)
+
+foldParts :: Int -> [StringPart] -> (Set (Int, Int), Bool, Bool)
+foldParts depth = foldl' combine (Set.empty, False, False)
+  where
+    combine (!acc, !h, !w) (StrLit _) = (acc, h, w)
+    combine (!acc, !h, !w) (StrInterp e) =
+      let (vs, hv, wv) = collectFreeVars depth e
+       in (Set.union acc vs, h || hv, w || wv)
+
+foldKeys :: Int -> [AttrKey] -> (Set (Int, Int), Bool, Bool)
+foldKeys depth = foldl' combine (Set.empty, False, False)
+  where
+    combine (!acc, !h, !w) (StaticKey _) = (acc, h, w)
+    combine (!acc, !h, !w) (DynamicKey e) =
+      let (vs, hv, wv) = collectFreeVars depth e
+       in (Set.union acc vs, h || hv, w || wv)
+
+foldBindings :: Int -> [Binding] -> (Set (Int, Int), Bool, Bool)
+foldBindings depth = foldl' combine (Set.empty, False, False)
+  where
+    combine (!acc, !h, !w) (NamedBinding path bodyExpr) =
+      let (vs1, h1, w1) = foldKeys depth path
+          (vs2, h2, w2) = collectFreeVars depth bodyExpr
+       in (Set.unions [acc, vs1, vs2], h || h1 || h2, w || w1 || w2)
+    combine (!acc, !h, !w) (Inherit (Just fromExpr) _) =
+      let (vs, hv, wv) = collectFreeVars depth fromExpr
+       in (Set.union acc vs, h || hv, w || wv)
+    combine (!acc, !h, !w) (Inherit Nothing _) = (acc, h, w)
+
+foldFormalsDefaults :: Int -> Formals -> (Set (Int, Int), Bool, Bool)
+foldFormalsDefaults _ (FormalName _) = (Set.empty, False, False)
+foldFormalsDefaults depth (FormalSet formals _) = foldFormals depth formals
+foldFormalsDefaults depth (FormalNamedSet _ formals _) = foldFormals depth formals
+
+foldFormals :: Int -> [Formal] -> (Set (Int, Int), Bool, Bool)
+foldFormals depth = foldl' combine (Set.empty, False, False)
+  where
+    combine (!acc, !h, !w) (Formal _ Nothing) = (acc, h, w)
+    combine (!acc, !h, !w) (Formal _ (Just defExpr)) =
+      let (vs, hv, wv) = collectFreeVars depth defExpr
+       in (Set.union acc vs, h || hv, w || wv)
+
+-- | Rewrite 'EResolvedVar' references in a lambda body to point at
+-- capture positions instead of parent-chain levels.
+--
+-- An outer reference @EResolvedVar level idx@ where @level > depth@
+-- maps to @EResolvedVar (depth + 1) captureIdx@.  The @depth + 1@
+-- accounts for the fact that 'matchFormals' creates an argEnv with
+-- @envParent = trimmedEnv@, so level 0 = formals, level 1 = captures.
+rewriteBody :: Int -> Map (Int, Int) Int -> Expr -> Expr
+rewriteBody depth captureMap expr = case expr of
+  ELit {} -> expr
+  EStr parts -> EStr (map (rewritePart depth captureMap) parts)
+  EIndStr parts -> EIndStr (map (rewritePart depth captureMap) parts)
+  EVar {} -> expr
+  EWithVar {} -> expr
+  EResolvedVar level idx
+    | level > depth ->
+        let outerKey = (level - depth - 1, idx)
+         in case Map.lookup outerKey captureMap of
+              Just captureIdx -> EResolvedVar (depth + 1) captureIdx
+              -- Unreachable: collectFreeVars found all outer refs
+              Nothing -> expr
+    | otherwise -> expr
+  EAttrs True bindings captureInfo ->
+    EAttrs True (map (rewriteBinding (depth + 1) captureMap) bindings) (rewriteCaptures depth captureMap captureInfo)
+  EAttrs False bindings captureInfo ->
+    EAttrs False (map (rewriteBinding depth captureMap) bindings) captureInfo
+  EList elems -> EList (map (rewriteBody depth captureMap) elems)
+  ESelect target path defExpr ->
+    ESelect
+      (rewriteBody depth captureMap target)
+      (map (rewriteKey depth captureMap) path)
+      (fmap (rewriteBody depth captureMap) defExpr)
+  EHasAttr target path ->
+    EHasAttr
+      (rewriteBody depth captureMap target)
+      (map (rewriteKey depth captureMap) path)
+  EApp f x ->
+    EApp (rewriteBody depth captureMap f) (rewriteBody depth captureMap x)
+  ELambda formals body_ captures ->
+    ELambda
+      (rewriteFormals (depth + 1) captureMap formals)
+      (rewriteBody (depth + 1) captureMap body_)
+      (rewriteCaptures depth captureMap captures)
+  ELet bindings body_ captureInfo ->
+    ELet
+      (map (rewriteBinding (depth + 1) captureMap) bindings)
+      (rewriteBody (depth + 1) captureMap body_)
+      (rewriteCaptures depth captureMap captureInfo)
+  EIf c t f ->
+    EIf
+      (rewriteBody depth captureMap c)
+      (rewriteBody depth captureMap t)
+      (rewriteBody depth captureMap f)
+  EWith scope body_ ->
+    EWith (rewriteBody depth captureMap scope) (rewriteBody depth captureMap body_)
+  EAssert cond body_ ->
+    EAssert (rewriteBody depth captureMap cond) (rewriteBody depth captureMap body_)
+  EUnary op operand -> EUnary op (rewriteBody depth captureMap operand)
+  EBinary op l r ->
+    EBinary op (rewriteBody depth captureMap l) (rewriteBody depth captureMap r)
+  ESearchPath {} -> expr
+
+rewritePart :: Int -> Map (Int, Int) Int -> StringPart -> StringPart
+rewritePart _ _ p@(StrLit _) = p
+rewritePart depth captureMap (StrInterp e) =
+  StrInterp (rewriteBody depth captureMap e)
+
+rewriteKey :: Int -> Map (Int, Int) Int -> AttrKey -> AttrKey
+rewriteKey _ _ k@(StaticKey _) = k
+rewriteKey depth captureMap (DynamicKey e) =
+  DynamicKey (rewriteBody depth captureMap e)
+
+rewriteBinding :: Int -> Map (Int, Int) Int -> Binding -> Binding
+rewriteBinding depth captureMap (NamedBinding path bodyExpr) =
+  NamedBinding
+    (map (rewriteKey depth captureMap) path)
+    (rewriteBody depth captureMap bodyExpr)
+rewriteBinding depth captureMap (Inherit (Just fromExpr) names) =
+  Inherit (Just (rewriteBody depth captureMap fromExpr)) names
+rewriteBinding _ _ b@(Inherit Nothing _) = b
+
+rewriteFormals :: Int -> Map (Int, Int) Int -> Formals -> Formals
+rewriteFormals _ _ f@(FormalName _) = f
+rewriteFormals depth captureMap (FormalSet formals ellipsis) =
+  FormalSet (map (rewriteFormal depth captureMap) formals) ellipsis
+rewriteFormals depth captureMap (FormalNamedSet name formals ellipsis) =
+  FormalNamedSet name (map (rewriteFormal depth captureMap) formals) ellipsis
+
+rewriteFormal :: Int -> Map (Int, Int) Int -> Formal -> Formal
+rewriteFormal depth captureMap (Formal name defExpr) =
+  Formal name (fmap (rewriteBody depth captureMap) defExpr)
+
+-- | Rewrite capture coordinates on an already-trimmed inner lambda.
+--
+-- When an outer lambda is trimmed, inner lambdas that were trimmed in a
+-- previous pass hold capture coordinates relative to the old env layout.
+-- Those coordinates must be remapped to point at the outer lambda's
+-- capture slots instead.
+rewriteCaptures :: Int -> Map (Int, Int) Int -> CaptureInfo -> CaptureInfo
+rewriteCaptures _ _ NoCaptureInfo = NoCaptureInfo
+rewriteCaptures depth captureMap (Captures caps) =
+  Captures (map rewriteOne caps)
+  where
+    rewriteOne (lvl, idx)
+      | lvl > depth =
+          let outerKey = (lvl - depth - 1, idx)
+           in case Map.lookup outerKey captureMap of
+                Just captureIdx -> (depth + 1, captureIdx)
+                -- Unreachable: collectFreeVars propagated all inner captures
+                Nothing -> (lvl, idx)
+      | otherwise = (lvl, idx)
+rewriteCaptures depth captureMap (CapturesWithScopes caps) =
+  CapturesWithScopes (map rewriteOne caps)
+  where
+    rewriteOne (lvl, idx)
+      | lvl > depth =
+          let outerKey = (lvl - depth - 1, idx)
+           in case Map.lookup outerKey captureMap of
+                Just captureIdx -> (depth + 1, captureIdx)
+                Nothing -> (lvl, idx)
+      | otherwise = (lvl, idx)
+
+-- | Extract names introduced by lambda formals.
+formalsNames :: Formals -> Set Text
+formalsNames (FormalName name) = Set.singleton name
+formalsNames (FormalSet formals _) =
+  Set.fromList (map fName formals)
+formalsNames (FormalNamedSet name formals _) =
+  Set.insert name (Set.fromList (map fName formals))
+
+-- | Analyze a let block's binding bodies + body for parent references,
+-- produce a capture list + rewritten bindings + body, or return the
+-- originals unchanged if untrimable.
+--
+-- Level 0 = self-references within the let scope (untouched).
+-- Level 1+ = parent chain references (trimmed).
+trimOneLetBlock :: [Binding] -> Expr -> Either ([Binding], Expr) ([(Int, Int)], [Binding], Expr, Bool)
+trimOneLetBlock bindings body =
+  let (bindingVars, bindingHasEVar, bindingHasWithVar) = foldBindings 0 bindings
+      (bodyVars, bodyHasEVar, bodyHasWithVar) = collectFreeVars 0 body
+      freeVars = Set.union bindingVars bodyVars
+      hasOuterEVar = bindingHasEVar || bodyHasEVar
+      needsWithScopes = bindingHasWithVar || bodyHasWithVar
+   in if hasOuterEVar || (Set.null freeVars && not needsWithScopes)
+        then Left (bindings, body)
+        else
+          let captureList = sortBy (comparing fst <> comparing snd) (Set.toList freeVars)
+              captureMap = Map.fromList (zip captureList [0 ..])
+              rewrittenBindings = map (rewriteBinding 0 captureMap) bindings
+              rewrittenBody = rewriteBody 0 captureMap body
+           in Right (captureList, rewrittenBindings, rewrittenBody, needsWithScopes)
+
+-- | Analyze a recursive attr set's bindings for parent references,
+-- produce a capture list + rewritten bindings, or return the originals
+-- unchanged if untrimable.
+trimOneRecAttrs :: [Binding] -> Either [Binding] ([(Int, Int)], [Binding], Bool)
+trimOneRecAttrs bindings =
+  let (bindingVars, bindingHasEVar, bindingHasWithVar) = foldBindings 0 bindings
+   in if bindingHasEVar || (Set.null bindingVars && not bindingHasWithVar)
+        then Left bindings
+        else
+          let captureList = sortBy (comparing fst <> comparing snd) (Set.toList bindingVars)
+              captureMap = Map.fromList (zip captureList [0 ..])
+              rewrittenBindings = map (rewriteBinding 0 captureMap) bindings
+           in Right (captureList, rewrittenBindings, bindingHasWithVar)
+
+-- | Collect top-level binding names (mirrors 'Nix.Expr.Resolve.collectBindingNames').
+collectBindingNames :: [Binding] -> Set Text
+collectBindingNames = foldl' addNames Set.empty
+  where
+    addNames acc (NamedBinding (StaticKey name : _) _) = Set.insert name acc
+    addNames acc (NamedBinding _ _) = acc
+    addNames acc (Inherit _ names) = foldl' (flip Set.insert) acc names
diff --git a/src/Nix/Expr/Resolve.hs b/src/Nix/Expr/Resolve.hs
--- a/src/Nix/Expr/Resolve.hs
+++ b/src/Nix/Expr/Resolve.hs
@@ -1,10 +1,10 @@
 -- | Variable resolution pass: replaces 'EVar' with 'EResolvedVar'
--- for variables bound by lambda formals.
+-- for variables bound by lambda formals and let\/rec bindings.
 --
--- Lambda formals get positional (de Bruijn-style) indices.  Let\/rec
--- bindings act as name barriers (preventing resolution through them)
--- since they use 'envLazyScope' at runtime, which is already zero-cost.
--- With-scopes and builtins remain name-based.
+-- Lambda formals and eligible let\/rec bindings get positional
+-- (de Bruijn-style) indices via 'LexicalScope'.  Let\/rec blocks with
+-- dynamic keys or nested paths fall back to 'NameBarrier' (name-based
+-- lookup at runtime).  With-scopes and builtins remain name-based.
 --
 -- Called once at parse time ('Nix.Parser.parseNix').
 module Nix.Expr.Resolve
@@ -26,10 +26,16 @@
     LexicalScope !(Map Text Int)
   | -- | Let/rec binding names: blocks resolution (handled by name at runtime).
     NameBarrier !(Set Text)
+  | -- | Marks a with-scope boundary on the stack.
+    -- Does NOT increment the de Bruijn level (with doesn't create a
+    -- parent env level at runtime).  Variables not found in any lexical
+    -- scope below a WithBarrier are upgraded to 'EWithVar'.
+    WithBarrier
 
 -- | Resolve variables in an expression.  Replaces 'EVar' with
 -- 'EResolvedVar' where the variable is lexically bound by a lambda
--- formal.  All other variables remain as 'EVar' for name-based lookup.
+-- formal or an eligible let\/rec binding.  Variables in with-scopes,
+-- builtins, and fallback let\/rec blocks remain as 'EVar'.
 resolveVars :: Expr -> Expr
 resolveVars = resolve []
 
@@ -41,33 +47,47 @@
   EIndStr parts -> EIndStr (map (resolvePart stack) parts)
   EVar name -> resolveVar stack 0 name
   EResolvedVar _ _ -> expr
-  EAttrs True bindings ->
-    -- Recursive: binding RHSes see their own names (as NameBarrier).
-    let names = collectBindingNames bindings
-        newStack = NameBarrier names : stack
-     in EAttrs True (concatMap (resolveBinding newStack) bindings)
-  EAttrs False bindings ->
+  EAttrs True bindings _captureInfo
+    | allStaticSingleKey bindings ->
+        -- Positional: all bindings are single static keys or inherits.
+        let scope = lexicalScopeFromBindings bindings
+            innerStack = scope : stack
+         in EAttrs True (concatMap (resolveLetBinding stack innerStack) bindings) NoCaptureInfo
+    | otherwise ->
+        -- Fallback: dynamic keys or nested paths — use NameBarrier.
+        let names = collectBindingNames bindings
+            newStack = NameBarrier names : stack
+         in EAttrs True (concatMap (resolveBinding newStack) bindings) NoCaptureInfo
+  EAttrs False bindings _captureInfo ->
     -- Non-recursive: bindings use the outer scope.
-    EAttrs False (concatMap (resolveBinding stack) bindings)
+    EAttrs False (concatMap (resolveBinding stack) bindings) NoCaptureInfo
   EList elems -> EList (map (resolve stack) elems)
   ESelect target path defExpr ->
     ESelect (resolve stack target) (map (resolveKey stack) path) (fmap (resolve stack) defExpr)
   EHasAttr target path ->
     EHasAttr (resolve stack target) (map (resolveKey stack) path)
   EApp f x -> EApp (resolve stack f) (resolve stack x)
-  ELambda formals body ->
+  ELambda formals body _captures ->
     let scope = lexicalScopeFromFormals formals
         newStack = scope : stack
-     in ELambda (resolveFormalsDefaults newStack formals) (resolve newStack body)
-  ELet bindings body ->
-    -- Both body and binding RHSes see the let names (as NameBarrier).
-    let names = collectBindingNames bindings
-        newStack = NameBarrier names : stack
-     in ELet (concatMap (resolveBinding newStack) bindings) (resolve newStack body)
+     in ELambda (resolveFormalsDefaults newStack formals) (resolve newStack body) NoCaptureInfo
+  ELet bindings body _captureInfo
+    | allStaticSingleKey bindings ->
+        -- Positional: all bindings are single static keys or inherits.
+        let scope = lexicalScopeFromBindings bindings
+            innerStack = scope : stack
+         in ELet (concatMap (resolveLetBinding stack innerStack) bindings) (resolve innerStack body) NoCaptureInfo
+    | otherwise ->
+        -- Fallback: dynamic keys or nested paths — use NameBarrier.
+        let names = collectBindingNames bindings
+            newStack = NameBarrier names : stack
+         in ELet (concatMap (resolveBinding newStack) bindings) (resolve newStack body) NoCaptureInfo
   EIf c t f -> EIf (resolve stack c) (resolve stack t) (resolve stack f)
+  EWithVar _ -> expr
   EWith scope body ->
-    -- with-scope names are unknown statically; no scope change.
-    EWith (resolve stack scope) (resolve stack body)
+    -- Push WithBarrier for the body so that unresolved names
+    -- inside a with-scope are upgraded to EWithVar.
+    EWith (resolve stack scope) (resolve (WithBarrier : stack) body)
   EAssert cond body ->
     EAssert (resolve stack cond) (resolve stack body)
   EUnary op operand -> EUnary op (resolve stack operand)
@@ -91,6 +111,14 @@
   if Set.member name names
     then EVar name
     else resolveVar rest (level + 1) name
+resolveVar (WithBarrier : rest) level name =
+  -- WithBarrier does NOT increment level (with doesn't create an env level).
+  -- Continue searching lexical scopes below.  If the name is found in a
+  -- lower LexicalScope, use that.  If it reaches the bottom as EVar
+  -- (not found lexically), upgrade to EWithVar.
+  case resolveVar rest level name of
+    EVar _ -> EWithVar name
+    resolved -> resolved
 
 -- | Build a 'LexicalScope' from lambda formals.
 --
@@ -141,6 +169,50 @@
 resolveBinding stack (Inherit Nothing names) =
   -- Desugar: inherit x y; → x = x; y = y;
   [NamedBinding [StaticKey name] (resolve stack (EVar name)) | name <- names]
+
+-- | Check if all bindings are eligible for positional resolution:
+-- each binding must be either a single static key or an inherit.
+-- Blocks with dynamic keys (@${expr} = val@) or nested paths
+-- (@a.b = val@) are ineligible and fall back to 'NameBarrier'.
+allStaticSingleKey :: [Binding] -> Bool
+allStaticSingleKey = all isEligible
+  where
+    isEligible (NamedBinding [StaticKey _] _) = True
+    isEligible (Inherit _ _) = True
+    isEligible _ = False
+
+-- | Build a 'LexicalScope' from let\/rec bindings, assigning positional
+-- indices in declaration order.  Inherits are expanded in-place (each
+-- inherited name gets its own index).  Later duplicates win (via
+-- 'Map.fromList' right-bias), matching Nix's last-definition-wins
+-- semantics.
+lexicalScopeFromBindings :: [Binding] -> ScopeEntry
+lexicalScopeFromBindings bindings =
+  LexicalScope (Map.fromList (zip names [0 ..]))
+  where
+    names = concatMap bindingNames bindings
+    bindingNames (NamedBinding [StaticKey name] _) = [name]
+    bindingNames (Inherit _ inheritNames) = inheritNames
+    -- Unreachable: allStaticSingleKey guards this path.
+    bindingNames _ = []
+
+-- | Resolve bindings in a let\/rec block that uses positional resolution.
+-- Takes two stacks: @outerStack@ (before the let scope) and @innerStack@
+-- (with the let's 'LexicalScope' pushed).
+--
+-- Regular bindings resolve their RHS against @innerStack@ (recursive).
+-- @inherit x@ desugars to @x = x@ where the RHS resolves against
+-- @outerStack@ — the inherited name must reference the enclosing scope,
+-- not the let scope being defined.
+resolveLetBinding :: [ScopeEntry] -> [ScopeEntry] -> Binding -> [Binding]
+resolveLetBinding _ innerStack (NamedBinding path bodyExpr) =
+  [NamedBinding (map (resolveKey innerStack) path) (resolve innerStack bodyExpr)]
+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]
 
 -- | Resolve variables inside formal default expressions.
 resolveFormalsDefaults :: [ScopeEntry] -> Formals -> Formals
diff --git a/src/Nix/Expr/Types.hs b/src/Nix/Expr/Types.hs
--- a/src/Nix/Expr/Types.hs
+++ b/src/Nix/Expr/Types.hs
@@ -21,6 +21,9 @@
     Formals (..),
     Formal (..),
 
+    -- * Closure capture info
+    CaptureInfo (..),
+
     -- * Operators
     UnaryOp (..),
     BinaryOp (..),
@@ -86,6 +89,22 @@
     FormalNamedSet !Text ![Formal] !Bool
   deriving (Eq, Show)
 
+-- | Closure capture information for lambda trimming.
+--
+-- After 'Nix.Expr.ClosureTrim.trimClosures', lambdas that only reference
+-- a subset of their outer scope carry a 'Captures' list.  At eval time
+-- this drives extraction of exactly those thunks into a flat 'Env',
+-- breaking the retention chain to the full parent scope.
+data CaptureInfo
+  = -- | No capture analysis performed (lambda untrimmed).
+    NoCaptureInfo
+  | -- | Sorted list of @(level, index)@ pairs to extract from the parent env.
+    Captures ![(Int, Int)]
+  | -- | Like 'Captures', but the trimmed env must preserve 'envWithScopes'
+    -- (with builtins appended) so that 'EWithVar' references can still be resolved.
+    CapturesWithScopes ![(Int, Int)]
+  deriving (Eq, Show)
+
 -- | Unary operators.
 data UnaryOp
   = OpNot
@@ -126,7 +145,11 @@
   | -- | Variable reference.
     EVar !Text
   | -- | Attribute set: @{ bindings }@ or @rec { bindings }@.
-    EAttrs !Bool ![Binding]
+    --
+    -- The 'CaptureInfo' field is populated by 'Nix.Expr.ClosureTrim.trimClosures'
+    -- for recursive attr sets after variable resolution.  Non-recursive sets
+    -- always carry 'NoCaptureInfo'.
+    EAttrs !Bool ![Binding] !CaptureInfo
   | -- | List: @[ e1 e2 e3 ]@.
     EList ![Expr]
   | -- | Attribute selection: @expr.attrpath@ or @expr.attrpath or default@.
@@ -136,9 +159,19 @@
   | -- | Function application: @f x@.
     EApp !Expr !Expr
   | -- | Lambda: @formals: body@.
-    ELambda !Formals !Expr
+    --
+    -- The 'CaptureInfo' field is populated by 'Nix.Expr.ClosureTrim.trimClosures'
+    -- after variable resolution.  'NoCaptureInfo' means untrimmed (eval
+    -- captures the full parent env); 'Captures' lists exactly which outer
+    -- slots to extract into a flat env.
+    ELambda !Formals !Expr !CaptureInfo
   | -- | Let binding: @let bindings in body@.
-    ELet ![Binding] !Expr
+    --
+    -- The 'CaptureInfo' field is populated by 'Nix.Expr.ClosureTrim.trimClosures'
+    -- after variable resolution.  Positional let blocks that only reference
+    -- a subset of their parent scope carry a 'Captures' list; at eval time
+    -- this drives construction of a trimmed parent env.
+    ELet ![Binding] !Expr !CaptureInfo
   | -- | If-then-else: @if cond then t else f@.
     EIf !Expr !Expr !Expr
   | -- | With expression: @with expr; body@.
@@ -156,4 +189,9 @@
     -- bound by lambda formals.  @level@ counts parent-chain hops;
     -- @index@ is the positional slot within that env's 'envSlots'.
     EResolvedVar !Int !Int
+  | -- | Variable resolved via with-scopes (not lexical).
+    -- Produced by 'Nix.Expr.Resolve.resolveVars' for names inside
+    -- a @with@ body that are not found in any lexical scope.
+    -- At runtime, looked up in 'envWithScopes' (with builtins fallback).
+    EWithVar !Text
   deriving (Eq, Show)
diff --git a/src/Nix/Parser.hs b/src/Nix/Parser.hs
--- a/src/Nix/Parser.hs
+++ b/src/Nix/Parser.hs
@@ -42,14 +42,19 @@
     parseNix,
     parseNixFile,
 
+    -- * Encoding
+    readFileAutoEncoding,
+
     -- * Errors
     ParseError (..),
   )
 where
 
+import qualified Data.ByteString as BS
 import Data.Text (Text)
 import qualified Data.Text as T
-import qualified Data.Text.IO as TIO
+import qualified Data.Text.Encoding as TE
+import Nix.Expr.ClosureTrim (trimClosures)
 import Nix.Expr.Resolve (resolveVars)
 import Nix.Expr.Types (Expr)
 import Nix.Parser.Expr (parseTopLevel)
@@ -67,7 +72,7 @@
   tokens <- tokenize fileName (stripBOM source)
   let st = ParseState {psTokens = tokens, psFile = fileName}
   (expr, _remaining) <- runParser parseTopLevel st
-  pure (resolveVars expr)
+  pure (trimClosures (resolveVars expr))
 
 -- | Strip a leading UTF-8 byte order mark (U+FEFF) if present.
 stripBOM :: Text -> Text
@@ -76,12 +81,44 @@
   _ -> t
 
 -- | Parse a @.nix@ file from disk.
+-- Uses 'readFileAutoEncoding' to handle UTF-8, UTF-16 LE, and UTF-16 BE.
 parseNixFile :: FilePath -> IO (Either ParseError Expr)
 parseNixFile path = do
-  source <- TIO.readFile path
-  let fileName = packFilePath path
-  pure $ parseNix fileName source
+  source <- readFileAutoEncoding path
+  pure $ parseNix (T.pack path) source
 
--- | Convert a FilePath to Text for error reporting.
-packFilePath :: FilePath -> Text
-packFilePath = T.pack
+-- ---------------------------------------------------------------------------
+-- Encoding detection
+-- ---------------------------------------------------------------------------
+
+-- | Read a file, detecting encoding from its byte order mark.
+--
+-- Windows tools (PowerShell's @>@ operator, Notepad) commonly write
+-- UTF-16 LE.  Nix files are normally UTF-8, but on a Windows-first
+-- implementation we handle all three BOM variants:
+--
+-- * @FF FE@       → UTF-16 LE (PowerShell default)
+-- * @FE FF@       → UTF-16 BE
+-- * @EF BB BF@    → UTF-8 (BOM stripped)
+-- * No BOM        → UTF-8
+readFileAutoEncoding :: FilePath -> IO Text
+readFileAutoEncoding path = decodeAutoEncoding <$> BS.readFile path
+
+-- | Decode bytes to 'Text' based on byte order mark.
+decodeAutoEncoding :: BS.ByteString -> Text
+decodeAutoEncoding bytes
+  | BS.length bytes >= 2,
+    BS.index bytes 0 == 0xFF,
+    BS.index bytes 1 == 0xFE =
+      TE.decodeUtf16LE (BS.drop 2 bytes)
+  | BS.length bytes >= 2,
+    BS.index bytes 0 == 0xFE,
+    BS.index bytes 1 == 0xFF =
+      TE.decodeUtf16BE (BS.drop 2 bytes)
+  | BS.length bytes >= 3,
+    BS.index bytes 0 == 0xEF,
+    BS.index bytes 1 == 0xBB,
+    BS.index bytes 2 == 0xBF =
+      TE.decodeUtf8 (BS.drop 3 bytes)
+  | otherwise =
+      TE.decodeUtf8 bytes
diff --git a/src/Nix/Parser/Expr.hs b/src/Nix/Parser/Expr.hs
--- a/src/Nix/Parser/Expr.hs
+++ b/src/Nix/Parser/Expr.hs
@@ -69,7 +69,7 @@
 trySimpleLambda = do
   name <- expectIdent
   expect TokColon
-  ELambda (FormalName name) <$> parseExpr
+  (\body -> ELambda (FormalName name) body NoCaptureInfo) <$> parseExpr
 
 -- | Try @name\@{ formals }: body@
 tryNamedSetLambda :: Parser Expr
@@ -79,7 +79,7 @@
   expect TokLBrace
   (formals, hasEllipsis) <- parseFormalsBody
   expect TokColon
-  ELambda (FormalNamedSet name formals hasEllipsis) <$> parseExpr
+  (\body -> ELambda (FormalNamedSet name formals hasEllipsis) body NoCaptureInfo) <$> parseExpr
 
 -- | Brace at start: could be @{ formals }: body@, @{ formals }\@name: body@, or attr set.
 -- Falls back to parsing as attr set (via implication) if lambda parse fails.
@@ -98,12 +98,12 @@
   case tok of
     TokColon -> do
       _ <- advance
-      ELambda (FormalSet formals hasEllipsis) <$> parseExpr
+      (\body -> ELambda (FormalSet formals hasEllipsis) body NoCaptureInfo) <$> parseExpr
     TokAt -> do
       _ <- advance
       name <- expectIdent
       expect TokColon
-      ELambda (FormalNamedSet name formals hasEllipsis) <$> parseExpr
+      (\body -> ELambda (FormalNamedSet name formals hasEllipsis) body NoCaptureInfo) <$> parseExpr
     _ -> parseError ("expected ':' or '@' after formals, got " <> showToken tok)
 
 -- | Parse the inside of @{ ... }@ formals (comma-separated, optional defaults,
@@ -457,7 +457,7 @@
 
 parseAttrSet :: Bool -> Parser Expr
 parseAttrSet isRec =
-  EAttrs isRec <$> parseBindings
+  (\bs -> EAttrs isRec bs NoCaptureInfo) <$> parseBindings
 
 parseBindings :: Parser [Binding]
 parseBindings = go []
@@ -559,7 +559,7 @@
   expect TokLet
   bindings <- parseLetBindings
   expect TokIn
-  ELet bindings <$> parseExpr
+  (\body -> ELet bindings body NoCaptureInfo) <$> parseExpr
 
 parseLetBindings :: Parser [Binding]
 parseLetBindings = go []
diff --git a/src/Nix/Store/Path.hs b/src/Nix/Store/Path.hs
--- a/src/Nix/Store/Path.hs
+++ b/src/Nix/Store/Path.hs
@@ -33,6 +33,7 @@
     StoreDir (..),
     defaultStoreDir,
     defaultStoreDirText,
+    platformStoreDirText,
     windowsStoreDir,
 
     -- * Store paths
@@ -49,6 +50,7 @@
 import Data.Text (Text)
 import qualified Data.Text as T
 import System.FilePath ((</>))
+import qualified System.Info
 
 -- | The base directory of the Nix store.
 newtype StoreDir = StoreDir {unStoreDir :: FilePath}
@@ -61,6 +63,16 @@
 -- | Default store directory as 'Text', for use in the evaluator.
 defaultStoreDirText :: Text
 defaultStoreDirText = T.pack (unStoreDir defaultStoreDir)
+
+-- | Platform-appropriate store directory as 'Text'.
+-- Returns @C:\\nix\\store@ on Windows, @\/nix\/store@ on Unix.
+-- Used for user-facing values like @builtins.storeDir@.
+platformStoreDirText :: Text
+platformStoreDirText = T.pack (unStoreDir platformStoreDir)
+  where
+    platformStoreDir = case System.Info.os of
+      "mingw32" -> windowsStoreDir
+      _ -> defaultStoreDir
 
 -- | Default store directory on Windows: @C:\\nix\\store@.
 windowsStoreDir :: StoreDir
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -24,7 +24,7 @@
 import Nix.Parser.Lexer (Located (..), Token (..), tokenize)
 import Nix.Store (Store (..), addToStore, closeStore, isValid, openStore, pathExists, scanReferences, setReadOnly, writeDrv)
 import Nix.Store.DB (PathInfo (..), PathRegistration (..), closeStoreDB, isValidPath, openStoreDB, queryDeriver, queryPathInfo, queryReferences, registerPath)
-import Nix.Store.Path (StoreDir (..), StorePath (..), defaultStoreDir, parseStorePath, storePathToFilePath, storePathToText, windowsStoreDir)
+import Nix.Store.Path (StoreDir (..), StorePath (..), defaultStoreDir, parseStorePath, platformStoreDirText, storePathToFilePath, storePathToText, windowsStoreDir)
 import qualified Nix.Substituter as Subst
 import System.Directory (createDirectoryIfMissing, doesDirectoryExist, getPermissions, getTemporaryDirectory, removeDirectoryRecursive, writable)
 import qualified System.Directory as Dir
@@ -156,13 +156,13 @@
         let expr = EBinary OpAdd (ELit (NixInt 1)) (ELit (NixInt 2))
          in assertEqual "EBinary" expr expr,
       runTest "lambda" $
-        let expr = ELambda (FormalName "x") (EVar "x")
+        let expr = ELambda (FormalName "x") (EVar "x") NoCaptureInfo
          in assertEqual "ELambda" expr expr,
       runTest "let binding" $
-        let expr = ELet [NamedBinding [StaticKey "x"] (ELit (NixInt 1))] (EVar "x")
+        let expr = ELet [NamedBinding [StaticKey "x"] (ELit (NixInt 1))] (EVar "x") NoCaptureInfo
          in assertEqual "ELet" expr expr,
       runTest "attrs" $
-        let expr = EAttrs False [NamedBinding [StaticKey "a"] (ELit (NixInt 1))]
+        let expr = EAttrs False [NamedBinding [StaticKey "a"] (ELit (NixInt 1))] NoCaptureInfo
          in assertEqual "EAttrs" expr expr,
       runTest "if-then-else" $
         let expr = EIf (ELit (NixBool True)) (ELit (NixInt 1)) (ELit (NixInt 2))
@@ -447,7 +447,58 @@
     [ runTest "with basic" $
         assertEval "with" "with { a = 1; }; a" (VInt 1),
       runTest "with lexical wins" $
-        assertEval "lexical" "let a = 1; in with { a = 2; }; a" (VInt 1)
+        assertEval "lexical" "let a = 1; in with { a = 2; }; a" (VInt 1),
+      -- EWithVar: with-scoped variable inside lambda (closure trimming must preserve with-scopes)
+      runTest "with inside lambda" $
+        assertEval
+          "with-lambda"
+          "with { a = 1; }; let f = x: a + x; in f 2"
+          (VInt 3),
+      -- Nested with: inner with wins
+      runTest "nested with inner wins" $
+        assertEval
+          "nested-with"
+          "with { a = 1; }; with { a = 2; }; a"
+          (VInt 2),
+      -- Let shadows with
+      runTest "let shadows with" $
+        assertEval
+          "let-shadows-with"
+          "with { a = 1; }; let a = 2; in a"
+          (VInt 2),
+      -- Builtin fallback: builtins still accessible inside with
+      runTest "with builtin fallback" $
+        assertEval
+          "with-builtin"
+          "with { a = 1; }; true"
+          (VBool True),
+      -- Builtin attr fallback: builtins.length still works inside with
+      runTest "with builtin attr fallback" $
+        assertEval
+          "with-builtin-attr"
+          "with { a = 1; }; builtins.length [1 2 3]"
+          (VInt 3),
+      -- With in rec attrs
+      runTest "with in rec attrs" $
+        assertEval
+          "with-rec"
+          "with { a = 1; }; rec { b = a + 1; c = b + 1; }.c"
+          (VInt 3),
+      -- AST test: parse "with a; b" produces EWithVar
+      runTest "parse with produces EWithVar" $
+        assertRight "with-ast" (parseNix "<test>" "with a; b") $ \case
+          EWith (EVar "a") (EWithVar "b") -> Pass
+          other -> Fail ("expected EWith (EVar a) (EWithVar b), got: " <> T.pack (show other)),
+      -- AST test: formal wins over with
+      runTest "parse lambda formal wins over with" $
+        assertRight "formal-wins" (parseNix "<test>" "x: with a; x") $ \case
+          ELambda _ (EWith _ (EResolvedVar 0 0)) _ -> Pass
+          other -> Fail ("expected formal to win, got: " <> T.pack (show other)),
+      -- Trimming test: lambda inside with gets CapturesWithScopes
+      runTest "with lambda trimmed with CapturesWithScopes" $
+        assertRight "with-trim" (parseNix "<test>" "with a; x: b + x") $ \case
+          EWith _ (ELambda _ _ (CapturesWithScopes _)) -> Pass
+          other -> Fail ("expected CapturesWithScopes, got: " <> T.pack (show other))
     ]
 
 -- ---------------------------------------------------------------------------
@@ -807,7 +858,7 @@
       -- FormalName "x" → slot 0; FormalSet [a,b] → a=0, b=1;
       -- FormalNamedSet "args" [a] → args=0, a=1.
       runTest "parse simple lambda" $
-        assertParse "lambda" "x: x" (ELambda (FormalName "x") (EResolvedVar 0 0)),
+        assertParse "lambda" "x: x" (ELambda (FormalName "x") (EResolvedVar 0 0) NoCaptureInfo),
       runTest "parse set pattern lambda" $
         assertParse
           "set pattern"
@@ -815,6 +866,7 @@
           ( ELambda
               (FormalSet [Formal "a" Nothing, Formal "b" Nothing] False)
               (EResolvedVar 0 0)
+              NoCaptureInfo
           ),
       runTest "parse set pattern with defaults" $
         assertParse
@@ -823,6 +875,7 @@
           ( ELambda
               (FormalSet [Formal "a" (Just (ELit (NixInt 1)))] False)
               (EResolvedVar 0 0)
+              NoCaptureInfo
           ),
       runTest "parse set pattern with ellipsis" $
         assertParse
@@ -831,6 +884,7 @@
           ( ELambda
               (FormalSet [Formal "a" Nothing] True)
               (EResolvedVar 0 0)
+              NoCaptureInfo
           ),
       runTest "parse named set pattern (name@{...})" $
         assertParse
@@ -839,6 +893,7 @@
           ( ELambda
               (FormalNamedSet "args" [Formal "a" Nothing] False)
               (EResolvedVar 0 1)
+              NoCaptureInfo
           ),
       runTest "parse named set pattern ({...}@name)" $
         assertParse
@@ -847,6 +902,7 @@
           ( ELambda
               (FormalNamedSet "args" [Formal "a" Nothing] False)
               (EResolvedVar 0 1)
+              NoCaptureInfo
           ),
       -- Application
       runTest "parse application" $
@@ -875,35 +931,35 @@
         assertParse "has-attr" "a ? b" (EHasAttr (EVar "a") [StaticKey "b"]),
       -- Attr sets
       runTest "parse empty attrs" $
-        assertParse "empty attrs" "{ }" (EAttrs False []),
+        assertParse "empty attrs" "{ }" (EAttrs False [] NoCaptureInfo),
       runTest "parse attrs with binding" $
         assertParse
           "attrs"
           "{ a = 1; }"
-          (EAttrs False [NamedBinding [StaticKey "a"] (ELit (NixInt 1))]),
+          (EAttrs False [NamedBinding [StaticKey "a"] (ELit (NixInt 1))] NoCaptureInfo),
       runTest "parse rec attrs" $
         assertParse
           "rec attrs"
           "rec { a = 1; }"
-          (EAttrs True [NamedBinding [StaticKey "a"] (ELit (NixInt 1))]),
+          (EAttrs True [NamedBinding [StaticKey "a"] (ELit (NixInt 1))] NoCaptureInfo),
       -- inherit x y; is desugared to x = x; y = y; by the resolution pass
       -- (needed because lambda formals are positional, not name-based).
       runTest "parse inherit" $
         assertParse
           "inherit"
           "{ inherit x y; }"
-          (EAttrs False [NamedBinding [StaticKey "x"] (EVar "x"), NamedBinding [StaticKey "y"] (EVar "y")]),
+          (EAttrs False [NamedBinding [StaticKey "x"] (EVar "x"), NamedBinding [StaticKey "y"] (EVar "y")] NoCaptureInfo),
       runTest "parse inherit from" $
         assertParse
           "inherit from"
           "{ inherit (a) x; }"
-          (EAttrs False [Inherit (Just (EVar "a")) ["x"]]),
+          (EAttrs False [Inherit (Just (EVar "a")) ["x"]] NoCaptureInfo),
       -- Let/if/with/assert
       runTest "parse let" $
         assertParse
           "let"
           "let x = 1; in x"
-          (ELet [NamedBinding [StaticKey "x"] (ELit (NixInt 1))] (EVar "x")),
+          (ELet [NamedBinding [StaticKey "x"] (ELit (NixInt 1))] (EResolvedVar 0 0) NoCaptureInfo),
       runTest "parse if-then-else" $
         assertParse
           "if"
@@ -913,7 +969,7 @@
         assertParse
           "with"
           "with a; b"
-          (EWith (EVar "a") (EVar "b")),
+          (EWith (EVar "a") (EWithVar "b")),
       runTest "parse assert" $
         assertParse
           "assert"
@@ -938,7 +994,7 @@
         assertParse
           "or attr key"
           "{ or = 1; }"
-          (EAttrs False [NamedBinding [StaticKey "or"] (ELit (NixInt 1))])
+          (EAttrs False [NamedBinding [StaticKey "or"] (ELit (NixInt 1))] NoCaptureInfo)
     ]
 
 -- ---------------------------------------------------------------------------
@@ -985,7 +1041,8 @@
               [ NamedBinding [StaticKey "x"] (ELit (NixInt 1)),
                 NamedBinding [StaticKey "y"] (ELit (NixInt 2))
               ]
-              (EBinary OpAdd (EVar "x") (EVar "y"))
+              (EBinary OpAdd (EResolvedVar 0 0) (EResolvedVar 0 1))
+              NoCaptureInfo
           ),
       runTest "nested attr set" $
         assertParse
@@ -994,13 +1051,36 @@
           ( EAttrs
               False
               [ NamedBinding [StaticKey "a", StaticKey "b", StaticKey "c"] (ELit (NixInt 1)),
-                NamedBinding [StaticKey "d"] (EAttrs False [NamedBinding [StaticKey "e"] (ELit (NixInt 2))])
+                NamedBinding [StaticKey "d"] (EAttrs False [NamedBinding [StaticKey "e"] (ELit (NixInt 2))] NoCaptureInfo)
               ]
+              NoCaptureInfo
           ),
       runTest "indented string" $
         assertRight "ind string" (parseNix "<test>" "''hello''") $ \case
           EIndStr _ -> Pass
-          other -> Fail ("expected EIndStr, got: " <> T.pack (show other))
+          other -> Fail ("expected EIndStr, got: " <> T.pack (show other)),
+      -- Positional let/rec resolution tests
+      runTest "let inherit from outer lambda" $
+        assertRight "let-inherit-lambda" (parseNix "<test>" "x: let inherit x; in x") $ \case
+          -- x: let inherit x; in x
+          -- The lambda formal x is at level 0, index 0.
+          -- The let scope is level 0 (for the let body).
+          -- inherit x desugars to x = x where RHS resolves against outer
+          -- (the lambda scope), so the let binding's RHS is EResolvedVar 0 0
+          -- (one level up from the let to the lambda).
+          -- The body x resolves to level 0, index 0 (the let scope).
+          ELambda _ (ELet [NamedBinding [StaticKey "x"] _rhsExpr] (EResolvedVar 0 0) _) _ -> Pass
+          other -> Fail ("expected ELambda with let-inherit, got: " <> T.pack (show other)),
+      runTest "nested lambda in let" $
+        assertEval
+          "let-nested-lambda"
+          "let f = x: x + 1; g = y: f y; in g 5"
+          (VInt 6),
+      runTest "rec attrs positional resolution" $
+        assertEval
+          "rec-positional"
+          "let s = rec { a = 1; b = a + 1; }; in s.b"
+          (VInt 2)
     ]
 
 -- ---------------------------------------------------------------------------
@@ -1074,7 +1154,7 @@
         assertEval "lt-str" "builtins.lessThan \"a\" \"b\"" (VBool True),
       -- Constants
       runTest "storeDir" $
-        assertEval "storeDir" "builtins.storeDir" (mkStr "/nix/store"),
+        assertEval "storeDir" "builtins.storeDir" (mkStr platformStoreDirText),
       runTest "nixVersion" $
         assertEval "nixVersion" "builtins.nixVersion" (mkStr "2.24.0"),
       runTest "langVersion" $
@@ -2751,6 +2831,22 @@
              in if names == ["out", "dev"]
                   then Pass
                   else Fail ("output names: " <> T.pack (show names))
+          _ -> Fail ("expected VDerivation, got " <> T.pack (show val)),
+      -- builtinDerivation populates drvEnv with drvPath and output paths
+      runTest "builtinDerivation populates drvEnv"
+        $ assertRight
+          "drvEnv"
+          (evalNix "let d = derivation { name = \"test\"; system = \"x86_64-linux\"; builder = \"/bin/sh\"; }; in d._derivation")
+        $ \val -> case val of
+          VDerivation drv
+            | Just dp <- Map.lookup "drvPath" (drvEnv drv),
+              "/nix/store/" `T.isPrefixOf` dp,
+              ".drv" `T.isSuffixOf` dp,
+              Just op <- Map.lookup "out" (drvEnv drv),
+              "/nix/store/" `T.isPrefixOf` op ->
+                Pass
+            | otherwise ->
+                Fail ("drvEnv keys: " <> T.pack (show (Map.toList (drvEnv drv))))
           _ -> Fail ("expected VDerivation, got " <> T.pack (show val))
     ]
 
