diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,22 @@
 # Changelog
 
+## 0.1.3.0 — 2026-02-25
+
+### nixpkgs Compatibility: Module System, Callable Sets, Parser Fixes
+
+- **`__functor` support** — Attribute sets with `__functor` are now callable, matching real Nix. `(set.__functor set) arg` dispatch enables nixpkgs patterns like `lib.makeOverridable` and `lib.setFunctionArgs`.
+- **`builtins.setFunctionArgs`** — Wraps a function in a callable attrset with `__functor` and `__functionArgs` metadata. Used by `lib.mirrorFunctionArgs`, `lib.makeOverridable`, and the entire nixpkgs override system.
+- **Null dynamic keys** — `{ ${null} = val; }` now skips the binding instead of erroring (matching real Nix). Used extensively by the nixpkgs module system for conditional attributes like `${if cond then "key" else null}`.
+- **Indented string interpolation fix** — `''${expr}''` now parses correctly. The lexer had an overzealous guard (`isIndStringEscape`) that blocked `''` from opening an indented string when followed by `$`. This broke `lib.generators` and many nixpkgs files.
+- **`splitVersion` fix** — Dots are separators, not components. `splitVersion "1.2.3"` now returns `["1" "2" "3"]` (was `["1" "." "2" "." "3"]`). Fixes `lib.versions.majorMinor` and all version comparison logic.
+- **`builtins.functionArgs` on callable sets** — Now inspects `__functionArgs` metadata on attrsets produced by `setFunctionArgs`, enabling `lib.functionArgs` to work on overridable functions.
+- **CLI eval sub-parser** — `--strict` and `--nix-path` flags now work after `eval` command (e.g. `nova-nix eval --strict --expr '...'`). Previously only worked before the command.
+- **Bundled `<nix/fetchurl.nix>`** — Ships as a Cabal data-file, added to search paths automatically. nixpkgs stdenv bootstrap imports this file; no system Nix install needed.
+- **nixpkgs module system working** — `lib.evalModules`, `lib.mkOption`, `lib.mkIf`, `lib.types.*` all evaluate correctly.
+- **nixpkgs functional patterns working** — `lib.makeOverridable`, `lib.makeExtensible`, `lib.fix`, `lib.generators.toKeyValue` all correct.
+- **`lib.systems.elaborate` instant** — Was taking minutes before lazy builtins; now returns immediately for any system string.
+- 101 builtins (up from 91), 511 tests
+
 ## 0.1.2.0 — 2026-02-24
 
 ### Lazy Builtins + Correctness Fixes
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
 
 - **Parser** — Hand-rolled recursive descent parser for the full Nix expression language. 13 precedence levels, 17 AST constructors, all syntax forms including search paths (`<nixpkgs>`) and dynamic attribute keys (`{ ${expr} = val; }`). Direct `Text` consumption for maximum throughput.
 - **Lazy Evaluator** — Thunk-based evaluation with environment closures, knot-tying for recursive bindings via Haskell laziness. All 17 AST constructors handled: literals, strings with interpolation, attribute sets (recursive and non-recursive), let bindings, lambdas with formal parameters, if/then/else, with, assert, unary/binary operators, function application, list construction, attribute selection, has-attribute checks, and search path resolution.
-- **91 Built-in Functions** — Type checks, arithmetic, bitwise, strings, lists, attribute sets, higher-order (`map`, `filter`, `foldl'`, `sort`, `genList`, `concatMap`), JSON (`toJSON`/`fromJSON`), hashing (SHA-256/SHA-512/SHA-1/MD5), version parsing, `replaceStrings`, `tryEval`, `deepSeq`, `genericClosure`, string context introspection (`hasContext`, `getContext`, `appendContext`), IO builtins (`import`, `readFile`, `pathExists`, `readDir`, `getEnv`, `toPath`, `toFile`, `findFile`, `scopedImport`, `fetchurl`, `fetchTarball`, `fetchGit`), `derivation`, `placeholder`, `storePath`, and more. 16 builtins available at top level without `builtins.` prefix (`toString`, `map`, `throw`, `import`, `derivation`, `abort`, `baseNameOf`, `dirOf`, `isNull`, `removeAttrs`, `placeholder`, `scopedImport`, `fetchTarball`, `fetchGit`, `fetchurl`, `toFile`) — matching the real Nix language spec.
+- **101 Built-in Functions** — Type checks, arithmetic, bitwise, strings, lists, attribute sets, higher-order (`map`, `filter`, `foldl'`, `sort`, `genList`, `concatMap`, `mapAttrs`), JSON (`toJSON`/`fromJSON`), hashing (SHA-256/SHA-512/SHA-1/MD5), version parsing, `replaceStrings`, `tryEval`, `deepSeq`, `genericClosure`, `setFunctionArgs`/`functionArgs`, string context introspection (`hasContext`, `getContext`, `appendContext`), IO builtins (`import`, `readFile`, `pathExists`, `readDir`, `getEnv`, `toPath`, `toFile`, `findFile`, `scopedImport`, `fetchurl`, `fetchTarball`, `fetchGit`), `derivation`, `placeholder`, `storePath`, and more. 16 builtins available at top level without `builtins.` prefix (`toString`, `map`, `throw`, `import`, `derivation`, `abort`, `baseNameOf`, `dirOf`, `isNull`, `removeAttrs`, `placeholder`, `scopedImport`, `fetchTarball`, `fetchGit`, `fetchurl`, `toFile`) — matching the real Nix language spec.
 - **Search Path Resolution** — `<nixpkgs>` desugars to `builtins.findFile builtins.nixPath "nixpkgs"` — matching real Nix semantics. `NIX_PATH` environment variable is parsed at startup, and `--nix-path` CLI flags merge with it. Directory imports (`import ./dir`) resolve to `dir/default.nix` automatically.
 - **Dynamic Attribute Keys** — `{ ${expr} = val; }` fully supported in all contexts: non-recursive attrs, recursive attrs, let bindings, attribute selection, and has-attribute checks. Key resolution is cleanly separated from value thunk construction to preserve knot-tying in recursive bindings.
 - **String Context Tracking** — Every string carries invisible metadata tracking which store paths it references. Context propagates through interpolation, concatenation, `replaceStrings`, and all string operations. The `derivation` builtin collects contexts into `drvInputDrvs` and `drvInputSrcs` — matching real Nix semantics.
