Evaluate expression in debug

hey everybody,

I'm using IntelliJ for working with any rust project, however one limitation I found was that the debugger isn't working exactly as in other languages.

Can you please share with me what's the best way to have something close to "Evaluate Expression" in IntelliJ, where I can manipulate the data in the current frame scope?

This is what I found in IntelliJ Rust's docs:

The evaluation is performed by GDB or LLDB, which means the functionality is limited to what the particular debugger’s parser can provide (for GDB, check out the supported Rust features). Due to this, Evaluate Expression currently works only in simple cases like arithmetic and logic operations with possible access to structure elements and pointers. The same limitations apply to the expressions you can set for conditional breakpoints.

I'm guessing that means you can't do anything more complex than field access from the debugger. Implementing "Evaluate Expression" completely would require creating a whole Rust interpreter/REPL and making it fit the shape IntelliJ's debugger wants, which isn't a trivial task.

1 Like

This is my intuition too. I don't see how you can do 'eval arbitrary expr' without an interpreter / JIT

1 Like

thanks @Michael-F-Bryan I started using rust-lldb debugger, but I also don't think it supports more advanced evaluation of expressions, like I'm supposed to do in Java i.e.

Can you, please, let me know what's your personal workaround in this case :thinking:

To tell you the truth, in 6 years of writing Rust I've used a debugger maybe 5 times[1]. Meanwhile, at a previous job I wrote 100+ kloc of C# and would be dropping into the debugger every day or two.

I wouldn't say I have a workaround - it's more a mindset shift.

Rust has a strong type system which means a lot of your invariants and assumptions are known at compile time (i.e. by looking at the code and what the compiler lets you do with a variable) instead of at runtime (i.e. in Java all object references can be null or some sub-type, so the only way to find out what is going on is to check a variable's value dynamically). It's hard to overstate just how powerful Option and match can be for writing code that Just Works... That means code with bad logic often won't compile, so you don't get to the debugger stage in the first place.

The language puts more emphasis on "plain old data" types rather than smart "objects". That means, often adding a println!("{some_variable:?}") to your code and re-running the tests is enough to tell you everything you need to know. Data types also tend to be small and tree-like as opposed to the vast webs of interconnected objects you'll get in traditional OO codebases.

You can also write unit tests directly inside the module being tested, so in practice it's really easy to split your logic into a small function and then test it without breaking encapsulation.


  1. All of them were when I was writing unsafe FFI code and was messing up the "contract" when calling Rust from other languages. ↩︎

2 Likes

thank you for the elaborate answer!
I guess changing the way I think about code, just like you've said, is the best approach.

One things which makes Rust less reliant on debugging is its anti-mutability stance. Rust favours immutable code (even if not as dogmatically as some FP languages), and when you have mutability, it's much more reasonable than in GC languages. The only way to (safely) mutate something is to get &mut reference, and the strict borrowing and aliasing rules mean that there are fewer &mut references in general, and they almost always follow the simple tree-like pattern (rather than "everything can mutate anything" as in most languages).

Another benefit is Rust's "private by default" rules, together with a powerful flexible privacy system and strong guarantees of inter-crate compatibility. This means that usually you need to reason only about one crate at a time, which makes the problem much more tractable.

The lack of good debugger experience is indeed a significant shortcoming, though. I can't imagine navigating a codebase spanning millions of lines without it, no matter how readable the code is. However, given how young Rust is and how modular its design is, this has not been a problem for me so far. I imagine that the lack of proper debugging will become more pressing as Rust gains industry adoption.

As a side note, you can get very far just with macros. dbg! macro in particular is a godsend.

2 Likes

My typical approach is to open up the API docs and click around. Often, there are only a handful of really important types/functions in each namespace, and by seeing how they interact with each other (accepting others as method arguments, the different trait implementations, etc.) you can start to build up a map of the codebase.

I find that easier than using a debugger because I get to see things from far away, whereas when using a debugger it's easy to not see the forest for the trees.

1 Like

If youre proactive about logging that can make a big difference when debugging an issue too. The log crate provides a logging API that can be hooked up to any number of logging backends, and can capture quite a bit of useful information.

If you're debugging a lot of async code tracing can be extremely useful too. (Even if youre NOT using async it can be really useful, rustc actually uses it now)

I would love to see better debugger support in Rust, but as others have said I find myself missing it a lot less than I expected.

2 Likes