hnix/Nix/Eval.hs
2018-03-27 21:59:27 -07:00

334 lines
12 KiB
Haskell

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeApplications #-}
module Nix.Eval where
import Control.Applicative
import Control.Arrow
import Control.Monad
import Data.Align.Key
import Data.Fix
import Data.Foldable (foldl')
import Data.Functor.Identity
import Data.List (intercalate)
import qualified Data.Map.Lazy as Map
import Data.Maybe (fromMaybe)
import Data.Monoid (appEndo, Endo)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.These
import Data.Typeable (Typeable)
import GHC.Generics
import Nix.Atoms
import Nix.Expr
import Nix.StringOperations (runAntiquoted)
type DList a = Endo [a]
-- | An 'NValue' is the most reduced form of an 'NExpr' after evaluation
-- is completed.
data NValueF r
= NVConstant NAtom
-- | A string has a value and a context, which can be used to record what a
-- string has been build from
| NVStr Text (DList Text)
| NVList [r]
| NVSet (Map.Map Text r)
| NVFunction (Params (ValueSet -> r)) (ValueSet -> r)
-- ^ A function is a closed set of terms representing the "call
-- signature", used at application time to check the type of arguments
-- passed to the function. Since it supports default values which may
-- depend on other values within the final argument set, this
-- dependency is represented as a set of pending evaluations. The
-- arguments are finally normalized into a set which is passed to the
-- function.
| NVLiteralPath FilePath
| NVEnvPath FilePath
| NVBuiltin String (NValue -> r)
deriving (Generic, Typeable, Functor)
instance Show f => Show (NValueF f) where
showsPrec = flip go where
go (NVConstant atom) = showsCon1 "NVConstant" atom
go (NVStr text context) = showsCon2 "NVStr" text (appEndo context [])
go (NVList list) = showsCon1 "NVList" list
go (NVSet attrs) = showsCon1 "NVSet" attrs
go (NVFunction r _) = showsCon1 "NVFunction" (() <$ r)
go (NVLiteralPath p) = showsCon1 "NVLiteralPath" p
go (NVEnvPath p) = showsCon1 "NVEnvPath" p
go (NVBuiltin name _) = showsCon1 "NVBuiltin" name
showsCon1 :: Show a => String -> a -> Int -> String -> String
showsCon1 con a d = showParen (d > 10) $ showString (con ++ " ") . showsPrec 11 a
showsCon2 :: (Show a, Show b) => String -> a -> b -> Int -> String -> String
showsCon2 con a b d = showParen (d > 10) $ showString (con ++ " ") . showsPrec 11 a . showString " " . showsPrec 11 b
type NValue = Fix NValueF
type ValueSet = Map.Map Text NValue
-- | A pending evaluation awaits an attribute environment, and a monadic
-- context, in order to finally evaluate to the resulting value.
type PendingEval = ValueSet -> NValue
builtin :: String -> (NValue -> NValue) -> NValue
builtin name f = Fix (NVBuiltin name f)
builtin2 :: String -> (NValue -> NValue -> NValue) -> NValue
builtin2 name f = builtin name (builtin name . f)
valueText :: NValue -> (Text, DList Text)
valueText = cata phi where
phi (NVConstant a) = (atomText a, mempty)
phi (NVStr t c) = (t, c)
phi (NVList _) = error "Cannot coerce a list to a string"
phi (NVSet set)
| Just asString <- Map.lookup "__asString" set = asString
| otherwise = error "Cannot coerce a set to a string"
phi (NVFunction _ _) = error "Cannot coerce a function to a string"
phi (NVLiteralPath p) = (Text.pack p, mempty)
phi (NVEnvPath p) = (Text.pack p, mempty)
phi (NVBuiltin _ _) = error "Cannot coerce a function to a string"
valueTextNoContext :: NValue -> Text
valueTextNoContext = fst . valueText
-- | Translate an atom into its nix representation.
atomText :: NAtom -> Text
atomText (NInt i) = Text.pack (show i)
atomText (NBool b) = if b then "true" else "false"
atomText NNull = "null"
atomText (NUri uri) = uri
loeb :: Functor f => f (f a -> a) -> f a
loeb x = go where go = fmap ($ go) x
buildArgument :: Params PendingEval -> NValue -> ValueSet
buildArgument params arg = case params of
Param name -> Map.singleton name arg
ParamSet (FixedParamSet s) m -> go s m
ParamSet (VariadicParamSet s) m -> go s m
where
go :: Map.Map Text (Maybe PendingEval) -> Maybe Text -> ValueSet
go s m = case arg of
Fix (NVSet args) ->
let res = loeb (alignWithKey assemble args s) in
maybe res (\n -> Map.insert n arg res) m
_ -> error $ "Expected set in function call, received: " ++ show arg
assemble k = \case
That Nothing ->
error $ "Missing value for parameter: " ++ Text.unpack k
That (Just f) -> f
This x -> const x
These x _ -> const x
(&) :: a -> (a -> c) -> c
(&) = flip ($)
-- | adi is Abstracting Definitional Interpreters:
--
-- https://arxiv.org/abs/1707.04755
--
-- Essentially, it does for evaluation what recursion schemes do for
-- representation: allows threading layers through existing structure, only
-- in this case through behavior.
adi :: (Monoid b, Applicative s, Traversable t)
=> (t a -> a)
-> ((Fix t -> (b, s a)) -> Fix t -> (b, s a))
-> Fix t -> (b, s a)
adi f g = g (go . traverse (adi f g) . unFix)
where
go = fmap (fmap f . sequenceA)
adiM :: (Monoid b, Applicative s, Traversable s, Traversable t, Monad m)
=> (t a -> m a)
-> ((Fix t -> m (b, s a)) -> Fix t -> m (b, s a))
-> Fix t -> m (b, s a)
adiM f g = g ((go <=< traverse (adiM f g)) . unFix)
where
go = traverse (traverse f . sequenceA) . sequenceA
-- | Evaluate an nix expression, with a given ValueSet as environment
evalExpr :: NExpr -> PendingEval
evalExpr = cata phi
phi :: NExprF PendingEval -> PendingEval
phi (NSym var) = fromMaybe err . Map.lookup var
where err = error ("Undefined variable: " ++ show var)
phi (NConstant x) = const $ Fix $ NVConstant x
phi (NStr str) = evalString str
phi (NLiteralPath p) = const $ Fix $ NVLiteralPath p
phi (NEnvPath p) = const $ Fix $ NVEnvPath p
phi (NUnary op arg) = \env -> arg env & \case
Fix (NVConstant c) -> Fix $ NVConstant $ case (op, c) of
(NNeg, NInt i) -> NInt (-i)
(NNot, NBool b) -> NBool (not b)
_ -> error $ "unsupported argument type for unary operator " ++ show op
_ -> error "argument to unary operator must evaluate to an atomic type"
phi (NBinary op larg rarg) = \env ->
let Fix lval = larg env
Fix rval = rarg env
unsupportedTypes =
"unsupported argument types for binary operator "
++ show (lval, op, rval)
in case (lval, rval) of
(NVConstant lc, NVConstant rc) -> Fix $ NVConstant $ case (op, lc, rc) of
(NEq, l, r) -> NBool $ l == r
(NNEq, l, r) -> NBool $ l /= r
(NLt, l, r) -> NBool $ l < r
(NLte, l, r) -> NBool $ l <= r
(NGt, l, r) -> NBool $ l > r
(NGte, l, r) -> NBool $ l >= r
(NAnd, NBool l, NBool r) -> NBool $ l && r
(NOr, NBool l, NBool r) -> NBool $ l || r
(NImpl, NBool l, NBool r) -> NBool $ not l || r
(NPlus, NInt l, NInt r) -> NInt $ l + r
(NMinus, NInt l, NInt r) -> NInt $ l - r
(NMult, NInt l, NInt r) -> NInt $ l * r
(NDiv, NInt l, NInt r) -> NInt $ l `div` r
_ -> error unsupportedTypes
(NVStr ls lc, NVStr rs rc) -> case op of
NPlus -> Fix $ NVStr (ls `mappend` rs) (lc `mappend` rc)
_ -> error unsupportedTypes
(NVSet ls, NVSet rs) -> case op of
NUpdate -> Fix $ NVSet $ rs `Map.union` ls
_ -> error unsupportedTypes
(NVList ls, NVList rs) -> case op of
NConcat -> Fix $ NVList $ ls ++ rs
_ -> error unsupportedTypes
(NVLiteralPath ls, NVLiteralPath rs) -> case op of
NPlus -> Fix $ NVLiteralPath $ ls ++ rs -- TODO: Canonicalise path
_ -> error unsupportedTypes
(NVLiteralPath ls, NVStr rs rc) -> case op of
NPlus -> Fix $ NVStr (Text.pack ls `mappend` rs) rc -- TODO: Canonicalise path
_ -> error unsupportedTypes
_ -> error unsupportedTypes
phi (NSelect aset attr alternative) = go where
go env =
let aset' = aset env
ks = evalSelector True attr env
in case extract aset' ks of
Just v -> v
Nothing -> case alternative of
Just v -> v env
Nothing -> error $ "could not look up attribute '"
++ intercalate "." (map show ks) ++ "' in value " ++ show aset'
extract (Fix (NVSet s)) (k:ks) = case Map.lookup k s of
Just v -> extract v ks
Nothing -> Nothing
extract _ (_:_) = Nothing
extract v [] = Just v
phi (NHasAttr aset attr) = \env -> aset env & \case
Fix (NVSet s) -> evalSelector True attr env & \case
[keyName] -> Fix $ NVConstant $ NBool $ keyName `Map.member` s
_ -> error "attribute name argument to hasAttr is not a single-part name"
_ -> error "argument to hasAttr has wrong type"
phi (NList l) = \env ->
Fix . NVList $ map ($ env) l
phi (NSet binds) =
Fix . NVSet . evalBinds True binds
phi (NRecSet binds) = \env ->
let mergedEnv = evaledBinds `Map.union` env
evaledBinds = evalBinds True binds mergedEnv
in Fix . NVSet $ evaledBinds
phi (NLet binds e) = \env ->
let mergedEnv = evaledBinds `Map.union` env
evaledBinds = evalBinds True binds mergedEnv
in e mergedEnv
phi (NIf cond t f) = \env ->
let Fix cval = cond env
in case cval of
NVConstant (NBool True) -> t env
NVConstant (NBool False) -> f env
_ -> error "condition must be a boolean"
phi (NWith scope e) = \env ->
let s = scope env
in case s of
(Fix (NVSet scope')) -> e $ Map.union scope' env
_ -> error "scope must be a set in with statement"
phi (NAssert cond e) = \env ->
let Fix cond' = cond env
in case cond' of
(NVConstant (NBool True)) -> e env
(NVConstant (NBool False)) -> error "assertion failed"
_ -> error "assertion condition must be boolean"
phi (NApp fun x) = \env ->
let fun' = fun env
in case fun' of
Fix (NVFunction params f) ->
f (buildArgument params (x env))
Fix (NVBuiltin _ f) -> f (x env)
_ -> error "Attempt to call non-function"
phi (NAbs a b) = \env ->
-- It is the environment at the definition site, not the call site,
-- that needs to be used when evaluation the body and the default
-- arguments
let extend f env' = f (env' `Map.union` env)
in Fix $ NVFunction (fmap extend a) (extend b)
tracingExprEval :: NExpr -> IO PendingEval
tracingExprEval =
fmap (runIdentity . snd) . adiM @() (pure <$> phi) psi
where
psi k v@(Fix x) = do
putStrLn $ "Evaluating: " ++ show x
k v
evalString :: NString PendingEval -> PendingEval
evalString nstr env =
let fromParts parts =
let (t, c) = mconcat $ map
(runAntiquoted (, mempty) (valueText . ($ env)))
parts
in Fix (NVStr t c)
in case nstr of
Indented parts -> fromParts parts
DoubleQuoted parts -> fromParts parts
evalBinds :: Bool -> [Binding PendingEval] -> ValueSet -> ValueSet
evalBinds allowDynamic xs env = buildResult (concatMap go xs) where
buildResult :: [([Text], NValue)] -> Map.Map Text NValue
buildResult = foldl' insert Map.empty . map (first reverse) where
insert _ ([], _) = error "invalid selector with no components"
insert m (p:ps, v) = modifyPath ps (insertIfNotMember p v) where
alreadyDefinedErr = error $ "attribute " ++ attr ++ " already defined"
attr = show $ Text.intercalate "." $ reverse (p:ps)
modifyPath [] f = f m
modifyPath (x:parts) f = modifyPath parts $ \m' -> case Map.lookup x m' of
Nothing -> Map.singleton x $ g Map.empty
Just (Fix (NVSet m'')) -> Map.insert x (g m'') m'
Just _ -> alreadyDefinedErr
where g = Fix . NVSet . f
insertIfNotMember k x m'
| Map.notMember k m' = Map.insert k x m'
| otherwise = alreadyDefinedErr
-- TODO: Inherit
go (NamedVar x y) = [(evalSelector allowDynamic x env, y env)]
go _ = [] -- HACK! But who cares right now
evalSelector :: Bool -> NAttrPath PendingEval -> ValueSet -> [Text]
evalSelector dyn x env = map evalKeyName x where
evalKeyName (StaticKey k) = k
evalKeyName (DynamicKey k)
| dyn = valueTextNoContext . runAntiquoted (`evalString` env) ($ env) $ k
| otherwise = error "dynamic attribute not allowed in this context"