Conformance¶
Correctness is defined against the reference implementations and measured on symbols real compilers actually emit, not on hand-picked examples.
Every corpus count here is pinned exactly, in tests/test_conformance.py or in the
scheme's own test module (tests/test_rust.py for the Rust vectors, tests/test_d.py
for D's), so an improvement cannot quietly mask a regression, and tests/test_readme.py
checks this page against those pins. The whole symbol tables
are live runs, not pinned counts.
Contents — checked-in corpora · notes on the corpora · whole symbol tables · printing the name without its signature · on trusting the references · schemes not covered
Checked-in corpora¶
Replayed by the test suite. No compiler and no reference demangler needed.
| Corpus | Reference | Exact |
|---|---|---|
| Real shipped libstdc++ | llvm-cxxfilt 18.1.3 |
5913 / 5913 |
| Rust, both schemes | rustfilt 0.2.1 (rustc-demangle 0.1.x) |
5316 / 5316 |
| MSVC — LLVM's own test corpus | llvm-undname 18.1.3 |
609 / 609 |
| MSVC RTTI type descriptors, both forms | llvm-undname 18.1.3 |
106 / 106 |
| MSVC ARM64EC hybrid names | LLVM's own mangling rule 4 | 606 / 606 |
MSVC — the five flags llvm-undname has, over the differences they make |
llvm-undname 18.1.3 |
1250 / 1253 2 |
MSVC — the mask bits llvm-undname has no flag for 1 |
dbghelp.dll 10.0.26100.8328 |
1064 / 1064 |
Rust toolchain (rustc_driver, libstd) |
rustfilt 0.2.1 |
394 / 394 |
| Purpose-built C++, llvm style | llvm-cxxfilt 18.1.3 |
318 / 318 |
| Purpose-built C++, gnu style | GNU c++filt 2.42 |
311 / 311 |
Reported names, one file per scheme in tests/conformance/reported/ |
the reference each report cites | every entry |
| Names a reference reads wrongly 17 | the declaration | 37 / 37 |
Bare <type> encodings, llvm style |
llvm-cxxfilt --types 18.1.3 |
1076 / 1076 |
Bare <type> encodings, gnu style |
GNU c++filt -t 2.42 |
1076 / 1076 3 |
| Swift runtime + the compiler's own test corpus | swift-demangle, built from source 6 |
8494 / 8494 |
| Swift — the compiler's own demangler vectors | swift-demangle, built from source 6 |
531 / 531 |
| Nim 1.6 and 2.2, against the compiler's own record 14 | .ndi debug mapping |
2115 / 2115 |
| Free Pascal 3.2.2 runtime and packages 5 | re-assembly + ppudump |
3899 / 3899 |
| Go, from the shipped toolchain | round trip 16 | 1517 / 1517 |
| D, from the shipped libgphobos | GNU c++filt --format=dlang |
1257 / 1257 |
| Objective-C, three ABIs, against the declaration 9 | clang 18.1.3 + libobjc.a |
2665 / 2665 |
| Swift, simplified spelling — the compiler's own vectors | swift-demangle --simplified 6 |
217 / 217 |
| Swift names the reference reads wrongly 6 | the demangling tree | 5 / 5 |
| Delphi/C++Builder, against Embarcadero's unmangler 13 | recorded tdump -um |
11363 / 11363 |
Pre-Itanium C++ — libiberty's own vectors, both DMGL_PARAMS settings 10 |
libiberty cplus-dem.c (GCC 8.3.0) |
1324 / 1324 |
| CodeWarrior — the reference's own vectors 11 | cwdemangle 1.0 |
47 / 47 |
| Ada/GNAT — libiberty's own vectors 12 | GNU c++filt --format=gnat |
34 / 34 |
JNI native method names, from real native declarations 15 |
round trip 15 | 50 / 50 |
Replayed and pinned the same way: more of the references' own vectors, and more samples of what compilers shipped.
| Corpus | Reference | Exact |
|---|---|---|
LLVM's own Itanium vectors, DemangleTestCases.inc 21 |
LLVM's demangler | 29913 / 29928 |
Rust — rustc-demangle's own #[test] vectors 23 |
rustc-demangle's own assertions | 49 / 51 |
MSVC — real compiler output, clang++ --target=x86_64-pc-windows-msvc |
llvm-undname 18.1.3 |
161 / 161 |
| MSVC — Boost 1.84's NuGet packages, a sample 19 | llvm-undname 18.1.3 |
5843 / 5843 |
MSVC names llvm-undname reads wrongly |
the declaration | 9 / 9 |
| D — libiberty's own vectors 22 | GNU c++filt --format=dlang |
366 / 366 |
| Pre-Itanium C++ from gcc 2.95 binaries 20 | libiberty cplus-dem.c (GCC 8.3.0), gnu style |
12661 / 12661 |
| Ada/GNAT, from the shipped libgnat and libgnarl | GNU c++filt --format=gnat |
1438 / 1438 |
| Delphi/C++Builder, a per-kind sample of the whole dump | recorded tdump -um |
68 / 68 |
| Delphi/C++Builder constructs the export tables lack, hand-built 13 | an independent unmangler | 53 / 53 |
Notes on the corpora¶
What the numbered marks in the tables above point at, and the ones in whole symbol tables below. Each says what a row is measured against where that is not simply "a reference demangler said so", or what the names it does not account for are.
1. The dbghelp mask bits¶
UnDecorateSymbolName with the mask set, driven over the same 609 names by
tools/generate_msvc_dbghelp_corpus.py. The two references do not spell a name alike —
dbghelp prints __ptr64, writes char * const, and puts no space after a comma — so
its answer goes through eight spacing rewrites before it is recorded, and each rewrite is
proved on every name it is used for: a name is in the corpus only if rewriting the
reference's unflagged answer reproduces the row msvc-llvm-corpus.txt already pins
against llvm-undname. 478 of the 609 qualified; the reference declines four, and the
other 127 are names where the two disagree about spelling rather than spacing —
char *const __restrict against char *__restrict const is an ordering, not a gap —
and inventing a rule to reconcile those would be inventing a spelling. Cross-checking the
four bits that do mean what an llvm-undname flag means found the two references
disagreeing about how far a flag reaches; UNDNAME_REACH_DIVERGENCES in
tests/test_conformance.py names one of each.
2. Where a dropped return type leaves a bracket open¶
The three are names where llvm-undname --no-return-type leaves an unclosed
bracket — int (__cdecl * (__cdecl B::*volatile memptrtofun7)(void) is not a declaration
of anything. This keeps the balanced spelling, which is what the reference itself prints
with no flag.
3. Bare types under both references¶
The same 1,076 encodings under both references, which spell them differently: each
style reads every row of its own reference's corpus. Three rows carry a doubled KK
cv-qualifier, which c++filt folds away and llvm-cxxfilt keeps. No compiler emits
the literal KK, but the same doubling arrives through an already-qualified template
argument, and the shipped libLLVM has three of those.
4. ARM64EC hybrid names¶
Nothing demangles ARM64EC's $$h marker — llvm-undname refuses these and so does
current upstream — so there is no reference spelling to copy. There is a normative
rule: LLVM's getArm64ECDemangledFunctionName says what an ARM64EC name is the hybrid
form of, and it is what the compiler emits an EXPORTAS directive against, so its answer
is the name the linker resolves. Every name in LLVM's own corpus with the marker inserted
where LLVM's mangler puts it must demangle to what the name without it demangles to —
and that column came from llvm-undname.
5. Free Pascal¶
Free Pascal ships no demangler either. The property is re-assembly — the parts this
splits out, rejoined with the compiler's own separators, must reproduce the symbol — and
it holds for all 236,570 readable symbols in the shipped runtime, not only the sample
above. Independently, ppudump prints both a unit's mangled names and the names it
declares, and every name read is one the unit declares. Case is not recoverable: Pascal
is case-insensitive and the compiler upper-cases before mangling.
6. The Swift reference is built here¶
Nothing a distribution ships reads a Swift name — llvm-cxxfilt and c++filt both
decline a $s outright — so tools/swift-demangle-reference/ builds swiftlang/swift's
own lib/Demangling at a pinned commit on main. Its
README
has the build and the measurements behind that choice of revision. The one row where
following it would be the defect — NodePrinter reads an extended existential shape one
child too high and spells the type as <null node pointer>, a path its own test corpus
never exercises — is pinned in tests/conformance/swift-reference-defects.txt against
what the tree says instead. The 8494 in the summary is the whole real-world corpus; the
main build reads 8493 of them, because it refuses the Tgm5 specialisation, whose row
carries the spelling Swift 5.10.1 gives it.
7. Swift symbolic references¶
A mangled name in Swift metadata can hold a one-byte marker and a four-byte offset in
place of a type the image already describes, so reading one needs the image:
swift.demangle_symbolic takes bytes and a resolver, and resolve.ContextResolver is
one. Each reference resolves to a descriptor; the check is that the symbol the linker
put at that address demangles to the same name, with swift-demangle reading both
sides. Splicing each resolved fragment back in gives a self-contained name the reference
can read, and it agrees with our spelling on 4,528 of 4,528. A shared object's
indirect references point at pointer slots the loader fills, so resolve.elf_image
applies the dynamic relocations first and answers a slot filled from another image by
the symbol's own name, which is the mangling a descriptor's symbol carries; the same
symbols stand in where the walk declines, at a type declared in an extension or an
opaque type descriptor; an anonymous context, the scope of a type declared inside a
function, is spelled (unknown context at $<address>) as the runtime spells it. Over
the 6.1.2 runtime that resolves 7,071 typerefs: 6,811 spell what the reference spells
for the spliced form, and the other 260 are names the reference cannot be given, because
a fragment spliced into a name shifts the substitution indices and word substitutions
around it — checked one by one.
8. How Apple clang numbers a substitution table¶
Apple's clang numbers the substitution table by a rule no other compiler uses: an
undeduced auto return type, Da, is a candidate — Clang 6.0's accident, kept by
Apple's fork in every version since — so every back-reference after one is one higher
than GCC or upstream clang would write, and llvm-cxxfilt and c++filt read every such
Mach-O name by the wrong entries. Of the 17,310 names carrying a Da in the bottles,
2,300 overrun the table under the common rule and 6,385 come back as a plausible wrong
declaration; this reads a __Z name by Apple's rule, retries the other when a
back-reference overruns or names something no type can be, and takes
ItaniumOptions.undeduced_auto_substitution to force either. Checked against clang 18
under -fclang-abi-compat=6, which writes the same numbering, and against upstream
clang targeting Darwin, which does not.
Of the 12,350 names in the first batch of bottles where this and llvm-cxxfilt -_ part,
4,690 are that rule and 7,660 the template-parameter rebinding
17 describes, 7,573 of
them one generic lambda in ceres. Not counted in the 12,350: 67 names neither reads,
which are $tlv$init, the thread-local initialiser a Mach-O linker names after its
variable. A second batch — Arrow, DuckDB, RocksDB, gRPC, Cap'n Proto, libtorrent,
Xerces-C, RE2, libomp and Boost.Python, 295,281 more names — parts from
llvm-cxxfilt -_ on 6,134: 677 are Apple's closure-prefix rule, described below, and
every other one either the auto rule or the rebinding. Arrow's VisitVoid joins the
reference-defects corpus, checked against its header, and so do a RocksDB and a DuckDB
closure under the closure-prefix rule. Not counted in the 6,134 either: 74 more
$tlv$init, and 22 gRPC promise types whose spelling runs past the 64K
Limits.max_output, so this refuses them by default — one is 69,094 characters under
RELAXED_LIMITS, and the reference refuses it outright.
The other place two compilers number the same name differently is a lambda in a
variable's or a member's initialiser. Its name goes through the variable —
ns::g3::'lambda'(...), written 2ns2g3M... — and the prefix before that M is a
substitution candidate under the ABI, upstream clang and GCC 13; GCC 12 and every
version before it wrote the M and skipped the entry, and so does Apple's clang in
every version, so every later back-reference is one lower. GCC 13 still emits the old
spelling as an alias beside the new. Read by the ABI's rule, such a name's references
resolve one entry early, and llvm-cxxfilt, LLVM's main branch and c++filt all print
operator()(ns::Box, ns::Box) for a lambda declared over ns::Box<int> — a template
with no arguments, standing as a type — and, for the 663 such names in Homebrew's
bottles of Apache Arrow, DuckDB and RocksDB, std::function's allocator as an allocator
of the member the lambda initialised. This reads a __Z name by Apple's rule and a
_Z name by the ABI's, and where a back-reference then runs past the table or lands on
something no type can be — a closure prefix, or a template with no arguments after it
— reads the name again under the other; ItaniumOptions.closure_prefix_substitution
forces either. Established against g++ 13 under -fabi-version=17 and 18, against
clang 18 targeting Linux and Darwin, and against the bottles' own symbols.
9. Objective-C¶
Objective-C has no reference demangler, and barely a mangling: what there is comes from
the compiler rather than the language, so the rules are transcribed from clang's
Mangle.cpp, CGObjCMac.cpp and CGObjCGNU.cpp. The expected column is not this
library's own output but what the declaration said — every symbol was emitted by clang
for Objective-C this package wrote, or read out of the shipped GCC runtime. The
GNU-family method mangling is not injective, and clang says so where it writes it:
_i_A_B_c is -[A(B) c] and -[A_B c] alike. Every reading that re-mangles is found and
the preferred one is flagged ambiguous; the 26 names where the preference differs from
the declaration are listed in tests/conformance/objc-lossy.txt rather than rounded off.
10. The pre-Itanium family¶
Pre-Itanium C++ is five manglings that share one demangler: g++ before 3.0, Lucid's
lcc, the ARM/cfront encoding, HP aCC and EDG. The reference is libiberty's
cplus-dem.c at GCC 8.3.0 (GCC 8.5.0 carries the same file), and the corpus is the 662
cases that tree's own demangle-expected marks --format=gnu, --format=lucid,
--format=arm or --format=hp — scored under both settings of DMGL_PARAMS, which is
what makes it 1,324. Nothing in one of these names says which of the five compilers
wrote it, so the style is an option and the default is gnu; a caller who knows the
binary passes demangle.style("llvm", gnuv2={"style": "arm"}). Because a name in this
family is an ordinary C identifier with a __ in it, this scheme is offered after every
other except CodeWarrior, and its detection reads the whole name rather than a prefix:
of the names in every other scheme's checked-in corpus it claims none, which
tests/test_gnuv2.py checks on every run, and over 339,117 symbols from this machine's
own shared libraries it claims none: drm_intel_gem_bo_map__wc, where wc is a valid
argument list, is left alone, though language="gnuv2" reads it as libiberty does. That
reference is built by tools/cplus-dem-reference/, whose
README
has the build. It reproduces the corpus 1,324 of 1,324, and over 420,000 mutants of it
this library never reads a name libiberty refuses; where the two part, libiberty is
spelling a gap round something it should have refused, and the README has the families.
11. CodeWarrior¶
Metrowerks CodeWarrior is the other pre-Itanium C++ mangling, and the one libiberty
never read: cplus-dem.c has no CodeWarrior flag and demangle-expected has no vectors
for it, so the reference is encounter/cwdemangle — the tool decompilation projects for
GameCube and Wii titles run, dedicated to the public domain — and the corpus is its own
test module. Detection is held to the same bar as the pre-Itanium family above: 0
claims over the other schemes' corpora and 0 over 339,117 real symbols.
Where the two pre-Itanium schemes overlap — and they do, the encodings being that close —
GNU v2 is offered first (priority 290 to CodeWarrior's 300), because a name valid under
both should go to the commoner mangling. AtEnd__13ivRubberGroup parses either way and
they differ only in spelling.
What is unambiguously CodeWarrior — a template argument list written literally into the
symbol, @LOCAL@, $localstatic, a __dt the gnu style does not know — GNU v2 refuses
and it falls through. language="codewarrior" gets the whole scheme regardless.
12. Ada and GNAT¶
Ada, as GNAT encodes it, is the last of the pre-Itanium formats libiberty still carries:
the GNU v2, lucid, ARM and HP styles were dropped and --format=gnat stayed. The
reference is ada_demangle in cplus-dem.c, with GCC's own exp_dbug.ads documenting
the encoding normatively, and the corpus is the 34 cases demangle-expected marks
--format=gnat. One of them is a name the reference declines, printing <x_E>; that is
recorded as the name unchanged, which is how this says the same thing.
Detection is the whole difficulty, because an Ada symbol carries no types and no marker:
yz__qrs is a package and a subprogram, and it is also exactly what a C program writes.
Parsing the name and claiming whatever parses reads 6,764 real symbols from this
machine's own libraries as Ada. So a name is claimed only when it carries something GNAT
wrote and a C compiler would not — _ada_, an O-operator, a TK task suffix, a P/N
protected subprogram, a stream S[RWIO], a controlled D[FA], an X body-nested marker,
a ___elabb-style special name, a _B/_E entry body, an overload number — and the
whole name is accounted for. Measured under that rule: 0 claims over every other
scheme's corpus, libcxxabi's included, and 0 over 339,117 real symbols.
The cost is that 4 of the 34 vectors — yz__qrs, x__m1, x__m3, x__y__j, lower-case
identifiers joined by __ and nothing else — are not auto-detected. They demangle under
language="ada". That is the same bargain the Go scheme makes: not claiming a name
returns it unchanged, which is what an unreadable name does anyway, while claiming
someone else's rewrites it into a plausible lie.
The reference has a defect of its own here, and it is a crash rather than a reading: see where a reference crashes.
13. Delphi and C++Builder¶
Delphi and C++Builder share one mangling, @Unit@Class@Method$qqrv. There is no Delphi
compiler on the platforms this is developed on, so the grammar is Embarcadero's own
unmangle.c — the code TDUMP, the linker and the debugger run. The reference is a
recorded one: tests/conformance/delphi-tdump.txt is a dump of the real tdump.exe
-q -um over the export tables of real BPLs and C++Builder DLLs, and its expected column
is what that unmangler printed. All 11,363 readable entries are replayed in CI, and the
10 MSVC @name@N decorations sitting in the same tables are refused — Microsoft's 32-bit
__fastcall C decoration is not this scheme. That includes what the unmangler prints
where a C++ programmer would not: SetFlat(const const bool) for qqrxo, which is
copy_args emitting the qualifier and the type spelling it again. Free Pascal's
$-delimited names are a different mangling and are read by the pascal scheme.
Those export tables do not contain every construct the grammar has: they exercise 75% of
the parser, and the missing quarter is C++Builder territory — pointer to member,
char16_t, rvalue references, __saveregs, the special tables, the Delphi 4 template
forms. tests/conformance/delphi-constructs.txt covers those with 53 hand-built names,
corroborated against an independent implementation of the same unmangler rather than
against TDUMP. That is weaker evidence, it is kept in its own file so it cannot be
read as part of the row above, and its header says so. A C++Builder .map or TDUMP dump
carrying these shapes would replace it with real evidence.
14. Nim¶
Nim has no reference demangler either, and its mangling is not injective: mangle
drops an underscore before a digit, so len0_16 and len016 are the same symbol. What
carries correctness is the same round-trip property Go uses — re-mangling what we read
must reproduce the bytes — plus agreement with the name the compiler recorded for a
debugger. Names that had an underscore before a digit cannot come back exactly, and are
left out of the corpus and listed in tests/conformance/nim-lossy.txt instead: seven
symbols, three of them the FarmHash helpers len0_16, len17_32 and len33_64 in
pure/hashes.nim and four purpose-built. Across the 5,946 routine names in the Nim 1.6
and 2.2 standard libraries eight have that shape — those three and five MySQL wrapper
routines in wrappers/mysql.nim, which are importc and so emit raw C symbols — and
they are the eight the whole-library row misses.
15. JNI¶
JNI has no reference demangler either — neither binutils, LLVM, Ghidra nor IDA reads one — but it is the only scheme here transcribed from a normative specification rather than from a reference implementation, so the check is the specification's own rule run backwards: re-encoding a reading must reproduce the symbol the compiler wrote, escapes and overload descriptor included.
16. Go¶
Go has no reference demangler — go tool nm prints symbol names with their escapes
intact and nothing in the toolchain decodes one. So the check is a property instead:
re-escaping a decoded package path must reproduce the bytes the linker wrote, where the
escaping is a transcription of Go's own objabi.PathToPrefix. It is verified over every
symbol in the shipped toolchain binaries, not just the recorded sample.
17. Where following a reference would be the defect¶
A <template-param> recorded as a substitution candidate — and any component built over
one — is the parameter, not the argument bound to it where the entry was made. The
mangler canonicalises a template type parameter by level and index, so it reuses one
entry across two different templates, and the two readings differ in any name that
mentions a local entity. Freezing it prints a type the source disproves:
std::__insertion_sort<llvm::cfg::Update<llvm::BasicBlock*>*, C> taking
llvm::BasicBlock*, a generic lambda's operator()<int> taking auto, or a closure
declared [](auto x) taking int.
Settled against eleven reduced sources compiled by g++ 13.3.0 and clang++ 18.1.3 —
insort, insort_composite, generic_lambda, member_template_lambda,
prepare_execution, auto_marshall, value_init_new, division, closure_prefix,
template_template_param and inheriting_constructor — checked in under
tools/corpus_sources/reference_defects/. They give 25 of the 37 names in
tests/conformance/itanium-reference-defects.txt; two more come from the general corpus
sources and ten from shipped libraries and bottles. It is the Itanium corpus whose
expected column comes from the declaration rather than from a demangler.
tools/generate_corpus.py excludes those names, so a regeneration cannot record the
wrong answer again.
template_template_param is a different defect in the same table, and about the count
of entries rather than what one of them holds. T_ I ... E — a template template
parameter applied to arguments — is two grammar components, and 5.1.10 makes each a
candidate, so it contributes two entries. llvm-cxxfilt 18.1.3 records only the
specialisation, so every index at or after the parameter's is one out: it refuses
_Z1fI1AiEvT_IT0_ES3_S3_ outright, and answers void g<A, char>(A<char>, char<int>)
for its neighbour, where char<int> is not a type. Both g++ 13.3.0 and clang++ 18.1.3
emit the two names byte-identically for an ordinary void f(C<T>, C<T>, C<T>), and GNU
c++filt 2.42 agrees with the declarations.
inheriting_constructor is a third defect again, and the only one whose two sides each
refuse the other's output. An inheriting constructor's <base class type> — the 1C of
CI2 1C — is a <type> and so a candidate, and the two compilers disagree about
entering it: for struct D : C { using C::C; } with C(Kind, Kind), g++ 13.3.0 writes
_ZN1DCI21CENS0_4KindES1_ and clang++ 18.1.3 writes _ZN1DCI21CEN1C4KindES1_, one
declaration under two numberings. llvm-cxxfilt 18.1.3 implements clang's and refuses
g++'s outright; GNU c++filt 2.42 reads both parameter lists and then names the
constructor after the base, D::C, which is neither compiler's declaration. This reads
both, by clang's rule with a retry under g++'s, and
ItaniumOptions.inherited_constructor_substitution forces either.
llvm-cxxfilt 18.1.3 freezes a recorded <template-param>, which is why the llvm-style
rows under whole symbol tables are not 100%: over the 217,730
distinct Itanium symbols in every shared library a stock Ubuntu 24.04 ships, it differs
from this on 322. GNU c++filt 2.42 refuses 227 of those and agrees with this on 91 of
the 95 it reads. Of the four left, three are the std::once_flag::_Prepare_execution
shape, where libstdc++'s own header settles it against both references; the fourth is
a generic lambda's own parameter in libclang-cpp, which c++filt numbers auto:1&
where the llvm style writes auto& — the same reading spelled differently, and the gnu
style spells it as c++filt does.
18. The gnu style over whole libraries¶
The gnu style reads every one of the 44,093 names c++filt reads in libLLVM.so.18.1
and spells all of them byte for byte as it does. Over the 217,057 of the 217,730 Ubuntu
symbols that c++filt reads, three differ: the _Prepare_execution shape of note
17, where libstdc++'s own header
settles it against GNU. c++filt refuses 673 of those 217,730 outright, and this reads
457 of them.
Five differences from GNU are reproduced: &A::f inside a template
argument, which GNU prints without the parameter list the mangling carries; the
{default arg#1} scope of an entity declared in a default argument; a generic lambda's
auto:1; how GNU brackets an operand by kind rather than by precedence; and a const
applied to a type that already carries one, which the mangling really does say and which
GNU folds away because no declaration spells const const.
A sixth is reproduced too, though it is a slip rather than a rule. GNU omits the space
it otherwise puts between two closing angle brackets when the last template argument is
an empty pack, because libiberty decides on a field it updates on every append and does
not restore when it rewinds the separator in front of an argument that printed nothing,
so the character it tests is that separator's space. The same output shows both
spellings in one name — f<A<B<C>>, JE> comes out void f<A<B<C> >>(A<B<C> >). The
gnu style exists to reproduce c++filt, slip and all, so it does; the default
llvm style spaces neither, as llvm-cxxfilt does.
19. MSVC over whole libraries¶
436,546 of the 436,644 read exactly as llvm-undname reads them, and none reads
differently. Of the 98 left, 91 are MD5-hashed names, which both hand back as they
stand; six are local statics carrying a .0-style suffix, which the reference reads by
stopping where the name ends and saying nothing about the rest — it reads ?x@@3HAjunk
as int x — and this refuses rather than drop; and one is a ?filt$0 exception-filter
name both hand back.
Of the 471,881 from ITK, OpenCV, Qt 5, libzmq, leveldb and restbed, 467,844 read exactly
and none differently; 151 carry the deduced return type the release refuses; 3,662 are a
debug build's run-time-check data, $rtcFrameData, $rtcName$N and $rtcVarDesc after
a function's whole decorated name, which the reference reads as the function and this
refuses rather than drop, as with .0; 155 are $initializer$ variables the reference
misreads — ??ALL$initializer$@DataSpace@H5@@... comes back as
H5::DataSpace::LL$initializer$::operator[], the identifier's first letter taken for an
operator code — and this refuses; and 67 are MD5-hashed names. The record accounts for
471,879 of the 471,881.
Of Boost's 122,162, 116,870 read exactly as llvm-undname reads them and none reads
differently; 722 carry a deduced return type, ?A_P for auto and ?A_T for
decltype(auto), which the release refuses and LLVM's main branch reads as this does;
4,372 are MD5-hashed names both hand back; and 198 are $initializer$ variables neither
reads.
20. A pre-Itanium thunk for a positive delta¶
All 396 are one form: a thunk gcc 2.95 wrote as __thunk_n8_ for a positive delta, which
libiberty reads as a method named n8_setInstance. The compiler's own make_thunk is the
authority, and tests/conformance/gnuv2-real-world.txt carries 12,661 of the rest.
21. LLVM's own Itanium vectors¶
libcxxabi/test/DemangleTestCases.inc is what the demangler behind llvm-cxxfilt is
tested against, so this row is that reference measuring itself. Fifteen are left, none of
them a name read wrongly, and tests/test_conformance.py names each one:
- Four are bare
<type>manglings with no_Z—i,PKFvRiE— refused as symbols on purpose, asllvm-cxxfiltrefuses them: a demangler offered every symbol in a binary and willing to readiasintwill rename half a C library.demangle_type()anddemangle --typesread all four. - Nine record
llvm-cxxfilt's own reading of a recorded<template-param>, which note 17 settles against the compilers' output. - One is a self-referential conversion operator,
_Zcv1BIRT_EIS1_E, from LLVM's fuzz corpus. It has no declaration; the reference printsoperator B<><>, and this refuses it, asc++filt2.42 does. - One,
_ZNK1xMUlTyT_E_clIiEEDaS_, is numbered by GCC 12's closure-prefix rule and the vector reads it by the ABI's; see note 8.
22. libiberty's own D vectors¶
d-demangle-expected reaches further than the shipped libgphobos does, and what its last
vectors needed was not in the D ABI at all: the five characters the reference names
inside a string, the different rule for a character literal, hex float and complex
values, associative-array values written as pairs where the type says so, struct and
function-literal values, extern(Pascal), the anonymous and __S<n> path components it
leaves out, and the malformed template instances it refuses outright. Each was derived by
running the reference over the input space, since no specification describes it.
23. rustc-demangle's own vectors¶
The two this does not match are detection rather than spelling: _ZN3foo5h05afE and
_ZN3foo20h05af221e174051e9abcE carry a hash that is not rustc's 17h and sixteen hex
digits, so this reads them as the C++ foo::h05af… they could equally be; asked for Rust
by name, it prints foo as the crate does. rustc-demangle can afford the wider rule
because it is only handed names a caller has already decided are Rust's; this is offered
every symbol in a binary.
Against the crate rather than its vectors: tools/rustc-demangle-reference/ is a front
end over rustc-demangle 0.1.28 itself, and over the 5,753 distinct Rust symbols in the
corpora this spells every one identically.
Whole symbol tables¶
Run live against the reference, not replayed.
| Binary | Symbols | Agree |
|---|---|---|
libLLVM.so.18.1 17 |
44,186 | 31 differ |
libclang-cpp.so + Polly + LTO 17 |
39,020 | 236 differ |
librustc_driver, libstd, libtest |
20,697 | 100% |
libstdc++.so.6, gnu style |
5,990 | 100% |
libLLVM.so.18.1, gnu style |
44,093 | 100% 18 |
| Every shared library Ubuntu 24.04 ships, llvm style 17 | 217,730 | 322 differ |
| Swift runtime + Foundation | 48,368 | 100% |
| Swift 6.1.2 toolchain, all 29 runtime libraries 6 | 135,492 | 1 differ |
Swift 6.1.2 toolchain C++ (swift-frontend, liblldb, libsourcekitdInProc), names not in the rows above 17 |
185,532 | 62 differ |
| Nim standard library routine names 14 | 5,946 | 99.87% |
| Free Pascal runtime and packages 5 | 236,570 | 100% |
libgphobos + libgdruntime (D) |
16,333 | 100% |
libgnat + libgnarl (Ada), against c++filt --format=gnat |
11,237 | 100% |
LDC 1.40 runtime (D 2.110 frontend), the names c++filt reads |
15,350 | 100% |
| LLVM 18.1.8 Windows release, every static library (MSVC) | 436,644 | 100% 19 |
Boost 1.84, twenty-nine boost_*-vc143 NuGet packages (MSVC 14.3, x64 and x86) |
122,162 | 100% 19 |
| ITK 5.0, OpenCV 5, Qt 5.9, libzmq 4.3, leveldb and restbed, NuGet packages (MSVC 2017 to 2022) | 471,881 | 100% 19 |
| 46 C++ development packages' static libraries (libc++, Boost, Abseil, gRPC, RocksDB, Cap'n Proto...) 17 | 109,606 | 74 differ |
Go toolchain (go, compile, link) |
30,733 | round trip 16 |
Objective-C, 3 ABIs + shipped libobjc.a 9 |
3,163 | 100% |
| Swift metadata symbolic references 7 | 4,528 | 100% |
| Swift 6.1.2 runtime typerefs, resolved through the dynamic relocations and symbols 7 | 7,071 | 6,811 agree; 260 unspliceable |
| KDE 2.2.2, omniORB 3.0.4, gtkmm 1.2 and libstdc++ 2.10, as gcc 2.95 mangled them (pre-Itanium), against libiberty | 56,347 | 396 differ 20 |
| Homebrew bottles of Boost, folly, Abseil, protobuf, Poco, fmt, TBB, ceres, ICU and glog (Apple clang, Mach-O) 17 8 | 108,839 | 12,350 differ 17 8 |
| Homebrew bottles of Arrow, DuckDB, RocksDB, gRPC, Cap'n Proto, libtorrent, Xerces-C, RE2, libomp and Boost.Python (Apple clang, Mach-O) 17 8 | 295,281 | 6,134 differ 17 8 |
| Delphi/C++Builder BPL and DLL export tables 13 | 11,363 | 100% |
The rows add up to 2,684,162 names. Some are counted in more than one row — libLLVM is read in both styles, and libLLVM, libclang-cpp and libstdc++ are also among the Ubuntu row's shared libraries — so the distinct figure is lower: about 2,500,000.
Every row is exact except the ten marked and the 6.1.2 typeref row, whose 260 are names
the reference cannot be handed at all 7. On
the ten, every difference is accounted for in the notes above, name by name, and all but
three rows' worth are a name a reference reads wrongly
17
8. The three are their own
case: the one in the Swift 6.1.2 toolchain row
6 is the shape
tests/conformance/swift-reference-defects.txt pins; the 396
20 are one form of
pre-Itanium thunk; and Nim's eight 14 are routine names with an
underscore before a digit, which Nim's own mangling discards, so no reading can recover
them.
The 62 in the Swift toolchain's C++, the 74 in the development packages and 7,660 of the
12,350 in the Homebrew bottles are the template-parameter rebinding
_Prepare_execution shows — std::call_once, Cap'n Proto's kj::evalNow and the
ArrayRefView lambdas — where this spells the parameter the header declares. The rest
of the first batch's differences are Apple's own numbering rules, and the second
batch's 6,134 are those, the closure-prefix rule and the same rebinding
8.
The purpose-built corpus scores 100% on its own, and libstdc++ stays the harder test: all but one of its 5,913 symbols carry an ELF version suffix, a shape no hand-written test thinks to include. Real shipped binaries surface defects a purpose-built corpus does not.
The libstdc++ row is against GNU rather than LLVM because llvm-cxxfilt 18 reads 78 of
those 5,990 names as unreadable and echoes them back: the transaction-safe clone prefix
_ZGTt, and the DF floating-point productions. Checking against GNU over the whole
library rather than over the recorded sample surfaces two gaps a sample misses: DF16b
is std::bfloat16_t and DF<n>x is _FloatNx terminated by its own x, and GNU puts
a space before a template argument list whose name ends with <, so operator<< <int>.
No name in either checked-in corpus carries any of them.
The purpose-built corpus still earns its place: compiled by both clang++ and g++
across C++11 through C++23 at two optimisation levels, it reaches constructs a released
library happens not to contain — concepts, coroutine frames, generic lambdas,
requires-clauses.
Regenerate with tools/generate_corpus.py and tools/generate_rust_corpus.py; compare
against a live reference with tools/differential.py.
Printing the name without its signature¶
-p is c++filt -p: over the shipped libstdc++ and the GNU-style corpus the two agree
on 6138 / 6224 names. Of the 86, 85 are deliberate. c++filt strips the parameter
list only from the outermost declaration, so a thunk keeps its target's — non-virtual
thunk to X::~X() — and it drops a [clone .cold] suffix while keeping an
@@GLIBCXX_3.4 one. This strips throughout and keeps both suffixes, because a filter over
a symbol table should not quietly discard part of the symbol: 72 thunks and 13 clones.
The last one is a name c++filt refuses and this reads.
On trusting the references¶
Not blindly. clang-cl emits ?f@@YAX_L@Z for __int128 and llvm-undname — LLVM's
own demangler — rejects it. The two C++ references contradict each other on substitution
numbering. Both echo their input on failure, which reads exactly like success.
So the split is deliberate: the ABI specification governs grammar and structure, the
references govern spelling. Ambiguities were settled by probing both references and
accepted only where they agreed (tools/probe_substitutions.py makes one print its own
substitution table). Where they genuinely differ, the difference is a style rather than
a silent winner.
Where a reference crashes¶
One reference defect is a crash rather than a reading, and it is binutils' to fix.
c++filt 2.42 aborts on an eight-character GNAT name:
$ printf 'aSO__bDF\n' | c++filt --format=gnat
*** buffer overflow detected ***: terminated
Aborted
Two stream-attribute expansions in one name are what overrun it: aSO__bSO goes the
same way and aSO__bSR does not, because the buffer has seven characters of slack,
'Read grows the name by three and 'Output by five. ada_demangle sizes the buffer
strlen (mangled) + 7 + 1 on the reasoning that the expanding cases occur only once, but
its loop comes back round for every __-separated component, so a long enough symbol
writes about four bytes past the end for every five characters of its own. nm
--demangle=gnat and objdump --demangle=gnat abort on an object file carrying such a
symbol too. Only the GNAT format reaches it; c++filt left to detect the scheme does not.
It is GCC PR 92453, rediscovered as GCC PR 103893 and binutils PR 28736, and still
unfixed on GCC master.
This reads the name as a'Output.b.Finalize, pinned in tests/test_ada.py, and
ask_tolerantly in tools/mutate.py splits a batch down to the name that crashed the
reference and leaves that one out of the comparison.
Schemes not covered¶
The fourteen schemes this reads are the ones it set out to read. These were surveyed and are deliberately not planned:
- Zig, Erlang, Julia, V, Odin and Kotlin/Native — not mangling schemes: they emit readable or unencoded names.
- Borland C++Builder — already covered: it is the Delphi scheme.
- Objective-C++ — already covered: it is Itanium with Objective-C types.
- Watcom C++, Sun Studio (whose
libdemangleis undocumented) and gcj (libiberty'sDMGL_JAVA) — extinct enough not to be worth the transcription.
A request for one of these is still worth making if it comes with real names out of a binary, which is what would change the reasoning.