Auto merge of #143461 - folkertdev:cfg-select-builtin-macro, r=petrochenkov
make `cfg_select` a builtin macro
tracking issue: https://github.com/rust-lang/rust/issues/115585
This parses mostly the same as the `macro cfg_select` version, except:
1. wrapping in double brackets is no longer supported (or needed): `cfg_select {{ /* ... */ }}` is now rejected.
2. in an expression context, the rhs is no longer wrapped in a block, so that this now works:
```rust
fn main() {
println!(cfg_select! {
unix => { "foo" }
_ => { "bar" }
});
}
```
3. a single wildcard rule is now supported: `cfg_select { _ => 1 }` now works
I've also added an error if none of the rules evaluate to true, and warnings for any arms that follow the `_` wildcard rule.
cc `@traviscross` if I'm missing any feature that should/should not be included
r? `@petrochenkov` for the macro logic details
diff --git a/compiler/rustc_builtin_macros/messages.ftl b/compiler/rustc_builtin_macros/messages.ftl
index 3594c7e..183927e 100644
--- a/compiler/rustc_builtin_macros/messages.ftl
+++ b/compiler/rustc_builtin_macros/messages.ftl
@@ -81,6 +81,12 @@
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 0000000..2dc387d
--- /dev/null
+++ b/compiler/rustc_builtin_macros/src/cfg_select.rs
@@ -0,0 +1,63 @@
+use rustc_ast::tokenstream::TokenStream;
+use rustc_attr_parsing as attr;
+use rustc_expand::base::{DummyResult, ExpandResult, 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. We store the fully parsed branch for rustfmt.
+ 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) {
+ return ExpandResult::from_tts(
+ ecx,
+ tts,
+ sp,
+ arm_span,
+ Ident::with_dummy_span(sym::cfg_select),
+ );
+ } 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 a5ee734..6bcf4d3 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 6bf590d..7bc448a 100644
--- a/compiler/rustc_builtin_macros/src/lib.rs
+++ b/compiler/rustc_builtin_macros/src/lib.rs
@@ -33,6 +33,7 @@
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/base.rs b/compiler/rustc_expand/src/base.rs
index d6d8980..757a7e1 100644
--- a/compiler/rustc_expand/src/base.rs
+++ b/compiler/rustc_expand/src/base.rs
@@ -34,6 +34,7 @@
use crate::base::ast::MetaItemInner;
use crate::errors;
use crate::expand::{self, AstFragment, Invocation};
+use crate::mbe::macro_rules::ParserAnyMacro;
use crate::module::DirOwnership;
use crate::stats::MacroStat;
@@ -262,6 +263,25 @@ pub fn map<E, F: FnOnce(T) -> E>(self, f: F) -> ExpandResult<E, U> {
}
}
+impl<'cx> MacroExpanderResult<'cx> {
+ /// Creates a [`MacroExpanderResult::Ready`] from a [`TokenStream`].
+ ///
+ /// The `TokenStream` is forwarded without any expansion.
+ pub fn from_tts(
+ cx: &'cx mut ExtCtxt<'_>,
+ tts: TokenStream,
+ site_span: Span,
+ arm_span: Span,
+ macro_ident: Ident,
+ ) -> Self {
+ // Emit the SEMICOLON_IN_EXPRESSIONS_FROM_MACROS deprecation lint.
+ let is_local = true;
+
+ let parser = ParserAnyMacro::from_tts(cx, tts, site_span, arm_span, is_local, macro_ident);
+ ExpandResult::Ready(Box::new(parser))
+ }
+}
+
pub trait MultiItemModifier {
/// `meta_item` is the attribute, and `item` is the item being modified.
fn expand(
diff --git a/compiler/rustc_expand/src/mbe/macro_rules.rs b/compiler/rustc_expand/src/mbe/macro_rules.rs
index 8954708..2d79235 100644
--- a/compiler/rustc_expand/src/mbe/macro_rules.rs
+++ b/compiler/rustc_expand/src/mbe/macro_rules.rs
@@ -96,6 +96,30 @@ pub(crate) fn make(mut self: Box<ParserAnyMacro<'a>>, kind: AstFragmentKind) ->
ensure_complete_parse(parser, &path, kind.name(), site_span);
fragment
}
+
+ #[instrument(skip(cx, tts))]
+ pub(crate) fn from_tts<'cx>(
+ cx: &'cx mut ExtCtxt<'a>,
+ tts: TokenStream,
+ site_span: Span,
+ arm_span: Span,
+ is_local: bool,
+ macro_ident: Ident,
+ ) -> Self {
+ Self {
+ 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,
+ macro_ident,
+ lint_node_id: cx.current_expansion.lint_node_id,
+ is_trailing_mac: cx.current_expansion.is_trailing_mac,
+ arm_span,
+ is_local,
+ }
+ }
}
pub(super) struct MacroRule {
@@ -207,9 +231,6 @@ fn expand_macro<'cx>(
rules: &[MacroRule],
) -> Box<dyn MacResult + 'cx> {
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);
-
+ let is_local = is_defined_in_current_crate(node_id);
if is_local {
- cx.resolver.record_macro_rule_usage(node_id, i);
+ 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,
- })
+ // Let the context choose how to interpret the result. Weird, but useful for X-macros.
+ Box::new(ParserAnyMacro::from_tts(cx, tts, sp, arm_span, is_local, name))
}
Err(CanRetry::No(guar)) => {
debug!("Will not retry matching as an error was emitted already");
@@ -373,9 +380,9 @@ 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);
+ let is_local = is_defined_in_current_crate(node_id);
SyntaxExtension::new(sess, kind, span, Vec::new(), edition, ident.name, attrs, is_local)
};
let dummy_syn_ext = |guar| (mk_syn_ext(Arc::new(DummyExpander(guar))), 0);
@@ -439,7 +446,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 +1041,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 +1301,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 0000000..24a05af
--- /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;
+
+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.
+ /// These branches are kept for formatting.
+ 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.eat_keyword(exp!(Underscore)) {
+ let underscore = p.prev_token;
+ 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 2787be4..90491e5 100644
--- a/compiler/rustc_parse/src/parser/mod.rs
+++ b/compiler/rustc_parse/src/parser/mod.rs
@@ -1,4 +1,3 @@
-pub mod asm;
pub mod attr;
mod attr_wrapper;
mod diagnostics;
@@ -12,6 +11,11 @@
pub mod token_type;
mod ty;
+// Parsers for non-functionlike builtin macros are defined in rustc_parse so they can be used by
+// both rustc_builtin_macros and rustfmt.
+pub mod asm;
+pub mod cfg_select;
+
use std::assert_matches::debug_assert_matches;
use std::{fmt, mem, slice};
diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs
index 4df91cc..8b12edf 100644
--- a/compiler/rustc_span/src/symbol.rs
+++ b/compiler/rustc_span/src/symbol.rs
@@ -624,6 +624,7 @@
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 8035dcc..6b9cbb0 100644
--- a/library/core/src/macros/mod.rs
+++ b/library/core/src/macros/mod.rs
@@ -230,32 +230,16 @@ macro_rules! assert_ne {
/// ```
/// #![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 d220e62..1c6aa90 100644
--- a/library/coretests/tests/macros.rs
+++ b/library/coretests/tests/macros.rs
@@ -48,11 +48,12 @@ fn matches_leading_pipe() {
fn cfg_select_basic() {
cfg_select! {
target_pointer_width = "64" => { fn f0_() -> bool { true }}
+ _ => {}
}
cfg_select! {
unix => { fn f1_() -> bool { true } }
- any(target_os = "macos", target_os = "linux") => { fn f1_() -> bool { false }}
+ _ => { fn f1_() -> bool { false }}
}
cfg_select! {
@@ -70,6 +71,8 @@ fn cfg_select_basic() {
#[cfg(unix)]
assert!(f1_());
+ #[cfg(not(unix))]
+ assert!(!f1_());
#[cfg(target_pointer_width = "32")]
assert!(!f2_());
@@ -183,6 +186,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 +204,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 392ad1c..47dadd5 100644
--- a/tests/auxiliary/minicore.rs
+++ b/tests/auxiliary/minicore.rs
@@ -142,6 +142,10 @@ pub trait Tuple {}
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 0000000..a4d9483
--- /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 0000000..fef5e95
--- /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
+