REPL.hs (10329B)
1 module REPL where 2 3 import Eval (evalTricu, result) 4 import FileEval 5 ( LoadedSource(..) 6 , defaultStorePath 7 , loadFileWithStore 8 ) 9 import Parser (parseTricu) 10 import Research (EvaluatedForm(..), Env, formatT) 11 import ContentStore 12 ( AliasKind(..) 13 , ObjectRef(..) 14 , StorePath(..) 15 , cachedFilesystemResolver 16 , getTreeTerm 17 , readAlias 18 , treeTermDomain 19 , unDomain 20 ) 21 import Module.Resolver (resolveModuleImport, resolvedModulesEnv) 22 23 import Control.Exception (SomeException, catch, displayException) 24 import Control.Monad.IO.Class (liftIO) 25 import Data.IORef (IORef, newIORef, readIORef, writeIORef) 26 import Data.List (isPrefixOf, sort) 27 import Data.Version (showVersion) 28 import Paths_tricu (version) 29 import System.Console.Haskeline 30 import System.Directory (doesFileExist) 31 32 import qualified Data.Map as Map 33 import qualified Data.Text as T 34 35 -- | Source-local REPL with the same filesystem CAS/module loader used by the 36 -- CLI. 37 data REPLState = REPLState 38 { replForm :: EvaluatedForm 39 , replEnv :: Env 40 , replStore :: StorePath 41 , replEnvRef :: IORef Env 42 } 43 44 repl :: IO () 45 repl = defaultStorePath >>= replWithStore 46 47 replWithStore :: StorePath -> IO () 48 replWithStore store = do 49 envRef <- newIORef Map.empty 50 let settings = Settings 51 { complete = completeRepl envRef 52 , historyFile = Just "~/.local/state/tricu/history" 53 , autoAddHistory = True 54 } 55 runInputT settings (loop (REPLState Decode Map.empty store envRef)) 56 where 57 58 loop :: REPLState -> InputT IO () 59 loop state = do 60 minput <- getInputLine "tricu < " 61 case minput of 62 Nothing -> return () 63 Just raw -> do 64 let s = strip raw 65 case s of 66 "" -> loop state 67 "!exit" -> outputStrLn "Exiting tricu" 68 "!clear" -> liftIO (putStr "\ESC[2J\ESC[H") >> loop state 69 "!reset" -> do 70 liftIO $ writeIORef (replEnvRef state) Map.empty 71 outputStrLn "Environment reset" 72 loop state { replEnv = Map.empty } 73 "!help" -> printHelp >> loop state 74 "!output" -> handleOutput state 75 "!env" -> handleEnv state >> loop state 76 _ | "!load" `isPrefixOf` s -> handleLoad state (strip $ drop 5 s) 77 | "!use" `isPrefixOf` s -> handleUse state (strip $ drop 4 s) 78 | "!name" `isPrefixOf` s -> handleName state (strip $ drop 5 s) 79 | "!store" `isPrefixOf` s -> handleStore state (strip $ drop 6 s) 80 | "!format" `isPrefixOf` s -> handleFormat state (strip $ drop 7 s) 81 | take 2 s == "--" -> loop state 82 | otherwise -> do 83 next <- liftIO $ catch (processInput state raw) (errorHandler state) 84 loop next 85 86 printHelp :: InputT IO () 87 printHelp = do 88 outputStrLn $ "tricu version " ++ showVersion version 89 outputStrLn "Available commands:" 90 outputStrLn " !exit - Exit the REPL" 91 outputStrLn " !clear - Clear the screen" 92 outputStrLn " !reset - Reset the in-memory environment" 93 outputStrLn " !help - Show this help" 94 outputStrLn " !output - Change output format interactively" 95 outputStrLn " !format FORM - Set output format: tree, fsl, ast, ternary, ascii, decode, number, string" 96 outputStrLn " !load FILE - Load and evaluate a .tri file into the environment" 97 outputStrLn " !use MODULE [NS] - Load a module alias/manifest from the store (NS defaults to !Local)" 98 outputStrLn " !name NAME [LOCAL] - Load a name alias/tree-term hash from the store" 99 outputStrLn " !store [PATH] - Show or set the content-addressed store path" 100 outputStrLn " !env - List names currently in the REPL environment" 101 102 handleOutput :: REPLState -> InputT IO () 103 handleOutput state = do 104 let formats = outputFormats 105 outputStrLn "Available output formats:" 106 mapM_ (\(i, f) -> outputStrLn $ show (i :: Int) ++ ". " ++ show f) 107 (zip [1..] formats) 108 input <- getInputLine "Select output format (1-8) < " 109 case input >>= readMaybeInt of 110 Just n | n >= 1 && n <= length formats -> do 111 let newForm = formats !! (n - 1) 112 outputStrLn $ "Output format changed to: " ++ show newForm 113 loop state { replForm = newForm } 114 _ -> outputStrLn "Invalid selection. Keeping current output format." >> loop state 115 116 handleFormat :: REPLState -> String -> InputT IO () 117 handleFormat state arg = 118 case readEvaluatedForm arg of 119 Just form -> outputStrLn ("Output format changed to: " ++ show form) >> loop state { replForm = form } 120 Nothing -> outputStrLn "Usage: !format tree|fsl|ast|ternary|ascii|decode|number|string" >> loop state 121 122 handleLoad :: REPLState -> String -> InputT IO () 123 handleLoad state path 124 | null path = outputStrLn "Usage: !load FILE" >> loop state 125 | otherwise = do 126 exists <- liftIO $ doesFileExist path 127 if not exists 128 then outputStrLn ("File not found: " ++ path) >> loop state 129 else do 130 loaded <- liftIO $ loadFileWithStore (replStore state) path 131 let env' = evalTricu (Map.union (loadedImports loaded) (replEnv state)) (loadedAst loaded) 132 liftIO $ writeIORef (replEnvRef state) env' 133 outputStrLn $ "Loaded " ++ path 134 loop state { replEnv = env' } 135 136 handleUse :: REPLState -> String -> InputT IO () 137 handleUse state arg = case words arg of 138 [] -> outputStrLn "Usage: !use MODULE [NAMESPACE]" >> loop state 139 [moduleTarget] -> loadModule moduleTarget "!Local" 140 [moduleTarget, namespace] -> loadModule moduleTarget namespace 141 _ -> outputStrLn "Usage: !use MODULE [NAMESPACE]" >> loop state 142 where 143 loadModule moduleTarget namespace = do 144 resolver <- liftIO $ cachedFilesystemResolver (replStore state) 145 resolved <- liftIO $ resolveModuleImport resolver moduleTarget namespace 146 let importedEnv = resolvedModulesEnv [resolved] 147 env' = Map.union importedEnv (replEnv state) 148 liftIO $ writeIORef (replEnvRef state) env' 149 outputStrLn $ "Loaded " ++ show (Map.size importedEnv) ++ " export(s) from store module " ++ moduleTarget 150 loop state { replEnv = env' } 151 152 handleName :: REPLState -> String -> InputT IO () 153 handleName state arg = case words arg of 154 [] -> outputStrLn "Usage: !name NAME [LOCAL]" >> loop state 155 [name] -> loadName name name 156 [name, localName] -> loadName name localName 157 _ -> outputStrLn "Usage: !name NAME [LOCAL]" >> loop state 158 where 159 loadName name localName = do 160 let store = replStore state 161 nameText = T.pack name 162 mAlias <- liftIO $ readAlias store NameAlias nameText 163 let root = maybe nameText objectRefHash mAlias 164 badKind = case mAlias of 165 Just ref -> objectRefKind ref /= unDomain treeTermDomain 166 Nothing -> False 167 if badKind 168 then outputStrLn ("Name alias does not point at a tree term: " ++ name) >> loop state 169 else do 170 mTerm <- liftIO $ getTreeTerm store root 171 case mTerm of 172 Nothing -> outputStrLn ("Tree term not found in store: " ++ name) >> loop state 173 Just term -> do 174 let env' = Map.insert localName term (replEnv state) 175 liftIO $ writeIORef (replEnvRef state) env' 176 outputStrLn $ "Loaded " ++ name ++ " as " ++ localName 177 loop state { replEnv = env' } 178 179 handleStore :: REPLState -> String -> InputT IO () 180 handleStore state path 181 | null path = do 182 outputStrLn $ "Store: " ++ storePathString (replStore state) 183 loop state 184 | otherwise = do 185 outputStrLn $ "Store changed to: " ++ path 186 loop state { replStore = StorePath path } 187 188 handleEnv :: REPLState -> InputT IO () 189 handleEnv state = 190 case sort (Map.keys (replEnv state)) of 191 [] -> outputStrLn "Environment is empty" 192 names -> mapM_ outputStrLn names 193 194 processInput :: REPLState -> String -> IO REPLState 195 processInput state input = do 196 let env' = evalTricu (replEnv state) (parseTricu input) 197 writeIORef (replEnvRef state) env' 198 putStrLn $ formatT (replForm state) (result env') 199 return state { replEnv = env' } 200 201 errorHandler :: REPLState -> SomeException -> IO REPLState 202 errorHandler state e = do 203 putStrLn $ "Error: " ++ displayException e 204 return state 205 206 completeRepl :: IORef Env -> CompletionFunc IO 207 completeRepl envRef input@(left, _right) 208 | commandWantsFile line = completeFilename input 209 | "!" `isPrefixOf` line = completeWord Nothing " \t" completeCommands input 210 | otherwise = completeWord Nothing termBreakChars completeTerms input 211 where 212 line = reverse left 213 completeCommands str = return $ map simpleCompletion $ 214 filter (str `isPrefixOf`) commands 215 completeTerms str = do 216 env <- readIORef envRef 217 return $ map simpleCompletion $ 218 filter (str `isPrefixOf`) (sort $ Map.keys env) 219 commands = 220 [ "!exit" 221 , "!output" 222 , "!format" 223 , "!clear" 224 , "!reset" 225 , "!help" 226 , "!load" 227 , "!use" 228 , "!name" 229 , "!store" 230 , "!env" 231 ] 232 commandWantsFile inputLine = "!load " `isPrefixOf` inputLine 233 termBreakChars = " \t\n\r()[]{}\"'" 234 235 outputFormats :: [EvaluatedForm] 236 outputFormats = [Decode, Tree, FSL, AST, Ternary, Ascii, Number, StringLit] 237 238 readEvaluatedForm :: String -> Maybe EvaluatedForm 239 readEvaluatedForm s = case s of 240 "tree" -> Just Tree 241 "fsl" -> Just FSL 242 "ast" -> Just AST 243 "ternary" -> Just Ternary 244 "ascii" -> Just Ascii 245 "decode" -> Just Decode 246 "number" -> Just Number 247 "string" -> Just StringLit 248 _ -> Nothing 249 250 storePathString :: StorePath -> FilePath 251 storePathString (StorePath path) = path 252 253 strip :: String -> String 254 strip = f . f 255 where f = reverse . dropWhile (`elem` [' ', '\t', '\n', '\r']) 256 257 readMaybeInt :: String -> Maybe Int 258 readMaybeInt s = case reads s of 259 [(n, "")] -> Just n 260 _ -> Nothing