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
+