packages feed

tricorder-0.1.1.0: src/Tricorder/Effects/GhciSession.hs

module Tricorder.Effects.GhciSession
    ( -- * Effect
      GhciSession
    , Controls (..)
    , 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.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 Tricorder.BuildState (BuildPhase (..), BuildProgress (..))
import Tricorder.Effects.BuildStore (BuildStore, modifyPhase)
import Tricorder.Effects.GhciSession.GhciParser
    ( GhciLoading (..)
    , LoadResult (..)
    , LoadedModule (..)
    )
import Tricorder.Effects.GhciSession.GhciProcess (addGhci, collectGhciResult, interruptGhci, reloadGhci, unaddGhci, withGhciProcess)
import Tricorder.Runtime (ProjectRoot (..))
import Tricorder.Session (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.
    WithGhci :: 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


-- | 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
            WithGhci _ _ 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.Effects.GhciSession.GhciProcess'
-- and 'Tricorder.Effects.GhciSession.GhciParser'.
runGhciSession
    :: ( BuildStore :> es
       , Conc :> es
       , Concurrent :> es
       , File :> es
       , Log :> es
       , Process :> es
       , Timeout :> es
       )
    => Eff (GhciSession : es) a -> Eff es a
runGhciSession = interpret $ \env -> \case
    WithGhci cmd (ProjectRoot dir) handler -> do
        let onProgress loading =
                modifyPhase \_ ->
                    Building
                        $ Just
                        $ BuildProgress {compiled = loading.index, total = loading.total}
        withGhciProcess def cmd dir onProgress (\_ -> pure ()) \process startupLines ->
            localLift env (ConcUnlift Persistent Unlimited) \liftEff ->
                localUnlift env (ConcUnlift Persistent Unlimited) \unlift -> do
                    let doReload = liftEff $ reloadGhci process dir onProgress
                    initialResult <- unlift $ liftEff $ collectGhciResult process startupLines dir
                    unlift
                        $ handler
                            initialResult
                            Controls
                                { reload = doReload
                                , interrupt = liftEff (interruptGhci process)
                                , add = \fp -> liftEff $ addGhci process fp dir onProgress
                                , unadd = \mn -> liftEff $ unaddGhci process mn dir onProgress
                                }