nova-nix-0.4.0.0: src/Nix/Eval.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternSynonyms #-}
-- | Nix expression evaluator.
--
-- Nix evaluation is LAZY. Attribute set members and list elements
-- are stored as thunks and only forced when their value is demanded.
-- Function arguments are likewise thunked — @(x: 1) (throw "boom")@
-- returns @1@ because @x@ is never referenced.
--
-- The evaluator maintains an environment ('Env') that maps variable
-- names to thunks. @let@, @with@, function application, and
-- recursive attribute sets all extend the environment.
module Nix.Eval
( -- * Values (re-exported from Types)
NixValue (..),
CompiledRegex (..),
Thunk (..),
-- * Attribute sets (re-exported from Types)
AttrSet (..),
CAttrSet,
attrSetFromMap,
attrSetLookup,
attrSetKeys,
attrSetToMap,
attrSetToAscList,
attrSetMember,
attrSetElems,
attrSetNull,
attrSetRemoveKeys,
attrSetSize,
-- * String context (re-exported from Types)
StringContextElement (..),
StringContext (..),
emptyContext,
mkStr,
-- * Environment (re-exported from Types)
Env (..),
emptyEnv,
-- * Evaluation monad (re-exported from Types)
MonadEval (..),
PureEval,
runPureEval,
-- * Evaluation
eval,
evalBytecode,
force,
-- * Helpers (for Builtins)
typeName,
evaluated,
readThunkValue,
-- * Builtin registry
BuiltinDef (..),
builtinRegistry,
builtinNames,
-- * Platform
currentSystemStr,
)
where
import Control.Monad (foldM, when, (>=>))
import qualified Crypto.Hash as CH
import qualified Data.Array as Array
import Data.Bits (complement, xor, (.&.), (.|.))
import qualified Data.ByteArray as BA
import qualified Data.ByteString as BS
import Data.Char (chr, digitToInt, isAlpha, isDigit, isHexDigit, isOctDigit, ord)
import Data.IORef (IORef, atomicModifyIORef', newIORef)
import Data.Int (Int64)
import Data.List (find, foldl', sort)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, maybeToList)
import Data.Sequence (Seq (..))
import qualified Data.Sequence as Seq
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Word (Word32, Word8)
import Foreign.Ptr (Ptr, castPtr, nullPtr, ptrToWordPtr, wordPtrToPtr)
import Foreign.Storable (peekElemOff, pokeElemOff)
import Nix.Derivation (Derivation (..), DerivationOutput (..), textToPlatform, toATerm, toATermForHash)
import Nix.Eval.CBytecode (cbcArg1, cbcArg2, cbcArg3, cbcData, cbcFlags, cbcOpcode, cbcShortArg, pattern OpApp, pattern OpAssert, pattern OpAttrs, pattern OpBinary, pattern OpHasAttr, pattern OpIf, pattern OpIndStr, pattern OpLambda, pattern OpLet, pattern OpList, pattern OpLitBool, pattern OpLitFloat, pattern OpLitInt, pattern OpLitNull, pattern OpLitPath, pattern OpLitUri, pattern OpResolvedVar, pattern OpSearchPath, pattern OpSelect, pattern OpStr, pattern OpUnary, pattern OpVar, pattern OpWith, pattern OpWithVar)
import Nix.Eval.CEnv (cenvPushWith)
import Nix.Eval.CList (CList (..), clistGet)
import Nix.Eval.CThunk (CThunkPtr)
import Nix.Eval.Compile (BcAttrKey (..), BcBinding (..), compileExpr, decodeBcBindings, decodeBcCaptureInfo, decodeBcFormals, reassembleDouble, reassembleInt64)
import Nix.Eval.Context (extractInputDrvs, extractInputSrcs, plainContext)
import Nix.Eval.Operator (evalBinary, evalUnary, nixCompare, nixEqual)
import Nix.Eval.StringInterp (coerceToString, formatNixFloat, stripIndentedChunks)
import Nix.Eval.Symbol (Symbol (..), symbolText)
import Nix.Eval.Types
( AttrSet (..),
CAttrSet,
CompiledRegex (..),
Env (..),
EvalFormal (..),
EvalFormals (..),
MonadEval (..),
NixValue (..),
PureEval,
StringContext (..),
StringContextElement (..),
Thunk (..),
allocCSlots,
attrSetElems,
attrSetFromMap,
attrSetKeys,
attrSetLookup,
attrSetMapWithKey,
attrSetMember,
attrSetNull,
attrSetRemoveKeys,
attrSetSize,
attrSetToAscList,
attrSetToMap,
attrSetUnionWith,
buildCAttrSetKeys,
buildCSlots,
cheapThunkBc,
clistFromThunks,
clistLen,
clistThunks,
emptyContext,
emptyEnv,
envFromSlots,
envLookup,
envLookupResolved,
envWithScopesRaw,
evaluated,
fillCAttrSetValues,
fillCSlots,
mkStr,
mkSyntheticThunk,
mkThunk,
mkThunkBc,
newCEnv,
newMinimalEnv,
readThunkValue,
runPureEval,
thunkToCPtr,
typeName,
withScopesForCapture,
)
import Nix.Expr.Types
( AttrKey (..),
BinaryOp (..),
CaptureInfo (..),
Expr (..),
NixAtom (..),
UnaryOp (..),
)
import Nix.Hash (bytesToHexText, hashPlaceholder, hexToBytes, makeFixedOutputPath, makeOutputPath, makeTextPath, sha256Digest, sha256Hex)
import Nix.Store.Path (StorePath (..), defaultStoreDir, defaultStoreDirText, parseStorePath, storePathToFilePath, storePathToText)
import qualified NovaCache.Base32 as Nix32
import qualified NovaCache.Base64 as B64
import System.IO.Unsafe (unsafePerformIO)
import qualified System.Info
import Text.Regex.TDFA (matchAllText)
import qualified Text.Regex.TDFA as RE
-- | Evaluate a Nix expression in an environment.
-- Compiles the expression to bytecode and dispatches to 'evalBytecode'.
eval :: (MonadEval m) => Env -> Expr -> m NixValue
eval env expr =
let bcIdx = unsafePerformIO (compileExpr expr)
in evalBytecode env bcIdx
-- | Force a thunk to a value.
--
-- Delegates to 'forceThunk' which is a 'MonadEval' method — this
-- allows IO evaluators to implement memoization (caching the result
-- after the first force) while pure evaluators simply re-evaluate.
force :: (MonadEval m) => Thunk -> m NixValue
force = forceThunk evalBytecode
-- | Evaluate a bytecode instruction by index.
-- This is the primary evaluator — reads opcodes from the C bytecode
-- store and dispatches. All recursive evaluation goes through this
-- function, never through 'eval' directly.
evalBytecode :: (MonadEval m) => Env -> Word32 -> m NixValue
evalBytecode env bcIdx =
let opcode = unsafePerformIO (cbcOpcode bcIdx)
in case opcode of
OpLitInt ->
let lo = unsafePerformIO (cbcArg1 bcIdx)
hi = unsafePerformIO (cbcArg2 bcIdx)
in pure (VInt (reassembleInt64 lo hi))
OpLitFloat ->
let lo = unsafePerformIO (cbcArg1 bcIdx)
hi = unsafePerformIO (cbcArg2 bcIdx)
in pure (VFloat (reassembleDouble lo hi))
OpLitBool ->
let flag = unsafePerformIO (cbcShortArg bcIdx)
in pure (VBool (flag /= 0))
OpLitNull -> pure VNull
OpLitUri ->
let sym = unsafePerformIO (cbcArg1 bcIdx)
in pure (mkStr (symbolText (Symbol sym)))
OpLitPath ->
let sym = unsafePerformIO (cbcArg1 bcIdx)
in VPath <$> resolvePathLiteral (symbolText (Symbol sym))
OpStr -> evalBcStr env bcIdx
OpIndStr -> evalBcIndStr env bcIdx
OpVar ->
let sym = unsafePerformIO (cbcArg1 bcIdx)
in evalVar env (symbolText (Symbol sym))
OpWithVar ->
let sym = unsafePerformIO (cbcArg1 bcIdx)
name = symbolText (Symbol sym)
in evalWithVar env name
OpResolvedVar ->
let level = fromIntegral (unsafePerformIO (cbcArg1 bcIdx))
idx = fromIntegral (unsafePerformIO (cbcArg2 bcIdx))
in force (envLookupResolved level idx env)
OpAttrs -> evalBcAttrs env bcIdx
OpList -> evalBcList env bcIdx
OpSelect -> evalBcSelect env bcIdx
OpHasAttr -> evalBcHasAttr env bcIdx
OpApp -> evalBcApp env bcIdx
OpLambda -> evalBcLambda env bcIdx
OpLet -> evalBcLet env bcIdx
OpIf ->
let condIdx = unsafePerformIO (cbcArg1 bcIdx)
thenIdx = unsafePerformIO (cbcArg2 bcIdx)
elseIdx = unsafePerformIO (cbcArg3 bcIdx)
in do
condVal <- evalBytecode env condIdx
case condVal of
VBool True -> evalBytecode env thenIdx
VBool False -> evalBytecode env elseIdx
_ -> throwEvalError ("'if' condition must be a Boolean, got " <> typeName condVal)
OpWith ->
let scopeIdx = unsafePerformIO (cbcArg1 bcIdx)
bodyIdx = unsafePerformIO (cbcArg2 bcIdx)
-- Lazy with: defer forcing the scope until a WITH_VAR lookup
-- actually needs it. This is critical for nixpkgs where
-- all-packages.nix uses `with pkgs;` inside a fixpoint —
-- eagerly forcing the scope would blackhole.
scopeThunk = cheapThunkBc env scopeIdx
in evalBytecode (pushLazyWithScope scopeThunk env) bodyIdx
OpAssert ->
let condIdx = unsafePerformIO (cbcArg1 bcIdx)
bodyIdx = unsafePerformIO (cbcArg2 bcIdx)
in do
condVal <- evalBytecode env condIdx
case condVal of
VBool True -> evalBytecode env bodyIdx
VBool False -> throwEvalError "assertion failed"
_ -> throwEvalError ("assertion condition must be a Boolean, got " <> typeName condVal)
OpUnary ->
let flags_ = unsafePerformIO (cbcFlags bcIdx)
operandIdx = unsafePerformIO (cbcArg1 bcIdx)
in do
val <- evalBytecode env operandIdx
evalUnary (decodeUnaryOp flags_) val
OpBinary -> evalBcBinary env bcIdx
OpSearchPath ->
let sym = unsafePerformIO (cbcArg1 bcIdx)
in evalSearchPath env (symbolText (Symbol sym))
_ -> throwEvalError "evalBytecode: unknown opcode"
-- ---------------------------------------------------------------------------
-- Bytecode helpers
-- ---------------------------------------------------------------------------
-- | Decode a UnaryOp from bytecode flags.
decodeUnaryOp :: Word8 -> UnaryOp
decodeUnaryOp 0 = OpNot
decodeUnaryOp 1 = OpNegate
-- Unreachable: the tag is written by Nix.Eval.Compile's encodeUnaryOp, which
-- only emits 0 or 1.
decodeUnaryOp n = error ("decodeUnaryOp: unknown tag " <> show n)
-- | Decode a BinaryOp from bytecode flags.
decodeBinaryOp :: Word8 -> BinaryOp
decodeBinaryOp 0 = OpAdd
decodeBinaryOp 1 = OpSub
decodeBinaryOp 2 = OpMul
decodeBinaryOp 3 = OpDiv
decodeBinaryOp 4 = OpAnd
decodeBinaryOp 5 = OpOr
decodeBinaryOp 6 = OpImpl
decodeBinaryOp 7 = OpEq
decodeBinaryOp 8 = OpNeq
decodeBinaryOp 9 = OpLt
decodeBinaryOp 10 = OpLte
decodeBinaryOp 11 = OpGt
decodeBinaryOp 12 = OpGte
decodeBinaryOp 13 = OpConcat
decodeBinaryOp 14 = OpUpdate
-- Unreachable: the tag is written by Nix.Eval.Compile's encodeBinaryOp, which
-- only emits 0..14.
decodeBinaryOp n = error ("decodeBinaryOp: unknown tag " <> show n)
-- | Evaluate a binary operation from bytecode, with short-circuit
-- support for &&, ||, ->.
evalBcBinary :: (MonadEval m) => Env -> Word32 -> m NixValue
evalBcBinary env bcIdx0 =
let flags_ = unsafePerformIO (cbcFlags bcIdx0)
leftIdx = unsafePerformIO (cbcArg1 bcIdx0)
rightIdx = unsafePerformIO (cbcArg2 bcIdx0)
op = decodeBinaryOp flags_
in case op of
OpAnd -> evalShortCircuitAnd env leftIdx rightIdx
OpOr -> evalShortCircuitOr env leftIdx rightIdx
OpImpl -> evalShortCircuitImpl env leftIdx rightIdx
OpAdd -> do
leftVal <- evalBytecode env leftIdx
rightVal <- evalBytecode env rightIdx
evalAddWithCoercion leftVal rightVal
_ -> do
leftVal <- evalBytecode env leftIdx
rightVal <- evalBytecode env rightIdx
evalBinary force op leftVal rightVal
-- | Addition with string coercion fallback, matching C++ Nix behavior.
-- C++ Nix's ExprOpAdd falls through to concatStrings when operands
-- are not both numeric and neither is a path. concatStrings calls
-- coerceToString on each part, which handles attrsets via outPath.
evalAddWithCoercion :: (MonadEval m) => NixValue -> NixValue -> m NixValue
evalAddWithCoercion left right = case (left, right) of
-- Numeric: direct arithmetic (matches C++ Nix priority)
(VFloat _, _) -> evalBinary force OpAdd left right
(_, VFloat _) -> evalBinary force OpAdd left right
(VInt _, VInt _) -> evalBinary force OpAdd left right
-- Path: direct concat
(VPath _, _) -> evalBinary force OpAdd left right
-- String-like: direct concat when both are string/path
(VStr {}, VStr {}) -> evalBinary force OpAdd left right
(VStr {}, VPath _) -> evalBinary force OpAdd left right
-- Otherwise: string concatenation with STRICT coercion (Nix coerceMore=false)
-- — only strings and sets with __toString/outPath coerce; numbers, bools,
-- null, lists, and functions are type errors, matching C++ Nix's `+`.
_ -> do
(leftStr, leftCtx) <- coerceAddOperand left
(rightStr, rightCtx) <- coerceAddOperand right
pure (VStr (leftStr <> rightStr) (leftCtx <> rightCtx))
-- | Strict string coercion for the @+@ operator (Nix @coerceMore = false@):
-- strings, and sets with @__toString@/@outPath@, coerce; numbers, booleans,
-- null, lists, and functions are type errors.
coerceAddOperand :: (MonadEval m) => NixValue -> m (Text, StringContext)
coerceAddOperand v@(VStr {}) = coerceToString False force applyValue v
coerceAddOperand v@(VAttrs {}) = coerceToString False force applyValue v
coerceAddOperand v =
throwEvalError ("cannot coerce " <> typeName v <> " to a string with the + operator")
-- | Bytecode short-circuit &&
evalShortCircuitAnd :: (MonadEval m) => Env -> Word32 -> Word32 -> m NixValue
evalShortCircuitAnd env leftIdx rightIdx = do
leftVal <- evalBytecode env leftIdx
case leftVal of
VBool False -> pure (VBool False)
VBool True -> do
rightVal <- evalBytecode env rightIdx
case rightVal of
VBool _ -> pure rightVal
_ -> throwEvalError ("second operand of && must be a Boolean, got " <> typeName rightVal)
_ -> throwEvalError ("first operand of && must be a Boolean, got " <> typeName leftVal)
-- | Bytecode short-circuit ||
evalShortCircuitOr :: (MonadEval m) => Env -> Word32 -> Word32 -> m NixValue
evalShortCircuitOr env leftIdx rightIdx = do
leftVal <- evalBytecode env leftIdx
case leftVal of
VBool True -> pure (VBool True)
VBool False -> do
rightVal <- evalBytecode env rightIdx
case rightVal of
VBool _ -> pure rightVal
_ -> throwEvalError ("second operand of || must be a Boolean, got " <> typeName rightVal)
_ -> throwEvalError ("first operand of || must be a Boolean, got " <> typeName leftVal)
-- | Bytecode short-circuit ->
evalShortCircuitImpl :: (MonadEval m) => Env -> Word32 -> Word32 -> m NixValue
evalShortCircuitImpl env leftIdx rightIdx = do
leftVal <- evalBytecode env leftIdx
case leftVal of
VBool False -> pure (VBool True)
VBool True -> do
rightVal <- evalBytecode env rightIdx
case rightVal of
VBool _ -> pure rightVal
_ -> throwEvalError ("second operand of -> must be a Boolean, got " <> typeName rightVal)
_ -> throwEvalError ("first operand of -> must be a Boolean, got " <> typeName leftVal)
-- | Evaluate a string literal from bytecode data buffer.
evalBcStr :: (MonadEval m) => Env -> Word32 -> m NixValue
evalBcStr env bcIdx0 = do
let count = fromIntegral (unsafePerformIO (cbcShortArg bcIdx0))
dataOff = unsafePerformIO (cbcArg1 bcIdx0)
chunks <- evalBcStringParts env count dataOff
pure (VStr (T.concat [t | (_, t, _) <- chunks]) (mconcat [c | (_, _, c) <- chunks]))
-- | Evaluate an indented string literal from bytecode data buffer. The common
-- indentation is stripped from the LITERAL chunks before concatenation, so an
-- interpolated multi-line value cannot drag the computed indent down — matching
-- C++ Nix.
evalBcIndStr :: (MonadEval m) => Env -> Word32 -> m NixValue
evalBcIndStr env bcIdx0 = do
let count = fromIntegral (unsafePerformIO (cbcShortArg bcIdx0))
dataOff = unsafePerformIO (cbcArg1 bcIdx0)
chunks <- evalBcStringParts env count dataOff
let (text, ctx) = stripIndentedChunks chunks
pure (VStr text ctx)
-- | Evaluate string parts from the bytecode data buffer. Each part is two
-- words: (tag, value). tag=0 -> literal (value = symbol), tag=1 ->
-- interpolation (value = bc_idx). The 'Bool' marks literal (@True@) vs
-- interpolated (@False@) so indented strings strip only the literals.
evalBcStringParts :: (MonadEval m) => Env -> Int -> Word32 -> m [(Bool, Text, StringContext)]
evalBcStringParts _ 0 _ = pure []
evalBcStringParts env n off = do
let tag = unsafePerformIO (cbcData off)
val = unsafePerformIO (cbcData (off + 1))
chunk <- case tag of
0 -> pure (True, symbolText (Symbol val), emptyContext)
_ -> do
v <- evalBytecode env val
(txt, ctx) <- coerceToStringInterp v
pure (False, txt, ctx)
rest <- evalBcStringParts env (n - 1) (off + 2)
pure (chunk : rest)
-- | Evaluate a list from bytecode data buffer.
evalBcList :: (MonadEval m) => Env -> Word32 -> m NixValue
evalBcList env bcIdx0 =
let count = fromIntegral (unsafePerformIO (cbcShortArg bcIdx0)) :: Int
dataOff = unsafePerformIO (cbcArg1 bcIdx0)
readChildren 0 _ = []
readChildren n off =
let childIdx = unsafePerformIO (cbcData off)
in cheapThunkBc env childIdx : readChildren (n - 1) (off + 1)
in pure (VList (clistFromThunks (map thunkToCPtr (readChildren count dataOff))))
-- | Evaluate a function application from bytecode.
evalBcApp :: (MonadEval m) => Env -> Word32 -> m NixValue
evalBcApp env bcIdx0 = do
let funcIdx = unsafePerformIO (cbcArg1 bcIdx0)
argIdx = unsafePerformIO (cbcArg2 bcIdx0)
funcVal <- evalBytecode env funcIdx
case funcVal of
VLambda closureEnv formals bodyBcIdx -> do
let argThunk = cheapThunkBc env argIdx
extEnv <- matchFormals closureEnv formals argThunk
evalBytecode extEnv bodyBcIdx
VBuiltin "tryEval" [] -> do
result <- catchEvalError (evalBytecode env argIdx)
case result of
Right val ->
pure
( VAttrs
( attrSetFromMap $
Map.fromList
[ ("success", evaluated (VBool True)),
("value", evaluated val)
]
)
)
Left _ ->
pure
( VAttrs
( attrSetFromMap $
Map.fromList
[ ("success", evaluated (VBool False)),
("value", evaluated (VBool False))
]
)
)
VBuiltin name accArgs -> do
argVal <- evalBytecode env argIdx
applyBuiltin name accArgs argVal
VAttrs attrs
| Just functorThunk <- attrSetLookup "__functor" attrs -> do
functor <- force functorThunk
partiallyApplied <- applyValue functor funcVal
-- Maintain laziness: thunk the argument for lambdas, like
-- the normal VLambda path above. Builtins force anyway.
case partiallyApplied of
VLambda closureEnv formals bodyBcIdx -> do
let argThunk = cheapThunkBc env argIdx
extEnv <- matchFormals closureEnv formals argThunk
evalBytecode extEnv bodyBcIdx
_ -> do
argVal <- evalBytecode env argIdx
applyValue partiallyApplied argVal
_ -> throwEvalError ("attempt to call " <> typeName funcVal <> ", which is not a function")
-- | Evaluate a lambda literal from bytecode — returns VLambda.
evalBcLambda :: (MonadEval m) => Env -> Word32 -> m NixValue
evalBcLambda env bcIdx0 =
let flags_ = unsafePerformIO (cbcFlags bcIdx0)
formalsOff = unsafePerformIO (cbcArg1 bcIdx0)
bodyBcIdx = unsafePerformIO (cbcArg2 bcIdx0)
captureOff = unsafePerformIO (cbcArg3 bcIdx0)
formals = unsafePerformIO (decodeBcFormals flags_ formalsOff)
captureInfo = unsafePerformIO (decodeBcCaptureInfo captureOff)
in pure (VLambda (buildCaptureEnv env captureInfo) formals bodyBcIdx)
-- | Evaluate a select expression from bytecode.
evalBcSelect :: (MonadEval m) => Env -> Word32 -> m NixValue
evalBcSelect env bcIdx0 = do
let hasDef = unsafePerformIO (cbcFlags bcIdx0) /= 0
pathLen = fromIntegral (unsafePerformIO (cbcShortArg bcIdx0))
targetIdx = unsafePerformIO (cbcArg1 bcIdx0)
pathOff = unsafePerformIO (cbcArg2 bcIdx0)
defIdx = unsafePerformIO (cbcArg3 bcIdx0)
targetVal <- evalBytecode env targetIdx
result <- walkBcAttrPath env pathLen pathOff targetVal
case result of
Just val -> pure val
Nothing
| hasDef -> evalBytecode env defIdx
| otherwise -> do
let pathName = collectBcAttrPathNames pathLen pathOff
targetKeys = case targetVal of
VAttrs attrs -> T.intercalate ", " (take 20 (attrSetKeys attrs))
_ -> ""
throwEvalError ("attribute '" <> pathName <> "' not found in " <> typeName targetVal <> " {" <> targetKeys <> "}")
-- | Evaluate a hasAttr expression from bytecode.
evalBcHasAttr :: (MonadEval m) => Env -> Word32 -> m NixValue
evalBcHasAttr env bcIdx0 = do
let pathLen = fromIntegral (unsafePerformIO (cbcShortArg bcIdx0))
targetIdx = unsafePerformIO (cbcArg1 bcIdx0)
pathOff = unsafePerformIO (cbcArg2 bcIdx0)
targetVal <- evalBytecode env targetIdx
result <- walkBcAttrPath env pathLen pathOff targetVal
pure (VBool (isJust result))
-- | Collect static attribute path names for error reporting.
collectBcAttrPathNames :: Int -> Word32 -> Text
collectBcAttrPathNames pathLen pathOff = T.intercalate "." (go pathLen pathOff)
where
go 0 _ = []
go n off =
let isExpr = unsafePerformIO (cbcData off)
keyVal = unsafePerformIO (cbcData (off + 1))
name = if isExpr /= 0 then "<expr>" else symbolText (Symbol keyVal)
in name : go (n - 1) (off + 2)
-- | Walk an attribute path stored in the bytecode data buffer.
-- Each element is two words: (is_expr, key_or_bc_idx).
walkBcAttrPath :: (MonadEval m) => Env -> Int -> Word32 -> NixValue -> m (Maybe NixValue)
walkBcAttrPath _ 0 _ val = pure (Just val)
walkBcAttrPath env n off val = case val of
VAttrs attrs -> do
let isExpr = unsafePerformIO (cbcData off)
keyVal = unsafePerformIO (cbcData (off + 1))
resolved <-
if isExpr /= 0
then do
keyResult <- evalBytecode env keyVal
case keyResult of
VStr s _ -> pure (Just s)
VNull -> pure Nothing
_ -> throwEvalError ("dynamic attribute key must be a string, got " <> typeName keyResult)
else pure (Just (symbolText (Symbol keyVal)))
case resolved >>= (`attrSetLookup` attrs) of
Just thunk -> do
inner <- force thunk
walkBcAttrPath env (n - 1) (off + 2) inner
Nothing -> pure Nothing
-- Non-attrset: attribute path cannot continue. Return Nothing so
-- that callers with a default (``a.b or def'') or hasAttr (``a ? b'')
-- can handle it gracefully, matching C++ Nix behaviour.
_ -> pure Nothing
-- | Evaluate attribute set from bytecode.
evalBcAttrs :: (MonadEval m) => Env -> Word32 -> m NixValue
evalBcAttrs env bcIdx0 = do
let isRec = unsafePerformIO (cbcFlags bcIdx0) /= 0
bindCount = unsafePerformIO (cbcShortArg bcIdx0)
dataOff = unsafePerformIO (cbcArg1 bcIdx0)
captureOff = unsafePerformIO (cbcArg2 bcIdx0)
bindings = unsafePerformIO (decodeBcBindings bindCount dataOff)
if isRec
then do
let captureInfo = unsafePerformIO (decodeBcCaptureInfo captureOff)
evalBcRecAttrs env bindings captureInfo
else evalBcNonRecAttrs env bindings
-- | Evaluate a non-recursive attr set from bytecode bindings.
evalBcNonRecAttrs :: (MonadEval m) => Env -> [BcBinding] -> m NixValue
evalBcNonRecAttrs env bindings = do
thunkMap <- buildBcThunkMap env bindings
pure (VAttrs (attrSetFromMap thunkMap))
-- | Evaluate a recursive attr set from bytecode bindings.
evalBcRecAttrs :: (MonadEval m) => Env -> [BcBinding] -> CaptureInfo -> m NixValue
evalBcRecAttrs env bindings captureInfo
| allBcPositional bindings =
-- Positional path: slots for variable lookup, CAttrSet for return.
let slotCount = bcBindingSlotCount bindings
slotsPtr = allocCSlots slotCount
parentEnv = buildCaptureEnv env captureInfo
(withArr, withCount) = case captureInfo of
NoCaptureInfo -> envWithScopesRaw env
Captures _ -> (nullPtr, 0)
CapturesWithScopes _ -> withScopesForCapture env
recEnv = newCEnv slotsPtr slotCount Nothing (Just parentEnv) withArr withCount
thunkList = buildBcSlotThunks recEnv env bindings
filled = fillCSlots slotsPtr thunkList
attrMap = buildBcAttrMapFromSlots bindings thunkList
in filled `seq` pure (VAttrs (attrSetFromMap attrMap))
| otherwise = do
-- Fallback: dynamic/nested keys — two-phase CAttrSet.
allKeys <- bcBindingAllKeys env bindings
let cset = buildCAttrSetKeys allKeys
attrSet = AttrSet cset
parentEnv = buildCaptureEnv env captureInfo
(withArr, withCount) = case captureInfo of
NoCaptureInfo -> envWithScopesRaw env
Captures _ -> (nullPtr, 0)
CapturesWithScopes _ -> withScopesForCapture env
recEnv = newCEnv nullPtr 0 (Just attrSet) (Just parentEnv) withArr withCount
thunkMap <- buildBcThunkMap recEnv bindings
let filled = fillCAttrSetValues cset thunkMap
in filled `seq` pure (VAttrs attrSet)
-- | Evaluate a let expression from bytecode.
evalBcLet :: (MonadEval m) => Env -> Word32 -> m NixValue
evalBcLet env bcIdx0 = do
let bindCount = unsafePerformIO (cbcShortArg bcIdx0)
dataOff = unsafePerformIO (cbcArg1 bcIdx0)
bodyIdx = unsafePerformIO (cbcArg2 bcIdx0)
captureOff = unsafePerformIO (cbcArg3 bcIdx0)
bindings = unsafePerformIO (decodeBcBindings bindCount dataOff)
captureInfo = unsafePerformIO (decodeBcCaptureInfo captureOff)
if allBcPositional bindings
then do
-- Positional path: slots for O(1) lookup.
let slotCount = bcBindingSlotCount bindings
slotsPtr = allocCSlots slotCount
parentEnv = buildCaptureEnv env captureInfo
(withArr, withCount) = case captureInfo of
NoCaptureInfo -> envWithScopesRaw env
Captures _ -> (nullPtr, 0)
CapturesWithScopes _ -> withScopesForCapture env
letEnv = newCEnv slotsPtr slotCount Nothing (Just parentEnv) withArr withCount
filled = fillCSlots slotsPtr (buildBcSlotThunks letEnv env bindings)
in filled `seq` evalBytecode letEnv bodyIdx
else do
-- Fallback: dynamic/nested keys — two-phase CAttrSet.
allKeys <- bcBindingAllKeys env bindings
let cset = buildCAttrSetKeys allKeys
parentEnv = buildCaptureEnv env captureInfo
(withArr, withCount) = case captureInfo of
NoCaptureInfo -> envWithScopesRaw env
Captures _ -> (nullPtr, 0)
CapturesWithScopes _ -> withScopesForCapture env
letEnv = newCEnv nullPtr 0 (Just (AttrSet cset)) (Just parentEnv) withArr withCount
thunkMap <- buildBcThunkMap letEnv bindings
let filled = fillCAttrSetValues cset thunkMap
in filled `seq` evalBytecode letEnv bodyIdx
-- | Check if all bytecode bindings are single static keys (eligible for positional).
-- Must stay in sync with 'allStaticSingleKey' in 'Nix.Expr.Resolve'.
allBcPositional :: [BcBinding] -> Bool
allBcPositional = all isEligible
where
isEligible (BcNamed [BcStaticKey _] _) = True
isEligible (BcInherit _) = True
isEligible (BcInheritFrom _ _) = True
isEligible _ = False
-- | Count positional slots for bytecode bindings.
-- Must stay in sync with 'lexicalScopeFromBindings' in 'Nix.Expr.Resolve'.
bcBindingSlotCount :: [BcBinding] -> Int
bcBindingSlotCount = foldl' countOne 0
where
countOne !acc (BcNamed [BcStaticKey _] _) = acc + 1
countOne !acc (BcInherit syms) = acc + length syms
countOne !acc (BcInheritFrom _ syms) = acc + length syms
countOne !acc _ = acc
-- | Build thunks for positional bytecode bindings in declaration order.
buildBcSlotThunks :: Env -> Env -> [BcBinding] -> [Thunk]
buildBcSlotThunks recEnv outerEnv = concatMap slotThunk
where
slotThunk (BcNamed [BcStaticKey _] valBcIdx) =
[mkThunkBc recEnv valBcIdx]
slotThunk (BcInherit syms) =
map (inheritLookup outerEnv . symbolText . Symbol) syms
slotThunk (BcInheritFrom fromBcIdx syms) =
-- inherit (from) x y z; → one thunk per name that selects from the from-expr.
-- Each thunk gets a minimal env with the from-value at slot 0.
let fromThunk = mkThunkBc recEnv fromBcIdx
mkInheritThunk sym =
let name = symbolText (Symbol sym)
selectExpr = ESelect (EResolvedVar 0 0) [StaticKey name] Nothing
(sp, sc) = buildCSlots [fromThunk]
fromEnv = newMinimalEnv sp sc
in mkSyntheticThunk fromEnv selectExpr
in map mkInheritThunk syms
-- Unreachable: allBcPositional guards this path.
slotThunk _ = []
-- | Build attr map from slots for positional bytecode bindings.
buildBcAttrMapFromSlots :: [BcBinding] -> [Thunk] -> Map Text Thunk
buildBcAttrMapFromSlots bindings thunks = go bindings thunks Map.empty
where
go [] _ !acc = acc
go (BcNamed [BcStaticKey sym] _ : bs) (t : ts) !acc =
go bs ts (Map.insert (symbolText (Symbol sym)) t acc)
go (BcInherit syms : bs) ts !acc =
let (used, rest) = splitAt (length syms) ts
accMerged = foldl' (\a (sym, t0) -> Map.insert (symbolText (Symbol sym)) t0 a) acc (zip syms used)
in go bs rest accMerged
go (BcInheritFrom _ syms : bs) ts !acc =
let (used, rest) = splitAt (length syms) ts
accMerged = foldl' (\a (sym, t0) -> Map.insert (symbolText (Symbol sym)) t0 a) acc (zip syms used)
in go bs rest accMerged
-- Unreachable: allBcPositional guards this path.
go (_ : bs) ts !acc = go bs ts acc
-- | Build thunk map for bytecode attrs (non-rec or fallback rec path).
buildBcThunkMap :: (MonadEval m) => Env -> [BcBinding] -> m (Map Text Thunk)
buildBcThunkMap thunkEnv = foldM addBinding Map.empty
where
addBinding acc (BcNamed keys valBcIdx) = do
resolvedKeys <- mapM (resolveBcKey thunkEnv) keys
case sequence resolvedKeys of
Nothing -> pure acc -- null key -> skip
Just [key] ->
pure (insertWithMerge acc key (mkThunkBc thunkEnv valBcIdx))
Just path ->
let nested = buildBcNestedAttr thunkEnv path valBcIdx
in pure (foldl' (\a (k, t0) -> insertWithMerge a k t0) acc (Map.toList nested))
addBinding acc (BcInherit syms) =
pure (foldl' (\a sym -> let name = symbolText (Symbol sym) in insertWithMerge a name (inheritLookup thunkEnv name)) acc syms)
addBinding acc (BcInheritFrom fromBcIdx syms) =
-- inherit (from) name -> select name from the from-expr.
-- Create a small env with the from value at slot 0, then a
-- synthetic expression that selects name from slot 0.
let addInheritFrom a sym =
let name = symbolText (Symbol sym)
selectExpr = ESelect (EResolvedVar 0 0) [StaticKey name] Nothing
(sp, sc) = buildCSlots [mkThunkBc thunkEnv fromBcIdx]
fromEnv = newMinimalEnv sp sc
in insertWithMerge a name (mkSyntheticThunk fromEnv selectExpr)
in pure (foldl' addInheritFrom acc syms)
insertWithMerge acc key thunk =
case Map.lookup key acc of
Nothing -> Map.insert key thunk acc
Just existing -> Map.insert key (mergeThunks existing thunk) acc
-- | Resolve a bytecode attr key to text.
resolveBcKey :: (MonadEval m) => Env -> BcAttrKey -> m (Maybe Text)
resolveBcKey _env (BcStaticKey sym) = pure (Just (symbolText (Symbol sym)))
resolveBcKey env (BcDynamicKey bcIdx0) = do
val <- evalBytecode env bcIdx0
case val of
VStr s _ -> pure (Just s)
VNull -> pure Nothing
_ -> throwEvalError ("dynamic attribute key must be a string, got " <> typeName val)
-- | Build a nested attribute structure from a resolved dotted path (bytecode).
buildBcNestedAttr :: Env -> [Text] -> Word32 -> Map Text Thunk
buildBcNestedAttr _thunkEnv [] _valBcIdx = Map.empty
buildBcNestedAttr thunkEnv [key] valBcIdx =
Map.singleton key (mkThunkBc thunkEnv valBcIdx)
buildBcNestedAttr thunkEnv (key : rest) valBcIdx =
Map.singleton key (evaluated (VAttrs (attrSetFromMap (buildBcNestedAttr thunkEnv rest valBcIdx))))
-- | Extract all top-level keys from bytecode bindings, resolving
-- dynamic keys as needed. Used by the fallback path (two-phase
-- CAttrSet construction) where all keys must be known before thunks.
bcBindingAllKeys :: (MonadEval m) => Env -> [BcBinding] -> m [Text]
bcBindingAllKeys env bindings = fmap concat (mapM oneBinding bindings)
where
oneBinding (BcNamed (key : _) _) = do
resolved <- resolveBcKey env key
pure (maybeToList resolved)
oneBinding (BcNamed [] _) = pure []
oneBinding (BcInherit syms) =
pure (map (symbolText . Symbol) syms)
oneBinding (BcInheritFrom _ syms) =
pure (map (symbolText . Symbol) syms)
-- ---------------------------------------------------------------------------
-- Search paths (<nixpkgs>, <nixpkgs/lib>)
-- ---------------------------------------------------------------------------
-- | Evaluate a search path expression.
-- Desugars to @builtins.findFile builtins.nixPath "name"@ — exactly how
-- real Nix handles @\<name\>@ expressions.
evalSearchPath :: (MonadEval m) => Env -> Text -> m NixValue
evalSearchPath env name = do
builtinsVal <- evalVar env "builtins"
case builtinsVal of
VAttrs builtinsAttrs ->
case attrSetLookup "nixPath" builtinsAttrs of
Just nixPathThunk -> do
nixPathVal <- force nixPathThunk
builtinFindFile nixPathVal (mkStr name)
Nothing ->
throwEvalError ("file '" <> name <> "' was not found in the Nix search path")
_ ->
throwEvalError ("file '" <> name <> "' was not found in the Nix search path")
-- ---------------------------------------------------------------------------
-- Variables
-- ---------------------------------------------------------------------------
evalVar :: (MonadEval m) => Env -> Text -> m NixValue
evalVar env name =
case envLookup name env of
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.
-- Supports both resolved (CAttrSet*) and lazy (CThunk*, tagged with bit 0)
-- with-scope entries. Lazy entries are forced on first lookup and the
-- pointer is updated in place so subsequent lookups hit the resolved
-- attrset directly.
evalWithVar :: (MonadEval m) => Env -> Text -> m NixValue
evalWithVar env name =
let (withArr, withCount) = envWithScopesRaw env
in evalWithVarScopes env name withArr (fromIntegral withCount) 0
evalWithVarScopes :: (MonadEval m) => Env -> Text -> Ptr (Ptr ()) -> Int -> Int -> m NixValue
evalWithVarScopes env name withArr count idx
| idx >= count = evalVar env name
| otherwise = do
let scopePtr = unsafePerformIO (peekElemOff withArr idx)
if isLazyWithScope scopePtr
then do
-- Lazy with-scope: force the thunk to get the attrset
let thunkPtr = untagWithScope scopePtr
scopeVal <- force (Thunk thunkPtr)
case scopeVal of
VAttrs (AttrSet cset) -> do
-- Cache: replace the tagged thunk pointer with the resolved
-- attrset pointer so future lookups are fast.
let resolvedPtr = castPtr cset
seq (unsafePerformIO (pokeElemOff withArr idx resolvedPtr)) (pure ())
case attrSetLookup name (AttrSet cset) of
Just thunk -> force thunk
Nothing -> evalWithVarScopes env name withArr count (idx + 1)
_ -> throwEvalError ("'with' requires a set, got " <> typeName scopeVal)
else
-- Resolved attrset scope (normal path)
case attrSetLookup name (AttrSet (castPtr scopePtr)) of
Just thunk -> force thunk
Nothing -> evalWithVarScopes env name withArr count (idx + 1)
-- | Check if a with-scope pointer is tagged as lazy (bit 0 set).
isLazyWithScope :: Ptr () -> Bool
isLazyWithScope ptr = ptrToWordPtr ptr .&. 1 /= 0
-- | Remove the lazy tag from a with-scope pointer, returning a CThunkPtr.
untagWithScope :: Ptr () -> CThunkPtr
untagWithScope ptr =
let tagged = ptrToWordPtr ptr
in wordPtrToPtr (tagged .&. complement 1)
-- | Tag a CThunkPtr as a lazy with-scope (set bit 0).
tagLazyWithScope :: CThunkPtr -> Ptr ()
tagLazyWithScope ptr =
let raw = ptrToWordPtr (castPtr ptr)
in wordPtrToPtr (raw .|. 1)
-- | Push a lazy (thunk-based) with-scope onto the environment.
-- The scope is NOT forced until a WITH_VAR lookup actually needs it.
{-# NOINLINE pushLazyWithScope #-}
pushLazyWithScope :: Thunk -> Env -> Env
pushLazyWithScope (Thunk thunkPtr) =
pushWithScopeRaw (tagLazyWithScope thunkPtr)
-- | Push a raw pointer as a with-scope.
{-# NOINLINE pushWithScopeRaw #-}
pushWithScopeRaw :: Ptr () -> Env -> Env
pushWithScopeRaw ptr (Env envPtr) =
Env (unsafePerformIO (cenvPushWith envPtr ptr))
-- ---------------------------------------------------------------------------
-- Formals matching + env helpers (used by evalBcApp, applyValue, etc.)
-- ---------------------------------------------------------------------------
-- | Match a lambda's formals against an argument thunk.
matchFormals :: (MonadEval m) => Env -> EvalFormals -> Thunk -> m Env
matchFormals closureEnv (EFName _) argThunk =
let (sp, sc) = buildCSlots [argThunk]
in pure (envFromSlots sp sc closureEnv)
matchFormals closureEnv (EFSet formals allowExtra) argThunk = do
argVal <- force argThunk
matchFormalSet closureEnv formals allowExtra argVal Nothing
matchFormals closureEnv (EFNamedSet _ formals allowExtra) argThunk = do
argVal <- force argThunk
matchFormalSet closureEnv formals allowExtra argVal (Just argThunk)
-- | Match destructuring set pattern formals against an attrset value.
matchFormalSet :: (MonadEval m) => Env -> [EvalFormal] -> Bool -> NixValue -> Maybe Thunk -> m Env
matchFormalSet closureEnv formals allowExtra argVal atThunk =
case argVal of
VAttrs attrs -> do
checkExtraKeys formals allowExtra attrs
checkMissingFormals attrs formals
let formalEnv = envFromSlots formalSlotsPtr formalSlotCount closureEnv
(formalSlotsPtr, formalSlotCount) = buildCSlots formalThunks
formalThunks = case atThunk of
Nothing -> map resolveOneFormal formals
Just at -> at : map resolveOneFormal formals
resolveOneFormal (EvalFormal name defBcIdx) =
case attrSetLookup name attrs of
Just thunk -> thunk
Nothing -> case defBcIdx of
Just bcIdx -> mkThunkBc formalEnv bcIdx
Nothing -> error "matchFormalSet: missing required formal (unreachable)"
pure formalEnv
_ -> throwEvalError ("function expects a set argument, got " <> typeName argVal)
checkExtraKeys :: (MonadEval m) => [EvalFormal] -> Bool -> AttrSet -> m ()
checkExtraKeys _ True _ = pure ()
checkExtraKeys formals False attrs =
let expected = map efName formals
actual = attrSetKeys attrs
extra = filter (`notElem` expected) actual
in case extra of
[] -> pure ()
(k : _) -> throwEvalError ("unexpected attribute '" <> k <> "' in function argument")
checkMissingFormals :: (MonadEval m) => AttrSet -> [EvalFormal] -> m ()
checkMissingFormals attrs formals =
let allMissing = [efName f | f <- formals, isNothing (efDefault f), not (attrSetMember (efName f) attrs)]
in case allMissing of
[] -> pure ()
(name : _) ->
let provided = attrSetKeys attrs
provSnippet = T.intercalate ", " (take 20 provided)
missSnippet = T.intercalate ", " allMissing
in throwEvalError ("missing required attribute '" <> name <> "'; all missing: [" <> missSnippet <> "]; provided keys (" <> T.pack (show (length provided)) <> "): [" <> provSnippet <> "]")
-- | Build capture environment from capture info.
-- NoCaptureInfo: no trimming, use env as-is.
-- Captures: build minimal env from captured slots.
-- CapturesWithScopes: build minimal env + copy with-scopes.
buildCaptureEnv :: Env -> CaptureInfo -> Env
buildCaptureEnv env NoCaptureInfo = env
buildCaptureEnv env (Captures captureList) =
let (slotsPtr, slotCount) = buildCSlots [envLookupResolved lvl idx env | (lvl, idx) <- captureList]
in newMinimalEnv slotsPtr slotCount
buildCaptureEnv env (CapturesWithScopes captureList) =
let (slotsPtr, slotCount) = buildCSlots [envLookupResolved lvl idx env | (lvl, idx) <- captureList]
(withArr, withCount) = withScopesForCapture env
in newCEnv slotsPtr slotCount Nothing Nothing withArr withCount
-- | Look up a name in the environment and return its thunk.
-- Used by @inherit@ bindings (both bytecode and Expr paths).
inheritLookup :: Env -> Text -> Thunk
inheritLookup env name =
case envLookup name env of
Just thunk -> thunk
Nothing -> error ("inheritLookup: undefined variable '" <> T.unpack name <> "' (unreachable)")
-- | Merge two thunks for nested attribute update.
-- If both are computed attrsets, recursively merge with attrSetUnionWith.
-- Otherwise the new value wins.
mergeThunks :: Thunk -> Thunk -> Thunk
mergeThunks a b =
case (readThunkValue a, readThunkValue b) of
(Just (VAttrs aa), Just (VAttrs bb)) ->
evaluated (VAttrs (attrSetUnionWith mergeThunks aa bb))
_ -> b
-- ---------------------------------------------------------------------------
-- Builtin registry (single-definition-site for all builtins)
-- ---------------------------------------------------------------------------
-- | A builtin function definition: its arity and implementation.
data BuiltinDef m = BuiltinDef
{ bdArity :: !Int,
bdApply :: [NixValue] -> m NixValue
}
-- | Define an arity-1 builtin.
builtin1 :: (MonadEval m) => Text -> (NixValue -> m NixValue) -> (Text, BuiltinDef m)
builtin1 name f =
( name,
BuiltinDef 1 $ \case
[a] -> f a
_ -> throwEvalError ("builtins." <> name <> ": internal arity error")
)
-- | Define an arity-2 builtin.
builtin2 ::
(MonadEval m) =>
Text ->
(NixValue -> NixValue -> m NixValue) ->
(Text, BuiltinDef m)
builtin2 name f =
( name,
BuiltinDef 2 $ \case
[a, b] -> f a b
_ -> throwEvalError ("builtins." <> name <> ": internal arity error")
)
-- | Define an arity-3 builtin.
builtin3 ::
(MonadEval m) =>
Text ->
(NixValue -> NixValue -> NixValue -> m NixValue) ->
(Text, BuiltinDef m)
builtin3 name f =
( name,
BuiltinDef 3 $ \case
[a, b, c] -> f a b c
_ -> throwEvalError ("builtins." <> name <> ": internal arity error")
)
-- | Central registry of all builtins. Adding a new builtin is a single
-- entry here plus its implementation function — no other files need changes.
builtinRegistry :: (MonadEval m) => Map Text (BuiltinDef m)
builtinRegistry =
Map.fromList
[ -- Type checking (arity 1)
builtin1 "typeOf" (pure . mkStr . typeOfValue),
builtin1 "isNull" (pure . VBool . isNullVal),
builtin1 "isInt" (pure . VBool . isIntVal),
builtin1 "isFloat" (pure . VBool . isFloatVal),
builtin1 "isBool" (pure . VBool . isBoolVal),
builtin1 "isString" (pure . VBool . isStringVal),
builtin1 "isList" (pure . VBool . isListVal),
builtin1 "isAttrs" (pure . VBool . isAttrsVal),
builtin1 "isFunction" (pure . VBool . isFunctionVal),
-- List operations (arity 1)
builtin1 "length" builtinLength,
builtin1 "head" builtinHead,
builtin1 "tail" builtinTail,
-- String operations (arity 1)
builtin1 "toString" (fmap (uncurry VStr) . coerceToStringPermissive),
builtin1 "stringLength" builtinStringLength,
-- Control (arity 1)
builtin1 "throw" builtinThrow,
builtin1 "abort" builtinAbort,
-- Attr set operations (arity 1)
builtin1 "attrNames" builtinAttrNames,
builtin1 "attrValues" builtinAttrValues,
builtin1 "listToAttrs" builtinListToAttrs,
-- Attr set operations (arity 2)
builtin2 "hasAttr" builtinHasAttr,
builtin2 "getAttr" builtinGetAttr,
builtin2 "removeAttrs" builtinRemoveAttrs,
builtin2 "intersectAttrs" builtinIntersectAttrs,
builtin2 "catAttrs" builtinCatAttrs,
-- List higher-order (arity 2)
builtin2 "map" builtinMap,
builtin2 "filter" builtinFilter,
builtin2 "genList" builtinGenList,
builtin2 "sort" builtinSort,
builtin2 "concatMap" builtinConcatMap,
builtin2 "any" builtinAny,
builtin2 "all" builtinAll,
builtin2 "elem" builtinElem,
builtin2 "elemAt" builtinElemAt,
builtin2 "partition" builtinPartition,
builtin2 "groupBy" builtinGroupBy,
-- String operations (arity 2)
builtin2 "concatStringsSep" builtinConcatStringsSep,
-- Arity 3
builtin3 "foldl'" builtinFoldl,
builtin3 "substring" builtinSubstring,
-- Numeric
builtin1 "isPath" (pure . VBool . isPathVal),
builtin1 "ceil" builtinCeil,
builtin1 "floor" builtinFloor,
builtin2 "seq" builtinSeq,
builtin2 "trace" builtinTrace,
builtin2 "warn" builtinWarn,
builtin1 "unsafeDiscardStringContext" builtinDiscardContext,
builtin1 "unsafeDiscardOutputDependency" builtinDiscardOutputDep,
-- String context introspection
builtin1 "hasContext" builtinHasContext,
builtin1 "getContext" builtinGetContext,
builtin2 "appendContext" builtinAppendContext,
builtin1 "baseNameOf" builtinBaseNameOf,
builtin1 "dirOf" builtinDirOf,
builtin1 "concatLists" builtinConcatLists,
builtin2 "lessThan" builtinLessThan,
-- Arithmetic + bitwise
builtin2 "add" builtinAdd,
builtin2 "sub" builtinSub,
builtin2 "mul" builtinMul,
builtin2 "div" builtinDiv,
builtin2 "mod" builtinMod,
builtin2 "bitAnd" builtinBitAnd,
builtin2 "bitOr" builtinBitOr,
builtin2 "bitXor" builtinBitXor,
builtin2 "min" builtinMin,
builtin2 "max" builtinMax,
-- Attr set higher-order
builtin2 "mapAttrs" builtinMapAttrs,
builtin1 "functionArgs" builtinFunctionArgs,
builtin2 "setFunctionArgs" builtinSetFunctionArgs,
builtin2 "zipAttrsWith" builtinZipAttrsWith,
-- String manipulation
builtin2 "match" builtinMatch,
builtin2 "split" builtinSplit,
builtin3 "replaceStrings" builtinReplaceStrings,
builtin2 "compareVersions" builtinCompareVersions,
builtin1 "splitVersion" builtinSplitVersion,
builtin1 "parseDrvName" builtinParseDrvName,
-- Serialization + hashing
builtin1 "toJSON" builtinToJSON,
builtin1 "fromJSON" builtinFromJSON,
builtin2 "hashString" builtinHashString,
-- Error handling + sequencing
builtin1 "tryEval" (\_ -> throwEvalError "unreachable: tryEval handled in evalApp"),
builtin2 "deepSeq" builtinDeepSeq,
-- Graph traversal
builtin1 "genericClosure" builtinGenericClosure,
-- IO builtins (delegate to MonadEval methods)
builtin1 "import" builtinImport,
builtin1 "readFile" builtinReadFile,
builtin1 "pathExists" builtinPathExists,
builtin1 "readDir" builtinReadDir,
builtin1 "getEnv" builtinGetEnv,
builtin1 "toPath" builtinToPath,
-- Store path operations
builtin1 "placeholder" builtinPlaceholder,
builtin1 "storePath" builtinStorePath,
builtin2 "findFile" builtinFindFile,
builtin2 "toFile" builtinToFile,
builtin2 "scopedImport" builtinScopedImport,
-- Network fetchers
builtin1 "fetchurl" builtinFetchurl,
builtin1 "fetchTarball" builtinFetchTarball,
builtin1 "fetchGit" builtinFetchGit,
-- Derivation construction: lazy 'derivation' wrapper over the eager
-- 'derivationStrict' primop (matches C++ Nix corepkgs/derivation.nix).
builtin1 "derivation" builtinDerivationLazy,
builtin1 "derivationStrict" builtinDerivationStrict,
-- Error context (pass-through — context only matters on error)
builtin2 "addErrorContext" (\_ val -> pure val),
-- Attr position (return null — nixpkgs handles this gracefully)
builtin2 "unsafeGetAttrPos" (\_ _ -> pure VNull),
-- Debugging (traceVerbose: same as trace for now, --trace-verbose not yet gated)
builtin2 "traceVerbose" builtinTrace,
builtin1 "break" pure,
-- IO: file hashing + type detection
builtin2 "hashFile" builtinHashFile,
builtin1 "readFileType" builtinReadFileType,
-- Serialization
builtin1 "fromTOML" builtinFromTOML,
-- Hash conversion
builtin1 "convertHash" builtinConvertHash,
-- XML serialization
builtin1 "toXML" builtinToXML,
-- Source filtering + path import
builtin1 "path" builtinPath,
builtin2 "filterSource" builtinFilterSource,
-- Experimental feature stubs
builtin2 "outputOf" builtinOutputOf,
builtin1 "fetchTree" builtinFetchTree,
builtin1 "fetchClosure" builtinFetchClosure
]
-- | Names of all registered builtins.
builtinNames :: [Text]
builtinNames = Map.keys (builtinRegistry :: Map Text (BuiltinDef PureEval))
-- ---------------------------------------------------------------------------
-- Builtin dispatch (partial application via accumulated args)
-- ---------------------------------------------------------------------------
-- | Arity of a builtin (how many arguments before execution).
builtinArity :: Text -> Int
builtinArity name = maybe 1 bdArity (Map.lookup name (builtinRegistry :: Map Text (BuiltinDef PureEval)))
-- | Apply a builtin with accumulated args. If we have enough args,
-- execute; otherwise return a partially applied builtin.
applyBuiltin :: (MonadEval m) => Text -> [NixValue] -> NixValue -> m NixValue
applyBuiltin name accArgs arg =
let allArgs = accArgs ++ [arg]
arity = builtinArity name
in if length allArgs < arity
then pure (VBuiltin name (precompileArgs name allArgs))
else executeBuiltin name allArgs
-- | Pre-compile regex patterns at partial application time.
-- When builtins.match or builtins.split receives its first argument
-- (the pattern string), compile it immediately and store the compiled
-- RE.Regex in a VCompiledRegex, replacing the raw VStr. The compiled
-- form is carried in VBuiltin's accumulated args and reused on every
-- subsequent application — zero recompilation.
precompileArgs :: Text -> [NixValue] -> [NixValue]
precompileArgs "match" [VStr pat _] =
let anchored = "^" <> pat <> "$"
in case cachedCompileRegex anchored of
Just compiled -> [VCompiledRegex (CompiledRegex pat compiled)]
Nothing -> [VStr pat emptyContext] -- fail later at execute time
precompileArgs "split" [VStr pat _] =
case cachedCompileRegex pat of
Just compiled -> [VCompiledRegex (CompiledRegex pat compiled)]
Nothing -> [VStr pat emptyContext] -- fail later at execute time
precompileArgs _ args = args
-- | Apply a function value (lambda or builtin) to one argument.
-- Used by higher-order builtins to invoke user-supplied functions.
applyValue :: (MonadEval m) => NixValue -> NixValue -> m NixValue
applyValue (VLambda closureEnv formals bodyBcIdx) arg = do
extEnv <- matchFormals closureEnv formals (evaluated arg)
evalBytecode extEnv bodyBcIdx
applyValue (VBuiltin name accArgs) arg =
applyBuiltin name accArgs arg
applyValue other _ =
throwEvalError ("attempt to call " <> typeName other <> ", which is not a function")
-- | Execute a builtin once all arguments are collected.
--
-- Direct case dispatch avoids rebuilding the polymorphic 'builtinRegistry'
-- Map on every call. 'builtinRegistry' is polymorphic in @m@ so GHC
-- cannot cache it as a CAF — it gets reconstructed on every use.
-- Pattern matching on the name is zero-allocation.
executeBuiltin :: (MonadEval m) => Text -> [NixValue] -> m NixValue
executeBuiltin name args = case name of
-- Type checking (arity 1)
"typeOf" -> apply1 (pure . mkStr . typeOfValue)
"isNull" -> apply1 (pure . VBool . isNullVal)
"isInt" -> apply1 (pure . VBool . isIntVal)
"isFloat" -> apply1 (pure . VBool . isFloatVal)
"isBool" -> apply1 (pure . VBool . isBoolVal)
"isString" -> apply1 (pure . VBool . isStringVal)
"isList" -> apply1 (pure . VBool . isListVal)
"isAttrs" -> apply1 (pure . VBool . isAttrsVal)
"isFunction" -> apply1 (pure . VBool . isFunctionVal)
-- List operations (arity 1)
"length" -> apply1 builtinLength
"head" -> apply1 builtinHead
"tail" -> apply1 builtinTail
-- String operations (arity 1)
"toString" -> apply1 (fmap (uncurry VStr) . coerceToStringPermissive)
"stringLength" -> apply1 builtinStringLength
-- Control (arity 1)
"throw" -> apply1 builtinThrow
"abort" -> apply1 builtinAbort
-- Attr set operations (arity 1)
"attrNames" -> apply1 builtinAttrNames
"attrValues" -> apply1 builtinAttrValues
"listToAttrs" -> apply1 builtinListToAttrs
-- Attr set operations (arity 2)
"hasAttr" -> apply2 builtinHasAttr
"getAttr" -> apply2 builtinGetAttr
"removeAttrs" -> apply2 builtinRemoveAttrs
"intersectAttrs" -> apply2 builtinIntersectAttrs
"catAttrs" -> apply2 builtinCatAttrs
-- List higher-order (arity 2)
"map" -> apply2 builtinMap
"filter" -> apply2 builtinFilter
"genList" -> apply2 builtinGenList
"sort" -> apply2 builtinSort
"concatMap" -> apply2 builtinConcatMap
"any" -> apply2 builtinAny
"all" -> apply2 builtinAll
"elem" -> apply2 builtinElem
"elemAt" -> apply2 builtinElemAt
"partition" -> apply2 builtinPartition
"groupBy" -> apply2 builtinGroupBy
-- String operations (arity 2)
"concatStringsSep" -> apply2 builtinConcatStringsSep
-- Arity 3
"foldl'" -> apply3 builtinFoldl
"substring" -> apply3 builtinSubstring
-- Numeric
"isPath" -> apply1 (pure . VBool . isPathVal)
"ceil" -> apply1 builtinCeil
"floor" -> apply1 builtinFloor
"seq" -> apply2 builtinSeq
"trace" -> apply2 builtinTrace
"warn" -> apply2 builtinWarn
"unsafeDiscardStringContext" -> apply1 builtinDiscardContext
"unsafeDiscardOutputDependency" -> apply1 builtinDiscardOutputDep
-- String context introspection
"hasContext" -> apply1 builtinHasContext
"getContext" -> apply1 builtinGetContext
"appendContext" -> apply2 builtinAppendContext
"baseNameOf" -> apply1 builtinBaseNameOf
"dirOf" -> apply1 builtinDirOf
"concatLists" -> apply1 builtinConcatLists
"lessThan" -> apply2 builtinLessThan
-- Arithmetic + bitwise
"add" -> apply2 builtinAdd
"sub" -> apply2 builtinSub
"mul" -> apply2 builtinMul
"div" -> apply2 builtinDiv
"mod" -> apply2 builtinMod
"bitAnd" -> apply2 builtinBitAnd
"bitOr" -> apply2 builtinBitOr
"bitXor" -> apply2 builtinBitXor
"min" -> apply2 builtinMin
"max" -> apply2 builtinMax
-- Attr set higher-order
"mapAttrs" -> apply2 builtinMapAttrs
"functionArgs" -> apply1 builtinFunctionArgs
"setFunctionArgs" -> apply2 builtinSetFunctionArgs
"zipAttrsWith" -> apply2 builtinZipAttrsWith
-- String manipulation
"match" -> apply2 builtinMatch
"split" -> apply2 builtinSplit
"replaceStrings" -> apply3 builtinReplaceStrings
"compareVersions" -> apply2 builtinCompareVersions
"splitVersion" -> apply1 builtinSplitVersion
"parseDrvName" -> apply1 builtinParseDrvName
-- Serialization + hashing
"toJSON" -> apply1 builtinToJSON
"fromJSON" -> apply1 builtinFromJSON
"hashString" -> apply2 builtinHashString
-- Error handling + sequencing
"tryEval" -> apply1 (\_ -> throwEvalError "unreachable: tryEval handled in evalApp")
"deepSeq" -> apply2 builtinDeepSeq
-- Graph traversal
"genericClosure" -> apply1 builtinGenericClosure
-- IO builtins (delegate to MonadEval methods)
"import" -> apply1 builtinImport
"readFile" -> apply1 builtinReadFile
"pathExists" -> apply1 builtinPathExists
"readDir" -> apply1 builtinReadDir
"getEnv" -> apply1 builtinGetEnv
"toPath" -> apply1 builtinToPath
-- Store path operations
"placeholder" -> apply1 builtinPlaceholder
"storePath" -> apply1 builtinStorePath
"findFile" -> apply2 builtinFindFile
"toFile" -> apply2 builtinToFile
"scopedImport" -> apply2 builtinScopedImport
-- Network fetchers
"fetchurl" -> apply1 builtinFetchurl
"fetchTarball" -> apply1 builtinFetchTarball
"fetchGit" -> apply1 builtinFetchGit
-- Derivation construction: lazy 'derivation' over eager 'derivationStrict'
"derivation" -> apply1 builtinDerivationLazy
"derivationStrict" -> apply1 builtinDerivationStrict
-- Error context (pass-through — context only matters on error)
"addErrorContext" -> apply2 (\_ val -> pure val)
-- Attr position (return null — nixpkgs handles this gracefully)
"unsafeGetAttrPos" -> apply2 (\_ _ -> pure VNull)
-- Debugging (traceVerbose: same as trace for now)
"traceVerbose" -> apply2 builtinTrace
"break" -> apply1 pure
-- IO: file hashing + type detection
"hashFile" -> apply2 builtinHashFile
"readFileType" -> apply1 builtinReadFileType
-- Serialization
"fromTOML" -> apply1 builtinFromTOML
-- Hash conversion
"convertHash" -> apply1 builtinConvertHash
-- XML serialization
"toXML" -> apply1 builtinToXML
-- Source filtering + path import
"path" -> apply1 builtinPath
"filterSource" -> apply2 builtinFilterSource
-- Experimental feature stubs
"outputOf" -> apply2 builtinOutputOf
"fetchTree" -> apply1 builtinFetchTree
"fetchClosure" -> apply1 builtinFetchClosure
_ -> throwEvalError ("unknown builtin '" <> name <> "'")
where
apply1 f = case args of
[a] -> f a
_ -> throwEvalError ("builtins." <> name <> ": internal arity error")
apply2 f = case args of
[a, b] -> f a b
_ -> throwEvalError ("builtins." <> name <> ": internal arity error")
apply3 f = case args of
[a, b, c] -> f a b c
_ -> throwEvalError ("builtins." <> name <> ": internal arity error")
-- ---------------------------------------------------------------------------
-- Builtin implementations — type checking
-- ---------------------------------------------------------------------------
typeOfValue :: NixValue -> Text
typeOfValue val = case val of
VInt _ -> "int"
VFloat _ -> "float"
VBool _ -> "bool"
VNull -> "null"
VStr _ _ -> "string"
VPath _ -> "path"
VList _ -> "list"
VAttrs _ -> "set"
VLambda {} -> "lambda"
VBuiltin _ _ -> "lambda"
VDerivation _ -> "set"
VCompiledRegex _ -> "lambda"
isNullVal :: NixValue -> Bool
isNullVal VNull = True
isNullVal _ = False
isIntVal :: NixValue -> Bool
isIntVal (VInt _) = True
isIntVal _ = False
isFloatVal :: NixValue -> Bool
isFloatVal (VFloat _) = True
isFloatVal _ = False
isBoolVal :: NixValue -> Bool
isBoolVal (VBool _) = True
isBoolVal _ = False
isStringVal :: NixValue -> Bool
isStringVal (VStr _ _) = True
isStringVal _ = False
isListVal :: NixValue -> Bool
isListVal (VList _) = True
isListVal _ = False
isAttrsVal :: NixValue -> Bool
isAttrsVal (VAttrs _) = True
isAttrsVal _ = False
isFunctionVal :: NixValue -> Bool
isFunctionVal (VLambda {}) = True
isFunctionVal (VBuiltin _ _) = True
isFunctionVal _ = False
-- ---------------------------------------------------------------------------
-- Builtin implementations — list (arity 1)
-- ---------------------------------------------------------------------------
builtinLength :: (MonadEval m) => NixValue -> m NixValue
builtinLength (VList cl) = pure (VInt (fromIntegral (clistLen cl)))
builtinLength other = throwEvalError ("builtins.length: expected a list, got " <> typeName other)
builtinHead :: (MonadEval m) => NixValue -> m NixValue
builtinHead (VList cl)
| clistLen cl == 0 = throwEvalError "builtins.head: empty list"
| otherwise = case clistThunks cl of
(p : _) -> force (Thunk p)
[] -> throwEvalError "builtins.head: empty list" -- unreachable: clistLen > 0
builtinHead other = throwEvalError ("builtins.head: expected a list, got " <> typeName other)
builtinTail :: (MonadEval m) => NixValue -> m NixValue
builtinTail (VList cl)
| clistLen cl == 0 = throwEvalError "builtins.tail: empty list"
| otherwise = pure (VList (clistFromThunks (drop 1 (clistThunks cl))))
builtinTail other = throwEvalError ("builtins.tail: expected a list, got " <> typeName other)
-- ---------------------------------------------------------------------------
-- Builtin implementations — string (arity 1)
-- ---------------------------------------------------------------------------
builtinStringLength :: (MonadEval m) => NixValue -> m NixValue
builtinStringLength (VStr s _) = pure (VInt (fromIntegral (T.length s)))
builtinStringLength other =
throwEvalError ("builtins.stringLength: expected a string, got " <> typeName other)
-- ---------------------------------------------------------------------------
-- Builtin implementations — control
-- ---------------------------------------------------------------------------
builtinThrow :: (MonadEval m) => NixValue -> m NixValue
builtinThrow (VStr msg _) = throwEvalError msg
builtinThrow other = throwEvalError ("builtins.throw: expected a string, got " <> typeName other)
builtinAbort :: (MonadEval m) => NixValue -> m NixValue
builtinAbort (VStr msg _) = abortEvaluation msg
builtinAbort other = abortEvaluation ("builtins.abort: expected a string, got " <> typeName other)
-- ---------------------------------------------------------------------------
-- Builtin implementations — attr set (arity 1)
-- ---------------------------------------------------------------------------
builtinAttrNames :: (MonadEval m) => NixValue -> m NixValue
builtinAttrNames (VAttrs attrs) =
-- Nix returns attribute names lexicographically sorted; the C array is in
-- interned-symbol order, so sort here (consistent with builtins.attrValues,
-- which sorts via attrSetElems).
let thunks = map (evaluated . mkStr) (sort (attrSetKeys attrs))
in pure (VList (clistFromThunks (map thunkToCPtr thunks)))
builtinAttrNames other =
throwEvalError ("builtins.attrNames: expected a set, got " <> typeName other)
builtinAttrValues :: (MonadEval m) => NixValue -> m NixValue
builtinAttrValues (VAttrs attrs) =
pure (VList (clistFromThunks (map thunkToCPtr (attrSetElems attrs))))
builtinAttrValues other =
throwEvalError ("builtins.attrValues: expected a set, got " <> typeName other)
builtinListToAttrs :: (MonadEval m) => NixValue -> m NixValue
builtinListToAttrs (VList cl) = do
let thunks = map Thunk (clistThunks cl)
pairs <- mapM listToAttrsPair thunks
-- Nix listToAttrs uses first-wins: if duplicate name, first element wins.
let firstWins = Map.fromListWith (\_ kept -> kept) pairs
pure (VAttrs (attrSetFromMap firstWins))
builtinListToAttrs other =
throwEvalError ("builtins.listToAttrs: expected a list, got " <> typeName other)
-- | Extract { name, value } from a thunk for listToAttrs.
listToAttrsPair :: (MonadEval m) => Thunk -> m (Text, Thunk)
listToAttrsPair thunk = do
val <- force thunk
case val of
VAttrs attrs -> do
nameThunk <-
maybe (throwEvalError "builtins.listToAttrs: element missing 'name'") pure $
attrSetLookup "name" attrs
nameVal <- force nameThunk
case nameVal of
VStr keyName _ ->
case attrSetLookup "value" attrs of
Just valueThunk -> pure (keyName, valueThunk)
Nothing -> throwEvalError "builtins.listToAttrs: element missing 'value'"
_ -> throwEvalError "builtins.listToAttrs: 'name' must be a string"
_ -> throwEvalError "builtins.listToAttrs: element must be a set"
-- ---------------------------------------------------------------------------
-- Builtin implementations — attr set (arity 2)
-- ---------------------------------------------------------------------------
builtinHasAttr :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinHasAttr (VStr key _) (VAttrs attrs) =
pure (VBool (attrSetMember key attrs))
builtinHasAttr (VStr _ _) other =
throwEvalError ("builtins.hasAttr: expected a set, got " <> typeName other)
builtinHasAttr other _ =
throwEvalError ("builtins.hasAttr: expected a string, got " <> typeName other)
builtinGetAttr :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinGetAttr (VStr key _) (VAttrs attrs) =
case attrSetLookup key attrs of
Just thunk -> force thunk
Nothing -> throwEvalError ("builtins.getAttr: attribute '" <> key <> "' not found")
builtinGetAttr (VStr _ _) other =
throwEvalError ("builtins.getAttr: expected a set, got " <> typeName other)
builtinGetAttr other _ =
throwEvalError ("builtins.getAttr: expected a string, got " <> typeName other)
builtinRemoveAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinRemoveAttrs (VAttrs attrs) (VList cl) = do
let thunks = map Thunk (clistThunks cl)
keys <- mapM forceToString thunks
pure (VAttrs (attrSetRemoveKeys keys attrs))
where
forceToString thunk = do
val <- force thunk
case val of
VStr s _ -> pure s
_ -> throwEvalError "builtins.removeAttrs: key list must contain strings"
builtinRemoveAttrs (VAttrs _) other =
throwEvalError ("builtins.removeAttrs: expected a list, got " <> typeName other)
builtinRemoveAttrs other _ =
throwEvalError ("builtins.removeAttrs: expected a set, got " <> typeName other)
builtinIntersectAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinIntersectAttrs (VAttrs a) (VAttrs b) =
-- Iterate keys of 'a' (typically the smaller set, e.g. functionArgs)
-- and point-lookup each in 'b' (typically the large set, e.g. nixpkgs).
-- This avoids materializing all thunks in 'b'.
let keysA = attrSetKeys a
result = Map.fromList [(k, thunk) | k <- keysA, Just thunk <- [attrSetLookup k b]]
in pure (VAttrs (attrSetFromMap result))
builtinIntersectAttrs (VAttrs _) other =
throwEvalError ("builtins.intersectAttrs: expected a set, got " <> typeName other)
builtinIntersectAttrs other _ =
throwEvalError ("builtins.intersectAttrs: expected a set, got " <> typeName other)
builtinCatAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinCatAttrs (VStr key _) (VList cl) = do
let thunks = map Thunk (clistThunks cl)
vals <- catAttrsCollect key thunks
pure (VList (clistFromThunks (map thunkToCPtr vals)))
builtinCatAttrs (VStr _ _) other =
throwEvalError ("builtins.catAttrs: expected a list, got " <> typeName other)
builtinCatAttrs other _ =
throwEvalError ("builtins.catAttrs: expected a string, got " <> typeName other)
-- | Collect values for a given key from a list of attrsets.
-- Tail-recursive with accumulator to avoid stack overflow on large lists.
catAttrsCollect :: (MonadEval m) => Text -> [Thunk] -> m [Thunk]
catAttrsCollect key = go []
where
go !acc [] = pure (reverse acc)
go !acc (thunk : rest) = do
val <- force thunk
case val of
VAttrs attrs ->
case attrSetLookup key attrs of
Just found -> go (found : acc) rest
Nothing -> go acc rest
_ -> throwEvalError "builtins.catAttrs: list element must be a set"
-- ---------------------------------------------------------------------------
-- Builtin implementations — list higher-order (arity 2)
-- ---------------------------------------------------------------------------
builtinMap :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinMap func (VList cl) =
-- Lazy: each element is a deferred application, forced only on demand.
let thunks = map Thunk (clistThunks cl)
in pure (VList (clistFromThunks (map (thunkToCPtr . deferApply func) thunks)))
builtinMap _ other =
throwEvalError ("builtins.map: expected a list, got " <> typeName other)
builtinFilter :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinFilter predFn (VList cl) = do
let thunks = map Thunk (clistThunks cl)
filtered <- filterThunks predFn thunks
pure (VList (clistFromThunks (map thunkToCPtr filtered)))
builtinFilter _ other =
throwEvalError ("builtins.filter: expected a list, got " <> typeName other)
filterThunks :: (MonadEval m) => NixValue -> [Thunk] -> m [Thunk]
filterThunks _ [] = pure []
filterThunks predFn (thunk : rest) = do
val <- force thunk
result <- applyValue predFn val
case result of
VBool True -> (thunk :) <$> filterThunks predFn rest
VBool False -> filterThunks predFn rest
_ -> throwEvalError "builtins.filter: predicate must return a bool"
builtinGenList :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinGenList func (VInt n)
| n < 0 = throwEvalError "builtins.genList: length must be non-negative"
| otherwise =
-- Lazy: each element is a deferred @f i@, forced only on demand.
-- Slot 0 = function.
let fnThunk = evaluated func
(sp, sc) = buildCSlots [fnThunk]
env = newMinimalEnv sp sc
mkIndexThunk i = mkThunk env (EApp (EResolvedVar 0 0) (ELit (NixInt i)))
in pure (VList (clistFromThunks (map (thunkToCPtr . mkIndexThunk) [0 .. n - 1])))
builtinGenList _ other =
throwEvalError ("builtins.genList: expected an integer, got " <> typeName other)
builtinSort :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinSort comparator (VList cl) = do
let thunks = map Thunk (clistThunks cl)
vals <- mapM force thunks
sorted <- mergeSort comparator vals
pure (VList (clistFromThunks (map (thunkToCPtr . evaluated) sorted)))
builtinSort _ other =
throwEvalError ("builtins.sort: expected a list, got " <> typeName other)
-- | Stable O(n log n) merge sort using a user-supplied comparator.
-- The comparator takes two args (curried) and returns bool.
mergeSort :: (MonadEval m) => NixValue -> [NixValue] -> m [NixValue]
mergeSort _ [] = pure []
mergeSort _ [x] = pure [x]
mergeSort cmp xs = do
let half = length xs `div` 2
(left, right) = splitAt half xs
sortedLeft <- mergeSort cmp left
sortedRight <- mergeSort cmp right
mergeSorted cmp sortedLeft sortedRight
mergeSorted :: (MonadEval m) => NixValue -> [NixValue] -> [NixValue] -> m [NixValue]
mergeSorted _ [] ys = pure ys
mergeSorted _ xs [] = pure xs
mergeSorted cmp (x : xs) (y : ys) = do
partial <- applyValue cmp x
result <- applyValue partial y
case result of
VBool True -> (x :) <$> mergeSorted cmp xs (y : ys)
VBool False -> (y :) <$> mergeSorted cmp (x : xs) ys
_ -> throwEvalError "builtins.sort: comparator must return a bool"
builtinConcatMap :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinConcatMap func (VList cl) = do
-- Semi-eager: must force each application to discover list structure for
-- concatenation, but element thunks within those sub-lists stay lazy.
let thunks = map Thunk (clistThunks cl)
deferredApps = map (deferApply func) thunks
results <- mapM force deferredApps
concatted <- mapM extractList results
pure (VList (clistFromThunks (map thunkToCPtr (concat concatted))))
builtinConcatMap _ other =
throwEvalError ("builtins.concatMap: expected a list, got " <> typeName other)
extractList :: (MonadEval m) => NixValue -> m [Thunk]
extractList (VList cl) = pure (map Thunk (clistThunks cl))
extractList other =
throwEvalError ("builtins.concatMap: function must return a list, got " <> typeName other)
builtinAny :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinAny predFn (VList cl) = do
let thunks = map Thunk (clistThunks cl)
result <- anyThunk predFn thunks
pure (VBool result)
builtinAny _ other =
throwEvalError ("builtins.any: expected a list, got " <> typeName other)
anyThunk :: (MonadEval m) => NixValue -> [Thunk] -> m Bool
anyThunk _ [] = pure False
anyThunk predFn (thunk : rest) = do
val <- force thunk
result <- applyValue predFn val
case result of
VBool True -> pure True
VBool False -> anyThunk predFn rest
_ -> throwEvalError "builtins.any: predicate must return a bool"
builtinAll :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinAll predFn (VList cl) = do
let thunks = map Thunk (clistThunks cl)
result <- allThunk predFn thunks
pure (VBool result)
builtinAll _ other =
throwEvalError ("builtins.all: expected a list, got " <> typeName other)
allThunk :: (MonadEval m) => NixValue -> [Thunk] -> m Bool
allThunk _ [] = pure True
allThunk predFn (thunk : rest) = do
val <- force thunk
result <- applyValue predFn val
case result of
VBool True -> allThunk predFn rest
VBool False -> pure False
_ -> throwEvalError "builtins.all: predicate must return a bool"
builtinElem :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinElem needle (VList cl) = do
let thunks = map Thunk (clistThunks cl)
found <- elemCheck needle thunks
pure (VBool found)
builtinElem _ other =
throwEvalError ("builtins.elem: expected a list, got " <> typeName other)
elemCheck :: (MonadEval m) => NixValue -> [Thunk] -> m Bool
elemCheck _ [] = pure False
elemCheck needle (thunk : rest) = do
val <- force thunk
eq <- nixEqual force needle val
if eq then pure True else elemCheck needle rest
builtinElemAt :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinElemAt (VList cl) (VInt idx)
| idx < 0 || fromIntegral idx >= clistLen cl = elemAtOOB idx cl
| otherwise =
-- O(1) direct C array access instead of materializing the whole list
let ptr = unsafePerformIO (clistGet (unCList cl) (fromIntegral idx))
in force (Thunk ptr)
where
elemAtOOB i c =
throwEvalError
( "builtins.elemAt: index "
<> T.pack (show i)
<> " out of bounds for list of length "
<> T.pack (show (clistLen c))
)
builtinElemAt (VList _) other =
throwEvalError ("builtins.elemAt: expected an integer, got " <> typeName other)
builtinElemAt other _ =
throwEvalError ("builtins.elemAt: expected a list, got " <> typeName other)
builtinPartition :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinPartition predFn (VList cl) = do
let thunks = map Thunk (clistThunks cl)
(rightThunks, wrongThunks) <- partitionThunks predFn thunks
pure
( VAttrs
( attrSetFromMap $
Map.fromList
[ ("right", evaluated (VList (clistFromThunks (map thunkToCPtr rightThunks)))),
("wrong", evaluated (VList (clistFromThunks (map thunkToCPtr wrongThunks))))
]
)
)
builtinPartition _ other =
throwEvalError ("builtins.partition: expected a list, got " <> typeName other)
-- | Tail-recursive partition with accumulator to avoid stack overflow.
partitionThunks :: (MonadEval m) => NixValue -> [Thunk] -> m ([Thunk], [Thunk])
partitionThunks predFn = go [] []
where
go !rs !ws [] = pure (reverse rs, reverse ws)
go !rs !ws (thunk : rest) = do
val <- force thunk
result <- applyValue predFn val
case result of
VBool True -> go (thunk : rs) ws rest
VBool False -> go rs (thunk : ws) rest
_ -> throwEvalError "builtins.partition: predicate must return a bool"
builtinGroupBy :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinGroupBy func (VList cl) = do
let thunks = map Thunk (clistThunks cl)
groups <- groupByCollect func thunks Map.empty
pure (VAttrs (attrSetFromMap (Map.map (evaluated . VList . clistFromThunks . map thunkToCPtr . reverse) groups)))
builtinGroupBy _ other =
throwEvalError ("builtins.groupBy: expected a list, got " <> typeName other)
groupByCollect ::
(MonadEval m) =>
NixValue ->
[Thunk] ->
Map Text [Thunk] ->
m (Map Text [Thunk])
groupByCollect _ [] acc = pure acc
groupByCollect func (thunk : rest) acc = do
val <- force thunk
result <- applyValue func val
case result of
VStr key _ ->
groupByCollect func rest (Map.insertWith (++) key [thunk] acc)
_ -> throwEvalError "builtins.groupBy: function must return a string"
-- ---------------------------------------------------------------------------
-- Builtin implementations — string (arity 2)
-- ---------------------------------------------------------------------------
builtinConcatStringsSep :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinConcatStringsSep (VStr sep sepCtx) (VList cl) = do
let thunks = map Thunk (clistThunks cl)
pairs <- mapM forceToStrCtx thunks
let texts = map fst pairs
mergedCtx = sepCtx <> mconcat (map snd pairs)
pure (VStr (T.intercalate sep texts) mergedCtx)
where
forceToStrCtx thunk = do
val <- force thunk
-- Nix coerces list elements to strings (paths, derivations, etc.).
-- concatStringsSep is strict (coerceMore=False): non-string scalars error.
coerceToString False force applyValue val
builtinConcatStringsSep (VStr _ _) other =
throwEvalError ("builtins.concatStringsSep: expected a list, got " <> typeName other)
builtinConcatStringsSep other _ =
throwEvalError ("builtins.concatStringsSep: expected a string, got " <> typeName other)
-- ---------------------------------------------------------------------------
-- Builtin implementations — arity 3
-- ---------------------------------------------------------------------------
builtinFoldl :: (MonadEval m) => NixValue -> NixValue -> NixValue -> m NixValue
builtinFoldl op initial (VList cl) =
foldlStrict op initial (map Thunk (clistThunks cl))
builtinFoldl _ _ other =
throwEvalError ("builtins.foldl': expected a list, got " <> typeName other)
-- | Strict left fold: apply @op acc elem@ for each element.
-- @op@ is curried so we call @applyValue op acc@ then @applyValue partial elem@.
foldlStrict :: (MonadEval m) => NixValue -> NixValue -> [Thunk] -> m NixValue
foldlStrict _ acc [] = pure acc
foldlStrict op acc (thunk : rest) = do
val <- force thunk
partial <- applyValue op acc
stepped <- applyValue partial val
foldlStrict op stepped rest
builtinSubstring :: (MonadEval m) => NixValue -> NixValue -> NixValue -> m NixValue
builtinSubstring (VInt start) (VInt len) (VStr s ctx)
| start < 0 = throwEvalError "builtins.substring: negative start position"
| otherwise =
let startPos = fromIntegral start
-- Nix clamps len to available length (negative len means rest of string)
available = T.length s - startPos
clampedLen =
if len < 0
then available
else min (fromIntegral len) available
in -- Context is preserved through substring (matching real Nix).
pure (VStr (T.take clampedLen (T.drop startPos s)) ctx)
builtinSubstring _ _ (VStr _ _) =
throwEvalError "builtins.substring: start and length must be integers"
builtinSubstring _ _ other =
throwEvalError ("builtins.substring: expected a string, got " <> typeName other)
-- ---------------------------------------------------------------------------
-- Builtin helpers
-- ---------------------------------------------------------------------------
-- | Build a thunk that defers @f arg@ — the application only happens when
-- the thunk is forced. Reuses the existing eval machinery via a synthetic
-- @EApp (EResolvedVar 0 0) (EResolvedVar 0 1)@ in a self-contained env.
-- Slot 0 = function, slot 1 = argument.
deferApply :: NixValue -> Thunk -> Thunk
deferApply func argThunk =
let (sp, sc) = buildCSlots [evaluated func, argThunk]
env = newMinimalEnv sp sc
in mkSyntheticThunk env deferApplyExpr
-- | Shared expression for 'deferApply'. Allocated once as a CAF.
deferApplyExpr :: Expr
deferApplyExpr = EApp (EResolvedVar 0 0) (EResolvedVar 0 1)
{-# NOINLINE deferApplyExpr #-}
-- | 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 cl) = do
let thunks = map Thunk (clistThunks cl)
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 True force applyValue other
-- | Coerce a value to a string for a DERIVATION field (an env value or an
-- arg). Like 'coerceToStringPermissive', but a path literal is copied into
-- the store: it becomes its source store path, with that path added to the
-- string context so it lands in the derivation's @inputSrcs@ — matching C++
-- Nix's copy-to-store coercion of paths in derivation arguments/environment.
coerceToStoreString :: (MonadEval m) => NixValue -> m (Text, StringContext)
coerceToStoreString (VPath p) = do
spText <- storeSourcePath p
case parseStorePath defaultStoreDir spText of
Just sp -> pure (spText, StringContext (Set.singleton (SCPlain sp)))
Nothing -> pure (spText, mempty)
coerceToStoreString (VList cl) = do
let thunks = map Thunk (clistThunks cl)
parts <- mapM (force >=> coerceToStoreString) thunks
pure (T.intercalate " " (map fst parts), mconcat (map snd parts))
coerceToStoreString other = coerceToStringPermissive other
-- | Coerce a value for string interpolation (@"${...}"@). Like
-- 'coerceToString', but a path literal is copied into the store and replaced by
-- its source store path (with context) — matching C++ Nix, where interpolation
-- uses @copyToStore = true@, unlike 'builtins.toString', which does not copy.
coerceToStringInterp :: (MonadEval m) => NixValue -> m (Text, StringContext)
coerceToStringInterp (VPath p) = do
spText <- storeSourcePath p
case parseStorePath defaultStoreDir spText of
Just sp -> pure (spText, StringContext (Set.singleton (SCPlain sp)))
Nothing -> pure (spText, mempty)
coerceToStringInterp other = coerceToString False force applyValue other
-- | The current system platform string.
currentSystemStr :: Text
currentSystemStr = case (System.Info.arch, System.Info.os) of
("x86_64", "mingw32") -> "x86_64-windows"
("x86_64", "darwin") -> "x86_64-darwin"
("aarch64", "darwin") -> "aarch64-darwin"
("aarch64", "linux") -> "aarch64-linux"
("x86_64", "linux") -> "x86_64-linux"
(arch, os) -> T.pack arch <> "-" <> T.pack os
-- | Store dir with trailing slash, for building store paths.
storeDirPrefix :: Text
storeDirPrefix = defaultStoreDirText <> "/"
-- ---------------------------------------------------------------------------
-- Builtin implementations — numeric + context
-- ---------------------------------------------------------------------------
isPathVal :: NixValue -> Bool
isPathVal (VPath _) = True
isPathVal _ = False
builtinCeil :: (MonadEval m) => NixValue -> m NixValue
builtinCeil (VFloat f) = pure (VInt (ceiling f))
builtinCeil (VInt n) = pure (VInt n)
builtinCeil other = throwEvalError ("builtins.ceil: expected a number, got " <> typeName other)
builtinFloor :: (MonadEval m) => NixValue -> m NixValue
builtinFloor (VFloat f) = pure (VInt (floor f))
builtinFloor (VInt n) = pure (VInt n)
builtinFloor other = throwEvalError ("builtins.floor: expected a number, got " <> typeName other)
builtinDiscardContext :: (MonadEval m) => NixValue -> m NixValue
builtinDiscardContext (VStr s _) = pure (mkStr s)
builtinDiscardContext other =
throwEvalError ("builtins.unsafeDiscardStringContext: expected a string, got " <> typeName other)
-- | Strip only derivation output dependencies (SCDrvOutput, SCAllOutputs),
-- keeping plain store path references (SCPlain).
builtinDiscardOutputDep :: (MonadEval m) => NixValue -> m NixValue
builtinDiscardOutputDep (VStr s (StringContext ctx)) =
let kept = Set.filter isPlain ctx
in pure (VStr s (StringContext kept))
where
isPlain (SCPlain _) = True
isPlain _ = False
builtinDiscardOutputDep other =
throwEvalError ("builtins.unsafeDiscardOutputDependency: expected a string, got " <> typeName other)
-- | Check whether a string has any context elements.
builtinHasContext :: (MonadEval m) => NixValue -> m NixValue
builtinHasContext (VStr _ ctx) = pure (VBool (ctx /= emptyContext))
builtinHasContext other =
throwEvalError ("builtins.hasContext: expected a string, got " <> typeName other)
-- | Return the context of a string as an attrset.
--
-- Each key is a store path string. Each value is an attrset with:
-- - @path@: true if there's a SCPlain reference
-- - @allOutputs@: true if there's a SCAllOutputs reference
-- - @outputs@: list of output names from SCDrvOutput references
builtinGetContext :: (MonadEval m) => NixValue -> m NixValue
builtinGetContext (VStr _ (StringContext ctx)) = do
let grouped = groupContextByPath (Set.toList ctx)
attrMap = Map.map contextEntryToAttrs grouped
pure (VAttrs (attrSetFromMap attrMap))
builtinGetContext other =
throwEvalError ("builtins.getContext: expected a string, got " <> typeName other)
-- | Intermediate representation for grouping context elements by store path.
data ContextEntry = ContextEntry
{ cePath :: !Bool,
ceAllOutputs :: !Bool,
ceOutputs :: ![Text]
}
-- | Group context elements by their store path.
groupContextByPath :: [StringContextElement] -> Map Text ContextEntry
groupContextByPath = foldl' addElement Map.empty
where
addElement acc (SCPlain sp) =
Map.insertWith mergeEntry (spToText sp) (ContextEntry True False []) acc
addElement acc (SCDrvOutput sp outName) =
Map.insertWith mergeEntry (spToText sp) (ContextEntry False False [outName]) acc
addElement acc (SCAllOutputs sp) =
Map.insertWith mergeEntry (spToText sp) (ContextEntry False True []) acc
mergeEntry new old =
ContextEntry
(cePath new || cePath old)
(ceAllOutputs new || ceAllOutputs old)
(ceOutputs new ++ ceOutputs old)
spToText sp =
T.pack (storePathToFilePath defaultStoreDir sp)
-- | Convert a ContextEntry to an attrset thunk.
contextEntryToAttrs :: ContextEntry -> Thunk
contextEntryToAttrs entry =
let fields =
[("path", evaluated (VBool True)) | cePath entry]
++ [("allOutputs", evaluated (VBool True)) | ceAllOutputs entry]
++ [("outputs", evaluated (VList (clistFromThunks [thunkToCPtr (evaluated (mkStr o)) | o <- ceOutputs entry]))) | not (null (ceOutputs entry))]
in evaluated (VAttrs (attrSetFromMap (Map.fromList fields)))
-- | Append context entries to a string from an attrset.
--
-- @builtins.appendContext string contextAttrset@ adds the specified
-- context elements to the string.
builtinAppendContext :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinAppendContext (VStr s ctx) (VAttrs contextAttrs) = do
newCtx <- parseContextAttrs (attrSetToMap contextAttrs)
pure (VStr s (ctx <> newCtx))
builtinAppendContext (VStr _ _) other =
throwEvalError ("builtins.appendContext: second argument must be a set, got " <> typeName other)
builtinAppendContext other _ =
throwEvalError ("builtins.appendContext: first argument must be a string, got " <> typeName other)
-- | Parse a context attrset into a StringContext.
-- Each key is a store path; each value is an attrset with optional
-- @path@, @allOutputs@, and @outputs@ fields.
parseContextAttrs :: (MonadEval m) => Map Text Thunk -> m StringContext
parseContextAttrs attrs = do
elements <- mapM parseOneCtx (Map.toList attrs)
pure (StringContext (Set.fromList (concat elements)))
where
parseOneCtx (pathText, thunk) = do
val <- force thunk
case val of
VAttrs inner -> do
let sp = case parseStorePath defaultStoreDir pathText of
Just parsed -> parsed
Nothing -> StorePath (T.take 32 (T.drop (T.length storeDirPrefix) pathText)) (T.drop 33 (T.drop (T.length storeDirPrefix) pathText))
hasPath <- getBoolAttr "path" inner
hasAllOuts <- getBoolAttr "allOutputs" inner
outNames <- getOutputsList inner
let pathElems = [SCPlain sp | hasPath]
allOutElems = [SCAllOutputs sp | hasAllOuts]
outElems = [SCDrvOutput sp o | o <- outNames]
pure (pathElems ++ allOutElems ++ outElems)
_ -> throwEvalError "builtins.appendContext: context entry must be a set"
getBoolAttr key attrs' = case attrSetLookup key attrs' of
Nothing -> pure False
Just thunk -> do
val <- force thunk
case val of
VBool b -> pure b
_ -> pure False
getOutputsList attrs' = case attrSetLookup "outputs" attrs' of
Nothing -> pure []
Just thunk -> do
val <- force thunk
case val of
VList cl -> mapM (forceToOutputName . Thunk) (clistThunks cl)
_ -> pure []
forceToOutputName thunk = do
val <- force thunk
case val of
VStr s _ -> pure s
_ -> throwEvalError "builtins.appendContext: output name must be a string"
builtinBaseNameOf :: (MonadEval m) => NixValue -> m NixValue
builtinBaseNameOf (VStr s ctx) = pure (VStr (lastComponent s) ctx)
builtinBaseNameOf (VPath p) = pure (mkStr (lastComponent p))
builtinBaseNameOf other =
throwEvalError ("builtins.baseNameOf: expected a string or path, got " <> typeName other)
lastComponent :: Text -> Text
lastComponent t = case T.splitOn "/" t of
[] -> t
parts -> case reverse (filter (not . T.null) parts) of
[] -> ""
(final : _) -> final
builtinDirOf :: (MonadEval m) => NixValue -> m NixValue
builtinDirOf (VStr s ctx) = pure (VStr (dirComponent s) ctx)
builtinDirOf (VPath p) = pure (VPath (dirComponent p))
builtinDirOf other =
throwEvalError ("builtins.dirOf: expected a string or path, got " <> typeName other)
dirComponent :: Text -> Text
dirComponent t =
let idx = T.findIndex (== '/') (T.reverse t)
in case idx of
Nothing -> "."
Just n -> T.take (T.length t - n - 1) t
builtinConcatLists :: (MonadEval m) => NixValue -> m NixValue
builtinConcatLists (VList cl) = do
let thunks = map Thunk (clistThunks cl)
sublists <- mapM forceThenExtractList thunks
pure (VList (clistFromThunks (concat sublists)))
where
forceThenExtractList thunk = do
val <- force thunk
case val of
VList innerCl -> pure (clistThunks innerCl)
_ -> throwEvalError "builtins.concatLists: element must be a list"
builtinConcatLists other =
throwEvalError ("builtins.concatLists: expected a list, got " <> typeName other)
builtinLessThan :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinLessThan a b = VBool <$> nixCompare force a b
-- ---------------------------------------------------------------------------
-- Builtin implementations — arithmetic + bitwise
-- ---------------------------------------------------------------------------
builtinAdd :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinAdd (VInt a) (VInt b) = pure (VInt (a + b))
builtinAdd (VInt a) (VFloat b) = pure (VFloat (fromIntegral a + b))
builtinAdd (VFloat a) (VInt b) = pure (VFloat (a + fromIntegral b))
builtinAdd (VFloat a) (VFloat b) = pure (VFloat (a + b))
builtinAdd l r = throwEvalError ("builtins.add: expected numbers, got " <> typeName l <> " and " <> typeName r)
builtinSub :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinSub (VInt a) (VInt b) = pure (VInt (a - b))
builtinSub (VInt a) (VFloat b) = pure (VFloat (fromIntegral a - b))
builtinSub (VFloat a) (VInt b) = pure (VFloat (a - fromIntegral b))
builtinSub (VFloat a) (VFloat b) = pure (VFloat (a - b))
builtinSub l r = throwEvalError ("builtins.sub: expected numbers, got " <> typeName l <> " and " <> typeName r)
builtinMul :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinMul (VInt a) (VInt b) = pure (VInt (a * b))
builtinMul (VInt a) (VFloat b) = pure (VFloat (fromIntegral a * b))
builtinMul (VFloat a) (VInt b) = pure (VFloat (a * fromIntegral b))
builtinMul (VFloat a) (VFloat b) = pure (VFloat (a * b))
builtinMul l r = throwEvalError ("builtins.mul: expected numbers, got " <> typeName l <> " and " <> typeName r)
builtinDiv :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinDiv _ (VInt 0) = throwEvalError "builtins.div: division by zero"
builtinDiv (VInt a) (VInt b)
| a == minBound && b == -1 = pure (VInt minBound)
| otherwise = pure (VInt (quot a b))
builtinDiv _ (VFloat 0) = throwEvalError "builtins.div: division by zero"
builtinDiv (VInt a) (VFloat b) = pure (VFloat (fromIntegral a / b))
builtinDiv (VFloat a) (VInt b) = pure (VFloat (a / fromIntegral b))
builtinDiv (VFloat a) (VFloat b) = pure (VFloat (a / b))
builtinDiv l r = throwEvalError ("builtins.div: expected numbers, got " <> typeName l <> " and " <> typeName r)
builtinMod :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinMod _ (VInt 0) = throwEvalError "builtins.mod: division by zero"
builtinMod (VInt a) (VInt b)
| a == minBound && b == -1 = pure (VInt 0)
| otherwise = pure (VInt (rem a b))
builtinMod l r = throwEvalError ("builtins.mod: expected two integers, got " <> typeName l <> " and " <> typeName r)
builtinMin :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinMin a b = do
aIsLess <- nixCompare force a b
pure (if aIsLess then a else b)
builtinMax :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinMax a b = do
aIsLess <- nixCompare force a b
pure (if aIsLess then b else a)
builtinBitAnd :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinBitAnd (VInt a) (VInt b) = pure (VInt (a .&. b))
builtinBitAnd _ _ = throwEvalError "builtins.bitAnd: expected two integers"
builtinBitOr :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinBitOr (VInt a) (VInt b) = pure (VInt (a .|. b))
builtinBitOr _ _ = throwEvalError "builtins.bitOr: expected two integers"
builtinBitXor :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinBitXor (VInt a) (VInt b) = pure (VInt (xor a b))
builtinBitXor _ _ = throwEvalError "builtins.bitXor: expected two integers"
-- ---------------------------------------------------------------------------
-- Builtin implementations — attr set higher-order
-- ---------------------------------------------------------------------------
builtinMapAttrs :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinMapAttrs func (VAttrs attrs) =
-- Each attr value is a deferred @f key val@, forced only on demand.
-- Eagerly builds all thunks via attrSetMapWithKey — with C arena thunks
-- (~16 bytes each), this is cheaper than the former MappedAttrs overhead
-- and keeps all data off the GHC heap.
-- Slot 0 = function, slot 1 = key, slot 2 = value.
pure (VAttrs (attrSetMapWithKey deferAttr attrs))
where
deferAttr key valThunk =
let (sp, sc) = buildCSlots [evaluated func, evaluated (mkStr key), valThunk]
env = newMinimalEnv sp sc
in mkSyntheticThunk env mapAttrsExpr
builtinMapAttrs _ other =
throwEvalError ("builtins.mapAttrs: expected a set, got " <> typeName other)
-- | Shared expression for 'builtinMapAttrs'. Allocated once as a CAF.
mapAttrsExpr :: Expr
mapAttrsExpr = EApp (EApp (EResolvedVar 0 0) (EResolvedVar 0 1)) (EResolvedVar 0 2)
{-# NOINLINE mapAttrsExpr #-}
builtinFunctionArgs :: (MonadEval m) => NixValue -> m NixValue
builtinFunctionArgs (VLambda _ formals _) = pure (formalsToAttrs formals)
builtinFunctionArgs (VBuiltin _ _) = pure (VAttrs (attrSetFromMap Map.empty))
-- Callable sets with __functionArgs metadata (from setFunctionArgs).
builtinFunctionArgs (VAttrs attrs)
| Just faThunk <- attrSetLookup "__functionArgs" attrs = force faThunk
builtinFunctionArgs other =
throwEvalError ("builtins.functionArgs: expected a function, got " <> typeName other)
formalsToAttrs :: EvalFormals -> NixValue
formalsToAttrs (EFName _) = VAttrs (attrSetFromMap Map.empty)
formalsToAttrs (EFSet formals _) = formalsListToAttrs formals
formalsToAttrs (EFNamedSet _ formals _) = formalsListToAttrs formals
formalsListToAttrs :: [EvalFormal] -> NixValue
formalsListToAttrs formals =
VAttrs $
attrSetFromMap $
Map.fromList
[(efName f, evaluated (VBool (isJust (efDefault 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) =
-- Closure env holds the function at slot 0. The __functor lambda
-- body is EResolvedVar 1 0: level 1 (past _self's slot), index 0.
let (sp, sc) = buildCSlots [evaluated func]
closureEnv = newMinimalEnv sp sc
bodyBcIdx = unsafePerformIO (compileExpr (EResolvedVar 1 0))
-- __functor = self: __fn (ignores self, returns the original function)
functorLambda = VLambda closureEnv (EFName "_self") bodyBcIdx
in pure $
VAttrs $
attrSetFromMap $
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 cl) = do
let thunks = map Thunk (clistThunks cl)
attrSets <- mapM forceToAttrSet thunks
let merged = mergeAllAttrs attrSets
-- Lazy: each result is a deferred f(name)(values) thunk, not eagerly
-- evaluated. Critical for nixpkgs evalModules fixpoint — config is a
-- self-referencing lazy attrset that must be COMPUTED (holding the lazy
-- result) before any individual attribute thunks are forced.
resultPairs = map (deferZip func) (Map.toList merged)
pure (VAttrs (attrSetFromMap (Map.fromList resultPairs)))
where
forceToAttrSet thunk = do
val <- force thunk
case val of
VAttrs attrs -> pure (attrSetToMap attrs)
_ -> throwEvalError "builtins.zipAttrsWith: list element must be a set"
mergeAllAttrs = foldl' (\acc m -> Map.unionWith (++) acc (Map.map (: []) m)) Map.empty
-- Slot 0 = function, slot 1 = name, slot 2 = values list.
deferZip fn (key, thunkList) =
let valueList = VList (clistFromThunks (map thunkToCPtr thunkList))
(slots, slotCount) = buildCSlots [evaluated fn, evaluated (mkStr key), evaluated valueList]
env = newMinimalEnv slots slotCount
in (key, mkSyntheticThunk env mapAttrsExpr)
builtinZipAttrsWith _ other =
throwEvalError ("builtins.zipAttrsWith: expected a list, got " <> typeName other)
-- ---------------------------------------------------------------------------
-- Builtin implementations — string manipulation
-- ---------------------------------------------------------------------------
builtinReplaceStrings ::
(MonadEval m) => NixValue -> NixValue -> NixValue -> m NixValue
builtinReplaceStrings (VList fromCl) (VList toCl) (VStr input inputCtx) = do
let fromThunks = map Thunk (clistThunks fromCl)
toThunks = map Thunk (clistThunks toCl)
froms <- mapM forceStr fromThunks
toStrs <- mapM forceStr toThunks
when (length froms /= length toStrs) $
throwEvalError "builtins.replaceStrings: 'from' and 'to' must have the same length"
let fromTexts = map fst froms
toTexts = map fst toStrs
-- Nix semantics: input context + 'to' string contexts (not 'from').
-- Ideally only 'to' contexts for patterns that matched, but including
-- all 'to' contexts is the common implementation.
mergedCtx = inputCtx <> mconcat (map snd toStrs)
pairs = zip fromTexts toTexts
pure (VStr (replaceAll pairs input) mergedCtx)
where
forceStr thunk = do
val <- force thunk
case val of
VStr s ctx -> pure (s, ctx)
_ -> throwEvalError "builtins.replaceStrings: elements must be strings"
builtinReplaceStrings _ _ (VStr _ _) =
throwEvalError "builtins.replaceStrings: first two arguments must be lists"
builtinReplaceStrings _ _ other =
throwEvalError ("builtins.replaceStrings: expected a string, got " <> typeName other)
-- | Replace all occurrences, O(n) via chunk list + T.concat.
replaceAll :: [(Text, Text)] -> Text -> Text
replaceAll pairs input = T.concat (go input)
where
go remaining
| T.null remaining =
-- At end of string, still check for empty-from match
case findMatch pairs remaining of
Just (replacement, _, _) -> [replacement]
Nothing -> []
| otherwise = case findMatch pairs remaining of
Just (replacement, rest, matched) ->
if T.null matched
then case T.uncons remaining of
-- empty-from: insert replacement then advance 1 char
Just (ch, after) -> replacement : T.singleton ch : go after
Nothing -> [replacement]
else replacement : go rest
Nothing -> case T.uncons remaining of
Just (ch, after) -> T.singleton ch : go after
Nothing -> []
findMatch [] _ = Nothing
findMatch ((from, to) : rest) txt
| T.null from = Just (to, txt, from)
| Just suffix <- T.stripPrefix from txt = Just (to, suffix, from)
| otherwise = findMatch rest txt
-- ---------------------------------------------------------------------------
-- Builtin implementations — regex (POSIX ERE via regex-tdfa)
-- ---------------------------------------------------------------------------
-- ---------------------------------------------------------------------------
-- Regex compilation cache
-- ---------------------------------------------------------------------------
-- | Global regex compilation cache. Keyed by the raw pattern string
-- (including anchoring for match). Idempotent memoization via
-- unsafePerformIO — same rationale as thunk memoization.
{-# NOINLINE regexCacheRef #-}
regexCacheRef :: IORef (Map Text RE.Regex)
regexCacheRef = unsafePerformIO (newIORef Map.empty)
-- | Compile a regex, using the global cache to avoid recompilation.
-- Returns Nothing for invalid patterns. NOINLINE prevents GHC from
-- inlining and floating the unsafePerformIO reads.
{-# NOINLINE cachedCompileRegex #-}
cachedCompileRegex :: Text -> Maybe RE.Regex
cachedCompileRegex pat =
unsafePerformIO $ do
cache <- atomicModifyIORef' regexCacheRef (\c -> (c, c))
case Map.lookup pat cache of
Just compiled -> pure (Just compiled)
Nothing -> case RE.makeRegexM (T.unpack pat) :: Maybe RE.Regex of
Nothing -> pure Nothing
Just compiled -> do
atomicModifyIORef' regexCacheRef (\c -> (Map.insert pat compiled c, ()))
pure (Just compiled)
-- ---------------------------------------------------------------------------
-- Regex builtins
-- ---------------------------------------------------------------------------
-- | @builtins.match regex str@: match a POSIX ERE against a string.
-- The regex is implicitly anchored (must match the entire string).
-- Returns @null@ if no match, or a list of capture group strings
-- (empty string for unmatched optional groups).
builtinMatch :: (MonadEval m) => NixValue -> NixValue -> m NixValue
-- Pre-compiled path: regex was compiled at partial-application time.
builtinMatch (VCompiledRegex (CompiledRegex _ compiled)) (VStr str _) =
matchWithCompiled compiled str
-- Direct 2-arg call: use global compilation cache.
builtinMatch (VStr regex _) (VStr str _) =
let anchored = "^" <> regex <> "$"
in case cachedCompileRegex anchored of
Nothing -> throwEvalError ("builtins.match: invalid regex: " <> regex)
Just compiled -> matchWithCompiled compiled str
builtinMatch (VStr _ _) other =
throwEvalError ("builtins.match: expected a string, got " <> typeName other)
builtinMatch (VCompiledRegex _) other =
throwEvalError ("builtins.match: expected a string, got " <> typeName other)
builtinMatch other _ =
throwEvalError ("builtins.match: expected a string (regex), got " <> typeName other)
-- | Shared match logic for pre-compiled and freshly-compiled regex paths.
matchWithCompiled :: (MonadEval m) => RE.Regex -> Text -> m NixValue
matchWithCompiled compiled str =
let matches = matchAllText compiled (T.unpack str)
in case matches of
[] -> pure VNull
(match : _) ->
-- match is an Array of (String, (offset, len)) pairs.
-- Index 0 is the full match; indices 1.. are capture groups.
let groups = Array.elems match
-- Skip index 0 (full match) — return only capture groups.
captureGroups = drop 1 groups
-- A non-participating capture group has offset (-1); C++ Nix
-- yields null for it, not the empty string.
toThunk (s, (off, _)) =
if off < 0 then evaluated VNull else evaluated (mkStr (T.pack s))
in pure (VList (clistFromThunks (map (thunkToCPtr . toThunk) captureGroups)))
-- | @builtins.split regex str@: split a string by a POSIX ERE.
-- Returns an alternating list of non-matched strings and match-group lists.
-- Example: @split "(x)" "axbxc"@ → @["a" ["x"] "b" ["x"] "c"]@
builtinSplit :: (MonadEval m) => NixValue -> NixValue -> m NixValue
-- Pre-compiled path: regex was compiled at partial-application time.
builtinSplit (VCompiledRegex (CompiledRegex _ compiled)) (VStr str _) =
splitWithCompiled compiled str
-- Direct 2-arg call: use global compilation cache.
builtinSplit (VStr regex _) (VStr str _) =
case cachedCompileRegex regex of
Nothing -> throwEvalError ("builtins.split: invalid regex: " <> regex)
Just compiled -> splitWithCompiled compiled str
builtinSplit (VStr _ _) other =
throwEvalError ("builtins.split: expected a string, got " <> typeName other)
builtinSplit (VCompiledRegex _) other =
throwEvalError ("builtins.split: expected a string, got " <> typeName other)
builtinSplit other _ =
throwEvalError ("builtins.split: expected a string (regex), got " <> typeName other)
-- | Shared split logic for pre-compiled and freshly-compiled regex paths.
splitWithCompiled :: (MonadEval m) => RE.Regex -> Text -> m NixValue
splitWithCompiled compiled str =
let allMatches = matchAllText compiled (T.unpack str)
strText = T.unpack str
result = buildSplitResult strText 0 allMatches
in pure (VList (clistFromThunks (map thunkToCPtr result)))
-- | Build the alternating list for builtins.split.
buildSplitResult :: String -> Int -> [Array.Array Int (String, (Int, Int))] -> [Thunk]
buildSplitResult remaining pos [] =
-- No more matches — emit the rest of the string.
[evaluated (mkStr (T.pack (drop pos remaining)))]
buildSplitResult remaining pos (match : rest) =
let elems = Array.elems match
(_, (matchStart, matchLen)) = case elems of
(full : _) -> full
[] -> ("", (pos, 0)) -- defensive: should not happen from matchAllText
-- Text before this match
before = T.pack (take (matchStart - pos) (drop pos remaining))
-- Capture groups (indices 1..)
groups = drop 1 (Array.elems match)
-- A non-participating capture group has offset (-1) → null (as 'match').
groupThunks =
map (\(s, (off, _)) -> if off < 0 then evaluated VNull else evaluated (mkStr (T.pack s))) groups
-- Continue after this match
afterPos = matchStart + matchLen
in evaluated (mkStr before)
: evaluated (VList (clistFromThunks (map thunkToCPtr groupThunks)))
: buildSplitResult remaining afterPos rest
builtinCompareVersions :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinCompareVersions (VStr a _) (VStr b _) =
pure (VInt (compareVersionParts (splitVersionStr a) (splitVersionStr b)))
builtinCompareVersions _ _ = throwEvalError "builtins.compareVersions: expected two strings"
-- | Convert 'Ordering' to the Nix compareVersions convention: -1, 0, 1.
ordToNix :: Ordering -> Int64
ordToNix LT = -1
ordToNix EQ = 0
ordToNix GT = 1
compareVersionParts :: [Text] -> [Text] -> Int64
compareVersionParts [] [] = 0
-- When one version runs out, Nix pads the shorter side with an empty
-- component and keeps comparing — so "1.0" > "1.0pre" (empty sorts AFTER
-- "pre"), not "<" as a naive length comparison would give.
compareVersionParts [] (b : bs) =
case compareComponent "" b of
EQ -> compareVersionParts [] bs
cmp -> ordToNix cmp
compareVersionParts (a : as) [] =
case compareComponent a "" of
EQ -> compareVersionParts as []
cmp -> ordToNix cmp
compareVersionParts (a : as) (b : bs) =
case compareComponent a b of
EQ -> compareVersionParts as bs
cmp -> ordToNix cmp
compareComponent :: Text -> Text -> Ordering
compareComponent a b
| a == b = EQ
| allDigits a && allDigits b = compare (readInt a) (readInt b)
-- "pre" sorts before everything else (Nix pre-release convention)
| a == "pre" = LT
| b == "pre" = GT
| a == "" = LT
| b == "" = GT
-- Alphabetic components sort before numeric in Nix
| isAlphaComp a && allDigits b = LT
| allDigits a && isAlphaComp b = GT
| otherwise = compare a b
where
allDigits t = not (T.null t) && T.all isDigit t
isAlphaComp t = case T.uncons t of
Just (c, _) -> isAlpha c
Nothing -> False
readInt :: Text -> Int64
readInt = T.foldl' (\acc c -> acc * 10 + fromIntegral (digitToInt c)) (0 :: Int64)
splitVersionStr :: Text -> [Text]
splitVersionStr t
| T.null t = []
| otherwise =
let (component, rest) = spanComponent t
in component : splitVersionAfterComponent rest
splitVersionAfterComponent :: Text -> [Text]
splitVersionAfterComponent t = case T.uncons t of
Nothing -> []
Just ('.', rest) -> splitVersionStr rest
Just _ -> splitVersionStr t
spanComponent :: Text -> (Text, Text)
spanComponent t = case T.uncons t of
Nothing -> ("", "")
Just (c, rest)
| isDigit c -> T.span isDigit t
| isAlpha c -> T.span isAlpha t
| otherwise -> (T.singleton c, rest)
builtinSplitVersion :: (MonadEval m) => NixValue -> m NixValue
builtinSplitVersion (VStr s _) =
pure (VList (clistFromThunks (map (thunkToCPtr . evaluated . mkStr) (splitVersionComponents s))))
builtinSplitVersion other =
throwEvalError ("builtins.splitVersion: expected a string, got " <> typeName other)
splitVersionComponents :: Text -> [Text]
splitVersionComponents t = case T.uncons t of
Nothing -> []
Just ('.', rest) -> splitVersionComponents rest
Just (c, _)
| isDigit c ->
let (digits, rest) = T.span isDigit t
in digits : splitVersionComponents rest
| isAlpha c ->
let (alpha, rest) = T.span isAlpha t
in alpha : splitVersionComponents rest
| otherwise ->
-- Non-alphanumeric separator (e.g. '-', '_'): consume as single char
let (_, rest) = T.splitAt 1 t
in splitVersionComponents rest
builtinParseDrvName :: (MonadEval m) => NixValue -> m NixValue
builtinParseDrvName (VStr s _) =
let (name, version) = parseName s
in pure
( VAttrs
( attrSetFromMap $
Map.fromList
[ ("name", evaluated (mkStr name)),
("version", evaluated (mkStr version))
]
)
)
builtinParseDrvName other =
throwEvalError ("builtins.parseDrvName: expected a string, got " <> typeName other)
parseName :: Text -> (Text, Text)
parseName t =
case findVersionDash t 0 of
Nothing -> (t, "")
Just idx -> (T.take idx t, T.drop (idx + 1) t)
findVersionDash :: Text -> Int -> Maybe Int
findVersionDash t idx = case T.uncons (T.drop idx t) of
Nothing -> Nothing
Just ('-', after)
| Just (d, _) <- T.uncons after,
isDigit d ->
Just idx
Just _ -> findVersionDash t (idx + 1)
-- ---------------------------------------------------------------------------
-- Builtin implementations — serialization + hashing
-- ---------------------------------------------------------------------------
builtinToJSON :: (MonadEval m) => NixValue -> m NixValue
builtinToJSON val = do
(json, ctx) <- valueToJSON val
pure (VStr json ctx)
valueToJSON :: (MonadEval m) => NixValue -> m (Text, StringContext)
valueToJSON VNull = pure ("null", emptyContext)
valueToJSON (VBool True) = pure ("true", emptyContext)
valueToJSON (VBool False) = pure ("false", emptyContext)
valueToJSON (VInt n) = pure (T.pack (show n), emptyContext)
valueToJSON (VFloat f) = pure (formatNixFloat f, emptyContext)
valueToJSON (VStr s ctx) = pure (jsonEscapeString s, ctx)
valueToJSON (VList cl) = do
let thunks = map Thunk (clistThunks cl)
vals <- mapM force thunks
results <- mapM valueToJSON vals
let jsonVals = map fst results
ctx = mconcat (map snd results)
pure ("[" <> T.intercalate "," jsonVals <> "]", ctx)
valueToJSON (VAttrs attrs) =
-- C++ Nix serializes an attrset via __toString first, then outPath, and only
-- falls back to an object when neither is present.
case (attrSetLookup "__toString" attrs, attrSetLookup "outPath" attrs) of
(Nothing, Nothing) -> do
let m = attrSetToMap attrs
sortedKeys = Map.keys m
results <- mapM (jsonPair m) sortedKeys
let pairs = map fst results
ctx = mconcat (map snd results)
pure ("{" <> T.intercalate "," pairs <> "}", ctx)
_ -> do
(s, ctx) <- coerceToString True force applyValue (VAttrs attrs)
pure (jsonEscapeString s, ctx)
where
jsonPair attrMap key = case Map.lookup key attrMap of
Nothing -> pure ("", emptyContext)
Just thunk -> do
val <- force thunk
(jsonVal, ctx) <- valueToJSON val
pure (jsonEscapeString key <> ":" <> jsonVal, ctx)
valueToJSON (VPath p) = pure (jsonEscapeString p, emptyContext)
valueToJSON (VLambda {}) = throwEvalError "builtins.toJSON: cannot convert a function to JSON"
valueToJSON (VBuiltin _ _) = throwEvalError "builtins.toJSON: cannot convert a function to JSON"
valueToJSON (VDerivation _) = throwEvalError "builtins.toJSON: cannot convert a derivation to JSON"
valueToJSON (VCompiledRegex _) = throwEvalError "builtins.toJSON: cannot convert a function to JSON"
jsonEscapeString :: Text -> Text
jsonEscapeString s = "\"" <> T.concatMap escapeChar s <> "\""
where
escapeChar '"' = "\\\""
escapeChar '\\' = "\\\\"
escapeChar '\n' = "\\n"
escapeChar '\r' = "\\r"
escapeChar '\t' = "\\t"
escapeChar c
| ord c < 0x20 = "\\u" <> T.pack (padHex 4 (showHex' (ord c)))
| otherwise = T.singleton c
padHex n str = replicate (n - length str) '0' ++ str
showHex' 0 = "0"
showHex' num = go num ""
where
go 0 acc = acc
go v acc =
let (q, r) = quotRem v 16
in go q (hexDigit r : acc)
-- | Safe hex digit lookup (total for 0–15).
hexDigit :: Int -> Char
hexDigit n
| n >= 0 && n <= 9 = chr (ord '0' + n)
| n >= 10 && n <= 15 = chr (ord 'a' + n - 10)
| otherwise = '?' -- unreachable for valid hex
builtinFromJSON :: (MonadEval m) => NixValue -> m NixValue
builtinFromJSON (VStr s _) = case parseJSON (T.strip s) of
Just (val, rest)
| T.null (T.strip rest) -> pure val
| otherwise -> throwEvalError "builtins.fromJSON: trailing content after JSON value"
Nothing -> throwEvalError "builtins.fromJSON: invalid JSON"
builtinFromJSON other =
throwEvalError ("builtins.fromJSON: expected a string, got " <> typeName other)
parseJSON :: Text -> Maybe (NixValue, Text)
parseJSON t = case T.uncons (T.stripStart t) of
Nothing -> Nothing
Just ('n', rest)
| Just suffix <- T.stripPrefix "ull" rest -> Just (VNull, suffix)
Just ('t', rest)
| Just suffix <- T.stripPrefix "rue" rest -> Just (VBool True, suffix)
Just ('f', rest)
| Just suffix <- T.stripPrefix "alse" rest -> Just (VBool False, suffix)
Just ('"', _) -> parseJSONString (T.stripStart t)
Just ('[', rest) -> parseJSONArray rest
Just ('{', rest) -> parseJSONObject rest
Just (c, _)
| c == '-' || isDigit c -> parseJSONNumber (T.stripStart t)
_ -> Nothing
parseJSONString :: Text -> Maybe (NixValue, Text)
parseJSONString t = case T.uncons t of
Just ('"', rest) ->
let (strVal, remaining) = parseJSONStringContent rest
in Just (mkStr strVal, remaining)
_ -> Nothing
-- | Parse JSON string content, O(n) via chunk list + T.concat.
parseJSONStringContent :: Text -> (Text, Text)
parseJSONStringContent = go []
where
go !chunks t = case T.uncons t of
Nothing -> (T.concat (reverse chunks), "")
Just ('"', rest) -> (T.concat (reverse chunks), rest)
Just ('\\', rest) -> case T.uncons rest of
Just ('"', r) -> go ("\"" : chunks) r
Just ('\\', r) -> go ("\\" : chunks) r
Just ('/', r) -> go ("/" : chunks) r
Just ('n', r) -> go ("\n" : chunks) r
Just ('r', r) -> go ("\r" : chunks) r
Just ('t', r) -> go ("\t" : chunks) r
Just ('u', r) -> case parseHex4 r of
Just (hi, r2)
-- UTF-16 surrogate pair: high surrogate followed by \uXXXX low
| hi >= 0xD800 && hi <= 0xDBFF ->
case T.stripPrefix "\\u" r2 of
Just r3 -> case parseHex4 r3 of
Just (lo, r4)
| lo >= 0xDC00 && lo <= 0xDFFF ->
let combined = 0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00)
in go (T.singleton (chr combined) : chunks) r4
_ -> go (T.singleton (chr hi) : chunks) r2
Nothing -> go (T.singleton (chr hi) : chunks) r2
Just (codepoint, r2) ->
go (T.singleton (chr codepoint) : chunks) r2
Nothing -> go ("u" : chunks) r
_ -> (T.concat (reverse chunks), rest)
Just (c, rest) -> go (T.singleton c : chunks) rest
parseHex4 :: Text -> Maybe (Int, Text)
parseHex4 t
| T.length t >= 4 =
let hex = T.take 4 t
in if T.all isHexDigit hex
then Just (readHex4 hex, T.drop 4 t)
else Nothing
| otherwise = Nothing
readHex4 :: Text -> Int
readHex4 = T.foldl' (\acc c -> acc * 16 + digitToInt c) 0
parseJSONNumber :: Text -> Maybe (NixValue, Text)
parseJSONNumber t =
let (numStr, rest) = T.span (\c -> isDigit c || c == '.' || c == '-' || c == 'e' || c == 'E' || c == '+') t
in if T.null numStr
then Nothing
else
if T.any (\c -> c == '.' || c == 'e' || c == 'E') numStr
then case reads (T.unpack numStr) :: [(Double, String)] of
[(d, "")] -> Just (VFloat d, rest)
_ -> Nothing
else case reads (T.unpack numStr) :: [(Int64, String)] of
[(n, "")] -> Just (VInt n, rest)
_ -> Nothing
parseJSONArray :: Text -> Maybe (NixValue, Text)
parseJSONArray t = parseJSONArrayElements (T.stripStart t) []
parseJSONArrayElements :: Text -> [Thunk] -> Maybe (NixValue, Text)
parseJSONArrayElements t acc = case T.uncons (T.stripStart t) of
Just (']', rest) -> Just (VList (clistFromThunks (map thunkToCPtr (reverse acc))), rest)
_ -> case parseJSON t of
Just (val, rest) ->
let stripped = T.stripStart rest
in case T.uncons stripped of
Just (',', rest2) -> parseJSONArrayElements rest2 (evaluated val : acc)
Just (']', rest2) -> Just (VList (clistFromThunks (map thunkToCPtr (reverse (evaluated val : acc)))), rest2)
_ -> Nothing
Nothing -> Nothing
parseJSONObject :: Text -> Maybe (NixValue, Text)
parseJSONObject t = parseJSONObjectEntries (T.stripStart t) Map.empty
parseJSONObjectEntries :: Text -> Map Text Thunk -> Maybe (NixValue, Text)
parseJSONObjectEntries t acc = case T.uncons (T.stripStart t) of
Just ('}', rest) -> Just (VAttrs (attrSetFromMap acc), rest)
_ -> case parseJSONString (T.stripStart t) of
Just (VStr key _, rest) -> case T.uncons (T.stripStart rest) of
Just (':', rest2) -> case parseJSON rest2 of
Just (val, rest3) ->
let stripped = T.stripStart rest3
updated = Map.insert key (evaluated val) acc
in case T.uncons stripped of
Just (',', rest4) -> parseJSONObjectEntries rest4 updated
Just ('}', rest4) -> Just (VAttrs (attrSetFromMap updated), rest4)
_ -> Nothing
Nothing -> Nothing
_ -> Nothing
_ -> Nothing
builtinHashString :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinHashString (VStr algo _) (VStr input _) =
hashBytesWithAlgo "hashString" algo (TE.encodeUtf8 input)
builtinHashString (VStr _ _) other =
throwEvalError ("builtins.hashString: expected a string, got " <> typeName other)
builtinHashString other _ =
throwEvalError ("builtins.hashString: expected a string, got " <> typeName other)
digestToHex :: (BA.ByteArrayAccess a) => a -> Text
digestToHex = bytesToHexText . BA.convert
-- ---------------------------------------------------------------------------
-- Builtin implementations — deep evaluation
-- ---------------------------------------------------------------------------
builtinDeepSeq :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinDeepSeq first second = do
deepForce first
pure second
deepForce :: (MonadEval m) => NixValue -> m ()
deepForce (VList cl) = mapM_ ((force >=> deepForce) . Thunk) (clistThunks cl)
deepForce (VAttrs attrs) = mapM_ (force >=> deepForce) (attrSetElems attrs)
deepForce _ = pure ()
-- | @builtins.seq a b@ — evaluate @a@ to WHNF, then return @b@.
builtinSeq :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinSeq !_first = pure
-- | @builtins.trace msg val@ — print @msg@ to stderr, return @val@.
builtinTrace :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinTrace msgVal result = do
msg <- case msgVal of
VStr s _ -> pure s
other -> pure (showValueForTrace other)
traceMessage ("trace: " <> msg)
pure result
-- | @builtins.warn msg val@ — print warning to stderr, return @val@.
builtinWarn :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinWarn msgVal result = do
msg <- case msgVal of
VStr s _ -> pure s
other -> pure (showValueForTrace other)
traceMessage ("warning: " <> msg)
pure result
-- | Pretty-print a value for trace/warn, matching C++ Nix's printValue.
showValueForTrace :: NixValue -> Text
showValueForTrace (VInt n) = T.pack (show n)
showValueForTrace (VFloat f) = formatNixFloat f
showValueForTrace (VBool True) = "true"
showValueForTrace (VBool False) = "false"
showValueForTrace VNull = "null"
showValueForTrace (VPath p) = p
showValueForTrace other = "<<" <> typeName other <> ">>"
-- ---------------------------------------------------------------------------
-- Builtin implementations — graph traversal
-- ---------------------------------------------------------------------------
builtinGenericClosure :: (MonadEval m) => NixValue -> m NixValue
builtinGenericClosure (VAttrs attrs) = do
startSetThunk <-
maybe (throwEvalError "builtins.genericClosure: missing 'startSet'") pure $
attrSetLookup "startSet" attrs
operatorThunk <-
maybe (throwEvalError "builtins.genericClosure: missing 'operator'") pure $
attrSetLookup "operator" attrs
startSetVal <- force startSetThunk
operatorVal <- force operatorThunk
case startSetVal of
VList cl -> do
let items = map Thunk (clistThunks cl)
result <- closureLoop operatorVal (Seq.fromList items) [] []
pure (VList (clistFromThunks (map (thunkToCPtr . evaluated) result)))
_ -> throwEvalError "builtins.genericClosure: 'startSet' must be a list"
builtinGenericClosure other =
throwEvalError ("builtins.genericClosure: expected a set, got " <> typeName other)
-- | BFS loop for genericClosure. Uses Data.Sequence for O(1) queue
-- append (the old list-based version was O(n) per operator call).
-- seenKeys is still a linear scan — Nix value equality is monadic so
-- Set/HashMap is not directly applicable without specialising on key type.
closureLoop ::
(MonadEval m) =>
NixValue ->
Seq Thunk ->
[NixValue] ->
[NixValue] ->
m [NixValue]
closureLoop _ Empty _ acc = pure (reverse acc)
closureLoop operator (thunk :<| rest) seenKeys acc = do
item <- force thunk
key <- extractKey item
alreadySeen <- keyInList key seenKeys
if alreadySeen
then closureLoop operator rest seenKeys acc
else do
newItems <- applyValue operator item
case newItems of
VList newCl ->
closureLoop operator (rest <> Seq.fromList (map Thunk (clistThunks newCl))) (key : seenKeys) (item : acc)
_ -> throwEvalError "builtins.genericClosure: operator must return a list"
extractKey :: (MonadEval m) => NixValue -> m NixValue
extractKey (VAttrs attrs) =
case attrSetLookup "key" attrs of
Just thunk -> force thunk
Nothing -> throwEvalError "builtins.genericClosure: item missing 'key' attribute"
extractKey _ = throwEvalError "builtins.genericClosure: item must be a set with 'key'"
keyInList :: (MonadEval m) => NixValue -> [NixValue] -> m Bool
keyInList _ [] = pure False
keyInList key (seen : rest) = do
eq <- nixEqual force key seen
if eq then pure True else keyInList key rest
-- ---------------------------------------------------------------------------
-- IO builtins (delegate to MonadEval methods)
-- ---------------------------------------------------------------------------
-- | Coerce a value to a path 'Text'. Accepts 'VPath' and 'VStr';
-- throws a type error for anything else.
coerceToPath :: (MonadEval m) => Text -> NixValue -> m Text
coerceToPath _ (VPath p) = pure p
coerceToPath _ (VStr s _) = pure s
coerceToPath name other =
throwEvalError ("builtins." <> name <> ": expected a path or string, got " <> typeName other)
builtinImport :: (MonadEval m) => NixValue -> m NixValue
builtinImport (VPath p) = importFile p
builtinImport (VStr s _) = importFile s
builtinImport other =
throwEvalError ("import: expected a path or string, got " <> typeName other)
builtinReadFile :: (MonadEval m) => NixValue -> m NixValue
builtinReadFile val = do
p <- coerceToPath "readFile" val
mkStr <$> readFileText p
builtinPathExists :: (MonadEval m) => NixValue -> m NixValue
builtinPathExists val = do
p <- coerceToPath "pathExists" val
VBool <$> doesPathExist p
builtinReadDir :: (MonadEval m) => NixValue -> m NixValue
builtinReadDir val = do
p <- coerceToPath "readDir" val
entries <- listDirectory p
pure (VAttrs (attrSetFromMap (Map.fromList [(name, evaluated (mkStr fileType)) | (name, fileType) <- entries])))
-- ---------------------------------------------------------------------------
-- Builtin implementations — environment + paths
-- ---------------------------------------------------------------------------
builtinGetEnv :: (MonadEval m) => NixValue -> m NixValue
builtinGetEnv (VStr name _) = mkStr <$> getEnvVar name
builtinGetEnv other =
throwEvalError ("builtins.getEnv: expected a string, got " <> typeName other)
builtinToPath :: (MonadEval m) => NixValue -> m NixValue
builtinToPath (VPath p) = pure (VPath p)
builtinToPath (VStr s _) = case T.uncons s of
Nothing -> throwEvalError "builtins.toPath: empty path"
Just ('/', _) -> pure (VPath s)
Just _ -> throwEvalError ("builtins.toPath: path must be absolute, got " <> s)
builtinToPath other =
throwEvalError ("builtins.toPath: expected a string or path, got " <> typeName other)
-- ---------------------------------------------------------------------------
-- Builtin implementations — store path operations
-- ---------------------------------------------------------------------------
builtinPlaceholder :: (MonadEval m) => NixValue -> m NixValue
builtinPlaceholder (VStr outputName _) = pure (mkStr (hashPlaceholder outputName))
builtinPlaceholder other =
throwEvalError ("builtins.placeholder: expected a string, got " <> typeName other)
builtinStorePath :: (MonadEval m) => NixValue -> m NixValue
builtinStorePath (VPath p) = validateStorePath p
builtinStorePath (VStr s _) = validateStorePath s
builtinStorePath other =
throwEvalError ("builtins.storePath: expected a path or string, got " <> typeName other)
validateStorePath :: (MonadEval m) => Text -> m NixValue
validateStorePath p
| storeDirPrefix `T.isPrefixOf` p,
T.length p > T.length storeDirPrefix,
let basename = T.drop (T.length storeDirPrefix) p,
T.length basename >= 33,
Just ('-', _) <- T.uncons (T.drop 32 basename) =
pure (VPath p)
| otherwise =
throwEvalError ("builtins.storePath: not a valid store path: " <> p)
-- ---------------------------------------------------------------------------
-- Builtin implementations — Nix search path
-- ---------------------------------------------------------------------------
builtinFindFile :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinFindFile (VList cl) (VStr name _) = do
let searchPath = map Thunk (clistThunks cl)
entries <- mapM forceSearchEntry searchPath
findFirst entries name
builtinFindFile (VList _) other =
throwEvalError ("builtins.findFile: expected a string, got " <> typeName other)
builtinFindFile other _ =
throwEvalError ("builtins.findFile: expected a list, got " <> typeName other)
-- | Extract {prefix, path} from a search path entry thunk.
forceSearchEntry :: (MonadEval m) => Thunk -> m (Text, Text)
forceSearchEntry thunk = do
val <- force thunk
case val of
VAttrs attrs -> do
prefixThunk <-
maybe (throwEvalError "builtins.findFile: entry missing 'prefix'") pure $
attrSetLookup "prefix" attrs
pathThunk <-
maybe (throwEvalError "builtins.findFile: entry missing 'path'") pure $
attrSetLookup "path" attrs
prefixVal <- force prefixThunk
pathVal <- force pathThunk
prefix <- case prefixVal of
VStr s _ -> pure s
_ -> throwEvalError "builtins.findFile: 'prefix' must be a string"
path <- case pathVal of
VStr s _ -> pure s
VPath s -> pure s
_ -> throwEvalError "builtins.findFile: 'path' must be a string or path"
pure (prefix, path)
_ -> throwEvalError "builtins.findFile: search path entry must be a set"
-- | Iterate search path entries, checking for a match.
findFirst :: (MonadEval m) => [(Text, Text)] -> Text -> m NixValue
findFirst [] name =
throwEvalError ("file '" <> name <> "' was not found in the Nix search path")
findFirst ((prefix, path) : rest) name
| prefix == name || (not (T.null prefix) && (prefix <> "/") `T.isPrefixOf` name) =
let suffix = if prefix == name then "" else T.drop (T.length prefix + 1) name
candidate = if T.null suffix then path else path <> "/" <> suffix
in do
exists <- doesPathExist candidate
if exists
then pure (VPath candidate)
else findFirst rest name
| T.null prefix =
let candidate = path <> "/" <> name
in do
exists <- doesPathExist candidate
if exists
then pure (VPath candidate)
else findFirst rest name
| otherwise = findFirst rest name
-- ---------------------------------------------------------------------------
-- Builtin implementations — store file creation
-- ---------------------------------------------------------------------------
builtinToFile :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinToFile (VStr name _) (VStr contents _) = do
storePath <- writeToStore name contents
pure (VPath storePath)
builtinToFile (VStr _ _) other =
throwEvalError ("builtins.toFile: expected a string, got " <> typeName other)
builtinToFile other _ =
throwEvalError ("builtins.toFile: expected a string, got " <> typeName other)
-- ---------------------------------------------------------------------------
-- Builtin implementations — scoped import
-- ---------------------------------------------------------------------------
builtinScopedImport :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinScopedImport (VAttrs attrs) pathVal = do
p <- coerceToPath "scopedImport" pathVal
let scope = Map.toList (attrSetToMap attrs)
scopedImportFile scope p
builtinScopedImport other _ =
throwEvalError ("builtins.scopedImport: expected a set, got " <> typeName other)
-- ---------------------------------------------------------------------------
-- Builtin implementations — network fetchers
-- ---------------------------------------------------------------------------
builtinFetchurl :: (MonadEval m) => NixValue -> m NixValue
builtinFetchurl (VStr url _) = fetchUrlSimple url Nothing
builtinFetchurl (VAttrs attrs) = do
url <- forceAttrStr "builtins.fetchurl" "url" attrs
sha256 <- forceOptionalAttrStr attrs "sha256"
fetchUrlSimple url sha256
builtinFetchurl other =
throwEvalError ("builtins.fetchurl: expected a string or set, got " <> typeName other)
builtinFetchTarball :: (MonadEval m) => NixValue -> m NixValue
builtinFetchTarball (VStr url _) = fetchAndExtractTarball url
builtinFetchTarball (VAttrs attrs) = do
url <- forceAttrStr "builtins.fetchTarball" "url" attrs
fetchAndExtractTarball url
builtinFetchTarball other =
throwEvalError ("builtins.fetchTarball: expected a string or set, got " <> typeName other)
-- | Download a tarball, extract it, and return the path to the extracted
-- directory. Uses a content-hashed temp directory. Downloads and extracts
-- in a single shell pipeline to avoid binary-as-text encoding issues.
fetchAndExtractTarball :: (MonadEval m) => Text -> m NixValue
fetchAndExtractTarball url = do
sysTmp <- getTempDir
let urlHash = sha256Hex (TE.encodeUtf8 url)
extractDir = sysTmp <> "/nova-nix-tarball-" <> urlHash
-- Single pipeline: mkdir, download, extract with --strip-components=1
-- The -- separator prevents argument injection from the URL.
(code, _, errOut) <-
runProcess
"sh"
[ "-c",
"mkdir -p \"$1\" && curl -sSfL -- \"$2\" | tar -xz -C \"$1\" --strip-components=1",
"--",
extractDir,
url
]
""
case code of
0 -> pure (VPath extractDir)
_ -> throwEvalError ("builtins.fetchTarball: " <> errOut)
builtinFetchGit :: (MonadEval m) => NixValue -> m NixValue
builtinFetchGit (VStr url _) = do
sysTmp <- getTempDir
let urlHash = sha256Hex (TE.encodeUtf8 url)
cloneDir = sysTmp <> "/nova-nix-fetchgit-" <> urlHash
(code, _, errOut) <- runProcess "git" ["clone", "--depth", "1", "--", url, cloneDir] ""
case code of
0 -> pure (VPath cloneDir)
_ -> throwEvalError ("builtins.fetchGit: git clone failed: " <> errOut)
builtinFetchGit (VAttrs attrs) = do
url <- forceAttrStr "builtins.fetchGit" "url" attrs
builtinFetchGit (mkStr url)
builtinFetchGit other =
throwEvalError ("builtins.fetchGit: expected a string or set, got " <> typeName other)
-- | Resolve the system temp directory. Checks @TMPDIR@ (Unix), then
-- @TEMP@ (Windows), falls back to @\/tmp@.
getTempDir :: (MonadEval m) => m Text
getTempDir = do
candidates <- mapM getEnvVar ["TMPDIR", "TEMP"]
pure (fromMaybe "/tmp" (find (not . T.null) candidates))
-- | Fetch a URL and optionally verify its hash.
fetchUrlSimple :: (MonadEval m) => Text -> Maybe Text -> m NixValue
fetchUrlSimple url _sha256 = do
(code, stdout, stderr) <- runProcess "curl" ["-sSfL", "--", url] ""
if code /= 0
then throwEvalError ("fetch failed: " <> stderr)
else do
storePath <- writeToStore "fetchurl-result" stdout
pure (VPath storePath)
-- | Force a required string attribute from an attrset, using full Nix string
-- coercion (VStr, VPath, VInt, VBool, VAttrs via __toString/outPath).
forceAttrStr :: (MonadEval m) => Text -> Text -> AttrSet -> m Text
forceAttrStr builtin key attrs =
case attrSetLookup key attrs of
Nothing -> throwEvalError (builtin <> ": missing required attribute '" <> key <> "'")
Just thunk -> do
val <- force thunk
result <- catchEvalError (coerceToString True force applyValue val)
case result of
Right (s, _ctx) -> pure s
Left _ -> throwEvalError (builtin <> ": '" <> key <> "' must be a string")
-- | Force an optional string attribute via full Nix coercion.
forceOptionalAttrStr :: (MonadEval m) => AttrSet -> Text -> m (Maybe Text)
forceOptionalAttrStr attrs key =
case attrSetLookup key attrs of
Nothing -> pure Nothing
Just thunk -> do
val <- force thunk
result <- catchEvalError (coerceToString True force applyValue val)
case result of
Right (s, _ctx) -> pure (Just s)
Left _ -> pure Nothing
-- ---------------------------------------------------------------------------
-- Builtin implementations — derivation construction
-- ---------------------------------------------------------------------------
-- | Eager derivation computation — @builtins.derivationStrict@. Forces all
-- input attrs into env vars, content-hashes, and returns the full derivation
-- attrset (drvPath, outPath, per-output, _derivation). Called LAZILY by the
-- @derivation@ wrapper ('builtinDerivationLazy'), so forcing a derivation to
-- WHNF never forces this — matching C++ Nix's derivationStrict/derivation split.
builtinDerivationStrict :: (MonadEval m) => NixValue -> m NixValue
builtinDerivationStrict (VAttrs attrs) = do
-- Extract required attributes
drvName <- forceAttrStr "derivation" "name" attrs
system <- forceAttrStr ("derivation \"" <> drvName <> "\"") "system" attrs
builder <- forceAttrStr ("derivation \"" <> drvName <> "\"") "builder" attrs
-- Extract optional outputs (default ["out"])
outputNames <- case attrSetLookup "outputs" attrs of
Nothing -> pure ["out"]
Just thunk -> do
val <- force thunk
case val of
VList cl -> mapM (forceToText . Thunk) (clistThunks cl)
_ -> throwEvalError "derivation: 'outputs' must be a list of strings"
-- Extract optional args (default []). Path literals in args (e.g. stdenv's
-- ./default-builder.sh) are copied into the store; their source paths flow
-- into inputSrcs via the returned context.
(builderArgs, argsContext) <- case attrSetLookup "args" attrs of
Nothing -> pure ([], mempty)
Just thunk -> do
val <- force thunk
case val of
VList cl -> do
parts <- mapM (\t -> force (Thunk t) >>= coerceToStoreString) (clistThunks cl)
pure (map fst parts, mconcat (map snd parts))
_ -> throwEvalError "derivation: 'args' must be a list of strings"
-- Materialize once, reuse for both env collection and result merge
let materialized = attrSetToMap attrs
-- Collect string-coercible attrs into the build env, EXCLUDING "args"
-- (C++ Nix puts args in the Derive() args field, never the env). The
-- per-output env vars ($out, …) are added below. Carries merged context.
(drvEnvPairs, envContext) <- collectDrvEnvWithContext (Map.delete "__ignoreNulls" (Map.delete "args" materialized))
let fullContext = envContext <> argsContext
inputDrvs = extractInputDrvs fullContext
inputSrcs = extractInputSrcs fullContext
platform = textToPlatform system
baseEnv = Map.fromList drvEnvPairs
drvRefs = inputSrcs ++ Map.keys inputDrvs
drvFileName = drvName <> ".drv"
-- Build a Derivation sharing this call's inputs/platform/builder/args.
mkDrv outs env =
Derivation
{ drvOutputs = outs,
drvInputDrvs = inputDrvs,
drvInputSrcs = inputSrcs,
drvPlatform = platform,
drvBuilder = builder,
drvArgs = builderArgs,
drvEnv = env
}
-- Output carrying only its name; the path is masked at render time.
maskedOutput name = DerivationOutput name (StorePath "" "") "" ""
-- Resolve an input derivation's modulo hash (hex) from the cache,
-- populated bottom-up by earlier 'builtinDerivationStrict' calls.
resolveInputModulo (sp, outs) = do
let inputPathText = storePathToText defaultStoreDir sp
cached <- lookupDrvHash inputPathText
case cached of
Just hex -> pure (hex, outs)
Nothing -> do
-- Eval is bottom-up, so inputs evaluated this session are always
-- cached by the time a dependent hashes. A miss means an input
-- referenced but not evaluated here (a pure-eval synthetic context,
-- or a pre-built store drv): fall back to its store hash so
-- evaluation still produces a value, and warn for visibility.
traceMessage
("derivation: input modulo hash not cached, using store hash for " <> inputPathText)
pure (spHash sp, outs)
-- Fixed-output derivations (fetchurl etc.) are content-addressed and hash
-- via the @fixed:out:@ scheme; input-addressed derivations recurse through
-- the modulo hashes of their inputs.
mFixed <- detectFixedOutput attrs
(drvPathText, drvSP, outPaths, completeDrv) <- case mFixed of
Just (foAlgo, foMode, foDigest) -> do
let foPath = makeFixedOutputPath drvName foAlgo foMode foDigest
foPathText = storePathToText defaultStoreDir foPath
algoField = (if foMode == "recursive" then "r:" else "") <> foAlgo
foHashHex = bytesToHexText foDigest
foModulo =
sha256Digest
(TE.encodeUtf8 ("fixed:out:" <> algoField <> ":" <> foHashHex <> ":" <> foPathText))
contents =
mkDrv
[DerivationOutput "out" foPath algoField foHashHex]
(Map.insert "out" foPathText baseEnv)
drvSp = makeTextPath drvFileName (sha256Digest (TE.encodeUtf8 (toATerm contents))) drvRefs
drvText = storePathToText defaultStoreDir drvSp
cacheDrvHash drvText (bytesToHexText foModulo)
pure (drvText, drvSp, [("out", foPathText)], contents)
Nothing -> do
inputSubst <- mapM resolveInputModulo (Map.toList inputDrvs)
let maskedEnv = foldr (`Map.insert` "") baseEnv outputNames
maskedDrv = mkDrv (map maskedOutput outputNames) maskedEnv
-- Masked modulo hash → this derivation's own output paths.
moduloMasked = sha256Digest (TE.encodeUtf8 (toATermForHash True (Just inputSubst) maskedDrv))
outStorePaths = [(n, makeOutputPath n moduloMasked drvName) | n <- outputNames]
outPathTexts = [(n, storePathToText defaultStoreDir sp) | (n, sp) <- outStorePaths]
realEnv = foldr (\(n, t) e -> Map.insert n t e) baseEnv outPathTexts
contents = mkDrv [DerivationOutput n sp "" "" | (n, sp) <- outStorePaths] realEnv
-- Unmasked modulo hash (real outputs, inputs substituted) cached for
-- when this derivation is itself an input to another.
moduloUnmasked = sha256Digest (TE.encodeUtf8 (toATermForHash False (Just inputSubst) contents))
drvSp = makeTextPath drvFileName (sha256Digest (TE.encodeUtf8 (toATerm contents))) drvRefs
drvText = storePathToText defaultStoreDir drvSp
cacheDrvHash drvText (bytesToHexText moduloUnmasked)
pure (drvText, drvSp, outPathTexts, contents)
-- Record this derivation's full .drv ATerm (the exact bytes whose hash is its
-- store path) so the build driver can materialize the entire input-.drv
-- closure before building. Bottom-up eval guarantees every transitive input
-- is recorded by the time a dependent is.
recordDrvAterm drvPathText (toATerm completeDrv)
let mainOutPath = case outPaths of
((_, p) : _) -> p
[] -> ""
-- The default output is the FIRST in @outputs@ (matching C++ Nix, which
-- returns @(head outputsList).value@) — not necessarily @out@.
mainOutName = case outPaths of
((n, _) : _) -> n
[] -> "out"
-- Context for output paths: each output carries SCDrvOutput context
-- Context for drvPath: carries SCAllOutputs context
let drvPathCtx = StringContext (Set.singleton (SCAllOutputs drvSP))
outPathCtx outName = StringContext (Set.singleton (SCDrvOutput drvSP outName))
-- Build per-output attrsets matching Nix: drv.out = { outPath, drvPath, type }
let mkOutputAttrs outName outP =
let outCtx = outPathCtx outName
outputAttrMap =
Map.fromList
[ ("outPath", evaluated (VStr outP outCtx)),
("drvPath", evaluated (VStr drvPathText drvPathCtx)),
("type", evaluated (mkStr "derivation"))
]
in evaluated (VAttrs (attrSetFromMap outputAttrMap))
-- Build result attrset: original attrs + drvPath, outPath, type, per-output attrs
let baseAttrs =
Map.fromList $
[ ("type", evaluated (mkStr "derivation")),
("drvPath", evaluated (VStr drvPathText drvPathCtx)),
("outPath", evaluated (VStr mainOutPath (outPathCtx mainOutName))),
("name", evaluated (mkStr drvName)),
("system", evaluated (mkStr system)),
("builder", evaluated (mkStr builder)),
("_derivation", evaluated (VDerivation completeDrv))
]
++ [(outName, mkOutputAttrs outName outP) | (outName, outP) <- outPaths]
-- Merge original attrs underneath so computed attrs take priority
resultAttrs = Map.union baseAttrs materialized
pure (VAttrs (attrSetFromMap resultAttrs))
builtinDerivationStrict other =
throwEvalError ("derivation: expected a set, got " <> typeName other)
-- | Detect a fixed-output derivation. Returns @Just (algo, mode, rawDigest)@
-- when @outputHash@ is present and non-empty (fetchurl, fetchgit, …), else
-- 'Nothing' for an ordinary input-addressed derivation. @mode@ is
-- @\"flat\"@ or @\"recursive\"@; @algo@ is e.g. @\"sha256\"@.
detectFixedOutput :: (MonadEval m) => AttrSet -> m (Maybe (Text, Text, BS.ByteString))
detectFixedOutput attrs =
case attrSetLookup "outputHash" attrs of
Nothing -> pure Nothing
Just thunk -> do
val <- force thunk
case val of
VStr ohash _
| not (T.null ohash) -> do
ohAlgo <- optDrvStrAttr "outputHashAlgo" attrs
ohMode <- optDrvStrAttr "outputHashMode" attrs
(algo, digest) <- normalizeFixedHash ohash ohAlgo
let mode = if ohMode == "recursive" then "recursive" else "flat"
pure (Just (algo, mode, digest))
_ -> pure Nothing
-- | Read an optional string attribute, defaulting to @\"\"@ when absent or
-- not a string.
optDrvStrAttr :: (MonadEval m) => Text -> AttrSet -> m Text
optDrvStrAttr key attrs =
case attrSetLookup key attrs of
Nothing -> pure ""
Just thunk -> do
val <- force thunk
case val of
VStr s _ -> pure s
_ -> pure ""
-- | Decode a fixed-output hash (SRI @algo-base64@, @algo:hash@, or a bare
-- hash plus a separate algorithm) to its algorithm name and raw bytes.
normalizeFixedHash :: (MonadEval m) => Text -> Text -> m (Text, BS.ByteString)
normalizeFixedHash ohash ohAlgo
| Just (algo, b64) <- parseSRI ohash = do
bytes <- decodeBase64E "derivation" b64
pure (algo, bytes)
| Just (algo, rest) <- parseAlgoPrefix ohash = decodeWithAlgo algo rest
| not (T.null ohAlgo) = decodeWithAlgo ohAlgo ohash
| otherwise =
throwEvalError ("derivation: cannot determine outputHash algorithm for " <> ohash)
-- | Lazy @derivation@ wrapper — mirrors C++ Nix's @corepkgs/derivation.nix@.
-- Returns a WHNF attrset whose @drvPath@/@outPath@/output-path/@_derivation@
-- attrs are LAZY thunks that defer to 'builtinDerivationStrict'. Forcing a
-- derivation to WHNF therefore does NOT force its input/env closure — which is
-- essential for nixpkgs, where merely referencing a derivation (e.g.
-- @drv != null@, @assert (libxcrypt != null)@) must not build its whole closure.
--
-- The lazy thunks are built with the same synthetic-select pattern used by
-- @inherit (from)@: a single shared @strict@ thunk (so the eager computation
-- runs at most once) selected from via fresh minimal envs.
builtinDerivationLazy :: (MonadEval m) => NixValue -> m NixValue
builtinDerivationLazy (VAttrs attrs) = do
-- Output names are cheap (matches @drvAttrs @ { outputs ? [ "out" ], ... }@).
outputNames <- case attrSetLookup "outputs" attrs of
Nothing -> pure ["out"]
Just thunk -> do
val <- force thunk
case val of
VList cl -> mapM (forceToText . Thunk) (clistThunks cl)
_ -> throwEvalError "derivation: 'outputs' must be a list of strings"
-- One shared thunk computing @derivationStrict attrs@, forced only when an
-- output path / drvPath is actually read.
let drvAttrsThunk = evaluated (VAttrs attrs)
strictBuiltinThunk = evaluated (VBuiltin "derivationStrict" [])
strictThunk =
let (sp, sc) = buildCSlots [drvAttrsThunk, strictBuiltinThunk]
envDS = newMinimalEnv sp sc
in mkSyntheticThunk envDS (EApp (EResolvedVar 0 1) (EResolvedVar 0 0))
selectStrict field =
let (sp, sc) = buildCSlots [strictThunk]
envF = newMinimalEnv sp sc
in mkSyntheticThunk envF (ESelect (EResolvedVar 0 0) [StaticKey field] Nothing)
-- WHNF spine: input attrs (unforced) overlaid with the lazy computed attrs.
let computedAttrs =
Map.fromList $
[ ("type", evaluated (mkStr "derivation")),
("drvPath", selectStrict "drvPath"),
("outPath", selectStrict "outPath"),
("_derivation", selectStrict "_derivation")
]
++ [(outName, selectStrict outName) | outName <- outputNames]
resultAttrs = Map.union computedAttrs (attrSetToMap attrs)
pure (VAttrs (attrSetFromMap resultAttrs))
builtinDerivationLazy other =
throwEvalError ("derivation: expected a set, got " <> typeName other)
-- | Force a thunk to a Text string via full Nix coercion.
forceToText :: (MonadEval m) => Thunk -> m Text
forceToText thunk = do
val <- force thunk
(s, _ctx) <- coerceToString True force applyValue val
pure s
-- | Collect all string-coercible attributes for the derivation environment,
-- along with the merged string context from all collected values.
-- Uses 'coerceToStringPermissive' for full Nix coercion including
-- __toString, outPath, and list-to-space-separated-string.
collectDrvEnvWithContext :: (MonadEval m) => Map Text Thunk -> m ([(Text, Text)], StringContext)
collectDrvEnvWithContext attrs = do
let pairs = Map.toList attrs
results <- mapM tryCoerce pairs
let envPairs = catMaybes [fmap (\(k, v, _) -> (k, v)) r | r <- results]
mergedCtx = mconcat [ctx | Just (_, _, ctx) <- results]
pure (envPairs, mergedCtx)
where
tryCoerce (key, thunk) = do
val <- force thunk
case val of
VNull -> pure Nothing
_ -> do
result <- catchEvalError (coerceToStoreString val)
case result of
Right (s, ctx) -> pure (Just (key, s, ctx))
Left _ -> pure Nothing
-- ---------------------------------------------------------------------------
-- Builtin implementations — hashFile, readFileType
-- ---------------------------------------------------------------------------
-- | @builtins.hashFile algo path@ — hash raw bytes of a file on disk.
-- Returns base-16 hex string, matching @builtins.hashString@ output format.
builtinHashFile :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinHashFile (VStr algo _) (VPath path) = do
bytes <- readFileBytes path
hashBytesWithAlgo "hashFile" algo bytes
builtinHashFile (VStr algo _) (VStr path _) = do
bytes <- readFileBytes path
hashBytesWithAlgo "hashFile" algo bytes
builtinHashFile (VStr _ _) other =
throwEvalError ("builtins.hashFile: expected a path, got " <> typeName other)
builtinHashFile other _ =
throwEvalError ("builtins.hashFile: expected a string, got " <> typeName other)
-- | Shared hash dispatch for raw 'ByteString' input.
hashBytesWithAlgo :: (MonadEval m) => Text -> Text -> BS.ByteString -> m NixValue
hashBytesWithAlgo ctx algo bytes = case algo of
"sha256" -> pure (mkStr (digestToHex (CH.hash bytes :: CH.Digest CH.SHA256)))
"sha512" -> pure (mkStr (digestToHex (CH.hash bytes :: CH.Digest CH.SHA512)))
"sha1" -> pure (mkStr (digestToHex (CH.hash bytes :: CH.Digest CH.SHA1)))
"md5" -> pure (mkStr (digestToHex (CH.hash bytes :: CH.Digest CH.MD5)))
_ -> throwEvalError ("builtins." <> ctx <> ": unknown hash algorithm '" <> algo <> "'")
-- | @builtins.readFileType path@ — classify a filesystem entry.
-- Returns @"regular"@, @"directory"@, @"symlink"@, or @"unknown"@.
builtinReadFileType :: (MonadEval m) => NixValue -> m NixValue
builtinReadFileType (VPath path) = mkStr <$> getFileType path
builtinReadFileType (VStr path _) = mkStr <$> getFileType path
builtinReadFileType other =
throwEvalError ("builtins.readFileType: expected a path, got " <> typeName other)
-- ---------------------------------------------------------------------------
-- Builtin implementations — convertHash
-- ---------------------------------------------------------------------------
-- | @builtins.convertHash { hash, hashAlgo?, toHashFormat }@ — convert
-- between hash representations. Supports base16, nix32, base64, and sri.
builtinConvertHash :: (MonadEval m) => NixValue -> m NixValue
builtinConvertHash (VAttrs attrs) = do
hashVal <- requireStrAttr "convertHash" "hash" attrs
toFmt <- requireStrAttr "convertHash" "toHashFormat" attrs
-- Detect input format and decode to raw bytes + algo
(algo, rawBytes) <- decodeHashInput attrs hashVal
-- Encode to target format
case toFmt of
"base16" -> pure (mkStr (bytesToHexText rawBytes))
"nix32" -> pure (mkStr (Nix32.encode rawBytes))
"base32" -> pure (mkStr (Nix32.encode rawBytes)) -- deprecated alias
"base64" -> pure (mkStr (bytesToBase64 rawBytes))
"sri" -> pure (mkStr (algo <> "-" <> bytesToBase64 rawBytes))
_ -> throwEvalError ("builtins.convertHash: unknown toHashFormat '" <> toFmt <> "'")
builtinConvertHash other =
throwEvalError ("builtins.convertHash: expected a set, got " <> typeName other)
-- | Extract algo + raw bytes from the hash input, handling SRI, prefixed, and plain formats.
decodeHashInput :: (MonadEval m) => AttrSet -> Text -> m (Text, BS.ByteString)
decodeHashInput attrs hashStr
-- SRI format: algo-base64
| Just (algo, b64) <- parseSRI hashStr = do
bytes <- decodeBase64E "convertHash" b64
pure (algo, bytes)
-- Prefixed format: algo:hex or algo:nix32
| Just (algo, rest) <- parseAlgoPrefix hashStr =
decodeWithAlgo algo rest
-- Plain hash — need hashAlgo attribute
| otherwise = do
algo <- requireStrAttr "convertHash" "hashAlgo" attrs
decodeWithAlgo algo hashStr
-- | Try to decode as hex, then nix32, then base64.
decodeWithAlgo :: (MonadEval m) => Text -> Text -> m (Text, BS.ByteString)
decodeWithAlgo algo s
| Just bytes <- hexToBytes s = pure (algo, bytes)
| Right bytes <- Nix32.decode s = pure (algo, bytes)
| Right bytes <- decodeBase64Pure s = pure (algo, bytes)
| otherwise = throwEvalError ("builtins.convertHash: cannot decode hash '" <> s <> "'")
-- | Parse @sha256-base64...@ SRI format.
parseSRI :: Text -> Maybe (Text, Text)
parseSRI t = case T.breakOn "-" t of
(algo, rest)
| not (T.null rest) && algo `elem` ["sha256", "sha512", "sha1", "md5"] ->
Just (algo, T.drop 1 rest)
_ -> Nothing
-- | Parse @sha256:value@ prefixed format.
parseAlgoPrefix :: Text -> Maybe (Text, Text)
parseAlgoPrefix t = case T.breakOn ":" t of
(algo, rest)
| not (T.null rest) && algo `elem` ["sha256", "sha512", "sha1", "md5"] ->
Just (algo, T.drop 1 rest)
_ -> Nothing
-- | Require a string attribute from an attrset.
requireStrAttr :: (MonadEval m) => Text -> Text -> AttrSet -> m Text
requireStrAttr ctx key attrs = case attrSetLookup key attrs of
Just thunk -> do
val <- force thunk
case val of
VStr s _ -> pure s
_ -> throwEvalError ("builtins." <> ctx <> ": " <> key <> " must be a string")
Nothing -> throwEvalError ("builtins." <> ctx <> ": missing required attribute '" <> key <> "'")
-- ---------------------------------------------------------------------------
-- Base64 encode/decode — delegates to nova-cache (base64-bytestring under the hood)
-- ---------------------------------------------------------------------------
-- | Encode bytes to base64.
bytesToBase64 :: BS.ByteString -> Text
bytesToBase64 = B64.encode
-- | Decode base64 text to bytes (pure).
decodeBase64Pure :: Text -> Either Text BS.ByteString
decodeBase64Pure t =
-- Strip whitespace and any existing padding, then re-pad to a multiple of
-- 4. base64-bytestring's 'decode' requires correct padding, so SRI hashes
-- (correctly-padded standard base64, e.g. @sha256-…NQ=@) would otherwise be
-- rejected once their trailing @=@ was removed.
let stripped = T.filter (\c -> c /= '\n' && c /= '\r' && c /= '=') t
padLen = (4 - (T.length stripped `mod` 4)) `mod` 4
padded = stripped <> T.replicate padLen "="
in case B64.decode padded of
Right bytes -> Right bytes
Left _ -> Left "invalid base64"
-- | Decode base64 with error context for builtins.
decodeBase64E :: (MonadEval m) => Text -> Text -> m BS.ByteString
decodeBase64E ctx t = case decodeBase64Pure t of
Right bytes -> pure bytes
Left _ -> throwEvalError ("builtins." <> ctx <> ": invalid base64 encoding")
-- ---------------------------------------------------------------------------
-- Builtin implementations — fromTOML
-- ---------------------------------------------------------------------------
-- | @builtins.fromTOML str@ — parse a TOML document to a Nix value.
-- Hand-rolled parser covering the TOML v1.0 subset used by nixpkgs:
-- bare/quoted keys, dotted keys, basic/literal strings (multiline),
-- integers (dec/hex/oct/bin), floats (inc. inf/nan), booleans,
-- inline tables, arrays, array-of-tables, and standard tables.
-- Datetimes are represented as strings (matching real Nix).
builtinFromTOML :: (MonadEval m) => NixValue -> m NixValue
builtinFromTOML (VStr s _) = case parseTOML s of
Right val -> pure val
Left err -> throwEvalError ("builtins.fromTOML: " <> err)
builtinFromTOML other =
throwEvalError ("builtins.fromTOML: expected a string, got " <> typeName other)
-- | Intermediate TOML value before conversion to NixValue.
data TOMLValue
= TOMLStr !Text
| TOMLInt !Int64
| TOMLFloat !Double
| TOMLBool !Bool
| TOMLArray ![TOMLValue]
| TOMLTable !(Map Text TOMLValue)
deriving (Show)
-- | Parse a TOML document into a NixValue.
parseTOML :: Text -> Either Text NixValue
parseTOML input = do
table <- parseTOMLDoc (T.lines input)
pure (tomlToNix (TOMLTable table))
-- | Convert a TOMLValue to NixValue.
tomlToNix :: TOMLValue -> NixValue
tomlToNix val = case val of
TOMLStr s -> mkStr s
TOMLInt n -> VInt n
TOMLFloat d -> VFloat d
TOMLBool b -> VBool b
TOMLArray xs -> VList (clistFromThunks (map (thunkToCPtr . evaluated . tomlToNix) xs))
TOMLTable m -> VAttrs (attrSetFromMap (Map.map (evaluated . tomlToNix) m))
-- | Parse all lines of a TOML document into a table.
parseTOMLDoc :: [Text] -> Either Text (Map Text TOMLValue)
parseTOMLDoc lns = go lns [] Map.empty
where
go [] _ root = Right root
go (line : rest) currentPath root
| T.null stripped || T.isPrefixOf "#" stripped =
-- Empty line or comment
go rest currentPath root
| T.isPrefixOf "[[" stripped && T.isSuffixOf "]]" stripped =
-- Array of tables: [[key]]
let keyStr = T.strip (T.drop 2 (T.dropEnd 2 stripped))
keys = parseDottedKey keyStr
in go rest keys (insertArrayTable keys root)
| T.isPrefixOf "[" stripped && T.isSuffixOf "]" stripped =
-- Standard table: [key]
let keyStr = T.strip (T.drop 1 (T.dropEnd 1 stripped))
keys = parseDottedKey keyStr
in go rest keys (ensureTable keys root)
| otherwise =
-- Key = value pair
case parseKVLine stripped of
Right (keys, val) ->
go rest currentPath (insertNested (currentPath ++ keys) val root)
Left err -> Left err
where
stripped = T.strip line
-- | Parse a key = value line.
parseKVLine :: Text -> Either Text ([Text], TOMLValue)
parseKVLine line =
let (keyPart, afterEq) = splitAtEquals line
in case T.uncons afterEq of
Nothing -> Left ("expected '=' in: " <> line)
Just _ -> do
let val = T.strip afterEq
parsed <- parseTOMLValue val
Right (parseDottedKey (T.strip keyPart), parsed)
-- | Split a line at the first unquoted '=' sign.
splitAtEquals :: Text -> (Text, Text)
splitAtEquals = go T.empty
where
go acc t = case T.uncons t of
Nothing -> (acc, T.empty)
Just ('=', rest) -> (acc, rest)
Just ('"', rest) ->
let (quoted, after) = T.break (== '"') rest
in case T.uncons after of
Just ('"', r) -> go (acc <> "\"" <> quoted <> "\"") r
_ -> go (acc <> "\"" <> quoted) after
Just ('\'', rest) ->
let (quoted, after) = T.break (== '\'') rest
in case T.uncons after of
Just ('\'', r) -> go (acc <> "'" <> quoted <> "'") r
_ -> go (acc <> "'" <> quoted) after
Just (c, rest) -> go (T.snoc acc c) rest
-- | Parse dotted key like @foo.bar."baz qux"@ into @["foo", "bar", "baz qux"]@.
parseDottedKey :: Text -> [Text]
parseDottedKey t
| T.null t = []
| otherwise = case T.uncons t of
Just ('"', rest) ->
let (key, after) = T.break (== '"') rest
in key : parseDottedKey (T.drop 1 (T.stripStart (dropDot after)))
Just ('\'', rest) ->
let (key, after) = T.break (== '\'') rest
in key : parseDottedKey (T.drop 1 (T.stripStart (dropDot after)))
_ ->
let (key, after) = T.break (\c -> c == '.' || c == '"') t
in T.strip key : case T.uncons after of
Nothing -> []
Just _ -> parseDottedKey (T.drop 1 (T.stripStart after))
where
dropDot txt = case T.uncons txt of
Just ('.', rest) -> rest
_ -> txt
-- | Parse a TOML value (right side of '=').
parseTOMLValue :: Text -> Either Text TOMLValue
parseTOMLValue t =
let stripped = T.strip t
-- Strip inline comments (not inside strings)
cleaned = stripInlineComment stripped
in case T.uncons cleaned of
Nothing -> Left "empty value"
Just ('"', _)
| T.isPrefixOf "\"\"\"" cleaned -> parseMultilineBasicStr (T.drop 3 cleaned)
| otherwise -> parseBasicStr (T.drop 1 cleaned)
Just ('\'', _)
| T.isPrefixOf "'''" cleaned -> parseMultilineLiteralStr (T.drop 3 cleaned)
| otherwise -> parseLiteralStr (T.drop 1 cleaned)
Just ('{', _) -> parseInlineTable cleaned
Just ('[', _) -> parseInlineArray cleaned
Just ('t', _)
| T.isPrefixOf "true" cleaned -> Right (TOMLBool True)
Just ('f', _)
| T.isPrefixOf "false" cleaned -> Right (TOMLBool False)
Just ('i', _)
| T.isPrefixOf "inf" cleaned -> Right (TOMLFloat (1 / 0))
Just ('+', rest)
| T.isPrefixOf "inf" rest -> Right (TOMLFloat (1 / 0))
| T.isPrefixOf "nan" rest -> Right (TOMLFloat (0 / 0))
Just ('-', rest)
| T.isPrefixOf "inf" rest -> Right (TOMLFloat (negate (1 / 0)))
| T.isPrefixOf "nan" rest -> Right (TOMLFloat (0 / 0))
Just ('n', _)
| T.isPrefixOf "nan" cleaned -> Right (TOMLFloat (0 / 0))
_ -> parseTOMLNumberOrDatetime cleaned
-- | Strip inline comment from a value (not inside quotes).
stripInlineComment :: Text -> Text
stripInlineComment = go (0 :: Int)
where
go _ t | T.null t = T.empty
go depth t = case T.uncons t of
Nothing -> T.empty
Just ('#', _) | depth == (0 :: Int) -> T.empty
Just ('"', rest)
| depth == 0 ->
let (str, after) = T.break (== '"') rest
in T.cons '"' (str <> T.take 1 after <> go depth (T.drop 1 after))
Just ('[', rest) -> T.cons '[' (go (depth + 1) rest)
Just ('{', rest) -> T.cons '{' (go (depth + 1) rest)
Just (']', rest) -> T.cons ']' (go (max 0 (depth - 1)) rest)
Just ('}', rest) -> T.cons '}' (go (max 0 (depth - 1)) rest)
Just (c, rest) -> T.cons c (go depth rest)
-- | Parse a basic (double-quoted) TOML string.
-- O(n) via chunk list + T.concat instead of O(n^2) T.snoc.
parseBasicStr :: Text -> Either Text TOMLValue
parseBasicStr = go []
where
go !chunks t = case T.uncons t of
Nothing -> Left "unterminated basic string"
Just ('"', _) -> Right (TOMLStr (T.concat (reverse chunks)))
Just ('\\', rest) -> case T.uncons rest of
Just ('n', r) -> go ("\n" : chunks) r
Just ('t', r) -> go ("\t" : chunks) r
Just ('r', r) -> go ("\r" : chunks) r
Just ('\\', r) -> go ("\\" : chunks) r
Just ('"', r) -> go ("\"" : chunks) r
Just ('b', r) -> go ("\b" : chunks) r
Just ('f', r) -> go ("\f" : chunks) r
Just ('u', r) -> case parseHex4 r of
Just (cp, r2) -> go (T.singleton (chr cp) : chunks) r2
Nothing -> Left "invalid \\u escape"
_ -> Left "invalid escape sequence"
Just (c, rest) -> go (T.singleton c : chunks) rest
-- | Parse a literal (single-quoted) TOML string.
parseLiteralStr :: Text -> Either Text TOMLValue
parseLiteralStr t =
let (content, rest) = T.break (== '\'') t
in case T.uncons rest of
Just ('\'', _) -> Right (TOMLStr content)
_ -> Left "unterminated literal string"
-- | Parse a multiline basic string.
parseMultilineBasicStr :: Text -> Either Text TOMLValue
parseMultilineBasicStr t =
case T.breakOn "\"\"\"" t of
(content, rest)
| T.isPrefixOf "\"\"\"" rest ->
Right (TOMLStr (T.replace "\\\n" "" (stripLeadingNewline content)))
| otherwise -> Left ("unterminated multiline basic string, remaining: " <> T.take 20 rest)
-- | Parse a multiline literal string.
parseMultilineLiteralStr :: Text -> Either Text TOMLValue
parseMultilineLiteralStr t =
case T.breakOn "'''" t of
(content, rest)
| T.isPrefixOf "'''" rest -> Right (TOMLStr (stripLeadingNewline content))
| otherwise -> Left "unterminated multiline literal string"
-- | Strip a leading newline (TOML spec: first newline after opening quotes is trimmed).
stripLeadingNewline :: Text -> Text
stripLeadingNewline t = case T.uncons t of
Just ('\n', rest) -> rest
Just ('\r', rest) -> case T.uncons rest of
Just ('\n', r) -> r
_ -> rest
_ -> t
-- | Parse a TOML number or datetime.
parseTOMLNumberOrDatetime :: Text -> Either Text TOMLValue
parseTOMLNumberOrDatetime s
-- Hex, octal, binary integers
| T.isPrefixOf "0x" s || T.isPrefixOf "0X" s = parseHexInt (T.drop 2 s)
| T.isPrefixOf "0o" s || T.isPrefixOf "0O" s = parseOctInt (T.drop 2 s)
| T.isPrefixOf "0b" s || T.isPrefixOf "0B" s = parseBinInt (T.drop 2 s)
-- Contains date separators → treat as datetime string
| T.any (== 'T') s && T.any (== '-') s = Right (TOMLStr s)
| T.count "-" s >= 2 && T.any isDigit s = Right (TOMLStr s)
| T.any (== ':') s && T.any isDigit s = Right (TOMLStr s)
-- Float (has dot or exponent)
| T.any (== '.') s || T.any (\c -> c == 'e' || c == 'E') s = parseFloat s
-- Plain integer
| otherwise = parseInt s
-- | Parse a plain decimal integer, ignoring underscores.
parseInt :: Text -> Either Text TOMLValue
parseInt t =
let cleaned = T.filter (/= '_') t
(sign, digits) = case T.uncons cleaned of
Just ('+', rest) -> (1, rest)
Just ('-', rest) -> (-1, rest)
_ -> (1, cleaned)
in case readDecimal digits of
Just n -> Right (TOMLInt (sign * n))
Nothing -> Left ("invalid integer: " <> t)
parseHexInt :: Text -> Either Text TOMLValue
parseHexInt t =
let cleaned = T.filter (/= '_') t
in case readHexT cleaned of
Just n -> Right (TOMLInt n)
Nothing -> Left ("invalid hex integer: " <> t)
parseOctInt :: Text -> Either Text TOMLValue
parseOctInt t =
let cleaned = T.filter (/= '_') t
in case readOctT cleaned of
Just n -> Right (TOMLInt n)
Nothing -> Left ("invalid octal integer: " <> t)
parseBinInt :: Text -> Either Text TOMLValue
parseBinInt t =
let cleaned = T.filter (/= '_') t
in case readBinT cleaned of
Just n -> Right (TOMLInt n)
Nothing -> Left ("invalid binary integer: " <> t)
parseFloat :: Text -> Either Text TOMLValue
parseFloat t =
let cleaned = T.filter (/= '_') t
in case readDouble cleaned of
Just d -> Right (TOMLFloat d)
Nothing -> Left ("invalid float: " <> t)
-- | Read a decimal integer from Text.
readDecimal :: Text -> Maybe Int64
readDecimal t
| T.null t = Nothing
| T.all isDigit t = Just (T.foldl' (\acc c -> acc * 10 + fromIntegral (digitToInt c)) 0 t)
| otherwise = Nothing
readHexT :: Text -> Maybe Int64
readHexT t
| T.null t = Nothing
| T.all isHexDigit t = Just (T.foldl' (\acc c -> acc * 16 + fromIntegral (digitToInt c)) 0 t)
| otherwise = Nothing
readOctT :: Text -> Maybe Int64
readOctT t
| T.null t = Nothing
| T.all isOctDigit t =
Just (T.foldl' (\acc c -> acc * 8 + fromIntegral (digitToInt c)) 0 t)
| otherwise = Nothing
readBinT :: Text -> Maybe Int64
readBinT t
| T.null t = Nothing
| T.all (\c -> c == '0' || c == '1') t =
Just (T.foldl' (\acc c -> acc * 2 + fromIntegral (digitToInt c)) 0 t)
| otherwise = Nothing
readDouble :: Text -> Maybe Double
readDouble t = case reads (T.unpack t) of
[(d, "")] -> Just d
_ -> Nothing
-- | Parse an inline table: @{ key = val, ... }@.
parseInlineTable :: Text -> Either Text TOMLValue
parseInlineTable t = case T.uncons t of
Just ('{', rest) ->
let inner = T.strip (T.dropWhileEnd (== '}') (T.strip rest))
in if T.null inner
then Right (TOMLTable Map.empty)
else do
pairs <- mapM parseInlineKV (splitCommas inner)
Right (TOMLTable (Map.fromList (concatMap flattenPair pairs)))
_ -> Left "expected '{'"
where
flattenPair (keys, val) = case keys of
[] -> []
[k] -> [(k, val)]
(k : ks) -> [(k, nestKeys ks val)]
nestKeys [] v = v
nestKeys (k : ks) v = TOMLTable (Map.singleton k (nestKeys ks v))
-- | Parse an inline array: @[ val, ... ]@.
parseInlineArray :: Text -> Either Text TOMLValue
parseInlineArray t = case T.uncons t of
Just ('[', rest) ->
let inner = T.strip (T.dropWhileEnd (== ']') (T.strip rest))
in if T.null inner
then Right (TOMLArray [])
else do
vals <- mapM (parseTOMLValue . T.strip) (splitCommas inner)
Right (TOMLArray vals)
_ -> Left "expected '['"
-- | Parse a single key=value pair in an inline table.
parseInlineKV :: Text -> Either Text ([Text], TOMLValue)
parseInlineKV t =
let (keyPart, afterEq) = splitAtEquals (T.strip t)
in do
val <- parseTOMLValue (T.strip afterEq)
Right (parseDottedKey (T.strip keyPart), val)
-- | Split on commas not inside brackets or braces.
-- O(n) via chunk list + T.concat instead of O(n^2) T.snoc.
splitCommas :: Text -> [Text]
splitCommas = go (0 :: Int) []
where
finalize chunks =
let t = T.concat (reverse chunks)
in [t | not (T.null (T.strip t))]
go _ !chunks t | T.null t = finalize chunks
go depth !chunks t = case T.uncons t of
Nothing -> finalize chunks
Just (',', rest) | depth == 0 -> T.concat (reverse chunks) : go 0 [] rest
Just ('[', rest) -> go (depth + 1) ("[" : chunks) rest
Just ('{', rest) -> go (depth + 1) ("{" : chunks) rest
Just (']', rest) -> go (max 0 (depth - 1)) ("]" : chunks) rest
Just ('}', rest) -> go (max 0 (depth - 1)) ("}" : chunks) rest
Just ('"', rest) ->
let (str, after) = T.break (== '"') rest
consumed = "\"" <> str <> T.take 1 after
in go depth (consumed : chunks) (T.drop 1 after)
Just (c, rest) -> go depth (T.singleton c : chunks) rest
-- | Insert a value at a nested key path into a table.
insertNested :: [Text] -> TOMLValue -> Map Text TOMLValue -> Map Text TOMLValue
insertNested [] _ m = m
insertNested [k] v m = Map.insert k v m
insertNested (k : ks) v m =
let sub = case Map.lookup k m of
Just (TOMLTable inner) -> inner
_ -> Map.empty
in Map.insert k (TOMLTable (insertNested ks v sub)) m
-- | Ensure a table path exists (for @[table]@ headers).
ensureTable :: [Text] -> Map Text TOMLValue -> Map Text TOMLValue
ensureTable [] m = m
ensureTable [k] m = case Map.lookup k m of
Just (TOMLTable _) -> m
Nothing -> Map.insert k (TOMLTable Map.empty) m
_ -> m
ensureTable (k : ks) m =
let sub = case Map.lookup k m of
Just (TOMLTable inner) -> inner
_ -> Map.empty
in Map.insert k (TOMLTable (ensureTable ks sub)) m
-- | Insert an entry into an array-of-tables (@[[table]]@).
insertArrayTable :: [Text] -> Map Text TOMLValue -> Map Text TOMLValue
insertArrayTable [] m = m
insertArrayTable [k] m = case Map.lookup k m of
Just (TOMLArray xs) -> Map.insert k (TOMLArray (xs ++ [TOMLTable Map.empty])) m
Nothing -> Map.insert k (TOMLArray [TOMLTable Map.empty]) m
_ -> Map.insert k (TOMLArray [TOMLTable Map.empty]) m
insertArrayTable (k : ks) m =
let sub = case Map.lookup k m of
Just (TOMLTable inner) -> inner
Just (TOMLArray xs) ->
-- Descend into the last element of the array
case reverse xs of
(TOMLTable inner : _) -> inner
_ -> Map.empty
_ -> Map.empty
updated = insertArrayTable ks sub
in case Map.lookup k m of
Just (TOMLArray xs) ->
case reverse xs of
(TOMLTable _ : prev) ->
Map.insert k (TOMLArray (reverse prev ++ [TOMLTable updated])) m
_ -> Map.insert k (TOMLTable updated) m
_ -> Map.insert k (TOMLTable updated) m
-- ---------------------------------------------------------------------------
-- Builtin implementations — toXML
-- ---------------------------------------------------------------------------
-- | @builtins.toXML val@ — convert a Nix value to its XML representation.
-- Matches the format defined by the Nix manual: strings, ints, floats,
-- bools, nulls, lists, and attrsets map to their XML counterparts.
builtinToXML :: (MonadEval m) => NixValue -> m NixValue
builtinToXML val = do
xml <- valueToXML 0 val
pure (mkStr ("<?xml version='1.0' encoding='utf-8'?>\n<expr>\n" <> xml <> "</expr>\n"))
valueToXML :: (MonadEval m) => Int -> NixValue -> m Text
valueToXML depth val = case val of
VStr s _ ->
pure (indent depth <> "<string value=" <> xmlQuote s <> " />\n")
VInt n ->
pure (indent depth <> "<int value=\"" <> T.pack (show n) <> "\" />\n")
VFloat d ->
pure (indent depth <> "<float value=\"" <> T.pack (show d) <> "\" />\n")
VBool True ->
pure (indent depth <> "<bool value=\"true\" />\n")
VBool False ->
pure (indent depth <> "<bool value=\"false\" />\n")
VNull ->
pure (indent depth <> "<null />\n")
VPath p ->
pure (indent depth <> "<path value=" <> xmlQuote p <> " />\n")
VList cl -> do
let thunks = map Thunk (clistThunks cl)
items <- mapM (force >=> valueToXML (depth + 1)) thunks
pure (indent depth <> "<list>\n" <> T.concat items <> indent depth <> "</list>\n")
VAttrs attrs -> do
let pairs = attrSetToAscList attrs
items <- mapM (attrToXML (depth + 1)) pairs
pure (indent depth <> "<attrs>\n" <> T.concat items <> indent depth <> "</attrs>\n")
VLambda {} ->
pure (indent depth <> "<function />\n")
VBuiltin _ _ ->
pure (indent depth <> "<function />\n")
VDerivation _ ->
pure (indent depth <> "<derivation />\n")
VCompiledRegex _ ->
pure (indent depth <> "<function />\n")
where
attrToXML d (name, thunk) = do
v <- force thunk
inner <- valueToXML d v
pure (indent d <> "<attr name=" <> xmlQuote name <> ">\n" <> inner <> indent d <> "</attr>\n")
indent :: Int -> Text
indent n = T.replicate (n * 2) " "
xmlQuote :: Text -> Text
xmlQuote s = "\"" <> T.concatMap escapeChar s <> "\""
where
escapeChar '<' = "<"
escapeChar '>' = ">"
escapeChar '&' = "&"
escapeChar '"' = """
escapeChar c = T.singleton c
-- ---------------------------------------------------------------------------
-- Builtin implementations — builtins.path
-- ---------------------------------------------------------------------------
-- | @builtins.path { path; name?; filter?; sha256?; recursive?; }@
--
-- Copy a path to the store and return the store path as a string with
-- context. @name@ defaults to the basename of @path@. @filter@ is
-- accepted but not yet applied (copies everything).
builtinPath :: (MonadEval m) => NixValue -> m NixValue
builtinPath (VAttrs attrs) = do
pathStr <- forceAttrStr "builtins.path" "path" attrs
nameOverride <- forceOptionalAttrStr attrs "name"
let name = fromMaybe (extractBaseName pathStr) nameOverride
storePathText <- copyPathToStore pathStr name
-- Parse the store path to construct proper SCPlain context
case parseStorePath defaultStoreDir storePathText of
Just sp -> pure (VStr storePathText (plainContext sp))
Nothing -> pure (VStr storePathText emptyContext)
builtinPath other =
throwEvalError ("builtins.path: expected an attribute set, got " <> typeName other)
-- | Extract the last path component from a path string.
extractBaseName :: Text -> Text
extractBaseName path =
let stripped = T.dropWhileEnd (\c -> c == '/' || c == '\\') path
in case T.breakOnEnd "/" stripped of
("", _) -> case T.breakOnEnd "\\" stripped of
("", _) -> stripped
(_, name) -> name
(_, name) -> name
-- ---------------------------------------------------------------------------
-- Builtin implementations — filterSource
-- ---------------------------------------------------------------------------
-- | @builtins.filterSource filter path@ — copy a path to the store,
-- filtering entries via a predicate. The filter function receives
-- @(path, type)@ where type is @"regular"@, @"directory"@, @"symlink"@,
-- or @"unknown"@.
builtinFilterSource :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinFilterSource _filterFn (VPath _path) =
throwEvalError "builtins.filterSource: not yet implemented (requires store integration)"
builtinFilterSource _filterFn (VStr _path _) =
throwEvalError "builtins.filterSource: not yet implemented (requires store integration)"
builtinFilterSource _ other =
throwEvalError ("builtins.filterSource: expected a path, got " <> typeName other)
-- ---------------------------------------------------------------------------
-- Builtin stubs — experimental features
-- ---------------------------------------------------------------------------
builtinOutputOf :: (MonadEval m) => NixValue -> NixValue -> m NixValue
builtinOutputOf _ _ =
throwEvalError "builtins.outputOf: requires experimental feature 'dynamic-derivations'"
builtinFetchTree :: (MonadEval m) => NixValue -> m NixValue
builtinFetchTree _ =
throwEvalError "builtins.fetchTree: requires experimental feature 'fetch-tree'"
builtinFetchClosure :: (MonadEval m) => NixValue -> m NixValue
builtinFetchClosure _ =
throwEvalError "builtins.fetchClosure: requires experimental feature 'fetch-closure'"