Today I learned — episode 2 (hacking on Rust language)

October 28, 2016

This blog series (in short: TIL) is supposed to cover topics in software development, learnings from working in software companies, tooling, but also private matters (family, baby, hobbys).

Hacking on the Rust language — error messages

Right now I’m totally digging Rust, a modern systems programming language which covers for instance thread-safety and memory access checks at compile time to guarantee code safety — while still being close to hardware like C/C++ — and has many more benefits such as a well-designed standard library, fast paced community and release cycle, etc.

Since I’m professionally working in C++, I am currently drafting a presentation that compares C++ with Rust with the goal of finding out where C++ could improve. The plan is to first present the slides in the Munich C++ meetup when completed.

One topic where C++ lags behind are macros — in Rust (macro documentation), one can match language elements, instead of doing direct text preprocessing (pre = before the compiler even parses the code).

// Simple macro to autogenerate an enum-to-int function
macro_rules! enum_mapper {
    // `ident` means identifier, `expr` is an expression. `,*` is comma-separated repetition (optional trailing comma)
    ( $enum_name:ident, $( ($enum_variant:ident, $int_value:expr) ),* ) => {
        impl $enum_name {
            fn to_int(&self) -> i32 {
                match *self {
                    // I can put comments in a macro, and don't need to have backslashes everywhere!
                    $(
                        $enum_name::$enum_variant => $int_value
                    ),* // repetition consumes matches in lockstep
                }
            }
        }
    };
}

// Totally stupid enum
#[derive(Debug)]
enum State {
    Succeeded,
    Failed(String), // error message
    Timeout,
}

enum_mapper!(
    State,
    (Succeeded, 1),
    (Failed, 2),
    (Timeout, 3)
);

fn main() {
    let st = State::Failed("myerror".to_string());
    println!("{:?} maps to int {}", st, st.to_int());
}

That code snippet produces the error error[E0532]: expected unit struct/variant or constant, found tuple variant State::Failed with the nightly compiler. To me, reading such a verbose error was like learning C++ in my childhood — I just had no idea of the terminology used with the language, so "unit struct/variant" and "tuple variant" were totally unclear to me and not immediately intuitive. The displayed error location also wasn’t helpful, and neither provided me the expanded macro, nor the failing code line. In this sense, the error messages are on par with the C++ preprocessor (just as bad 😂). Normally, Rust provides error explanations with examples, displayed by rustc --explain E0532. But in this case: error: no extended information for E0532.

So I found out myself — removing the variant parameter (String) from State::Failed(String) (so the enum only has simple variants), my macro was working fine, and after some thinking it was clear that I had previously commented out the consideration of variant parameters (that’s how I call them at the moment). Here’s how I could match State::Failed(String):

$enum_name::$enum_variant(..) => $int_value

Note that this is not a solution because now it won’t match State::Succeeded and State::Timeout anymore (maybe it used to work earlier), but this article is more about getting to understand the problem by the error message.

Having found the problem, I still didn’t feel happy because that debug session cost me time and might happen again for me and surely for others as well. Hence, let’s hack Rust!

Getting started with hacking Rust is elegantly simple: clone, ./configure, make. That will build the whole compiler and rustdoc toolchain, but not the cargo build tool, but that’s already enough hard disk space and download/build time spent. On my slow connection, the configure script was still cloning LLVM after 15 minutes 🐢💨…​

make tips hints at different targets to limit what is built, like if you’re only working on the compiler (→ make rustc-stage1, make <target-triple>/stage1/bin/rustc).

In the meantime I searched the code for existing error messages (just grepped for the one of E0003), and immediately found the source file src/librustc_const_eval/diagnostics.rs. I found it strange that the list of diagnostic error messages was so short, so I did ag -l '^\s*E[0-9]{4}:' and discovered that the error messages belong to the respective crate. In case of the example error I grepped (E0003), it’s the crate for "constant evaluation on the HIR and code to validate patterns/matches" (HIR stands for High-level Intermediate Representation). My desired error explanation should therefore go into the right crate, which turned out to be librustc_resolve.

Finally the compiler build completed, but to my surprise, x86_64-apple-darwin/stage1/bin/rustc --explain E0003 could not find an explanation. That was peculiar as stage 1 already should give me a working compiler (as of the writeup in make tips and the great summary Contributing to the Rust compiler). The solution of the riddle was easy: E0003 has vanished with the following commit:

commit 76fb7d90ecde3659021341779fea598a6daab013
Author: Ariel Ben-Yehuda <ariel.byd@gmail.com>
Date:   Mon Oct 3 21:39:21 2016 +0300

    remove StaticInliner and NaN checking

    NaN checking was a lint for a deprecated feature. It can go away.

Using another error code, it displays the explanation just fine, e.g. x86_64-apple-darwin/stage1/bin/rustc --explain E0004. Only missing point was to get in my desired explanation and example for E0532, and test it in the same way. This part is too detailed for a blog post, but I ultimately ended up with a pull request (still pending at the time of writing).

Now I’m happy to have started my first contribution. There will surely be more blog posts following about my experiences with Rust!

P.S. Only later I found that the stable rustc 1.12.1 would’ve given a slightly better error for the initial problem (State::Failed does not name a unit variant, unit struct or a constant). Remember you can play around with Rust versions online or with rustup.