haskell-debugger-0.12.0.0: haskell-debugger/GHC/Debugger/Stopped/Exception.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultilineStrings #-}
-- | Helpers used when the debugger is stopped due to an exception.
-- These helpers execute code on the remote process which teach us information
-- about the exception we are stopped at.
module GHC.Debugger.Stopped.Exception
( exceptionSourceSpanFromContext
, getExceptionInfo
, fallbackExceptionSourceSpan
, defaultExceptionInfo
, currentlyStoppedOnException
) where
import Data.Maybe (fromMaybe, isNothing)
import GHC
import GHC.Types.SrcLoc
import GHC.Data.FastString (unpackFS)
import GHC.Utils.Outputable as Ppr
import GHC.Debugger.Monad
import GHC.Debugger.Interface.Messages
( SourceSpan(..)
, ExceptionInfo(..)
, RemoteThreadId(..)
)
import qualified Colog.Core as Logger
import GHC.Debugger.Runtime.Thread
import qualified GHC.Debugger.Runtime.Eval.RemoteExpr as Remote
import GHC.Debugger.Runtime.Term.Parser
import GHCi.RemoteTypes (castForeignRef)
import GHC.Builtin.Types (anyTy)
-- | Attempt to obtain a more precise 'SourceSpan' for the exception we stopped
-- on by consulting its context, if available.
exceptionSourceSpanFromContext :: Debugger (Maybe SourceSpan)
exceptionSourceSpanFromContext = do
GHC.getResumeContext >>= \case
r:_ | Nothing <- GHC.resumeBreakpointId r -> do
let excRef = resumeApStack r
evalRes <- Remote.eval
(Remote.raw exceptionLocationExpr `Remote.app` Remote.untypedRef excRef)
case evalRes of
Left err -> do
logSDoc Logger.Debug $
Ppr.text "Failed to evaluate exception context:" Ppr.<+> Ppr.text (show err)
return Nothing
Right fhv -> do
parsed <- obtainParsedTerm "Exception context location" 4 True anyTy (castForeignRef fhv)
(maybeParser exceptionLocationTupleParser)
case parsed of
Left errs -> do
logSDoc Logger.Debug $
Ppr.text "Failed to parse exception context location:"
Ppr.<+> Ppr.vcat (map (Ppr.text . getTermErrorMessage) errs)
return Nothing
Right Nothing -> return Nothing
Right (Just (file, srcLine, col)) ->
return $ Just SourceSpan
{ file = file
, startLine = srcLine
, startCol = col
, endLine = srcLine
, endCol = col
}
_ -> return Nothing
exceptionLocationTupleParser :: TermParser (String, Int, Int)
exceptionLocationTupleParser =
(,,) <$> subtermWith 0 stringParser
<*> subtermWith 1 intParser
<*> subtermWith 2 intParser
-- | This helper looks at the exception context for an exception, and retrieves
-- the last entry of the HasCallStack backtrace.
exceptionLocationExpr :: String
exceptionLocationExpr ="""
let
fromCallStack cs = case Data.Maybe.listToMaybe (GHC.Exception.getCallStack cs) of
Just (_, loc) -> Just ( GHC.Exception.srcLocFile loc
, GHC.Exception.srcLocStartLine loc
, GHC.Exception.srcLocStartCol loc)
go exc =
let ctx = Control.Exception.someExceptionContext exc
bts :: [Control.Exception.Backtrace.Backtraces]
bts = Control.Exception.Context.getExceptionAnnotations ctx
in case bts of
bt : _ -> case GHC.Internal.Exception.Backtrace.btrHasCallStack bt of
Just cs -> fromCallStack cs
Nothing -> Nothing
[] -> Nothing
in go
"""
-- | Retrieve structured exception information for the requested thread when
-- the debugger is currently stopped on an exception.
getExceptionInfo :: RemoteThreadId -> Debugger ExceptionInfo
getExceptionInfo req_tid = GHC.getResumeContext >>= \case
[] -> return defaultExceptionInfo
r:_ -> do
r_tid <- getRemoteThreadIdFromRemoteContext (GHC.resumeContext r)
case (r_tid == req_tid, GHC.resumeBreakpointId r) of
(True, Nothing) -> do
let excRef = resumeApStack r
fromMaybe defaultExceptionInfo <$> exceptionInfoFromContext excRef
_ -> return defaultExceptionInfo
-- | Evaluate helper code inside the debuggee that turns the exception context
-- into our 'ExceptionInfo' structure.
exceptionInfoFromContext :: ForeignHValue -> Debugger (Maybe ExceptionInfo)
exceptionInfoFromContext excRef = do
-- 1. Add a "data" declaration for the datatype the expression will return
_ <- runDecls exceptionInfoData
-- 2. Gather information about the exception.
evalRes <- Remote.eval
(Remote.raw exceptionInfoExpr `Remote.app` Remote.untypedRef excRef)
case evalRes of
Left err -> do
logSDoc Logger.Debug $
Ppr.text "Failed to evaluate exception info:" Ppr.<+> Ppr.text (show err)
return Nothing
Right fhv -> do
parsed <- obtainParsedTerm "Exception info" 4 True anyTy (castForeignRef fhv)
exceptionInfoParser
case parsed of
Left errs -> do
logSDoc Logger.Debug $
Ppr.text "Failed to parse exception info:"
Ppr.<+> Ppr.vcat (map (Ppr.text . getTermErrorMessage) errs)
return Nothing
Right info -> return (Just info)
-- | Parse the helper 'ExceptionInfoNode' structure produced inside the
-- debuggee into our externally facing 'ExceptionInfo'.
exceptionInfoParser :: TermParser ExceptionInfo
exceptionInfoParser = do
ExceptionInfo
<$> subtermWith 0 stringParser
<*> subtermWith 1 stringParser
<*> subtermWith 2 stringParser
<*> subtermWith 3 (maybeParser stringParser)
<*> subtermWith 4 (parseList exceptionInfoParser)
-- | Definition for the helper 'ExceptionInfoNode' data type compiled into the
-- debuggee to aid in transporting nested exception information.
-- We need a specific datatype because ExceptionInfoNode is recursive.
exceptionInfoData :: String
exceptionInfoData =
"data ExceptionInfoNode = ExceptionInfoNode String String String (Maybe String) [ExceptionInfoNode]"
-- | Helper expression run in the debuggee that walks the exception context and
-- populates the 'ExceptionInfoNode' structure.
exceptionInfoExpr :: String
exceptionInfoExpr = """
let collectExceptionInfo :: SomeException -> ExceptionInfoNode
collectExceptionInfo se' =
case se' of
SomeException exc ->
let ctx = Control.Exception.someExceptionContext se'
rendered = Control.Exception.Context.displayExceptionContext ctx
whileHandling = Control.Exception.Context.getExceptionAnnotations ctx
innerNodes = map (collectExceptionInfo . unwrap) whileHandling
simpleTypeName = Data.Typeable.tyConName tc
modulePrefix = case Data.Typeable.tyConModule tc of
mdl | null mdl -> \"\"
| otherwise -> mdl ++ \".\"
packagePrefix = case Data.Typeable.tyConPackage tc of
pkg | null pkg -> \"\"
| otherwise -> pkg ++ \":\"
tc = Data.Typeable.typeRepTyCon (Data.Typeable.typeOf exc)
fullTypeName = packagePrefix ++ modulePrefix ++ simpleTypeName
unwrap (Control.Exception.WhileHandling inner) = inner
contextText = if null rendered then Nothing else Just rendered
in ExceptionInfoNode
simpleTypeName
fullTypeName
(Control.Exception.displayException se')
contextText
innerNodes
in collectExceptionInfo
"""
-- | When no precise exception location is available, fall back to displaying a
-- label derived from the provided 'SrcSpan'.
fallbackExceptionSourceSpan :: Maybe SrcSpan -> SourceSpan
fallbackExceptionSourceSpan mspan =
let fileLabel = maybe "<exception>" spanLabel mspan
in SourceSpan
{ file = fileLabel
, startLine = 0
, startCol = 0
, endLine = 0
, endCol = 0
}
where
spanLabel (RealSrcSpan rss _) = unpackFS (srcSpanFile rss)
spanLabel (UnhelpfulSpan reason) = unpackFS (unhelpfulSpanFS reason)
-- | Placeholder exception info returned when the context could not be
-- inspected.
defaultExceptionInfo :: ExceptionInfo
defaultExceptionInfo = ExceptionInfo
{ exceptionInfoTypeName = "Exception"
, exceptionInfoFullTypeName = "Exception"
, exceptionInfoMessage = "Exception information not available"
, exceptionInfoContext = Nothing
, exceptionInfoInner = []
}
-- | Determine whether the debugger is currently stopped because of an
-- exception (as opposed to a breakpoint).
currentlyStoppedOnException :: Debugger Bool
currentlyStoppedOnException = do
resumes <- GHC.getResumeContext
return $ case resumes of
[] -> False
r:_ -> isNothing (GHC.resumeBreakpointId r)