packages feed

zuramaru-0.1.0.0: src/Maru/Type/Eval.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE UndecidableInstances #-}

-- |
-- Integrate types of extensible's Effect.
--
-- `MaruMacro` is evaluated by `MaruEvaluator`.
-- `MaruFunc` is calculated by `MaruCalculator`.
module Maru.Type.Eval
  ( Fail
  , FailKey
  , FailValue
  , FailAssociation
  , throwFail
  , includeFail
  , includeFail'
  , SimplificationSteps
  , reportSteps
  , SimplifSteps
  , SimplifStepsKey
  , SimplifStepsValue
  , SimplifStepsAssociation
  , MaruScopes
  , MaruScopesKey
  , MaruScopesValue
  , MaruScopesAssociation
  , insertGlobalVar
  , newScope
  , popNewerScope
  , MaruEnv
  , getMaruEnv
  , putMaruEnv
  , modifyMaruEnv
  , IOEff
  , IOEffKey
  , IOEffValue
  , IOEffAssociation
  , liftIOEff
  , ExceptionCause
  , MaruEvaluator
  , runMaruEvaluator
  , newSymbol
  , MaruScope
  , MaruFunc (..)
  , MaruMacro (..)
  , lookup
  , lookupVar
  , (^$)
  , MaruCalculator
  , runMaruCalculator
  , First' (..)
  , first'
  , expandVars
  , substituteVar
  ) where

import Control.Lens hiding ((<|))
import Control.Monad.Fail (MonadFail(..))
import Data.Extensible
import Data.List.NonEmpty (NonEmpty(..), (<|))
import Data.Map.Lazy (Map)
import Data.Monoid (First(..))
import Data.Proxy (Proxy(..))
import Data.Semigroup (Semigroup(..))
import Data.Text (Text)
import Data.Typeable (Typeable, typeRep)
import Data.Unique (newUnique, hashUnique)
import Maru.Type.SExpr
import Prelude hiding (fail, lookup)
import TextShow (TextShow(..))
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Lazy as M
import qualified Data.Text as T
import qualified Maru.Type.SExpr as MTS

-- $setup
-- >>> :set -XOverloadedStrings
-- >>> import Data.Either
-- >>> import qualified Maru.Eval as E

-- | A message of @Fail@
type ExceptionCause = Text

-- |
-- An effect of @MaruEvaluator@.
-- A possible of the failure.
type Fail      = FailKey >: FailValue
type FailKey   = "fail"
type FailValue = EitherEff ExceptionCause
-- | `Fail`'s `Associate`
type FailAssociation = Associate FailKey FailValue

-- | `throwEff` for `Fail`
throwFail :: FailAssociation xs => ExceptionCause -> Eff xs a
throwFail = throwEff #fail

-- |
-- Include `Maybe` to `Fail` context.
-- If it is Nothing,
-- the whole of `Fail a` to be failed.
includeFail :: FailAssociation xs => ExceptionCause -> Eff xs (Maybe a) -> Eff xs a
includeFail cause mm = do
  maybeIt <- mm
  case maybeIt of
    Nothing -> throwFail cause
    Just x  -> return x

-- | Same as `includeFail`
includeFail' :: FailAssociation xs => Eff xs (Either ExceptionCause a) -> Eff xs a
includeFail' e = do
  maybeIt <- e
  case maybeIt of
    Left  c -> throwFail c
    Right x -> return x


-- | A log for 簡約s
type SimplificationSteps = [SExpr]

-- |
-- Append numbers to steps
--
-- >>> import Maru.Type.SExpr
-- >>> reportSteps [Cons (AtomInt 1) (Cons (AtomInt 2) Nil), Cons (AtomInt 2) Nil]
-- ["1: (1 2)","2: (2)"]
reportSteps :: SimplificationSteps -> [Text]
reportSteps = zipWith appendStepNumber [1..] . map readable
  where
    appendStepNumber :: Int -> Text -> Text
    appendStepNumber n x = showt n <> ": " <> x

