Skip to content
Merged
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
94 changes: 94 additions & 0 deletions compiler/rustc_builtin_macros/src/deriving/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ fn show_fieldless_enum(
def: &EnumDef,
type_ident: Ident,
) -> BlockOrExpr {
if let Some(expr) = show_fieldless_enum_concat_str(cx, span, def, type_ident) {
return BlockOrExpr::new_expr(expr);
}
let fmt = cx.expr_name(span, sym::f);
let arms = def
.variants
Expand All @@ -256,3 +259,94 @@ fn show_fieldless_enum(
let fn_path_write_str = cx.std_path(span, &[sym::fmt, sym::Formatter, sym::write_str]);
BlockOrExpr::new_expr(cx.expr_call_global(span, fn_path_write_str, thin_vec![fmt, name]))
}

/// Special case for large enums with no fields. Builds:
/// ```text
/// impl ::core::fmt::Debug for A {
/// fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
/// ::core::fmt::Formatter::debug_c_like_enum_write_str(f,
/// "A B C D E F",
/// match self {
/// A::A => 0usize,
/// A::B => 2usize,
/// A::C => 4usize,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is slightly confusing that the example only has three arms, and so couldn't actually be produced. Expand it to have 6 arms, and use ... to omit repetitive lines?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not 100% sure I understood the sentence right, but I just made the example with 6 arms. I guess more explicit example doesn't hurt

/// A::D => 6usize,
/// A::E => 8usize,
/// A::F => 10usize,
/// })
/// }
/// }
/// ```
fn show_fieldless_enum_concat_str(
cx: &ExtCtxt<'_>,
span: Span,
def: &EnumDef,
type_ident: Ident,
) -> Option<Box<ast::Expr>> {
// Minimum variant count where this optimization starts to pay off.
// Initial value of 10 came from https://github.com/rust-lang/rust/pull/155452.
// Refined with the new impl in https://github.com/rust-lang/rust/pull/162535.
// Tested with thresholds 10,5,2 on rustc-perf
// and by comparing MIR sizes for various enums in GodBolt.
//
// `match` with strings generates:
// - a local for each arm
// - a basic block with 5 statements for each arm,
//
// `match` with integers to index a concatenated string generates
// - a single local for the string
// - a single local for the index
// - a basic block with 2 statements for each arm
//
// This reduces borrowck costs on larger enums.
const THRESHOLD: usize = 5;
let variants_count = def.variants.len();
if variants_count < THRESHOLD {
return None;
}
let fmt = cx.expr_name(span, sym::f);

let mut concatenated_names = String::new();

let arms = def
.variants
.iter()
.enumerate()
.map(|(i, v)| {
let name_offset = concatenated_names.len();

let name = v.ident.name.as_str();
concatenated_names.push_str(name);

let is_last = i == def.variants.len() - 1;
if !is_last {
concatenated_names.push(' ');
}

let variant_path = cx.path(span, &[type_ident, v.ident]);
let pat = match &v.data {
ast::VariantData::Tuple(fields, _) => {
debug_assert!(fields.is_empty());
cx.pat_tuple_struct(span, variant_path, ThinVec::new())
}
ast::VariantData::Struct { fields, .. } => {
debug_assert!(fields.is_empty());
cx.pat_struct(span, variant_path, ThinVec::new())
}
ast::VariantData::Unit(_) => cx.pat_path(span, variant_path),
};

cx.arm(span, pat, cx.expr_usize(span, name_offset))
})
.collect::<ThinVec<_>>();

let names_str_body = cx.expr_str(span, Symbol::intern(&concatenated_names));

let variant_index_expr = cx.expr_match(span, cx.expr_self(span), arms);

let fn_path = cx.std_path(span, &[sym::fmt, sym::Formatter, sym::debug_c_like_enum_write_str]);
let call_expr =
cx.expr_call_global(span, fn_path, thin_vec![fmt, names_str_body, variant_index_expr]);

Some(call_expr)
}
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,7 @@ symbols! {
debug_assert_macro,
debug_assert_ne_macro,
debug_assertions,
debug_c_like_enum_write_str,
debug_struct_fields_finish,
debug_tuple_fields_finish,
debugger_visualizer,
Expand Down
13 changes: 13 additions & 0 deletions library/core/src/fmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2619,6 +2619,19 @@ impl<'a> Formatter<'a> {
builder.finish()
}

/// Shrinks `derive(Debug)` code, for faster compilation and smaller binaries.
/// For C-like enums with concatenated variant name strings.
/// Names should be separated by space.
#[doc(hidden)]
#[unstable(feature = "fmt_helpers_for_derive", issue = "none")]
pub fn debug_c_like_enum_write_str<'b>(
&'b mut self,
names: &str,
start_index: usize,
) -> Result {
self.write_str(names[start_index..].split(' ').next().unwrap())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If you passed the start_index and the end_index you could just get the substring with names[start_index..end_index] and avoid the iterator and also the spaces between the variant names.

It would require the match returning a pair of indices (or a Range)... not sure if it would be a win overall.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think that would erase big part of the win. This version has 2 statements per block, but two index version would have at least 4 I think, I'd have to check.

There's a way to do this by packing the two indexes into one number, but it felt like a pretty convoluted solution just for the debug impl. I'll maybe try it later though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe this line causes a significant binary size regression, particularly on embedded, because it pulls in panic and formatting machinery.

It is not guaranteed that the start_index is in bounds, and especially not that it is at a valid unicode boundary.

In https://godbolt.org/z/z8rTKe6j6 (LLM-generated but the issue is pretty clear), when using "Link to binary", the output size grows from ~1000 lines to ~2600.

Putting both sides into a diff viewer, I see that slice_error_fail_rt is now included, as is core::unicode::unicode_data::white_space::lookup and all of CharSearcher and a bunch of char methods.

In many applications those might be included anyway, but especially on embedded (or wasm) binary size is a real constraint. This was reported in #165189.

@panstromek panstromek Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yea, I think the split is an issue. I'm doing some experiments in #165179 to fix this (and improve perf). #165140 should help as well (it deduplicates those helper functions), that one is already in the queue

}

/// Creates a `DebugTuple` builder designed to assist with creation of
/// `fmt::Debug` implementations for tuple structs.
///
Expand Down
20 changes: 20 additions & 0 deletions tests/ui/derives/debug/derive-Debug-enum-variants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ enum Bar {
D,
}

#[derive(Debug)]
enum Large {
A,
Bar,
C,
Distance,
Emily,
F,
G,
}

pub fn main() {
// Test variant with data
let foo_a = Foo::A(22);
Expand All @@ -27,4 +38,13 @@ pub fn main() {

// Test unit variant from different enum
assert_eq!("D".to_string(), format!("{:?}", Bar::D));

// Test large c-like enum, those use a different implementation.
assert_eq!("A".to_string(), format!("{:?}", Large::A));
assert_eq!("Bar".to_string(), format!("{:?}", Large::Bar));
assert_eq!("C".to_string(), format!("{:?}", Large::C));
assert_eq!("Distance".to_string(), format!("{:?}", Large::Distance));
assert_eq!("Emily".to_string(), format!("{:?}", Large::Emily));
assert_eq!("F".to_string(), format!("{:?}", Large::F));
assert_eq!("G".to_string(), format!("{:?}", Large::G));
}
23 changes: 23 additions & 0 deletions tests/ui/derives/deriving-all-codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,29 @@ enum Fieldless {
C,
}

// A C-like, fieldless enum with variants of varying name lengths.
#[derive(Debug)]
enum Fieldless0 {
A,
BBB,
CC,
}

// A C-like, fieldless enum with 10 variants.
#[derive(Debug)]
enum Fieldless10 {
AAAAA,
BBBB,
CC,
DDDDDDDD,
E,
FFFFFFFFFFFFF,
GGGGGG,
Hatsune,
IIIIIII,
JJJJJJJJJ,
}

// An enum with multiple fieldless and fielded variants.
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
enum Mixed {
Expand Down
49 changes: 49 additions & 0 deletions tests/ui/derives/deriving-all-codegen.stdout
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,55 @@ impl ::core::cmp::Ord for Fieldless {
}
}

// A C-like, fieldless enum with variants of varying name lengths.
enum Fieldless0 { A, BBB, CC, }
#[automatically_derived]
impl ::core::fmt::Debug for Fieldless0 {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
Fieldless0::A => "A",
Fieldless0::BBB => "BBB",
Fieldless0::CC => "CC",
})
}
}

// A C-like, fieldless enum with 10 variants.
enum Fieldless10 {
AAAAA,
BBBB,
CC,
DDDDDDDD,
E,
FFFFFFFFFFFFF,
GGGGGG,
Hatsune,
IIIIIII,
JJJJJJJJJ,
}
#[automatically_derived]
impl ::core::fmt::Debug for Fieldless10 {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_c_like_enum_write_str(f,
"AAAAA BBBB CC DDDDDDDD E FFFFFFFFFFFFF GGGGGG Hatsune IIIIIII JJJJJJJJJ",
match self {
Fieldless10::AAAAA => 0usize,
Fieldless10::BBBB => 6usize,
Fieldless10::CC => 11usize,
Fieldless10::DDDDDDDD => 14usize,
Fieldless10::E => 23usize,
Fieldless10::FFFFFFFFFFFFF => 25usize,
Fieldless10::GGGGGG => 39usize,
Fieldless10::Hatsune => 46usize,
Fieldless10::IIIIIII => 54usize,
Fieldless10::JJJJJJJJJ => 62usize,
})
}
}

// An enum with multiple fieldless and fielded variants.
enum Mixed {

Expand Down
Loading