packages feed

halive-0.1.5: src/Halive/Recompiler.hs

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RankNTypes #-}
module Halive.Recompiler where
import Halive.SubHalive
import Halive.FileListener
import System.Mem
import Control.Concurrent.STM
import Control.Concurrent
import Control.Monad.Trans
import Control.Monad
import Data.Text (Text)
import qualified Data.Text as Text
import Data.IORef
import Data.Typeable
import GHC
data CompilationRequest = CompilationRequest
    { crFilePath          :: FilePath
    , crExpressionStrings :: [String]
    , crResultTChan       :: TChan CompilationResult
    , crFileContents      :: Maybe String
    -- ^ This is intentionally lazy, since we want to evaluate the string on
    -- the SubHalive thread (as it may be e.g. a TextSeq that needs conversion)
    -- In the future, we may want to pass GHC's StringBuffer type here instead,
    -- and construct those in a smarter way.
    }

type CompilationResult = Either String [CompiledValue]

-- This is used to implement a workaround for the GHC API crashing
-- when used after application startup, when it tries to load libraries
-- for the first time. By wrapping main in withGHC, startGHC will block until
-- the GHC API is initialized before allowing the application to start.
withGHC :: MonadIO m
        => GHCSessionConfig
        -> (TChan CompilationRequest -> m b)
        -> m b
withGHC ghcSessionConfig action = do
    ghcChan <- startGHC ghcSessionConfig
    action ghcChan

startGHCDefault :: MonadIO m => m (TChan CompilationRequest)
startGHCDefault = startGHC defaultGHCSessionConfig

startGHC :: MonadIO m => GHCSessionConfig -> m (TChan CompilationRequest)
startGHC ghcSessionConfig = liftIO $ do
    ghcChan <- newTChanIO

    -- Grab this thread's ID (need to run this on the main thread, of course)
    mainThreadID <- case gscMainThreadID ghcSessionConfig of
        Just threadID -> return threadID
        Nothing -> myThreadId

    initialFileLock <- liftIO newEmptyMVar
    _ <- forkIO . void $ do

        case gscKeepLibsInMemory ghcSessionConfig of

            -- In this mode we keep the ghc session alive continuously,
            -- and process all compilation requests in it.
            -- This trades possibly high memory usage for very fast compilation,
            -- since libraries don't have to be loaded in repeatedly.
            Always -> do

                -- See SubHalive.hs:GHCSessionConfig
                withGHCSession mainThreadID ghcSessionConfig $ do
                    compileInitialFile ghcSessionConfig
                    liftIO $ putMVar initialFileLock ()

                    forever $ do
                        CompilationRequest{..} <- readTChanIO ghcChan

                        result <- recompileExpressionsInFile
                            crFilePath crFileContents crExpressionStrings
                        writeTChanIO crResultTChan result

            -- In this mode we create a fresh GHC session for each set of compilation
            -- requests. This trades slower compilations for lower memory usage
            -- when not compiling.
            Opportunistic -> do
                withGHCSession mainThreadID ghcSessionConfig $
                    compileInitialFile ghcSessionConfig
                liftIO $ putMVar initialFileLock ()

                forever $ do
                    requests <- waitExhaustTChanIO ghcChan

                    withGHCSession mainThreadID ghcSessionConfig $
                        forM_ requests $ \CompilationRequest{..} -> do
                            result <- recompileExpressionsInFile
                                crFilePath crFileContents crExpressionStrings
                            writeTChanIO crResultTChan result
                    liftIO performGC

    -- Wait for the initial file to complete
    () <- liftIO (takeMVar initialFileLock)

    return ghcChan

compileInitialFile :: GHCSessionConfig -> Ghc ()
compileInitialFile ghcSessionConfig =
    forM_ (gscStartupFile ghcSessionConfig) $
        \(startupFile, startupExpr) ->
            recompileExpressionsInFile startupFile Nothing [startupExpr]

data Recompiler = Recompiler
    { recResultTChan       :: TChan CompilationResult
    , recFileEventListener :: FileEventListener
    , recListenerThread    :: ThreadId
    }

recompilerForExpression :: MonadIO m
                        => TChan CompilationRequest
                        -> FilePath
                        -> String
                        -> m Recompiler
