ginger2-2.1.0.0: src/Language/Ginger/Interpret/Type.hs
{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
module Language.Ginger.Interpret.Type
where
import Language.Ginger.AST
import Language.Ginger.RuntimeError
import Language.Ginger.SourcePosition
import Language.Ginger.Value
import Control.Applicative ((<|>))
import Control.Monad (forM)
import Control.Monad.Except
( ExceptT (..)
, MonadError (..)
, runExceptT
, throwError
)
import Control.Monad.Reader
( ReaderT
, MonadReader
, runReaderT
, asks
)
import Control.Monad.State
( StateT (..)
, MonadState (..)
, MonadTrans (..)
, evalStateT
, get
, gets
, modify
)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import System.Random (SplitGen (..))
-- | The Ginger interpreter monad. Provides error reporting / handling via
-- 'MonadError', an execution context ('Context'), and an evaluation state
-- ('EvalState').
newtype GingerT m a =
GingerT { unGingerT :: ReaderT (Context m) (StateT (EvalState m) (ExceptT RuntimeError m)) a }
deriving (Functor, Applicative, Monad)
-- | Evaluation state. This keeps track of variables in scope, as well as
-- loaded templates and blocks, and the current source position.
data EvalState m =
EvalState
{ evalEnv :: !(Env m)
, evalMutables :: !(Map RefID (Value m))
, evalNextRefID :: !RefID
, evalLoadedTemplates :: !(Map Text CachedTemplate)
, evalBlocks :: !(Map Identifier LoadedBlock)
, evalSourcePosition :: !(Maybe SourcePosition)
, evalPRNG :: !SomePRNG
}
data LoadedBlock =
LoadedBlock
{ loadedBlock :: !Block
, loadedBlockParent :: !(Maybe LoadedBlock)
}
deriving (Show)
instance Semigroup (EvalState m) where
a <> b =
EvalState
{ evalEnv = evalEnv a <> evalEnv b
, evalMutables = evalMutables a <> evalMutables b
, evalNextRefID = max (evalNextRefID a) (evalNextRefID b)
, evalLoadedTemplates = evalLoadedTemplates a <> evalLoadedTemplates b
, evalBlocks = evalBlocks a <> evalBlocks b
, evalSourcePosition = evalSourcePosition a <|> evalSourcePosition b
, evalPRNG = evalPRNG a
}
data CachedTemplate
= CachedTemplate !LoadedTemplate
| MissingTemplate
data LoadedTemplate =
LoadedTemplate
{ loadedTemplateParent :: !(Maybe LoadedTemplate)
, loadedTemplateBody :: !Statement
}
runGingerT :: (Monad m, SplitGen g)
=> GingerT m a
-> Context m
-> Env m
-> g
-> m (Either RuntimeError a)
runGingerT g ctx env rng =
runExceptT
(evalStateT
(runReaderT (unGingerT g) ctx)
(EvalState
{ evalEnv = env { envRootMay = Just env }
, evalMutables = mempty
, evalNextRefID = RefID 0
, evalLoadedTemplates = mempty
, evalBlocks = mempty
, evalSourcePosition = Nothing
, evalPRNG = SomePRNG rng
}
)
)
decorateError :: Monad m
=> SourcePosition
-> RuntimeError
-> GingerT m a
decorateError pos err =
throwError (PositionedError pos err)
deriving instance Monad m => MonadState (EvalState m) (GingerT m)
deriving instance Monad m => MonadReader (Context m) (GingerT m)
deriving instance Monad m => MonadError RuntimeError (GingerT m)
instance MonadTrans GingerT where
lift = GingerT . lift . lift . lift
lookupVar :: Monad m
=> Identifier
-> GingerT m (Value m)
lookupVar name =
lookupVarMaybe name >>= maybe (throwError $ NotInScopeError (identifierName name)) pure
lookupVarMaybe :: Monad m
=> Identifier
-> GingerT m (Maybe (Value m))
lookupVarMaybe name = do
valEnv <- gets (Map.lookup name . envVars . evalEnv)
case valEnv of
Nothing ->
asks (Map.lookup name . contextVars)
Just val ->
pure $ Just val
modifyEnv :: Monad m
=> (Env m -> Env m)
-> GingerT m ()
modifyEnv f =
modify (\s -> s { evalEnv = f (evalEnv s) })
setVar :: Monad m
=> Identifier
-> Value m
-> GingerT m ()
setVar name val =
modifyEnv (\e -> e { envVars = Map.insert name val $ envVars e })
setVars :: Monad m
=> Map Identifier (Value m)
-> GingerT m ()
setVars vars = modifyEnv (\e -> e { envVars = vars <> envVars e })
setMutable :: forall m. Monad m
=> Identifier
-> Identifier
-> Value m
-> GingerT m ()
setMutable name attr val = do
varVal <- lookupVar name
refID <- case varVal of
MutableRefV i -> pure i
x -> throwError $ TagError (identifierName name) "mutable ref" (tagNameOf x)
modifyMutable refID go
where
go :: Value m -> GingerT m (Value m)
go (DictV m) = pure (DictV $ Map.insert (toScalar attr) val m)
go x = throwError $
TagError
(identifierName name)
"dict"
(tagNameOf x)
setBlock :: Monad m
=> Identifier
-> Block
-> GingerT m LoadedBlock
setBlock name block = do
mparent <- gets (Map.lookup name . evalBlocks)
let lblock = LoadedBlock block Nothing
lblock' = maybe lblock (appendLoadedBlock lblock) mparent
modify (\s -> s { evalBlocks = Map.insert name lblock' (evalBlocks s) })
pure lblock'
allocMutable :: (Monad m, MonadTrans t, MonadState (EvalState m) (t m))
=> Value m
-> t m RefID
allocMutable val = do
refID <- gets evalNextRefID
modify (\s ->
s { evalNextRefID = succ (evalNextRefID s)
, evalMutables = Map.insert refID val (evalMutables s)
})
pure refID
assignMutable :: Monad m
=> RefID
-> Value m
-> GingerT m ()
assignMutable refID val =
modify (\s -> s { evalMutables = Map.insert refID val (evalMutables s) })
modifyMutable :: Monad m
=> RefID
-> (Value m -> GingerT m (Value m))
-> GingerT m ()
modifyMutable refID f = do
mval <- derefMutable refID
mval' <- f mval
modify (\s -> s { evalMutables = Map.insert refID mval' (evalMutables s) })
derefMutableMaybe :: Monad m
=> RefID
-> GingerT m (Maybe (Value m))
derefMutableMaybe refID =
gets (Map.lookup refID . evalMutables)
derefMutable :: Monad m
=> RefID
-> GingerT m (Value m)
derefMutable refID =
derefMutableMaybe refID >>=
maybe (throwError $ NotInScopeError ("ref#" <> Text.show refID)) pure
setSourcePosition :: Monad m
=> SourcePosition
-> GingerT m ()
setSourcePosition pos = do
modify (\s -> s { evalSourcePosition = Just pos })
clearSourcePosition :: Monad m
=> GingerT m ()
clearSourcePosition =
modify (\s -> s { evalSourcePosition = Nothing })
appendLoadedBlock :: LoadedBlock -> LoadedBlock -> LoadedBlock
appendLoadedBlock t h =
case loadedBlockParent h of
Nothing -> h { loadedBlockParent = Just t }
Just p -> h { loadedBlockParent = Just (appendLoadedBlock t p) }
getBlock :: Monad m
=> Identifier
-> GingerT m (Maybe LoadedBlock)
getBlock name = gets (Map.lookup name . evalBlocks)
scoped :: Monad m
=> GingerT m a
-> GingerT m a
scoped action = do
s <- get
retval <- action
modifyEnv $ const (evalEnv s)
return retval
-- | Run a 'GingerT' action in a fresh environment; however, any globals set
-- by the invoked action will be ported back to the calling environment.
withoutContext :: Monad m
=> GingerT m a
-> GingerT m a
withoutContext action = do
e <- gets evalEnv
modifyEnv envRoot
retval <- action
e' <- gets evalEnv
modifyEnv $ const (e' <> e)
return retval
withEnv :: Monad m
=> Env m
-> GingerT m a
-> GingerT m a
withEnv env action = do
s <- get
modifyEnv (const env)
retval <- action
modifyEnv (const $ evalEnv s)
return retval
bind :: Monad m => (Env m -> a) -> GingerT m a
bind f = f <$> gets evalEnv
-- | Lift a dictionary value into the current scope, such that dictionary keys
-- become variables bound to the respective values in the dictionary.
scopify :: forall m. Monad m
=> Value m
-> GingerT m ()
scopify = \case
DictV items -> do
items' <- forM (Map.toList items) $ \(k, v) -> do
k' <- scalarToIdentifier k
pure (k', v)
setVars $ Map.fromList items'
x -> throwError $ TagError "liftScope" "dict" (tagNameOf x)
where
scalarToIdentifier :: Scalar -> GingerT m Identifier
scalarToIdentifier (StringScalar txt) = pure $ Identifier txt
scalarToIdentifier x = throwError $ TagError "liftScope" "string" (tagNameOf $ ScalarV x)
withJinjaFilters :: (Monad m)
=> GingerT m a
-> GingerT m a
withJinjaFilters = withJinjaKey "filters"
withJinjaTests :: (Monad m)
=> GingerT m a
-> GingerT m a
withJinjaTests = withJinjaKey "tests"
withJinjaKey :: (Monad m)
=> Identifier
-> GingerT m a
-> GingerT m a
withJinjaKey key inner =
scoped $ do
jinjaFilters <-
fmap (fromMaybe NoneV . Map.lookup (toScalar key)) $
lookupVar "__jinja__" >>= eitherExceptM . asDictVal
scopify jinjaFilters
inner