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