.NET Tools How-To's

Another Look into the Future with Rider’s Predictive Debugger

In the 2023.2 release cycle, we’ve introduced the Predictive Debugger in ReSharper, which gives you predictions about code paths and variables beyond the current execution pointer. We’ve written extensively about its advantages compared to alternative debugging strategies like thorough thinking, log statements, and unit tests. The bottom line is that the debugger is just another companion on our developer adventure that gives us a reliable “view into a system that’s too complicated to understand” while our “head is a faulty interpreter of all that code” that we have to deal with (John Carmack in an interview with Lex Fridman).

We’ve already hinted that Rider would catch up with ReSharper, and just one major release later, we are excited to tell you everything about our new Predictive Debugger in Rider.

How does Rider’s Predictive Debugger work?

Rider’s predictive debugger feels similar to ReSharper’s in that it can make predictions about the future program flow. However, it works quite differently.

ReSharper’s implementation executes code through Visual Studio’s Debugger API and is subject to side effects. It’s pretty much the same as if you would evaluate expressions or execute statements in the Immediate tool window.

On the other hand, Rider’s implementation is based on a no-side-effects (NSE) interpreter that interprets annotated IL code. In this case, “annotated” does NOT refer to Roslyn or JetBrains annotations (as in ReSharper’s implementation) but to the multi-level process of analyzing nested methods. During this process, your source code is automatically recompiled using Roslyn with annotations made by our NSE interpreter so that it can capture intermediate values and correlate to IL code:

// Unannotated
void M(Person p)
{
    if (p.GetFullName().Length == 12)
        Console.WriteLine("Exactly 12 chars!");
}

// Annotated
void M(Person p)
{
    if (Tracker.Annotate(p.GetFullName().Length == 12, startLine: 3, startColumn: 7, endLine: 3, endColumn: 20))
        Console.WriteLine("Exactly 12 chars!");
}

These annotations allow our debugger to show helpful highlightings. Stay tuned for the next section to see it in action!

A lot of code eventually calls into native/extern APIs. Because Rider’s predictive debugger works without side effects, these API calls (call and callvirt) are interpreted via hooks. For instance, for all calls of the String.Length method, the following hook is executed instead:

PrimitiveStackValue String__get_Length(
		MethodInstantiation methodInstantiation,
		StringStackValue stackValue)
{
    var length = stackValue.Value.Length;
    return new PrimitiveStackValue(
				length,
				methodInstantiation.MethodSpecification.Method.ReturnValue.Type.SubstituteGenericArguments(methodInstantiation));
}

Compared to ReSharper’s implementation, this approach gives us more opportunity for analysis and ensures that you never have to deal with side effects. But that should be enough about the theory and internals of Rider’s predictive debugger – let’s see it in action!

Rider’s Predictive Debugger in Action

The predictive debugger is enabled by default, and you will see it working immediately once you stop at a breakpoint and suspend the execution. In order to illustrate the different visual cues, we will use the sample code from our previous post:

Predicting boolean expressions and unreachable code
Predicting boolean expressions and unreachable code

Boolean expressions are highlighted in green and red, indicating their evaluation as true and false, respectively. Unreachable code is “muted” with a grey text color and a strikeout effect.

You may have also noticed the violet-colored bar on the left. This gutter bar acts as an indicator of how far the debugger could interpret your code:

Gutter highlights prediction scope
Gutter highlights prediction scope

File.Exists also calls into native code. We’re on it to implement a hook!

Of course, you will also see exceptions that are going to be thrown. The exception type and message are displayed next to the line as an inlay hint, along with an exception icon. That same exception icon is also displayed in the gutter for faster recognition. A squiggly allows you to identify the statement/expression that is throwing the exception:

Predicting an exception
Predicting an exception

The predictive debugger also helps you predict what’s happening inside a loop for different iterations. Using the inlay hints, you can move forward and backward:

Predicting different loop iterations
Predicting different loop iterations

Another great advantage reveals itself in combination with the run-to-cursor floating action. Previously, you may have thought that your code could run to the desired location while it actually returns much earlier, and suddenly, you’re debugging the next call of the function. With Rider 2023.3, this behavior becomes predictable:

Predictive debugger improving run-to-cursor action
Predictive debugger improving run-to-cursor action

Configuring the Predictive Debugger in Rider

The way predictions are displayed is customizable in Rider’s settings dialog under Editor | Color Scheme | Debugger | Predictive debugger. From here, you can change how unreachable code and false/true values should look:

Color Scheme settings for Predictive Debugger
Color Scheme settings for Predictive Debugger

So, for instance, if you prefer unreachable code not to have a strikeout effect (as in ReSharper’s Predictive Debugger), you may change that here.

Of course, you can also disable all colorizations, the gutter indicator, or the predictions as a whole under Build, Execution, Deployment | Debugger. Similar to properties in the watch window, you can also change the evaluation timeout:

General Settings for Predictive Debugger
General Settings for Predictive Debugger

Future Work

Unlike ReSharper, Rider’s predictive debugger does not show data tips about predicted values yet. Be assured that we will let you know once those arrive!

As for interpreter hooks – we’ve got a large set implemented already! However, we expect more hooks coming for BCL and extern methods to make the interpreter faster. Also, libraries that use P/Invoke might need some help.

Please don’t hesitate to share your code samples with us where you don’t see the expected results.

Conclusion

Rider has quickly caught up with ReSharper’s ability to predict the future program flow. It works quite differently and, therefore, isn’t as prone to side effects as ReSharper’s Predictive Debugger. At the end of the day, though, both features greatly enhance your overall debugging experience.

Give it a go with the latest Rider 2023.3 EAP, and let us know if you have any questions or suggestions in the comments section below. Thanks for reading!

Image credit: Immo Wegmann

image description

Discover more