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
10use crate::errors;
11
12pub(crate) const RAW_IDENT_ERR: &str = "`${concat(..)}` currently does not support raw identifiers";
13pub(crate) const UNSUPPORTED_CONCAT_ELEM_ERR: &str = "expected identifier or string literal";
14
15/// A meta-variable expression, for expansions based on properties of meta-variables.
16#[derive(Debug, PartialEq, Encodable, Decodable)]
17pub(crate) enum MetaVarExpr {
18    /// Unification of two or more identifiers.
19    Concat(Box<[MetaVarExprConcatElem]>),
20
21    /// The number of repetitions of an identifier.
22    Count(Ident, usize),
23
24    /// Ignore a meta-variable for repetition without expansion.
25    Ignore(Ident),
26
27    /// The index of the repetition at a particular depth, where 0 is the innermost
28    /// repetition. The `usize` is the depth.
29    Index(usize),
30
31    /// The length of the repetition at a particular depth, where 0 is the innermost
32    /// repetition. The `usize` is the depth.
33    Len(usize),
34}
35
36impl MetaVarExpr {
37    /// Attempt to parse a meta-variable expression from a token stream.
38    pub(crate) fn parse<'psess>(
39        input: &TokenStream,
40        outer_span: Span,
41        psess: &'psess ParseSess,
42    ) -> PResult<'psess, MetaVarExpr> {
43        let mut iter = input.iter();
44        let ident = parse_ident(&mut iter, psess, outer_span)?;
45        let next = iter.next();
46        let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, args)) = next else {
47            // No `()`; wrong or no delimiters. Point at a problematic span or a place to
48            // add parens if it makes sense.
49            let (unexpected_span, insert_span) = match next {
50                Some(TokenTree::Delimited(..)) => (None, None),
51                Some(tt) => (Some(tt.span()), None),
52                None => (None, Some(ident.span.shrink_to_hi())),
53            };
54            let err =
55                errors::MveMissingParen { ident_span: ident.span, unexpected_span, insert_span };
56            return Err(psess.dcx().create_err(err));
57        };
58
59        // Ensure there are no trailing tokens in the braces, e.g. `${foo() extra}`
60        if iter.peek().is_some() {
61            let span = iter_span(&iter).expect("checked is_some above");
62            let err = errors::MveExtraTokens {
63                span,
64                ident_span: ident.span,
65                extra_count: iter.count(),
66                ..Default::default()
67            };
68            return Err(psess.dcx().create_err(err));
69        }
70
71        let mut iter = args.iter();
72        let rslt = match ident.as_str() {
73            "concat" => parse_concat(&mut iter, psess, outer_span, ident.span)?,
74            "count" => parse_count(&mut iter, psess, ident.span)?,
75            "ignore" => {
76                eat_dollar(&mut iter, psess, ident.span)?;
77                MetaVarExpr::Ignore(parse_ident(&mut iter, psess, ident.span)?)
78            }
79            "index" => MetaVarExpr::Index(parse_depth(&mut iter, psess, ident.span)?),
80            "len" => MetaVarExpr::Len(parse_depth(&mut iter, psess, ident.span)?),
81            _ => {
82                let err = errors::MveUnrecognizedExpr {
83                    span: ident.span,
84                    valid_expr_list: "`count`, `ignore`, `index`, `len`, and `concat`",
85                };
86                return Err(psess.dcx().create_err(err));
87            }
88        };
89        check_trailing_tokens(&mut iter, psess, ident)?;
90        Ok(rslt)
91    }
92
93    pub(crate) fn for_each_metavar<A>(&self, mut aux: A, mut cb: impl FnMut(A, &Ident) -> A) -> A {
94        match self {
95            MetaVarExpr::Concat(elems) => {
96                for elem in elems {
97                    if let MetaVarExprConcatElem::Var(ident) = elem {
98                        aux = cb(aux, ident)
99                    }
100                }
101                aux
102            }
103            MetaVarExpr::Count(ident, _) | MetaVarExpr::Ignore(ident) => cb(aux, ident),
104            MetaVarExpr::Index(..) | MetaVarExpr::Len(..) => aux,
105        }
106    }
107}
108
109/// Checks if there are any remaining tokens (for example, `${ignore($valid, extra)}`) and create
110/// a diag with the correct arg count if so.
111fn check_trailing_tokens<'psess>(
112    iter: &mut TokenStreamIter<'_>,
113    psess: &'psess ParseSess,
114    ident: Ident,
115) -> PResult<'psess, ()> {
116    if iter.peek().is_none() {
117        // All tokens consumed, as expected
118        return Ok(());
119    }
120
121    // `None` for max indicates the arg count must be exact, `Some` indicates a range is accepted.
122    let (min_or_exact_args, max_args) = match ident.as_str() {
123        "concat" => panic!("concat takes unlimited tokens but didn't eat them all"),
124        "ignore" => (1, None),
125        // 1 or 2 args
126        "count" => (1, Some(2)),
127        // 0 or 1 arg
128        "index" => (0, Some(1)),
129        "len" => (0, Some(1)),
130        other => unreachable!("unknown MVEs should be rejected earlier (got `{other}`)"),
131    };
132
133    let err = errors::MveExtraTokens {
134        span: iter_span(iter).expect("checked is_none above"),
135        ident_span: ident.span,
136        extra_count: iter.count(),
137
138        exact_args_note: if max_args.is_some() { None } else { Some(()) },
139        range_args_note: if max_args.is_some() { Some(()) } else { None },
140        min_or_exact_args,
141        max_args: max_args.unwrap_or_default(),
142        name: ident.to_string(),
143    };
144    Err(psess.dcx().create_err(err))
145}
146
147/// Returns a span encompassing all tokens in the iterator if there is at least one item.
148fn iter_span(iter: &TokenStreamIter<'_>) -> Option<Span> {
149    let mut iter = iter.clone(); // cloning is cheap
150    let first_sp = iter.next()?.span();
151    let last_sp = iter.last().map(TokenTree::span).unwrap_or(first_sp);
152    let span = first_sp.with_hi(last_sp.hi());
153    Some(span)
154}
155
156/// Indicates what is placed in a `concat` parameter. For example, literals
157/// (`${concat("foo", "bar")}`) or adhoc identifiers (`${concat(foo, bar)}`).
158#[derive(Debug, Decodable, Encodable, PartialEq)]
159pub(crate) enum MetaVarExprConcatElem {
160    /// Identifier WITHOUT a preceding dollar sign, which means that this identifier should be
161    /// interpreted as a literal.
162    Ident(Ident),
163    /// For example, a number or a string.
164    Literal(Symbol),
165    /// Identifier WITH a preceding dollar sign, which means that this identifier should be
166    /// expanded and interpreted as a variable.
167    Var(Ident),
168}
169
170/// Parse a meta-variable `concat` expression: `concat($metavar, ident, ...)`.
171fn parse_concat<'psess>(
172    iter: &mut TokenStreamIter<'_>,
173    psess: &'psess ParseSess,
174    outer_span: Span,
175    expr_ident_span: Span,
176) -> PResult<'psess, MetaVarExpr> {
177    let mut result = Vec::new();
178    loop {
179        let is_var = try_eat_dollar(iter);
180        let token = parse_token(iter, psess, outer_span)?;
181        let element = if is_var {
182            MetaVarExprConcatElem::Var(parse_ident_from_token(psess, token)?)
183        } else if let TokenKind::Literal(Lit { kind: token::LitKind::Str, symbol, suffix: None }) =
184            token.kind
185        {
186            MetaVarExprConcatElem::Literal(symbol)
187        } else {
188            match parse_ident_from_token(psess, token) {
189                Err(err) => {
190                    err.cancel();
191                    return Err(psess
192                        .dcx()
193                        .struct_span_err(token.span, UNSUPPORTED_CONCAT_ELEM_ERR));
194                }
195                Ok(elem) => MetaVarExprConcatElem::Ident(elem),
196            }
197        };
198        result.push(element);
199        if iter.peek().is_none() {
200            break;
201        }
202        if !try_eat_comma(iter) {
203            return Err(psess.dcx().struct_span_err(outer_span, "expected comma"));
204        }
205    }
206    if result.len() < 2 {
207        return Err(psess
208            .dcx()
209            .struct_span_err(expr_ident_span, "`concat` must have at least two elements"));
210    }
211    Ok(MetaVarExpr::Concat(result.into()))
212}
213
214/// Parse a meta-variable `count` expression: `count(ident[, depth])`
215fn parse_count<'psess>(
216    iter: &mut TokenStreamIter<'_>,
217    psess: &'psess ParseSess,
218    span: Span,
219) -> PResult<'psess, MetaVarExpr> {
220    eat_dollar(iter, psess, span)?;
221    let ident = parse_ident(iter, psess, span)?;
222    let depth = if try_eat_comma(iter) {
223        if iter.peek().is_none() {
224            return Err(psess.dcx().struct_span_err(
225                span,
226                "`count` followed by a comma must have an associated index indicating its depth",
227            ));
228        }
229        parse_depth(iter, psess, span)?
230    } else {
231        0
232    };
233    Ok(MetaVarExpr::Count(ident, depth))
234}
235
236/// Parses the depth used by index(depth) and len(depth).
237fn parse_depth<'psess>(
238    iter: &mut TokenStreamIter<'_>,
239    psess: &'psess ParseSess,
240    span: Span,
241) -> PResult<'psess, usize> {
242    let Some(tt) = iter.next() else { return Ok(0) };
243    let TokenTree::Token(Token { kind: TokenKind::Literal(lit), .. }, _) = tt else {
244        return Err(psess
245            .dcx()
246            .struct_span_err(span, "meta-variable expression depth must be a literal"));
247    };
248    if let Ok(lit_kind) = LitKind::from_token_lit(*lit)
249        && let LitKind::Int(n_u128, LitIntType::Unsuffixed) = lit_kind
250        && let Ok(n_usize) = usize::try_from(n_u128.get())
251    {
252        Ok(n_usize)
253    } else {
254        let msg = "only unsuffixes integer literals are supported in meta-variable expressions";
255        Err(psess.dcx().struct_span_err(span, msg))
256    }
257}
258
259/// Parses an generic ident
260fn parse_ident<'psess>(
261    iter: &mut TokenStreamIter<'_>,
262    psess: &'psess ParseSess,
263    fallback_span: Span,
264) -> PResult<'psess, Ident> {
265    let token = parse_token(iter, psess, fallback_span)?;
266    parse_ident_from_token(psess, token)
267}
268
269fn parse_ident_from_token<'psess>(
270    psess: &'psess ParseSess,
271    token: &Token,
272) -> PResult<'psess, Ident> {
273    if let Some((elem, is_raw)) = token.ident() {
274        if let IdentIsRaw::Yes = is_raw {
275            return Err(psess.dcx().struct_span_err(elem.span, RAW_IDENT_ERR));
276        }
277        return Ok(elem);
278    }
279    let token_str = pprust::token_to_string(token);
280    let mut err = psess
281        .dcx()
282        .struct_span_err(token.span, format!("expected identifier, found `{token_str}`"));
283    err.span_suggestion(
284        token.span,
285        format!("try removing `{token_str}`"),
286        "",
287        Applicability::MaybeIncorrect,
288    );
289    Err(err)
290}
291
292fn parse_token<'psess, 't>(
293    iter: &mut TokenStreamIter<'t>,
294    psess: &'psess ParseSess,
295    fallback_span: Span,
296) -> PResult<'psess, &'t Token> {
297    let Some(tt) = iter.next() else {
298        return Err(psess.dcx().struct_span_err(fallback_span, UNSUPPORTED_CONCAT_ELEM_ERR));
299    };
300    let TokenTree::Token(token, _) = tt else {
301        return Err(psess.dcx().struct_span_err(tt.span(), UNSUPPORTED_CONCAT_ELEM_ERR));
302    };
303    Ok(token)
304}
305
306/// Tries to move the iterator forward returning `true` if there is a comma. If not, then the
307/// iterator is not modified and the result is `false`.
308fn try_eat_comma(iter: &mut TokenStreamIter<'_>) -> bool {
309    if let Some(TokenTree::Token(Token { kind: token::Comma, .. }, _)) = iter.peek() {
310        let _ = iter.next();
311        return true;
312    }
313    false
314}
315
316/// Tries to move the iterator forward returning `true` if there is a dollar sign. If not, then the
317/// iterator is not modified and the result is `false`.
318fn try_eat_dollar(iter: &mut TokenStreamIter<'_>) -> bool {
319    if let Some(TokenTree::Token(Token { kind: token::Dollar, .. }, _)) = iter.peek() {
320        let _ = iter.next();
321        return true;
322    }
323    false
324}
325
326/// Expects that the next item is a dollar sign.
327fn eat_dollar<'psess>(
328    iter: &mut TokenStreamIter<'_>,
329    psess: &'psess ParseSess,
330    span: Span,
331) -> PResult<'psess, ()> {
332    if try_eat_dollar(iter) {
333        return Ok(());
334    }
335    Err(psess.dcx().struct_span_err(
336        span,
337        "meta-variables within meta-variable expressions must be referenced using a dollar sign",
338    ))
339}