{-# LANGUAGE ScopedTypeVariables #-} -- | -- Module : Main -- Copyright : (c) Tim Williams 2017-2019 -- License : BSD3 -- -- Maintainer : info@timphilipwilliams.com -- Stability : experimental -- Portability : portable -- -- Expresso Read-Eval-Print-Loop. -- module Main where import Control.Applicative import Control.Monad (forM_) import Control.Monad.IO.Class import Control.Monad.State.Strict import Data.Char import System.Console.Haskeline (InputT) import System.Console.Haskeline.MonadException () import System.Directory import System.FilePath import Text.Parsec.String (Parser) import qualified System.Console.Haskeline as HL import qualified Text.Parsec as P import Expresso import Expresso.Parser ( pExp, pLetDecl, topLevel , reserved, reservedOp, stringLiteral ) import Expresso.Utils ps1 :: String ps1 = "λ" data Mode = SingleLine | MultiLine | Quitting data ReplState = ReplState { stateMode :: Mode , stateBuffer :: [String] , stateEnv :: Environments } data Command = Peek ExpI | Type ExpI | ChangeCWD FilePath | BeginMulti | Reset | DumpEnv | Quit | Help data Line = Command Command | Term ExpI | Decl (Bind Name) ExpI | NoOp type Repl = InputT (StateT ReplState IO) main :: IO () main = do cwd <- liftIO getCurrentDirectory let preludePath = cwd "Prelude.x" runRepl $ do mapM_ spew [ "Expresso REPL" , "Type :help or :h for a list of commands" ] HL.catch (loadPrelude preludePath) (\(e :: HL.SomeException) -> spew $ "Warning: Couldn't open " ++ preludePath ++ ": " ++ show e) repl -- | The read-eval-print-loop repl :: Repl () repl = step repl `HL.catch` (\(e :: HL.SomeException) -> spew (show e) >> repl) where step :: Repl () -> Repl () step cont = HL.withInterrupt $ do mode <- lift $ gets stateMode case mode of MultiLine -> do minput <- HL.getInputLine $ ps1 ++ "| " whenJust minput $ \input -> if isEndMulti input then doEndMulti else lift $ modify (addToBuffer input) cont SingleLine -> do minput <- HL.getInputLine $ ps1 ++ "> " whenJust minput process cont Quitting -> do spew "Goodbye." return () process :: String -> Repl () process str = do case parseLine str of Left err -> spew err Right (Command c) -> doCommand c Right (Term e) -> doEval showValue' e Right (Decl b e) -> doDecl b e Right NoOp -> return () `HL.catch` handler where handler :: HL.SomeException -> Repl () handler ex = spew $ "Caught exception: " ++ show ex runRepl :: Repl a -> IO a runRepl m = do historyFile <- ( ".expresso_history") <$> getHomeDirectory let settings = HL.defaultSettings {HL.historyFile = Just historyFile} evalStateT (HL.runInputT settings m) emptyReplState emptyReplState :: ReplState emptyReplState = ReplState { stateMode = SingleLine , stateBuffer = mempty , stateEnv = initEnvironments } loadPrelude :: FilePath -> Repl () loadPrelude path = do doDecl RecWildcard $ Fix (InR (K (Import path)) :*: K dummyPos) spew $ "Loaded Prelude from " ++ path doCommand :: Command -> Repl () doCommand c = case c of Peek e -> doEval (return . showValue) e Type e -> doTypeOf e ChangeCWD path -> liftIO $ setCurrentDirectory path Quit -> lift $ modify (setMode Quitting) BeginMulti -> lift $ modify (setMode MultiLine) Reset -> doReset DumpEnv -> doDumpEnv Help -> mapM_ spew [ "REPL commands available from the prompt:" , "" , " evaluate an expression" , ":peek evaluate, but not deeply" , ":{\\n ..lines.. \\n:}\\n multiline command" , ":cd change current working directory" , ":type show the type of " , ":reset reset REPL, unloading all definitions" , ":env dump bound symbols in the environment" , ":quit exit REPL" , ":help display this list of commands" , "" ] doEval :: (Value -> IO String) -> ExpI -> Repl () doEval pp e = do envs <- lift $ gets stateEnv v'e <- liftIO $ evalWithEnv envs e case v'e of Left err -> spew err Right val -> liftIO (pp val) >>= spew doDecl :: Bind Name -> ExpI -> Repl () doDecl b e = do envs <- lift $ gets stateEnv envs'e <- liftIO $ runEvalM $ bind envs b e case envs'e of Left err -> spew err Right envs' -> lift $ modify (setEnv envs') doTypeOf :: ExpI -> Repl () doTypeOf e = do envs <- lift $ gets stateEnv ms <- liftIO $ typeOfWithEnv envs e case ms of Left err -> spew err Right sigma -> spew (showType sigma) doReset :: Repl () doReset = lift $ modify (setEnv $ stateEnv emptyReplState) doDumpEnv :: Repl () doDumpEnv = do envs <- lift $ gets stateEnv forM_ (dumpTypeEnv envs) $ \(name, sigma) -> spew $ name ++ " : " ++ showType sigma parseLine :: String -> Either String Line parseLine str | all isSpace str = return NoOp | otherwise = showError $ P.parse (topLevel pLine) "" str pLine :: Parser Line pLine = pCommand <|> P.try pTerm <|> pDecl pTerm :: Parser Line pTerm = Term <$> pExp pDecl :: Parser Line pDecl = uncurry Decl <$> (reserved "let" *> pLetDecl) pCommand :: Parser Line pCommand = Command <$> (reservedOp ":" *> p) where p = (reserved "peek" <|> reserved "p") *> (Peek <$> pExp) <|> (reserved "type" <|> reserved "t") *> (Type <$> pExp) <|> reserved "cd" *> (ChangeCWD <$> pFilePath) <|> (reserved "reset" <|> reserved "r") *> pure Reset <|> (reserved "env" <|> reserved "e") *> pure DumpEnv <|> (reserved "quit" <|> reserved "q") *> pure Quit <|> (reserved "help" <|> reserved "h" <|> reserved "?") *> pure Help <|> reserved "{" *> pure BeginMulti pFilePath :: Parser FilePath pFilePath = stringLiteral -- TODO setMode :: Mode -> ReplState -> ReplState setMode m s = s { stateMode = m } setEnv :: Environments -> ReplState -> ReplState setEnv envs s = s { stateEnv = envs } addToBuffer :: String -> ReplState -> ReplState addToBuffer str s = s { stateBuffer = stateBuffer s ++ [str] } doEndMulti :: Repl () doEndMulti = do str <- lift $ gets (unlines . stateBuffer) lift $ modify $ clearBuffer . setMode SingleLine process str clearBuffer :: ReplState -> ReplState clearBuffer s = s { stateBuffer = mempty } isEndMulti :: String -> Bool isEndMulti ":}" = True isEndMulti _ = False spew :: String -> Repl () spew = HL.outputStrLn whenJust :: Applicative m => Maybe a -> (a -> m ()) -> m () whenJust mg f = maybe (pure ()) f mg