-- | An effect of @MaruEvaluator@, for logging simplifications
type SimplifSteps      = SimplifStepsKey >: SimplifStepsValue
type SimplifStepsKey   = "simplifSteps"
type SimplifStepsValue = WriterEff SimplificationSteps
type SimplifStepsAssociation = Associate SimplifStepsKey SimplifStepsValue


-- |
-- An effect of @MaruEvaluator@.
-- This is a stack for the lexical scope.
type MaruScopes      = MaruScopesKey >: MaruScopesValue
type MaruScopesKey   = "maruScopes"
type MaruScopesValue = State MaruEnv
type MaruScopesAssociation = Associate MaruScopesKey MaruScopesValue

-- |
-- The whole of the runtime state.
-- This is `NonEmpty`, because the global scope is defined with the program startup
--
-- and
--
-- 'getMaruEnv >>= return . last' is the toplevel (and this is used as global scope).
-- 'getMaruEnv >>= return . head' is the newest scope.
--
-- ( This means the cons operation makes a new scope, it is not '++ [x]'.
--   Please see `makeScope`.
-- )
type MaruEnv = NonEmpty MaruScope

-- |
-- Insert a variable to the toplevel scope
--
-- >>> (_, env, _) <- flip runMaruEvaluator E.initialEnv $ insertGlobalVar "x" (AtomInt 10)
-- >>> lookup "x" env
-- Just (AtomInt 10)
insertGlobalVar :: MaruScopesAssociation xs => MaruSymbol -> SExpr -> Eff xs ()
insertGlobalVar sym val = do
  env <- getMaruEnv
  let env'   = NE.init env
      global = M.insert sym val $ NE.last env
  putMaruEnv $ case NE.nonEmpty env' of
    Nothing    -> (global :| [])
    Just env'' -> env'' <> [global]

-- |
-- Make a scope in the state, with a variable.
--
-- NOTE:
-- The scope must be created with one or more variables.
-- It keeps any safety.
--
-- e.g.
--   1. unintended empty scope is never created
--   2. high affinity of `MaruEnv` (`NonEmpty`)  is kept
newScope :: MaruScopesAssociation xs => MaruSymbol -> SExpr -> Eff xs ()
newScope sym val = modifyMaruEnv ([(sym, val)] <|)

-- |
-- Remove the newest scope (about the newest scope is written in `MaruEnv`),
-- and Return removed scope
popNewerScope :: MaruScopesAssociation xs => Eff xs MaruScope
popNewerScope = do
  (newest:|restEnv) <- getMaruEnv
  case NE.nonEmpty restEnv of
    Nothing       -> error "localScope(fatal error!): unexpected empty stack is detected! (In the correct case, the global scope is existed)"
    Just restEnv' -> putMaruEnv restEnv' >> return newest

-- |
-- The runtime state.
-- This associates the variable name and the value.
type MaruScope = Map MaruSymbol SExpr

-- | `getEff` for `MaruScopes`
getMaruEnv :: MaruScopesAssociation xs => Eff xs MaruEnv
getMaruEnv = getEff #maruScopes

-- | `putEff` for `MaruScopes`
putMaruEnv :: MaruScopesAssociation xs => MaruEnv -> Eff xs ()
putMaruEnv = putEff #maruScopes

-- | `modifyEff` for `maruScopes`
modifyMaruEnv :: MaruScopesAssociation xs => (MaruEnv -> MaruEnv) -> Eff xs ()
modifyMaruEnv = modifyEff #maruScopes

-- | Find a variable from the whole of the runtime environment with a symbol
lookup :: MaruSymbol -> MaruEnv -> Maybe SExpr
lookup sym xs = getFirst . mconcat . NE.toList $ NE.map (First . M.lookup sym) xs


-- | An effect of @MaruEvaluator@, this is same as `IO` in `Eff`
type IOEff      = IOEffKey >: IOEffValue
type IOEffKey   = "ioEff"
type IOEffValue = IO
type IOEffAssociation = Associate IOEffKey IOEffValue

-- | `liftEff` for `IOEff`
liftIOEff :: IOEffAssociation xs => IO a -> Eff xs a
liftIOEff = liftEff #ioEff


