packages feed

ginger2-2.2.0.0: src/Language/Ginger/Interpret/Eval.hs

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE LambdaCase #-}

module Language.Ginger.Interpret.Eval
( Eval (..)
, EvalState (..)
, evalE
, evalS
, evalSs
, evalT
, stringify
, valuesEqual
, asBool
, asTruth
, getAttr
, getAttrRaw
, getItem
, getItemRaw
, loadTemplate
, splitRNG
)
where

import Language.Ginger.AST
import Language.Ginger.Interpret.Builtins
import Language.Ginger.Interpret.Type
import Language.Ginger.Parse (parseGinger)
import qualified Language.Ginger.Parse as Parse
import Language.Ginger.RuntimeError
import Language.Ginger.SourcePosition
import Language.Ginger.StringFormatting
import Language.Ginger.Value

import Control.Monad (foldM, forM, void)
import Control.Monad.Except
  ( MonadError (..)
  , throwError
  )
import Control.Monad.Reader (ask , asks, local, MonadReader (..))
import Control.Monad.State (gets, modify)
import Control.Monad.Trans (lift, MonadTrans (..))
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Lazy as LBS
import Data.Digest.Pure.SHA (sha256, showDigest)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes, fromMaybe)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Encoding (encodeUtf8)
import Data.Vector (Vector)
import qualified Data.Vector as V
import qualified System.Random as R

hashShow :: Show a => a -> Text
hashShow = Text.pack . showDigest . sha256 . LBS.fromStrict . encodeUtf8 . Text.show

loadTemplate :: Monad m => Text -> GingerT m LoadedTemplate
loadTemplate name = do
  sMay <- loadTemplateMaybe name
  case sMay of
    Nothing -> throwError $ TemplateFileNotFoundError name
    Just s -> pure s

loadTemplateMaybe :: Monad m => Text -> GingerT m (Maybe LoadedTemplate)
loadTemplateMaybe name = do
  loader <- asks contextLoadTemplateFile
  srcMay <- lift (loader name)
  case srcMay of
    Nothing -> pure Nothing
    Just src -> do
      let result = parseGinger Parse.template (Text.unpack name) src
      case result of
        Left err ->
          throwError $ TemplateParseError name (Text.pack err)
        Right t -> do
          parent <- forM (templateParent t) loadTemplate
          let body = templateBody t
          pure . Just $ LoadedTemplate parent body

mapArgs :: forall m. Monad m
        => Text
        -> [(Identifier, Maybe (Value m))]
        -> [(Maybe Identifier, Value m)]
        -> GingerT m (Map Identifier (Value m))
