packages feed

haskell-debugger-0.13.0.0: haskell-debugger/GHC/Debugger/Interface/Messages.hs

{-# OPTIONS_GHC -Wno-orphans #-} -- TODO: drop this and Show GHC.InternalBreakpointId...
{-# LANGUAGE LambdaCase,
             StandaloneDeriving,
             DataKinds,
             OverloadedStrings,
             DuplicateRecordFields,
             TypeApplications
             #-}

-- | Types for sending and receiving messages to/from haskell-debugger
module GHC.Debugger.Interface.Messages where

import qualified GHC
import qualified GHC.Utils.Outputable as GHC

import GHC.Debugger.Runtime.Term.Key
import Data.Binary (Binary)
import qualified GHC.Stack as Stack

--------------------------------------------------------------------------------
-- Commands
--------------------------------------------------------------------------------

-- | The commands sent to the debugger
data Command

  -- | Set a breakpoint on a given function, or module by line number
  = SetBreakpoint { brk       :: Breakpoint
                  , hitCount  :: Maybe Int
                  -- ^ Stop after N hits (if @isJust condition@, count down only when @eval condition == True@)
                  , condition :: Maybe String
                  -- ^ Stop if condition evalutes to True
                  , logMessage :: Maybe String
                  -- ^ Log only if @condition@ (and @hitCondition@ when supported) are @True@.
                  }

  -- | Delete a breakpoint on a given function, or module by line number
  | DelBreakpoint Breakpoint

  -- | Find the valid breakpoints locations for the given module Breakpoint
  | GetBreakpointsAt Breakpoint

  -- | Clear all breakpoints in the specified file.
  -- This is useful because DAP's `setBreakpoints` re-sets all breakpoints from zero for a source rather than incrementally.
  | ClearModBreakpoints { file :: FilePath }

  -- | Clear all function breakpoints
  | ClearFunctionBreakpoints

  -- | Get all threads
  | GetThreads

  -- | Get the evaluation stacktrace until the current breakpoint.
  | GetStacktrace RemoteThreadId

  -- | Get the list of available scopes at the current breakpoint
  | GetScopes RemoteThreadId Int

  -- | Get the variables in scope for the current breakpoint.
  --
  -- Note: for GHCs <9.16 this only reports the variables free in the expression
  -- we're stopped at rather than all variables in scope.
  | GetVariables RemoteThreadId Int{-stack frame positional ix-} VariableReference

  -- | Evaluate an expression at the current breakpoint.
  | DoEval String

  -- | Get information about the current exception (if any) on a thread.
  | GetExceptionInfo RemoteThreadId

  -- | Continue executing from the current breakpoint
  | DoContinue

  -- | Step local, which executes until next breakpoint in the same function.
  | DoStepLocal

  -- | Single step always to the next breakpoint. Used for "step-in".
  | DoSingleStep

  -- | Step out to the breakpoint immediately following a return.
  | DoStepOut

  -- | Execute a prog with debugging enabled. Breaks on the existing breakpoints.
  --
  -- Constructed with an entry point function name and the arguments to pass it.
  --
  -- When the @'EntryPoint'@ is @'Main'@, @'runArgs'@ are set as process
  -- invocation arguments (as in @argv@) rather than passed directly as a
  -- Haskell function arguments.
  | DebugExecution { entryPoint :: EntryPoint, entryFile :: FilePath, runArgs :: [String] }

-- | An entry point for program execution.
data EntryPoint = MainEntry { mainName :: Maybe String } | FunctionEntry { fnName :: String }
  deriving (Show)

-- | A breakpoint can be set/removed on functions by name, or in modules by
-- line number. And, globally, for all exceptions, or just uncaught exceptions.
data Breakpoint
  = ModuleBreak { path :: FilePath, lineNum :: Int, columnNum :: Maybe Int }
  | FunctionBreak { function  :: String }
  | OnExceptionsBreak
  | OnUncaughtExceptionsBreak
  deriving (Show)

-- | Information about a scope
data ScopeInfo = ScopeInfo
      { kind :: ScopeVariablesReference
      , sourceSpan :: SourceSpan
      , numVars :: Maybe Int
      , expensive :: Bool }
  deriving (Show)

newtype VarFields = VarFields [VarInfo]

-- | Information about a variable
data VarInfo = VarInfo
      { varName  :: String
      , varType  :: String
      , varValue :: String
      , isThunk  :: Bool
      , varRef   :: VariableReference
      -- ^ A reference back to this variable

      -- TODO:
      --  memory reference using ghc-debug.
      }

-- | Result of requesting variables.
--
-- If the variable you requested is a thunk then `ForcedVariable` is returned, which
-- is the variable you requested but forced.
--
-- If the variable you requested is not a thunk, then 'VariableFields` is returned which
-- contains the subfields of the variable.
data VariableResult
  = ForcedVariable VarInfo
  | VariableFields [VarInfo]

variableResultToList :: VariableResult -> [VarInfo]
variableResultToList = \case
  ForcedVariable vi -> [vi]
  VariableFields vis -> vis

-- | What kind of breakpoint are we referring to, module or function breakpoints?
-- Used e.g. in the 'ClearBreakpoints' request
data BreakpointKind
  -- | Module breakpoints
  = ModuleBreakpointKind
  -- | Function breakpoints
  | FunctionBreakpointKind
  deriving (Show, Eq)

instance GHC.Outputable BreakpointKind where ppr = GHC.text . show

-- | Referring to existing scopes
data ScopeVariablesReference
  = LocalVariablesScope
  | ModuleVariablesScope
  | GlobalVariablesScope
  deriving (Show, Eq, Ord)

-- | The type of variables referenced, or a particular variable referenced for its fields or value (when inspecting a thunk)
data VariableReference
  -- | A void reference to nothing at all. Used e.g. for ty cons and data cons
  = NoVariables

  -- | Variables in the local context (includes arguments, previous bindings)
  | LocalVariables

  -- | Variables in the module where we're stopped
  | ModuleVariables

  -- | Variables in the global context
  | GlobalVariables

  -- | A reference to a specific variable.
  -- Used to force its result or get its structured children
  | SpecificVariable TermKey

scopeToVarRef :: ScopeVariablesReference -> VariableReference
scopeToVarRef = \case
  LocalVariablesScope -> LocalVariables
  ModuleVariablesScope -> ModuleVariables
  GlobalVariablesScope -> GlobalVariables

-- | A source span type for the interface. Like 'RealSrcSpan'.
data SourceSpan = SourceSpan
      { file :: !FilePath
      -- ^ Path to file where this span is located
      , startLine :: {-# UNPACK #-} !Int
      -- ^ RealSrcSpan start line
      , endLine :: {-# UNPACK #-} !Int
      -- ^ RealSrcSpan end line
      , startCol :: {-# UNPACK #-} !Int
      -- ^ RealSrcSpan start col
      , endCol :: {-# UNPACK #-} !Int
      -- ^ RealSrcSpan end col
      }
      deriving (Show, Eq)

-- | This is a completely unhelpful source span!
-- It doesn't point to anything and is of no help to the user
-- whatsoever.
--
-- Use this only as a last resort if no other source span can be provided.
unhelpfulSourceSpan :: SourceSpan
unhelpfulSourceSpan = SourceSpan
  { file = ""
  , startLine = 0
  , endLine = 0
  , startCol = 0
  , endCol = 0
  }

srcLocToSourceSpan :: Stack.SrcLoc -> SourceSpan
srcLocToSourceSpan srcLoc =
  SourceSpan
    { file = Stack.srcLocFile srcLoc
    , startLine = Stack.srcLocStartLine srcLoc
    , endLine = Stack.srcLocEndLine srcLoc
    , startCol = Stack.srcLocStartCol srcLoc
    , endCol = Stack.srcLocEndCol srcLoc
    }

--------------------------------------------------------------------------------
-- Responses
--------------------------------------------------------------------------------

-- | The responses sent by `haskell-debugger` to the client
data Response
  = DidEval EvalResult
  | DidSetBreakpoint BreakFound
  | DidRemoveBreakpoint BreakFound
  | DidGetBreakpoints (Maybe SourceSpan)
  | DidClearBreakpoints
  | DidContinue EvalResult
  | DidStep EvalResult
  | DidExec EvalResult
  | GotThreads [DebuggeeThread]
  | GotStacktrace [DbgStackFrame]
  | GotScopes [ScopeInfo]
  | GotVariables VariableResult
  | GotExceptionInfo ExceptionInfo
  | Aborted String
  | Initialised

data BreakFound
  = BreakFound
    { changed :: !Bool
    -- ^ Did the status of the found breakpoint change?
    , breakId :: [GHC.InternalBreakpointId]
    -- ^ Internal breakpoint identifier (module + ix) (TODO: Don't expose GHC)
    , sourceSpan :: SourceSpan
    -- ^ Source span for interface
    }
  -- | Breakpoint found but without location info.
  -- This happens when setting breakpoints on exceptions.
  | BreakFoundNoLoc
    { changed :: Bool }
  -- | No breakpoints found
  | BreakNotFound
  -- | Found many breakpoints.
  -- Caused by setting breakpoint on a name with multiple matches or many equations.
  | ManyBreaksFound [BreakFound]
  deriving (Show)

-- | A reference to a remote thread by remote id
-- See 'getRemoteThreadId'.
newtype RemoteThreadId = RemoteThreadId
    { remoteThreadIntRef :: Int
    -- ^ The number identifier of the thread on the (remote) interpreter. To
    -- find the proper remote 'ThreadId' corresponding to this numeric
    -- identifier, lookup the 'remoteThreadIntRef' in the 'ThreadMap'
    }
    deriving (Show, Eq, Ord, Binary)

data SourceKind = IsExpr | IsStmt

data EvalResult
  = EvalCompleted { resultVal :: String
                  , resultType :: String
                  , resultSourceKind :: Maybe SourceKind
                  , resultStructureRef :: VariableReference
                  -- ^ A structured representation of the result of evaluating
                  -- the expression given as a "virtual" 'VariableReference'
                  -- that the user can use to refer to the result and inspect
                  -- interactively and expand it.
                  }
  | EvalException { resultVal :: String, resultType :: String }
  | EvalStopped   { breakId :: Maybe GHC.InternalBreakpointId
                  -- ^ Did we stop at an exception (@Nothing@) or at a breakpoint (@Just@)?
                  , breakThread :: RemoteThreadId
                  -- ^ In which thread did we hit the breakpoint?
                  }
  -- | Evaluation failed for some reason other than completed/completed-with-exception/stopped.
  | EvalAbortedWith String

data DebuggeeThread
  = DebuggeeThread
    { tId :: !RemoteThreadId
    -- ^ An identifier for a thread on the (possibly remote) debuggee process
    , tName :: !(Maybe String)
    -- ^ Thread label, if there is one
    }
    deriving (Show)

data DbgStackFrame
  = DbgStackFrame
    { name :: String
    -- ^ Title of stack frame
    , sourceSpan :: SourceSpan
    -- ^ Source span for this stack frame
    , breakId :: Maybe GHC.InternalBreakpointId
    -- ^ Is this a BCO continuation frame with a breakpoint?
    -- If yes, we can leverage the breakpoint info to report scopes.
    }
  deriving (Show)

data ExceptionInfo = ExceptionInfo
  { exceptionInfoTypeName     :: String
  , exceptionInfoFullTypeName :: String
  , exceptionInfoMessage      :: String
  , exceptionInfoContext      :: Maybe String
  , exceptionInfoSourceSpan   :: Maybe SourceSpan
  , exceptionInfoInner        :: [ExceptionInfo]
  }
  deriving (Show)

--------------------------------------------------------------------------------
-- Instances
--------------------------------------------------------------------------------

instance Show GHC.InternalBreakpointId where
  show (GHC.InternalBreakpointId m ix) = "InternalBreakpointId " ++ GHC.showPprUnsafe m ++ " " ++ show ix