tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

REPL.hs (10041B)


      1 -- Interactive input is assembled into native modules, never parsed by the
      2 -- recovery frontend. Each successful submission is a transaction on source state.
      3 module REPL where
      4 
      5 import Runtime (EvaluatedForm(..), Env, T, formatT)
      6 import ContentStore (StorePath(..), AliasKind(..), ObjectRef(..), readAlias, unDomain)
      7 import Module.Manifest (manifestDomain)
      8 import Control.Exception (SomeException, AsyncException, catch, displayException, evaluate, fromException, throwIO)
      9 import Control.Monad (unless)
     10 import Control.Monad.IO.Class (liftIO)
     11 import Data.IORef (IORef, newIORef, readIORef, writeIORef)
     12 import Data.List (isPrefixOf, sort)
     13 import Data.Char (isAlpha, isAlphaNum)
     14 import System.Console.Haskeline
     15 import qualified Data.Map as Map
     16 import qualified Data.Text as Text
     17 
     18 -- Replacing a definition replaces its whole equation group. Other definitions
     19 -- are rebuilt against the new source, rather than retaining obsolete closures.
     20 data Session = Session
     21   { sessionImports :: [String]
     22   , sessionDefinitions :: [(String, String)]
     23   , sessionForm :: EvaluatedForm
     24   , sessionStore :: StorePath
     25   }
     26 
     27 -- Classification is a convenience only; syntax/namespace validation belongs to
     28 -- the portable compiler. !def NAME blocks support every equation/layout form;
     29 -- !expr blocks remove any ambiguity for expressions containing '='.
     30 definitionName :: String -> Maybe String
     31 definitionName input = case words input of
     32   name : _ | valid name && name `notElem` ["let", "case", "do", "t"]
     33              && definitionEquals input -> Just name
     34   _ -> Nothing
     35   where
     36     valid [] = False
     37     valid (c:cs) = (isAlpha c || c == '_') && all (\x -> isAlphaNum x || x `elem` ("_?'-" :: String)) cs
     38 
     39 -- Ignore quoted '=', comments and nested expressions when selecting the
     40 -- convenience definition route. Ambiguous advanced heads use !def explicitly.
     41 definitionEquals :: String -> Bool
     42 definitionEquals = scan (0 :: Int) False
     43   where
     44     scan _ _ [] = False
     45     scan depth True ('\\':_:rest) = scan depth True rest
     46     scan depth quoted ('"':rest) = scan depth (not quoted) rest
     47     scan depth True (_:rest) = scan depth True rest
     48     scan _ False ('-':'-':_) = False
     49     scan depth False (c:rest)
     50       | c `elem` ("([{" :: String) = scan (depth + 1) False rest
     51       | c `elem` (")]}" :: String) && c /= ' ' = scan (depth - 1) False rest
     52       | c == '=' && depth == 0 = True
     53       | otherwise = scan depth False rest
     54 
     55 sessionSource :: Session -> String -> String
     56 sessionSource state expression = unlines (sessionImports state) ++
     57   "!export *\n" ++ concatMap ((++ "\n") . snd) (sessionDefinitions state) ++
     58   "replResult = (" ++ expression ++ ")\n"
     59 
     60 portableRepl :: StorePath -> (StorePath -> String -> IO [(String, T)])
     61              -> (StorePath -> FilePath -> IO Text.Text) -> IO ()
     62 portableRepl store compile load = do
     63   ref <- newIORef Map.empty
     64   let settings = Settings (completeRepl ref) Nothing True
     65   runInputT settings (loop ref (Session [] [] Decode store))
     66   where
     67     loop ref state = do
     68       input <- getInputLine "tricu < "
     69       case fmap strip input of
     70         Nothing -> pure ()
     71         Just "!exit" -> pure ()
     72         Just "" -> loop ref state
     73         Just "!clear" -> outputStr "\ESC[2J\ESC[H" >> loop ref state
     74         Just "!help" -> outputStrLn help >> loop ref state
     75         Just "!env" -> do
     76           env <- liftIO $ readIORef ref
     77           mapM_ outputStrLn (Map.keys env)
     78           loop ref state
     79         Just "!reset" -> do
     80           liftIO $ writeIORef ref Map.empty
     81           loop ref state { sessionImports = [], sessionDefinitions = [] }
     82         Just "!output" -> do
     83           outputStrLn "tree|fsl|ast|ternary|ascii|decode|number|string"
     84           choice <- getInputLine "format < "
     85           setFormat ref state (maybe "" id choice)
     86         Just line | "!format " `isPrefixOf` line -> setFormat ref state (drop 8 line)
     87                   | line == "!store" -> outputStrLn (unStorePath $ sessionStore state) >> loop ref state
     88                   | "!store " `isPrefixOf` line -> loop ref state { sessionStore = StorePath (drop 7 line) }
     89                   | "!def " `isPrefixOf` line -> do
     90                       body <- block
     91                       submit ref state (Just $ strip $ drop 5 line) body
     92                   | line == "!expr" -> block >>= submit ref state Nothing
     93                   | otherwise -> submit ref state (definitionName line) line
     94 
     95     block = do
     96       outputStrLn "Enter source; !end submits (EOF cancels)."
     97       gather []
     98       where
     99         gather rows = do
    100           next <- getInputLine "... "
    101           case next of
    102             Nothing -> pure ""
    103             Just "!end" -> pure $ unlines $ reverse rows
    104             Just row -> gather (row:rows)
    105 
    106     setFormat ref state value = case readEvaluatedForm value of
    107       Nothing -> outputStrLn "Unknown output format" >> loop ref state
    108       Just form -> loop ref state { sessionForm = form }
    109 
    110     submit ref state named input = do
    111       next <- liftIO $ transaction ref state named input `catch` \(err :: SomeException) ->
    112         case fromException err :: Maybe AsyncException of
    113           Just asynchronous -> throwIO asynchronous
    114           Nothing -> putStrLn ("Error: " ++ displayException err) >> pure state
    115       loop ref next
    116 
    117     transaction ref state (Just name) input
    118       | null input = pure state -- EOF cancels a multiline block.
    119       | name == "replResult" = ioError $ userError "replResult is reserved by the session"
    120       | otherwise = commit ref state { sessionDefinitions =
    121           filter ((/= name) . fst) (sessionDefinitions state) ++ [(name,input)] } "t" False
    122     transaction ref state Nothing input
    123       | null input || "--" `isPrefixOf` input = pure state
    124       | "!load " `isPrefixOf` input = do
    125           pin <- load (sessionStore state) (strip $ drop 6 input)
    126           commit ref state { sessionImports = sessionImports state ++ ["!import " ++ Text.unpack pin ++ " *"] } "t" False
    127       | "!use " `isPrefixOf` input = case words (drop 5 input) of
    128           [pin] -> useModule pin " *"
    129           [pin, namespace] -> useModule pin (" as " ++ namespace)
    130           _ -> ioError $ userError "Usage: !use MANIFEST [NAMESPACE]"
    131       | "!import " `isPrefixOf` input = addImport input
    132       | "!" `isPrefixOf` input = ioError $ userError "Unknown command; see !help"
    133       | otherwise = commit ref state input True
    134       where
    135         useModule target suffix = do
    136           alias <- readAlias (sessionStore state) ModuleAlias (Text.pack target)
    137           pin <- case alias of
    138             Nothing -> pure target
    139             Just value | objectRefKind value == unDomain manifestDomain -> pure (Text.unpack $ objectRefHash value)
    140             _ -> ioError $ userError "Module alias has wrong object kind"
    141           addImport ("!import " ++ pin ++ suffix)
    142         addImport line = commit ref state { sessionImports = sessionImports state ++ [line] } "t" False
    143 
    144     commit ref candidate expression display = do
    145       exports <- compile (sessionStore candidate) (sessionSource candidate expression)
    146       -- The portable compiler's complete local interface is authoritative.
    147       -- A named block must replace exactly its group, not silently add/remove
    148       -- other names. Validate before publishing session/completion state.
    149       let expected = sort ("replResult" : map fst (sessionDefinitions candidate))
    150           actual = sort (map fst exports)
    151       unless (actual == expected) $ ioError $ userError
    152         "Definition block must define exactly its named equation group"
    153       let env = Map.fromList exports
    154       value <- maybe (ioError $ userError "Portable session result missing") pure (Map.lookup "replResult" env)
    155       if display then do
    156         let rendered = formatT (sessionForm candidate) value
    157         _ <- evaluate (length rendered)
    158         putStrLn rendered
    159         else pure ()
    160       writeIORef ref (Map.delete "replResult" env)
    161       pure candidate
    162 
    163     help = unlines
    164       [ "Definitions replace the previous equation group; expressions do not persist."
    165       , "!def NAME / !expr: multiline input through !end; !import: native import"
    166       , "!load FILE: compile and open its exports; !use MANIFEST [NS]: pinned import"
    167       , "!env !reset !store [PATH] !format FORM !output !clear !exit"
    168       , "Imports resolve from the current directory; no implicit prelude."
    169       , "Failed submissions preserve successful session source and completion state."
    170       ]
    171 
    172 completeRepl :: IORef Env -> CompletionFunc IO
    173 completeRepl envRef input@(left, _right)
    174   | commandWantsFile line = completeFilename input
    175   | "!" `isPrefixOf` line = completeWord Nothing " \t" completeCommands input
    176   | otherwise = completeWord Nothing termBreakChars completeTerms input
    177   where
    178     line = reverse left
    179     completeCommands str = return $ map simpleCompletion $
    180       filter (str `isPrefixOf`) commands
    181     completeTerms str = do
    182       env <- readIORef envRef
    183       return $ map simpleCompletion $
    184         filter (str `isPrefixOf`) (sort $ Map.keys env)
    185     commands =
    186       [ "!exit"
    187       , "!output"
    188       , "!format"
    189       , "!clear"
    190       , "!reset"
    191       , "!help"
    192       , "!load"
    193       , "!use"
    194       , "!store"
    195       , "!env"
    196       , "!def"
    197       , "!expr"
    198       , "!import"
    199       ]
    200     commandWantsFile inputLine = "!load " `isPrefixOf` inputLine
    201     termBreakChars = " \t\n\r()[]{}\"'"
    202 
    203 outputFormats :: [EvaluatedForm]
    204 outputFormats = [Decode, Tree, FSL, AST, Ternary, Ascii, Number, StringLit]
    205 
    206 readEvaluatedForm :: String -> Maybe EvaluatedForm
    207 readEvaluatedForm s = case s of
    208   "tree"    -> Just Tree
    209   "fsl"     -> Just FSL
    210   "ast"     -> Just AST
    211   "ternary" -> Just Ternary
    212   "ascii"   -> Just Ascii
    213   "decode"  -> Just Decode
    214   "number"  -> Just Number
    215   "string"  -> Just StringLit
    216   _         -> Nothing
    217 
    218 storePathString :: StorePath -> FilePath
    219 storePathString (StorePath path) = path
    220 
    221 strip :: String -> String
    222 strip = f . f
    223   where f = reverse . dropWhile (`elem` [' ', '\t', '\n', '\r'])
    224 
    225 readMaybeInt :: String -> Maybe Int
    226 readMaybeInt s = case reads s of
    227   [(n, "")] -> Just n
    228   _ -> Nothing