Skip to content

Implement declarative (macro_rules!) derive macros (RFC 3698) #145208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/rustc_expand/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ 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 = macro `{$rule_kw}` argument matchers require parentheses
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expand_macro_args_bad_delim = macro `{$rule_kw}` argument matchers require parentheses
expand_macro_args_bad_delim = `{$rule_kw}` macro argument matchers require parentheses

Nit: the current wording reads weirdly.

expand_macro_args_bad_delim_sugg = the delimiters should be `(` and `)`
expand_macro_body_stability =
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_expand/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ pub(crate) struct MacroArgsBadDelim {
pub span: Span,
#[subdiagnostic]
pub sugg: MacroArgsBadDelimSugg,
pub rule_kw: Symbol,
}

#[derive(Subdiagnostic)]
Expand Down
30 changes: 30 additions & 0 deletions compiler/rustc_expand/src/expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use rustc_attr_parsing::{EvalConfigResult, ShouldEmit};
use rustc_data_structures::flat_map_in_place::FlatMapInPlace;
use rustc_errors::PResult;
use rustc_feature::Features;
use rustc_hir::def::MacroKinds;
use rustc_parse::parser::{
AttemptLocalParseRecovery, CommaRecoveryMode, ForceCollect, Parser, RecoverColon, RecoverComma,
token_descr,
Expand Down Expand Up @@ -922,6 +923,35 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
}
fragment
}
SyntaxExtensionKind::MacroRules(expander)
if expander.kinds().contains(MacroKinds::DERIVE) =>
{
if is_const {
let guar = self
.cx
.dcx()
.span_err(span, "macro `derive` does not support const derives");
return ExpandResult::Ready(fragment_kind.dummy(span, guar));
}
let body = item.to_tokens();
match expander.expand_derive(self.cx, span, &body) {
Ok(tok_result) => {
let fragment =
self.parse_ast_fragment(tok_result, fragment_kind, &path, span);
if macro_stats {
update_derive_macro_stats(
self.cx,
fragment_kind,
span,
&path,
&fragment,
);
}
fragment
}
Err(guar) => return ExpandResult::Ready(fragment_kind.dummy(span, guar)),
}
}
_ => unreachable!(),
},
InvocationKind::GlobDelegation { item, of_trait } => {
Expand Down
24 changes: 17 additions & 7 deletions compiler/rustc_expand/src/mbe/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,22 @@ use super::macro_rules::{MacroRule, NoopTracker, parser_from_cx};
use crate::expand::{AstFragmentKind, parse_ast_fragment};
use crate::mbe::macro_parser::ParseResult::*;
use crate::mbe::macro_parser::{MatcherLoc, NamedParseResult, TtParser};
use crate::mbe::macro_rules::{Tracker, try_match_macro, try_match_macro_attr};
use crate::mbe::macro_rules::{
Tracker, try_match_macro, try_match_macro_attr, try_match_macro_derive,
};

pub(super) enum FailedMacro<'a> {
Func,
Attr(&'a TokenStream),
Derive,
}

pub(super) fn failed_to_match_macro(
psess: &ParseSess,
sp: Span,
def_span: Span,
name: Ident,
attr_args: Option<&TokenStream>,
args: FailedMacro<'_>,
body: &TokenStream,
rules: &[MacroRule],
) -> (Span, ErrorGuaranteed) {
Expand All @@ -36,10 +44,12 @@ pub(super) fn failed_to_match_macro(
// diagnostics.
let mut tracker = CollectTrackerAndEmitter::new(psess.dcx(), sp);

let try_success_result = if let Some(attr_args) = attr_args {
try_match_macro_attr(psess, name, attr_args, body, rules, &mut tracker)
} else {
try_match_macro(psess, name, body, rules, &mut tracker)
let try_success_result = match args {
FailedMacro::Func => try_match_macro(psess, name, body, rules, &mut tracker),
FailedMacro::Attr(attr_args) => {
try_match_macro_attr(psess, name, attr_args, body, rules, &mut tracker)
}
FailedMacro::Derive => try_match_macro_derive(psess, name, body, rules, &mut tracker),
};

if try_success_result.is_ok() {
Expand Down Expand Up @@ -90,7 +100,7 @@ pub(super) fn failed_to_match_macro(
}

// Check whether there's a missing comma in this macro call, like `println!("{}" a);`
if attr_args.is_none()
if let FailedMacro::Func = args
&& let Some((body, comma_span)) = body.add_comma()
{
for rule in rules {
Expand Down
177 changes: 166 additions & 11 deletions compiler/rustc_expand/src/mbe/macro_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ use rustc_session::Session;
use rustc_session::parse::{ParseSess, feature_err};
use rustc_span::edition::Edition;
use rustc_span::hygiene::Transparency;
use rustc_span::{Ident, Span, kw, sym};
use rustc_span::{Ident, Span, Symbol, kw, sym};
use tracing::{debug, instrument, trace, trace_span};

use super::diagnostics::failed_to_match_macro;
use super::diagnostics::{FailedMacro, failed_to_match_macro};
use super::macro_parser::{NamedMatches, NamedParseResult};
use super::{SequenceRepetition, diagnostics};
use crate::base::{
Expand Down Expand Up @@ -138,6 +138,8 @@ pub(super) enum MacroRule {
body_span: Span,
rhs: mbe::TokenTree,
},
/// A derive rule, for use with `#[m]`
Derive { body: Vec<MatcherLoc>, body_span: Span, rhs: mbe::TokenTree },
}

pub struct MacroRulesMacroExpander {
Expand All @@ -157,13 +159,71 @@ impl MacroRulesMacroExpander {
MacroRule::Attr { args_span, body_span, ref rhs, .. } => {
(MultiSpan::from_spans(vec![args_span, body_span]), rhs)
}
MacroRule::Derive { body_span, ref rhs, .. } => (MultiSpan::from_span(body_span), rhs),
};
if has_compile_error_macro(rhs) { None } else { Some((&self.name, span)) }
}

pub fn kinds(&self) -> MacroKinds {
self.kinds
}

pub fn expand_derive(
&self,
cx: &mut ExtCtxt<'_>,
sp: Span,
body: &TokenStream,
) -> Result<TokenStream, ErrorGuaranteed> {
// This is similar to `expand_macro`, but they have very different signatures, and will
// diverge further once derives support arguments.
let Self { name, ref rules, node_id, .. } = *self;
let psess = &cx.sess.psess;

if cx.trace_macros() {
let msg = format!("expanding `#[derive({name})] {}`", pprust::tts_to_string(body));
trace_macros_note(&mut cx.expansions, sp, msg);
}

match try_match_macro_derive(psess, name, body, rules, &mut NoopTracker) {
Ok((rule_index, rule, named_matches)) => {
let MacroRule::Derive { rhs, .. } = rule else {
panic!("try_match_macro_derive returned non-derive rule");
};
let mbe::TokenTree::Delimited(rhs_span, _, rhs) = rhs else {
cx.dcx().span_bug(sp, "malformed macro derive rhs");
};

let id = cx.current_expansion.id;
let tts = transcribe(psess, &named_matches, rhs, *rhs_span, self.transparency, id)
.map_err(|e| e.emit())?;

if cx.trace_macros() {
let msg = format!("to `{}`", pprust::tts_to_string(&tts));
trace_macros_note(&mut cx.expansions, sp, msg);
}

if is_defined_in_current_crate(node_id) {
cx.resolver.record_macro_rule_usage(node_id, rule_index);
}

Ok(tts)
}
Err(CanRetry::No(guar)) => Err(guar),
Err(CanRetry::Yes) => {
let (_, guar) = failed_to_match_macro(
cx.psess(),
sp,
self.span,
name,
FailedMacro::Derive,
body,
rules,
);
cx.macro_error_and_trace_macros_diag();
Err(guar)
}
}
}
}

impl TTMacroExpander for MacroRulesMacroExpander {
Expand Down Expand Up @@ -325,8 +385,15 @@ fn expand_macro<'cx>(
}
Err(CanRetry::Yes) => {
// Retry and emit a better error.
let (span, guar) =
failed_to_match_macro(cx.psess(), sp, def_span, name, None, &arg, rules);
let (span, guar) = failed_to_match_macro(
cx.psess(),
sp,
def_span,
name,
FailedMacro::Func,
&arg,
rules,
);
cx.macro_error_and_trace_macros_diag();
DummyResult::any(span, guar)
}
Expand Down Expand Up @@ -388,8 +455,15 @@ fn expand_macro_attr(
Err(CanRetry::No(guar)) => Err(guar),
Err(CanRetry::Yes) => {
// Retry and emit a better error.
let (_, guar) =
failed_to_match_macro(cx.psess(), sp, def_span, name, Some(&args), &body, rules);
let (_, guar) = failed_to_match_macro(
cx.psess(),
sp,
def_span,
name,
FailedMacro::Attr(&args),
&body,
rules,
);
cx.trace_macros_diag();
Err(guar)
}
Expand Down Expand Up @@ -536,6 +610,44 @@ pub(super) fn try_match_macro_attr<'matcher, T: Tracker<'matcher>>(
Err(CanRetry::Yes)
}

/// Try expanding the macro derive. Returns the index of the successful arm and its
/// named_matches if it was successful, and nothing if it failed. On failure, it's the caller's job
/// to use `track` accordingly to record all errors correctly.
#[instrument(level = "debug", skip(psess, body, rules, track), fields(tracking = %T::description()))]
pub(super) fn try_match_macro_derive<'matcher, T: Tracker<'matcher>>(
psess: &ParseSess,
name: Ident,
body: &TokenStream,
rules: &'matcher [MacroRule],
track: &mut T,
) -> Result<(usize, &'matcher MacroRule, NamedMatches), CanRetry> {
// This uses the same strategy as `try_match_macro`
let body_parser = parser_from_cx(psess, body.clone(), T::recovery());
let mut tt_parser = TtParser::new(name);
for (i, rule) in rules.iter().enumerate() {
let MacroRule::Derive { body, .. } = rule else { continue };

let mut gated_spans_snapshot = mem::take(&mut *psess.gated_spans.spans.borrow_mut());

let result = tt_parser.parse_tt(&mut Cow::Borrowed(&body_parser), body, track);
track.after_arm(true, &result);

match result {
Success(named_matches) => {
psess.gated_spans.merge(gated_spans_snapshot);
return Ok((i, rule, named_matches));
}
Failure(_) => {
mem::swap(&mut gated_spans_snapshot, &mut psess.gated_spans.spans.borrow_mut())
}
Error(_, _) => return Err(CanRetry::Yes),
ErrorReported(guar) => return Err(CanRetry::No(guar)),
}
}

Err(CanRetry::Yes)
}

/// Converts a macro item into a syntax extension.
pub fn compile_declarative_macro(
sess: &Session,
Expand Down Expand Up @@ -569,7 +681,7 @@ pub fn compile_declarative_macro(
let mut rules = Vec::new();

while p.token != token::Eof {
let args = if p.eat_keyword_noexpect(sym::attr) {
let (args, is_derive) = if p.eat_keyword_noexpect(sym::attr) {
kinds |= MacroKinds::ATTR;
if !features.macro_attr() {
feature_err(sess, sym::macro_attr, span, "`macro_rules!` attributes are unstable")
Expand All @@ -579,16 +691,46 @@ pub fn compile_declarative_macro(
return dummy_syn_ext(guar);
}
let args = p.parse_token_tree();
check_args_parens(sess, &args);
check_args_parens(sess, sym::attr, &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)
(Some(args), false)
} else if p.eat_keyword_noexpect(sym::derive) {
kinds |= MacroKinds::DERIVE;
let derive_keyword_span = p.prev_token.span;
if !features.macro_derive() {
feature_err(sess, sym::macro_attr, span, "`macro_rules!` derives are unstable")
.emit();
}
if let Some(guar) = check_no_eof(sess, &p, "expected `()` after `derive`") {
return dummy_syn_ext(guar);
}
let args = p.parse_token_tree();
check_args_parens(sess, sym::derive, &args);
let args_empty_result = check_args_empty(sess, &args);
let args_not_empty = args_empty_result.is_err();
check_emission(args_empty_result);
if let Some(guar) = check_no_eof(sess, &p, "expected macro derive body") {
return dummy_syn_ext(guar);
}
// If the user has `=>` right after the `()`, they might have forgotten the empty
// parentheses.
if p.token == token::FatArrow {
let mut err = sess
.dcx()
.struct_span_err(p.token.span, "expected macro derive body, got `=>`");
if args_not_empty {
err.span_label(derive_keyword_span, "need `()` after this `derive`");
}
return dummy_syn_ext(err.emit());
}
(None, true)
} else {
kinds |= MacroKinds::BANG;
None
(None, false)
};
let lhs_tt = p.parse_token_tree();
let lhs_tt = parse_one_tt(lhs_tt, RulePart::Pattern, sess, node_id, features, edition);
Expand Down Expand Up @@ -619,6 +761,8 @@ pub fn compile_declarative_macro(
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 if is_derive {
rules.push(MacroRule::Derive { body: lhs, body_span: lhs_span, rhs: rhs_tt });
} else {
rules.push(MacroRule::Func { lhs, lhs_span, rhs: rhs_tt });
}
Expand Down Expand Up @@ -665,18 +809,29 @@ fn check_no_eof(sess: &Session, p: &Parser<'_>, msg: &'static str) -> Option<Err
None
}

fn check_args_parens(sess: &Session, args: &tokenstream::TokenTree) {
fn check_args_parens(sess: &Session, rule_kw: Symbol, 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 },
rule_kw,
});
}
}

fn check_args_empty(sess: &Session, args: &tokenstream::TokenTree) -> Result<(), ErrorGuaranteed> {
match args {
tokenstream::TokenTree::Delimited(.., delimited) if delimited.is_empty() => Ok(()),
_ => {
let msg = "`derive` rules do not accept arguments; `derive` must be followed by `()`";
Err(sess.dcx().span_err(args.span(), msg))
}
}
}

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));
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_feature/src/unstable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,8 @@ declare_features! (
(incomplete, loop_match, "1.90.0", Some(132306)),
/// Allow `macro_rules!` attribute rules
(unstable, macro_attr, "CURRENT_RUSTC_VERSION", Some(83527)),
/// Allow `macro_rules!` derive rules
(unstable, macro_derive, "CURRENT_RUSTC_VERSION", Some(143549)),
/// 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.
Expand Down
Loading
Loading