Skip to content

Commit 570c16a

Browse files
authored
Rollup merge of rust-lang#79944 - sivadeilra:syms_proc_macro_testing, r=petrochenkov
Improve error handling in `symbols` proc-macro This improves how the `symbols` proc-macro handles errors. If it finds an error in its input, the macro does not panic. Instead, it still produces an output token stream. That token stream will contain `compile_error!(...)` macro invocations. This will still cause compilation to fail (which is what we want), but it will prevent meaningless errors caused by the output not containing symbols that the macro normally generates. This solves a small (but annoying) problem. When you're editing rustc_span/src/symbol.rs, and you get something wrong (dup symbol name, misordered symbol), you want to get only the errors that are relevant, not a burst of errors that are irrelevant. This change also uses the correct Span when reporting errors, so you get errors that point to the correct place in rustc_span/src/symbol.rs where something is wrong. This also adds several unit tests which test the `symbols` proc-macro. This commit also makes it easy to run the `symbols` proc-macro as an ordinary Cargo test. Just run `cargo test`. This makes it easier to do development on the macro itself, such as running it under a debugger. This commit also uses the `Punctuated` type in `syn` for parsing comma-separated lists, rather than doing it manually. The output of the macro is not changed at all by this commit, so rustc should be completely unchanged. This just improves quality of life during development.
2 parents 4fceedb + 1a5b9b0 commit 570c16a

File tree

3 files changed

+204
-54
lines changed

3 files changed

+204
-54
lines changed

Diff for: compiler/rustc_macros/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub fn rustc_queries(input: TokenStream) -> TokenStream {
2121

2222
#[proc_macro]
2323
pub fn symbols(input: TokenStream) -> TokenStream {
24-
symbols::symbols(input)
24+
symbols::symbols(input.into()).into()
2525
}
2626

2727
decl_derive!([HashStable, attributes(stable_hasher)] => hash_stable::hash_stable_derive);

Diff for: compiler/rustc_macros/src/symbols.rs

+101-53
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,35 @@
1-
use proc_macro::TokenStream;
1+
//! Proc macro which builds the Symbol table
2+
//!
3+
//! # Debugging
4+
//!
5+
//! Since this proc-macro does some non-trivial work, debugging it is important.
6+
//! This proc-macro can be invoked as an ordinary unit test, like so:
7+
//!
8+
//! ```bash
9+
//! cd compiler/rustc_macros
10+
//! cargo test symbols::test_symbols -- --nocapture
11+
//! ```
12+
//!
13+
//! This unit test finds the `symbols!` invocation in `compiler/rustc_span/src/symbol.rs`
14+
//! and runs it. It verifies that the output token stream can be parsed as valid module
15+
//! items and that no errors were produced.
16+
//!
17+
//! You can also view the generated code by using `cargo expand`:
18+
//!
19+
//! ```bash
20+
//! cargo install cargo-expand # this is necessary only once
21+
//! cd compiler/rustc_span
22+
//! cargo expand > /tmp/rustc_span.rs # it's a big file
23+
//! ```
24+
25+
use proc_macro2::{Span, TokenStream};
226
use quote::quote;
3-
use std::collections::HashSet;
27+
use std::collections::HashMap;
428
use syn::parse::{Parse, ParseStream, Result};
5-
use syn::{braced, parse_macro_input, Ident, LitStr, Token};
29+
use syn::{braced, punctuated::Punctuated, Ident, LitStr, Token};
30+
31+
#[cfg(test)]
32+
mod tests;
633

734
mod kw {
835
syn::custom_keyword!(Keywords);
@@ -19,7 +46,6 @@ impl Parse for Keyword {
1946
let name = input.parse()?;
2047
input.parse::<Token![:]>()?;
2148
let value = input.parse()?;
22-
input.parse::<Token![,]>()?;
2349

2450
Ok(Keyword { name, value })
2551
}
@@ -37,78 +63,101 @@ impl Parse for Symbol {
3763
Ok(_) => Some(input.parse()?),
3864
Err(_) => None,
3965
};
40-
input.parse::<Token![,]>()?;
4166

4267
Ok(Symbol { name, value })
4368
}
4469
}
4570

46-
/// A type used to greedily parse another type until the input is empty.
47-
struct List<T>(Vec<T>);
48-
49-
impl<T: Parse> Parse for List<T> {
50-
fn parse(input: ParseStream<'_>) -> Result<Self> {
51-
let mut list = Vec::new();
52-
while !input.is_empty() {
53-
list.push(input.parse()?);
54-
}
55-
Ok(List(list))
56-
}
57-
}
58-
5971
struct Input {
60-
keywords: List<Keyword>,
61-
symbols: List<Symbol>,
72+
keywords: Punctuated<Keyword, Token![,]>,
73+
symbols: Punctuated<Symbol, Token![,]>,
6274
}
6375

