Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
F# Script Debugging Speclet

#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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.