diff --git a/compiler/rustc_builtin_macros/messages.ftl b/compiler/rustc_builtin_macros/messages.ftl index 3594c7ec21045..183927edb0220 100644 --- a/compiler/rustc_builtin_macros/messages.ftl +++ b/compiler/rustc_builtin_macros/messages.ftl @@ -81,6 +81,12 @@ builtin_macros_cfg_accessible_literal_path = `cfg_accessible` path cannot be a l builtin_macros_cfg_accessible_multiple_paths = multiple `cfg_accessible` paths are specified builtin_macros_cfg_accessible_unspecified_path = `cfg_accessible` path is not specified +builtin_macros_cfg_select_no_matches = none of the rules in this `cfg_select` evaluated to true + +builtin_macros_cfg_select_unreachable = unreachable rule + .label = always matches + .label2 = this rules is never reached + builtin_macros_coerce_pointee_requires_maybe_sized = `derive(CoercePointee)` requires `{$name}` to be marked `?Sized` builtin_macros_coerce_pointee_requires_one_field = `CoercePointee` can only be derived on `struct`s with at least one field diff --git a/compiler/rustc_builtin_macros/src/cfg_select.rs b/compiler/rustc_builtin_macros/src/cfg_select.rs new file mode 100644 index 0000000000000..9bf08dffbfa53 --- /dev/null +++ b/compiler/rustc_builtin_macros/src/cfg_select.rs @@ -0,0 +1,64 @@ +use rustc_ast::tokenstream::TokenStream; +use rustc_attr_parsing as attr; +use rustc_expand::base::{ExtCtxt, MacroExpanderResult, *}; +use rustc_parse::parser::cfg_select::{CfgSelectBranches, CfgSelectRule, parse_cfg_select}; +use rustc_span::{Ident, Span, sym}; + +use crate::errors::{CfgSelectNoMatches, CfgSelectUnreachable}; + +/// Selects the first arm whose rule evaluates to true. +fn select_arm(ecx: &ExtCtxt<'_>, branches: CfgSelectBranches) -> Option<(TokenStream, Span)> { + for (cfg, tt, arm_span) in branches.reachable { + if attr::cfg_matches( + &cfg, + &ecx.sess, + ecx.current_expansion.lint_node_id, + Some(ecx.ecfg.features), + ) { + return Some((tt, arm_span)); + } + } + + branches.wildcard.map(|(_, tt, span)| (tt, span)) +} + +pub(super) fn expand_cfg_select<'cx>( + ecx: &'cx mut ExtCtxt<'_>, + sp: Span, + tts: TokenStream, +) -> MacroExpanderResult<'cx> { + ExpandResult::Ready(match parse_cfg_select(&mut ecx.new_parser_from_tts(tts)) { + Ok(branches) => { + if let Some((underscore, _, _)) = branches.wildcard { + // Warn for every unreachable rule. + for (rule, _, _) in &branches.unreachable { + let span = match rule { + CfgSelectRule::Wildcard(underscore) => underscore.span, + CfgSelectRule::Cfg(cfg) => cfg.span(), + }; + let err = CfgSelectUnreachable { span, wildcard_span: underscore.span }; + ecx.dcx().emit_warn(err); + } + } + + if let Some((tts, arm_span)) = select_arm(ecx, branches) { + rustc_expand::expand_token_stream( + ecx, + sp, + arm_span, + ecx.current_expansion.lint_node_id, + Ident::with_dummy_span(sym::cfg_select), + tts, + ) + } else { + // Emit a compiler error when none of the rules matched. + let guar = ecx.dcx().emit_err(CfgSelectNoMatches { span: sp }); + DummyResult::any(sp, guar) + } + } + Err(err) => { + let guar = err.emit(); + DummyResult::any(sp, guar) + } + }) +} diff --git a/compiler/rustc_builtin_macros/src/errors.rs b/compiler/rustc_builtin_macros/src/errors.rs index a5ee7349fc68b..6bcf4d3e0a2ee 100644 --- a/compiler/rustc_builtin_macros/src/errors.rs +++ b/compiler/rustc_builtin_macros/src/errors.rs @@ -954,3 +954,21 @@ pub(crate) struct AsmExpectedOther { pub(crate) span: Span, pub(crate) is_inline_asm: bool, } + +#[derive(Diagnostic)] +#[diag(builtin_macros_cfg_select_no_matches)] +pub(crate) struct CfgSelectNoMatches { + #[primary_span] + pub span: Span, +} + +#[derive(Diagnostic)] +#[diag(builtin_macros_cfg_select_unreachable)] +pub(crate) struct CfgSelectUnreachable { + #[primary_span] + #[label(builtin_macros_label2)] + pub span: Span, + + #[label] + pub wildcard_span: Span, +} diff --git a/compiler/rustc_builtin_macros/src/lib.rs b/compiler/rustc_builtin_macros/src/lib.rs index 6bf590df5c9da..7bc448a9acb8b 100644 --- a/compiler/rustc_builtin_macros/src/lib.rs +++ b/compiler/rustc_builtin_macros/src/lib.rs @@ -33,6 +33,7 @@ mod autodiff; mod cfg; mod cfg_accessible; mod cfg_eval; +mod cfg_select; mod compile_error; mod concat; mod concat_bytes; @@ -79,6 +80,7 @@ pub fn register_builtin_macros(resolver: &mut dyn ResolverExpand) { asm: asm::expand_asm, assert: assert::expand_assert, cfg: cfg::expand_cfg, + cfg_select: cfg_select::expand_cfg_select, column: source_util::expand_column, compile_error: compile_error::expand_compile_error, concat: concat::expand_concat, diff --git a/compiler/rustc_expand/src/lib.rs b/compiler/rustc_expand/src/lib.rs index 64be7649775f3..d19de56ba7163 100644 --- a/compiler/rustc_expand/src/lib.rs +++ b/compiler/rustc_expand/src/lib.rs @@ -22,7 +22,7 @@ mod placeholders; mod proc_macro_server; mod stats; -pub use mbe::macro_rules::compile_declarative_macro; +pub use mbe::macro_rules::{compile_declarative_macro, expand_token_stream}; pub mod base; pub mod config; pub mod expand; diff --git a/compiler/rustc_expand/src/mbe/macro_rules.rs b/compiler/rustc_expand/src/mbe/macro_rules.rs index 89547088f501f..f790e32f674fe 100644 --- a/compiler/rustc_expand/src/mbe/macro_rules.rs +++ b/compiler/rustc_expand/src/mbe/macro_rules.rs @@ -194,6 +194,30 @@ impl<'matcher> Tracker<'matcher> for NoopTracker { } } +#[instrument(skip(cx, tts))] +pub fn expand_token_stream<'cx>( + cx: &'cx mut ExtCtxt<'_>, + sp: Span, + arm_span: Span, + node_id: NodeId, + name: Ident, + tts: TokenStream, +) -> Box { + Box::new(ParserAnyMacro { + parser: Parser::new(&cx.sess.psess, tts, None), + + // Pass along the original expansion site and the name of the macro + // so we can print a useful error message if the parse of the expanded + // macro leaves unparsed tokens. + site_span: sp, + macro_ident: name, + lint_node_id: cx.current_expansion.lint_node_id, + is_trailing_mac: cx.current_expansion.is_trailing_mac, + arm_span, + is_local: is_defined_in_current_crate(node_id), + }) +} + /// Expands the rules based macro defined by `rules` for a given input `arg`. #[instrument(skip(cx, transparency, arg, rules))] fn expand_macro<'cx>( @@ -207,9 +231,6 @@ fn expand_macro<'cx>( rules: &[MacroRule], ) -> Box { let psess = &cx.sess.psess; - // Macros defined in the current crate have a real node id, - // whereas macros from an external crate have a dummy id. - let is_local = node_id != DUMMY_NODE_ID; if cx.trace_macros() { let msg = format!("expanding `{}! {{ {} }}`", name, pprust::tts_to_string(&arg)); @@ -220,7 +241,7 @@ fn expand_macro<'cx>( let try_success_result = try_match_macro(psess, name, &arg, rules, &mut NoopTracker); match try_success_result { - Ok((i, rule, named_matches)) => { + Ok((rule_index, rule, named_matches)) => { let mbe::TokenTree::Delimited(rhs_span, _, ref rhs) = rule.rhs else { cx.dcx().span_bug(sp, "malformed macro rhs"); }; @@ -241,27 +262,13 @@ fn expand_macro<'cx>( trace_macros_note(&mut cx.expansions, sp, msg); } - let p = Parser::new(psess, tts, None); - - if is_local { - cx.resolver.record_macro_rule_usage(node_id, i); + if is_defined_in_current_crate(node_id) { + cx.resolver.record_macro_rule_usage(node_id, rule_index); } // Let the context choose how to interpret the result. // Weird, but useful for X-macros. - Box::new(ParserAnyMacro { - parser: p, - - // Pass along the original expansion site and the name of the macro - // so we can print a useful error message if the parse of the expanded - // macro leaves unparsed tokens. - site_span: sp, - macro_ident: name, - lint_node_id: cx.current_expansion.lint_node_id, - is_trailing_mac: cx.current_expansion.is_trailing_mac, - arm_span, - is_local, - }) + expand_token_stream(cx, sp, arm_span, node_id, name, tts) } Err(CanRetry::No(guar)) => { debug!("Will not retry matching as an error was emitted already"); @@ -373,10 +380,18 @@ pub fn compile_declarative_macro( node_id: NodeId, edition: Edition, ) -> (SyntaxExtension, usize) { - let is_local = node_id != DUMMY_NODE_ID; let mk_syn_ext = |expander| { let kind = SyntaxExtensionKind::LegacyBang(expander); - SyntaxExtension::new(sess, kind, span, Vec::new(), edition, ident.name, attrs, is_local) + SyntaxExtension::new( + sess, + kind, + span, + Vec::new(), + edition, + ident.name, + attrs, + is_defined_in_current_crate(node_id), + ) }; let dummy_syn_ext = |guar| (mk_syn_ext(Arc::new(DummyExpander(guar))), 0); @@ -439,7 +454,7 @@ pub fn compile_declarative_macro( } // Return the number of rules for unused rule linting, if this is a local macro. - let nrules = if is_local { rules.len() } else { 0 }; + let nrules = if is_defined_in_current_crate(node_id) { rules.len() } else { 0 }; let expander = Arc::new(MacroRulesMacroExpander { name: ident, span, node_id, transparency, rules }); @@ -1034,9 +1049,7 @@ fn check_matcher_core<'tt>( // definition of this macro_rules, not while (re)parsing // the macro when compiling another crate that is using the // macro. (See #86567.) - // Macros defined in the current crate have a real node id, - // whereas macros from an external crate have a dummy id. - if node_id != DUMMY_NODE_ID + if is_defined_in_current_crate(node_id) && matches!(kind, NonterminalKind::Pat(PatParam { inferred: true })) && matches!( next_token, @@ -1296,6 +1309,12 @@ fn quoted_tt_to_string(tt: &mbe::TokenTree) -> String { } } +fn is_defined_in_current_crate(node_id: NodeId) -> bool { + // Macros defined in the current crate have a real node id, + // whereas macros from an external crate have a dummy id. + node_id != DUMMY_NODE_ID +} + pub(super) fn parser_from_cx( psess: &ParseSess, mut tts: TokenStream, diff --git a/compiler/rustc_parse/src/parser/cfg_select.rs b/compiler/rustc_parse/src/parser/cfg_select.rs new file mode 100644 index 0000000000000..8cde070b9b900 --- /dev/null +++ b/compiler/rustc_parse/src/parser/cfg_select.rs @@ -0,0 +1,73 @@ +use rustc_ast::token::Token; +use rustc_ast::tokenstream::{TokenStream, TokenTree}; +use rustc_ast::{MetaItemInner, token}; +use rustc_errors::PResult; +use rustc_span::{Span, kw}; + +use crate::exp; +use crate::parser::Parser; + +pub enum CfgSelectRule { + Cfg(MetaItemInner), + Wildcard(Token), +} + +#[derive(Default)] +pub struct CfgSelectBranches { + /// All the conditional branches. + pub reachable: Vec<(MetaItemInner, TokenStream, Span)>, + /// The first wildcard `_ => { ... }` branch. + pub wildcard: Option<(Token, TokenStream, Span)>, + /// All branches after the first wildcard, including further wildcards. + pub unreachable: Vec<(CfgSelectRule, TokenStream, Span)>, +} + +/// Parses a `TokenTree` that must be of the form `{ /* ... */ }`, and returns a `TokenStream` where +/// the surrounding braces are stripped. +fn parse_token_tree<'a>(p: &mut Parser<'a>) -> PResult<'a, TokenStream> { + // Generate an error if the `=>` is not followed by `{`. + if p.token != token::OpenBrace { + p.expect(exp!(OpenBrace))?; + } + + // Strip the outer '{' and '}'. + match p.parse_token_tree() { + TokenTree::Token(..) => unreachable!("because of the expect above"), + TokenTree::Delimited(.., tts) => Ok(tts), + } +} + +pub fn parse_cfg_select<'a>(p: &mut Parser<'a>) -> PResult<'a, CfgSelectBranches> { + let mut branches = CfgSelectBranches::default(); + + while p.token != token::Eof { + if p.token.is_keyword(kw::Underscore) { + let underscore = p.token; + p.bump(); + p.expect(exp!(FatArrow))?; + + let tts = parse_token_tree(p)?; + let span = underscore.span.to(p.token.span); + + match branches.wildcard { + None => branches.wildcard = Some((underscore, tts, span)), + Some(_) => { + branches.unreachable.push((CfgSelectRule::Wildcard(underscore), tts, span)) + } + } + } else { + let meta_item = p.parse_meta_item_inner()?; + p.expect(exp!(FatArrow))?; + + let tts = parse_token_tree(p)?; + let span = meta_item.span().to(p.token.span); + + match branches.wildcard { + None => branches.reachable.push((meta_item, tts, span)), + Some(_) => branches.unreachable.push((CfgSelectRule::Cfg(meta_item), tts, span)), + } + } + } + + Ok(branches) +} diff --git a/compiler/rustc_parse/src/parser/mod.rs b/compiler/rustc_parse/src/parser/mod.rs index cfc0399b0ca96..cd2d60c0c904a 100644 --- a/compiler/rustc_parse/src/parser/mod.rs +++ b/compiler/rustc_parse/src/parser/mod.rs @@ -1,6 +1,7 @@ pub mod asm; pub mod attr; mod attr_wrapper; +pub mod cfg_select; mod diagnostics; mod expr; mod generics; diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 09f01d8704e2a..930c245168a3d 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -624,6 +624,7 @@ symbols! { cfg_relocation_model, cfg_sanitize, cfg_sanitizer_cfi, + cfg_select, cfg_target_abi, cfg_target_compact, cfg_target_feature, diff --git a/library/core/src/macros/mod.rs b/library/core/src/macros/mod.rs index 8035dccc632d4..3d8a91258e88c 100644 --- a/library/core/src/macros/mod.rs +++ b/library/core/src/macros/mod.rs @@ -230,32 +230,17 @@ pub macro assert_matches { /// ``` /// #![feature(cfg_select)] /// -/// let _some_string = cfg_select! {{ +/// let _some_string = cfg_select! { /// unix => { "With great power comes great electricity bills" } /// _ => { "Behind every successful diet is an unwatched pizza" } -/// }}; +/// }; /// ``` #[unstable(feature = "cfg_select", issue = "115585")] #[rustc_diagnostic_item = "cfg_select"] #[rustc_macro_transparency = "semitransparent"] -pub macro cfg_select { - ({ $($tt:tt)* }) => {{ - $crate::cfg_select! { $($tt)* } - }}, - (_ => { $($output:tt)* }) => { - $($output)* - }, - ( - $cfg:meta => $output:tt - $($( $rest:tt )+)? - ) => { - #[cfg($cfg)] - $crate::cfg_select! { _ => $output } - $( - #[cfg(not($cfg))] - $crate::cfg_select! { $($rest)+ } - )? - }, +#[rustc_builtin_macro] +pub macro cfg_select($($tt:tt)*) { + /* compiler built-in */ } /// Asserts that a boolean expression is `true` at runtime. diff --git a/library/coretests/tests/macros.rs b/library/coretests/tests/macros.rs index d220e628d7339..fafd3d29aef57 100644 --- a/library/coretests/tests/macros.rs +++ b/library/coretests/tests/macros.rs @@ -183,6 +183,12 @@ fn _accepts_expressions() -> i32 { } } +fn _accepts_only_wildcard() -> i32 { + cfg_select! { + _ => { 1 } + } +} + // The current implementation expands to a macro call, which allows the use of expression // statements. fn _allows_stmt_expr_attributes() { @@ -195,12 +201,12 @@ fn _allows_stmt_expr_attributes() { } fn _expression() { - let _ = cfg_select!({ + let _ = cfg_select!( windows => { " XP" } _ => { "" } - }); + ); } diff --git a/tests/auxiliary/minicore.rs b/tests/auxiliary/minicore.rs index 392ad1cee1425..47dadd51ce0fb 100644 --- a/tests/auxiliary/minicore.rs +++ b/tests/auxiliary/minicore.rs @@ -142,6 +142,10 @@ pub macro naked_asm("assembly template", $(operands,)* $(options($(option),*))?) pub macro global_asm("assembly template", $(operands,)* $(options($(option),*))?) { /* compiler built-in */ } +#[rustc_builtin_macro] +pub macro cfg_select($($tt:tt)*) { + /* compiler built-in */ +} #[rustc_builtin_macro] #[macro_export] diff --git a/tests/ui/macros/cfg_select.rs b/tests/ui/macros/cfg_select.rs new file mode 100644 index 0000000000000..a4d94836a09a1 --- /dev/null +++ b/tests/ui/macros/cfg_select.rs @@ -0,0 +1,27 @@ +#![feature(cfg_select)] +#![crate_type = "lib"] + +fn print() { + println!(cfg_select! { + unix => { "unix" } + _ => { "not unix" } + }); +} + +fn arm_rhs_must_be_in_braces() -> i32 { + cfg_select! { + true => 1 + //~^ ERROR: expected `{`, found `1` + } +} + +cfg_select! { + _ => {} + true => {} + //~^ WARN unreachable rule +} + +cfg_select! { + //~^ ERROR none of the rules in this `cfg_select` evaluated to true + false => {} +} diff --git a/tests/ui/macros/cfg_select.stderr b/tests/ui/macros/cfg_select.stderr new file mode 100644 index 0000000000000..fef5e95a6bce9 --- /dev/null +++ b/tests/ui/macros/cfg_select.stderr @@ -0,0 +1,25 @@ +error: expected `{`, found `1` + --> $DIR/cfg_select.rs:13:17 + | +LL | true => 1 + | ^ expected `{` + +warning: unreachable rule + --> $DIR/cfg_select.rs:20:5 + | +LL | _ => {} + | - always matches +LL | true => {} + | ^^^^ this rules is never reached + +error: none of the rules in this `cfg_select` evaluated to true + --> $DIR/cfg_select.rs:24:1 + | +LL | / cfg_select! { +LL | | +LL | | false => {} +LL | | } + | |_^ + +error: aborting due to 2 previous errors; 1 warning emitted +