The debug adapter protocol (DAP) is becoming standard for IDEs to implement graphical debugging interfaces that communicate with language-specific debuggers. The IDE acts as a debug server, and a language-specific tool acts as a client, receiving and responding to signals to synchronize a debugger state between an R session and the IDE.
Implementing a debug adapter client in R allows for an IDE to provide a graphical interface to the evaluation environment and the various debug actions.
A familiar example of a preferred workflow is RStudio's debug interface, allowing for stepping through code using either the graphical interface or using the R Console. A similar experience could be provided for any IDE that provides a debug server (VSCode, vim, among many others) if a running R session (similar to the RStudio R Console), could be attached as a debugging client and handle server requests at opportune times while providing an R REPL.
R Behavior
Assuming a running R session was listening for a debug server, the R session would:
listen for various breakpoint updates and trace the appropriate expressions upon receiving a breakpoint message from the server
provide debugger state information to the IDE when entering a browser() session
relay state from the browser() session to the IDE to update the graphical representation of debugger state (highlighted expression, breakpoint state, scoped environment variables)
ideally perform all of these actions in the background so that the familiar R debugging experience is uninterrupted.
Challenges
Listening for and inserting breakpoints
Assuming the R session is actively listening (via tcp or stdio) for breakpoint updates, there is a challenge of inserting those breakpoints before the next top-level expression is executed.
browser() commands
After entering the debugger, the other challenge is to synchronize state within the debugging session. This could be handled by adding hooks into the browser() interface to send debug state updates back to the IDE. For example, this may include sending information about the file and line number currently being evaluated in the debugger.
Existing Solutions & Their Limitations
Implementations often shim the R REPL, making the R REPL appear like a user session, but opaquely evaluate additional code to manage debug state in the background.
RStudio
From what I can tell, RStudio runs an R session in the background that both the console and debug manager communicate with, which allows additional R code to be executed to manage debug state that is hidden from the R Console.
VSCode "R Debugger" extension
Similarly, this extension intercepts and hides additional R code from being displayed in the REPL pane of a VSCode session ManuelHentschel/VSCode-R-Debugger. This is a viable solution for VSCode where you have tight coupling of the extension and display within the IDE, but means that any debugger solution is similarly tightly coupled to the IDE, which foregoes a lot of the value of implementing a generic protocol.
The developer of this extension shares similar difficulties preventing a more agnostic solution.
This is my own attempt with the goal of avoiding a REPL shim. It is built on layer upon layer of horrible hack. Breakpoints always lag behind the IDE by one top-level expression evaluation and stdin/stdout are rerouted between forked processes to opaquely intercept the browser() REPL to synchronize debugger state. This relies on a fork of processx that allows rerouting stdin and use of forked processes that are unsupported on Windows.
Proposal
Add new hook events that allow injecting code before a top-level expression is evaluated, and at the start and end of each browser() step.
The browser step hooks could be used to indicate to an IDE that the browser()continued or stopped
What does ideal look like?
If a user wants their running R session to be able to be attached as a debugger, they would just need to start listening for server connections in that REPL (or always listen for connections by default by adding it as part of a .Rprofile)
# .Rprofile
dap::listen()
After this, entering debug mode (in "attach" mode) in their IDE would connect to a background R process, which would flush any debug state updates to the parent R session before the next-evaluated user expression.
Should the user evaluate code that would hit the breakpoint, the user would be able to step through the code and observe their IDE's debugger reflecting their debug state, often by highlighting the active expression and updating the scoped environment variables.
Background
The debug adapter protocol (DAP) is becoming standard for IDEs to implement graphical debugging interfaces that communicate with language-specific debuggers. The IDE acts as a debug server, and a language-specific tool acts as a client, receiving and responding to signals to synchronize a debugger state between an R session and the IDE.
Implementing a debug adapter client in R allows for an IDE to provide a graphical interface to the evaluation environment and the various debug actions.
A familiar example of a preferred workflow is RStudio's debug interface, allowing for stepping through code using either the graphical interface or using the R Console. A similar experience could be provided for any IDE that provides a debug server (VSCode, vim, among many others) if a running R session (similar to the RStudio R Console), could be attached as a debugging client and handle server requests at opportune times while providing an R REPL.
R Behavior
Assuming a running R session was listening for a debug server, the R session would:
browser()
sessionbrowser()
session to the IDE to update the graphical representation of debugger state (highlighted expression, breakpoint state, scoped environment variables)Challenges
Listening for and inserting breakpoints
Assuming the R session is actively listening (via
tcp
orstdio
) for breakpoint updates, there is a challenge of inserting those breakpoints before the next top-level expression is executed.browser()
commandsAfter entering the debugger, the other challenge is to synchronize state within the debugging session. This could be handled by adding hooks into the
browser()
interface to send debug state updates back to the IDE. For example, this may include sending information about the file and line number currently being evaluated in the debugger.Existing Solutions & Their Limitations
Implementations often shim the R REPL, making the R REPL appear like a user session, but opaquely evaluate additional code to manage debug state in the background.
RStudio
From what I can tell, RStudio runs an R session in the background that both the console and debug manager communicate with, which allows additional R code to be executed to manage debug state that is hidden from the R Console.
VSCode "R Debugger" extension
Similarly, this extension intercepts and hides additional R code from being displayed in the REPL pane of a VSCode session ManuelHentschel/VSCode-R-Debugger. This is a viable solution for VSCode where you have tight coupling of the extension and display within the IDE, but means that any debugger solution is similarly tightly coupled to the IDE, which foregoes a lot of the value of implementing a generic protocol.
The developer of this extension shares similar difficulties preventing a more agnostic solution.
dgkf/debugadapter
This is my own attempt with the goal of avoiding a REPL shim. It is built on layer upon layer of horrible hack. Breakpoints always lag behind the IDE by one top-level expression evaluation and
stdin
/stdout
are rerouted between forked processes to opaquely intercept thebrowser()
REPL to synchronize debugger state. This relies on a fork ofprocessx
that allows reroutingstdin
and use of forked processes that are unsupported on Windows.Proposal
Add new hook events that allow injecting code before a top-level expression is evaluated, and at the start and end of each
browser()
step.browser()
continued or stoppedWhat does ideal look like?
If a user wants their running R session to be able to be attached as a debugger, they would just need to start listening for server connections in that REPL (or always listen for connections by default by adding it as part of a
.Rprofile
)After this, entering debug mode (in "attach" mode) in their IDE would connect to a background R process, which would flush any debug state updates to the parent R session before the next-evaluated user expression.
Should the user evaluate code that would hit the breakpoint, the user would be able to step through the code and observe their IDE's debugger reflecting their debug state, often by highlighting the active expression and updating the scoped environment variables.