module REPL where import Check (checkFileWithStore) import Eval (evalTricu, result) import FileEval ( ContractMode(..) , LoadedSource(..) , defaultStorePath , loadFileWithStoreMode ) import Parser (parseTricu) import Research (EvaluatedForm(..), Env, formatT) import ContentStore (StorePath(..)) import Control.Exception (SomeException, catch, displayException) import Control.Monad.IO.Class (liftIO) import Data.IORef (IORef, newIORef, readIORef, writeIORef) import Data.List (isPrefixOf, sort) import Data.Version (showVersion) import Paths_tricu (version) import System.Console.Haskeline import System.Directory (doesFileExist) import qualified Data.Map as Map -- | Source-local REPL with the same filesystem CAS/module loader used by the -- CLI. View Contract checking is explicit (`!check`); evaluation can run in -- normal publishing mode or unchecked mode. data REPLState = REPLState { replForm :: EvaluatedForm , replEnv :: Env , replStore :: StorePath , replContracts :: ContractMode , replEnvRef :: IORef Env } repl :: IO () repl = do store <- defaultStorePath envRef <- newIORef Map.empty let settings = Settings { complete = completeRepl envRef , historyFile = Just "~/.local/state/tricu/history" , autoAddHistory = True } runInputT settings (loop (REPLState Decode Map.empty store EnforceContracts envRef)) where loop :: REPLState -> InputT IO () loop state = do minput <- getInputLine "tricu < " case minput of Nothing -> return () Just raw -> do let s = strip raw case s of "" -> loop state "!exit" -> outputStrLn "Exiting tricu" "!clear" -> liftIO (putStr "\ESC[2J\ESC[H") >> loop state "!reset" -> do liftIO $ writeIORef (replEnvRef state) Map.empty outputStrLn "Environment reset" loop state { replEnv = Map.empty } "!help" -> printHelp >> loop state "!output" -> handleOutput state "!env" -> handleEnv state >> loop state _ | "!load" `isPrefixOf` s -> handleLoad state (strip $ drop 5 s) | "!check" `isPrefixOf` s -> handleCheck state (strip $ drop 6 s) | "!store" `isPrefixOf` s -> handleStore state (strip $ drop 6 s) | "!format" `isPrefixOf` s -> handleFormat state (strip $ drop 7 s) | "!unchecked" `isPrefixOf` s -> handleUnchecked state (strip $ drop 10 s) | take 2 s == "--" -> loop state | otherwise -> do next <- liftIO $ catch (processInput state raw) (errorHandler state) loop next printHelp :: InputT IO () printHelp = do outputStrLn $ "tricu version " ++ showVersion version outputStrLn "Available commands:" outputStrLn " !exit - Exit the REPL" outputStrLn " !clear - Clear the screen" outputStrLn " !reset - Reset the in-memory environment" outputStrLn " !help - Show this help" outputStrLn " !output - Change output format interactively" outputStrLn " !format FORM - Set output format: tree, fsl, ast, ternary, ascii, decode, number, string" outputStrLn " !load FILE - Load and evaluate a .tri file into the environment" outputStrLn " !check FILE - Check View Contract annotations in a .tri file" outputStrLn " !store [PATH] - Show or set the content-addressed store path" outputStrLn " !unchecked [on|off] - Show or set unchecked eval mode" outputStrLn " !env - List names currently in the REPL environment" handleOutput :: REPLState -> InputT IO () handleOutput state = do let formats = outputFormats outputStrLn "Available output formats:" mapM_ (\(i, f) -> outputStrLn $ show (i :: Int) ++ ". " ++ show f) (zip [1..] formats) input <- getInputLine "Select output format (1-8) < " case input >>= readMaybeInt of Just n | n >= 1 && n <= length formats -> do let newForm = formats !! (n - 1) outputStrLn $ "Output format changed to: " ++ show newForm loop state { replForm = newForm } _ -> outputStrLn "Invalid selection. Keeping current output format." >> loop state handleFormat :: REPLState -> String -> InputT IO () handleFormat state arg = case readEvaluatedForm arg of Just form -> outputStrLn ("Output format changed to: " ++ show form) >> loop state { replForm = form } Nothing -> outputStrLn "Usage: !format tree|fsl|ast|ternary|ascii|decode|number|string" >> loop state handleLoad :: REPLState -> String -> InputT IO () handleLoad state path | null path = outputStrLn "Usage: !load FILE" >> loop state | otherwise = do exists <- liftIO $ doesFileExist path if not exists then outputStrLn ("File not found: " ++ path) >> loop state else do loaded <- liftIO $ loadFileWithStoreMode (replContracts state) (replStore state) path let env' = evalTricu (Map.union (loadedImports loaded) (replEnv state)) (loadedAst loaded) liftIO $ writeIORef (replEnvRef state) env' outputStrLn $ "Loaded " ++ path loop state { replEnv = env' } handleCheck :: REPLState -> String -> InputT IO () handleCheck state path | null path = outputStrLn "Usage: !check FILE" >> loop state | otherwise = do exists <- liftIO $ doesFileExist path if not exists then outputStrLn ("File not found: " ++ path) >> loop state else do output <- liftIO $ checkFileWithStore (replStore state) path outputStrLn output loop state handleStore :: REPLState -> String -> InputT IO () handleStore state path | null path = do outputStrLn $ "Store: " ++ storePathString (replStore state) loop state | otherwise = do outputStrLn $ "Store changed to: " ++ path loop state { replStore = StorePath path } handleUnchecked :: REPLState -> String -> InputT IO () handleUnchecked state arg = setUnchecked state arg setUnchecked :: REPLState -> String -> InputT IO () setUnchecked state arg = case arg of "" -> reportContracts state >> loop state "on" -> setMode IgnoreContracts "off" -> setMode EnforceContracts _ -> outputStrLn "Usage: !unchecked [on|off]" >> loop state where setMode mode = do outputStrLn $ contractModeMessage mode loop state { replContracts = mode } reportContracts :: REPLState -> InputT IO () reportContracts state = outputStrLn $ contractModeMessage (replContracts state) handleEnv :: REPLState -> InputT IO () handleEnv state = case sort (Map.keys (replEnv state)) of [] -> outputStrLn "Environment is empty" names -> mapM_ outputStrLn names processInput :: REPLState -> String -> IO REPLState processInput state input = do let env' = evalTricu (replEnv state) (parseTricu input) writeIORef (replEnvRef state) env' putStrLn $ formatT (replForm state) (result env') return state { replEnv = env' } errorHandler :: REPLState -> SomeException -> IO REPLState errorHandler state e = do putStrLn $ "Error: " ++ displayException e return state completeRepl :: IORef Env -> CompletionFunc IO completeRepl envRef input@(left, _right) | commandWantsFile line = completeFilename input | "!" `isPrefixOf` line = completeWord Nothing " \t" completeCommands input | otherwise = completeWord Nothing termBreakChars completeTerms input where line = reverse left completeCommands str = return $ map simpleCompletion $ filter (str `isPrefixOf`) commands completeTerms str = do env <- readIORef envRef return $ map simpleCompletion $ filter (str `isPrefixOf`) (sort $ Map.keys env) commands = [ "!exit" , "!output" , "!format" , "!clear" , "!reset" , "!help" , "!load" , "!check" , "!store" , "!unchecked" , "!env" ] commandWantsFile inputLine = any (`isPrefixOf` inputLine) ["!load ", "!check "] termBreakChars = " \t\n\r()[]{}\"'" outputFormats :: [EvaluatedForm] outputFormats = [Decode, Tree, FSL, AST, Ternary, Ascii, Number, StringLit] readEvaluatedForm :: String -> Maybe EvaluatedForm readEvaluatedForm s = case s of "tree" -> Just Tree "fsl" -> Just FSL "ast" -> Just AST "ternary" -> Just Ternary "ascii" -> Just Ascii "decode" -> Just Decode "number" -> Just Number "string" -> Just StringLit _ -> Nothing contractModeMessage :: ContractMode -> String contractModeMessage EnforceContracts = "Contracts: on" contractModeMessage IgnoreContracts = "Contracts: off (unchecked eval)" storePathString :: StorePath -> FilePath storePathString (StorePath path) = path strip :: String -> String strip = f . f where f = reverse . dropWhile (`elem` [' ', '\t', '\n', '\r']) readMaybeInt :: String -> Maybe Int readMaybeInt s = case reads s of [(n, "")] -> Just n _ -> Nothing