Expand `$(..)` repetition in macro_rules
Recounting the 1,833 definitions in the 400-crate sample by the expander's
actual boundary -- one rule, any repetition, no :tt -- gives 661 without
repetition and 271 with, so 932 of 1,833 are now expandable. The rest are
multi-rule (481) or token-tree munchers (420).
Two things here are easy to get wrong and are both pinned by
tests/cases/038. First, `>` closes `=>` and `->` as well as a generic
argument list, so the nesting depth used to find where a fragment ends must
not go negative; without that `$($a:expr => $b:expr),*` swallows the whole
invocation. Second, a repetition's separator may be inside the pattern rather
than between iterations -- `$first:expr $(, $rest:expr)*` has no separator,
its comma leads each iteration -- so a trailing fragment stops at the
separator when there is one and at whatever starts the next iteration when
there is not.
Repetition macros are usually written around a block expression with
statements, `{{ let mut m = ..; m }}`, so those now lower to Nim's `block:`
expression. Hoisting the statements into the enclosing scope was the obvious
thing and is wrong: the same macro expanded at two call sites then declares
the same binding twice in one scope. That is a general improvement, not a
macro-specific one -- Rust code uses block expressions directly too.
The survey did not move: 18 of 400 before, 18 after, 12 compilable either
way. Seventh feature running with that result, for the same reason each time
-- the crates it unblocks hit their next blocker. The capability is real and
tested; the crate count is gated by something else.
44 differential cases, all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>3d4a7d2 parent: 04eb29f modified
DESIGN.md +42 -9 | @@ -630,15 +630,48 @@ which is exactly the guessing the project refuses. Expanding at the call site | ||
| 630 | 630 | yields ordinary Rust in a context where the types are known, so it lowers like |
| 631 | 631 | anything else. Same applicability, faithful output. |
| 632 | 632 | |
| 633 | -`src/mrules.rs` implements the 36% case: one rule, no repetition, no `:tt`. A | |
| 634 | -definition it cannot handle is recorded *with its reason*, so a call site says | |
| 635 | -"`foo!` cannot be expanded: `$(..)` repetition is not implemented yet" rather | |
| 636 | -than "unknown macro". Captured fragments are parenthesised on substitution, so | |
| 637 | -`square!(2 + 3)` is 25 and not 11. | |
| 638 | - | |
| 639 | -**This moved the survey more than everything before it combined: 3 → 18 of | |
| 640 | -400 crates accepted.** A `macro_rules!` used to be a hard stop at item level, | |
| 641 | -failing a whole crate on sight. | |
| 633 | +`src/mrules.rs` implements single-rule macros, **with `$(..)` repetition**. | |
| 634 | +Recounting the 1,833 definitions by the expander's actual boundary — one rule, | |
| 635 | +any repetition, no `:tt`: | |
| 636 | + | |
| 637 | +| | count | | |
| 638 | +|---|---| | |
| 639 | +| single rule, no repetition | 661 | | |
| 640 | +| single rule, with repetition | 271 | | |
| 641 | +| **expandable** | **932 (51%)** | | |
| 642 | +| multiple rules | 481 | | |
| 643 | +| `:tt` | 420 | | |
| 644 | + | |
| 645 | +A definition it cannot handle is recorded *with its reason*, so a call site | |
| 646 | +says "`foo!` cannot be expanded: nested `$(..)` repetition is not implemented | |
| 647 | +yet" rather than "unknown macro". Captured fragments are parenthesised on | |
| 648 | +substitution, so `square!(2 + 3)` is 25 and not 11. | |
| 649 | + | |
| 650 | +Repetition needed two things that are easy to get wrong, both caught by | |
| 651 | +`tests/cases/038`: | |
| 652 | + | |
| 653 | +- `>` closes `=>` and `->` as well as a generic argument list, so the nesting | |
| 654 | + depth used to find a fragment's end must not go negative. Without that, | |
| 655 | + `$($a:expr => $b:expr),*` swallows the whole invocation. | |
| 656 | +- A repetition's separator may be *inside* the pattern rather than between | |
| 657 | + iterations — `$first:expr $(, $rest:expr)*` has no separator, its comma | |
| 658 | + leads each iteration. A trailing fragment therefore stops at the separator | |
| 659 | + when there is one and at whatever starts the next iteration when there is | |
| 660 | + not. | |
| 661 | + | |
| 662 | +A block expression with statements (`{{ let mut m = ..; m }}`, which is how | |
| 663 | +these macros are usually written) now lowers to Nim's `block:` expression | |
| 664 | +rather than being hoisted, so the same macro expanded at two call sites does | |
| 665 | +not collide in one scope. | |
| 666 | + | |
| 667 | +**Expansion moved the survey more than everything before it combined: 3 → 18 | |
| 668 | +of 400 crates accepted.** A `macro_rules!` used to be a hard stop at item | |
| 669 | +level, failing a whole crate on sight. | |
| 670 | + | |
| 671 | +Repetition then moved it by **zero** — 18 before, 18 after — for the reason | |
| 672 | +every previous feature did: the crates it unblocked hit their next blocker. | |
| 673 | +That is now seven features running. The capability is real and tested; the | |
| 674 | +crate count is gated by something else. | |
| 642 | 675 | |
| 643 | 676 | ### A stricter number |
| 644 | 677 | |
| @@ -630,15 +630,48 @@ which is exactly the guessing the project refuses. Expanding at the call site | |||
| 630 | yields ordinary Rust in a context where the types are known, so it lowers like | 630 | yields ordinary Rust in a context where the types are known, so it lowers like |
| 631 | anything else. Same applicability, faithful output. | 631 | anything else. Same applicability, faithful output. |
| 632 | 632 | ||
| 633 | -`src/mrules.rs` implements the 36% case: one rule, no repetition, no `:tt`. A | 633 | +`src/mrules.rs` implements single-rule macros, **with `$(..)` repetition**. |
| 634 | -definition it cannot handle is recorded *with its reason*, so a call site says | 634 | +Recounting the 1,833 definitions by the expander's actual boundary — one rule, |
| 635 | -"`foo!` cannot be expanded: `$(..)` repetition is not implemented yet" rather | 635 | +any repetition, no `:tt`: |
| 636 | -than "unknown macro". Captured fragments are parenthesised on substitution, so | 636 | + |
| 637 | -`square!(2 + 3)` is 25 and not 11. | 637 | +| | count | |
| 638 | - | 638 | +|---|---| |
| 639 | -**This moved the survey more than everything before it combined: 3 → 18 of | 639 | +| single rule, no repetition | 661 | |
| 640 | -400 crates accepted.** A `macro_rules!` used to be a hard stop at item level, | 640 | +| single rule, with repetition | 271 | |
| 641 | -failing a whole crate on sight. | 641 | +| **expandable** | **932 (51%)** | |
| 642 | +| multiple rules | 481 | | ||
| 643 | +| `:tt` | 420 | | ||
| 644 | + | ||
| 645 | +A definition it cannot handle is recorded *with its reason*, so a call site | ||
| 646 | +says "`foo!` cannot be expanded: nested `$(..)` repetition is not implemented | ||
| 647 | +yet" rather than "unknown macro". Captured fragments are parenthesised on | ||
| 648 | +substitution, so `square!(2 + 3)` is 25 and not 11. | ||
| 649 | + | ||
| 650 | +Repetition needed two things that are easy to get wrong, both caught by | ||
| 651 | +`tests/cases/038`: | ||
| 652 | + | ||
| 653 | +- `>` closes `=>` and `->` as well as a generic argument list, so the nesting | ||
| 654 | + depth used to find a fragment's end must not go negative. Without that, | ||
| 655 | + `$($a:expr => $b:expr),*` swallows the whole invocation. | ||
| 656 | +- A repetition's separator may be *inside* the pattern rather than between | ||
| 657 | + iterations — `$first:expr $(, $rest:expr)*` has no separator, its comma | ||
| 658 | + leads each iteration. A trailing fragment therefore stops at the separator | ||
| 659 | + when there is one and at whatever starts the next iteration when there is | ||
| 660 | + not. | ||
| 661 | + | ||
| 662 | +A block expression with statements (`{{ let mut m = ..; m }}`, which is how | ||
| 663 | +these macros are usually written) now lowers to Nim's `block:` expression | ||
| 664 | +rather than being hoisted, so the same macro expanded at two call sites does | ||
| 665 | +not collide in one scope. | ||
| 666 | + | ||
| 667 | +**Expansion moved the survey more than everything before it combined: 3 → 18 | ||
| 668 | +of 400 crates accepted.** A `macro_rules!` used to be a hard stop at item | ||
| 669 | +level, failing a whole crate on sight. | ||
| 670 | + | ||
| 671 | +Repetition then moved it by **zero** — 18 before, 18 after — for the reason | ||
| 672 | +every previous feature did: the crates it unblocked hit their next blocker. | ||
| 673 | +That is now seven features running. The capability is real and tested; the | ||
| 674 | +crate count is gated by something else. | ||
| 642 | 675 | ||
| 643 | ### A stricter number | 676 | ### A stricter number |
| 644 | 677 | ||
modified
README.md +6 -2 | @@ -111,8 +111,12 @@ A `macro_rules!` is **expanded at the call site**, not translated into a Nim | ||
| 111 | 111 | template — but a template body is untyped, and this lowering is type-directed |
| 112 | 112 | throughout. Expanding gives ordinary Rust where the types are known. |
| 113 | 113 | |
| 114 | -That change alone took the survey from 3 to 18 of 400 crates accepted, of | |
| 115 | -which 12 produce Nim the Nim compiler accepts. (Accepted, compiles, and | |
| 114 | +Single-rule macros are supported, with `$(..)` repetition — 932 of the 1,833 | |
| 115 | +definitions in that sample, 51%. The rest are multi-rule (481) or token-tree | |
| 116 | +munchers (420). | |
| 117 | + | |
| 118 | +Expansion took the survey from 3 to 18 of 400 crates accepted, of which 12 | |
| 119 | +produce Nim the Nim compiler accepts. (Accepted, compiles, and | |
| 116 | 120 | behaviourally verified are three different bars; only `tests/cases/` clears |
| 117 | 121 | the third.) |
| 118 | 122 | |
| @@ -111,8 +111,12 @@ A `macro_rules!` is **expanded at the call site**, not translated into a Nim | |||
| 111 | template — but a template body is untyped, and this lowering is type-directed | 111 | template — but a template body is untyped, and this lowering is type-directed |
| 112 | throughout. Expanding gives ordinary Rust where the types are known. | 112 | throughout. Expanding gives ordinary Rust where the types are known. |
| 113 | 113 | ||
| 114 | -That change alone took the survey from 3 to 18 of 400 crates accepted, of | 114 | +Single-rule macros are supported, with `$(..)` repetition — 932 of the 1,833 |
| 115 | -which 12 produce Nim the Nim compiler accepts. (Accepted, compiles, and | 115 | +definitions in that sample, 51%. The rest are multi-rule (481) or token-tree |
| 116 | +munchers (420). | ||
| 117 | + | ||
| 118 | +Expansion took the survey from 3 to 18 of 400 crates accepted, of which 12 | ||
| 119 | +produce Nim the Nim compiler accepts. (Accepted, compiles, and | ||
| 116 | behaviourally verified are three different bars; only `tests/cases/` clears | 120 | behaviourally verified are three different bars; only `tests/cases/` clears |
| 117 | the third.) | 121 | the third.) |
| 118 | 122 | ||
modified
src/lower.rs +49 -1 | @@ -3610,6 +3610,51 @@ impl Lowerer { | ||
| 3610 | 3610 | // `if` and `match` are expressions in both languages, but only |
| 3611 | 3611 | // when every arm is itself a single expression. |
| 3612 | 3612 | Expr::If(i) => self.if_expr(i, expect), |
| 3613 | + // A block with statements used as a value: the statements are | |
| 3614 | + // emitted ahead of the line being built and the block's trailing | |
| 3615 | + // expression becomes the value. Every caller lowers its | |
| 3616 | + // sub-expressions before emitting its own line, which is what | |
| 3617 | + // makes that ordering hold. | |
| 3618 | + Expr::Block(b) | |
| 3619 | + if b.label.is_none() | |
| 3620 | + && b.block.stmts.len() > 1 | |
| 3621 | + && matches!(b.block.stmts.last(), Some(Stmt::Expr(_, None))) => | |
| 3622 | + { | |
| 3623 | + if self.in_loop_cond { | |
| 3624 | + return Err("a block expression in a loop condition is not \ | |
| 3625 | + implemented yet: its statements would run once, \ | |
| 3626 | + before the loop" | |
| 3627 | + .into()); | |
| 3628 | + } | |
| 3629 | + // Nim's `block:` is an expression too, so the statements get | |
| 3630 | + // their own scope rather than being hoisted into the enclosing | |
| 3631 | + // one -- which would collide if the same block is written | |
| 3632 | + // twice, as a macro expanded at two call sites is. | |
| 3633 | + let tmp = self.fresh("Blk"); | |
| 3634 | + self.line(&format!("let {} = block:", tmp)); | |
| 3635 | + self.indent += 1; | |
| 3636 | + self.push_scope(); | |
| 3637 | + let saved = self.target.take(); | |
| 3638 | + let before = self.out.len(); | |
| 3639 | + let tail = self.block_body_at(&b.block, expect)?; | |
| 3640 | + let ty = tail.as_ref().and_then(|v| v.ty.clone()); | |
| 3641 | + match tail { | |
| 3642 | + Some(v) => { | |
| 3643 | + let code = v.code.clone(); | |
| 3644 | + self.line(&code); | |
| 3645 | + } | |
| 3646 | + None => { | |
| 3647 | + return Err( | |
| 3648 | + "a block used as a value needs a trailing expression".into() | |
| 3649 | + ) | |
| 3650 | + } | |
| 3651 | + } | |
| 3652 | + let _ = before; | |
| 3653 | + self.target = saved; | |
| 3654 | + self.pop_scope(); | |
| 3655 | + self.indent -= 1; | |
| 3656 | + Ok(Val::new(tmp, ty)) | |
| 3657 | + } | |
| 3613 | 3658 | Expr::Block(b) if b.block.stmts.len() == 1 => { |
| 3614 | 3659 | if let Some(Stmt::Expr(e, None)) = b.block.stmts.first() { |
| 3615 | 3660 | self.expr_at(e, expect) |
| @@ -5183,7 +5228,10 @@ fn expressible(e: &Expr) -> bool { | ||
| 5183 | 5228 | // `unsafe { .. }` is transparent, so it is an expression exactly when |
| 5184 | 5229 | // its block is one. |
| 5185 | 5230 | Expr::Unsafe(u) => single_expr(&u.block).is_some_and(expressible), |
| 5186 | - Expr::Match(_) | Expr::Block(_) | Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => false, | |
| 5231 | + Expr::Block(b) => { | |
| 5232 | + b.label.is_none() && matches!(b.block.stmts.last(), Some(Stmt::Expr(_, None))) | |
| 5233 | + } | |
| 5234 | + Expr::Match(_) | Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => false, | |
| 5187 | 5235 | _ => true, |
| 5188 | 5236 | } |
| 5189 | 5237 | } |
| @@ -3610,6 +3610,51 @@ impl Lowerer { | |||
| 3610 | // `if` and `match` are expressions in both languages, but only | 3610 | // `if` and `match` are expressions in both languages, but only |
| 3611 | // when every arm is itself a single expression. | 3611 | // when every arm is itself a single expression. |
| 3612 | Expr::If(i) => self.if_expr(i, expect), | 3612 | Expr::If(i) => self.if_expr(i, expect), |
| 3613 | + // A block with statements used as a value: the statements are | ||
| 3614 | + // emitted ahead of the line being built and the block's trailing | ||
| 3615 | + // expression becomes the value. Every caller lowers its | ||
| 3616 | + // sub-expressions before emitting its own line, which is what | ||
| 3617 | + // makes that ordering hold. | ||
| 3618 | + Expr::Block(b) | ||
| 3619 | + if b.label.is_none() | ||
| 3620 | + && b.block.stmts.len() > 1 | ||
| 3621 | + && matches!(b.block.stmts.last(), Some(Stmt::Expr(_, None))) => | ||
| 3622 | + { | ||
| 3623 | + if self.in_loop_cond { | ||
| 3624 | + return Err("a block expression in a loop condition is not \ | ||
| 3625 | + implemented yet: its statements would run once, \ | ||
| 3626 | + before the loop" | ||
| 3627 | + .into()); | ||
| 3628 | + } | ||
| 3629 | + // Nim's `block:` is an expression too, so the statements get | ||
| 3630 | + // their own scope rather than being hoisted into the enclosing | ||
| 3631 | + // one -- which would collide if the same block is written | ||
| 3632 | + // twice, as a macro expanded at two call sites is. | ||
| 3633 | + let tmp = self.fresh("Blk"); | ||
| 3634 | + self.line(&format!("let {} = block:", tmp)); | ||
| 3635 | + self.indent += 1; | ||
| 3636 | + self.push_scope(); | ||
| 3637 | + let saved = self.target.take(); | ||
| 3638 | + let before = self.out.len(); | ||
| 3639 | + let tail = self.block_body_at(&b.block, expect)?; | ||
| 3640 | + let ty = tail.as_ref().and_then(|v| v.ty.clone()); | ||
| 3641 | + match tail { | ||
| 3642 | + Some(v) => { | ||
| 3643 | + let code = v.code.clone(); | ||
| 3644 | + self.line(&code); | ||
| 3645 | + } | ||
| 3646 | + None => { | ||
| 3647 | + return Err( | ||
| 3648 | + "a block used as a value needs a trailing expression".into() | ||
| 3649 | + ) | ||
| 3650 | + } | ||
| 3651 | + } | ||
| 3652 | + let _ = before; | ||
| 3653 | + self.target = saved; | ||
| 3654 | + self.pop_scope(); | ||
| 3655 | + self.indent -= 1; | ||
| 3656 | + Ok(Val::new(tmp, ty)) | ||
| 3657 | + } | ||
| 3613 | Expr::Block(b) if b.block.stmts.len() == 1 => { | 3658 | Expr::Block(b) if b.block.stmts.len() == 1 => { |
| 3614 | if let Some(Stmt::Expr(e, None)) = b.block.stmts.first() { | 3659 | if let Some(Stmt::Expr(e, None)) = b.block.stmts.first() { |
| 3615 | self.expr_at(e, expect) | 3660 | self.expr_at(e, expect) |
| @@ -5183,7 +5228,10 @@ fn expressible(e: &Expr) -> bool { | |||
| 5183 | // `unsafe { .. }` is transparent, so it is an expression exactly when | 5228 | // `unsafe { .. }` is transparent, so it is an expression exactly when |
| 5184 | // its block is one. | 5229 | // its block is one. |
| 5185 | Expr::Unsafe(u) => single_expr(&u.block).is_some_and(expressible), | 5230 | Expr::Unsafe(u) => single_expr(&u.block).is_some_and(expressible), |
| 5186 | - Expr::Match(_) | Expr::Block(_) | Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => false, | 5231 | + Expr::Block(b) => { |
| 5232 | + b.label.is_none() && matches!(b.block.stmts.last(), Some(Stmt::Expr(_, None))) | ||
| 5233 | + } | ||
| 5234 | + Expr::Match(_) | Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => false, | ||
| 5187 | _ => true, | 5235 | _ => true, |
| 5188 | } | 5236 | } |
| 5189 | } | 5237 | } |
modified
src/mrules.rs +218 -19 | @@ -28,8 +28,19 @@ enum Pat { | ||
| 28 | 28 | Tok(String), |
| 29 | 29 | /// A delimited group, matched recursively. |
| 30 | 30 | Group(Delimiter, Vec<Pat>), |
| 31 | + /// `$( .. ) sep? op` — a repetition. `op` is `*`, `+` or `?`. | |
| 32 | + Rep { inner: Vec<Pat>, sep: Option<String>, op: char }, | |
| 31 | 33 | } |
| 32 | 34 | |
| 35 | +/// What a fragment captured: once, or once per repetition. | |
| 36 | +#[derive(Debug, Clone)] | |
| 37 | +enum Cap { | |
| 38 | + One(TokenStream), | |
| 39 | + Seq(Vec<TokenStream>), | |
| 40 | +} | |
| 41 | + | |
| 42 | +type Binds = HashMap<String, Cap>; | |
| 43 | + | |
| 33 | 44 | #[derive(Debug, Clone)] |
| 34 | 45 | pub struct MacroDef { |
| 35 | 46 | pattern: Vec<Pat>, |
| @@ -84,12 +95,33 @@ fn parse_pattern(ts: TokenStream) -> Result<Vec<Pat>, String> { | ||
| 84 | 95 | } |
| 85 | 96 | out.push(Pat::Frag(name.to_string())); |
| 86 | 97 | } |
| 87 | - Some(TokenTree::Group(_)) => { | |
| 88 | - return Err( | |
| 89 | - "`$(..)` repetition is not implemented yet; it maps to \ | |
| 90 | - `varargs` in a Nim macro" | |
| 91 | - .into(), | |
| 92 | - ) | |
| 98 | + Some(TokenTree::Group(g)) => { | |
| 99 | + let inner = parse_pattern(g.stream())?; | |
| 100 | + if inner.iter().any(|p| matches!(p, Pat::Rep { .. })) { | |
| 101 | + return Err("nested `$(..)` repetition is not implemented yet".into()); | |
| 102 | + } | |
| 103 | + // `$( .. ) sep? op`: an optional separator token, then | |
| 104 | + // the operator. | |
| 105 | + let mut sep = None; | |
| 106 | + let op = loop { | |
| 107 | + match it.next() { | |
| 108 | + Some(TokenTree::Punct(p)) | |
| 109 | + if matches!(p.as_char(), '*' | '+' | '?') => | |
| 110 | + { | |
| 111 | + break p.as_char() | |
| 112 | + } | |
| 113 | + Some(t) => { | |
| 114 | + if sep.is_some() { | |
| 115 | + return Err( | |
| 116 | + "a repetition separator must be one token".into() | |
| 117 | + ); | |
| 118 | + } | |
| 119 | + sep = Some(t.to_string()); | |
| 120 | + } | |
| 121 | + None => return Err("a repetition needs `*`, `+` or `?`".into()), | |
| 122 | + } | |
| 123 | + }; | |
| 124 | + out.push(Pat::Rep { inner, sep, op }); | |
| 93 | 125 | } |
| 94 | 126 | _ => return Err("unexpected token after `$`".into()), |
| 95 | 127 | } |
| @@ -106,9 +138,9 @@ fn parse_pattern(ts: TokenStream) -> Result<Vec<Pat>, String> { | ||
| 106 | 138 | impl MacroDef { |
| 107 | 139 | /// Match an invocation's tokens and substitute them into the body. |
| 108 | 140 | pub fn expand(&self, input: TokenStream) -> Result<TokenStream, String> { |
| 109 | - let mut binds = HashMap::new(); | |
| 141 | + let mut binds = Binds::new(); | |
| 110 | 142 | let toks: Vec<TokenTree> = input.into_iter().collect(); |
| 111 | - let used = match_seq(&self.pattern, &toks, &mut binds)?; | |
| 143 | + let used = match_seq(&self.pattern, &toks, &mut binds, None)?; | |
| 112 | 144 | if used != toks.len() { |
| 113 | 145 | return Err("the invocation has tokens the matcher does not consume".into()); |
| 114 | 146 | } |
| @@ -117,10 +149,14 @@ impl MacroDef { | ||
| 117 | 149 | } |
| 118 | 150 | |
| 119 | 151 | /// Match `pats` against the front of `toks`, returning how many were consumed. |
| 152 | +/// `outer_stop` is where a trailing fragment must stop when the pattern | |
| 153 | +/// itself does not say: inside `$(, $rest:expr)*` that is the repetition's own | |
| 154 | +/// leading comma, which is where the next iteration begins. | |
| 120 | 155 | fn match_seq( |
| 121 | 156 | pats: &[Pat], |
| 122 | 157 | toks: &[TokenTree], |
| 123 | - binds: &mut HashMap<String, TokenStream>, | |
| 158 | + binds: &mut Binds, | |
| 159 | + outer_stop: Option<&str>, | |
| 124 | 160 | ) -> Result<usize, String> { |
| 125 | 161 | let mut i = 0; |
| 126 | 162 | for (k, p) in pats.iter().enumerate() { |
| @@ -140,20 +176,67 @@ fn match_seq( | ||
| 140 | 176 | return Err("mismatched delimiter".into()); |
| 141 | 177 | } |
| 142 | 178 | let sub: Vec<TokenTree> = g.stream().into_iter().collect(); |
| 143 | - let n = match_seq(inner, &sub, binds)?; | |
| 179 | + let n = match_seq(inner, &sub, binds, None)?; | |
| 144 | 180 | if n != sub.len() { |
| 145 | 181 | return Err("group has tokens the matcher does not consume".into()); |
| 146 | 182 | } |
| 147 | 183 | i += 1; |
| 148 | 184 | } |
| 185 | + Pat::Rep { inner, sep, op } => { | |
| 186 | + // Everything after the repetition that is a literal token | |
| 187 | + // marks where it has to stop. | |
| 188 | + let stop = next_literal(&pats[k + 1..]).or_else(|| outer_stop.map(str::to_string)); | |
| 189 | + // A trailing fragment inside the repetition stops at the | |
| 190 | + // separator if there is one, or otherwise at whatever starts | |
| 191 | + // the next iteration. | |
| 192 | + let inner_stop = sep.clone().or_else(|| next_literal(inner)); | |
| 193 | + let names = frag_names(inner); | |
| 194 | + let mut count = 0usize; | |
| 195 | + for n in &names { | |
| 196 | + binds.insert(n.clone(), Cap::Seq(Vec::new())); | |
| 197 | + } | |
| 198 | + while i < toks.len() { | |
| 199 | + if let Some(stop) = &stop { | |
| 200 | + if toks[i].to_string() == *stop { | |
| 201 | + break; | |
| 202 | + } | |
| 203 | + } | |
| 204 | + let mut one = Binds::new(); | |
| 205 | + let used = match_seq(inner, &toks[i..], &mut one, inner_stop.as_deref())?; | |
| 206 | + if used == 0 { | |
| 207 | + break; | |
| 208 | + } | |
| 209 | + for n in &names { | |
| 210 | + let v = match one.remove(n) { | |
| 211 | + Some(Cap::One(ts)) => ts, | |
| 212 | + _ => return Err(format!("`${n}` did not capture in a repetition")), | |
| 213 | + }; | |
| 214 | + match binds.get_mut(n) { | |
| 215 | + Some(Cap::Seq(v0)) => v0.push(v), | |
| 216 | + _ => unreachable!(), | |
| 217 | + } | |
| 218 | + } | |
| 219 | + count += 1; | |
| 220 | + i += used; | |
| 221 | + match sep { | |
| 222 | + Some(sp) if i < toks.len() && toks[i].to_string() == *sp => i += 1, | |
| 223 | + Some(_) => break, | |
| 224 | + None => {} | |
| 225 | + } | |
| 226 | + if *op == '?' { | |
| 227 | + break; | |
| 228 | + } | |
| 229 | + } | |
| 230 | + if *op == '+' && count == 0 { | |
| 231 | + return Err("`$(..)+` needs at least one repetition".into()); | |
| 232 | + } | |
| 233 | + } | |
| 149 | 234 | Pat::Frag(name) => { |
| 150 | 235 | // A fragment runs to the next literal token in the matcher, or |
| 151 | 236 | // to the end. That is what makes `$a:expr, $b:expr` split on |
| 152 | 237 | // the comma rather than swallowing it. |
| 153 | - let stop = pats[k + 1..].iter().find_map(|p| match p { | |
| 154 | - Pat::Tok(s) => Some(s.clone()), | |
| 155 | - _ => None, | |
| 156 | - }); | |
| 238 | + let stop = next_literal(&pats[k + 1..]) | |
| 239 | + .or_else(|| outer_stop.map(str::to_string)); | |
| 157 | 240 | let start = i; |
| 158 | 241 | let mut depth = 0i32; |
| 159 | 242 | while i < toks.len() { |
| @@ -163,9 +246,12 @@ fn match_seq( | ||
| 163 | 246 | break; |
| 164 | 247 | } |
| 165 | 248 | } |
| 249 | + // `<`/`>` are counted so a generic argument list is not | |
| 250 | + // split, but `>` also closes `=>` and `->`, so the depth | |
| 251 | + // never goes negative. | |
| 166 | 252 | match &toks[i] { |
| 167 | 253 | TokenTree::Punct(p) if p.as_char() == '<' => depth += 1, |
| 168 | - TokenTree::Punct(p) if p.as_char() == '>' => depth -= 1, | |
| 254 | + TokenTree::Punct(p) if p.as_char() == '>' => depth = (depth - 1).max(0), | |
| 169 | 255 | _ => {} |
| 170 | 256 | } |
| 171 | 257 | i += 1; |
| @@ -175,7 +261,7 @@ fn match_seq( | ||
| 175 | 261 | } |
| 176 | 262 | binds.insert( |
| 177 | 263 | name.clone(), |
| 178 | - toks[start..i].iter().cloned().collect::<TokenStream>(), | |
| 264 | + Cap::One(toks[start..i].iter().cloned().collect::<TokenStream>()), | |
| 179 | 265 | ); |
| 180 | 266 | } |
| 181 | 267 | } |
| @@ -183,13 +269,99 @@ fn match_seq( | ||
| 183 | 269 | Ok(i) |
| 184 | 270 | } |
| 185 | 271 | |
| 186 | -/// Replace every `$name` in the body with what it captured. | |
| 187 | -fn substitute(ts: TokenStream, binds: &HashMap<String, TokenStream>) -> TokenStream { | |
| 272 | +/// The first literal token that can follow, looking through a repetition: | |
| 273 | +/// after `$first:expr` in `($first:expr $(, $rest:expr)*)` the next literal is | |
| 274 | +/// the repetition's leading comma, which is where `$first` has to stop. | |
| 275 | +fn next_literal(pats: &[Pat]) -> Option<String> { | |
| 276 | + for p in pats { | |
| 277 | + match p { | |
| 278 | + Pat::Tok(s) => return Some(s.clone()), | |
| 279 | + Pat::Rep { inner, sep, .. } => { | |
| 280 | + if let Some(s) = next_literal(inner) { | |
| 281 | + return Some(s); | |
| 282 | + } | |
| 283 | + if let Some(s) = sep { | |
| 284 | + return Some(s.clone()); | |
| 285 | + } | |
| 286 | + } | |
| 287 | + Pat::Group(..) | Pat::Frag(_) => return None, | |
| 288 | + } | |
| 289 | + } | |
| 290 | + None | |
| 291 | +} | |
| 292 | + | |
| 293 | +/// The fragment names a pattern captures. | |
| 294 | +fn frag_names(pats: &[Pat]) -> Vec<String> { | |
| 295 | + let mut out = Vec::new(); | |
| 296 | + for p in pats { | |
| 297 | + match p { | |
| 298 | + Pat::Frag(n) => out.push(n.clone()), | |
| 299 | + Pat::Group(_, inner) => out.extend(frag_names(inner)), | |
| 300 | + Pat::Rep { inner, .. } => out.extend(frag_names(inner)), | |
| 301 | + Pat::Tok(_) => {} | |
| 302 | + } | |
| 303 | + } | |
| 304 | + out | |
| 305 | +} | |
| 306 | + | |
| 307 | +/// Replace every `$name` in the body with what it captured, and expand every | |
| 308 | +/// `$( .. ) sep? op` once per repetition. | |
| 309 | +fn substitute(ts: TokenStream, binds: &Binds) -> TokenStream { | |
| 188 | 310 | let mut out = Vec::new(); |
| 189 | 311 | let mut it = ts.into_iter().peekable(); |
| 190 | 312 | while let Some(t) = it.next() { |
| 191 | 313 | match t { |
| 192 | 314 | TokenTree::Punct(p) if p.as_char() == '$' => { |
| 315 | + // `$( .. ) sep? op` in the transcriber. | |
| 316 | + if let Some(TokenTree::Group(g)) = it.peek() { | |
| 317 | + let g = g.clone(); | |
| 318 | + it.next(); | |
| 319 | + let mut sep = None; | |
| 320 | + loop { | |
| 321 | + match it.peek() { | |
| 322 | + Some(TokenTree::Punct(p)) | |
| 323 | + if matches!(p.as_char(), '*' | '+' | '?') => | |
| 324 | + { | |
| 325 | + it.next(); | |
| 326 | + break; | |
| 327 | + } | |
| 328 | + Some(t) => { | |
| 329 | + sep = Some(t.clone()); | |
| 330 | + it.next(); | |
| 331 | + } | |
| 332 | + None => break, | |
| 333 | + } | |
| 334 | + } | |
| 335 | + let names: Vec<String> = fragments_in(g.stream()) | |
| 336 | + .into_iter() | |
| 337 | + .filter(|n| matches!(binds.get(n), Some(Cap::Seq(_)))) | |
| 338 | + .collect(); | |
| 339 | + let n = names | |
| 340 | + .iter() | |
| 341 | + .filter_map(|n| match binds.get(n) { | |
| 342 | + Some(Cap::Seq(v)) => Some(v.len()), | |
| 343 | + _ => None, | |
| 344 | + }) | |
| 345 | + .max() | |
| 346 | + .unwrap_or(0); | |
| 347 | + for idx in 0..n { | |
| 348 | + let mut one: Binds = binds.clone(); | |
| 349 | + for nm in &names { | |
| 350 | + if let Some(Cap::Seq(v)) = binds.get(nm) { | |
| 351 | + if let Some(x) = v.get(idx) { | |
| 352 | + one.insert(nm.clone(), Cap::One(x.clone())); | |
| 353 | + } | |
| 354 | + } | |
| 355 | + } | |
| 356 | + if idx > 0 { | |
| 357 | + if let Some(s) = &sep { | |
| 358 | + out.push(s.clone()); | |
| 359 | + } | |
| 360 | + } | |
| 361 | + out.extend(substitute(g.stream(), &one)); | |
| 362 | + } | |
| 363 | + continue; | |
| 364 | + } | |
| 193 | 365 | match it.peek() { |
| 194 | 366 | Some(TokenTree::Ident(name)) => { |
| 195 | 367 | let n = name.to_string(); |
| @@ -197,10 +369,18 @@ fn substitute(ts: TokenStream, binds: &HashMap<String, TokenStream>) -> TokenStr | ||
| 197 | 369 | match binds.get(&n) { |
| 198 | 370 | // Parenthesised so that a captured expression keeps |
| 199 | 371 | // its own precedence, as Rust's `expr` fragments do. |
| 200 | - Some(v) => out.push(TokenTree::Group(Group::new( | |
| 372 | + Some(Cap::One(v)) => out.push(TokenTree::Group(Group::new( | |
| 201 | 373 | Delimiter::Parenthesis, |
| 202 | 374 | v.clone(), |
| 203 | 375 | ))), |
| 376 | + Some(Cap::Seq(_)) => { | |
| 377 | + // Used outside a `$(..)`; Rust rejects this too. | |
| 378 | + out.push(TokenTree::Punct(p)); | |
| 379 | + out.push(TokenTree::Ident(Ident::new( | |
| 380 | + &n, | |
| 381 | + proc_macro2::Span::call_site(), | |
| 382 | + ))); | |
| 383 | + } | |
| 204 | 384 | None => { |
| 205 | 385 | out.push(TokenTree::Punct(p)); |
| 206 | 386 | out.push(TokenTree::Ident(Ident::new( |
| @@ -222,3 +402,22 @@ fn substitute(ts: TokenStream, binds: &HashMap<String, TokenStream>) -> TokenStr | ||
| 222 | 402 | } |
| 223 | 403 | out.into_iter().collect() |
| 224 | 404 | } |
| 405 | + | |
| 406 | +/// The `$name` references appearing in a token stream. | |
| 407 | +fn fragments_in(ts: TokenStream) -> Vec<String> { | |
| 408 | + let mut out = Vec::new(); | |
| 409 | + let mut it = ts.into_iter().peekable(); | |
| 410 | + while let Some(t) = it.next() { | |
| 411 | + match t { | |
| 412 | + TokenTree::Punct(p) if p.as_char() == '$' => { | |
| 413 | + if let Some(TokenTree::Ident(n)) = it.peek() { | |
| 414 | + out.push(n.to_string()); | |
| 415 | + it.next(); | |
| 416 | + } | |
| 417 | + } | |
| 418 | + TokenTree::Group(g) => out.extend(fragments_in(g.stream())), | |
| 419 | + _ => {} | |
| 420 | + } | |
| 421 | + } | |
| 422 | + out | |
| 423 | +} | |
| @@ -28,8 +28,19 @@ enum Pat { | |||
| 28 | Tok(String), | 28 | Tok(String), |
| 29 | /// A delimited group, matched recursively. | 29 | /// A delimited group, matched recursively. |
| 30 | Group(Delimiter, Vec<Pat>), | 30 | Group(Delimiter, Vec<Pat>), |
| 31 | + /// `$( .. ) sep? op` — a repetition. `op` is `*`, `+` or `?`. | ||
| 32 | + Rep { inner: Vec<Pat>, sep: Option<String>, op: char }, | ||
| 31 | } | 33 | } |
| 32 | 34 | ||
| 35 | +/// What a fragment captured: once, or once per repetition. | ||
| 36 | +#[derive(Debug, Clone)] | ||
| 37 | +enum Cap { | ||
| 38 | + One(TokenStream), | ||
| 39 | + Seq(Vec<TokenStream>), | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +type Binds = HashMap<String, Cap>; | ||
| 43 | + | ||
| 33 | #[derive(Debug, Clone)] | 44 | #[derive(Debug, Clone)] |
| 34 | pub struct MacroDef { | 45 | pub struct MacroDef { |
| 35 | pattern: Vec<Pat>, | 46 | pattern: Vec<Pat>, |
| @@ -84,12 +95,33 @@ fn parse_pattern(ts: TokenStream) -> Result<Vec<Pat>, String> { | |||
| 84 | } | 95 | } |
| 85 | out.push(Pat::Frag(name.to_string())); | 96 | out.push(Pat::Frag(name.to_string())); |
| 86 | } | 97 | } |
| 87 | - Some(TokenTree::Group(_)) => { | 98 | + Some(TokenTree::Group(g)) => { |
| 88 | - return Err( | 99 | + let inner = parse_pattern(g.stream())?; |
| 89 | - "`$(..)` repetition is not implemented yet; it maps to \ | 100 | + if inner.iter().any(|p| matches!(p, Pat::Rep { .. })) { |
| 90 | - `varargs` in a Nim macro" | 101 | + return Err("nested `$(..)` repetition is not implemented yet".into()); |
| 91 | - .into(), | 102 | + } |
| 92 | - ) | 103 | + // `$( .. ) sep? op`: an optional separator token, then |
| 104 | + // the operator. | ||
| 105 | + let mut sep = None; | ||
| 106 | + let op = loop { | ||
| 107 | + match it.next() { | ||
| 108 | + Some(TokenTree::Punct(p)) | ||
| 109 | + if matches!(p.as_char(), '*' | '+' | '?') => | ||
| 110 | + { | ||
| 111 | + break p.as_char() | ||
| 112 | + } | ||
| 113 | + Some(t) => { | ||
| 114 | + if sep.is_some() { | ||
| 115 | + return Err( | ||
| 116 | + "a repetition separator must be one token".into() | ||
| 117 | + ); | ||
| 118 | + } | ||
| 119 | + sep = Some(t.to_string()); | ||
| 120 | + } | ||
| 121 | + None => return Err("a repetition needs `*`, `+` or `?`".into()), | ||
| 122 | + } | ||
| 123 | + }; | ||
| 124 | + out.push(Pat::Rep { inner, sep, op }); | ||
| 93 | } | 125 | } |
| 94 | _ => return Err("unexpected token after `$`".into()), | 126 | _ => return Err("unexpected token after `$`".into()), |
| 95 | } | 127 | } |
| @@ -106,9 +138,9 @@ fn parse_pattern(ts: TokenStream) -> Result<Vec<Pat>, String> { | |||
| 106 | impl MacroDef { | 138 | impl MacroDef { |
| 107 | /// Match an invocation's tokens and substitute them into the body. | 139 | /// Match an invocation's tokens and substitute them into the body. |
| 108 | pub fn expand(&self, input: TokenStream) -> Result<TokenStream, String> { | 140 | pub fn expand(&self, input: TokenStream) -> Result<TokenStream, String> { |
| 109 | - let mut binds = HashMap::new(); | 141 | + let mut binds = Binds::new(); |
| 110 | let toks: Vec<TokenTree> = input.into_iter().collect(); | 142 | let toks: Vec<TokenTree> = input.into_iter().collect(); |
| 111 | - let used = match_seq(&self.pattern, &toks, &mut binds)?; | 143 | + let used = match_seq(&self.pattern, &toks, &mut binds, None)?; |
| 112 | if used != toks.len() { | 144 | if used != toks.len() { |
| 113 | return Err("the invocation has tokens the matcher does not consume".into()); | 145 | return Err("the invocation has tokens the matcher does not consume".into()); |
| 114 | } | 146 | } |
| @@ -117,10 +149,14 @@ impl MacroDef { | |||
| 117 | } | 149 | } |
| 118 | 150 | ||
| 119 | /// Match `pats` against the front of `toks`, returning how many were consumed. | 151 | /// Match `pats` against the front of `toks`, returning how many were consumed. |
| 152 | +/// `outer_stop` is where a trailing fragment must stop when the pattern | ||
| 153 | +/// itself does not say: inside `$(, $rest:expr)*` that is the repetition's own | ||
| 154 | +/// leading comma, which is where the next iteration begins. | ||
| 120 | fn match_seq( | 155 | fn match_seq( |
| 121 | pats: &[Pat], | 156 | pats: &[Pat], |
| 122 | toks: &[TokenTree], | 157 | toks: &[TokenTree], |
| 123 | - binds: &mut HashMap<String, TokenStream>, | 158 | + binds: &mut Binds, |
| 159 | + outer_stop: Option<&str>, | ||
| 124 | ) -> Result<usize, String> { | 160 | ) -> Result<usize, String> { |
| 125 | let mut i = 0; | 161 | let mut i = 0; |
| 126 | for (k, p) in pats.iter().enumerate() { | 162 | for (k, p) in pats.iter().enumerate() { |
| @@ -140,20 +176,67 @@ fn match_seq( | |||
| 140 | return Err("mismatched delimiter".into()); | 176 | return Err("mismatched delimiter".into()); |
| 141 | } | 177 | } |
| 142 | let sub: Vec<TokenTree> = g.stream().into_iter().collect(); | 178 | let sub: Vec<TokenTree> = g.stream().into_iter().collect(); |
| 143 | - let n = match_seq(inner, &sub, binds)?; | 179 | + let n = match_seq(inner, &sub, binds, None)?; |
| 144 | if n != sub.len() { | 180 | if n != sub.len() { |
| 145 | return Err("group has tokens the matcher does not consume".into()); | 181 | return Err("group has tokens the matcher does not consume".into()); |
| 146 | } | 182 | } |
| 147 | i += 1; | 183 | i += 1; |
| 148 | } | 184 | } |
| 185 | + Pat::Rep { inner, sep, op } => { | ||
| 186 | + // Everything after the repetition that is a literal token | ||
| 187 | + // marks where it has to stop. | ||
| 188 | + let stop = next_literal(&pats[k + 1..]).or_else(|| outer_stop.map(str::to_string)); | ||
| 189 | + // A trailing fragment inside the repetition stops at the | ||
| 190 | + // separator if there is one, or otherwise at whatever starts | ||
| 191 | + // the next iteration. | ||
| 192 | + let inner_stop = sep.clone().or_else(|| next_literal(inner)); | ||
| 193 | + let names = frag_names(inner); | ||
| 194 | + let mut count = 0usize; | ||
| 195 | + for n in &names { | ||
| 196 | + binds.insert(n.clone(), Cap::Seq(Vec::new())); | ||
| 197 | + } | ||
| 198 | + while i < toks.len() { | ||
| 199 | + if let Some(stop) = &stop { | ||
| 200 | + if toks[i].to_string() == *stop { | ||
| 201 | + break; | ||
| 202 | + } | ||
| 203 | + } | ||
| 204 | + let mut one = Binds::new(); | ||
| 205 | + let used = match_seq(inner, &toks[i..], &mut one, inner_stop.as_deref())?; | ||
| 206 | + if used == 0 { | ||
| 207 | + break; | ||
| 208 | + } | ||
| 209 | + for n in &names { | ||
| 210 | + let v = match one.remove(n) { | ||
| 211 | + Some(Cap::One(ts)) => ts, | ||
| 212 | + _ => return Err(format!("`${n}` did not capture in a repetition")), | ||
| 213 | + }; | ||
| 214 | + match binds.get_mut(n) { | ||
| 215 | + Some(Cap::Seq(v0)) => v0.push(v), | ||
| 216 | + _ => unreachable!(), | ||
| 217 | + } | ||
| 218 | + } | ||
| 219 | + count += 1; | ||
| 220 | + i += used; | ||
| 221 | + match sep { | ||
| 222 | + Some(sp) if i < toks.len() && toks[i].to_string() == *sp => i += 1, | ||
| 223 | + Some(_) => break, | ||
| 224 | + None => {} | ||
| 225 | + } | ||
| 226 | + if *op == '?' { | ||
| 227 | + break; | ||
| 228 | + } | ||
| 229 | + } | ||
| 230 | + if *op == '+' && count == 0 { | ||
| 231 | + return Err("`$(..)+` needs at least one repetition".into()); | ||
| 232 | + } | ||
| 233 | + } | ||
| 149 | Pat::Frag(name) => { | 234 | Pat::Frag(name) => { |
| 150 | // A fragment runs to the next literal token in the matcher, or | 235 | // A fragment runs to the next literal token in the matcher, or |
| 151 | // to the end. That is what makes `$a:expr, $b:expr` split on | 236 | // to the end. That is what makes `$a:expr, $b:expr` split on |
| 152 | // the comma rather than swallowing it. | 237 | // the comma rather than swallowing it. |
| 153 | - let stop = pats[k + 1..].iter().find_map(|p| match p { | 238 | + let stop = next_literal(&pats[k + 1..]) |
| 154 | - Pat::Tok(s) => Some(s.clone()), | 239 | + .or_else(|| outer_stop.map(str::to_string)); |
| 155 | - _ => None, | ||
| 156 | - }); | ||
| 157 | let start = i; | 240 | let start = i; |
| 158 | let mut depth = 0i32; | 241 | let mut depth = 0i32; |
| 159 | while i < toks.len() { | 242 | while i < toks.len() { |
| @@ -163,9 +246,12 @@ fn match_seq( | |||
| 163 | break; | 246 | break; |
| 164 | } | 247 | } |
| 165 | } | 248 | } |
| 249 | + // `<`/`>` are counted so a generic argument list is not | ||
| 250 | + // split, but `>` also closes `=>` and `->`, so the depth | ||
| 251 | + // never goes negative. | ||
| 166 | match &toks[i] { | 252 | match &toks[i] { |
| 167 | TokenTree::Punct(p) if p.as_char() == '<' => depth += 1, | 253 | TokenTree::Punct(p) if p.as_char() == '<' => depth += 1, |
| 168 | - TokenTree::Punct(p) if p.as_char() == '>' => depth -= 1, | 254 | + TokenTree::Punct(p) if p.as_char() == '>' => depth = (depth - 1).max(0), |
| 169 | _ => {} | 255 | _ => {} |
| 170 | } | 256 | } |
| 171 | i += 1; | 257 | i += 1; |
| @@ -175,7 +261,7 @@ fn match_seq( | |||
| 175 | } | 261 | } |
| 176 | binds.insert( | 262 | binds.insert( |
| 177 | name.clone(), | 263 | name.clone(), |
| 178 | - toks[start..i].iter().cloned().collect::<TokenStream>(), | 264 | + Cap::One(toks[start..i].iter().cloned().collect::<TokenStream>()), |
| 179 | ); | 265 | ); |
| 180 | } | 266 | } |
| 181 | } | 267 | } |
| @@ -183,13 +269,99 @@ fn match_seq( | |||
| 183 | Ok(i) | 269 | Ok(i) |
| 184 | } | 270 | } |
| 185 | 271 | ||
| 186 | -/// Replace every `$name` in the body with what it captured. | 272 | +/// The first literal token that can follow, looking through a repetition: |
| 187 | -fn substitute(ts: TokenStream, binds: &HashMap<String, TokenStream>) -> TokenStream { | 273 | +/// after `$first:expr` in `($first:expr $(, $rest:expr)*)` the next literal is |
| 274 | +/// the repetition's leading comma, which is where `$first` has to stop. | ||
| 275 | +fn next_literal(pats: &[Pat]) -> Option<String> { | ||
| 276 | + for p in pats { | ||
| 277 | + match p { | ||
| 278 | + Pat::Tok(s) => return Some(s.clone()), | ||
| 279 | + Pat::Rep { inner, sep, .. } => { | ||
| 280 | + if let Some(s) = next_literal(inner) { | ||
| 281 | + return Some(s); | ||
| 282 | + } | ||
| 283 | + if let Some(s) = sep { | ||
| 284 | + return Some(s.clone()); | ||
| 285 | + } | ||
| 286 | + } | ||
| 287 | + Pat::Group(..) | Pat::Frag(_) => return None, | ||
| 288 | + } | ||
| 289 | + } | ||
| 290 | + None | ||
| 291 | +} | ||
| 292 | + | ||
| 293 | +/// The fragment names a pattern captures. | ||
| 294 | +fn frag_names(pats: &[Pat]) -> Vec<String> { | ||
| 295 | + let mut out = Vec::new(); | ||
| 296 | + for p in pats { | ||
| 297 | + match p { | ||
| 298 | + Pat::Frag(n) => out.push(n.clone()), | ||
| 299 | + Pat::Group(_, inner) => out.extend(frag_names(inner)), | ||
| 300 | + Pat::Rep { inner, .. } => out.extend(frag_names(inner)), | ||
| 301 | + Pat::Tok(_) => {} | ||
| 302 | + } | ||
| 303 | + } | ||
| 304 | + out | ||
| 305 | +} | ||
| 306 | + | ||
| 307 | +/// Replace every `$name` in the body with what it captured, and expand every | ||
| 308 | +/// `$( .. ) sep? op` once per repetition. | ||
| 309 | +fn substitute(ts: TokenStream, binds: &Binds) -> TokenStream { | ||
| 188 | let mut out = Vec::new(); | 310 | let mut out = Vec::new(); |
| 189 | let mut it = ts.into_iter().peekable(); | 311 | let mut it = ts.into_iter().peekable(); |
| 190 | while let Some(t) = it.next() { | 312 | while let Some(t) = it.next() { |
| 191 | match t { | 313 | match t { |
| 192 | TokenTree::Punct(p) if p.as_char() == '$' => { | 314 | TokenTree::Punct(p) if p.as_char() == '$' => { |
| 315 | + // `$( .. ) sep? op` in the transcriber. | ||
| 316 | + if let Some(TokenTree::Group(g)) = it.peek() { | ||
| 317 | + let g = g.clone(); | ||
| 318 | + it.next(); | ||
| 319 | + let mut sep = None; | ||
| 320 | + loop { | ||
| 321 | + match it.peek() { | ||
| 322 | + Some(TokenTree::Punct(p)) | ||
| 323 | + if matches!(p.as_char(), '*' | '+' | '?') => | ||
| 324 | + { | ||
| 325 | + it.next(); | ||
| 326 | + break; | ||
| 327 | + } | ||
| 328 | + Some(t) => { | ||
| 329 | + sep = Some(t.clone()); | ||
| 330 | + it.next(); | ||
| 331 | + } | ||
| 332 | + None => break, | ||
| 333 | + } | ||
| 334 | + } | ||
| 335 | + let names: Vec<String> = fragments_in(g.stream()) | ||
| 336 | + .into_iter() | ||
| 337 | + .filter(|n| matches!(binds.get(n), Some(Cap::Seq(_)))) | ||
| 338 | + .collect(); | ||
| 339 | + let n = names | ||
| 340 | + .iter() | ||
| 341 | + .filter_map(|n| match binds.get(n) { | ||
| 342 | + Some(Cap::Seq(v)) => Some(v.len()), | ||
| 343 | + _ => None, | ||
| 344 | + }) | ||
| 345 | + .max() | ||
| 346 | + .unwrap_or(0); | ||
| 347 | + for idx in 0..n { | ||
| 348 | + let mut one: Binds = binds.clone(); | ||
| 349 | + for nm in &names { | ||
| 350 | + if let Some(Cap::Seq(v)) = binds.get(nm) { | ||
| 351 | + if let Some(x) = v.get(idx) { | ||
| 352 | + one.insert(nm.clone(), Cap::One(x.clone())); | ||
| 353 | + } | ||
| 354 | + } | ||
| 355 | + } | ||
| 356 | + if idx > 0 { | ||
| 357 | + if let Some(s) = &sep { | ||
| 358 | + out.push(s.clone()); | ||
| 359 | + } | ||
| 360 | + } | ||
| 361 | + out.extend(substitute(g.stream(), &one)); | ||
| 362 | + } | ||
| 363 | + continue; | ||
| 364 | + } | ||
| 193 | match it.peek() { | 365 | match it.peek() { |
| 194 | Some(TokenTree::Ident(name)) => { | 366 | Some(TokenTree::Ident(name)) => { |
| 195 | let n = name.to_string(); | 367 | let n = name.to_string(); |
| @@ -197,10 +369,18 @@ fn substitute(ts: TokenStream, binds: &HashMap<String, TokenStream>) -> TokenStr | |||
| 197 | match binds.get(&n) { | 369 | match binds.get(&n) { |
| 198 | // Parenthesised so that a captured expression keeps | 370 | // Parenthesised so that a captured expression keeps |
| 199 | // its own precedence, as Rust's `expr` fragments do. | 371 | // its own precedence, as Rust's `expr` fragments do. |
| 200 | - Some(v) => out.push(TokenTree::Group(Group::new( | 372 | + Some(Cap::One(v)) => out.push(TokenTree::Group(Group::new( |
| 201 | Delimiter::Parenthesis, | 373 | Delimiter::Parenthesis, |
| 202 | v.clone(), | 374 | v.clone(), |
| 203 | ))), | 375 | ))), |
| 376 | + Some(Cap::Seq(_)) => { | ||
| 377 | + // Used outside a `$(..)`; Rust rejects this too. | ||
| 378 | + out.push(TokenTree::Punct(p)); | ||
| 379 | + out.push(TokenTree::Ident(Ident::new( | ||
| 380 | + &n, | ||
| 381 | + proc_macro2::Span::call_site(), | ||
| 382 | + ))); | ||
| 383 | + } | ||
| 204 | None => { | 384 | None => { |
| 205 | out.push(TokenTree::Punct(p)); | 385 | out.push(TokenTree::Punct(p)); |
| 206 | out.push(TokenTree::Ident(Ident::new( | 386 | out.push(TokenTree::Ident(Ident::new( |
| @@ -222,3 +402,22 @@ fn substitute(ts: TokenStream, binds: &HashMap<String, TokenStream>) -> TokenStr | |||
| 222 | } | 402 | } |
| 223 | out.into_iter().collect() | 403 | out.into_iter().collect() |
| 224 | } | 404 | } |
| 405 | + | ||
| 406 | +/// The `$name` references appearing in a token stream. | ||
| 407 | +fn fragments_in(ts: TokenStream) -> Vec<String> { | ||
| 408 | + let mut out = Vec::new(); | ||
| 409 | + let mut it = ts.into_iter().peekable(); | ||
| 410 | + while let Some(t) = it.next() { | ||
| 411 | + match t { | ||
| 412 | + TokenTree::Punct(p) if p.as_char() == '$' => { | ||
| 413 | + if let Some(TokenTree::Ident(n)) = it.peek() { | ||
| 414 | + out.push(n.to_string()); | ||
| 415 | + it.next(); | ||
| 416 | + } | ||
| 417 | + } | ||
| 418 | + TokenTree::Group(g) => out.extend(fragments_in(g.stream())), | ||
| 419 | + _ => {} | ||
| 420 | + } | ||
| 421 | + } | ||
| 422 | + out | ||
| 423 | +} | ||
added
tests/cases/038-macro-repetition.rs +46 -0 | new file mode 100644 | ||
| @@ -0,0 +1,46 @@ | ||
| 1 | +// `$(..)` repetition — 28% of the `macro_rules!` in a 400-crate sample. The | |
| 2 | +// matcher binds each fragment once per iteration, and the transcriber emits | |
| 3 | +// its body once per iteration, joined by the separator. | |
| 4 | + | |
| 5 | +macro_rules! sum_all { | |
| 6 | + ($($x:expr),*) => { | |
| 7 | + 0 $(+ $x)* | |
| 8 | + }; | |
| 9 | +} | |
| 10 | + | |
| 11 | +macro_rules! max_of { | |
| 12 | + ($first:expr $(, $rest:expr)*) => {{ | |
| 13 | + let mut m = $first; | |
| 14 | + $( if $rest > m { m = $rest; } )* | |
| 15 | + m | |
| 16 | + }}; | |
| 17 | +} | |
| 18 | + | |
| 19 | +macro_rules! count_args { | |
| 20 | + ($($x:expr),*) => { | |
| 21 | + 0 $(+ { let _ = $x; 1 })* | |
| 22 | + }; | |
| 23 | +} | |
| 24 | + | |
| 25 | +macro_rules! pairs_sum { | |
| 26 | + ($($a:expr => $b:expr),*) => { | |
| 27 | + 0 $(+ $a * $b)* | |
| 28 | + }; | |
| 29 | +} | |
| 30 | + | |
| 31 | +fn main() { | |
| 32 | + println!("{}", sum_all!()); | |
| 33 | + println!("{}", sum_all!(1)); | |
| 34 | + println!("{}", sum_all!(1, 2, 3, 4)); | |
| 35 | + // Precedence survives: each capture is parenthesised. | |
| 36 | + println!("{}", sum_all!(1 + 1, 2 * 3)); | |
| 37 | + | |
| 38 | + println!("{}", max_of!(3)); | |
| 39 | + println!("{}", max_of!(3, 9, 2)); | |
| 40 | + println!("{}", max_of!(-5, -9, -1)); | |
| 41 | + | |
| 42 | + println!("{}", count_args!(7, 8, 9)); | |
| 43 | + println!("{}", count_args!()); | |
| 44 | + | |
| 45 | + println!("{}", pairs_sum!(2 => 3, 4 => 5)); | |
| 46 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,46 @@ | |||
| 1 | +// `$(..)` repetition — 28% of the `macro_rules!` in a 400-crate sample. The | ||
| 2 | +// matcher binds each fragment once per iteration, and the transcriber emits | ||
| 3 | +// its body once per iteration, joined by the separator. | ||
| 4 | + | ||
| 5 | +macro_rules! sum_all { | ||
| 6 | + ($($x:expr),*) => { | ||
| 7 | + 0 $(+ $x)* | ||
| 8 | + }; | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +macro_rules! max_of { | ||
| 12 | + ($first:expr $(, $rest:expr)*) => {{ | ||
| 13 | + let mut m = $first; | ||
| 14 | + $( if $rest > m { m = $rest; } )* | ||
| 15 | + m | ||
| 16 | + }}; | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +macro_rules! count_args { | ||
| 20 | + ($($x:expr),*) => { | ||
| 21 | + 0 $(+ { let _ = $x; 1 })* | ||
| 22 | + }; | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +macro_rules! pairs_sum { | ||
| 26 | + ($($a:expr => $b:expr),*) => { | ||
| 27 | + 0 $(+ $a * $b)* | ||
| 28 | + }; | ||
| 29 | +} | ||
| 30 | + | ||
| 31 | +fn main() { | ||
| 32 | + println!("{}", sum_all!()); | ||
| 33 | + println!("{}", sum_all!(1)); | ||
| 34 | + println!("{}", sum_all!(1, 2, 3, 4)); | ||
| 35 | + // Precedence survives: each capture is parenthesised. | ||
| 36 | + println!("{}", sum_all!(1 + 1, 2 * 3)); | ||
| 37 | + | ||
| 38 | + println!("{}", max_of!(3)); | ||
| 39 | + println!("{}", max_of!(3, 9, 2)); | ||
| 40 | + println!("{}", max_of!(-5, -9, -1)); | ||
| 41 | + | ||
| 42 | + println!("{}", count_args!(7, 8, 9)); | ||
| 43 | + println!("{}", count_args!()); | ||
| 44 | + | ||
| 45 | + println!("{}", pairs_sum!(2 => 3, 4 => 5)); | ||
| 46 | +} | ||