mapArgs context spec args =
  go spec posArgs kwArgs
  where
    posArgs = [ v | (Nothing, v) <- args ]
    kwArgs = Map.fromList [ (k, v) | (Just k, v) <- args ]

    go :: [(Identifier, Maybe (Value m))]
       -> [Value m]
       -> Map Identifier (Value m)
       -> GingerT m (Map Identifier (Value m))
    go ((name, defEMay):specs) ps kw = do
      case Map.lookup name kw of
        Just val -> do
          -- Found keyword argument
          let cur = Map.singleton name val
          rest <- go specs ps (Map.delete name kw)
          pure $ cur <> rest
        Nothing ->
          -- No keyword argument found, look for positional argument
          case ps of
            (val:ps') -> do
              let cur = Map.singleton name val
              rest <- go specs ps' kw
              pure $ cur <> rest
            [] -> do
              -- No positional argument found, see if we have a default
              case defEMay of
                Just defE -> do
                  let cur = Map.singleton name defE
                  rest <- go specs ps kw
                  pure $ cur <> rest
                Nothing ->
                  throwError $ ArgumentError context (identifierName name) "argument" "end of arguments"
    go [] _ _ =
      pure mempty
  
evalCallArgs :: Monad m => [Expr] -> [(Identifier, Expr)] -> GingerT m [(Maybe Identifier, Value m)]
evalCallArgs posArgsExpr namedArgsExpr = do
  posArgs <- mapM evalE posArgsExpr
  namedArgs <- mapM evalNamedArg namedArgsExpr
  pure $ zip (repeat Nothing) posArgs ++ namedArgs

splitRNG :: Monad m => GingerT m SomePRNG
splitRNG = do
  rng <- gets evalPRNG
  let (rngL, rngR) = R.splitGen rng
  modify (\e -> e { evalPRNG = rngL })
  pure rngR

callTest :: Monad m => Value m -> Expr -> [Expr] -> [(Identifier, Expr)] -> GingerT m (Value m)
callTest testV scrutinee posArgsExpr namedArgsExpr = do
  case testV of
    TestV t -> do
      args <- evalCallArgs posArgsExpr namedArgsExpr
      ctx <- ask
      env <- gets evalEnv
      rng <- splitRNG
      BoolV <$> native (runTest t scrutinee args ctx env rng)

    ScalarV {} -> do
      BoolV <$> (valuesEqual testV =<< evalE scrutinee)
      `catchError` \err -> case err of
        NotInScopeError {} -> pure FalseV
        _ -> throwError err
      
    x -> do
      call Nothing x (scrutinee : posArgsExpr) namedArgsExpr

callFilter :: Monad m => Value m -> Expr -> [Expr] -> [(Identifier, Expr)] -> GingerT m (Value m)
callFilter filterV scrutinee posArgsExpr namedArgsExpr = do
  case filterV of
    FilterV f -> do
      args <- evalCallArgs posArgsExpr namedArgsExpr
      ctx <- ask
      env <- gets evalEnv
      rng <- splitRNG
      native (runFilter f scrutinee args ctx env rng)

    ScalarV {} -> do
      BoolV <$> (valuesEqual filterV =<< evalE scrutinee)
      `catchError` \err -> case err of
        NotInScopeError {} -> pure FalseV
        _ -> throwError err
      
    x -> do
      call Nothing x (scrutinee : posArgsExpr) namedArgsExpr


call :: Monad m => Maybe (Value m) -> Value m -> [Expr] -> [(Identifier, Expr)] -> GingerT m (Value m)
call callerMay callable posArgsExpr namedArgsExpr = do
  args <- evalCallArgs posArgsExpr namedArgsExpr
  case callable of
    ProcedureV (NativeProcedure _ _ f) ->
      withEnv mempty $ do
        ctx <- ask
        rng <- splitRNG
        native $ f args ctx rng
    ProcedureV (GingerProcedure env argsSig f) -> do
      withEnv env $ do
        maybe (pure ()) (setVar "caller") callerMay
        argDict <- mapArgs "macro" argsSig args
        scoped $ do
          setVars argDict
          evalE f
    ProcedureV NamespaceProcedure -> do
      refID <- allocMutable (DictV mempty)
      pure $ MutableRefV refID
    DictV m -> do
      let callable' = Map.lookup "__call__" m
      case callable' of
        Nothing -> throwError $ NonCallableObjectError "dict"
        Just c -> call callerMay c posArgsExpr namedArgsExpr
    NativeV obj -> do
      case nativeObjectCall obj of
        Just f -> native $ f obj args
        Nothing -> throwError $ NonCallableObjectError "native object"
    x ->
      throwError $ NonCallableObjectError (tagNameOf x)

-- | 'Eval' represents types that can be evaluated in some 'GingerT m' monadic
-- context.
class Eval m a where
  eval :: a -> GingerT m (Value m)

instance Monad m => Eval m Expr where
  eval = evalE

instance Monad m => Eval m Statement where
  eval = evalS

instance Monad m => Eval m Template where
  eval = evalT

-- | Evaluate an expression, dereferencing mutable refs.
evalE :: Monad m => Expr -> GingerT m (Value m)
evalE expr =
  evalE' expr >>= \case
    MutableRefV refID -> derefMutable refID
    v -> pure v

-- | Evaluate an expression without dereferencing mutable refs.
evalE' :: Monad m => Expr -> GingerT m (Value m)
evalE' (PositionedE pos e) = do
  evalE e `catchError` decorateError pos
evalE' NoneE = pure NoneV
evalE' (BoolE b) = pure (BoolV b)
evalE' (StringLitE s) = pure (StringV s)
evalE' (IntLitE i) = pure (IntV i)
evalE' (FloatLitE d) = pure (FloatV d)
evalE' (ListE xs) = ListV <$> V.mapM evalE xs
evalE' (DictE xs) =
  DictV . Map.fromList <$> mapM evalKV xs
evalE' (UnaryE op expr) = do
  v <- evalE expr
  evalUnary op v
evalE' (BinaryE op aExpr bExpr) = do
  a <- evalE aExpr
  b <- evalE bExpr
  evalBinary op a b
evalE' (DotE aExpr b) = do
  a <- evalE aExpr
  attrMay <- getAttr a b
  case attrMay of
    Just attr -> pure attr
    Nothing -> do
      itemMay <- getItem a (StringV . identifierName $ b)
      case itemMay of
        Just item -> pure item
        Nothing -> throwError $ NotInScopeError (Text.show a <> "." <> Text.show b)
evalE' (SliceE sliceeE beginEMay endEMay) = do
  slicee <- evalE sliceeE
  beginMay <- mapM evalE beginEMay
  endMay <- mapM evalE endEMay
  sliceValue slicee beginMay endMay
evalE' (CallE callableExpr posArgsExpr namedArgsExpr) = do
  callable <- evalE callableExpr
  call Nothing callable posArgsExpr namedArgsExpr
evalE' (FilterE scrutinee filterE args kwargs) = do
  f <- withJinjaFilters (eval filterE)
  callFilter f scrutinee args kwargs
evalE' (TernaryE condExpr yesExpr noExpr) = do
  cond <- evalE condExpr >>= asTruth "condition"
  evalE (if cond then yesExpr else noExpr)
evalE' (VarE name) =
  lookupVar name
evalE' (StatementE statement) = do
  evalS statement
evalE' (IsE scrutinee testE args kwargs) = do
  t <- withJinjaTests (evalE testE)
  callTest t scrutinee args kwargs

evalKV :: Monad m => (Expr, Expr) -> GingerT m (Scalar, Value m)
evalKV (kExpr, vExpr) = do
  kVal <- evalE kExpr
  kScalar <- case kVal of
    ScalarV s -> pure s
    x -> throwError $ TagError "dict key" "scalar" (tagNameOf x)
  vVal <- evalE vExpr
  return (kScalar, vVal)

evalNamedArg :: Monad m => (Identifier, Expr) -> GingerT m (Maybe Identifier, Value m)
evalNamedArg (kIdent, vExpr) = do
  vVal <- evalE vExpr
  return (Just kIdent, vVal)

sliceVector :: Vector a -> Maybe Int -> Maybe Int -> Vector a
sliceVector xs startMay endMay =
  let start = case startMay of
                Nothing -> 0
                Just n | n < 0 -> V.length xs + n
                Just n -> n
      end = case endMay of
                Nothing -> V.length xs - start
                Just n | n < 0 -> V.length xs - start + n
                Just n -> n
  in V.take end . V.drop start $ xs

sliceText :: Text -> Maybe Int -> Maybe Int -> Text
sliceText xs startMay endMay =
  let start = case startMay of
                Nothing -> 0
                Just n | n < 0 -> Text.length xs + n
                Just n -> n
      end = case endMay of
                Nothing -> Text.length xs - start
                Just n | n < 0 -> Text.length xs - start + n
                Just n -> n
  in Text.take end . Text.drop start $ xs

sliceByteString :: ByteString -> Maybe Int -> Maybe Int -> ByteString
sliceByteString xs startMay endMay =
  let start = case startMay of
                Nothing -> 0
                Just n | n < 0 -> ByteString.length xs + n
                Just n -> n
      end = case endMay of
                Nothing -> ByteString.length xs - start
                Just n | n < 0 -> ByteString.length xs - start + n
                Just n -> n
  in ByteString.take end . ByteString.drop start $ xs

sliceValue :: Monad m
           => Value m
           -> Maybe (Value m)
           -> Maybe (Value m)
           -> GingerT m (Value m)
sliceValue (ListV xs) startValMay endValMay = do
  startMay <- mapM (native . pure . asIntVal "slice start") startValMay
  endMay <- mapM (native . pure . asIntVal "slice end") endValMay
  pure . ListV $ sliceVector xs (fromIntegral <$> startMay) (fromIntegral <$> endMay)
sliceValue (StringV xs) startValMay endValMay = do
  startMay <- mapM (native . pure . asIntVal "slice start") startValMay
  endMay <- mapM (native . pure . asIntVal "slice end") endValMay
  pure . StringV $ sliceText xs (fromIntegral <$> startMay) (fromIntegral <$> endMay)
sliceValue (BytesV xs) startValMay endValMay = do
  startMay <- mapM (native . pure . asIntVal "slice start") startValMay
  endMay <- mapM (native . pure . asIntVal "slice end") endValMay
  pure . BytesV $ sliceByteString xs (fromIntegral <$> startMay) (fromIntegral <$> endMay)
sliceValue (EncodedV (Encoded xs)) startValMay endValMay = do
  startMay <- mapM (native . pure . asIntVal "slice start") startValMay
  endMay <- mapM (native . pure . asIntVal "slice end") endValMay
  pure . EncodedV . Encoded $ sliceText xs (fromIntegral <$> startMay) (fromIntegral <$> endMay)
sliceValue x _ _ =
  throwError $
    TagError "slicee" "list or string" (tagNameOf x)

numericBinop :: Monad m
             => (Integer -> Integer -> Integer)
             -> (Double -> Double -> Double)
             -> Value m
             -> Value m
             -> GingerT m (Value m)
numericBinop f g a b = native . pure $ numericFunc2 f g a b

numericBinopCatch :: Monad m
                  => (Integer -> Integer -> Either RuntimeError Integer)
                  -> (Double -> Double -> Either RuntimeError Double)
                  -> Value m
                  -> Value m
                  -> GingerT m (Value m)
numericBinopCatch f g a b = native . pure $ numericFunc2Catch f g a b

intBinop :: Monad m
         => (Integer -> Integer -> Either RuntimeError Integer)
         -> Value m
         -> Value m
         -> GingerT m (Value m)
intBinop f a b = native . pure $ intFunc2 f a b

floatBinop :: Monad m
         => (Double -> Double -> Either RuntimeError Double)
         -> Value m
         -> Value m
         -> GingerT m (Value m)
floatBinop f a b = native . pure $ floatFunc2 f a b

boolBinop :: Monad m
         => (Bool -> Bool -> Bool)
         -> Value m
         -> Value m
         -> GingerT m (Value m)
boolBinop f a b = native . pure $ boolFunc2 f a b

valuesEqual :: Monad m
            => Value m
            -> Value m
            -> GingerT m Bool
valuesEqual NoneV NoneV = pure True
valuesEqual (IntV a) (IntV b) = pure (a == b)
valuesEqual (FloatV a) (FloatV b) = pure (a == b)
valuesEqual (StringV a) (StringV b) = pure (a == b)
valuesEqual (BoolV a) (BoolV b) = pure (a == b)
valuesEqual (BytesV a) (BytesV b) = pure (a == b)
valuesEqual (EncodedV a) (EncodedV b) = pure (a == b)
valuesEqual (ListV a) (ListV b)
  | V.length a /= V.length b
  = pure False
  | otherwise
  = V.and <$> V.zipWithM valuesEqual a b
valuesEqual (DictV a) (DictV b) = dictsEqual a b
valuesEqual (NativeV a) (NativeV b) =
  native $ a --> nativeObjectEq b
valuesEqual a b = pure (a == b)

compareValues :: Monad m => Value m -> Value m -> GingerT m Ordering
compareValues NoneV NoneV = pure $ EQ
compareValues (BoolV a) (BoolV b) = pure $ compare a b
compareValues (IntV a) (IntV b) = pure $ compare a b
compareValues (FloatV a) (FloatV b) = pure $ compare a b
compareValues (IntV a) (FloatV b) = pure $ compare (fromInteger a) b
compareValues (FloatV a) (IntV b) = pure $ compare a (fromInteger b)
compareValues (StringV a) (StringV b) = pure $ compare a b
compareValues (EncodedV a) (EncodedV b) = pure $ compare a b
compareValues a b = throwError $ TagError "comparison" "comparable types" (tagNameOf a <> ", " <> tagNameOf b)

valueComparison :: Monad m => (Ordering -> Bool) -> Value m -> Value m -> GingerT m (Value m)
valueComparison f a b = do
  ordering <- compareValues a b
  pure $ BoolV (f ordering)

printfValues :: Monad m => Text -> Value m -> GingerT m (Value m)
printfValues fmtText (ListV args) = do
  pure . StringV . Text.pack $ printfList (Text.unpack fmtText) (V.toList args)
printfValues fmtText x = do
  pure . StringV . Text.pack $ printfList (Text.unpack fmtText) [x]

dictsEqual :: forall m. Monad m
           => Map Scalar (Value m)
           -> Map Scalar (Value m)
           -> GingerT m Bool
dictsEqual m1 m2 =
  and <$> mapM (\k -> (valuesEqual (toValue $ Map.lookup k m1) (toValue $ Map.lookup k m2))) keys
  where
    keys = Set.toList (Map.keysSet m1 <> Map.keysSet m2)

evalUnary :: Monad m => UnaryOperator -> Value m -> GingerT m (Value m)
evalUnary UnopNot (BoolV b) = pure (BoolV $ not b)
evalUnary UnopNot x = throwError $ TagError "not" "boolean" (tagNameOf x)
evalUnary UnopNegate (IntV x) = pure (IntV $ negate x)
evalUnary UnopNegate (FloatV x) = pure (FloatV $ negate x)
evalUnary UnopNegate x = throwError $ TagError "unary -" "number" (tagNameOf x)

evalBinary :: Monad m => BinaryOperator -> Value m -> Value m -> GingerT m (Value m)
evalBinary BinopPlus a b = numericBinop (+) (+) a b
evalBinary BinopMinus a b = numericBinop (-) (-) a b
evalBinary BinopDiv a b = floatBinop safeDiv a b
evalBinary BinopIntDiv a b = intBinop safeIntDiv a b
evalBinary BinopMod (StringV a) b = printfValues a b
evalBinary BinopMod a b = intBinop safeIntMod a b
evalBinary BinopMul a b = numericBinop (*) (*) a b
evalBinary BinopPower a b = numericBinopCatch safeIntPow (\x y -> Right (x ** y)) a b
evalBinary BinopEqual a b = BoolV <$> valuesEqual a b
evalBinary BinopNotEqual a b = BoolV . not <$> valuesEqual a b
evalBinary BinopGT a b = valueComparison (== GT) a b
evalBinary BinopGTE a b = valueComparison (/= LT) a b
evalBinary BinopLT a b = valueComparison (== LT) a b
evalBinary BinopLTE a b = valueComparison (/= GT) a b

evalBinary BinopAnd a b = boolBinop (&&) a b
evalBinary BinopOr a b = boolBinop (||) a b
evalBinary BinopIn a b = case b of
  DictV m -> case a of
    ScalarV k -> pure . BoolV $ k `Map.member` m
    x -> throwError $ TagError "in" "scalar" (tagNameOf x)
  ListV v -> case V.uncons v of
    Nothing ->
      pure FalseV
    Just (x, xs) -> do
      found <- valuesEqual a x
      if found then
        pure . BoolV $ True
      else
        evalBinary BinopIn a (ListV xs)
  x -> throwError $ TagError "in" "list or dict" (tagNameOf x)
evalBinary BinopIndex a b = do
  itemMay <- getItem a b
  case itemMay of
    Just item -> pure item
    Nothing -> do
      attrMay <- case b of
        StringV s -> getAttr a (Identifier s)
        _ -> pure Nothing
      case attrMay of
        Just attr -> pure attr
        Nothing -> pure NoneV
evalBinary BinopConcat a b = concatValues a b

getItem :: Monad m
        => Value m
        -> Value m
        -> GingerT m (Maybe (Value m))
getItem a b = lift $ getItemRaw a b


getAttr :: Monad m
        => Value m
        -> Identifier
        -> GingerT m (Maybe (Value m))
getAttr a b = native $ getAttrRaw a b

safeIntPow :: Integer -> Integer -> Either RuntimeError Integer
safeIntPow _ b | b < 0 = Left (NumericError "**" "negative exponent")
safeIntPow a b = Right (a ^ b)

safeIntDiv :: Integer -> Integer -> Either RuntimeError Integer
safeIntDiv _ 0 = Left (NumericError "//" "division by zero")
safeIntDiv a b = Right (a `div` b)

safeIntMod :: Integer -> Integer -> Either RuntimeError Integer
safeIntMod _ 0 = Left (NumericError "%" "modulo by zero")
safeIntMod a b = Right (a `mod` b)

safeDiv :: Double -> Double -> Either RuntimeError Double
safeDiv a b =
  case a / b of
    c | isNaN c -> Left (NumericError "/" "not a number")
    c | isInfinite c -> Left (NumericError "/" ("division by zero"))
    c -> Right c

concatValues :: Monad m => (Value m) -> (Value m) -> GingerT m (Value m)
concatValues a b = case (a, b) of
  -- Strings, blobs and encoded values concatenate directly
  (StringV x, StringV y) -> pure $ StringV $ x <> y
  (BytesV x, BytesV y) -> pure $ BytesV $ x <> y
  (EncodedV (Encoded x), EncodedV (Encoded y)) -> pure . EncodedV . Encoded $ x <> y

  -- None is a neutral element
  (NoneV, y) -> pure $ y
  (x, NoneV) -> pure $ x

  -- Anything involving encoded values yields encoded results
  (EncodedV x, y) -> do
    yEnc <- encode y
    pure $ EncodedV (x <> yEnc)
  (x, EncodedV y) -> do
    xEnc <- encode x
    pure $ EncodedV (xEnc <> y)

  -- Anything else is cast to and concatenated as strings
  (x, y) -> do
    xStr <- stringify x
    yStr <- stringify y
    pure . StringV $ xStr <> yStr

evalT :: Monad m => Template -> GingerT m (Value m)
evalT t = do
  case templateParent t of
    Nothing ->
      evalS (templateBody t)
    Just parentName -> do
      parent <- loadTemplate parentName
      hush_ $ evalS (templateBody t)
      evalLT parent

evalLT :: Monad m => LoadedTemplate -> GingerT m (Value m)
evalLT t = do
  case loadedTemplateParent t of
    Nothing ->
      evalS (loadedTemplateBody t)
    Just parent -> do
      hush_ $ evalS (loadedTemplateBody t)
      evalLT parent

evalS :: Monad m => Statement -> GingerT m (Value m)
evalS (PositionedS pos s) = do
  evalS s `catchError` decorateError pos
evalS (ImmediateS enc) = pure (EncodedV enc)
evalS (InterpolationS expr) = whenOutputPolicy $ do
  evalE expr
evalS (CommentS _) = pure NoneV
evalS (ForS loopKeyMay loopName itereeE loopCondMay recursivity bodyS elseSMay) = do
  iteree <- evalE itereeE
  evalLoop loopKeyMay loopName iteree loopCondMay recursivity bodyS elseSMay 0
evalS (IfS condE yesS noSMay) = do
  cond <- evalE condE >>= asTruth "condition"
  if cond then evalS yesS else maybe (pure NoneV) evalS noSMay
evalS (MacroS name argsSig body) = do
  env <- gets evalEnv
  argsSig' <- mapM (\(argname, defEMay) -> do
                  defMay <- maybe (pure Nothing) (fmap Just . evalE) defEMay
                  pure (argname, defMay)
                )
                argsSig
  setVar name . ProcedureV $ GingerProcedure env argsSig' (StatementE body)
  pure NoneV

