Core¶
The scheme-agnostic half. A new mangling scheme is written against these and nothing else; Architecture has the layering rules.
The builder protocol¶
demangle.core.builder
¶
The contract between a parser and its output.
Every parser in this package is written against Builder and never constructs its own
strings or nodes. It recognises a grammar production and reports it -- builder.pointer(
inner), builder.template(base, args) -- leaving what that becomes to the builder.
The reason is that the two things callers want from a demangler have opposite cost profiles. Labelling every symbol in a binary wants a string and nothing else, several hundred thousand times. Inspecting one symbol's template arguments wants a tree. Writing the parser twice to serve both would guarantee the two copies disagree, and the ways they would disagree are exactly the subtle ones -- declarator placement, substitution numbering -- that are hardest to notice.
So: one parser, two builders (SpellingBuilder, AstBuilder), and a third is a class
rather than a fork.
Values passed between these methods are handles. A parser may hold one, pass it back
in, and ask the builder to spell it, but must never inspect it -- a handle is a
Spelling under one builder and a Node under another. spell() exists for the few
places where a grammar genuinely requires the text of something already built (naming
the class in Foo::Foo, for instance), and is the only legal way to look inside.
Handle
module-attribute
¶
Handle = Any
An opaque builder product. Its concrete type is the builder's business.
Builder
¶
Bases: Protocol
What a parser may ask for. Implementations choose what to produce.
builtin
¶
builtin(spelling)
A primitive type: int, void, char.
Source code in src/demangle/core/builder.py
35 36 | |
name
¶
name(text)
A single identifier, already decoded from its length-prefixed form.
Source code in src/demangle/core/builder.py
38 39 | |
raw
¶
raw(text)
Pre-rendered text with no further structure.
The escape hatch for productions whose spelling a scheme fixes outright, such
as std::nullptr_t. Reach for a specific method first: text passed here is
invisible to structured consumers.
Source code in src/demangle/core/builder.py
41 42 43 44 45 46 47 | |
literal
¶
literal(kind, value)
A constant appearing in a type, such as a non-type template argument.
Source code in src/demangle/core/builder.py
49 50 | |
expression
¶
expression(form, parts)
A constant expression: sizeof (T), a + b, x ? y : z.
parts interleaves the fixed text of the production with the handles of its
operands, in output order, so a builder that wants text concatenates and one
that wants a tree keeps the boundaries. form names the shape -- binary,
call, sizeof, conditional -- so a consumer can ask what an expression is
without matching on its spelling.
One method rather than one per operator, because the alternative is fifteen
methods that every future builder has to implement and that still would not
cover the next operator someone mangles. The parser already owns operator
spelling: it comes from the tables in tables.py, which exist to be checked
against the ABI. What the builder decides is structure.
Brackets are a part like any other. Whether an operand needs them is a question
about precedence that only the parser can answer, so it arrives here already
settled, as "(" and ")" around the operand it applies to.
Source code in src/demangle/core/builder.py
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | |
qualified
¶
qualified(parts)
A scoped name: the parts of a::b::c, outermost first.
Source code in src/demangle/core/builder.py
72 73 | |
template
¶
template(base, arguments)
A template specialisation, base<arguments...>.
Source code in src/demangle/core/builder.py
75 76 | |
qualify
¶
qualify(inner, qualifiers)
Apply cv-qualifiers, in the canonical order the caller has already imposed.
Source code in src/demangle/core/builder.py
78 79 | |
pointer
¶
pointer(inner)
inner*.
Source code in src/demangle/core/builder.py
81 82 | |
reference
¶
reference(inner)
inner&.
Source code in src/demangle/core/builder.py
84 85 | |
rvalue_reference
¶
rvalue_reference(inner)
inner&&.
Source code in src/demangle/core/builder.py
87 88 | |
member_pointer
¶
member_pointer(owner, inner)
inner Owner::* -- a pointer to member.
Source code in src/demangle/core/builder.py
90 91 | |
array
¶
array(inner, dimension)
inner [dimension]. An empty dimension means an unbounded array.
Source code in src/demangle/core/builder.py
93 94 | |
function
¶
function(returns, parameters, suffix='', name=None)
A function type or a whole function declaration.
returns is None where the scheme encodes no return type -- constructors,
destructors, conversion operators, and plain non-template functions under the
Itanium ABI. suffix carries what trails the parameter list and binds to the
function rather than the return type: cv-qualifiers, a ref-qualifier,
noexcept. name is the declarator, absent for a bare function type.
Source code in src/demangle/core/builder.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
pack
¶
pack(inner)
An unexpanded parameter pack expansion, inner....
Source code in src/demangle/core/builder.py
112 113 | |
parameter_pack
¶
parameter_pack(members)
A pack of concrete arguments.
Must stay a sequence rather than collapse to joined text: a declarator applied
to a pack applies to every member, so Dp O T_ over three arguments is three
rvalue references.
Source code in src/demangle/core/builder.py
115 116 117 118 119 120 121 | |
vendor_qualify
¶
vendor_qualify(inner, qualifier)
A vendor extended qualifier, spelled after the type it applies to.
Source code in src/demangle/core/builder.py
123 124 | |
special
¶
special(label, inner)
A symbol that is about an entity: vtable for Foo, typeinfo for Bar.
Source code in src/demangle/core/builder.py
126 127 | |
decorated
¶
decorated(inner, decoration)
A symbol carrying a symbol-table decoration: an ELF version, a clone suffix.
Kept as structure rather than folded into text so a caller can ask which copy of a function a symbol names, or strip the version and compare.
Source code in src/demangle/core/builder.py
129 130 131 132 133 134 | |
spell
¶
spell(handle, declarator='')
Render a handle, placing declarator in the type's declarator position.
Source code in src/demangle/core/builder.py
136 137 | |
size
¶
size(handle)
How many characters handle would render to, without rendering it.
Must be O(1). This is what lets a parser enforce an output bound while
building: the substitution scheme allows each component to be assembled from two
copies of an earlier one, so output can double every few input bytes. Checking
the bound by measuring spell() would mean materialising the very string the
bound exists to prevent -- a few hundred bytes of input can reach gigabytes.
Source code in src/demangle/core/builder.py
139 140 141 142 143 144 145 146 147 | |
Spelling¶
demangle.core.spelling
¶
The fast builder: C++ declaration syntax, built directly as text.
C++ does not write a type before the name, it writes it around the name. The type
"pointer to function taking char and returning int" is spelled int (*)(char), and
giving it a name puts that name in a hole in the middle: int (*f)(char). Arrays do
the same: int a[10], and a pointer to one is int (*)[10].
So a partially built type is a pair of strings, left and right, and rendering is
left + declarator + right. Every function here exists to keep that hole in the right
place as declarators nest. It is the part of a demangler most often subtly wrong, which
is why it is one small module with its own tests rather than logic spread through three
parsers.
Spellings are immutable. Nothing here mutates a handle it was given, so a builder product can be stored in a substitution table and reused without defensive copying -- which matters, because that is exactly what the Itanium back-reference scheme does.
Spelling
¶
Spelling(
left,
right="",
is_function=False,
is_array=False,
ref_kind="",
members=None,
)
One type or name, split around its declarator position.
Source code in src/demangle/core/spelling.py
46 47 48 49 50 51 52 53 54 55 56 | |
spell
¶
spell(declarator='')
This type with declarator -- a name -- written where a declarator goes.
Both references space the name off the type unless the type has a right part
to put after it, which is what NonTypeTemplateParamDecl::printLeft asks
hasRHSComponent in LLVM's ItaniumDemangle.h: int* $N and int A::* $N
against int$N [3] and int (*$N) [3]. A function type's left half already
ends in a space, so int $N() comes out with exactly one either way. Verified
against both references over every declarator shape; int*$N, which is what a
rule about the last character gives, is what neither writes.
Source code in src/demangle/core/spelling.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
SpellingBuilder
¶
SpellingBuilder(
legacy_angle_spacing=False,
gnu_clone_suffix=False,
collapse_duplicate_qualifiers=False,
tight_after_star=False,
tight_member_array=True,
)
Bases: Builder
Builds C++ declaration text. The backend behind demangle().
Stateless, so one instance is shared by every parse rather than allocated per name.
Source code in src/demangle/core/spelling.py
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | |
qualify
¶
qualify(inner, qualifiers, cv=True)
Apply qualifiers to inner, on the right, the way C++ writes them.
cv says whether these are cv-qualifiers, which is what decides whether a
repeat collapses. _Complex and _Imaginary go through here too and are not
cv-qualifiers: [basic.type.qualifier] folds a duplicate const, and nothing
folds a duplicate _Imaginary. c++filt 2.42 writes signed char _Imaginary
_Imaginary for _Z1fGGa; collapsing it to one would lose a word of the name,
and only the gnu style collapses at all.
Source code in src/demangle/core/spelling.py
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | |
pack
¶
pack(inner)
An unexpanded pack expansion: the whole type, then ....
The reference's ParameterPackExpansion prints its child -- both halves -- and
appends the ellipsis, so a declarator type keeps its shape and the dots follow
it: void ()..., void (*)()..., int [3].... Putting them in the left half
alone would put them where a declarator's name goes, void (*...)() and
int... [3], which is not what any reference prints. A type with no right half
is unchanged.
Source code in src/demangle/core/spelling.py
428 429 430 431 432 433 434 435 436 437 438 | |
vendor_qualify
¶
vendor_qualify(inner, qualifier)
A vendor qualifier goes after the whole type, declarator and all.
The reference prints the type and then the extension, so a function type comes
out void () block_pointer -- not void block_pointer(), which is what putting
the word in the left half alone gives for anything that has a right half.
Distributes over a pack like every other declarator: an empty pack gives nothing
rather than a bare enable_if, a qualifier on nothing.
Source code in src/demangle/core/spelling.py
440 441 442 443 444 445 446 447 448 449 450 451 452 | |
members
¶
members(handle)
The members of a parameter pack, or None if this is not one.
Source code in src/demangle/core/spelling.py
466 467 468 | |
pack_of
¶
pack_of(members)
A parameter pack, spelled as its members and still addressable as a sequence.
Nested packs are spliced. A pack whose one member is an expansion of an empty pack
is empty, not a pack of one empty thing -- otherwise AnalysisManager<T_, J Dp T0_ E>
with T0_ bound to nothing prints the separator for an argument that is not there.
Source code in src/demangle/core/spelling.py
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
The tree¶
The node classes a tree is made of, and the builder a scheme hands them to. Every class
declares __match_args__, so a caller can match on the shape of a subtree rather than
compare node.kind against a string:
import demangle
from demangle.core.ast import Builtin, Pointer
match demangle.parse("_Z1fPi").parameters[0]:
case Pointer(Builtin(spelling)):
print("pointer to", spelling)
A scheme's own node kinds are under its section on the schemes page, and
demangle.node_kinds() lists the kind strings a scheme can produce.
demangle.core.ast
¶
The structured builder: a tree a caller can walk.
demangle() answers "what does this name say". parse() answers "what are its parts" --
which namespace, which template arguments, how many parameters and of what types. Tools
that rename functions, group symbols by library, or match call signatures need the
second, and recovering it by pattern-matching the string is not workable: C++
declaration syntax nests, and nesting is what regular expressions cannot follow.
Every node re-emits itself into any builder through build(). That is what keeps this
module free of rendering logic: turning a tree back into text drives SpellingBuilder
over it, so there is exactly one implementation of declarator placement in the package
and a tree can never disagree with the fast path. It also means a new output format --
JSON, HTML, a token stream -- is a builder, not a second traversal to keep in sync.
Node
¶
Base class for every parsed component.
__slots__ throughout: a single C++ symbol can be a few hundred nodes and a binary
a few hundred thousand symbols, so per-instance dictionaries are not affordable.
children
¶
children()
Direct children, in source order. Leaves return an empty tuple.
Source code in src/demangle/core/ast.py
99 100 101 | |
to_dict
¶
to_dict()
This subtree as plain data -- dicts, lists and strings -- for json.dumps.
Every node becomes a dict with a kind and the fields that node kind has, so the
role of each child survives: a function has returns, parameters and
name, not three anonymous children. A field holding a node becomes a nested
dict and one holding a sequence of them becomes a list; anything else -- a
spelling, a qualifier list, a flag -- is carried as it is.
kind is the vocabulary to switch on, and demangle.node_kinds() is all of it.
A node reached more than once is written out once, with an id, and afterwards
as {"$ref": id}. This is not a size optimisation, it is the difference between
terminating and not: the structure is a graph, not a tree. Itanium's
substitutions make a component reachable from several places, and a Rust node
names its own children twice over -- parts orders them and base and
arguments say what they are -- so writing each occurrence out in full doubles
per level. A real symbol from the Rust toolchain costs seconds and hundreds
of megabytes that way, and there is no bound on how much worse it can get.
Source code in src/demangle/core/ast.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | |
build
¶
build(builder)
Re-emit this subtree into builder, returning its handle.
Source code in src/demangle/core/ast.py
125 126 127 | |
walk
¶
walk()
Yield this node and every descendant, depth first.
Iterative rather than recursive, and not for speed: a yield from per level
means one interpreter frame per level for the whole traversal, so a tree well
inside max_depth -- which bounds the parse, in frames of a different size --
could still exhaust the stack here. An explicit stack cannot.
Source code in src/demangle/core/ast.py
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | |
find
¶
find(kind)
Yield every descendant of the given kind, including this node.
Source code in src/demangle/core/ast.py
145 146 147 | |
render
¶
render()
The text of this subtree, under the default spelling policy.
spell() is the method to reach for -- it takes the style the tree was parsed
under, which this cannot. A scheme whose spelling rules do not fit C++
declarator syntax carries its fragments as text and renders by concatenating
them, so render() is what its own nodes and its own tests use, and spell()
delegates to it.
It is defined on the base class so every node answers it: walking a tree and
asking each node for its text -- the obvious thing to do with walk() -- must
not fail partway through on a Decorated wrapper or an Itanium node. For a node
that carries pre-rendered text this is that text; for one spelled from structure
it is spell() with no declarator, which is the same string.
Source code in src/demangle/core/ast.py
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |
spell
¶
spell(declarator='', style=None)
Render as declaration text.
style selects the spelling policy, and should be the style the tree was parsed
under: some choices -- whether a std:: abbreviation is expanded, for instance --
are made during parsing and are already baked into the tree, so spelling a
gnu-parsed tree with LLVM's builder gives a mixture of the two.
Source code in src/demangle/core/ast.py
166 167 168 169 170 171 172 173 174 175 | |
Builtin
¶
Builtin(spelling)
Bases: Node
A primitive type.
Source code in src/demangle/core/ast.py
508 509 | |
Name
¶
Name(text)
Bases: Node
A single identifier.
Source code in src/demangle/core/ast.py
522 523 | |
Raw
¶
Raw(text)
Bases: Node
Text whose spelling the scheme fixes outright, with no further structure.
Source code in src/demangle/core/ast.py
536 537 | |
Literal
¶
Literal(type_, value)
Bases: Node
A constant value, such as a non-type template argument.
Source code in src/demangle/core/ast.py
550 551 552 | |
Expression
¶
Expression(form, parts)
Bases: Node
A constant expression appearing in a type or a template argument.
parts interleaves fixed text with operand subtrees, in output order, exactly as
the parser reported them -- so rendering concatenates and cannot spell the
expression differently from the text path. form says which shape it is: binary,
unary, call, conditional, sizeof, and so on.
Brackets, where the spelling needs them, are plain parts. Precedence is settled by the parser before it reports the production, because only the parser knows what the operand was; a consumer reading the tree sees the operands, not the punctuation.
Source code in src/demangle/core/ast.py
578 579 580 | |
Qualified
¶
Qualified(parts)
Bases: Node
A scoped name, a::b::c, outermost part first.
Source code in src/demangle/core/ast.py
601 602 | |
Template
¶
Template(base, arguments, angle_space=True)
Bases: Node
A template specialisation, base<arguments...>.
Source code in src/demangle/core/ast.py
618 619 620 621 622 623 | |
Qualify
¶
Qualify(inner, qualifiers, cv=True)
Bases: Node
Qualifiers applied to a type, written on the right.
cv says whether they are cv-qualifiers. _Complex and _Imaginary arrive here
too and are not, which matters only because a repeated cv-qualifier collapses under
the gnu style and a repeated _Imaginary does not. Carried on the node so that a
tree spells what the text path spelled.
Source code in src/demangle/core/ast.py
645 646 647 648 | |
Pointer
¶
Pointer(inner)
Bases: _Unary
A pointer to inner: int*.
Source code in src/demangle/core/ast.py
664 665 | |
Reference
¶
Reference(inner)
Bases: _Unary
An lvalue reference to inner: int&.
Source code in src/demangle/core/ast.py
664 665 | |
RValueReference
¶
RValueReference(inner)
Bases: _Unary
An rvalue reference to inner: int&&.
Source code in src/demangle/core/ast.py
664 665 | |
Pack
¶
Pack(inner)
Bases: _Unary
A parameter pack expansion.
Source code in src/demangle/core/ast.py
664 665 | |
ParameterPack
¶
ParameterPack(members)
Bases: Node
A pack of concrete template arguments.
Source code in src/demangle/core/ast.py
725 726 | |
MemberPointer
¶
MemberPointer(owner, inner)
Bases: Node
A pointer to member, Type Owner::*.
Source code in src/demangle/core/ast.py
742 743 744 | |
Array
¶
Array(inner, dimension)
Bases: Node
An array type. An empty dimension means an unbounded array.
Source code in src/demangle/core/ast.py
760 761 762 | |
VendorQualify
¶
VendorQualify(inner, qualifier)
Bases: Node
A vendor extended qualifier, spelled after the type it applies to.
Source code in src/demangle/core/ast.py
778 779 780 | |
Function
¶
Function(returns, parameters, suffix='', name=None)
Bases: Node
A function type, or a whole function declaration when name is present.
Source code in src/demangle/core/ast.py
796 797 798 799 800 | |
Decorated
¶
Decorated(inner, decoration)
Bases: Node
A symbol plus what the symbol table appended to it.
decoration keeps its separator: "@@GLIBCXX_3.4", ".cold". A tool that wants
the undecorated entity walks to inner; one that wants to know which clone or which
library version this is reads decoration.
Source code in src/demangle/core/ast.py
828 829 830 | |
Special
¶
Special(label, inner)
Bases: Node
A symbol about an entity: vtable for Foo, typeinfo for Bar.
Source code in src/demangle/core/ast.py
846 847 848 | |
AstBuilder
¶
AstBuilder(style=None)
Bases: Builder
Builds Node trees. The backend behind parse().
One instance serves every call. The only state is the leaf table below, which is a cache and does not make an answer depend on what was parsed before it.
Each method records on the node it builds an upper bound on that subtree's rendered
length, which is what makes size() constant time. Over-estimating is deliberate --
the number enforces max_output, where erring high is the safe direction -- and it
is carried rather than measured because the tree is a graph with shared subtrees, and
walking it would be exponential in exactly the cases the bound exists to stop.
Leaves are interned; nothing else is. Measured over the 5,913 trees of the shipped
libstdc++: leaves are 55% of all nodes and repeat 49 times over on average -- 15,654
builtin instances hold 32 distinct spellings, 8,807 raw instances hold 16.
Interning them costs nothing because a leaf is keyed by its text, so a lookup is
one string hash and no comparison of subtrees; it is 40% less memory over that corpus
and, because allocating fewer objects is less work than allocating more, about 6%
faster as well.
Composite nodes are not interned: a composite hashes by walking its children, so
building that table would cost several times the whole parse to save memory a caller
may not be short of.
Node.__eq__ still compares composites by value, so a caller wanting to deduplicate
a particular result can.
Source code in src/demangle/core/ast.py
918 919 920 921 922 923 924 | |
members
¶
members(handle)
The members of a parameter pack, or None if this is not one.
Source code in src/demangle/core/ast.py
1121 1122 1123 | |
builder_for
¶
builder_for(style)
The tree builder for style.
A registered style is held by name, so one registered again under a name it already
had is picked up rather than remembered, and the common path -- parse(name), whose
style is the string "llvm" -- is a dictionary lookup.
A style object that is not the registered one under its name gets its own builder,
holding the object. Keyed by name it would be wrong in both directions: a composed
style like demangle.style("llvm", msvc={...}) keeps the name "llvm" and would
silently be served the registered style's builder, and a style whose name is not
registered at all would make parse() raise unknown style from inside the parser
while demangle() accepts the very same object. Building one per call matches the
bargain demangle() makes for a style object: it is not cached either.
setdefault rather than a lock: two threads arriving together each build one and
the loser's is discarded, which costs an allocation and cannot produce two builders
in use for one style.
Source code in src/demangle/core/ast.py
1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 | |
Reading¶
demangle.core.reader
¶
A bounds-checked cursor over a mangled name.
Every parser in the package reads through this and never indexes the string directly.
That is not ceremony: the schemes are full of places where the next character decides
the production, and a bare text[pos] at the end of a truncated name raises
IndexError -- which then has to be caught somewhere far away and translated into
something meaningful. Reading past the end here is simply "no character", and the
parser decides whether that is fatal.
Kept deliberately small and __slots__-ed: an instance exists per name demangled, and
its methods are the innermost loop in the package.
Reader
¶
Reader(text)
A cursor over text, with the lookahead and consumption parsers need.
length is the end of input and is not always len(text). A parser may shorten it
to bound a nested encoding it has found the end of by other means -- the Itanium
scheme does this for ___Z<encoding>_block_invoke, where a regex says where the
encoding stops and the parser must not read the literal that follows. Every method
here answers against length rather than against the string, because a bound only
half the class honours is worse than none: if peek reported the end of input
while expect stepped past it, a truncated S would borrow the _ of
_block_invoke for its terminator and read as though it were whole.
Source code in src/demangle/core/reader.py
54 55 56 57 58 59 60 61 | |
peek
¶
peek()
The character at the cursor, or "" past the end.
Returning a string rather than raising is what lets a parser write
if reader.peek() == "N" without first checking for the end of input.
A parser that has just had a non-empty character out of peek, with nothing
between the two, may consume it by advancing pos itself rather than calling
take: the bounds test take would repeat has already happened. Nothing weaker
licenses it. An intervening call may have moved the cursor, and an empty result
means there was no character to consume -- either way pos += 1 would step past
the end, which is the one thing this class exists to prevent. The productions
that do it say so at the site.
It is called around fifty times per name demangled -- more than any other method
in the package -- and CPython charges for a default it then has to bind, so it
takes no argument: that is a fifth off the cost of the call. ahead is the
offset form.
Source code in src/demangle/core/reader.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
ahead
¶
ahead(offset)
The character offset past the cursor, or "" past the end.
Source code in src/demangle/core/reader.py
93 94 95 96 | |
peek2
¶
peek2()
The next two characters, for the many two-letter codes in these grammars.
Source code in src/demangle/core/reader.py
98 99 100 101 102 103 | |
ahead2
¶
ahead2(offset)
The two characters offset past the cursor, bounded like peek2.
For the lookaheads that decide a branch without consuming anything -- whether an
abbreviation is followed by a constructor, whether gs introduces an allocation.
Reaching into text directly is the end of input's one blind spot: a bound only
half the class honours is worse than none.
Source code in src/demangle/core/reader.py
105 106 107 108 109 110 111 112 113 114 115 116 | |
take
¶
take()
Consume and return one character. Raises at the end of input.
Source code in src/demangle/core/reader.py
121 122 123 124 125 126 127 | |
take_exactly
¶
take_exactly(count)
Consume exactly count characters, or raise if they are not all there.
Source code in src/demangle/core/reader.py
129 130 131 132 133 134 135 136 | |
eat
¶
eat(literal)
Consume literal if it is next, reporting whether it was.
Source code in src/demangle/core/reader.py
138 139 140 141 142 143 144 145 146 147 148 149 150 | |
expect
¶
expect(literal)
Consume literal, or raise.
Source code in src/demangle/core/reader.py
152 153 154 155 156 157 158 159 160 161 162 163 164 165 | |
digits
¶
digits()
Consume a run of decimal digits and return it, or raise if there are none.
Bounded: an unbounded run would reach int(), which CPython refuses above 4300
digits with a ValueError -- not this package's error type, and so not something
a caller of parse() can be expected to catch.
Source code in src/demangle/core/reader.py
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | |
length_prefixed
¶
length_prefixed()
A decimal length and the characters it counts, as one production.
<source-name> is the commonest production in these grammars -- every component
of every qualified name is one -- and reading it as int(digits()) followed by
take_exactly() costs three frames for what is one scan and one slice. Raises
the same way each of those does: no digits, a run past the bound, or a length
the name is too short for.
Source code in src/demangle/core/reader.py
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | |
number
¶
number(allow_negative=True)
A n meaning negative, then digits.
Source code in src/demangle/core/reader.py
212 213 214 215 216 | |
seq_id
¶
seq_id()
A _, returned as a zero-based index.
The encoding is offset by one so that the empty sequence can mean something:
S_ is entry 0 and S0_ is entry 1.
Source code in src/demangle/core/reader.py
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | |
Plugins¶
A distribution can add a scheme of its own by calling register or by advertising a
demangle.languages entry point. Advertised plugins load only when a program calls
demangle.load_plugins() (the demangle command does); register always takes
effect. Adding a scheme is the walk-through.
demangle.core.plugin
¶
The contract a mangling scheme implements.
A language is a LanguagePlugin. Nothing in core imports a language module at import
time, and no language imports another, so a scheme can be developed, tested, replaced or
shipped separately from everything else here.
Adding one means providing three things and registering them. It does not mean understanding the rest of the codebase, which is the point: the project should be approachable to a contributor who knows one ABI well and nothing else about it.
LanguagePlugin
dataclass
¶
LanguagePlugin(
name,
detect,
parse,
parse_type=None,
node_kinds=(),
description="",
aliases=tuple(),
options_type=None,
symbol_table_decorations=False,
first_characters="",
priority=100,
)
One mangling scheme.
name
instance-attribute
¶
name
Stable identifier, used in the API and on the command line: itanium, msvc.
detect
instance-attribute
¶
detect
Cheap test for "is this plausibly mine".
Called on every symbol a caller offers, including the great majority that are not mangled at all, so it must not allocate or parse. A prefix comparison is the expected shape. False negatives lose symbols; false positives only cost a failed parse, so err towards accepting.
The exception is a scheme whose names carry no marker, gnuv2, codewarrior and
ada among them. Its detect screens with a substring test and then parses the
whole name, and it must not err towards accepting: it is ordered after the
marked schemes, and what justifies it is a measured false-positive count of zero over
the other schemes' corpora and over real symbol tables.
parse
instance-attribute
¶
parse
parse(mangled, builder, limits=..., options=...) -> handle.
Raises NotMangledError or ParseError on failure; never returns a partial result.
parse_type
class-attribute
instance-attribute
¶
parse_type = None
parse_type(mangled, builder, limits=..., options=...) -> handle, or None.
A type encoding rather than a whole symbol -- Pi for int*, an MSVC .PEAX, a
Swift type string. This is what typeinfo names, RTTI descriptors and vtable
entries carry, and it is a separate entry point because a type encoding carries no
marker saying which scheme it belongs to: i is a valid Itanium type and a valid
anything-else, so it can only be read when a caller says which scheme to read it as.
None for a scheme with no type grammar of its own, which is most of them: Go, Nim, Free Pascal and JNI names carry types only as text they already spell.
node_kinds
class-attribute
instance-attribute
¶
node_kinds = ()
Every kind string a tree from this scheme can hold.
The published vocabulary. parse() returns a walkable tree, and without this a
caller building on it has to read the source to learn what to switch on -- which is
what swift-demangle's kind= dumps and libiberty's hundred DEMANGLE_COMPONENT_*
enumerators exist to avoid. demangle.node_kinds() is the whole of it, and
tests/test_api_surface.py::TestThePublishedVocabulary
checks it against every corpus so it cannot drift.
description
class-attribute
instance-attribute
¶
description = ''
One line, shown by demangle --list-languages.
aliases
class-attribute
instance-attribute
¶
aliases = field(default_factory=tuple)
Other names callers may use: gnu and gcc both mean itanium.
options_type
class-attribute
instance-attribute
¶
options_type = None
The dataclass this language accepts as options, if it takes any.
symbol_table_decorations
class-attribute
instance-attribute
¶
symbol_table_decorations = False
Whether names in this scheme may carry symbol-table decorations.
True for schemes that appear in ELF and Mach-O symbol tables, where the linker
appends version suffixes and the compiler appends clone suffixes. It must stay False
for MSVC, whose decorated names use @ as their own scope separator and would be
truncated at the first one.
first_characters
class-attribute
instance-attribute
¶
first_characters = ''
The characters a name of this scheme may begin with, if the set is small.
An optimisation, and one the registry checks rather than trusts: a name whose first
character is not in here is never offered to detect. Leave it empty -- the default
-- for a scheme whose names have no fixed start, which costs nothing but a call.
Getting it wrong loses symbols silently, so the rule is narrow: put a character here
only if detect returns False for every name that does not begin with one of
them. tests/test_core.py checks the built-in schemes against their own corpora.
priority
class-attribute
instance-attribute
¶
priority = 100
Detection order, lower first.
Matters only where two schemes share a prefix. Rust's legacy scheme is Itanium mangling, so Rust must be offered the name first or every Rust symbol demangles as a C++ one.
demangle.core.registry
¶
Discovery of mangling schemes.
Built-in languages are imported lazily, and not only when named: the registry knows
each one's detection order and first characters without importing it, so a scheme's
module is imported only once a name reaches its detect -- a name starting ? never
costs the Swift parser. Third-party languages are found through the
demangle.languages entry-point group, so a separate distribution can add a language
without a patch here -- but only once a caller asks, with load_plugins(). Nothing is
discovered on its own: an installed distribution cannot replace a built-in scheme
unnoticed, and two installs read a name alike.
The registry is process-global and populated once. Registration is idempotent, so importing a plugin module twice is harmless.
register
¶
register(plugin)
Add a plugin to the registry, replacing any earlier one of the same name.
Source code in src/demangle/core/registry.py
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
get
¶
get(name)
Look up a plugin by name or alias. Raises KeyError if unknown.
Source code in src/demangle/core/registry.py
357 358 359 360 361 362 363 364 365 366 367 | |
available
¶
available()
Every registered plugin, in detection order.
Source code in src/demangle/core/registry.py
388 389 390 391 392 393 394 395 | |
names
¶
names()
Registered language names, sorted.
Source code in src/demangle/core/registry.py
535 536 537 538 539 | |
aliases
¶
aliases()
Every alias, mapped to the plugin name it resolves to.
Source code in src/demangle/core/registry.py
542 543 544 545 546 | |
Symbol-table decorations¶
demangle.core.decorations
¶
Decorations a symbol table adds around a mangled name.
A symbol in a real binary is often not just a mangled name. The linker and the compiler both append things to it, and none of those additions belong to any mangling scheme -- they are conventions of the object format and the toolchain:
_ZNSt10moneypunctIcLb0EE2idE@@GLIBCXX_3.4 ELF symbol versioning
_ZN3Foo3barEv.cold a cold-path clone
_ZN3Foo3barEv.part.0 a partial inlining clone
_ZN3Foo3barEv.llvm.1234567890 an LLVM internalisation suffix
_ZN3Foo3barEv.constprop.0 a constant-propagation clone
Handling these in core rather than in each parser matters because they are orthogonal
to the grammar: a scheme should not have to know about ELF, and a new scheme gets this
for free. The reference demanglers pass them through verbatim, and so do we -- the
suffix identifies which copy of a function a symbol is, which is information a caller
split_decorations
¶
split_decorations(name)
Split name into (mangled, version-suffix).
Only the ELF version suffix is split here, because only it can be recognised
before parsing: @ appears in no scheme this applies to, so the first one is
necessarily the start of the suffix.
Clone suffixes deliberately are not. A . is not reserved: GCC names a coroutine's
frame type _ZN6modern9coroutineEi.Frame and writes that as a length-prefixed
identifier, so the coroutine's actor is
_ZN6modern9coroutineEPZNS_9coroutineEiE28_ZN6modern9coroutineEi.Frame.actor --
where the first . is inside the identifier and only the second begins the clone
suffix. Cutting at the first . truncates the name mid-production. A clone suffix
can only be recognised as what is left over once the grammar has consumed
everything it can, which means the parser has to do it.
The suffix is returned with its separator intact, so joining is concatenation.
Source code in src/demangle/core/decorations.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | |
describe
¶
describe(decoration, gnu_clone_suffix=False)
A readable spelling of a decoration.
ELF version suffixes are passed through as written by both references. Clone
suffixes are not: llvm-cxxfilt writes Foo::bar() (.cold) where GNU c++filt writes
Foo::bar() [clone .cold]. Neither is more correct, so it is a style choice.
Source code in src/demangle/core/decorations.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
Styles¶
demangle.core.style
¶
Output styles.
Some spelling questions have more than one correct answer, and the reference
implementations answer them differently: llvm-cxxfilt prints std::string and
Foo<Bar<int>>, GNU c++filt prints the full basic_string specialisation and
Foo<Bar<int> >. Both describe the same types.
A Style bundles those choices so a caller picks one name -- "llvm", "gnu" --
instead of threading half a dozen flags through every call. New languages add their own
options to the mapping without changing this type, which is why the per-language policy
is an opaque dict rather than a fixed set of fields.
Not every choice is a disagreement between references, though, and the second kind is
what with_options is for: how much of a name to print. Every peer tool lets a caller
compose those at the call site -- llvm-undname --no-calling-convention,
UnDecorateSymbolName's mask, c++filt -p -- because "the same names, with less around
them" is what makes a symbol table greppable. A style is immutable, so composing one
returns a new style rather than changing the shared default, and a style object handed to
demangle() is deliberately not cached: it is one call's policy, not the process's.
Style
dataclass
¶
Style(name, spelling_builder, language_options=dict())
A named set of output policy choices.
spelling_builder
instance-attribute
¶
spelling_builder
The builder used when a caller asks for text. Must be a Builder.
language_options
class-attribute
instance-attribute
¶
language_options = field(default_factory=dict)
Per-language option objects, keyed by language name.
options_for
¶
options_for(language)
The option object this style specifies for language, or None.
Source code in src/demangle/core/style.py
52 53 54 | |
with_options
¶
with_options(**languages)
This style with some language's options changed, as a new style.
Each keyword is a language name and each value is either that language's whole options object or a mapping of the fields to change in the one already in force:
>>> import demangle
>>> from demangle.core.style import get_style
>>> narrow = get_style("llvm").with_options(msvc={"calling_convention": False})
>>> demangle.demangle("?f@@YAXH@Z", style=narrow)
'void f(int)'
demangle.style() is the same thing spelled from the package.
The result is a Style object rather than a registered name, which is what keeps
it a per-call policy: demangle() does not cache a call that passes one, so one
caller's narrower spelling cannot be served to another asking for the default.
Source code in src/demangle/core/style.py
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
register_style
¶
register_style(style)
Add a style, so a downstream project can define its own house spelling.
Source code in src/demangle/core/style.py
236 237 238 239 240 241 242 243 | |
get_style
¶
get_style(name)
Look up a style by name. None gives the default, and a Style is returned as is.
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Source code in src/demangle/core/style.py
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | |
available_styles
¶
available_styles()
The registered style names, sorted. demangle.styles() is this.
Source code in src/demangle/core/style.py
246 247 248 | |
Caching¶
demangle.core.cache
¶
Bounded memoisation.
Symbol tables repeat themselves relentlessly: one C++ binary can name
std::allocator<char> thousands of times, and every one costs the same parse. Caching
is worth more here than almost any micro-optimisation.
It also cannot be unbounded. A tool that walks a corpus of binaries would otherwise accumulate an entry per distinct symbol ever seen, and there is no natural end to that.
BoundedCache
¶
BoundedCache(max_size=8192, max_weight=None, weigh=None)
A dict of at most max_size entries, kept as two generations.
New entries go into the young generation. When it holds half of max_size, it
becomes the old one -- the previous old one is dropped whole -- and a new, empty
young generation starts. A lookup tries the young generation, then the old; an entry
found in the old one moves back to the young, so whatever is still in use survives
every turnover while what went cold ages out within two.
A hit in the young generation costs one lookup, and a hit in the old one a second lookup and a move. True LRU would cost a linked-list update on every hit, the operation being optimised, and served a warm name 15% slower for a few points of hit rate.
Adding an entry -- a miss, or a promotion from the old generation -- takes a lock,
so under free threading the turnover and the weight are exact and the bound holds.
A hit in the young generation takes none, being the path this exists to make cheap.
The lock is re-entrant: a signal handler or finalizer that runs demangle() while
this thread is inside put proceeds rather than deadlocks, at worst leaving the
weight one entry off.
epoch counts clear() calls. A caller that computes a value from state whose
change clears the cache reads it before computing and passes it to put, which then
drops the value if a clear came in between, rather than keep an answer from before.
hits and misses count lookups: a hit in either generation is a hit. They are
not guarded, so under free threading concurrent lookups can lose an increment and
the figures are approximate.
weigh(key, value), with max_weight, also bounds what the entries hold.
Source code in src/demangle/core/cache.py
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
get_old
¶
get_old(key)
Finish a lookup whose young generation already missed.
Source code in src/demangle/core/cache.py
90 91 92 93 94 95 96 97 98 99 100 | |
put
¶
put(key, value, epoch=None)
Store value and return it; not stored if epoch is given and out of date.
Source code in src/demangle/core/cache.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |