Skip to content
Open
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
1 change: 1 addition & 0 deletions compiler/rustc_data_structures/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ pub mod sso;
pub mod stable_hash;
pub mod stack;
pub mod steal;
mod string_enum;
pub mod svh;
pub mod sync;
pub mod tagged_ptr;
Expand Down
145 changes: 145 additions & 0 deletions compiler/rustc_data_structures/src/string_enum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/// Like an enum, but the variants are tied to a string representation.
///
/// Each variant is declared in one of three forms:
/// * `Variant => "primary"` — single canonical CLI string.
/// * `Variant => "primary" | "alias1" | "alias2"` — canonical string plus
/// one or more aliases that also parse to this variant. `to_str` and
/// `Display` return the canonical form.
/// * `Variant` (no `=>`) — the variant exists in the enum but has no CLI
/// string representation. Reachable only by code that produces the value
/// directly (e.g. a parser handling no-value or boolean fallthrough).
/// Calling `to_str` or `Display` on such a variant panics, and `FromStr`
/// will not produce it.
///
/// Any variant may also carry an explicit discriminant
/// (`Variant = N` or `Variant = N => "primary"`), forwarded verbatim to
/// the generated enum. Use this when the discriminant values are
/// load-bearing (e.g. encoded on the wire or stable-hashed).
///
/// Variants with a CLI string may also be marked `@no_from_str`
/// (`Variant => "primary" @no_from_str`), which makes the string
/// available to `to_str`/`Display` but excludes it from `FromStr`.
/// Use this for variants that have a textual identity for display
/// purposes but should not be constructible from untrusted user input
/// (e.g. variants that require out-of-band context to build correctly).
/// Such variants still appear in `STR_VARIANTS`/`ALL_STR_VARIANTS`;
/// callers that want only the strings the user is allowed to supply
/// should use `FROM_STR_VARIANTS` instead.
///
/// Generates:
/// * `VARIANTS` — every variant, in declaration order.
/// * `STR_VARIANTS` — canonical string of each variant that has one, in
/// declaration order.
/// * `ALL_STR_VARIANTS` — every accepted string (canonical + aliases) in
/// declaration order. Use this when help text should list all accepted
/// forms.
/// * `FROM_STR_VARIANTS` — canonical string of each variant whose canonical
/// string is accepted by `FromStr` (i.e. `STR_VARIANTS` minus any variant
/// marked `@no_from_str`). Use this in diagnostics that list the inputs
/// the user is actually allowed to supply.
/// * `to_str()`, `Display`, `FromStr`. `FromStr::Err` is `()` because
/// diagnostic emission is handled by the caller.
#[macro_export]
macro_rules! string_enum {
(
$(#[$meta:meta])*
$vis:vis enum $name:ident {
$(
$(#[$variant_meta:meta])*
$variant:ident $( = $disc:expr )?
$( => $repr:literal $( | $alias:literal )*
$( @ $no_from_str:ident )? )? ,
)*
}
) => {
$(#[$meta])*
$vis enum $name {
$(
$(#[$variant_meta])*
$variant $( = $disc )?,
)*
}

impl $name {
#[allow(dead_code)]
$vis const VARIANTS: &'static [Self] = &[
$( Self::$variant, )*
];
#[allow(dead_code)]
$vis const STR_VARIANTS: &'static [&'static str] = &[
$( $( $repr, )? )*
];
#[allow(dead_code)]
$vis const ALL_STR_VARIANTS: &'static [&'static str] = &[
$( $( $repr, $( $alias, )* )? )*
];
Comment on lines +68 to +75

@fmease fmease Jul 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure if this assoc const pulls its weight. After all, this information can be obtained via Self::VARIANTS.iter().map(Self::to_str).collect::<Vec<_>>() which isn't that expensive.

I'd say the fewer things we macro-generate the better to avoid negatively impacting compile times of rustc itself.

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point. STR_VARIANTS's only use here was that print_request line (now inlined). ALL_STR_VARIANTS/FROM_STR_VARIANTS aren't used in this PR at all — only by later PRs in the stack. I'll move their generation (plus @no_from_str and the FROM_STR_VARIANTS commit) down to the PR that first needs them, leaving #158123 as just the macro core + migrations. Sound good?

#[allow(dead_code)]
$vis const FROM_STR_VARIANTS: &'static [&'static str] =
$crate::__string_enum_from_str_arr!(
@collect []
$( $( $repr $( @ $no_from_str )? , )? )*
);

#[allow(unreachable_patterns)]
$vis const fn to_str(&self) -> &'static str {
match self {
$( $( Self::$variant => $repr, )? )*
_ => panic!("variant has no CLI string representation"),
}
}
}

impl ::std::fmt::Display for $name {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
::std::fmt::Display::fmt(self.to_str(), f)
}
}

