Files
tricu/src/REPL.hs
James Eversole fdebb6c13d Tricu 2.0.0
Sorry for squashing all of this but 🤷
2026-05-25 12:44:24 -05:00

242 lines
9.2 KiB
Haskell

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