hnix/src/Nix/Builtins.hs

895 lines
36 KiB
Haskell
Raw Normal View History

2018-04-07 21:02:50 +02:00
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
module Nix.Builtins (MonadBuiltins, baseEnv) where
import Control.Monad
import Control.Monad.Fix
import Control.Monad.ListM (sortByM)
import Control.Monad.Reader
import qualified Crypto.Hash.MD5 as MD5
import qualified Crypto.Hash.SHA1 as SHA1
import qualified Crypto.Hash.SHA256 as SHA256
import qualified Crypto.Hash.SHA512 as SHA512
import Data.Aeson (toJSON)
import qualified Data.Aeson as A
import qualified Data.Aeson.Encoding as A
import Data.Align (alignWith)
import Data.Array
import Data.ByteString (ByteString)
import Data.ByteString.Base16 as Base16
import qualified Data.ByteString.Lazy as LBS
import Data.Char (isDigit)
import Data.Coerce
import Data.Foldable (foldlM)
import Data.Functor.Compose
import Data.HashMap.Lazy (HashMap)
import qualified Data.HashMap.Lazy as M
import qualified Data.HashMap.Strict.InsOrd as OM
import Data.List
import Data.Maybe
import Data.Scientific
import Data.Semigroup
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Encoding
import qualified Data.Text.Lazy as LazyText
import qualified Data.Text.Lazy.Builder as Builder
import Data.These (fromThese)
import Data.Traversable (mapM)
import qualified Data.Vector as V
import GHC.Stack.Types (HasCallStack)
import Nix.Atoms
import Nix.Eval
import Nix.Expr.Types
import Nix.Expr.Types.Annotated
import Nix.Monad
import Nix.Pretty
import Nix.Scope
import Nix.Stack
import Nix.Thunk
import Nix.Utils
import Nix.Value
import Nix.XML
import System.FilePath
import System.Posix.Files
import Text.Regex.TDFA
type MonadBuiltins e m =
(MonadEval e m, MonadNix m, MonadFix m, MonadFile m, MonadVar m)
baseEnv :: MonadBuiltins e m => m (Scopes m (NThunk m))
baseEnv = do
ref <- thunk $ flip NVSet M.empty <$> builtins
let pos = repeatingThunk curPos -- re-evaluate each time it's forced
lst <- ([ ("builtins", ref)
, ("__curPos", pos)
] ++)
<$> topLevelBuiltins
pushScope (M.fromList lst) currentScopes
where
topLevelBuiltins = map mapping . filter isTopLevel <$> builtinsList
builtins :: MonadBuiltins e m => m (ValueSet m)
builtins = M.fromList . map mapping <$> builtinsList
data BuiltinType = Normal | TopLevel
data Builtin m = Builtin
{ kind :: BuiltinType
, mapping :: (Text, NThunk m)
}
isTopLevel :: Builtin m -> Bool
isTopLevel b = case kind b of Normal -> False; TopLevel -> True
builtinsList :: forall e m. MonadBuiltins e m => m [ Builtin m ]
builtinsList = sequence [
pure $ Builtin Normal ("nixVersion", valueThunk $ NVStr "2.0" mempty)
, add TopLevel "toString" toString
, add TopLevel "import" import_
, add2 TopLevel "map" map_
, add' TopLevel "baseNameOf" (arity1 baseNameOf)
, add TopLevel "dirOf" dirOf
, add2 TopLevel "removeAttrs" removeAttrs
, add TopLevel "isNull" isNull
, add TopLevel "abort" throw_ -- for now
, add TopLevel "throw" throw_
, add2 TopLevel "scopedImport" scopedImport
, add Normal "getEnv" getEnv_
, add2 Normal "hasAttr" hasAttr
, add2 Normal "getAttr" getAttr
, add2 Normal "unsafeGetAttrPos" unsafeGetAttrPos
, add2 Normal "any" any_
, add2 Normal "all" all_
, add3 Normal "foldl'" foldl'_
, add Normal "head" head_
, add Normal "tail" tail_
, add Normal "splitVersion" splitVersion_
, add2 Normal "compareVersions" compareVersions_
, add2 Normal "match" match_
--TODO: Support floats for `add` and `sub`
, add' Normal "add" (arity2 ((+) @Integer))
, add' Normal "sub" (arity2 ((-) @Integer))
, add' Normal "parseDrvName" parseDrvName
, add' Normal "substring" substring
, add' Normal "stringLength" (arity1 Text.length)
, add Normal "length" length_
, add Normal "attrNames" attrNames
, add Normal "attrValues" attrValues
, add2 Normal "catAttrs" catAttrs
, add' Normal "concatStringsSep" (arity2 Text.intercalate)
, add Normal "unsafeDiscardStringContext" unsafeDiscardStringContext
, add2 Normal "seq" seq_
, add2 Normal "deepSeq" deepSeq
, add2 Normal "elem" elem_
, add2 Normal "elemAt" elemAt_
, add2 Normal "genList" genList
, add2 Normal "filter" filter_
, add' Normal "replaceStrings" replaceStrings
, add Normal "pathExists" pathExists_
, add Normal "toPath" toPath
, add Normal "isAttrs" isAttrs
, add Normal "isList" isList
, add Normal "isFunction" isFunction
, add Normal "isString" isString
, add Normal "isInt" isInt
, add Normal "isFloat" isFloat
, add Normal "isBool" isBool
, add2 Normal "sort" sort_
, add2 Normal "lessThan" lessThan
, add Normal "concatLists" concatLists
, add Normal "listToAttrs" listToAttrs
, add2 Normal "intersectAttrs" intersectAttrs
, add Normal "functionArgs" functionArgs
, add' Normal "hashString" hashString
, add Normal "readFile" readFile_
, add Normal "readDir" readDir_
, add' Normal "toJSON"
(arity1 $ decodeUtf8 . LBS.toStrict . A.encodingToLazyByteString
. toEncodingSorted)
, add Normal "fromJSON" fromJSON
, add Normal "toXML" toXML_
, add Normal "typeOf" typeOf
, add2 Normal "partition" partition_
, add0 Normal "currentSystem" currentSystem
]
where
wrap t n f = Builtin t (n, f)
arity1 f = Prim . pure . f
arity2 f = ((Prim . pure) .) . f
add0 t n v = wrap t n <$> thunk v
add t n v = wrap t n <$> thunk (builtin (Text.unpack n) v)
add2 t n v = wrap t n <$> thunk (builtin2 (Text.unpack n) v)
add3 t n v = wrap t n <$> thunk (builtin3 (Text.unpack n) v)
add' :: ToBuiltin m a => BuiltinType -> Text -> a -> m (Builtin m)
add' t n v = wrap t n <$> thunk (toBuiltin (Text.unpack n) v)
-- Helpers
mkBool :: Monad m => Bool -> m (NValue m)
mkBool = return . NVConstant . NBool
extractBool :: MonadBuiltins e m => NValue m -> m Bool
extractBool = \case
NVConstant (NBool b) -> return b
_ -> throwError "Not a boolean constant"
extractInt :: MonadBuiltins e m => NValue m -> m Int
extractInt = \case
NVConstant (NInt b) -> return $ fromIntegral b
_ -> throwError "Not an integer constant"
apply :: MonadBuiltins e m
=> NThunk m -> NThunk m -> m (NValue m)
apply f arg = force f $ \f' -> pure f' `evalApp` arg
-- Primops
deltaInfo :: Delta -> (Text, Int, Int)
deltaInfo = \case
Columns c _ -> ("<string>", 1, fromIntegral c + 1)
Tab {} -> ("<string>", 1, 1)
Lines l _ _ _ -> ("<string>", fromIntegral l + 1, 1)
Directed fn l c _ _ -> (decodeUtf8 fn,
fromIntegral l + 1, fromIntegral c + 1)
posFromDelta :: Delta -> NValue m
posFromDelta (deltaInfo -> (f, l, c)) =
flip NVSet M.empty $ M.fromList
[ ("file", valueThunk $ NVStr f mempty)
, ("line", valueThunk $ NVConstant (NInt (fromIntegral l)))
, ("column", valueThunk $ NVConstant (NInt (fromIntegral c)))
]
curPos :: forall e m. Framed e m => m (NValue m)
curPos = do
Compose (Ann (SrcSpan delta _) _):_ <-
asks (mapMaybe (either (const Nothing) Just)
. view @_ @Frames hasLens)
return $ posFromDelta delta
toString :: MonadBuiltins e m => NThunk m -> m (NValue m)
toString str = do
(s, d) <- force str $ normalForm >=> valueText False
return $ NVStr s d
hasAttr :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
hasAttr x y = force x $ \x' -> force y $ \y' -> case (x', y') of
(NVStr key _, NVSet aset _) ->
return . NVConstant . NBool $ M.member key aset
(x, y) -> throwError $ "Invalid types for builtin.hasAttr: "
++ show (void x, void y)
getAttr :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
getAttr x y = force x $ \x' -> force y $ \y' -> case (x', y') of
(NVStr key _, NVSet aset _) -> case M.lookup key aset of
Nothing -> throwError $ "getAttr: field does not exist: "
++ Text.unpack key
Just action -> force action pure
(x, y) -> throwError $ "Invalid types for builtin.getAttr: "
++ show (void x, void y)
unsafeGetAttrPos :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
unsafeGetAttrPos x y = force x $ \x' -> force y $ \y' -> case (x', y') of
(NVStr key _, NVSet _ apos) -> case M.lookup key apos of
Nothing ->
throwError $ "unsafeGetAttrPos: field '" ++ Text.unpack key
++ "' does not exist in attr set: " ++ show apos
Just delta -> return $ posFromDelta delta
(x, y) -> throwError $ "Invalid types for builtin.unsafeGetAttrPos: "
++ show (void x, void y)
length_ :: MonadBuiltins e m => NThunk m -> m (NValue m)
length_ = flip force $ \case
NVList l -> return $ NVConstant $ NInt (fromIntegral (length l))
arg -> throwError $ "builtins.length takes a list, not a "
++ show (void arg)
anyM :: Monad m => (a -> m Bool) -> [a] -> m Bool
anyM _ [] = return False
anyM p (x:xs) = do
q <- p x
if q then return True
else anyM p xs
any_ :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
any_ pred = flip force $ \case
NVList l ->
mkBool =<< anyM extractBool =<< mapM (apply pred) l
arg -> throwError $ "builtins.any takes a list as second argument, not a "
++ show (void arg)
allM :: Monad m => (a -> m Bool) -> [a] -> m Bool
allM _ [] = return True
allM p (x:xs) = do
q <- p x
if q then allM p xs
else return False
all_ :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
all_ pred = flip force $ \case
NVList l ->
mkBool =<< allM extractBool =<< mapM (apply pred) l
arg -> throwError $ "builtins.all takes a list as second argument, not a "
++ show (void arg)
--TODO: Strictness
foldl'_ :: MonadBuiltins e m => NThunk m -> NThunk m -> NThunk m -> m (NValue m)
foldl'_ f z = flip force $ \case
NVList vals -> (`force` pure) =<< foldlM go z vals
arg -> throwError $ "builtins.foldl' takes a list as third argument, not a "
++ show (void arg)
where
go b a = thunk $ f `apply` a `evalApp` b
head_ :: MonadBuiltins e m => NThunk m -> m (NValue m)
head_ = flip force $ \case
NVList vals -> case vals of
[] -> throwError "builtins.head: empty list"
h:_ -> force h pure
_ -> throwError "builtins.head: not a list"
tail_ :: MonadBuiltins e m => NThunk m -> m (NValue m)
tail_ = flip force $ \case
NVList vals -> case vals of
[] -> throwError "builtins.tail: empty list"
_:t -> return $ NVList t
_ -> throwError "builtins.tail: not a list"
data VersionComponent
= VersionComponent_Pre -- ^ The string "pre"
| VersionComponent_String Text -- ^ A string other than "pre"
| VersionComponent_Number Integer -- ^ A number
deriving (Show, Read, Eq, Ord)
versionComponentToString :: VersionComponent -> Text
versionComponentToString = \case
VersionComponent_Pre -> "pre"
VersionComponent_String s -> s
VersionComponent_Number n -> Text.pack $ show n
-- | Based on https://github.com/NixOS/nix/blob/4ee4fda521137fed6af0446948b3877e0c5db803/src/libexpr/names.cc#L44
versionComponentSeparators :: String
versionComponentSeparators = ".-"
splitVersion :: Text -> [VersionComponent]
splitVersion s = case Text.uncons s of
Nothing -> []
Just (h, t)
| h `elem` versionComponentSeparators -> splitVersion t
| isDigit h ->
let (digits, rest) = Text.span isDigit s
in VersionComponent_Number (read $ Text.unpack digits) : splitVersion rest
| otherwise ->
let (chars, rest) = Text.span (\c -> not $ isDigit c || c `elem` versionComponentSeparators) s
thisComponent = case chars of
"pre" -> VersionComponent_Pre
x -> VersionComponent_String x
in thisComponent : splitVersion rest
splitVersion_ :: MonadBuiltins e m => NThunk m -> m (NValue m)
splitVersion_ = flip force $ \case
NVStr s _ -> do
let vals = flip map (splitVersion s) $ \c ->
valueThunk $ NVStr (versionComponentToString c) mempty
return $ NVList vals
_ -> throwError "builtins.splitVersion: not a string"
compareVersions :: Text -> Text -> Ordering
compareVersions s1 s2 =
mconcat $ alignWith f (splitVersion s1) (splitVersion s2)
where
z = VersionComponent_String ""
f = uncurry compare . fromThese z z
compareVersions_ :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
compareVersions_ t1 t2 = force t1 $ \v1 -> force t2 $ \v2 -> case (v1, v2) of
(NVStr s1 _, NVStr s2 _) ->
return $ NVConstant $ NInt $ case compareVersions s1 s2 of
LT -> -1
EQ -> 0
GT -> 1
_ -> throwError "builtins.splitVersion: not a string"
splitDrvName :: Text -> (Text, Text)
splitDrvName s =
let sep = "-"
pieces = Text.splitOn sep s
isFirstVersionPiece p = case Text.uncons p of
Just (h, _) | isDigit h -> True
_ -> False
-- Like 'break', but always puts the first item into the first result
-- list
breakAfterFirstItem :: (a -> Bool) -> [a] -> ([a], [a])
breakAfterFirstItem f = \case
h : t ->
let (a, b) = break f t
in (h : a, b)
[] -> ([], [])
(namePieces, versionPieces) =
breakAfterFirstItem isFirstVersionPiece pieces
in (Text.intercalate sep namePieces, Text.intercalate sep versionPieces)
parseDrvName :: Applicative m => Text -> Prim m (HashMap Text Text)
parseDrvName s = Prim $ pure $ M.fromList [("name", name), ("version", version)]
where (name, version) = splitDrvName s
match_ :: forall e m. MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
match_ pat str = force pat $ \pat' -> force str $ \str' ->
case (pat', str') of
-- jww (2018-04-05): We should create a fundamental type for compiled
-- regular expressions if it turns out they get used often.
(NVStr p _, NVStr s _) -> return $ NVList $
let re = makeRegex (encodeUtf8 p) :: Regex
in case matchOnceText re (encodeUtf8 s) of
Just ("", sarr, "") -> let s = map fst (elems sarr) in
map (valueThunk @m . flip NVStr mempty . decodeUtf8)
(if length s > 1 then tail s else s)
_ -> []
(p, s) ->
throwError $ "builtins.match: expected a regex"
++ " and a string, but got: " ++ show (p, s)
substring :: Applicative m => Int -> Int -> Text -> Prim m Text
substring start len =
if start < 0 --NOTE: negative values of 'len' are OK
then error $ "builtins.substring: negative start position: " ++ show start
else Prim . pure . Text.take len . Text.drop start
attrNames :: MonadBuiltins e m => NThunk m -> m (NValue m)
attrNames = flip force $ \case
NVSet m _ -> toValue $ sort $ M.keys m
v -> error $ "builtins.attrNames: Expected attribute set, got "
++ showValue v
attrValues :: MonadBuiltins e m => NThunk m -> m (NValue m)
attrValues = flip force $ \case
NVSet m _ -> return $ NVList $ fmap snd $ sortOn fst $ M.toList m
v -> error $ "builtins.attrValues: Expected attribute set, got "
++ showValue v
map_ :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
map_ f = flip force $ \case
NVList l -> NVList <$> traverse (fmap valueThunk . apply f) l
v -> error $ "map: Expected list, got " ++ showValue v
filter_ :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
filter_ f = flip force $ \case
NVList l -> NVList <$> filterM (extractBool <=< apply f) l
v -> error $ "map: Expected list, got " ++ showValue v
catAttrs :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
catAttrs attrName lt = force lt $ \case
NVList l -> fmap (NVList . catMaybes) $ forM l $ flip force $ \case
NVSet m _ -> force attrName $ \case
NVStr n _ -> return $ M.lookup n m
v -> throwError $ "builtins.catAttrs: Expected a string, got "
++ showValue v
v -> throwError $ "builtins.catAttrs: Expected a set, got "
++ showValue v
v -> throwError $ "builtins.catAttrs: Expected a list, got "
++ showValue v
--TODO: Make this have similar logic to dirOf
baseNameOf :: Text -> Text
baseNameOf = Text.pack . takeFileName . Text.unpack
dirOf :: MonadBuiltins e m => NThunk m -> m (NValue m)
dirOf = flip force $ \case
--TODO: Only allow strings that represent absolute paths
NVStr path ctx -> pure $ NVStr (Text.pack $ takeDirectory $ Text.unpack path) ctx
NVLiteralPath path -> pure $ NVLiteralPath $ takeDirectory path
--TODO: NVEnvPath
v -> throwError $ "dirOf: expected string or path, got " ++ showValue v
unsafeDiscardStringContext :: MonadBuiltins e m => NThunk m -> m (NValue m)
unsafeDiscardStringContext = flip force $ \case
NVStr s _ -> pure $ NVStr s mempty
v -> throwError $ "builtins.unsafeDiscardStringContext: "
++ "Expected a string, got " ++ showValue v
seq_ :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
seq_ a b = force a (const (force b pure))
deepSeq :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
deepSeq a b = do
-- We evaluate 'a' only for its effects, so data cycles are ignored.
_ <- forceEffects (coerce a) $ \a' ->
normalFormBy (forceEffects . coerce) a'
-- Then we evaluate the other argument to deepseq, thus this function
-- should always produce a result (unlike applying 'deepseq' on infinitely
-- recursive data structures in Haskell).
force b pure
elem_ :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
elem_ x xs = force xs $ \case
NVList l -> toValue =<< anyM (thunkEq x) l
v -> throwError $ "builtins.elem: Expected a list, got " ++ showValue v
elemAt_ :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
elemAt_ xs n = force n $ extractInt >=> \n' -> force xs $ \case
NVList l | n' < length l -> force (l !! n') pure
| otherwise ->
throwError $ "builtins.elem: Index " ++ show n'
++ " too large for list of length " ++ show (length l)
v -> throwError $ "builtins.elem: Expected a list, got " ++ showValue v
genList :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
genList generator length = force length $ \case
NVConstant (NInt n) | n >= 0 -> fmap NVList $ forM [0 .. n - 1] $ \i ->
thunk $ apply generator =<< valueThunk <$> toValue i
v -> throwError $ "builtins.genList: Expected a non-negative number, got "
++ showValue v
--TODO: Preserve string context
replaceStrings :: MonadBuiltins e m => [Text] -> [Text] -> Text -> Prim m Text
replaceStrings from to s = Prim $ do
when (length from /= length to) $
throwError $ "'from' and 'to' arguments to 'replaceStrings'"
++ " have different lengths"
let lookupPrefix s = do
(prefix, replacement) <-
find ((`Text.isPrefixOf` s) . fst) $ zip from to
let rest = Text.drop (Text.length prefix) s
return (prefix, replacement, rest)
finish = LazyText.toStrict . Builder.toLazyText
go orig result = case lookupPrefix orig of
Nothing -> case Text.uncons orig of
Nothing -> finish result
Just (h, t) -> go t $ result <> Builder.singleton h
Just (prefix, replacement, rest) -> case prefix of
"" -> case Text.uncons rest of
Nothing -> finish $ result <> Builder.fromText replacement
Just (h, t) -> go t $ mconcat
[ result
, Builder.fromText replacement
, Builder.singleton h
]
_ -> go rest $ result <> Builder.fromText replacement
return $ go s mempty
removeAttrs :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
removeAttrs set list = fromThunk @[Text] list $ \toRemove ->
force set $ \case
NVSet m p -> return $ NVSet (go m toRemove) (go p toRemove)
v -> throwError $ "removeAttrs: expected set, got " ++ showValue v
where
go = foldl' (flip M.delete)
intersectAttrs :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
intersectAttrs set1 set2 = force set1 $ \set1' -> force set2 $ \set2' ->
case (set1', set2') of
(NVSet s1 p1, NVSet s2 p2) ->
return $ NVSet (s2 `M.intersection` s1) (p2 `M.intersection` p1)
(v1, v2) ->
throwError $ "builtins.intersectAttrs: expected two sets, got "
++ showValue v1 ++ " and " ++ showValue v2
functionArgs :: MonadBuiltins e m => NThunk m -> m (NValue m)
functionArgs fun = force fun $ \case
NVClosure _ p _ ->
-- jww (2018-04-05): Should we preserve the location where the
-- function arguments were declared for __unsafeGetAttrPos?
return $ flip NVSet M.empty $ valueThunk . NVConstant . NBool <$>
case p of
Param name -> M.singleton name False
ParamSet s _ _ -> isJust <$> OM.toHashMap s
v -> throwError $ "builtins.functionArgs: expected function, got "
++ showValue v
toPath :: MonadBuiltins e m => NThunk m -> m (NValue m)
toPath = flip force $ \case
NVStr p@(Text.uncons -> Just ('/', _)) _ ->
return $ NVLiteralPath (Text.unpack p)
v@(NVLiteralPath _) -> return v
v@(NVEnvPath _) -> return v
v -> throwError $ "builtins.toPath: expected string, got " ++ showValue v
pathExists_ :: MonadBuiltins e m => NThunk m -> m (NValue m)
pathExists_ = flip force $ \case
NVLiteralPath p -> mkBool =<< pathExists p
NVEnvPath p -> mkBool =<< pathExists p
v -> throwError $ "builtins.pathExists: expected path, got " ++ showValue v
isAttrs :: MonadBuiltins e m => NThunk m -> m (NValue m)
isAttrs = flip force $ \case
NVSet _ _ -> toValue True
_ -> toValue False
isList :: MonadBuiltins e m => NThunk m -> m (NValue m)
isList = flip force $ \case
NVList _ -> toValue True
_ -> toValue False
isFunction :: MonadBuiltins e m => NThunk m -> m (NValue m)
isFunction = flip force $ \case
NVClosure {} -> toValue True
_ -> toValue False
isString :: MonadBuiltins e m => NThunk m -> m (NValue m)
isString = flip force $ \case
NVStr _ _ -> toValue True
_ -> toValue False
isInt :: MonadBuiltins e m => NThunk m -> m (NValue m)
isInt = flip force $ \case
NVConstant (NInt _) -> toValue True
_ -> toValue False
isFloat :: MonadBuiltins e m => NThunk m -> m (NValue m)
isFloat = flip force $ \case
NVConstant (NFloat _) -> toValue True
_ -> toValue False
isBool :: MonadBuiltins e m => NThunk m -> m (NValue m)
isBool = flip force $ \case
NVConstant (NBool _) -> toValue True
_ -> toValue False
isNull :: MonadBuiltins e m => NThunk m -> m (NValue m)
isNull = flip force $ \case
NVConstant NNull -> toValue True
_ -> toValue False
throw_ :: MonadBuiltins e m => NThunk m -> m (NValue m)
throw_ = flip force $ \case
NVStr t _ -> throwError (Text.unpack t)
v -> throwError $ "builtins.throw: expected string, got " ++ showValue v
import_ :: MonadBuiltins e m => NThunk m -> m (NValue m)
import_ = flip force $ \case
NVLiteralPath p -> importFile M.empty p
NVEnvPath p -> importFile M.empty p -- jww (2018-04-06): is this right?
v -> throwError $ "import: expected path, got " ++ showValue v
scopedImport :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
scopedImport aset path = force aset $ \aset' -> force path $ \path' ->
case (aset', path') of
(NVSet s _, NVLiteralPath p) -> importFile s p
(NVSet s _, NVEnvPath p) -> importFile s p
(s, p) -> throwError $ "scopedImport: expected a set and a path, got "
++ showValue s ++ " and " ++ showValue p
getEnv_ :: MonadBuiltins e m => NThunk m -> m (NValue m)
getEnv_ = flip force $ \case
NVStr s _ -> do
mres <- getEnvVar (Text.unpack s)
return $ case mres of
Nothing -> NVStr "" mempty
Just v -> NVStr (Text.pack v) mempty
p -> error $ "Unexpected argument to getEnv: " ++ show (void p)
sort_ :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
sort_ comparator list = force list $ \case
NVList l -> NVList <$> sortByM cmp l
where
cmp a b = do
isLessThan <- comparator `apply` a `evalApp` b
fromValue isLessThan >>= \case
True -> pure LT
False -> do
isGreaterThan <- comparator `apply` b `evalApp` a
fromValue isGreaterThan >>= \case
True -> pure GT
False -> pure EQ
v -> throwError $ "builtins.sort: expected list, got " ++ showValue v
lessThan :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
lessThan ta tb = force ta $ \va -> force tb $ \vb -> do
let badType = throwError $ "builtins.lessThan: expected two numbers or two strings, "
++ "got " ++ showValue va ++ " and " ++ showValue vb
NVConstant . NBool <$> case (va, vb) of
(NVConstant ca, NVConstant cb) -> case (ca, cb) of
(NInt a, NInt b) -> pure $ a < b
(NFloat a, NInt b) -> pure $ a < fromInteger b
(NInt a, NFloat b) -> pure $ fromInteger a < b
(NFloat a, NFloat b) -> pure $ a < b
_ -> badType
(NVStr a _, NVStr b _) -> pure $ a < b
_ -> badType
concatLists :: MonadBuiltins e m => NThunk m -> m (NValue m)
concatLists = flip force $ \case
NVList l -> fmap (NVList . concat) $ forM l $ flip force $ \case
NVList i -> pure i
v -> throwError $ "builtins.concatLists: expected list, got " ++ showValue v
v -> throwError $ "builtins.concatLists: expected list, got " ++ showValue v
listToAttrs :: MonadBuiltins e m => NThunk m -> m (NValue m)
listToAttrs = flip force $ \case
NVList l -> fmap (flip NVSet M.empty . M.fromList . reverse) $
forM l $ flip force $ \case
NVSet s _ -> case (M.lookup "name" s, M.lookup "value" s) of
(Just name, Just value) -> force name $ \case
NVStr n _ -> return (n, value)
v -> throwError $
"builtins.listToAttrs: expected name to be a string, got "
++ showValue v
_ -> throwError $
"builtins.listToAttrs: expected set with name and value, got"
++ show s
v -> throwError $ "builtins.listToAttrs: expected set, got " ++ showValue v
v -> throwError $ "builtins.listToAttrs: expected list, got " ++ showValue v
hashString :: MonadBuiltins e m => Text -> Text -> Prim m Text
hashString algo s = Prim $ do
hash <- case algo of
"md5" -> pure MD5.hash
"sha1" -> pure SHA1.hash
"sha256" -> pure SHA256.hash
"sha512" -> pure SHA512.hash
_ -> throwError $ "builtins.hashString: "
++ "expected \"md5\", \"sha1\", \"sha256\", or \"sha512\", got " ++ show algo
pure $ decodeUtf8 $ Base16.encode $ hash $ encodeUtf8 s
absolutePathFromValue :: MonadBuiltins e m => NValue m -> m FilePath
absolutePathFromValue = \case
NVStr pathText _ -> do
let path = Text.unpack pathText
unless (isAbsolute path) $
throwError $ "string " ++ show path ++ " doesn't represent an absolute path"
pure path
NVLiteralPath path -> pure path
NVEnvPath path -> pure path
v -> throwError $ "expected a path, got " ++ showValue v
--TODO: Move all liftIO things into MonadNixEnv or similar
readFile_ :: MonadBuiltins e m => NThunk m -> m (NValue m)
readFile_ pathThunk = do
path <- force pathThunk absolutePathFromValue
toValue =<< Nix.Stack.readFile path
data FileType
= FileType_Regular
| FileType_Directory
| FileType_Symlink
| FileType_Unknown
deriving (Show, Read, Eq, Ord)
instance ToNix FileType where
toValue = toValue . \case
FileType_Regular -> "regular" :: Text
FileType_Directory -> "directory"
FileType_Symlink -> "symlink"
FileType_Unknown -> "unknown"
readDir_ :: MonadBuiltins e m => NThunk m -> m (NValue m)
readDir_ pathThunk = do
path <- force pathThunk absolutePathFromValue
items <- listDirectory path
itemsWithTypes <- forM items $ \item -> do
s <- Nix.Monad.getSymbolicLinkStatus $ path </> item
let t = if
| isRegularFile s -> FileType_Regular
| isDirectory s -> FileType_Directory
| isSymbolicLink s -> FileType_Symlink
| otherwise -> FileType_Unknown
pure (Text.pack item, t)
toValue $ M.fromList itemsWithTypes
fromJSON :: MonadBuiltins e m => NThunk m -> m (NValue m)
fromJSON t = fromThunk t $ \encoded ->
case A.eitherDecodeStrict' @A.Value $ encodeUtf8 encoded of
Left jsonError -> throwError $ "builtins.fromJSON: " ++ jsonError
Right v -> toValue v
toXML_ :: MonadBuiltins e m => NThunk m -> m (NValue m)
toXML_ = flip force $ normalForm >=> \x ->
pure $ NVStr (Text.pack (toXML x)) mempty
typeOf :: MonadBuiltins e m => NThunk m -> m (NValue m)
typeOf t = force t $ \v -> toValue @Text $ case v of
NVConstant a -> case a of
NInt _ -> "int"
NFloat _ -> "float"
NBool _ -> "bool"
NNull -> "null"
NUri _ -> "string" --TODO: Should we get rid of NUri?
NVStr _ _ -> "string"
NVList _ -> "list"
NVSet _ _ -> "set"
NVClosure {} -> "lambda"
NVLiteralPath _ -> "path"
NVEnvPath _ -> "path"
NVBuiltin _ _ -> "lambda"
partition_ :: MonadBuiltins e m => NThunk m -> NThunk m -> m (NValue m)
partition_ f = flip force $ \case
NVList l -> do
let match t = apply f t >>= \case
NVConstant (NBool b) -> return (b, t)
v -> error $ "partition: Expected boolean, got " ++ showValue v
selection <- traverse match l
let (right, wrong) = partition fst selection
let makeSide = valueThunk . NVList . map snd
return $ flip NVSet M.empty $
M.fromList [("right", makeSide right), ("wrong", makeSide wrong)]
v -> error $ "partition: Expected list, got " ++ showValue v
currentSystem :: MonadNix m => m (NValue m)
currentSystem = do
os <- getCurrentSystemOS
arch <- getCurrentSystemArch
return $ NVStr (os <> "-" <> arch) mempty
newtype Prim m a = Prim { runPrim :: m a }
class ToNix a where
toValue :: MonadBuiltins e m => a -> m (NValue m)
instance ToNix Bool where
toValue = return . NVConstant . NBool
instance ToNix Text where
toValue s = return $ NVStr s mempty
instance ToNix ByteString where
toValue s = return $ NVStr (decodeUtf8 s) mempty
instance ToNix Int where
toValue = toValue . toInteger
instance ToNix Integer where
toValue = return . NVConstant . NInt
instance ToNix a => ToNix (HashMap Text a) where
toValue m = flip NVSet M.empty <$> traverse (thunk . toValue) m
instance ToNix a => ToNix [a] where
toValue m = NVList <$> traverse (thunk . toValue) m
instance ToNix A.Value where
toValue = \case
A.Object m -> flip NVSet M.empty <$> traverse (thunk . toValue) m
A.Array l -> NVList <$> traverse (thunk . toValue) (V.toList l)
A.String s -> pure $ NVStr s mempty
A.Number n -> pure $ NVConstant $ case floatingOrInteger n of
Left r -> NFloat r
Right i -> NInt i
A.Bool b -> pure $ NVConstant $ NBool b
A.Null -> pure $ NVConstant NNull
-- | Types that support conversion to nix in a particular monad
class ToBuiltin m a | a -> m where
toBuiltin :: String -> a -> m (NValue m)
instance (MonadBuiltins e m, ToNix a) => ToBuiltin m (Prim m a) where
toBuiltin _ p = toValue =<< runPrim p
instance (MonadBuiltins e m, FromNix a, ToBuiltin m b)
=> ToBuiltin m (a -> b) where
toBuiltin name f =
return $ NVBuiltin name $ fromThunk ?? (toBuiltin name . f)
class FromNix a where
--TODO: Get rid of the HasCallStack - it should be captured by whatever
--error reporting mechanism we add
fromValue :: (HasCallStack, MonadBuiltins e m) => NValue m -> m a
fromThunk :: (FromNix a, HasCallStack, MonadBuiltins e m)
=> NThunk m -> (a -> m r) -> m r
fromThunk t f = force t (f <=< fromValue)
instance FromNix Bool where
fromValue = \case
NVConstant (NBool b) -> pure b
v -> throwError $ "fromValue: Expected bool, got " ++ showValue v
instance FromNix Text where
fromValue = \case
NVStr s _ -> pure s
v -> throwError $ "fromValue: Expected string, got " ++ showValue v
instance FromNix Int where
fromValue = fmap fromInteger . fromValue
instance FromNix Integer where
fromValue = \case
NVConstant (NInt n) -> pure n
v -> throwError $ "fromValue: Expected number, got " ++ showValue v
instance FromNix a => FromNix [a] where
fromValue = \case
NVList l -> traverse (`force` fromValue) l
v -> throwError $ "fromValue: Expected list, got " ++ showValue v
toEncodingSorted :: A.Value -> A.Encoding
toEncodingSorted = \case
A.Object m -> A.pairs $ mconcat $ fmap (\(k, v) -> A.pair k $ toEncodingSorted v) $ sortOn fst $ M.toList m
A.Array l -> A.list toEncodingSorted $ V.toList l
v -> A.toEncoding v
instance FromNix A.Value where
fromValue = \case
NVConstant a -> pure $ case a of
NInt n -> toJSON n
NFloat n -> toJSON n
NBool b -> toJSON b
NNull -> A.Null
NUri u -> toJSON u
NVStr s _ -> pure $ toJSON s
NVList l -> A.Array . V.fromList <$> traverse (`force` fromValue) l
NVSet m _ -> A.Object <$> traverse (`force` fromValue) m
NVClosure {} -> throwError "cannot convert a function to JSON"
NVLiteralPath p -> toJSON . unStorePath <$> addPath p
NVEnvPath p -> toJSON . unStorePath <$> addPath p
NVBuiltin _ _ -> throwError "cannot convert a built-in function to JSON"