-- | A monad for evaluating a maru's program
type MaruEvaluator = Eff '[Fail, MaruScopes, SimplifSteps, IOEff]

instance MonadFail MaruEvaluator where
  fail = throwFail . T.pack


-- | Run an evaluation of @MaruEvaluator a@
runMaruEvaluator :: MaruEvaluator a -> MaruEnv -> IO (Either ExceptionCause a, MaruEnv, SimplificationSteps)
runMaruEvaluator m env = flatten <$> runMaruEvaluator' m env
  where
    runMaruEvaluator' :: MaruEvaluator a -> MaruEnv -> IO (((Either ExceptionCause a), MaruEnv), [SExpr])
    runMaruEvaluator' m env = retractEff . runWriterEff . flip runStateEff env $ runEitherEff m
    flatten :: ((a, b), c) -> (a, b, c)
    flatten ((x, y), z) = (x, y, z)

-- |
-- Create a symbol of the variable name,
-- it is unique (is not duplicated) in the runtime.
newSymbol :: MaruEvaluator MaruSymbol
newSymbol = ("n" <>) <$> newSymbol' ""
  where
    -- Take a base of the name.
    newSymbol' :: MaruSymbol -> MaruEvaluator MaruSymbol
    newSymbol' baseName = do
      name <- liftIOEff $ (baseName <>) . MTS.pack . show . hashUnique <$> newUnique
      env <- getMaruEnv
      case lookup name env of
           Nothing -> newSymbol' name
           Just  _ -> return name

--NOTE: Can this is alternated by some lens's function ?
-- |
-- This is like `Prism`'s accessor,
-- but don't return result as `Maybe`.
--
-- Similar to 'x <&> review f' but Nothing is included as a failure of the whole of `MaruEvaluator`.
--
-- `Typeable` for the error message.
(^$) :: (Typeable s, Typeable a) => MaruEvaluator s -> Getting (First a) s a -> MaruEvaluator a
(m :: MaruEvaluator s) ^$ (acs :: Getting (First a) s a) = do
  let typeNameOfS = T.pack . show $ typeRep (Proxy :: Proxy s)
      typeNameOfA = T.pack . show $ typeRep (Proxy :: Proxy a)
      cause = "(^$): `" <> typeNameOfA <> "` couldn't be getten from `" <> typeNameOfS <> "`"
  includeFail cause $ m <&> preview acs


-- |
-- This has effects of the part of `MaruEvaluator`.
-- Calculate pure functions.
type MaruCalculator = Eff '[Fail]

instance MonadFail MaruCalculator where
  fail = throwFail . T.pack

-- | Extract the pure calculation from `MaruCalculator`
runMaruCalculator :: MaruCalculator a -> Either ExceptionCause a
runMaruCalculator = leaveEff . runEitherEff


-- | Simular to `First`, but using '`Either` `ExceptionCause`' instead of `Maybe`
newtype First' a = First'
  { getFirst' :: Either ExceptionCause a
  } deriving (Functor)