@@ -187,13 +187,13 @@
 
 | Module | Purpose | Status |
 |--------|---------|--------|
-| `Nix.Eval` | Lazy evaluator — all 17 AST constructors, thunk forcing, env operations, 91-builtin dispatch, search path resolution, dynamic attribute keys. Polymorphic via `MonadEval` | Done |
+| `Nix.Eval` | Lazy evaluator — all 17 AST constructors, thunk forcing, env operations, 101-builtin dispatch, `__functor` callable sets, search path resolution, dynamic attribute keys. Polymorphic via `MonadEval` | Done |
 | `Nix.Eval.Types` | Shared types — `NixValue` (11 constructors), `Thunk` (lazy env for knot-tying), `Env` (lexical + with-scope chain), `StringContext` (store path tracking), `MonadEval` typeclass, `PureEval` runner | Done |
 | `Nix.Eval.Operator` | Binary/unary operators — arithmetic with float promotion, deep structural equality, division-by-zero checks | Done |
 | `Nix.Eval.StringInterp` | String interpolation — value coercion with context propagation, indented string whitespace stripping | Done |
 | `Nix.Eval.Context` | String context construction, queries, extraction — pure helpers for building and inspecting store path references | Done |
 | `Nix.Eval.IO` | IO evaluation monad — real filesystem access, import cache (with directory import), process execution, store writes, NIX_PATH parsing, per-thunk IORef memoization (matching real Nix in-place mutation) | Done |
-| `Nix.Builtins` | Built-in function environment — 91 builtins, search path plumbing (`parseNixPath`), top-level builtin exposure | Done |
+| `Nix.Builtins` | Built-in function environment — 101 builtins, search path plumbing (`parseNixPath`), top-level builtin exposure | Done |
 
 ### Store + Builder
 
@@ -256,7 +256,7 @@
 **Key numbers:**
 
 - **22 modules** — all implemented
-- **508 tests** — hand-rolled harness, no framework dependencies
+- **511 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)
 
@@ -289,7 +289,7 @@
 - [x] **Lexer** — Full Nix tokenization (integers, floats, strings with interpolation, paths, URIs, search paths, operators, keywords)
 - [x] **Parser** — 13 precedence levels, all Nix syntax, structured error reporting
 - [x] **Evaluator** — All 17 AST constructors, lazy thunks, recursive let/rec via knot-tying, with-scope chain, dynamic attribute keys
-- [x] **91 builtins** — Type checks, arithmetic, bitwise, strings, lists, attrsets, higher-order, JSON, hashing, regex (`match`/`split` via `regex-tdfa`), version parsing, tryEval, deepSeq, genericClosure, string context introspection, all IO builtins, derivation
+- [x] **101 builtins** — Type checks, arithmetic, bitwise, strings, lists, attrsets, higher-order, JSON, hashing, regex (`match`/`split` via `regex-tdfa`), version parsing, `setFunctionArgs`/`functionArgs`, tryEval, deepSeq, genericClosure, string context introspection, all IO builtins, derivation
 - [x] **MonadEval refactor** — Evaluator polymorphic in effect monad (`PureEval` for tests, `EvalIO` for real evaluation)
 - [x] **IO builtins** — `import` (with directory import support), `readFile`, `pathExists`, `readDir`, `getEnv`, `toPath`, `toFile`, `findFile`, `scopedImport`, `fetchurl`, `fetchTarball`, `fetchGit`, `currentTime`
 - [x] **`derivation`** — Attrset to `.drv` build recipe with computed `drvPath` and `outPath`, context-aware input population
