packages feed

ede-0.3.3.0: lib/Text/EDE/Internal/Eval.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}

-- |
-- Module      : Text.EDE.Internal.Eval
-- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
-- License     : This Source Code Form is subject to the terms of
--               the Mozilla Public License, v. 2.0.
--               A copy of the MPL can be found in the LICENSE file or
--               you can obtain it at http://mozilla.org/MPL/2.0/.
-- Maintainer  : Brendan Hay <brendan.g.hay@gmail.com>
-- Stability   : experimental
-- Portability : non-portable (GHC extensions)
--
-- /Warning/: this is an internal module, and does not have a stable
-- API or name. Functions in this module may not check or enforce
-- preconditions expected by public modules. Use at your own risk!
module Text.EDE.Internal.Eval where

import Control.Comonad.Cofree (Cofree ((:<)))
import qualified Control.Monad as Monad
import Control.Monad.Reader (ReaderT)
import qualified Control.Monad.Reader as Reader
import Control.Monad.Trans (lift)
import Data.Aeson ((.=))
import qualified Data.Aeson as Aeson
import Data.Aeson.Types (Value (..))
import qualified Data.Foldable as Foldable
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import Data.Scientific (isFloating)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Lazy.Builder (Builder)
import qualified Data.Text.Lazy.Builder as Text.Builder
import Data.Text.Lazy.Builder.Scientific (FPFormat (Fixed), formatScientificBuilder)
import Data.Text.Manipulate (toOrdinal)
import Prettyprinter ((<+>))
import qualified Prettyprinter as PP
import Text.EDE.Internal.Compat
import Text.EDE.Internal.Filters (stdlib)
import Text.EDE.Internal.Quoting
import Text.EDE.Internal.Types
import Text.Trifecta.Delta (Delta)
import qualified Text.Trifecta.Delta as Trifecta.Delta

data Env = Env
  { _templates :: HashMap Id (Exp Delta),
    _quoted :: HashMap Id Term,
    _values :: HashMap Id Value
  }

type Context = ReaderT Env Result

render ::
  HashMap Id (Exp Delta) ->
  HashMap Id Term ->
  Exp Delta ->
  HashMap Id Value ->
  Result Builder
render ts fs e o =
  Reader.runReaderT (eval e >>= nf) (Env ts (stdlib <> fs) o)
  where
    nf (TVal v) = build (Trifecta.Delta.delta e) v
    nf _ =
      lift $
        Failure
          "unable to evaluate partially applied template to normal form."

eval :: Exp Delta -> Context Term
eval (_ :< ELit l) = pure (qprim l)
eval (d :< EVar v) = quote (Text.pack (show v)) 0 <$> variable d v
eval (d :< EFun i) = do
  q <- HashMap.lookup i <$> Reader.asks _quoted
  maybe
    (throwError d $ "filter" <+> bold (pp i) <+> "doesn't exist.")
    pure
    q
eval (_ :< EApp (_ :< EFun "defined") e) = predicate e
eval (d :< EApp a b) = do
  x <- eval a
  y <- eval b
  binding d x y
eval (_ :< ELet k rhs bdy) = do
  q <- eval rhs
  v <- lift (unquote k 0 q)
  bind (HashMap.insert k v) (eval bdy)

-- FIXME: We have to recompute c everytime due to the predicate
eval (d :< ECase p ws) = go ws
  where
    go [] = pure (qprim (String mempty))
    go ((a, e) : as) =
      case a of
        PWild -> eval e
        PVar v -> eval (d :< EVar v) >>= cond e as
        PLit l -> eval (d :< ELit l) >>= cond e as

    cond e as y@(TVal Bool {}) = do
      x <- predicate p
      if x `eq` y
        then eval e
        else go as
    cond e as y@TVal {} = do
      x <- eval p
      if x `eq` y
        then eval e
        else go as
    cond _ as _ = go as

    eq (TVal a) (TVal b) = a == b
    eq _ _ = False
