Skip to content

Public API

What import demangle gives a caller. A few of its exports belong to modules documented with the rest of the core; the list at the end says where.

import demangle

demangle.demangle("_ZNSt6vectorIiSaIiEE9push_backERKi")
# 'std::vector<int, std::allocator<int>>::push_back(int const&)'
demangle.demangle_type("PKFvRiE", language="itanium")
# 'void (*)(int&) const'
demangle.demangle("?f@@YAXH@Z", style=demangle.style("llvm", msvc={"calling_convention": False}))
# 'void f(int)'
demangle.signature("_ZNSt6vectorIiSaIiEE9push_backERKi").base_name
# 'push_back'

Functions

demangle.api

The public API.

Deliberately small. A handful of functions cover what callers actually do, and every one of them is stable ground that later versions can build on without breaking:

demangle(name)          the readable spelling, or the name unchanged   -- never raises
demangle_strict(name)   the readable spelling                          -- raises
parse(name)             a tree to inspect                              -- raises
detect(name)            which scheme, if any
demangle_all(names)     the batch form, cached
preload(*languages)     import schemes now, not on first use

demangle_type(enc, language=...)  a bare type encoding, spelled       -- raises
parse_type(enc, language=...)     a bare type encoding, as a tree     -- raises

"Raises" means over an unreadable name. Each also raises TypeError or ValueError for an argument that is wrong rather than unreadable -- an unknown language, say; detect only the ValueError, since it answers None to any name, a str or not.

language is one scheme's name, which forces that scheme, or None, which detects among all of them -- or, everywhere but the two type readers, a sequence of names, which detects among those alone.

Each single-name entry point has a ...b form taking and returning bytes, because a symbol table holds bytes rather than text.

The split between demangle and demangle_strict is the important one. A tool labelling every symbol in a binary meets far more non-mangled names than mangled ones, and treating that as an error would mean an exception per symbol; it wants the name back. A tool demangling one name a user typed wants to be told what was wrong with it. Serving both from one function -- with a sentinel, or a flag -- makes the common case carry the uncommon one's error handling, so they are two functions.

demangle

