#F# Script Debugging
Adds supprt the to Visual F# tooling for rich debugging of F# scripts.
##Motivation F# developers love the low-overhead, iterative REPL experience they have today, but when scripts become larger and more complex, they become difficult to debug. There is no interaction whatsoever today between Editor + F# Interactive and the VS Ddbugger. Thus to debug complex scripts, devs must resort to one of:
printf
debugging- Create a console app, paste in script code, F5-debug the console app
It would be much nicer if we could have the VS debugger participate in the F# script workflow directly.
##Approach The approach to enabling the F# script debugging scenario is quite simple - provide a user-friendly way (context menu items, keyboard shortcuts, etc) to attach the VS debugger to the already-running fsi.exe process which is powering the hosted F# Interactive tool window. From a technical standpoint this is exactly equivalent to executing existing menu actions Debug -> Attach to Process -> (find fsi.exe) -> Attach.
Thankfully, the vast majority of the hard work is already done, and for the most part things "just work" once the debugger is attached.
##Details
###UI Items
The following user-friendly items are provided with this feature
####F# Code Editor Context Menu Items
- "Debug in F# Interactive"
- Like "Send to F# Interactive" but also
- Attaches debugger to fsi.exe if not already attached
- Triggers debugger break at the first executable line from the interaction
- "Debug Line in F# Interactive"
- Like "Send Line to F# Interactive" but also
- Attaches debugger to fsi.exe if not already attached
- Triggers debugger break at the first executable line from the interaction
These 2 are hidden when the debugger is already running and attached to any process besides the fsi.exe process backing the current F# Interactive session. They are greyed out with the same logic as their existing analogues.
####F# Interactive Context Menu Items
- "Start Debugging"
- Attaches debugger to the backing fsi.exe process
- Hidden any time the debugger is already running
- "Stop Debugging"
- Detaches debugger from the backing fsi.exe process
- Hidden unless debugger is attached to backing fsi.exe process
###FSI Changes
####Emit symbol info for locals fsi.exe does not currently emit debug symbol information for local variables in its dynamically-compiled code (though it does emit debug info for most other things). This prevents locals from appearing during script debugging, which is a major drawback.
With this feature, we change fsi.exe codegen such that symbol info for locals is emitted. This allows for a much better debug experience.
####Inject debug break
With this feature, we want the ability to automatically break in the debugger before the first line of code is executed (similar to existing Ctrl-F11
debug launch for exes). To enable this, we add a new private directive #dbgbreak
which signals to fsi.exe that a debug break should be added before the first executable line of code in the current interaction.
A call to System.Diagnostics.Debugger.Break()
is injected before the first top-level let
or do
binding in the interaction which has a sequence point. (i.e. we don't break the debugger before function definitions, class declarations, open statements, etc)
##Open Design Items
Suggestions welcome!
###Attach/Detach Behavior
When doing console app debugging, expected workflow is that F5
launches the app with debugger attached, then the debugger detaches and goes away when the app has finished running.
In our case, the "app" is fsi.exe, and it persists even when the original F# code is done executing.
So what is the "best" behavior for this feature when invoking "Debug in F# Interactive"?
- Debugger attaches, code is executed, debugger remains attached
- Debugger attaches, code is executed, debugger detaches automatically
Option #1 is easy, and doesn't seem totally wrong.
Option #2 is much more difficult, as we would need to invent some kind of IPC mechanism for FSI to notify VS that a particular snippet is done executing and it is ok to detach. This needs to be resilient to user manually attaching/reattaching in the middle of execution, aborting execution, etc.
###Keyboard Shortcuts
Should the new menu items have associated keyboard shortcuts? Debugging is theoretically much less frequent that simple code execution, so perhaps these aren't needed at all and UI menus are sufficient. Strawman suggestions:
For editor, just add Alt-D
prefix to existing shortcuts. Since Alt
doesn't need to be released, for the user this means simply adding a D
in between the current shortcut keys:
- "Debug in F# Interactive" =
Alt-D-Enter
- "Debug Line in F# Interactive" =
Alt-D-'
Rejected: Using Alt-F5
as a mix of Alt-Enter
and standard debug command F5
. Much too easy to fat-finger Alt-F4
and close VS!
For F# Interactive tool window, continue the existing theme of Ctrl-Atl-*
- "Attach Debugger" =
Ctrl-Alt-D
- "Detach Debugger" =
Ctrl-Shift-D
###Icons Should there be little icons in the context menus? Which ones?
##Test matrix
Test | Status |
---|---|
Editor context menu entries successfully attach debugger and run code | works |
Tool window menu entries successfully attach/detach debugger | works |
|
Editor menu items don't appear when editing other languages | works
Keyboard shortcuts work | works
|
Editor context menu entries grey same as existing analogues | works
Editor context menu entries hidden when debugger attached to non-FSI | works
Tool window entry "Attach" hidden when debugger running | works
Tool window entry "Detach" hidden unless debugger attached to fsi | works
|
Multiple-attach is safe/doesn't crash | works
Multiple-detach is safe/doesn't crash | works
FSI reset while debugging gracefully detaches debugger | works
|
Debug experience - code stepping | works
Debug experience - locals | works
Debug experience - immediate window | works
Debug experience - breakpoints | works as well as expected
Debug experience - callstack | works, but FSI innards consume many frames
|
Auto-break - break at non-function let
bindings | works
Auto-break - break at do
bindings | works
Auto-break - break at binding which is also the it
binding | works
Auto-break - don't inject extra break when interaction contains #directives
in the middle | works
Auto-break - skip curried function defs | works
Auto-break - skip #directives
| works
Auto-break - skip type decls | works
Auto-break - skip open
statements | works
Auto-break - skip module decls | works
Auto-break - skip module abbrevs | works
Auto-break - skip exception decls | works