nandi/rustnimpublic Fork 0
04eb29f2f77152e460b50f58935cb88305227b43
Commits
Clone
git clone https://git.rickub.com/nandi/rustnim.git
git clone ssh://git@rickub.com/nandi/rustnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

mrules.rs · 224 lines · 9.0 KBRust Blame HistoryRaw
Expand `macro_rules!` rather than translating it to a Nim template 04eb29f nandithebull 16h ago1//! A `macro_rules!` expander.
2//!
3//! The alternative was translating each `macro_rules!` into a Nim `template`,
4//! which is the shape-level correspondence: 36% of the 1,833 definitions in a
5//! 400-crate sample are a single rule with no repetition, which is exactly a
6//! template, and another 40% map to a Nim `macro` over `varargs`.
7//!
8//! That correspondence is real but it does not survive contact with this
9//! project's approach. A Nim template body is *untyped*: it is substituted and
10//! only then type-checked. Our lowering is type-directed throughout — it needs
11//! a type to choose `div` over `/`, to size a `cast`, to pick an integer
12//! literal's width. Translating a macro body would mean lowering Rust with no
13//! type information, which is precisely the guessing the project refuses.
14//!
15//! Expanding at the call site does not have that problem: the expansion is
16//! ordinary Rust in a context where types are known, so it lowers like any
17//! other code. Same applicability, faithful output.
18//!
19//! Implemented here: a single rule, no repetition, no `:tt`. That is the 36%.
20
21use proc_macro2::{Delimiter, Group, Ident, TokenStream, TokenTree};
22use std::collections::HashMap;
23
24/// One element of a matcher: a captured fragment, or a token to match exactly.
25#[derive(Debug, Clone)]
26enum Pat {
27 Frag(String),
28 Tok(String),
29 /// A delimited group, matched recursively.
30 Group(Delimiter, Vec<Pat>),
31}
32
33#[derive(Debug, Clone)]
34pub struct MacroDef {
35 pattern: Vec<Pat>,
36 body: TokenStream,
37}
38
39/// Parse `macro_rules!`'s body: `( $matcher ) => { $transcriber };`.
40pub fn parse(tokens: TokenStream) -> Result<MacroDef, String> {
41 let t: Vec<TokenTree> = tokens.into_iter().collect();
42 // matcher group, `=`, `>`, transcriber group, optional `;`
43 let (matcher, rest) = match t.split_first() {
44 Some((TokenTree::Group(g), rest)) => (g.clone(), rest),
45 _ => return Err("expected a matcher group".into()),
46 };
47 let arrow: String = rest.iter().take(2).map(|t| t.to_string()).collect();
48 if arrow != "=>" {
49 return Err("expected `=>`".into());
50 }
51 let body = match rest.get(2) {
52 Some(TokenTree::Group(g)) => g.stream(),
53 _ => return Err("expected a transcriber group".into()),
54 };
55 if rest.len() > 4 || (rest.len() == 4 && rest[3].to_string() != ";") {
56 return Err("more than one rule is not implemented yet".into());
57 }
58 Ok(MacroDef { pattern: parse_pattern(matcher.stream())?, body })
59}
60
61fn parse_pattern(ts: TokenStream) -> Result<Vec<Pat>, String> {
62 let mut out = Vec::new();
63 let mut it = ts.into_iter().peekable();
64 while let Some(t) = it.next() {
65 match t {
66 TokenTree::Punct(p) if p.as_char() == '$' => {
67 match it.next() {
68 Some(TokenTree::Ident(name)) => {
69 // `$name:fragment`
70 match it.next() {
71 Some(TokenTree::Punct(c)) if c.as_char() == ':' => {}
72 _ => return Err("expected `:` after a fragment name".into()),
73 }
74 let kind = match it.next() {
75 Some(TokenTree::Ident(k)) => k.to_string(),
76 _ => return Err("expected a fragment specifier".into()),
77 };
78 if kind == "tt" {
79 return Err(
80 "`:tt` makes a macro a token-tree interpreter, which \
81 has no mechanical translation"
82 .into(),
83 );
84 }
85 out.push(Pat::Frag(name.to_string()));
86 }
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 )
93 }
94 _ => return Err("unexpected token after `$`".into()),
95 }
96 }
97 TokenTree::Group(g) => {
98 out.push(Pat::Group(g.delimiter(), parse_pattern(g.stream())?))
99 }
100 other => out.push(Pat::Tok(other.to_string())),
101 }
102 }
103 Ok(out)
104}
105
106impl MacroDef {
107 /// Match an invocation's tokens and substitute them into the body.
108 pub fn expand(&self, input: TokenStream) -> Result<TokenStream, String> {
109 let mut binds = HashMap::new();
110 let toks: Vec<TokenTree> = input.into_iter().collect();
111 let used = match_seq(&self.pattern, &toks, &mut binds)?;
112 if used != toks.len() {
113 return Err("the invocation has tokens the matcher does not consume".into());
114 }
115 Ok(substitute(self.body.clone(), &binds))
116 }
117}
118
119/// Match `pats` against the front of `toks`, returning how many were consumed.
120fn match_seq(
121 pats: &[Pat],
122 toks: &[TokenTree],
123 binds: &mut HashMap<String, TokenStream>,
124) -> Result<usize, String> {
125 let mut i = 0;
126 for (k, p) in pats.iter().enumerate() {
127 match p {
128 Pat::Tok(s) => {
129 let t = toks.get(i).ok_or("the invocation ends before the matcher does")?;
130 if t.to_string() != *s {
131 return Err(format!("expected `{s}`, found `{t}`"));
132 }
133 i += 1;
134 }
135 Pat::Group(d, inner) => {
136 let Some(TokenTree::Group(g)) = toks.get(i) else {
137 return Err("expected a delimited group".into());
138 };
139 if g.delimiter() != *d {
140 return Err("mismatched delimiter".into());
141 }
142 let sub: Vec<TokenTree> = g.stream().into_iter().collect();
143 let n = match_seq(inner, &sub, binds)?;
144 if n != sub.len() {
145 return Err("group has tokens the matcher does not consume".into());
146 }
147 i += 1;
148 }
149 Pat::Frag(name) => {
150 // 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
152 // 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 });
157 let start = i;
158 let mut depth = 0i32;
159 while i < toks.len() {
160 let s = toks[i].to_string();
161 if let Some(stop) = &stop {
162 if depth == 0 && s == *stop {
163 break;
164 }
165 }
166 match &toks[i] {
167 TokenTree::Punct(p) if p.as_char() == '<' => depth += 1,
168 TokenTree::Punct(p) if p.as_char() == '>' => depth -= 1,
169 _ => {}
170 }
171 i += 1;
172 }
173 if i == start {
174 return Err(format!("nothing matched fragment `${name}`"));
175 }
176 binds.insert(
177 name.clone(),
178 toks[start..i].iter().cloned().collect::<TokenStream>(),
179 );
180 }
181 }
182 }
183 Ok(i)
184}
185
186/// Replace every `$name` in the body with what it captured.
187fn substitute(ts: TokenStream, binds: &HashMap<String, TokenStream>) -> TokenStream {
188 let mut out = Vec::new();
189 let mut it = ts.into_iter().peekable();
190 while let Some(t) = it.next() {
191 match t {
192 TokenTree::Punct(p) if p.as_char() == '$' => {
193 match it.peek() {
194 Some(TokenTree::Ident(name)) => {
195 let n = name.to_string();
196 it.next();
197 match binds.get(&n) {
198 // Parenthesised so that a captured expression keeps
199 // its own precedence, as Rust's `expr` fragments do.
200 Some(v) => out.push(TokenTree::Group(Group::new(
201 Delimiter::Parenthesis,
202 v.clone(),
203 ))),
204 None => {
205 out.push(TokenTree::Punct(p));
206 out.push(TokenTree::Ident(Ident::new(
207 &n,
208 proc_macro2::Span::call_site(),
209 )));
210 }
211 }
212 }
213 _ => out.push(TokenTree::Punct(p)),
214 }
215 }
216 TokenTree::Group(g) => {
217 let inner = substitute(g.stream(), binds);
218 out.push(TokenTree::Group(Group::new(g.delimiter(), inner)));
219 }
220 other => out.push(other),
221 }
222 }
223 out.into_iter().collect()
224}