From 6c0f3aacde12d32f4393e5d577c70ce76e7075eb Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:57:52 +0200 Subject: [PATCH] macro_metavar_expr_concat: support concatenating into string literals --- compiler/rustc_expand/src/mbe/metavar_expr.rs | 20 ++++-- compiler/rustc_expand/src/mbe/quoted.rs | 21 +++++-- compiler/rustc_expand/src/mbe/transcribe.rs | 62 ++++++++++++++----- compiler/rustc_span/src/symbol.rs | 1 + .../feature-gate-macro-metavar-expr-concat.rs | 3 +- ...ture-gate-macro-metavar-expr-concat.stderr | 14 ++++- .../concat-allowed-operations.rs | 51 ++++++++++++++- .../metavar-expressions/concat-str-escape.rs | 20 ++++++ .../metavar-expressions/concat-str-horror.rs | 13 ++++ .../macros/metavar-expressions/concat-str.rs | 20 ++++++ .../metavar-expressions/concat-str.stderr | 35 +++++++++++ 11 files changed, 228 insertions(+), 32 deletions(-) create mode 100644 tests/ui/macros/metavar-expressions/concat-str-escape.rs create mode 100644 tests/ui/macros/metavar-expressions/concat-str-horror.rs create mode 100644 tests/ui/macros/metavar-expressions/concat-str.rs create mode 100644 tests/ui/macros/metavar-expressions/concat-str.stderr diff --git a/compiler/rustc_expand/src/mbe/metavar_expr.rs b/compiler/rustc_expand/src/mbe/metavar_expr.rs index 96b39f0ae6863..9774165c73400 100644 --- a/compiler/rustc_expand/src/mbe/metavar_expr.rs +++ b/compiler/rustc_expand/src/mbe/metavar_expr.rs @@ -15,8 +15,11 @@ pub(crate) const UNSUPPORTED_CONCAT_ELEM_ERR: &str = "expected identifier or str /// A meta-variable expression, for expansions based on properties of meta-variables. #[derive(Debug, PartialEq, Encodable, Decodable)] pub(crate) enum MetaVarExpr { - /// Unification of two or more identifiers. - Concat(Box<[MetaVarExprConcatElem]>), + /// Unification of two or more identifiers/literals/metavariables into an identifier. + ConcatIdent(Box<[MetaVarExprConcatElem]>), + + /// Unification of two or more identifiers/literals/metavariables into a string literal. + ConcatStr(Box<[MetaVarExprConcatElem]>), /// The number of repetitions of an identifier. Count(Ident, usize), @@ -73,7 +76,12 @@ impl MetaVarExpr { let mut iter = args.iter(); let rslt = match ident.name { - sym::concat => parse_concat(&mut iter, psess, outer_span, ident.span)?, + sym::concat => { + MetaVarExpr::ConcatIdent(parse_concat(&mut iter, psess, outer_span, ident.span)?) + } + sym::concat_str => { + MetaVarExpr::ConcatStr(parse_concat(&mut iter, psess, outer_span, ident.span)?) + } sym::count => parse_count(&mut iter, psess, ident.span)?, sym::ignore => { eat_dollar(&mut iter, psess, ident.span)?; @@ -95,7 +103,7 @@ impl MetaVarExpr { pub(crate) fn for_each_metavar(&self, mut aux: A, mut cb: impl FnMut(A, &Ident) -> A) -> A { match self { - MetaVarExpr::Concat(elems) => { + MetaVarExpr::ConcatIdent(elems) | MetaVarExpr::ConcatStr(elems) => { for elem in elems { if let MetaVarExprConcatElem::Var(ident) = elem { aux = cb(aux, ident) @@ -175,7 +183,7 @@ fn parse_concat<'psess>( psess: &'psess ParseSess, outer_span: Span, expr_ident_span: Span, -) -> PResult<'psess, MetaVarExpr> { +) -> PResult<'psess, Box<[MetaVarExprConcatElem]>> { let mut result = Vec::new(); loop { let is_var = try_eat_dollar(iter); @@ -210,7 +218,7 @@ fn parse_concat<'psess>( .dcx() .struct_span_err(expr_ident_span, "`concat` must have at least two elements")); } - Ok(MetaVarExpr::Concat(result.into())) + Ok(result.into()) } /// Parse a meta-variable `count` expression: `count(ident[, depth])` diff --git a/compiler/rustc_expand/src/mbe/quoted.rs b/compiler/rustc_expand/src/mbe/quoted.rs index aed69c9f5d938..ff1607c9e4456 100644 --- a/compiler/rustc_expand/src/mbe/quoted.rs +++ b/compiler/rustc_expand/src/mbe/quoted.rs @@ -7,7 +7,7 @@ use rustc_feature::Features; use rustc_session::Session; use rustc_session::diagnostics::feature_err; use rustc_span::edition::Edition; -use rustc_span::{Ident, Span, kw, sym}; +use rustc_span::{Ident, Span, Symbol, kw, sym}; use crate::diagnostics; use crate::mbe::macro_parser::count_metavar_decls; @@ -204,9 +204,14 @@ fn maybe_emit_macro_metavar_expr_feature(features: &Features, sess: &Session, sp } } -fn maybe_emit_macro_metavar_expr_concat_feature(features: &Features, sess: &Session, span: Span) { +fn maybe_emit_macro_metavar_expr_concat_feature( + features: &Features, + sess: &Session, + span: Span, + what: Symbol, +) { if !features.macro_metavar_expr_concat() { - let msg = "the `concat` meta-variable expression is unstable"; + let msg = format!("the `{what}` meta-variable expression is unstable"); feature_err(sess, sym::macro_metavar_expr_concat, span, msg).emit(); } } @@ -278,11 +283,19 @@ fn parse_tree<'a>( return TokenTree::token(token::Dollar, dollar_span); } Ok(elem) => { - if let MetaVarExpr::Concat(_) = elem { + if matches!(elem, MetaVarExpr::ConcatIdent(_)) { + maybe_emit_macro_metavar_expr_concat_feature( + features, + sess, + delim_span.entire(), + sym::concat, + ); + } else if matches!(elem, MetaVarExpr::ConcatStr(_)) { maybe_emit_macro_metavar_expr_concat_feature( features, sess, delim_span.entire(), + sym::concat_str, ); } else { maybe_emit_macro_metavar_expr_feature( diff --git a/compiler/rustc_expand/src/mbe/transcribe.rs b/compiler/rustc_expand/src/mbe/transcribe.rs index c258d9e471079..8d036972ad592 100644 --- a/compiler/rustc_expand/src/mbe/transcribe.rs +++ b/compiler/rustc_expand/src/mbe/transcribe.rs @@ -597,7 +597,8 @@ fn transcribe_metavar_expr<'tx>( ) -> PResult<'tx, ()> { let dcx = tscx.psess.dcx(); let tt = match *expr { - MetaVarExpr::Concat(ref elements) => metavar_expr_concat(tscx, dspan, elements)?, + MetaVarExpr::ConcatIdent(ref elements) => metavar_expr_concat_ident(tscx, dspan, elements)?, + MetaVarExpr::ConcatStr(ref elements) => metavar_expr_concat_str(tscx, dspan, elements)?, MetaVarExpr::Count(original_ident, depth) => { let matched = matched_from_ident(dcx, original_ident, tscx.interp)?; let count = count_repetitions(dcx, depth, matched, &tscx.repeats, &dspan)?; @@ -635,11 +636,52 @@ fn transcribe_metavar_expr<'tx>( } /// Handle the `${concat(...)}` metavariable expression. -fn metavar_expr_concat<'tx>( +fn metavar_expr_concat_ident<'tx>( + tscx: &mut TranscrCtx<'tx, '_>, + dspan: DelimSpan, + elements: &[MetaVarExprConcatElem], +) -> PResult<'tx, TokenTree> { + let (symbol, concatenated_span) = metavar_expr_concat(tscx, dspan, elements)?; + if !rustc_lexer::is_ident(symbol.as_str()) { + return Err(tscx.psess.dcx().create_err(ConcatInvalidIdent { + span: concatenated_span, + reason: InvalidIdentReason::new(symbol), + })); + } + tscx.psess.symbol_gallery.insert(symbol, concatenated_span); + + // The current implementation marks the span as coming from the macro regardless of + // contexts of the concatenated identifiers but this behavior may change in the + // future. + Ok(TokenTree::Token( + Token::from_ast_ident(Ident::new(symbol, concatenated_span)), + Spacing::Alone, + )) +} + +/// Handle the `${concat_str(...)}` metavariable expression. +fn metavar_expr_concat_str<'tx>( tscx: &mut TranscrCtx<'tx, '_>, dspan: DelimSpan, elements: &[MetaVarExprConcatElem], ) -> PResult<'tx, TokenTree> { + let (symbol, concatenated_span) = metavar_expr_concat(tscx, dspan, elements)?; + + // The current implementation marks the span as coming from the macro regardless of + // contexts of the concatenated identifiers but this behavior may change in the + // future. + Ok(TokenTree::Token( + Token::new(TokenKind::lit(LitKind::Str, symbol, None), concatenated_span), + Spacing::Alone, + )) +} + +/// Shared logic for concat/concat_str metavariable expressions +fn metavar_expr_concat<'tx>( + tscx: &mut TranscrCtx<'tx, '_>, + dspan: DelimSpan, + elements: &[MetaVarExprConcatElem], +) -> PResult<'tx, (Symbol, Span)> { let dcx = tscx.psess.dcx(); let mut concatenated = String::new(); for element in elements { @@ -668,21 +710,7 @@ fn metavar_expr_concat<'tx>( } let symbol = nfc_normalize(&concatenated); let concatenated_span = tscx.visited_dspan(dspan); - if !rustc_lexer::is_ident(symbol.as_str()) { - return Err(dcx.create_err(ConcatInvalidIdent { - span: concatenated_span, - reason: InvalidIdentReason::new(symbol), - })); - } - tscx.psess.symbol_gallery.insert(symbol, concatenated_span); - - // The current implementation marks the span as coming from the macro regardless of - // contexts of the concatenated identifiers but this behavior may change in the - // future. - Ok(TokenTree::Token( - Token::from_ast_ident(Ident::new(symbol, concatenated_span)), - Spacing::Alone, - )) + Ok((symbol, concatenated_span)) } /// Store the metavariable span for this original span into a side table. diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 098b6595f166e..bd70e5acab4d7 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -680,6 +680,7 @@ symbols! { complex, concat, concat_bytes, + concat_str, conservative_impl_trait, console, const_allocate, diff --git a/tests/ui/feature-gates/feature-gate-macro-metavar-expr-concat.rs b/tests/ui/feature-gates/feature-gate-macro-metavar-expr-concat.rs index e700999ae4bd8..898bc28974c88 100644 --- a/tests/ui/feature-gates/feature-gate-macro-metavar-expr-concat.rs +++ b/tests/ui/feature-gates/feature-gate-macro-metavar-expr-concat.rs @@ -1,7 +1,8 @@ macro_rules! join { ($lhs:ident, $rhs:ident) => { - let ${concat($lhs, $rhs)}: () = (); + let ${concat($lhs, $rhs)}: &'static str = ${concat_str($lhs, $rhs)}; //~^ ERROR the `concat` meta-variable expression is unstable + //~| ERROR the `concat_str` meta-variable expression is unstable }; } diff --git a/tests/ui/feature-gates/feature-gate-macro-metavar-expr-concat.stderr b/tests/ui/feature-gates/feature-gate-macro-metavar-expr-concat.stderr index 5b2589d8c89be..47c09a119298d 100644 --- a/tests/ui/feature-gates/feature-gate-macro-metavar-expr-concat.stderr +++ b/tests/ui/feature-gates/feature-gate-macro-metavar-expr-concat.stderr @@ -1,13 +1,23 @@ error[E0658]: the `concat` meta-variable expression is unstable --> $DIR/feature-gate-macro-metavar-expr-concat.rs:3:14 | -LL | let ${concat($lhs, $rhs)}: () = (); +LL | let ${concat($lhs, $rhs)}: &'static str = ${concat_str($lhs, $rhs)}; | ^^^^^^^^^^^^^^^^^^^^ | = note: see issue #124225 for more information = help: add `#![feature(macro_metavar_expr_concat)]` to the crate attributes to enable = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date -error: aborting due to 1 previous error +error[E0658]: the `concat_str` meta-variable expression is unstable + --> $DIR/feature-gate-macro-metavar-expr-concat.rs:3:52 + | +LL | let ${concat($lhs, $rhs)}: &'static str = ${concat_str($lhs, $rhs)}; + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: see issue #124225 for more information + = help: add `#![feature(macro_metavar_expr_concat)]` to the crate attributes to enable + = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date + +error: aborting due to 2 previous errors For more information about this error, try `rustc --explain E0658`. diff --git a/tests/ui/macros/metavar-expressions/concat-allowed-operations.rs b/tests/ui/macros/metavar-expressions/concat-allowed-operations.rs index 5ac50c943d030..cd0c23da1dd4f 100644 --- a/tests/ui/macros/metavar-expressions/concat-allowed-operations.rs +++ b/tests/ui/macros/metavar-expressions/concat-allowed-operations.rs @@ -5,7 +5,7 @@ macro_rules! create_things { ($lhs:ident) => { - struct ${concat($lhs, _separated_idents_in_a_struct)} { + struct ${concat($lhs, _separated_idents_in_a_struct)} { foo: i32, ${concat($lhs, _separated_idents_in_a_field)}: i32, } @@ -20,19 +20,23 @@ macro_rules! create_things { macro_rules! many_idents { ($a:ident, $c:ident) => { + #[allow(dead_code, reason = ${concat_str($a, B, $c, D)})] const ${concat($a, B, $c, D)}: i32 = 1; }; } macro_rules! valid_tts { ($_0:tt, $_1:tt) => { + #[allow(dead_code, reason = ${concat_str($_0, $_1)})] const ${concat($_0, $_1)}: i32 = 1; } } macro_rules! without_dollar_sign_is_an_ident { ($ident:ident) => { + #[allow(dead_code, reason = ${concat_str(VAR, ident)})] const ${concat(VAR, ident)}: i32 = 1; + #[allow(dead_code, reason = ${concat_str(VAR, $ident)})] const ${concat(VAR, $ident)}: i32 = 2; }; } @@ -40,60 +44,103 @@ macro_rules! without_dollar_sign_is_an_ident { macro_rules! combinations { ($ident:ident, $literal:literal, $tt_ident:tt, $tt_literal:tt) => {{ // tt ident + #[allow(dead_code, reason = ${concat_str($tt_ident, b)})] let ${concat($tt_ident, b)} = (); + #[allow(dead_code, reason = ${concat_str($tt_ident, _b)})] let ${concat($tt_ident, _b)} = (); + #[allow(dead_code, reason = ${concat_str($tt_ident, "b")})] let ${concat($tt_ident, "b")} = (); + #[allow(dead_code, reason = ${concat_str($tt_ident, $tt_ident)})] let ${concat($tt_ident, $tt_ident)} = (); + #[allow(dead_code, reason = ${concat_str($tt_ident, $tt_literal)})] let ${concat($tt_ident, $tt_literal)} = (); + #[allow(dead_code, reason = ${concat_str($tt_ident, $ident)})] + let ${concat($tt_ident, $ident)} = (); + #[allow(dead_code, reason = ${concat_str($tt_ident, $ident)})] let ${concat($tt_ident, $ident)} = (); - let ${concat($tt_ident, $literal)} = (); // tt literal + #[allow(dead_code, reason = ${concat_str($tt_literal, b)})] let ${concat($tt_literal, b)} = (); + #[allow(dead_code, reason = ${concat_str($tt_literal, _b)})] let ${concat($tt_literal, _b)} = (); + #[allow(dead_code, reason = ${concat_str($tt_literal, "b")})] let ${concat($tt_literal, "b")} = (); + #[allow(dead_code, reason = ${concat_str($tt_literal, $tt_ident)})] let ${concat($tt_literal, $tt_ident)} = (); + #[allow(dead_code, reason = ${concat_str($tt_literal, $tt_literal)})] let ${concat($tt_literal, $tt_literal)} = (); + #[allow(dead_code, reason = ${concat_str($tt_literal, $ident)})] let ${concat($tt_literal, $ident)} = (); + #[allow(dead_code, reason = ${concat_str($tt_literal, $literal)})] let ${concat($tt_literal, $literal)} = (); // ident (adhoc) + #[allow(dead_code, reason = ${concat_str(_b, b)})] let ${concat(_b, b)} = (); + #[allow(dead_code, reason = ${concat_str(_b, _b)})] let ${concat(_b, _b)} = (); + #[allow(dead_code, reason = ${concat_str(_b, "b")})] let ${concat(_b, "b")} = (); + #[allow(dead_code, reason = ${concat_str(_b, $tt_ident)})] let ${concat(_b, $tt_ident)} = (); + #[allow(dead_code, reason = ${concat_str(_b, $tt_literal)})] let ${concat(_b, $tt_literal)} = (); + #[allow(dead_code, reason = ${concat_str(_b, $ident)})] let ${concat(_b, $ident)} = (); + #[allow(dead_code, reason = ${concat_str(_b, $literal)})] let ${concat(_b, $literal)} = (); // ident (param) + #[allow(dead_code, reason = ${concat_str($ident, b)})] let ${concat($ident, b)} = (); + #[allow(dead_code, reason = ${concat_str($ident, _b)})] let ${concat($ident, _b)} = (); + #[allow(dead_code, reason = ${concat_str($ident, "b")})] let ${concat($ident, "b")} = (); + #[allow(dead_code, reason = ${concat_str($ident, $tt_ident)})] let ${concat($ident, $tt_ident)} = (); + #[allow(dead_code, reason = ${concat_str($ident, $tt_literal)})] let ${concat($ident, $tt_literal)} = (); + #[allow(dead_code, reason = ${concat_str($ident, $ident)})] let ${concat($ident, $ident)} = (); + #[allow(dead_code, reason = ${concat_str($ident, $literal)})] let ${concat($ident, $literal)} = (); // literal (adhoc) + #[allow(dead_code, reason = ${concat_str("a", b)})] let ${concat("a", b)} = (); + #[allow(dead_code, reason = ${concat_str("a", _b)})] let ${concat("a", _b)} = (); + #[allow(dead_code, reason = ${concat_str("a", "b")})] let ${concat("a", "b")} = (); + #[allow(dead_code, reason = ${concat_str("a", $tt_ident)})] let ${concat("a", $tt_ident)} = (); + #[allow(dead_code, reason = ${concat_str("a", $tt_literal)})] let ${concat("a", $tt_literal)} = (); + #[allow(dead_code, reason = ${concat_str("a", $ident)})] let ${concat("a", $ident)} = (); + #[allow(dead_code, reason = ${concat_str("a", $literal)})] let ${concat("a", $literal)} = (); // literal (param) + #[allow(dead_code, reason = ${concat_str($literal, b)})] let ${concat($literal, b)} = (); + #[allow(dead_code, reason = ${concat_str($literal, _b)})] let ${concat($literal, _b)} = (); + #[allow(dead_code, reason = ${concat_str($literal, "b")})] let ${concat($literal, "b")} = (); + #[allow(dead_code, reason = ${concat_str($literal, $tt_ident)})] let ${concat($literal, $tt_ident)} = (); + #[allow(dead_code, reason = ${concat_str($literal, $tt_literal)})] let ${concat($literal, $tt_literal)} = (); + #[allow(dead_code, reason = ${concat_str($literal, $ident)})] let ${concat($literal, $ident)} = (); + #[allow(dead_code, reason = ${concat_str($literal, $literal)})] let ${concat($literal, $literal)} = (); }}; } macro_rules! int_struct { ($n: literal) => { + #[allow(dead_code, reason = ${concat_str(E, $n)})] struct ${concat(E, $n)}; } } diff --git a/tests/ui/macros/metavar-expressions/concat-str-escape.rs b/tests/ui/macros/metavar-expressions/concat-str-escape.rs new file mode 100644 index 0000000000000..612c957b831eb --- /dev/null +++ b/tests/ui/macros/metavar-expressions/concat-str-escape.rs @@ -0,0 +1,20 @@ +//@ run-pass + +#![feature(macro_metavar_expr_concat)] + +macro_rules! escape_literal { + ($literal:literal, $concatted:literal) => { + let lit = ${concat_str("_", "\"foo", $literal)}; + assert_eq!(lit, concat!("_", "\"foo", $literal)); + assert_eq!(lit, $concatted); + + } +} + +fn main(){ + escape_literal!("\u{00BD}", "_\"foo\u{00BD}"); + escape_literal!("\x41", "_\"foo\x41"); + escape_literal!("🤷", "_\"foo🤷"); + escape_literal!("\u{1F980}", "_\"foo🦀"); + escape_literal!("aaa \"bbb\" ccc", "_\"fooaaa \"bbb\" ccc"); +} diff --git a/tests/ui/macros/metavar-expressions/concat-str-horror.rs b/tests/ui/macros/metavar-expressions/concat-str-horror.rs new file mode 100644 index 0000000000000..558e55539e870 --- /dev/null +++ b/tests/ui/macros/metavar-expressions/concat-str-horror.rs @@ -0,0 +1,13 @@ +//@ run-pass +#![feature(macro_metavar_expr_concat)] + +macro_rules! format_horror{ + ($arg1:ident, $arg2:expr, $arg3:expr) => { + format!(${concat_str("horror ", $arg1, " horror {} horror {}")}, $arg2, $arg3) + } +} + +fn main(){ + let x = format_horror!(a, "b", "c"); + assert_eq!(x, "horror a horror b horror c"); +} diff --git a/tests/ui/macros/metavar-expressions/concat-str.rs b/tests/ui/macros/metavar-expressions/concat-str.rs new file mode 100644 index 0000000000000..f04e620d6826e --- /dev/null +++ b/tests/ui/macros/metavar-expressions/concat-str.rs @@ -0,0 +1,20 @@ +#![feature(macro_metavar_expr_concat)] +#![crate_type = "lib"] + +macro_rules! make_stuff { + ($prefix:literal, $name:ident) => { + #[deprecated(since = "1.0.0", note = ${concat_str($prefix, " ", $name, " trait")})] + #[diagnostic::on_unimplemented(message = ${concat_str("please do not the ", $name)})] + pub trait ${concat(New, $name)} {} + } +} + +make_stuff!("blah blah blah", AAAAAA); + +pub fn foo(x: impl NewAAAAAA) {} +//~^ WARN use of deprecated trait `NewAAAAAA`: blah blah blah AAAAAA trait [deprecated] + +fn bar(){ + foo(()); + //~^ ERROR please do not the AAAAAA [E0277] +} diff --git a/tests/ui/macros/metavar-expressions/concat-str.stderr b/tests/ui/macros/metavar-expressions/concat-str.stderr new file mode 100644 index 0000000000000..2e7e8cda4af06 --- /dev/null +++ b/tests/ui/macros/metavar-expressions/concat-str.stderr @@ -0,0 +1,35 @@ +warning: use of deprecated trait `NewAAAAAA`: blah blah blah AAAAAA trait + --> $DIR/concat-str.rs:14:20 + | +LL | pub fn foo(x: impl NewAAAAAA) {} + | ^^^^^^^^^ + | + = note: `#[warn(deprecated)]` on by default + +error[E0277]: please do not the AAAAAA + --> $DIR/concat-str.rs:18:9 + | +LL | macro_rules! make_stuff { +... +LL | foo(()); + | --- ^^ the trait `NewAAAAAA` is not implemented for `()` + | | + | required by a bound introduced by this call + | +help: this trait has no implementations, consider adding one + --> $DIR/concat-str.rs:8:9 + | +LL | pub trait ${concat(New, $name)} {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +... +LL | make_stuff!("blah blah blah", AAAAAA); + | ------------------------------------- in this macro invocation +note: required by a bound in `foo` + --> $DIR/concat-str.rs:14:20 + | +LL | pub fn foo(x: impl NewAAAAAA) {} + | ^^^^^^^^^ required by this bound in `foo` + +error: aborting due to 1 previous error; 1 warning emitted + +For more information about this error, try `rustc --explain E0277`.