@@ -306,20 +306,26 @@
 - [x] **CLI** — `nova-nix eval FILE.nix`, `nova-nix eval --expr 'EXPR'`, `nova-nix build FILE.nix`, `--nix-path NAME=PATH`
 - [x] **Thunk memoization** — Per-thunk `IORef` memo cells matching real Nix in-place mutation. GC reclaims dead thunks naturally.
 - [x] **Regex builtins** — `builtins.match` and `builtins.split` (POSIX ERE via `regex-tdfa`, pure Haskell, cross-platform)
-- [x] **508 tests** — parser, evaluator, store, builder, substituter, dependency graph, search paths, dynamic keys, directory imports, CLI end-to-end
+- [x] **Callable attribute sets** — `__functor` dispatch: sets with `__functor` are callable, enabling `lib.makeOverridable`, `lib.makeExtensible`, and the nixpkgs override system
+- [x] **Lazy builtins** — `map`, `genList`, `mapAttrs` return deferred thunks (O(1) until demanded). `concatMap` is semi-lazy. Critical for nixpkgs which maps over 80,000+ packages.
+- [x] **Null dynamic keys** — `{ ${null} = val; }` skips the binding, used by the nixpkgs module system for conditional attributes
+- [x] **Bundled `<nix/fetchurl.nix>`** — Ships as Cabal data-file for nixpkgs stdenv bootstrap
+- [x] **nixpkgs module system** — `lib.evalModules`, `lib.mkOption`, `lib.mkIf`, `lib.types.*` all working
+- [x] **nixpkgs lib fully working** — trivial, strings, lists, attrsets, systems, generators, functional patterns (`fix`, `makeOverridable`, `makeExtensible`)
+- [x] **511 tests** — parser, evaluator, store, builder, substituter, dependency graph, search paths, dynamic keys, directory imports, laziness, CLI end-to-end
 
 ### Next
 
-- [ ] **nixpkgs evaluation** — The ultimate test: `import <nixpkgs> {}` evaluates correctly and fast
-- [ ] **Missing builtins** — Any others nixpkgs demands (discovered iteratively)
-- [ ] **Performance** — Target ~2-5 seconds for full nixpkgs eval
+- [ ] **Full `import <nixpkgs> {}` performance** — nixpkgs lib layer evaluates correctly; stdenv bootstrap runs but needs performance optimization for the full 80,000+ package set
+- [ ] **Remaining builtins** — 13 missing (`fromTOML`, `hashFile`, `readFileType`, `traceVerbose`, `break`, `filterSource`, `outputOf`, and fetch variants)
+- [ ] **`nova-nix shell`** — Enter a development shell (like `nix shell`)
+- [ ] **`nova-nix repl`** — Interactive evaluator
 
 ### Long-Term
 
-- [ ] **`nova-nix shell`** — Enter a development shell (like `nix shell`)
-- [ ] **`nova-nix repl`** — Interactive evaluator
 - [ ] **Nix daemon protocol compatibility**
 - [ ] **XZ decompression** — Enable nova-cache compression flag for real binary cache downloads
+- [ ] **Store bootstrap** — Ship prebuilt bash + coreutils for Windows builds
 
 ---
 
@@ -327,7 +333,7 @@
 
 ```bash
 cabal build                              # Build library + CLI
-cabal test                               # Run all 508 tests
+cabal test                               # Run all 511 tests
 cabal build --ghc-options="-Werror"      # Warnings as errors (CI default)
 cabal haddock                            # Generate API docs
 ```
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -28,6 +28,7 @@
 import Nix.Parser (parseNix)
 import Nix.Store (Store, closeStore, openStore, writeDrv)
 import Nix.Store.Path (StorePath, defaultStoreDir, parseStorePath, storePathToFilePath)
+import Paths_nova_nix (getDataDir)
 import System.Directory (getCurrentDirectory, getTemporaryDirectory)
 import System.Environment (getArgs)
 import System.Exit (exitFailure)
@@ -59,18 +60,25 @@
       go (opts {optNixPaths = optNixPaths opts ++ [T.pack val]}) rest
     go opts ("--strict" : rest) =
       go (opts {optStrict = True}) rest
-    go opts ("eval" : "--expr" : expr : rest) =
-      go (opts {optCommand = CmdEvalExpr (T.pack expr)}) rest
-    go opts ("eval" : path : rest) =
-      go (opts {optCommand = CmdEvalFile path}) rest
+    go opts ("eval" : rest) = goEval opts rest
     go opts ("build" : path : rest) =
       go (opts {optCommand = CmdBuild path}) rest
     go opts _ = opts
+    -- Sub-parser for eval: handles --strict and --expr interleaved with the file arg.
+    goEval opts [] = opts
+    goEval opts ("--strict" : rest) = goEval (opts {optStrict = True}) rest
+    goEval opts ("--nix-path" : val : rest) =
+      goEval (opts {optNixPaths = optNixPaths opts ++ [T.pack val]}) rest
+    goEval opts ("--expr" : expr : rest) =
+      go (opts {optCommand = CmdEvalExpr (T.pack expr)}) rest
+    goEval opts (path : rest) =
+      go (opts {optCommand = CmdEvalFile path}) rest
 
