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