{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} module GHC.Debugger.Monad where import Prelude hiding (mod) import Data.Function import System.Exit import System.IO import System.FilePath (normalise) import System.Directory (makeAbsolute) import Control.Monad import Control.Monad.IO.Class import Control.Exception (assert) import Control.Monad.Catch import GHC import qualified GHCi.BreakArray as BA import GHC.Driver.DynFlags as GHC import GHC.Unit.Module.ModSummary as GHC import GHC.Utils.Outputable as GHC import GHC.Utils.Logger as GHC import GHC.Types.Unique.Supply as GHC import GHC.Runtime.Loader as GHC import GHC.Runtime.Interpreter as GHCi import GHC.Runtime.Heap.Inspect import GHC.Unit.Module.Env as GHC import GHC.Runtime.Debugger.Breakpoints import GHC.Driver.Env import Data.IORef import Data.Maybe import qualified Data.List.NonEmpty as NonEmpty import qualified Data.IntMap as IM import Control.Monad.Reader import GHC.Debugger.Interface.Messages import GHC.Debugger.Runtime.Term.Key import GHC.Debugger.Runtime.Term.Cache import GHC.Debugger.Session import System.Posix.Signals -- | A debugger action. newtype Debugger a = Debugger { unDebugger :: ReaderT DebuggerState GHC.Ghc a } deriving ( Functor, Applicative, Monad, MonadIO , MonadThrow, MonadCatch, MonadMask , GHC.HasDynFlags, MonadReader DebuggerState ) -- | State required to run the debugger. -- -- - Keep track of active breakpoints to easily unset them all. data DebuggerState = DebuggerState { activeBreakpoints :: IORef (ModuleEnv (IM.IntMap (BreakpointStatus, BreakpointKind))) -- ^ Maps a 'BreakpointId' in Trie representation to the -- 'BreakpointStatus' it was activated with. , varReferences :: IORef (IM.IntMap TermKey, TermKeyMap Int) -- ^ When we're stopped at a breakpoint, this maps variable reference to -- Terms to allow further inspection and forcing by reference. -- -- This map is only valid while stopped in this context. After stepping -- or resuming evaluation in any available way, this map becomes invalid -- and should therefore be cleaned. -- -- The TermKeyMap map is a reverse lookup map to find which references -- already exist for given names , termCache :: IORef TermCache -- ^ TermCache , genUniq :: IORef Int -- ^ Generates unique ints } -- | Enabling/Disabling a breakpoint data BreakpointStatus -- | Breakpoint is disabled -- -- Note: this must be the first constructor s.t. -- @BreakpointDisabled < {BreakpointEnabled, BreakpointAfterCount}@ = BreakpointDisabled -- | Breakpoint is enabled | BreakpointEnabled -- | Breakpoint is disabled the first N times and enabled afterwards | BreakpointAfterCount Int deriving (Eq, Ord) -------------------------------------------------------------------------------- -- Operations -------------------------------------------------------------------------------- -- | Additional settings configuring the debugger data RunDebuggerSettings = RunDebuggerSettings { supportsANSIStyling :: Bool , supportsANSIHyperlinks :: Bool } -- | Run a 'Debugger' action on a session constructed from a given GHC invocation. runDebugger :: Handle -- ^ The handle to which GHC's output is logged. The debuggee output is not affected by this parameter. -> FilePath -- ^ Cradle root directory -> FilePath -- ^ Component root directory -> FilePath -- ^ The libdir (given with -B as an arg) -> [String] -- ^ The list of units included in the invocation -> [String] -- ^ The full ghc invocation (as constructed by hie-bios flags) -> FilePath -- ^ Path to the main function -> RunDebuggerSettings -- ^ Other debugger run settings -> Debugger a -- ^ 'Debugger' action to run on the session constructed from this invocation -> IO a runDebugger dbg_out rootDir compDir libdir units ghcInvocation' mainFp conf (Debugger action) = do let ghcInvocation = filter (\case ('-':'B':_) -> False; _ -> True) ghcInvocation' GHC.runGhc (Just libdir) $ do -- Workaround #4162 _ <- liftIO $ installHandler sigINT Default Nothing dflags0 <- GHC.getSessionDynFlags let dflags1 = dflags0 { GHC.ghcMode = GHC.CompManager , GHC.ghcLink = GHC.LinkInMemory , GHC.verbosity = 1 , GHC.canUseColor = conf.supportsANSIStyling , GHC.canUseErrorLinks = conf.supportsANSIHyperlinks } -- Default GHCi settings `GHC.gopt_set` GHC.Opt_ImplicitImportQualified `GHC.gopt_set` GHC.Opt_IgnoreOptimChanges `GHC.gopt_set` GHC.Opt_IgnoreHpcChanges `GHC.gopt_set` GHC.Opt_UseBytecodeRatherThanObjects `GHC.gopt_set` GHC.Opt_InsertBreakpoints & setBytecodeBackend & enableByteCodeGeneration GHC.modifyLogger $ -- Override the logger to output to the given handle GHC.pushLogHook (const $ debuggerLoggerAction dbg_out) -- TODO: this is weird, we set the session dynflags now to initialise -- the hsc_interp. -- This is incredibly dubious _ <- GHC.setSessionDynFlags dflags1 -- Initialise plugins here because the plugin author might already expect this -- subsequent call to `getLogger` to be affected by a plugin. GHC.initializeSessionPlugins flagsAndTargets <- parseHomeUnitArguments mainFp compDir units ghcInvocation dflags1 rootDir setupHomeUnitGraph (NonEmpty.toList flagsAndTargets) dflags6 <- GHC.getSessionDynFlags -- Should this be done in GHC= liftIO $ GHC.initUniqSupply (GHC.initialUnique dflags6) (GHC.uniqueIncrement dflags6) ok_flag <- GHC.load GHC.LoadAllTargets when (GHC.failed ok_flag) (liftIO $ exitWith (ExitFailure 1)) -- TODO: Shouldn't initLoaderState be called somewhere? -- Set interactive context to import all loaded modules -- TODO: Think about Note [GHCi and local Preludes] and what is done in `getImplicitPreludeImports` let preludeImp = GHC.IIDecl . GHC.simpleImportDecl $ GHC.mkModuleName "Prelude" mss <- getAllLoadedModules GHC.setContext $ preludeImp : map (GHC.IIModule . GHC.ms_mod) mss runReaderT action =<< initialDebuggerState -- | The logger action used to log GHC output debuggerLoggerAction :: Handle -> LogAction debuggerLoggerAction h a b c d = do hSetEncoding h utf8 -- GHC output uses utf8 defaultLogActionWithHandles h h a b c d -- | Registers or deletes a breakpoint in the GHC session and from the list of -- active breakpoints that is kept in 'DebuggerState', depending on the -- 'BreakpointStatus' being set. -- -- Returns @True@ when the breakpoint status is changed. #if MIN_VERSION_ghc(9,14,2) registerBreakpoint :: GHC.BreakpointId -> BreakpointStatus -> BreakpointKind -> Debugger (Bool, [GHC.InternalBreakpointId]) #else registerBreakpoint :: GHC.BreakpointId -> BreakpointStatus -> BreakpointKind -> Debugger Bool #endif registerBreakpoint bp@GHC.BreakpointId { GHC.bi_tick_mod = mod , GHC.bi_tick_index = bid } status kind = do -- Set breakpoint in GHC session let breakpoint_count = breakpointStatusInt status hsc_env <- GHC.getSession #if MIN_VERSION_ghc(9,14,2) internal_break_ids <- getInternalBreaksOf bp forM_ internal_break_ids $ \ibi -> do GHC.setupBreakpoint (hscInterp hsc_env) ibi breakpoint_count #else GHC.setupBreakpoint (hscInterp hsc_env) bp breakpoint_count #endif -- Register breakpoint in Debugger state brksRef <- asks activeBreakpoints oldBrks <- liftIO $ readIORef brksRef let (newBrks, changed) = case status of -- Disabling the breakpoint; using the `Maybe` monad: -- * If we reach the return stmt then the breakpoint is active and we delete it. -- * Any other case, return False and change Nothing BreakpointDisabled -> fromMaybe (oldBrks, False) $ do im <- lookupModuleEnv oldBrks mod _status <- IM.lookup bid im let im' = IM.delete bid im brks = extendModuleEnv oldBrks mod im' return (brks, True) -- We're enabling the breakpoint: _ -> case lookupModuleEnv oldBrks mod of Nothing -> let im = IM.singleton bid (status, kind) brks = extendModuleEnv oldBrks mod im in (brks, True) Just im -> case IM.lookup bid im of Nothing -> -- Not yet in IntMap, extend with new Breakpoint let im' = IM.insert bid (status, kind) im brks = extendModuleEnv oldBrks mod im' in (brks, True) Just (status', _kind) -> -- Found in IntMap if status' == status then (oldBrks, False) else let im' = IM.insert bid (status, kind) im brks = extendModuleEnv oldBrks mod im' in (brks, True) -- no races since the debugger execution is run in a single thread liftIO $ writeIORef brksRef newBrks #if MIN_VERSION_ghc(9,14,2) return (changed, internal_break_ids) #else return changed #endif -- | Get a list with all currently active breakpoints on the given module (by path) -- -- If the path argument is @Nothing@, get all active function breakpoints instead #if MIN_VERSION_ghc(9,14,2) getActiveBreakpoints :: Maybe FilePath -> Debugger [GHC.InternalBreakpointId] #else getActiveBreakpoints :: Maybe FilePath -> Debugger [GHC.BreakpointId] #endif getActiveBreakpoints mfile = do m <- asks activeBreakpoints >>= liftIO . readIORef case mfile of Just file -> do mms <- getModuleByPath file case mms of Right ms -> #if MIN_VERSION_ghc(9,14,2) concat <$> mapM getInternalBreaksOf #else return #endif [ GHC.BreakpointId mod bix | (mod, im) <- moduleEnvToList m , mod == ms_mod ms , bix <- IM.keys im -- assert: status is always > disabled ] Left e -> do displayWarnings [e] return [] Nothing -> do #if MIN_VERSION_ghc(9,14,2) concat <$> mapM getInternalBreaksOf #else return #endif [ GHC.BreakpointId mod bix | (mod, im) <- moduleEnvToList m , (bix, (status, kind)) <- IM.assocs im -- Keep only function breakpoints in this case , FunctionBreakpointKind == kind , assert (status > BreakpointDisabled) True ] -- | List all loaded modules 'ModSummary's getAllLoadedModules :: GHC.GhcMonad m => m [GHC.ModSummary] getAllLoadedModules = (GHC.mgModSummaries <$> GHC.getModuleGraph) >>= filterM (\ms -> GHC.isLoadedModule (GHC.ms_unitid ms) (GHC.ms_mod_name ms)) -- | Get a 'ModSummary' of a loaded module given its 'FilePath' getModuleByPath :: FilePath -> Debugger (Either String ModSummary) getModuleByPath path = do -- do this every time as the loaded modules may have changed lms <- getAllLoadedModules absPath <- liftIO $ makeAbsolute path let matches ms = normalise (msHsFilePath ms) == normalise absPath return $ case filter matches lms of [x] -> Right x [] -> Left $ "No module matched " ++ path ++ ".\nLoaded modules:\n" ++ show (map msHsFilePath lms) ++ "\n. Perhaps you've set a breakpoint on a module that isn't loaded into the session?" xs -> Left $ "Too many modules (" ++ showPprUnsafe xs ++ ") matched " ++ path ++ ". Please report a bug at https://github.com/well-typed/haskell-debugger." -------------------------------------------------------------------------------- -- Variable references -------------------------------------------------------------------------------- -- | Find a variable's associated Term and Name by reference ('Int') lookupVarByReference :: Int -> Debugger (Maybe TermKey) lookupVarByReference i = do ioref <- asks varReferences (rm, _) <- readIORef ioref & liftIO return $ IM.lookup i rm -- | Finds or creates an integer var reference for the given 'TermKey'. -- TODO: Arguably, this mapping should be part of the debug-adapter, and -- haskell-debugger should deal in 'TermKey' terms only. getVarReference :: TermKey -> Debugger Int getVarReference key = do ioref <- asks varReferences (rm, tkm) <- readIORef ioref & liftIO (i, tkm') <- case lookupTermKeyMap key tkm of Nothing -> do new_i <- freshInt return (new_i, insertTermKeyMap key new_i tkm) Just existing_i -> return (existing_i, tkm) let rm' = IM.insert i key rm writeIORef ioref (rm', tkm') & liftIO return i -- | Whenever we run a request that continues execution from the current -- suspended state, such as Next,Step,Continue, this function should be called -- to delete the variable references that become invalid as we leave the -- suspended state. -- -- In particular, @'varReferences'@ is reset. -- -- See also section "Lifetime of Objects References" in the DAP specification. leaveSuspendedState :: Debugger () leaveSuspendedState = do ioref <- asks varReferences liftIO $ writeIORef ioref mempty -------------------------------------------------------------------------------- -- Utilities -------------------------------------------------------------------------------- defaultDepth :: Int defaultDepth = 2 -- the depth determines how much of the runtime structure is traversed. -- @obtainTerm@ and friends handle fetching arbitrarily nested data structures -- so we only depth enough to get to the next level of subterms. -- | Evaluate a suspended Term to WHNF. -- -- Used in @'getVariables'@ to reply to a variable introspection request. seqTerm :: Term -> Debugger Term seqTerm term = do hsc_env <- GHC.getSession let interp = hscInterp hsc_env unit_env = hsc_unit_env hsc_env case term of Suspension{val, ty} -> liftIO $ do r <- GHCi.seqHValue interp unit_env val () <- fromEvalResult r let forceThunks = False {- whether to force the thunk subterms -} forceDepth = defaultDepth cvObtainTerm hsc_env forceDepth forceThunks ty val NewtypeWrap{wrapped_term} -> do wrapped_term' <- seqTerm wrapped_term return term{wrapped_term=wrapped_term'} _ -> return term -- | Evaluate a Term to NF deepseqTerm :: Term -> Debugger Term deepseqTerm t = case t of Suspension{} -> do t' <- seqTerm t deepseqTerm t' Term{subTerms} -> do subTerms' <- mapM deepseqTerm subTerms return t{subTerms = subTerms'} NewtypeWrap{wrapped_term} -> do wrapped_term' <- deepseqTerm wrapped_term return t{wrapped_term = wrapped_term'} _ -> do seqTerm t -- | Resume execution with single step mode 'RunToCompletion', skipping all breakpoints we hit, until we reach 'ExecComplete'. -- -- We use this in 'doEval' because we want to ignore breakpoints in expressions given at the prompt. continueToCompletion :: Debugger GHC.ExecResult continueToCompletion = do execr <- GHC.resumeExec GHC.RunToCompletion Nothing case execr of GHC.ExecBreak{} -> continueToCompletion GHC.ExecComplete{} -> return execr -- | Turn a 'BreakpointStatus' into its 'Int' representation for 'BreakArray' breakpointStatusInt :: BreakpointStatus -> Int breakpointStatusInt = \case BreakpointEnabled -> BA.breakOn -- 0 BreakpointDisabled -> BA.breakOff -- -1 BreakpointAfterCount n -> n -- n -- | Generate a new unique 'Int' freshInt :: Debugger Int freshInt = do ioref <- asks genUniq i <- readIORef ioref & liftIO let !i' = i+1 writeIORef ioref i' & liftIO return i -- | Initialize a 'DebuggerState' initialDebuggerState :: GHC.Ghc DebuggerState initialDebuggerState = DebuggerState <$> liftIO (newIORef emptyModuleEnv) <*> liftIO (newIORef mempty) <*> liftIO (newIORef mempty) <*> liftIO (newIORef 0) -- | Lift a 'Ghc' action into a 'Debugger' one. liftGhc :: GHC.Ghc a -> Debugger a liftGhc = Debugger . ReaderT . const -------------------------------------------------------------------------------- type Warning = String displayWarnings :: [Warning] -> Debugger () displayWarnings = liftIO . putStrLn . unlines -------------------------------------------------------------------------------- -- Instances -------------------------------------------------------------------------------- instance GHC.HasLogger Debugger where getLogger = liftGhc GHC.getLogger instance GHC.GhcMonad Debugger where getSession = liftGhc GHC.getSession setSession s = liftGhc $ GHC.setSession s -------------------------------------------------------------------------------- #if MIN_VERSION_ghc(9,14,2) -- | Find all the internal breakpoints that use the given source-level breakpoint id getInternalBreaksOf :: BreakpointId -> Debugger [InternalBreakpointId] getInternalBreaksOf bi = do bs <- mkBreakpointOccurrences return $ fromMaybe [] {- still not found after refresh -} $ lookupBreakpointOccurrences bs bi #endif