impl ::std::str::FromStr for $name {
type Err = ();

#[allow(unreachable_code)]
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
$( $( $repr $( | $alias )* => {
$(
$crate::__string_enum_check_no_from_str!($no_from_str);
return Err(());
)?
Ok(Self::$variant)
}, )? )*
_ => Err(()),
}
}
}
}
}

/// Validates that a `string_enum!` variant's `@`-marker is spelled exactly
/// `no_from_str`. Used internally by [`string_enum!`]; not part of the public
/// surface.
#[doc(hidden)]
#[macro_export]
macro_rules! __string_enum_check_no_from_str {
(no_from_str) => {};
}

/// Builds a `&[&str]` literal containing only the canonical strings of
/// variants accepted by `FromStr`. Token-tree munches a comma-terminated
/// stream of `$repr` (or `$repr @ no_from_str`) entries into an accumulator,
/// then emits the full slice literal in one go — needed because in
/// expression position a macro must expand to a single expression. Used
/// internally by [`string_enum!`]; not part of the public surface.
#[doc(hidden)]
#[macro_export]
macro_rules! __string_enum_from_str_arr {
(@collect [$($acc:literal,)*]) => {
&[ $($acc,)* ]
};
(@collect [$($acc:literal,)*] $repr:literal @ no_from_str , $($rest:tt)*) => {
$crate::__string_enum_from_str_arr!(@collect [$($acc,)*] $($rest)*)
};
(@collect [$($acc:literal,)*] $repr:literal , $($rest:tt)*) => {
$crate::__string_enum_from_str_arr!(@collect [$($acc,)* $repr,] $($rest)*)
};
}
4 changes: 2 additions & 2 deletions compiler/rustc_driver_impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ fn print_crate_info(
continue;
}
let level = builder.lint_level_spec(lint).level();
println_info!("{}={}", lint.name_lower(), level.as_str());
println_info!("{}={}", lint.name_lower(), level.to_str());
}
}
Cfg => {
Expand Down Expand Up @@ -1027,7 +1027,7 @@ Available lint options:
safe_println!(
" {} {:7.7} {}",
padded(&name),
lint.default_level(sess.edition()).as_str(),
lint.default_level(sess.edition()).to_str(),
lint.desc
);
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_errors/src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ impl Emitter for JsonEmitter {
}

fn emit_unused_externs(&mut self, lint_level: rustc_lint_defs::Level, unused_externs: &[&str]) {
let lint_level = lint_level.as_str();
let lint_level = lint_level.to_str();
let data = UnusedExterns { lint_level, unused_extern_names: unused_externs };
let result = self.emit(EmitTyped::UnusedExtern(data));
if let Err(e) = result {
Expand Down
40 changes: 22 additions & 18 deletions compiler/rustc_hir/src/attrs/data_structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,24 +681,28 @@ impl<E: rustc_span::SpanEncoder> rustc_serialize::Encodable<E> for DocAttribute
}
}

/// How to perform collapse macros debug info
/// if-ext - if macro from different crate (related to callsite code)
/// | cmd \ attr | no | (unspecified) | external | yes |
/// | no | no | no | no | no |
/// | (unspecified) | no | no | if-ext | yes |
/// | external | no | if-ext | if-ext | yes |
/// | yes | yes | yes | yes | yes |
#[derive(Copy, Clone, Debug, Hash, PartialEq)]
#[derive(StableHash, Encodable, Decodable, PrintAttribute)]
pub enum CollapseMacroDebuginfo {
/// Don't collapse debuginfo for the macro
No = 0,
/// Unspecified value
Unspecified = 1,
/// Collapse debuginfo if the macro comes from a different crate
External = 2,
/// Collapse debuginfo for the macro
Yes = 3,
rustc_data_structures::string_enum! {
/// How to perform collapse macros debug info
/// if-ext - if macro from different crate (related to callsite code)
/// | cmd \ attr | no | (unspecified) | external | yes |
/// | no | no | no | no | no |
/// | (unspecified) | no | no | if-ext | yes |
/// | external | no | if-ext | if-ext | yes |
/// | yes | yes | yes | yes | yes |
#[derive(Copy, Clone, Debug, Hash, PartialEq)]
#[derive(StableHash, Encodable, Decodable, PrintAttribute)]
pub enum CollapseMacroDebuginfo {
/// Don't collapse debuginfo for the macro. Reachable only via boolean
/// false (`no`, `off`, `false`, etc.).
No = 0,
/// Unspecified value. Reachable only as the default when the flag is
/// not passed; never CLI-settable.
Unspecified = 1,
/// Collapse debuginfo if the macro comes from a different crate
External = 2 => "external",
/// Collapse debuginfo for the macro. Reachable only via boolean true.
Yes = 3,
}
}

/// Crate type, as specified by `#![crate_type]`
Expand Down
6 changes: 3 additions & 3 deletions compiler/rustc_lint/src/levels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ where
self.sess.dcx().emit_err(OverruledAttribute {
span: src.span(),
overruled: src.span(),
lint_level: level.as_str(),
lint_level: level.to_str(),
lint_source: src.name(),
sub,
});
Expand All @@ -629,7 +629,7 @@ where
src.span().into(),
OverruledAttributeLint {
overruled: src.span(),
lint_level: level.as_str(),
lint_level: level.to_str(),
lint_source: src.name(),
sub,
},
Expand Down Expand Up @@ -928,7 +928,7 @@ where
UNUSED_ATTRIBUTES,
lint_attr_span.into(),
IgnoredUnlessCrateSpecified {
level: level_spec.level().as_str(),
level: level_spec.level().to_str(),
name: lint_attr_name,
},
);
Expand Down
98 changes: 38 additions & 60 deletions compiler/rustc_lint_defs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,70 +146,48 @@ impl From<StableLintExpectationId> for LintExpectationId {
}
}

