Skip to content
Open
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
89 changes: 84 additions & 5 deletions src/v0.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use core::convert::TryFrom;
use core::{char, fmt, iter, mem, str};

// As of 2026, our custom formatting code is significantly faster than using `write!`.
#[allow(unused_macros)]
macro_rules! write {
($($ignored:tt)*) => {
Expand Down Expand Up @@ -395,6 +396,41 @@ fn basic_type(tag: u8) -> Option<&'static str> {
})
}

/// Wrapper type that ensures disambiguators are formatted correctly.
#[derive(Copy, Clone, Eq, PartialEq)]
struct Disambiguator(u64);

impl Disambiguator {
/// The zero-fill required for a minimum-length disambiguator ("0" to "f").
const MAX_HEX_FILL: &str = "000000000000000";

/// Creates a new disambiguator from a u64.
fn new(disambiguator: u64) -> Self {
Self(disambiguator)
}

/// Formats a fixed-length 16 character hex representation of the disambiguator.
fn fmt_hex(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
// Handle the edge case of a zero disambiguator, where the bit width is zero, but the
// format is "0" (not the empty string).
// This code is about 4% faster than `write!(out, "{:016x}", self.0)`.
let hex_len = self.0.bit_width().div_ceil(4).max(1);
let fill_len = 16 - (hex_len as usize);
out.write_str(&Self::MAX_HEX_FILL[0..fill_len])?;
fmt::LowerHex::fmt(&self.0, out)

@bjorn3 bjorn3 Jul 31, 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.

write!(out, "{:016x}", self.0) would work too, right?

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.

Unfortunately this is core-only code:

macro_rules! write {
($($ignored:tt)*) => {
compile_error!(
"use `self.print(value)` or `fmt::Trait::fmt(&value, self.out)`, \
instead of `write!(self.out, \"{...}\", value)`"
)
};
}

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.

write!() is in core. I don't know if the reason given for avoiding write!() in #50 is still fully valid given that we had a change to the fmt implementation since:

The second commit is mostly stylistic, trying to enforce a pattern of "not using write! in demangling logic", and only using fmt::Trait::fmt(&value, self.out) - which I'm guessing we do want (for performance reasons?).

In any case I guess the current code is fine.

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 have a slight preference for the standard fixed-width code, it's more maintainable and less likely to have bugs.

But my custom code benchmarks at:

test v0::tests::bench_demangle_crate_with_zero_disambiguator ... bench:         173.72 ns/iter (+/- 8.86)
test v0::tests::bench_short_crate_disambiguator_bug          ... bench:       3,842.77 ns/iter (+/- 69.09)

And format_args! (the expansion of write!) is slightly slower:

test v0::tests::bench_demangle_crate_with_zero_disambiguator ... bench:         175.11 ns/iter (+/- 6.15)
test v0::tests::bench_short_crate_disambiguator_bug          ... bench:       4,005.31 ns/iter (+/- 240.34)

Specifically, it expands to out.write_fmt(core::format_args!("{:016x}", self.0)).

I'm using a MacOS M1 laptop, so I don't know if these results carry across. But it seems like custom code may still be a winner over write!. I'll add a comment to explain this, and push an update.

}

/// Formats a variable-length decimal representation of the disambiguator.
fn fmt_decimal(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, out)
}

/// Returns true if the disambiguator is zero.
fn is_zero(&self) -> bool {
self.0 == 0
}
}

struct Parser<'s> {
sym: &'s str,
next: usize,
Expand Down Expand Up @@ -489,8 +525,8 @@ impl<'s> Parser<'s> {
self.integer_62()?.checked_add(1).ok_or(ParseError::Invalid)
}

fn disambiguator(&mut self) -> Result<u64, ParseError> {
self.opt_integer_62(b's')
fn disambiguator(&mut self) -> Result<Disambiguator, ParseError> {
self.opt_integer_62(b's').map(Disambiguator::new)
}

fn namespace(&mut self) -> Result<Option<char>, ParseError> {
Expand Down Expand Up @@ -803,9 +839,9 @@ impl<'a, 'b, 's> Printer<'a, 'b, 's> {

self.print(name)?;
if let Some(out) = &mut self.out {
if !out.alternate() && dis != 0 {
if !out.alternate() && !dis.is_zero() {
out.write_str("[")?;
fmt::LowerHex::fmt(&dis, out)?;
dis.fmt_hex(out)?;
out.write_str("]")?;
}
}
Expand Down Expand Up @@ -841,7 +877,9 @@ impl<'a, 'b, 's> Printer<'a, 'b, 's> {
self.print(name)?;
}
self.print("#")?;
self.print(dis)?;
if let Some(out) = &mut self.out {
dis.fmt_decimal(out)?;
}
self.print("}")?;
}

Expand Down Expand Up @@ -1616,4 +1654,45 @@ mod tests {

assert_contains!(::demangle(&sym).to_string(), "{recursion limit reached}");
}

#[test]
fn short_crate_disambiguator_bug() {
// Cover 1 & 2 character disambiguators.
// A zero value is impossible because the parsing code adds 1 twice.
t!("_RNvCs0_5basic4main", "basic[0000000000000002]::main");
t!("_RNvCs1_5basic4main", "basic[0000000000000003]::main");
t!("_RNvCsd_5basic4main", "basic[000000000000000f]::main");
t!("_RNvCse_5basic4main", "basic[0000000000000010]::main");
// This is not the canonical format, but it works
t!(
"_RNvCs0000000000Z_5basic4main",
"basic[000000000000003f]::main"
);
// Cover 15 and 16 character disambiguators
t!(
"_RNvCs0ZZZZZZZZZZ_5basic4main",
"basic[0ba5ca5392cb0401]::main"
);
t!(
"_RNvCs1naolCOL8Qt_5basic4main",
"basic[0fffffffffffffff]::main"
);
t!(
"_RNvCs1naolCOL8Qu_5basic4main",
"basic[1000000000000000]::main"
);
t!(
"_RNvCslYGhA16ahyd_5basic4main",
"basic[ffffffffffffffff]::main"
);
// Real-world test failure, see rust-lang/rust#160050
t!(
"_RMse_NvCsiksvdpJ6Jnj_14splat_mangling4mainINtB3_4TypeINtNtCsmKzDxHvwyd_5alloc5boxed3BoxFmwTFuEuaEdEuEE",
"<splat_mangling[d580343ed36ed69b]::main::Type<alloc[04462ec5c505d9a3]::boxed::Box<fn(u32, #[splat] (fn(()), i8), f64)>>>"
);
t!(
"_RMsf_NvCsiksvdpJ6Jnj_14splat_mangling4mainINtB3_4TypeINtNtCsmKzDxHvwyd_5alloc5boxed3BoxFmTFuEuaEdEuEE",
"<splat_mangling[d580343ed36ed69b]::main::Type<alloc[04462ec5c505d9a3]::boxed::Box<fn(u32, (fn(()), i8), f64)>>>"
);
}
}
Loading