packages feed

haskell-debugger-0.13.0.0: haskell-debugger/GHC/Debugger/Stopped.hs

{-# LANGUAGE CPP, NamedFieldPuns, TupleSections, LambdaCase,
   DuplicateRecordFields, RecordWildCards, TupleSections, ViewPatterns,
   TypeApplications, ScopedTypeVariables, BangPatterns, MultiWayIf, OverloadedRecordDot #-}
module GHC.Debugger.Stopped where

import Control.Monad
import Control.Monad.Reader
import Data.IORef
import qualified Data.List as L

import GHC
import GHC.Types.Unique.FM
import GHC.Types.Name.Occurrence (sizeOccEnv)
import GHC.ByteCode.Breakpoints
import GHC.Types.Name.Reader
import GHC.Unit.Home.ModInfo
import GHC.Unit.Module.ModDetails
import GHC.Types.TypeEnv
import GHC.Data.Maybe
import GHC.Driver.Env as GHC
import GHC.Runtime.Eval
import GHC.Types.SrcLoc
import GHC.InfoProv
import GHC.Utils.Outputable as Ppr
import qualified GHC.Unit.Home.Graph as HUG

import GHC.Debugger.Stopped.Exception
import GHC.Debugger.Stopped.Variables
import GHC.Debugger.Runtime
import GHC.Debugger.Runtime.Thread
import GHC.Debugger.Runtime.Thread.Stack
import GHC.Debugger.Runtime.Thread.Map
import GHC.Debugger.Monad
import GHC.Debugger.Interface.Messages
import qualified GHC.Debugger.Interface.Messages as DbgStackFrame (DbgStackFrame(..))
import GHC.Debugger.Utils
import qualified Colog.Core as Logger

{-
Note [Don't crash if not stopped]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Requests such as `stacktrace`, `scopes`, or `variables` may end up
coming after the execution of a program has terminated. For instance,
consider this interleaving:

1. SENT Stopped event         <-- we're stopped
2. RECEIVED StackTrace req    <-- client issues after stopped event
3. RECEIVED Next req          <-- user clicks step-next
4. <program execution resumes and fails>
5. SENT Terminate event       <-- execution failed and we report it to exit cleanly
6. RECEIVED Scopes req        <-- happens as a sequence of 2 that wasn't canceled
7. <used to crash! because we're no longer at a breakpoint>

Now, we simply returned empty responses when these requests come in
while we're no longer at a breakpoint. The client will soon come to a halt
because of the termination event we sent.
-}

--------------------------------------------------------------------------------
-- * Threads
--------------------------------------------------------------------------------

getThreads :: Debugger [DebuggeeThread]
getThreads = do
  -- TODO: we want something more like 'listThreads', but ensure that we only
  -- report the threads of the debuggee (and not the debugger, if they
  -- are the same process). Perhaps the solution is to not allow them to be in
  -- the same process, in which case 'listThreads' would be correct as is by
  -- construction.
  --
  -- For now, we approximate by just listing out the ThreadsMap, under the
  -- assumption the debugger client will only care about threads we've already
  -- stopped at (which are the only ones we've inserted in the threads map),
  -- but for full multi threaded debugging we need the listThreads.
  --
  -- tmap <- liftIO . readIORef =<< asks threadMap
  -- let (t_ids, remote_refs) = unzip (threadMapToList tmap)
  --
  -- Oh, try the listThreads just for fun.
  (t_ids, t_infos) <- unzip <$> listAllLiveRemoteThreads
  let
    _mkDebuggeeThread tid tinfo
      = DebuggeeThread
        { tId = tid
        , tName = tinfo.threadInfoLabel
        }
    _all_threads
      = zipWith _mkDebuggeeThread t_ids t_infos

  -- TODO: We ignore _all_threads and report only the main execution thread for now.
  -- See #138 for progress on Multi-threaded debugging.
  GHC.getResumeContext >>= \case
    [] ->
      -- See Note [Don't crash if not stopped]
      return []
    r:_ -> do
      r_tid <- getRemoteThreadIdFromRemoteContext (GHC.resumeContext r)
      return
        [ DebuggeeThread
          { tId = r_tid
          , tName = Just "Main Thread"
          }
        ]

--------------------------------------------------------------------------------
-- * Stack trace
--------------------------------------------------------------------------------

-- | Get the stack frames at the point we're stopped at
getStacktrace :: RemoteThreadId -> Debugger [DbgStackFrame]
getStacktrace req_tid = do

  tm <- liftIO . readIORef =<< asks threadMap
  let m_f_tid = lookupThreadMap (remoteThreadIntRef req_tid) tm

  hsc_env <- getSession
  let hug = hsc_HUG hsc_env
  decoded_frames <- catMaybes <$> case m_f_tid of
    Nothing -> pure []
    Just f_tid -> do
      -- Try decoding a stack with interpreter continuation frames (RetBCOs)
      -- and use the BRK_FUN src locations.
      stack_frames <- getRemoteThreadStackCopy f_tid
      forM stack_frames $ \case
        StackFrameBreakpointInfo ibi -> do
          info_brks <- liftIO $ readIModBreaks hug ibi
          let modl  = getBreakSourceMod ibi info_brks
          srcSpan   <- liftIO $ getBreakLoc (readIModModBreaks hug) ibi info_brks
          decl <- liftIO $ L.intercalate "." <$> getBreakDecls (readIModModBreaks hug) ibi info_brks

          modl_str  <- display modl
          return $ Just DbgStackFrame
            { name = modl_str ++ "." ++ decl
            , sourceSpan = realSrcSpanToSourceSpan $ realSrcSpan srcSpan
            , breakId = Just ibi
            }
        StackFrameIPEInfo ipe -> do
          case srcSpanStringToSourceSpan (ipLoc ipe) of
            Left err -> do
              -- Couldn't parse. The srcLoc may be invalid so just keep this as info, not warning.
              logSDoc Logger.Info $
                text "Couldn't parse StackEntry srcLoc \"" Ppr.<> text (ipLoc ipe)
                                                           Ppr.<> text "\":" <+> text err
              return Nothing
            Right sourceSpan ->
              return $ Just DbgStackFrame
                { name = ipMod ipe ++ "." ++ ipLabel ipe
                , sourceSpan = sourceSpan
                , breakId = Nothing
                }
        StackFrameAnnotation srcLoc ann -> do
            return $ Just DbgStackFrame
              { name = ann
              , sourceSpan = maybe unhelpfulSourceSpan srcLocToSourceSpan srcLoc
              , breakId = Nothing
              }

  -- Add the latest resume context at the head.
  head_frame <- GHC.getResumeContext >>= \case
    [] ->
      -- See Note [Don't crash if not stopped]
      return Nothing
    r:_ -> do
      let resumeSpanR = GHC.resumeSpan r
          mRealSpan   = realSrcSpanToSourceSpan <$> srcSpanToRealSrcSpan resumeSpanR
          firstSpan   = DbgStackFrame.sourceSpan <$> listToMaybe decoded_frames
      r_tid <- getRemoteThreadIdFromRemoteContext (GHC.resumeContext r)
      if r_tid /= req_tid then
        return Nothing
      else case GHC.resumeBreakpointId r of
        Just ibi
          | Just ss <- mRealSpan
          , Just ss /= firstSpan -> do
              -- We're getting the stacktrace for the thread we're stopped at.
              info_brks <- liftIO $ readIModBreaks hug ibi
              let modl  = getBreakSourceMod ibi info_brks
              modl_str  <- display modl
              return $
                Just DbgStackFrame
                  { name = modl_str ++ "." ++ GHC.resumeDecl r
                  , sourceSpan = ss
                  , breakId = Just ibi
                  }
        _ -> do
          mExcSpan <- exceptionInfoSourceSpan <$> getExceptionInfo req_tid
          case mExcSpan of
            Just sourceSpan ->  return $ Just DbgStackFrame
                                  { name = GHC.resumeDecl r
                                  , sourceSpan
                                  , breakId = Nothing
                                  }
            Nothing -> return Nothing
  return (maybe id (:) head_frame $ decoded_frames)

--------------------------------------------------------------------------------
-- * Scopes
--------------------------------------------------------------------------------

-- | Get the stack frames at the point we're stopped at
getScopes :: RemoteThreadId -> Int -> Debugger [ScopeInfo]
getScopes threadId frameIx = do
  frames <- getStacktrace threadId
  let frame = frames !! frameIx
  let sourceSpan = DbgStackFrame.sourceSpan frame
      localsScope = ScopeInfo
        { kind = LocalVariablesScope
        , expensive = False
        , numVars = Nothing
        , sourceSpan
        }
  if
    | frameIx < length frames
    , Just ibi <- DbgStackFrame.breakId frame
    -> do
      hsc_env   <- getSession
      info_brks <- liftIO $ readIModBreaks (hsc_HUG hsc_env) ibi
      let brk_modl = getBreakSourceMod ibi info_brks
      -- It is /very important/ to report a number of variables (numVars) for
      -- larger scopes. If we just say "Nothing", then all variables of all
      -- scopes will be fetched at every stopped event.
      in_mod   <- getTopEnv brk_modl
      imported <- getTopImported brk_modl
      return
        [ localsScope
        , ScopeInfo { kind = ModuleVariablesScope
                    , expensive = True
                    , numVars = Just (sizeUFM in_mod)
                    , sourceSpan
                    }
        , ScopeInfo { kind = GlobalVariablesScope
                    , expensive = True
                    , numVars = Just (sizeOccEnv imported)
                    , sourceSpan
                    }
        ]
    | otherwise ->
      return [localsScope]

--------------------------------------------------------------------------------
-- * Variables
--------------------------------------------------------------------------------
-- Note [Variables Requests]
-- ~~~~~~~~~~~~~~~~~~~~~~~~~
-- We can receive a Variables request for three different reasons
--
-- 1. To get the variables in a certain scope
-- 2. To inspect the value of a lazy variable
-- 3. To expand the structure of a variable
--
-- The replies are, respectively:
--
-- (VARR)
-- (a) All the variables in the request scope
-- (b) ONLY the variable requested
-- (c) The fields of the variable requested but NOT the original variable

-- | Get variables using a variable/variables reference.
--
-- When the request forces a variable, return 'ForcedVariable'. Otherwise return
-- the resulting variables/fields.
--
-- See Note [Variables Requests]
getVariables :: RemoteThreadId -> Int{-stack frame index-} -> VariableReference -> Debugger VariableResult
getVariables threadId frameIx vk = do
  frames <- getStacktrace threadId
  let frame = frames !! frameIx
  hsc_env <- getSession
  fam_envs <- getFamInstEnvs'
  case vk of
    -- Only `seq` the variable when inspecting a specific one (`SpecificVariable`)
    -- (VARR)(b,c)
    SpecificVariable key -> do
      term <- obtainTerm key

      case term of

        -- (VARR)(b)
        Suspension{} -> do

          -- Original Term was a suspension:
          -- It is a "lazy" DAP variable: our reply can ONLY include
          -- this single variable.

          term' <- forceTerm term

          vi <- termToVarInfo fam_envs key term'

          return (ForcedVariable vi)

        -- (VARR)(c)
        _ -> do

          -- Original Term was already something other than a Suspension;
          -- Meaning the @SpecificVariable@ request means to inspect the structure.
          -- Return ONLY the fields

          termVarFields fam_envs key term >>= \case
            VarFields vfs -> pure (VariableFields vfs)

    -- (VARR)(a) from here onwards

    LocalVariables -> fmap VariableFields $ do
      -- bindLocalsAtBreakpoint hsc_env (GHC.resumeApStack r) (GHC.resumeSpan r) (GHC.resumeBreakpointId r)
      mapM (tyThingToVarInfo fam_envs) =<< GHC.getBindings

    ModuleVariables
      | frameIx < length frames
      , Just ibi <- DbgStackFrame.breakId frame
      -> fmap VariableFields $ do
        curr_modl <- liftIO $ getBreakSourceMod ibi <$>
                      readIModBreaks (hsc_HUG hsc_env) ibi
        things <- typeEnvElts <$> getTopEnv curr_modl
        mapM (\tt -> do
          nameStr <- display (getName tt)
          vi <- tyThingToVarInfo fam_envs tt
          return vi{varName = nameStr}) things

    GlobalVariables
      | frameIx < length frames
      , Just ibi <- DbgStackFrame.breakId frame
      -> fmap VariableFields $ do
        curr_modl <- liftIO $ getBreakSourceMod ibi <$>
                      readIModBreaks (hsc_HUG hsc_env) ibi
        names <- map greName . globalRdrEnvElts <$> getTopImported curr_modl
        mapM (\n-> do
          nameStr <- display n
          liftIO (GHC.lookupType hsc_env n) >>= \case
            Nothing ->
              return VarInfo
                { varName = nameStr
                , varType = ""
                , varValue = ""
                , isThunk = False
                , varRef = NoVariables
                }
            Just tt -> do
              vi <- tyThingToVarInfo fam_envs tt
              return vi{varName = nameStr}
          ) names

    NoVariables -> pure (VariableFields [])

    -- Couldn't find ibi or frame
    _otherwise -> pure (VariableFields [])

--------------------------------------------------------------------------------
-- Inspect
--------------------------------------------------------------------------------

-- | All top-level things from a module, including unexported ones.
getTopEnv :: Module -> Debugger TypeEnv
getTopEnv modl = do
  hsc_env <- getSession
  liftIO $ HUG.lookupHugByModule modl (hsc_HUG hsc_env) >>= \case
    Nothing -> return emptyTypeEnv
    Just HomeModInfo
      { hm_details = ModDetails
        { md_types = things
        }
      } -> return things

-- | All bindings imported at a given module
getTopImported :: Module -> Debugger GlobalRdrEnv
getTopImported modl = do
  hsc_env <- getSession
  liftIO $ HUG.lookupHugByModule modl (hsc_HUG hsc_env) >>= \case
    Nothing -> return emptyGlobalRdrEnv
    Just hmi -> mkTopLevImportedEnv hsc_env hmi