{-# LANGUAGE LambdaCase #-} module Main where import Control.Exception (AsyncException (..)) import Control.Monad (when) import Control.Monad.Catch (catch) import Control.Monad.Reader import Data.List (intercalate) import qualified Data.Text as T import Data.Version import System.Console.Haskeline (InputT, Settings (..), getHistory, getInputLine, putHistory, runInputT) import System.Console.Haskeline.History (addHistoryUnlessConsecutiveDupe) import System.Directory (getHomeDirectory) import System.Exit (exitFailure, exitSuccess) import System.FilePath (()) import System.IO import Text.Regex.TDFA ((=~)) import Language.Egison import Language.Egison.Completion import Options.Applicative main :: IO () main = execParser cmdParser >>= runWithOptions isInValidMathOption :: EgisonOpts -> Bool isInValidMathOption EgisonOpts{ optMathExpr = Just lang } = lang `notElem` ["asciimath", "latex", "mathematica", "maxima", "haskell"] isInValidMathOption EgisonOpts{ optMathExpr = Nothing } = False runWithOptions :: EgisonOpts -> IO () runWithOptions opts | isInValidMathOption opts = hPrint stderr (Default "this output lang is not supported") >> exitFailure runWithOptions EgisonOpts{ optShowVersion = True } = putStrLn (showVersion version) >> exitSuccess runWithOptions opts = evalRuntimeT opts run run :: RuntimeM () run = do opts <- ask -- Collect all files to load (core libs, user libs, load files, and test files) -- Load core libraries first unless --no-prelude is set isNoPrelude <- asks optNoPrelude mathNormalize <- asks optMathNormalize let coreLibExprs = if isNoPrelude then [] else map Load coreLibraries -- Add math normalization library based on option mathLibExpr = if isNoPrelude then [] else [Load (if mathNormalize then "lib/math/normalize.egi" else "lib/math/no-normalize.egi")] libExprs = map Load (optLoadLibs opts) loadFileExprs = map LoadFile (optLoadFiles opts) -- Include test file in the initial load to preserve type class environment testFileExprs = case (optTestOnly opts, optExecFile opts) of (True, Just (file, _)) -> [LoadFile file] _ -> [] -- Load core libraries first, then math library, then user libraries and files allLoadExprs = coreLibExprs ++ mathLibExpr ++ libExprs ++ loadFileExprs ++ testFileExprs -- Load all libraries and user files in a single EvalM context to preserve EvalState mResult <- fromEvalTWithState initialEvalState $ do env <- initialEnv -- Only primitive environment evalTopExprs' env allLoadExprs True True case mResult of Left err -> liftIO $ print err Right (env, evalState) -> handleOptionWithState env evalState opts handleOption :: Env -> EgisonOpts -> RuntimeM () handleOption env opts = handleOptionWithState env initialEvalState opts handleOptionWithState :: Env -> EvalState -> EgisonOpts -> RuntimeM () handleOptionWithState env evalState opts = case opts of -- Evaluate the given string EgisonOpts { optEvalString = Just expr } -> runAndPrintExprWithState env evalState expr -- Execute the given string EgisonOpts { optExecuteString = Just cmd } -> executeTopExprWithState env evalState $ "execute (" ++ cmd ++ ")" -- Operate input in tsv format as infinite stream EgisonOpts { optSubstituteString = Just sub } -> let (sopts, copts) = unzip (optFieldInfo opts) sopts' = "[" ++ intercalate ", " sopts ++ "]" copts' = "[" ++ intercalate ", " copts ++ "]" expr = "load \"lib/core/shell.egi\"\n" ++ "execute (let SH.input := SH.genInput " ++ sopts' ++ " " ++ copts' ++ "\n" ++ if optTsvOutput opts then " in each (\\x -> print (showTsv x)) ((" ++ sub ++ ") SH.input))" else " in each (\\x -> print (show x)) ((" ++ sub ++ ") SH.input))" in executeTopExprWithState env evalState expr -- Execute a script (test only) -- Note: The test file has already been loaded in the run function -- to preserve the type class environment from library files. -- Here we just need to evaluate the test expressions. EgisonOpts { optTestOnly = True, optExecFile = Just (_file, _) } -> do -- Test file has already been loaded, nothing more to do return () -- Execute a script from the main function EgisonOpts { optExecFile = Just (file, args) } -> do result <- fromEvalT $ evalTopExprs env [LoadFile file, Execute (makeApply "main" [CollectionExpr (map (ConstantExpr . StringExpr . T.pack) args)])] liftIO $ either print (const $ return ()) result EgisonOpts { optMapTsvInput = Just expr } -> handleOption env (opts { optSubstituteString = Just $ "\\x -> map (" ++ expr ++ ") x" }) EgisonOpts { optFilterTsvInput = Just expr } -> handleOption env (opts { optSubstituteString = Just $ "\\x -> filter (" ++ expr ++ ") x" }) -- Start the read-eval-print-loop _ -> do when (optShowBanner opts) (liftIO showBanner) replWithState env evalState when (optShowBanner opts) (liftIO showByebyeMessage) liftIO exitSuccess runAndPrintExpr :: Env -> String -> RuntimeM () runAndPrintExpr env expr = do isTsvOutput <- asks optTsvOutput if isTsvOutput then executeTopExpr env $ "execute (each (\\x -> print (showTsv x)) (" ++ expr ++ "))" else executeTopExpr env $ "execute (print (show (" ++ expr ++ ")))" runAndPrintExprWithState :: Env -> EvalState -> String -> RuntimeM () runAndPrintExprWithState env evalState expr = do isTsvOutput <- asks optTsvOutput if isTsvOutput then executeTopExprWithState env evalState $ "execute (each (\\x -> print (showTsv x)) (" ++ expr ++ "))" else executeTopExprWithState env evalState $ "execute (print (show (" ++ expr ++ ")))" executeTopExpr :: Env -> String -> RuntimeM () executeTopExpr env expr = do cmdRet <- fromEvalT (runTopExprs env expr) case cmdRet of Left err -> liftIO $ hPrint stderr err >> exitFailure _ -> liftIO exitSuccess executeTopExprWithState :: Env -> EvalState -> String -> RuntimeM () executeTopExprWithState env evalState expr = do cmdRet <- fromEvalTWithState evalState (runTopExprs env expr) case cmdRet of Left err -> liftIO $ hPrint stderr err >> exitFailure Right _ -> liftIO exitSuccess showBanner :: IO () showBanner = do putStrLn $ "Egison Version " ++ showVersion version putStrLn "https://www.egison.org" putStrLn "Welcome to Egison Interpreter!" -- putStrLn $ "** Information **" -- putStrLn $ "We can use the tab key to complete keywords on the interpreter." -- putStrLn $ "If we press the tab key after a closed parenthesis, the next closed parenthesis will be completed." -- putStrLn $ "*****************" showByebyeMessage :: IO () showByebyeMessage = putStrLn "Leaving Egison Interpreter." settings :: MonadIO m => FilePath -> Env -> Settings m settings home env = Settings { complete = completeEgison env , historyFile = Just (home ".egison_history") , autoAddHistory = False } repl :: Env -> RuntimeM () repl env = replWithState env initialEvalState replWithState :: Env -> EvalState -> RuntimeM () replWithState env evalState = (do home <- liftIO getHomeDirectory input <- runInputT (settings home env) (getReplInput env) case input of Nothing -> return () Just (ReplExpr topExpr) -> do result <- fromEvalTWithState evalState (evalTopExprStr env topExpr) case result of Left err -> liftIO (print err) >> replWithState env evalState Right ((Just str, env'), evalState') -> liftIO (putStrLn str) >> replWithState env' evalState' Right ((Nothing, env'), evalState') -> replWithState env' evalState' Just (ReplTypeStr exprStr) -> do -- Parse and type check the expression -- Note: This feature is temporarily disabled due to refactoring. -- TODO: Re-implement using Infer pipeline. liftIO $ putStrLn $ "Type checking in REPL is temporarily disabled during refactoring." liftIO $ putStrLn $ "Expression: " ++ exprStr replWithState env evalState Just ReplHelp -> do liftIO showReplHelp replWithState env evalState Just ReplQuit -> return () ) `catch` (\case UserInterrupt -> liftIO (putStrLn "") >> replWithState env evalState StackOverflow -> liftIO (putStrLn "Stack over flow!") >> replWithState env evalState HeapOverflow -> liftIO (putStrLn "Heap over flow!") >> replWithState env evalState _ -> liftIO (putStrLn "error!") >> replWithState env evalState ) -- | REPL input types data ReplInput = ReplExpr TopExpr -- ^ Regular expression to evaluate | ReplTypeStr String -- ^ :type command with expression string | ReplHelp -- ^ :help command | ReplQuit -- ^ :quit command -- | Show REPL help showReplHelp :: IO () showReplHelp = do putStrLn "REPL Commands:" putStrLn " :type - Show the type of an expression" putStrLn " :t - Short for :type" putStrLn " :help - Show this help" putStrLn " :h - Short for :help" putStrLn " :quit - Exit the REPL" putStrLn " :q - Short for :quit" -- |Get REPL input from the prompt. We can handle multiline input and special commands. getReplInput :: Env -> InputT RuntimeM (Maybe ReplInput) getReplInput env = getReplInput' "" where getReplInput' prev = do opts <- lift ask mLine <- case prev of "" -> getInputLine $ optPrompt opts _ -> getInputLine $ replicate (length $ optPrompt opts) ' ' case mLine of Nothing -> return Nothing Just [] | null prev -> getReplInput env Just [] -> getReplInput' prev Just line -> do history <- getHistory putHistory $ addHistoryUnlessConsecutiveDupe line history let input = prev ++ line -- Check for special commands case parseReplCommand input of Just cmd -> return $ Just cmd Nothing -> do parsedExpr <- lift $ parseTopExpr (replaceNewLine input) case parsedExpr of Left err | err =~ "unexpected end of input" -> getReplInput' (input ++ "\n") Left err -> do liftIO $ putStrLn ("Parse error at: " ++ err) getReplInput env Right topExpr -> return $ Just (ReplExpr topExpr) -- | Parse REPL special commands parseReplCommand :: String -> Maybe ReplInput parseReplCommand input = case words input of [":quit"] -> Just ReplQuit [":q"] -> Just ReplQuit [":help"] -> Just ReplHelp [":h"] -> Just ReplHelp (":type":rest) -> parseTypeCommand (unwords rest) (":t":rest) -> parseTypeCommand (unwords rest) _ -> Nothing -- | Parse :type command (returns expression string to be parsed later) parseTypeCommand :: String -> Maybe ReplInput parseTypeCommand exprStr | null exprStr = Nothing | otherwise = Just $ ReplTypeStr exprStr replaceNewLine :: String -> String replaceNewLine input = let (before, _, after) = input =~ "#newline" :: (String, String, String) in case after of "" -> before _ -> before ++ "\n" ++ replaceNewLine after