Skip to main content

chunked_quote_impl/
lib.rs

1use proc_macro2::{
2	Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree,
3	token_stream::IntoIter,
4};
5use quote::{TokenStreamExt, quote as oquote, quote_spanned as oquote_spanned};
6
7/// append to [`TokenStream`] code generating the input tokens.
8///
9/// **syntax:** `chunk!(stream: ident, ...)`.
10///
11/// all not interpolated tokens inherit the [`Span::call_site`] span.
12///
13/// supported syntax:
14/// - `#ident`: append the resolved value of `ident` through [`ToTokens`](quote::ToTokens).
15/// - `#{expr}`: append the resolved value of `expr` through [`ToTokens`](quote::ToTokens).
16/// - `##`: append a `#`.
17/// - `#op expr #{tokens}`: append `tokens` based on the evaluation of `op expr`. `op` can be `if`, `for`, `while`, `else`, `match`.
18/// - `#do {expr}`: execute `expr` at its definition point in the strucutre.
19/// - other tokens gets appended.
20///
21/// # example
22/// ```
23/// let fields = &[
24///     (Ident::new("a", Span::call_site()), Ident::new("u32", Span::call_site())),
25/// 	(Ident::new("b", Span::call_site()), Ident::new("bool", Span::call_site())),
26/// 	(Ident::new("c", Span::call_site()), Ident::new("char", Span::call_site())),
27/// 	];
28/// let mut stream = TokenStream::new();
29/// let public = true;
30/// chunk!(stream,
31/// 	#if public #{ pub }
32/// 	struct Example {
33///         #for (field, ty) in fields #{ #field: #ty, }
34/// 	}
35/// 	impl Example {
36///         #do { gen_accessors(stream, fields) }
37/// 	}
38/// );
39/// fn gen_accessors(mut stream: &mut TokenStream, fields: &[(Ident, Ident)]) {
40///     chunk!(stream, #for (field, ty) in fields #{
41///         fn #{format_ident!("get_{field}")} (&self) -> #ty {
42///             self.#field
43/// 		}
44/// 	});
45/// }
46/// ```
47#[proc_macro]
48pub fn chunk(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
49	chunk_impl(TokenStream::from(input), true, false).into()
50}
51
52/// [`chunk`] but with specified span for all tokens not interpolated.
53///
54/// **syntax:** `chunk_spanned!(stream: ident, span: expr, ...)`
55#[proc_macro]
56pub fn chunk_spanned(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
57	chunk_impl(TokenStream::from(input), true, true).into()
58}
59
60/// create a [`TokenStream`] from the input tokens.
61#[proc_macro]
62pub fn quote(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
63	chunk_impl(TokenStream::from(input), false, false).into()
64}
65
66/// [`quote!`] but with specified span for all tokens not interpolated.
67///
68/// **syntax:** `chunk_spanned!(span: expr, ...)`
69#[proc_macro]
70pub fn quote_spanned(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
71	chunk_impl(TokenStream::from(input), false, true).into()
72}
73
74/// simplify parsing.
75struct Cursor {
76	iter: IntoIter,
77	/// used for peek
78	cur_token: Option<TokenTree>,
79	/// the result
80	res: TokenStream,
81	/// the span after the last token, group end delim
82	end_span: Span,
83}
84
85impl Cursor {
86	/// create a new [`Cursor`]
87	fn new(iter: TokenStream, end_span: Span) -> Self {
88		Self {
89			iter: iter.into_iter(),
90			res: TokenStream::new(),
91			cur_token: None,
92			end_span,
93		}
94	}
95	/// get the next [`TokenTree`]
96	fn next(&mut self) -> Option<TokenTree> {
97		self.cur_token.take().or_else(|| self.iter.next())
98	}
99	/// get the next [`TokenTree`] without consuming
100	fn peek(&mut self) -> Option<&TokenTree> {
101		if self.cur_token.is_none() {
102			self.cur_token = self.iter.next();
103		}
104		self.cur_token.as_ref()
105	}
106	/// test if the next [`TokenTree`] is a keyword
107	fn peek_kw(&mut self, kw: &str) -> bool {
108		match self.peek() {
109			Some(TokenTree::Ident(ident)) => ident == kw,
110			_ => false,
111		}
112	}
113	/// push a chunk into the result
114	fn add<T>(&mut self, tokens: impl IntoIterator<Item = T>)
115	where
116		TokenStream: Extend<T>,
117	{
118		self.res.extend(tokens);
119	}
120	/// raise an error of `expected token`
121	fn expected(&mut self, expected: &str, span: Option<Span>) {
122		let span = span.unwrap_or(self.end_span);
123		let msg = &format!("expected {expected}");
124		self.res.extend(oquote_spanned! { span => ::core::compile_error!(#msg); });
125	}
126	/// comsume an identifier, raising an error if not
127	fn eat_ident(&mut self) -> Option<Ident> {
128		match self.next() {
129			Some(TokenTree::Ident(ident)) => return Some(ident),
130			t => self.expected("identifier", t.map(|t| t.span())),
131		}
132		None
133	}
134	/// comsume a punctuation, raising an error if not
135	fn eat_punct(&mut self, char: char) -> bool {
136		match self.next() {
137			Some(TokenTree::Punct(punct)) if punct.as_char() == char => return true,
138			t => self.expected(&format!("`{char}`"), t.map(|t| t.span())),
139		}
140		false
141	}
142	/// comsume a brace group, raising an error if not
143	fn eat_brace(&mut self) -> Option<Group> {
144		match self.next() {
145			Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => {
146				return Some(g);
147			}
148			t => self.expected("`(`", t.map(|t| t.span())),
149		}
150		None
151	}
152	/// continously eat tokens until `pred` returns `true` or end of input, raising an error if empty.
153	fn eat_until(
154		&mut self, expected: &str, pred: impl Fn(&TokenTree) -> bool,
155	) -> Option<TokenStream> {
156		let mut tokens = TokenStream::new();
157		while let Some(token) = self.peek() {
158			if pred(token) {
159				break;
160			}
161			tokens.append(self.next().unwrap());
162		}
163		if tokens.is_empty() {
164			let span = self.peek().map(|t| t.span());
165			self.expected(expected, span);
166			return None;
167		}
168		Some(tokens)
169	}
170}
171
172/// check if a [`TokenTree`] is a punctuation of symbol `char`
173fn is_punct(token: &TokenTree, char: char) -> bool {
174	matches!(token, TokenTree::Punct(punct) if punct.as_char() == char)
175}
176
177/// main function of `chunk` macro generation
178///
179/// **syntax:** `
180///     ({when(chunked)} -> stream_ident:ident ',')
181///     ({when(spanned)} -> span:expr ',')
182///     tokens+:tt
183/// `
184fn chunk_impl(input: TokenStream, chunked: bool, spanned: bool) -> TokenStream {
185	let mut cur = Cursor::new(input, Span::call_site());
186
187	cur.add(oquote! { #[allow(unused)] use ::chunked_quote::__private::{
188		TokenStream as __TS, TokenStreamExt as _, Delimiter as __Del, ToTokens as __TT,
189	}; });
190
191	let stream_ident = if chunked {
192		let Some(stream_ident) = cur.eat_ident() else { return cur.res };
193		if !cur.eat_punct(',') {
194			return cur.res;
195		};
196		stream_ident
197	} else {
198		cur.add(oquote! { let mut __stream = __TS::new(); });
199		Ident::new("__stream", Span::call_site())
200	};
201
202	if spanned {
203		let Some(span) = cur.eat_until("an expression", |t| is_punct(t, ',')) else {
204			return cur.res;
205		};
206		if !cur.eat_punct(',') {
207			return cur.res;
208		}
209		cur.add(oquote! { let __span = #span; });
210	} else {
211		cur.add(oquote! { let __span = ::chunked_quote::__private::Span::call_site(); })
212	}
213
214	quote_stream(&mut cur, &stream_ident);
215	// `quote` and `quote_spanned` returns the created stream
216	if !chunked {
217		cur.add(Some(stream_ident));
218	}
219	let res = cur.res;
220	oquote! { { #res } }
221}
222
223/// generate [`Punct`] append logic
224fn quote_punct(cur: &mut Cursor, punct: &Punct, stream_ident: &Ident) {
225	let char = punct.as_char();
226	match punct.spacing() {
227		Spacing::Joint => {
228			cur.add(oquote! { #stream_ident.__push_punct_joint(#char, __span); })
229		}
230		Spacing::Alone => {
231			cur.add(oquote! { #stream_ident.__push_punct_alone(#char, __span); })
232		}
233	}
234}
235
236/// generate [`Ident`] append logic
237fn quote_ident(cur: &mut Cursor, ident: &Ident, stream_ident: &Ident) {
238	let name = ident.to_string();
239	match name.strip_prefix("r#") {
240		Some(name) => cur.add(oquote! { #stream_ident.__push_ident_raw(#name, __span); }),
241		None => cur.add(oquote! { #stream_ident.__push_ident(#name, __span); }),
242	}
243}
244
245/// generate [`Literal`] append logic
246fn quote_literal(cur: &mut Cursor, lit: &Literal, stream_ident: &Ident) {
247	let text = lit.to_string();
248	cur.add(oquote! { #stream_ident.__push_literal(#text, __span); });
249}
250
251/// generate [`Group`] append logic
252fn quote_group(cur: &mut Cursor, group: &Group, stream_ident: &Ident) {
253	let delimiter = match group.delimiter() {
254		Delimiter::Parenthesis => oquote! { __Del::Parenthesis },
255		Delimiter::Brace => oquote! { __Del::Brace },
256		Delimiter::Bracket => oquote! { __Del::Bracket },
257		Delimiter::None => oquote! { __Del::None },
258	};
259
260	let mut inner_cur = Cursor::new(group.stream(), group.span_close());
261	quote_stream(&mut inner_cur, stream_ident);
262	let inner = inner_cur.res;
263
264	cur.add(oquote! { #stream_ident.__push_group(#delimiter, __span, {
265		let mut #stream_ident = __TS::new();
266		#inner
267		#stream_ident
268	}); });
269}
270
271/// generate append logic for whole [`TokenStream`]
272fn quote_stream(cur: &mut Cursor, stream_ident: &Ident) {
273	while let Some(token) = cur.next() {
274		match token {
275			TokenTree::Ident(ident) => quote_ident(cur, &ident, stream_ident),
276			TokenTree::Literal(lit) => quote_literal(cur, &lit, stream_ident),
277			TokenTree::Punct(p) if p.as_char() == '#' => {
278				handle_directive(cur, stream_ident);
279			}
280			TokenTree::Punct(punct) => quote_punct(cur, &punct, stream_ident),
281			TokenTree::Group(group) => quote_group(cur, &group, stream_ident),
282		}
283	}
284}
285
286/// transform `kw:ident expr:expr '#' body:curly_group` -> `#kw #expr #{ quote_stream(body) }`
287fn kw_expr_body(cur: &mut Cursor, ident: Ident, stream_ident: &Ident) -> Option<()> {
288	cur.add(Some(ident));
289
290	// we are a utility macro not rustc itself, and this just work.
291	let expr = cur.eat_until("an expression", |t| is_punct(t, '#'))?;
292	cur.add(expr);
293
294	body(cur, true, stream_ident)
295}
296
297/// transform `'#' body:curly_group` -> `quote_stream(body)`
298fn body(cur: &mut Cursor, eat_hash: bool, stream_ident: &Ident) -> Option<()> {
299	if eat_hash {
300		cur.eat_punct('#').then_some(())?;
301	}
302	let body = cur.eat_brace()?;
303
304	let mut inner_cur = Cursor::new(body.stream(), body.span_close());
305	quote_stream(&mut inner_cur, stream_ident);
306
307	cur.add(Some(Group::new(Delimiter::Brace, inner_cur.res)));
308	Some(())
309}
310
311/// check if a [`TokenTree`] is a brace group
312fn is_brace(token: &TokenTree) -> bool {
313	matches!(token, TokenTree::Group(group) if group.delimiter() == Delimiter::Brace)
314}
315
316/// generate append logic for `# 'match' expr '{' pat => '#' body:curly_group '}'`
317fn handle_match(cur: &mut Cursor, ident: Ident, stream_ident: &Ident) -> Option<()> {
318	cur.add(Some(ident));
319	let expr = cur.eat_until("an expression", is_brace)?;
320	cur.add(expr);
321
322	let arms = cur.eat_brace()?;
323	let mut arms_cur = Cursor::new(arms.stream(), arms.span_close());
324
325	while let Some(token) = arms_cur.next() {
326		if is_punct(&token, '#') {
327			body(&mut arms_cur, false, stream_ident)?;
328		} else {
329			arms_cur.add(Some(token));
330		}
331	}
332
333	cur.add(Some(Group::new(Delimiter::Brace, arms_cur.res)));
334	Some(())
335}
336
337/// router of the interpolation logic
338fn handle_directive(cur: &mut Cursor, stream_ident: &Ident) -> Option<()> {
339	match cur.next() {
340		// `'#' '#'` -> `#`
341		Some(TokenTree::Punct(p)) if p.as_char() == '#' => {
342			quote_punct(cur, &p, stream_ident)
343		}
344		// `'#' body:curly_group` -> `to_tokens(body)`
345		Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => {
346			cur.add(oquote! { __TT::to_tokens(&#group, &mut #stream_ident); })
347		}
348		Some(TokenTree::Ident(ident)) => match &*ident.to_string() {
349			// `'#' op:ident expr:expr '#' body:curly_group`
350			"if" | "for" | "while" => kw_expr_body(cur, ident, stream_ident)?,
351			"else" => {
352				if cur.peek_kw("if") {
353					kw_expr_body(cur, ident, stream_ident)?
354				} else {
355					cur.add(Some(ident));
356					body(cur, true, stream_ident)?
357				}
358			}
359			"match" => handle_match(cur, ident, stream_ident)?,
360			// `'#' 'do' block:curly_group` -> apend `block` directly
361			"do" => {
362				let block = cur.eat_brace()?;
363				cur.add(oquote! {{#[allow(unused_braces)] #block }})
364			}
365			// `'#' ident` -> `to_tokens(ident)`
366			_ => cur.add(oquote! { __TT::to_tokens(&#ident, &mut #stream_ident); }),
367		},
368		t => cur.expected("an identifier, `#` or `{`", t.map(|t| t.span())),
369	};
370	Some(())
371}