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 difinition 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
74struct Cursor {
75	iter: IntoIter,
76	cur_token: Option<TokenTree>,
77	res: TokenStream,
78	end_span: Span,
79}
80
81impl Cursor {
82	fn new(iter: TokenStream, end_span: Span) -> Self {
83		Self { iter: iter.into_iter(), res: TokenStream::new(), cur_token: None, end_span }
84	}
85	fn next(&mut self) -> Option<TokenTree> {
86		self.cur_token.take().or_else(|| self.iter.next())
87	}
88	fn peek(&mut self) -> Option<&TokenTree> {
89		if self.cur_token.is_none() {
90			self.cur_token = self.iter.next();
91		}
92		self.cur_token.as_ref()
93	}
94	fn peek_kw(&mut self, kw: &str) -> bool {
95		match self.peek() {
96			Some(TokenTree::Ident(ident)) => ident == kw,
97			_ => false,
98		}
99	}
100	fn add<T>(&mut self, tokens: impl IntoIterator<Item = T>)
101	where
102		TokenStream: Extend<T>,
103	{
104		self.res.extend(tokens);
105	}
106	fn expected(&mut self, expected: &str, span: Option<Span>) {
107		let span = span.unwrap_or(self.end_span);
108		let msg = &format!("expected {expected}");
109		self.res.extend(oquote_spanned! { span => ::core::compile_error!(#msg); });
110	}
111	fn eat_ident(&mut self) -> Option<Ident> {
112		match self.next() {
113			Some(TokenTree::Ident(ident)) => return Some(ident),
114			t => self.expected("identifier", t.map(|t| t.span())),
115		}
116		None
117	}
118	fn eat_punct(&mut self, char: char) -> bool {
119		match self.next() {
120			Some(TokenTree::Punct(punct)) if punct.as_char() == char => return true,
121			t => self.expected(&format!("`{char}`"), t.map(|t| t.span())),
122		}
123		false
124	}
125	fn eat_brace(&mut self) -> Option<Group> {
126		match self.next() {
127			Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => return Some(g),
128			t => self.expected("`(`", t.map(|t| t.span())),
129		}
130		None
131	}
132	fn eat_until(
133		&mut self, expected: &str, pred: impl Fn(&TokenTree) -> bool,
134	) -> Option<TokenStream> {
135		let mut tokens = TokenStream::new();
136		while let Some(token) = self.peek() {
137			if pred(token) {
138				break;
139			}
140			tokens.append(self.next().unwrap());
141		}
142		if tokens.is_empty() {
143			let span = self.peek().map(|t| t.span());
144			self.expected(expected, span);
145			return None;
146		}
147		Some(tokens)
148	}
149}
150
151fn is_punct(token: &TokenTree, char: char) -> bool {
152	matches!(token, TokenTree::Punct(punct) if punct.as_char() == char)
153}
154
155fn chunk_impl(input: TokenStream, chunked: bool, spanned: bool) -> TokenStream {
156	let mut cur = Cursor::new(input, Span::call_site());
157
158	let stream_ident = if chunked {
159		let Some(stream_ident) = cur.eat_ident() else { return cur.res };
160		if !cur.eat_punct(',') {
161			return cur.res;
162		};
163		stream_ident
164	} else {
165		cur.add(oquote! { let mut __stream = TokenStream::new(); });
166		Ident::new("__stream", Span::call_site())
167	};
168
169	if spanned {
170		let Some(span) = cur.eat_until("an expression", |t| is_punct(t, ',')) else {
171			return cur.res;
172		};
173		if !cur.eat_punct(',') {
174			return cur.res;
175		}
176		cur.add(oquote! { let __span = #span; });
177	} else {
178		cur.add(oquote! { let __span = ::chunked_quote::__private::Span::call_site(); })
179	}
180
181	quote_stream(&mut cur, &stream_ident);
182	if !chunked {
183		cur.add(Some(stream_ident));
184	}
185	let res = cur.res;
186	oquote! { { #res } }
187}
188
189fn quote_punct(cur: &mut Cursor, punct: &Punct, stream_ident: &Ident) {
190	let spacing = match punct.spacing() {
191		Spacing::Joint => oquote! { ::chunked_quote::__private::Spacing::Joint },
192		Spacing::Alone => oquote! { ::chunked_quote::__private::Spacing::Alone },
193	};
194	let char = punct.as_char();
195	cur.add(oquote! {{
196		let mut __punct = ::chunked_quote::__private::Punct::new(#char, #spacing);
197		__punct.set_span(__span);
198		#stream_ident.extend(Some(__punct));
199	}});
200}
201
202fn quote_ident(cur: &mut Cursor, ident: &Ident, stream_ident: &Ident) {
203	let name = ident.to_string();
204	match name.strip_prefix("r#") {
205		Some(name) => cur.add(oquote! {
206			#stream_ident.extend(Some(::chunked_quote::__private::Ident::new_raw(#name, __span)));
207		}),
208		None => cur.add(oquote! {
209			#stream_ident.extend(Some(::chunked_quote::__private::Ident::new(#name, __span)));
210		}),
211	}
212}
213
214fn quote_literal(cur: &mut Cursor, lit: &Literal, stream_ident: &Ident) {
215	let text = lit.to_string();
216	cur.add(oquote! {{
217		let mut __lit = <::chunked_quote::__private::Literal as ::core::str::FromStr>
218			::from_str(#text).unwrap();
219		__lit.set_span(__span);
220		#stream_ident.extend(Some(__lit));
221	}});
222}
223
224fn quote_group(cur: &mut Cursor, group: &Group, stream_ident: &Ident) {
225	let delimiter = match group.delimiter() {
226		Delimiter::Parenthesis => oquote! { ::chunked_quote::__private::Delimiter::Parenthesis },
227		Delimiter::Brace => oquote! { ::chunked_quote::__private::Delimiter::Brace },
228		Delimiter::Bracket => oquote! { ::chunked_quote::__private::Delimiter::Bracket },
229		Delimiter::None => oquote! { ::chunked_quote::__private::Delimiter::None },
230	};
231	let mut inner_cur = Cursor::new(group.stream(), group.span_close());
232	quote_stream(&mut inner_cur, stream_ident);
233	let inner = inner_cur.res;
234	cur.add(oquote! { #stream_ident.extend(Some({
235		let mut #stream_ident = ::chunked_quote::__private::TokenStream::new();
236		#inner
237		let mut __group = ::chunked_quote::__private::Group::new(#delimiter, #stream_ident);
238		__group.set_span(__span);
239		__group
240	})); })
241}
242
243fn quote_stream(cur: &mut Cursor, stream_ident: &Ident) {
244	while let Some(token) = cur.next() {
245		match token {
246			TokenTree::Ident(ident) => quote_ident(cur, &ident, stream_ident),
247			TokenTree::Literal(lit) => quote_literal(cur, &lit, stream_ident),
248			TokenTree::Punct(p) if p.as_char() == '#' => {
249				handle_directive(cur, stream_ident);
250			}
251			TokenTree::Punct(punct) => quote_punct(cur, &punct, stream_ident),
252			TokenTree::Group(group) => quote_group(cur, &group, stream_ident),
253		}
254	}
255}
256
257fn kw_expr_body(cur: &mut Cursor, ident: Ident, stream_ident: &Ident) -> Option<()> {
258	cur.add(Some(ident));
259	let expr = cur.eat_until("an expression", |t| is_punct(t, '#'))?;
260	cur.add(expr);
261	body(cur, true, stream_ident)
262}
263fn body(cur: &mut Cursor, eat_hash: bool, stream_ident: &Ident) -> Option<()> {
264	if eat_hash {
265		cur.eat_punct('#').then_some(())?;
266	}
267	let body = cur.eat_brace()?;
268	let mut inner_cur = Cursor::new(body.stream(), body.span_close());
269	quote_stream(&mut inner_cur, stream_ident);
270	cur.add(Some(Group::new(Delimiter::Brace, inner_cur.res)));
271	Some(())
272}
273
274fn is_brace(token: &TokenTree) -> bool {
275	matches!(token, TokenTree::Group(group) if group.delimiter() == Delimiter::Brace)
276}
277
278fn handle_match(cur: &mut Cursor, ident: Ident, stream_ident: &Ident) -> Option<()> {
279	cur.add(Some(ident));
280	let expr = cur.eat_until("an expression", is_brace)?;
281	cur.add(expr);
282	let arms = cur.eat_brace()?;
283	let mut arms_cur = Cursor::new(arms.stream(), arms.span_close());
284	while let Some(token) = arms_cur.next() {
285		if is_punct(&token, '#') {
286			body(&mut arms_cur, false, stream_ident)?;
287		} else {
288			arms_cur.add(Some(token));
289		}
290	}
291	cur.add(Some(Group::new(Delimiter::Brace, arms_cur.res)));
292	Some(())
293}
294
295fn handle_directive(cur: &mut Cursor, stream_ident: &Ident) -> Option<()> {
296	match cur.next() {
297		Some(TokenTree::Punct(p)) if p.as_char() == '#' => quote_punct(cur, &p, stream_ident),
298		Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => {
299			cur.add(oquote! {
300				::chunked_quote::__private::ToTokens::to_tokens(&#group, &mut #stream_ident);
301			})
302		}
303		Some(TokenTree::Ident(ident)) => match &*ident.to_string() {
304			"if" | "for" | "while" => kw_expr_body(cur, ident, stream_ident)?,
305			"else" => {
306				if cur.peek_kw("if") {
307					kw_expr_body(cur, ident, stream_ident)?
308				} else {
309					cur.add(Some(ident));
310					body(cur, true, stream_ident)?
311				}
312			}
313			"do" => {
314				let block = cur.eat_brace()?;
315				cur.add(
316					oquote! {{ let #stream_ident = &mut #stream_ident; #[allow(unused_braces)] #block }},
317				)
318			}
319			"match" => handle_match(cur, ident, stream_ident)?,
320			_ => cur.add(oquote! {
321				::chunked_quote::__private::ToTokens::to_tokens(&#ident, &mut #stream_ident);
322			}),
323		},
324		t => cur.expected("an identifier, `#` or `{`", t.map(|t| t.span())),
325	};
326	Some(())
327}