Custom error diagnostics with procedural macros on (almost) stable Rust


#1

I realized a capability of the compiler today that I don’t think I’ve seen used much, and I wanted to share! Currently the proc_macro API, the foundation of procedural macros, is quite slim on its implementation. The upcoming stabilization for 1.30 will bring a long-awaited feature, working with Span information!

Currently today if you’re parsing something, say in a custom derive, when you hit an error your only recourse is to panic!. This is a pretty bad UI experience for your users because the panic message isn’t attached to the right span of code that the error originated from, it just points to an attribute. While today proc_macro has some unstable APIs for diagnostics, they’re unlikely to be stable for the 2018 edition’s initial release. As it turns out, though, we’ll be able to have stable custom errors with custom spans as part of the 2018 edition release!

It turns out the compiler already has a feature for custom error messages, the compile_error! macro. Furthermore, as a procedural macro, you can manipulate the spans of each of the tokens of the compile_error!("foo") invocation. This means, subsequently, that you can target any error at any span!

For example given this demo library:

extern crate proc_macro;

use proc_macro::*;

#[proc_macro]
pub fn span_one_token(x: TokenStream) -> TokenStream {
    let span = x.into_iter().next().unwrap().span();

    error("this is a single token error", span, span)
}

#[proc_macro]
pub fn span_two_tokens(x: TokenStream) -> TokenStream {
    let mut iter = x.into_iter();
    let start = iter.next().unwrap().span();
    let end = iter.next().unwrap().span();

    error("this error spans two tokens, maybe more!", start, end)
}

#[proc_macro]
pub fn generate_two_errors(x: TokenStream) -> TokenStream {
    let mut iter = x.into_iter();
    let err1 = iter.next().unwrap().span();
    let err2 = iter.next().unwrap().span();

    let a = error("this error is for one token ...", err1, err1);
    let b = error("... and this error is for another", err2, err2);

    a.into_iter().chain(b.into_iter()).collect()
}

fn error(s: &str, start: Span, end: Span) -> TokenStream {
    let mut v = Vec::new();
    v.push(respan(Literal::string(&s), Span::call_site()));
    let group = v.into_iter().collect();

    let mut r = Vec::<TokenTree>::new();
    r.push(respan(Ident::new("compile_error", start), start));
    r.push(respan(Punct::new('!', Spacing::Alone), Span::call_site()));
    r.push(respan(Group::new(Delimiter::Brace, group), end));

    r.into_iter().collect()
}

fn respan<T: Into<TokenTree>>(t: T, span: Span) -> TokenTree {
    let mut t = t.into();
    t.set_span(span);
    t
}

and this invocation:

#![feature(use_extern_macros)]

extern crate foo;

foo::span_one_token! { a }
foo::span_two_tokens! { c d }
foo::generate_two_errors! { e f }

fn main() {
    println!("Hello, world!");
}

you get these errors:

   Compiling foo v0.1.0 (file:///home/alex/code/foo)
error: this is a single token error
 --> src/main.rs:5:24
  |
5 | foo::span_one_token! { a }
  |                        ^

error: this error spans two tokens, maybe more!
 --> src/main.rs:6:25
  |
6 | foo::span_two_tokens! { c d }
  |                         ^^^

error: this error is for one token ...
 --> src/main.rs:7:29
  |
7 | foo::generate_two_errors! { e f }
  |                             ^

error: ... and this error is for another
 --> src/main.rs:7:31
  |
7 | foo::generate_two_errors! { e f }
  |                               ^

error: aborting due to 4 previous errors

and voila! Custom errors on stable Rust as soon as macros 1.2 is stabilized in the 2018 edition release.

If you’re curious about seeing this in action, I’ve opened an issue on wasm-bindgen to make use of this trick and have started to capitalize on this ability in rustwasm/wasm-bindgen#608


#2

Awesome! Is it possible to emit those errors conditionally on type checking results, maybe by wrapping it with another macro? Even just gating them on an associated const or type comparison would open up a lot of possibilities, especially when working with recursive types where just the crate::mod:: path eats up 95% of the error and makes it nearly impossible to identify where in the chain of trait bounds the problem is, let alone what the types are.


#3

An idea with is slightly off-topic to this thread but which I’d like to record for future reference is…

… It would be cool if you could control the color and boldness of certain segments of your custom error message; for example, I might have custom error codes (and an index of the errors elsewhere) and I want to color the error codes with red and make them bold…

… I have tried ANSI escape codes in compile_error!(..) but that did not work.


#4

Ah unfortunately I don’t know how to do that sort of conditional error handling in the compiler. This idea though could perhaps be extended to different strategies in the future though! We can also just spend time stabilizing the Diagnostic API in the proc_macro crate too :slight_smile: