packages feed

haskell-debugger-0.13.0.0: haskell-debugger/GHC/Debugger/Runtime/Interpreter.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE ViewPatterns #-}

-- | Custom interpreter commands in addition to the existing
-- 'GHC.Runtime.Interpreter' ones.
--
-- Similarly to 'GHC.Runtime.Interpreter', the functions in this module
-- abstract whether the external or internal interpreter is being used.
--
-- When using the external interpreter, we leverage the "Custom external commands"
-- feature introduced in GHC 10.0's ghci API to extend the external interpreter
-- server with custom command messages and handlers. When using
--
-- In GHC 9.14, there isn't support for extending the external interpreter server with new commands.
-- Thus, it is impossible to get the external interpreter to execute custom
-- code. When custom commands aren't available for the external interpreter we
-- fallback to an implementation which uses more primitive functions on the
-- external interpreter, decode the heap values directly into 'Term', and use
-- 'GHC.Debugger.Runtime.Term.Parser' to parse the heap 'Term's into structured
-- data. That implementation doesn't live in this module, which is meant only
-- to be the interface to the custom commands supported by GHC > 9.14.
--
-- When using the internal interpreter, we can execute the custom command
-- handler directly in this process -- regardless of whether using GHC 9.14 or GHC 10.0.
-- In practice, for simplicity, we just use the fallback mechanism of 9.14 for both internal and external.
--
-- The custom commands and their handlers are implemented in
-- 'GHC.Debugger.Runtime.Interpreter.Custom'
--
-- Meant to be imported with
--
-- @
-- import qualified GHC.Debugger.Runtime.Interpreter as Debuggee
-- @
--
-- since the exported
-- functions run on the debuggee (possibly external) process. Even on GHC 9.14,
-- where we use more primitive messages and do some extra parsing on the
-- debugger/host side
module GHC.Debugger.Runtime.Interpreter
  ( listThreads
  , decodeThreadStack
  , collectExceptionInfo

  -- * Re-exports
  , ThreadInfo(..)
  ) where

import Control.Monad
import Control.Monad.IO.Class

import GHCi.RemoteTypes
import GHC.Debugger.Monad

import GHC.Debugger.Interface.Messages (ExceptionInfo)
import GHC.Debugger.Runtime.Interpreter.Custom

import Data.Binary
import GHC.Driver.Env (hscInterp)
import GHC.Driver.Monad (getSession)
import GHC.Runtime.Interpreter
import Control.Concurrent
import Control.Exception

--------------------------------------------------------------------------------
-- * Debugger abstraction
--
-- | The functions in this section need to be in 'Debugger' because the
-- 'TermParser' (needed to implement these top-level functions in GHC 9.14)
-- works in 'Debugger'. This also allows us to call debugger-interpreter
-- functions without 'liftIO' everywhere.
--------------------------------------------------------------------------------

listThreads :: Debugger [ThreadInfo ForeignRef]
listThreads = do
  interp  <- hscInterp <$> getSession
  ts_info <- liftIO $ interpDbgCmd interp ListThreads
  forM ts_info $ \ti@ThreadInfo{threadInfoRef} -> do
    threadInfoForeignRef <- liftIO $ mkFinalizedHValue interp threadInfoRef
    pure ti{threadInfoRef = threadInfoForeignRef}

decodeThreadStack :: ForeignRef ThreadId -> Debugger [StackFrameInfo]
decodeThreadStack ftid = do
  interp  <- hscInterp <$> getSession
  liftIO $ withForeignRef ftid $
    interpDbgCmd interp . DecodeThreadStack

collectExceptionInfo :: ForeignRef SomeException -> Debugger ExceptionInfo
collectExceptionInfo excRef = do
  interp <- hscInterp <$> getSession
  liftIO $ withForeignRef excRef $
    interpDbgCmd interp . CollectExceptionInfo

--------------------------------------------------------------------------------
-- * IO+interpreter abstraction
--
-- | Functions on IO which abstract calling the external interpreter or
-- internal interpreter using custom commands. Note that 'CustomMessage' is not
-- available in GHC 9.14 so we don't make these functions available in GHC 9.14
--------------------------------------------------------------------------------

-- | Run a 'DbgInterpCmd' in the interpreter's context. By default, the command is
-- serialized and sent to an external iserv process, and the response is
-- deserialized (hence the @Binary@ constraint). With @--internal-interpreter@
-- we execute the command directly here.
--
-- To run a builtin 'Message' command, use 'interpCmd' instead.
interpDbgCmd :: Binary a => Interp -> DbgInterpCmd a -> IO a
interpDbgCmd interp command = case interpInstance interp of
  InternalInterp ->
    -- Run it directly on this process!
    runDbgInterpCmd command
  ExternalInterp{}
    -- Use interpCmd to send the command as a custom message to external process.
    -- The custom message will be processed according to the custom command
    -- handlers registered with `iservWithCustom`
    | let payload = encodePayload (Some @Binary command) -> do
      respBytes <- interpCmd interp (CustomMessage dbgInterpCmdTag payload)
      case decodePayload respBytes of
        Left err -> fail err
        Right r  -> pure r