eval (_ :< ELoop i v bdy) = eval v >>= lift . unquote i 0 >>= loop
  where
    d = Trifecta.Delta.delta bdy

    loop :: Collection -> Context Term
    loop (Col l xs) = snd <$> Foldable.foldlM iter (1, qprim (String mempty)) xs
      where
        iter (n, p) x = do
          shadowed n
          q <- bind (HashMap.insert i (context n x)) (eval bdy)
          r <- binding d p q
          pure (n + 1, r)

        shadowed n = do
          m <- Reader.asks _values
          maybe
            (pure ())
            (shadowedErr n)
            (HashMap.lookup i m)

        shadowedErr n x =
          throwError d $
            "variable"
              <+> bold (pp i)
              <+> "shadows"
              <+> pp x
              <+> "in"
              <+> pp (toOrdinal n)
              <+> "loop iteration."

        context n (k, x) =
          Aeson.object $
            [ "value" .= x,
              "length" .= l,
              "index" .= n,
              "index0" .= (n - 1),
              "remainder" .= (l - n),
              "remainder0" .= (l - n - 1),
              "first" .= (n == 1),
              "last" .= (n == l),
              "odd" .= (n `mod` 2 == 1),
              "even" .= (n `mod` 2 == 0)
            ]
              ++ key k

        key (Just k) = ["key" .= k]
        key Nothing = []
eval (d :< EIncl i) = do
  ts <- Reader.asks _templates
  case HashMap.lookup i ts of
    Just e -> eval e
    Nothing ->
      throwError d $
        "template"
          <+> bold (pp i)
          <+> "is not in scope:"
          <+> PP.brackets (pp (Text.intercalate "," $ HashMap.keys ts))
{-# INLINEABLE eval #-}

bind :: (HashMap Text Value -> HashMap Text Value) -> Context a -> Context a
bind f = Reader.withReaderT (\x -> x {_values = f (_values x)})
{-# INLINEABLE bind #-}

variable :: Delta -> Var -> Context Value
variable d (Var is) =
  Reader.asks _values >>= go (NonEmpty.toList is) [] . Object . fromHashMapText
  where
    go [] _ v = pure v
    go (k : ks) r v = do
      m <- nest v
      maybe
        (throwError d $ "variable" <+> apretty cur <+> "doesn't exist.")
        (go ks (k : r))
        (HashMap.lookup k m)
      where
        cur = Var (k :| r)

        nest :: Value -> Context (HashMap Text Value)
        nest (Object o) = pure (toHashMapText o)
        nest x =
          throwError d $
            "variable"
              <+> apretty cur
              <+> "::"
              <+> pp x
              <+> "doesn't supported nested accessors."
{-# INLINEABLE variable #-}

-- | A variable can be tested for truthiness, but a non-whnf expr cannot.
predicate :: Exp Delta -> Context Term
predicate x =
  Reader.runReaderT (eval x) <$> Reader.ask
    >>= lift . \case
      Success q
        | TVal Bool {} <- q -> Success q
      Success q
        | TVal Null <- q -> Success (qprim False)
      Success _ -> Success (qprim True)
      Failure _
        | _ :< EVar {} <- x -> Success (qprim False)
      Failure e -> Failure e
{-# INLINEABLE predicate #-}

binding :: Delta -> Term -> Term -> Context Term
binding d x y =
  case (x, y) of
    (TVal l, TVal r) -> quote "<>" 0 <$> Monad.liftM2 (<>) (build d l) (build d r)
    _ -> lift (qapply d x y)
{-# INLINEABLE binding #-}

build :: Delta -> Value -> Context Builder
build _ Null = pure mempty
build _ (String t) = pure (Text.Builder.fromText t)
build _ (Bool True) = pure "true"
build _ (Bool False) = pure "false"
build _ (Number n)
  | isFloating n = pure (formatScientificBuilder Fixed Nothing n)
  | otherwise = pure (formatScientificBuilder Fixed (Just 0) n)
build d x =
  throwError d ("unable to render literal" <+> pp x)
{-# INLINEABLE build #-}

-- FIXME: Add delta information to the thrown error document.
throwError :: Delta -> AnsiDoc -> Context a
throwError d doc =
  lift . Failure $ Trifecta.Delta.prettyDelta d <+> red "error:" <+> doc