--- | Merge --nix-path entries with the search paths from NIX_PATH.
-mergeSearchPaths :: [T.Text] -> [Thunk] -> [Thunk]
-mergeSearchPaths extraPaths envPaths =
-  concatMap parseNixPath extraPaths ++ envPaths
+-- | Merge --nix-path entries, bundled data dir, and NIX_PATH search paths.
+-- The data dir is appended last so user paths take priority.
+mergeSearchPaths :: [T.Text] -> FilePath -> [Thunk] -> [Thunk]
+mergeSearchPaths extraPaths dataDir envPaths =
+  concatMap parseNixPath extraPaths ++ envPaths ++ parseNixPath (T.pack dataDir)
 
 -- ---------------------------------------------------------------------------
 -- Main
@@ -80,11 +88,12 @@
 main = do
   hSetBuffering stdout LineBuffering
   args <- getArgs
+  dataDir <- getDataDir
   let opts = parseArgs args
   case optCommand opts of
-    CmdEvalFile filePath -> evalFile (optStrict opts) (optNixPaths opts) filePath
-    CmdEvalExpr expr -> evalExpr (optStrict opts) (optNixPaths opts) expr
-    CmdBuild filePath -> buildFile (optNixPaths opts) filePath
+    CmdEvalFile filePath -> evalFile (optStrict opts) (optNixPaths opts) dataDir filePath
+    CmdEvalExpr expr -> evalExpr (optStrict opts) (optNixPaths opts) dataDir expr
+    CmdBuild filePath -> buildFile (optNixPaths opts) dataDir filePath
     CmdHelp -> do
       hPutStrLn stderr "Usage: nova-nix [--nix-path NAME=PATH] <command>"
       hPutStrLn stderr ""
@@ -99,8 +108,8 @@
       exitFailure
 
 -- | Evaluate a .nix file and print the result.
-evalFile :: Bool -> [T.Text] -> FilePath -> IO ()
-evalFile strict extraPaths filePath = do
+evalFile :: Bool -> [T.Text] -> FilePath -> FilePath -> IO ()
+evalFile strict extraPaths dataDir filePath = do
   source <- TIO.readFile filePath
   case parseNix (T.pack filePath) source of
     Left err -> do
@@ -108,7 +117,7 @@
       exitFailure
     Right expr -> do
       st <- newEvalState (takeDirectory filePath)
-      let searchPaths = mergeSearchPaths extraPaths (esSearchPaths st)
+      let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st)
       result <-
         runEvalIO st $
           eval (builtinEnv (esTimestamp st) searchPaths) expr >>= finalize strict
@@ -119,8 +128,8 @@
         Right forced -> TIO.putStrLn (prettyValue forced)
 
 -- | Evaluate an inline expression and print the result.
-evalExpr :: Bool -> [T.Text] -> T.Text -> IO ()
-evalExpr strict extraPaths source = do
+evalExpr :: Bool -> [T.Text] -> FilePath -> T.Text -> IO ()
+evalExpr strict extraPaths dataDir source = do
   case parseNix "<expr>" source of
     Left err -> do
       hPutStrLn stderr ("parse error: " ++ show err)
@@ -128,7 +137,7 @@
     Right expr -> do
       cwd <- getCurrentDirectory
       st <- newEvalState cwd
-      let searchPaths = mergeSearchPaths extraPaths (esSearchPaths st)
+      let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st)
       result <-
         runEvalIO st $
           eval (builtinEnv (esTimestamp st) searchPaths) expr >>= finalize strict
@@ -139,8 +148,8 @@
         Right forced -> TIO.putStrLn (prettyValue forced)
 
 -- | Parse, evaluate, extract derivation, build, and print result.
-buildFile :: [T.Text] -> FilePath -> IO ()
-buildFile extraPaths filePath = do
+buildFile :: [T.Text] -> FilePath -> FilePath -> IO ()
+buildFile extraPaths dataDir filePath = do
   source <- TIO.readFile filePath
   case parseNix (T.pack filePath) source of
     Left err -> do
@@ -148,7 +157,7 @@
       exitFailure
     Right expr -> do
       st <- newEvalState (takeDirectory filePath)
-      let searchPaths = mergeSearchPaths extraPaths (esSearchPaths st)
+      let searchPaths = mergeSearchPaths extraPaths dataDir (esSearchPaths st)
       result <- runEvalIO st (eval (builtinEnv (esTimestamp st) searchPaths) expr)
       case result of
         Left err -> do