/// Setting for how to handle a lint.
///
/// See: <https://doc.rust-lang.org/rustc/lints/levels.html>
#[derive(
Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash, Encodable, Decodable, StableHash
)]
pub enum Level {
/// The `allow` level will not issue any message.
Allow,
/// The `expect` level will suppress the lint message but in turn produce a message
/// if the lint wasn't issued in the expected scope. `Expect` should not be used as
/// an initial level for a lint.
///
/// Note that this still means that the lint is enabled in this position and should
/// be emitted, this will in turn fulfill the expectation and suppress the lint.
///
/// See RFC 2383.
rustc_data_structures::string_enum! {
/// Setting for how to handle a lint.
///
/// Requires a [`LintExpectationId`] to later link a lint emission to the actual
/// expectation. It can be ignored in most cases.
Expect,
/// The `warn` level will produce a warning if the lint was violated, however the
/// compiler will continue with its execution.
Warn,
/// This lint level is a special case of [`Warn`], that can't be overridden. This is used
/// to ensure that a lint can't be suppressed. This lint level can currently only be set
/// via the console and is therefore session specific.
///
/// Requires a [`LintExpectationId`] to fulfill expectations marked via the
/// `#[expect]` attribute, that will still be suppressed due to the level.
ForceWarn,
/// The `deny` level will produce an error and stop further execution after the lint
/// pass is complete.
Deny,
/// `Forbid` is equivalent to the `deny` level but can't be overwritten like the previous
/// levels.
Forbid,
/// See: <https://doc.rust-lang.org/rustc/lints/levels.html>
#[derive(
Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash, Encodable, Decodable, StableHash
)]
pub enum Level {
/// The `allow` level will not issue any message.
Allow => "allow",
/// The `expect` level will suppress the lint message but in turn produce a message
/// if the lint wasn't issued in the expected scope. `Expect` should not be used as
/// an initial level for a lint.
///
/// Note that this still means that the lint is enabled in this position and should
/// be emitted, this will in turn fulfill the expectation and suppress the lint.
///
/// See RFC 2383.
///
/// Requires a [`LintExpectationId`] to later link a lint emission to the actual
/// expectation. It can be ignored in most cases.
Expect => "expect" @no_from_str,
/// The `warn` level will produce a warning if the lint was violated, however the
/// compiler will continue with its execution.
Warn => "warn",
/// This lint level is a special case of [`Warn`], that can't be overridden. This is used
/// to ensure that a lint can't be suppressed. This lint level can currently only be set
/// via the console and is therefore session specific.
///
/// Requires a [`LintExpectationId`] to fulfill expectations marked via the
/// `#[expect]` attribute, that will still be suppressed due to the level.
ForceWarn => "force-warn" @no_from_str,
/// The `deny` level will produce an error and stop further execution after the lint
/// pass is complete.
Deny => "deny",
/// `Forbid` is equivalent to the `deny` level but can't be overwritten like the previous
/// levels.
Forbid => "forbid",
}
}

