rustc_expand/mbe/
metavar_expr.rs

1use rustc_ast::token::{self, Delimiter, IdentIsRaw, Lit, Token, TokenKind};
2use rustc_ast::tokenstream::{TokenStream, TokenStreamIter, TokenTree};
3use rustc_ast::{LitIntType, LitKind};
4use rustc_ast_pretty::pprust;
5use rustc_errors::{Applicability, PResult};
6use rustc_macros::{Decodable, Encodable};
7use rustc_session::parse::ParseSess;
8use rustc_span::{Ident, Span, Symbol};
9
10pub(crate) const RAW_IDENT_ERR: &str = "`${concat(..)}` currently does not support raw identifiers";
11pub(crate) const UNSUPPORTED_CONCAT_ELEM_ERR: &str = "expected identifier or string literal";
12
13/// A meta-variable expression, for expansions based on properties of meta-variables.
14#[derive(Debug, PartialEq, Encodable, Decodable)]
15pub(crate) enum MetaVarExpr {
16    /// Unification of two or more identifiers.
17    Concat(Box<[MetaVarExprConcatElem]>),
18
19    /// The number of repetitions of an identifier.
20    Count(Ident, usize),
21
22    /// Ignore a meta-variable for repetition without expansion.
23    Ignore(Ident),
24
25    /// The index of the repetition at a particular depth, where 0 is the innermost
26    /// repetition. The `usize` is the depth.
27    Index(usize),
28
29    /// The length of the repetition at a particular depth, where 0 is the innermost
30    /// repetition. The `usize` is the depth.
31    Len(usize),
32}
33
34impl MetaVarExpr {
35    /// Attempt to parse a meta-variable expression from a token stream.
36    pub(crate) fn parse<'psess>(
37        input: &TokenStream,
38        outer_span: Span,
39        psess: &'psess ParseSess,
40    ) -> PResult<'psess, MetaVarExpr> {
41        let mut iter = input.iter();
42        let ident = parse_ident(&mut iter, psess, outer_span)?;
43        let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, args)) = iter.next() else {
44            let msg = "meta-variable expression parameter must be wrapped in parentheses";
45            return Err(psess.dcx().struct_span_err(ident.span, msg));
46        };
47        check_trailing_token(&mut iter, psess)?;
48        let mut iter = args.iter();
49        let rslt = match ident.as_str() {
50            "concat" => {
51                let mut result = Vec::new();
52                loop {
53                    let is_var = try_eat_dollar(&mut iter);
54                    let token = parse_token(&mut iter, psess, outer_span)?;
55                    let element = if is_var {
56                        MetaVarExprConcatElem::Var(parse_ident_from_token(psess, token)?)
57                    } else if let TokenKind::Literal(Lit {
58                        kind: token::LitKind::Str,
59                        symbol,
60                        suffix: None,
61                    }) = token.kind
62                    {
63                        MetaVarExprConcatElem::Literal(symbol)
64                    } else {
65                        match parse_ident_from_token(psess, token) {
66                            Err(err) => {
67                                err.cancel();
68                                return Err(psess
69                                    .dcx()
70                                    .struct_span_err(token.span, UNSUPPORTED_CONCAT_ELEM_ERR));
71                            }
72                            Ok(elem) => MetaVarExprConcatElem::Ident(elem),
73                        }
74                    };
75                    result.push(element);
76                    if iter.peek().is_none() {
77                        break;
78                    }
79                    if !try_eat_comma(&mut iter) {
80                        return Err(psess.dcx().struct_span_err(outer_span, "expected comma"));
81                    }
82                }
83                if result.len() < 2 {
84                    return Err(psess
85                        .dcx()
86                        .struct_span_err(ident.span, "`concat` must have at least two elements"));
87                }
88                MetaVarExpr::Concat(result.into())
89            }
90            "count" => parse_count(&mut iter, psess, ident.span)?,
91            "ignore" => {
92                eat_dollar(&mut iter, psess, ident.span)?;
93                MetaVarExpr::Ignore(parse_ident(&mut iter, psess, ident.span)?)
94            }
95            "index" => MetaVarExpr::Index(parse_depth(&mut iter, psess, ident.span)?),
96            "len" => MetaVarExpr::Len(parse_depth(&mut iter, psess, ident.span)?),
97            _ => {
98                let err_msg = "unrecognized meta-variable expression";
99                let mut err = psess.dcx().struct_span_err(ident.span, err_msg);
100                err.span_suggestion(
101                    ident.span,
102                    "supported expressions are count, ignore, index and len",
103                    "",
104                    Applicability::MachineApplicable,
105                );
106                return Err(err);
107            }
108        };
109        check_trailing_token(&mut iter, psess)?;
110        Ok(rslt)
111    }
112
113    pub(crate) fn for_each_metavar<A>(&self, mut aux: A, mut cb: impl FnMut(A, &Ident) -> A) -> A {
114        match self {
115            MetaVarExpr::Concat(elems) => {
116                for elem in elems {
117                    if let MetaVarExprConcatElem::Var(ident) = elem {
118                        aux = cb(aux, ident)
119                    }
120                }
121                aux
122            }
123            MetaVarExpr::Count(ident, _) | MetaVarExpr::Ignore(ident) => cb(aux, ident),
124            MetaVarExpr::Index(..) | MetaVarExpr::Len(..) => aux,
125        }
126    }
127}
128
129/// Indicates what is placed in a `concat` parameter. For example, literals
130/// (`${concat("foo", "bar")}`) or adhoc identifiers (`${concat(foo, bar)}`).
131#[derive(Debug, Decodable, Encodable, PartialEq)]
132pub(crate) enum MetaVarExprConcatElem {
133    /// Identifier WITHOUT a preceding dollar sign, which means that this identifier should be
134    /// interpreted as a literal.
135    Ident(Ident),
136    /// For example, a number or a string.
137    Literal(Symbol),
138    /// Identifier WITH a preceding dollar sign, which means that this identifier should be
139    /// expanded and interpreted as a variable.
140    Var(Ident),
141}
142
143// Checks if there are any remaining tokens. For example, `${ignore(ident ... a b c ...)}`
144fn check_trailing_token<'psess>(
145    iter: &mut TokenStreamIter<'_>,
146    psess: &'psess ParseSess,
147) -> PResult<'psess, ()> {
148    if let Some(tt) = iter.next() {
149        let mut diag = psess
150            .dcx()
151            .struct_span_err(tt.span(), format!("unexpected token: {}", pprust::tt_to_string(tt)));
152        diag.span_note(tt.span(), "meta-variable expression must not have trailing tokens");
153        Err(diag)
154    } else {
155        Ok(())
156    }
157}
158
159/// Parse a meta-variable `count` expression: `count(ident[, depth])`
160fn parse_count<'psess>(
161    iter: &mut TokenStreamIter<'_>,
162    psess: &'psess ParseSess,
163    span: Span,
164) -> PResult<'psess, MetaVarExpr> {
165    eat_dollar(iter, psess, span)?;
166    let ident = parse_ident(iter, psess, span)?;
167    let depth = if try_eat_comma(iter) {
168        if iter.peek().is_none() {
169            return Err(psess.dcx().struct_span_err(
170                span,
171                "`count` followed by a comma must have an associated index indicating its depth",
172            ));
173        }
174        parse_depth(iter, psess, span)?
175    } else {
176        0
177    };
178    Ok(MetaVarExpr::Count(ident, depth))
179}
180
181/// Parses the depth used by index(depth) and len(depth).
182fn parse_depth<'psess>(
183    iter: &mut TokenStreamIter<'_>,
184    psess: &'psess ParseSess,
185    span: Span,
186) -> PResult<'psess, usize> {
187    let Some(tt) = iter.next() else { return Ok(0) };
188    let TokenTree::Token(Token { kind: TokenKind::Literal(lit), .. }, _) = tt else {
189        return Err(psess
190            .dcx()
191            .struct_span_err(span, "meta-variable expression depth must be a literal"));
192    };
193    if let Ok(lit_kind) = LitKind::from_token_lit(*lit)
194        && let LitKind::Int(n_u128, LitIntType::Unsuffixed) = lit_kind
195        && let Ok(n_usize) = usize::try_from(n_u128.get())
196    {
197        Ok(n_usize)
198    } else {
199        let msg = "only unsuffixes integer literals are supported in meta-variable expressions";
200        Err(psess.dcx().struct_span_err(span, msg))
201    }
202}
203
204/// Parses an generic ident
205fn parse_ident<'psess>(
206    iter: &mut TokenStreamIter<'_>,
207    psess: &'psess ParseSess,
208    fallback_span: Span,
209) -> PResult<'psess, Ident> {
210    let token = parse_token(iter, psess, fallback_span)?;
211    parse_ident_from_token(psess, token)
212}
213
214fn parse_ident_from_token<'psess>(
215    psess: &'psess ParseSess,
216    token: &Token,
217) -> PResult<'psess, Ident> {
218    if let Some((elem, is_raw)) = token.ident() {
219        if let IdentIsRaw::Yes = is_raw {
220            return Err(psess.dcx().struct_span_err(elem.span, RAW_IDENT_ERR));
221        }
222        return Ok(elem);
223    }
224    let token_str = pprust::token_to_string(token);
225    let mut err = psess
226        .dcx()
227        .struct_span_err(token.span, format!("expected identifier, found `{token_str}`"));
228    err.span_suggestion(
229        token.span,
230        format!("try removing `{token_str}`"),
231        "",
232        Applicability::MaybeIncorrect,
233    );
234    Err(err)
235}
236
237fn parse_token<'psess, 't>(
238    iter: &mut TokenStreamIter<'t>,
239    psess: &'psess ParseSess,
240    fallback_span: Span,
241) -> PResult<'psess, &'t Token> {
242    let Some(tt) = iter.next() else {
243        return Err(psess.dcx().struct_span_err(fallback_span, UNSUPPORTED_CONCAT_ELEM_ERR));
244    };
245    let TokenTree::Token(token, _) = tt else {
246        return Err(psess.dcx().struct_span_err(tt.span(), UNSUPPORTED_CONCAT_ELEM_ERR));
247    };
248    Ok(token)
249}
250
251/// Tries to move the iterator forward returning `true` if there is a comma. If not, then the
252/// iterator is not modified and the result is `false`.
253fn try_eat_comma(iter: &mut TokenStreamIter<'_>) -> bool {
254    if let Some(TokenTree::Token(Token { kind: token::Comma, .. }, _)) = iter.peek() {
255        let _ = iter.next();
256        return true;
257    }
258    false
259}
260
261/// Tries to move the iterator forward returning `true` if there is a dollar sign. If not, then the
262/// iterator is not modified and the result is `false`.
263fn try_eat_dollar(iter: &mut TokenStreamIter<'_>) -> bool {
264    if let Some(TokenTree::Token(Token { kind: token::Dollar, .. }, _)) = iter.peek() {
265        let _ = iter.next();
266        return true;
267    }
268    false
269}
270
271/// Expects that the next item is a dollar sign.
272fn eat_dollar<'psess>(
273    iter: &mut TokenStreamIter<'_>,
274    psess: &'psess ParseSess,
275    span: Span,
276) -> PResult<'psess, ()> {
277    if try_eat_dollar(iter) {
278        return Ok(());
279    }
280    Err(psess.dcx().struct_span_err(
281        span,
282        "meta-variables within meta-variable expressions must be referenced using a dollar sign",
283    ))
284}