diff --git a/data/nix/fetchurl.nix b/data/nix/fetchurl.nix
new file mode 100644
--- /dev/null
+++ b/data/nix/fetchurl.nix
@@ -0,0 +1,74 @@
+# Nix built-in fetchurl — creates a fixed-output derivation that downloads a URL.
+# This file is normally shipped with the Nix daemon as <nix/fetchurl.nix>.
+# nova-nix bundles it so that nixpkgs bootstrap works without a system Nix install.
+{
+  system ? "", # obsolete
+  url,
+  hash ? "", # an SRI hash
+
+  # Legacy hash specification
+  md5 ? "",
+  sha1 ? "",
+  sha256 ? "",
+  sha512 ? "",
+  outputHash ?
+    if hash != "" then
+      hash
+    else if sha512 != "" then
+      sha512
+    else if sha1 != "" then
+      sha1
+    else if md5 != "" then
+      md5
+    else
+      sha256,
+  outputHashAlgo ?
+    if hash != "" then
+      ""
+    else if sha512 != "" then
+      "sha512"
+    else if sha1 != "" then
+      "sha1"
+    else if md5 != "" then
+      "md5"
+    else
+      "sha256",
+
+  executable ? false,
+  unpack ? false,
+  name ? baseNameOf (toString url),
+  impure ? false,
+}:
+
+derivation (
+  {
+    builder = "builtin:fetchurl";
+
+    # New-style output content requirements.
+    outputHashMode = if unpack || executable then "recursive" else "flat";
+
+    inherit
+      name
+      url
+      executable
+      unpack
+      ;
+
+    system = "builtin";
+
+    # No need to double the amount of network traffic
+    preferLocalBuild = true;
+
+    impureEnvVars = [
+      "http_proxy"
+      "https_proxy"
+      "ftp_proxy"
+      "all_proxy"
+      "no_proxy"
+    ];
+
+    # To make "nix-prefetch-url" work.
+    urls = [ url ];
+  }
+  // (if impure then { __impure = true; } else { inherit outputHashAlgo outputHash; })
+)
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.2.0
+version:            0.1.3.0
 synopsis:           Windows-native Nix implementation in pure Haskell
 description:
   A pure Haskell implementation of the Nix package manager that runs natively
@@ -24,6 +24,9 @@
 extra-doc-files:
     CHANGELOG.md
     README.md
+data-files:
+    nix/fetchurl.nix
+data-dir: data
 
 -- ---------------------------------------------------------------------------
 -- Library: core modules (parser, evaluator, store, builder, substituter)
@@ -90,6 +93,8 @@
 executable nova-nix
   main-is:          Main.hs
   hs-source-dirs:   app
+  autogen-modules:  Paths_nova_nix
+  other-modules:    Paths_nova_nix
   default-language:  Haskell2010
   default-extensions:
     OverloadedStrings
diff --git a/src/Nix/Eval.hs b/src/Nix/Eval.hs
--- a/src/Nix/Eval.hs
+++ b/src/Nix/Eval.hs
@@ -75,6 +75,7 @@
     Thunk (..),
     emptyContext,
     emptyEnv,
+    envInsert,
     envInsertThunk,
     envLookup,
     evaluated,
@@ -266,15 +267,17 @@
 -- | Resolve all attribute keys in a list of bindings, evaluating
 -- dynamic keys against the given environment.
 resolveBindingKeys :: (MonadEval m) => Env -> [Binding] -> m [ResolvedBinding]
-resolveBindingKeys keyEnv = mapM resolveOne
+resolveBindingKeys keyEnv = fmap catMaybes . mapM resolveOne
   where
     resolveOne (NamedBinding path bodyExpr) = do
       resolvedPath <- mapM (resolveKey keyEnv) path
-      pure (ResolvedNamed resolvedPath bodyExpr)
+      -- If any key segment is null, skip the entire binding
+      -- (matching real Nix; used by the module system for conditional attrs).
+      pure (fmap (`ResolvedNamed` bodyExpr) (sequence resolvedPath))
     resolveOne (Inherit Nothing names) =
-      pure (ResolvedInherit names)
+      pure (Just (ResolvedInherit names))
     resolveOne (Inherit (Just fromExpr) names) =
-      pure (ResolvedInheritFrom fromExpr names)
+      pure (Just (ResolvedInheritFrom fromExpr names))
 
 -- | Build a flat attribute map from pre-resolved bindings (pure).
 -- Thunks capture @thunkEnv@ — suitable for knot-tying.
@@ -324,22 +327,28 @@
 buildNestedAttrM _thunkEnv _keyEnv [] _bodyExpr =
   throwEvalError "empty attribute path"
 buildNestedAttrM thunkEnv keyEnv [key] bodyExpr = do
-  keyText <- resolveKey keyEnv key
-  pure (Map.singleton keyText (mkThunk thunkEnv bodyExpr))
+  resolved <- resolveKey keyEnv key
+  case resolved of
+    Nothing -> pure Map.empty
+    Just keyText -> pure (Map.singleton keyText (mkThunk thunkEnv bodyExpr))
 buildNestedAttrM thunkEnv keyEnv (key : rest) bodyExpr = do