impl Level {
/// Converts a level to a lower-case string.
pub fn as_str(self) -> &'static str {
match self {
Level::Allow => "allow",
Level::Expect => "expect",
Level::Warn => "warn",
Level::ForceWarn => "force-warn",
Level::Deny => "deny",
Level::Forbid => "forbid",
}
}

/// Converts a lower-case string to a level. This will never construct the expect
/// level as that would require a [`LintExpectationId`].
pub fn from_str(x: &str) -> Option<Self> {
match x {
"allow" => Some(Level::Allow),
"warn" => Some(Level::Warn),
"deny" => Some(Level::Deny),
"forbid" => Some(Level::Forbid),
"expect" | _ => None,
}
}

/// Converts an `Option<Symbol>` to a level.
pub fn from_opt_symbol(s: Option<Symbol>) -> Option<Self> {
s.and_then(Self::from_symbol)
Expand Down
4 changes: 2 additions & 2 deletions compiler/rustc_middle/src/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ fn explain_lint_level_source(
}
match src {
LintLevelSource::Default => {
let level_str = level.as_str();
let level_str = level.to_str();
match lint_group_name(lint) {
Some(group_name) => {
err.note_once(format!("`#[{level_str}({name})]` (part of `#[{level_str}({group_name})]`) on by default"));
Expand Down Expand Up @@ -354,7 +354,7 @@ fn explain_lint_level_source(
}
err.span_note_once(span, "the lint level is defined here");
if lint_attr_name.as_str() != name {
let level_str = level.as_str();
let level_str = level.to_str();
err.note_once(format!(
"`#[{level_str}({name})]` implied by `#[{level_str}({lint_attr_name})]`"
));
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_pattern_analysis/src/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ pub(crate) fn lint_nonexhaustive_missing_variants<'p, 'tcx>(
span: arm.pat.data().span,
lint_span: level_spec.src.span(),
suggest_lint_on_match: rcx.whole_match_span.map(|span| span.shrink_to_lo()),
lint_level: level.as_str(),
lint_level: level.to_str(),
lint_name: "non_exhaustive_omitted_patterns",
});
}
Expand Down
Loading
Loading