tricorder-0.2.0.0: src/Tricorder/Daemon/GhciSession.hs
module Tricorder.Daemon.GhciSession
( -- * Effect
GhciSession
, Controls (..)
, transformControls
, withGhciWith
, withGhci
-- * Types
, LoadResult (..)
, LoadedModule (..)
-- * Interpreters
, runGhciSession
, runGhciSessionScripted
) where
import Atelier.Effects.Conc (Conc)
import Atelier.Effects.File (File)
import Atelier.Effects.Log (Log)
import Atelier.Effects.Process (Process)
import Atelier.Effects.Publishing.Pub (Pub)
import Atelier.Effects.Timeout (Timeout)
import Data.Default (def)
import Effectful
( Effect
, Limit (..)
, Persistence (..)
, UnliftStrategy (..)
)
import Effectful.Concurrent (Concurrent)
import Effectful.Dispatch.Dynamic
( interpret
, localLift
, localSeqLift
, localSeqUnlift
, localUnlift
, reinterpret
)
import Effectful.Exception (throwIO)
import Effectful.State.Static.Shared (State, evalState, state)
import Effectful.TH (makeEffect)
import Atelier.Effects.Publishing.Pub qualified as Pub
import Tricorder.Build (BuildProgress (..))
import Tricorder.Daemon.GhciSession.GhciParser
( GhciLoading (..)
, LoadResult (..)
, LoadedModule (..)
)
import Tricorder.Daemon.GhciSession.GhciProcess (addGhci, collectGhciResult, interruptGhci, reloadGhci, unaddGhci, withGhciProcess)
import Tricorder.Runtime (ProjectRoot (..))
import Tricorder.Session.Command (Command)
data GhciSession :: Effect where
-- | Start a new GHCi session and run the handler with that session active.
-- The handler is also provided an action to reload the GHCi session,
-- returning new messages with module counts. The GHCi session is closed
-- when the handler returns.
WithGhciWith
:: (BuildProgress -> m ())
-- ^ Action to run when reporting progress
-> Command
-> ProjectRoot
-> (LoadResult -> Controls m -> m a)
-> GhciSession m a
data Controls m = Controls
{ reload :: m LoadResult
, interrupt :: m ()
, add :: FilePath -> m LoadResult
, unadd :: Text -> m LoadResult
}
makeEffect ''GhciSession
transformControls :: (forall a. m a -> n a) -> Controls m -> Controls n
transformControls f ctrls =
Controls
{ reload = f ctrls.reload
, interrupt = f ctrls.interrupt
, add = f . ctrls.add
, unadd = f . ctrls.unadd
}
withGhci
:: (GhciSession :> es, Pub BuildProgress :> es)
=> Command
-> ProjectRoot
-> (LoadResult -> Controls (Eff es) -> Eff es a)
-> Eff es a
withGhci cmd root handler = do
withGhciWith Pub.publish cmd root handler
-- | Scripted interpreter for testing.
--
-- Each call to 'startGhci' or 'reloadGhci' pops the next result from the
-- pre-loaded list. 'Left' results are re-thrown as exceptions, simulating
-- GHCi crashes. 'stopGhci' is always a no-op.
runGhciSessionScripted :: forall es a. [Either SomeException LoadResult] -> Eff (GhciSession : es) a -> Eff es a
runGhciSessionScripted results = reinterpret (evalState results) $ \env ->
let popResult :: Eff (State [Either SomeException LoadResult] : es) LoadResult
popResult = do
x <- state \case
x : xs -> (x, xs)
[] -> error "GhciSessionScripted: no more results in queue"
case x of
Left ex -> throwIO ex
Right r -> pure r
in \case
WithGhciWith _ _ _ handler -> do
initial <- popResult
localSeqLift env \liftEff ->
localSeqUnlift env \unlift ->
unlift
$ handler
initial
Controls
{ reload = liftEff popResult
, interrupt = pure ()
, add = \_ -> liftEff popResult
, unadd = \_ -> liftEff popResult
}
-- | GHCi session manager backed by 'Tricorder.Daemon.GhciSession.GhciProcess'
-- and 'Tricorder.Daemon.GhciSession.GhciParser'.
runGhciSession
:: ( Conc :> es
, Concurrent :> es
, File :> es
, Log :> es
, Process :> es
, Timeout :> es
)
=> Eff (GhciSession : es) a -> Eff es a
runGhciSession = interpret $ \env -> \case
WithGhciWith onProgress cmd (ProjectRoot dir) handler -> do
localLift env (ConcUnlift Persistent Unlimited) \liftEff ->
localUnlift env (ConcUnlift Persistent Unlimited) \unlift -> do
let reportProgress loading =
unlift
$ onProgress
$ BuildProgress
{ compiled = loading.index
, total = loading.total
}
withGhciProcess def cmd dir reportProgress (\_ -> pure ()) \process startupLines -> do
initialResult <- collectGhciResult process startupLines dir
unlift
$ handler initialResult
$ transformControls liftEff
$ Controls
{ reload = reloadGhci process dir reportProgress
, interrupt = interruptGhci process
, add = \fp -> addGhci process fp dir reportProgress
, unadd = \mn -> unaddGhci process mn dir reportProgress
}