diff --git a/compiler/rustc_expand/src/mbe/metavar_expr.rs b/compiler/rustc_expand/src/mbe/metavar_expr.rs
index 6e919615019fb..4f8b09fdf1a0d 100644
--- a/compiler/rustc_expand/src/mbe/metavar_expr.rs
+++ b/compiler/rustc_expand/src/mbe/metavar_expr.rs
@@ -1,4 +1,4 @@
-use rustc_ast::token::{self, Delimiter};
+use rustc_ast::token::{self, Delimiter, TokenKind};
 use rustc_ast::tokenstream::{RefTokenTreeCursor, TokenStream, TokenTree};
 use rustc_ast::{LitIntType, LitKind};
 use rustc_ast_pretty::pprust;
@@ -10,6 +10,8 @@ use rustc_span::Span;
 /// A meta-variable expression, for expansions based on properties of meta-variables.
 #[derive(Debug, Clone, PartialEq, Encodable, Decodable)]
 pub(crate) enum MetaVarExpr {
+    Concat(Ident, Ident),
+
     /// The number of repetitions of an identifier, optionally limited to a number
     /// of outer-most repetition depths. If the depth limit is `None` then the depth is unlimited.
     Count(Ident, Option<usize>),
@@ -42,6 +44,14 @@ impl MetaVarExpr {
         check_trailing_token(&mut tts, sess)?;
         let mut iter = args.trees();
         let rslt = match ident.as_str() {
+            "concat" => {
+                let lhs = parse_ident_or_literal_as_ident(&mut iter, sess, ident.span)?;
+                if !try_eat_comma(&mut iter) {
+                    return Err(sess.span_diagnostic.struct_span_err(ident.span, "expected comma"));
+                }
+                let rhs = parse_ident_or_literal_as_ident(&mut iter, sess, ident.span)?;
+                MetaVarExpr::Concat(lhs, rhs)
+            }
             "count" => parse_count(&mut iter, sess, ident.span)?,
             "ignore" => MetaVarExpr::Ignore(parse_ident(&mut iter, sess, ident.span)?),
             "index" => MetaVarExpr::Index(parse_depth(&mut iter, sess, ident.span)?),
@@ -65,7 +75,7 @@ impl MetaVarExpr {
     pub(crate) fn ident(&self) -> Option<Ident> {
         match *self {
             MetaVarExpr::Count(ident, _) | MetaVarExpr::Ignore(ident) => Some(ident),
-            MetaVarExpr::Index(..) | MetaVarExpr::Length(..) => None,
+            MetaVarExpr::Concat(..) | MetaVarExpr::Index(..) | MetaVarExpr::Length(..) => None,
         }
     }
 }
@@ -150,6 +160,27 @@ fn parse_ident<'sess>(
     Err(sess.span_diagnostic.struct_span_err(span, "expected identifier"))
 }
 
+fn parse_ident_or_literal_as_ident<'sess>(
+    iter: &mut RefTokenTreeCursor<'_>,
+    sess: &'sess ParseSess,
+    span: Span,
+) -> PResult<'sess, Ident> {
+    if let Some(tt) = iter.look_ahead(0)
+        && let TokenTree::Token(token, _) = tt
+        && let TokenKind::Literal(lit) = token.kind
+    {
+        let ident = Ident::new(lit.symbol, token.span);
+        let _ = iter.next();
+        Ok(ident)
+    }
+    else if let Ok(ident) = parse_ident(iter, sess, span) {
+        Ok(ident)
+    }
+    else {
+        Err(sess.span_diagnostic.struct_span_err(span, "expected identifier or literal"))
+    }
+}
+
 /// Tries to move the iterator forward returning `true` if there is a comma. If not, then the
 /// iterator is not modified and the result is `false`.
 fn try_eat_comma(iter: &mut RefTokenTreeCursor<'_>) -> bool {
diff --git a/compiler/rustc_expand/src/mbe/transcribe.rs b/compiler/rustc_expand/src/mbe/transcribe.rs
index d523d3eacbeb9..7507973d2952c 100644
--- a/compiler/rustc_expand/src/mbe/transcribe.rs
+++ b/compiler/rustc_expand/src/mbe/transcribe.rs
@@ -6,14 +6,14 @@ use crate::errors::{
 use crate::mbe::macro_parser::{MatchedNonterminal, MatchedSeq, MatchedTokenTree, NamedMatch};
 use crate::mbe::{self, MetaVarExpr};
 use rustc_ast::mut_visit::{self, MutVisitor};
-use rustc_ast::token::{self, Delimiter, Token, TokenKind};
+use rustc_ast::token::{self, Delimiter, Nonterminal, Token, TokenKind};
 use rustc_ast::tokenstream::{DelimSpan, Spacing, TokenStream, TokenTree};
 use rustc_data_structures::fx::FxHashMap;
 use rustc_errors::{pluralize, PResult};
 use rustc_errors::{DiagnosticBuilder, ErrorGuaranteed};
 use rustc_span::hygiene::{LocalExpnId, Transparency};
 use rustc_span::symbol::{sym, Ident, MacroRulesNormalizedIdent};
-use rustc_span::Span;
+use rustc_span::{Span, Symbol};
 
 use smallvec::{smallvec, SmallVec};
 use std::mem;
@@ -528,6 +528,26 @@ fn transcribe_metavar_expr<'a>(
         span
     };
     match *expr {
+        MetaVarExpr::Concat(lhs, rhs) => {
+            let string = |ident| {
+                let mrni = MacroRulesNormalizedIdent::new(ident);
+                if let Some(nm) = lookup_cur_matched(mrni, interp, &repeats)
+                    && let MatchedNonterminal(nt) = nm
+                    && let Nonterminal::NtIdent(nt_ident, _) = &**nt
+                {
+                    nt_ident.to_string()
+                } else {
+                    ident.to_string()
+                }
+            };
+            let symbol_span = lhs.span.to(rhs.span);
+            let mut symbol_string = string(lhs);
+            symbol_string.push_str(&string(rhs));
+            result.push(TokenTree::Token(
+                Token::from_ast_ident(Ident::new(Symbol::intern(&symbol_string), symbol_span)),
+                Spacing::Alone,
+            ));
+        }
         MetaVarExpr::Count(original_ident, depth_opt) => {
             let matched = matched_from_ident(cx, original_ident, interp)?;
             let count = count_repetitions(cx, depth_opt, matched, &repeats, sp)?;
diff --git a/tests/ui/macros/rfc-3086-metavar-expr/concat.rs b/tests/ui/macros/rfc-3086-metavar-expr/concat.rs
new file mode 100644
index 0000000000000..ed5ed32335d6b
--- /dev/null
+++ b/tests/ui/macros/rfc-3086-metavar-expr/concat.rs
@@ -0,0 +1,39 @@
+// run-pass
+
+#![allow(dead_code, non_camel_case_types)]
+#![feature(macro_metavar_expr)]
+
+macro_rules! simple_ident {
+    ( $lhs:ident, $rhs:ident ) => { ${concat(lhs, rhs)} };
+}
+
+macro_rules! create_things {
+    ( $lhs:ident ) => {
+        struct ${concat(lhs, _separated_idents_in_a_struct)} {
+            foo: i32,
+            ${concat(lhs, _separated_idents_in_a_field)}: i32,
+        }
+
+        mod ${concat(lhs, _separated_idents_in_a_module)} {
+            pub const FOO: () = ();
+        }
+
+        fn ${concat(lhs, _separated_idents_in_a_fn)}() {}
+    };
+}
+
+create_things!(look_ma);
+
+fn main() {
+    let abcdef = 1;
+    let _another = simple_ident!(abc, def);
+
+    look_ma_separated_idents_in_a_fn();
+
+    let _ = look_ma_separated_idents_in_a_module::FOO;
+
+    let _ = look_ma_separated_idents_in_a_struct {
+        foo: 1,
+        look_ma_separated_idents_in_a_field: 2,
+    };
+}