{-# 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) "" (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