zuramaru-0.1.0.0: src/Maru/Main.hs
-- Suppress warnings what is happend by TemplateHaskell
{-# OPTIONS_GHC -Wno-unused-top-binds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE UndecidableInstances #-}
module Maru.Main
( runRepl
) where
import Control.Exception.Safe (SomeException)
import Control.Lens (view, (%=), (<>=), (.=), Iso', iso)
import Control.Monad (mapM, when, void, forM_)
import Control.Monad.State.Class (MonadState(..), gets)
import Data.Data (Data)
import Data.Extensible
import Data.Monoid ((<>))
import Data.Text (Text)
import Data.Typeable (Typeable)
import Maru.Preprocessor (preprocess)
import Maru.TH (makeLensesA)
import Maru.Type
import Safe (tailMay)
import System.Console.CmdArgs (cmdArgs, summary, program, help, name, explicit, (&=))
import TextShow (showt)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import qualified Maru.Eval as E
import qualified Maru.Parser as Parser
import qualified Maru.Type as MT
import qualified System.Console.Readline as R
-- | Command line options
data CliOptions = CliOptions
{ debugMode :: Bool
, doEval :: Bool
} deriving (Show, Data, Typeable)
makeLensesA ''CliOptions
-- | Default of `CliOptions`
cliOptions :: CliOptions
cliOptions = CliOptions
{ debugMode = False &= name "debug"
, doEval = True &= name "do-eval"
&= help "If you don't want to evaluation, disable this"
&= explicit
}
&= summary "マルのLisp処理系ずら〜〜"
&= program "maru"
-- |
-- Logs of REPL.
--
-- This is collected in 'Read' and 'Eval' phase of REPL,
-- and this is shown in 'Print' phase of REPL.
--
-- This is not shown if you doesn't specifiy --debug.
data DebugLogs = DebugLogs
{ readLogs :: [Text]
, evalLogs :: [Text]
} deriving (Show)
makeLensesA ''DebugLogs
-- | An empty value of `DebugLogs`
emptyDebugLog :: DebugLogs
emptyDebugLog = DebugLogs [] []
-- | Integrate any type as @State@ of REPL.
data ReplState = ReplState
{ replOpts :: CliOptions -- ^ specified CLI options (not an initial value)
, replEnv :: MaruEnv -- ^ The symbols of zuramaru
, replLogs :: DebugLogs -- ^ this value is appended in the runtime
}
makeLensesA ''ReplState
-- | For Lens Accessors
instance Associate "stateRepl" (State ReplState) xs => MonadState ReplState (Eff xs) where
get = getEff #stateRepl
put = putEff #stateRepl
-- |
-- The eval phase do parse and evaluation,
-- take its error or a rightly result
data EvalPhaseResult = ParseError ParseErrorResult -- ^ An error is happened in the parse
| EvalError SomeException -- ^ An error is happend in the evaluation
| RightResult SExpr -- ^ A result is made by the parse and the evaulation without errors
type Evaluator = MaruEnv -> SExpr -> IO (Either SomeException (SExpr, MaruEnv, SimplificationSteps))
-- | Run REPL of zuramaru
runRepl :: IO ()
runRepl = do
options <- cmdArgs cliOptions
let initialState = ReplState options E.initialEnv emptyDebugLog
void . retractEff @ IOEffKey $ runStateEff @ "stateRepl" repl initialState
-- |
-- Do 'Loop' of 'Read', 'eval', and 'Print',
-- with the startup options.
--
-- The state of `ReplState` is initialized before the one of "READ-EVAL-PRINT" of the "LOOP" is ran.
--
-- If some command line arguments are given, enable debug mode.
-- Debug mode shows the parse and the evaluation's optionally result.
repl :: Eff '["stateRepl" >: State ReplState, IOEff] ()
repl = do
loopIsRequired <- view _iso <$> runMaybeEff @ "maybe" rep
modifyEff #stateRepl $ \x -> x { replLogs = emptyDebugLog }
when loopIsRequired repl
-- |
-- Do 'Read', 'Eval', and 'Print' of 'REPL'.
-- Return False if Ctrl+d is input.
-- Return True otherwise.
--
-- If @rep@ throws a () of the error, it means what the loop of REP exiting is required.
rep :: Eff '[ "maybe" >: MaybeEff
, "stateRepl" >: State ReplState
, IOEff
] ()
rep = do
input <- castEff readPhase
evalResult <- castEff $ evalPhase input
printPhase evalResult
-- |
-- Read line from stdin.
-- If stdin gives to interrupt, return Nothing.
-- If it's not, return it and it is added to history file
readPhase :: Eff '["maybe" >: MaybeEff, IOEff] Text
readPhase = do
maybeInput <- liftIOEff $ R.readline "zuramaru> "
liftIOEff $ mapM R.addHistory maybeInput
T.pack <$> liftMaybe maybeInput
-- | Lift up `Nothing` to the failure of the whole
liftMaybe :: Associate "maybe" MaybeEff xs => Maybe a -> Eff xs a
liftMaybe Nothing = throwEff #maybe ()
liftMaybe (Just x) = return x
-- |
-- Do parse and evaluate a Text to a SExpr.
-- Return @SExpr@ (maru's AST) if both parse and evaluation is succeed.
-- Otherwise, return a error result.
--
-- Execute the evaluation.
-- A state of @DebugLogs@ is updated by got logs which can be gotten in the evaluation.
-- A state of @MaruEnv@ is updated by new environment of the result.
evalPhase :: Text -> Eff '["stateRepl" >: State ReplState, IOEff] EvalPhaseResult
evalPhase code = do
evalIsNeeded <- gets $ doEval . replOpts
-- Get a real evaluator or an empty evaluator.
-- The empty evaluator doesn't touch any arguments.
let eval' = if evalIsNeeded then E.eval
else fakeEval
case Parser.debugParse code of
(Left parseErrorResult, _) -> return $ ParseError parseErrorResult
(Right sexpr', xs) -> do
let parseLogs = map unParseLog xs
parseLog = "parse result: " <> showt sexpr'
let sexpr = preprocess sexpr'
preprLog = "preprocess result: " <> showt sexpr
replLogsA . evalLogsA <>= (parseLog : parseLogs)
replLogsA . evalLogsA <>= [preprLog]
env <- gets replEnv
evalResult <- liftIOEff $ eval' env sexpr
case evalResult of
Left evalErrorResult -> return $ EvalError evalErrorResult
Right (result, newEnv, steps) -> do
replEnvA .= newEnv
replLogsA . evalLogsA %= (++ reportSteps steps)
return $ RightResult result
where
-- Do nothing
fakeEval :: Evaluator
fakeEval = (return .) . (Right .) . flip (,,[])
-- | Do 'Print' for a result of 'Read' and 'Eval'
printPhase :: ( Associate "stateRepl" (State ReplState) xs
, IOEffAssociation xs
) => EvalPhaseResult -> Eff xs ()
printPhase result = do
DebugLogs readLogs' evalLogs' <- gets replLogs
debugMode' <- gets $ debugMode . replOpts
liftIOEff $ case result of
ParseError e -> TIO.putStrLn . T.pack . forgetMatrixAnnotation $ Parser.parseErrorPretty e
EvalError e -> TIO.putStrLn . T.pack $ show e
RightResult sexpr -> TIO.putStrLn $ MT.readable sexpr
liftIOEff . when debugMode' $ do
forM_ readLogs' $ TIO.putStrLn . ("<debug>(readPhase): " <>)
forM_ evalLogs' $ TIO.putStrLn . ("<debug>(evalPhase): " <>)
where
--NOTE: A result of Megaparse's `parseErrorPretty` may have about column and row of an error
forgetMatrixAnnotation :: String -> String
forgetMatrixAnnotation parseErrorInfo =
case tailMay $ lines parseErrorInfo of
Nothing -> ""
Just xs -> unlines xs
-- |
-- An isomorphism of between `Maybe ()` and `Bool`.
-- `Just ()` is mapped to `True`
_iso :: Iso' (Maybe ()) Bool
_iso = iso to from
where
to :: Maybe () -> Bool
to (Just ()) = True
to Nothing = False
from :: Bool -> Maybe ()
from True = Just ()
from False = Nothing