Runtime Diagnostics
— 2019-11-03

Rust is well-known for its helpful error messages, good tooling, and generally empathic compiler interface. If something goes wrong, Rust tries hard to help you get back on track.

In this post I'd like to talk about the runtime aspect of debugging errors.

Compiler Diagnostics

The umbrella term for the messages the compiler prints during compilation is "diagnostics". When something goes wrong, the compiler has a lot of code in place to try and identify what kind of error occurred, and if there's perhaps a suggestion available to help you fix it.

fn main() {
    printline!("hello world!");
}
error: cannot find macro `printline!` in this scope
 --> src/main.rs:2:5
  |
2 |     printline!("hello world!");
  |     ^^^^^^^^^ help: a macro with a similar name exists: `println`

As ekuber once described it: in order to provide helpful error messages, the compiler needs to understand a super set of the language. That way when it rejects invalid code, it can still understand what you were trying to do and guide you to the allowed subset.

Runtime Diagnostics

During runtime the compiler no longer plays a role, and all error messages are handled by the binary we've compiled. If a bug occurs in a Rust program during Runtime, there several methods we can employ to figure out why the program was rejected:

Individual tools are often referred to as "backtrace support", "debugging", or "performance analysis". But I think these are all quite closely related, and and it would make sense to group group them together under the umbrella of "runtime diagnostics". Because in the end all of these are similarly tools built to help diagnose invalid code, and convert it to valid code. The only difference is at which stage of development these tools are run.

Looking Ahead

If you consider Rust's runtime diagnostics to be a single system, then there are probably some things that could be improved. The first one being: figuring out how to debug something often requires memorizing quite a few different tools.

It'd be nice if cargo run integrated nicely with debuggers, so you could for example do: cargo run --debug to spin up a debugger on a breakpoint. Or cargo run --backtrace to enable backtraces? Or what if we had rust's diagnostics printing available when something panics as well?

fn main() {
    todo!();
}
thread 'main' panicked at 'todo!', src/main.rs:2:5
 --> src/main.rs:2:5
  |
2 |     todo!();
  |     ^^^^^^^^^ help: `fn main` has not yet been implemented

Or what about being able to detect and report deadlocks?

use std::sync::Mutex;

fn main() {
    let data = Mutex::new(());
    let d1 = data.lock();
    let d2 = data.lock();
}
mutex deadlocked, src/main.rs:6:5
  |
4 |     let data = Mutex::new(());
  |         ---- mutex was created here.
5 |     let d1 = data.lock();
  |         -- lock was first acquired here
6 |     let d2 = data.lock();
  |         ^^ lock could not be acquired here

There are a whole range of tools we may want to apply, from miri to thread-safety sanitizers.

I think it would be fantastic if we could treat all of these systems as a cohesive set of runtime diagnostics, that form a counterpart to Rust's already excellent compile diagnostics.

Conclusion

In this post we've looked at compile diagnostics, discussed what runtime diagnostics are, and looked at possible future directions.

This post is not so much intended to present any new innovations, but instead provide a lens through which we can look at a general group of challenges and the tools that help address them.

I think the Rust compiler truly nailed the ergonomics of reporting errors during compilation. My hope is that someday we'll be have a similarly cohesive UX for runtime errors too.


Thanks to ekuber for all the work he's done on compiler diagnostics, and a very entertaining chat during Rustconf about diagnostics.