packages feed

slab-0.0.2.0: src/Slab/Evaluate.hs

{-# LANGUAGE RecordWildCards #-}

-- |
-- Module      : Slab.Evaluate
-- Description : Evaluate an AST (to a non-reducible AST)
--
-- @Slab.Evaluate@ implements the evaluation stage of Slab, following both the
-- parsing and pre-processing stages. This is responsible of reducing for
-- instance @1 + 2@ to @3@, or transforming a loop construct to an actual list
-- of HTML blocks.
--
-- Evaluation works on an abstract syntax tree (defined in "Slab.Syntax") and
-- currently reuses the sames types for its result.
--
-- The stage following evaluation is "Slab.Execute", responsible of running
-- external commands.
module Slab.Evaluate
  ( evaluateFile
  , evaluate
  , evalExpr
  , defaultEnv
  , simplify
  ) where

import Control.Monad (forM)
import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
import Data.Aeson qualified as Aeson
import Data.Aeson.Key qualified as Aeson.Key
import Data.Aeson.KeyMap qualified as Aeson.KeyMap
import Data.Maybe (isJust)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Vector qualified as V
import Slab.Error qualified as Error
import Slab.PreProcess qualified as PreProcess
import Slab.Syntax

--------------------------------------------------------------------------------

-- | Similar to `preprocessFile` but evaluate the template.
evaluateFile :: FilePath -> IO (Either Error.Error [Block])
evaluateFile = runExceptT . evaluateFileE

evaluateFileE :: FilePath -> ExceptT Error.Error IO [Block]
evaluateFileE path =
  PreProcess.preprocessFileE path >>= evaluate defaultEnv [T.pack path]

--------------------------------------------------------------------------------
defaultEnv :: Env
defaultEnv =
  Env
    [ ("true", Bool True)
    , ("false", Bool False)
    , ("show", BuiltIn "show")
    , ("null", BuiltIn "null")
    , mkElem "div" Div
    , mkElem "html" Html
    , mkElem "body" Body
    , mkElem "span" Span
    , mkElem "h1" H1
    , mkElem "h2" H2
    , mkElem "h3" H3
    , mkElem "h4" H4
    , mkElem "h5" H5
    , mkElem "h6" H6
    , mkElem "header" Header
    , mkElem "head" Head
    , mkElem "main" Main
    , mkElem "audio" Audio
    , mkElem "a" A
    , mkElem "code" Code
    , mkElem "iframe" IFrame
    , mkElem "i" I
    , mkElem "pre" Pre
    , mkElem "p" P
    , mkElem "ul" Ul
    , mkElem "li" Li
    , mkElem "title" Title
    , mkElem "table" Table
    , mkElem "thead" Thead
    , mkElem "tbody" Tbody
    , mkElem "tr" Tr
    , mkElem "td" Td
    , mkElem "dl" Dl
    , mkElem "dt" Dt
    , mkElem "dd" Dd
    , mkElem "footer" Footer
    , mkElem "figure" Figure
    , mkElem "form" Form
    , mkElem "label" Label
    , mkElem "blockquote" Blockquote
    , mkElem "button" Button
    , mkElem "figcaption" Figcaption
    , mkElem "script" Script
    , mkElem "style" Style
    , mkElem "small" Small
    , mkElem "svg" Svg
    , mkElem "textarea" Textarea
    , mkElem "canvas" Canvas
    , -- Elements with no content.
      ("br", Block (BlockElem Br NoSym [] []))
    , ("hr", Block (BlockElem Hr NoSym [] []))
    , ("meta", Block (BlockElem Meta NoSym [] []))
    , ("link", Block (BlockElem Link NoSym [] []))
    , ("source", Block (BlockElem Source NoSym [] []))
    , ("img", Block (BlockElem Img NoSym [] []))
    , ("input", Block (BlockElem Input NoSym [] []))
    ]
 where
  mkElem name el =
    (name, Frag ["content"] emptyEnv [BlockElem el NoSym [] [BlockDefault "content" []]])

--------------------------------------------------------------------------------

-- Process mixin calls. This should be done after processing the include statement
-- since mixins may be defined in included files.
evaluate :: Monad m => Env -> [Text] -> [Block] -> ExceptT Error.Error m [Block]
evaluate env stack nodes = do
  -- Note that we pass the environment that we are constructing, so that each
  -- definition sees all definitions (including later ones and itself).
  let vars = extractVariables env' nodes
      env' = augmentVariables env vars
  mapM (eval env' stack) nodes

eval :: Monad m => Env -> [Text] -> Block -> ExceptT Error.Error m Block
eval env stack b
  | length stack > 100 =
      throwE $
        Error.EvaluateError $
          "Stack overflow. Is there an infinite loop?"
            <> " "
            <> T.pack (show $ reverse stack)
            <> " "
            <> displayEnv env
eval env stack bl = case bl of
  node@BlockDoctype -> pure node
  BlockElem name mdot attrs nodes -> do
    nodes' <- evaluate env stack nodes
    pure $ BlockElem name mdot attrs nodes'
  BlockText syn template -> do
    template' <- evalTemplate env template
    pure $ BlockText syn template'
  BlockInclude mname path mnodes -> do
    case mnodes of
      Just nodes -> do
        nodes' <- evaluate env ("include" : stack) nodes
        pure $ BlockInclude mname path (Just nodes')
      Nothing ->
        pure $ BlockInclude mname path Nothing
  node@(BlockFragmentDef _ _ _) -> pure node
  BlockFragmentCall name mdot attrs values args -> do
    body <- call env stack name values args
    let body' = setAttrs attrs body
    pure $ BlockFragmentCall name mdot attrs values body'
  BlockFor name mindex values nodes -> do
    -- Re-use BlockFor to construct a single node to return.
    let zero :: Int
        zero = 0
    values' <- evalExpr env values
    collection <- case values' of
      List xs -> pure $ zip xs $ map Int [zero ..]
      Object xs -> pure $ map (\(k, v) -> (v, k)) xs
      _ -> throwE $ Error.EvaluateError $ "Iterating on something that is not a collection"
    nodes' <- forM collection $ \(value, index) -> do
      let env' = case mindex of
            Just idxname -> augmentVariables env [(name, value), (idxname, index)]
            Nothing -> augmentVariables env [(name, value)]
      evaluate env' ("each" : stack) nodes
    pure $ BlockFor name mindex values $ concat nodes'
  node@(BlockComment _ _) -> pure node
  node@(BlockFilter _ _) -> pure node
  node@(BlockRawElem _ _) -> pure node
  BlockDefault name nodes -> do
    -- If the fragment is not given as an argument, we return the default block,
    -- but recursively trying to replace the blocks found within its own body.
    case lookupVariable name env of
      Nothing -> do
        nodes' <- evaluate env ("?block" : stack) nodes
        pure $ BlockDefault name nodes'
      Just (Frag _ capturedEnv nodes') -> do
        nodes'' <- evaluate capturedEnv ("default block " <> name : stack) nodes'
        pure $ BlockDefault name nodes''
      Just _ -> throwE $ Error.EvaluateError $ "Calling something that is not a fragment \"" <> name <> "\" in " <> T.pack (show stack)
  BlockImport path _ args -> do
    body <- call env stack (T.pack path) [] args
    pure $ BlockImport path (Just body) args
  node@(BlockRun _ _) -> pure node
  node@(BlockReadJson _ _ _) -> pure node
  node@(BlockAssignVar _ _) -> pure node
  BlockIf cond as bs -> do
    cond' <- evalExpr env cond
    case cond' of
      Bool True -> do
        as' <- evaluate env ("then" : stack) as
        pure $ BlockIf cond as' []
      Bool False -> do
        bs' <- evaluate env ("else" : stack) bs
        pure $ BlockIf cond [] bs'
      _ ->
        throwE . Error.EvaluateError $
          "Conditional is not a boolean: " <> T.pack (show cond')
  BlockList nodes -> do
    nodes' <- evaluate env stack nodes
    pure $ BlockList nodes'
  BlockCode code -> do
    code' <- evalExpr env code
    pure $ BlockCode code'

call :: Monad m => Env -> [Text] -> Text -> [Expr] -> [Block] -> ExceptT Error.Error m [Block]
call env stack name values args =
  case lookupVariable name env of
    Just frag@(Frag _ _ _) -> evalFrag env stack name values args frag
    Just (Block x) -> pure [x]
    Just _ -> throwE $ Error.EvaluateError $ "Calling something that is not a fragment \"" <> name <> "\" in " <> T.pack (show stack)
    Nothing -> throwE $ Error.EvaluateError $ "Can't find fragment \"" <> name <> "\" while evaluating " <> T.pack (show $ reverse stack) <> " with environment " <> displayEnv env

lookupVariable :: Text -> Env -> Maybe Expr
lookupVariable name Env {..} = lookup name envVariables

augmentVariables :: Env -> [(Text, Expr)] -> Env
augmentVariables Env {..} xs = Env {envVariables = xs <> envVariables}

evalFrag :: Monad m => Env -> [Text] -> Text -> [Expr] -> [Block] -> Expr -> ExceptT Error.Error m [Block]
evalFrag env stack name values args (Frag names capturedEnv body) = do
  env' <- extractVariables' env args
  let env'' = augmentVariables capturedEnv env'
      arguments = zip names (map (thunk env) values)
      env''' = augmentVariables env'' arguments
  body' <- evaluate env''' ("frag " <> name : stack) body
  pure body'

evalExpr :: Monad m => Env -> Expr -> ExceptT Error.Error m Expr
evalExpr env = \case
  Variable name ->
    case lookupVariable name env of
      Just val -> evalExpr env val
      Nothing -> throwE $ Error.EvaluateError $ "Can't find variable \"" <> name <> "\""
  Lookup name key ->
    case lookupVariable name env of
      Just (Object obj) -> do
        -- key' <- evalExpr env key
        case lookup key obj of
          Just val -> evalExpr env val
          Nothing -> pure $ Bool False -- TODO Either crash, or we have to implement on option type.
      Just _ -> throwE $ Error.EvaluateError $ "Variable \"" <> name <> "\" is not an object"
      Nothing -> throwE $ Error.EvaluateError $ "Can't find variable \"" <> name <> "\""
  Add a b -> do
    a' <- evalExpr env a
    b' <- evalExpr env b
    case (a', b') of
      (Int i, Int j) -> pure . Int $ i + j
      (SingleQuoteString s, SingleQuoteString t) ->
        pure . SingleQuoteString $ s <> t
      (Block a, Block b) ->
        pure . Block $ pasteBlocks a b
      _ -> throwE $ Error.EvaluateError $ "Unimplemented (add): " <> T.pack (show (Add a' b'))
  Sub a b -> do
    a' <- evalExpr env a
    b' <- evalExpr env b
    case (a', b') of
      (Int i, Int j) -> pure . Int $ i - j
      _ -> throwE $ Error.EvaluateError $ "Unimplemented (sub): " <> T.pack (show (Sub a' b'))
  Times a b -> do
    a' <- evalExpr env a
    b' <- evalExpr env b
    case (a', b') of
      (Int i, Int j) -> pure . Int $ i * j
      _ -> throwE $ Error.EvaluateError $ "Unimplemented (times): " <> T.pack (show (Times a' b'))
  Divide a b -> do
    a' <- evalExpr env a
    b' <- evalExpr env b
    case (a', b') of
      (Int i, Int j) -> pure . Int $ i `div` j
      _ -> throwE $ Error.EvaluateError $ "Unimplemented (divide): " <> T.pack (show (Divide a' b'))
  GreaterThan a b -> do
    a' <- evalExpr env a
    b' <- evalExpr env b
    case (a', b') of
      (Int i, Int j) -> pure . Bool $ i > j
      _ -> throwE $ Error.EvaluateError $ "Unimplemented (greater-than): " <> T.pack (show (GreaterThan a' b'))
  LesserThan a b -> do
    a' <- evalExpr env a
    b' <- evalExpr env b
    case (a', b') of
      (Int i, Int j) -> pure . Bool $ i < j
      _ -> throwE $ Error.EvaluateError $ "Unimplemented (lesser-than): " <> T.pack (show (LesserThan a' b'))
  Equal a b -> do
    a' <- evalExpr env a
    b' <- evalExpr env b
    case (a', b') of
      (Bool i, Bool j) -> pure . Bool $ i == j
      (Int i, Int j) -> pure . Bool $ i == j
      (SingleQuoteString s, SingleQuoteString t) -> pure . Bool $ s == t
      _ -> throwE $ Error.EvaluateError $ "Unimplemented (equal): " <> T.pack (show (Equal a' b'))
  Cons a b -> do
    a' <- evalExpr env a
    b' <- evalExpr env b
    case (a', b') of
      (Block bl, Block c) ->
        pure . Block $ setContent [c] bl
      (Block bl, SingleQuoteString s) ->
        pure . Block $ setContent [BlockText Normal [Lit s]] bl
      _ -> throwE $ Error.EvaluateError $ "Unimplemented (cons): " <> T.pack (show (Cons a' b'))
  Application a b -> do
    a' <- evalExpr env a
    b' <- evalExpr env b
    evalApplication env a' b'
  Thunk capturedEnv code ->
    evalExpr capturedEnv code
  frag@(Frag _ _ _) -> do
    blocks <- evalFrag env ["frag"] "-" [] [] frag
    case blocks of
      [bl] -> pure $ Block bl
      _ -> pure . Block $ BlockList blocks
  Block b -> do
    b' <- eval env ["block"] b
    pure $ Block b'
  code -> pure code

evalApplication :: Monad m => Env -> Expr -> Expr -> ExceptT Error.Error m Expr
evalApplication env a b =
  case a of
    BuiltIn "show" -> case b of
      Int i -> pure . SingleQuoteString . T.pack $ show i
      _ -> throwE $ Error.EvaluateError $ "Cannot apply show to: " <> T.pack (show b)
    BuiltIn "null" -> case b of
      SingleQuoteString s -> pure . Bool $ T.null s
      -- TODO Lookup returns False when the key is not present,
      -- but I have this code around:
      --   if null entry['journal']
      -- We need something like:
      --   if 'journal' in entry
      --   if elem 'journal' (keys entry)
      --   ...
      Bool False -> pure . Bool $ True
      _ -> throwE $ Error.EvaluateError $ "Cannot apply null to: " <> T.pack (show b)
    _ -> throwE $ Error.EvaluateError $ "Cannot apply: " <> T.pack (show a)

evalTemplate :: Monad m => Env -> [Inline] -> ExceptT Error.Error m [Inline]
evalTemplate env inlines =
  traverse (evalInline env) inlines

evalInline :: Monad m => Env -> Inline -> ExceptT Error.Error m Inline
evalInline env = \case
  Lit s -> pure $ Lit s
  Place code -> do
    code' <- evalExpr env code
    case code' of
      SingleQuoteString _ -> pure $ Place code'
      Bool _ -> pure $ Place code'
      Int _ -> pure $ Place code'
      Block _ -> pure $ Place code'
      -- Variable x -> context x -- Should not happen after evalExpr
      x -> error $ "evalInline: unhandled value: " <> show x

-- | Same as `extractVariables` plus an implicit @content@ block.
-- Note that unlike `extractVariables`, this version takes also care of
-- passing the environment being constructed to each definition.
extractVariables' :: Monad m => Env -> [Block] -> ExceptT Error.Error m [(Text, Expr)]
extractVariables' env nodes = do
  let named = extractVariables env' nodes
      unnamed = concatMap unnamedBlock nodes
      content = if null unnamed then [] else [("content", Frag [] env' unnamed)]
      vars = named <> content
      env' = augmentVariables env named -- Note we don't add the implicit "content" entry.
  if isJust (lookup "content" named) && not (null unnamed)
    then
      throwE $
        Error.EvaluateError $
          "A block of content and a content argument are provided"
    else pure vars

unnamedBlock :: Block -> [Block]
unnamedBlock (BlockImport path _ args) = [BlockFragmentCall (T.pack path) NoSym [] [] args]
unnamedBlock (BlockFragmentDef _ _ _) = []
unnamedBlock node = [node]

-- Extract both fragments and assignments.
-- TODO This should be merged with extractVariables'.
-- TODO We could filter the env, keeping only the free variables that appear
-- in the bodies.
extractVariables :: Env -> [Block] -> [(Text, Expr)]
extractVariables env = concatMap (extractVariable env)

extractVariable :: Env -> Block -> [(Text, Expr)]
extractVariable env = \case
  BlockDoctype -> []
  (BlockElem _ _ _ _) -> []
  (BlockText _ _) -> []
  (BlockInclude _ _ children) -> maybe [] (extractVariables env) children
  (BlockFor _ _ _ _) -> []
  (BlockFragmentDef name names children) -> [(name, Frag names env children)]
  (BlockFragmentCall _ _ _ _ _) -> []
  (BlockComment _ _) -> []
  (BlockFilter _ _) -> []
  (BlockRawElem _ _) -> []
  (BlockDefault _ _) -> []
  (BlockImport path (Just body) _) -> [(T.pack path, Frag [] env body)]
  (BlockImport _ _ _) -> []
  (BlockRun _ _) -> []
  (BlockReadJson name _ (Just val)) -> [(name, jsonToExpr val)]
  (BlockReadJson _ _ Nothing) -> []
  (BlockAssignVar name val) -> [(name, val)]
  (BlockIf _ _ _) -> []
  (BlockList _) -> []
  (BlockCode _) -> []

jsonToExpr :: Aeson.Value -> Expr
jsonToExpr = \case
  Aeson.String s -> SingleQuoteString s
  Aeson.Array xs ->
    List $ map jsonToExpr (V.toList xs)
  Aeson.Object kvs ->
    let f (k, v) = (SingleQuoteString $ Aeson.Key.toText k, jsonToExpr v)
     in Object $ map f (Aeson.KeyMap.toList kvs)
  x -> error $ "jsonToExpr: " <> show x

--------------------------------------------------------------------------------
simplify :: [Block] -> [Block]
simplify = concatMap simplify'

simplify' :: Block -> [Block]
simplify' = \case
  node@BlockDoctype -> [node]
  BlockElem name mdot attrs nodes -> [BlockElem name mdot attrs $ simplify nodes]
  node@(BlockText _ _) -> [node]
  BlockInclude _ _ mnodes -> maybe [] simplify mnodes
  BlockFragmentDef _ _ _ -> []
  BlockFragmentCall _ _ _ _ args -> simplify args
  BlockFor _ _ _ nodes -> simplify nodes
  node@(BlockComment _ _) -> [node]
  node@(BlockFilter _ _) -> [node]
  node@(BlockRawElem _ _) -> [node]
  BlockDefault _ nodes -> simplify nodes
  BlockImport _ mbody _ -> maybe [] simplify mbody
  BlockRun _ mbody -> maybe [] simplify mbody
  BlockReadJson _ _ _ -> []
  BlockAssignVar _ _ -> []
  BlockIf _ [] bs -> simplify bs
  BlockIf _ as _ -> simplify as
  BlockList nodes -> simplify nodes
  node@(BlockCode _) -> [node]