demangle(
    mangled,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

Return the readable spelling of mangled, or mangled unchanged.

Never raises over the name: whatever string it is given, a name this library cannot read comes back exactly as it went in, because a wrong expansion is worse than a mangled name -- it matches neither the original symbol nor the real declaration, so it corrupts every downstream lookup that trusted it. That promise is about the name, not about the other arguments: a mangled that is not a str, or a language, style or limits that names nothing, is a mistake in the calling code and is reported as one.

A sequence of names is an allow-list, for a caller that knows which schemes a table can hold -- a Mach-O image's C++ and Swift, say -- and wants nothing else rewriting its Objective-C metadata or its paths:

>>> import demangle
>>> demangle.demangle("_OBJC_CLASS_$_NSData", language=("itanium", "swift"))
'_OBJC_CLASS_$_NSData'
>>> demangle.demangle("$s10Foundation4DataV5countSivg", language=["itanium", "swift"])
'Foundation.Data.count.getter : Swift.Int'

It detects, among those schemes, in the usual order. So ("gnuv2",) is not "gnuv2", which forces the scheme on a name its detection would decline: PyInit__lldb stays as it is under the first and is read as pre-Itanium C++ under the second. One name or several, a sequence is a filter on detection, so an allow-list built at run time means the same whatever its length.

Parameters:

Name Type Description Default
mangled str

the symbol name. Any string; need not be mangled.

required
language str | Sequence[str] | None

force a scheme by name, detect among a sequence of names, or None to detect among all of them.

None
style str | Style | None

output spelling policy -- a style name ("llvm", the default, or "gnu"), a Style object such as style() returns, or None for the default.

DEFAULT_STYLE
limits Limits

resource bounds for the parse.

DEFAULT_LIMITS

Returns:

Type Description
str

The demangled name, or mangled unchanged.

Raises:

Type Description
TypeError

mangled is not a str. Use demangleb() for bytes.

ValueError

language or style is not a registered name, language is an empty sequence or names one that is not, or limits is not a Limits.

Source code in src/demangle/api.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
def demangle(
    mangled: str,
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> str:
    """Return the readable spelling of `mangled`, or `mangled` unchanged.

    Never raises over the *name*: whatever string it is given, a name this library
    cannot read comes back exactly as it went in, because a wrong expansion is worse
    than a mangled name -- it matches neither the original symbol nor the real
    declaration, so it corrupts every downstream lookup that trusted it. That promise is
    about the name, not about the other arguments: a `mangled` that is not a `str`, or a
    `language`, `style` or `limits` that names nothing, is a mistake in the calling code
    and is reported as one.

    A sequence of names is an allow-list, for a caller that knows which schemes a table
    can hold -- a Mach-O image's C++ and Swift, say -- and wants nothing else rewriting
    its Objective-C metadata or its paths:

        >>> import demangle
        >>> demangle.demangle("_OBJC_CLASS_$_NSData", language=("itanium", "swift"))
        '_OBJC_CLASS_$_NSData'
        >>> demangle.demangle("$s10Foundation4DataV5countSivg", language=["itanium", "swift"])
        'Foundation.Data.count.getter : Swift.Int'

    It detects, among those schemes, in the usual order. So `("gnuv2",)` is not
    `"gnuv2"`, which forces the scheme on a name its detection would decline:
    `PyInit__lldb` stays as it is under the first and is read as pre-Itanium C++ under
    the second. One name or several, a sequence is a filter on detection, so an
    allow-list built at run time means the same whatever its length.

    Args:
        mangled: the symbol name. Any string; need not be mangled.
        language: force a scheme by name, detect among a sequence of names, or None to
            detect among all of them.
        style: output spelling policy -- a style name (`"llvm"`, the default, or
            `"gnu"`), a `Style` object such as `style()` returns, or None for the
            default.
        limits: resource bounds for the parse.

    Returns:
        The demangled name, or `mangled` unchanged.

    Raises:
        TypeError: `mangled` is not a `str`. Use `demangleb()` for bytes.
        ValueError: `language` or `style` is not a registered name, `language` is an
            empty sequence or names one that is not, or `limits` is not a `Limits`.
    """
    if not isinstance(mangled, str):
        _refuse_non_string(mangled)
    if not mangled:
        _check_language(language)
        get_style(style)
        _check_limits(limits)
        return mangled
    # Loaded before the cache is touched: loading registers plugins, which clears it.
    # The registry flag rather than `_load()`, and `__class__ is str` rather than
    # `isinstance`, because this is the hottest path in the package.
    if not _registry._loaded:
        _registry._load()
    # A `Style` object (or a `str` subclass) is not cached: two objects can share a name,
    # and a style does not hash by value. A name is looked up before it is resolved, so a
    # hit costs no style lookup; one that is not registered misses, and is refused below.
    if style.__class__ is str or style is None:
        # The default limits keyed by a sentinel: a `Limits` hashes through a Python-level
        # `__hash__`, twice on every miss, and nearly every call passes the default.
        if limits is DEFAULT_LIMITS:
            limits_key = _DEFAULT_LIMITS_KEY
        elif isinstance(limits, Limits):
            limits_key = limits
        else:
            _refuse_limits(limits)
        key = (mangled, language, DEFAULT_STYLE if style is None else style, limits_key)
    else:
        key = None
        # The same checks, in the same order, as a cached call, so caching never decides
        # what is accepted or which argument the refusal names.
        if limits is not DEFAULT_LIMITS and not isinstance(limits, Limits):
            _refuse_limits(limits)
        if _is_allow_list(language):
            language = tuple(language)
        try:
            hash((language, limits))
        except TypeError:
            _refuse_unhashable(language, limits)
    if key is not None:
        # An unhashable argument surfaces here as a `TypeError`; not checked in advance,
        # which would hash `limits` twice on every warm call.
        try:
            # Repeated names usually live in the young generation. A direct lookup
            # avoids a Python call while the miss path retains promotion and bounds.
            cached = _CACHE._young.get(key, MISSING)
            if cached is MISSING:
                cached = _CACHE.get_old(key)
            else:
                _CACHE.hits += 1
        except TypeError:
            if _is_allow_list(language) and type(language) is not tuple:
                # The allow-list a tuple would be, keyed as one; a tuple that cannot be
                # keyed holds something that is not a name.
                return demangle(mangled, language=tuple(language), style=style, limits=limits)
            _refuse_unhashable(language, limits)
        if cached is not MISSING:
            return cached
        epoch = _CACHE.epoch

    # `get_style`'s lookup, inline where the key already holds a registered name.
    styles = _style_module._STYLES
    resolved_style = None if key is None or styles is None else styles.get(key[2])
    if resolved_style is None:
        resolved_style = get_style(style)
    builder = resolved_style.spelling_builder
    if language is None:
        tried = candidates(mangled)
        if not tried:
            return mangled
        base = False
        detecting = True
    elif isinstance(language, str):
        tried = (_resolve(language),)
        base = None
        detecting = False
    else:
        tried = _among(mangled, _allowed(language))
        if not tried:
            return mangled
        base = False
        detecting = True

    for candidate in tried:
        try:
            # `_claims` inlined for the hot path; a `detect` that throws is caught by this
            # loop's own handler with the same effect.
            if detecting and not candidate.detect(mangled):
                if not candidate.symbol_table_decorations:
                    continue
                if base is False:
                    base = _undecorated(mangled)
                if base is None or not candidate.detect(base):
                    continue
            handle = _parse_with(candidate, mangled, builder, limits, resolved_style)
        except LimitExceeded:
            # A limit is not "this name is not mine": offering it to the next scheme would
            # read `_ZN11Expressions2f2ILi1EEEvPApsT__i` as a pre-Itanium name.
            break
        except Exception as exc:
            reraise_if_operational(exc)
            # Detection may be wrong and a plugin may be buggy; either way try the next.
            continue
        result = builder.spell(handle)
        return result if key is None else _CACHE.put(key, result, epoch)

    return mangled if key is None else _CACHE.put(key, mangled, epoch)

demangle_strict

demangle_strict(
    mangled,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

Return the readable spelling of mangled, raising when it cannot be read.

Takes the same arguments as demangle(). Every failure to read the name is a DemanglingError; a plugin that fails some other way is wrapped in a ParseError, with the original chained.

Raises:

Type Description
NotMangledError

the name matches no known scheme, or none that language allows.

ParseError

the name has a known prefix but does not follow the grammar.

LimitExceeded

a resource bound was hit.

TypeError

mangled is not a str. Use demangleb_strict() for bytes.

ValueError

language or style is not a registered name, language is an empty sequence or names one that is not, or limits is not a Limits.

Source code in src/demangle/api.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
def demangle_strict(
    mangled: str,
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> str:
    """Return the readable spelling of `mangled`, raising when it cannot be read.

    Takes the same arguments as `demangle()`. Every failure to read the name is a
    `DemanglingError`; a plugin that fails some other way is wrapped in a `ParseError`,
    with the original chained.

    Raises:
        NotMangledError: the name matches no known scheme, or none that `language`
            allows.
        ParseError: the name has a known prefix but does not follow the grammar.
        LimitExceeded: a resource bound was hit.
        TypeError: `mangled` is not a `str`. Use `demangleb_strict()` for bytes.
        ValueError: `language` or `style` is not a registered name, `language` is an
            empty sequence or names one that is not, or `limits` is not a `Limits`.
    """
    resolved_style = get_style(style)
    builder = resolved_style.spelling_builder
    return builder.spell(_read(mangled, builder, language, resolved_style, limits)[1])

parse

parse(
    mangled,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

Parse mangled into a tree.

Use this when the parts matter -- the namespace, the template arguments, the parameter types -- rather than the spelling. The result is a core.ast.Node supporting .walk(), .find(kind) and .spell().

Raises the same errors as demangle_strict. Which kinds of node a tree can hold differs from scheme to scheme; see node_kinds().

Source code in src/demangle/api.py
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
def parse(
    mangled: str,
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> Node:
    """Parse `mangled` into a tree.

    Use this when the *parts* matter -- the namespace, the template arguments, the
    parameter types -- rather than the spelling. The result is a `core.ast.Node`
    supporting `.walk()`, `.find(kind)` and `.spell()`.

    Raises the same errors as `demangle_strict`. Which kinds of node a tree can hold
    differs from scheme to scheme; see `node_kinds()`.
    """
    resolved = get_style(style)
    return _read(mangled, builder_for(resolved), language, resolved, limits)[1]

detect

detect(mangled, *, language=None, strict=False)

Name the scheme mangled appears to use, or None.

Most schemes are recognised by a prefix, so this reports what the name looks like, not that it will parse. The schemes whose names carry no marker -- gnuv2, codewarrior and ada -- parse the whole name to claim it, and so does a legacy Rust name written without its underscore (a bare ZN...), which is read up to max_input to decide.

strict=True asks the other question: which scheme reads the name. The answer is the one demangle() would use, under the default style and limits -- the first in detection order that claims the name and then parses it -- or None where it would hand the name back. It costs a parse, which is why it is not the default:

>>> import demangle
>>> demangle.detect("_ZN3Foo")
'itanium'
>>> demangle.detect("_ZN3Foo", strict=True) is None
True

language narrows the question to the schemes it names, one or a sequence: the answer is the first of them, in the usual order, that claims the name. With strict, one name forces that scheme as it does for demangle(), and the answer is whether it reads the name.

Never raises over the name: like demangle(), it is called on every symbol in a table. A language that names nothing is the calling code's mistake, and a ValueError.

Source code in src/demangle/api.py
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
def detect(
    mangled: str,
    *,
    language: str | Sequence[str] | None = None,
    strict: bool = False,
) -> str | None:
    """Name the scheme `mangled` appears to use, or None.

    Most schemes are recognised by a prefix, so this reports what the name looks like,
    not that it will parse. The schemes whose names carry no marker -- `gnuv2`,
    `codewarrior` and `ada` -- parse the whole name to claim it, and so does a legacy
    Rust name written without its underscore (a bare `ZN...`), which is read up to
    `max_input` to decide.

    `strict=True` asks the other question: which scheme *reads* the name. The answer is
    the one `demangle()` would use, under the default style and limits -- the first in
    detection order that claims the name and then parses it -- or None where it would
    hand the name back. It costs a parse, which is why it is not the default:

        >>> import demangle
        >>> demangle.detect("_ZN3Foo")
        'itanium'
        >>> demangle.detect("_ZN3Foo", strict=True) is None
        True

    `language` narrows the question to the schemes it names, one or a sequence: the
    answer is the first of them, in the usual order, that claims the name. With
    `strict`, one name forces that scheme as it does for `demangle()`, and the answer
    is whether it reads the name.

    Never raises over the name: like `demangle()`, it is called on every symbol in a
    table. A `language` that names nothing is the calling code's mistake, and a
    `ValueError`.
    """
    if language is not None:
        allowed = _allowed((language,) if isinstance(language, str) else language)
    if not isinstance(mangled, str):
        # `detect` answers a verdict for anything in a symbol table, never an exception.
        return None
    if not mangled:
        return None
    if strict:
        resolved = get_style(DEFAULT_STYLE)
        try:
            return _read(mangled, resolved.spelling_builder, language, resolved, DEFAULT_LIMITS)[0].name
        except DemanglingError:
            return None
    tried = candidates(mangled) if language is None else _among(mangled, allowed)
    if not tried:
        return None
    base = _undecorated(mangled)
    for plugin in tried:
        # `_claims` inlined, as in `demangle`.
        try:
            if plugin.detect(mangled) or (base is not None and plugin.symbol_table_decorations and plugin.detect(base)):
                return plugin.name
        except Exception as exc:
            reraise_if_operational(exc)
    return None

demangle_all

demangle_all(
    names_,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

Demangle an iterable of names, yielding results in order.

Returns a generator, so a caller streaming a large symbol table never holds more than one result at a time beyond what it keeps itself. Shares the module cache, which is where the real gain is: symbol tables repeat names heavily.

Arguments are validated before the generator is created, so a bad language, style or limits is reported at the call rather than at the first next().

Source code in src/demangle/api.py
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
def demangle_all(
    names_: Iterable[str],
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> Iterator[str]:
    """Demangle an iterable of names, yielding results in order.

    Returns a generator, so a caller streaming a large symbol table never holds more
    than one result at a time beyond what it keeps itself. Shares the module cache,
    which is where the real gain is: symbol tables repeat names heavily.

    Arguments are validated before the generator is created, so a bad `language`,
    `style` or `limits` is reported at the call rather than at the first `next()`.
    """
    language = _check_language(language)
    get_style(style)
    _check_limits(limits)

    def _stream():
        for name in names_:
            yield demangle(name, language=language, style=style, limits=limits)

    return _stream()

demangle_type

demangle_type(
    mangled,
    *,
    language,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

Return the readable spelling of a bare type encoding.

Pi is int*, PEAX is void *, SaySiG is [Swift.Int]. This is what a typeinfo name, an RTTI type descriptor and a Swift metadata typeref carry: a type on its own, with none of the surrounding symbol a name has.

language is required, and cannot be made optional. A whole symbol says which scheme it belongs to -- _Z, ?, $s -- but a type encoding says nothing at all: i is a valid Itanium type, a valid Swift type, and a valid C identifier, so there is no evidence to detect on. Guessing would mean reading plain C symbols as types, which is why every reference puts this behind a flag of its own -- c++filt -t, libiberty's DMGL_TYPES, UnDecorateSymbolName's UNDNAME_TYPE_ONLY.

Raises rather than passing the name through, unlike demangle(). A caller who named the scheme is asking a question about one encoding they already believe is one, not labelling a table of symbols, and wants to be told when it is not.

Parameters:

Name Type Description Default
mangled str

the type encoding.

required
language str

which scheme to read it as -- required. See languages().

required
style str | Style | None

output spelling policy -- a style name ("llvm", the default, or "gnu"), a Style object such as style() returns, or None for the default.

DEFAULT_STYLE
limits Limits

resource bounds for the parse.

DEFAULT_LIMITS

Raises:

Type Description
ValueError

language is unknown, or names a scheme with no type grammar, style is not a registered name, or limits is not a Limits.

TypeError

mangled is not a str. Use demangleb_type() for bytes.

NotMangledError

the encoding is empty.

ParseError

the encoding does not follow the scheme's type grammar.

LimitExceeded

a resource bound was hit.

Source code in src/demangle/api.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
def demangle_type(
    mangled: str,
    *,
    language: str,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> str:
    """Return the readable spelling of a bare *type* encoding.

    `Pi` is `int*`, `PEAX` is `void *`, `SaySiG` is `[Swift.Int]`. This is what a
    `typeinfo` name, an RTTI type descriptor and a Swift metadata typeref carry: a type
    on its own, with none of the surrounding symbol a name has.

    `language` is required, and cannot be made optional. A whole symbol says which
    scheme it belongs to -- `_Z`, `?`, `$s` -- but a type encoding says nothing at all:
    `i` is a valid Itanium type, a valid Swift type, and a valid C identifier, so there
    is no evidence to detect on. Guessing would mean reading plain C symbols as types,
    which is why every reference puts this behind a flag of its own -- `c++filt -t`,
    libiberty's `DMGL_TYPES`, `UnDecorateSymbolName`'s `UNDNAME_TYPE_ONLY`.

    Raises rather than passing the name through, unlike `demangle()`. A caller who named
    the scheme is asking a question about one encoding they already believe is one, not
    labelling a table of symbols, and wants to be told when it is not.

    Args:
        mangled: the type encoding.
        language: which scheme to read it as -- required. See `languages()`.
        style: output spelling policy -- a style name (`"llvm"`, the default, or
            `"gnu"`), a `Style` object such as `style()` returns, or None for the
            default.
        limits: resource bounds for the parse.

    Raises:
        ValueError: `language` is unknown, or names a scheme with no type grammar,
            `style` is not a registered name, or `limits` is not a `Limits`.
        TypeError: `mangled` is not a `str`. Use `demangleb_type()` for bytes.
        NotMangledError: the encoding is empty.
        ParseError: the encoding does not follow the scheme's type grammar.
        LimitExceeded: a resource bound was hit.
    """
    resolved = get_style(style)
    builder = resolved.spelling_builder
    return builder.spell(_parse_type_handle(mangled, builder, language, resolved, limits))

parse_type

parse_type(
    mangled,
    *,
    language,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

Parse a bare type encoding into a tree.

demangle_type() is to demangle_strict() as this is to parse(): same input, same required language, same errors -- a core.ast.Node instead of a string.

Source code in src/demangle/api.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
def parse_type(
    mangled: str,
    *,
    language: str,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> Node:
    """Parse a bare *type* encoding into a tree.

    `demangle_type()` is to `demangle_strict()` as this is to `parse()`: same input,
    same required `language`, same errors -- a `core.ast.Node` instead of a string.
    """
    resolved = get_style(style)
    return _parse_type_handle(mangled, builder_for(resolved), language, resolved, limits)

style

style(base=DEFAULT_STYLE, /, **languages)

Compose a style for one call: a named one, with some per-language changes.

The two named styles say how to spell a name; what a caller usually wants to vary is how much of it to spell. Every peer tool composes that at the call site -- llvm-undname --no-calling-convention, UnDecorateSymbolName's mask -- and this is the same thing without a global registration:

>>> import demangle
>>> narrow = demangle.style("llvm", msvc={"calling_convention": False})
>>> demangle.demangle("?f@@YAXH@Z", style=narrow)
'void f(int)'
>>> demangle.demangle("?f@@YAXH@Z")
'void __cdecl f(int)'

Each keyword is a language name; each value is that language's options object or a mapping of the fields to change. languages() lists the names, and each scheme's options module documents what it accepts.

The result is an object rather than a registered name, and that is what keeps it to one call: demangle() does not cache a call that passes a style object, so a narrower spelling asked for here is never served to a caller asking for the default.

Raises:

Type Description
ValueError

base is not a known style, or a language named has no options.

Source code in src/demangle/api.py
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
def style(base: str | Style | None = DEFAULT_STYLE, /, **languages: Any) -> Style:
    """Compose a style for one call: a named one, with some per-language changes.

    The two named styles say how to spell a name; what a caller usually wants to vary is
    how *much* of it to spell. Every peer tool composes that at the call site --
    `llvm-undname --no-calling-convention`, `UnDecorateSymbolName`'s mask -- and this is
    the same thing without a global registration:

        >>> import demangle
        >>> narrow = demangle.style("llvm", msvc={"calling_convention": False})
        >>> demangle.demangle("?f@@YAXH@Z", style=narrow)
        'void f(int)'
        >>> demangle.demangle("?f@@YAXH@Z")
        'void __cdecl f(int)'

    Each keyword is a language name; each value is that language's options object or a
    mapping of the fields to change. `languages()` lists the names, and each scheme's
    `options` module documents what it accepts.

    The result is an object rather than a registered name, and that is what keeps it to
    one call: `demangle()` does not cache a call that passes a style object, so a
    narrower spelling asked for here is never served to a caller asking for the default.

    Raises:
        ValueError: `base` is not a known style, or a language named has no options.
    """
    return get_style(base).with_options(**languages)

node_kinds

node_kinds(language=None)

Every kind a parse() tree can hold, sorted. One scheme's, or all of them.

The vocabulary to switch on, published rather than left to be read out of the source:

>>> import demangle
>>> demangle.node_kinds("d")
('name', 'path', 'symbol')

What each kind means differs with what the language has to say. A C++ tree carries declarator shape -- pointer, array, function -- because a C++ type wraps the name it declares. Rust has no declarator syntax, so its trees carry path structure instead: symbol, path, impl, namespace. name, template and literal mean the same thing wherever they appear.

Raises:

Type Description
ValueError

language is not a known scheme.

Source code in src/demangle/api.py
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
def node_kinds(language: str | None = None) -> tuple[str, ...]:
    """Every `kind` a `parse()` tree can hold, sorted. One scheme's, or all of them.

    The vocabulary to switch on, published rather than left to be read out of the source:

        >>> import demangle
        >>> demangle.node_kinds("d")
        ('name', 'path', 'symbol')

    What each kind *means* differs with what the language has to say. A C++ tree carries
    declarator shape -- `pointer`, `array`, `function` -- because a C++ type wraps the
    name it declares. Rust has no declarator syntax, so its trees carry path structure
    instead: `symbol`, `path`, `impl`, `namespace`. `name`, `template` and `literal` mean
    the same thing wherever they appear.

    Raises:
        ValueError: `language` is not a known scheme.
    """
    if language is not None:
        return tuple(sorted(_resolve(language).node_kinds))
    from .core.registry import available

    return tuple(sorted({kind for plugin in available() for kind in plugin.node_kinds}))

languages

languages()

Registered language names. Third-party plugins appear once load_plugins() has run.

Source code in src/demangle/api.py
908
909
910
def languages() -> list[str]:
    """Registered language names. Third-party plugins appear once `load_plugins()` has run."""
    return names()

styles

styles()

Registered output style names.

Source code in src/demangle/api.py
941
942
943
def styles() -> list[str]:
    """Registered output style names."""
    return available_styles()

preload

preload(*languages)

Import schemes now rather than when a name first reaches them.

A built-in scheme is imported on first use, so a script reading one name pays for the scheme that name needs and no other -- and the first MSVC name a process reads pays about 7 ms for the import. A long-running service would rather pay at start-up:

>>> import demangle
>>> demangle.preload("itanium", "msvc")
>>> demangle.preload()

Names, or aliases, import those schemes; none imports every built-in. The order and every answer are the same either way: this moves the cost, nothing else.

Raises:

Type Description
ValueError

a name is not a registered language. Nothing is imported then.

Source code in src/demangle/api.py
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
def preload(*languages: str) -> None:
    """Import schemes now rather than when a name first reaches them.

    A built-in scheme is imported on first use, so a script reading one name pays for
    the scheme that name needs and no other -- and the first MSVC name a process reads
    pays about 7 ms for the import. A long-running service would rather pay at start-up:

        >>> import demangle
        >>> demangle.preload("itanium", "msvc")
        >>> demangle.preload()

    Names, or aliases, import those schemes; none imports every built-in. The order and
    every answer are the same either way: this moves the cost, nothing else.

    Raises:
        ValueError: a name is not a registered language. Nothing is imported then.
    """
    for language in languages:
        try:
            canonical(language)
        except KeyError:
            _refuse_language(language)
    if not languages:
        _registry.available()
    for language in languages:
        get(language)

load_plugins

load_plugins()

Register the plugins installed distributions advertise, once.

Third-party languages are opt-in: a distribution advertising the demangle.languages entry-point group is not read until this is called, because a plugin may carry the name of a built-in and register lets it replace that scheme. Calling it is the caller's decision to accept that. Explicit register calls are not affected.

Safe to call from any thread and as often as you like; only the first call looks. A plugin that fails to load is skipped with a RuntimeWarning.

Source code in src/demangle/core/registry.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
def load_plugins():
    """Register the plugins installed distributions advertise, once.

    Third-party languages are opt-in: a distribution advertising the `demangle.languages`
    entry-point group is not read until this is called, because a plugin may carry the
    name of a built-in and `register` lets it replace that scheme. Calling it is the
    caller's decision to accept that. Explicit `register` calls are not affected.

    Safe to call from any thread and as often as you like; only the first call looks.
    A plugin that fails to load is skipped with a `RuntimeWarning`.
    """
    global _plugins_loaded
    _load()
    with _lock:
        if _plugins_loaded:
            return
        _plugins_loaded = True
        _load_entry_points()

demangleb

demangleb(
    mangled,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

demangle() over bytes, returning bytes.

The form to use when the names come from a symbol table, which is where they usually do: an ELF or Mach-O string table holds bytes, and a tool that has read one should not have to guess an encoding to ask what a name says.

Like demangle(), never raises for a name it cannot read -- it hands the bytes back exactly as they arrived, including any that are not valid UTF-8. Raises TypeError if given something that is not bytes-like, and ValueError for the arguments demangle() refuses, which are mistakes in the calling code rather than properties of the symbol.

>>> import demangle
>>> demangle.demangleb(b"_ZN3foo3barEv")
b'foo::bar()'
Source code in src/demangle/api.py
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
def demangleb(
    mangled: bytes,
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> bytes:
    """`demangle()` over bytes, returning bytes.

    The form to use when the names come from a symbol table, which is where they usually
    do: an ELF or Mach-O string table holds bytes, and a tool that has read one should
    not have to guess an encoding to ask what a name says.

    Like `demangle()`, never raises for a name it cannot read -- it hands the bytes back
    exactly as they arrived, including any that are not valid UTF-8. Raises `TypeError`
    if given something that is not bytes-like, and `ValueError` for the arguments
    `demangle()` refuses, which are mistakes in the calling code rather than properties
    of the symbol.

        >>> import demangle
        >>> demangle.demangleb(b"_ZN3foo3barEv")
        b'foo::bar()'
    """
    return demangle(_decode(mangled), language=language, style=style, limits=limits).encode(
        _BYTES_ENCODING, _BYTES_ERRORS
    )

demangleb_strict

demangleb_strict(
    mangled,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

demangle_strict() over bytes, returning bytes. Raises what it raises.

Source code in src/demangle/api.py
813
814
815
816
817
818
819
820
821
822
823
def demangleb_strict(
    mangled: bytes,
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> bytes:
    """`demangle_strict()` over bytes, returning bytes. Raises what it raises."""
    return demangle_strict(_decode(mangled), language=language, style=style, limits=limits).encode(
        _BYTES_ENCODING, _BYTES_ERRORS
    )

demangleb_type

demangleb_type(
    mangled,
    *,
    language,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

demangle_type() over bytes, returning bytes.

A type encoding is read out of a binary as often as a symbol is -- an Itanium typeinfo name sits in .rodata and an MSVC type descriptor's name in .rdata -- so it gets the same bytes form the rest of the package has. Raises what demangle_type() raises.

Source code in src/demangle/api.py
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
def demangleb_type(
    mangled: bytes,
    *,
    language: str,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> bytes:
    """`demangle_type()` over bytes, returning bytes.

    A type encoding is read out of a binary as often as a symbol is -- an Itanium
    `typeinfo` name sits in `.rodata` and an MSVC type descriptor's name in `.rdata` --
    so it gets the same bytes form the rest of the package has. Raises what
    `demangle_type()` raises.
    """
    return demangle_type(_decode(mangled), language=language, style=style, limits=limits).encode(
        _BYTES_ENCODING, _BYTES_ERRORS
    )

parseb

parseb(
    mangled,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

parse() over bytes. The tree it returns spells str, as every tree does.

Source code in src/demangle/api.py
826
827
828
829
830
831
832
833
834
def parseb(
    mangled: bytes,
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> Node:
    """`parse()` over bytes. The tree it returns spells `str`, as every tree does."""
    return parse(_decode(mangled), language=language, style=style, limits=limits)

parseb_type

parseb_type(
    mangled,
    *,
    language,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

parse_type() over bytes. The tree it returns spells str, as every tree does.

Source code in src/demangle/api.py
856
857
858
859
860
861
862
863
864
def parseb_type(
    mangled: bytes,
    *,
    language: str,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> Node:
    """`parse_type()` over bytes. The tree it returns spells `str`, as every tree does."""
    return parse_type(_decode(mangled), language=language, style=style, limits=limits)

detectb

detectb(mangled, *, language=None, strict=False)

detect() over bytes. The scheme's name is a str; it is this package's own.

Source code in src/demangle/api.py
867
868
869
870
871
872
873
874
875
876
877
878
def detectb(
    mangled: bytes,
    *,
    language: str | Sequence[str] | None = None,
    strict: bool = False,
) -> str | None:
    """`detect()` over bytes. The scheme's name is a `str`; it is this package's own."""
    if not isinstance(mangled, _BYTES_LIKE):
        # Refused as `detect` refuses it, whatever the name is.
        detect("", language=language)
        return None
    return detect(bytes(mangled).decode(_BYTES_ENCODING, _BYTES_ERRORS), language=language, strict=strict)

cache_clear

cache_clear()

Empty the module-level result cache.

Source code in src/demangle/api.py
946
947
948
def cache_clear() -> None:
    """Empty the module-level result cache."""
    _CACHE.clear()

cache_stats

cache_stats()

Hit rate and occupancy of the result cache.

A name no scheme is offered is looked up but never stored, so it counts as a miss on every call.

Source code in src/demangle/api.py
951
952
953
954
955
956
957
def cache_stats() -> dict[str, Any]:
    """Hit rate and occupancy of the result cache.

    A name no scheme is offered is looked up but never stored, so it counts as a miss on
    every call.
    """
    return _CACHE.stats

A file, not a name

demangle.filter

Demangling the symbols out of text that is not only symbols.

nm writes an address and a type letter before a name; a crash log writes a frame number; an objdump listing writes a whole disassembly around one. So the useful operation over a file is not "demangle this name" but "substitute every symbol-shaped word and copy everything else through" -- which is what c++filt, demumble and rustfilt all do, and what the demangle command does with no arguments.

rustc-demangle ships demangle_stream as a crate function rather than only inside rustfilt, and this is the same thing, so a Python caller need neither shell out to the command nor re-implement its tokeniser:

>>> import demangle
>>> demangle.demangle_text("0000000000001139 T _ZN3foo3barEv\n")
'0000000000001139 T foo::bar()\n'

find_symbols is the same scan without the substitution, for a caller who needs to know where in a line a symbol was -- which is the question microsoftDemangle's n_read out-parameter and llvm-undname --warn-trailing are asked in C.

Found

Bases: NamedTuple

One symbol inside a longer string, and what it says.

start instance-attribute

start

Index of the first character of the mangled name.

end instance-attribute

end

Index one past its last character, so text[start:end] is the name.

mangled instance-attribute

mangled

The name as it appeared.

demangled instance-attribute

demangled

Its readable spelling.

find_symbols

find_symbols(
    text,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

Yield every symbol in text that this library can read, in order.

Only the ones it can read: a word that looks symbol-shaped and does not demangle is not a symbol, and is left out rather than reported as one that spells itself.

The spans do not overlap and are in increasing order, so a caller can rebuild the string around them -- which is all demangle_text does.

Source code in src/demangle/filter.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def find_symbols(
    text: str,
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> Iterator[Found]:
    """Yield every symbol in `text` that this library can read, in order.

    Only the ones it *can* read: a word that looks symbol-shaped and does not demangle is
    not a symbol, and is left out rather than reported as one that spells itself.

    The spans do not overlap and are in increasing order, so a caller can rebuild the
    string around them -- which is all `demangle_text` does.
    """
    # Validated here: a text with no symbols would never reach `demangle`.
    language = _check_language(language)
    get_style(style)
    _check_limits(limits)
    return _find_symbols(text, language=language, style=style, limits=limits)

demangle_text

demangle_text(
    text,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

Substitute every symbol in text, copying everything else through unchanged.

Never raises over the text, for the reason demangle() does not: this is run over whole files, and one unreadable word must not end the run. Like demangle(), it raises ValueError when language or style is not a registered name, or limits is not a Limits.

Source code in src/demangle/filter.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def demangle_text(
    text: str,
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> str:
    """Substitute every symbol in `text`, copying everything else through unchanged.

    Never raises over the text, for the reason `demangle()` does not: this is run over
    whole files, and one unreadable word must not end the run. Like `demangle()`, it
    raises `ValueError` when `language` or `style` is not a registered name, or `limits`
    is not a `Limits`.
    """
    language = _check_language(language)
    get_style(style)
    _check_limits(limits)
    return _demangle_text(text, language=language, style=style, limits=limits)

demangle_stream

demangle_stream(
    fin,
    fout,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

demangle_text over a file, a line at a time.

A line at a time rather than all at once, so this stays a pipe: someone watching nm ... | demangle should not have to wait for the input to end. fin is anything iterable by lines and fout anything with write.

Encoding is the caller's: a symbol table holds bytes and they are not reliably UTF-8, so open both ends with errors="surrogateescape" if the input is one. The demangle command does exactly that.

Source code in src/demangle/filter.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def demangle_stream(
    fin,
    fout,
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> None:
    """`demangle_text` over a file, a line at a time.

    A line at a time rather than all at once, so this stays a pipe: someone watching
    `nm ... | demangle` should not have to wait for the input to end. `fin` is anything
    iterable by lines and `fout` anything with `write`.

    Encoding is the caller's: a symbol table holds bytes and they are not reliably UTF-8,
    so open both ends with `errors="surrogateescape"` if the input is one. The `demangle`
    command does exactly that.
    """
    language = _check_language(language)
    get_style(style)
    _check_limits(limits)
    for line in fin:
        fout.write(_demangle_text(line, language=language, style=style, limits=limits))

The parts of a name

The parts of a symbol, in one shape, for every scheme.

demangle() answers "what does this name say"; this answers "what are its pieces". A disassembler labelling a call site wants the base name without its namespace; a cross-reference wants the namespace without the base name; a signature matcher wants the parameter types and nothing else. All three can be had by splitting the spelling, and all three get it wrong the same way, because :: and , and ( occur inside template arguments and inside function types as well as between the parts a caller means.

So the split is done on the tree the parser already built. Two shapes of tree, because the schemes come in two kinds:

  • Itanium and MSVC build a declaration -- a function node with its name inside its own type, or a declarator standing beside one. The fields are read off it directly.
  • Swift, D, Go, Nim, Free Pascal, Delphi, Ada, JNI and the pre-Itanium C++ schemes (gnuv2, codewarrior) build a node that is its own fragments, in output order: parts interleaves the literal text the printer wrote with the subtrees it wrote between them. That interleaving is the structure -- a . in parts separates two components and a . inside a name does not -- so reading it gives the same answer the printer gave, which splitting the printer's output could not.

Rust's path and Objective-C's class and selector are read off their nodes as they stand, and an Objective-C runtime data symbol is split into the label it opens with and the class, category, instance variable or selector the label is about.

Text is the last resort, for the places where a tree has already flattened the answer, and even there with a reader that counts brackets and that declines a spelling which is a phrase rather than a name.

What each scheme can say still differs, and the difference is real rather than a gap to be filled in later:

  • C++, in all four of its manglings here -- Itanium, MSVC, pre-Itanium g++ and its relatives, CodeWarrior -- encodes parameter types, the return type where the mangling writes one, cv- and ref-qualifiers, and for MSVC the calling convention and the declared access.
  • Swift encodes the whole function type, so parameters, result and throws all come back. D, Free Pascal and Delphi encode their parameter types, Free Pascal a function's result, and Delphi the result of a template function and its calling convention; JNI encodes them for an overloaded method, the one form that needs them.
  • Rust, Go, Nim, Ada and Objective-C encode a path, or a class and a selector, and no signature at all. parameters is None for them, which is not the same as (): one says "the name does not carry this", the other says "it carries an empty list".

The Nones are the point. A field that guesses is worse than a field that declines.

Signature dataclass

Signature(
    language,
    demangled,
    qualified_name,
    base_name,
    namespace,
    parameters,
    return_type,
    calling_convention,
    qualifiers=(),
    special=None,
    decoration="",
    is_function=False,
    is_data=False,
    is_ctor_or_dtor=False,
)

What a mangled name says about the entity it names.

Every field is either what the name encodes or None/empty where it encodes nothing. Nothing here is inferred from a spelling that does not say it.

The three name fields hold to one invariant, which is what makes them safe to recombine: where namespace is not empty, joining it to base_name with the scheme's separator gives qualified_name exactly. Template arguments therefore stay on base_name -- sort<int*>, not sort -- because they are part of the component they belong to.

language instance-attribute

language

The scheme that read the name: "itanium", "msvc", "rust" and so on.

demangled instance-attribute

demangled

The whole readable spelling, the same string demangle() returns.

qualified_name instance-attribute

qualified_name

The entity's name with its scope and without its type.

base_name instance-attribute

base_name

The last component of that. Never empty for a name that parsed.

namespace instance-attribute

namespace

Everything before the last component. Empty at the top level.

parameters instance-attribute

parameters

The parameter types, spelled, or None where the name encodes no parameter list.

() and None mean different things: an Itanium function taking no arguments has (), and a Rust path has None because Rust does not put the signature in the symbol. MSVC and pre-Itanium g++ spell an empty list (void), and it comes back as they spell it: ('void',).

return_type instance-attribute

return_type

The return type, spelled, or None where the name does not encode one.

Most C++ functions do not: the ABI omits it, because overloads cannot differ by it. A template specialisation does. For a data symbol this is the type of the object.

calling_convention instance-attribute

calling_convention

__cdecl, __stdcall, ... . MSVC and Delphi; no other scheme writes one.

Delphi's __saveregs is kept with it, after the convention it modifies.

qualifiers class-attribute instance-attribute

qualifiers = ()

What qualifies the declaration rather than its type.

The trailing cv- and ref-qualifiers of a member function -- const, &&, noexcept -- for MSVC the leading access and storage words private, static, virtual, and for Swift the effects a function declares: throws, async.

special class-attribute instance-attribute

special = None

What the symbol is about, where it is about something rather than being it.

vtable for, typeinfo for, guard variable for, non-virtual thunk to; D's initializer for; Swift's protocol requirements base descriptor for; Objective-C's Objective-C class and instance variable offset for. The other fields then describe the entity the symbol is about, so base_name on a vtable for std::ostream is ostream, and on _OBJC_CLASS_$_NSData is NSData.

Not every scheme writes its label first. MSVC writes most of its labels after the name, Base::`vftable', C::f`adjustor{16}', and Delphi writes __linkproc__ between a unit and the procedure, System::__linkproc__ Abort; the label is taken from wherever it sits, so qualified_name is Base, C::f, System::Abort. The label comes without its quotes and without what follows it about its own table: adjustor, not adjustor{16}; RTTI Base Class Descriptor, without its at (0, -1, 0, 64); vftable, without its {for `A'}. A label with nothing to be about stands as the name too: MSVC's `vector ctor iterator' has special and qualified_name both vector ctor iterator. Only the last label is taken: a thunk over a compiler-made member, Base::`vector deleting dtor'`adjustor{4}', is adjustor, and the member it adjusts, Base::`vector deleting dtor', is the name.

decoration class-attribute instance-attribute

decoration = ''

What the symbol table appended, separator included: "@@GLIBCXX_3.4", ".cold".

is_function class-attribute instance-attribute

is_function = False

Whether the name says it encodes a function.

False where the name does not say. A Rust or Go symbol is a path, and a path to a function reads exactly like a path to a static, so neither is claimed to be either.

is_data class-attribute instance-attribute

is_data = False

Whether the name encodes an object rather than a function.

Of the symbols with a special label, MSVC's tables, descriptors and guards set it. The others -- Itanium's vtable for and typeinfo for, Delphi's __tpdsc__, Free Pascal's run-time type information for -- leave it False.

is_ctor_or_dtor class-attribute instance-attribute

is_ctor_or_dtor = False

Whether it is a constructor or a destructor of the type it sits in.

C++ repeats the class's own name, or negates it; Swift writes init and deinit; Delphi's $bctr and $bdtr spell C++'s TForm::TForm and TForm::~TForm, and its `class constructor` and `class destructor` are structors of the class too. No other scheme here marks the two, so no other scheme reports them. MSVC's scalar deleting dtor, vector deleting dtor and vbase dtor are destructors as Itanium's deleting destructor is, and so is a thunk to one, Base::`vector deleting dtor'`adjustor{4}'; its closures and iterators are not.

is_special property

is_special

Whether the symbol is about an entity rather than being one.

signature

signature(
    mangled,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

The parts of mangled.

Raises the same errors as demangle_strict: a name that cannot be read has no parts, and a Signature full of None would say that it did and that they were all empty.

Source code in src/demangle/_signature.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def signature(
    mangled: str,
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> Signature:
    """The parts of `mangled`.

    Raises the same errors as `demangle_strict`: a name that cannot be read has no
    parts, and a `Signature` full of `None` would say that it did and that they were
    all empty.
    """
    resolved = get_style(style)
    # The plugin that read the name, under the name it is registered by, so an alias
    # (`objective-c`) finds its separator.
    plugin, tree = _read(mangled, builder_for(resolved), language, resolved, limits)
    return _extract(_Reading(plugin.name, _SEPARATORS.get(plugin.name, "::"), style), tree)

signatureb

signatureb(
    mangled,
    *,
    language=None,
    style=DEFAULT_STYLE,
    limits=DEFAULT_LIMITS,
)

signature() over bytes. Its fields are str, as a tree's spellings are.

The form to use when the names come from a symbol table: an ELF or Mach-O string table holds bytes, and a tool that has read one should not have to guess an encoding to ask what a name's parts are.

Source code in src/demangle/_signature.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def signatureb(
    mangled: bytes,
    *,
    language: str | Sequence[str] | None = None,
    style: str | Style | None = DEFAULT_STYLE,
    limits: Limits = DEFAULT_LIMITS,
) -> Signature:
    """`signature()` over bytes. Its fields are `str`, as a tree's spellings are.

    The form to use when the names come from a symbol table: an ELF or Mach-O string
    table holds bytes, and a tool that has read one should not have to guess an encoding
    to ask what a name's parts are.
    """
    return signature(_decode(mangled), language=language, style=style, limits=limits)

Errors

What the strict entry points raise when a name cannot be read. An argument that is wrong rather than unreadable is a TypeError or a ValueError, as each function says.

demangle.core.errors

The exception hierarchy.

Two audiences need different things from a failure. A tool labelling every symbol in a binary wants the name back and no exception, because most symbols are not mangled and that is not an error. A tool parsing one specific name the user typed wants to be told precisely what was wrong with it. The split between demangle() and parse() in the public API is exactly this split, and these are the types the second half raises.

DemanglingError

Bases: Exception

Base class for every failure this package reports.

NotMangledError

NotMangledError(mangled, message='not a mangled name')

Bases: DemanglingError

The name carries no recognised mangling prefix.

Distinct from ParseError: this is "not addressed to us", not "malformed". A plain C symbol like memcpy is not an error, it is simply not a mangled name.

Source code in src/demangle/core/errors.py
32
33
34
def __init__(self, mangled, message="not a mangled name"):
    self.mangled = mangled
    super().__init__(f"{message}: {mangled!r}")

ParseError

ParseError(
    mangled, position=None, message="malformed mangled name"
)

Bases: DemanglingError

The name carries a known prefix but does not follow the grammar.

Records where the parser gave up, which is the difference between a usable bug report and a shrug.

Source code in src/demangle/core/errors.py
44
45
46
47
48
def __init__(self, mangled, position=None, message="malformed mangled name"):
    self.mangled = mangled
    self.position = position
    where = f" at offset {position}" if position is not None else ""
    super().__init__(f"{message}{where}: {mangled!r}")

TruncatedError

TruncatedError(mangled, position=None)

Bases: ParseError

The name ended in the middle of a production.

Common enough in real symbol tables -- names get cut by fixed-width fields and by tools that assume a length limit -- to be worth telling apart from other malformations.

Source code in src/demangle/core/errors.py
58
59
def __init__(self, mangled, position=None):
    super().__init__(mangled, position, "mangled name ends mid-production")

LimitExceeded

LimitExceeded(mangled, limit_name, limit_value)

Bases: ParseError

A bound protecting against pathological input was hit.

A mangled name is attacker-controlled input in any tool that opens files it did not produce. Recursion depth, output size and substitution count are all capped, and hitting a cap is reported rather than being allowed to exhaust the process.

Source code in src/demangle/core/errors.py
70
71
72
73
def __init__(self, mangled, limit_name, limit_value):
    self.limit_name = limit_name
    self.limit_value = limit_value
    super().__init__(mangled, None, f"exceeded {limit_name} limit of {limit_value}")

Limits

demangle.core.limits

Bounds on what a parser will do before giving up.

Every one of these exists because a mangled name is untrusted input. Symbol tables are read from files the tool did not write, and the schemes are recursive: a few dozen bytes can describe a type nested deeply enough to exhaust the C stack, or one whose spelling is larger than memory. The defaults are set well above anything a real compiler emits.

DEFAULT_LIMITS module-attribute

DEFAULT_LIMITS = Limits()

The bounds every entry point uses unless it is passed others.

RELAXED_LIMITS module-attribute

RELAXED_LIMITS = Limits(
    max_depth=2048,
    max_output=1 << 22,
    max_substitutions=1 << 16,
    max_input=1 << 22,
)

Bounds for callers that trust their input and want the ceiling out of the way.

Still finite: "trusted" is a statement about intent, not about correctness.

max_depth here is not reachable at the interpreter's default recursion limit -- see the note on Limits.max_depth. It is left high on purpose: it says what this package will allow, and a caller who raises sys.setrecursionlimit() gets it.

Limits dataclass

Limits(
    max_depth=256,
    max_output=1 << 16,
    max_substitutions=8192,
    max_input=1 << 16,
)

Resource bounds for one parse.

Frozen so a limit set can be shared between threads and cached without defensive copying.

max_depth class-attribute instance-attribute

max_depth = 256

Nesting depth of recursive productions. Real names rarely pass 20.

At the defaults this is the bound that decides: an Itanium name nested past it stops with LimitExceeded naming max_depth, whatever the shape. The interpreter's recursion limit is a second, independent ceiling. A production costs several Python frames, so at the default limit of 1000 it binds only when this number is raised, at roughly 140 levels of nested template, 164 of function type and 494 of pointer, or when the caller's stack is already deep. It is reported the same way, as LimitExceeded("recursion depth") naming this bound, and it is then the caller's bound in fact. sys.setrecursionlimit() moves it.

max_output class-attribute instance-attribute

max_output = 1 << 16

Characters of rendered output. Template expansion can amplify enormously.

max_substitutions class-attribute instance-attribute

max_substitutions = 8192

Entries in the back-reference dictionary.

max_input class-attribute instance-attribute

max_input = 1 << 16

Characters of mangled input considered at all.

Documented elsewhere