-  keyText <- resolveKey keyEnv key
-  inner <- buildNestedAttrM thunkEnv keyEnv rest bodyExpr
-  pure (Map.singleton keyText (evaluated (VAttrs inner)))
+  resolved <- resolveKey keyEnv key
+  case resolved of
+    Nothing -> pure Map.empty
+    Just keyText -> do
+      inner <- buildNestedAttrM thunkEnv keyEnv rest bodyExpr
+      pure (Map.singleton keyText (evaluated (VAttrs inner)))
 
--- | Resolve an attribute key to its text name.
--- Static keys return immediately; dynamic keys evaluate the expression
--- and coerce the result to a string.
-resolveKey :: (MonadEval m) => Env -> AttrKey -> m Text
-resolveKey _env (StaticKey name) = pure name
+-- | Resolve an attribute key.  Static keys always return 'Just'.
+-- Dynamic keys evaluate to a string or 'null'; null means "skip this binding"
+-- (matching real Nix, used by the module system for conditional attributes).
+resolveKey :: (MonadEval m) => Env -> AttrKey -> m (Maybe Text)
+resolveKey _env (StaticKey name) = pure (Just name)
 resolveKey env (DynamicKey expr) = do
   val <- eval env expr
   case val of
-    VStr s _ -> pure s
+    VStr s _ -> pure (Just s)
+    VNull -> pure Nothing
     _ -> throwEvalError ("dynamic attribute key must be a string, got " <> typeName val)
 
 -- | Merge two attribute maps.  For overlapping keys where both sides
@@ -376,8 +385,8 @@
 walkAttrPath _env [] val = pure (Just val)
 walkAttrPath env (key : rest) val = case val of
   VAttrs attrs -> do
-    keyText <- resolveKey env key
-    case Map.lookup keyText attrs of
+    resolved <- resolveKey env key
+    case resolved >>= (`Map.lookup` attrs) of
       Just thunk -> do
         inner <- force thunk
         walkAttrPath env rest inner
@@ -428,6 +437,16 @@
     VBuiltin name accArgs -> do
       argVal <- eval env argExpr
       applyBuiltin name accArgs argVal
+    -- __functor support: a set with __functor is callable.
+    -- Semantics: (attrs.__functor attrs) arg
+    VAttrs attrs
+      | Just functorThunk <- Map.lookup "__functor" attrs -> do
+          functor <- force functorThunk
+          -- Apply __functor to the set itself, yielding a function
+          partiallyApplied <- applyValue functor funcVal
+          -- Apply the resulting function to the argument
+          argVal <- eval env argExpr
+          applyValue partiallyApplied argVal
     _ -> throwEvalError ("attempt to call " <> typeName funcVal <> ", which is not a function")
 
 -- | Match function formals against an argument thunk.
@@ -617,7 +636,7 @@
       builtin1 "head" builtinHead,
       builtin1 "tail" builtinTail,
       -- String operations (arity 1)
-      builtin1 "toString" (fmap (uncurry VStr) . coerceToString),
+      builtin1 "toString" (fmap (uncurry VStr) . coerceToStringPermissive),
       builtin1 "stringLength" builtinStringLength,
       -- Control (arity 1)
       builtin1 "throw" builtinThrow,
@@ -676,6 +695,7 @@
       -- Attr set higher-order
       builtin2 "mapAttrs" builtinMapAttrs,
       builtin1 "functionArgs" builtinFunctionArgs,
+      builtin2 "setFunctionArgs" builtinSetFunctionArgs,
       builtin2 "zipAttrsWith" builtinZipAttrsWith,
       -- String manipulation
       builtin2 "match" builtinMatch,
@@ -774,7 +794,7 @@
   "head" -> apply1 builtinHead
   "tail" -> apply1 builtinTail
   -- String operations (arity 1)
-  "toString" -> apply1 (fmap (uncurry VStr) . coerceToString)
+  "toString" -> apply1 (fmap (uncurry VStr) . coerceToStringPermissive)
   "stringLength" -> apply1 builtinStringLength
   -- Control (arity 1)
   "throw" -> apply1 builtinThrow
@@ -833,6 +853,7 @@
   -- Attr set higher-order
   "mapAttrs" -> apply2 builtinMapAttrs
   "functionArgs" -> apply1 builtinFunctionArgs
+  "setFunctionArgs" -> apply2 builtinSetFunctionArgs
   "zipAttrsWith" -> apply2 builtinZipAttrsWith
   -- String manipulation
   "match" -> apply2 builtinMatch
@@ -1349,6 +1370,23 @@
           }
    in mkSyntheticThunk env (EApp (EVar "__fn") (EVar "__arg"))
 
+-- | Permissive coercion used by @builtins.toString@.
+--
+-- Like 'coerceToString' but additionally handles lists: elements are
+-- recursively coerced and joined with spaces, matching real Nix semantics.
+-- @toString [1 2 3]@ gives @"1 2 3"@.
+coerceToStringPermissive :: (MonadEval m) => NixValue -> m (Text, StringContext)
+coerceToStringPermissive (VList thunks) = do
+  parts <- mapM coerceThunk thunks
+  let texts = map fst parts
+      ctx = mconcat (map snd parts)
+  pure (T.intercalate " " texts, ctx)
+  where
+    coerceThunk thunk = do
+      val <- force thunk
+      coerceToStringPermissive val
+coerceToStringPermissive other = coerceToString other
+
 -- | The current system platform string.
 currentSystemStr :: Text
 currentSystemStr = case (System.Info.arch, System.Info.os) of
