doctest-0.5.0: src/Interpreter.hs
module Interpreter (
Interpreter
, eval
, withInterpreter
) where
import System.IO
import System.Process
import System.Exit
import System.Directory (getPermissions, executable)
import Control.Monad(when)
import Control.Exception (bracket)
import Data.Char
import Data.List
import GHC.Paths (ghc)
-- | Truly random marker, used to separate expressions.
--
-- IMPORTANT: This module relies upon the fact that this marker is unique. It
-- has been obtained from random.org. Do not expect this module to work
-- properly, if you reuse it for any purpose!
marker :: String
marker = show "dcbd2a1e20ae519a1c7714df2859f1890581d57fac96ba3f499412b2f5c928a1"
data Interpreter = Interpreter {
hIn :: Handle
, hOut :: Handle
, process :: ProcessHandle
}
newInterpreter :: [String] -> IO Interpreter
newInterpreter flags = do
-- in a perfect world this permission check should never fail, but I know of
-- at least one case where it did..
x <- getPermissions ghc
when (not $ executable x) $ do
fail $ ghc ++ " is not executable!"
(Just stdin_, Just stdout_, Nothing, processHandle ) <- createProcess $ (proc ghc myFlags) {std_in = CreatePipe, std_out = CreatePipe, std_err = UseHandle stdout}
setMode stdin_
setMode stdout_
return Interpreter {hIn = stdin_, hOut = stdout_, process = processHandle}
where
myFlags = ["-v0", "--interactive", "-ignore-dot-ghci"] ++ flags
setMode handle = do
hSetBinaryMode handle False
hSetBuffering handle LineBuffering
hSetEncoding handle utf8
-- | Run an interpreter session.
--
-- Example:
--
-- >>> withInterpreter [] $ \i -> eval i "23 + 42"
-- "65\n"
withInterpreter
:: [String] -- ^ List of flags, passed to GHC
-> (Interpreter -> IO a) -- ^ Action to run
-> IO a -- ^ Result of action
withInterpreter flags = bracket (newInterpreter flags) closeInterpreter
closeInterpreter :: Interpreter -> IO ()
closeInterpreter repl = do
hClose $ hIn repl
-- It is crucial not to close `hOut` before calling `waitForProcess`,
-- otherwise ghci may not cleanly terminate on SIGINT (ctrl-c) and hang
-- around consuming 100% CPU. This happens when ghci tries to print
-- something to stdout in its signal handler (e.g. when it is blocked in
-- threadDelay it writes "Interrupted." on SIGINT).
e <- waitForProcess $ process repl
hClose $ hOut repl
when (e /= ExitSuccess) $ error $ "Interpreter exited with an error: " ++ show e
return ()
putExpression :: Interpreter -> String -> IO ()
putExpression repl e = do
hPutStrLn stdin_ $ filterExpression e
hPutStrLn stdin_ marker
hFlush stdin_
return ()
where
stdin_ = hIn repl
-- | Fail on unterminated multiline commands.
--
-- Examples:
--
-- >>> filterExpression ""
-- ""
--
-- >>> filterExpression "foobar"
-- "foobar"
--
-- >>> filterExpression ":{"
-- "*** Exception: unterminated multiline command
--
-- >>> filterExpression " :{ "
-- "*** Exception: unterminated multiline command
--
-- >>> filterExpression " :{ \nfoobar"
-- "*** Exception: unterminated multiline command
--
-- >>> filterExpression " :{ \nfoobar \n :} "
-- " :{ \nfoobar \n :} "
--
filterExpression :: String -> String
filterExpression e =
case lines e of
[] -> e
l -> if firstLine == ":{" && lastLine /= ":}" then fail_ else e
where
firstLine = strip $ head l
lastLine = strip $ last l
fail_ = error "unterminated multiline command"
where
strip :: String -> String
strip = dropWhile isSpace . reverse . dropWhile isSpace . reverse
getResult :: Interpreter -> IO String
getResult repl = do
line <- hGetLine stdout_
if isSuffixOf marker line
then
return $ stripMarker line
else do
result <- getResult repl
return $ line ++ '\n' : result
where
stdout_ = hOut repl
stripMarker l = take (length l - length marker) l
-- | Evaluate an expresion
eval
:: Interpreter
-> String -- Expression
-> IO String -- Result
eval repl expr = do
putExpression repl expr
getResult repl