6476
impl Parse for Input {
6577
fn parse(input: ParseStream<'_>) -> Result<Self> {
6678
input.parse::<kw::Keywords>()?;
6779
let content;
6880
braced!(content in input);
69-
let keywords = content.parse()?;
81+
let keywords = Punctuated::parse_terminated(&content)?;
7082

7183
input.parse::<kw::Symbols>()?;
7284
let content;
7385
braced!(content in input);
74-
let symbols = content.parse()?;
86+
let symbols = Punctuated::parse_terminated(&content)?;
7587

7688
Ok(Input { keywords, symbols })
7789
}
7890
}
7991

92+
#[derive(Default)]
93+
struct Errors {
94+
list: Vec<syn::Error>,
95+
}
96+
97+
impl Errors {
98+
fn error(&mut self, span: Span, message: String) {
99+
self.list.push(syn::Error::new(span, message));
100+
}
101+
}
102+
80103
pub fn symbols(input: TokenStream) -> TokenStream {
81-
let input = parse_macro_input!(input as Input);
104+
let (mut output, errors) = symbols_with_errors(input);
105+
106+
// If we generated any errors, then report them as compiler_error!() macro calls.
107+
// This lets the errors point back to the most relevant span. It also allows us
108+
// to report as many errors as we can during a single run.
109+
output.extend(errors.into_iter().map(|e| e.to_compile_error()));
110+
111+
output
112+
}
113+
114+
fn symbols_with_errors(input: TokenStream) -> (TokenStream, Vec<syn::Error>) {
115+
let mut errors = Errors::default();
116+
117+
let input: Input = match syn::parse2(input) {
118+
Ok(input) => input,
119+
Err(e) => {
120+
// This allows us to display errors at the proper span, while minimizing
121+
// unrelated errors caused by bailing out (and not generating code).
122+
errors.list.push(e);
123+
Input { keywords: Default::default(), symbols: Default::default() }
124+
}
125+
};
82126

83127
let mut keyword_stream = quote! {};
84128
let mut symbols_stream = quote! {};
85129
let mut digits_stream = quote! {};
86130
let mut prefill_stream = quote! {};
87131
let mut counter = 0u32;
88-
let mut keys = HashSet::<String>::new();
89-
let mut prev_key: Option<String> = None;
90-
let mut errors = Vec::<String>::new();
91-
92-
let mut check_dup = |str: &str, errors: &mut Vec<String>| {
93-
if !keys.insert(str.to_string()) {
94-
errors.push(format!("Symbol `{}` is duplicated", str));
132+
let mut keys =
133+
HashMap::<String, Span>::with_capacity(input.keywords.len() + input.symbols.len() + 10);
134+
let mut prev_key: Option<(Span, String)> = None;
135+
136+
let mut check_dup = |span: Span, str: &str, errors: &mut Errors| {
137+
if let Some(prev_span) = keys.get(str) {
138+
errors.error(span, format!("Symbol `{}` is duplicated", str));
139+
errors.error(*prev_span, format!("location of previous definition"));
140+
} else {
141+
keys.insert(str.to_string(), span);
95142
}
96143
};
97144

98-
let mut check_order = |str: &str, errors: &mut Vec<String>| {
99-
if let Some(ref prev_str) = prev_key {
145+
let mut check_order = |span: Span, str: &str, errors: &mut Errors| {
146+
if let Some((prev_span, ref prev_str)) = prev_key {
100147
if str < prev_str {
101-
errors.push(format!("Symbol `{}` must precede `{}`", str, prev_str));
148+
errors.error(span, format!("Symbol `{}` must precede `{}`", str, prev_str));
149+
errors.error(prev_span, format!("location of previous symbol `{}`", prev_str));
102150
}
103151
}
104-
prev_key = Some(str.to_string());
152+
prev_key = Some((span, str.to_string()));
105153
};
106154

107155
// Generate the listed keywords.
108-
for keyword in &input.keywords.0 {
156+
for keyword in input.keywords.iter() {
109157
let name = &keyword.name;
110158
let value = &keyword.value;
111-
check_dup(&value.value(), &mut errors);
159+
let value_string = value.value();
160+
check_dup(keyword.name.span(), &value_string, &mut errors);
112161
prefill_stream.extend(quote! {
113162
#value,
114163
});
@@ -120,14 +169,15 @@ pub fn symbols(input: TokenStream) -> TokenStream {
120169
}
121170

122171
// Generate the listed symbols.
123-
for symbol in &input.symbols.0 {
172+
for symbol in input.symbols.iter() {
124173
let name = &symbol.name;
125174
let value = match &symbol.value {
126175
Some(value) => value.value(),
127176
None => name.to_string(),
128177
};
129-
check_dup(&value, &mut errors);
130-
check_order(&name.to_string(), &mut errors);
178+
check_dup(symbol.name.span(), &value, &mut errors);
179+
check_order(symbol.name.span(), &name.to_string(), &mut errors);
180+
131181
prefill_stream.extend(quote! {
132182
#value,
133183
});
@@ -142,7 +192,7 @@ pub fn symbols(input: TokenStream) -> TokenStream {
142192
// Generate symbols for the strings "0", "1", ..., "9".
143193
for n in 0..10 {
144194
let n = n.to_string();
145-
check_dup(&n, &mut errors);
195+
check_dup(Span::call_site(), &n, &mut errors);
146196
prefill_stream.extend(quote! {
147197
#n,
148198
});
@@ -152,14 +202,7 @@ pub fn symbols(input: TokenStream) -> TokenStream {
152202
counter += 1;
153203
}
154204

155-
if !errors.is_empty() {
156-
for error in errors.into_iter() {
157-
eprintln!("error: {}", error)
158-
}
159-
panic!("errors in `Keywords` and/or `Symbols`");
160-
}
161-
162-
let tt = TokenStream::from(quote! {
205+
let output = quote! {
163206
macro_rules! keywords {
164207
() => {
165208
#keyword_stream
@@ -184,11 +227,16 @@ pub fn symbols(input: TokenStream) -> TokenStream {
184227
])
185228
}
186229
}
187-
});
230+
};
188231

189-
// To see the generated code generated, uncomment this line, recompile, and
190-
// run the resulting output through `rustfmt`.
191-
//eprintln!("{}", tt);
232+
(output, errors.list)
192233

193-
tt
234+
// To see the generated code, use the "cargo expand" command.
235+
// Do this once to install:
236+
// cargo install cargo-expand
237+
//
238+
// Then, cd to rustc_span and run:
239+
// cargo expand > /tmp/rustc_span_expanded.rs
240+
//
241+
// and read that file.
194242
}

Diff for: compiler/rustc_macros/src/symbols/tests.rs

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use super::*;
2+
3+
// This test is mainly here for interactive development. Use this test while
4+
// you're working on the proc-macro defined in this file.
5+
#[test]
6+
fn test_symbols() {
7+
// We textually include the symbol.rs file, which contains the list of all
8+
// symbols, keywords, and common words. Then we search for the
9+
// `symbols! { ... }` call.
10+
11+
static SYMBOL_RS_FILE: &str = include_str!("../../../rustc_span/src/symbol.rs");
12+
13+
let file = syn::parse_file(SYMBOL_RS_FILE).unwrap();
14+
let symbols_path: syn::Path = syn::parse_quote!(symbols);
15+
16+
let m: &syn::ItemMacro = file
17+
.items
18+
.iter()
19+
.filter_map(|i| {
20+
if let syn::Item::Macro(m) = i {
21+
if m.mac.path == symbols_path { Some(m) } else { None }
22+
} else {
23+
None
24+
}
25+
})
26+
.next()
27+
.expect("did not find `symbols!` macro invocation.");
28+
29+
let body_tokens = m.mac.tokens.clone();
30+
31+
test_symbols_macro(body_tokens, &[]);
32+
}
33+
34+
fn test_symbols_macro(input: TokenStream, expected_errors: &[&str]) {
35+
let (output, found_errors) = symbols_with_errors(input);
36+
37+
// It should always parse.
38+
let _parsed_file = syn::parse2::<syn::File>(output).unwrap();
39+
40+
assert_eq!(
41+
found_errors.len(),
42+
expected_errors.len(),
43+
"Macro generated a different number of errors than expected"
44+
);
45+
46+
for (found_error, &expected_error) in found_errors.iter().zip(expected_errors.iter()) {
47+
let found_error_str = format!("{}", found_error);
48+
assert_eq!(found_error_str, expected_error);
49+
}
50+
}
51+
52+
#[test]
53+
fn check_dup_keywords() {
54+
let input = quote! {
55+
Keywords {
56+
Crate: "crate",
57+
Crate: "crate",
58+
}
59+
Symbols {}
60+
};
61+
test_symbols_macro(input, &["Symbol `crate` is duplicated", "location of previous definition"]);
62+
}
63+
64+
#[test]
65+
fn check_dup_symbol() {
66+
let input = quote! {
67+
Keywords {}
68+
Symbols {
69+
splat,
70+
splat,
71+
}
72+
};
73+
test_symbols_macro(input, &["Symbol `splat` is duplicated", "location of previous definition"]);
74+
}
75+
76+
#[test]
77+
fn check_dup_symbol_and_keyword() {
78+
let input = quote! {
79+
Keywords {
80+
Splat: "splat",
81+
}
82+
Symbols {
83+
splat,
84+
}
85+
};
86+
test_symbols_macro(input, &["Symbol `splat` is duplicated", "location of previous definition"]);
87+
}
88+
89+
#[test]
90+
fn check_symbol_order() {
91+
let input = quote! {
92+
Keywords {}
93+
Symbols {
94+
zebra,
95+
aardvark,
96+
}
97+
};
98+
test_symbols_macro(
99+
input,
100+
&["Symbol `aardvark` must precede `zebra`", "location of previous symbol `zebra`"],
101+
);
102+
}

0 commit comments

Comments
 (0)