about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJosh Triplett <josh@joshtriplett.org>2025-07-06 15:15:01 -0700
committerJosh Triplett <josh@joshtriplett.org>2025-08-08 11:00:54 -0700
commitbad0d45b2dc2b2be36e9e82604a6c3dd95dba08a (patch)
tree161e336b95a92df8aabd2da30d516c7a0527f5e5
parent2054a0c56b063c195d316e4ff44d0c9f8ad2c012 (diff)
downloadrust-bad0d45b2dc2b2be36e9e82604a6c3dd95dba08a.tar.gz
rust-bad0d45b2dc2b2be36e9e82604a6c3dd95dba08a.zip
mbe: Parse macro attribute rules
This handles various kinds of errors, but does not allow applying the
attributes yet.

This adds the feature gate `macro_attr`.
-rw-r--r--compiler/rustc_expand/messages.ftl3
-rw-r--r--compiler/rustc_expand/src/errors.rs18
-rw-r--r--compiler/rustc_expand/src/mbe/diagnostics.rs3
-rw-r--r--compiler/rustc_expand/src/mbe/macro_check.rs6
-rw-r--r--compiler/rustc_expand/src/mbe/macro_rules.rs96
-rw-r--r--compiler/rustc_feature/src/unstable.rs2
-rw-r--r--compiler/rustc_span/src/symbol.rs1
-rw-r--r--tests/ui/feature-gates/feature-gate-macro-attr.rs4
-rw-r--r--tests/ui/feature-gates/feature-gate-macro-attr.stderr13
-rw-r--r--tests/ui/parser/macro/macro-attr-bad.rs32
-rw-r--r--tests/ui/parser/macro/macro-attr-bad.stderr80
11 files changed, 238 insertions, 20 deletions
diff --git a/compiler/rustc_expand/messages.ftl b/compiler/rustc_expand/messages.ftl
index 5a53670c865..1f8f3be6809 100644
--- a/compiler/rustc_expand/messages.ftl
+++ b/compiler/rustc_expand/messages.ftl
@@ -70,6 +70,9 @@ expand_invalid_fragment_specifier =
     invalid fragment specifier `{$fragment}`
     .help = {$help}
 
+expand_macro_args_bad_delim = macro attribute argument matchers require parentheses
+expand_macro_args_bad_delim_sugg = the delimiters should be `(` and `)`
+
 expand_macro_body_stability =
     macros cannot have body stability attributes
     .label = invalid body stability attribute
diff --git a/compiler/rustc_expand/src/errors.rs b/compiler/rustc_expand/src/errors.rs
index fd1391a554a..e58269991fc 100644
--- a/compiler/rustc_expand/src/errors.rs
+++ b/compiler/rustc_expand/src/errors.rs
@@ -482,3 +482,21 @@ mod metavar_exprs {
         pub key: MacroRulesNormalizedIdent,
     }
 }
