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