module REPL (REPL, runREPL, emptyREPLState) where import qualified Lexer as L import qualified SourceParser as SP import qualified DesugarSI as DS import PureSyntax import InterSyntax import SugarSyntax import PureInterpreter (evalExpr, Store) import Prelude hiding (break) import Text.Read (readMaybe) import Data.Either (lefts, either) import Data.List (isPrefixOf, intersperse, intercalate) import qualified Data.Map as M import qualified Data.Set as S import Control.Monad.State.Strict import Control.Monad.Except import qualified System.Console.Haskeline as HL import qualified System.Console.Repline as RL -------------------------------------------------------------------------------- -- Types -------------------------------------------------------------------------------- type REPL a = RL.HaskelineT (StateT REPLState IO) a type Breakpoint = (FilePath, Int) data DebugOp = WhileRead Name ETree | WhileWrite Name | Message String deriving (Show, Eq, Ord) namesDebugOp :: DebugOp -> S.Set Name namesDebugOp dbo = case dbo of WhileRead n _ -> S.singleton n WhileWrite n -> S.singleton n _ -> S.empty type REPLState = ( Store -- The store for interactive execution , [Either InCommand DebugOp] -- A loaded program that can be executed. -- This is a list of either a while command -- to execute, or a debugger-level -- instruction to carry out. , S.Set Breakpoint -- Breakpoints that we pause at , ETree -> String -- Tree printing function , FilePath -- Current file search path ) emptyREPLState :: REPLState emptyREPLState = (M.empty, [], S.empty, show, ".") -------------------------------------------------------------------------------- -- Repline bits and pieces -------------------------------------------------------------------------------- runREPL :: StateT REPLState IO () runREPL = RL.evalRepl "HWhile> " execute options (RL.Word0 completer) initialise execute :: String -> REPL () execute str = do printFn <- getPrintFn path <- getPath expTry <- parseExpr str case expTry of Left err -> do suCommTry <- parseComm str case suCommTry of Left err -> replPutStrLn err Right suComm -> do commTry <- desugarComm path suComm case commTry of Left err -> replPutStrLn err Right comm -> do store <- getStore putStore $ evalInBlock store comm Right exp -> do store <- getStore replPutStrLn $ printFn $ evalExpr store exp initialise :: REPL () initialise = replPutStrLn welcomeString options :: [(String, [String] -> REPL ())] options = [ ("help" , help ) , ("load" , load ) , ("run" , run ) , ("printmode", printmode) , ("cd" , cd ) , ("store" , store ) , ("step" , step ) , ("break" , break ) , ("delbreak" , delbreak ) ] help :: [String] -> REPL () help _ = liftIO $ putStrLn helpString load :: [String] -> REPL () load args = if length args <= 1 then replPutStrLn $ "Please supply a single while program name, and an " ++ "argument literal (e.g. )." else do path <- getPath progTry <- loadProg path (head args) argTry <- parseLVal (intercalate " " (tail args)) case (progTry, argTry) of (Left err1, Left err2) -> do replPutStrLn err1 ; replPutStrLn err2 (_ , Left err ) -> replPutStrLn err (Left err , _ ) -> replPutStrLn err (Right prog, Right arg ) -> do clearStore replPutStrLn $ "Program '" ++ (head args) ++ "' loaded." case prog of InProgram _ rd blk wr -> putProg $ [Right (WhileRead rd arg)] ++ map Left blk ++ [Right (WhileWrite wr)] run :: [String] -> REPL () run args = if length args /= 0 then replPutStrLn $ "Error: did not expect '" ++ intercalate " " args ++ "'." else do store <- getStore bps <- getBreakpoints blk <- getProg printFn <- getPrintFn let (store', blk', msg) = runToBreakpoint store bps blk printFn putStore store' putProg blk' replPutStrLn msg printmode :: [String] -> REPL () printmode args = if length args /= 1 then replPutStrLn "Please supply a single print mode string." else case head args of "i" -> putPrintFn $ \tree -> maybe "E" show $ parseInt tree "iv" -> putPrintFn $ \tree -> maybe (show tree) show $ parseInt tree "l" -> putPrintFn $ show . toHaskellList "li" -> putPrintFn $ showIntListTree False "liv" -> putPrintFn $ showIntListTree True "L" -> putPrintFn $ showNestedIntListTree "La" -> putPrintFn $ showNestedAtomIntListTree _ -> replPutStrLn $ "Error - valid modes are i, iv, l, li, liv, " ++ "L, and La. Quit interactive mode and then run 'hwhile -h' for " ++ "more information." cd :: [String] -> REPL () cd args = if length args /= 1 then replPutStrLn "Please supply a single directory path." else putPath (head args) store :: [String] -> REPL () store args = if length args /= 0 then replPutStrLn $ "Error: did not expect '" ++ intercalate " " args ++ "'." else do store <- getStore printFn <- getPrintFn let output = intercalate "\n" $ map (\(Name (fp, n), tree) -> "(" ++ fp ++ ") " ++ n ++ " := " ++ printFn tree) $ M.assocs store replPutStrLn output step :: [String] -> REPL () step args = if length args /= 0 then replPutStrLn $ "Error: did not expect '" ++ intercalate " " args ++ "'." else do store <- getStore blk <- getProg printFn <- getPrintFn let (store', blk', msg) = doStep store blk printFn putStore store' putProg blk' replPutStrLn msg break :: [String] -> REPL () break [lineStr] = do fp <- getCurrentFilePath ; doBreak fp (readMaybe lineStr) break [lineStr, fp] = doBreak fp (readMaybe lineStr) break _ = replPutStrLn $ "Please supply a line number and optionally a single filename." doBreak :: String -> Maybe Int -> REPL() doBreak fp maybeLine = if fp == "" then replPutStrLn $ "Cannot set breakpoint as no program is loaded. Load one with " ++ "':load' and try again." else case maybeLine of Nothing -> replPutStrLn "Please supply a line number." Just n -> do putBreakpoint (fp, n) replPutStrLn $ "Breakpoint set in program " ++ fp ++ " at line " ++ show n ++ "." delbreak :: [String] -> REPL () delbreak [lineStr] = do fp <- getCurrentFilePath doDelBreak fp (readMaybe lineStr) delbreak [lineStr, fp] = doDelBreak fp (readMaybe lineStr) delbreak _ = replPutStrLn $ "Please supply a line number and optionally a single filename." doDelBreak :: String -> Maybe Int -> REPL() doDelBreak fp maybeLine = if fp == "" then replPutStrLn "Cannot delete breakpoint as no program is loaded." else case maybeLine of Nothing -> replPutStrLn "Please supply a line number." Just n -> do delBreakpoint (fp, n) replPutStrLn $ "Breakpoint removed from program " ++ fp ++ " at line " ++ show n ++ "." completer :: Monad m => RL.WordCompleter m completer str = do let completionWords = [ "nil" , "cons" , "hd" , "tl" , "while" , "switch" , "case" , "default" , "if" , "else" , "true" , "false" , "@:=" , "@asgn" , "@doAsgn" , "@while" , "@doWhile" , "@if" , "@doIf" , "@var" , "@quote" , "@hd" , "@doHd" , "@tl" , "@doTl" , "@cons" , "@doCons" ] ++ map ((':' :) . fst) options return $ filter (str `isPrefixOf`) completionWords replPutStrLn :: String -> REPL () replPutStrLn = lift . lift . putStrLn -------------------------------------------------------------------------------- -- InProgram/InCommand/Inblock evaluators for interactivity -------------------------------------------------------------------------------- runToBreakpoint :: Store -> S.Set Breakpoint -> [Either InCommand DebugOp] -> (ETree -> String) -> (Store, [Either InCommand DebugOp], String) runToBreakpoint store bps blk printFn = let res@(store', blk', msg) = doStep store blk printFn in case blk' of [] -> res Right _ : _ -> runToBreakpoint store' bps blk' printFn Left inComm : _ -> case info inComm of Info i@(fp, line) -> if S.member i bps then (store', blk', "Stopping at line " ++ show line ++ " of program " ++ fp ++ ".") else runToBreakpoint store' bps blk' printFn doStep :: Store -> [Either InCommand DebugOp] -> (ETree -> String) -> (Store, [Either InCommand DebugOp], String) doStep store [] printFn = (store, [], "No program loaded.") doStep store (comm : rest) printFn = case comm of Left (InAssign _ n e) -> let evalE = evalExpr store e in (M.insert n evalE store, rest, show n ++ " := " ++ printFn evalE) Left (InWhile _ gd blk) -> case evalExpr store gd of ENil -> (store, rest, "Skipped or exited while-loop.") _ -> (store, (map Left blk) ++ (comm : rest), "while " ++ show gd ++ " { ...\nEntered or re-entered while-loop.") Left (InIfElse _ gd tb fb) -> case evalExpr store gd of ENil -> (store, (map Left fb) ++ rest, "if " ++ show gd ++ " { ...\nTook if-branch.") _ -> (store, (map Left tb) ++ rest, "if " ++ show gd ++ " { ...\nTook else-branch or skipped.") Left (InSwitch _ gd [] def) -> (store, (map Left def) ++ rest, "switch " ++ show gd ++ " { ... \nTook default case.") Left (InSwitch i gd ((e, blk) : cases) def) -> if evalExpr store gd == evalExpr store e then (store, (map Left blk) ++ rest, "switch " ++ show gd ++ " { ...\n case " ++ show e ++ ": ...\nTook switch-case.") else (store, Left (InSwitch i gd cases def) : rest, "switch " ++ show gd ++ " { ...\n case " ++ show e ++ ": ...\nSkipped switch-case.") Right (WhileRead n arg) -> (M.insert n arg store, rest, "read " ++ show n ++ " := " ++ printFn arg) Right (WhileWrite n) -> (store, rest, "wrote " ++ show n ++ " := " ++ (printFn (M.findWithDefault ENil n store))) Right (Message m) -> (store, rest, m) evalInCommand :: Store -> InCommand -> Store evalInCommand store comm = case comm of InAssign _ n e -> M.insert n (evalExpr store e) store InWhile _ gd blk -> case evalExpr store gd of ENil -> store _ -> evalInCommand (evalInBlock store blk) comm InIfElse _ gd tb fb -> case evalExpr store gd of ENil -> evalInBlock store fb _ -> evalInBlock store tb InSwitch _ gd [] def -> evalInBlock store def InSwitch i gd ((e, blk) : cases) def -> if evalExpr store gd == evalExpr store e then evalInBlock store blk else evalInCommand store (InSwitch i gd cases def) evalInBlock :: Store -> InBlock -> Store evalInBlock = foldl evalInCommand -------------------------------------------------------------------------------- -- REPLState monadic getters and setters -------------------------------------------------------------------------------- getStore :: REPL Store getStore = do (store, _, _, _, _) <- lift get ; return store putStore :: Store -> REPL () putStore st = lift $ modify $ \(_, p, bps, pf, fp) -> (st, p, bps, pf, fp) clearStore :: REPL () clearStore = putStore M.empty getFromStore :: Name -> REPL (Maybe ETree) getFromStore name = do store <- getStore ; return $ M.lookup name store putInStore :: Name -> ETree -> REPL () putInStore n t = do store <- getStore ; putStore $ M.insert n t store getProg :: REPL [Either InCommand DebugOp] getProg = do (_, p, _, _, _) <- lift get ; return p putProg :: [Either InCommand DebugOp] -> REPL () putProg blk = lift $ modify $ \(st, _, bps, pf, fp) -> (st, blk, bps, pf, fp) getCurrentFilePath :: REPL FilePath getCurrentFilePath = do blk <- getProg case lefts blk of [] -> return "" cs -> return $ case info (head cs) of Info (fp, line) -> fp getBreakpoints :: REPL (S.Set Breakpoint) getBreakpoints = do (_, _, bps, _, _) <- lift get ; return bps putBreakpoints :: S.Set Breakpoint -> REPL () putBreakpoints bps = lift $ modify $ \(st, mp, _, pf, fp) -> (st, mp, bps, pf, fp) putBreakpoint :: Breakpoint -> REPL () putBreakpoint bp = do bps <- getBreakpoints ; putBreakpoints $ S.insert bp bps delBreakpoint :: Breakpoint -> REPL () delBreakpoint bp = do bps <- getBreakpoints ; putBreakpoints $ S.delete bp bps isBreakpoint :: Breakpoint -> REPL Bool isBreakpoint bp = do bps <- getBreakpoints ; return $ S.member bp bps getPrintFn :: REPL (ETree -> String) getPrintFn = do (_, _, _, pf, _) <- lift get ; return pf putPrintFn :: (ETree -> String) -> REPL () putPrintFn pf = lift $ modify $ \(st, mp, bps, _, fp) -> (st, mp, bps, pf, fp) getPath :: REPL FilePath getPath = do (_, _, _, _, fp) <- lift get ; return fp putPath :: String -> REPL () putPath fp = lift $ modify $ \(st, mp, bps, pf, _) -> (st, mp, bps, pf, fp) -------------------------------------------------------------------------------- -- Parser helpers in the REPL monad -------------------------------------------------------------------------------- parseExpr :: String -> REPL (Either String Expression) parseExpr str = do namePath <- getCurrentFilePath lift . lift . runExceptT $ SP.parseExpr (L.scan str namePath) parseComm :: String -> REPL (Either String SuCommand) parseComm str = do namePath <- getCurrentFilePath lift . lift . runExceptT $ SP.parseComm (L.scan str namePath) parseLVal :: String -> REPL (Either String ETree) parseLVal str = do namePath <- getCurrentFilePath lift . lift . runExceptT $ SP.parseLVal (L.scan str namePath) desugarComm :: FilePath {- Macro search path -} -> SuCommand -> REPL (Either String InBlock) desugarComm path = lift. lift . runExceptT . DS.desugarComm path [] loadProg :: FilePath -> -- Search path FilePath -> -- Program name REPL (Either String InProgram) loadProg path progName = lift . lift . runExceptT $ DS.loadProg path progName [] -------------------------------------------------------------------------------- -- Some printable contents -------------------------------------------------------------------------------- helpString :: String helpString = concat $ (intersperse "\n") $ [ "HWhile interactive mode. Possible options:" , " - Evaluate a while expression." , " - Execute a while command." , " :help - Print this message." , " :load p - Load a while program 'p' (i.e. from the file " , " 'p.while') for execution with argument . Note" , " that this clears the current store contents." , " :run - Run the loaded program up until the next " , " breakpoint." , " :step - Step through a single line of the loaded program." , " :store - Print the current store contents." , " :printmode m - Set the print mode to mode 'm'. Valid modes are i, " , " iv, l, li, liv, L, and La. Quit interactive mode" , " and then run 'hwhile -h' for more info on print " , " modes." , " :cd dir - Change the current file search path to 'dir'." , " :break n - Add a breakpoint to line 'n' of the loaded program." , " :break n p - Add a breakpoint to line 'n' of program 'p'." , " :delbreak n - Delete the breakpoint on line 'n' of the loaded" , " program." , " :delbreak n p - Delete the breakpoint on line 'n' of program 'p'." , " (Ctrl+D) - Quit interactive mode." ] welcomeString :: String welcomeString = "Welcome to HWhile interactive mode. Type ':help' for more " ++ "information."