Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3e4d3d2

Browse files
committedNov 12, 2021
proc_macro: Add an expand_expr method to TokenStream
This feature is aimed at giving proc macros access to powers similar to those used by builtin macros such as `format_args!` or `concat!`. These macros are able to accept macros in place of string literal parameters, such as the format string, as they perform recursive macro expansion while being expanded. This can be especially useful in many cases thanks to helper macros like `concat!`, `stringify!` and `include_str!` which are often used to construct string literals at compile-time in user code. For now, this method only allows expanding macros which produce literals, although more expresisons will be supported before the method is stabilized.
1 parent 800a156 commit 3e4d3d2

File tree

8 files changed

+421
-75
lines changed

8 files changed

+421
-75
lines changed
 

‎compiler/rustc_expand/src/proc_macro.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ impl base::ProcMacro for BangProcMacro {
2424
span: Span,
2525
input: TokenStream,
2626
) -> Result<TokenStream, ErrorReported> {
27+
let proc_macro_backtrace = ecx.ecfg.proc_macro_backtrace;
2728
let server = proc_macro_server::Rustc::new(ecx);
28-
self.client.run(&EXEC_STRATEGY, server, input, ecx.ecfg.proc_macro_backtrace).map_err(|e| {
29+
self.client.run(&EXEC_STRATEGY, server, input, proc_macro_backtrace).map_err(|e| {
2930
let mut err = ecx.struct_span_err(span, "proc macro panicked");
3031
if let Some(s) = e.as_str() {
3132
err.help(&format!("message: {}", s));
@@ -48,9 +49,10 @@ impl base::AttrProcMacro for AttrProcMacro {
4849
annotation: TokenStream,
4950
annotated: TokenStream,
5051
) -> Result<TokenStream, ErrorReported> {
52+
let proc_macro_backtrace = ecx.ecfg.proc_macro_backtrace;
5153
let server = proc_macro_server::Rustc::new(ecx);
5254
self.client
53-
.run(&EXEC_STRATEGY, server, annotation, annotated, ecx.ecfg.proc_macro_backtrace)
55+
.run(&EXEC_STRATEGY, server, annotation, annotated, proc_macro_backtrace)
5456
.map_err(|e| {
5557
let mut err = ecx.struct_span_err(span, "custom attribute panicked");
5658
if let Some(s) = e.as_str() {
@@ -97,19 +99,19 @@ impl MultiItemModifier for ProcMacroDerive {
9799
nt_to_tokenstream(&item, &ecx.sess.parse_sess, CanSynthesizeMissingTokens::No)
98100
};
99101

102+
let proc_macro_backtrace = ecx.ecfg.proc_macro_backtrace;
100103
let server = proc_macro_server::Rustc::new(ecx);
101-
let stream =
102-
match self.client.run(&EXEC_STRATEGY, server, input, ecx.ecfg.proc_macro_backtrace) {
103-
Ok(stream) => stream,
104-
Err(e) => {
105-
let mut err = ecx.struct_span_err(span, "proc-macro derive panicked");
106-
if let Some(s) = e.as_str() {
107-
err.help(&format!("message: {}", s));
108-
}
109-
err.emit();
110-
return ExpandResult::Ready(vec![]);
104+
let stream = match self.client.run(&EXEC_STRATEGY, server, input, proc_macro_backtrace) {
105+
Ok(stream) => stream,
106+
Err(e) => {
107+
let mut err = ecx.struct_span_err(span, "proc-macro derive panicked");
108+
if let Some(s) = e.as_str() {
109+
err.help(&format!("message: {}", s));
111110
}
112-
};
111+
err.emit();
112+
return ExpandResult::Ready(vec![]);
113+
}
114+
};
113115

114116
let error_count_before = ecx.sess.parse_sess.span_diagnostic.err_count();
115117
let mut parser =

‎compiler/rustc_expand/src/proc_macro_server.rs

Lines changed: 107 additions & 55 deletions
Large diffs are not rendered by default.

‎library/proc_macro/src/bridge/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ macro_rules! with_api {
6262
fn clone($self: &$S::TokenStream) -> $S::TokenStream;
6363
fn new() -> $S::TokenStream;
6464
fn is_empty($self: &$S::TokenStream) -> bool;
65+
fn expand_expr($self: &$S::TokenStream) -> Result<$S::TokenStream, ()>;
6566
fn from_str(src: &str) -> $S::TokenStream;
6667
fn to_string($self: &$S::TokenStream) -> String;
6768
fn from_token_tree(

‎library/proc_macro/src/lib.rs

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,6 @@ impl !Sync for TokenStream {}
8888
#[derive(Debug)]
8989
pub struct LexError;
9090

91-
impl LexError {
92-
fn new() -> Self {
93-
LexError
94-
}
95-
}
96-
9791
#[stable(feature = "proc_macro_lexerror_impls", since = "1.44.0")]
9892
impl fmt::Display for LexError {
9993
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -109,6 +103,28 @@ impl !Send for LexError {}
109103
#[stable(feature = "proc_macro_lib", since = "1.15.0")]
110104
impl !Sync for LexError {}
111105

106+
/// Error returned from `TokenStream::expand_expr`.
107+
#[unstable(feature = "proc_macro_expand", issue = "90765")]
108+
#[non_exhaustive]
109+
#[derive(Debug)]
110+
pub struct ExpandError;
111+
112+
#[unstable(feature = "proc_macro_expand", issue = "90765")]
113+
impl fmt::Display for ExpandError {
114+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115+
f.write_str("macro expansion failed")
116+
}
117+
}
118+
119+
#[unstable(feature = "proc_macro_expand", issue = "90765")]
120+
impl error::Error for ExpandError {}
121+
122+
#[unstable(feature = "proc_macro_expand", issue = "90765")]
123+
impl !Send for ExpandError {}
124+
125+
#[unstable(feature = "proc_macro_expand", issue = "90765")]
126+
impl !Sync for ExpandError {}
127+
112128
impl TokenStream {
113129
/// Returns an empty `TokenStream` containing no token trees.
114130
#[stable(feature = "proc_macro_lib2", since = "1.29.0")]
@@ -121,6 +137,24 @@ impl TokenStream {
121137
pub fn is_empty(&self) -> bool {
122138
self.0.is_empty()
123139
}
140+
141+
/// Parses this `TokenStream` as an expression and attempts to expand any
142+
/// macros within it. Returns the expanded `TokenStream`.
143+
///
144+
/// Currently only expressions expanding to literals will succeed, although
145+
/// this may be relaxed in the future.
146+
///
147+
/// NOTE: In error conditions, `expand_expr` may leave macros unexpanded,
148+
/// report an error, failing compilation, and/or return an `Err(..)`. The
149+
/// specific behavior for any error condition, and what conditions are
150+
/// considered errors, is unspecified and may change in the future.
151+
#[unstable(feature = "proc_macro_expand", issue = "90765")]
152+
pub fn expand_expr(&self) -> Result<TokenStream, ExpandError> {
153+
match bridge::client::TokenStream::expand_expr(&self.0) {
154+
Ok(stream) => Ok(TokenStream(stream)),
155+
Err(_) => Err(ExpandError),
156+
}
157+
}
124158
}
125159

126160
/// Attempts to break the string into tokens and parse those tokens into a token stream.
@@ -1211,7 +1245,7 @@ impl FromStr for Literal {
12111245
fn from_str(src: &str) -> Result<Self, LexError> {
12121246
match bridge::client::Literal::from_str(src) {
12131247
Ok(literal) => Ok(Literal(literal)),
1214-
Err(()) => Err(LexError::new()),
1248+
Err(()) => Err(LexError),
12151249
}
12161250
}
12171251
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// force-host
2+
// no-prefer-dynamic
3+
4+
#![crate_type = "proc-macro"]
5+
#![deny(warnings)]
6+
#![feature(proc_macro_expand, proc_macro_span)]
7+
8+
extern crate proc_macro;
9+
10+
use proc_macro::*;
11+
use std::str::FromStr;
12+
13+
#[proc_macro]
14+
pub fn expand_expr_is(input: TokenStream) -> TokenStream {
15+
let mut iter = input.into_iter();
16+
let mut expected_tts = Vec::new();
17+
loop {
18+
match iter.next() {
19+
Some(TokenTree::Punct(ref p)) if p.as_char() == ',' => break,
20+
Some(tt) => expected_tts.push(tt),
21+
None => panic!("expected comma"),
22+
}
23+
}
24+
25+
let expected = expected_tts.into_iter().collect::<TokenStream>();
26+
let expanded = iter.collect::<TokenStream>().expand_expr().expect("expand_expr failed");
27+
assert!(
28+
expected.to_string() == expanded.to_string(),
29+
"assert failed\nexpected: `{}`\nexpanded: `{}`",
30+
expected.to_string(),
31+
expanded.to_string()
32+
);
33+
34+
TokenStream::new()
35+
}
36+
37+
#[proc_macro]
38+
pub fn expand_expr_fail(input: TokenStream) -> TokenStream {
39+
match input.expand_expr() {
40+
Ok(ts) => panic!("expand_expr unexpectedly succeeded: `{}`", ts),
41+
Err(_) => TokenStream::new(),
42+
}
43+
}
44+
45+
#[proc_macro]
46+
pub fn check_expand_expr_file(ts: TokenStream) -> TokenStream {
47+
// Check that the passed in `file!()` invocation and a parsed `file!`
48+
// invocation expand to the same literal.
49+
let input_t = ts.expand_expr().expect("expand_expr failed on macro input").to_string();
50+
let parse_t = TokenStream::from_str("file!{}")
51+
.unwrap()
52+
.expand_expr()
53+
.expect("expand_expr failed on internal macro")
54+
.to_string();
55+
assert_eq!(input_t, parse_t);
56+
57+
// Check that the literal matches `Span::call_site().source_file().path()`
58+
let expect_t =
59+
Literal::string(&Span::call_site().source_file().path().to_string_lossy()).to_string();
60+
assert_eq!(input_t, expect_t);
61+
62+
TokenStream::new()
63+
}
64+
65+
#[proc_macro]
66+
pub fn recursive_expand(_: TokenStream) -> TokenStream {
67+
// Recursively call until we hit the recursion limit and get an error.
68+
//
69+
// NOTE: This doesn't panic if expansion fails because that'll cause a very
70+
// large number of errors to fill the output.
71+
TokenStream::from_str("recursive_expand!{}")
72+
.unwrap()
73+
.expand_expr()
74+
.unwrap_or(std::iter::once(TokenTree::Literal(Literal::u32_suffixed(0))).collect())
75+
}
76+
77+
#[proc_macro]
78+
pub fn echo_pm(input: TokenStream) -> TokenStream {
79+
input
80+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Included file contents

‎src/test/ui/proc-macro/expand-expr.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// aux-build:expand-expr.rs
2+
3+
extern crate expand_expr;
4+
5+
use expand_expr::{
6+
check_expand_expr_file, echo_pm, expand_expr_fail, expand_expr_is, recursive_expand,
7+
};
8+
9+
// Check builtin macros can be expanded.
10+
11+
expand_expr_is!(11u32, line!());
12+
expand_expr_is!(24u32, column!());
13+
14+
expand_expr_is!("Hello, World!", concat!("Hello, ", "World", "!"));
15+
expand_expr_is!("int10floats5.3booltrue", concat!("int", 10, "floats", 5.3, "bool", true));
16+
expand_expr_is!("Hello", concat!(r##"Hello"##));
17+
18+
expand_expr_is!("Included file contents\n", include_str!("auxiliary/included-file.txt"));
19+
expand_expr_is!(b"Included file contents\n", include_bytes!("auxiliary/included-file.txt"));
20+
21+
expand_expr_is!(
22+
"contents: Included file contents\n",
23+
concat!("contents: ", include_str!("auxiliary/included-file.txt"))
24+
);
25+
26+
// Correct value is checked for multiple sources.
27+
check_expand_expr_file!(file!());
28+
29+
expand_expr_is!("hello", stringify!(hello));
30+
expand_expr_is!("10 + 20", stringify!(10 + 20));
31+
32+
macro_rules! echo_tts {
33+
($($t:tt)*) => { $($t)* }; //~ ERROR: expected expression, found `$`
34+
}
35+
36+
macro_rules! echo_lit {
37+
($l:literal) => {
38+
$l
39+
};
40+
}
41+
42+
macro_rules! echo_expr {
43+
($e:expr) => {
44+
$e
45+
};
46+
}
47+
48+
macro_rules! simple_lit {
49+
($l:literal) => {
50+
expand_expr_is!($l, $l);
51+
expand_expr_is!($l, echo_lit!($l));
52+
expand_expr_is!($l, echo_expr!($l));
53+
expand_expr_is!($l, echo_tts!($l));
54+
expand_expr_is!($l, echo_pm!($l));
55+
const _: () = {
56+
macro_rules! mac {
57+
() => {
58+
$l
59+
};
60+
}
61+
expand_expr_is!($l, mac!());
62+
expand_expr_is!($l, echo_expr!(mac!()));
63+
expand_expr_is!($l, echo_tts!(mac!()));
64+
expand_expr_is!($l, echo_pm!(mac!()));
65+
};
66+
};
67+
}
68+
69+
simple_lit!("Hello, World");
70+
simple_lit!('c');
71+
simple_lit!(b'c');
72+
simple_lit!(10);
73+
simple_lit!(10.0);
74+
simple_lit!(10.0f64);
75+
simple_lit!(-3.14159);
76+
simple_lit!(-3.5e10);
77+
simple_lit!(0xFEED);
78+
simple_lit!(-0xFEED);
79+
simple_lit!(0b0100);
80+
simple_lit!(-0b0100);
81+
simple_lit!("string");
82+
simple_lit!(r##"raw string"##);
83+
simple_lit!(b"byte string");
84+
simple_lit!(br##"raw byte string"##);
85+
simple_lit!(true);
86+
simple_lit!(false);
87+
88+
// Ensure char escapes aren't normalized by expansion
89+
simple_lit!("\u{0}");
90+
simple_lit!("\0");
91+
simple_lit!("\x00");
92+
simple_lit!('\u{0}');
93+
simple_lit!('\0');
94+
simple_lit!('\x00');
95+
simple_lit!(b"\x00");
96+
simple_lit!(b"\0");
97+
simple_lit!(b'\x00');
98+
simple_lit!(b'\0');
99+
100+
// Extra tokens after the string literal aren't ignored
101+
expand_expr_fail!("string"; hello); //~ ERROR: expected one of `.`, `?`, or an operator, found `;`
102+
103+
// Invalid expressions produce errors in addition to returning `Err(())`.
104+
expand_expr_fail!($); //~ ERROR: expected expression, found `$`
105+
expand_expr_fail!(echo_tts!($));
106+
expand_expr_fail!(echo_pm!($)); //~ ERROR: expected expression, found `$`
107+
108+
// We get errors reported and recover during macro expansion if the macro
109+
// doesn't produce a valid expression.
110+
expand_expr_is!("string", echo_tts!("string"; hello)); //~ ERROR: macro expansion ignores token `hello` and any following
111+
expand_expr_is!("string", echo_pm!("string"; hello)); //~ ERROR: macro expansion ignores token `;` and any following
112+
113+
// For now, fail if a non-literal expression is expanded.
114+
expand_expr_fail!(arbitrary_expression() + "etc");
115+
expand_expr_fail!(echo_tts!(arbitrary_expression() + "etc"));
116+
expand_expr_fail!(echo_expr!(arbitrary_expression() + "etc"));
117+
expand_expr_fail!(echo_pm!(arbitrary_expression() + "etc"));
118+
119+
const _: u32 = recursive_expand!(); //~ ERROR: recursion limit reached while expanding `recursive_expand!`
120+
121+
fn main() {}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
error: expected one of `.`, `?`, or an operator, found `;`
2+
--> $DIR/expand-expr.rs:101:27
3+
|
4+
LL | expand_expr_fail!("string"; hello);
5+
| ^ expected one of `.`, `?`, or an operator
6+
7+
error: expected expression, found `$`
8+
--> $DIR/expand-expr.rs:104:19
9+
|
10+
LL | expand_expr_fail!($);
11+
| ^ expected expression
12+
13+
error: expected expression, found `$`
14+
--> $DIR/expand-expr.rs:33:23
15+
|
16+
LL | ($($t:tt)*) => { $($t)* };
17+
| ^^^^ expected expression
18+
19+
error: expected expression, found `$`
20+
--> $DIR/expand-expr.rs:106:28
21+
|
22+
LL | expand_expr_fail!(echo_pm!($));
23+
| ^ expected expression
24+
25+
error: macro expansion ignores token `hello` and any following
26+
--> $DIR/expand-expr.rs:110:47
27+
|
28+
LL | expand_expr_is!("string", echo_tts!("string"; hello));
29+
| --------------------^^^^^-- help: you might be missing a semicolon here: `;`
30+
| |
31+
| caused by the macro expansion here
32+
|
33+
= note: the usage of `echo_tts!` is likely invalid in expression context
34+
35+
error: macro expansion ignores token `;` and any following
36+
--> $DIR/expand-expr.rs:111:44
37+
|
38+
LL | expand_expr_is!("string", echo_pm!("string"; hello));
39+
| -----------------^-------- help: you might be missing a semicolon here: `;`
40+
| |
41+
| caused by the macro expansion here
42+
|
43+
= note: the usage of `echo_pm!` is likely invalid in expression context
44+
45+
error: recursion limit reached while expanding `recursive_expand!`
46+
--> $DIR/expand-expr.rs:119:16
47+
|
48+
LL | const _: u32 = recursive_expand!();
49+
| ^^^^^^^^^^^^^^^^^^^
50+
|
51+
= help: consider increasing the recursion limit by adding a `#![recursion_limit = "256"]` attribute to your crate (`expand_expr`)
52+
= note: this error originates in the macro `recursive_expand` (in Nightly builds, run with -Z macro-backtrace for more info)
53+
54+
error: aborting due to 7 previous errors
55+

0 commit comments

Comments
 (0)
Please sign in to comment.