packages feed

halive-0.1.6: src/Halive/SubHalive.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE CPP #-}
module Halive.SubHalive (
    module Halive.SubHalive
#if __GLASGOW_HASKELL__ >= 800
    , module GHC.LanguageExtensions
#else
    , ExtensionFlag(..)
#endif

    ) where

import GHC
#if __GLASGOW_HASKELL__ >= 800
import GHC.LanguageExtensions
#else
import Module
#endif
import DynFlags
import Exception
import ErrUtils
import HscTypes
import GHC.Paths
import Outputable
import StringBuffer

-- import Packages
import Linker

#if __GLASGOW_HASKELL__ < 800
import Control.Monad
#endif
import Control.Monad.IO.Class
import Data.IORef
import Data.Time
import Halive.FindPackageDBs

import Control.Concurrent
import System.Signal
import Data.Dynamic

import System.Directory
import System.FilePath
import Data.Time.Clock.POSIX

import qualified Data.Text as Text

data FixDebounce = DebounceFix | NoDebounceFix deriving Eq

data CompliationMode = Interpreted | Compiled deriving Eq

data KeepLibsInMemory = Always | Opportunistic

data GHCSessionConfig = GHCSessionConfig
    { gscFixDebounce        :: FixDebounce
    , gscImportPaths        :: [FilePath]
    , gscPackageDBs         :: [FilePath]
    , gscLibDir             :: FilePath
#if __GLASGOW_HASKELL__ >= 800
    , gscLanguageExtensions :: [Extension]
    , gscNoLanguageExtensions :: [Extension]
#else
    , gscLanguageExtensions :: [ExtensionFlag]
    , gscNoLanguageExtensions :: [ExtensionFlag]
#endif
    , gscCompilationMode    :: CompliationMode
    , gscStartupFile        :: Maybe (FilePath, String)
        -- ^ Allow API users to block until a given file is compiled,
        -- to work around a bug where the GHC API crashes while
        -- loading libraries if the main thread is doing work
        -- (possibly due to accessing said libraries in some way)
    , gscVerbosity          :: Int
    , gscMainThreadID       :: Maybe ThreadId
    , gscKeepLibsInMemory   :: KeepLibsInMemory
        -- ^ Chooses between keeping the GHC session alive continuously
        -- (which uses a lot of memory but makes compilation fast)
        -- or disposing of it between compilations
        -- (which saves memory but slows compilation)
        -- or keeping it around for sequences of compilations
        -- (which lies in-between these)
    }

defaultGHCSessionConfig :: GHCSessionConfig
defaultGHCSessionConfig = GHCSessionConfig
    { gscFixDebounce = DebounceFix
    , gscImportPaths = []
    , gscPackageDBs  = []
    , gscLanguageExtensions = []
    , gscNoLanguageExtensions = []
    , gscLibDir = libdir
    , gscCompilationMode = Interpreted
    , gscStartupFile = Nothing
    , gscVerbosity = 0
    , gscMainThreadID = Nothing
    , gscKeepLibsInMemory = Always
    }