recompilerForExpression ghcChan filePath expressionString =
    recompilerWithConfig ghcChan RecompilerConfig
        { rccWatchAll = Nothing
        , rccExpressions = [expressionString]
        , rccFilePath = filePath
        }

data RecompilerConfig = RecompilerConfig
    { rccWatchAll :: Maybe (FilePath, [String]) -- if Nothing, just watch given file
    , rccExpressions :: [String]
    , rccFilePath :: FilePath
    }

recompilerWithConfig :: MonadIO m
                     => TChan CompilationRequest
                     -> RecompilerConfig
                     -> m Recompiler
recompilerWithConfig ghcChan RecompilerConfig{..} = liftIO $ do
    resultTChan <- newTChanIO
    let compilationRequest = CompilationRequest
            { crFilePath          = rccFilePath
            , crExpressionStrings = rccExpressions
            , crResultTChan       = resultTChan
            , crFileContents      = Nothing
            }

    -- Recompile on file event notifications
    fileEventListener <- case rccWatchAll of
        Nothing -> eventListenerForFile rccFilePath JustReportEvents
        Just (watchDir, fileTypes) -> eventListenerForDirectory watchDir fileTypes
    listenerThread <- forkIO . forever $ do
        _ <- readFileEvent fileEventListener
        writeTChanIO ghcChan compilationRequest

    -- Compile for the first time immediately
    writeTChanIO ghcChan compilationRequest

    return Recompiler
        { recResultTChan       = resultTChan
        , recFileEventListener = fileEventListener
        , recListenerThread    = listenerThread
        }

killRecompiler :: MonadIO m => Recompiler -> m ()
killRecompiler recompiler = do
    liftIO $ killThread (recListenerThread recompiler)

renameRecompilerForExpression :: MonadIO m => Recompiler
                                           -> TChan CompilationRequest
                                           -> FilePath
                                           -> String
                                           -> m Recompiler
renameRecompilerForExpression recompiler ghcChan filePath expressionString = do
    killRecompiler recompiler
    recompilerForExpression ghcChan filePath expressionString

compileExpressions :: MonadIO m
                   => TChan CompilationRequest
                   -> Text
                   -> [String]
                   -> m (TChan CompilationResult)
compileExpressions ghcChan code expressionStrings = do
    resultTChan <- liftIO newTChanIO
    liftIO $ atomically $ writeTChan ghcChan $ CompilationRequest
        { crFilePath          = ""
        , crExpressionStrings = expressionStrings
        , crResultTChan       = resultTChan
        , crFileContents      = Just $ Text.unpack code
        }
    return resultTChan

compileExpression :: MonadIO m
                  => TChan CompilationRequest
                  -> Text
                  -> String
                  -> m (TChan CompilationResult)
compileExpression ghcChan code expressionString =
    compileExpressions ghcChan code [expressionString]

compileExpressionInFile :: MonadIO m
                        => TChan CompilationRequest
                        -> FilePath
                        -> String
                        -> m (TChan CompilationResult)
compileExpressionInFile ghcChan fileName expressionString = do
    resultTChan <- liftIO newTChanIO
    liftIO $ atomically $ writeTChan ghcChan $ CompilationRequest
        { crFilePath          = fileName
        , crExpressionStrings = [expressionString]
        , crResultTChan       = resultTChan
        , crFileContents      = Nothing
        }
    return resultTChan

-- | liveExpression returns an action to get to the latest version of the expression,
-- updating it whenever the code changes (unless there is an error).
-- It also takes a default argument to use until the first compilation completes.
-- The action is meant to be called before each use of the value.
liveExpression :: Typeable a
               => TChan CompilationRequest
               -> FilePath
               -> String
               -> a
               -> IO (IO a)
liveExpression ghcChan fileName expression defaultVal = do
    recompiler <- recompilerForExpression ghcChan fileName expression
    valueRef <- newIORef defaultVal
    _ <- forkIO . forever $ do
        result <- atomically (readTChan (recResultTChan recompiler))
        case result of
            Left errors -> putStrLn errors
            Right values ->
                case values of
                    [value] ->
                        case getCompiledValue value of
                            Just newVal -> writeIORef valueRef newVal
                            Nothing -> putStrLn ("Got incorrect type for " ++ fileName ++ ":" ++ expression)
                    _ ->
                        error "Unexpected number of values received on recResultTChan"

    return (readIORef valueRef)