super_seq_macro/
lib.rs

1//! [![github]](https://github.com/ervanalb/super-seq-macro) [![crates-io]](https://crates.io/crates/super-seq-macro) [![docs-rs]](https://docs.rs/super-seq-macro)
2//!
3//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
4//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
5//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
6//!
7//! <br>
8//!
9//! # For-loops over any iterable in a macro
10//!
11//! This crate provides a `seq!` macro to repeat a fragment of source code and
12//! substitute into each repetition a value of your choosing,
13//! drawn from an iterable [RHAI](https://rhai.rs/) expression.
14//!
15//! This is mostly compatible with the [seq-macro](https://github.com/dtolnay/seq-macro) crate.
16//!
17//! ```
18//! use super_seq_macro::seq;
19//!
20//! fn main() {
21//!     let tuple = (1000, 100, 10);
22//!     let mut sum = 0;
23//!
24//!     // Expands to:
25//!     //
26//!     //     sum += tuple.0;
27//!     //     sum += tuple.1;
28//!     //     sum += tuple.2;
29//!     //
30//!     // This cannot be written using an ordinary for-loop because elements of
31//!     // a tuple can only be accessed by their integer literal index, not by a
32//!     // variable.
33//!     seq!(N in 0..=2 {
34//!         sum += tuple.N;
35//!     });
36//!
37//!     assert_eq!(sum, 1110);
38//! }
39//! ```
40//!
41//! - If the input tokens contain a section surrounded by `#(` ... `)*` then
42//!   only that part is repeated.
43//!
44//! - The numeric counter can be pasted onto the end of some prefix to form
45//!   sequential identifiers.
46//!
47//! ```
48//! use super_seq_macro::seq;
49//!
50//! seq!(N in 64..=127 {
51//!     #[derive(Debug)]
52//!     enum Demo {
53//!         // Expands to Variant64, Variant65, ...
54//!         #(
55//!             Variant~N,
56//!         )*
57//!     }
58//! });
59//!
60//! fn main() {
61//!     assert_eq!("Variant99", format!("{:?}", Demo::Variant99));
62//! }
63//! ```
64//!
65//! - RHAI provides functional tools like `.filter()` and `.map()` on arrays.
66//!
67//! ```
68//! use super_seq_macro::seq;
69//!
70//! seq!(A in 0..3 {#(
71//!     const WITHOUT_~A: [u32; 2] = seq!(B in (0..3).collect().filter(|x| x != A) {
72//!         [ #( B, )* ]
73//!     });
74//! )*});
75//!
76//! assert_eq!(WITHOUT_0, [1, 2]);
77//! assert_eq!(WITHOUT_1, [0, 2]);
78//! assert_eq!(WITHOUT_2, [0, 1]);
79//! ```
80//!
81//! - Since the backtick character is not available, the syntax `$"..."$` is provided for string
82//!   interpolation.
83//!
84//! ```
85//! use super_seq_macro::seq;
86//!
87//! seq!(P in (0x000..0x00F).collect()
88//!     .map(|x| x.to_hex().to_upper()) // Convert to uppercase hex
89//!     .map(|x| "000".sub_string($"${x}"$.len()) + $"${x}"$) // Pad on the left with zeros
90//!     {
91//!     // expands to structs Pin000, ..., Pin009, Pin00A, ..., Pin00F
92//!     struct Pin~P;
93//! });
94//! ```
95//!
96//! - If the input tokens contain a section surrounded by `#(` ... `)#` then
97//!   that part is taken verbatim and not repeated.
98//!   Markers such as`#(`...`)*` within that segment are ignored
99//!   (this can be useful for nesting `seq!` invocations.)
100
101#![doc(html_root_url = "https://docs.rs/seq-macro/0.3.5")]
102#![allow(
103    clippy::cast_lossless,
104    clippy::cast_possible_truncation,
105    clippy::derive_partial_eq_without_eq,
106    clippy::into_iter_without_iter,
107    clippy::let_underscore_untyped,
108    clippy::needless_doctest_main,
109    clippy::single_match_else,
110    clippy::wildcard_imports
111)]
112
113use proc_macro2::{Delimiter, Group, Ident, Spacing, Span, TokenStream, TokenTree};
114use std::iter::{self, FromIterator};
115use std::mem;
116use syn::{
117    parse::{Parse, ParseStream},
118    parse_macro_input, Error, Result, Token,
119};
120
121#[derive(Debug)]
122struct SeqInput {
123    ident: Ident,
124    script: TokenStream,
125    block: TokenStream,
126}
127
128impl Parse for SeqInput {
129    fn parse(input: ParseStream) -> Result<Self> {
130        let ident: Ident = input.parse()?;
131        input.parse::<Token![in]>()?;
132        let (script, block) = input.step(|cursor| {
133            let mut cur = *cursor;
134            let mut script = TokenStream::new();
135            let mut block: Option<TokenTree> = None;
136
137            while let Some((tt, next)) = cur.token_tree() {
138                if let Some(ref mut block) = block {
139                    let old_block = mem::replace(block, tt.clone());
140                    script.extend(std::iter::once(old_block));
141                } else {
142                    block = Some(tt.clone());
143                }
144                cur = next;
145            }
146            Ok(((script, block), cur))
147        })?;
148
149        let Some(block) = block else {
150            return Err(Error::new(Span::call_site(), "Expected block"));
151        };
152        let TokenTree::Group(block) = block else {
153            return Err(Error::new(block.span(), "Expected block"));
154        };
155
156        Ok(SeqInput {
157            ident,
158            script,
159            block: block.stream(),
160        })
161    }
162}
163
164#[proc_macro]
165pub fn seq(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
166    let input = parse_macro_input!(input as SeqInput);
167
168    let output = seq_impl(input).unwrap_or_else(Error::into_compile_error);
169    proc_macro::TokenStream::from(output)
170}
171
172fn seq_impl(
173    SeqInput {
174        ident,
175        script,
176        block,
177    }: SeqInput,
178) -> Result<TokenStream> {
179    // Run script
180    let script = rewrite_script(script);
181    let script_span = Span::call_site(); //script.span(); TODO
182
183    let mut engine = rhai::Engine::new();
184
185    fn rhai_collect<T: IntoIterator<Item: Into<rhai::Dynamic>>>(inp: T) -> rhai::Array {
186        inp.into_iter().map(|x| x.into()).collect()
187    }
188    engine.register_fn("collect", rhai_collect::<std::ops::Range<i64>>);
189    engine.register_fn("collect", rhai_collect::<std::ops::RangeInclusive<i64>>);
190
191    let output: rhai::Dynamic = engine
192        .eval(&script)
193        .map_err(|e| Error::new(script_span, e.to_string()))?;
194
195    // See if output is a range of int
196    let list: Vec<_> = if let Some(r) = output.clone().try_cast::<std::ops::Range<i64>>() {
197        r.map(|x| x.to_string()).collect()
198    } else if let Some(r) = output.clone().try_cast::<std::ops::RangeInclusive<i64>>() {
199        r.map(|x| x.to_string()).collect()
200    } else if let Some(a) = output.clone().try_cast::<Vec<rhai::Dynamic>>() {
201        a.into_iter().map(|d| d.to_string()).collect()
202    } else {
203        return Err(Error::new(script_span, "Bad expression type"));
204    };
205
206    //let list = list
207    //    .into_iter()
208    //    .map(|x| x.into_string())
209    //    .collect::<std::result::Result<Vec<_>, _>>()
210    //    .map_err(|e| Error::new(script_span, e))?;
211
212    let mut found_repetition = false;
213    let expanded = expand_repetitions(&ident, &list, block.clone(), &mut found_repetition);
214    if found_repetition {
215        Ok(expanded)
216    } else {
217        // If no `#(...)*`, repeat the entire body.
218        Ok(repeat(&ident, &list, &block))
219    }
220}
221
222fn rewrite_script(script: TokenStream) -> String {
223    fn dollar_str(tokens: &[TokenTree]) -> Option<String> {
224        assert!(tokens.len() == 3);
225        match &tokens[0] {
226            TokenTree::Punct(punct) if punct.as_char() == '$' => {}
227            _ => return None,
228        }
229        match &tokens[2] {
230            TokenTree::Punct(punct) if punct.as_char() == '$' => {}
231            _ => return None,
232        }
233        match &tokens[1] {
234            TokenTree::Literal(lit) => {
235                let content = lit.to_string();
236                let mut chars = content.chars();
237                match chars.next() {
238                    Some('"') => {}
239                    _ => return None,
240                }
241                match chars.next_back() {
242                    Some('"') => {}
243                    _ => return None,
244                }
245                return Some(format!("`{}`", chars.as_str()));
246            }
247            _ => return None,
248        }
249    }
250
251    // Look for `$"..."$`.
252    let tokens = Vec::from_iter(script);
253    let mut output = String::new();
254    let mut i = 0;
255    while i < tokens.len() {
256        if let TokenTree::Group(group) = &tokens[i] {
257            match group.delimiter() {
258                Delimiter::Parenthesis => {
259                    output.push('(');
260                }
261                Delimiter::Brace => {
262                    output.push('{');
263                }
264                Delimiter::Bracket => {
265                    output.push('[');
266                }
267                Delimiter::None => {}
268            }
269            output.push(' ');
270            output.push_str(&rewrite_script(group.stream()));
271            match group.delimiter() {
272                Delimiter::Parenthesis => {
273                    output.push(')');
274                }
275                Delimiter::Brace => {
276                    output.push('}');
277                }
278                Delimiter::Bracket => {
279                    output.push(']');
280                }
281                Delimiter::None => {}
282            }
283            output.push(' ');
284            i += 1;
285            continue;
286        }
287        if i + 3 <= tokens.len() {
288            if let Some(backtick_str) = dollar_str(&tokens[i..i + 3]) {
289                output.push_str(&backtick_str);
290                output.push(' ');
291                i += 3;
292                continue;
293            }
294        }
295
296        match &tokens[i] {
297            TokenTree::Group(_) => {
298                unreachable!();
299            }
300            TokenTree::Ident(i) => {
301                output.push_str(&i.to_string());
302                output.push(' ');
303            }
304            TokenTree::Punct(p) => {
305                output.push_str(&p.to_string());
306                match p.spacing() {
307                    Spacing::Alone => {
308                        output.push(' ');
309                    }
310                    Spacing::Joint => {}
311                }
312            }
313            TokenTree::Literal(l) => {
314                output.push_str(&l.to_string());
315                output.push(' ');
316            }
317        }
318        i += 1;
319        //let template = match enter_repetition(&tokens[i..i + 3]) {
320        //    Some(template) => template,
321        //    None => {
322        //        i += 1;
323        //        continue;
324        //    }
325        //};
326        //*found_repetition = true;
327        //let mut repeated = Vec::new();
328        //for value in range {
329        //    repeated.extend(substitute_value(var, &value, template.clone()));
330        //}
331        //let repeated_len = repeated.len();
332        //tokens.splice(i..i + 3, repeated);
333        //i += repeated_len;
334    }
335
336    //let script = TokenStream::from_iter(tokens);
337
338    output
339}
340
341fn repeat(var: &Ident, list: &[String], body: &TokenStream) -> TokenStream {
342    let mut repeated = TokenStream::new();
343    for value in list {
344        repeated.extend(substitute_value(var, value, body.clone()));
345    }
346    repeated
347}
348
349fn substitute_value(var: &Ident, value: &str, body: TokenStream) -> TokenStream {
350    let mut tokens = Vec::from_iter(body);
351
352    let mut i = 0;
353    while i < tokens.len() {
354        // Substitute our variable by itself, e.g. `N`.
355        let replace = match &tokens[i] {
356            TokenTree::Ident(ident) => ident.to_string() == var.to_string(),
357            _ => false,
358        };
359        if replace {
360            let original_span = tokens[i].span();
361
362            let new_tokens = value.parse::<TokenStream>().unwrap();
363
364            tokens.splice(
365                i..i + 1,
366                new_tokens.into_iter().map(|mut t| {
367                    t.set_span(original_span);
368                    t
369                }),
370            );
371
372            //let mut iter = new_tokens.into_iter();
373            //let mut t = match iter.next() {
374            //    Some(t) => t,
375            //    None => panic!("Empty token"),
376            //};
377            //assert!(iter.next().is_none(), "Multiple tokens");
378
379            //t.set_span(original_span);
380            //tokens[i] = t;
381            //i += 1;
382            continue;
383        }
384
385        // Substitute our variable concatenated onto some prefix, `Prefix~N`.
386        if i + 3 <= tokens.len() {
387            let prefix = match &tokens[i..i + 3] {
388                [first, TokenTree::Punct(tilde), TokenTree::Ident(ident)]
389                    if tilde.as_char() == '~' && ident.to_string() == var.to_string() =>
390                {
391                    match first {
392                        TokenTree::Ident(ident) => Some(ident.clone()),
393                        TokenTree::Group(group) => {
394                            let mut iter = group.stream().into_iter().fuse();
395                            match (iter.next(), iter.next()) {
396                                (Some(TokenTree::Ident(ident)), None) => Some(ident),
397                                _ => None,
398                            }
399                        }
400                        _ => None,
401                    }
402                }
403                _ => None,
404            };
405            if let Some(prefix) = prefix {
406                let concat = format!("{}{}", prefix, value);
407                let ident = Ident::new(&concat, prefix.span());
408                tokens.splice(i..i + 3, iter::once(TokenTree::Ident(ident)));
409                i += 1;
410                continue;
411            }
412        }
413
414        // Recursively substitute content nested in a group.
415        if let TokenTree::Group(group) = &mut tokens[i] {
416            let original_span = group.span();
417            let content = substitute_value(var, value, group.stream());
418            *group = Group::new(group.delimiter(), content);
419            group.set_span(original_span);
420        }
421
422        i += 1;
423    }
424
425    TokenStream::from_iter(tokens)
426}
427
428fn enter_repetition(tokens: &[TokenTree]) -> Option<TokenStream> {
429    assert!(tokens.len() == 3);
430    match &tokens[0] {
431        TokenTree::Punct(punct) if punct.as_char() == '#' => {}
432        _ => return None,
433    }
434    match &tokens[2] {
435        TokenTree::Punct(punct) if punct.as_char() == '*' => {}
436        _ => return None,
437    }
438    match &tokens[1] {
439        TokenTree::Group(group) if group.delimiter() == Delimiter::Parenthesis => {
440            Some(group.stream())
441        }
442        _ => None,
443    }
444}
445
446fn enter_single(tokens: &[TokenTree]) -> Option<TokenStream> {
447    assert!(tokens.len() == 3);
448    match &tokens[0] {
449        TokenTree::Punct(punct) if punct.as_char() == '#' => {}
450        _ => return None,
451    }
452    match &tokens[2] {
453        TokenTree::Punct(punct) if punct.as_char() == '#' => {}
454        _ => return None,
455    }
456    match &tokens[1] {
457        TokenTree::Group(group) if group.delimiter() == Delimiter::Parenthesis => {
458            Some(group.stream())
459        }
460        _ => None,
461    }
462}
463
464fn expand_repetitions(
465    var: &Ident,
466    range: &[String],
467    body: TokenStream,
468    found_repetition: &mut bool,
469) -> TokenStream {
470    let mut tokens = Vec::from_iter(body);
471
472    // Look for `#(...)*` or `#(...)#`.
473    let mut i = 0;
474    while i < tokens.len() {
475        if let TokenTree::Group(group) = &mut tokens[i] {
476            let content = expand_repetitions(var, range, group.stream(), found_repetition);
477            let original_span = group.span();
478            *group = Group::new(group.delimiter(), content);
479            group.set_span(original_span);
480            i += 1;
481            continue;
482        }
483        if i + 3 > tokens.len() {
484            i += 1;
485            continue;
486        }
487        if let Some(template) = enter_repetition(&tokens[i..i + 3]) {
488            *found_repetition = true;
489            let mut repeated = Vec::new();
490            for value in range {
491                repeated.extend(substitute_value(var, &value, template.clone()));
492            }
493            let repeated_len = repeated.len();
494            tokens.splice(i..i + 3, repeated);
495            i += repeated_len;
496            continue;
497        }
498        if let Some(template) = enter_single(&tokens[i..i + 3]) {
499            tokens.splice(i..i + 3, template);
500            *found_repetition = true;
501            i += 1;
502            continue;
503        }
504        // Normal token
505        i += 1;
506        continue;
507    }
508
509    TokenStream::from_iter(tokens)
510}
511
512#[cfg(test)]
513mod test {
514    use crate::{rewrite_script, seq_impl};
515    use quote::quote;
516
517    #[test]
518    fn test_string_rewrite() {
519        let inp = quote! {
520            let a = (
521                $"${x}"$
522            );
523        };
524
525        let result = rewrite_script(inp);
526        assert_eq!(result, "let a = ( `${x}` ) ; ");
527    }
528
529    #[test]
530    fn test_range() {
531        let inp = quote! {
532            A in 0..3 {
533                println("{}", A);
534            }
535        };
536
537        let result = seq_impl(syn::parse2(inp).unwrap()).unwrap();
538        assert_eq!(
539            result.to_string(),
540            "println (\"{}\" , 0) ; println (\"{}\" , 1) ; println (\"{}\" , 2) ;"
541        );
542    }
543
544    #[test]
545    fn test_int_array() {
546        let inp = quote! {
547            A in [0,1,2] {
548                println("{}", A);
549            }
550        };
551
552        let result = seq_impl(syn::parse2(inp).unwrap()).unwrap();
553        assert_eq!(
554            result.to_string(),
555            "println (\"{}\" , 0) ; println (\"{}\" , 1) ; println (\"{}\" , 2) ;"
556        );
557    }
558
559    #[test]
560    fn test_str_array() {
561        let inp = quote! {
562            A in (0..3).collect().map(|x| $"${x}"$) {
563                println("{}", A);
564            }
565        };
566
567        let result = seq_impl(syn::parse2(inp).unwrap()).unwrap();
568        assert_eq!(
569            result.to_string(),
570            "println (\"{}\" , 0) ; println (\"{}\" , 1) ; println (\"{}\" , 2) ;"
571        );
572    }
573}