-- Starts up a GHC session and then runs the given action within it
withGHCSession :: ThreadId -> GHCSessionConfig -> Ghc a -> IO a
withGHCSession mainThreadID GHCSessionConfig{..} action = do
    -- Work around https://ghc.haskell.org/trac/ghc/ticket/4162
    let restoreControlC f = do
            liftIO $ installHandler sigINT
                (\_signal -> killThread mainThreadID)
            f

    -- defaultErrorHandler defaultFatalMessager defaultFlushOut $ runGhc (Just libdir) $ do
    runGhc (Just gscLibDir) . restoreControlC $ do

        -- initialFlags <- getSessionDynFlags
        -- (newFlags, leftovers, warnings) <- parseDynamicFlagsCmdLine initialFlags [noLoc "-prof"]
        -- setSessionDynFlags newFlags
        -- liftIO $ print (compilerInfo newFlags)

        packageIDs <-
                getSessionDynFlags
            >>= updateDynFlagsWithGlobalDB
            -- If this is a stack project, add its package DBs
            >>= updateDynFlagsWithStackDB
            -- If there's a sandbox, add its package DB
            >>= updateDynFlagsWithCabalSandbox
            -- Add passed-in package DBs
            >>= (pure . addExtraPkgConfs gscPackageDBs)
            -- Make sure we're configured for live-reload
            >>= (\d -> pure d
                { hscTarget   = if gscCompilationMode == Compiled then HscAsm else HscInterpreted
                , optLevel    = if gscCompilationMode == Compiled then 1 else 0
                , ghcLink     = LinkInMemory
                , ghcMode     = CompManager
                , importPaths = gscImportPaths
                , objectDir   = Just ".halive"
                , hiDir       = Just ".halive"
                , stubDir     = Just ".halive"
                , dumpDir     = Just ".halive"
                , verbosity   = gscVerbosity
                })
            -- turn off the GHCi sandbox
            -- since it breaks OpenGL/GUI usage
            >>= (pure . (`gopt_unset` Opt_GhciSandbox))
            -- Allows us to work in dynamic executables
            -- >>= (pure . (if dynamicGhc then addWay' WayDyn else id))
            -- >>= (pure . (addWay' WayProf))
            -- >>= (pure . (if rtsIsProfiled then addWay' WayProf else id))
            -- >>= (pure . (addWay' WayDyn))
            -- GHC seems to try to "debounce" compilations within
            -- about a half second (i.e., it won't recompile)
            -- This fixes that, but probably isn't quite what we want
            -- since it will cause extra files to be recompiled...
            >>= (pure . (if gscFixDebounce == DebounceFix
                            then (`gopt_set` Opt_ForceRecomp)
                            else id))
            >>= (pure . flip (foldl xopt_unset) gscNoLanguageExtensions
                      . flip (foldl xopt_set) gscLanguageExtensions)
            -- We must call setSessionDynFlags before calling initPackages or any other GHC API
            >>= setSessionDynFlags

        -- Initialize the package database and dynamic linker.
        -- Explicitly calling these avoids crashes on some of my machines.
#if __GLASGOW_HASKELL__ >= 800
        -- (dflags,_pkgs) <- liftIO . initPackages =<< getSessionDynFlags
        -- setSessionDynFlags dflags

        getSession >>= \hscEnv ->
            liftIO $ linkPackages hscEnv packageIDs
        liftIO . initDynLinker =<< getSession
#else
        getSessionDynFlags >>= \dflags ->
            liftIO $ linkPackages dflags packageIDs
        liftIO . initDynLinker =<< getSessionDynFlags
#endif

        result <- action

        -- Unload libraries to keep from leaking memory & overloading the GC
        getSession >>= \hscEnv ->
            liftIO (unload hscEnv [])

        return result



newtype CompiledValue = CompiledValue Dynamic deriving Show

getCompiledValue :: Typeable a => CompiledValue -> Maybe a
getCompiledValue (CompiledValue r) = fromDynamic r

fileContentsStringToBuffer :: (MonadIO m) => String -> m (StringBuffer, UTCTime)
fileContentsStringToBuffer fileContents = do
    now <- liftIO getCurrentTime
    return (stringToStringBuffer fileContents, now)

createTempFile :: MonadIO m => m FilePath
createTempFile = liftIO $ do
    tempDir <- getTemporaryDirectory
    now <- show . diffTimeToPicoseconds . realToFrac <$> getPOSIXTime
    let tempFile = tempDir </> "halive_" ++ now <.> "hs"
    writeFile tempFile ""
    return tempFile

-- | Takes a filename, optionally its contents, and a list of expressions.
-- Returns a list of errors or a list of Dynamic compiled values
recompileExpressionsInFile :: FilePath
                           -> Maybe String
                           -> [String]
                           -> Ghc (Either String [CompiledValue])
recompileExpressionsInFile fileName mFileContents expressions =

    catchExceptions . handleSourceError (fmap Left . gatherErrors) $ do

        -- Set up an error accumulator
        errorsRef <- liftIO (newIORef "")
        _ <- getSessionDynFlags >>=
            \dflags -> setSessionDynFlags dflags
                { log_action = logHandler errorsRef }

        mFileContentsBuffer <- mapM fileContentsStringToBuffer mFileContents

        -- Set the target
        (tempFileName, target) <- case fileName of
            -- We'd like to just use a Module name for the target,
            -- but load/depanal fails with "Foo is a package module"
            -- We use a blank temp file as a workaround.
            ""    -> do
                tempFileName <- createTempFile
                (tempFileName,) <$> guessTarget' tempFileName
            other -> ("",) <$> guessTarget' other

        -- logIO "Setting targets..."
        setTargets [target { targetContents = mFileContentsBuffer }]

        -- Reload the main target
        -- logIO "Loading..."
        loadSuccess <- load LoadAllTargets

        if succeeded loadSuccess
            then do

                -- logIO "Analyzing deps..."
                -- Get the dependencies of the main target (and update the session with them)
                graph <- depanal [] False

                #if __GLASGOW_HASKELL__ >= 804
                let modSummaries = mgModSummaries graph
                #else
                let modSummaries = graph
                #endif

                -- Load the dependencies of the main target
                setContext
                    (IIDecl . simpleImportDecl . ms_mod_name <$> modSummaries)

                -- Compile the expressions and return the results
                results <- mapM dynCompileExpr expressions

                return (Right (CompiledValue <$> results))
            else do
                -- Extract the errors from the accumulator
                errors <- liftIO (readIORef errorsRef)
                -- Strip out the temp file name when using anonymous code
                let cleanErrors = if null tempFileName then errors
                        else Text.unpack $
                            Text.replace
                            (Text.pack tempFileName)
                            "<anonymous code>"
                            (Text.pack errors)
                return (Left cleanErrors)

-- Prepend a '*' to prevent GHC from trying to load from any previously compiled object files
-- see http://stackoverflow.com/questions/12790341/haskell-ghc-dynamic-compliation-only-works-on-first-compile
guessTarget' :: GhcMonad m => String -> m Target
guessTarget' fileName = guessTarget ('*':fileName) Nothing

catchExceptions :: ExceptionMonad m => m (Either String a) -> m (Either String a)
catchExceptions a = gcatch a
    (\(_x :: SomeException) -> do
        liftIO (putStrLn ("Caught exception during recompileExpressionInFile: " ++ show _x))
        return (Left (show _x))
        )



logHandler :: IORef String -> LogAction
#if __GLASGOW_HASKELL__ >= 800
logHandler ref dflags _warnReason severity srcSpan style msg =
#else
logHandler ref dflags severity srcSpan style msg =
#endif
    case severity of
       SevError   -> modifyIORef' ref (++ ('\n':messageWithLocation))
       SevFatal   -> modifyIORef' ref (++ ('\n':messageWithLocation))
       SevWarning -> modifyIORef' ref (++ ('\n':messageWithLocation))
       _          -> do
            putStr messageOther
            return () -- ignore the rest
    where cntx = initSDocContext dflags style
          locMsg = mkLocMessage severity srcSpan msg
          messageWithLocation = show (runSDoc locMsg cntx)
          messageOther = show (runSDoc msg cntx)



-- A helper from interactive-diagrams to print out GHC API values,
-- useful while debugging the API.
-- | Outputs any value that can be pretty-printed using the default style
output :: (GhcMonad m, Outputable a) => a -> m ()
output a = do
    dfs <- getSessionDynFlags
    let style = defaultUserStyle dfs
    let cntx  = initSDocContext dfs style
    liftIO $ print $ runSDoc (ppr a) cntx


-- NOTE: handleSourceError (which calls gatherErrors above)
-- doesn't actually seem to do anything, so we use
-- the IORef + log_action solution instead.
-- The API docs claim 'load' should
-- throw SourceErrors but it doesn't afaict.
gatherErrors :: GhcMonad m => SourceError -> m String
gatherErrors sourceError = do
    printException sourceError
    dflags <- getSessionDynFlags
    let errorSDocs = pprErrMsgBagWithLoc (srcErrorMessages sourceError)
        errorStrings = map (showSDoc dflags) errorSDocs
    return (concat errorStrings)


--pkgConfRefToString = \case
--    GlobalPkgConf -> "GlobalPkgConf"
--    UserPkgConf -> "UserPkgConf"
--    PkgConfFile file -> "PkgConfFile " ++ show file

--extraPkgConfsToString dflags = show $ map pkgConfRefToString $ extraPkgConfs dflags $ []

logIO :: MonadIO m => String -> m ()
logIO = liftIO . putStrLn