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#[proc_macro]
48pub fn chunk(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
49 chunk_impl(TokenStream::from(input), true, false).into()
50}
51
52#[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#[proc_macro]
62pub fn quote(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
63 chunk_impl(TokenStream::from(input), false, false).into()
64}
65
66#[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}