@@ -1626,6 +1664,9 @@
 builtinFunctionArgs :: (MonadEval m) => NixValue -> m NixValue
 builtinFunctionArgs (VLambda _ formals _) = pure (formalsToAttrs formals)
 builtinFunctionArgs (VBuiltin _ _) = pure (VAttrs Map.empty)
+-- Callable sets with __functionArgs metadata (from setFunctionArgs).
+builtinFunctionArgs (VAttrs attrs)
+  | Just faThunk <- Map.lookup "__functionArgs" attrs = force faThunk
 builtinFunctionArgs other =
   throwEvalError ("builtins.functionArgs: expected a function, got " <> typeName other)
 
@@ -1640,6 +1681,26 @@
     Map.fromList
       [(fName f, evaluated (VBool (isJust (fDefault f)))) | f <- formals]
 
+-- | @builtins.setFunctionArgs f args@ — wraps @f@ in a callable attrset
+-- with @__functor@ (so it remains callable) and @__functionArgs@ metadata.
+-- Used by nixpkgs @lib.mirrorFunctionArgs@ / @lib.makeOverridable@.
+--
+-- @__functor@ is @self: f@ — a lambda that ignores @self@ and returns
+-- the original function.  The function is captured in the closure env.
+builtinSetFunctionArgs :: (MonadEval m) => NixValue -> NixValue -> m NixValue
+builtinSetFunctionArgs func (VAttrs argSpec) =
+  let closureEnv = envInsert "__fn" func emptyEnv
+      -- __functor = self: __fn  (ignores self, returns the original function)
+      functorLambda = VLambda closureEnv (FormalName "_self") (EVar "__fn")
+   in pure $
+        VAttrs $
+          Map.fromList
+            [ ("__functor", evaluated functorLambda),
+              ("__functionArgs", evaluated (VAttrs argSpec))
+            ]
+builtinSetFunctionArgs func args =
+  throwEvalError ("builtins.setFunctionArgs: expected a set as second argument, got " <> typeName args <> " (func: " <> typeName func <> ")")
+
 builtinZipAttrsWith :: (MonadEval m) => NixValue -> NixValue -> m NixValue
 builtinZipAttrsWith func (VList thunks) = do
   attrSets <- mapM forceToAttrSet thunks
@@ -1842,7 +1903,7 @@
 splitVersionComponents :: Text -> [Text]
 splitVersionComponents t = case T.uncons t of
   Nothing -> []
-  Just ('.', rest) -> "." : splitVersionComponents rest
+  Just ('.', rest) -> splitVersionComponents rest
   Just (c, _)
     | isDigit c ->
         let (digits, rest) = T.span isDigit t
diff --git a/src/Nix/Eval/Operator.hs b/src/Nix/Eval/Operator.hs
--- a/src/Nix/Eval/Operator.hs
+++ b/src/Nix/Eval/Operator.hs
@@ -17,6 +17,7 @@
   ( MonadEval (..),
     NixValue (..),
     Thunk,
+    thunkSameRef,
     typeName,
   )
 import Nix.Expr.Types (BinaryOp (..), UnaryOp (..))
@@ -167,19 +168,24 @@
 -- | Pairwise equality of two thunk lists (for list comparison).
 listEqual :: (MonadEval m) => Force m -> [Thunk] -> [Thunk] -> m Bool
 listEqual _ [] [] = pure True
-listEqual forceFn (a : as) (b : bs) = do
-  va <- forceFn a
-  vb <- forceFn b
-  eq <- nixEqual forceFn va vb
-  if eq then listEqual forceFn as bs else pure False
+listEqual forceFn (a : as) (b : bs)
+  | thunkSameRef a b = listEqual forceFn as bs
+  | otherwise = do
+      va <- forceFn a
+      vb <- forceFn b
+      eq <- nixEqual forceFn va vb
+      if eq then listEqual forceFn as bs else pure False
 listEqual _ _ _ = pure False
 
 -- | Compare two thunks for equality by forcing both.
+-- Short-circuits on thunk identity (same IORef = same value).
 thunkPairEqual :: (MonadEval m) => Force m -> (Thunk, Thunk) -> m Bool
-thunkPairEqual forceFn (a, b) = do
-  va <- forceFn a
-  vb <- forceFn b
-  nixEqual forceFn va vb
+thunkPairEqual forceFn (a, b)
+  | thunkSameRef a b = pure True
+  | otherwise = do
+      va <- forceFn a
+      vb <- forceFn b
+      nixEqual forceFn va vb
 
 -- ---------------------------------------------------------------------------
 -- List / attrset operators
