packages feed

tricorder-0.2.0.0: src/Tricorder/Daemon/Builder.hs

module Tricorder.Daemon.Builder
    ( Builder (..)
    , NewLoadResult (..)
    , BuildConsideration (..)
    , BuildFailure (..)
    , build
    , consider
    , with
    , compileBuildResults
    ) where

import Atelier.Effects.Clock (Clock, UTCTime)
import Atelier.Effects.FileWatcher (FileEvent)
import Atelier.Effects.Log (Log)
import Atelier.Effects.Publishing.Pub (Pub)
import Atelier.Time (Millisecond, nominalDiffTime)
import Data.Time (diffUTCTime)
import Effectful (Effect, inject)
import Effectful.Dispatch.Dynamic (reinterpretWith_)
import Effectful.Exception (trySync)
import Effectful.Reader.Static (Reader, ask)
import Effectful.State.Static.Shared (State, get, modify)
import Effectful.TH (makeEffect)
import System.FilePath (normalise)

import Atelier.Effects.Clock qualified as Clock
import Atelier.Effects.Log qualified as Log
import Atelier.Effects.Publishing.Pub qualified as Pub
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Effectful.State.Static.Shared qualified as State

import Tricorder.Build (BuildId (..), BuildProgress, BuildResult (..), Diagnostic (..))
import Tricorder.Daemon.Dispatch
    ( BuilderState (..)
    , DiagnosticMap
    , DispatchAction (..)
    , KnownTargetNames (..)
    , dispatch
    , emptyBuilderState
    , filterToWatchDirs
    , mergeDiagnostics
    , preserveFailureVisibility
    )
import Tricorder.Daemon.GhciSession (GhciSession, LoadResult (..))
import Tricorder.Daemon.GhciSession.GhciParser (resolveKnownTargets)
import Tricorder.Runtime (ProjectRoot (..))
import Tricorder.Session.Command (Command)
import Tricorder.Session.WatchDirs (WatchDirs)

import Tricorder.Daemon.GhciSession qualified as GhciSession


data Builder :: Effect where
    Consider :: FilePath -> FileEvent -> Builder m BuildConsideration
    Build :: DispatchAction -> Builder m (Either BuildFailure NewLoadResult)


data BuildConsideration
    = SkipBuilding
    | ShouldBuild DispatchAction


data BuildFailure
    = ReloadFailed SomeException
    deriving stock (Show)


data NewLoadResult = NewLoadResult
    { startTime :: UTCTime
    , endTime :: UTCTime
    , loadResult :: LoadResult
    }
    deriving stock (Eq, Show)


makeEffect ''Builder


with
    :: ( Clock :> es
       , GhciSession :> es
       , Log :> es
       , Pub BuildProgress :> es
       , Reader ProjectRoot :> es
       )
    => BuildId
    -> Command
    -> WatchDirs
    -> (BuilderState -> NewLoadResult -> Eff (Builder : es) a)
    -> Eff es (Either SomeException a)
with buildId command watchDirs action = do
    root <- ask
    startTime <- Clock.currentTime
    trySync $ GhciSession.withGhciWith Pub.publish command root \initialLoad controls -> do
        endTime <- Clock.currentTime
        let filteredMsgs = filterToWatchDirs root.getProjectRoot watchDirs initialLoad.diagnostics
        Log.info
            $ mconcat
                [ "GHCi started (session #"
                , show buildId.getBuildId
                , "): "
                , show (length filteredMsgs)
                , " diagnostics"
                ]
        let initialLoadResult = NewLoadResult {startTime, endTime, loadResult = initialLoad}
        let initialBuilderState =
                emptyBuilderState
                    { loadedModules = resolveKnownTargets Map.empty initialLoad
                    , knownTargets = KnownTargetNames (Set.fromList initialLoad.targetNames)
                    }
        reinterpretWith_
            (State.evalState initialBuilderState)
            (action initialBuilderState initialLoadResult)
            \case
                Consider fp event -> considerBuild fp event
                Build dispatchAction ->
                    buildSource (GhciSession.transformControls inject controls) dispatchAction


considerBuild
    :: (State BuilderState :> es)
    => FilePath
    -> FileEvent
    -> Eff es BuildConsideration
considerBuild fp event = do
    builderState <- get
    let known = Map.lookup (normalise fp) builderState.loadedModules
    case dispatch builderState.knownTargets known fp event of
        Nothing ->
            -- File not loaded in GHCi, so we skip building.
            pure SkipBuilding
        Just action -> do
            pure $ ShouldBuild action


buildSource
    :: (Clock :> es, State BuilderState :> es)
    => GhciSession.Controls (Eff es)
    -> DispatchAction
    -> Eff es (Either BuildFailure NewLoadResult)
buildSource controls action = do
    res <- trySync do
        startTime <- Clock.currentTime
        res <- runAction controls action
        endTime <- Clock.currentTime
        pure (startTime, endTime, res)

    case res of
        Left e -> do
            pure $ Left $ ReloadFailed e
        Right (startTime, endTime, loadResult) -> do
            modify \s ->
                s
                    { loadedModules = resolveKnownTargets s.loadedModules loadResult
                    , knownTargets = KnownTargetNames (Set.fromList loadResult.targetNames)
                    }
            pure $ Right $ NewLoadResult {startTime, endTime, loadResult}


compileBuildResults
    :: ProjectRoot
    -> WatchDirs
    -> DiagnosticMap
    -> NewLoadResult
    -> (DiagnosticMap, BuildResult)
compileBuildResults (ProjectRoot projectRoot) watchDirs diagnosticMap newLoadResult =
    (merged, buildResult)
  where
    NewLoadResult {startTime, endTime, loadResult} = newLoadResult
    merged = mergeDiagnostics diagnosticMap filteredResult
    filteredResult =
        loadResult
            { GhciSession.diagnostics =
                preserveFailureVisibility loadResult.diagnostics
                    $ filterToWatchDirs projectRoot watchDirs loadResult.diagnostics
            }
    buildResult =
        BuildResult
            { completedAt = endTime
            , duration = nominalDiffTime (diffUTCTime endTime startTime) :: Millisecond
            , moduleCount = loadResult.moduleCount
            , diagnostics = sortOn (\d -> (d.severity, d.file, d.line, d.col)) $ concat $ Map.elems merged
            }


runAction :: GhciSession.Controls (Eff es) -> DispatchAction -> Eff es LoadResult
runAction controls = \case
    Reload -> controls.reload
    Add fp -> controls.add fp
    Unadd mn -> controls.unadd mn