evalS (CallS name posArgsExpr namedArgsExpr bodyS) = whenOutputPolicy $ do
  callee <- lookupVar name
  callerVal <- eval bodyS
  srcPosMay <- gets evalSourcePosition
  let callerID =
        objectIDFromContext "caller" callerVal srcPosMay
  let caller =
        ProcedureV $
          NativeProcedure
            callerID
            (Just ProcedureDoc
              { procedureDocName = "caller"
              , procedureDocArgs = mempty
              , procedureDocReturnType = Just $ TypeDocSingle "markup"
              , procedureDocDescription =
                  "Runs the body of the {% call %} statement that called the " <>
                  "current macro."
              }
            )
            (const . const . const . pure . Right $ callerVal)
  call (Just caller) callee posArgsExpr namedArgsExpr
evalS (FilterS name posArgsExpr namedArgsExpr bodyS) = whenOutputPolicy $ do
  callee <- lookupVar name
  let posArgsExpr' = StatementE bodyS : posArgsExpr
  call Nothing callee posArgsExpr' namedArgsExpr

evalS (SetS target valE) = do
  val <- evalE' valE
  case target of
    SetVar name -> setVar name val
    SetMutable name attr -> setMutable name attr val
  pure NoneV
evalS (SetBlockS target bodyS filterEMaybe) = do
  body <- case filterEMaybe of
            Nothing ->
              evalS bodyS
            Just filterE -> case filterE of
              CallE callee posArgs kwArgs ->
                evalE (CallE callee (StatementE bodyS : posArgs) kwArgs)
              callee ->
                evalE (CallE callee [StatementE bodyS] mempty)
  case target of
    SetVar name -> setVar name body
    SetMutable name path -> setMutable name path body
  pure NoneV
