packages feed

sensei-0.7.0: src/Language/Haskell/GhciWrapper.hs

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NamedFieldPuns #-}
module Language.Haskell.GhciWrapper (
  Config(..)
, Interpreter
, withInterpreter
, eval
, evalVerbose
) where

import           Imports

import qualified Data.ByteString as B
import           Data.Text.Encoding (decodeUtf8)
import qualified Data.Text as T
import           System.IO hiding (stdin, stdout, stderr)
import qualified System.IO as System
import           System.Directory (doesFileExist)
import           System.Process
import           System.Exit (ExitCode(..))

import qualified ReadHandle
import           ReadHandle (ReadHandle, toReadHandle)

data Config = Config {
  configIgnoreDotGhci :: Bool
, configVerbose :: Bool
, configStartupFile :: FilePath
} deriving (Eq, Show)

data Interpreter = Interpreter {
  hIn  :: Handle
, hOut :: Handle
, readHandle :: ReadHandle
, process :: ProcessHandle
}

die :: String -> IO a
die = throwIO . ErrorCall

withInterpreter :: Config -> [String] -> (Interpreter -> IO r) -> IO r
withInterpreter config args = bracket (new config args) (close $ verbosity config)

new :: Config -> [String] -> IO Interpreter
new config@Config{..} args_ = do

  requireFile configStartupFile

  (Just stdin_, Just stdout_, Nothing, processHandle ) <- createProcess (proc "ghci" args) {
    std_in  = CreatePipe
  , std_out = CreatePipe
  , std_err = Inherit
  }

  setMode stdin_
  readHandle <- toReadHandle stdout_ 1024

  let
    interpreter = Interpreter {
      hIn = stdin_
    , readHandle
    , hOut = stdout_
    , process = processHandle
    }

  _ <- printStartupMessages interpreter

  evalThrow interpreter "GHC.IO.Handle.hDuplicateTo System.IO.stdout System.IO.stderr"

  -- GHCi uses NoBuffering for stdout and stderr by default:
  -- https://downloads.haskell.org/ghc/9.4.4/docs/users_guide/ghci.html
  evalThrow interpreter "GHC.IO.Handle.hSetBuffering System.IO.stdout GHC.IO.Handle.LineBuffering"
  evalThrow interpreter "GHC.IO.Handle.hSetBuffering System.IO.stderr GHC.IO.Handle.LineBuffering"

  evalThrow interpreter "GHC.IO.Handle.hSetEncoding System.IO.stdout GHC.IO.Encoding.utf8"
  evalThrow interpreter "GHC.IO.Handle.hSetEncoding System.IO.stderr GHC.IO.Encoding.utf8"

  return interpreter
  where
    requireFile name = do
      exists <- doesFileExist name
      unless exists $ do
        die $ "Required file " <> show name <> " does not exist!"

    args = "-ghci-script" : configStartupFile : args_ ++ catMaybes [
        if configIgnoreDotGhci then Just "-ignore-dot-ghci" else Nothing
      ]

    setMode h = do
      hSetBinaryMode h False
      hSetBuffering h LineBuffering
      hSetEncoding h utf8

    printStartupMessages interpreter = evalWith (verbosity config) interpreter ""

    evalThrow :: Interpreter -> String -> IO ()
    evalThrow interpreter expr = do
      output <- eval interpreter expr
      unless (null output) $ do
        close (verbosity config) interpreter
        die output

close :: (ByteString -> IO ()) -> Interpreter -> IO ()
close echo Interpreter{..} = do
  hClose hIn
  ReadHandle.drain readHandle echo
  hClose hOut
  e <- waitForProcess process
  when (e /= ExitSuccess) $ do
    throwIO (userError $ "Language.Haskell.GhciWrapper.close: Interpreter exited with an error (" ++ show e ++ ")")

putExpression :: Interpreter -> String -> IO ()
putExpression Interpreter{hIn = stdin} e = do
  hPutStrLn stdin e
  B.hPut stdin ReadHandle.marker
  hFlush stdin

getResult :: Interpreter -> (ByteString -> IO ()) -> IO String
getResult Interpreter{readHandle = h} = fmap (T.unpack . decodeUtf8) . ReadHandle.getResult h

verbosity :: Config -> ByteString -> IO ()
verbosity config
  | configVerbose config = verbose
  | otherwise = silent

verbose :: ByteString -> IO ()
verbose string = B.putStr string >> hFlush System.stdout

silent :: ByteString -> IO ()
silent _ = pass

evalWith :: (ByteString -> IO ()) -> Interpreter -> String -> IO String
evalWith echo repl expr = do
  putExpression repl expr
  getResult repl echo

eval :: Interpreter -> String -> IO String
eval = evalWith silent

evalVerbose :: Interpreter -> String -> IO String
evalVerbose = evalWith verbose