packages feed

egison-5.0.0: hs-src/Interpreter/egison.hs

{-# 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 <expr>  - Show the type of an expression"
  putStrLn "  :t <expr>     - 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