instance Monoid (First' a) where
  mempty = First' $ Left "mempty: `First'` couldn't find the right value"
  w@(First' (Right _)) `mappend` _ = w
  _ `mappend` w@(First' (Right _)) = w
  _ `mappend` _                    = mempty


-- |
-- Like a consturctor, but from '`Maybe` a'.
-- If `Nothing` is given, return `mempty`.
first' :: Maybe a -> First' a
first' (Just a) = First' $ Right a
first' Nothing  = mempty


-- |
-- A function of maru.
-- This keeps the purity, don't happen effects.
--
-- Take [`SExpr`] as arguments, its length is checked by each function.
-- If it is not the expected length, `Nothing` maybe given.
--
-- Notice:
--
-- The function is Haskell's function, is represented by Haskell.
-- The function is not maru's (runtime's) function (cannot be defined in the runtime).
newtype MaruFunc = MaruFunc
  { execFunc :: [SExpr] -> MaruCalculator SExpr
  }

-- |
-- A macro of maru,
-- this means the impure function.
--
-- Similar to `MaruFunc`,
-- but this is possibility to update the state of the environment.
newtype MaruMacro = MaruMacro
  { execMacro :: SExpr -> MaruEvaluator SExpr
  }


-- |
-- Take a variable from `MaruScopes` effect.
-- If `sym` is not exists, the whole of this `Eff` to be failed
lookupVar :: forall xs.
             ( FailAssociation xs
             , MaruScopesAssociation xs
             ) => MaruSymbol -> Eff xs SExpr
lookupVar sym = do
  env <- getMaruEnv
  let cause = "A symbol '" <> unMaruSymbol sym <> "' is not found"
  includeFail cause . return $ lookup sym env


-- |
-- Expand the value of the variables, but these are not evaluated.
--
-- And +, -, *, and / are not expanded
-- (because it is regarded as like the axioms)
--
-- simply expanding
--
-- >>> (sexpr, _, _) <- flip runMaruEvaluator E.initialEnv $ newScope "x" (AtomInt 10) >> expandVars (AtomSymbol "x")
-- >>> sexpr
-- Right (AtomInt 10)
--
-- multi variables
--
-- >>> (sexpr, _, _) <- flip runMaruEvaluator E.initialEnv $ newScope "x" (AtomInt 10) >> newScope "y" (AtomBool True) >> expandVars (Cons (AtomSymbol "x") (Cons (AtomSymbol "y") Nil))
-- >>> sexpr
-- Right (Cons (AtomInt 10) (Cons (AtomBool True) Nil))
--
-- nested expanding
--
-- >>> (sexpr, _, _) <- flip runMaruEvaluator E.initialEnv $ newScope "x" (AtomInt 10) >> newScope "y" (AtomSymbol "x") >> expandVars (AtomSymbol "y")
-- >>> sexpr
-- Right (AtomInt 10)
--
-- the quote is kept
--
-- >>> (sexpr, _, _) <- flip runMaruEvaluator E.initialEnv $ expandVars (Quote (AtomSymbol "xxx"))
-- >>> sexpr
-- Right (Quote (AtomSymbol "xxx"))
expandVars :: (MaruScopesAssociation xs, FailAssociation xs) => SExpr -> Eff xs SExpr
expandVars (AtomSymbol "+") = return $ AtomSymbol "+"
expandVars (AtomSymbol "-") = return $ AtomSymbol "-"
expandVars (AtomSymbol "*") = return $ AtomSymbol "*"
expandVars (AtomSymbol "/") = return $ AtomSymbol "/"
expandVars Nil = return Nil
expandVars (AtomInt x) = return $ AtomInt x
expandVars (AtomBool x) = return $ AtomBool x
expandVars (Cons x y) = Cons <$> expandVars x <*> expandVars y
expandVars (AtomSymbol var) = lookupVar var >>= expandVars
expandVars (Quote x) = return $ Quote x


-- |
-- Substitute a value to a variable.
--
-- "x" is substituted by `AtomInt 10` in `Cons (AtomInt 1) (Cons (AtomSymbol "x") Nil)`.
--
-- >>> substituteVar "x" (AtomInt 10) $ Cons (AtomInt 1) (Cons (AtomSymbol "x") Nil)
-- Cons (AtomInt 1) (Cons (AtomInt 10) Nil)
--
-- >>> substituteVar "x" (AtomInt 10) $ Cons (AtomSymbol "x") (Cons (AtomSymbol "x") Nil)
-- Cons (AtomInt 10) (Cons (AtomInt 10) Nil)
substituteVar :: MaruSymbol -> SExpr -> SExpr -> SExpr
substituteVar var val (AtomSymbol var') =
  if var == var' then val
                 else AtomSymbol var'
substituteVar _ _ Nil = Nil
substituteVar _ _ (AtomInt x) = AtomInt x
substituteVar _ _ (AtomBool x) = AtomBool x
substituteVar _ _ (Quote x) = Quote x
substituteVar var val (Cons x y) = Cons (substituteVar var val x) (substituteVar var val y)