ghc-9.14.1: GHC/Runtime/Debugger/Breakpoints.hs
-- | GHC API debugger module for finding and setting breakpoints.
--
-- This module is user facing and is at least used by `GHCi` and `ghc-debugger`
-- to find and set breakpoints.
module GHC.Runtime.Debugger.Breakpoints where
import GHC.Prelude
import Control.Monad.Catch
import Control.Monad
import Data.Array
import Data.Function
import Data.List
import Data.Maybe
import qualified Data.List.NonEmpty as NE
import qualified Data.Semigroup as S
import GHC.HsToCore.Breakpoints
import GHC.ByteCode.Breakpoints
import GHC.Driver.Env
import GHC.Driver.Monad
import GHC.Driver.Session.Inspect
import GHC.Runtime.Eval
import GHC.Runtime.Eval.Utils
import GHC.Types.Name
import GHC.Types.SrcLoc
import GHC.Unit.Module
import GHC.Unit.Module.Graph
import GHC.Unit.Module.ModSummary
import GHC.Utils.Outputable
import GHC.Utils.Panic
import qualified GHC.Data.Strict as Strict
import qualified Data.IntMap.Strict as IntMap
import qualified GHC.Unit.Home.Graph as HUG
import qualified GHC.Unit.Home.PackageTable as HPT
--------------------------------------------------------------------------------
-- Finding Module breakpoints
--------------------------------------------------------------------------------
-- | Find a breakpoint given a Module's 'TickArray' and the line number.
--
-- When a line number is specified, the current policy for choosing
-- the best breakpoint is this:
-- - the leftmost complete subexpression on the specified line, or
-- - the leftmost subexpression starting on the specified line, or
-- - the rightmost subexpression enclosing the specified line
--
findBreakByLine :: Int {-^ Line number -} -> TickArray -> Maybe (BreakTickIndex, RealSrcSpan)
findBreakByLine line arr
| not (inRange (bounds arr) line) = Nothing
| otherwise =
listToMaybe (sortBy (leftmostLargestRealSrcSpan `on` snd) comp) `mplus`
listToMaybe (sortBy (compare `on` snd) incomp) `mplus`
listToMaybe (sortBy (flip compare `on` snd) ticks)
where
ticks = arr ! line
starts_here = [ (ix,pan) | (ix, pan) <- ticks,
srcSpanStartLine pan == line ]
(comp, incomp) = partition ends_here starts_here
where ends_here (_,pan) = srcSpanEndLine pan == line
-- | Find a breakpoint in the 'TickArray' of a module, given a line number and a column coordinate.
findBreakByCoord :: (Int, Int) -> TickArray -> Maybe (BreakTickIndex, RealSrcSpan)
findBreakByCoord (line, col) arr
| not (inRange (bounds arr) line) = Nothing
| otherwise =
listToMaybe (sortBy (flip compare `on` snd) contains ++
sortBy (compare `on` snd) after_here)
where
ticks = arr ! line
-- the ticks that span this coordinate
contains = [ tick | tick@(_,pan) <- ticks, RealSrcSpan pan Strict.Nothing `spans` (line,col) ]
after_here = [ tick | tick@(_,pan) <- ticks,
srcSpanStartLine pan == line,
srcSpanStartCol pan >= col ]
leftmostLargestRealSrcSpan :: RealSrcSpan -> RealSrcSpan -> Ordering
leftmostLargestRealSrcSpan = on compare realSrcSpanStart S.<> on (flip compare) realSrcSpanEnd
-- | Returns the span of the largest tick containing the srcspan given
enclosingTickSpan :: TickArray -> SrcSpan -> RealSrcSpan
enclosingTickSpan _ (UnhelpfulSpan _) = panic "enclosingTickSpan UnhelpfulSpan"
enclosingTickSpan ticks (RealSrcSpan src _) =
assert (inRange (bounds ticks) line) $
Data.List.minimumBy leftmostLargestRealSrcSpan $ enclosing_spans
where
line = srcSpanStartLine src
enclosing_spans = [ pan | (_,pan) <- ticks ! line
, realSrcSpanEnd pan >= realSrcSpanEnd src]
--------------------------------------------------------------------------------
-- Finding Function breakpoints
--------------------------------------------------------------------------------
-- | Process and validate the user string of form @[Module.]function@ into the
-- relevant module information and function name.
--
-- Validation guarantees
-- 1. The module exists
-- 2. The identifier is in an interpreted module
-- 3. The identifier has a breakpoint entry in the module's 'ModBreaks'
--
-- Returns either an error SDoc or the 'Module' and 'ModuleInfo' for the relevant module
-- paired with the function name
--
-- See also Note [Setting Breakpoints by Id]
resolveFunctionBreakpoint :: GhcMonad m => String -> m (Either SDoc (Module, ModuleInfo, String))
resolveFunctionBreakpoint inp = do
let (mod_str, top_level, fun_str) = splitIdent inp
mod_top_lvl = combineModIdent mod_str top_level
mb_mod <- catch (lookupModuleInscope mod_top_lvl)
(\(_ :: SomeException) -> lookupModuleInGraph mod_str)
-- If the top-level name is not in scope, `lookupModuleInscope` will
-- throw an exception, then lookup the module name in the module graph.
mb_err_msg <- validateBP mod_str fun_str mb_mod
case mb_err_msg of
Just err_msg -> pure . Left $
text "Cannot set breakpoint on" <+> quotes (text inp)
<> text ":" <+> err_msg
Nothing -> do
-- No errors found, go and return the module info
let mod = fromMaybe (panic "resolveFunctionBreakpoint") mb_mod
mb_mod_info <- getModuleInfo mod
case mb_mod_info of
Nothing -> pure . Left $
text "Could not find ModuleInfo of " <> ppr mod
Just mod_info -> pure $ Right (mod, mod_info, fun_str)
where
-- Try to lookup the module for an identifier that is in scope.
-- `parseName` throws an exception, if the identifier is not in scope
lookupModuleInscope :: GhcMonad m => String -> m (Maybe Module)
lookupModuleInscope mod_top_lvl = do
names <- parseName mod_top_lvl
pure $ Just $ NE.head $ nameModule <$> names
-- Lookup the Module of a module name in the module graph
lookupModuleInGraph :: GhcMonad m => String -> m (Maybe Module)
lookupModuleInGraph mod_str = do
graph <- getModuleGraph
let hmods = ms_mod <$> mgModSummaries graph
pure $ find ((== mod_str) . moduleNameString . moduleName) hmods
-- Check validity of an identifier to set a breakpoint:
-- 1. The module of the identifier must exist
-- 2. the identifier must be in an interpreted module
-- 3. the ModBreaks array for module `mod` must have an entry
-- for the function
validateBP :: GhcMonad m => String -> String -> Maybe Module
-> m (Maybe SDoc)
validateBP mod_str fun_str Nothing = pure $ Just $ quotes (text
(combineModIdent mod_str (takeWhile (/= '.') fun_str)))
<+> text "not in scope"
validateBP _ "" (Just _) = pure $ Just $ text "Function name is missing"
validateBP _ fun_str (Just modl) = do
isInterpr <- moduleIsInterpreted modl
mb_err_msg <- case isInterpr of
False -> pure $ Just $ text "Module" <+> quotes (ppr modl) <+> text "is not interpreted"
True -> do
mb_modbreaks <- getModBreak modl
let found = case mb_modbreaks of
Nothing -> False
Just mb -> fun_str `elem` (intercalate "." <$> elems (modBreaks_decls mb))
if found
then pure Nothing
else pure $ Just $ text "No breakpoint found for" <+> quotes (text fun_str)
<+> text "in module" <+> quotes (ppr modl)
pure mb_err_msg
-- | The aim of this function is to find the breakpoints for all the RHSs of
-- the equations corresponding to a binding. So we find all breakpoints
-- for
-- (a) this binder only (it maybe a top-level or a nested declaration)
-- (b) that do not have an enclosing breakpoint
findBreakForBind :: String {-^ Name of bind to break at -} -> ModBreaks -> [(BreakTickIndex, RealSrcSpan)]
findBreakForBind str_name modbreaks = filter (not . enclosed) ticks
where
ticks = [ (index, span)
| (index, decls) <- assocs (modBreaks_decls modbreaks),
str_name == intercalate "." decls,
RealSrcSpan span _ <- [modBreaks_locs modbreaks ! index] ]
enclosed (_,sp0) = any subspan ticks
where subspan (_,sp) = sp /= sp0 &&
realSrcSpanStart sp <= realSrcSpanStart sp0 &&
realSrcSpanEnd sp0 <= realSrcSpanEnd sp
--------------------------------------------------------------------------------
-- Mapping line numbers to ticks
--------------------------------------------------------------------------------
-- | Maps line numbers to the breakpoint ticks existing at that line for a module.
type TickArray = Array Int [(BreakTickIndex,RealSrcSpan)]
-- | Construct the 'TickArray' for the given module.
makeModuleLineMap :: GhcMonad m => Module -> m (Maybe TickArray)
makeModuleLineMap m = do
mi <- getModuleInfo m
return $ mkTickArray . assocs . modBreaks_locs . imodBreaks_modBreaks <$> (modInfoModBreaks =<< mi)
where
mkTickArray :: [(BreakTickIndex, SrcSpan)] -> TickArray
mkTickArray ticks
= accumArray (flip (:)) [] (1, max_line)
[ (line, (nm,pan)) | (nm,RealSrcSpan pan _) <- ticks, line <- srcSpanLines pan ]
where
max_line = foldr max 0 [ srcSpanEndLine sp | (_, RealSrcSpan sp _) <- ticks ]
srcSpanLines pan = [ srcSpanStartLine pan .. srcSpanEndLine pan ]
-- | Get the 'ModBreaks' of the given 'Module' when available
getModBreak :: GhcMonad m => Module -> m (Maybe ModBreaks)
getModBreak m = do
mod_info <- fromMaybe (panic "getModBreak") <$> getModuleInfo m
pure $ imodBreaks_modBreaks <$> modInfoModBreaks mod_info
--------------------------------------------------------------------------------
-- Mapping source-level BreakpointIds to IBI occurrences
-- (See Note [Breakpoint identifiers])
--------------------------------------------------------------------------------
-- | A source-level breakpoint may have been inlined into many occurrences, now
-- referred by 'InternalBreakpointId'. When a breakpoint is set on a certain
-- source breakpoint, it means all *ocurrences* of that breakpoint across
-- modules should be stopped at -- hence we keep a trie from BreakpointId to
-- the list of internal break ids using it.
-- See also Note [Breakpoint identifiers]
type BreakpointOccurrences = ModuleEnv (IntMap.IntMap [InternalBreakpointId])
-- | Lookup all InternalBreakpointIds matching the given BreakpointId
-- Nothing if BreakpointId not in map
lookupBreakpointOccurrences :: BreakpointOccurrences -> BreakpointId -> Maybe [InternalBreakpointId]
lookupBreakpointOccurrences bmp (BreakpointId md tick) =
lookupModuleEnv bmp md >>= IntMap.lookup tick
-- | Construct a mapping from Source 'BreakpointId's to 'InternalBreakpointId's from the given list of 'ModInfo's
mkBreakpointOccurrences :: forall m. GhcMonad m => m BreakpointOccurrences
mkBreakpointOccurrences = do
hug <- hsc_HUG <$> getSession
liftIO $ foldr go (pure emptyModuleEnv) hug
where
go :: HUG.HomeUnitEnv -> IO BreakpointOccurrences -> IO BreakpointOccurrences
go hue mbmp = do
bmp <- mbmp
ibrkss <- HPT.concatHpt (\hmi -> maybeToList (getModBreaks hmi))
(HUG.homeUnitEnv_hpt hue)
return $ foldr addBreakToMap bmp ibrkss
addBreakToMap :: InternalModBreaks -> BreakpointOccurrences -> BreakpointOccurrences
addBreakToMap ibrks bmp0 = do
let imod = modBreaks_module $ imodBreaks_modBreaks ibrks
IntMap.foldrWithKey (\info_ix cgi bmp -> do
let ibi = InternalBreakpointId imod info_ix
case cgb_tick_id cgi of
Right (BreakpointId tick_mod tick_ix)
-> extendModuleEnvWith (IntMap.unionWith (S.<>)) bmp tick_mod (IntMap.singleton tick_ix [ibi])
Left _
-- Do not include internal breakpoints in the visible breakpoint
-- occurrences!
-> bmp
) bmp0 (imodBreaks_breakInfo ibrks)
--------------------------------------------------------------------------------
-- Getting current breakpoint information
--------------------------------------------------------------------------------
getCurrentBreakSpan :: GhcMonad m => m (Maybe SrcSpan)
getCurrentBreakSpan = do
hug <- hsc_HUG <$> getSession
resumes <- getResumeContext
case resumes of
[] -> return Nothing
(r:_) -> do
let ix = resumeHistoryIx r
if ix == 0
then return (Just (resumeSpan r))
else do
let hist = resumeHistory r !! (ix-1)
pan <- liftIO $ getHistorySpan hug hist
return (Just pan)
getCurrentBreakModule :: GhcMonad m => m (Maybe Module)
getCurrentBreakModule = do
resumes <- getResumeContext
hug <- hsc_HUG <$> getSession
liftIO $ case resumes of
[] -> pure Nothing
(r:_) -> case resumeHistoryIx r of
0 -> case resumeBreakpointId r of
Nothing -> pure Nothing
Just ibi -> do
brks <- readIModBreaks hug ibi
return $ Just $ getBreakSourceMod ibi brks
ix ->
Just <$> getHistoryModule hug (resumeHistory r !! (ix-1))