diff --git a/src/Nix/Eval/StringInterp.hs b/src/Nix/Eval/StringInterp.hs
--- a/src/Nix/Eval/StringInterp.hs
+++ b/src/Nix/Eval/StringInterp.hs
@@ -46,9 +46,9 @@
 
 -- | Coerce a Nix value to a string for interpolation.
 --
--- Nix coercion rules: strings pass through (with context), integers
--- and floats are shown, paths pass through, null becomes the empty
--- string.  Other types (lists, sets, functions) cannot be coerced.
+-- Strict coercion: strings, ints, floats, paths, null, bools.
+-- Lists, sets (without @__toString@/@outPath@), and functions are errors.
+-- Used by string interpolation (@"${...}"@).
 coerceToString :: (MonadEval m) => NixValue -> m (Text, StringContext)
 coerceToString val = case val of
   VStr s ctx -> pure (s, ctx)
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
@@ -28,6 +28,7 @@
     mkThunk,
     mkSyntheticThunk,
     evaluated,
+    thunkSameRef,
 
     -- * Display
     typeName,
@@ -226,6 +227,14 @@
 -- | Wrap an already-computed value as a thunk.
 evaluated :: NixValue -> Thunk
 evaluated = Evaluated
+
+-- | Check if two thunks share the same memoization cell (IORef pointer
+-- equality).  When true, both thunks will always produce the same value,
+-- so deep equality can short-circuit to 'True' without forcing.
+-- Matching real Nix which uses pointer identity for same-thunk comparison.
+thunkSameRef :: Thunk -> Thunk -> Bool
+thunkSameRef (Thunk _ _ ref1) (Thunk _ _ ref2) = ref1 == ref2
+thunkSameRef _ _ = False
 
 -- | Human-readable type name for error messages.
 typeName :: NixValue -> Text
diff --git a/src/Nix/Parser/Lexer.hs b/src/Nix/Parser/Lexer.hs
--- a/src/Nix/Parser/Lexer.hs
+++ b/src/Nix/Parser/Lexer.hs
@@ -161,9 +161,7 @@
                   newSt = advanceCol 1 st {lsInput = rest, lsModes = ModeString : lsModes st}
                in lexLoop newSt (tok : acc)
             '\''
-              | Just '\'' <- safeHead rest,
-                -- make sure it's not ''$ or ''\ or ''' (those are inside indented strings only)
-                not (isIndStringEscape (T.drop 2 (lsInput st))) ->
+              | Just '\'' <- safeHead rest ->
                   let tok = Located (lsLine st) (lsCol st) TokIndStringOpen
                       newSt = advanceCol 2 st {lsInput = T.drop 2 (lsInput st), lsModes = ModeIndString : lsModes st}
                    in lexLoop newSt (tok : acc)
@@ -312,10 +310,10 @@
                       newSt = advanceCol 3 st {lsInput = rest2}
                    in lexIndStringMode newSt (litTok : acc)
                 Just ('$', rest3)
-                  | Just ('{', _) <- T.uncons rest3 ->
+                  | Just ('{', rest4) <- T.uncons rest3 ->
                       -- ''${ → literal ${
                       let litTok = Located (lsLine st) (lsCol st) (TokStringLit "${")
-                          newSt = advanceCol 4 st {lsInput = rest3}
+                          newSt = advanceCol 4 st {lsInput = rest4}
                        in lexIndStringMode newSt (litTok : acc)
                 Just ('\\', rest3) ->
                   -- ''\x → escape sequence
@@ -638,13 +636,6 @@
 canFollowWithDivision TokFalse = True
 canFollowWithDivision TokNull = True
 canFollowWithDivision _ = False
-
-isIndStringEscape :: Text -> Bool
-isIndStringEscape t = case T.uncons t of
-  Just ('\'', _) -> True
-  Just ('$', _) -> True
-  Just ('\\', _) -> True
-  _ -> False
 
 -- ---------------------------------------------------------------------------
 -- Emit helpers
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1189,9 +1189,9 @@
         assertEvalFail "cmpVer-err" "builtins.compareVersions 1 2",
       -- splitVersion
       runTest "splitVersion basic" $
-        assertEval "splitVer" "builtins.splitVersion \"1.2.3\" == [ \"1\" \".\" \"2\" \".\" \"3\" ]" (VBool True),
+        assertEval "splitVer" "builtins.splitVersion \"1.2.3\" == [ \"1\" \"2\" \"3\" ]" (VBool True),
       runTest "splitVersion pre" $
-        assertEval "splitVer-pre" "builtins.splitVersion \"1.2pre\" == [ \"1\" \".\" \"2\" \"pre\" ]" (VBool True),
+        assertEval "splitVer-pre" "builtins.splitVersion \"1.2pre\" == [ \"1\" \"2\" \"pre\" ]" (VBool True),
       runTest "splitVersion type error" $
         assertEvalFail "splitVer-err" "builtins.splitVersion 42",
       -- parseDrvName
