ivo/app/Main.hs

238 lines
7.4 KiB
Haskell

module Main (main) where
import Ivo
import Control.Exception (IOException, catch)
import Data.Maybe (isJust)
import Control.Monad (when)
import Control.Monad.Catch (MonadMask)
import Control.Monad.Except (MonadError, ExceptT, runExceptT, throwError, liftEither)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Loops (whileJust_)
import Control.Monad.State (MonadState, StateT, evalStateT, gets, modify)
import Control.Monad.Trans (lift)
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HM
import Data.Text qualified as T
import System.Console.Haskeline
( InputT, runInputT, defaultSettings
, outputStrLn, getInputLine, handleInterrupt, withInterrupt
)
import Text.Parsec
import Text.Parsec.Text (Parser)
outputTextLn :: MonadIO m => Text -> InputT m ()
outputTextLn = outputStrLn . T.unpack
-- | Immediately quit the program when interrupted
-- without performing any additional actions.
-- (Without this, it will print an extra newline for some reason.)
justDie :: (MonadIO m, MonadMask m) => InputT m () -> InputT m ()
justDie = handleInterrupt (pure ()) . withInterrupt
data AppState = AppState
{ traceOptions :: TraceOptions
, checkOptions :: CheckOptions
, definitions :: HashMap Text CheckExpr
}
data TraceOptions
-- | Print the entire expression in traces.
= TraceGlobal
-- | Print only the modified part of the expression in traces.
| TraceLocal
-- | Do not trace evaluation.
| TraceOff
data CheckOptions = CheckOptions
-- | Require that an expression typechecks to run it.
{ shouldTypecheck :: Bool
-- | Print the inferred type of an expressions.
, shouldPrintType :: CheckPrintOptions
}
data CheckPrintOptions = PrintAlways | PrintDecls | PrintOff
deriving Eq
shouldPrintTypeErrorsQ :: Bool -> CheckOptions -> Bool
shouldPrintTypeErrorsQ isDecl opts
= shouldTypecheck opts
|| shouldPrintTypeQ isDecl opts
shouldPrintTypeQ :: Bool -> CheckOptions -> Bool
shouldPrintTypeQ isDecl opts
= shouldPrintType opts == PrintAlways
|| shouldPrintType opts == PrintDecls && isDecl
defaultAppState :: AppState
defaultAppState = AppState
{ traceOptions = TraceOff
, checkOptions = CheckOptions
{ shouldTypecheck = True
, shouldPrintType = PrintDecls
}
, definitions = HM.empty
}
data Command
= Trace TraceOptions
| Check CheckOptions
| Load FilePath
| Clear
commandParser :: Parser Command
commandParser = do
char ':'
trace <|> check <|> load <|> clear
where
trace = Trace <$> do
try $ string "trace "
try traceOff <|> try traceLocal <|> try traceGlobal
traceOff = TraceOff <$ string "off"
traceLocal = TraceLocal <$ string "local"
traceGlobal = TraceGlobal <$ string "global"
check = Check <$> do
try $ string "check "
spaces
tc <- (True <$ try (string "on ")) <|> (False <$ try (string "off "))
spaces
pr <- try printAlways <|> try printDecls <|> try printOff
pure $ CheckOptions tc pr
printAlways = PrintAlways <$ string "always"
printDecls = PrintDecls <$ string "decls"
printOff = PrintOff <$ string "off"
load = Load <$> do
try $ string "load "
spaces
filename <- many1 (noneOf " ")
spaces
pure filename
clear = Clear <$ try (string "clear")
class MonadState AppState m => MonadApp m where
parsed :: Either ParseError a -> m a
typecheckDecl :: Text -> CheckExpr -> m (Maybe Scheme)
typecheckExpr :: CheckExpr -> m (Maybe Scheme)
execute :: CheckExpr -> m EvalExpr
type AppM = ExceptT Text (StateT AppState (InputT IO))
liftInput :: InputT IO a -> AppM a
liftInput = lift . lift
instance MonadApp AppM where
parsed (Left err) = throwError $ T.pack $ show err
parsed (Right ok) = pure ok
typecheckDecl = typecheck . Just
typecheckExpr = typecheck Nothing
execute checkExpr = do
defs <- gets definitions
let expr = check2eval $ substitute defs checkExpr
traceOpts <- gets traceOptions
case traceOpts of
TraceOff -> do
let value = eval expr
liftInput $ outputTextLn $ unparseEval value
pure value
TraceLocal -> do
let (value, trace) = evalTrace expr
liftInput $ mapM_ (outputTextLn . unparseEval) trace
pure value
TraceGlobal -> do
let (value, trace) = evalTraceGlobal expr
liftInput $ mapM_ (outputTextLn . unparseEval) trace
pure value
typecheck :: Maybe Text -> CheckExpr -> AppM (Maybe Scheme)
typecheck decl expr = do
defs <- gets definitions
let type_ = infer $ substitute defs expr
checkOpts <- gets checkOptions
if shouldTypecheck checkOpts
then case type_ of
Left err -> throwError $ "Typecheck error: " <> err
Right t -> do
printType checkOpts t
pure $ Just t
else do
case type_ of
Left err ->
when (shouldPrintTypeErrorsQ isDecl checkOpts) $
liftInput $ outputStrLn $ "Typecheck error: " <> T.unpack err
Right t -> printType checkOpts t
pure Nothing
where
isDecl = isJust decl
printType opts t =
when (shouldPrintTypeQ isDecl opts) $
liftInput $ outputTextLn $ prefix <> unparseScheme t
prefix = case decl of
Just name -> name <> " : "
Nothing -> ": "
define :: MonadApp m => Text -> CheckExpr -> m ()
define name expr = modify \appState ->
let expr' = substitute (definitions appState) expr
in appState { definitions = HM.insert name expr' $ definitions appState }
runDeclOrExpr :: MonadApp m => DeclOrExprAST -> m ()
runDeclOrExpr (Left (name, exprAST)) = do
defs <- gets definitions
let expr = substitute defs $ ast2check exprAST
_ <- typecheckDecl name expr
define name expr
runDeclOrExpr (Right exprAST) = do
defs <- gets definitions
let expr = substitute defs $ ast2check exprAST
_ <- typecheckExpr expr
_ <- execute expr
pure ()
runProgram :: MonadApp m => ProgramAST -> m ()
runProgram = mapM_ runDeclOrExpr
runCommand :: forall m. (MonadApp m, MonadIO m, MonadError Text m) => Command -> m ()
runCommand (Trace traceOpts) = modify \app -> app { traceOptions = traceOpts }
runCommand (Check checkOpts) = modify \app -> app { checkOptions = checkOpts }
runCommand Clear = modify \app -> app { definitions = HM.empty }
runCommand (Load filePath) = do
input <- safeReadFile
program <- parsed $ parse programParser filePath input
runProgram program
where
safeReadFile :: m Text
safeReadFile = liftEither =<< liftIO (
(Right . T.pack <$> readFile filePath)
`catch` handleException)
handleException :: IOException -> IO (Either Text Text)
handleException = pure . Left . T.pack . show
parseCommandOrDeclOrExpr :: MonadApp m => Text -> m (Either Command DeclOrExprAST)
parseCommandOrDeclOrExpr input = parsed $ parse commandOrDeclOrExprParser "input" input
where
commandOrDeclOrExprParser =
(Left <$> try commandParser) <|> (Right <$> declOrExprParser) <* spaces <* eof
main :: IO ()
main = runInputT defaultSettings $ justDie $ flip evalStateT defaultAppState $
whileJust_ (fmap T.pack <$> lift (getInputLine ">> ")) \inputText ->
handleErrors do
input <- parseCommandOrDeclOrExpr inputText
either runCommand runDeclOrExpr input
where
handleErrors :: ExceptT Text (StateT AppState (InputT IO)) () -> StateT AppState (InputT IO) ()
handleErrors m = do
result <- runExceptT m
case result of
Left err -> lift $ outputTextLn err
Right _ -> pure ()