evalS (IncludeS nameE missingPolicy contextPolicy) = do
  name <- eval nameE >>= (eitherExcept . asTextVal)
  templateMay <- case missingPolicy of
    RequireMissing -> Just <$> loadTemplate name
    IgnoreMissing -> loadTemplateMaybe name
  case templateMay of
    Nothing ->
      pure NoneV
    Just template -> do
      withScopeModifier (contextPolicy == WithContext) $ evalLT template
evalS (ImportS srcE nameMay identifiers missingPolicy contextPolicy) = hush $ do
  src <- eval srcE >>= (eitherExcept . asTextVal)
  templateMay <- case missingPolicy of
    RequireMissing -> Just <$> loadTemplate src
    IgnoreMissing -> loadTemplateMaybe src
  case templateMay of
    Nothing ->
      pure NoneV
    Just template -> do
      e' <- scoped . withScopeModifier (contextPolicy == WithContext) $ do
              void $ evalLT template
              gets evalEnv
      let vars = case identifiers of
            Nothing ->
              case nameMay of
                Nothing -> envVars e'
                Just name -> Map.singleton name (DictV . Map.mapKeys toScalar $ envVars e')
            Just importees -> Map.fromList . catMaybes $
              [ (fromMaybe varName alias,) <$> Map.lookup varName (envVars e')
              | (varName, alias) <- importees
              ]
      setVars vars
      pure NoneV
evalS (BlockS name block) =
  evalBlock name block
evalS (WithS varEs bodyS) = do
  vars <- Map.fromList <$> mapM (\(k, valE) -> (k,) <$> evalE valE) varEs
  scoped $ do
    setVars vars
    evalS bodyS
evalS (GroupS xs) = evalSs xs

objectIDFromContext :: Show a
                    => Text
                    -> a
                    -> Maybe SourcePosition
                    -> ObjectID
objectIDFromContext prefix x posMay =
  ObjectID $
    prefix <> ":" <> maybe (hashShow x) hashShow posMay

hush :: Monad m => GingerT m a -> GingerT m a
hush = local (\c -> c { contextOutput = Quiet })

hush_ :: Monad m => GingerT m a -> GingerT m ()
hush_ = void . hush

whenOutputPolicy :: Monad m => GingerT m (Value m) -> GingerT m (Value m)
whenOutputPolicy action = do
  outputPolicy <- asks contextOutput
  if outputPolicy == Output then
    action
  else
    pure NoneV

withScopeModifier :: Monad m => Bool -> GingerT m a -> GingerT m a
withScopeModifier policy inner = do
  let scopeModifier = if policy then id else withoutContext
  scopeModifier inner

evalBlock :: Monad m => Identifier -> Block -> GingerT m (Value m)
evalBlock name block = do
  lblock <- setBlock name block
  super <- makeSuper (loadedBlockParent lblock)
  whenOutputPolicy .
    withScopeModifier (is $ lblockScoped lblock) .
      scoped $ do
        setVar "super" super
        evalS (blockBody . loadedBlock $ lblock)

lblockScoped :: LoadedBlock -> Scoped
lblockScoped lb =
  case loadedBlockParent lb of
    Nothing -> blockScoped (loadedBlock lb)
    Just parent -> lblockScoped parent

makeSuper :: Monad m => Maybe LoadedBlock -> GingerT m (Value m)
makeSuper Nothing = pure NoneV
makeSuper (Just lblock) = do
  ctx <- ask
  env <- gets evalEnv
  rng <- splitRNG
  parent <- makeSuper (loadedBlockParent lblock)
  pure $ dictV
    [ "__call__" .=
        ProcedureV
          (mkFn0 "super()"
              "Evaluate the parent template"
              Nothing $
              eitherExceptM $
                runGingerT
                  (evalS . blockBody . loadedBlock $ lblock)
                  ctx
                  env
                  rng
          )
    , "super" .= parent
    ]

asBool :: Monad m => Text -> Value m -> GingerT m Bool
asBool context x = either throwError pure $ asBoolVal context x

asTruth :: Monad m => Text -> Value m -> GingerT m Bool
asTruth context x = either throwError pure $ asTruthVal context x

evalLoop :: forall m. Monad m
         => Maybe Identifier
         -> Identifier
         -> Value m
         -> Maybe Expr
         -> Recursivity
         -> Statement
         -> Maybe Statement
         -> Int
         -> GingerT m (Value m)
evalLoop loopKeyMay loopName iteree loopCondMay recursivity bodyS elseSMay recursionLevel = do
  -- First, convert the iteree into a plain list.

  itemPairs <- case iteree of
    ListV items -> pure (V.zip (fmap IntV [0..]) items)
    DictV dict -> (pure . V.fromList) [ (ScalarV k, v) | (k, v) <- Map.toList dict ]
    NoneV -> pure mempty
    x -> throwError $ TagError "iteree" "list or dict" (tagNameOf x)

  filtered <- maybe (pure itemPairs) (goFilter itemPairs) loopCondMay

  if null filtered then
    case elseSMay of
      Nothing -> pure NoneV
      Just elseS -> evalS elseS
  else
    go 0 (length filtered) Nothing filtered
  where
    goFilter :: Vector (Value m, Value m) -> Expr -> GingerT m (Vector (Value m, Value m))
    goFilter pairs condE =
      case V.uncons pairs of
        Nothing ->
          pure mempty
        Just ((k, v), xs) -> do
          keep <- scoped $ do
            -- Bind key and value
            maybe (pure ()) (\loopKey -> setVar loopKey k) loopKeyMay
            setVar loopName v
            asTruth "loop condition" =<< evalE condE
          rest <- goFilter xs condE
          if keep then
            pure $ V.cons (k, v) rest
          else
            pure rest

    go :: Int -> Int -> Maybe (Value m) -> Vector (Value m, Value m) -> GingerT m (Value m)
    go n num prevVal pairs = do
      case V.uncons pairs of
        Nothing -> pure NoneV
        Just ((k, v), xs) -> do
          (prevVal', body) <- scoped $ do
            -- Bind key and value
            maybe (pure ()) (\loopKey -> setVar loopKey k) loopKeyMay
            setVar loopName v
            env <- gets evalEnv
            srcPosMay <- gets evalSourcePosition
            let recurFuncID =
                  objectIDFromContext
                    "loop.recur" bodyS srcPosMay
            let cycleFuncID =
                  objectIDFromContext
                    "loop.cycle" bodyS srcPosMay
            setVar "loop" $
              dictV
                [ "index" .= (n + 1)
                , "index0" .= n
                , "revindex" .= (num - n)
                , "revindex0" .= (num - n - 1)
                , "first" .= (n == 0)
                , "last" .= (n == num - 1)
                , "length" .= num
                , "cycle" .= cycleFunc cycleFuncID n
                , "depth" .= (recursionLevel + 1)
                , "depth0" .= recursionLevel
                , "previtem" .= prevVal
                , "nextitem" .= (snd <$> xs V.!? 0)
                , "changed" .= changedFunc env v
                , "__call__" .= if is recursivity then Just (recurFunc recurFuncID env) else Nothing
                ]
            body <- evalS bodyS
            pure (Just v, body)

          rest <- go (succ n) num prevVal' xs
          concatValues body rest

    changedFunc :: Env m -> Value m -> Value m
    changedFunc env v = ProcedureV $ GingerProcedure env [("val", Just v)] $
      EqualE (IndexE (VarE "loop") (StringLitE "previtem")) (VarE "val")

    recurFunc :: ObjectID -> Env m -> Value m
    recurFunc oid env =
      ProcedureV .
        NativeProcedure
          oid
          (Just ProcedureDoc
            { procedureDocName = "loop.recur"
            , procedureDocArgs = mempty
            , procedureDocReturnType = Just $ TypeDocSingle "markup"
            , procedureDocDescription =
                "Recurse one level deeper into the iteree"
            }
          )
          $ \args ctx rng -> do
                case args of
                  [(_, iteree')] ->
                    runGingerT
                      (evalLoop
                        loopKeyMay
                        loopName
                        iteree'
                        loopCondMay
                        recursivity
                        bodyS
                        elseSMay
                        (succ recursionLevel))
                      ctx
                      env
                      rng
                  [] -> pure . Left $
                          ArgumentError "loop()" "1" "argument" "end of arguments"
                  _ -> pure . Left $
                          ArgumentError "loop()" "2" "end of arguments" "argument"
      

    cycleFunc :: ObjectID -> Int -> Value m
    cycleFunc oid n =
      ProcedureV .
        NativeProcedure
          oid
          (Just ProcedureDoc
            { procedureDocName = "loop.cycle"
            , procedureDocArgs =
                [ ArgumentDoc
                    "items"
                    (Just $ TypeDocSingle "list<any>")
                    Nothing
                    ""
                ]
            , procedureDocReturnType = Just TypeDocAny
            , procedureDocDescription =
                "Cycle through 'items': on the n-th iteration of the loop, " <>
                "cycle(items) will return items[n % length(items)]."
            }
          )
          $ \args _ctx _rng -> do
              case args of
                [(_, items)] ->
                  case items of
                    ListV [] ->
                      pure . Right $ NoneV
                    ListV xs -> do
                      let n' = n `mod` V.length xs
                      pure . Right . toValue $ xs V.!? n'
                    _ ->
                      pure . Right $ NoneV
                _ -> pure . Left $
                        ArgumentError "cycle()" "1" "end of arguments" "argument"


evalSs :: Monad m => [Statement] -> GingerT m (Value m)
evalSs stmts = mapM evalS stmts >>= foldM concatValues NoneV