+
+#[derive(Diagnostic)]
+#[diag(expand_macro_args_bad_delim)]
+pub(crate) struct MacroArgsBadDelim {
+    #[primary_span]
+    pub span: Span,
+    #[subdiagnostic]
+    pub sugg: MacroArgsBadDelimSugg,
+}
+
+#[derive(Subdiagnostic)]
+#[multipart_suggestion(expand_macro_args_bad_delim_sugg, applicability = "machine-applicable")]
+pub(crate) struct MacroArgsBadDelimSugg {
+    #[suggestion_part(code = "(")]
+    pub open: Span,
+    #[suggestion_part(code = ")")]
+    pub close: Span,
+}
diff --git a/compiler/rustc_expand/src/mbe/diagnostics.rs b/compiler/rustc_expand/src/mbe/diagnostics.rs
index 7a280d671f4..468488ebf7d 100644
--- a/compiler/rustc_expand/src/mbe/diagnostics.rs
+++ b/compiler/rustc_expand/src/mbe/diagnostics.rs
@@ -81,11 +81,12 @@ pub(super) fn failed_to_match_macro(
     // Check whether there's a missing comma in this macro call, like `println!("{}" a);`
     if let Some((arg, comma_span)) = arg.add_comma() {
         for rule in rules {
+            let MacroRule::Func { lhs, .. } = rule else { continue };
             let parser = parser_from_cx(psess, arg.clone(), Recovery::Allowed);
             let mut tt_parser = TtParser::new(name);
 
             if let Success(_) =
-                tt_parser.parse_tt(&mut Cow::Borrowed(&parser), &rule.lhs, &mut NoopTracker)
+                tt_parser.parse_tt(&mut Cow::Borrowed(&parser), lhs, &mut NoopTracker)
             {
                 if comma_span.is_dummy() {
                     err.note("you might be missing a comma");
diff --git a/compiler/rustc_expand/src/mbe/macro_check.rs b/compiler/rustc_expand/src/mbe/macro_check.rs
index bbdff866feb..25987a50366 100644
--- a/compiler/rustc_expand/src/mbe/macro_check.rs
+++ b/compiler/rustc_expand/src/mbe/macro_check.rs
@@ -193,15 +193,19 @@ struct MacroState<'a> {
 /// Arguments:
 /// - `psess` is used to emit diagnostics and lints
 /// - `node_id` is used to emit lints
-/// - `lhs` and `rhs` represent the rule
+/// - `args`, `lhs`, and `rhs` represent the rule
 pub(super) fn check_meta_variables(
     psess: &ParseSess,
     node_id: NodeId,
+    args: Option<&TokenTree>,
     lhs: &TokenTree,
     rhs: &TokenTree,
 ) -> Result<(), ErrorGuaranteed> {
     let mut guar = None;
     let mut binders = Binders::default();
+    if let Some(args) = args {
+        check_binders(psess, node_id, args, &Stack::Empty, &mut binders, &Stack::Empty, &mut guar);
+    }
     check_binders(psess, node_id, lhs, &Stack::Empty, &mut binders, &Stack::Empty, &mut guar);
     check_occurrences(psess, node_id, rhs, &Stack::Empty, &binders, &Stack::Empty, &mut guar);
     guar.map_or(Ok(()), Err)
diff --git a/compiler/rustc_expand/src/mbe/macro_rules.rs b/compiler/rustc_expand/src/mbe/macro_rules.rs
index 52d38c35f98..d72f01a4dbd 100644
--- a/compiler/rustc_expand/src/mbe/macro_rules.rs
+++ b/compiler/rustc_expand/src/mbe/macro_rules.rs
@@ -6,12 +6,12 @@ use std::{mem, slice};
 use ast::token::IdentIsRaw;
 use rustc_ast::token::NtPatKind::*;
 use rustc_ast::token::TokenKind::*;
-use rustc_ast::token::{self, NonterminalKind, Token, TokenKind};
-use rustc_ast::tokenstream::{DelimSpan, TokenStream};
+use rustc_ast::token::{self, Delimiter, NonterminalKind, Token, TokenKind};
+use rustc_ast::tokenstream::{self, DelimSpan, TokenStream};
 use rustc_ast::{self as ast, DUMMY_NODE_ID, NodeId};
 use rustc_ast_pretty::pprust;
 use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
-use rustc_errors::{Applicability, Diag, ErrorGuaranteed};
+use rustc_errors::{Applicability, Diag, ErrorGuaranteed, MultiSpan};
 use rustc_feature::Features;
 use rustc_hir as hir;
 use rustc_hir::attrs::AttributeKind;
@@ -23,7 +23,7 @@ use rustc_lint_defs::builtin::{
 use rustc_parse::exp;
 use rustc_parse::parser::{Parser, Recovery};
 use rustc_session::Session;
-use rustc_session::parse::ParseSess;
+use rustc_session::parse::{ParseSess, feature_err};
 use rustc_span::edition::Edition;
 use rustc_span::hygiene::Transparency;
 use rustc_span::{Ident, Span, kw, sym};
@@ -35,11 +35,13 @@ use crate::base::{
     DummyResult, ExpandResult, ExtCtxt, MacResult, MacroExpanderResult, SyntaxExtension,
     SyntaxExtensionKind, TTMacroExpander,
 };
+use crate::errors;
 use crate::expand::{AstFragment, AstFragmentKind, ensure_complete_parse, parse_ast_fragment};
+use crate::mbe::macro_check::check_meta_variables;
 use crate::mbe::macro_parser::{Error, ErrorReported, Failure, MatcherLoc, Success, TtParser};
 use crate::mbe::quoted::{RulePart, parse_one_tt};
 use crate::mbe::transcribe::transcribe;
-use crate::mbe::{self, KleeneOp, macro_check};
+use crate::mbe::{self, KleeneOp};
 
 pub(crate) struct ParserAnyMacro<'a> {
     parser: Parser<'a>,
@@ -123,10 +125,18 @@ impl<'a> ParserAnyMacro<'a> {
     }
 }
 
-pub(super) struct MacroRule {
-    pub(super) lhs: Vec<MatcherLoc>,
-    lhs_span: Span,
-    rhs: mbe::TokenTree,
+pub(super) enum MacroRule {
+    /// A function-style rule, for use with `m!()`
+    Func { lhs: Vec<MatcherLoc>, lhs_span: Span, rhs: mbe::TokenTree },
+    /// An attr rule, for use with `#[m]`
+    #[expect(unused)]
+    Attr {
+        args: Vec<MatcherLoc>,
+        args_span: Span,
+        body: Vec<MatcherLoc>,
+        body_span: Span,
+        rhs: mbe::TokenTree,
+    },
 }
 
 pub struct MacroRulesMacroExpander {
@@ -138,10 +148,15 @@ pub struct MacroRulesMacroExpander {
 }
 
 impl MacroRulesMacroExpander {
-    pub fn get_unused_rule(&self, rule_i: usize) -> Option<(&Ident, Span)> {
+    pub fn get_unused_rule(&self, rule_i: usize) -> Option<(&Ident, MultiSpan)> {
         // If the rhs contains an invocation like `compile_error!`, don't report it as unused.
-        let rule = &self.rules[rule_i];
-        if has_compile_error_macro(&rule.rhs) { None } else { Some((&self.name, rule.lhs_span)) }
+        let (span, rhs) = match self.rules[rule_i] {
+            MacroRule::Func { lhs_span, ref rhs, .. } => (MultiSpan::from_span(lhs_span), rhs),
+            MacroRule::Attr { args_span, body_span, ref rhs, .. } => {
+                (MultiSpan::from_spans(vec![args_span, body_span]), rhs)
+            }
+        };
+        if has_compile_error_macro(rhs) { None } else { Some((&self.name, span)) }
     }
 }
 
@@ -245,14 +260,17 @@ fn expand_macro<'cx>(
 
     match try_success_result {
         Ok((rule_index, rule, named_matches)) => {
-            let mbe::TokenTree::Delimited(rhs_span, _, ref rhs) = rule.rhs else {
+            let MacroRule::Func { rhs, .. } = rule else {
+                panic!("try_match_macro returned non-func rule");
+            };
+            let mbe::TokenTree::Delimited(rhs_span, _, rhs) = rhs else {
                 cx.dcx().span_bug(sp, "malformed macro rhs");
             };
-            let arm_span = rule.rhs.span();
+            let arm_span = rhs_span.entire();
 
             // rhs has holes ( `$id` and `$(...)` that need filled)
             let id = cx.current_expansion.id;
-            let tts = match transcribe(psess, &named_matches, rhs, rhs_span, transparency, id) {
+            let tts = match transcribe(psess, &named_matches, rhs, *rhs_span, transparency, id) {
                 Ok(tts) => tts,
                 Err(err) => {
                     let guar = err.emit();
@@ -327,6 +345,7 @@ pub(super) fn try_match_macro<'matcher, T: Tracker<'matcher>>(
     // Try each arm's matchers.
     let mut tt_parser = TtParser::new(name);
     for (i, rule) in rules.iter().enumerate() {
+        let MacroRule::Func { lhs, .. } = rule else { continue };
         let _tracing_span = trace_span!("Matching arm", %i);
 
         // Take a snapshot of the state of pre-expansion gating at this point.
@@ -335,7 +354,7 @@ pub(super) fn try_match_macro<'matcher, T: Tracker<'matcher>>(
         // are not recorded. On the first `Success(..)`ful matcher, the spans are merged.
         let mut gated_spans_snapshot = mem::take(&mut *psess.gated_spans.spans.borrow_mut());
 
-        let result = tt_parser.parse_tt(&mut Cow::Borrowed(&parser), &rule.lhs, track);
+        let result = tt_parser.parse_tt(&mut Cow::Borrowed(&parser), lhs, track);
 
         track.after_arm(&result);
 
@@ -404,6 +423,25 @@ pub fn compile_declarative_macro(
     let mut rules = Vec::new();
 
     while p.token != token::Eof {
+        let args = if p.eat_keyword_noexpect(sym::attr) {
+            if !features.macro_attr() {
+                feature_err(sess, sym::macro_attr, span, "`macro_rules!` attributes are unstable")
+                    .emit();
+            }
+            if let Some(guar) = check_no_eof(sess, &p, "expected macro attr args") {
+                return dummy_syn_ext(guar);
+            }
+            let args = p.parse_token_tree();
+            check_args_parens(sess, &args);
+            let args = parse_one_tt(args, RulePart::Pattern, sess, node_id, features, edition);
+            check_emission(check_lhs(sess, node_id, &args));
+            if let Some(guar) = check_no_eof(sess, &p, "expected macro attr body") {
+                return dummy_syn_ext(guar);
+            }
+            Some(args)
+        } else {
+            None
+        };
         let lhs_tt = p.parse_token_tree();
         let lhs_tt = parse_one_tt(lhs_tt, RulePart::Pattern, sess, node_id, features, edition);
         check_emission(check_lhs(sess, node_id, &lhs_tt));
@@ -416,7 +454,7 @@ pub fn compile_declarative_macro(
         let rhs_tt = p.parse_token_tree();
         let rhs_tt = parse_one_tt(rhs_tt, RulePart::Body, sess, node_id, features, edition);
         check_emission(check_rhs(sess, &rhs_tt));
-        check_emission(macro_check::check_meta_variables(&sess.psess, node_id, &lhs_tt, &rhs_tt));
+        check_emission(check_meta_variables(&sess.psess, node_id, args.as_ref(), &lhs_tt, &rhs_tt));
         let lhs_span = lhs_tt.span();
         // Convert the lhs into `MatcherLoc` form, which is better for doing the
         // actual matching.
@@ -425,7 +463,17 @@ pub fn compile_declarative_macro(
         } else {
             return dummy_syn_ext(guar.unwrap());
         };
-        rules.push(MacroRule { lhs, lhs_span, rhs: rhs_tt });
+        if let Some(args) = args {
+            let args_span = args.span();
+            let mbe::TokenTree::Delimited(.., delimited) = args else {
+                return dummy_syn_ext(guar.unwrap());
+            };
+            let args = mbe::macro_parser::compute_locs(&delimited.tts);
+            let body_span = lhs_span;
+            rules.push(MacroRule::Attr { args, args_span, body: lhs, body_span, rhs: rhs_tt });
+        } else {
+            rules.push(MacroRule::Func { lhs, lhs_span, rhs: rhs_tt });
+        }
         if p.token == token::Eof {
             break;
         }
@@ -469,6 +517,18 @@ fn check_no_eof(sess: &Session, p: &Parser<'_>, msg: &'static str) -> Option<Err
     None
 }
 
+fn check_args_parens(sess: &Session, args: &tokenstream::TokenTree) {
+    // This does not handle the non-delimited case; that gets handled separately by `check_lhs`.
+    if let tokenstream::TokenTree::Delimited(dspan, _, delim, _) = args
+        && *delim != Delimiter::Parenthesis
+    {
+        sess.dcx().emit_err(errors::MacroArgsBadDelim {
+            span: dspan.entire(),
+            sugg: errors::MacroArgsBadDelimSugg { open: dspan.open, close: dspan.close },
+        });
+    }
+}
+
 fn check_lhs(sess: &Session, node_id: NodeId, lhs: &mbe::TokenTree) -> Result<(), ErrorGuaranteed> {
     let e1 = check_lhs_nt_follows(sess, node_id, lhs);
     let e2 = check_lhs_no_empty_seq(sess, slice::from_ref(lhs));
diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs
index ca71bcebfdd..ae1f57c1ef6 100644
--- a/compiler/rustc_feature/src/unstable.rs
+++ b/compiler/rustc_feature/src/unstable.rs
@@ -554,6 +554,8 @@ declare_features! (
     (unstable, link_arg_attribute, "1.76.0", Some(99427)),
     /// Allows fused `loop`/`match` for direct intraprocedural jumps.
     (incomplete, loop_match, "1.90.0", Some(132306)),
+    /// Allow `macro_rules!` attribute rules
+    (unstable, macro_attr, "CURRENT_RUSTC_VERSION", Some(83527)),
     /// Give access to additional metadata about declarative macro meta-variables.
     (unstable, macro_metavar_expr, "1.61.0", Some(83527)),
     /// Provides a way to concatenate identifiers using metavariable expressions.
diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs
index 36197950221..5462ed38dd3 100644
--- a/compiler/rustc_span/src/symbol.rs
+++ b/compiler/rustc_span/src/symbol.rs
@@ -1311,6 +1311,7 @@ symbols! {
         lt,
         m68k_target_feature,
         macro_at_most_once_rep,
+        macro_attr,
         macro_attributes_in_derive_output,
         macro_concat,
         macro_escape,
diff --git a/tests/ui/feature-gates/feature-gate-macro-attr.rs b/tests/ui/feature-gates/feature-gate-macro-attr.rs
new file mode 100644
index 00000000000..8419d851147
--- /dev/null
+++ b/tests/ui/feature-gates/feature-gate-macro-attr.rs
@@ -0,0 +1,4 @@
+#![crate_type = "lib"]
+
+macro_rules! myattr { attr() {} => {} }
+//~^ ERROR `macro_rules!` attributes are unstable
diff --git a/tests/ui/feature-gates/feature-gate-macro-attr.stderr b/tests/ui/feature-gates/feature-gate-macro-attr.stderr
new file mode 100644
index 00000000000..b58418527c5
--- /dev/null
+++ b/tests/ui/feature-gates/feature-gate-macro-attr.stderr
@@ -0,0 +1,13 @@
+error[E0658]: `macro_rules!` attributes are unstable
+  --> $DIR/feature-gate-macro-attr.rs:3:1
+   |
+LL | macro_rules! myattr { attr() {} => {} }
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: see issue #83527 <https://github.com/rust-lang/rust/issues/83527> for more information
+   = help: add `#![feature(macro_attr)]` 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
+
+For more information about this error, try `rustc --explain E0658`.
diff --git a/tests/ui/parser/macro/macro-attr-bad.rs b/tests/ui/parser/macro/macro-attr-bad.rs
new file mode 100644
index 00000000000..4313a4d04ab
--- /dev/null
+++ b/tests/ui/parser/macro/macro-attr-bad.rs
@@ -0,0 +1,32 @@
+#![crate_type = "lib"]
+#![feature(macro_attr)]
+
+macro_rules! attr_incomplete_1 { attr }
+//~^ ERROR macro definition ended unexpectedly
+
+macro_rules! attr_incomplete_2 { attr() }
+//~^ ERROR macro definition ended unexpectedly
+
+macro_rules! attr_incomplete_3 { attr() {} }
+//~^ ERROR expected `=>`
+
+macro_rules! attr_incomplete_4 { attr() {} => }
+//~^ ERROR macro definition ended unexpectedly
+
+macro_rules! attr_noparens_1 { attr{} {} => {} }
+//~^ ERROR macro attribute argument matchers require parentheses
+
+macro_rules! attr_noparens_2 { attr[] {} => {} }
+//~^ ERROR macro attribute argument matchers require parentheses
+
+macro_rules! attr_noparens_3 { attr _ {} => {} }
+//~^ ERROR invalid macro matcher
+
+macro_rules! attr_dup_matcher_1 { attr() {$x:ident $x:ident} => {} }
+//~^ ERROR duplicate matcher binding
+
+macro_rules! attr_dup_matcher_2 { attr($x:ident $x:ident) {} => {} }
+//~^ ERROR duplicate matcher binding
+
+macro_rules! attr_dup_matcher_3 { attr($x:ident) {$x:ident} => {} }
+//~^ ERROR duplicate matcher binding
diff --git a/tests/ui/parser/macro/macro-attr-bad.stderr b/tests/ui/parser/macro/macro-attr-bad.stderr
new file mode 100644
index 00000000000..4d286b66649
--- /dev/null
+++ b/tests/ui/parser/macro/macro-attr-bad.stderr
@@ -0,0 +1,80 @@
+error: macro definition ended unexpectedly
+  --> $DIR/macro-attr-bad.rs:4:38
+   |
+LL | macro_rules! attr_incomplete_1 { attr }
+   |                                      ^ expected macro attr args
+
+error: macro definition ended unexpectedly
+  --> $DIR/macro-attr-bad.rs:7:40
+   |
+LL | macro_rules! attr_incomplete_2 { attr() }
+   |                                        ^ expected macro attr body
+
+error: expected `=>`, found end of macro arguments
+  --> $DIR/macro-attr-bad.rs:10:43
+   |
+LL | macro_rules! attr_incomplete_3 { attr() {} }
+   |                                           ^ expected `=>`
+
+error: macro definition ended unexpectedly
+  --> $DIR/macro-attr-bad.rs:13:46
+   |
+LL | macro_rules! attr_incomplete_4 { attr() {} => }
+   |                                              ^ expected right-hand side of macro rule
+
+error: macro attribute argument matchers require parentheses
+  --> $DIR/macro-attr-bad.rs:16:36
+   |
+LL | macro_rules! attr_noparens_1 { attr{} {} => {} }
+   |                                    ^^
+   |
+help: the delimiters should be `(` and `)`
+   |
+LL - macro_rules! attr_noparens_1 { attr{} {} => {} }
+LL + macro_rules! attr_noparens_1 { attr() {} => {} }
+   |
+
+error: macro attribute argument matchers require parentheses
+  --> $DIR/macro-attr-bad.rs:19:36
+   |
+LL | macro_rules! attr_noparens_2 { attr[] {} => {} }
+   |                                    ^^
+   |
+help: the delimiters should be `(` and `)`
+   |
+LL - macro_rules! attr_noparens_2 { attr[] {} => {} }
+LL + macro_rules! attr_noparens_2 { attr() {} => {} }
+   |
+
+error: invalid macro matcher; matchers must be contained in balanced delimiters
+  --> $DIR/macro-attr-bad.rs:22:37
+   |
+LL | macro_rules! attr_noparens_3 { attr _ {} => {} }
+   |                                     ^
+
+error: duplicate matcher binding
+  --> $DIR/macro-attr-bad.rs:25:52
+   |
+LL | macro_rules! attr_dup_matcher_1 { attr() {$x:ident $x:ident} => {} }
+   |                                           -------- ^^^^^^^^ duplicate binding
+   |                                           |
+   |                                           previous binding
+
+error: duplicate matcher binding
+  --> $DIR/macro-attr-bad.rs:28:49
+   |
+LL | macro_rules! attr_dup_matcher_2 { attr($x:ident $x:ident) {} => {} }
+   |                                        -------- ^^^^^^^^ duplicate binding
+   |                                        |
+   |                                        previous binding
+
+error: duplicate matcher binding
+  --> $DIR/macro-attr-bad.rs:31:51
+   |
+LL | macro_rules! attr_dup_matcher_3 { attr($x:ident) {$x:ident} => {} }
+   |                                        --------   ^^^^^^^^ duplicate binding
+   |                                        |
+   |                                        previous binding
+
+error: aborting due to 10 previous errors
+