Skip to content

Schemes

Each mangling scheme is a plugin, so any of them can be developed, replaced or shipped separately -- see Adding a scheme and Architecture.

Each section renders what the scheme's package exports, then the modules behind it: its options, its node kinds and, where it is worth reading, its parser. Each object appears once, with the module that says most about it -- an options class with its options.

Itanium C++ ABI

demangle.schemes.itanium

The Itanium C++ ABI mangling scheme.

Used by GCC, Clang, ICC and essentially every C++ toolchain outside the Microsoft ecosystem, on every platform they target. Rust's legacy scheme is a dialect of it.

Reference: https://itanium-cxx-abi.github.io/cxx-abi/abi.html section 5.1.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="itanium",
    node_kinds=(
        "array",
        "builtin",
        "decorated",
        "expression",
        "function",
        "member_pointer",
        "name",
        "pack",
        "parameter_pack",
        "pointer",
        "qualified",
        "qualify",
        "raw",
        "reference",
        "rvalue_reference",
        "special",
        "template",
        "vendor_qualify",
    ),
    detect=detect,
    parse=parse,
    parse_type=parse_type,
    description="Itanium C++ ABI (GCC, Clang, and compatible toolchains)",
    aliases=("gnu", "gcc", "clang", "cxx", "c++"),
    options_type=ItaniumOptions,
    symbol_table_decorations=True,
    first_characters="_",
    priority=200,
)

The scheme as the registry holds it, registered when this package is imported.

ItaniumParser

ItaniumParser(
    mangled,
    builder,
    limits=DEFAULT_LIMITS,
    options=DEFAULT_OPTIONS,
)

Parses one mangled name into one builder. Single use.

State -- cursor, substitution table, template scope -- is per name, so an instance is cheap and never shared. The builder, by contrast, is stateless and shared.

Source code in src/demangle/schemes/itanium/parser.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
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
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
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
def __init__(self, mangled, builder, limits=DEFAULT_LIMITS, options=DEFAULT_OPTIONS):
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)
    self._mangled = mangled
    self._ascii_input = mangled.isascii()
    self.options = options
    #: Whether `Da` and `Dc` enter the substitution table; see
    #: `ItaniumOptions.undeduced_auto_substitution`. Decided by the name's form when
    #: the option leaves it open: the Mach-O underscore means Apple's clang.
    auto_rule = options.undeduced_auto_substitution
    self._auto_substitutes = mangled.startswith("__Z") if auto_rule is None else auto_rule
    #: Whether the prefix before a closure's `M` enters the table; see
    #: `ItaniumOptions.closure_prefix_substitution`. Open means: Mach-O underscore
    #: is Apple's clang, which leaves it out; else the ABI's rule, and `parse`
    #: retries the other.
    closure_rule = options.closure_prefix_substitution
    self._closure_prefix_substitutes = not mangled.startswith("__Z") if closure_rule is None else closure_rule
    #: Whether a closure prefix was read at all, which is what makes the retry worth
    #: making: the other rule changes nothing else.
    self._closure_prefix_seen = False
    #: Table indices of the entries a `<type>` may not name: closure prefixes, and
    #: template names whose arguments must follow.
    self._closure_prefix_entries = None
    self._template_name_entries = None
    #: The lowest table index a `<template-template-param>` took, and whether an `S<n>_`
    #: resolved at or after it. `T_ I ... E` records the parameter and its specialisation
    #: (5.1.10; g++ and clang agree, `T_IT0_Li3EES5_`); llvm-cxxfilt 18 records only the
    #: second. Read by tools/enumerate.py.
    self._template_template_param_floor = 1 << 30
    self._template_template_shifted = False
    #: The index the last `S<n>_` named, or None after an abbreviation.
    self._last_entry_index = None
    #: Whether an `S<n>_` named a pack-bound `<template-param>`'s entry and got the
    #: pack (CONFORMANCE.md note 17); llvm-cxxfilt records the first member there,
    #: c++filt the last. Read by tools/enumerate.py. See `_note_pack_binding`.
    self._pack_named_through_a_back_reference = False
    #: How many `Dp` patterns are being read for their arity: a modifier over an
    #: empty pack is the pattern's business there, not a refusal's.
    self._reading_pattern = 0
    #: Whether a pack with members stood as a type outside any expansion (no
    #: compiler writes it); llvm-cxxfilt reads the first member, c++filt refuses.
    #: Read by tools/enumerate.py.
    self._bare_pack_used = False
    #: Whether the next `<type>` stands directly as a template argument, where a
    #: template's bare name is allowed (`ScalarMemoTable<int, HashTable>`). Set by
    #: `template_arg`, cleared by the first `<type>` that looks.
    self._template_name_argument = False
    expanded = STD_ABBREVIATIONS_EXPANDED_GNU if options.expand_std_abbreviations else STD_ABBREVIATIONS_EXPANDED
    self._entity_shape = None
    self._simple_name = False
    self._entity_local = False
    self._abbrev_expanded = expanded
    self._abbrev = expanded if options.expand_std_abbreviations else STD_ABBREVIATIONS
    self.reader = Reader(mangled)
    self.builder = builder
    # Called on every type read; bound once.
    self._size = builder.size
    self.limits = limits
    # Hot path: read once rather than through the frozen `limits` dataclass.
    self._max_depth = limits.max_depth
    self._max_output = limits.max_output
    self._prefixes = _PREFIX_BUDGET * limits.max_output
    self.subs = SubstitutionTable(mangled, limits.max_substitutions)
    self.targs = TemplateArgumentTable()
    # Template-parameter resolutions so far. A production that advanced this depended on
    # its scope, so its entry is kept as its input span -- see `DeferredProduction`.
    self._parameter_uses = 0
    # Re-read spans, keyed by (entry index, scope generation). Without it a chain
    # `T_`, `P S0_`, `P S1_`, ... is quadratic;
    # tests/test_substitution_parameters.py pins that.
    self._deferred = None
    self._depth = 0
    # Precedence of the expression just parsed, read by a containing operator to decide
    # on brackets.
    self._precedence = PRIMARY_PRECEDENCE
    # Pack handles seen in this name. The list holds strong references so an `id()` in
    # the set can never be reused by a new handle.
    self._packs = None
    self._pack_ids = None

    # Whether the name just parsed was a constructor or destructor. They are the
    # one case where a template specialisation still encodes no return type.
    self._ctor_dtor = False
    #: Whether an inheriting constructor's `<base class type>` was read, and whether
    #: it was entered in the table; see
    #: `ItaniumOptions.inherited_constructor_substitution`.
    self._inherited_base_seen = False
    self._inherited_base_substitutes = options.inherited_constructor_substitution is True
    # True while a special name's operand is read: a local entity there takes no function
    # type, and a template parameter is refused (both references refuse `_ZTVN1AIcT_EE`).
    # See `local_name` and `bind_template_param`.
    self._in_special_name = False
    # Whether the <unqualified-name> just read has no base name for a constructor
    # or destructor to repeat, and whether the component `_prefix_bare` holds is
    # one; see `enclosing_class_name`.
    self._component_has_no_base_name = False
    #: The last <source-name> read, template arguments aside: libiberty's
    #: `di->last_name`, which is what c++filt names a constructor or destructor
    #: after when the scope itself has no name to repeat. See `enclosing_class_name`.
    self._last_source_name = ""
    self._prefix_bare_has_no_base_name = False
    # Whether the last component of the nested name being read carries template
    # arguments, and the handles of every specialisation entered in the
    # substitution table -- see the `I` branch of `prefix_component`.
    self._prefix_has_args = False
    # Keyed by identity and holding the handle too, so that a handle the parser has
    # let go of cannot lend its address to a later one.
    self._specialised_handles = None
    # The handles the `Dp` production recorded as expansions, as distinct from the
    # packs they range over; see `_pack_aware`.
    self._expansion_handles = None
    # True while reading the declared entity's name. Only its own template arguments
    # become the `T_` scope, not those of a type in the parameter list.
    self._naming = True
    # Module names by handle identity, plus strong references. A module name is a
    # substitution candidate that decorates the following name rather than being a
    # component.
    self._modules = None
    self._module_names = None
    # Protocol-qualified `objc_object` handles, which one pointer collapses into
    # `id<...>`; tracked by identity like packs and module names.
    self._objc_ids = None
    self._objc_protocols = None
    # Types read so far, and how many this parse may re-read. Conversion-operator
    # types and pack patterns are re-read, and nested expansions multiply, so the
    # actual re-reading work is charged as it is spent, generously against
    # `max_output`.
    self._productions = 0
    self._rework = 2 * limits.max_output + 4096
    # Set only by `parse_type`: a bare `<type>` has no enclosing template, so an
    # unbound `T_` is refused rather than spelled `auto` (see `template_param`).
    self._reject_unbound_parameters = False
    # Set for the function enclosing a local name, whose return type GNU c++filt omits:
    # parsed, then discarded.
    self._drop_return = False
    # Set by a conversion operator, which encodes no return type at all. Unlike
    # `_drop_return`, there is nothing in the input to read.
    self._no_return_type = False
    # Whether the name just read declares its object parameter explicitly, `N H ...`.
    # C++23 lets a member function name the object it is called on, and the
    # reference marks that parameter `this`.
    self._explicit_object = False
    # Where an unread conversion operator's type starts, and how many substitution
    # entries there were before it: `(position, mark)`, or None. See
    # `_reread_conversion`.
    self._pending_conversion = None
    #: The requires-clause in the entity's own template arguments,
    #: `I ... Q <constraint> E`, spelled under the gnu style, which prints it after
    #: the parameters; None otherwise.
    self._argument_constraint = None
    #: Whether the conversion operator's type just read resolved a template
    #: parameter with nothing in scope -- a spelling that stands only until the
    #: operator's own arguments bind it. See `_reread_conversion`.
    self._conversion_unbound = False
    self._ambiguous_unresolved_name = False
    self._old_unresolved_names = False
    #: True while an expression is being read as a template argument under the
    #: llvm style and no bracket has opened since: where a `>` would be taken for
    #: the end of the list, and `BinaryExpr::printLeft` wraps one. See
    #: `_bracketed_expression`.
    self._bare_angle = False
    # True while that type is being read for the first time, before the arguments
    # exist. The one place a `<template-param>` may resolve to nothing with no
    # <template-args> in scope at all -- see `bind_template_param`.
    self._reading_conversion_type = False
    # True while a `<closure-type-name>`'s signature is read: a generic lambda's `auto`
    # parameters are unbound `<template-param>`s (ABI 5.1.8). See `bind_template_param`.
    self._reading_closure_signature = False
    self._closure_level = None
    #: Whether an argument pack was read from the pre-`J` form, `I <template-arg>* E`.
    #: Read by tools/enumerate.py, which knows `llvm-cxxfilt` refuses that form.
    self._legacy_pack_used = False
    #: Whether such a pack stood as a direct member of another pack, `J ... I ... E
    #: ... E`, a shape no compiler writes. Read by tools/enumerate.py as well.
    self._legacy_pack_nested = False
    #: Whether an `<expr-primary>` used the g++ bare `Z` rather than `_Z`, which
    #: llvm-cxxfilt refuses inside an expression. Read by tools/enumerate.py. See
    #: `expr_primary`.
    self._bare_entity_prefix_used = False
    #: Whether a `Dk`/`DK` placeholder was recorded as a candidate (it is a
    #: `<type>`, 5.1.10); llvm-cxxfilt 18 records nothing for it. Read by
    #: tools/enumerate.py. See `type_`.
    self._constrained_placeholder_recorded = False
    #: Whether a lambda declared a template parameter after a pack, which no
    #: compiler writes -- a pack must be last -- and where `c++filt` 2.42 stops the
    #: list rather than refusing the name. Read by tools/enumerate.py.
    self._declaration_after_a_pack = False
    # Whether the last component of the <prefix> being read was a `<substitution>`, which
    # cannot end a <nested-name> -- see `nested_name`.
    self._prefix_ended_on = ""
    # The last <prefix> component before its template arguments, or None where it was not
    # an <unqualified-name>: what a constructor repeats. See `enclosing_class_name`.
    self._prefix_bare = None
    # Whether a head `<template-param>` may take template arguments. False in a
    # conversion operator's type, where a following `I` opens the operator's own
    # list (`cvT_I4MerpE`).
    self._try_template_args = True
    # Whether the arguments in scope include a pack. If so a `Dp` expansion is spelled as
    # the members and already has its ellipsis; if not, the ellipsis is printed.
    self._scope_has_pack = False
    # Per-kind counters for the synthetic `$T` / `$N` / `$TT` names a generic
    # lambda's declared template parameters are spelled with.
    self._parameter_counts = None
    # True while reading a requires-clause, where parameters are spelled by name.
    self._in_constraint = False
    # Set when a <template-param> resolved to an empty pack: the only way to tell that a
    # `Dp` pattern expands to nothing.
    self._saw_empty_pack = False
    # Whether the type just read mentioned a pack-bound parameter, so is already expanded
    # and takes no ellipsis.
    self._saw_pack = False
    # The arity of the pack a pattern mentions, and the member being spelled:
    # `Dp unary<T_>` becomes `unary<int>, unary<float>`, not `unary<int, float>`.
    self._pack_arity = None
    self._pack_index = None
    # Whether the list just read ended in an empty pack, which is dropped: GNU
    # c++filt then writes `A<B<int>>` rather than `A<B<int> >`.
    self._trailing_empty_pack = False

parse

parse()

::= _Z [. ]

Source code in src/demangle/schemes/itanium/parser.py
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
def parse(self):
    """<mangled-name> ::= _Z <encoding> [. <vendor-specific suffix>]"""
    reader = self.reader
    mangled = reader.text
    if mangled.startswith("_Z"):
        reader.pos = 2
        import_thunk = False
        alloc_token = ""
    else:
        if mangled.startswith(("___Z", "____Z")):
            return self.block_invocation()
        # See `_IMPORT_THUNK`. `__imp_ReadFile` is not one: what follows must be a
        # mangled name itself, as it must behind `_ALLOC_TOKEN`, which may come next.
        import_thunk = mangled.startswith(_IMPORT_THUNK_OPENINGS)
        if import_thunk:
            reader.pos += len(_IMPORT_THUNK)
        alloc_token = ""
        if reader.text.startswith(_ALLOC_TOKEN, reader.pos):
            after = reader.pos + len(_ALLOC_TOKEN)
            digits = after
            while digits < reader.length and reader.text[digits] in DIGITS:
                digits += 1
            if digits > after and digits < reader.length and reader.text[digits] == "_":
                after = digits + 1
            if reader.text.startswith(("_Z", "__Z"), after):
                reader.pos = after
                alloc_token = " (.alloc_token)"
        # A Mach-O symbol carries the linker's extra underscore. Strip it only before a
        # second one, or `_Z1fv` loses the underscore the grammar needs.
        if reader.startswith("__Z"):
            reader.take()
        if not reader.eat("_Z"):
            raise NotMangledError(self._mangled, "not an Itanium mangled name")

    result = self.encoding()

    if not reader.eof:
        suffix = reader.remaining
        # A clone suffix (`.cold`, `.part.0`, `.llvm.<hash>`) is only identifiable
        # as what the grammar did not consume: `.` also occurs inside Clang's
        # coroutine frame identifiers.
        if suffix.startswith("."):
            result = self.builder.decorated(result, suffix)
        else:
            raise ParseError(self._mangled, reader.pos, f"unconsumed input {suffix!r}")

    if alloc_token:
        result = self.builder.decorated(result, alloc_token)
    if import_thunk:
        result = self.builder.special("import thunk for ", result)
    if self.builder.size(result) > self._max_output:
        raise LimitExceeded(self._mangled, "output length", self._max_output)
    return result

block_invocation

block_invocation()

___Z <encoding> _block_invoke [[_] <digits>] [. <suffix>], a Clang extension.

The function a block's body compiles to. Its name is the enclosing function's, with an underscore in front for the block and another for the leading one a Mach-O symbol table adds -- which is why ___Z is three underscores and ____Z is four.

The reference finds the encoding's end by parsing until a type will not read and then requiring the literal; this bounds the encoding at the literal instead, which is the same split and does not need the parameter loop to swallow an error. Bounded at the last occurrence, because a function may be called _block_invoke itself and the encoding is as long as it can be.

Neither the trailing number nor a . suffix is printed: the number distinguishes several blocks in one function, and what a caller is told is which function the block was written in.

Source code in src/demangle/schemes/itanium/parser.py
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
def block_invocation(self):
    """`___Z <encoding> _block_invoke [[_] <digits>] [. <suffix>]`, a Clang extension.

    The function a block's body compiles to. Its name is the enclosing function's,
    with an underscore in front for the block and another for the leading one a
    Mach-O symbol table adds -- which is why `___Z` is three underscores and
    `____Z` is four.

    The reference finds the encoding's end by parsing until a type will not read and
    then requiring the literal; this bounds the encoding at the literal instead,
    which is the same split and does not need the parameter loop to swallow an
    error. Bounded at the *last* occurrence, because a function may be called
    `_block_invoke` itself and the encoding is as long as it can be.

    Neither the trailing number nor a `.` suffix is printed: the number distinguishes
    several blocks in one function, and what a caller is told is which function the
    block was written in.
    """
    reader = self.reader
    # The prefix ends in `Z`, so the two spellings cannot be confused for one
    # another however they are tried: `____Z` does not start with `___Z`.
    if not (reader.eat("___Z") or reader.eat("____Z")):
        raise NotMangledError(self._mangled, "not an Itanium mangled name")
    found = _BLOCK_INVOKE.match(reader.remaining)
    if found is None:
        raise NotMangledError(self._mangled, "not a block invocation function")
    whole = reader.length
    reader.length = reader.pos + len(found.group(1))
    try:
        result = self.builder.special("invocation function for block in ", self.encoding())
        if not reader.eof:
            raise ParseError(self._mangled, reader.pos, f"unconsumed input {reader.remaining!r}")
    finally:
        reader.length = whole
    reader.pos = whole
    return result

encoding

encoding()

::= | |

Every path out of here records what the entity was in _entity_shape, for _entity_operand to read back. An encoding nested inside this one -- a L _Z... E in the template arguments -- writes it first and this overwrites it, which is the order that leaves the outermost answer standing.

Source code in src/demangle/schemes/itanium/parser.py
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
def encoding(self):
    """<encoding> ::= <function name> <bare-function-type> | <data name> | <special-name>

    Every path out of here records what the entity *was* in `_entity_shape`, for
    `_entity_operand` to read back. An encoding nested inside this one -- a
    `L _Z... E` in the template arguments -- writes it first and this overwrites it,
    which is the order that leaves the outermost answer standing.
    """
    reader = self.reader
    pos = reader.pos
    if pos < reader.length and reader.text[pos] in "TG":
        special = self.special_name()
        if special is not None:
            self._entity_shape = ("special", None)
            return special
    elif self._depth >= self._max_depth:
        # The guard `special_name` would have applied.
        raise LimitExceeded(self._mangled, "recursion depth", self._max_depth)

    name, quals, ref_qualifier, is_template = self.name()

    pos = reader.pos
    if pos >= reader.length or reader.text[pos] in "E.":
        # A data symbol: a name and nothing after it, and so no return type for a
        # conversion operator's name to have suppressed.
        self._drop_return = False
        self._no_return_type = False
        self._explicit_object = False
        self._entity_shape = ("data", name)
        return name
    return self.bare_function_type(name, quals, ref_qualifier, is_template)

bare_function_type

bare_function_type(
    name, quals=(), ref_qualifier="", is_template=False
)

::= +

The first type is the return type only for a template specialisation: a plain function does not encode its return type, because overloads cannot differ by it. For a template they can, so it is part of the signature (5.1.5.3).

Source code in src/demangle/schemes/itanium/parser.py
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
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
def bare_function_type(self, name, quals=(), ref_qualifier="", is_template=False):
    """<bare-function-type> ::= <signature type>+

    The first type is the return type *only* for a template specialisation: a plain
    function does not encode its return type, because overloads cannot differ by it.
    For a template they can, so it is part of the signature (5.1.5.3).
    """
    builder = self.builder
    reader = self.reader
    attributes = ""
    pos = reader.pos
    if pos < reader.length and reader.text[pos] == "U" and reader.startswith("Ua9enable_ifI"):
        # `Ua <source-name> <template-arg>* E` is a vendor attribute; `enable_if` is
        # the only one either reference spells, and it goes after the signature.
        reader.pos += len("Ua9enable_ifI")
        conditions = []
        while not reader.eat("E"):
            if reader.eof:
                raise ParseError(self._mangled, reader.pos, "unterminated enable_if attribute")
            condition, _ = self.template_arg()
            if condition is not None:
                conditions.append(builder.spell(condition))
        attributes = f" [enable_if:{', '.join(conditions)}]"
    explicit_object = self._explicit_object
    self._explicit_object = False
    was_naming = self._naming
    self._naming = False
    try:
        if self._no_return_type:
            # A conversion operator encodes no return type: `_ZN1ScviEiv` is
            # `S::operator int(int, void)`.
            self._no_return_type = False
            returns = None
        else:
            returns = self.type_() if is_template else None
            if self._drop_return:
                # In the input but not printed; see `_drop_return`.
                returns = None
                self._drop_return = False

        parameters = []
        read = 0
        # Noted before reading: a `T_` bound to `void` also spells `void`, and does
        # not mean an empty list.
        wrote_void = False
        keep_empty = self.options.gnu_empty_pack_spelling
        text = reader.text
        size = self._size
        while True:
            pos = reader.pos
            if pos >= reader.length:
                break
            char = text[pos]
            if char == "E" or char == "." or char == "Q":
                break
            if not read:
                wrote_void = char == "v"
            parameter = self.type_()
            read += 1
            if not size(parameter):
                # An expansion over an empty pack is not the explicit object
                # parameter. c++filt still prints it as an empty entry, `f(, int)`,
                # unless it ends the list.
                if keep_empty:
                    parameters.append(parameter)
                continue
            if explicit_object:
                explicit_object = False
                parameter = builder.raw("this " + builder.spell(parameter))
            parameters.append(parameter)
        if keep_empty:
            self._drop_trailing_empties(parameters)
        if not read:
            # `<signature type>+`: a specialisation spends its first type on the
            # return, so `_Z1fIiEi` has no parameters, and both references refuse
            # it. Counted, because `v` and empty expansions legitimately leave
            # `parameters` empty.
            raise ParseError(self._mangled, reader.pos, "a function signature has no parameter types")
    finally:
        self._naming = was_naming

    if read == 1 and wrote_void:
        # `f(void)` is `f()` only when the one signature type was written `v`, as
        # both references read it: `_Z1fIvEvT_` is `f<void>(void)`, and `_Z1fvv` is
        # `f(void, void)` to c++filt (llvm-cxxfilt refuses it).
        parameters = []

    suffix = ""
    if quals:
        suffix += " " + " ".join(quals)
    if ref_qualifier:
        suffix += " " + ref_qualifier
    suffix += attributes
    pos = reader.pos
    if pos < reader.length and reader.text[pos] == "Q":
        reader.pos = pos + 1
        # The requires-clause closes the declaration, after the qualifiers.
        suffix += " requires " + builder.spell(self.constraint_expression())
    if self._argument_constraint is not None:
        # c++filt then writes the argument list's clause:
        # `requires D<int> requires C<int>` for `IiQ1CIT_EE ... Q1DIT_E`. Only for
        # the function's own list; c++filt refuses the other.
        if is_template:
            suffix += " requires " + self._argument_constraint
        self._argument_constraint = None
    # GNU prints `&Name` only for a function that is a bare name and parameter list; as a
    # callee it prints the name with its suffix. See `gnu_entity_operand_spelling`.
    self._entity_shape = ("function", name, returns is None and not suffix, suffix, is_template)
    return builder.function(returns, parameters, suffix, name)

special_name

special_name()

Vtables, typeinfo, thunks, guard variables. None if this is not one.

Guarded: several of these productions contain an <encoding>, which can be another special name, so _Z followed by GA repeated is unbounded recursion.

Source code in src/demangle/schemes/itanium/parser.py
898
899
900
901
902
903
904
905
906
907
908
909
910
def special_name(self):
    """Vtables, typeinfo, thunks, guard variables. None if this is not one.

    Guarded: several of these productions contain an `<encoding>`, which can be
    another special name, so `_Z` followed by `GA` repeated is unbounded recursion.
    """
    depth = self._depth = self._depth + 1
    if depth > self._max_depth:
        raise LimitExceeded(self._mangled, "recursion depth", self._max_depth)
    try:
        return self._special_name()
    finally:
        self._depth = depth - 1

call_offset

call_offset()

::= h _ | v _

Source code in src/demangle/schemes/itanium/parser.py
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
def call_offset(self):
    """<call-offset> ::= h <nv-offset> _ | v <v-offset> _"""
    reader = self.reader
    kind = reader.take()
    if kind not in ("h", "v"):
        raise ParseError(self._mangled, reader.pos, "expected a call offset")
    reader.number()
    if kind == "v":
        reader.expect("_")
        reader.number()
    reader.expect("_")

name

name(as_type=False)

::= | | |

Returns (handle, cv-qualifiers, ref-qualifier, is-template-specialisation). The qualifiers belong to the function the name introduces rather than to the name, but the nested-name production is where they are encoded, so they travel back out with it.

Source code in src/demangle/schemes/itanium/parser.py
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
def name(self, as_type=False):
    """<name> ::= <nested-name> | <unscoped-name>
                | <unscoped-template-name> <template-args> | <local-name>

    Returns (handle, cv-qualifiers, ref-qualifier, is-template-specialisation).
    The qualifiers belong to the *function* the name introduces rather than to the
    name, but the nested-name production is where they are encoded, so they travel
    back out with it.
    """
    reader = self.reader
    pos = reader.pos
    char = reader.text[pos] if pos < reader.length else ""

    if char == "N":
        return self.nested_name(as_type)
    if char == "Z":
        return self.local_name(as_type), (), "", False

    if char == "S":
        # Either an abbreviation or a back-reference, each of which may be an
        # <unscoped-template-name> that template arguments then attach to.
        candidate = reader.ahead(1) == "t"
        if candidate:
            reader.pos += 2
            inner = self.unqualified_name()
            base = self.builder.qualified([self.builder.name("std"), inner])
        else:
            base = self.substitution()
            named = self._module_of(base)
            if named is not None:
                base = self.unqualified_name(module=named)
                candidate = True
        if reader.peek() == "I":
            # An <unscoped-template-name> is a candidate (5.1.10), recorded before
            # its arguments: in `_ZSt4sortIPiEvT_S_`, `S_` is `std::sort`. A
            # <substitution> is not recorded again (llvm-cxxfilt refuses
            # `_ZSbIwEvS_`).
            if candidate:
                self.subs.remember(base, "unscoped-template-name")
                self._template_name_entries = self._note_entry(self._template_name_entries)
            specialised = self.apply_template_args(base)
            self._record_specialised(specialised)
            return specialised, (), "", True
        if not candidate:
            # A back-reference is a <name> only as an <unscoped-template-name>, so
            # needs <template-args>. llvm-cxxfilt refuses `_ZZ1fPiES_`; c++filt's
            # `d_name` does not check.
            raise ParseError(self._mangled, reader.pos, "a substitution as a name must carry template arguments")
        return base, (), "", False

    base = self.unqualified_name()
    if reader.peek() == "I":
        self.subs.remember(base, "unscoped-template-name")
        self._template_name_entries = self._note_entry(self._template_name_entries)
        specialised = self.apply_template_args(base)
        self._record_specialised(specialised)
        return specialised, (), "", True
    return base, (), "", False

apply_template_args

apply_template_args(base)

Attach to a name.

Deliberately records nothing. A function template specialisation is not a substitution candidate: 5.1.10 excludes function names, and both reference demanglers reject _Z1fIiEvS0_, proving the table for _Z1fIiEv... holds only f. Where the specialisation is a type rather than a function, _type() records it, because there the candidate is .

Source code in src/demangle/schemes/itanium/parser.py
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
def apply_template_args(self, base):
    """Attach <template-args> to a name.

    Deliberately records nothing. A function template specialisation is not a
    substitution candidate: 5.1.10 excludes function names, and both reference
    demanglers reject `_Z1fIiEvS0_`, proving the table for `_Z1fIiEv...` holds only
    `f`. Where the specialisation is a *type* rather than a function, `_type()`
    records it, because there the candidate is <type>.
    """
    pending = self._conversion_pending()
    arguments = self.template_arguments(install_scope=True)
    angle_space = not self._trailing_empty_pack
    if pending is not None:
        base = self.builder.name(self._reread_conversion(pending))
    return self.builder.template(base, arguments, angle_space)

nested_name

nested_name(as_type=False)

A qualified name.

<nested-name> ::= N [<CV-qualifiers>] [<ref-qualifier>] <prefix> <unqualified-name> E
                | N [<CV-qualifiers>] [<ref-qualifier>] <template-prefix> <template-args> E
Source code in src/demangle/schemes/itanium/parser.py
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
def nested_name(self, as_type=False):
    """A qualified name.

    ```
    <nested-name> ::= N [<CV-qualifiers>] [<ref-qualifier>] <prefix> <unqualified-name> E
                    | N [<CV-qualifiers>] [<ref-qualifier>] <template-prefix> <template-args> E
    ```
    """
    reader = self.reader
    text = reader.text
    # Only `block_invocation` moves the end, and not while a name is being read.
    end = reader.length
    pos = reader.pos
    if pos >= end or text[pos] != "N":
        raise ParseError(text, pos, "expected 'N'")
    pos += 1
    reader.pos = pos
    # `N H`: a C++23 explicit object member function, which has no qualifiers of its own.
    quals = ()
    ref_qualifier = ""
    char = text[pos] if pos < end else ""
    if char == "H":
        # Licensed by `peek`: the character it just returned is the one consumed.
        reader.pos += 1
        # `H` marks an explicit object parameter for the entity's own name only;
        # `parseNestedName` ignores it in a type.
        if not as_type:
            self._explicit_object = True
    else:
        if char in QUALIFIER_LETTERS:
            quals = self.cv_qualifiers()
            char = reader.peek()
        if char == "R":
            reader.pos += 1
            ref_qualifier = "&"
        elif char == "O":
            reader.pos += 1
            ref_qualifier = "&&"

    parts = []
    is_template = False
    module = ""
    outer_ctor_dtor = self._ctor_dtor
    self._ctor_dtor = False
    outer_has_args = self._prefix_has_args
    self._prefix_has_args = False
    # A nested name in a template argument keeps its own prefix state, or `A<B::C>::A`
    # would come back as `A<B::C>::C`; likewise the no-base-name flag (`NSbIw...IEC2E`).
    outer_bare = self._prefix_bare
    outer_bare_has_no_base_name = self._prefix_bare_has_no_base_name
    self._prefix_bare = None
    self._prefix_bare_has_no_base_name = False
    builder = self.builder
    subs = self.subs
    try:
        max_depth = self._max_depth
        while True:
            pos = reader.pos
            if pos >= end:
                raise ParseError(self._mangled, pos, "unterminated nested name")
            char = text[pos]
            if char == "E":
                reader.pos = pos + 1
                break
            if char in DIGITS:
                # `prefix_component` for its commonest case, a bare <source-name>,
                # inlined. It neither recurses nor carries a module name on, and
                # nothing here raises the depth.
                if self._depth >= max_depth:
                    raise LimitExceeded(self._mangled, "recursion depth", max_depth)
                self._prefix_ended_on = ""
                self._component_has_no_base_name = False
                # `plain_source_name`, inlined but for the anonymous namespace.
                start = pos
                pos += 1
                if pos < end and text[pos] in DIGITS:
                    while pos < end and text[pos] in DIGITS:
                        pos += 1
                        if pos - start > MAX_NUMBER_DIGITS:
                            raise ParseError(text, start, "number too long")
                    if char == "0":
                        reader.padded_length = True
                    length = int(text[start:pos])
                else:
                    length = ord(char) - 48
                stop = pos + length if self._ascii_input else self._source_name_end(pos, length)
                if stop > end:
                    raise TruncatedError(text, pos)
                if not length or text[pos] == "_":
                    reader.pos = start
                    name = self.plain_source_name()
                else:
                    reader.pos = stop
                    name = self._last_source_name = text[pos:stop]
                if module:
                    name = f"{name}@{module}"
                    module = ""
                pos = reader.pos
                following = text[pos] if pos < end else ""
                if following == "B":
                    name += self.abi_tags()
                    pos = reader.pos
                    following = text[pos] if pos < end else ""
                component = builder.name(name)
                parts.append(component)
                self._prefix_bare = component
                self._prefix_bare_has_no_base_name = False
                self._prefix_has_args = False
                is_template = False
                if following == "E":
                    continue
                if following == "M":
                    self._closure_prefix_seen = True
                    if not self._closure_prefix_substitutes:
                        continue
                combined = parts[0] if len(parts) == 1 else builder.qualified(parts)
                self._prefixes -= self._size(combined)
                if self._prefixes < 0:
                    raise LimitExceeded(self._mangled, "output length", self._max_output)
                if subs.recording:
                    # `SubstitutionTable.remember(combined, "prefix")`, inlined.
                    entries = subs._entries
                    if len(entries) >= subs._limit:
                        raise LimitExceeded(self._mangled, "substitution", subs._limit)
                    entries.append(combined)
                if following == "M":
                    self._closure_prefix_entries = self._note_entry(self._closure_prefix_entries)
                elif following == "I":
                    self._template_name_entries = self._note_entry(self._template_name_entries)
                continue
            depth = self._depth = self._depth + 1
            if depth > max_depth:
                raise LimitExceeded(self._mangled, "recursion depth", max_depth)
            try:
                is_template, module = self.prefix_component(parts, as_type, module)
            finally:
                self._depth = depth - 1

        if not parts:
            raise ParseError(self._mangled, reader.pos, "empty nested name")
        if self._prefix_ended_on:
            # `<nested-name> ::= N ... <prefix> <unqualified-name> E`: the last
            # component cannot be a <substitution> (`_ZNSaEv`) or an `M` closing a
            # data-member or closure prefix (`_Z1fN1aME`). c++filt refuses both;
            # llvm-cxxfilt accepts only `_Z1fN1aME`. As interior components
            # (`_ZNSaC1Ev`) they are fine.
            raise ParseError(self._mangled, reader.pos, f"a nested name ending in {self._prefix_ended_on}")
        # A template constructor has no return type to encode, so the signature's
        # leading type is a parameter.
        if self._ctor_dtor:
            is_template = False
    finally:
        self._ctor_dtor = outer_ctor_dtor
        self._prefix_bare = outer_bare
        self._prefix_bare_has_no_base_name = outer_bare_has_no_base_name
        self._prefix_has_args = outer_has_args
    name = parts[0] if len(parts) == 1 else self.builder.qualified(parts)
    if is_template:
        # The enclosing <type> production records this name; a back reference to
        # it takes no arguments either.
        self._record_specialised(name)
    return name, quals, ref_qualifier, is_template

prefix_component

prefix_component(parts, as_type=False, module='')

One component of a , appended to parts.

Returns (is-template-specialisation, module). Only the first answer for the final component reaches the caller, and this is where it is known. The second is a C++20 module name this component turned out to be rather than contain, which decorates the component after it.

Every is a substitution candidate (5.1.10) except the last, which is an -- and function and operator names are explicitly excluded. We cannot tell we are on the last component until the closing E is in sight, so the check is "is the next character E".

Source code in src/demangle/schemes/itanium/parser.py
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
def prefix_component(self, parts, as_type=False, module=""):
    """One component of a <prefix>, appended to `parts`.

    Returns `(is-template-specialisation, module)`. Only the first answer for the
    final component reaches the caller, and this is where it is known. The second is
    a C++20 module name this component turned out to be rather than contain, which
    decorates the component *after* it.

    Every <prefix> is a substitution candidate (5.1.10) *except* the last, which is
    an <unqualified-name> -- and function and operator names are explicitly excluded.
    We cannot tell we are on the last component until the closing `E` is in sight,
    so the check is "is the next character `E`".
    """
    reader = self.reader
    builder = self.builder
    self._prefix_ended_on = ""
    text = reader.text
    pos = reader.pos
    char = text[pos] if pos < reader.length else ""

    # `nested_name` reads a <source-name> itself; one set test skips the markers below.
    if char in _PREFIX_MARKERS:
        if char == "S":
            expanded = text[pos : min(pos + 2, reader.length)] in STD_ABBREVIATIONS and (
                self._abbreviation_scopes_a_structor()
            )
            component = self.substitution(expanded=expanded)
            named = self._module_names.get(id(component)) if self._module_names else None
            if named is not None:
                # A module name, not a scope: it belongs to the component that
                # follows, and it decorates that component wherever it stands. The
                # position rule below is about a substitution used as a *scope*.
                return False, named
            if parts:
                self._only_a_base_production(parts, "a substitution")
            self._prefix_ended_on = "a substitution"
            self._prefix_bare = None
            self._prefix_has_args = bool(self._specialised_handles and id(component) in self._specialised_handles)
            parts.append(component)
            return False, module

        if char == "I":
            # <template-prefix> <template-args>: the arguments attach to the component
            # just read, and the pair becomes one substitutable component.
            if not parts:
                raise ParseError(self._mangled, reader.pos, "template arguments with no name")
            if self._prefix_has_args:
                # A specialisation takes no further arguments: llvm-cxxfilt refuses
                # `_Z1fN1AIiEIcEE`.
                raise ParseError(self._mangled, reader.pos, "template arguments on a specialisation")
            pending = self._conversion_pending()
            # Captured before the arguments are read: an argument may be a nested
            # name of its own, and reading it moves `_prefix_bare` on.
            bare = parts[-1]
            bare_is_conversion = self._prefix_bare_has_no_base_name
            arguments = self.template_arguments(install_scope=True)
            angle_space = not self._trailing_empty_pack
            if pending is not None:
                bare = parts[-1] = builder.name(self._reread_conversion(pending))
            self._prefix_bare = bare
            self._prefix_bare_has_no_base_name = bare_is_conversion
            parts[-1] = builder.template(parts[-1], arguments, angle_space)
            combined = parts[0] if len(parts) == 1 else builder.qualified(parts)
            self._prefix_has_args = True
            self._record_specialised(parts[-1])
            self._record_specialised(combined)
            # Only an interior <template-prefix> <template-args> is a separate
            # candidate. Before the closing `E` the enclosing <type> records it, and
            # recording twice shifts every index.
            if reader.peek() != "E":
                self.subs.remember(self._spend_prefix(combined), "prefix")
            return True, module

        if char == "T":
            self._only_a_base_production(parts, "a template parameter")
            self._prefix_bare = None
            component, reference = self.template_param_binding()
            parts.append(component)
            self.subs.remember(reference if reference is not None else component, "template-template-param")
            self._note_template_template_param()
            return False, module

        if char == "D" and reader.ahead(1) in ("t", "T"):
            self._only_a_base_production(parts, "a decltype")
            self._prefix_bare = None
            parts.append(self.decltype_())
            return False, module

        if char == "M":
            # <closure-prefix> / <data-member-prefix> terminator; carries no spelling
            # of its own. It ends a <prefix>, so an <unqualified-name> still has to
            # follow before the `E` -- see `nested_name`.
            if self._prefix_bare is None:
                # The prefix before `M` must be a spelled name: llvm-cxxfilt refuses
                # `_ZNStM1xE`, which c++filt reads as `std::x`.
                raise ParseError(self._mangled, reader.pos, "a data member or closure prefix over no spelled name")
            reader.take()
            self._prefix_ended_on = "a data member or closure prefix"
            self._prefix_bare = None
            return False, module

        # A <nested-name> has no requires-clause: `Q` falls through to
        # `unqualified_name`, which refuses it, as both references refuse
        # `_ZN4llvm12_GLOBAL__N_1L1UQ13_SuperRegsSetE`.

    component = self.unqualified_name(scope=parts, module=module)
    parts.append(component)
    self._prefix_bare = component
    self._prefix_bare_has_no_base_name = self._component_has_no_base_name
    self._prefix_has_args = False
    pos = reader.pos
    following = text[pos] if pos < reader.length else ""
    if following != "E":
        if following == "M":
            # The prefix of a closure or data member: a candidate under the ABI,
            # clang and GCC 13, not under GCC 12 and earlier. See
            # `ItaniumOptions.closure_prefix_substitution`.
            self._closure_prefix_seen = True
            if not self._closure_prefix_substitutes:
                return False, ""
        combined = parts[0] if len(parts) == 1 else builder.qualified(parts)
        # `_spend_prefix`, inlined.
        self._prefixes -= self._size(combined)
        if self._prefixes < 0:
            raise LimitExceeded(self._mangled, "output length", self._max_output)
        self.subs.remember(combined, "prefix")
        if following == "M":
            self._closure_prefix_entries = self._note_entry(self._closure_prefix_entries)
        elif following == "I":
            self._template_name_entries = self._note_entry(self._template_name_entries)
    return False, ""

local_name

local_name(as_type=False)

::= Z E [] | Z E s [] | Z Ed [] _

Source code in src/demangle/schemes/itanium/parser.py
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
def local_name(self, as_type=False):
    """<local-name> ::= Z <function encoding> E <entity name> [<discriminator>]
    | Z <function encoding> E s [<discriminator>]
    | Z <function encoding> Ed [<parameter number>] _ <entity name>
    """
    reader = self.reader
    builder = self.builder
    reader.expect("Z")
    # `Z <encoding> E` has template parameters of its own: without a fresh naming
    # context its `T_` would resolve against an enclosing template's (a lambda
    # passed between templates).
    outer_naming = self._naming
    outer_scope = self.targs.snapshot()
    self._naming = True
    outer_drop_return = self._drop_return
    outer_no_return_type = self._no_return_type
    outer_explicit_object = self._explicit_object
    self._drop_return = not self.options.local_name_return_type
    self._no_return_type = False
    self._explicit_object = False
    # `_in_special_name` is about the entity after the `E`, not the enclosing
    # function; left set, `_ZGVZZN1A1fEvENKUlvE_clEvE1y` would lose its `const`.
    outer_special = self._in_special_name
    self._in_special_name = False
    try:
        outer = self.encoding()
    finally:
        self._in_special_name = outer_special
        self._drop_return = outer_drop_return
        self._no_return_type = outer_no_return_type
        self._explicit_object = outer_explicit_object

    if reader.eat("Ed"):
        # `d [<number>] _` says the entity lives in a default argument rather than
        # in the function body. The number is a compact one -- absent is zero -- and
        # names which default argument, so GNU's `{default arg#1}` is it plus one.
        argument = 0
        if reader.peek() != "_":
            argument = int(reader.number(allow_negative=False)) + 1
        reader.expect("_")
        try:
            entity_is_type = as_type or reader.peek() == "U"
            inner, quals, ref_qualifier, is_template = self.name()
            parts = [outer, inner]
            if self.options.gnu_default_argument_scope:
                parts.insert(1, builder.raw(f"{{default arg#{argument + 1}}}"))
            combined = builder.qualified(parts)
            if (
                not entity_is_type
                and not self._in_special_name
                and not reader.eof
                and reader.peek() not in ("E", "_", ".")
            ):
                combined = self.bare_function_type(combined, quals, ref_qualifier, is_template)
        finally:
            self._naming = outer_naming
            self.targs.restore(outer_scope)
        return combined

    reader.expect("E")

    if reader.eat("s"):
        self.discriminator()
        self._naming = outer_naming
        self.targs.restore(outer_scope)
        return builder.qualified([outer, builder.raw("string literal")])

    try:
        # A closure or unnamed type is a type, so no signature follows it; otherwise
        # `Z1gvEUlvE_S_` reads the next parameter as the lambda's signature.
        entity_is_type = as_type or reader.peek() == "U"
        inner, quals, ref_qualifier, is_template = self.name()
        # The signature is applied to the *combined* name, not to the entity alone:
        # a return type belongs at the front of the whole declaration, so a generic
        # lambda's `operator()` reads `auto f()::'lambda'<...>::operator()(...)` and
        # not `f()::auto 'lambda'...`.
        combined = builder.qualified([outer, inner])
        # A `.` is a clone suffix (`_ZZ1fvE1x.0`), which belongs to `parse`, not to a
        # signature.
        if (
            not entity_is_type
            and not self._in_special_name
            and not reader.eof
            and reader.peek() not in ("E", "_", ".")
            and not self._at_bare_discriminator()
        ):
            combined = self.bare_function_type(combined, quals, ref_qualifier, is_template)
        self.discriminator()
    finally:
        self._naming = outer_naming
        self.targs.restore(outer_scope)
    return combined

discriminator

discriminator()

Tells apart same-named entities in one function.

<discriminator> ::= _ <non-negative number> | __ <number> _

Tells apart same-named entities in one function. It carries no spelling, but it has to be consumed or it looks like trailing junk.

Source code in src/demangle/schemes/itanium/parser.py
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
def discriminator(self):
    """Tells apart same-named entities in one function.

    ```
    <discriminator> ::= _ <non-negative number> | __ <number> _
    ```

    Tells apart same-named entities in one function. It carries no spelling, but it
    has to be consumed or it looks like trailing junk.
    """
    reader = self.reader
    if reader.peek() != "_":
        if self._at_bare_discriminator():
            reader.pos = reader.length
        return
    if reader.ahead(1) == "_":
        saved = reader.pos
        reader.pos += 2
        try:
            reader.number(allow_negative=False)
            reader.expect("_")
        except ParseError:
            reader.pos = saved
        return
    if reader.ahead(1) in DIGITS:
        reader.take()
        reader.number(allow_negative=False)

unqualified_name

unqualified_name(scope=None, module='', internal=False)

::= [] []

<name body> ::= <operator-name> | <ctor-dtor-name> | <source-name>
              | <unnamed-type-name> | DC <source-name>+ E

module is a C++20 module name the caller already read -- a can reach one through a substitution -- and is spelled after the name: Foo@MOD.

Source code in src/demangle/schemes/itanium/parser.py
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
def unqualified_name(self, scope=None, module="", internal=False):
    """<unqualified-name> ::= [<module-name>] <name body> [<abi-tags>]

    ```
    <name body> ::= <operator-name> | <ctor-dtor-name> | <source-name>
                  | <unnamed-type-name> | DC <source-name>+ E
    ```

    `module` is a C++20 module name the caller already read -- a <prefix> can reach
    one through a substitution -- and is spelled after the name: `Foo@MOD`.
    """
    reader = self.reader
    builder = self.builder
    self._component_has_no_base_name = False
    char = reader.peek()
    if char == "W":
        module = self.module_name(module)
        char = reader.peek()
        if (char == "C" and reader.ahead(1) in CONSTRUCTOR_KINDS) or (
            char == "D" and reader.ahead(1) in DESTRUCTOR_KINDS
        ):
            # `<unqualified-name> ::= [<module-name>] <operator-name> | <ctor-dtor-name>
            # | ...`: a structor's module is on the class name (`_ZNW4llvm6ModuleC1Ev`),
            # so llvm-cxxfilt refuses `_ZNStW9rGPRClassC2Ev`.
            raise ParseError(self._mangled, reader.pos, "a module name on a constructor or destructor")
    # `F` marks a friend declared inside the class it is a friend of. The scope is
    # already in `parts`, which `qualified` joins with `::`, so the marker decorates
    # the name that follows the last `::` -- see `_befriended` for where it goes.
    friend = False
    if char == "F" and scope and not internal:
        # `F` before `L`, as `parseUnqualifiedName` reads them: llvm-cxxfilt refuses
        # `_ZN1ALF3fooEv`. Licensed by `peek`.
        reader.pos += 1
        friend = True
        char = reader.peek()

    if char in DIGITS:
        name = self.plain_source_name()
        # `_in_module` is a call for a question that is almost always no, on the
        # production every component of every qualified name goes through.
        if module:
            name = f"{name}@{module}"
        tags = self.abi_tags() if reader.peek() == "B" else ""
        return builder.name(self._befriended(name + tags) if friend else name + tags)

    if char == "L":
        # At most one `L`:
        # `<unqualified-name> ::= [<module-name>] [L] <name body> [<abi-tags>]`, and
        # both references refuse `_Z1fLL1A`.
        if internal:
            raise ParseError(self._mangled, reader.pos, "a second internal-linkage marker")
        reader.take()
        depth = self._depth = self._depth + 1
        if depth > self._max_depth:
            raise LimitExceeded(self._mangled, "recursion depth", self._max_depth)
        try:
            inner = self.unqualified_name(scope, module, internal=True)
        finally:
            self._depth = depth - 1
        return builder.name(self._befriended(builder.spell(inner))) if friend else inner

    if char == "C":
        # A constructor can be a friend too: `_ZN1AFC1Ev`.
        self._component_has_no_base_name = True
        name = self.constructor_name(scope)
        return builder.name(self._befriended(builder.spell(name))) if friend else name

    if char == "D":
        following = reader.ahead(1)
        if following in DESTRUCTOR_KINDS:
            reader.pos += 2
            self._ctor_dtor = True
            # The bare class name, with no module attached; see `constructor_name`.
            spelled = "~" + self.enclosing_class_name(scope)
            # `<ctor-dtor-name> [<abi-tags>]`: libc++ 18 tags destructors,
            # `D2B8ne180100`.
            if reader.peek() == "B":
                spelled += self.abi_tags()
            self._component_has_no_base_name = True
            return builder.name(self._befriended(spelled) if friend else spelled)
        if following == "C":
            # A structured binding declaration: DC <source-name>+ E
            reader.pos += 2
            names = []
            while not reader.eat("E"):
                if reader.eof:
                    raise ParseError(self._mangled, reader.pos, "unterminated structured binding")
                names.append(self.source_name())
            if not names:
                # `<source-name>+`: both references refuse `DCE`.
                raise ParseError(self._mangled, reader.pos, "a structured binding with no names")
            self._component_has_no_base_name = True
            spelled = self._in_module("[" + ", ".join(names) + "]", module)
            return builder.name(self._befriended(spelled) if friend else spelled)

    if char == "U":
        name = self.unnamed_type_name()
        # Set after the closure's signature, whose parameter types may clear it
        # (`_ZN1AUlN1XEE_D1Ev`).
        self._component_has_no_base_name = True
        return builder.name(self._befriended(builder.spell(name))) if friend else name

    # A vendor extended operator, `v <digit> <source-name>`, is a
    # `ConversionOperatorType` to `llvm-cxxfilt`, and a literal operator has no base
    # name either; see `enclosing_class_name`.
    code = reader.peek2()
    has_no_base_name = code in ("cv", "li") or (code[:1] == "v" and code[1:2].isdigit())
    spelled = self._in_module(self.operator_name(), module) + self.abi_tags()
    # Assigned after the operator is read: its type may be a class name, whose
    # reading clears the flag (`_ZN1Scv1AC2Ev` is `S::operator A::()` to
    # llvm-cxxfilt).
    self._component_has_no_base_name = has_no_base_name
    operator = builder.name(self._befriended(spelled) if friend else spelled)
    if reader.peek() != "I":
        # Nothing will bind a conversion operator's template parameters here;
        # cleared because the next argument list in the name is somebody else's.
        self._pending_conversion = None
        if self._conversion_unbound:
            # Both references refuse `_Zcv1BIRT_E`, whose parameter nothing binds. See
            # `bind_template_param`.
            raise ParseError(
                self._mangled, reader.pos, "a conversion operator's template parameter with no arguments"
            )
    return operator

constructor_name

constructor_name(scope)

A constructor name.

<ctor-dtor-name> ::= C1 | C2 | C3 | CI1 <base class type> | CI2 <base class type>

The class's bare name, with no module attached: _ZNW4llvm6ModuleC1Ev is Module@llvm::Module() to both references -- CtorDtorName prints the scope's getBaseName(), and a ModuleEntity's base name is the name inside it -- not Module@llvm::Module@llvm(). The destructor the same.

Source code in src/demangle/schemes/itanium/parser.py
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
def constructor_name(self, scope):
    """A constructor name.

    ```
    <ctor-dtor-name> ::= C1 | C2 | C3 | CI1 <base class type> | CI2 <base class type>
    ```

    The class's bare name, with no module attached: `_ZNW4llvm6ModuleC1Ev` is
    `Module@llvm::Module()` to both references -- `CtorDtorName` prints the scope's
    `getBaseName()`, and a `ModuleEntity`'s base name is the name inside it -- not
    `Module@llvm::Module@llvm()`. The destructor the same.
    """
    reader = self.reader
    reader.expect("C")
    self._ctor_dtor = True
    if reader.eat("I"):
        # `CI1` to `CI5` only, the variants an ordinary constructor has:
        # `_ZN1BCIT1AEi` is refused.
        marker = reader.take()
        if marker not in CONSTRUCTOR_KINDS:
            raise ParseError(self._mangled, reader.pos, f"unknown constructor variant {marker!r}")
        # Read off the scope before the base type, a <type> whose own nested name would
        # otherwise stand where the class should.
        spelled = self.enclosing_class_name(scope)
        # The base type is a 5.1.10 candidate that g++ 13.3 enters
        # (`_ZN1DCI21CENS0_4KindES1_`) and clang++ 18.1.3 does not. Clang's rule
        # first, g++'s on retry; see
        # `ItaniumOptions.inherited_constructor_substitution`.
        self._inherited_base_seen = True
        mark = self.subs.mark()
        self.type_()
        if not self._inherited_base_substitutes:
            self.subs.drop_last(mark)
        if reader.peek() == "B":
            spelled += self.abi_tags()
        return self.builder.name(spelled)
    marker = reader.take()
    if marker not in CONSTRUCTOR_KINDS:
        raise ParseError(self._mangled, reader.pos, f"unknown constructor variant {marker!r}")
    return self.builder.name(self.enclosing_class_name(scope) + self.abi_tags())

enclosing_class_name

enclosing_class_name(scope)

The bare class name that a constructor or destructor repeats.

Foo::Foo is encoded as "the class, then a constructor marker" -- the marker has no spelling of its own, so the name is read back off the scope we are standing in. A specialisation's constructor drops the arguments: the constructor of Foo<int> is spelled Foo<int>::Foo, not Foo<int>::Foo<int>.

Source code in src/demangle/schemes/itanium/parser.py
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
def enclosing_class_name(self, scope):
    """The bare class name that a constructor or destructor repeats.

    `Foo::Foo` is encoded as "the class, then a constructor marker" -- the marker
    has no spelling of its own, so the name is read back off the scope we are
    standing in. A specialisation's constructor drops the arguments: the constructor
    of `Foo<int>` is spelled `Foo<int>::Foo`, not `Foo<int>::Foo<int>`.
    """
    if not scope:
        raise ParseError(self._mangled, self.reader.pos, "constructor outside any class scope")
    bare = self._prefix_bare
    if bare is not None and self._prefix_bare_has_no_base_name:
        # No base name (conversion, vendor or literal operator, structor, closure,
        # unnamed type, structured binding): llvm-cxxfilt's `CtorDtorName` prints
        # nothing, `A::operator int::~()` for `_ZN1AcviD0Ev`; libiberty uses the
        # last <source-name> read.
        return self._last_source_name if self.options.gnu_closure_spelling else ""
    if bare is not None:
        # The component before its template arguments; cutting the spelling at `<` breaks
        # operator-named classes (`_ZNssC1Ev`).
        spelled = _ABI_TAGS_AT_END.sub("", self.builder.spell(bare))
    else:
        # A scope not from an <unqualified-name> (substitution, `<template-param>`,
        # `<decltype>`): the arguments and tags come off the spelling, which has no
        # `<` or `[` of its own.
        spelled = self.builder.spell(scope[-1])
        cut = min((index for index in (spelled.find("<"), spelled.find("[")) if index > 0), default=-1)
        if cut > 0:
            spelled = spelled[:cut]
    # An abbreviation is one part (`Sa` is `std::allocator`), so only its tail is
    # the class name.
    separator = spelled.rfind("::")
    spelled = spelled[separator + 2 :] if separator >= 0 else spelled
    # The module comes off too: `_ZNW4llvm6ModuleC1Ev` is `Module@llvm::Module()` to both
    # references.
    module = spelled.find("@")
    spelled = spelled[:module] if module > 0 else spelled
    # Both references drop the friend marker from the repeated name: `_ZN1AF3fooC1Ev` is
    # `A::friend foo::foo()`.
    if self.options.gnu_friend_spelling:
        if spelled.endswith("[friend]"):
            spelled = spelled[: -len("[friend]")]
    elif spelled.startswith("friend "):
        spelled = spelled[len("friend ") :]
    return spelled

source_name

source_name()

::=

Source code in src/demangle/schemes/itanium/parser.py
1942
1943
1944
1945
def source_name(self):
    """<source-name> ::= <positive length number> <identifier>"""
    text = self.plain_source_name()
    return text + self.abi_tags() if self.reader.peek() == "B" else text

plain_source_name

plain_source_name()

A without the ABI tags that may follow it.

Split out because a module name goes between the two: 3FooB3ABI inside module MOD is Foo@MOD[abi:ABI], not Foo[abi:ABI]@MOD.

Source code in src/demangle/schemes/itanium/parser.py
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
def plain_source_name(self):
    """A <source-name> without the ABI tags that may follow it.

    Split out because a module name goes *between* the two: `3FooB3ABI` inside
    module `MOD` is `Foo@MOD[abi:ABI]`, not `Foo[abi:ABI]@MOD`.
    """
    # `Reader.length_prefixed` inlined, errors and all: this runs for nearly every
    # component of every name.
    reader = self.reader
    mangled, end = reader.text, reader.length
    start = pos = reader.pos
    while pos < end and mangled[pos] in DIGITS:
        pos += 1
        if pos - start > MAX_NUMBER_DIGITS:
            raise ParseError(mangled, start, "number too long")
    if pos == start:
        raise ParseError(mangled, start, "expected a number")
    if mangled[start] == "0":
        reader.padded_length = True
    length = int(mangled[start:pos])
    stop = pos + length if self._ascii_input else self._source_name_end(pos, length)
    if stop > end:
        raise TruncatedError(mangled, pos)
    reader.pos = stop
    text = mangled[pos:stop]
    self._last_source_name = text
    if length <= 0:
        raise ParseError(self._mangled, stop, "source name of non-positive length")
    if text[0] == "_" and text.startswith("_GLOBAL_"):
        if text.startswith("_GLOBAL__N"):
            return "(anonymous namespace)"
        if self.options.gnu_expression_spelling and len(text) >= 10 and text[8] in "._$" and text[9] == "N":
            # `d_source_name` takes any of the three markers assemblers have used
            # between `_GLOBAL_` and the `N` -- `_`, `.` and `$` -- where llvm-cxxfilt
            # knows the underscore alone and prints the others as written.
            return "(anonymous namespace)"
    return text

module_name

module_name(module='')

A C++20 module name, as the text that goes after the @.

<module-name>    ::= <module-subname>+ | <substitution>
<module-subname> ::= W <source-name> | W P <source-name>

Submodules join with . and a partition with :, so W3FooWP3BarW3Baz is Foo:Bar.Baz. Every prefix of the run is a substitution candidate in its own right, which is how S1_ comes to stand for FOO.BAR in a later parameter.

Newer than the grammar snapshot in docs/specs/.

Source code in src/demangle/schemes/itanium/parser.py
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
def module_name(self, module=""):
    """A C++20 module name, as the text that goes after the `@`.

    ```
    <module-name>    ::= <module-subname>+ | <substitution>
    <module-subname> ::= W <source-name> | W P <source-name>
    ```

    Submodules join with `.` and a partition with `:`, so `W3FooWP3BarW3Baz` is
    `Foo:Bar.Baz`. Every prefix of the run is a substitution candidate in its own
    right, which is how `S1_` comes to stand for `FOO.BAR` in a later parameter.

    Newer than the grammar snapshot in docs/specs/.
    """
    reader = self.reader
    while reader.peek() == "W":
        reader.pos += 1
        partition = reader.eat("P")
        subname = self.plain_source_name()
        if module or partition:
            module += ":" if partition else "."
        module += subname
        handle = self.builder.raw(module)
        # Strong reference plus identity, as for packs.
        modules = self._modules
        module_names = self._module_names
        if modules is None or module_names is None:
            modules = self._modules = []
            module_names = self._module_names = {}
        modules.append(handle)
        module_names[id(handle)] = module
        self.subs.remember(handle, "module-name")
    return module

abi_tags

abi_tags()

ABI tags, spelled [abi:tag].

<abi-tags> ::= <abi-tag>*     <abi-tag> ::= B <source-name>

Callers on a hot path ask reader.peek() == "B" before calling: the answer is no for all but a handful of names in a symbol table, and asking it here costs a whole interpreter frame to return the empty string -- five times per name over the Itanium corpus, which is 9% of every peek the parser makes. The test is repeated below so that a caller who does not ask is still right.

Source code in src/demangle/schemes/itanium/parser.py
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
def abi_tags(self):
    """ABI tags, spelled `[abi:tag]`.

    ```
    <abi-tags> ::= <abi-tag>*     <abi-tag> ::= B <source-name>
    ```

    Callers on a hot path ask `reader.peek() == "B"` before calling: the answer is no
    for all but a handful of names in a symbol table, and asking it here costs a whole
    interpreter frame to return the empty string -- five times per name over the
    Itanium corpus, which is 9% of every `peek` the parser makes. The test is
    repeated below so that a caller who does not ask is still right.
    """
    reader = self.reader
    if reader.peek() != "B":
        return ""
    tags = []
    while reader.peek() == "B":
        reader.pos += 1
        tags.append(f"[abi:{self._identifier()}]")
    return "".join(tags)

unnamed_type_name

unnamed_type_name(lambda_expression=False)

A closure or unnamed type.

lambda_expression spells the closure as the expression that made it rather than as its type -- [](){...}, which is what a lambda written as a template argument reads as.

<unnamed-type-name> ::= Ut [<number>] _ | <closure-type-name>
<closure-type-name> ::= Ul <lambda-sig> E [<number>] _
Source code in src/demangle/schemes/itanium/parser.py
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
def unnamed_type_name(self, lambda_expression=False):
    """A closure or unnamed type.

    `lambda_expression` spells the closure as the *expression* that made it rather
    than as its type -- `[](){...}`, which is what a lambda written as a template
    argument reads as.

    ```
    <unnamed-type-name> ::= Ut [<number>] _ | <closure-type-name>
    <closure-type-name> ::= Ul <lambda-sig> E [<number>] _
    ```
    """
    reader = self.reader
    reader.expect("U")

    if reader.eat("t"):
        index = reader.digits() if reader.peek() in DIGITS else ""
        reader.expect("_")
        if self.options.gnu_closure_spelling:
            return self.builder.raw(f"{{unnamed type#{int(index) + 2 if index else 1}}}")
        return self.builder.raw(f"'unnamed{index}'")

    if reader.eat("l"):
        # <lambda-sig> ::= <template-param-decl>* [Q <constraint>] <parameter type>+
        #
        # A generic lambda's declared parameters replace the enclosing `T_` scope:
        # by ABI 5.1.8 an undeclared `T_` in its signature is an `auto`, not the
        # enclosing template's argument.
        declarations = []
        saved_counts = self._parameter_counts
        saved_scope = self.targs.snapshot()
        self._parameter_counts = {}
        if self._naming:
            # A lambda that is the named entity starts from nothing. One inside a
            # type or expression keeps the levels around it with its own on top, so
            # `TL0__` reaches the lambda while `T_` reaches the enclosing function's
            # arguments.
            self.targs.clear()
        declared = []
        # The level the lambda's own parameters occupy, and the only one a reference
        # that binds nothing may be an `auto` at. See `bind_template_param`.
        was_closure_level = self._closure_level
        self._closure_level = self.targs.depth()
        self.targs.push(declared)
        constraint = ""
        saw_pack = False
        was_reading_closure = self._reading_closure_signature
        self._reading_closure_signature = True
        try:
            while reader.peek2() in _PARAMETER_DECLARATIONS:
                # A pack must be last; `c++filt` 2.42 stops the list at `Tp Ty Ty`
                # rather than refuse. Kept here, so a `TL0_<n>_` can still name it.
                # See `_declaration_after_a_pack`.
                if reader.peek2() == "Tp":
                    saw_pack = True
                elif saw_pack:
                    self._declaration_after_a_pack = True
                _, declaration = self.template_param_decl(params=declared)
                declarations.append(declaration)
            if reader.eat("Q"):
                constraint = f" requires {self.builder.spell(self.constraint_expression())} "
            parameters = []
            while not reader.eat("E"):
                if reader.eof:
                    raise ParseError(self._mangled, reader.pos, "unterminated lambda signature")
                if reader.eat("Q"):
                    # A trailing requires-clause, after the parameters.
                    if not parameters:
                        raise ParseError(self._mangled, reader.pos, "a lambda signature with no parameter types")
                    trailing = self.builder.spell(self.constraint_expression())
                    reader.expect("E")
                    return self._closure(
                        declarations, constraint, parameters, f" requires {trailing}", lambda_expression
                    )
                parameters.append(self.builder.spell(self.type_()))
            if not parameters:
                # `<parameter type>+`: a lambda taking nothing is written `v`; both
                # references refuse `UlE_`.
                raise ParseError(self._mangled, reader.pos, "a lambda signature with no parameter types")
        finally:
            self._reading_closure_signature = was_reading_closure
            self._closure_level = was_closure_level
            self._parameter_counts = saved_counts
            self.targs.restore(saved_scope)
        return self._closure(declarations, constraint, parameters, "", lambda_expression)

    if reader.eat("b"):
        # <unnamed-type-name> ::= Ub [<number>] _ -- an Objective-C block literal.
        if reader.peek() in DIGITS:
            reader.digits()
        reader.expect("_")
        return self.builder.raw("'block-literal'")

    raise ParseError(self._mangled, reader.pos, "unknown unnamed-type-name")

operator_name

operator_name()

, including conversions and literal operators.

Source code in src/demangle/schemes/itanium/parser.py
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
def operator_name(self):
    """<operator-name>, including conversions and literal operators."""
    reader = self.reader
    code = reader.peek2()

    if code == "cv":
        # The type may use the operator's own template parameters, written after it;
        # position and table mark are kept for `_reread_conversion`. No return type
        # is encoded.
        reader.pos += 2
        start = reader.pos
        mark = self.subs.mark()
        was_trying = self._try_template_args
        was_reading = self._reading_conversion_type
        self._try_template_args = False
        # The one reading that may resolve a template parameter with nothing in
        # scope; `_reread_conversion` redoes it once the arguments are. See
        # `bind_template_param`.
        self._reading_conversion_type = True
        self._conversion_unbound = False
        try:
            spelled = "operator " + self.builder.spell(self.type_())
        finally:
            self._reading_conversion_type = was_reading
            self._try_template_args = was_trying
        self._pending_conversion = (start, mark)
        # Only the entity's own name suppresses a return type, not a conversion
        # operator inside an expression (`&A::operator int` as a template argument).
        if self._naming:
            self._no_return_type = True
        return spelled

    if code == "li":
        reader.pos += 2
        return 'operator"" ' + self.source_name()

    if len(code) == 2 and code[0] == "v" and code[1] in DIGITS:
        # A vendor extended operator: v <digit> <source-name>
        reader.pos += 2
        return "operator " + self.source_name()

    if code in OPERATORS:
        reader.pos += 2
        spelling, needs_space = OPERATORS[code]
        return "operator" + (" " if needs_space else "") + spelling

    raise ParseError(self._mangled, reader.pos, f"unknown operator code {code!r}")

substitution

substitution(expanded=False)

A back-reference or a predefined abbreviation.

<substitution> ::= S <seq-id> _ | S_ | St | Sa | Sb | Ss | Si | So | Sd

expanded forces the full spelling of an abbreviation regardless of style, which is what a constructor's scope needs: a constructor is named for its class, and the class is the template rather than the typedef.

Source code in src/demangle/schemes/itanium/parser.py
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
def substitution(self, expanded=False):
    """A back-reference or a predefined abbreviation.

    ```
    <substitution> ::= S <seq-id> _ | S_ | St | Sa | Sb | Ss | Si | So | Sd
    ```

    `expanded` forces the full spelling of an abbreviation regardless of style,
    which is what a constructor's scope needs: a constructor is named for its class,
    and the class is the template rather than the typedef.
    """
    reader = self.reader
    text, end = reader.text, reader.length
    pos = reader.pos
    if pos >= end or text[pos] != "S":
        raise ParseError(text, pos, "expected 'S'")
    pos += 1
    reader.pos = pos
    code = "S" + text[pos] if pos < end else "S"
    table = self._abbrev_expanded if expanded else self._abbrev
    self._last_entry_index = None
    if code in table:
        reader.pos = pos + 1
        tags = self.abi_tags() if pos + 1 < end and text[pos + 1] == "B" else ""
        if tags:
            # 5.1.2: an abbreviation carrying ABI tags is substitutable as the
            # tagged whole; the bare abbreviation is not.
            return self.subs.remember(self.builder.raw(table[code] + tags), "type")
        return self.builder.raw(table[code])
    index = reader.seq_id()
    if index >= self._template_template_param_floor:
        self._template_template_shifted = True
    entries = self.subs._entries
    entry = entries[index] if index < len(entries) else self.subs.lookup(index)
    kind = type(entry)
    try:
        if kind is ParameterReference:
            # The entry is the parameter, not its binding where recorded: the
            # back-reference may be read under a different scope.
            # `bind_template_param` does the pack handling, so `_pack_aware` is not
            # applied twice. See `ParameterReference`.
            try:
                return self._note_pack_binding(self.bind_template_param(entry.index, entry.level))
            except ParseError:
                if entry.symbolic is None:
                    raise
                return self.builder.raw(entry.symbolic)
        if kind is DeferredProduction:
            # The same, for a component built *over* a parameter. See
            # `DeferredProduction`.
            return self._pack_aware(self._reread(index, entry))
        if self.builder.members(entry) is None and (
            not self._expansion_handles or id(entry) not in self._expansion_handles
        ):
            # `_pack_aware`'s answer for anything that is not a pack.
            return entry
        return self._pack_aware(entry)
    finally:
        # Set on the way out: re-reading a deferred production resolves
        # back-references through this method too, and the caller asks about its
        # own.
        self._last_entry_index = index

template_param

template_param()

A reference to a template parameter, resolved against the scope in force.

Source code in src/demangle/schemes/itanium/parser.py
2387
2388
2389
def template_param(self):
    """A reference to a template parameter, resolved against the scope in force."""
    return self.template_param_binding()[0]

template_param_binding

template_param_binding()

A , as both what it resolves to and the reference itself.

<template-param> ::= T_ | T <parameter-2 non-negative number> _
                   | TL <level> _ [<parameter-2 non-negative number>] _

The TL form names a parameter of an enclosing template by level as well as by index, which Clang emits inside the constraints of a nested template. It is newer than the grammar snapshot in docs/specs/.

Returns (handle, reference). The reference is what a caller recording this parameter as a substitution candidate must store -- see ParameterReference for why the resolved handle is the wrong thing to keep. It is None inside a constraint, where the parameter is spelled by its own mangled text and so cannot mean anything different when the entry is read again.

Source code in src/demangle/schemes/itanium/parser.py
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
def template_param_binding(self):
    """A <template-param>, as both what it resolves to and the reference itself.

    ```
    <template-param> ::= T_ | T <parameter-2 non-negative number> _
                       | TL <level> _ [<parameter-2 non-negative number>] _
    ```

    The `TL` form names a parameter of an enclosing template by level as well as by
    index, which Clang emits inside the constraints of a nested template. It is
    newer than the grammar snapshot in docs/specs/.

    Returns `(handle, reference)`. The reference is what a caller recording this
    parameter as a substitution candidate must store -- see `ParameterReference` for
    why the resolved handle is the wrong thing to keep. It is `None` inside a
    constraint, where the parameter is spelled by its own mangled text and so cannot
    mean anything different when the entry is read again.
    """
    reader = self.reader
    begin = reader.pos
    reader.expect("T")

    # `TL <level-1> _` names the level; a bare `T` is level 0, the innermost
    # <template-args>. Higher levels are what generic lambdas and template template
    # parameters declare. Only `L` may follow here: `Tp` and `Ts` are other
    # productions, and both references refuse `_Z1fIiEvTp_`.
    level = 0
    if reader.eat("L"):
        level = int(reader.digits()) + 1
        reader.expect("_")
    index = 0 if reader.peek() == "_" else reader.integer(allow_negative=False) + 1
    reader.expect("_")

    if self._in_constraint:
        # Inside a requires-clause a parameter is spelled by its own mangled name
        # (`T_` is `T`, `TL0__` is `TL0_`) rather than by its binding.
        symbolic = reader.text[begin : reader.pos - 1]
        if self.options.symbolic_constraint_parameters:
            return self.builder.raw(symbolic), None
        # GNU substitutes the binding, but a clause may name an enclosing template's
        # parameter with nothing bound (`TL0__` in `Q` on a member of `A<int>`). The
        # style must not decide whether a name parses, so that falls back to the
        # symbolic spelling, and the recorded reference carries it for a later `S_`.
        # See `substitution`.
        uses = self._parameter_uses
        try:
            bound = self.bind_template_param(index, level)
        except ParseError:
            bound = self.builder.raw(symbolic)
            reference = None
        else:
            reference = ParameterReference(index, level, symbolic)
        # Put back either way, so a production inside a clause is not deferred; a
        # deferred one re-read under the signature's scope would spell
        # `typename 1234`.
        self._parameter_uses = uses
        return bound, reference
    return self.bind_template_param(index, level), ParameterReference(index, level)

bind_template_param

bind_template_param(index, level=0)

What TL<level>_<index>_ names under the arguments currently in scope.

Source code in src/demangle/schemes/itanium/parser.py
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
def bind_template_param(self, index, level=0):
    """What `TL<level>_<index>_` names under the arguments currently in scope."""
    reader = self.reader
    if self._in_special_name:
        # Both references refuse `_ZTVN1AIcT_EE`; compilers write `_ZTVN1AIccEE`.
        raise ParseError(self._mangled, reader.pos, "a template parameter in a special name")
    self._parameter_uses += 1
    bound = self.targs.lookup(index, level)
    if bound is not None:
        if self._pack_ids and id(bound) in self._pack_ids:
            self._saw_pack = True
            if not self.builder.spell(bound):
                self._saw_empty_pack = True
            members = self.builder.members(bound)
            if members is not None:
                if self._pack_arity is None:
                    self._pack_arity = len(members)
                if self._pack_index is not None and self._pack_index < len(members):
                    return members[self._pack_index]
                if members and not self._reading_pattern:
                    self._bare_pack_used = True
        return bound

    if self._reject_unbound_parameters:
        # A bare `<type>` can never bind this parameter; `c++filt -t` and
        # `__cxa_demangle` refuse it.
        raise ParseError(self._mangled, reader.pos, "template parameter with nothing to bind it")

    if level and level >= self.targs.depth():
        # A level not in scope at all (`TL8_1_`): the reference refuses it. Inside a
        # requires-clause it takes the symbolic branch above.
        raise ParseError(self._mangled, reader.pos, f"no template parameter level {level} in scope")

    if not (self._reading_closure_signature or self._reading_conversion_type):
        # No <template-args> in scope, and none will come: both references refuse
        # `_Z1f1AT_`. The two readings that legitimately find nothing bound (a
        # generic lambda's level, a conversion operator's type) are excluded above.
        raise ParseError(self._mangled, reader.pos, "template parameter with no arguments in scope")
    if self._reading_conversion_type and not self._reading_closure_signature:
        # Provisional: `operator_name` refuses the name if no arguments follow.
        self._conversion_unbound = True
    elif level != self._closure_level:
        # A lambda's `auto` is a parameter of its own level (ABI 5.1.8):
        # llvm-cxxfilt refuses `_Z1fIEvDTLUlT_E_EE` and `_Z1fIiEvDTLUlT0_E_EE`.
        raise ParseError(
            self._mangled, reader.pos, "template parameter with no argument at a level that is not the lambda's"
        )

    # A generic lambda's `auto` (ABI 5.1.8), or a conversion operator's type read
    # before its arguments. llvm-cxxfilt spells `auto`; GNU numbers by parameter
    # index, so `Ul T0_ T_ E` is `(auto:2, auto:1)`.
    if self.options.gnu_closure_spelling:
        return self.builder.raw(f"auto:{index + 1}")
    return self.builder.raw("auto")

decltype_

decltype_()

::= Dt E | DT E

Source code in src/demangle/schemes/itanium/parser.py
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
def decltype_(self):
    """<decltype> ::= Dt <expression> E | DT <expression> E"""
    reader = self.reader
    reader.expect("D")
    marker = reader.take()
    if marker not in ("t", "T"):
        raise ParseError(self._mangled, reader.pos, "expected a decltype")
    expression = self._bracketed_expression()
    reader.expect("E")
    keyword = "decltype " if self.options.gnu_expression_spelling else "decltype"
    return self.builder.expression("decltype", [keyword, "(", expression, ")"])

cv_qualifiers

cv_qualifiers()

cv-qualifiers, returned in C++'s canonical order.

<CV-qualifiers> ::= [r] [V] [K]

At most one of each, in the order the grammar gives. Consuming a whole run instead would fold KK into a single qualified type where the ABI has two nested ones -- one substitution entry rather than two, shifting every later back-reference in the name.

Source code in src/demangle/schemes/itanium/parser.py
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
def cv_qualifiers(self):
    """cv-qualifiers, returned in C++'s canonical order.

    ```
    <CV-qualifiers> ::= [r] [V] [K]
    ```

    At most one of each, in the order the grammar gives. Consuming a whole run
    instead would fold `KK` into a single qualified type where the ABI has two
    nested ones -- one substitution entry rather than two, shifting every later
    back-reference in the name.
    """
    reader = self.reader
    text, end = reader.text, reader.length
    pos = reader.pos
    mask = 0
    if pos < end and text[pos] == "r":
        mask = 1
        pos += 1
    if pos < end and text[pos] == "V":
        mask |= 2
        pos += 1
    if pos < end and text[pos] == "K":
        mask |= 4
        pos += 1
    reader.pos = pos
    return CV_COMBINATIONS[mask]

extended_type

extended_type()

The D-introduced types: extended builtins, decltype, packs, vectors.

Source code in src/demangle/schemes/itanium/parser.py
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
def extended_type(self):
    """The `D`-introduced types: extended builtins, decltype, packs, vectors."""
    reader = self.reader
    builder = self.builder
    pair = reader.peek2()

    if pair in EXTENDED_BUILTIN_TYPES:
        reader.pos += 2
        if pair in ("DB", "DU"):
            # _BitInt(N): DB <number> _ | DB <expression> _
            #
            # A candidate, unlike other builtins, since it carries a width:
            # `_Z6myfuncRDB8_S0_` is `myfunc(_BitInt(8)&, _BitInt(8)&)`.
            width = reader.digits() if reader.peek() in DIGITS else self.expression_text()
            reader.expect("_")
            return self.subs.remember(builder.raw(f"{EXTENDED_BUILTIN_TYPES[pair]}({width})"), "type")
        if pair == "Dn" and self.options.gnu_nullptr_spelling:
            return builder.builtin("decltype(nullptr)")
        if self._auto_substitutes and pair in _UNDEDUCED_AUTO:
            # Apple's clang counts an undeduced `auto` among the candidates, as
            # Clang 6.0 did; nothing else does. See
            # `ItaniumOptions.undeduced_auto_substitution`.
            return self.subs.remember(builder.builtin(EXTENDED_BUILTIN_TYPES[pair]), "type")
        return builder.builtin(EXTENDED_BUILTIN_TYPES[pair])

    if pair == "DF":
        # `DF <n> _` is `_FloatN`, `DF <n> x` is `_FloatNx` (no `_` after the `x`),
        # and `DF16b` is `std::bfloat16_t`.
        reader.pos += 2
        width = reader.digits()
        if reader.eat("b"):
            if width != "16":
                raise ParseError(self._mangled, reader.pos, "bfloat is only 16 bits wide")
            return builder.builtin("std::bfloat16_t")
        if reader.eat("x"):
            return builder.builtin(f"_Float{width}x")
        reader.expect("_")
        return builder.builtin(f"_Float{width}")

    if pair in ("Dt", "DT"):
        return self.subs.remember(self.decltype_(), "type")

    if pair == "Dp":
        reader.pos += 2
        start = reader.pos
        first = reader.peek()
        mark = self.subs.mark()
        outer_empty = self._saw_empty_pack
        outer_pack = self._saw_pack
        outer_arity = self._pack_arity
        self._saw_empty_pack = False
        self._saw_pack = False
        self._pack_arity = None
        self._reading_pattern += 1
        try:
            inner = self.type_()
            over_empty = self._saw_empty_pack
            over_pack = self._saw_pack
            arity = self._pack_arity
        finally:
            self._reading_pattern -= 1
            self._saw_empty_pack = outer_empty
            # An expansion consumes its pack (`ParameterPackExpansion::printLeft`),
            # so an outer pattern reaching a pack only through it takes the dots:
            # `DpPFvDpT_E` is `void (*)(int, char)...`.
            self._saw_pack = outer_pack
            self._pack_arity = outer_arity
        if over_empty:
            # Over an empty pack the pattern expands to nothing, as both references
            # print (libstdc++'s `std::async`); it is still a <type> and enters the
            # table.
            return self.subs.remember(self._expansion(builder.parameter_pack([])), "type")
        if over_pack and arity and len(builder.members(inner) or ()) != arity:
            # A pattern more than a declarator round the parameter is re-read per member:
            # `Dp unary<T_>` over `{int, float}` is `unary<int>, unary<float>`.
            return self.subs.remember(self._expansion(self._expand_pattern(start, mark, arity)), "type")
        if (self._pack_ids and id(inner) in self._pack_ids) or over_pack:
            # The expansion is a <type> recorded separately from its pattern
            # (`Dp R T1_` gives two entries), as both references do. `inner`'s
            # members are already spelled, so no ellipsis; and it gets a fresh
            # handle, as an expansion differs from the pack a `T_` entry is.
            return self.subs.remember(self._expansion(builder.parameter_pack(builder.members(inner))), "type")
        # No pack in the pattern: an unexpanded expansion, printed with `...` as
        # `ParameterPackExpansion::printLeft` does (`_Z1fIJifcEEvDpC1E` is
        # `E complex...`). GNU brackets anything but a name, so `Dp i` is
        # `(int)...`; builtins and template parameters are excluded by opening
        # character, as they spell as one identifier too.
        if self.options.gnu_expression_spelling and not (
            first not in BUILTIN_TYPES
            and first != "D"
            and first != "T"
            and _PLAIN_CALLEE.match(builder.spell(inner)) is not None
        ):
            inner = builder.expression("paren", ["(", inner, ")"])
        return self.subs.remember(builder.pack(inner), "type")

    if pair in ("Dk", "DK"):
        # <type> ::= Dk <type-constraint>   # `C auto`
        #          | DK <type-constraint>   # `C decltype(auto)`
        #
        # A <type>, so a candidate; llvm-cxxfilt 18 does not record it. See
        # `_constrained_placeholder_recorded`.
        placeholder = "auto" if pair == "Dk" else "decltype(auto)"
        reader.pos += 2
        constraint = builder.spell(self.name()[0])
        self._constrained_placeholder_recorded = True
        return self.subs.remember(builder.raw(f"{constraint} {placeholder}"), "type")

    if pair == "Dy":
        # <type> ::= Dy <pack> <index> -- C++26 pack indexing.
        reader.pos += 2
        pattern = self.type_()
        index = self.expression()
        return self.subs.remember(builder.raw(f"({builder.spell(pattern)})[{builder.spell(index)}]"), "type")

    if pair == "Dv":
        return self.subs.remember(self.vector_type(), "type")

    # <function-type> ::= [<CV-qualifiers>] [<exception-spec>] [Dx] F ... E: the
    # spec opens a function type, so it spells `void (*)() noexcept`, not
    # `void () noexcept*`.
    if pair in ("Do", "DO", "Dw", "Dx"):
        return self.function_type_production()

    if pair in ("DA", "DR") or (pair == "DS" and reader.ahead(2) == "D" and reader.ahead(3) in "AR"):
        return builder.builtin(self.fixed_point_type())

    return None

fixed_point_type

fixed_point_type()

::= [DS] DA | [DS] DR

Embedded C's saturating and non-saturating fixed-point types (N1169), added to the ABI in 2023. DS is the _Sat qualifier and comes first.

Source code in src/demangle/schemes/itanium/parser.py
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
def fixed_point_type(self):
    """<builtin-type> ::= [DS] DA <int-code> | [DS] DR <int-code>

    Embedded C's saturating and non-saturating fixed-point types (N1169), added to
    the ABI in 2023. `DS` is the `_Sat` qualifier and comes first.
    """
    reader = self.reader
    saturating = reader.eat("DS")
    kind = reader.take_exactly(2)
    if kind not in ("DA", "DR"):
        reader.fail("expected a fixed-point type")
    code = reader.take()
    integer = _FIXED_POINT_INTEGERS.get(code)
    if integer is None:
        reader.fail(f"unknown fixed-point underlying type {code!r}")
    prefix = "_Sat " if saturating else ""
    return f"{prefix}{integer}{'_Accum' if kind == 'DA' else '_Fract'}"

vector_type

vector_type()

::= Dv _ | Dv _ _

And two shapes the ABI text does not have. Clang writes a dependent size with no underscore before it, Dv <expression> _ <type> -- mangleType for a DependentSizedExtVectorType is Out << "Dv"; mangleExpression(Size); Out << '_' -- which is what llvm-cxxfilt reads and c++filt refuses; the form with the underscore is what c++filt reads and llvm-cxxfilt refuses. Both are read. And AltiVec's __vector pixel is Dv <number> _ p, a p where the element type would be; Dv4_b is __vector bool to the mangler and bool to every demangler, so it is left as the type letter it also is.

Source code in src/demangle/schemes/itanium/parser.py
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
def vector_type(self):
    """<type> ::= Dv <number> _ <type> | Dv _ <expression> _ <type>

    And two shapes the ABI text does not have. Clang writes a dependent size with
    no underscore before it, `Dv <expression> _ <type>` -- `mangleType` for a
    `DependentSizedExtVectorType` is `Out << "Dv"; mangleExpression(Size); Out <<
    '_'` -- which is what `llvm-cxxfilt` reads and `c++filt` refuses; the form with
    the underscore is what `c++filt` reads and `llvm-cxxfilt` refuses. Both are
    read. And AltiVec's `__vector pixel` is `Dv <number> _ p`, a `p` where the
    element type would be; `Dv4_b` is `__vector bool` to the mangler and `bool` to
    every demangler, so it is left as the type letter it also is.
    """
    reader = self.reader
    reader.expect("Dv")
    if reader.eat("_"):
        size = self.expression_text()
    elif reader.peek() in DIGITS:
        size = reader.digits()
    else:
        size = self.expression_text()
    reader.expect("_")
    if reader.eat("p"):
        # Both references refuse `_Z1hDv0_p`, a zero-length AltiVec pixel vector.
        # `Dv0_i` is still read, as c++filt prints `__vector(0)`.
        if size.isdigit() and int(size) == 0:
            raise ParseError(self._mangled, reader.pos, "a pixel vector of dimension 0")
        spelled = "pixel"
    else:
        inner = self.type_()
        spelled = self.builder.spell(inner)
    if self.options.gnu_vector_spelling:
        # c++filt's `d_vector_type` prints the dimension's value: `Dv07_b` is
        # `__vector(7)`.
        return self.builder.raw(f"{spelled} __vector({int(size) if size.isdigit() else size})")
    return self.builder.raw(f"{spelled} vector[{size}]")

class_enum_type

class_enum_type()

::= | Ts | Tu | Te

as_type matters for a : whether a signature follows the entity is not decidable from the grammar alone, only from where the name sits. Z <encoding> E <entity> reached as an may be a local function, and the types after it are its parameters; reached as a it names a local class, and what follows belongs to whatever mentioned it.

Source code in src/demangle/schemes/itanium/parser.py
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
def class_enum_type(self):
    """<class-enum-type> ::= <name> | Ts <name> | Tu <name> | Te <name>

    `as_type` matters for a <local-name>: whether a signature follows the entity is
    not decidable from the grammar alone, only from where the name sits.
    `Z <encoding> E <entity>` reached as an <encoding> may be a local *function*, and
    the types after it are its parameters; reached as a <type> it names a local class,
    and what follows belongs to whatever mentioned it.
    """
    name, _, _, _ = self.name(as_type=True)
    return name

qualified_type

qualified_type()

, as the reference reads it: one production, one candidate.

A vendor qualifier and the cv-qualifiers underneath it are read together, so U3AS1Ki enters one substitution and not two. That matters for every later back-reference in the name: with two, the S0_ in _Z1fPU3AS1KiS0_ names int const AS1 where it should name int const AS1*, and the second parameter loses its pointer.

Source code in src/demangle/schemes/itanium/parser.py
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
def qualified_type(self):
    """<qualified-type>, as the reference reads it: one production, one candidate.

    A vendor qualifier and the cv-qualifiers underneath it are read together, so
    `U3AS1Ki` enters *one* substitution and not two. That matters for every later
    back-reference in the name: with two, the `S0_` in `_Z1fPU3AS1KiS0_` names
    `int const AS1` where it should name `int const AS1*`, and the second parameter
    loses its pointer.
    """
    reader = self.reader
    builder = self.builder
    if reader.peek() == "U":
        reader.pos += 1
        qualifier = self.source_name()
        if reader.peek() == "I":
            arguments = self.template_arguments()
            qualifier += self._angled(", ".join([builder.spell(argument) for argument in arguments]))
        inner = self.qualified_type()
        if qualifier.startswith(_OBJC_PROTOCOL) and not self.options.gnu_objc_protocol_spelling:
            # `U <n>objcproto<protocol> <type>`: llvm-cxxfilt writes `NSArray<A>`,
            # c++filt `NSArray objcproto1A` (see `gnu_objc_protocol_spelling`); a
            # qualified `objc_object` becomes `id<A>` in the `P` branch. The
            # protocol is length-prefixed within the qualifier, and
            # `parseQualifiedType` refuses a short one, `objcproto15`.
            protocol = qualifier[len(_OBJC_PROTOCOL) :]
            digits = 0
            while digits < len(protocol) and protocol[digits] in DIGITS:
                digits += 1
            length = int(protocol[:digits]) if digits else 0
            if not length or digits + length > len(protocol):
                raise ParseError(self._mangled, reader.pos, "an Objective-C protocol that is not a source name")
            protocol = protocol[digits : digits + length]
            spelled = builder.spell(inner)
            handle = builder.raw(f"{spelled}<{protocol}>")
            if spelled == _OBJC_OBJECT:
                # `objc_object<A>` alone; the first pointer makes it `id<A>`, and
                # further ones are ordinary: `id<A>*`.
                objc_ids = self._objc_ids
                objc_protocols = self._objc_protocols
                if objc_ids is None or objc_protocols is None:
                    objc_ids = self._objc_ids = []
                    objc_protocols = self._objc_protocols = {}
                objc_ids.append(handle)
                objc_protocols[id(handle)] = protocol
            return handle
        return builder.vendor_qualify(self._over_a_pack(inner), qualifier)
    if self._at_function_type():
        # A cv-qualified function type is its own production either way round.
        return self.type_()
    qualifiers = self.cv_qualifiers()
    inner = self.type_()
    if qualifiers:
        return builder.qualify(self._over_a_pack(inner), qualifiers)
    return inner

function_type_production

function_type_production()

The whole of [<CV-qualifiers>] [<exception-spec>] [Dx] F ... E.

One grammar component, and so one substitution entry, however many of the optional prefixes are present. Reading the exception specification as a type in its own right and then qualifying the result would enter two, shifting every later back-reference in the name.

The pieces are spelled in the order C++ writes them -- cv-qualifiers, then the ref-qualifier, then the exception specification -- which is not the order the scheme encodes them in: M1XKFivOE is int (X::*)() const &&, with the K read first and printed second.

Source code in src/demangle/schemes/itanium/parser.py
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
def function_type_production(self):
    """The whole of `[<CV-qualifiers>] [<exception-spec>] [Dx] F ... E`.

    One grammar component, and so one substitution entry, however many of the
    optional prefixes are present. Reading the exception specification as a type in
    its own right and then qualifying the result would enter two, shifting every
    later back-reference in the name.

    The pieces are spelled in the order C++ writes them -- cv-qualifiers, then the
    ref-qualifier, then the exception specification -- which is not the order the
    scheme encodes them in: `M1XKFivOE` is `int (X::*)() const &&`, with the `K` read
    first and printed second.
    """
    reader = self.reader
    builder = self.builder
    qualifiers = self.cv_qualifiers()
    specification = ""
    pair = reader.peek2()
    if pair == "Do":
        reader.pos += 2
        specification = " noexcept"
    elif pair == "DO":
        # DO <expression> E -- a computed noexcept, `noexcept(sizeof(T) < 8)`.
        reader.pos += 2
        condition = self.expression_text()
        reader.expect("E")
        specification = f" noexcept({condition})"
    elif pair == "Dw":
        # Dw <type>* E -- the pre-C++17 dynamic specification, `throw(int, char)`.
        reader.pos += 2
        thrown = []
        while not reader.eat("E"):
            if reader.eof:
                raise ParseError(self._mangled, reader.pos, "unterminated throw specification")
            spelled = builder.spell(self.type_())
            # An empty pack expansion contributes no type.
            if spelled:
                thrown.append(spelled)
        specification = f" throw({', '.join(thrown)})"
    if reader.eat("Dx"):
        specification += " transaction_safe"
    return self.subs.remember(self.function_type(specification, qualifiers), "type")

function_type

function_type(exception_spec='', cv_qualifiers=())

A function type.

<function-type> ::= [<CV-qualifiers>] [<exception-spec>] [Dx] F [Y]
                    <bare-function-type> [<ref-qualifier>] E

exception_spec is already-spelled text from the caller, which read it before the F because that is where the grammar puts it.

Source code in src/demangle/schemes/itanium/parser.py
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
def function_type(self, exception_spec="", cv_qualifiers=()):
    """A function type.

    ```
    <function-type> ::= [<CV-qualifiers>] [<exception-spec>] [Dx] F [Y]
                        <bare-function-type> [<ref-qualifier>] E
    ```

    `exception_spec` is already-spelled text from the caller, which read it before
    the `F` because that is where the grammar puts it.
    """
    reader = self.reader
    builder = self.builder
    reader.eat("Dx")  # a transaction-safe function
    reader.expect("F")
    reader.eat("Y")  # extern "C"

    returns = self.type_()
    parameters = []
    #: One flag per kept parameter: was it written `v`, rather than merely spelling
    #: `void` after a substitution?
    written_void = []
    suffix = ""
    while not reader.eat("E"):
        if reader.eof:
            raise ParseError(self._mangled, reader.pos, "unterminated function type")
        if reader.peek() == "R" and reader.ahead(1) == "E":
            reader.take()
            suffix = " &"
            continue
        if reader.peek() == "O" and reader.ahead(1) == "E":
            reader.take()
            suffix = " &&"
            continue
        wrote_void = reader.peek() == "v"
        parameter = self.type_()
        if not _drops_out(builder, parameter):
            parameters.append(parameter)
            written_void.append(wrote_void)
        elif self.options.gnu_empty_pack_spelling:
            # See `bare_function_type`: an empty entry to c++filt, unless it ends
            # the list. It is not a `void` written.
            parameters.append(parameter)
            written_void.append(False)
    if self.options.gnu_empty_pack_spelling:
        while parameters and _drops_out(builder, parameters[-1]):
            parameters.pop()
            written_void.pop()

    if parameters and all(written_void):
        # llvm-cxxfilt drops every literal `void` parameter and c++filt keeps them;
        # they agree when all are literal. A `T_` bound to `void` is not one:
        # `_Z1fIvEvPFvT_E` is `void f<void>(void (*)(void))` to both.
        parameters = []
    written = "".join([f" {qualifier}" for qualifier in cv_qualifiers])
    if self.options.gnu_exception_spec_first:
        # c++filt writes the exception specification before the qualifiers,
        # `void (A::*)() noexcept const &`; llvm-cxxfilt writes it last.
        return builder.function(returns, parameters, exception_spec + written + suffix)
    return builder.function(returns, parameters, written + suffix + exception_spec)

array_type

array_type()

An array type.

<array-type> ::= A [<number>] _ <type> | A <expression> _ <type>
Source code in src/demangle/schemes/itanium/parser.py
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
def array_type(self):
    """An array type.

    ```
    <array-type> ::= A [<number>] _ <type> | A <expression> _ <type>
    ```
    """
    reader = self.reader
    reader.expect("A")
    if reader.peek() == "_":
        dimension = ""
    elif reader.peek() in DIGITS:
        dimension = reader.digits()
    else:
        # `Builder.array` takes a dimension as characters: an array bound is part of
        # the type's spelling, not an operand position a consumer would walk into.
        dimension = self.expression_text()
    reader.expect("_")
    return self.builder.array(self._over_a_pack(self.type_()), dimension)

member_pointer_type

member_pointer_type()

::= M

Source code in src/demangle/schemes/itanium/parser.py
3167
3168
3169
3170
3171
3172
3173
def member_pointer_type(self):
    """<pointer-to-member-type> ::= M <class type> <member type>"""
    reader = self.reader
    reader.expect("M")
    owner = self._over_a_pack(self.type_())
    member = self._over_a_pack(self.type_())
    return self.builder.member_pointer(owner, member)

template_param_decl

template_param_decl(ellipsis='', params=None, named=True)

Guarded wrapper: Tp and Tt both recurse into this production.

Source code in src/demangle/schemes/itanium/parser.py
3177
3178
3179
3180
3181
3182
3183
3184
3185
def template_param_decl(self, ellipsis="", params=None, named=True):
    """Guarded wrapper: `Tp` and `Tt` both recurse into this production."""
    depth = self._depth = self._depth + 1
    if depth > self._max_depth:
        raise LimitExceeded(self._mangled, "recursion depth", self._max_depth)
    try:
        return self._template_param_decl(ellipsis, params, named)
    finally:
        self._depth = depth - 1

template_arguments

template_arguments(install_scope=False)

A template argument list.

<template-args> ::= I <template-arg>+ E

T_ names a parameter of the innermost enclosing template declaration, so which argument list is in scope matters and the two callers differ:

install_scope=True -- these arguments belong to the entity being named, so they become the T_ scope, replacing any outer one. In Class<char>::method<char const*>, T_ inside the signature is char const*, not char. They are installed one at a time because a later argument may reference an earlier one.

install_scope=False -- these arguments belong to a type mentioned in passing, such as the std::allocator<char> inside a parameter list. They are not template parameters of anything being declared here, so they leave the scope untouched; letting them append would corrupt every later T_.

Source code in src/demangle/schemes/itanium/parser.py
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
def template_arguments(self, install_scope=False):
    """A template argument list.

    ```
    <template-args> ::= I <template-arg>+ E
    ```

    `T_` names a parameter of the innermost enclosing template *declaration*, so
    which argument list is in scope matters and the two callers differ:

    `install_scope=True` -- these arguments belong to the entity being named, so
    they become the `T_` scope, replacing any outer one. In
    `Class<char>::method<char const*>`, `T_` inside the signature is `char const*`,
    not `char`. They are installed one at a time because a later argument may
    reference an earlier one.

    `install_scope=False` -- these arguments belong to a *type* mentioned in
    passing, such as the `std::allocator<char>` inside a parameter list. They are
    not template parameters of anything being declared here, so they leave the
    scope untouched; letting them append would corrupt every later `T_`.
    """
    reader = self.reader
    reader.expect("I")
    install_scope = install_scope and self._naming
    # `_scope_has_pack` describes this list; leaked outward, a pack inside an argument
    # (`f<std::tuple<int>>`) suppresses the enclosing `Dp`'s ellipsis.
    outer_has_pack = self._scope_has_pack
    self._scope_has_pack = False
    if install_scope:
        self.targs.install()
    # Arguments are mentioned in passing, not declared, so a nested list must not install
    # its scope: `__normal_iterator<wchar_t*, ...>` would replace a constructor's `T_`.
    was_naming = self._naming
    self._naming = False
    arguments = []
    keep_empty = self.options.gnu_empty_pack_spelling
    empties = [] if keep_empty else None
    # "Preserve the last name we saw -- don't let the template arguments clobber
    # it", as `d_template_args_1` puts it: the constructor of `A<X>::{unnamed
    # type#1}` is `A`, not `X`.
    last_source_name = self._last_source_name
    trailing_empty_pack = False
    seen = False
    # A clause here is the entity's only when this list is the entity's own; c++filt
    # refuses one in a parameter's type.
    outer_constraint = self._argument_constraint
    self._argument_constraint = None
    text = reader.text
    try:
        while True:
            pos = reader.pos
            if pos >= reader.length:
                raise ParseError(self._mangled, pos, "unterminated template argument list")
            char = text[pos]
            if char == "E":
                reader.pos = pos + 1
                break
            if char == "Q" and not seen:
                # `I <template-arg>+ [Q <constraint>] E`: llvm-cxxfilt refuses
                # `_ZN5test21jIQ4TrueITL0__EEEvz`.
                raise ParseError(self._mangled, reader.pos, "a requires-clause with no template arguments")
            seen = True
            depth = self._depth = self._depth + 1
            if depth > self._max_depth:
                raise LimitExceeded(self._mangled, "recursion depth", self._max_depth)
            try:
                argument, is_empty_pack = self.template_arg()
            finally:
                self._depth = depth - 1
            if argument is None:
                # A <template-param-decl> or a requires-clause: neither prints nor
                # occupies a slot.
                continue
            trailing_empty_pack = is_empty_pack
            if not is_empty_pack or keep_empty:
                arguments.append(argument)
                if empties is not None:
                    empties.append(is_empty_pack)
            if install_scope:
                self.targs.add(argument)
    finally:
        self._naming = was_naming
        self._last_source_name = last_source_name
        if not install_scope:
            self._scope_has_pack = outer_has_pack
            self._argument_constraint = outer_constraint
    if empties is not None:
        # c++filt prints an empty pack as an empty argument and keeps the comma,
        # `thread<main::{lambda()#1}, , void>`, and drops the empty ones at the end
        # of the list: `f<int, JE>` is `f<int>`. See `gnu_empty_pack_spelling`.
        while empties and empties[-1]:
            arguments.pop()
            empties.pop()
    self._trailing_empty_pack = trailing_empty_pack
    return arguments

template_arg

template_arg()

One template argument.

<template-arg> ::= <type> | X <expression> E | <expr-primary> | J <template-arg>* E

An argument pack is also read from I <template-arg>* E: the form g++ wrote for one under -fabi-version 2 through 5, the default of GCC 3.4 through 4.9, and still writes as a compatibility alias beside the J form when asked for those versions. Nothing else can stand where an argument does and begin with I, so there is nothing to tell it from; libiberty's d_template_arg reads I and J alike, and llvm-cxxfilt refuses the older one.

Returns (handle, is_empty_pack). The handle is None for a , which declares a parameter rather than supplying one.

Emptiness is returned rather than recorded on the parser because argument lists nest: an empty pack inside AnalysisManager<Module, JE> would otherwise still be flagged when the enclosing PassManager<Function, AnalysisManager<...>> finished its own argument, and the enclosing argument would be dropped.

Source code in src/demangle/schemes/itanium/parser.py
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
def template_arg(self):
    """One template argument.

    ```
    <template-arg> ::= <type> | X <expression> E | <expr-primary> | J <template-arg>* E
    ```

    An argument pack is also read from `I <template-arg>* E`: the form g++ wrote for
    one under `-fabi-version` 2 through 5, the default of GCC 3.4 through 4.9, and
    still writes as a compatibility alias beside the `J` form when asked for those
    versions. Nothing else can stand where an argument does and begin with `I`, so
    there is nothing to tell it from; libiberty's `d_template_arg` reads `I` and
    `J` alike, and `llvm-cxxfilt` refuses the older one.

    Returns `(handle, is_empty_pack)`. The handle is None for a
    <template-param-decl>, which declares a parameter rather than supplying one.

    Emptiness is returned rather than recorded on the parser because argument lists
    nest: an empty pack inside `AnalysisManager<Module, JE>` would otherwise still be
    flagged when the enclosing `PassManager<Function, AnalysisManager<...>>` finished
    its own argument, and the enclosing argument would be dropped.
    """
    reader = self.reader
    builder = self.builder

    # One lookahead for all five alternatives: this runs once per template argument.
    ahead = reader.peek2()
    char = ahead[:1]

    if ahead in _PARAMETER_DECLARATIONS:
        # A declaration qualifies the next argument (`parseTemplateArg`), so llvm-cxxfilt
        # refuses a list ending on one, `ITyE`.
        self.template_param_decl()
        if reader.peek() == "E":
            raise ParseError(self._mangled, reader.pos, "a template parameter declaration with no argument")
        return None, False

    if char == "Q":
        # A requires-clause closing an argument list: llvm-cxxfilt does not print
        # it; c++filt prints it after the function's parameters (kept under the gnu
        # style).
        reader.take()
        outer_constraint = self._in_constraint
        outer_naming = self._naming
        self._in_constraint = True
        self._naming = False
        try:
            constraint = self.expression()
        finally:
            self._in_constraint = outer_constraint
            self._naming = outer_naming
        if self.options.gnu_expression_spelling:
            self._argument_constraint = builder.spell(constraint)
        return None, False

    if char == "X":
        reader.take()
        # `>`, `>>` and `,` are bracketed inside an argument list, or they read as
        # its end or separator; the opening two characters say which.
        angled = reader.peek2() in ("gt", "rs", "cm")
        if self.options.gnu_expression_spelling:
            # c++filt brackets each operand instead (`enable_if<(4u),(4), void>`)
            # and leaves `>>` bare, `f<(1)>>(2)>`; a `>` is bracketed by its own
            # spelling.
            angled = False
        # llvm-cxxfilt brackets a `>` or `>>` anywhere in an argument list until
        # some bracket opens round it (`(1 > 0) && true`); this wrap is such a
        # bracket.
        outer_bare = self._bare_angle
        self._bare_angle = not angled and not self.options.gnu_expression_spelling
        try:
            expression = self.expression()
        finally:
            self._bare_angle = outer_bare
        reader.expect("E")
        if angled:
            expression = builder.expression("paren", ["(", expression, ")"])
        return expression, False

    if char == "L":
        return builder.raw(self.expr_primary()), False

    if char in ("J", "I"):
        reader.take()
        members = []
        while True:
            # `peek` answers `""` past the end, so one call decides between the
            # closing `E`, another member, and a truncated name.
            next_char = reader.peek()
            if next_char == "E":
                reader.take()
                break
            if not next_char:
                raise ParseError(self._mangled, reader.pos, "unterminated argument pack")
            if next_char == "I":
                self._legacy_pack_nested = True
            member, _ = self.template_arg()
            if member is not None:
                members.append(member)
        self._scope_has_pack = True
        if char == "I":
            self._legacy_pack_used = True
        handle = builder.parameter_pack(members)
        packs = self._packs
        pack_ids = self._pack_ids
        if packs is None or pack_ids is None:
            packs = self._packs = []
            pack_ids = self._pack_ids = set()
        packs.append(handle)
        pack_ids.add(id(handle))
        # An empty pack takes a `T_` slot but spells nothing
        # (`AnalysisManager<Module, JE>`). The builder decides, as it flattened any
        # nested empty packs.
        return handle, not builder.spell(handle)

    # A template template argument is the template's bare name, and a back-reference
    # to one is legal here and nowhere else a <type> is read.
    self._template_name_argument = char == "S"
    argument = self.type_()
    # An expansion over an empty pack takes no slot either. Checked on the handle: `Dp`
    # records an expansion, not a pack (`1AIDpT_T0_E` over an empty `T_` is `A<int>`).
    return argument, builder.members(argument) is not None and not builder.spell(argument)

expr_primary

expr_primary()

A literal or a reference to a declared entity.

<expr-primary> ::= L <type> <value number> E | L <mangled-name> E | L _Z <encoding> E
Source code in src/demangle/schemes/itanium/parser.py
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
def expr_primary(self):
    """A literal or a reference to a declared entity.

    ```
    <expr-primary> ::= L <type> <value number> E | L <mangled-name> E | L _Z <encoding> E
    ```
    """
    reader = self.reader
    builder = self.builder
    reader.expect("L")

    if reader.startswith("_Z") or reader.startswith("Z"):
        # A reference to a declared entity (5.1.6.2), parsed with this parser's
        # state because Clang writes substitutions inside it that index the
        # enclosing table. The bare `Z` is g++'s compatibility form (libcxxabi's
        # `_ZN5test52f2ENS_2t2ILZ4mainEEE`); llvm-cxxfilt accepts it only as a
        # template argument, c++filt anywhere, as here. See
        # `_bare_entity_prefix_used`.
        if reader.peek() == "Z":
            self._bare_entity_prefix_used = True
        reader.eat("_")
        reader.expect("Z")
        # `Z <encoding> E <entity>`, a local name, detected here because a nested
        # encoding reaches `name()` first.
        local = reader.peek() == "Z"
        was_naming = self._naming
        outer_scope = self.targs.snapshot()
        # `_drop_return` is about the enclosing function, not this embedded encoding
        # (`f<h()>()::{lambda()#1}`).
        outer_drop_return = self._drop_return
        outer_no_return_type = self._no_return_type
        outer_explicit_object = self._explicit_object
        self._naming = True
        self._drop_return = False
        self._no_return_type = False
        self._explicit_object = False
        try:
            handle = self.encoding()
        finally:
            # The table is shared, but the embedded entity's own `T_` scope must not
            # outlive it.
            self._naming = was_naming
            self.targs.restore(outer_scope)
            self._drop_return = outer_drop_return
            self._no_return_type = outer_no_return_type
            self._explicit_object = outer_explicit_object
        reader.expect("E")
        self._entity_local = local
        return builder.spell(handle)

    if reader.peek2() == "Ul":
        # `L <closure-type-name> E`: a closure object as a value. The reference
        # spells the lambda expression and accepts only `Ul` here.
        handle = self.unnamed_type_name(lambda_expression=True)
        reader.expect("E")
        return builder.spell(handle)

    # `L <array-type> E` is a string literal whose contents are not mangled; the
    # reference prints the type in angle brackets inside the quotes.
    was_array = reader.peek() == "A"
    # Decided by the characters written: both references refuse `LSt9nullptr_tE`.
    wrote_nullptr = reader.peek2() == "Dn"
    # Decided by the type code, not the spelling: a class called `float` is not one.
    wrote_float = reader.peek() in ("d", "e", "f")
    kind = self.type_()
    spelling = builder.spell(kind)
    if was_array and reader.eat("E"):
        return f'"<{spelling}>"'

    if reader.eat("E"):
        if not wrote_nullptr:
            # A value is not optional: both references refuse `LaE`.
            raise ParseError(self._mangled, reader.pos, "a literal with no value")
        # `LDnE` is `decltype(nullptr)` to c++filt and `nullptr` to llvm-cxxfilt.
        return spelling if self.options.gnu_nullptr_spelling else "nullptr"

    start = reader.pos
    if wrote_float:
        while not reader.eat("E"):
            if reader.eof:
                raise ParseError(self._mangled, reader.pos, "unterminated literal")
            reader.take()
        return self.spell_float_literal(spelling, reader.text[start : reader.pos - 1])
    # `<value number>`: digits after an optional `n`; llvm-cxxfilt refuses `Li4JE`
    # and `LinE`.
    reader.eat("n")
    if reader.peek() not in DIGITS:
        raise ParseError(self._mangled, reader.pos, "a literal whose value is not a number")
    reader.digits()
    value = reader.text[start : reader.pos]
    reader.expect("E")
    if spelling == "bool" and value not in ("0", "1"):
        # `Lb0E` and `Lb1E` are the two bool literals there are. `llvm-cxxfilt`
        # refuses `Lb2E`; `c++filt` spells `(bool)2`, a value no bool has.
        raise ParseError(self._mangled, reader.pos, "a bool literal that is neither 0 nor 1")
    return self.spell_literal(spelling, value)

spell_float_literal

spell_float_literal(kind, value)

L <d|e|f> <hex> E: the value's bytes, most significant first, in hex.

llvm-cxxfilt decodes them and prints the number with C's %a, f after a float and L after a long double; c++filt writes the hex as it stands, in brackets after the type: (double)[4048f5c28f5c28f6]. Each spelling here is one of those.

The width is the type's: eight digits for a float and sixteen for a double, and for a long double whichever the target has -- sixteen where it is a double, twenty for the x87 extended format and thirty-two for the IEEE quad, plus the x87 format as g++ pads it to the type's size, twenty-four digits on i386 and thirty-two on x86-64; see _c_hex_float. llvm-cxxfilt insists on the width of the machine it runs on and refuses the rest; c++filt brackets any run of characters at all. Neither is a reading of Ld4048E, which is no value, so the width has to be one of those and every character a hex digit -- a lowercase one, as the ABI says and as LLVM's main branch requires.

Source code in src/demangle/schemes/itanium/parser.py
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
def spell_float_literal(self, kind, value):
    """`L <d|e|f> <hex> E`: the value's bytes, most significant first, in hex.

    `llvm-cxxfilt` decodes them and prints the number with C's `%a`, `f` after a
    float and `L` after a long double; `c++filt` writes the hex as it stands, in
    brackets after the type: `(double)[4048f5c28f5c28f6]`. Each spelling here is one
    of those.

    The width is the type's: eight digits for a float and sixteen for a double, and
    for a long double whichever the target has -- sixteen where it is a double,
    twenty for the x87 extended format and thirty-two for the IEEE quad, plus the
    x87 format as g++ pads it to the type's size, twenty-four digits on i386 and
    thirty-two on x86-64; see `_c_hex_float`. `llvm-cxxfilt` insists on the width
    of the machine it runs on and refuses the rest; `c++filt` brackets any run of
    characters at all. Neither is a reading of `Ld4048E`, which is no value, so the
    width has to be one of those and every character a hex digit -- a lowercase
    one, as the ABI says and as LLVM's main branch requires.
    """
    widths = _FLOAT_WIDTHS[kind]
    if len(value) not in widths or not _HEX.fullmatch(value):
        raise ParseError(self._mangled, self.reader.pos, "a floating-point literal of the wrong width")
    if len(value) == 24 and not value.startswith("0000"):
        # Twenty-four digits are g++'s i386 form, which opens with two zero bytes.
        # Refused under both styles: a style never decides whether a name reads.
        raise ParseError(self._mangled, self.reader.pos, "a long double of twenty-four digits without its padding")
    if self.options.gnu_expression_spelling:
        return f"({kind})[{value}]"
    return _c_hex_float(kind, value)

spell_literal staticmethod

spell_literal(kind, value)

Render a literal the way the reference demanglers do.

The scheme encodes the value as digits with a leading n for negative, and leaves the suffix implied by the type. Reproducing the suffix matters: 1 and 1u are different template arguments and must not print alike.

Source code in src/demangle/schemes/itanium/parser.py
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
@staticmethod
def spell_literal(kind, value):
    """Render a literal the way the reference demanglers do.

    The scheme encodes the value as digits with a leading `n` for negative, and
    leaves the suffix implied by the type. Reproducing the suffix matters: `1` and
    `1u` are different template arguments and must not print alike.
    """
    if value.startswith("n"):
        value = "-" + value[1:]

    if kind == "std::nullptr_t":
        # `LDn0E`. The value is written and means nothing; there is one of these.
        return "nullptr"
    if kind == "bool":
        return {"0": "false", "1": "true"}[value]
    if kind == "int":
        return value
    suffixes = {
        "unsigned int": "u",
        "long": "l",
        "unsigned long": "ul",
        "long long": "ll",
        "unsigned long long": "ull",
    }
    if kind in suffixes:
        return value + suffixes[kind]
    if kind == "char":
        return f"(char){value}"
    return f"({kind}){value}"

unresolved_name

unresolved_name()

A name the compiler could not resolve.

<unresolved-name> ::= [gs] <base-unresolved-name>
                    | sr <unresolved-type> [<template-args>] <base-unresolved-name>
                    | srN <unresolved-type> [<template-args>]
                          <unresolved-qualifier-level>* E <base-unresolved-name>
                    | [gs] sr <unresolved-qualifier-level>+ E <base-unresolved-name>

A name written in a template that the compiler could not resolve, because it depends on a parameter: std::is_signed_v<T> inside an enable_if. These reach a mangled name through SFINAE return types, which is why they are everywhere in heavily templated C++ and absent from simple test cases.

The two sr forms without N are told apart by what follows: an is a and so begins with a digit, while an begins with T, D or S.

Source code in src/demangle/schemes/itanium/parser.py
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
def unresolved_name(self):
    """A name the compiler could not resolve.

    ```
    <unresolved-name> ::= [gs] <base-unresolved-name>
                        | sr <unresolved-type> [<template-args>] <base-unresolved-name>
                        | srN <unresolved-type> [<template-args>]
                              <unresolved-qualifier-level>* E <base-unresolved-name>
                        | [gs] sr <unresolved-qualifier-level>+ E <base-unresolved-name>
    ```

    A name written in a template that the compiler could not resolve, because it
    depends on a parameter: `std::is_signed_v<T>` inside an `enable_if`. These reach
    a mangled name through SFINAE return types, which is why they are everywhere in
    heavily templated C++ and absent from simple test cases.

    The two `sr` forms without `N` are told apart by what follows: an
    <unresolved-qualifier-level> is a <simple-id> and so begins with a digit, while
    an <unresolved-type> begins with `T`, `D` or `S`.
    """
    reader = self.reader
    prefix = "::" if reader.eat("gs") else ""

    if not reader.eat("sr"):
        text = prefix + self.base_unresolved_name()
        if prefix:
            self._simple_name = False
        return text

    levels = []
    if reader.peek() == "N" and (
        reader.ahead(1) in DIGITS or reader.startswith("NSt") or self.options.gnu_unresolved_scope_substitution
    ):
        # `sr N <prefix>+ E <name>`: the old form's nested-name shape, which g++ 13
        # writes for a non-dependent class scope with a dependent member
        # (`srN1A1B1CIT_EE1w`), recorded as one type. GNU c++filt reads every `srN`
        # this way; the option says when to follow it.
        levels.append(self.builder.spell(self.type_()))
    elif reader.eat("N"):
        levels.append(self._unresolved_head())
        # `*`, not the ABI's `+`: Clang emits `srN <type> <template-args> E` with no
        # levels, and the reference accepts it.
        while not reader.eat("E"):
            if reader.eof:
                raise ParseError(self._mangled, reader.pos, "unterminated qualifier levels")
            levels.append(self.simple_id())
    elif reader.peek() in DIGITS:
        self._qualifier_levels(levels)
    elif reader.startswith("St") and reader.ahead(2) in DIGITS:
        # The pre-<unresolved-name> form g++ still writes,
        # `sr <type> <unqualified-name>` with a complete type, as libiberty's
        # `d_expression_1` reads it. libstdc++ ships it
        # (`srSt23__is_random_access_iterIT0_...E7__valueE`); llvm-cxxfilt refuses
        # it, but `St` plus a name has no reading under the modern grammar.
        levels.append(self.builder.spell(self.type_()))
    else:
        levels.append(self._unresolved_head())

    levels.append(self.base_unresolved_name(qualified=True))
    if prefix:
        # `::x` is rooted at global scope, which c++filt brackets as an operand.
        self._simple_name = False
    return prefix + "::".join(levels)

unresolved_type

unresolved_type()

A dependent type at the head of an unresolved name.

<unresolved-type> ::= <template-param> | <decltype> | <substitution>
Source code in src/demangle/schemes/itanium/parser.py
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
def unresolved_type(self):
    """A dependent type at the head of an unresolved name.

    ```
    <unresolved-type> ::= <template-param> | <decltype> | <substitution>
    ```
    """
    reader = self.reader
    builder = self.builder
    if reader.peek() == "T":
        component, reference = self.template_param_binding()
        self.subs.remember(reference if reference is not None else component, "unresolved-type")
        return builder.spell(component)
    if reader.peek() == "D":
        return builder.spell(self.subs.remember(self.decltype_(), "unresolved-type"))
    return builder.spell(self.substitution())

simple_id

simple_id()
<simple-id> ::= <source-name> [<template-args>]
Source code in src/demangle/schemes/itanium/parser.py
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
def simple_id(self):
    """```
    <simple-id> ::= <source-name> [<template-args>]
    ```
    """
    text = self.source_name()
    if self.reader.peek() == "I":
        text += self.spelled_template_arguments()
        # Set after the arguments are read, not before: they are expressions and may
        # contain names of their own, whose reading would otherwise stand.
        self._simple_name = False
        return text
    self._simple_name = True
    return text

spelled_template_arguments

spelled_template_arguments()

A template argument list rendered as text, for use inside a name.

Source code in src/demangle/schemes/itanium/parser.py
3912
3913
3914
3915
def spelled_template_arguments(self):
    """A template argument list rendered as text, for use inside a name."""
    arguments = self.template_arguments()
    return self._angled(", ".join([self.builder.spell(argument) for argument in arguments]))

base_unresolved_name

base_unresolved_name(qualified=False)

::= | on [] | dn

qualified says an sr scope precedes it, which is what decides whether an operator name is a plain operand -- see below.

Source code in src/demangle/schemes/itanium/parser.py
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
def base_unresolved_name(self, qualified=False):
    """<base-unresolved-name> ::= <simple-id> | on <operator-name> [<template-args>]
    | dn <destructor-name>

    `qualified` says an `sr` scope precedes it, which is what decides whether an
    operator name is a plain operand -- see below.
    """
    reader = self.reader
    if reader.peek() in DIGITS:
        return self.simple_id()
    if reader.eat("dn"):
        # `_simple_name`: whether the whole name is a plain identifier path, which
        # GNU c++filt leaves unbracketed as an operand. Decided by the last
        # component, since arguments on a qualifier do not count.
        text = "~" + self.destructor_name()
        self._simple_name = False
        return text
    # The `on` marker is optional: `srT_pl` names `T::operator+` with nothing to say
    # so, and the reference reads the operator code either way.
    reader.eat("on")
    text = self.operator_name()
    # c++filt brackets an operator name on its own, `&(operator&)`; under an `sr`
    # scope it is a qualified name, `&A::operator&`, unless arguments follow:
    # `&(A::operator&<int>)`.
    simple = qualified and reader.peek() != "I"
    if reader.peek() == "I":
        text += self.spelled_template_arguments()
    self._simple_name = simple
    return text

destructor_name

destructor_name()

::= |

Source code in src/demangle/schemes/itanium/parser.py
3959
3960
3961
3962
3963
def destructor_name(self):
    """<destructor-name> ::= <unresolved-type> | <simple-id>"""
    if self.reader.peek() in DIGITS:
        return self.simple_id()
    return self.unresolved_type()

expression_name

expression_name()

A name appearing in an expression -- the callee of a call, most often.

Records nothing for the name itself. Section 5.1.10 excludes "function and operator names other than extern "C" functions" from the candidate set, and adds that "we do not substitute for expressions, though names appearing in them might be substituted": a name in an expression may refer to an existing entry, but it does not create one.

Template arguments applied to it still record their own types, which is how _ZSt12construct_atIcJRKcEE...cl7declvalIT0_EE... reaches S4_ -- that entry is the T0_ inside declval's argument list, not declval itself.

Source code in src/demangle/schemes/itanium/parser.py
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
def expression_name(self):
    """A name appearing in an expression -- the callee of a call, most often.

    Records nothing for the name itself. Section 5.1.10 excludes "function and
    operator names other than extern \"C\" functions" from the candidate set, and
    adds that "we do not substitute for expressions, though names appearing in them
    might be substituted": a name in an expression may *refer* to an existing entry,
    but it does not create one.

    Template arguments applied to it still record their own types, which is how
    `_ZSt12construct_atIcJRKcEE...cl7declvalIT0_EE...` reaches `S4_` -- that entry is
    the `T0_` inside `declval`'s argument list, not `declval` itself.
    """
    reader = self.reader
    builder = self.builder
    if reader.peek() in DIGITS:
        text = self.source_name()
        if reader.peek() == "I":
            arguments = self.template_arguments()
            rendered = ", ".join([builder.spell(argument) for argument in arguments])
            return text + self._angled(rendered)
        return text
    # `on <operator-name>` and `dn <destructor-name>`: a callee named by the
    # operator it is, as in an unresolved `a + b` in `decltype`
    # (`_Z1fI1AEDTclonplfp_fp_EET_`); not the builtin codes `o` and `n`.
    if reader.peek2() in ("on", "dn"):
        return self.base_unresolved_name()
    return builder.spell(self.type_())

initialiser

initialiser()

A parenthesised initialiser list.

<initializer> ::= pi <expression>* E
Source code in src/demangle/schemes/itanium/parser.py
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
def initialiser(self):
    """A parenthesised initialiser list.

    ```
    <initializer> ::= pi <expression>* E
    ```
    """
    reader = self.reader
    if reader.peek2() == "il":
        # `new T{...}`: both compilers write `il <expression>* E` straight after the
        # type, with no `E` of its own. libiberty reads it; llvm-cxxfilt 18 and 20
        # refuse it.
        return self.expression()
    if not reader.eat("pi"):
        # Only `pi`, `il` or the closing `E`, as `d_expression` and `parseNewExpr`
        # read it: both refuse `nw_icvi_E`.
        raise ParseError(self._mangled, reader.pos, "a new-expression's initialiser must be `pi` or `il`")
    arguments = []
    while not reader.eat("E"):
        if reader.eof:
            raise ParseError(self._mangled, reader.pos, "unterminated initialiser")
        arguments.append(self._element())
    return self.builder.expression("initialiser", ["(", *self._commas(arguments), ")"])

expression

expression()

A constant expression appearing in a type or template argument.

Returns a builder handle, so an expression is structure to a builder that wants structure and text to one that wants text. Only the shapes a compiler actually emits into a name are modelled: the full expression grammar is enormous and most of it cannot reach a mangled name, because it is not part of any signature.

Sets _precedence, which a containing operator reads immediately afterwards to decide whether this operand needs brackets.

Source code in src/demangle/schemes/itanium/parser.py
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
def expression(self):
    """A constant expression appearing in a type or template argument.

    Returns a builder handle, so an expression is structure to a builder that wants
    structure and text to one that wants text. Only the shapes a compiler actually
    emits into a name are modelled: the full expression grammar is enormous and most
    of it cannot reach a mangled name, because it is not part of any signature.

    Sets `_precedence`, which a containing operator reads immediately afterwards to
    decide whether this operand needs brackets.
    """
    depth = self._depth = self._depth + 1
    if depth > self._max_depth:
        raise LimitExceeded(self._mangled, "recursion depth", self._max_depth)
    try:
        self._precedence = PRIMARY_PRECEDENCE
        return self._expression()
    finally:
        self._depth = depth - 1

constraint_expression

constraint_expression()

Q <expression>, the C++20 requires-clause.

Every enclosing template's parameters are in scope inside it, and this parser does not track them all -- so a <template-param> inside a clause is spelled by its own mangled name rather than by whatever it is bound to, which is what the reference does for the same reason.

Source code in src/demangle/schemes/itanium/parser.py
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
def constraint_expression(self):
    """`Q <expression>`, the C++20 requires-clause.

    Every enclosing template's parameters are in scope inside it, and this parser
    does not track them all -- so a `<template-param>` inside a clause is spelled by
    its own mangled name rather than by whatever it is bound to, which is what the
    reference does for the same reason.
    """
    outer = self._in_constraint
    outer_naming = self._naming
    self._in_constraint = True
    # A clause names no entity, so a template-id in it must not install its
    # arguments as the `T_` scope (a nested requirement's `1234` would become the
    # next one's `T`).
    self._naming = False
    try:
        return self.expression()
    finally:
        self._in_constraint = outer
        self._naming = outer_naming

expression_text

expression_text()

An expression where the grammar around it needs characters, not a shape.

An array bound and a throw(...) specification are spelled into a type by productions that take text, so those ask for text. Everything reachable from _expression keeps the handle.

Source code in src/demangle/schemes/itanium/parser.py
4059
4060
4061
4062
4063
4064
4065
4066
def expression_text(self):
    """An expression where the grammar around it needs characters, not a shape.

    An array bound and a `throw(...)` specification are spelled *into* a type by
    productions that take text, so those ask for text. Everything reachable from
    `_expression` keeps the handle.
    """
    return self.builder.spell(self.expression())

requires_expression

requires_expression()

A C++20 requires-expression.

<expression>  ::= rq <requirement>+ E
                | rQ <bare-function-type> _ <requirement>+ E
<requirement> ::= X <expression> [N] [R <type-constraint>]
                | T <type>
                | Q <constraint-expression>

rQ is the form with a parameter list, requires (T) { ... }. Each requirement is written with a leading space and a closing semicolon, which is how the reference lays the braces out.

Source code in src/demangle/schemes/itanium/parser.py
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
def requires_expression(self):
    """A C++20 requires-expression.

    ```
    <expression>  ::= rq <requirement>+ E
                    | rQ <bare-function-type> _ <requirement>+ E
    <requirement> ::= X <expression> [N] [R <type-constraint>]
                    | T <type>
                    | Q <constraint-expression>
    ```

    `rQ` is the form with a parameter list, `requires (T) { ... }`. Each requirement
    is written with a leading space and a closing semicolon, which is how the
    reference lays the braces out.
    """
    reader = self.reader
    builder = self.builder
    parts = ["requires"]
    if reader.startswith("rQ"):
        reader.pos += 2
        parameters = []
        while not reader.eat("_"):
            if reader.eof:
                raise ParseError(self._mangled, reader.pos, "unterminated requires parameters")
            parameters.append(self.type_())
        parts += [" (", *self._commas(parameters), ")"]
    else:
        reader.pos += 2
    parts.append(" {")
    while not reader.eat("E"):
        if reader.eof:
            raise ParseError(self._mangled, reader.pos, "unterminated requires expression")
        marker = reader.take()
        if marker == "X":
            # A compound requirement is braced when it says anything more than that
            # the expression is valid.
            expression = self.expression()
            no_throw = reader.eat("N")
            constraint = self.name()[0] if reader.eat("R") else None
            braced = no_throw or constraint is not None
            parts += [" {", expression, "}"] if braced else [" ", expression]
            if no_throw:
                parts.append(" noexcept")
            if constraint is not None:
                parts += [" -> ", constraint]
        elif marker == "T":
            parts += [" typename ", self.type_()]
        elif marker == "Q":
            parts += [" requires ", self.constraint_expression()]
        else:
            raise ParseError(self._mangled, reader.pos, f"unknown requirement {marker!r}")
        parts.append(";")
    parts.append(" }")
    self._precedence = PRIMARY_PRECEDENCE
    return builder.expression("requires", parts)

detect

detect(name)

Cheap test for "is this plausibly an Itanium mangled name".

Runs on every symbol a caller passes, including the overwhelming majority that are not mangled at all, so it does no work beyond a prefix comparison.

_GLOBAL__ is deliberately not one of the prefixes: parse does not read those names -- GNU's "global constructors keyed to ..." extension -- and claiming them here would only hand them back unchanged one step later.

Source code in src/demangle/schemes/itanium/parser.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
def detect(name):
    """Cheap test for "is this plausibly an Itanium mangled name".

    Runs on every symbol a caller passes, including the overwhelming majority that are
    not mangled at all, so it does no work beyond a prefix comparison.

    `_GLOBAL__` is deliberately not one of the prefixes: `parse` does not read those
    names -- GNU's "global constructors keyed to ..." extension -- and claiming them
    here would only hand them back unchanged one step later.
    """
    return (
        name.startswith("_Z")
        or name.startswith("__Z")
        or name.startswith("___Z")
        or name.startswith("____Z")
        or name.startswith(_ALLOC_TOKEN)
        or name.startswith(_IMPORT_THUNK_OPENINGS)
    )

parse

parse(
    mangled,
    builder,
    limits=DEFAULT_LIMITS,
    options=DEFAULT_OPTIONS,
)

Parse an Itanium mangled name into builder, returning its handle.

Read twice when the first reading took an sr the modern way and then failed: the second reads every ambiguous sr as the old sr <type> <name> that g++ 13 still writes -- see _qualifier_levels. That is libiberty's unresolved_name_state exactly, a whole-name retry, so a name that reads either way reads as the reference reads it. A name that reads neither way reports the first reading's error, which is the one the modern grammar gives.

Read twice, too, when a back-reference ran past the substitution table and the name carries an undeduced auto: the table was numbered by the other of the two rules compilers apply to that type -- see ItaniumOptions.undeduced_auto_substitution -- and the second reading applies it. Only where the option left the rule to the name's form; a caller who chose one is not second-guessed.

And read twice when a back-reference named a closure prefix, or a template with no arguments, as a type, in a name that has a closure prefix: GCC 12 and earlier left the prefix out of the table, so every reference after it is one entry early -- see ItaniumOptions.closure_prefix_substitution -- and the second reading leaves it out too. The same rule about a caller who chose applies.

Source code in src/demangle/schemes/itanium/parser.py
4934
4935
4936
4937
4938
4939
4940
4941
4942
4943
4944
4945
4946
4947
4948
4949
4950
4951
4952
4953
4954
4955
4956
4957
4958
4959
4960
4961
4962
4963
4964
4965
4966
4967
4968
4969
4970
4971
4972
4973
4974
4975
4976
4977
4978
4979
4980
4981
4982
4983
4984
4985
4986
4987
4988
4989
4990
4991
4992
4993
4994
4995
4996
4997
4998
4999
5000
5001
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=DEFAULT_OPTIONS):
    """Parse an Itanium mangled name into `builder`, returning its handle.

    Read twice when the first reading took an `sr` the modern way and then failed:
    the second reads every ambiguous `sr` as the old `sr <type> <name>` that g++ 13
    still writes -- see `_qualifier_levels`. That is libiberty's `unresolved_name_state`
    exactly, a whole-name retry, so a name that reads either way reads as the
    reference reads it. A name that reads neither way reports the first reading's
    error, which is the one the modern grammar gives.

    Read twice, too, when a back-reference ran past the substitution table and the name
    carries an undeduced `auto`: the table was numbered by the other of the two rules
    compilers apply to that type -- see `ItaniumOptions.undeduced_auto_substitution` --
    and the second reading applies it. Only where the option left the rule to the name's
    form; a caller who chose one is not second-guessed.

    And read twice when a back-reference named a closure prefix, or a template with no
    arguments, as a type, in a name that has a closure prefix: GCC 12 and earlier left
    the prefix out of the table, so every reference after it is one entry early -- see
    `ItaniumOptions.closure_prefix_substitution` -- and the second reading leaves it out
    too. The same rule about a caller who chose applies.
    """
    parser = ItaniumParser(mangled, builder, limits, options)
    try:
        return parser.parse()
    except ParseError as error:
        if isinstance(error, LimitExceeded):
            raise
        # Numbering rules a second reading may apply, in order: each is one a compiler
        # is known to apply, tried only where the caller left it open and the name can
        # carry it.
        rules = []
        if isinstance(error, (SubstitutionOverrun, SubstitutionMisuse)):
            if options.closure_prefix_substitution is None and parser._closure_prefix_seen:
                # The other rule for a closure prefix: the ABI's, clang's and GCC 13's
                # against GCC 12's and Apple clang's, which leave it out of the table.
                # See `ItaniumOptions.closure_prefix_substitution`.
                rules.append({"_closure_prefix_substitutes": not parser._closure_prefix_substitutes})
            if options.undeduced_auto_substitution is None and any(pair in mangled for pair in _UNDEDUCED_AUTO):
                # The other rule for an undeduced `auto`; see
                # `ItaniumOptions.undeduced_auto_substitution`. Reached by a misuse too:
                # one entry short lands in range more often than past the end.
                rules.append({"_auto_substitutes": not parser._auto_substitutes})
            if len(rules) == 2:
                # Both at once: upstream clang targeting Darwin counts the closure
                # prefix and not the `auto`, the opposite of Apple's fork on both.
                rules.append({**rules[0], **rules[1]})
            if options.inherited_constructor_substitution is None and parser._inherited_base_seen:
                # g++'s rule for an inheriting constructor's base type, which clang's
                # first reading omits; a g++ name then runs past the table. Not combined
                # with the rules above: no corpus holds both shapes.
                rules.append({"_inherited_base_substitutes": True})
        for overrides in rules:
            retry = ItaniumParser(mangled, builder, limits, options)
            for attribute, value in overrides.items():
                setattr(retry, attribute, value)
            try:
                return retry.parse()
            except ParseError:
                continue
        if not parser._ambiguous_unresolved_name:
            raise
        retry = ItaniumParser(mangled, builder, limits, options)
        retry._old_unresolved_names = True
        try:
            return retry.parse()
        except ParseError:
            raise error from None

parse_type

parse_type(
    mangled,
    builder,
    limits=DEFAULT_LIMITS,
    options=DEFAULT_OPTIONS,
)

Parse a bare <type> -- Pi, PKFvRiE -- rather than a whole symbol.

What __cxa_demangle reads and llvm-cxxfilt refuses, and what a typeinfo name or an RTTI descriptor carries. It is a separate entry point rather than a fallback inside parse for the reason llvm-cxxfilt refuses it there: a demangler offered every symbol in a binary and willing to read i as int will rename half a C library. Asked for deliberately, it is exactly what the caller wants.

Source code in src/demangle/schemes/itanium/parser.py
5004
5005
5006
5007
5008
5009
5010
5011
5012
5013
5014
5015
5016
5017
5018
5019
5020
5021
5022
def parse_type(mangled, builder, limits=DEFAULT_LIMITS, options=DEFAULT_OPTIONS):
    """Parse a bare `<type>` -- `Pi`, `PKFvRiE` -- rather than a whole symbol.

    What `__cxa_demangle` reads and `llvm-cxxfilt` refuses, and what a `typeinfo` name
    or an RTTI descriptor carries. It is a separate entry point rather than a fallback
    inside `parse` for the reason `llvm-cxxfilt` refuses it there: a demangler offered
    every symbol in a binary and willing to read `i` as `int` will rename half a C
    library. Asked for deliberately, it is exactly what the caller wants.
    """
    if not mangled:
        raise NotMangledError(mangled, "empty type")
    parser = ItaniumParser(mangled, builder, limits, options)
    parser._reject_unbound_parameters = True
    handle = parser.type_()
    if not parser.reader.eof:
        raise ParseError(mangled, parser.reader.pos, f"unconsumed input {parser.reader.remaining!r}")
    if builder.size(handle) > limits.max_output:
        raise LimitExceeded(mangled, "output length", limits.max_output)
    return handle

Options

demangle.schemes.itanium.options

Caller-selectable policy for Itanium output.

Some questions a demangler faces have no single right answer -- the two reference implementations answer them differently, and both are correct. Those belong here, as explicit options with a documented default, rather than being decided silently in the parser.

Frozen, so an options object can be shared between threads and used as a cache key.

DEFAULT_OPTIONS module-attribute

DEFAULT_OPTIONS = ItaniumOptions()

The llvm style's options: every field at its default.

GNU_OPTIONS module-attribute

GNU_OPTIONS = ItaniumOptions(
    expand_std_abbreviations=True,
    gnu_nullptr_spelling=True,
    gnu_closure_spelling=True,
    local_name_return_type=False,
    gnu_expression_spelling=True,
    symbolic_constraint_parameters=False,
    gnu_complex_spelling=True,
    gnu_angle_spacing=True,
    gnu_vector_spelling=True,
    gnu_special_name_spelling=True,
    gnu_entity_operand_spelling=True,
    gnu_default_argument_scope=True,
    gnu_friend_spelling=True,
    gnu_unresolved_scope_substitution=True,
    gnu_empty_pack_spelling=True,
    gnu_objc_protocol_spelling=True,
    gnu_exception_spec_first=True,
)

The gnu style's options: every field on which GNU c++filt differs, set its way.

ItaniumOptions dataclass

ItaniumOptions(
    expand_std_abbreviations=False,
    gnu_closure_spelling=False,
    gnu_expression_spelling=False,
    symbolic_constraint_parameters=True,
    undeduced_auto_substitution=None,
    closure_prefix_substitution=None,
    inherited_constructor_substitution=None,
    gnu_nullptr_spelling=False,
    gnu_angle_spacing=False,
    gnu_complex_spelling=False,
    gnu_vector_spelling=False,
    gnu_special_name_spelling=False,
    gnu_entity_operand_spelling=False,
    gnu_default_argument_scope=False,
    gnu_friend_spelling=False,
    gnu_unresolved_scope_substitution=False,
    gnu_empty_pack_spelling=False,
    gnu_exception_spec_first=False,
    gnu_objc_protocol_spelling=False,
    local_name_return_type=True,
)

Output policy for the Itanium demangler.

expand_std_abbreviations class-attribute instance-attribute
expand_std_abbreviations = False

Spell Ss as std::basic_string<char, std::char_traits<char>, ...>.

False (the default) gives llvm-cxxfilt's short form, std::string. True gives GNU c++filt's expansion. The types are identical; only the spelling differs.

gnu_closure_spelling class-attribute instance-attribute
gnu_closure_spelling = False

Spell lambdas and unnamed types the way GNU c++filt does.

False gives llvm-cxxfilt's 'lambda'(int) and 'unnamed'. True gives GNU's {lambda(int)#1} and {unnamed type#1}, which number from one rather than suffixing the raw index.

It also numbers a generic lambda's invented parameters. [](auto a, auto b) is mangled as references to parameters the closure never declared (ABI 5.1.8): llvm-cxxfilt spells every one of them auto, and GNU spells them auto:1 and auto:2 by index -- so Ul T0_ T_ E is {lambda(auto:2, auto:1)#1}. The number is the only thing that tells two of them apart.

It also names a constructor or destructor of a closure or unnamed type the way libiberty does, after the last source name it read: A::{unnamed type#1}::~A(), where llvm-cxxfilt writes A::'unnamed'::~() because the type has no name to repeat. ICU ships the shape in MicroProps::{unnamed type#1}::~MicroProps().

gnu_expression_spelling class-attribute instance-attribute
gnu_expression_spelling = False

Spell expressions inside types the way GNU c++filt does.

The two references diverge throughout expression syntax, and neither is more correct -- they are printing the same expression. GNU writes decltype ({parm#1}+{parm#1}) where LLVM writes decltype(fp + fp): a space after the keyword, no spaces around binary operators, function parameters numbered from one, and a call's callee parenthesised. A floating-point literal is the hex the name carries, in brackets after the type -- (double)[4048f5c28f5c28f6] -- where LLVM decodes it and prints 0x1.8f5c28f5c28f6p+5.

symbolic_constraint_parameters class-attribute instance-attribute
symbolic_constraint_parameters = True

Spell a template parameter inside a requires-clause by name rather than by value.

llvm-cxxfilt prints T where GNU c++filt substitutes the bound argument. The clause itself is never printed, but it contributes entries to the substitution table that the signature refers back to, so the choice is visible in the output.

A clause names parameters of enclosing templates, and not all of them are in scope -- which is the reason llvm-cxxfilt spells them symbolically in the first place. Where there is nothing to substitute, this falls back to that spelling rather than refusing the name: an option chooses how a name is spelled and must not change which names read at all.

undeduced_auto_substitution class-attribute instance-attribute
undeduced_auto_substitution = None

Count an undeduced auto -- Da, and decltype(auto), Dc -- as a substitution candidate, the way Apple's clang does.

Not a spelling but a numbering, and the one place two compilers number the same name differently. The ABI leaves builtin types out of the substitution table, and GCC and upstream clang leave auto out with them; Clang through 6.0 counted an undeduced auto by accident, -fclang-abi-compat=6 still does, and Apple's clang has kept that rule in every version since, so every S<n>_ after a deduced return type in a Mach-O symbol is one higher than GCC or upstream clang would write. Read by the wrong rule, such a name resolves its back-references to the wrong entries: of the 17,310 names carrying a Da in Homebrew's bottles of Boost, folly, protobuf, ceres and the rest, 6,385 come back as a plausible-looking wrong declaration and 2,300 refuse, under llvm-cxxfilt and c++filt alike.

None, the default, decides by the symbol's form: a name with the extra leading underscore a Mach-O symbol table carries, __Z..., is read by Apple's rule, and a bare _Z... by everyone else's -- and either way a name whose back-references run past the table under one rule is read again under the other, since that is the one rule known to move them. True or False forces a rule and skips the retry. Nothing in the name itself says which compiler wrote it, so a Mach-O name from upstream clang with a back-reference in range under both rules is read by Apple's; set False for those.

closure_prefix_substitution class-attribute instance-attribute
closure_prefix_substitution = None

Count a closure prefix -- the ns::g1 of ns::g1::'lambda'(...), a lambda in a variable's or a member's initialiser, written 2ns2g1M... -- as a substitution candidate, the way the ABI says and upstream clang and GCC 13 do.

The other numbering, and the other place two compilers number the same name differently. GCC through 12 wrote the M but never entered the prefix before it in the table (-fabi-version=17 and below; 18, GCC 13's default, fixed it), and Apple's clang never has: Homebrew's macOS bottles of Apache Arrow, DuckDB and RocksDB carry 663 names that read only with the prefix left out and none that read only with it in, where upstream clang 13 through 18 -- targeting Darwin included -- enter it. So every S<n>_ after the lambda's opening in such a name is one lower than the ABI says. Read by the ABI's rule, the back-references resolve one entry early, and what comes out is a lie every demangler tells: _ZNK2ns2g3MUlNS_3BoxIiEES1_E_clES1_S1_ is ns::g3::'lambda'(ns::Box<int>, ns::Box<int>)::operator()(ns::Box<int>, ns::Box<int>) const -- and llvm-cxxfilt 18, LLVM's main branch and c++filt 2.42 all print ns::Box bare, a template with no arguments, as the second parameter; RocksDB's [](const Endpoint&, const Endpoint&) comes back (rocksdb::Endpoint const&, rocksdb::Endpoint const). GCC 13 still emits the old spelling as an alias beside the new, so a binary built today can carry both.

None, the default, decides by the symbol's form, as undeduced_auto_substitution does: a name with the extra leading underscore a Mach-O symbol table carries, __Z..., is read by Apple's rule, and a bare _Z... by the ABI's -- and either way a name whose back-references then run past the table, or name something no type can be (the closure prefix itself, or a template with no arguments after it), is read again under the other rule. That catches every misnumbered name whose shifted references land on one of those; a name that reads either way is one the retry never touches. True or False forces a rule and skips the retry: False for an ELF binary known to be GCC 12's or older, True for a Mach-O one known to be upstream clang's, since either's shifted references may land on a type and read as a different type without tripping anything.

inherited_constructor_substitution class-attribute instance-attribute
inherited_constructor_substitution = None

Count the <base class type> an inheriting constructor names -- the 1C of CI2 1C -- as a substitution candidate.

The third place two compilers number the same name differently, and the one whose two sides each refuse the other's output. For

struct C { struct Kind { int v; }; C(Kind, Kind); };
struct D : C { using C::C; };

g++ 13.3 writes _ZN1DCI21CENS0_4KindES1_ -- S0_ is the base type's own entry -- and clang++ 18.1.3 writes _ZN1DCI21CEN1C4KindES1_, spelling C again because it has no entry for it. Each is unreadable under the other's rule: llvm-cxxfilt 18 refuses g++'s name outright, and g++'s rule reads clang's S1_ as C where the declaration says C::Kind.

None, the default, is clang's rule with a retry under g++'s, which is the way round that catches the most: a g++ name refers to the base's entry as the highest index in use at that point, so under clang's rule that reference runs past the table and the retry fixes it, while a clang name gives no such signal at all and has to be right the first time. True or False forces a rule and skips the retry. What neither can catch is a name whose shifted references all land on some other entry -- S1_ for a parameter's own type rather than for the base -- which reads as a different type without tripping anything, and is the same residual ambiguity closure_prefix_substitution has.

gnu_nullptr_spelling class-attribute instance-attribute
gnu_nullptr_spelling = False

Spell Dn as decltype(nullptr) rather than std::nullptr_t.

False gives llvm-cxxfilt's spelling. True gives GNU c++filt's, which writes the expression the type is defined as rather than the library typedef for it. Both name the same type.

gnu_angle_spacing class-attribute instance-attribute
gnu_angle_spacing = False

Separate two consecutive closing angle brackets with a space.

C++03 needed it -- A<B<int> >, because >> was the shift operator -- and GNU c++filt still writes it where llvm-cxxfilt does not. It belongs here as well as on the style's spelling builder because a template argument list spelled into text, inside an unresolved name or a vendor qualifier, never reaches the builder.

gnu_complex_spelling class-attribute instance-attribute
gnu_complex_spelling = False

Spell C99's _Complex and _Imaginary the way GNU c++filt does.

Both references write the qualifier after the type it qualifies -- double complex* is a pointer to a complex double, not a complex pointer -- and differ only in the word. False (the default) gives llvm-cxxfilt's double complex and double imaginary; True gives GNU c++filt's double _Complex and double _Imaginary, which are the keywords C99 actually spells.

gnu_vector_spelling class-attribute instance-attribute
gnu_vector_spelling = False

Spell a Dv vector type the way GNU c++filt does.

False (the default) gives llvm-cxxfilt's int vector[4]; True gives GNU c++filt's int __vector(4), which is how GCC's own __attribute__((vector_size)) diagnostics write it. The distinction matters more than most: SIMD code mangles Dv constantly, so this is not an obscure corner of the grammar.

gnu_special_name_spelling class-attribute instance-attribute
gnu_special_name_spelling = False

Spell the thread-local special names the way GNU c++filt does.

False gives llvm-cxxfilt's thread-local initialization routine for x and thread-local wrapper routine for x; True gives GNU's TLS init function for x and TLS wrapper function for x. They name the same entity. Only TH and TW differ: every other special name is spelled identically by both references. It also numbers a reference temporary by its seq-id, reference temporary #0 for f()::x, where llvm-cxxfilt writes reference temporary for f()::x.

gnu_entity_operand_spelling class-attribute instance-attribute
gnu_entity_operand_spelling = False

Spell an embedded <mangled-name> under a unary operator the way GNU does.

X ad L _Z... E E -- the address of a function, passed as a template argument -- is where nearly all of these appear. llvm-cxxfilt prints the whole declaration after the &: &llvm::sandboxir::SwitchInst::setCondition(llvm::sandboxir::Value*). GNU c++filt prints &llvm::sandboxir::SwitchInst::setCondition, which is what the source wrote, and falls back to bracketing the declaration -- &(A::f() const) -- for every shape that is not a bare qualified function name.

The rule, read off c++filt rather than guessed: the name alone for & of a function whose name is qualified and which carries no return type, no cv- or ref-qualifier and no requires-clause; brackets around anything else that is not a plain data name. So &A::f and &std::f and &A::~A, but &(f()) for an unqualified one, &(void A::f<int>()) for a template, &(f()::x) for a local entity and &(vtable for A) for a special name.

A call is the other place one appears, decltype(h(t)) with h resolved -- clL_Z1hiEfp_E. libiberty prints the callee through its name alone, "function call used in an expression should not have printed types of the function arguments", so c++filt writes h({parm#1}) and A::s({parm#1}) where llvm-cxxfilt writes h(int)(fp). The name is an operand and bracketed unless it is a plain one: (h<int>)({parm#1}), (A::s const)({parm#1}), (h()::x)({parm#1}).

gnu_default_argument_scope class-attribute instance-attribute
gnu_default_argument_scope = False

Name the default argument a local entity was declared in.

Z <encoding> Ed [<number>] _ <entity> says the entity -- usually a lambda -- was written in a default argument rather than in the function body. GNU c++filt spells that scope, f(int, double)::{default arg#1}::x; llvm-cxxfilt drops it and prints f(int, double)::x, which is the same name for two different entities if a function has one lambda in its body and another in a default argument.

gnu_friend_spelling class-attribute instance-attribute
gnu_friend_spelling = False

Mark a friend declared inside its class the way GNU c++filt does.

<unqualified-name> ::= F <name> says the function was declared inside the class it is a friend of. False (the default) gives llvm-cxxfilt's A::friend f(), the word before the name; True gives GNU c++filt's A::f[friend](), a bracketed suffix after the name and its ABI tags but before its template arguments -- A::f[friend]<int>.

gnu_unresolved_scope_substitution class-attribute instance-attribute
gnu_unresolved_scope_substitution = False

Record the scope of an srN unresolved name as a substitution, the way g++ does.

srN T_ 3foo E 1v is T::foo::v. The ABI says the qualifier levels of an unresolved name are not substitution candidates, Clang writes names that way, and llvm-cxxfilt reads them that way: T is recorded, T::foo is not. g++ mangles the scope as a nested-name type and records it, so the parameter after decltype(T::foo::v + 1) in _Z2e1I1QEDTplsrNT_3fooE1vLi1EES1_S2_ is S2_, Q::foo, where Clang writes it out again as NS1_3fooE. GNU c++filt reads every srN as a type and so numbers as g++ does.

Neither reader can be right for both compilers: a back-reference written after the scope counts one entry more under g++ than under Clang. False (the default) reads as the ABI and llvm-cxxfilt do, and takes S2_ in the g++ name above to be the decltype itself. True reads as c++filt does.

gnu_empty_pack_spelling class-attribute instance-attribute
gnu_empty_pack_spelling = False

Print an expansion over an empty pack as an empty entry, the way GNU c++filt does.

std::thread::thread<F, Args...> instantiated with no arguments is _ZNSt6threadC1IZ4mainEUlvE_JEvEEOT_DpOT0_, and every program that starts a thread on a no-argument callable carries it. llvm-cxxfilt drops the empty pack from the list, thread<main::'lambda'(), void>; c++filt prints what it expands to, which is nothing, and keeps the comma: thread<main::{lambda()#1}, , void>. It does the same in a parameter list, f(, int), and in a call's arguments, g(, int) -- every comma-separated list -- and drops the empty entries at the end of a list, so f<int, JE> is f<int> there as here.

False (the default) drops every one, as llvm-cxxfilt does. True keeps c++filt's.

gnu_exception_spec_first class-attribute instance-attribute
gnu_exception_spec_first = False

Write a function type's exception specification before its qualifiers.

M1AKDoFvvRE is void (A::*)() const & noexcept to llvm-cxxfilt and void (A::*)() noexcept const & to GNU c++filt, which prints the specification -- noexcept, noexcept(...), throw(...), transaction_safe -- first. Neither order is the one C++ declares them in for a member function. False (the default) gives llvm-cxxfilt's, True c++filt's.

gnu_objc_protocol_spelling class-attribute instance-attribute
gnu_objc_protocol_spelling = False

Spell an Objective-C protocol qualifier as the vendor qualifier it is written as.

PU13objcproto3Bar11objc_object is id<Bar> to llvm-cxxfilt, and Foo<Bar>* for a class; GNU c++filt knows nothing of the convention and prints the qualifier as it prints any vendor qualifier, after the type: objc_object objcproto3Bar* and Foo objcproto3Bar*. False (the default) gives the former, True the latter.

local_name_return_type class-attribute instance-attribute
local_name_return_type = True

Show the return type of the function enclosing a local name.

For Z <function encoding> E <entity>, llvm-cxxfilt prints the enclosing template function's return type and GNU c++filt does not. True (the default) follows LLVM.

Substitutions

demangle.schemes.itanium.substitutions

Back-reference tables for the Itanium ABI.

The scheme compresses a name by letting any component that has already appeared be written back as S_, S0_, S1_. Nothing in the name says what those indices refer to: the numbering is implicit, determined entirely by which components the encoder considered substitutable and the order it met them. Get the candidate set wrong by one entry and every later back-reference in that name resolves to the wrong component -- usually producing output that still looks like a plausible C++ name, which is the worst kind of wrong.

So this is not a cache. It is a load-bearing part of the grammar, and it lives in its own module, with the specification quoted at each decision, so it can be tested directly rather than only through whole-name round trips.

Reference: Itanium C++ ABI section 5.1.10, "Compression".

SubstitutionOverrun

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

Bases: ParseError

A S<n>_ past the end of the table: the name was numbered by a rule this reading did not apply, or it is not a name. parse retries the one rule that is known to differ between compilers before giving up; see ItaniumOptions.undeduced_auto_substitution.

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}")

SubstitutionMisuse

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

Bases: ParseError

A S<n>_ naming an entry that cannot stand where it was read: a closure prefix, or a template name with no arguments after it, used as a type. Neither is a type -- the one is a variable's name and the other a template's -- so the name was numbered by a rule this reading did not apply, or it is not a name. parse retries the one rule known to move a closure prefix's neighbours before giving up; see ItaniumOptions.closure_prefix_substitution.

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}")

ParameterReference

ParameterReference(index, level=0, symbolic=None)

A recorded <template-param>, kept as the reference it is rather than resolved.

A <template-param> is a substitution candidate in its own right (5.1.10), and the entry it contributes is the parameter, not the argument bound to it at the moment it was written. The two differ whenever the back-reference is read under a different template scope, which happens in any name that mentions a local entity: the signature of the enclosing function is written against that function's parameters, so a T_ inside it enters the table, and a later S<n>_ naming that entry belongs to the outer template.

Settled against the mangler rather than a demangler. For

template <class T> void legalize(Update<T>*);
template <class I, class C> void insort(I, I, C);

with insort<Update<BB*>*, Wrap<lambda-in-legalize>> instantiated from inside legalize<BB*>, g++ 13.3 emits ...EEvS8_S8_T0_, where S8_ is the entry the T_ inside legalize's signature contributed. The parameters of insort are of type I -- nn::Update<nn::BB*>* -- and GNU c++filt prints exactly that. Resolving the entry to what T_ meant where it was recorded gives nn::BB* instead: a different type, spelled plausibly, which is the failure this package exists to avoid. llvm-cxxfilt 18 has that bug, and differs from this on 322 of the 217,730 distinct Itanium symbols in the shared libraries of a stock Ubuntu 24.04.

The level is carried as well as the index, because TL<k>_<n>_ names a parameter of an enclosing template and the entry stands for that parameter, not for level 0's.

Held opaque by the table -- it is the parser that owns the template scope, so it is the parser that turns one of these back into a handle.

Source code in src/demangle/schemes/itanium/substitutions.py
90
91
92
93
94
95
def __init__(self, index, level=0, symbolic=None):
    self.index = index
    self.level = level
    #: Mangled text of a parameter read inside a requires-clause whose scope may be
    #: gone when a later `S_` names it; None outside a clause.
    self.symbolic = symbolic

DeferredProduction

DeferredProduction(start, end)

A recorded component built over a <template-param>, kept as its input span.

ParameterReference covers the bare parameter. This covers everything wrapped around one -- R T_, P N S1_ I T_ E E, a template applied to it -- which is scope-dependent for exactly the same reason and which the mangler reuses across scopes for exactly the same reason: it canonicalises a template type parameter by level and index, so the composite over one canonicalises the same way.

From insort(Update<I>*, Update<I>*, C) instantiated inside legalize<T>, g++ 13.3 emits parameters written SA_, the entry P N S1_ I T_ E E contributed inside legalize's own signature. Under insort that is nn::Update<nn::Update<nn::BB*>*>*, which is what the declaration says and what GNU c++filt prints; frozen where it was recorded it is nn::Update<nn::BB*>*, which is a different type.

There is nothing to freeze or to defer in the handle itself, because a handle is already-built output. What is kept instead is where the production was written, so the parser can read it again under the scope now in force. The span is a complete <type>, and a <type> can only back-reference entries recorded before it, so re-reading one terminates: each step moves strictly earlier in the table.

Source code in src/demangle/schemes/itanium/substitutions.py
126
127
128
def __init__(self, start, end):
    self.start = start
    self.end = end

SubstitutionTable

SubstitutionTable(mangled, limit=8192)

The S_ dictionary for one mangled name.

One instance per parse. Most entries hold builder handles, so what is stored depends on which builder is driving -- the table itself is deliberately ignorant of that. Two kinds of entry are not handles at all: ParameterReference and DeferredProduction, which stand for a component whose spelling depends on the template scope it is read under rather than the one it was written under. The table holds them opaquely; it is the parser that owns the scope, so it is the parser that turns one back into a handle.

On not deduplicating

The specification says "No entity is added to the dictionary twice", which reads like an instruction to deduplicate. For a demangler it is the opposite.

The encoder only spells a component out in full when it has not seen it before; a repeat is written as a back-reference. So a well-formed name never spells the same component twice, and every spelled-out component we meet is one the encoder had just added. Appending unconditionally reproduces its numbering exactly.

Deduplicating on our side would actively break names the specification calls out: "The type of a non-static member function is considered to be different, for the purposes of substitution, from the type of a namespace-scope or static member function whose type appears similar." Those two spell identically and both get entries. Keyed on spelling, a deduplicating table would collapse them and shift every subsequent index.

Source code in src/demangle/schemes/itanium/substitutions.py
165
166
167
168
169
170
171
def __init__(self, mangled, limit=8192):
    self._entries = []
    self._limit = limit
    self._mangled = mangled
    #: False while re-reading a `DeferredProduction`, whose entries already exist;
    #: adding them again would renumber the table.
    self.recording = True
last property
last

The entry most recently recorded, or None.

remember
remember(handle, production='type')

Record a substitutable component and return it unchanged.

production names the grammar non-terminal being recorded. It is checked against the specification's candidate set, so a parser that records something the ABI does not fails loudly here instead of silently renumbering the table.

Source code in src/demangle/schemes/itanium/substitutions.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def remember(self, handle, production="type"):
    """Record a substitutable component and return it unchanged.

    `production` names the grammar non-terminal being recorded. It is checked
    against the specification's candidate set, so a parser that records something
    the ABI does not fails loudly here instead of silently renumbering the table.
    """
    if production != "type" and production != "prefix" and production not in CANDIDATE_PRODUCTIONS:
        raise AssertionError(
            f"{production!r} is not a substitution candidate under ABI 5.1.10; "
            f"candidates are {sorted(CANDIDATE_PRODUCTIONS)}"
        )
    if self.recording:
        entries = self._entries
        if len(entries) >= self._limit:
            raise LimitExceeded(self._mangled, "substitution", self._limit)
        entries.append(handle)
    return handle
defer_last
defer_last(start, end)

Replace the entry just recorded with the span it was written at.

Called by the production itself, once it knows a template parameter was resolved while it ran. Nothing is added or removed, so numbering is untouched -- only what the entry is changes.

Source code in src/demangle/schemes/itanium/substitutions.py
192
193
194
195
196
197
198
199
200
def defer_last(self, start, end):
    """Replace the entry just recorded with the span it was written at.

    Called by the production itself, once it knows a template parameter was resolved
    while it ran. Nothing is added or removed, so numbering is untouched -- only what
    the entry *is* changes.
    """
    if self._entries:
        self._entries[-1] = DeferredProduction(start, end)
drop_last
drop_last(mark)

Forget the entry a production just recorded, if it recorded one.

mark is mark() taken before the production ran, so nothing is dropped where nothing was added. The one caller is an inheriting constructor's base class type under the numbering clang uses, which reads that type without entering it while still entering the components inside it. See ItaniumOptions.inherited_constructor_substitution.

Source code in src/demangle/schemes/itanium/substitutions.py
202
203
204
205
206
207
208
209
210
211
212
def drop_last(self, mark):
    """Forget the entry a production just recorded, if it recorded one.

    `mark` is `mark()` taken before the production ran, so nothing is dropped where
    nothing was added. The one caller is an inheriting constructor's base class type
    under the numbering clang uses, which reads that type without entering it while
    still entering the components inside it. See
    `ItaniumOptions.inherited_constructor_substitution`.
    """
    if len(self._entries) > mark:
        del self._entries[-1]
mark
mark()

How many entries there are, for rewind.

Source code in src/demangle/schemes/itanium/substitutions.py
219
220
221
def mark(self):
    """How many entries there are, for `rewind`."""
    return len(self._entries)
capture
capture(mark)

Everything recorded since mark, for restore_from.

Source code in src/demangle/schemes/itanium/substitutions.py
223
224
225
def capture(self, mark):
    """Everything recorded since `mark`, for `restore_from`."""
    return self._entries[mark:]
restore_from
restore_from(mark, entries)

Put back exactly what capture took, discarding whatever replaced it.

Used to read one span of a name twice. A conversion operator's type is written before the template arguments that bind its parameters, so it cannot be spelled until they have been read -- and the second reading must leave the table exactly as the first did, or every later back-reference in the name shifts.

Source code in src/demangle/schemes/itanium/substitutions.py
227
228
229
230
231
232
233
234
235
def restore_from(self, mark, entries):
    """Put back exactly what `capture` took, discarding whatever replaced it.

    Used to read one span of a name twice. A conversion operator's type is written
    before the template arguments that bind its parameters, so it cannot be spelled
    until they have been read -- and the second reading must leave the table exactly
    as the first did, or every later back-reference in the name shifts.
    """
    self._entries[mark:] = entries
lookup
lookup(index)

Resolve S<index>_.

Source code in src/demangle/schemes/itanium/substitutions.py
237
238
239
240
241
242
243
def lookup(self, index):
    """Resolve `S<index>_`."""
    if index < 0 or index >= len(self._entries):
        raise SubstitutionOverrun(
            self._mangled, None, f"substitution S{index}_ refers past the {len(self._entries)} known entries"
        )
    return self._entries[index]

TemplateArgumentTable

TemplateArgumentTable()

The T_ dictionary: a stack of the parameter lists currently in scope.

Separate from SubstitutionTable because it is a separate mechanism with separate numbering -- T_ indexes the enclosing template's argument list, S_ indexes every substitutable component seen so far. Conflating them is a classic source of wrong output, so they do not share a type.

A stack rather than one list, because <template-param> names a level as well as an index. T_ and T<n>_ mean level 0, the innermost enclosing <template-args>. TL<k>_<n>_ means level k + 1, which is a list a generic lambda or a template template parameter declared -- and those nest, so a name can reach past one to the one outside it:

[]<typename $T, template<typename $T0, $T $N> typename $TT>(...)

Here $T is level 0 index 0 and $T0 is level 1 index 0, and the $N declaration reaches both. Held flat, the two levels would overwrite each other and the parameter would be spelled T, which names nothing.

A level with nothing in it is still a level: a generic lambda that declared no parameters occupies one, because its auto parameters are numbered against it and a TL reference from inside it counts through it.

Scopes nest in the other direction too -- a local entity has parameters of its own while the enclosing ones stay out of reach -- so snapshot() and restore() save and put back the whole stack.

Source code in src/demangle/schemes/itanium/substitutions.py
288
289
290
291
292
def __init__(self):
    self._levels = []
    #: Bumped on every change, so the parser can memoise a `DeferredProduction` per
    #: scope; re-reading it per back-reference is quadratic.
    self.generation = 0
install
install()

Begin a fresh level 0. These arguments replace every level in scope.

Source code in src/demangle/schemes/itanium/substitutions.py
296
297
298
299
def install(self):
    """Begin a fresh level 0. These arguments replace every level in scope."""
    self._levels = [[]]
    self.generation += 1
add
add(handle)

Bind the next argument of level 0, as an argument list is read.

Source code in src/demangle/schemes/itanium/substitutions.py
301
302
303
304
305
306
307
def add(self, handle):
    """Bind the next argument of level 0, as an argument list is read."""
    if not self._levels:
        self._levels = [[]]
    self._levels[0].append(handle)
    self.generation += 1
    return handle
outer
outer()

Level 0's bindings, for a caller that has to look at all of them at once.

Source code in src/demangle/schemes/itanium/substitutions.py
309
310
311
def outer(self):
    """Level 0's bindings, for a caller that has to look at all of them at once."""
    return self._levels[0] if self._levels else ()
push
push(declared)

Enter a nested level, which fills up as its declarations are read.

Source code in src/demangle/schemes/itanium/substitutions.py
315
316
317
318
def push(self, declared):
    """Enter a nested level, which fills up as its declarations are read."""
    self._levels.append(declared)
    self.generation += 1
clear
clear()

Drop every level. A lambda that is the entity being named starts fresh.

Source code in src/demangle/schemes/itanium/substitutions.py
327
328
329
330
def clear(self):
    """Drop every level. A lambda that *is* the entity being named starts fresh."""
    self._levels = []
    self.generation += 1
lookup
lookup(index, level=0)

Resolve TL<level>_<index>_, or None when the binding is not in scope.

Returning None rather than raising is deliberate. A generic lambda's auto parameter is mangled as a reference to a parameter it never declared (ABI 5.1.8), so a miss is a normal reading and the parser decides what it means.

Source code in src/demangle/schemes/itanium/substitutions.py
332
333
334
335
336
337
338
339
340
341
342
343
344
def lookup(self, index, level=0):
    """Resolve `TL<level>_<index>_`, or None when the binding is not in scope.

    Returning None rather than raising is deliberate. A generic lambda's `auto`
    parameter is mangled as a reference to a parameter it never declared (ABI
    5.1.8), so a miss is a normal reading and the parser decides what it means.
    """
    if not 0 <= level < len(self._levels):
        return None
    params = self._levels[level]
    if not 0 <= index < len(params):
        return None
    return params[index]

Parser

demangle.schemes.itanium.parser

A recursive-descent parser for the Itanium C++ ABI mangling scheme.

This is the scheme GCC and Clang emit, and by extension almost every C++ toolchain outside the Microsoft ecosystem. It is also used, in its older form, by Rust's legacy mangling.

The parser is written against core.builder.Builder: it recognises grammar productions and reports them, never constructing output itself. See ARCHITECTURE.md for why.

Structure of this file mirrors the specification's own ordering -- names, then types, then template arguments, then expressions -- so a production can be found by its section number. Grammar comments quote the ABI verbatim.

Reference: Itanium C++ ABI section 5.1, https://itanium-cxx-abi.github.io/cxx-abi/abi.html A transcription of the productions is vendored at docs/specs/itanium-grammar.txt.

Microsoft Visual C++

demangle.schemes.msvc

Microsoft's decorated name scheme.

The format UnDecorateSymbolName reverses, emitted by MSVC and by clang-cl. Microsoft publishes no complete grammar for it, so this implementation follows llvm-undname and is validated against LLVM's own demangler corpus; the reference output is recorded next to every name in tests/conformance.

Structured output

This parser builds its own nodes rather than writing to a builder. It cannot: MSVC spells a declaration differently enough that the shared spelling builder has nowhere to put a calling convention, and the alternative to a scheme-specific renderer would be a scheme-specific branch in core.

What it does instead is build its own tree, of nodes that are core.ast.Nodes, and spell that (see nodes.py). So parse() hands back a real tree to walk and demangle() hands back text, exactly as they do for the other schemes -- the seam is which of the two this module produces, rather than which builder the parser wrote to.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="msvc",
    node_kinds=(
        "array",
        "declaration",
        "function",
        "indirection",
        "name",
        "pointer_auth",
        "raw",
    ),
    detect=detect,
    parse=parse,
    parse_type=parse_type,
    description="Microsoft Visual C++ decorated names (MSVC, clang-cl)",
    options_type=MsvcOptions,
    aliases=("microsoft", "ms", "vc"),
    first_characters="?.",
    priority=100,
)

The scheme as the registry holds it, registered when this package is imported.

detect

detect(name)

A decorated name opens with ?; a type descriptor's name opens with ..

A symbol table is full of other names that open with a dot -- .text, .L1234, .refptr.foo, .constprop.0, .ARM.exidx, .CRT$XCU -- so the . form is claimed only where what follows it opens the way a whole type encoding does; see _opens_a_type. Claiming is still not reading: the type has to parse to the last character before anything is said about it.

Source code in src/demangle/schemes/msvc/__init__.py
 98
 99
100
101
102
103
104
105
106
107
def detect(name):
    """A decorated name opens with `?`; a type descriptor's name opens with `.`.

    A symbol table is full of other names that open with a dot -- `.text`, `.L1234`,
    `.refptr.foo`, `.constprop.0`, `.ARM.exidx`, `.CRT$XCU` -- so the `.` form is claimed
    only where what follows it opens the way a whole type encoding does; see
    `_opens_a_type`. Claiming is still not reading: the type has to parse to the last
    character before anything is said about it.
    """
    return bool(name) and (name[0] == "?" or (name[0] == "." and _opens_a_type(name)))

parse

parse(
    mangled,
    builder,
    limits=DEFAULT_LIMITS,
    options=DEFAULT_OPTIONS,
)

Parse an MSVC decorated name into builder.

Source code in src/demangle/schemes/msvc/__init__.py
181
182
183
184
185
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=DEFAULT_OPTIONS):
    """Parse an MSVC decorated name into `builder`."""
    options = options or DEFAULT_OPTIONS
    if not detect(mangled):
        raise NotMangledError(mangled, "not an MSVC decorated name")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)
    # Refused rather than copied into the output and on into a caller's report.
    if _CONTROL_CHARACTER.search(mangled) is not None:
        raise ParseError(mangled, None, "decorated name contains a control character")
    if mangled.startswith(_TYPE_DESCRIPTOR_NAME):
        # Not a declaration: the reference spells the type and appends what it is.
        try:
            tree = parse_msvc_type(mangled, limits, options)
            if tree is None:
                raise ParseError(mangled, None, "not a type descriptor name this demangler can read")
            # The marker goes where a declarator goes:
            # `int (*`RTTI Type Descriptor Name')[2]`.
            spelled = _render(tree, _TYPE_DESCRIPTOR_SUFFIX, options=options)
        except _LimitHit as hit:
            raise LimitExceeded(mangled, hit.what, hit.limit) from hit
        _check_length(mangled, len(spelled), limits)
        return builder.raw(spelled)
    # ARM64EC, as LLVM's `getArm64ECDemangledFunctionName`: a fallback, since a name
    # that already reads is not one to rewrite.
    plain_error = None
    try:
        if mangled.startswith(_MD5_PREFIX):
            # A hashed name carries no recoverable spelling, so it is its own expansion.
            hashed = _md5_name(mangled)
            if hashed is None:
                raise ParseError(mangled, None, "unterminated MD5-hashed name")
            _check_length(mangled, len(hashed), limits)
            return builder.raw(hashed)
        try:
            if _wants_structure(builder):
                tree = parse_msvc_symbol_strict(mangled, limits, options)
                if tree is None:
                    raise ParseError(mangled, None, "not a decorated name this demangler can read")
                # No output check: `_Demangler.rendered` has already bounded it.
                return tree
            tree = parse_msvc_symbol_strict(mangled, limits, options)
            if tree is None:
                raise ParseError(mangled, None, "not a decorated name this demangler can read")
            expanded = _render(tree, options=options)
            _check_length(mangled, len(expanded), limits)
            return builder.raw(expanded)
        except _LimitHit as hit:
            # `hit.limit`, not the caller's: this scheme narrows both bounds (see
            # `_LimitHit`).
            raise LimitExceeded(mangled, hit.what, hit.limit) from hit
    except ParseError as exc:
        plain_error = exc
    hybrid = _without_hybrid_marker(mangled)
    if hybrid is not None:
        if _HYBRID_MARKER in hybrid:
            # LLVM removes only the first marker; two is not something it can produce.
            raise ParseError(mangled, None, "more than one ARM64EC marker")
        return parse(hybrid, builder, limits, options)
    assert plain_error is not None
    raise plain_error

parse_type

parse_type(
    mangled,
    builder,
    limits=DEFAULT_LIMITS,
    options=DEFAULT_OPTIONS,
)

Parse a bare type encoding -- PEAX, PEAVFoo@@ -- rather than a whole symbol.

What an RTTI type descriptor carries, and what UnDecorateSymbolName's UNDNAME_TYPE_ONLY asks for. A leading . is accepted and dropped, because that is how the descriptor's own symbol spells it.

None of this is reachable from parse, and deliberately: a type encoding opens with no ?, so detect cannot see one coming and a demangler that guessed would read plain C symbols as types. It is readable only because the caller named the scheme.

Source code in src/demangle/schemes/msvc/__init__.py
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
def parse_type(mangled, builder, limits=DEFAULT_LIMITS, options=DEFAULT_OPTIONS):
    """Parse a bare *type* encoding -- `PEAX`, `PEAVFoo@@` -- rather than a whole symbol.

    What an RTTI type descriptor carries, and what `UnDecorateSymbolName`'s
    `UNDNAME_TYPE_ONLY` asks for. A leading `.` is accepted and dropped, because that is
    how the descriptor's own symbol spells it.

    None of this is reachable from `parse`, and deliberately: a type encoding opens with
    no `?`, so `detect` cannot see one coming and a demangler that guessed would read
    plain C symbols as types. It is readable only because the caller named the scheme.
    """
    if not mangled:
        raise NotMangledError(mangled, "empty type")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)
    try:
        tree = parse_msvc_type(mangled, limits, options)
        if tree is None:
            raise ParseError(mangled, None, "not a type this demangler can read")
        if _wants_structure(builder):
            return tree
        expanded = _render(tree, options=options)
    except _LimitHit as hit:
        raise LimitExceeded(mangled, hit.what, hit.limit) from hit
    _check_length(mangled, len(expanded), limits)
    return builder.raw(expanded)

Options

demangle.schemes.msvc.options

What to leave out of an MSVC spelling.

A decorated name expands to a great deal more than the name: public: virtual int __cdecl C::f(int) const is one identifier and five pieces of declaration around it. An analyst grepping a binary usually wants fewer of them, which is why UnDecorateSymbolName takes a mask and llvm-undname takes flags.

Both references are represented here, and which one defines a field decides how far the field reaches -- because the two disagree about that, measurably, and neither is wrong.

The five llvm-undname flags are declaration-level. They apply to the declaration and to the types written inside it, and stop at the edge of the symbol: a symbol naming a scope keeps its full spelling, and a function reached as a pointer's pointee keeps the convention the pointer prints for it. So --no-calling-convention over int (__cdecl * __cdecl fn(void))(int) drops the convention fn itself carries and keeps the one belonging to the function pointer it returns.

The four UnDecorateSymbolName flags are lexical. They reach every occurrence of what they name, wherever it stands -- inside a template argument, inside a parameter of function-pointer type, and inside the enclosing symbol a local name is scoped by. ms_keywords over the same name drops both conventions.

True keeps the piece, which is what either reference prints by default.

tests/conformance/msvc-suppressions.txt scores the first five against llvm-undname, over the 609 names of msvc-llvm-corpus.txt. tests/conformance/msvc-dbghelp.txt scores the last four against dbghelp.dll, over the 478 of those names the two references already spell alike unflagged -- the rest are left out rather than reconciled, because the two spell a name differently and a corpus cannot ask for both houses at once.

DEFAULT_OPTIONS module-attribute

DEFAULT_OPTIONS = MsvcOptions()

What both shipped styles use: every piece kept, as both references print by default.

MsvcOptions dataclass

MsvcOptions(
    calling_convention=True,
    access_specifier=True,
    member_type=True,
    return_type=True,
    variable_type=True,
    ms_keywords=True,
    leading_underscores=True,
    this_type=True,
    tag_kind=True,
)

Which parts of a decorated name's expansion to print.

calling_convention class-attribute instance-attribute
calling_convention = True

__cdecl, __stdcall, __thiscall. llvm-undname --no-calling-convention.

access_specifier class-attribute instance-attribute
access_specifier = True

public:, protected:, private:. llvm-undname --no-access-specifier.

member_type class-attribute instance-attribute
member_type = True

static and virtual. llvm-undname --no-member-type.

return_type class-attribute instance-attribute
return_type = True

The type before the name. llvm-undname --no-return-type.

Not the same as dropping a word: a return type wraps around the declarator in C, so suppressing it means spelling the declaration the way a constructor is spelled -- convention, name, parameters -- rather than cutting a prefix off the text.

variable_type class-attribute instance-attribute
variable_type = True

The type of a data symbol. llvm-undname --no-variable-type.

Only a variable's: a function keeps its return type, and so do the vtable and RTTI names, which are not variables however much they look like one.

ms_keywords class-attribute instance-attribute
ms_keywords = True

Every Microsoft keyword. UnDecorateSymbolName's UNDNAME_NO_MS_KEYWORDS.

The calling convention, __restrict and __unaligned -- and __ptr64, which this library never prints because llvm-undname does not. Not the same cut as calling_convention: that one is the declaration's own convention, this one is every convention in the spelling, so a function-pointer parameter loses its own too.

__int64 stays. It names a type rather than qualifying one, and the reference keeps it.

leading_underscores class-attribute instance-attribute
leading_underscores = True

The underscores on those keywords. UNDNAME_NO_LEADING_UNDERSCORES.

__cdecl becomes cdecl and __restrict becomes restrict, over the same words ms_keywords drops entirely. What comes out is not C++ -- it is what a linker map from the 16-bit era spelled -- and it is here because the mask has it.

LLVM's __attribute__((__swiftcall__)) is left alone. The reference spells that convention __swift_1 and strips it to swift_1; it has no answer for LLVM's spelling, and stripping the underscores off __attribute__ would invent one.

this_type class-attribute instance-attribute
this_type = True

What a member function writes after its parameter list. UNDNAME_NO_THISTYPE.

The whole group, not just the cv: const, volatile, __restrict, __unaligned and the &/&& ref-qualifier all describe the implicit this, and the reference drops them together.

tag_kind class-attribute instance-attribute
tag_kind = True

class, struct, union, enum before a tag name. UNDNAME_NO_ECSU.

What C++ calls an elaborated type specifier. MSVC writes one on every user-defined type it mangles, so a spelling full of them reads as struct S const * where the source said S const *.

for_a_scope
for_a_scope()

These options as they apply to a symbol that names a scope.

?1??f@@YAXXZ@ says "inside the second scope of void __cdecl f(void)", and the two references disagree about whether a flag reaches that inner symbol. llvm-undname applies its five to the symbol being named and not to the ones saying where it lives; UnDecorateSymbolName applies its four to the text throughout. Each flag follows the reference that defines it, so what a scope keeps is the five and what it loses is the four.

Source code in src/demangle/schemes/msvc/options.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def for_a_scope(self):
    """These options as they apply to a symbol that names a *scope*.

    `?1??f@@YAXXZ@` says "inside the second scope of `void __cdecl f(void)`", and the
    two references disagree about whether a flag reaches that inner symbol.
    `llvm-undname` applies its five to the symbol being named and not to the ones
    saying where it lives; `UnDecorateSymbolName` applies its four to the text
    throughout. Each flag follows the reference that defines it, so what a scope
    keeps is the five and what it loses is the four.
    """
    if self.ms_keywords and self.leading_underscores and self.this_type and self.tag_kind:
        return DEFAULT_OPTIONS
    return MsvcOptions(
        ms_keywords=self.ms_keywords,
        leading_underscores=self.leading_underscores,
        this_type=self.this_type,
        tag_kind=self.tag_kind,
    )
keyword
keyword(word)

word as this spelling wants it: as written, without underscores, or not at all.

The one place a Microsoft keyword becomes text, so that ms_keywords and leading_underscores are applied in a single spot rather than at each of the places the parser reads one.

Source code in src/demangle/schemes/msvc/options.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def keyword(self, word):
    """`word` as this spelling wants it: as written, without underscores, or not at all.

    The one place a Microsoft keyword becomes text, so that `ms_keywords` and
    `leading_underscores` are applied in a single spot rather than at each of the
    places the parser reads one.
    """
    if not word:
        return word
    if not self.ms_keywords:
        return ""
    if self.leading_underscores or not word.startswith("__") or word.startswith("__attribute__"):
        return word
    return word.lstrip("_")

Nodes

demangle.schemes.msvc.nodes

The MSVC type tree, and the declarator spelling that belongs to it.

core/spelling.py places a declarator the way a C declaration wants it, and for the Itanium schemes that is the whole story. It is not the whole story here. MSVC writes a calling convention inside the parentheses of a function pointer -- int (__cdecl *)(void) -- doubles the space after a convention spelled as an attribute, carries __unaligned along with const and volatile through every indirection, and still leaves room for a convention that spells as nothing at all: int ( *)(). None of that can be said through the shared builder without teaching core about one scheme, so the placement rules live here, next to the nodes they walk.

The nodes are otherwise ordinary core.ast.Nodes, so .walk(), .find(kind) and .spell() work as they do for any other scheme. Where a core node already means exactly what this scheme means -- a fixed piece of text, an array, an identifier -- it is used rather than copied. render below spells those the MSVC way as part of a declaration; asking one of them for its own spelling in isolation goes through the shared builder, which writes int [2] where this scheme writes int[2]. Nothing that is spelled as a whole symbol takes that path.

Array extents nest outermost first, so int[2][3] is an array of two arrays of three and each node's dimension means what it says on its own.

What the tree does not take apart is a name. Namespaces, template arguments and the class a member belongs to arrive here already spelled, because the mangling resolves its back-references against text and the parser has to render each one as it goes to number the next correctly. So the structure recovered is the declaration's shape -- what it declares, its indirections, its parameter list, its return type -- over leaves that are text.

Indirection

Indirection(sigil, qualifiers, inner, pointer_auth=None)

Bases: _Spelled

A pointer, a reference, or a pointer into a class.

sigil is *, &, &&, or Owner::* for a pointer to member -- the owner is part of the declarator in MSVC's spelling, not a type of its own. qualifiers are the indirection's own, the ones written to its right in char *const; what it points at carries its own.

Source code in src/demangle/schemes/msvc/nodes.py
105
106
107
108
109
def __init__(self, sigil, qualifiers, inner, pointer_auth=None):
    self.sigil = sigil
    self.qualifiers = tuple(qualifiers)
    self.inner = inner
    self.pointer_auth = pointer_auth
points_into_class property
points_into_class

Whether this is a pointer to member rather than an ordinary pointer.

FunctionType

FunctionType(
    convention, parameters, returns=None, member_cv=""
)

Bases: _Spelled

A function type: its calling convention, parameters, return type and member cv.

The declarator is deliberately absent. MSVC interleaves it with the convention -- int (__cdecl *p)(void) puts the pointer between them -- so the name a function declares belongs to the Declaration around this node, and render threads it through rather than storing it here.

returns is None for the forms the scheme writes no return type for: a constructor, a destructor, an operator that leaves the slot empty. member_cv is what trails the parameter list of a member function, already spelled with its leading space.

Source code in src/demangle/schemes/msvc/nodes.py
140
141
142
143
144
def __init__(self, convention, parameters, returns=None, member_cv=""):
    self.convention = convention
    self.parameters = tuple(parameters)
    self.returns = returns
    self.member_cv = member_cv

Declaration

Declaration(
    prefix,
    declarator,
    type_,
    suffix="",
    access="",
    member_type="",
)

Bases: _Spelled

A whole symbol: what it declares, and what MSVC writes around it.

What the spelling opens with is three pieces rather than one, because each can be left out on its own: prefix is what comes before everything ("[thunk]: ", an extern "C"), access is "public: " and its siblings, member_type is "static " or "virtual ". suffix is the qualifiers that trail a member function's parameter list. All four stay text because none is part of the type: they say how the symbol is reached, not what it is. declarator is the name, which lands in the hole its type leaves.

Source code in src/demangle/schemes/msvc/nodes.py
166
167
168
169
170
171
172
def __init__(self, prefix, declarator, type_, suffix="", access="", member_type=""):
    self.prefix = prefix
    self.access = access
    self.member_type = member_type
    self.declarator = declarator
    self.type = type_
    self.suffix = suffix
declares_a_function property
declares_a_function

Whether this declares a function rather than data.

A variable of function-pointer type is data: int (__cdecl *p)(int) has a FunctionType in it, but not as the thing being declared, which is why the reference leaves its convention alone under --no-calling-convention.

PointerAuth

PointerAuth(key, address_discriminated, discriminator)

Bases: _Spelled

An ARM64e pointer authentication qualifier: __ptrauth(key, address_discriminated, discriminator).

Source code in src/demangle/schemes/msvc/nodes.py
81
82
83
84
def __init__(self, key: int, address_discriminated: int, discriminator: int):
    self.key = key
    self.address_discriminated = int(address_discriminated)
    self.discriminator = discriminator

render

render(
    node,
    declarator="",
    declarator_is_function=False,
    options=DEFAULT_OPTIONS,
    member_cv="",
    as_pointee=False,
    sigil=False,
)

Spell a type around a declarator, the way C nests one inside the other.

A pointer or array binds to the declarator built so far, and a name therefore ends up inside its type: int (*j)[2], not int (*)[2] j.

options says which parts of a declaration to print. It reaches the declaration's own function type and stops there: a parameter, a template argument and a variable's pointee are all rendered with the defaults, which is what the reference does and why --no-calling-convention leaves int (__cdecl *)(int) alone in a parameter list.

sigil says the declarator opens with an indirection's, which an array has to bracket and cannot always see in the text: * and & it can, A::* it cannot, since a qualified data name opens the same way. int (A::*)[4], not int A::*[4].

Source code in src/demangle/schemes/msvc/nodes.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
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
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
def render(
    node,
    declarator="",
    declarator_is_function=False,
    options=DEFAULT_OPTIONS,
    member_cv="",
    as_pointee=False,
    sigil=False,
):
    """Spell a type around a declarator, the way C nests one inside the other.

    A pointer or array binds to the declarator built so far, and a name therefore ends up
    *inside* its type: `int (*j)[2]`, not `int (*)[2] j`.

    `options` says which parts of a *declaration* to print. It reaches the declaration's
    own function type and stops there: a parameter, a template argument and a variable's
    pointee are all rendered with the defaults, which is what the reference does and why
    `--no-calling-convention` leaves `int (__cdecl *)(int)` alone in a parameter list.

    `sigil` says the declarator opens with an indirection's, which an array has to
    bracket and cannot always see in the text: `*` and `&` it can, `A::*` it cannot,
    since a qualified data name opens the same way. `int (A::*)[4]`, not `int A::*[4]`.
    """
    kind = node.kind
    if kind == "raw":
        if not declarator:
            return node.text
        if declarator.startswith("["):
            return node.text + declarator
        # the reference spaces a declarator off a type ending in an alphanumeric character
        # or a template's ">", and abuts it to anything else: "struct S *" but "struct S_*"
        tail = node.text[-1:]
        sigil = declarator.startswith(("*", "&"))
        # `decltype(auto) *f`: LLVM's main branch writes `decltype(auto)*f`.
        word = tail == ">" or (tail.isascii() and tail.isalnum()) or node.text.endswith(_KEYWORDS_ENDING_IN_A_BRACKET)
        abuts = sigil and not word
        return node.text + ("" if abuts else " ") + declarator
    if kind == "indirection":
        token = node.sigil + " ".join(ordered_qualifiers(node.qualifiers))
        if node.pointer_auth is not None:
            token += node.pointer_auth.render()
        separator = " " if _spaced_off_the_sigil(node, declarator, declarator_is_function, options) else ""
        # Said here, where it is known, rather than guessed from the declarator's text.
        return render(node.inner, token + separator + declarator, options=options, as_pointee=True, sigil=True)
    if kind == "pointer_auth":
        return node.render()
    if kind == "array":
        if sigil or declarator.startswith(("*", "&")):
            declarator = f"({declarator})"
        return render(node.inner, f"{declarator}[{node.dimension}]", options=options)
    if kind == "declaration":
        # a declaration is never nested inside a type, so nothing may be threaded into it
        lead = node.prefix
        if options.access_specifier:
            lead += node.access
        if options.member_type:
            lead += node.member_type
        if not options.variable_type and not node.declares_a_function:
            # `public: static C::sm`, as the reference gives.
            return lead + node.declarator.text + node.suffix
        # Member qualifiers go inside the return type's wrapping, after the parameter
        # list (`FunctionSignatureNode::outputPost`), not after the whole declarator.
        return lead + render(node.type, node.declarator.text, options=options, member_cv=node.suffix)
    # A pointer's pointee has its convention printed by the pointer, which the reference's
    # `--no-calling-convention` does not reach: `int (__cdecl * fn(void))(int)`.
    as_pointee = as_pointee or declarator.startswith(("*", "&"))
    convention = node.convention if options.calling_convention or as_pointee else ""
    # Suppressed is not absent: `$$A6A_N_N@Z` reads `__cdecl(bool)`, not a constructor's
    # `__cdecl (bool)`.
    suppressed = node.returns is not None and not (options.return_type or as_pointee)
    member_cv = node.member_cv or member_cv
    params = ", ".join([render(parameter, options=options) for parameter in node.parameters])
    if node.returns is None:
        # No return type, nothing around the parameters: "public: __thiscall foo::foo(void)"
        return spelled_after(convention, f"{declarator}({params}){member_cv}")
    if not convention and not as_pointee:
        # a pointer keeps the space the convention would have filled: "int ( *)()"
        inner = f"{declarator}({params}){member_cv}"
        return inner if suppressed else render(node.returns, inner, True, options)
    if as_pointee:
        # "int (__attribute__((__swiftcall__))  *j)(int)". A suppressed convention takes
        # its space with it, as `dbghelp` under UNDNAME_NO_MS_KEYWORDS: "int (*)(void)".
        gap = "  " if convention.startswith("__attribute__") else " "
        if not convention and not options.ms_keywords:
            gap = ""
        declarator = f"({convention}{gap}{declarator})"
    else:
        declarator = f"{convention} {declarator}" if declarator else convention
    inner = f"{declarator}({params}){member_cv}"
    return inner if suppressed else render(node.returns, inner, True, options)

spelled_after

spelled_after(convention, text)

Join a calling convention to what follows it, skipping the ones spelled with nothing.

Source code in src/demangle/schemes/msvc/nodes.py
310
311
312
313
def spelled_after(convention, text):
    """Join a calling convention to what follows it, skipping the ones spelled with
    nothing."""
    return f"{convention} {text}" if convention else text

prefixed

prefixed(text, node)

Put text in front of a whole symbol, without disturbing the shape underneath it.

The one caller is extern "C", and it lands in the declaration's member_type rather than ahead of everything: the reference groups it with static and virtual, so --no-member-type drops all three together.

After them and not before, which is where the reference writes it: ?fn@@$$J0EAAHH@Z is private: virtual extern "C" int __cdecl fn(int), not extern "C" virtual. Only a name carrying both shows the difference, and every $$J in the corpora is on a free function or an ordinary member, where member_type is empty and the two orders are the same string.

Source code in src/demangle/schemes/msvc/nodes.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
def prefixed(text, node):
    """Put `text` in front of a whole symbol, without disturbing the shape underneath it.

    The one caller is `extern "C" `, and it lands in the declaration's `member_type`
    rather than ahead of everything: the reference groups it with `static` and `virtual`,
    so `--no-member-type` drops all three together.

    After them and not before, which is where the reference writes it: `?fn@@$$J0EAAHH@Z`
    is `private: virtual extern "C" int __cdecl fn(int)`, not `extern "C" virtual`.
    Only a name carrying both shows the difference, and every `$$J` in the corpora is on
    a free function or an ordinary member, where `member_type` is empty and the two
    orders are the same string.
    """
    if not text:
        return node
    if node.kind == "declaration":
        return Declaration(node.prefix, node.declarator, node.type, node.suffix, node.access, node.member_type + text)
    return Raw(text + render(node))

merge_qualifiers

merge_qualifiers(left, right)
Source code in src/demangle/schemes/msvc/nodes.py
336
337
338
def merge_qualifiers(left, right):
    # "int __unaligned *__unaligned *"
    return ordered_qualifiers(tuple(left) + tuple(right))

ordered_qualifiers

ordered_qualifiers(quals)

quals in _QUALIFIER_ORDER, deduplicated, in whatever spelling they arrived in.

Ranked by the word without its underscores, so a run that is dropping them orders the same qualifiers the same way. The spelling kept is the one that was passed in: this decides an order, not a wording.

Source code in src/demangle/schemes/msvc/nodes.py
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def ordered_qualifiers(quals):
    """`quals` in `_QUALIFIER_ORDER`, deduplicated, in whatever spelling they arrived in.

    Ranked by the word without its underscores, so a run that is dropping them orders
    the same qualifiers the same way. The spelling kept is the one that was passed in:
    this decides an order, not a wording.
    """
    if not quals:
        return ()
    ranked = {}
    for qual in quals:
        rank = _QUALIFIER_RANK.get(qual.lstrip("_"))
        if rank is not None:
            ranked.setdefault(rank, qual)
    return tuple(ranked[rank] for rank in sorted(ranked))

apply_qualifiers

apply_qualifiers(node, quals)

Qualify a named type, as a pointee qualifier or a $$C wrapper does.

Only ever reached with a named type: an indirection merges its qualifiers as it is built, and a back-reference declines rather than accept one.

In _QUALIFIER_ORDER and not in the order they arrived. Appending would make a type qualified twice -- a pointee qualifier and then the variable's own, which ?s4@PR13182@@3PCDD is -- come out char volatile const * where the reference writes char const volatile *. So any already at the end of the text are taken back off and the whole set is written in one order.

Source code in src/demangle/schemes/msvc/nodes.py
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
def apply_qualifiers(node, quals):
    """Qualify a named type, as a pointee qualifier or a $$C wrapper does.

    Only ever reached with a named type: an indirection merges its qualifiers as it is
    built, and a back-reference declines rather than accept one.

    In `_QUALIFIER_ORDER` and not in the order they arrived. Appending would make a
    type qualified twice -- a pointee qualifier and then the variable's own, which
    `?s4@PR13182@@3PCDD` is -- come out `char volatile const *` where the reference
    writes `char const volatile *`. So any already at the end of the text are taken back
    off and the whole set is written in one order.
    """
    if not quals:
        return node
    words = node.text.split(" ")
    trailing = []
    while words and words[-1].lstrip("_") in _QUALIFIER_RANK:
        trailing.insert(0, words.pop())
    ordered = ordered_qualifiers((*trailing, *quals))
    return Raw(" ".join([*words, *ordered]))

is_member_function_pointer

is_member_function_pointer(node)
Source code in src/demangle/schemes/msvc/nodes.py
388
389
def is_member_function_pointer(node):
    return node.kind == "indirection" and node.points_into_class and node.inner.kind == "function"

qualify_declared

qualify_declared(node, quals)

Place a data symbol's trailing qualifier on what the symbol declares.

It belongs one level inside an outermost pointer rather than on the pointer - "?s@@3PADB" is "char const s" and "?s@@3PAPADB" is "char const s" - and an array passes it on to its element, the way C spells one: "?arr@@3QAY01HB" is "int const (const arr)[2]".

Source code in src/demangle/schemes/msvc/nodes.py
392
393
394
395
396
397
398
399
400
401
402
403
404
def qualify_declared(node, quals):
    """Place a data symbol's trailing qualifier on what the symbol declares.

    It belongs one level inside an outermost pointer rather than on the pointer -
    "?s@@3PADB" is "char const *s" and "?s@@3PAPADB" is "char *const *s" - and an array
    passes it on to its element, the way C spells one: "?arr@@3QAY01HB" is
    "int const (*const arr)[2]".
    """
    if not quals:
        return node
    if node.kind == "indirection":
        return Indirection(node.sigil, node.qualifiers, _qualify_element(node.inner, quals), node.pointer_auth)
    return _qualify_element(node, quals)

Rust

demangle.schemes.rust

Rust symbol mangling: the legacy _ZN scheme and the v0 _R scheme.

Rust has used two schemes. The legacy one wraps a hashed path in Itanium mangling, so a legacy Rust symbol is also a valid Itanium symbol -- which is why this plugin is offered names first. The v0 scheme, specified in RFC 2603 and stabilised behind -Csymbol-mangling-version=v0, is a scheme of its own.

Derived from MIT-licensed code, with both grammars substantially reworked; see NOTICE.

Structured output

parse() returns a tree: a symbol holding a path of name components, with impl, template, type and literal nodes for what a path carries. See nodes.py for why the tree cannot spell a symbol differently from demangle().

DEFAULT_OPTIONS module-attribute

DEFAULT_OPTIONS = RustOptions()

What both shipped styles use: the hash left out, as rustfilt prints it.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="rust",
    node_kinds=(
        "decorated",
        "impl",
        "literal",
        "name",
        "namespace",
        "path",
        "symbol",
        "template",
        "type",
    ),
    detect=detect,
    parse=parse,
    options_type=RustOptions,
    description="Rust legacy (_ZN) and v0 (_R) symbol mangling",
    aliases=("rs",),
    symbol_table_decorations=False,
    first_characters="_Z",
    priority=50,
)

The scheme as the registry holds it, registered when this package is imported.

ManglingType

Bases: Enum

Which of Rust's two manglings a name uses: legacy (_ZN...E) or v0 (_R...).

RustDemangler

Routes a name to the scheme that mangled it.

Holds no state of its own, and hands out a fresh parser per name rather than reusing one. That is not fastidiousness: both parsers keep the name they are reading, its suffix and its path spans on self, so a single shared instance lets two threads overwrite each other's parse -- and the failure is silent. It does not raise; it returns another symbol's name, which is then memoised under the first symbol's key.

A parser is two attribute stores to allocate, against a parse that is tens of microseconds, so per-name construction does not show up in the benchmark.

demangle

demangle(
    inpstr,
    limit,
    keep_hash=False,
    max_depth=_DEFAULT_MAX_DEPTH,
)

Spell inpstr, in whichever of the two manglings it uses.

Parameters:

Name Type Description Default
inpstr str

the mangled name.

required
limit int

the most characters the printer may write.

required
keep_hash bool

spell the disambiguating hash rather than dropping it.

False
max_depth int

the deepest v0 nesting to read, as Limits.max_depth.

_DEFAULT_MAX_DEPTH
Source code in src/demangle/schemes/rust/_dispatch.py
38
39
40
41
42
43
44
45
46
47
def demangle(self, inpstr: str, limit: int, keep_hash: bool = False, max_depth: int = _DEFAULT_MAX_DEPTH) -> str:
    """Spell `inpstr`, in whichever of the two manglings it uses.

    Args:
        inpstr: the mangled name.
        limit: the most characters the printer may write.
        keep_hash: spell the disambiguating hash rather than dropping it.
        max_depth: the deepest v0 nesting to read, as `Limits.max_depth`.
    """
    return self._for(inpstr, keep_hash, max_depth).demangle(inpstr, limit)

structure

structure(
    inpstr,
    limit,
    keep_hash=False,
    max_depth=_DEFAULT_MAX_DEPTH,
)

Demangle to a tree rather than to text.

Same parser, same pass; only what it emits into differs. The tree renders to exactly what demangle returns for the same input -- keep_hash included, since both are one stream of fragments.

Source code in src/demangle/schemes/rust/_dispatch.py
49
50
51
52
53
54
55
56
def structure(self, inpstr: str, limit: int, keep_hash: bool = False, max_depth: int = _DEFAULT_MAX_DEPTH):
    """Demangle to a tree rather than to text.

    Same parser, same pass; only what it emits into differs. The tree renders to
    exactly what `demangle` returns for the same input -- `keep_hash` included, since
    both are one stream of fragments.
    """
    return self._for(inpstr, keep_hash, max_depth).structure(inpstr, limit)

determine_type

determine_type(inpstr)

Say which of the two manglings inpstr uses, by its prefix alone.

Parameters:

Name Type Description Default
inpstr str

the mangled name.

required

Returns:

Type Description
ManglingType

ManglingType.LEGACY or ManglingType.V0.

Raises:

Type Description
TypeNotFoundError

inpstr starts the way neither mangling does.

Note

A bare R is accepted here, like the bare ZN below it: some symbol tables have had the leading underscore stripped before the name got here, and the reference reads that form too. It stays out of detect, which is deliberately narrower -- a bare R is too broad a claim to make about every symbol in a binary -- so auto-detection still needs _R or __R.

Source code in src/demangle/schemes/rust/_dispatch.py
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
def determine_type(self, inpstr: str) -> ManglingType:
    """Say which of the two manglings `inpstr` uses, by its prefix alone.

    Args:
        inpstr: the mangled name.

    Returns:
        `ManglingType.LEGACY` or `ManglingType.V0`.

    Raises:
        TypeNotFoundError: `inpstr` starts the way neither mangling does.

    Note:
        A bare `R` is accepted here, like the bare `ZN` below it: some symbol tables
        have had the leading underscore stripped before the name got here, and the
        reference reads that form too. It stays out of `detect`, which is deliberately
        narrower -- a bare `R` is too broad a claim to make about every symbol in a
        binary -- so auto-detection still needs `_R` or `__R`.
    """
    if inpstr.startswith(("_ZN", "__ZN", "ZN")):
        return ManglingType.LEGACY
    elif inpstr.startswith(("_R", "__R", "R")):
        return ManglingType.V0
    else:
        raise TypeNotFoundError(inpstr)

RustOptions dataclass

RustOptions(keep_hash=False)

How much of a Rust name to spell.

keep_hash class-attribute instance-attribute

keep_hash = False

Spell the disambiguating hash rather than dropping it.

Off by default, which is what rustfilt and every corpus here is scored against.

detect

detect(name)

Cheap test for a Rust mangled name.

v0 needs its prefix and the letter that opens a <path>, which is one of seven. _R on its own is not enough: CodeWarrior writes __RTTI__40TObjOwnerDerivedFromIObj <12CStringTable>, and claiming it would make detect name the wrong scheme for a symbol in this package's own corpus. The parse would fail and demangle fall through to the scheme that owns it, so the spelling would be right and the label would not -- but detect is a public answer in its own right, and a caller labelling a symbol table gets that answer and no second chance. Nothing is lost by the narrower test: a v0 name that does not open a <path> is one the parser refuses on its next step.

Legacy shares Itanium's _ZN prefix, so it additionally requires evidence that the name is Rust's -- without that this plugin would claim every C++ symbol it was offered.

Two kinds of evidence, either of which is enough:

  • the hash component, 17h and sixteen hex digits, that rustc appends as the last element of the path. It is looked for by its marker rather than at a fixed offset from the end, because real symbols carry things after it: a .0 for a promoted constant, a .llvm.<hash> from LLVM's internaliser. Anchoring to the end would miss every one of those, and the C++ demangler would then claim them and produce a plausible-looking but quite wrong spelling.
  • a $...$ escape, which legacy Rust uses for characters the Itanium alphabet has no room for. _ZN8$RF$testE is &test; read as C++ it spells $RF$test, which is not a name anything has.

The leading underscore is optional -- some symbol tables have already had it stripped, and rustc-demangle reads ZN4testE as test. Without the underscore nothing else claims the name, so the evidence asked for there is the grammar itself: ZN, a digit, and a path the legacy reader consumes whole. None of the 652,000 symbols in this box's libraries and binaries starts ZN at all.

Deliberately narrower than rustc-demangle, which accepts any _ZN name and treats the hash as optional. It can afford to: it is only ever handed names a caller has already decided are Rust's. This plugin is offered every symbol in a binary, and the same rule here would claim _ZN1a4hbadE -- an ordinary C++ a::hbad -- and print a, having read hbad as a hash and dropped it. The cost of being narrow is that a Rust symbol carrying neither mark reads as C++; the cost of being wide is that C++ symbols read as Rust. The first loses information, the second invents it.

Source code in src/demangle/schemes/rust/__init__.py
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 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
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def detect(name):
    """Cheap test for a Rust mangled name.

    v0 needs its prefix *and* the letter that opens a `<path>`, which is one of seven.
    `_R` on its own is not enough: CodeWarrior writes `__RTTI__40TObjOwnerDerivedFromIObj
    <12CStringTable>`, and claiming it would make `detect` name the wrong scheme for a
    symbol in this package's own corpus. The parse would fail and `demangle` fall
    through to the scheme that owns it, so the spelling would be right and the label
    would not -- but `detect` is a public answer in its own right, and a caller
    labelling a symbol table gets that answer and no second chance. Nothing is lost by
    the narrower test: a v0 name that does not open a `<path>` is one the parser refuses
    on its next step.

    Legacy shares Itanium's `_ZN` prefix, so it additionally requires evidence that the
    name is Rust's -- without that this plugin would claim every C++ symbol it was
    offered.

    Two kinds of evidence, either of which is enough:

    - the hash component, `17h` and sixteen hex digits, that rustc appends as the last
      element of the path. It is looked for by its marker rather than at a fixed offset
      from the end, because real symbols carry things after it: a `.0` for a promoted
      constant, a `.llvm.<hash>` from LLVM's internaliser. Anchoring to the end would
      miss every one of those, and the C++ demangler would then claim them and produce a
      plausible-looking but quite wrong spelling.
    - a `$...$` escape, which legacy Rust uses for characters the Itanium alphabet has
      no room for. `_ZN8$RF$testE` is `&test`; read as C++ it spells `$RF$test`, which
      is not a name anything has.

    The leading underscore is optional -- some symbol tables have already had it
    stripped, and rustc-demangle reads `ZN4testE` as `test`. Without the underscore
    nothing else claims the name, so the evidence asked for there is the grammar itself:
    `ZN`, a digit, and a path the legacy reader consumes whole. None of the 652,000
    symbols in this box's libraries and binaries starts `ZN` at all.

    Deliberately narrower than rustc-demangle, which accepts any `_ZN` name and treats
    the hash as optional. It can afford to: it is only ever handed names a caller has
    already decided are Rust's. This plugin is offered every symbol in a binary, and the
    same rule here would claim `_ZN1a4hbadE` -- an ordinary C++ `a::hbad` -- and print
    `a`, having read `hbad` as a hash and dropped it. The cost of being narrow is that a
    Rust symbol carrying neither mark reads as C++; the cost of being wide is that C++
    symbols read as Rust. The first loses information, the second invents it.
    """
    if not name:
        return False
    if name.startswith(("_R", "__R")):
        opening = name[3:4] if name[1] == "_" else name[2:3]
        return opening in _V0_PATH_START
    if not name.startswith(("_ZN", "__ZN", "ZN")):
        return False
    version = name.find("@")
    if version > 0:
        name = name[:version]
    marker = name.rfind(_LEGACY_HASH_MARKER)
    if marker >= 0:
        start = marker + len(_LEGACY_HASH_MARKER)
        digits = name[start : start + _LEGACY_HASH_DIGITS]
        if (
            len(digits) == _LEGACY_HASH_DIGITS
            and _is_hex(digits)
            and name[start + _LEGACY_HASH_DIGITS : start + _LEGACY_HASH_DIGITS + 1] == "E"
        ):
            return True
    if "$" in name and _LEGACY_ESCAPE.search(name) is not None:
        return True
    # A bare `ZN` has no marker, so reading it is the only test; bounded like a parse.
    return name[0] == "Z" and name[2:3].isdigit() and len(name) <= DEFAULT_LIMITS.max_input and _reads_as_legacy(name)

parse

parse(
    mangled, builder, limits=DEFAULT_LIMITS, options=None
)

Parse a Rust mangled name into builder.

Source code in src/demangle/schemes/rust/__init__.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=None):
    """Parse a Rust mangled name into `builder`."""
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    # Checked before anything else looks at the string.
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)
    # rustc-demangle drops a `.llvm.<hash>` before anything else, and its hash alphabet
    # includes `@`: `_RC3foo.llvm.9D1C9369@@16` is `foo`. Splitting the ELF version off
    # first, as `core` does for other schemes, would strand the `@@16` after the name.
    name = mangled
    version = name.find("@")
    if version > 0 and name.find(_LLVM_MARKER, 0, version) >= 0:
        name = _strip_llvm_suffix(name)
    name, decoration = split_decorations(name)
    # `keep_hash` is the one thing a style changes here; see `options.py`.
    keep_hash = bool(getattr(options, "keep_hash", False))
    if _wants_structure(builder):
        handle = _guard(name, limits, _DEMANGLER.structure, keep_hash)
        _check_length(name, handle.size, limits)
        _refuse_empty(name, handle.size)
    else:
        expanded = _guard(name, limits, _DEMANGLER.demangle, keep_hash)
        _check_length(name, len(expanded), limits)
        _refuse_empty(name, len(expanded))
        handle = builder.raw(expanded)
    return builder.decorated(handle, decoration) if decoration else handle

Options

demangle.schemes.rust.options

Whether to spell the hash rustc writes into a symbol to keep it unique.

Both Rust manglings carry one and neither spells it by default, because it is noise to a reader: core::fmt::write says what the function is and core::fmt::write::h05af221e174051e9 says the same thing with a machine's bookkeeping after it. But it is the only thing telling two monomorphisations of one generic function apart, so a caller symbolising a profile or diffing two builds needs it.

This is rustc-demangle's own choice, spelled as a formatting flag rather than an option: {} on its Demangle prints the hash and {:#} suppresses it. rustfilt prints the suppressed form, and so does this by default. What the flag reaches differs between the two manglings, which is why it is one option and not two:

  • legacy appends the path's trailing 17h<16 hex> component -- a::f::h05af221e174051e9;
  • v0 prints the crate's disambiguator wherever a crate root is spelled -- features[9f05e0465351d495]::apply_hrtb, at every occurrence rather than the first.

DEFAULT_OPTIONS module-attribute

DEFAULT_OPTIONS = RustOptions()

What both shipped styles use: the hash left out, as rustfilt prints it.

RustOptions dataclass

RustOptions(keep_hash=False)

How much of a Rust name to spell.

keep_hash class-attribute instance-attribute
keep_hash = False

Spell the disambiguating hash rather than dropping it.

Off by default, which is what rustfilt and every corpus here is scored against.

Nodes

demangle.schemes.rust.nodes

The Rust symbol tree.

Rust's spelling has no declarator syntax at all. A C++ type wraps the name it declares -- int (*)(char) -- which is why core/spelling.py carries a left/right pair and why MSVC needs a renderer of its own. Rust writes &mut [u8; 4] strictly left to right, and a path is just its components joined by ::. So the structure worth recovering here is not a declarator shape: it is which part of the name is which -- the crate, the module path, the generic arguments, the trait an impl is for, the closure disambiguator.

Every node holds parts: a tuple of plain strings and child nodes, in output order. Rendering concatenates them. That is deliberate and is the whole correctness argument for this module -- the printer in _v0.py emits one linear stream of fragments whether it is building text or a tree, so a tree's rendering cannot drift from the text path. It is not that the two are tested to agree; there is only one stream, and the tree is that stream with the boundaries remembered.

Kinds are shared with the other schemes where they mean the same thing, so a tool can ask any tree for its name or template nodes without knowing which language produced it. impl and namespace are Rust's own: nothing in the C-family schemes means either.

RustName

RustName(parts)

Bases: _Rust

A single identifier: a crate name, a module, a function, a field.

Named RustName rather than Name because core.ast.Name already exists and means the same thing to a consumer -- hence the shared kind -- but spells itself through the shared builder, which is not this scheme's speller.

Source code in src/demangle/schemes/rust/nodes.py
53
54
55
56
57
58
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Path

Path(parts)

Bases: _Rust

A path: std::collections::HashMap, or a crate root on its own.

Source code in src/demangle/schemes/rust/nodes.py
53
54
55
56
57
58
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Namespace

Namespace(parts, tag, disambiguator)

Bases: _Rust

A compiler-generated path component: {closure#0}, {shim:vtable#0}.

tag is the namespace letter from the mangling -- C for a closure, S for a shim, or a vendor's own -- and disambiguator separates several in one scope.

Source code in src/demangle/schemes/rust/nodes.py
109
110
111
112
def __init__(self, parts, tag, disambiguator):
    super().__init__(parts)
    self.tag = tag
    self.disambiguator = disambiguator

Impl

Impl(parts, self_type, trait)

Bases: _Rust

An inherent or trait impl: <Foo>, <Foo as Bar>.

trait is the trait's subtree for a trait impl and None for an inherent one, so a caller can ask what a method implements without matching on rendered text.

Source code in src/demangle/schemes/rust/nodes.py
125
126
127
128
def __init__(self, parts, self_type, trait):
    super().__init__(parts)
    self.self_type = self_type
    self.trait = trait

Generics

Generics(parts, base, arguments)

Bases: _Rust

A generic argument application, Vec<u8> or Foo::<T>.

base is what is being applied to and arguments are the arguments themselves, both also present in parts -- the fields name them, parts orders them.

Source code in src/demangle/schemes/rust/nodes.py
141
142
143
144
def __init__(self, parts, base, arguments):
    super().__init__(parts)
    self.base = base
    self.arguments = tuple(arguments)

Type

Type(parts, form)

Bases: _Rust

A type. form says which shape: reference, pointer, array, slice, tuple, dyn, fn, basic, path.

One class rather than nine because nothing walks these structurally -- a caller asking "is this a reference" wants a field to test, and kind stays type so the common query finds every one of them.

Source code in src/demangle/schemes/rust/nodes.py
159
160
161
def __init__(self, parts, form):
    super().__init__(parts)
    self.form = form

Value

Value(parts, form)

Bases: _Rust

A const generic argument: an integer, a bool, a char, a structural const.

Source code in src/demangle/schemes/rust/nodes.py
170
171
172
def __init__(self, parts, form):
    super().__init__(parts)
    self.form = form

Symbol

Symbol(parts, hash=None, suffix='', vendor='')

Bases: _Rust

A whole Rust symbol.

hash is the value of the legacy scheme's trailing 17h<16 hex> component -- the sixteen digits, without the h marker or the length prefix. The mangling carries it and the spelling drops it; it is recorded because it is the only thing telling two monomorphisations of one generic function apart. suffix is whatever followed the mangled name -- an LLVM .llvm.<hash> from the internaliser, a .0 from a promoted constant.

Source code in src/demangle/schemes/rust/nodes.py
189
190
191
192
193
def __init__(self, parts, hash=None, suffix="", vendor=""):
    super().__init__(parts)
    self.hash = hash
    self.suffix = suffix
    self.vendor = vendor

render

render(part)

Text for a part, which is either a plain fragment or a subtree.

Source code in src/demangle/schemes/rust/nodes.py
37
38
39
def render(part):
    """Text for a part, which is either a plain fragment or a subtree."""
    return part if isinstance(part, str) else part.render()

Swift

demangle.schemes.swift

Swift mangled names.

A port of the Swift compiler's own demangler -- lib/Demangling/Demangler.cpp and lib/Demangling/NodePrinter.cpp -- because nothing smaller is enough. Swift's mangling is postfix and compresses aggressively against three tables that span a whole name, so a symbol cannot be read a piece at a time, and how a piece is spelled depends on where it sits in the tree. _demangler.py and _printer.py are those two files; nodes.py records the structure of the printer's traversal.

Conformance is exact against a reference built from Swift's own demangler at a pinned main commit (see tools/swift-demangle-reference/README.md):

  • every $s symbol in the shipped Swift runtime and Foundation -- 48,368 of them -- spelled identically, with nothing refused;
  • every case in the compiler's own test/Demangle/Inputs/manglings.txt -- the 531 rows of tests/conformance/swift-upstream.txt -- which is a much harder set: SIL function types, function-signature specialisations, key paths, autodiff thunks, macro expansions, and the Swift 3 mangling.

The Swift 3 mangling -- _T followed by anything but 0 -- is a different grammar with its own demangler in the compiler, and _old_demangler.py is a port of that one. It still matters: the ObjC runtime holds a Swift class's name in that form, so it turns up in any Apple binary with interop in it. It builds the same tree, so the printer spells it with no idea which mangling it came from, and the compiler's own Swift 3 test cases come out exactly.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="swift",
    node_kinds=(
        "extension",
        "function",
        "generics",
        "module",
        "name",
        "symbol",
        "template",
        "type",
        "variable",
    ),
    detect=detect,
    parse=parse,
    parse_type=parse_type,
    description="Swift symbol mangling",
    options_type=SwiftOptions,
    aliases=(),
    first_characters="$_@a",
    priority=45,
)

The scheme as the registry holds it, registered when this package is imported.

SymbolicReference

SymbolicReference(raw_kind, kind, directness, offset, at)

One symbolic reference, as the bytes described it.

at is where the offset field begins, because that is what the offset is relative to: the address a resolver wants is address_of(at) + offset. Keeping the position rather than the resolved address is what lets this module know nothing about images.

Source code in src/demangle/schemes/swift/symbolic.py
93
94
95
96
97
98
def __init__(self, raw_kind, kind, directness, offset, at):
    self.raw_kind = raw_kind
    self.kind = kind
    self.directness = directness
    self.offset = offset
    self.at = at

reserved property

reserved

Whether this is one of the introducers the reference declines to read.

Image

Image(segments, imports=None, symbols=None)

A flat view of a loaded binary: virtual address in, bytes out.

Deliberately the smallest thing a resolver can be given. A caller with a memory dump, a debugger connection or an object format this package does not read supplies its own read, and everything else here works unchanged.

Source code in src/demangle/schemes/swift/resolve.py
70
71
72
73
74
75
76
77
78
def __init__(self, segments, imports=None, symbols=None):
    #: `(address, bytes)` pairs; the first that contains an address answers for it.
    self._segments = tuple(segments)
    #: Symbols the loader would fill slots from, by slot address: the file lacks the
    #: address, but a descriptor symbol's mangled name is all a resolver needs.
    self.imports = dict(imports or {})
    #: Descriptor symbols the image defines, by address: the fallback for a
    #: descriptor the layout alone cannot spell (in an extension, or opaque).
    self.symbols = dict(symbols or {})

read

read(address, length)

length bytes at virtual address, or None if they are not all mapped.

Source code in src/demangle/schemes/swift/resolve.py
80
81
82
83
84
85
86
87
88
def read(self, address, length):
    """`length` bytes at virtual `address`, or None if they are not all mapped."""
    if length < 0:
        return None
    for start, blob in self._segments:
        offset = address - start
        if offset >= 0 and offset + length <= len(blob):
            return blob[offset : offset + length]
    return None

cstring

cstring(address, limit=1024)

The NUL-terminated string at address, or None.

Source code in src/demangle/schemes/swift/resolve.py
90
91
92
93
94
95
96
97
98
99
def cstring(self, address, limit=1024):
    """The NUL-terminated string at `address`, or None."""
    for start, blob in self._segments:
        offset = address - start
        if 0 <= offset < len(blob):
            stop = blob.find(b"\0", offset, offset + limit)
            if stop < 0:
                return None
            return blob[offset:stop]
    return None

ContextResolver

ContextResolver(image, pointer_size=8)

Resolves context references by walking the descriptor they point at.

The other kinds are not resolved and are not guessed at. An accessor-function reference names a function that has to be run to say what it yields, which is not something a file can be asked; the two existential-shape kinds describe a shape rather than name a context, and spelling one would mean inventing a name the mangling never had.

Source code in src/demangle/schemes/swift/resolve.py
388
389
390
def __init__(self, image, pointer_size=8):
    self._image = image
    self._pointer_size = pointer_size

fragment

fragment(descriptor)

The mangled fragment naming the context descriptor at descriptor.

The walk over the descriptors comes first, because it needs nothing but the layout and is what a stripped image still has. Where it declines -- a type declared in an extension, whose parent is an extension descriptor; an opaque type descriptor, which has no name of its own -- the symbol the image defines at that address is the fragment, when the image kept its symbols.

Source code in src/demangle/schemes/swift/resolve.py
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
def fragment(self, descriptor):
    """The mangled fragment naming the context descriptor at `descriptor`.

    The walk over the descriptors comes first, because it needs nothing but the
    layout and is what a stripped image still has. Where it declines -- a type
    declared in an extension, whose parent is an extension descriptor; an opaque
    type descriptor, which has no name of its own -- the symbol the image defines
    at that address is the fragment, when the image kept its symbols.
    """
    found = self._walk(descriptor)
    if found is not None:
        return found
    symbols = getattr(self._image, "symbols", None)
    symbol = symbols.get(descriptor) if symbols else None
    return _fragment_from_symbol(symbol) if symbol else None

detect

detect(name)

One of the prefixes either mangling uses.

_T covers Swift 3 as well as _T0; the two are different grammars and demangle_symbol dispatches between them, but from the outside they are one scheme.

Source code in src/demangle/schemes/swift/__init__.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def detect(name):
    """One of the prefixes either mangling uses.

    `_T` covers Swift 3 as well as `_T0`; the two are different grammars and
    `demangle_symbol` dispatches between them, but from the outside they are one scheme.
    """
    if not name:
        return False
    if name.startswith("__"):
        # Mach-O adds one underscore, which `swift-demangle` takes off.
        name = name[1:]
    if name.startswith(_ALL_SWIFT_PREFIXES):
        return True
    return bool(async_main_entry_point_length(name))

parse

parse(
    mangled,
    builder,
    limits=DEFAULT_LIMITS,
    options=DEFAULT_OPTIONS,
)

Parse a Swift mangled name into builder.

Source code in src/demangle/schemes/swift/__init__.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=DEFAULT_OPTIONS):
    """Parse a Swift mangled name into `builder`."""
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    if not detect(mangled):
        raise NotMangledError(mangled, "not a Swift mangled name")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)

    # Mach-O adds one leading underscore, which `swift-demangle` strips from a name that
    # opens with two. `_$s` is a prefix in its own right.
    name = mangled[1:] if mangled.startswith("__") else mangled
    try:
        root = demangle_symbol(name)
    except RecursionError as error:
        raise LimitExceeded(mangled, "recursion depth", limits.max_depth) from error
    return _finish(mangled, root, builder, limits, options)

parse_type

parse_type(
    mangled,
    builder,
    limits=DEFAULT_LIMITS,
    options=DEFAULT_OPTIONS,
)

Parse a Swift type mangling -- Si, SaySiG -- rather than a whole symbol.

Demangler::demangleType rather than demangleSymbol, and what a metadata typeref holds: a type mangling carries none of the $s a symbol opens with, so nothing about it says it is Swift, and it is only readable because the caller said so.

A type mangling that reads as nothing is refused rather than spelled. The reference hands back the whole input wrapped in a Suffix node when nothing at all parsed, which prints as with unmangled suffix "..." -- true, but not a demangling of anything, so it comes back here as the ParseError it is.

Source code in src/demangle/schemes/swift/__init__.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def parse_type(mangled, builder, limits=DEFAULT_LIMITS, options=DEFAULT_OPTIONS):
    """Parse a Swift *type* mangling -- `Si`, `SaySiG` -- rather than a whole symbol.

    `Demangler::demangleType` rather than `demangleSymbol`, and what a metadata typeref
    holds: a type mangling carries none of the `$s` a symbol opens with, so nothing about
    it says it is Swift, and it is only readable because the caller said so.

    A type mangling that reads as nothing is refused rather than spelled. The reference
    hands back the whole input wrapped in a `Suffix` node when nothing at all parsed,
    which prints as `with unmangled suffix "..."` -- true, but not a demangling of
    anything, so it comes back here as the `ParseError` it is.
    """
    if not mangled:
        raise NotMangledError(mangled, "empty type")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)
    try:
        root = demangle_type(mangled)
    except RecursionError as error:
        raise LimitExceeded(mangled, "recursion depth", limits.max_depth) from error
    if root is not None and root.kind == "Suffix":
        raise ParseError(mangled, None, "not a type this reads")
    return _finish(mangled, root, builder, limits, options)

demangle_symbol

demangle_symbol(name, resolver=None, *, byte_mode=False)

Read name into a Global node, or return None if it is not readable.

Swift 3's mangling is a different grammar with its own demangler in the compiler, and it is dispatched to here on the same test the reference uses: _Tt -- or, more exactly, _T not followed by 0, since _T0 is Swift 4.

Source code in src/demangle/schemes/swift/_demangler.py
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
def demangle_symbol(name, resolver=None, *, byte_mode=False):
    """Read `name` into a `Global` node, or return `None` if it is not readable.

    Swift 3's mangling is a different grammar with its own demangler in the compiler, and
    it is dispatched to here on the same test the reference uses: `_Tt` -- or, more
    exactly, `_T` not followed by `0`, since `_T0` is Swift 4.
    """
    if name.startswith("_T") and not name.startswith("_T0"):
        from ._old_demangler import demangle_old_symbol

        return demangle_old_symbol(name, byte_mode=byte_mode)
    return Demangler(name, resolver, byte_mode=byte_mode).demangle_symbol()

demangle_type

demangle_type(name, resolver=None, *, byte_mode=False)

Read name as a type rather than a whole symbol, for _TtGSa...-style names.

This is also the entry a metadata typeref takes: those are types, and they carry no $s prefix for demangle_symbol to find.

Source code in src/demangle/schemes/swift/_demangler.py
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
def demangle_type(name, resolver=None, *, byte_mode=False):
    """Read `name` as a type rather than a whole symbol, for `_TtGSa...`-style names.

    This is also the entry a metadata typeref takes: those are types, and they carry no
    `$s` prefix for `demangle_symbol` to find.
    """
    demangler = Demangler(name, resolver, byte_mode=byte_mode)
    demangler.parse_and_push()
    found = demangler.pop()
    return found if found is not None else Node("Suffix", text=name)

demangle_old_symbol

demangle_old_symbol(name, *, byte_mode=False)

Read a Swift 3 mangled name into the same tree the current mangling produces.

Source code in src/demangle/schemes/swift/_old_demangler.py
1500
1501
1502
def demangle_old_symbol(name, *, byte_mode=False):
    """Read a Swift 3 mangled name into the same tree the current mangling produces."""
    return OldDemangler(name, byte_mode=byte_mode).demangle_top_level()

print_root

print_root(root, options=DEFAULT_OPTIONS)

Spell a whole tree, or return "" if it is one the printer cannot spell.

Source code in src/demangle/schemes/swift/_printer.py
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
def print_root(root, options=DEFAULT_OPTIONS):
    """Spell a whole tree, or return `""` if it is one the printer cannot spell."""
    if root is None:
        return ""
    printer = Printer(options)
    try:
        printer.print(root, 0)
    except (_Invalid, IndexError, AttributeError, KeyError, RecursionError):
        # The reference asserts on a malformed tree.
        return ""
    return printer.result()

demangle_symbolic

demangle_symbolic(
    name, resolver=None, *, whole_symbol=None
)

Read a mangled name that may hold symbolic references, and spell it.

name is bytes, not str, because a name holding a symbolic reference is not text: the reference's four-byte offset is arbitrary bytes. This is the entry a metadata typeref takes, and the only one that can take it.

resolver is called with (reference, offset-of-its-offset-field-within-name) and returns a mangled fragment naming what the reference points at, or None to decline. Return a Unicode str for textual fragments, or bytes when a fragment contains binary symbolic references. For compatibility, a str whose grammar reaches a symbolic reference token or alignment padding is treated as a Latin1 byte view.

resolve.ContextResolver is one, over an Image; a caller with a debugger or a memory dump writes its own. With no resolver at all, a name holding a reference is refused -- which is what the reference demangler does, and better than inventing a name for something the bytes do not carry.

whole_symbol says whether to read the name as a whole symbol or as a type. Left None it is decided by the mangling prefix, which is what tells the two apart: a symbol carries $s or _T, a typeref carries nothing.

Returns the spelling, or None if the name could not be read.

Source code in src/demangle/schemes/swift/__init__.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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
211
def demangle_symbolic(name, resolver=None, *, whole_symbol=None):
    """Read a mangled name that may hold symbolic references, and spell it.

    `name` is `bytes`, not `str`, because a name holding a symbolic reference is not
    text: the reference's four-byte offset is arbitrary bytes. This is the entry a
    metadata typeref takes, and the only one that can take it.

    `resolver` is called with `(reference, offset-of-its-offset-field-within-name)` and
    returns a *mangled fragment* naming what the reference points at, or None to decline.
    Return a Unicode `str` for textual fragments, or `bytes` when a fragment contains
    binary symbolic references. For compatibility, a `str` whose grammar reaches a symbolic
    reference token or alignment padding is treated as a Latin1 byte view.

    `resolve.ContextResolver` is one, over an `Image`; a caller with a debugger or a
    memory dump writes its own. With no resolver at all, a name holding a reference is
    refused -- which is what the reference demangler does, and better than inventing a
    name for something the bytes do not carry.

    `whole_symbol` says whether to read the name as a whole symbol or as a type. Left
    None it is decided by the mangling prefix, which is what tells the two apart: a
    symbol carries `$s` or `_T`, a typeref carries nothing.

    Returns the spelling, or None if the name could not be read.
    """
    if isinstance(name, str):
        raise TypeError("demangle_symbolic reads bytes; a name holding a reference is not text")
    # Latin1 maps each byte to one code point, preserving binary resolver offsets.
    text = name.decode("latin-1")
    if whole_symbol is None:
        whole_symbol = detect(text)
    try:
        root = (
            demangle_symbol(text, resolver, byte_mode=True)
            if whole_symbol
            else demangle_type(text, resolver, byte_mode=True)
        )
    except RecursionError:
        return None
    if root is None:
        return None
    if root.kind == "Suffix":
        # A whole-input `Suffix` node is the reference saying it could not read the name.
        return None
    spelled = print_root(root)
    return spelled or None

typerefs

typerefs(blob)

Split a metadata blob of NUL-terminated mangled names into those names.

Not blob.split(b"\0"): a symbolic reference's offset is arbitrary bytes and very often holds a zero, so splitting cuts names in half. See symbolic.end_of_name.

Source code in src/demangle/schemes/swift/__init__.py
214
215
216
217
218
219
220
221
222
def typerefs(blob):
    """Split a metadata blob of NUL-terminated mangled names into those names.

    Not `blob.split(b"\\0")`: a symbolic reference's offset is arbitrary bytes and very
    often holds a zero, so splitting cuts names in half. See `symbolic.end_of_name`.
    """
    from .symbolic import names

    return names(blob)

scan

scan(data)

Every symbolic reference in data, in order.

Reading rather than searching: a byte inside an offset can look like an introducer, and a name that has been walked properly is the only way to know it is not one.

Source code in src/demangle/schemes/swift/symbolic.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def scan(data):
    """Every symbolic reference in `data`, in order.

    Reading rather than searching: a byte inside an offset can look like an introducer,
    and a name that has been walked properly is the only way to know it is not one.
    """
    found = []
    pos = 0
    length = len(data)
    while pos < length:
        byte = data[pos]
        if byte in INTRODUCERS:
            reference, pos = read(data, pos)
            found.append(reference)
        else:
            pos += 1
    return tuple(found)

end_of_name

end_of_name(data, start=0)

Where the NUL-terminated mangled name beginning at start ends.

The index returned is of the terminating NUL, or of the end of data if the name runs to it. This exists because the obvious thing does not work: a reference's four-byte offset is arbitrary bytes and very often holds a zero, so data.index(0) cuts a name in half. Swift's own reflection reader walks the name for the same reason.

Source code in src/demangle/schemes/swift/symbolic.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def end_of_name(data, start=0):
    """Where the NUL-terminated mangled name beginning at `start` ends.

    The index returned is of the terminating NUL, or of the end of `data` if the name
    runs to it. This exists because the obvious thing does not work: a reference's
    four-byte offset is arbitrary bytes and very often holds a zero, so `data.index(0)`
    cuts a name in half. Swift's own reflection reader walks the name for the same
    reason.
    """
    if start < 0 or start > len(data):
        raise ValueError(f"no byte at {start}")
    pos = start
    length = len(data)
    while pos < length:
        byte = data[pos]
        if byte == 0:
            return pos
        if byte in INTRODUCERS:
            if pos + 1 + OFFSET_WIDTH > length:
                # Truncated: the whole remainder is this name.
                return length
            pos += 1 + OFFSET_WIDTH
        else:
            pos += 1
    return length

elf_image

elf_image(data)

An Image over a 64-bit little-endian ELF file's PT_LOAD segments.

Raises MalformedImage -- a ValueError -- for anything that is not one, including a file too short to hold the header it claims.

Source code in src/demangle/schemes/swift/resolve.py
121
122
123
124
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
def elf_image(data):
    """An `Image` over a 64-bit little-endian ELF file's PT_LOAD segments.

    Raises `MalformedImage` -- a `ValueError` -- for anything that is not one, including
    a file too short to hold the header it claims.
    """
    if len(data) < 0x40 or data[:4] != b"\x7fELF" or data[4] != 2 or data[5] != 1:
        raise MalformedImage("not a 64-bit little-endian ELF file")
    program_offset = _field(_U64, data, 0x20, "the program header offset")
    entry_size = _field(_U16, data, 0x36, "the program header entry size")
    count = _field(_U16, data, 0x38, "the program header count")
    if count and entry_size < 0x38:
        raise MalformedImage(f"ELF program header entry claims {entry_size} bytes; at least 56 required")
    segments = []
    dynamic = None
    for index in range(count):
        at = program_offset + index * entry_size
        # A truncated image (core dump, partial download) is common: answer from what
        # is there.
        if at + 0x38 > len(data):
            break
        kind = _U32.unpack_from(data, at)[0]
        offset = _U64.unpack_from(data, at + 0x08)[0]
        filesz = _U64.unpack_from(data, at + 0x20)[0]
        if kind == 2:  # PT_DYNAMIC
            dynamic = data[offset : offset + filesz]
        if kind != 1:  # PT_LOAD
            continue
        vaddr = _U64.unpack_from(data, at + 0x10)[0]
        segments.append((vaddr, bytearray(data[offset : offset + filesz])))
    imports = _apply_dynamic_relocations(data, segments, dynamic) if dynamic else {}
    return Image([(vaddr, bytes(blob)) for vaddr, blob in segments], imports, _defined_descriptors(data))

macho_image

macho_image(data)

An Image over a 64-bit Mach-O file's LC_SEGMENT_64 commands.

A fat binary is not unwrapped: pick the slice first. Only the little-endian 64-bit magic is accepted, which is every Mach-O anyone demangles Swift out of.

Raises MalformedImage for anything else, including a header whose command count or command sizes do not fit the file.

Source code in src/demangle/schemes/swift/resolve.py
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
def macho_image(data):
    """An `Image` over a 64-bit Mach-O file's LC_SEGMENT_64 commands.

    A fat binary is not unwrapped: pick the slice first. Only the little-endian 64-bit
    magic is accepted, which is every Mach-O anyone demangles Swift out of.

    Raises `MalformedImage` for anything else, including a header whose command count or
    command sizes do not fit the file.
    """
    if len(data) < 0x20 or data[:4] != b"\xcf\xfa\xed\xfe":
        raise MalformedImage("not a 64-bit little-endian Mach-O file")
    commands = _field(_U32, data, 0x10, "the load command count")
    command_bytes = _field(_U32, data, 0x14, "the load command region size")
    if commands > command_bytes // _MIN_LOAD_COMMAND:
        raise MalformedImage("load command count does not fit the declared command region")
    command_end = 0x20 + command_bytes
    at = 0x20
    segments = []
    for _ in range(commands):
        if at + _MIN_LOAD_COMMAND > len(data):
            break
        command, size = struct.unpack_from("<II", data, at)
        if size < _MIN_LOAD_COMMAND:
            raise MalformedImage(f"load command at offset {at} claims {size} bytes")
        if at + size > command_end:
            raise MalformedImage(f"load command at offset {at} extends beyond the declared command region")
        if command == 0x19:  # LC_SEGMENT_64
            if size < 0x48:
                raise MalformedImage(f"segment command at offset {at} claims {size} bytes; at least 72 required")
            if at + 0x48 > len(data):
                break  # Preserve the complete segments of a partially downloaded image.
            sections = _U32.unpack_from(data, at + 0x40)[0]
            if size < 0x48 + sections * 0x50:
                raise MalformedImage(f"segment command at offset {at} cannot hold its {sections} sections")
            vmaddr = _U64.unpack_from(data, at + 0x18)[0]
            fileoff = _U64.unpack_from(data, at + 0x28)[0]
            filesize = _U64.unpack_from(data, at + 0x30)[0]
            segments.append((vmaddr, data[fileoff : fileoff + filesize]))
        at += size
    return Image(segments)

Options

demangle.schemes.swift.options

How much of a Swift name to spell.

The compiler's own printer takes a DemangleOptions and consults it in fifty-one places. This carries the ones the reference's SimplifiedUIDemangleOptions() bundle actually changes over test/Demangle/Inputs/simplified-manglings.txt, which is what swift-demangle --simplified prints and what Xcode and LLDB show a user.

Fields are named for the reference's, lower-cased. True is what the toolchain prints by default, so SwiftOptions() is the full spelling and SwiftOptions.simplified() is the bundle.

DEFAULT_OPTIONS module-attribute

DEFAULT_OPTIONS = SwiftOptions()

What both shipped styles use: the full spelling, as swift-demangle prints it.

SIMPLIFIED_OPTIONS module-attribute

SIMPLIFIED_OPTIONS = SwiftOptions.simplified()

SwiftOptions.simplified(): what swift-demangle --simplified prints.

SwiftOptions dataclass

SwiftOptions(
    synthesize_sugar_on_types=True,
    display_module_names=True,
    show_function_argument_types=True,
    display_where_clauses=True,
    display_entity_types=True,
    display_protocol_conformances=True,
    display_generic_specializations=True,
    display_extension_contexts=True,
    display_unmangled_suffix=True,
    show_private_discriminators=True,
    shorten_partial_apply=True,
    shorten_thunk=True,
    show_async_resume_partial=True,
    shorten_value_witness=True,
)

Which parts of a Swift name to print.

synthesize_sugar_on_types class-attribute instance-attribute
synthesize_sugar_on_types = True

Spell Swift.Optional<T> as T?, Swift.Array<T> as [T], and the dictionary and inline-array forms with them.

SynthesizeSugarOnTypes. The struct's own default is off; swift-demangle turns it on unless given --disable-sugar, and so does this, because the tool's output is what a reader has seen. Off is still reachable, and the printer itself needs it: a specialisation's propagated function is a mangled name the reference demangles again with the struct's defaults, so that one comes out unsugared inside a name that is otherwise sugared.

display_module_names class-attribute instance-attribute
display_module_names = True

Qualify a name with the module that declares it: Swift.Int, not Int.

DisplayModuleNames. The dot goes with it -- the reference prints nothing for the module node, and the separator is written only after a context that produced output.

show_function_argument_types class-attribute instance-attribute
show_function_argument_types = True

Spell a function's parameters and result, rather than their labels alone.

ShowFunctionArgumentTypes. False turns (Swift.Int) -> Swift.UInt into (_:): the parameter list keeps its shape and its labels, and everything after it -- async, a thrown error, -> result -- is left out, because what remains is a signature the way a user refers to one rather than a type.

display_where_clauses class-attribute instance-attribute
display_where_clauses = True

Spell the requirements of a generic signature: <A where B: Runcible>, not <A>.

DisplayWhereClauses. The parameter list stays; what goes is everything after the where.

display_entity_types class-attribute instance-attribute
display_entity_types = True

Spell the type a declaration has, where it is written after a colon.

DisplayEntityTypes. bar : Int becomes bar. Only the colon form: a function's signature is written around its name rather than after it, and show_function_argument_types is what governs that one.

display_protocol_conformances class-attribute instance-attribute
display_protocol_conformances = True

Spell which protocol a witness conforms to, and where: bar : barrable in foo.

DisplayProtocolConformances. False leaves the type alone -- protocol witness table for bar -- which is what identifies the entry; the conformance is what makes it long.

display_generic_specializations class-attribute instance-attribute
display_generic_specializations = True

Spell what a specialisation was specialised with.

DisplayGenericSpecializations. False writes specialized X in place of generic specialization <Int> of X and of the whole function signature specialization <Arg[0] = ...> of X, once, however many specialisation layers a symbol carries.

display_extension_contexts class-attribute instance-attribute
display_extension_contexts = True

Spell the module an extension was written in: (extension in Foo):A.test().

DisplayExtensionContexts, and the anonymous-context form with it.

display_unmangled_suffix class-attribute instance-attribute
display_unmangled_suffix = True

Report what followed a name the demangler could read only part of.

DisplayUnmangledSuffix. True writes with unmangled suffix "...".

show_private_discriminators class-attribute instance-attribute
show_private_discriminators = True

Spell the file-private discriminator: (getPrivateClass in _DISC), not the name.

ShowPrivateDiscriminators.

shorten_partial_apply class-attribute instance-attribute
shorten_partial_apply = True

Write partial apply forwarder for f rather than partial apply for f.

ShortenPartialApply, inverted, like shorten_value_witness.

shorten_thunk class-attribute instance-attribute
shorten_thunk = True

Spell a thunk in full: reabstraction thunk helper from A to B, not thunk for B.

ShortenThunk, inverted. It reaches a dozen kinds -- the reabstraction thunks, the autodiff ones, merged, distributed thunk, the dynamically-replaceable trio -- and in each the short form is the last child alone, or nothing at all.

show_async_resume_partial class-attribute instance-attribute
show_async_resume_partial = True

Spell an async resume point: (1) await resume partial function for f().

ShowAsyncResumePartial. False leaves the function it resumes into.

shorten_value_witness class-attribute instance-attribute
shorten_value_witness = True

Write destroy value witness for T rather than destroy for T.

ShortenValueWitness, inverted: the reference's flag says shorten, and every field here says print it in full, so that SwiftOptions() is the toolchain default with no field set to False.

simplified classmethod
simplified()

The reference's SimplifiedUIDemangleOptions(), as far as it differs here.

Upstream's bundle sets fourteen flags. The rest either match this printer's behaviour already (SynthesizeSugarOnTypes, QualifyEntities) or change nothing over the 217 vectors, and a flag no vector exercises is a flag with no reference behind it -- so it is left out rather than guessed at.

Source code in src/demangle/schemes/swift/options.py
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
@classmethod
def simplified(cls) -> "SwiftOptions":
    """The reference's `SimplifiedUIDemangleOptions()`, as far as it differs here.

    Upstream's bundle sets fourteen flags. The rest either match this printer's
    behaviour already (`SynthesizeSugarOnTypes`, `QualifyEntities`) or change nothing
    over the 217 vectors, and a flag no vector exercises is a flag with no reference
    behind it -- so it is left out rather than guessed at.
    """
    return replace(
        cls(),
        display_module_names=False,
        show_function_argument_types=False,
        display_where_clauses=False,
        display_entity_types=False,
        display_protocol_conformances=False,
        display_generic_specializations=False,
        display_extension_contexts=False,
        display_unmangled_suffix=False,
        show_private_discriminators=False,
        shorten_partial_apply=False,
        shorten_thunk=False,
        show_async_resume_partial=False,
        shorten_value_witness=False,
    )

Demangler

Reading a Swift mangled name into a tree.

A port of the algorithm in the Swift compiler's lib/Demangling/Demangler.cpp, which is the only complete description of the mangling that exists -- docs/ABI/Mangling.rst is a grammar sketch and is explicitly not normative about the tree that results.

The mangling is postfix, and the demangler is a stack machine because of it. The main loop reads one operator at a time and pushes what it produces; an operator that needs operands pops them back off. _ZN-style prefix manglings can be read by recursive descent, this cannot: in $sSiSgD the Si is pushed, Sg pops it to make Int?, and D pops that. Nothing is known about what a component is for until the operator that consumes it arrives.

Three pieces of state span a whole name and are what make it compress so well:

  • Substitutions. Every nominal type and identifier is remembered; A and a letter refer back to one. A single A operator can push several.
  • Words. Each identifier contributes its capitalised words to a table, and a later identifier can be spelled out of them -- 0c6ToHostD0 builds HostToPluginMessage from words of the module name. This is the compression no other mangling here has.
  • Position. pushBack is used as a one-character backtrack in several places, so the reader is not purely forward-only.

Failure is None throughout, exactly as the reference uses nullptr: an operator that cannot read its operands returns None, and every caller checks. The public entry point turns that into an exception at the boundary.

Demangler

Demangler(text, resolver=None, *, byte_mode=False)

The reference's Demangler, one instance per name.

None means failure everywhere, as nullptr does in the reference. Keeping that convention rather than raising is not a stylistic choice: several productions are speculative, reading ahead and accepting the failure as an answer, and an exception would have to be caught at each of them.

Source code in src/demangle/schemes/swift/_demangler.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
def __init__(self, text, resolver=None, *, byte_mode=False):
    self.has_non_ascii = not text.isascii()
    if self.has_non_ascii and not byte_mode:
        # Swift lengths and resolver positions address bytes. Symbolic callers
        # already supply a Latin1 view of raw bytes and must not be recoded.
        try:
            text = text.encode("utf-8").decode("latin-1")
        except UnicodeEncodeError:
            text = ""  # An unpaired surrogate cannot occur in a UTF8 identifier.
    self.text = text
    #: Called with `(SymbolicReference, offset-of-its-offset-field)`; returns a mangled
    #: fragment for the target, or None. Without one a symbolic reference is refused.
    self.resolver = resolver
    self.pos = 0
    self.end = len(text)
    self.stack = []
    self.substitutions = []
    #: The word table for identifier substitutions, capped as the reference caps it.
    self.words = []
    self.is_old_function_type_mangling = False
    #: How many resolved fragments this demangler stands inside.
    self.nesting = 0
next_char
next_char()

Consume and return one character, or "" at the end of input.

The cursor advances even when there was nothing to return, so push_back is the exact inverse of this call wherever it appears, including at the end. Reading past the end yields nothing because every test here is pos < end.

Source code in src/demangle/schemes/swift/_demangler.py
287
288
289
290
291
292
293
294
295
296
def next_char(self):
    """Consume and return one character, or `""` at the end of input.

    The cursor advances even when there was nothing to return, so `push_back` is the
    exact inverse of this call wherever it appears, including at the end. Reading
    past the end yields nothing because every test here is `pos < end`.
    """
    pos = self.pos
    self.pos = pos + 1
    return self.text[pos] if pos < self.end else ""
pop
pop(want=None)

Pop, optionally only if the top matches.

want is a kind or a predicate. A mismatch is not an error: several callers pop optional decorations, and a None there means "there was not one".

Source code in src/demangle/schemes/swift/_demangler.py
315
316
317
318
319
320
321
322
323
324
325
326
327
def pop(self, want=None):
    """Pop, optionally only if the top matches.

    `want` is a kind or a predicate. A mismatch is not an error: several callers pop
    optional decorations, and a `None` there means "there was not one".
    """
    if not self.stack:
        return None
    if want is not None:
        kind = self.stack[-1].kind
        if not (want(kind) if callable(want) else kind == want):
            return None
    return self.stack.pop()
natural
natural()

A run of digits, or None where the reference returns its -1000 sentinel.

Scanned here rather than through peek and _is_digit, which between them are the two most-called things in the Swift demangler: 122,000 calls to _is_digit from this method alone over the Swift corpus, and one peek beside each, for a comparison apiece. Reading past the end still stops the run, because the bound is the same one peek tests.

Source code in src/demangle/schemes/swift/_demangler.py
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 natural(self):
    """A run of digits, or `None` where the reference returns its -1000 sentinel.

    Scanned here rather than through `peek` and `_is_digit`, which between them are
    the two most-called things in the Swift demangler: 122,000 calls to `_is_digit`
    from this method alone over the Swift corpus, and one `peek` beside each, for a
    comparison apiece. Reading past the end still stops the run, because the bound is
    the same one `peek` tests.
    """
    text, end = self.text, self.end
    start = pos = self.pos
    while pos < end and "0" <= text[pos] <= "9":
        pos += 1
    if pos == start:
        return None
    if pos - start > 9:
        # The reference answers "no number" once the next digit would overflow an
        # `int`, leaving that digit unread; `int()` would accept far more.
        number = 0
        for at in range(start, pos):
            number = number * 10 + (ord(text[at]) - 48)
            if number > _INT_MAX:
                self.pos = at
                return None
        self.pos = pos
        return number
    self.pos = pos
    return int(text[start:pos])
index
index()

_ is 0 and <n>_ is n+1, so that 0 costs one character rather than two.

Source code in src/demangle/schemes/swift/_demangler.py
392
393
394
395
396
397
398
399
400
def index(self):
    """`_` is 0 and `<n>_` is n+1, so that 0 costs one character rather than two."""
    if self.next_if("_"):
        return 0
    number = self.natural()
    # `n + 1` has to fit the reference's `int` too.
    if number is not None and number < _INT_MAX and self.next_if("_"):
        return number + 1
    return None
demangle_symbol
demangle_symbol()

demangleSymbol: read a whole name and assemble the Global node.

Source code in src/demangle/schemes/swift/_demangler.py
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
def demangle_symbol(self):
    """`demangleSymbol`: read a whole name and assemble the `Global` node."""
    length = _prefix_length(self.text)
    if length == 0:
        # A node stands in for the unmangled name so funclet suffixes after it read
        # as they do after any symbol.
        length = async_main_entry_point_length(self.text)
        if length == 0:
            return None
        self.pos = length
        self.push(Node("AsyncMainEntryPoint"))
    else:
        self.is_old_function_type_mangling = _is_old_function_type_mangling(self.text)
        self.pos = length

    if not self.parse_and_push():
        return None

    top = Node("Global")
    # IRGen's `.<n>` suffix is pushed last; take it off first so the function
    # attributes beneath it are reached, and put it back at the end.
    suffix = self.pop("Suffix")
    parent = top
    while True:
        attribute = self.pop(lambda kind: kind in FUNCTION_ATTR_KINDS)
        if attribute is None:
            break
        parent.add(attribute)
        if attribute.kind in ("PartialApplyForwarder", "PartialApplyObjCForwarder"):
            parent = attribute

    for node in self.stack:
        # `Global` holds what the type is, not a `Type` wrapper.
        parent.add(node.first if node.kind == "Type" and node.children else node)

    if suffix is not None:
        top.add(suffix)

    return top if top.children else None
demangle_operator
demangle_operator()

One operator, dispatching on its first character.

The reference writes this as a switch with a goto recur for the padding byte; the loop here is that goto.

Source code in src/demangle/schemes/swift/_demangler.py
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
def demangle_operator(self):
    """One operator, dispatching on its first character.

    The reference writes this as a `switch` with a `goto recur` for the padding byte;
    the loop here is that `goto`.
    """
    while True:
        char = self.next_char()
        if char == "\xff":
            # Alignment padding in front of a symbolic reference.
            continue
        handler = _OPERATORS.get(char)
        if handler is not None:
            return handler(self)
        if char == "":
            return None
        if char in _SYMBOLIC_REFERENCE_BYTES:
            return self.demangle_symbolic_reference(ord(char))
        self.push_back()
        return self.demangle_identifier()
demangle_symbolic_reference
demangle_symbolic_reference(raw_kind)

demangleSymbolicReference: one introducer byte and a four-byte offset.

The offset is signed, little-endian, and relative to its own first byte. What the resolver hands back is a mangled fragment rather than a spelling, which is what lets the rest of the name use it: the reference may be the base of a bound generic, or a member the next operator qualifies, and only a real node can be either. A resolver that declines refuses the whole name, exactly as a null from the reference's own resolver does.

Source code in src/demangle/schemes/swift/_demangler.py
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
def demangle_symbolic_reference(self, raw_kind):
    """`demangleSymbolicReference`: one introducer byte and a four-byte offset.

    The offset is signed, little-endian, and relative to its own first byte. What the
    resolver hands back is a *mangled fragment* rather than a spelling, which is what
    lets the rest of the name use it: the reference may be the base of a bound
    generic, or a member the next operator qualifies, and only a real node can be
    either. A resolver that declines refuses the whole name, exactly as a null from
    the reference's own resolver does.
    """
    from .symbolic import CONTEXT, KINDS, OFFSET_WIDTH, SymbolicReference

    at = self.pos
    if at + OFFSET_WIDTH > self.end:
        return None
    raw = self.text[at : at + OFFSET_WIDTH].encode("latin-1")
    offset = int.from_bytes(raw, "little", signed=True)
    self.pos = at + OFFSET_WIDTH

    if self.resolver is None:
        return None
    kind, directness = KINDS.get(raw_kind, (None, None))
    if kind is None:
        # 3 through 8 and 0x0C reach the reference's switch only to fall through it.
        return None

    fragment = self.resolver(SymbolicReference(raw_kind, kind, directness, offset, at), at)
    if not fragment:
        return None
    if self.nesting >= _MAX_NESTING:
        return None
    # Preserve legacy Latin1 fragments when the grammar reaches a binary token.
    # A control byte inside a counted identifier is ordinary Unicode text.
    if isinstance(fragment, bytes):
        fragment = fragment.decode("latin-1")
        byte_mode = True
    else:
        byte_mode = False
        if not fragment.isascii() and any(char in _SYMBOLIC_REFERENCE_BYTES or char == "\xff" for char in fragment):
            probe = _BinaryFragmentProbe(fragment)
            probe.parse_and_push()
            byte_mode = probe.binary
    inner = Demangler(fragment, self.resolver, byte_mode=byte_mode)
    inner.nesting = self.nesting + 1
    resolved = inner.demangle_fragment()
    if resolved is None:
        return None
    # "Types register as substitutions even when symbolically referenced" -- except
    # for the two opaque-type kinds, which name a position rather than a type.
    if kind == CONTEXT and resolved.kind not in (
        "OpaqueTypeDescriptorSymbolicReference",
        "OpaqueReturnTypeOf",
    ):
        self.add_substitution(resolved)
    return resolved
demangle_fragment
demangle_fragment()

Read the whole text as one node, for a resolver's answer.

Source code in src/demangle/schemes/swift/_demangler.py
531
532
533
534
535
536
def demangle_fragment(self):
    """Read the whole text as one node, for a resolver's answer."""
    if not self.parse_and_push():
        return None
    found = self.pop()
    return found if found is not None and not self.stack else None
demangle_multi_substitutions
demangle_multi_substitutions()

A, which may name several substitutions at once.

A lower-case letter says "this one, and more follow"; an upper-case letter is the last. A number before a letter is a repeat count, so A2b pushes the same node twice -- except that a number followed by _ is a large index instead, which is why the count cannot be read until the character after it is known.

Source code in src/demangle/schemes/swift/_demangler.py
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
def demangle_multi_substitutions(self):
    """`A`, which may name several substitutions at once.

    A lower-case letter says "this one, and more follow"; an upper-case letter is the
    last. A number before a letter is a *repeat count*, so `A2b` pushes the same node
    twice -- except that a number followed by `_` is a large index instead, which is
    why the count cannot be read until the character after it is known.
    """
    repeat = None
    while True:
        char = self.next_char()
        if char == "":
            return None
        if "a" <= char <= "z":
            node = self.push_multi_substitutions(repeat, ord(char) - ord("a"))
            if node is None:
                return None
            self.push(node)
            repeat = None
            continue
        if "A" <= char <= "Z":
            return self.push_multi_substitutions(repeat, ord(char) - ord("A"))
        if char == "_":
            # An index: single letters cover 0-25, so a bare `A_` is 26 (the
            # reference's `-1 + 27`).
            at = (-1 if repeat is None else repeat) + 27
            if at >= len(self.substitutions):
                return None
            return self.substitutions[at]
        self.push_back()
        repeat = self.natural()
        if repeat is None:
            return None
demangle_standard_substitution
demangle_standard_substitution()

S and a letter: one of the standard library's common types.

Source code in src/demangle/schemes/swift/_demangler.py
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def demangle_standard_substitution(self):
    """`S` and a letter: one of the standard library's common types."""
    char = self.next_char()
    if char == "o":
        return Node("Module", text=MANGLING_MODULE_OBJC)
    if char == "C":
        return Node("Module", text=MANGLING_MODULE_CLANG_IMPORTER)
    if char == "g":
        optional = self.make_type(
            self.with_children(
                "BoundGenericEnum",
                self.swift_type("Enum", "Optional"),
                self.with_child("TypeList", self.pop("Type")),
            )
        )
        self.add_substitution(optional)
        return optional

    self.push_back()
    repeat = self.natural()
    if repeat is not None and repeat > _MAX_REPEAT_COUNT:
        return None
    second_level = self.next_if("c")
    table = CONCURRENCY_TYPES if second_level else STANDARD_TYPES
    found = table.get(self.next_char())
    if found is None:
        return None
    node = self.swift_type(*found)
    if repeat is not None:
        for _ in range(repeat - 1):
            self.push(node)
    return node
demangle_identifier
demangle_identifier()

A length-prefixed identifier, possibly built out of earlier ones.

Three forms, distinguished by what follows the first 0:

  • <n><n bytes> -- plain.
  • 00<n>_<n bytes> -- punycoded, for anything outside [$_A-Za-z0-9].
  • 0<letters and lengths> -- word substitutions. Each letter names a word of an earlier identifier; a lower-case one says more pieces follow, an upper-case one is the last. This is what makes a long Swift name short, and it is why an identifier cannot be read without the identifiers before it.
Source code in src/demangle/schemes/swift/_demangler.py
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
def demangle_identifier(self):
    """A length-prefixed identifier, possibly built out of earlier ones.

    Three forms, distinguished by what follows the first `0`:

    * `<n><n bytes>` -- plain.
    * `00<n>_<n bytes>` -- punycoded, for anything outside `[$_A-Za-z0-9]`.
    * `0<letters and lengths>` -- word substitutions. Each letter names a word of an
      earlier identifier; a lower-case one says more pieces follow, an upper-case one
      is the last. This is what makes a long Swift name short, and it is why an
      identifier cannot be read without the identifiers before it.
    """
    has_word_substitutions = False
    punycoded = False
    char = self.peek()
    # `_is_digit`, `_is_letter` and `_is_lower` inlined: this runs for every identifier.
    if not ("0" <= char <= "9"):
        return None
    if char == "0":
        self.pos += 1
        if self.peek() == "0":
            self.pos += 1
            punycoded = True
        else:
            has_word_substitutions = True

    pieces = []
    while True:
        while has_word_substitutions and ("a" <= (char := self.peek()) <= "z" or "A" <= char <= "Z"):
            char = self.next_char()
            if "a" <= char <= "z":
                at = ord(char) - ord("a")
            else:
                at = ord(char) - ord("A")
                has_word_substitutions = False
            if at >= len(self.words):
                return None
            pieces.append(self.words[at])
        if self.next_if("0"):
            break
        count = self.natural()
        if count is None or count <= 0:
            return None
        if punycoded:
            self.next_if("_")
        if self.pos + count > self.end:
            return None
        slice_ = self.text[self.pos : self.pos + count]
        if punycoded:
            try:
                pieces.append(punycode_decode(slice_))
            except PunycodeError:
                return None
        else:
            pieces.append(slice_)
            self._record_words(slice_)
        self.pos += count
        if not has_word_substitutions:
            break

    identifier = "".join(pieces)
    if not identifier:
        return None
    if not punycoded and self.has_non_ascii and not identifier.isascii():
        try:
            identifier = identifier.encode("latin-1").decode("utf-8")
        except UnicodeError:
            return None
    node = Node("Identifier", text=identifier)
    self.add_substitution(node)
    return node
demangle_operator_identifier
demangle_operator_identifier()

o -- an identifier that spells an operator, one letter per character.

Source code in src/demangle/schemes/swift/_demangler.py
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
def demangle_operator_identifier(self):
    """`o` -- an identifier that spells an operator, one letter per character."""
    identifier = self.pop("Identifier")
    if identifier is None:
        return None
    spelled = []
    for char in identifier.text:
        if ord(char) > 127:
            spelled.append(char)  # Unicode operators pass through.
            continue
        if not _is_lower(char):
            return None
        mapped = self._OPERATOR_CHARS[ord(char) - ord("a")]
        if mapped == " ":
            return None
        spelled.append(mapped)
    kind = {"i": "InfixOperator", "p": "PrefixOperator", "P": "PostfixOperator"}.get(self.next_char())
    return None if kind is None else Node(kind, text="".join(spelled))
demangle_local_identifier
demangle_local_identifier()

L -- a declaration not visible outside its file or function.

Source code in src/demangle/schemes/swift/_demangler.py
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
def demangle_local_identifier(self):
    """`L` -- a declaration not visible outside its file or function."""
    if self.next_if("L"):
        discriminator = self.pop("Identifier")
        name = self.pop(is_decl_name)
        return self.with_children("PrivateDeclName", discriminator, name)
    if self.next_if("l"):
        return self.with_child("PrivateDeclName", self.pop("Identifier"))
    char = self.peek()
    if ("a" <= char <= "j") or ("A" <= char <= "J"):
        self.pos += 1
        result = Node("RelatedEntityDeclName")
        self.add_child(result, Node("Identifier", text=char))
        return self.add_child(result, self.pop())
    return self.with_children("LocalDeclName", self.index_as_node(), self.pop(is_decl_name))
demangle_builtin_type
demangle_builtin_type()

B -- one of the primitives the compiler knows about directly.

Source code in src/demangle/schemes/swift/_demangler.py
782
783
784
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
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
def demangle_builtin_type(self):
    """`B` -- one of the primitives the compiler knows about directly."""
    max_size = 4096
    char = self.next_char()
    simple = _BUILTIN_SIMPLE.get(char)
    if simple is not None:
        return self.make_type(Node("BuiltinTypeName", text=simple))
    if char == "f" or char == "i":
        size = self.index()
        if size is None:
            return None
        size -= 1
        if size <= 0 or size > max_size:
            return None
        stem = "Builtin.FPIEEE" if char == "f" else "Builtin.Int"
        return self.make_type(Node("BuiltinTypeName", text=f"{stem}{size}"))
    if char == "v":
        count = self.index()
        if count is None:
            return None
        count -= 1
        if count <= 0 or count > max_size:
            return None
        element = self.pop_type_and_get_child()
        if (
            element is None
            or element.kind != "BuiltinTypeName"
            or not element.text.startswith(BUILTIN_TYPE_NAME_PREFIX)
        ):
            return None
        stem = element.text[len(BUILTIN_TYPE_NAME_PREFIX) :]
        return self.make_type(Node("BuiltinTypeName", text=f"Builtin.Vec{count}x{stem}"))
    if char == "V":
        # `Builtin.FixedArray<count, element>`, whose count is a value generic.
        element = self.pop("Type")
        if element is None:
            return None
        size = self.pop("Type")
        if size is None:
            return None
        return self.make_type(Node("BuiltinFixedArray", children=[size, element]))
    if char == "W":
        referent = self.pop("Type")
        if referent is None:
            return None
        return self.make_type(self.with_child("BuiltinBorrow", referent))
    if char == "T":
        return self.make_type(Node("BuiltinTupleType"))
    return None
pop_function_type
pop_function_type(kind, has_clang_type=False)

Assemble a function type from the annotations and the two parameter lists.

The order the annotations are popped in is the order they were pushed, reversed, and popFunctionParamLabels walks past them by that same order -- so the two lists have to agree. They do here because both are transcribed from the reference.

Source code in src/demangle/schemes/swift/_demangler.py
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
def pop_function_type(self, kind, has_clang_type=False):
    """Assemble a function type from the annotations and the two parameter lists.

    The order the annotations are popped in is the order they were pushed, reversed,
    and `popFunctionParamLabels` walks past them by that same order -- so the two
    lists have to agree. They do here because both are transcribed from the reference.
    """
    found = Node(kind)
    if has_clang_type:
        clang = self.demangle_clang_type()
        if clang is None:
            return None
        self.add_child(found, clang)
    for annotation in _FUNCTION_ANNOTATIONS:
        self.add_child(found, self.pop(annotation.__contains__))
    found = self.add_child(found, self.pop_function_params("ArgumentTuple"))
    self.add_child(found, self.pop("YieldTypes"))
    found = self.add_child(found, self.pop_function_params("ReturnType"))
    return self.make_type(found)
pop_function_param_labels
pop_function_param_labels(found)

The argument labels, which are mangled after the type rather than with it.

A label list is only produced when at least one parameter is labelled: an unlabelled signature carries none, and inventing an empty one would make the printer spell f(_:_:) where the reference spells f.

Source code in src/demangle/schemes/swift/_demangler.py
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
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
939
940
941
942
943
944
945
def pop_function_param_labels(self, found):
    """The argument labels, which are mangled *after* the type rather than with it.

    A label list is only produced when at least one parameter is labelled: an unlabelled
    signature carries none, and inventing an empty one would make the printer spell
    `f(_:_:)` where the reference spells `f`.
    """
    if not self.is_old_function_type_mangling and self.pop("EmptyList") is not None:
        return Node("LabelList")

    if found is None or found.kind != "Type":
        return None

    function = found.first
    if function.kind == "DependentGenericType":
        function = function.child(1).first
    if function.kind not in _LABELLED_FUNCTION_TYPES:
        return None

    at = 0
    for annotation in _FUNCTION_ANNOTATIONS:
        if at < len(function.children) and function.child(at).kind in annotation:
            at += 1
    if at >= len(function.children):
        return None
    parameters = function.child(at)
    if parameters.kind != "ArgumentTuple":
        return None

    params = parameters.first.first
    count = len(params.children) if params.kind == "Tuple" else 1
    if count == 0:
        return None

    labels = Node("LabelList")
    tuple_ = parameters.first.first

    if self.is_old_function_type_mangling and (tuple_ is None or tuple_.kind != "Tuple"):
        return labels

    has_labels = False
    for at in range(count):
        label = self._function_param_label(tuple_, at)
        if label is None:
            return None
        if label.kind not in ("Identifier", "FirstElementMarker"):
            return None
        labels.add(label)
        has_labels |= label.kind != "FirstElementMarker"

    if not has_labels:
        return Node("LabelList")
    if not self.is_old_function_type_mangling:
        labels.children.reverse()
    return labels
demangle_pack_protocol_conformance
demangle_pack_protocol_conformance()

HX -- one conformance per element of a parameter pack.

Source code in src/demangle/schemes/swift/_demangler.py
1056
1057
1058
def demangle_pack_protocol_conformance(self):
    """`HX` -- one conformance per element of a parameter pack."""
    return self.with_child("PackProtocolConformance", self.pop_any_protocol_conformance_list())
demangle_dependent_protocol_conformance_opaque
demangle_dependent_protocol_conformance_opaque()

HO -- the conformance an opaque result type carries.

Source code in src/demangle/schemes/swift/_demangler.py
1060
1061
1062
1063
1064
def demangle_dependent_protocol_conformance_opaque(self):
    """`HO` -- the conformance an opaque result type carries."""
    found = self.pop("Type")
    conformance = self.pop_dependent_protocol_conformance()
    return self.with_children("DependentProtocolConformanceOpaque", conformance, found)
demangle_dependent_conformance_index
demangle_dependent_conformance_index()

The index is offset by two: 0 was never valid and 1 means "unknown".

Source code in src/demangle/schemes/swift/_demangler.py
1089
1090
1091
1092
1093
1094
1095
1096
def demangle_dependent_conformance_index(self):
    """The index is offset by two: 0 was never valid and 1 means "unknown"."""
    index = self.index()
    if index is None or index <= 0:
        return None
    if index == 1:
        return Node("UnknownIndex")
    return Node("Index", index=index - 2)
demangle_bound_generics
demangle_bound_generics()

The argument lists, outermost last, and any retroactive conformances.

A nested generic type mangles one list per level, _-separated, so Outer<A>.Inner<B> carries two. They come back innermost first, which is why demangle_bound_generic_args walks the list backwards.

Source code in src/demangle/schemes/swift/_demangler.py
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
def demangle_bound_generics(self):
    """The argument lists, outermost last, and any retroactive conformances.

    A nested generic type mangles one list per level, `_`-separated, so
    `Outer<A>.Inner<B>` carries two. They come back innermost first, which is why
    `demangle_bound_generic_args` walks the list backwards.
    """
    conformances = self.pop_retroactive_conformances()
    lists = []
    while True:
        found = Node("TypeList")
        lists.append(found)
        while True:
            item = self.pop("Type")
            if item is None:
                break
            found.add(item)
        found.children.reverse()
        if self.pop("EmptyList") is not None:
            break
        if self.pop("FirstElementMarker") is None:
            return None, None
    return lists, conformances
demangle_bound_generic_args
demangle_bound_generic_args(nominal, lists, at)

Distribute the argument lists over the nesting levels of nominal.

The mangling is flat and the tree is not: the arguments belong to whichever enclosing type declared the parameters, and a context that declares none -- a closure, an initialiser -- has to be skipped without consuming a list.

Source code in src/demangle/schemes/swift/_demangler.py
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
def demangle_bound_generic_args(self, nominal, lists, at):
    """Distribute the argument lists over the nesting levels of `nominal`.

    The mangling is flat and the tree is not: the arguments belong to whichever
    enclosing type declared the parameters, and a context that declares none -- a
    closure, an initialiser -- has to be skipped without consuming a list.
    """
    if nominal is None or at >= len(lists):
        return None

    if nominal.kind in ("TypeSymbolicReference", "ProtocolSymbolicReference"):
        remaining = Node("TypeList")
        for each in reversed(lists[at:]):
            remaining.children.extend(each.children)
        return self.with_children("BoundGenericOtherNominalType", self.make_type(nominal), remaining)

    if not nominal.children:
        return None
    if nominal.kind == "AnonymousContext":
        # An anonymous context. The reference refuses here; like the runtime's
        # `_buildDemanglingForContext`, the arguments pass through to the real
        # parent (the second child).
        bound_parent = self.demangle_bound_generic_args(nominal.child(1), lists, at)
        if bound_parent is None:
            return None
        return self.with_children(nominal.kind, nominal.first, bound_parent, *nominal.children[2:])
    context = nominal.first
    consumes = nominal.kind not in _DOES_NOT_CONSUME_GENERIC_ARGS
    args = lists[at]
    if consumes:
        at += 1

    if at < len(lists):
        if context.kind == "Extension":
            bound_parent = self.demangle_bound_generic_args(context.child(1), lists, at)
            bound_parent = self.with_children("Extension", context.first, bound_parent)
            if len(context.children) == 3:
                self.add_child(bound_parent, context.child(2))
        else:
            bound_parent = self.demangle_bound_generic_args(context, lists, at)
        rebuilt = self.with_child(nominal.kind, bound_parent)
        if rebuilt is None:
            return None
        for child in nominal.children[1:]:
            self.add_child(rebuilt, child)
        nominal = rebuilt

    if not consumes or not args.children:
        return nominal

    if nominal.kind in ("Function", "Constructor"):
        # Not a nominal type at all, but it can still be generic.
        return self.with_children("BoundGenericFunction", nominal, args)
    kind = _BOUND_GENERIC_KINDS.get(nominal.kind)
    if kind is None:
        return None
    return self.with_children(kind, self.make_type(nominal), args)
demangle_impl_function_type
demangle_impl_function_type()

I -- a lowered SIL function type, spelled @convention(...) (...) -> ....

The conventions come first and their types come last, off the stack, so the loop at the end walks the children it just added in reverse and fills each one in.

Source code in src/demangle/schemes/swift/_demangler.py
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
def demangle_impl_function_type(self):
    """`I` -- a lowered SIL function type, spelled `@convention(...) (...) -> ...`.

    The conventions come first and their *types* come last, off the stack, so the
    loop at the end walks the children it just added in reverse and fills each one in.
    """
    found = Node("ImplFunctionType")

    if self.next_if("s"):
        lists, conformances = self.demangle_bound_generics()
        if lists is None or len(lists) != 1:
            return None
        signature = self.pop("DependentGenericSignature")
        if signature is None:
            return None
        substitutions = Node("ImplPatternSubstitutions", children=[signature, lists[0]])
        if conformances is not None:
            substitutions.add(conformances)
        found.add(substitutions)

    if self.next_if("I"):
        lists, conformances = self.demangle_bound_generics()
        if lists is None or len(lists) != 1:
            return None
        substitutions = Node("ImplInvocationSubstitutions", children=[lists[0]])
        if conformances is not None:
            substitutions.add(conformances)
        found.add(substitutions)

    signature = self.pop("DependentGenericSignature")
    if signature is not None and self.next_if("P"):
        signature = self.change_kind(signature, "DependentPseudogenericSignature")

    if self.next_if("e"):
        found.add(Node("ImplEscaping"))
    # The function's isolation, and how often it may be called. All three
    # come before the differentiability, where the coroutine kind's `A` cannot reach.
    if self.next_if("A"):
        found.add(Node("ImplErasedIsolation"))
    if self.next_if("N"):
        found.add(Node("ImplNonisolatedNonsendingIsolation"))
    if self.next_if("O"):
        found.add(Node("ImplCalledAtMostOnceFunction" if self.next_if("o") else "ImplCalledOnceFunction"))

    if self.peek() in ("d", "l", "f", "r"):
        found.add(Node("ImplDifferentiabilityKind", index=ord(self.next_char())))

    callee = _IMPL_CALLEE_CONVENTIONS.get(self.next_char())
    if callee is None:
        return None
    found.add(Node("ImplConvention", text=callee))

    convention = None
    has_clang_type = False
    char = self.next_char()
    if char == "z":
        inner = self.next_char()
        if inner in ("B", "C"):
            has_clang_type = True
            convention = "block" if inner == "B" else "c"
        else:
            self.push_back()
            self.push_back()
    else:
        convention = _IMPL_FUNCTION_CONVENTIONS.get(char)
        if convention is None:
            self.push_back()
    if convention is not None:
        attribute = Node("ImplFunctionConvention", children=[Node("ImplFunctionConventionName", text=convention)])
        if has_clang_type:
            self.add_child(attribute, self.demangle_clang_type())
        found.add(attribute)

    coroutine = None
    for marker, kind in (("A", "yield_once"), ("I", "yield_once_2"), ("G", "yield_many")):
        if self.next_if(marker):
            coroutine = kind
            break
    if coroutine is not None:
        found.add(Node("ImplCoroutineKind", text=coroutine))
    if self.next_if("h"):
        found.add(Node("ImplFunctionAttribute", text="@Sendable"))
    if self.next_if("H"):
        found.add(Node("ImplFunctionAttribute", text="@async"))
    if self.next_if("T"):
        found.add(Node("ImplSendingResult"))

    self.add_child(found, signature)

    pending = 0
    while True:
        parameter = self.demangle_impl_param_convention("ImplParameter")
        if parameter is None:
            break
        found = self.add_child(found, parameter)
        self.add_child(parameter, self.demangle_impl_differentiability())
        # Each of these is a marker with no operand, in this order.
        for marker, kind, text in (
            ("T", "ImplParameterSending", "sending"),
            ("I", "ImplParameterIsolated", "isolated"),
            ("L", "ImplParameterImplicitLeading", "sil_implicit_leading_param"),
        ):
            if self.next_if(marker):
                self.add_child(parameter, Node(kind, text=text))
        pending += 1
    while True:
        result = self.demangle_impl_result_convention("ImplResult")
        if result is None:
            break
        found = self.add_child(found, result)
        self.add_child(result, self.demangle_impl_differentiability())
        pending += 1
    while self.next_if("Y"):
        yielded = self.demangle_impl_param_convention("ImplYield")
        if yielded is None:
            return None
        found = self.add_child(found, yielded)
        pending += 1
    if self.next_if("z"):
        error = self.demangle_impl_result_convention("ImplErrorResult")
        if error is None:
            return None
        found = self.add_child(found, error)
        pending += 1
    if not self.next_if("_"):
        return None

    for at in range(pending):
        item = self.pop("Type")
        if item is None:
            return None
        found.children[len(found.children) - at - 1].add(item)

    return self.make_type(found)
demangle_metatype
demangle_metatype()

M -- one of the many runtime records that describe a type.

Source code in src/demangle/schemes/swift/_demangler.py
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
def demangle_metatype(self):
    """`M` -- one of the many runtime records that describe a type."""
    char = self.next_char()
    simple = _METATYPE_POPPED_TYPE.get(char)
    if simple is not None:
        return self.with_popped_type(simple)
    popped_any = _METATYPE_POPPED_NODE.get(char)
    if popped_any is not None:
        return self.with_child(popped_any, self.pop())
    if char == "A":
        return self.with_child("ReflectionMetadataAssocTypeDescriptor", self.pop_protocol_conformance())
    if char == "B":
        return self.with_child("ReflectionMetadataBuiltinDescriptor", self.pop("Type"))
    if char == "c":
        return self.with_child("ProtocolConformanceDescriptor", self.pop_protocol_conformance())
    if char == "C":
        found = self.pop("Type")
        if found is None or not found.children or not is_any_generic(found.first.kind):
            return None
        return self.with_child("ReflectionMetadataSuperclassDescriptor", found.first)
    if char == "F":
        return self.with_child("ReflectionMetadataFieldDescriptor", self.pop("Type"))
    if char == "p":
        return self.with_child("ProtocolDescriptor", self.pop_protocol())
    if char == "S":
        return self.with_child("ProtocolSelfConformanceDescriptor", self.pop_protocol())
    if char == "V":
        return self.with_child("PropertyDescriptor", self.pop(is_entity))
    if char == "X":
        return self.demangle_private_context_descriptor()
    return None
demangle_archetype
demangle_archetype()

Q -- a generic parameter, an associated type, or an opaque return type.

Source code in src/demangle/schemes/swift/_demangler.py
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
def demangle_archetype(self):
    """`Q` -- a generic parameter, an associated type, or an opaque return type."""
    char = self.next_char()
    if char == "a":
        identifier = self.pop("Identifier")
        archetype = self.pop_type_and_get_child()
        associated = self.make_type(self.with_children("AssociatedTypeRef", archetype, identifier))
        self.add_substitution(associated)
        return associated
    if char == "O":
        return self.with_child("OpaqueReturnTypeOf", self.pop_context())
    if char == "o":
        index = self.index()
        lists, conformances = self.demangle_bound_generics()
        if lists is None:
            return None
        name = self.pop()
        if name is None or index is None:
            return None
        opaque = self.with_children("OpaqueType", name, Node("Index", index=index))
        if opaque is None:
            return None
        bound = Node("TypeList")
        for each in reversed(lists):
            bound.add(each)
        opaque.add(bound)
        if conformances is not None:
            opaque.add(conformances)
        found = self.make_type(opaque)
        self.add_substitution(found)
        return found
    if char == "r":
        return self.make_type(Node("OpaqueReturnType"))
    if char == "R":
        ordinal = self.index()
        if ordinal is None:
            return None
        return self.make_type(self.with_child("OpaqueReturnType", Node("OpaqueReturnTypeIndex", index=ordinal)))
    if char in ("x", "X", "y", "Y", "z", "Z"):
        base = None
        if char in ("y", "Y"):
            base = self.demangle_generic_param_index()
        elif char in ("z", "Z"):
            base = self.dependent_generic_param_type(0, 0)
        if char in ("y", "Y", "z", "Z") and base is None:
            return None
        if char in ("x", "y", "z"):
            found = self.demangle_associated_type_simple(base)
        else:
            found = self.demangle_associated_type_compound(base)
        self.add_substitution(found)
        return found
    if char == "p":
        count = self.pop_type_and_get_child()
        pattern = self.pop_type_and_get_child()
        return self.make_type_or_none(self.with_children("PackExpansion", pattern, count))
    if char == "e":
        pack = self.pop_type_and_get_child()
        level = self.index()
        if level is None:
            return None
        return self.make_type_or_none(
            self.with_children("PackElement", pack, Node("PackElementLevel", index=level))
        )
    if char == "P":
        return self.pop_pack()
    if char == "S":
        return self.pop_sil_pack()
    return None
demangle_generic_param_index
demangle_generic_param_index()

x is <0, 0>; d gives both coordinates; a bare index is depth 0.

Depth and index are both biased so that the commonest parameter -- the first of the innermost signature -- costs a single character.

Source code in src/demangle/schemes/swift/_demangler.py
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
def demangle_generic_param_index(self):
    """`x` is `<0, 0>`; `d` gives both coordinates; a bare index is depth 0.

    Depth and index are both biased so that the commonest parameter -- the first of
    the innermost signature -- costs a single character.
    """
    if self.next_if("d"):
        depth = self.index()
        index = self.index()
        return self.dependent_generic_param_type(None if depth is None else depth + 1, index)
    if self.next_if("z"):
        return self.dependent_generic_param_type(0, 0)
    if self.next_if("s"):
        return Node("ConstrainedExistentialSelf")
    index = self.index()
    return self.dependent_generic_param_type(0, None if index is None else index + 1)
demangle_thunk_or_specialization
demangle_thunk_or_specialization()

T -- the largest operator, covering everything the compiler synthesises.

Source code in src/demangle/schemes/swift/_demangler.py
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
def demangle_thunk_or_specialization(self):
    """`T` -- the largest operator, covering everything the compiler synthesises."""
    char = self.next_char()
    # `TT` (thunk-instruction thunks, only `TTI` so far) is in none of the tables
    # below and would otherwise be refused.
    if char == "T":
        if self.next_char() != "I":
            return None
        return self.with_child("SILThunkIdentity", self.pop(is_entity))
    plain = _THUNK_PLAIN.get(char)
    if plain is not None:
        return Node(plain)
    entity = _THUNK_OF_ENTITY.get(char)
    if entity is not None:
        return self.with_child(entity, self.pop(is_entity))
    specialization = _GENERIC_SPECIALIZATIONS.get(char)
    if specialization is not None:
        return self.demangle_generic_specialization(specialization)
    if char == "t":
        return self.demangle_generic_specialization_with_dropped_arguments()

    if char in ("Y", "Q"):
        kind = "AsyncAwaitResumePartialFunction" if char == "Q" else "AsyncSuspendResumePartialFunction"
        return self.with_child(kind, self.index_as_node())
    if char == "C":
        return self.with_child("CoroutineContinuationPrototype", self.pop("Type"))
    if char in ("z", "Z"):
        flags = self.index_as_node()
        signature = self.pop("DependentGenericSignature")
        result = self.pop("Type")
        implementation = self.pop("Type")
        kind = "ObjCAsyncCompletionHandlerImpl" if char == "z" else "CheckedObjCAsyncCompletionHandlerImpl"
        node = self.with_children(kind, implementation, result, flags)
        if signature is not None:
            self.add_child(node, signature)
        return node
    if char == "V":
        base = self.pop(is_entity)
        derived = self.pop(is_entity)
        return self.with_children("VTableThunk", derived, base)
    if char == "W":
        witnessed = self.pop(is_entity)
        conformance = self.pop_protocol_conformance()
        return self.with_children("ProtocolWitness", conformance, witnessed)
    if char in ("R", "r", "y"):
        kind = {
            "R": "ReabstractionThunkHelper",
            "y": "ReabstractionThunkHelperWithSelf",
            "r": "ReabstractionThunk",
        }[char]
        thunk = Node(kind)
        self.add_child(thunk, self.pop("DependentGenericSignature"))
        if kind == "ReabstractionThunkHelperWithSelf":
            self.add_child(thunk, self.pop("Type"))
        self.add_child(thunk, self.pop("Type"))
        self.add_child(thunk, self.pop("Type"))
        return thunk
    if char in ("p", "P"):
        kind = "GenericPartialSpecialization" if char == "p" else "GenericPartialSpecializationNotReAbstracted"
        specialized = self.demangle_spec_attributes(kind)
        parameter = self.with_child("GenericSpecializationParam", self.pop("Type"))
        return self.add_child(specialized, parameter)
    if char == "f":
        return self.demangle_function_specialization()
    if char in ("K", "k"):
        return self._key_path_accessor_thunk(char)
    if char in ("H", "h"):
        return self._key_path_equality_thunk(char)
    if char == "l":
        return self.with_child("AssociatedTypeDescriptor", self.pop_assoc_type_name())
    if char == "L":
        return self.with_child("ProtocolRequirementsBaseDescriptor", self.pop_protocol())
    if char == "M":
        return self.with_child("DefaultAssociatedTypeMetadataAccessor", self.pop_assoc_type_name())
    if char in ("n", "N"):
        requirement = self.pop_protocol()
        path = self.pop_associated_conformance_subject()
        protocol = self.pop("Type")
        kind = "AssociatedConformanceDescriptor" if char == "n" else "DefaultAssociatedConformanceAccessor"
        return self.with_children(kind, protocol, path, requirement)
    if char == "b":
        requirement = self.pop_protocol()
        protocol = self.pop("Type")
        return self.with_children("BaseConformanceDescriptor", protocol, requirement)
    if char == "v":
        index = self.index()
        if index is None:
            return None
        kind = "OutlinedReadOnlyObject" if self.next_char() == "r" else "OutlinedVariable"
        return Node(kind, index=index)
    if char == "e":
        params = self.demangle_bridged_method_params()
        return None if not params else Node("OutlinedBridgedMethod", text=params)
    if char == "U":
        actor = self.pop("Type")
        if actor is None:
            return None
        reabstraction = self.pop()
        if reabstraction is None:
            return None
        return Node("ReabstractionThunkHelperWithGlobalActor", children=[reabstraction, actor])
    if char == "J":
        following = self.peek()
        if following == "S":
            self.pos += 1
            return self.demangle_auto_diff_subset_parameters_thunk()
        if following == "O":
            self.pos += 1
            return self.demangle_auto_diff_self_reordering_reabstraction_thunk()
        if following == "V":
            self.pos += 1
            return self.demangle_auto_diff_function_or_simple_thunk("AutoDiffDerivativeVTableThunk")
        return self.demangle_auto_diff_function_or_simple_thunk("AutoDiffFunction")
    if char == "w":
        return {
            "b": lambda: Node("BackDeploymentThunk"),
            "B": lambda: Node("BackDeploymentFallback"),
            "c": lambda: Node("CoroFunctionPointer"),
            "d": lambda: Node("DefaultOverride"),
            "S": lambda: Node("HasSymbolQuery"),
        }.get(self.next_char(), lambda: None)()
    return None
demangle_generic_specialization_with_dropped_arguments
demangle_generic_specialization_with_dropped_arguments()

Tt<n>... -- a specialisation the optimiser dropped some arguments from.

Each t is followed by the index of a dropped argument, and a t with no digits after it means the first. The letter that follows the run says which sort of specialisation it is.

Source code in src/demangle/schemes/swift/_demangler.py
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
def demangle_generic_specialization_with_dropped_arguments(self):
    """`Tt<n>...` -- a specialisation the optimiser dropped some arguments from.

    Each `t` is followed by the index of a dropped argument, and a `t` with no digits
    after it means the first. The letter that follows the run says which sort of
    specialisation it is.
    """
    self.pos -= 1
    dropped = []
    while self.next_if("t"):
        index = self.natural()
        dropped.append(Node("DroppedArgument", index=0 if index is None else index + 1))
    kind = _DROPPED_ARGUMENT_SPECIALIZATIONS.get(self.next_char())
    if kind is None:
        return None
    return self.demangle_generic_specialization(kind, dropped)
demangle_function_specialization
demangle_function_specialization()

Tf -- a copy of a function specialised for particular argument values.

The parameter kinds are mangled inline but their payloads -- a closure's name, a propagated constant -- are on the stack, so a second pass walks the parameters backwards and fills each one in from what is left.

Source code in src/demangle/schemes/swift/_demangler.py
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
def demangle_function_specialization(self):
    """`Tf` -- a copy of a function specialised for particular argument values.

    The parameter *kinds* are mangled inline but their payloads -- a closure's name,
    a propagated constant -- are on the stack, so a second pass walks the parameters
    backwards and fills each one in from what is left.
    """
    specialized = self.demangle_spec_attributes("FunctionSignatureSpecialization")
    if specialized is not None and specialized.first.kind == "RepresentationChanged":
        # Nothing about the arguments changed, only how the function is represented,
        # so there are no parameters to read.
        return specialized
    while specialized is not None and not self.next_if("_"):
        specialized = self.add_child(
            specialized, self.demangle_func_spec_param("FunctionSignatureSpecializationParam")
        )
    if not self.next_if("n"):
        specialized = self.add_child(
            specialized, self.demangle_func_spec_param("FunctionSignatureSpecializationReturn")
        )
    if specialized is None:
        return None

    for parameter in reversed(specialized.children):
        if parameter.kind != "FunctionSignatureSpecializationParam" or not parameter.children:
            continue
        # A `p` parameter may carry several kinds; serve each, innermost first.
        fixed = len(parameter.children)
        filled = parameter
        for child in reversed(parameter.children[:fixed]):
            if child.kind != "FunctionSignatureSpecializationParamKind":
                continue
            kind = child.index
            if kind in _PARAM_KINDS_TAKING_TYPES:
                while True:
                    found = self.pop("Type")
                    if found is None:
                        break
                    filled = self.add_child(filled, found)
            elif kind == _PARAM_CONSTANT_PROP_KEY_PATH:
                filled = self.add_child(filled, self.pop("Type"))
                filled = self.add_child(filled, self.pop("Type"))
            elif kind == _PARAM_CONSTANT_PROP_STRUCT:
                # A type and *no* identifier: the struct's own name is the type, and
                # the value it was propagated from is the next kind's payload.
                filled = self.add_child(filled, self.pop("Type"))
                continue
            elif kind not in _PARAM_KINDS_WITH_PAYLOAD:
                continue
            # The node, not its text: the printer tells an identifier from an inline
            # payload by node kind.
            filled = self.add_child(filled, self.pop("Identifier"))
        if filled is None:
            return None
        parameter.children[fixed:] = reversed(parameter.children[fixed:])
    return specialized
demangle_spec_attributes
demangle_spec_attributes(kind)

The flags between a specialisation's letter and its pass number.

m is not in the reference; it is kept because the shipped runtime still holds symbols carrying it -- ...Tgm5 -- and dropping it would misread them rather than refuse them.

Source code in src/demangle/schemes/swift/_demangler.py
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
def demangle_spec_attributes(self, kind):
    """The flags between a specialisation's letter and its pass number.

    `m` is not in the reference; it is kept because the shipped runtime
    still holds symbols carrying it -- `...Tgm5` -- and dropping it would misread
    them rather than refuse them.
    """
    metatype_params_removed = self.next_if("m")
    serialized = self.next_if("q")
    async_removed = self.next_if("a")
    representation_changed = self.next_if("r")
    pass_id = ord(self.next_char() or "\x00") - ord("0")
    if pass_id < 0 or pass_id > 9:
        return None
    specialized = Node(kind)
    if metatype_params_removed:
        specialized.add(Node("MetatypeParamsRemoved"))
    if serialized:
        specialized.add(Node("IsSerialized"))
    if async_removed:
        specialized.add(Node("AsyncRemoved"))
    if representation_changed:
        specialized.add(Node("RepresentationChanged"))
    specialized.add(Node("SpecializationPassID", index=pass_id))
    return specialized
demangle_witness
demangle_witness()

W -- the tables and accessors that implement a conformance.

Source code in src/demangle/schemes/swift/_demangler.py
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
def demangle_witness(self):
    """`W` -- the tables and accessors that implement a conformance."""
    char = self.next_char()
    conformance_kind = _WITNESS_OF_CONFORMANCE.get(char)
    if conformance_kind is not None:
        return self.with_child(conformance_kind, self.pop_protocol_conformance())
    if char == "C":
        return self.with_child("EnumCase", self.pop(is_entity))
    if char == "V":
        return self.with_child("ValueWitnessTable", self.pop("Type"))
    if char == "v":
        directness = {"d": 0, "i": 1}.get(self.next_char())
        if directness is None:
            return None
        return self.with_children("FieldOffset", Node("Directness", index=directness), self.pop(is_entity))
    if char == "S":
        return self.with_child("ProtocolSelfConformanceWitnessTable", self.pop_protocol())
    if char in ("l", "L"):
        kind = "LazyProtocolWitnessTableAccessor" if char == "l" else "LazyProtocolWitnessTableCacheVariable"
        conformance = self.pop_protocol_conformance()
        found = self.pop("Type")
        return self.with_children(kind, found, conformance)
    if char == "t":
        name = self.pop(is_decl_name)
        conformance = self.pop_protocol_conformance()
        return self.with_children("AssociatedTypeMetadataAccessor", conformance, name)
    if char == "T":
        protocol = self.pop("Type")
        conforming = self.pop_assoc_type_path()
        conformance = self.pop_protocol_conformance()
        return self.with_children("AssociatedTypeWitnessTableAccessor", conformance, conforming, protocol)
    if char == "b":
        protocol = self.pop("Type")
        conformance = self.pop_protocol_conformance()
        return self.with_children("BaseWitnessTableAccessor", conformance, protocol)
    if char == "O":
        marker = self.next_char()
        # The enum-payload operations carry a case index after the marker.
        kind = _OUTLINED_ENUM_OPERATIONS.get(marker)
        if kind is not None:
            if marker == "g":
                signature = self.pop("DependentGenericSignature")
                if signature is not None:
                    return self.with_children(kind, self.pop("Type"), signature)
                return self.with_child(kind, self.pop("Type"))
            # The case index is read before the operand is popped, and goes last.
            case_index = self.index_as_node()
            signature = self.pop("DependentGenericSignature")
            if signature is not None:
                return self.with_children(kind, self.pop("Type"), signature, case_index)
            return self.with_children(kind, self.pop("Type"), case_index)
        kind = _OUTLINED_VALUE_WITNESSES.get(marker)
        if kind is None:
            return None
        signature = self.pop("DependentGenericSignature")
        if signature is not None:
            return self.with_children(kind, self.pop("Type"), signature)
        return self.with_child(kind, self.pop("Type"))
    if char in ("Z", "z"):
        declarations = Node("GlobalVariableOnceDeclList")
        named = []
        while self.pop("FirstElementMarker") is not None:
            identifier = self.pop(is_decl_name)
            if identifier is None:
                return None
            named.append(identifier)
        for identifier in reversed(named):
            declarations.add(identifier)
        context = self.pop_context()
        if context is None:
            return None
        kind = "GlobalVariableOnceFunction" if char == "Z" else "GlobalVariableOnceToken"
        return self.with_children(kind, context, declarations)
    if char == "J":
        return self.demangle_differentiability_witness()
    return None
demangle_special_type
demangle_special_type()

X -- types with a spelling of their own rather than a name.

Source code in src/demangle/schemes/swift/_demangler.py
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
def demangle_special_type(self):
    """`X` -- types with a spelling of their own rather than a name."""
    char = self.next_char()
    function_kind = _SPECIAL_FUNCTION_TYPES.get(char)
    if function_kind is not None:
        if char == "O" and self.next_if("o"):
            function_kind = "CalledAtMostOnceFunctionType"
        return self.pop_function_type(function_kind)
    wrapper = _SPECIAL_TYPE_WRAPPERS.get(char)
    if wrapper is not None:
        return self.make_type_or_none(self.with_child(wrapper, self.pop("Type")))
    if char in ("g", "G"):
        return self.demangle_extended_existential_shape(char)
    if char == "j":
        return self.demangle_symbolic_extended_existential_type()
    if char == "y":
        # A coroutine's yields, which `popFunctionType` takes after the parameters.
        empty = self.pop("EmptyList") is not None
        return self.with_child("YieldTypes", self.make_type(Node("Tuple")) if empty else self.pop("Type"))
    if char == "z":
        inner = self.next_char()
        if inner == "B":
            return self.pop_function_type("ObjCBlock", True)
        if inner == "C":
            return self.pop_function_type("CFunctionPointer", True)
        return None
    if char in ("M", "m"):
        kind = "Metatype" if char == "M" else "ExistentialMetatype"
        representation = self.demangle_metatype_representation()
        found = self.pop("Type")
        return self.make_type_or_none(self.with_children(kind, representation, found))
    if char == "P":
        requirements = self.demangle_constrained_existential_requirement_list()
        base = self.pop("Type")
        return self.make_type_or_none(self.with_children("ConstrainedExistential", base, requirements))
    if char == "c":
        superclass = self.pop("Type")
        protocols = self.demangle_protocol_list()
        return self.make_type_or_none(self.with_children("ProtocolListWithClass", protocols, superclass))
    if char == "l":
        return self.make_type_or_none(self.with_child("ProtocolListWithAnyObject", self.demangle_protocol_list()))
    if char in ("X", "x"):
        return self._sil_box_type(char)
    if char == "Y":
        return self.demangle_any_generic_type("OtherNominalType")
    if char == "Z":
        types = self.pop_type_list()
        name = self.pop("Identifier")
        parent = self.pop_context()
        found = Node("AnonymousContext")
        found = self.add_child(found, name)
        found = self.add_child(found, parent)
        return self.add_child(found, types)
    if char == "e":
        return self.make_type(Node("ErrorType"))
    if char == "S":
        return self._sugared_type()
    return None
pop_associated_conformance_subject
pop_associated_conformance_subject()

What an associated conformance is about: a parameter, or a path to one.

mini.Seq.A is the bare parameter x, not an associated type path, and reading it as a path would refuse the name.

Source code in src/demangle/schemes/swift/_demangler.py
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
def pop_associated_conformance_subject(self):
    """What an associated conformance is *about*: a parameter, or a path to one.

    `mini.Seq.A` is the bare parameter `x`, not an associated type path, and reading
    it as a path would refuse the name.
    """
    found = self.pop("Type")
    if found is not None:
        if self._is_generic_param_type(found):
            return found
        self.stack.append(found)
    return self.pop_assoc_type_path()
demangle_integer_type
demangle_integer_type()

$ -- a value bound to a value generic parameter, Slab<2, Int>.

The value is an index, negated when an n precedes it, and it stands where a type would: $1_ is 2 and $n3_ is -4.

Source code in src/demangle/schemes/swift/_demangler.py
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
def demangle_integer_type(self):
    """`$` -- a value bound to a value generic parameter, `Slab<2, Int>`.

    The value is an index, negated when an `n` precedes it, and it stands where a
    type would: `$1_` is `2` and `$n3_` is `-4`.
    """
    negative = self.next_if("n")
    value = self.index()
    if value is None:
        return None
    kind = "NegativeInteger" if negative else "Integer"
    return self.make_type(Node(kind, index=-value if negative else value))
demangle_accessor
demangle_accessor(child)

A variable or subscript may be followed by which accessor of it this is.

Source code in src/demangle/schemes/swift/_demangler.py
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
def demangle_accessor(self, child):
    """A variable or subscript may be followed by which accessor of it this is."""
    char = self.next_char()
    if char == "p":
        # The pseudo-accessor: this names the variable itself.
        return child
    if char == "a":
        kind = _MUTABLE_ADDRESSORS.get(self.next_char())
    elif char == "l":
        kind = _ADDRESSORS.get(self.next_char())
    else:
        kind = _ACCESSORS.get(char)
    if kind is None:
        return None
    return self.with_child(kind, child)
demangle_function_entity
demangle_function_entity()

f -- an initialiser, deinitialiser, closure or similar.

Source code in src/demangle/schemes/swift/_demangler.py
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
def demangle_function_entity(self):
    """`f` -- an initialiser, deinitialiser, closure or similar."""
    char = self.next_char()
    if char == "m":
        return self.demangle_entity("Macro")
    if char == "M":
        return self.demangle_macro_expansion()
    if char == "p":
        return self.demangle_entity("GenericTypeParamDecl")
    found = _FUNCTION_ENTITIES.get(char)
    if found is None:
        return None
    args, kind = found

    name_or_index = None
    parameters = None
    labels = None
    if args == "type-and-maybe-private-name":
        name_or_index = self.pop("PrivateDeclName")
        parameters = self.pop("Type")
        labels = self.pop_function_param_labels(parameters)
    elif args == "type-and-index":
        name_or_index = self.index_as_node()
        parameters = self.pop("Type")
    elif args == "index":
        name_or_index = self.index_as_node()

    entity = self.with_child(kind, self.pop_context())
    if args == "index":
        entity = self.add_child(entity, name_or_index)
    elif args == "type-and-maybe-private-name":
        self.add_child(entity, labels)
        entity = self.add_child(entity, parameters)
        self.add_child(entity, name_or_index)
    elif args == "type-and-index":
        entity = self.add_child(entity, name_or_index)
        entity = self.add_child(entity, parameters)
    return entity
demangle_generic_signature
demangle_generic_signature(has_param_counts)

l or r -- the <T, U where ...> a generic declaration carries.

r spells out how many parameters each depth has; l is the shorthand for one parameter at one depth, which is what most generic declarations look like.

Source code in src/demangle/schemes/swift/_demangler.py
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
def demangle_generic_signature(self, has_param_counts):
    """`l` or `r` -- the `<T, U where ...>` a generic declaration carries.

    `r` spells out how many parameters each depth has; `l` is the shorthand for one
    parameter at one depth, which is what most generic declarations look like.
    """
    signature = Node("DependentGenericSignature")
    if has_param_counts:
        while not self.next_if("l"):
            if self.next_if("z"):
                count = 0
            else:
                count = self.index()
                if count is None:
                    return None
                count += 1
            signature.add(Node("DependentGenericParamCount", index=count))
    else:
        signature.add(Node("DependentGenericParamCount", index=1))
    counts = len(signature.children)
    while True:
        requirement = self.pop(is_requirement)
        if requirement is None:
            break
        signature.add(requirement)
    signature.children[counts:] = reversed(signature.children[counts:])
    return signature
demangle_generic_requirement
demangle_generic_requirement()

R -- one where clause.

The operator says two things at once: what kind of constraint it is, and how the constrained type is written. The default -- no letter at all -- is the commonest pair, a generic parameter conforming to a protocol.

Source code in src/demangle/schemes/swift/_demangler.py
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
def demangle_generic_requirement(self):
    """`R` -- one `where` clause.

    The operator says two things at once: what kind of constraint it is, and how the
    constrained type is written. The default -- no letter at all -- is the commonest
    pair, a generic parameter conforming to a protocol.
    """
    found = _GENERIC_REQUIREMENTS.get(self.peek())
    inverted = None
    if found is None:
        constraint, type_kind = "protocol", "generic"
    else:
        constraint, type_kind = found
        self.pos += 1
        if constraint == "inverse":
            inverted = self.index_as_node()
            if inverted is None:
                return None

    if type_kind == "generic":
        constrained = self.make_type_or_none(self.demangle_generic_param_index())
    elif type_kind == "assoc":
        constrained = self.demangle_associated_type_simple(self.demangle_generic_param_index())
        self.add_substitution(constrained)
    elif type_kind == "compound-assoc":
        constrained = self.demangle_associated_type_compound(self.demangle_generic_param_index())
        self.add_substitution(constrained)
    else:
        constrained = self.pop("Type")

    if constraint == "inverse":
        return self.with_children("DependentGenericInverseConformanceRequirement", constrained, inverted)
    if constraint == "value-marker":
        # `let N: Int`: the marker carries the parameter's *type* as well.
        return self.with_children("DependentGenericParamValueMarker", constrained, self.pop("Type"))
    if constraint == "pack-marker":
        return self.with_child("DependentGenericParamPackMarker", constrained)
    if constraint == "protocol":
        return self.with_children("DependentGenericConformanceRequirement", constrained, self.pop_protocol())
    if constraint == "base-class":
        return self.with_children("DependentGenericConformanceRequirement", constrained, self.pop("Type"))
    if constraint == "same-type":
        return self.with_children("DependentGenericSameTypeRequirement", constrained, self.pop("Type"))
    if constraint == "same-shape":
        return self.with_children("DependentGenericSameShapeRequirement", constrained, self.pop("Type"))
    return self._layout_requirement(constrained)

The Swift 3 mangling

Reading the Swift 3 mangling.

Swift changed its mangling in 4.0; _T followed by anything but 0 is the older one, and it is still what the ObjC runtime holds for a Swift class, so it turns up in any Apple binary with interop in it. The compiler keeps a separate demangler for it -- lib/Demangling/OldDemangler.cpp -- and this is a port of that.

It is a much easier read than the current one. The old mangling is prefix, so this is plain recursive descent: demangle_entity reads the context, then the name, then the type, in the order they are written. There is no node stack and no word table. What it does share is the tree it builds, so _printer.py spells the result with no idea which mangling it came from.

Two things are its own:

  • Substitutions are positional. S_, S0_, S1_ index a list of everything nominal seen so far, and a handful of one-letter codes -- Sa, SS, Sq -- stand for the standard library's common types, as they do in the current mangling.
  • Punycode is marked by X in front of the length, not by a 00 prefix, and the encoding is the same one _punycode.py implements.

OldDemangler

OldDemangler(text, *, byte_mode=False)

The reference's OldDemangler. None means failure, as nullptr does there.

Source code in src/demangle/schemes/swift/_old_demangler.py
184
185
186
187
188
189
190
191
192
193
def __init__(self, text, *, byte_mode=False):
    self.has_non_ascii = not text.isascii()
    if self.has_non_ascii and not byte_mode:
        try:
            text = text.encode("utf-8").decode("latin-1")
        except UnicodeEncodeError:
            text = ""
    self.reader = _Reader(text)
    #: Everything nominal seen so far. `S_`, `S0_`, `S1_` index this list.
    self.substitutions = []
demangle_identifier
demangle_identifier(depth, kind=None)

X in front of the length marks a punycoded name; o marks an operator.

Source code in src/demangle/schemes/swift/_old_demangler.py
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
def demangle_identifier(self, depth, kind=None):
    """`X` in front of the length marks a punycoded name; `o` marks an operator."""
    reader = self.reader
    if not reader:
        return None
    punycoded = reader.next_if("X")

    is_operator = False
    if reader.next_if("o"):
        is_operator = True
        if kind is not None:
            return None
        kind = {"p": "PrefixOperator", "P": "PostfixOperator", "i": "InfixOperator"}.get(reader.next())
        if kind is None:
            return None
    if kind is None:
        kind = "Identifier"

    length = self._natural()
    if length is None:
        return None
    identifier = reader.slice(length)
    if identifier is None:
        return None

    if punycoded:
        try:
            identifier = punycode_decode(identifier)
        except PunycodeError:
            return None
    if not identifier:
        return None
    if not punycoded and self.has_non_ascii and not identifier.isascii():
        try:
            identifier = identifier.encode("latin-1").decode("utf-8")
        except UnicodeError:
            return None

    if is_operator:
        spelled = []
        for char in identifier:
            if ord(char) > 127:
                spelled.append(char)
                continue
            if not ("a" <= char <= "z"):
                return None
            mapped = _OPERATOR_CHARS[ord(char) - ord("a")]
            if mapped == " ":
                return None
            spelled.append(mapped)
        identifier = "".join(spelled)

    return Node(kind, text=identifier)
demangle_protocol_name_impl
demangle_protocol_name_impl(depth)

S is ambiguous here: it may substitute the protocol or its context, so the two readings have to be told apart by what comes back.

Source code in src/demangle/schemes/swift/_old_demangler.py
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
def demangle_protocol_name_impl(self, depth):
    """`S` is ambiguous here: it may substitute the protocol or its context, so the
    two readings have to be told apart by what comes back."""
    if depth > MAX_DEPTH:
        return None
    reader = self.reader
    if reader.next_if("S"):
        found = self.demangle_substitution_index(depth + 1)
        if found is None:
            return None
        if found.kind == "Protocol":
            return found
        if found.kind != "Module":
            return None
        return self.demangle_protocol_name_given_context(found, depth + 1)
    if reader.next_if("s"):
        return self.demangle_protocol_name_given_context(Node("Module", text=STDLIB_NAME), depth + 1)
    return self.demangle_declaration_name("Protocol", depth + 1)
demangle_entity
demangle_entity(depth)

[Z] <kind> <context> <name> [<type>].

The old mangling wrote an accessor as the outer node with the variable inside it, which is the reverse of what the printer expects, so wrap rebuilds it the way the current mangling produces.

Source code in src/demangle/schemes/swift/_old_demangler.py
514
515
516
517
518
519
520
521
522
523
524
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
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
def demangle_entity(self, depth):
    """`[Z] <kind> <context> <name> [<type>]`.

    The old mangling wrote an accessor as the *outer* node with the variable inside
    it, which is the reverse of what the printer expects, so `wrap` rebuilds it the
    way the current mangling produces.
    """
    if depth > MAX_DEPTH:
        return None
    reader = self.reader
    is_static = reader.next_if("Z")

    basic = None
    for letter, kind in (("F", "Function"), ("v", "Variable"), ("I", "Initializer"), ("i", "Subscript")):
        if reader.next_if(letter):
            basic = kind
            break
    if basic is None:
        return self.demangle_nominal_type(depth + 1)

    context = self.demangle_context(depth + 1)
    if context is None:
        return None

    kind = None
    has_type = True
    wrap = False
    name = None

    simple = _ENTITY_WITHOUT_TYPE.get(reader.peek())
    if simple is not None:
        reader.next()
        kind, has_type = simple, False
    elif reader.next_if("C"):
        kind = "Allocator"
    elif reader.next_if("c"):
        kind = "Constructor"
    elif reader.peek() in ("a", "l"):
        table = _MUTABLE_ADDRESSORS if reader.next() == "a" else _ADDRESSORS
        kind = table.get(reader.next())
        if kind is None:
            return None
        wrap = True
        name = self.demangle_decl_name(depth + 1)
        if name is None:
            return None
    elif reader.peek() in _ACCESSORS:
        wrap = True
        kind = _ACCESSORS[reader.next()]
        name = self.demangle_decl_name(depth + 1)
        if name is None:
            return None
    elif reader.peek() in ("U", "u"):
        kind = "ExplicitClosure" if reader.next() == "U" else "ImplicitClosure"
        name = self._index_as_node()
        if name is None:
            return None
    elif basic == "Initializer":
        if reader.next_if("A"):
            kind = "DefaultArgumentInitializer"
            name = self._index_as_node()
            if name is None:
                return None
        elif reader.next_if("i"):
            kind = "Initializer"
        else:
            return None
        has_type = False
    else:
        kind = basic
        name = self.demangle_decl_name(depth + 1)
        if name is None:
            return None

    entity = Node(kind)
    if wrap:
        if name is None:
            # Unreachable; guarded because the reference dereferences it unguarded.
            return None
        # The old mangling spells a subscript's accessor `subscript`; that name is
        # dropped.
        is_subscript = False
        if name.kind == "Identifier" and name.text == "subscript":
            is_subscript = True
            name = None
        elif name.kind == "PrivateDeclName" and len(name.children) > 1 and name.child(1).text == "subscript":
            is_subscript = True
            name = Node("PrivateDeclName", children=[name.first])

        wrapped = Node("Subscript" if is_subscript else "Variable", children=[context])
        if not is_subscript:
            wrapped.add(name)
        if has_type:
            found = self.demangle_type(depth + 1)
            if found is None:
                return None
            wrapped.add(found)
        if is_subscript and name is not None:
            wrapped.add(name)
        entity.add(wrapped)
    else:
        entity.add(context)
        if name is not None:
            entity.add(name)
        if has_type:
            found = self.demangle_type(depth + 1)
            if found is None:
                return None
            entity.add(found)

    return Node("Static", children=[entity]) if is_static else entity
demangle_constrained_type
demangle_constrained_type(depth)

A constrained type is always a parameter or one of its associated types, so the q that would introduce a generic parameter elsewhere is left off.

Source code in src/demangle/schemes/swift/_old_demangler.py
695
696
697
698
699
700
701
702
703
704
705
706
707
def demangle_constrained_type(self, depth):
    """A constrained type is always a parameter or one of its associated types, so
    the `q` that would introduce a generic parameter elsewhere is left off."""
    reader = self.reader
    if reader.next_if("w"):
        found = self.demangle_associated_type_simple(depth + 1)
    elif reader.next_if("W"):
        found = self.demangle_associated_type_compound(depth + 1)
    else:
        found = self.demangle_generic_param_index(depth + 1)
    if found is None:
        return None
    return Node("Type", children=[found])

Printer

Spelling a Swift demangling tree.

A port of the compiler's lib/Demangling/NodePrinter.cpp. It is a separate pass from the demangler for a reason that shows up everywhere in it: how a node is spelled depends on where it sits. The same Class node is Foundation.NSData as a type and NSData as the prefix of a method's name; a Type under a function's result gets -> before it and the same node elsewhere does not.

Two mechanisms carry that context, and both are the reference's:

  • print returns a node. When an entity's context cannot be printed as a prefix -- because it is an extension, or a local declaration -- printEntity prints the entity first and hands the context back, and the caller spells it as in <context>. That is why the main function has a return value at all.
  • as_prefix_context. The same node prints differently when it is the Abc. in Abc.def(), and the flag says which it is.

The options are the ones swift-demangle uses with no flags: sugar on ([Int] rather than Array<Int>) and every "display" option left at its default of on. They are constants here rather than a parameter because a demangling that does not match the toolchain's is not useful, and every one of them changes the answer.

Printer

Printer(options=DEFAULT_OPTIONS)

The reference's NodePrinter, and the options it consults.

Source code in src/demangle/schemes/swift/_printer.py
256
257
258
259
260
261
262
263
264
def __init__(self, options=DEFAULT_OPTIONS):
    self.options = options
    #: Whether `specialized ` has been written; the reference latches it so nested
    #: specialisation layers say it once.
    self._said_specialized = False
    self.out = []
    #: Characters written so far. `print_entity` writes a `.` only if the last call
    #: produced characters, as the reference measures it.
    self.length = 0
print_context
print_context(context)

Whether a context is worth naming.

The standard library and __C are named; a module the debugger invented is not worth hiding, and the reference names it too. This returns False only for the HidingCurrentModule option, which is unset here -- but the shape is kept because the caller's control flow depends on it.

Source code in src/demangle/schemes/swift/_printer.py
303
304
305
306
307
308
309
310
311
def print_context(self, context):
    """Whether a context is worth naming.

    The standard library and `__C` are named; a module the debugger invented is not
    worth hiding, and the reference names it too. This returns `False` only for the
    `HidingCurrentModule` option, which is unset here -- but the shape is kept because
    the caller's control flow depends on it.
    """
    return True
find_sugar
find_sugar(node)

Which of the four sugared spellings, if any, a bound generic type has.

Source code in src/demangle/schemes/swift/_printer.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
def find_sugar(self, node):
    """Which of the four sugared spellings, if any, a bound generic type has."""
    if len(node.children) == 1 and node.kind == "Type":
        return self.find_sugar(node.first)
    if len(node.children) != 2:
        return None
    if node.kind not in ("BoundGenericEnum", "BoundGenericStructure"):
        return None
    unbound = node.first.first
    arguments = node.child(1)
    if len(unbound.children) < 2 or not _is_swift_module(unbound.first):
        return None
    name = unbound.child(1)
    if node.kind == "BoundGenericEnum":
        if _is_identifier(name, "Optional") and len(arguments.children) == 1:
            return "optional"
        if _is_identifier(name, "ImplicitlyUnwrappedOptional") and len(arguments.children) == 1:
            return "unwrapped-optional"
        return None
    if _is_identifier(name, "Array") and len(arguments.children) == 1:
        return "array"
    if _is_identifier(name, "Dictionary") and len(arguments.children) == 2:
        return "dictionary"
    return None
print_function_parameters
print_function_parameters(
    labels, parameters, depth, show_types=True
)

The parameter list, with or without the types in it.

Without them the list keeps its shape and its labels and nothing else, so (Swift.Int) -> Swift.UInt is (_:) and a labelled two-parameter function is (from:to:) -- which is how a Swift programmer refers to it. A parameter that carried no label at all is written _:, and there are no separators, because (_:_:) is one token in that spelling rather than a list.

Source code in src/demangle/schemes/swift/_printer.py
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
def print_function_parameters(self, labels, parameters, depth, show_types=True):
    """The parameter list, with or without the types in it.

    Without them the list keeps its shape and its labels and nothing else, so
    `(Swift.Int) -> Swift.UInt` is `(_:)` and a labelled two-parameter function is
    `(from:to:)` -- which is how a Swift programmer refers to it. A parameter that
    carried no label at all is written `_:`, and there are no separators, because
    `(_:_:)` is one token in that spelling rather than a list.
    """
    if parameters.kind not in ("ArgumentTuple", "YieldTypes"):
        raise _Invalid
    found = parameters.first.first
    if found.kind != "Tuple":
        # One unnamed parameter, which is not written as a tuple.
        if not show_types:
            self.write("(_:)")
            return
        self.write("(")
        self.print(found, depth + 1)
        self.write(")")
        return

    has_labels = labels is not None and labels.children
    self.write("(")
    for at, parameter in enumerate(found.children):
        if at and show_types:
            self.write(", ")
        if has_labels:
            label = labels.child(at)
            self.write(label.text if label.kind == "Identifier" else "_")
            self.write(":")
            if show_types:
                self.write(" ")
        elif not show_types:
            name = _child_of_kind(parameter, "TupleElementName")
            self.write(f"{name.text}:" if name is not None else "_:")
        if show_types:
            self.print(parameter, depth + 1)
    self.write(")")
print_impl_function_type
print_impl_function_type(node, depth)

A lowered SIL type: (params) -> (results), with the attributes in front.

Source code in src/demangle/schemes/swift/_printer.py
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
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
552
553
554
555
556
557
558
559
560
def print_impl_function_type(self, node, depth):
    """A lowered SIL type: `(params) -> (results)`, with the attributes in front."""
    pattern_substitutions = None
    invocation_substitutions = None
    sending_result = None
    state = 0  # 0 attributes, 1 inputs, 2 results

    def transition(wanted):
        nonlocal state
        while state != wanted:
            if state == 0:
                if pattern_substitutions is not None:
                    self.write("@substituted ")
                    self.print(pattern_substitutions.first, depth + 1)
                    self.write(" ")
                self.write("(")
            elif state == 1:
                self.write(") -> ")
                if sending_result is not None:
                    self.print(sending_result, depth + 1)
                    self.write(" ")
                self.write("(")
            state += 1

    for child in node.children:
        if child.kind == "ImplParameter":
            if state == 1:
                self.write(", ")
            transition(1)
            self.print(child, depth + 1)
        elif child.kind in ("ImplResult", "ImplYield", "ImplErrorResult"):
            if state == 2:
                self.write(", ")
            transition(2)
            self.print(child, depth + 1)
        elif child.kind == "ImplPatternSubstitutions":
            pattern_substitutions = child
        elif child.kind == "ImplInvocationSubstitutions":
            invocation_substitutions = child
        elif child.kind == "ImplSendingResult":
            sending_result = child
        else:
            self.print(child, depth + 1)
            self.write(" ")
    transition(2)
    self.write(")")

    if pattern_substitutions is not None:
        self.write(" for <")
        self.print_children(pattern_substitutions.child(1), depth)
        self.write(">")
    if invocation_substitutions is not None:
        self.write(" for <")
        self.print_children(invocation_substitutions.first, depth)
        self.write(">")
print_generic_signature
print_generic_signature(node, depth)

<A, B where A: P>, with the parameter counts turned back into names.

Source code in src/demangle/schemes/swift/_printer.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
def print_generic_signature(self, node, depth):
    """`<A, B where A: P>`, with the parameter counts turned back into names."""
    self.write("<")
    count = len(node.children)

    parameters = 0
    while parameters < count and node.child(parameters).kind == "DependentGenericParamCount":
        parameters += 1

    first_requirement = parameters
    while first_requirement < count:
        child = node.child(first_requirement)
        if child.kind == "Type":
            child = child.first
        if child.kind not in ("DependentGenericParamPackMarker", "DependentGenericParamValueMarker"):
            break
        first_requirement += 1

    def is_pack(at_depth, at_index):
        for pack_at in range(parameters, first_requirement):
            child = node.child(pack_at)
            if child.kind != "DependentGenericParamPackMarker":
                continue
            child = child.first
            if child.kind != "Type":
                continue
            child = child.first
            if child.kind != "DependentGenericParamType":
                continue
            # The reference compares index against child 0.
            if at_index == child.first.index and at_depth == child.child(1).index:
                return True
        return False

    def value_type(at_depth, at_index):
        """`(is a value parameter, the type to spell after it or None)`.

        The reference reads *both* the parameter and its type out of the marker's
        first child, which is the `Type` wrapping the parameter alone -- so the type
        is a child that is not there, and it prints `let A` with no `: Int` after it.
        Reading past the end is null there and would be an IndexError here, which
        would refuse the whole name; the two are the same answer and this says so.
        """
        for value_at in range(parameters, first_requirement):
            child = node.child(value_at)
            if child.kind != "DependentGenericParamValueMarker":
                continue
            child = child.first
            if child.kind != "Type":
                continue
            parameter = child.first
            if parameter.kind != "DependentGenericParamType":
                continue
            if at_index == parameter.first.index and at_depth == parameter.child(1).index:
                return True, (child.child(1) if len(child.children) > 1 else None)
        return False, None

    for at_depth in range(parameters):
        if at_depth:
            self.write("><")
        for index in range(node.child(at_depth).index):
            if index:
                self.write(", ")
            if index >= 128:
                # Only reachable for a malformed count; the reference caps it too.
                self.write("...")
                break
            if is_pack(at_depth, index):
                self.write("each ")
            is_value, found = value_type(at_depth, index)
            if is_value:
                self.write("let ")
            self.write(generic_parameter_name(at_depth, index))
            if found is not None:
                self.write(": ")
                self.print(found, depth + 1)

    if first_requirement != count and self.options.display_where_clauses:
        self.write(" where ")
        for at in range(first_requirement, count):
            if at > first_requirement:
                self.write(", ")
            self.print(node.child(at), depth + 1)
    self.write(">")
print_function_sig_specialization_params
print_function_sig_specialization_params(node, depth)

One parameter of a function-signature specialisation.

Two cursors, as the reference has: at walks the children looking for the next kind, and reads the payload the mangling wrote inline beside it -- an integer's digits, a string's encoding. argument walks the same children skipping every kind and payload, and serves the operands that were popped off the stack: the types, the identifiers.

One cursor would not do it, because a single parameter can carry several kinds -- pSSi3Si0 is a propagated struct, an integer, a struct, an integer -- and their two sorts of operand are interleaved differently from the kinds themselves.

Source code in src/demangle/schemes/swift/_printer.py
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
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
def print_function_sig_specialization_params(self, node, depth):
    """One parameter of a function-signature specialisation.

    Two cursors, as the reference has: `at` walks the children looking for the next
    *kind*, and reads the payload the mangling wrote inline beside it -- an integer's
    digits, a string's encoding. `argument` walks the same children skipping every
    kind and payload, and serves the operands that were popped off the stack: the
    types, the identifiers.

    One cursor would not do it, because a single parameter can carry several kinds --
    `pSSi3Si0` is a propagated struct, an integer, a struct, an integer -- and their
    two sorts of operand are interleaved differently from the kinds themselves.
    """
    at = 0
    argument = 0
    end = len(node.children)
    while at < end:
        child = node.child(at)
        if child.index is None:
            return
        kind = child.index
        if kind in (_PARAM_BOX_TO_VALUE, _PARAM_BOX_TO_STACK, _PARAM_IN_OUT_TO_OUT):
            self.print(node.child(at), depth + 1)
            at += 1
        elif kind in (_PARAM_CONSTANT_PROP_FUNCTION, _PARAM_CONSTANT_PROP_GLOBAL):
            self.write("[")
            self.print(node.child(at), depth + 1)
            at += 1
            self.write(" : ")
            argument = self.print_next_param_child(node, argument, kind, depth)
            self.write("]")
        elif kind in (_PARAM_CONSTANT_PROP_INTEGER, _PARAM_CONSTANT_PROP_FLOAT):
            if at + 2 > end:
                return
            self.write("[")
            self.print(node.child(at), depth + 1)
            at += 1
            self.write(" : ")
            self.print(node.child(at), depth + 1)
            at += 1
            self.write("]")
        elif kind == _PARAM_CONSTANT_PROP_STRING:
            if at + 2 > end:
                return
            self.write("[")
            self.print(node.child(at), depth + 1)
            at += 1
            self.write(" : ")
            self.print(node.child(at), depth + 1)
            at += 1
            self.write("'")
            argument = self.print_next_param_child(node, argument, kind, depth)
            self.write("'")
            self.write("]")
        elif kind == _PARAM_CONSTANT_PROP_KEY_PATH:
            self.write("[")
            self.print(node.child(at), depth + 1)
            at += 1
            self.write(" : ")
            argument = self.print_next_param_child(node, argument, kind, depth)
            self.write("<")
            argument = self.print_next_param_child(node, argument, kind, depth)
            self.write(",")
            argument = self.print_next_param_child(node, argument, kind, depth)
            self.write(">]")
        elif kind == _PARAM_CONSTANT_PROP_STRUCT:
            self.write("[")
            self.print(node.child(at), depth + 1)
            at += 1
            self.write(" : ")
            argument = self.print_next_param_child(node, argument, kind, depth)
            self.write("]")
        elif kind in (_PARAM_CLOSURE_PROP, _PARAM_ESCAPING_CLOSURE_PROP):
            if at + 2 > end:
                return
            self.write("[")
            self.print(node.child(at), depth + 1)
            at += 1
            self.write(" : ")
            self.print(node.child(at), depth + 1)
            at += 1
            self.write(", Argument Types : [")
            while at < end:
                following = node.child(at)
                if following.kind != "Type":
                    break
                self.print(following, depth + 1)
                at += 1
                if at < end and node.child(at).text is not None:
                    self.write(", ")
            self.write("]")
        elif kind == _PARAM_CLOSURE_PROP_PREVIOUS_ARG:
            if at + 2 > end:
                return
            self.write("[")
            self.print(node.child(at), depth + 1)
            at += 1
            self.write(" ")
            self.print(node.child(at), depth + 1)
            at += 1
            self.write("]")
        else:
            self.print(node.child(at), depth + 1)
            at += 1
print_next_param_child
print_next_param_child(node, argument, kind, depth)

The next operand of a parameter: what was popped rather than written inline.

Returns where the operand cursor reached, so the caller can pass it back in -- several of the kinds take more than one.

Source code in src/demangle/schemes/swift/_printer.py
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
def print_next_param_child(self, node, argument, kind, depth):
    """The next operand of a parameter: what was popped rather than written inline.

    Returns where the operand cursor reached, so the caller can pass it back in --
    several of the kinds take more than one.
    """
    end = len(node.children)
    while argument < end:
        child = node.child(argument)
        argument += 1
        if child.kind in (
            "FunctionSignatureSpecializationParamKind",
            "FunctionSignatureSpecializationParamPayload",
        ):
            continue
        if kind in (_PARAM_CONSTANT_PROP_FUNCTION, _PARAM_CONSTANT_PROP_GLOBAL):
            # A mangled name; the reference demangles it, falling back to the raw text.
            self.write(_spell_payload(child.text))
        elif kind == _PARAM_CONSTANT_PROP_STRING and child.text and child.text.startswith("_"):
            # `_` escapes a string constant that would otherwise start with a digit.
            self.write(child.text[1:])
        else:
            self.print(child, depth + 1)
        return argument
    return argument
print
print(node, depth, as_prefix_context=False)

Spell node, returning a context to be printed after it, or None.

The return value is what makes in <context> possible: an entity whose context cannot be written as a prefix prints itself first and hands the context back.

Source code in src/demangle/schemes/swift/_printer.py
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
def print(self, node, depth, as_prefix_context=False):
    """Spell `node`, returning a context to be printed after it, or `None`.

    The return value is what makes ` in <context>` possible: an entity whose context
    cannot be written as a prefix prints itself first and hands the context back.
    """
    if depth > MAX_DEPTH:
        self.write("<<too complex>>")
        return None
    if node is None:
        self.write("<null node pointer>")
        return None

    kind = node.kind

    prefix = _PREFIX_THEN_FIRST_CHILD.get(kind)
    if prefix is not None:
        self.write(prefix)
        self.print(node.first, depth + 1)
        return None

    literal = _JUST_TEXT.get(kind)
    if literal is not None:
        if kind in _THUNK_LEADS and not self.options.shorten_thunk:
            # The short form of a wrapper is nothing: what it leads is the symbol it
            # wraps.
            return None
        self.write(literal)
        return None

    entity = _ENTITY_KINDS.get(kind)
    if entity is not None:
        style, has_name, extra_name, overwrite = entity
        return self.print_entity(
            node,
            depth,
            as_prefix_context,
            style,
            has_name,
            extra_name,
            overwrite_name=overwrite,
        )

    handler = _HANDLERS.get(kind)
    if handler is not None:
        return handler(self, node, depth, as_prefix_context)

    # Anything left has no spelling; the reference asserts.
    raise _Invalid
print_entity
print_entity(
    entity,
    depth,
    as_prefix_context,
    style,
    has_name,
    extra_name="",
    extra_index=-1,
    overwrite_name="",
)

Spell a declaration: its context, its name, and its type.

The context goes in front -- Foo.bar() -- unless it cannot, in which case it is returned for the caller to spell as in Foo. It cannot when the name has spaces in it (closure #1 in ... reads wrongly the other way round) or when the context is itself something that needs a type printed.

Source code in src/demangle/schemes/swift/_printer.py
873
874
875
876
877
878
879
880
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
906
907
908
909
910
911
912
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
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
def print_entity(
    self,
    entity,
    depth,
    as_prefix_context,
    style,
    has_name,
    extra_name="",
    extra_index=-1,
    overwrite_name="",
):
    """Spell a declaration: its context, its name, and its type.

    The context goes in front -- `Foo.bar()` -- unless it cannot, in which case it is
    returned for the caller to spell as ` in Foo`. It cannot when the name has spaces
    in it (`closure #1 in ...` reads wrongly the other way round) or when the context
    is itself something that needs a type printed.
    """
    generic_arguments = None
    if entity.kind == "BoundGenericFunction":
        generic_arguments = entity.child(1)
        entity = entity.first

    multi_word = " " in extra_name
    local_name = has_name and entity.child(1).kind == "LocalDeclName"
    if local_name:
        multi_word = True

    if as_prefix_context and (style != "none" or multi_word):
        # A context printed as a prefix cannot carry a type, so hand it back.
        return entity

    postfix_context = None
    context = entity.first
    if self.print_context(context):
        if multi_word:
            postfix_context = context
        else:
            before = self.length
            postfix_context = self.print(context, depth + 1, True)
            if self.length != before:
                self.write(".")

    if has_name or overwrite_name:
        if extra_name and multi_word:
            self.write(extra_name)
            if extra_index >= 0:
                self.write(str(extra_index))
            self.write(" of ")
            extra_name = ""
            extra_index = -1
        before = self.length
        if overwrite_name:
            self.write(overwrite_name)
        else:
            name = entity.child(1)
            if name.kind != "PrivateDeclName":
                self.print(name, depth + 1)
            private = _child_of_kind(entity, "PrivateDeclName")
            if private is not None:
                self.print(private, depth + 1)
        if self.length != before and extra_name:
            self.write(".")

    if extra_name:
        self.write(extra_name)
        if extra_index >= 0:
            self.write(str(extra_index))

    if style != "none":
        found = _child_of_kind(entity, "Type")
        if found is None:
            raise _Invalid
        found = found.first
        if style == "function":
            # If what is there is not actually a function type, fall back to a colon.
            inner = found
            while inner.kind == "DependentGenericType":
                inner = inner.child(1).first
            if inner.kind not in (
                "FunctionType",
                "NoEscapeFunctionType",
                "UncurriedFunctionType",
                "CFunctionPointer",
                "ThinFunctionType",
                "CalledOnceFunctionType",
                "CalledAtMostOnceFunctionType",
            ):
                style = "colon"
        if style == "colon":
            if self.options.display_entity_types:
                self.write(" : ")
                self.print_entity_type(entity, found, generic_arguments, depth)
        else:
            if multi_word or _needs_space_before_type(found):
                self.write(" ")
            self.print_entity_type(entity, found, generic_arguments, depth)

    if not as_prefix_context and postfix_context is not None:
        if entity.kind in (
            "DefaultArgumentInitializer",
            "Initializer",
            "PropertyWrapperBackingInitializer",
            "PropertyWrappedFieldInitAccessor",
            "PropertyWrapperInitFromProjectedValue",
        ):
            self.write(" of ")
        else:
            self.write(" in ")
        self.print(postfix_context, depth + 1)
        postfix_context = None
    return postfix_context

Symbolic references

demangle.schemes.swift.symbolic

Symbolic references: the part of the Swift mangling that points into the binary.

A mangled name in a Swift binary's metadata is not always self-contained. Where a name would have to spell a type the image already describes, the compiler writes a symbolic reference instead: one byte saying what kind of thing is referred to and how, then a four-byte signed offset relative to itself. The bytes are transcribed from Demangler.cpp::demangleSymbolicReference in the Swift 5.10 sources.

01  a context descriptor, directly
02  a context descriptor, through a pointer
09  an accessor function that yields the entity when run
0A  a unique extended existential type shape
0B  a non-unique extended existential type shape
03-08, 0C  reserved, and refused: the reference itself refuses them
FF  alignment padding in front of a reference, and skipped

Two consequences fall out of the encoding and both matter to a reader:

  • A metadata blob cannot be split on NUL. The four-byte offset is arbitrary bytes and very often contains a zero, so finding where a mangled name ends means parsing it. end_of_name does exactly that and nothing else.
  • A name holding one is not text. It is bytes, so the entry points here take bytes where the rest of the package takes str.

Resolving a reference needs the image the name came out of, which is why this is a separate entry point rather than something demangle() could do: see resolve.py for a resolver that reads one, and demangle_symbolic for the parse that uses it.

read

read(data, pos)

Read the reference whose introducer is at data[pos].

Returns (reference, position after it). Raises ValueError if pos does not hold an introducer, or if the four offset bytes are not all there -- which the reference also treats as failure rather than reading a short integer.

Source code in src/demangle/schemes/swift/symbolic.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def read(data, pos):
    """Read the reference whose introducer is at `data[pos]`.

    Returns `(reference, position after it)`. Raises `ValueError` if `pos` does not hold
    an introducer, or if the four offset bytes are not all there -- which the reference
    also treats as failure rather than reading a short integer.
    """
    if pos < 0 or pos >= len(data):
        raise ValueError(f"no byte at {pos}")
    raw = data[pos]
    if raw not in INTRODUCERS:
        raise ValueError(f"0x{raw:02x} does not introduce a symbolic reference")
    start = pos + 1
    if start + OFFSET_WIDTH > len(data):
        raise ValueError("truncated symbolic reference")
    (offset,) = _OFFSET.unpack_from(data, start)
    kind, directness = KINDS.get(raw, (None, None))
    return SymbolicReference(raw, kind, directness, offset, start), start + OFFSET_WIDTH

names

names(data)

Split a metadata blob of NUL-terminated mangled names into those names.

Empty entries are dropped: a section is padded with them.

Source code in src/demangle/schemes/swift/symbolic.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def names(data):
    """Split a metadata blob of NUL-terminated mangled names into those names.

    Empty entries are dropped: a section is padded with them.
    """
    found = []
    pos = 0
    length = len(data)
    while pos < length:
        stop = end_of_name(data, pos)
        if stop > pos:
            found.append(data[pos:stop])
        pos = stop + 1
    return tuple(found)

Resolving one

demangle.schemes.swift.resolve

Resolving a symbolic reference, which needs the image the name came out of.

A symbolic reference is an offset from itself to a descriptor somewhere else in the same binary, so reading one means reading the binary. That is why it is here rather than in the demangler: demangle() is given a name and nothing else, and no amount of cleverness recovers what the name deliberately does not carry.

What a resolver has to produce is a mangled fragment in Swift's own grammar -- 4demo5PointV for demo.Point -- which the demangler then reads in place. Handing back a fragment rather than a finished spelling is what keeps the rest of the mangling working: the reference may be the base of a bound generic type, or a member the next operator qualifies, and only a real node can be used that way.

The descriptor layout is Swift's TargetContextDescriptor and the two that extend it:

uint32  Flags        low five bits are the kind
int32   Parent       a relative *indirectable* pointer: low bit set means
                     "the offset reaches a pointer to the descriptor"
int32   Name         a relative direct pointer to a NUL-terminated name;
                     present on module and on nominal type descriptors alike

Walking Parent to the module and writing each name back out with its length is the whole of it. The check that it is right is that the fragment demangles to the same name the type carries in the symbol table.

MalformedImage

Bases: ValueError

The file is not a well-formed image of the kind it claims to be.

A ValueError rather than whatever the arithmetic happened to raise. These loaders read a file the calling tool did not produce -- the same threat model as a mangled name -- so a truncated or hostile header has to arrive as a refusal a caller can catch, not as an IndexError from a header field or a struct.error from an offset that pointed past the end.

Nodes

demangle.schemes.swift.nodes

The Swift symbol tree a caller walks.

Swift's own demangling tree has 385 node kinds, most of which say something about the lowered representation rather than about the declaration -- ImplParameterResultDifferentiability is not what a tool wants to match on. So this exposes a small tree in the vocabulary the other schemes here use: name, module, type, template, function, parameters.

The tree is built during printing, not from the demangling tree. That is the point: the printer's spelling of a node depends on where it sits -- a class is Foundation.NSData as a type and NSData as a prefix -- so a subtree rendered on its own would not say what it says in the whole name. Recording the boundaries of one traversal means every node's text is exactly the run of characters it produced, and the tree cannot spell the symbol differently from demangle().

SwiftName

SwiftName(kind, swift_kind, parts)

Bases: Node

One part of a Swift symbol: the text it produced, and the nodes inside it.

parts interleaves strings and nodes in output order, so render() is a concatenation and cannot drift from what the printer wrote.

Source code in src/demangle/schemes/swift/nodes.py
66
67
68
69
70
71
72
73
74
def __init__(self, kind, swift_kind, parts):
    self.kind = kind
    #: The compiler's own kind name, for the finer distinction.
    self.swift_kind = swift_kind
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

build

build(root, options=DEFAULT_OPTIONS)

Spell root while recording its structure, returning the tree.

Returns None for a tree the printer refuses, which is the same answer the text path gives -- neither invents a partial reading.

Source code in src/demangle/schemes/swift/nodes.py
131
132
133
134
135
136
137
138
139
140
141
142
def build(root, options=DEFAULT_OPTIONS):
    """Spell `root` while recording its structure, returning the tree.

    Returns `None` for a tree the printer refuses, which is the same answer the text path
    gives -- neither invents a partial reading.
    """
    printer = _TreePrinter(options)
    try:
        printer.print(root, 0)
    except (_Invalid, IndexError, AttributeError, KeyError, RecursionError):
        return None
    return printer.tree()

D

demangle.schemes.d

D mangled names.

Every _D symbol is unambiguously D's: no other scheme here uses that prefix, and the grammar requires a type after the path, so a name that merely starts _D but is not one is refused rather than guessed at.

Conformance is 100% against GNU binutils' D demangler (c++filt --format=dlang) over every symbol it can read in the shipped libgphobos and libgdruntime -- 16,333 of 19,315 -- with nothing mis-spelled and nothing refused among them. See _parser.py for where the grammar comes from and which of its rules had to be settled by measurement.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="d",
    node_kinds=("name", "path", "symbol"),
    detect=detect,
    parse=parse,
    description="D symbol mangling (dlang)",
    aliases=("dlang",),
    first_characters="_",
    priority=40,
)

The scheme as the registry holds it, registered when this package is imported.

DSymbol

DSymbol(raw, path, text, suffix='', generated='')

One parsed D symbol: its dotted path, and the type of what it declares.

Source code in src/demangle/schemes/d/_parser.py
279
280
281
282
283
284
def __init__(self, raw, path, text, suffix="", generated=""):
    self.raw = raw
    self.generated = generated
    self.path = path
    self.text = text
    self.suffix = suffix

parse_d_symbol

parse_d_symbol(
    symbol,
    max_substitutions=max_substitutions,
    max_output=max_output,
)

Parse symbol, returning a DSymbol, or raise DemangleFailure.

Source code in src/demangle/schemes/d/_parser.py
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
def parse_d_symbol(
    symbol,
    max_substitutions=DEFAULT_LIMITS.max_substitutions,
    max_output=DEFAULT_LIMITS.max_output,
):
    """Parse `symbol`, returning a `DSymbol`, or raise `DemangleFailure`."""
    parser = _Parser(symbol, max_substitutions, max_output)
    text = parser.parse()
    if parser.reader.pos != parser.reader.end:
        raise DemangleFailure("unconsumed input")
    generated = ""
    body = text
    for prefix in SPECIAL_COMPONENTS.values():
        if text.startswith(prefix):
            generated, body = prefix, text[len(prefix) :]
            break
    path, _, rest = body.partition("(")
    return DSymbol(
        raw=symbol,
        path=path,
        text=text,
        suffix=f"({rest}" if rest else "",
        generated=generated,
    )

detect

detect(name)

_D and something after it.

Narrower than it looks: a D symbol's path components are length-prefixed, so the character after _D is a digit for every name a compiler emits. That keeps this from claiming an ordinary C identifier that happens to begin _D.

_Dmain is the one exception, and it has to be named: it is D's entry point, it has no path and no type, so the digit rule would reject it -- and it is the one symbol every D program has.

Source code in src/demangle/schemes/d/__init__.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def detect(name):
    """`_D` and something after it.

    Narrower than it looks: a D symbol's path components are length-prefixed, so the
    character after `_D` is a digit for every name a compiler emits. That keeps this from
    claiming an ordinary C identifier that happens to begin `_D`.

    `_Dmain` is the one exception, and it has to be named: it is D's entry point, it has
    no path and no type, so the digit rule would reject it -- and it is the one symbol
    every D program has.
    """
    name = _without_the_mach_o_underscore(name)
    if not name or not name.startswith("_D"):
        return False
    return name == _MAIN or (len(name) > 2 and "0" <= name[2] <= "9")

parse

parse(
    mangled, builder, limits=DEFAULT_LIMITS, options=None
)

Parse a D mangled name into builder.

Source code in src/demangle/schemes/d/__init__.py
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
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=None):
    """Parse a D mangled name into `builder`."""
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    original, mangled = mangled, _without_the_mach_o_underscore(mangled)
    if not mangled.startswith("_D"):
        raise NotMangledError(original, "not a D mangled name")
    if mangled == _MAIN:
        return (
            builder.raw("D main")
            if not _wants_structure(builder)
            else nodes.build(DSymbol(raw=mangled, path="D main", text="D main"))
        )
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)

    try:
        symbol = parse_d_symbol(mangled, limits.max_substitutions)
    except _Exhausted as error:
        raise LimitExceeded(mangled, "substitution", limits.max_substitutions) from error
    except DemangleFailure as error:
        raise ParseError(mangled, None, str(error)) from error
    except RecursionError as error:
        raise LimitExceeded(mangled, "recursion depth", limits.max_depth) from error

    if len(symbol.text) > limits.max_output:
        raise LimitExceeded(mangled, "output length", limits.max_output)

    if _wants_structure(builder):
        return nodes.build(symbol)
    return builder.raw(symbol.text)

Nodes

demangle.schemes.d.nodes

The D symbol tree.

The same parts-in-order shape as the Rust and Go trees: a node is the fragments it was built from, so rendering concatenates and a tree cannot spell a symbol differently from the text path.

What a caller wants from a D symbol is which module it belongs to, whether it is a method, and what a template was instantiated with -- none of which survives splitting the spelling on ., because a template argument list contains dots of its own.

DName

DName(parts)

Bases: _D

One path component.

Source code in src/demangle/schemes/d/nodes.py
24
25
26
27
28
29
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Path

Path(parts)

Bases: _D

The dotted path: module, then the entity within it.

Source code in src/demangle/schemes/d/nodes.py
24
25
26
27
28
29
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Symbol

Symbol(parts, generated='')

Bases: _D

A whole D symbol.

generated is the prefix the reference gives a compiler-generated symbol -- an initializer, a vtable, a ClassInfo -- and empty for an ordinary one.

Source code in src/demangle/schemes/d/nodes.py
73
74
75
def __init__(self, parts, generated=""):
    super().__init__(parts)
    self.generated = generated

build

build(symbol)

Assemble the tree for a parsed DSymbol.

Source code in src/demangle/schemes/d/nodes.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def build(symbol):
    """Assemble the tree for a parsed `DSymbol`."""
    parts = []
    if symbol.generated:
        parts.append(symbol.generated)
    components = symbol.path.split(".") if symbol.path else []
    inner = []
    for index, component in enumerate(components):
        if index:
            inner.append(".")
        inner.append(DName((component,)))
    parts.append(Path(inner))
    if symbol.suffix:
        parts.append(symbol.suffix)
    return Symbol(parts, generated=symbol.generated)

Go

demangle.schemes.go

Go symbol names.

Go is the one scheme here with no marker saying "this is mine". An Itanium symbol opens _Z, an MSVC one ?, a Rust v0 one _R; a Go one opens with a package path, and fmt.Println is a shape a C or Pascal symbol could have too. So detection asks for positive evidence and declines otherwise, and a caller who knows the binary is Go -- from its build info, which is how a tool would know -- passes language="go" and gets the whole scheme regardless.

That asymmetry is deliberate. Failing to claim a Go symbol returns it unchanged, which is what an unreadable name does anyway. Claiming a symbol that is not Go's would rewrite someone else's name into a plausible lie, and a plausible lie is the one outcome this library treats as worse than silence.

See _parser.py for where the rules come from: Go's own cmd/internal/objabi/path.go, not another demangler's reading of it.

GENERATED_PREFIXES module-attribute

GENERATED_PREFIXES = ('go:', 'type:', 'go.', 'type.')

Prefixes the linker gives its own generated symbols.

go: and type: are current; go. and type. are what Go wrote before 1.20 and still appear in older binaries.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="go",
    node_kinds=(
        "name",
        "path",
        "receiver",
        "symbol",
        "template",
    ),
    detect=detect,
    parse=parse,
    description="Go symbol names (package paths, receivers, generic instantiations)",
    aliases=("golang",),
    priority=10,
)

The scheme as the registry holds it, registered when this package is imported.

GoSymbol

GoSymbol(
    raw,
    package="",
    name="",
    receiver=None,
    pointer_receiver=False,
    generic=None,
    generated="",
)

One parsed Go symbol.

package and name are decoded; raw is what was read. receiver is the type a method is on, without its *, and pointer_receiver says which form it was written in. generic holds the text between the brackets of an instantiation, and generated names the linker's own prefix for a symbol that has one -- such a symbol has no package, receiver or instantiation of its own: name is the linker's text.

Source code in src/demangle/schemes/go/_parser.py
122
123
124
125
126
127
128
129
def __init__(self, raw, package="", name="", receiver=None, pointer_receiver=False, generic=None, generated=""):
    self.raw = raw
    self.package = package
    self.name = name
    self.receiver = receiver
    self.pointer_receiver = pointer_receiver
    self.generic = generic
    self.generated = generated

text property

text

The symbol with its escapes decoded, and nothing else changed.

Deliberately not a new spelling. Go's own tooling -- go tool nm, go tool objdump, pprof, delve -- prints these names as they stand, so a reader recognises them; inventing a prettier form would mean this library is the only thing in the ecosystem showing one. What it adds is the decoding, which no Go tool does and which is the one part that is genuinely encoded.

escape_path

escape_path(path)

Escape a package path the way objabi.PathToPrefix does.

Operates on UTF-8 bytes, as the Go implementation does. This is not a detail: ü is one Python character but two bytes, and Go writes it %c3%bc. Escaping the character would produce %fc, which is not what any Go binary contains.

Kept beside the decoder because it is what makes the decoder testable -- for every symbol in a real binary, escaping what we decoded must give back the original bytes. A decoder checked only against examples passes while quietly mishandling whatever the examples missed.

Source code in src/demangle/schemes/go/_parser.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def escape_path(path):
    """Escape a package path the way `objabi.PathToPrefix` does.

    Operates on UTF-8 *bytes*, as the Go implementation does. This is not a detail: `ü`
    is one Python character but two bytes, and Go writes it `%c3%bc`. Escaping the
    character would produce `%fc`, which is not what any Go binary contains.

    Kept beside the decoder because it is what makes the decoder testable -- for every
    symbol in a real binary, escaping what we decoded must give back the original bytes.
    A decoder checked only against examples passes while quietly mishandling whatever
    the examples missed.
    """
    raw = path.encode("utf-8", "surrogateescape")
    slash = raw.rfind(b"/")
    out = []
    for index, byte in enumerate(raw):
        if byte <= 0x20 or (byte == 0x2E and index > slash) or byte in (0x25, 0x22) or byte >= 0x7F:
            out.append(f"%{byte:02x}")
        else:
            out.append(chr(byte))
    return "".join(out)

parse_go_symbol

parse_go_symbol(symbol)

Parse symbol, returning a GoSymbol, or raise.

Raises NotMangledError for anything without the shape of a Go symbol and ParseError for something Go-shaped this cannot read.

Source code in src/demangle/schemes/go/_parser.py
214
215
216
217
218
219
220
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
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
def parse_go_symbol(symbol):
    """Parse `symbol`, returning a `GoSymbol`, or raise.

    Raises `NotMangledError` for anything without the shape of a Go symbol and
    `ParseError` for something Go-shaped this cannot read.
    """
    if not symbol:
        raise NotMangledError(symbol, "empty name")

    generated = ""
    body = symbol
    for prefix in GENERATED_PREFIXES:
        if symbol.startswith(prefix):
            generated, body = prefix, symbol[len(prefix) :]
            break

    if generated:
        # The linker's own text (`type:.eq.[2]string`), not a package-qualified
        # declaration: not split, escapes decoded in place.
        package, rest = "", body
    else:
        _, dot = _package_boundary(body)
        if dot < 0:
            raise NotMangledError(symbol, "no package separator")
        package, rest = body[:dot], body[dot + 1 :]
        if not package:
            raise NotMangledError(symbol, "empty package")
    if not rest:
        raise NotMangledError(symbol, "empty name")

    try:
        package = unescape_path(package)
        name = _unescape_outside_quotes(rest)
    except ValueError as error:
        raise ParseError(symbol, None, str(error)) from error
    if not _is_text(package) or not _is_text(name):
        # `PrefixToPath` works on bytes; a decoding that is not UTF-8 would come back as
        # lone surrogates Python cannot encode. No valid module path produces one.
        raise ParseError(symbol, None, "package path does not decode to text")

    receiver, pointer, generic = None, False, None
    if not generated:
        declared = name
        if declared.startswith("(*") and ")." in declared:
            close = declared.index(").")
            receiver, pointer = declared[2:close], True
            declared = declared[close + 2 :]
        if "[" in declared and declared.endswith("]"):
            generic = declared[declared.index("[") + 1 : -1]

    return GoSymbol(
        raw=symbol,
        package=package,
        name=name,
        receiver=receiver,
        pointer_receiver=pointer,
        generic=generic,
        generated=generated,
    )

unescape_path

unescape_path(prefix)

Reverse escape_path, as objabi.PrefixToPath does.

Raises ValueError on a malformed escape, matching the Go implementation, which refuses rather than passing the % through: a symbol carrying one is not a symbol the Go linker wrote.

Source code in src/demangle/schemes/go/_parser.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def unescape_path(prefix):
    """Reverse `escape_path`, as `objabi.PrefixToPath` does.

    Raises `ValueError` on a malformed escape, matching the Go implementation, which
    refuses rather than passing the `%` through: a symbol carrying one is not a symbol
    the Go linker wrote.
    """
    if "%" not in prefix:
        return prefix
    raw = prefix.encode("utf-8", "surrogateescape")
    out = bytearray()
    index = 0
    while index < len(raw):
        if raw[index] != 0x25:
            out.append(raw[index])
            index += 1
            continue
        # Go's own bound: `if i+2 >= len(s)` is the error case.
        if index + 2 >= len(raw):
            raise ValueError("escape sequence must contain two hex digits")
        digits = raw[index + 1 : index + 3].decode("ascii", "replace")
        if digits[0] not in _HEX or digits[1] not in _HEX:
            raise ValueError(f"escape sequence {chr(0x25) + digits!r} must contain two hex digits")
        out.append(int(digits, 16))
        index += 3
    return out.decode("utf-8", "surrogateescape")

detect

detect(name)

Whether name is Go-shaped beyond reasonable doubt.

Three kinds of evidence, each of which no other scheme here produces:

  • a linker-generated prefix, go: or type:;
  • a package path with a / in it, followed by a . -- an import path, which C and Pascal symbols do not have and which the C-family schemes encode rather than spell -- unless it is a file's path instead (see _is_absolute);
  • a method receiver written (*Type).Method.

fmt.Println and main.main are real Go symbols and are deliberately not claimed: nothing in them distinguishes a Go symbol from any other dotted name, and guessing would mean rewriting names this library was not sure about. They demangle when asked for by language.

Source code in src/demangle/schemes/go/__init__.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def detect(name):
    """Whether `name` is Go-shaped beyond reasonable doubt.

    Three kinds of evidence, each of which no other scheme here produces:

    - a linker-generated prefix, `go:` or `type:`;
    - a package path with a `/` in it, followed by a `.` -- an import path, which C and
      Pascal symbols do not have and which the C-family schemes encode rather than
      spell -- unless it is a file's path instead (see `_is_absolute`);
    - a method receiver written `(*Type).Method`.

    `fmt.Println` and `main.main` are real Go symbols and are deliberately *not* claimed:
    nothing in them distinguishes a Go symbol from any other dotted name, and guessing
    would mean rewriting names this library was not sure about. They demangle when asked
    for by language.
    """
    if not name:
        return False
    if name.startswith(GENERATED_PREFIXES):
        return True
    if "(*" in name and ")." in name:
        return True
    slash = name.rfind("/")
    if slash < 0:
        return False
    # The last slash of the *symbol*: a path inside generic arguments
    # (`main.F[internal/sync.node]`) is evidence of Go too.
    dot = name.find(".", slash + 1)
    return 0 < dot < len(name) - 1 and not _is_absolute(name)

parse

parse(
    mangled, builder, limits=DEFAULT_LIMITS, options=None
)

Parse a Go symbol into builder.

Source code in src/demangle/schemes/go/__init__.py
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=None):
    """Parse a Go symbol into `builder`."""
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)

    # No second detection test: `detect` claimed the name or the caller named the
    # language. Re-asking would refuse Go symbols that need no decoding
    # (`bytes.Compare`).
    symbol = parse_go_symbol(mangled)
    spelled = symbol.text
    if len(spelled) > limits.max_output:
        raise LimitExceeded(mangled, "output length", limits.max_output)

    if _wants_structure(builder):
        return nodes.build(symbol)
    return builder.raw(spelled)

Nodes

demangle.schemes.go.nodes

The Go symbol tree.

The same shape as the Rust tree, and for the same reason: a Go symbol spells strictly left to right, so a node is its parts in order and rendering concatenates them. A tree therefore cannot spell a symbol differently from the text path.

What the structure is for is the question a tool actually asks of a Go binary -- which package is this in, is it a method and on what, is it an instantiation of a generic -- none of which can be answered by splitting the raw name on ., because a package path may contain a dot of its own (written %2e) and a receiver may contain one too.

GoName

GoName(parts)

Bases: _Go

An identifier: a function, a method, a type.

Source code in src/demangle/schemes/go/nodes.py
25
26
27
28
29
30
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Package

Package(parts)

Bases: _Go

An import path, with its escapes already decoded.

Source code in src/demangle/schemes/go/nodes.py
25
26
27
28
29
30
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Receiver

Receiver(parts, pointer)

Bases: _Go

The type a method is defined on.

pointer says whether it was written (*T).M or T.M, which is a real distinction: they are different methods with different symbols.

Source code in src/demangle/schemes/go/nodes.py
75
76
77
def __init__(self, parts, pointer):
    super().__init__(parts)
    self.pointer = pointer

Generic

Generic(parts, arguments)

Bases: _Go

A generic instantiation's arguments, [go.shape.int] or [int].

Kept as text rather than parsed further: the arguments are Go type syntax, which is a language of its own, and nothing in a symbol table needs it taken apart. arguments is what stood between the brackets.

Source code in src/demangle/schemes/go/nodes.py
91
92
93
def __init__(self, parts, arguments):
    super().__init__(parts)
    self.arguments = arguments

Symbol

Symbol(parts, generated='')

Bases: _Go

A whole Go symbol.

generated is the linker's own prefix where there is one -- go: for an itab or a string, type: for a runtime type descriptor -- and empty for an ordinary symbol.

Source code in src/demangle/schemes/go/nodes.py
106
107
108
def __init__(self, parts, generated=""):
    super().__init__(parts)
    self.generated = generated

build

build(symbol)

Assemble the tree for a parsed GoSymbol.

Built from the decoded pieces in output order, so Symbol.render() reproduces GoSymbol.text exactly rather than approximately.

Source code in src/demangle/schemes/go/nodes.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def build(symbol):
    """Assemble the tree for a parsed `GoSymbol`.

    Built from the decoded pieces in output order, so `Symbol.render()` reproduces
    `GoSymbol.text` exactly rather than approximately.
    """
    parts = []
    if symbol.generated:
        parts.append(symbol.generated)
    if symbol.package:
        parts.append(Package((symbol.package,)))
        parts.append(".")

    name = symbol.name
    if symbol.receiver is not None:
        close = name.index(").")
        parts.append(Receiver(("(*", GoName((symbol.receiver,)), ")"), pointer=True))
        parts.append(".")
        name = name[close + 2 :]

    if symbol.generic is not None:
        open_bracket = name.index("[")
        parts.append(GoName((name[:open_bracket],)))
        parts.append(Generic(("[", symbol.generic, "]"), arguments=symbol.generic))
    else:
        parts.append(GoName((name,)))

    return Symbol(parts, generated=symbol.generated)

Nim

demangle.schemes.nim

Nim symbol names.

Nim compiles to C, so a Nim symbol is an ordinary C identifier and there is no prefix to key on. Nothing in the Nim toolchain reads one back either, so the grammar here is a transcription of the compiler's mangleutils.mangle and modulegraphs.uniqueModuleName, and the correctness argument is the one Go's uses: what this reads must re-mangle to the bytes the compiler wrote.

Measured, over the two shipped Nim compilers (1.6.14 and 2.2.0):

  • every routine name declared in either standard library -- 5,946 -- comes back exactly, bar 8, and all 8 are the same documented loss (see _parser.py);
  • every module path in either standard library -- 304 and 310 -- comes back exactly;
  • over 2,000 symbols the compilers actually emitted, checked against the name each recorded in its own .ndi debug-mapping file.

Detection is the whole parse rather than a shape test, because there is nothing else to test: a reading has to exist that re-mangles to the symbol. Over 59,000 symbols from the other schemes' corpora and from real C, C++, D and Pascal binaries, that claims none of them.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="nim",
    node_kinds=("name", "path", "symbol"),
    detect=detect,
    parse=parse,
    description="Nim symbol mangling",
    aliases=(),
    priority=10,
)

The scheme as the registry holds it, registered when this package is imported.

NimSymbol

NimSymbol(raw, text, kind, name='', module='')

One parsed Nim symbol.

Source code in src/demangle/schemes/nim/_parser.py
101
102
103
104
105
106
107
def __init__(self, raw, text, kind, name="", module=""):
    self.raw = raw
    self.text = text
    #: `routine`, `type`, `type-info`, `marker` or `temporary`.
    self.kind = kind
    self.name = name
    self.module = module

detect

detect(name)

Whether name is one this reads.

Deliberately the whole parse rather than a shape test. A Nim symbol is an ordinary C identifier -- there is no prefix to key on -- so the only honest test is whether a reading exists that re-mangles to it, and that is what parse_nim_symbol establishes.

What comes before that parse is a necessary condition rather than a guess, which is what makes it safe to screen on: a name that is not one of the compiler's own forms can only be <name>__<module>_[u]<id>, and <id> is a run of decimal digits at the very end. This plugin declares no first character -- a Nim symbol is an ordinary C identifier -- so it is offered every symbol in a binary, and running five anchored regular expressions over each of them would cost several times any other scheme's detection. __ alone does not screen -- std::__cxx11 has one, and so does a fifth of the shipped libstdc++ -- and neither does a trailing digit, which 95% of those symbols also have. The tail does: none of them survives it.

Both halves of that screen are necessary, so the order between them is free to be chosen on cost -- and it matters more than it looks. "__" in name is one C-level scan; _ROUTINE_TAIL.search is a regular expression whose $ anchor does not stop search trying every position first. Testing the regex first would spend it on every name that has no __ at all, which is most of them: 61% of the shipped libstdc++, and every ordinary C identifier. With the membership test first, those stop there; only the 39% carrying a __ reach the regex. This scheme declares no first character, so it is offered every symbol in a binary and this is the bulk of its detection cost.

_FOREIGN_PREFIXES is the other half, and it is a refusal rather than a screen: the shape this scheme reads is one another compiler also produces, and where a name carries that compiler's marker the marker wins. See the note there.

Source code in src/demangle/schemes/nim/_parser.py
435
436
437
438
439
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
def detect(name):
    """Whether `name` is one this reads.

    Deliberately the whole parse rather than a shape test. A Nim symbol is an ordinary C
    identifier -- there is no prefix to key on -- so the only honest test is whether a
    reading exists that re-mangles to it, and that is what `parse_nim_symbol` establishes.

    What comes before that parse is a *necessary* condition rather than a guess, which is
    what makes it safe to screen on: a name that is not one of the compiler's own forms
    can only be `<name>__<module>_[u]<id>`, and `<id>` is a run of decimal digits at the
    very end. This plugin declares no first character -- a Nim symbol is an ordinary C
    identifier -- so it is offered every symbol in a binary, and running five anchored
    regular expressions over each of them would cost several times any other scheme's
    detection. `__` alone does not screen -- `std::__cxx11` has one, and so does
    a fifth of the shipped libstdc++ -- and neither does a trailing digit, which 95% of
    those symbols also have. The tail does: none of them survives it.

    Both halves of that screen are necessary, so the order between them is free to be
    chosen on cost -- and it matters more than it looks. `"__" in name` is one C-level
    scan; `_ROUTINE_TAIL.search` is a regular expression whose `$` anchor does not stop
    `search` trying every position first. Testing the regex first would spend it on
    every name that has no `__` at all, which is most of them: 61% of the shipped
    libstdc++, and every ordinary C identifier. With the membership test first, those
    stop there; only the 39% carrying a `__` reach the regex. This scheme declares no
    first character, so it is offered *every* symbol in a binary and this is the bulk of
    its detection cost.

    `_FOREIGN_PREFIXES` is the other half, and it is a refusal rather than a screen: the
    shape this scheme reads is one another compiler also produces, and where a name
    carries that compiler's marker the marker wins. See the note there.
    """
    if not name:
        return False
    if name.startswith(_FOREIGN_PREFIXES):
        return False
    if not name.startswith(_COMPILER_PREFIXES) and not ("__" in name and _ROUTINE_TAIL.search(name)):
        return False
    try:
        parse_nim_symbol(name)
    except (DemangleFailure, UnicodeError):
        return False
    return True

parse_nim_symbol

parse_nim_symbol(name)

Parse name, returning a NimSymbol, or raise DemangleFailure.

Source code in src/demangle/schemes/nim/_parser.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
def parse_nim_symbol(name):
    """Parse `name`, returning a `NimSymbol`, or raise `DemangleFailure`."""
    # A routine's identity ends in an ASCII digit. Compiler-generated type-info
    # names end in `_`; skipping the routine regex avoids backtracking through their
    # hash on every parse. Digit-ended names keep the original reader precedence.
    readers = _READERS if name and "0" <= name[-1] <= "9" else _GENERATED_READERS
    for reader in readers:
        try:
            found = reader(name)
        except UnicodeError:
            continue
        if found is not None:
            return found
    raise DemangleFailure("not a Nim symbol")

parse

parse(
    mangled, builder, limits=DEFAULT_LIMITS, options=None
)

Parse a Nim symbol into builder.

Source code in src/demangle/schemes/nim/__init__.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=None):
    """Parse a Nim symbol into `builder`."""
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)

    try:
        symbol = parse_nim_symbol(mangled)
    except DemangleFailure as error:
        raise NotMangledError(mangled, str(error)) from error
    except UnicodeError as error:
        raise NotMangledError(mangled, f"undecodable name: {error}") from error

    if len(symbol.text) > limits.max_output:
        raise LimitExceeded(mangled, "output length", limits.max_output)

    if _wants_structure(builder):
        return nodes.build(symbol)
    return builder.raw(symbol.text)

Parser

Reading Nim's C symbol names.

Nim compiles to C, so what reaches the linker is whatever the code generator chose to call a thing. There is no reference demangler -- nothing in the Nim toolchain reads one of these back -- so the grammar here is a transcription of the compiler's own compiler/mangleutils.nim and compiler/modulegraphs.nim, and the correctness argument is the same shape as Go's: re-mangling what we read must reproduce the bytes the compiler wrote, checked over every symbol.

A routine or a global is written

<mangled identifier> "__" <mangled module path> "_" ["u"] <id>

where the u appeared in Nim 2 and marks the mangling of the module path as well: Nim 1 encodes digits in a path and Nim 2 leaves them alone, so the same pureZmd53 is pure/md5 under one and pure/md53 under the other. The u is the only thing that says which, which is why it is read as a version marker rather than skipped.

Two things are not recoverable, and both are the compiler's own doing.

mangle drops an underscore that precedes a digit, because it reserves _<digit> for its own disambiguation: len0_16 and len016 are the same symbol. Over every routine name declared in either shipped standard library -- 5,946 of them -- that is the only cause of a name this cannot return exactly, and it accounts for all 8 of them.

The escaping is also not injective. $ becomes dollar and < becomes lt, so result -- which contains lt -- could be read as resu<. What settles it is that Nim names have a grammar: a routine is an identifier, or a name made entirely of operator characters, or an identifier with = after it (a setter), or one of the compiler's own :tmp/=destroy forms. Reading result as resu< is not any of those. The decoder proposes the readings the grammar allows, in that order, and takes the first that re-mangles to the input.

mangle

mangle(name)

mangleutils.mangle, transcribed.

Kept so that a reading can be checked by re-mangling it, which is the whole correctness argument here.

Source code in src/demangle/schemes/nim/_parser.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def mangle(name):
    """`mangleutils.mangle`, transcribed.

    Kept so that a reading can be checked by re-mangling it, which is the whole
    correctness argument here.
    """
    out = []
    start = 0
    escaped = False
    if name and name[0].isdigit():
        out.append("X" + name[0])
        start = 1
    for at in range(start, len(name)):
        character = name[at]
        if character.isascii() and character.isalnum():
            out.append(character)
        elif character == "_":
            # The compiler reserves `_<digit>`, so `len0_16` and `len016` collide.
            if not (0 < at < len(name) - 1 and name[at + 1].isdigit()):
                out.append(character)
        elif character in _BY_CHARACTER:
            out.append(_BY_CHARACTER[character])
            escaped = True
        else:
            try:
                raw = character.encode("utf-8", "surrogateescape")
            except UnicodeEncodeError:
                raise DemangleFailure(f"cannot mangle {name!r}") from None
            out.extend(f"X{byte:02X}" for byte in raw)
            escaped = True
    if escaped:
        out.append("_")
    return "".join(out)

unmangle

unmangle(text)

The name text was mangled from, or None if no reading re-mangles to it.

The readings are proposed in the order Nim's own name grammar makes likely, and the re-mangle is what accepts one. A trailing _ usually marks an escape, but a name that is _ has one too, which is why the undecoded text is a candidate as well.

Source code in src/demangle/schemes/nim/_parser.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def unmangle(text):
    """The name `text` was mangled from, or `None` if no reading re-mangles to it.

    The readings are proposed in the order Nim's own name grammar makes likely, and the
    re-mangle is what accepts one. A trailing `_` usually marks an escape, but a name
    that *is* `_` has one too, which is why the undecoded text is a candidate as well.
    """
    candidates = []
    if text.endswith("_"):
        decoded, had_hex = _decode_hex(text[:-1])
        if decoded is not None:
            operator = _as_operator(decoded)
            compiler_name = _as_compiler_name(decoded)
            setter = _as_setter(decoded)
            if operator is not None:
                candidates.append(operator)
            if had_hex:
                # The escape that set the marker is accounted for; leave the words alone.
                candidates.append(decoded)
            if compiler_name is not None:
                candidates.append(compiler_name)
            if setter is not None:
                candidates.append(setter)
            if not had_hex:
                candidates.append(decoded)
    candidates.append(text)
    for candidate in candidates:
        try:
            if mangle(candidate) == text:
                return candidate
        except (DemangleFailure, UnicodeError):
            continue
    return None

mangle_module

mangle_module(path, nim2)

modulegraphs.uniqueModuleName, transcribed.

Nim 2 leaves digits alone; Nim 1 writes them as codes like everything else that is not a lower-case letter.

Source code in src/demangle/schemes/nim/_parser.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def mangle_module(path, nim2):
    """`modulegraphs.uniqueModuleName`, transcribed.

    Nim 2 leaves digits alone; Nim 1 writes them as codes like everything else that is
    not a lower-case letter.
    """
    out = []
    for character in path:
        if "a" <= character <= "z" or (nim2 and character.isascii() and character.isdigit()):
            out.append(character)
        elif character in "/\\":
            out.append("Z")
        elif character == ".":
            out.append("O")
        else:
            out.append(str(ord(character)))
    return "".join(out)

unmangle_module

unmangle_module(text, nim2)

Undo uniqueModuleName.

Under Nim 1 every digit is part of a code, so this is exact. Under Nim 2 a digit may be either, and the tie is broken towards what a real path holds -- see _PLAUSIBLE_IN_A_PATH. Verified over every module in both shipped standard libraries: 304 of 304 and 310 of 310.

Source code in src/demangle/schemes/nim/_parser.py
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
def unmangle_module(text, nim2):
    """Undo `uniqueModuleName`.

    Under Nim 1 every digit is part of a code, so this is exact. Under Nim 2 a digit may
    be either, and the tie is broken towards what a real path holds -- see
    `_PLAUSIBLE_IN_A_PATH`. Verified over every module in both shipped standard
    libraries: 304 of 304 and 310 of 310.
    """
    out = []
    at = 0
    while at < len(text):
        character = text[at]
        if character == "Z":
            out.append("/")
            at += 1
            continue
        if character == "O":
            out.append(".")
            at += 1
            continue
        if character.isdigit():
            # Only `{`, `|`, `}` and `~` (123-126) take three digits.
            if at + 3 <= len(text) and text[at : at + 3].isdigit() and 123 <= int(text[at : at + 3]) <= 126:
                out.append(chr(int(text[at : at + 3])))
                at += 3
                continue
            if at + 2 <= len(text) and text[at : at + 2].isdigit():
                value = int(text[at : at + 2])
                # Under Nim 1 every two-digit run is a code; under Nim 2 see
                # `_PLAUSIBLE_IN_A_PATH`.
                in_range = 32 <= value <= 96 and not ("a" <= chr(value) <= "z")
                if value in _PLAUSIBLE_IN_A_PATH if nim2 else in_range:
                    out.append(chr(value))
                    at += 2
                    continue
            if nim2:
                out.append(character)
                at += 1
                continue
        out.append(character)
        at += 1
    return "".join(out)

Nodes

demangle.schemes.nim.nodes

The Nim symbol tree.

The same parts-in-order shape as the Rust, Go and D trees: a node is the fragments it was built from, so rendering concatenates and the tree cannot spell a symbol differently from the text path.

Splitting the spelling on . would not do: a Nim module path holds /, and a routine name can be . itself -- dot___ops_17 is the operator ., whose demangling is ops... The tree hands the two halves over already separated.

NimName

NimName(parts)

Bases: _Nim

A routine or variable name, with its escapes undone.

Source code in src/demangle/schemes/nim/nodes.py
20
21
22
23
24
25
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Path

Path(parts)

Bases: _Nim

The module the symbol belongs to, as a path with / separators.

Source code in src/demangle/schemes/nim/nodes.py
20
21
22
23
24
25
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Symbol

Symbol(parts, nim_kind)

Bases: _Nim

A whole Nim symbol.

nim_kind is what the mangling said this is -- routine, type, type-info, marker or temporary -- which the spelling alone does not always make plain.

Source code in src/demangle/schemes/nim/nodes.py
69
70
71
def __init__(self, parts, nim_kind):
    super().__init__(parts)
    self.nim_kind = nim_kind

build

build(symbol)

Assemble the tree for a parsed NimSymbol.

Source code in src/demangle/schemes/nim/nodes.py
74
75
76
77
78
79
80
81
def build(symbol):
    """Assemble the tree for a parsed `NimSymbol`."""
    if symbol.kind == "routine":
        return Symbol([Path([symbol.module]), ".", NimName([symbol.name])], symbol.kind)
    if symbol.name:
        prefix = symbol.text[: len(symbol.text) - len(symbol.name)]
        return Symbol([prefix, NimName([symbol.name])], symbol.kind)
    return Symbol([symbol.text], symbol.kind)

Free Pascal

demangle.schemes.pascal

Free Pascal symbol names.

Free Pascal builds a symbol out of $-delimited parts in make_mangledname (compiler/symdef.pas), and ships nothing that reads one back. So the grammar here is a transcription of that function and the three that feed it, and correctness rests on re-assembly: the parts this splits out, rejoined with the compiler's own separators, must reproduce the symbol exactly.

Measured over every symbol in the 1,074 object files of the shipped Free Pascal 3.2.2 runtime and packages -- 237,326 of them:

  • 236,570 are read, and every one re-assembles to the bytes it was read from;
  • the 756 refused have no $ in them at all, being plain C names from the soft-float runtime rather than Pascal symbols;
  • checked independently against ppudump, which prints both the mangled name and what each unit declares, every name this reads is one the unit really declares -- bar the compiler's own init and finalize sections, which no source declares.

Case does not come back. Pascal is case-insensitive and the compiler upper-cases before it mangles, so Add and ADD are the same symbol and nothing can tell them apart.

This is Free Pascal's mangling, which is what Lazarus and any {$MODE DELPHI} code built with fpc produces. Borland and Embarcadero's own Delphi compilers use a different scheme -- @Unit@Class@Method$qqrv -- which the delphi plugin reads.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="pascal",
    node_kinds=("name", "parameters", "path", "symbol"),
    detect=detect,
    parse=parse,
    description="Free Pascal symbol mangling",
    aliases=("fpc", "freepascal"),
    priority=20,
)

The scheme as the registry holds it, registered when this package is imported.

PascalSymbol

PascalSymbol(
    raw,
    text,
    kind,
    unit="",
    scope=(),
    name="",
    parameters=(),
    result=None,
    indirect=False,
    raw_scope="",
    raw_signature="",
    elided=False,
    wrapped=None,
)

One parsed Free Pascal symbol.

Source code in src/demangle/schemes/pascal/_parser.py
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
164
165
166
167
168
169
170
def __init__(
    self,
    raw,
    text,
    kind,
    unit="",
    scope=(),
    name="",
    parameters=(),
    result=None,
    indirect=False,
    raw_scope="",
    raw_signature="",
    elided=False,
    wrapped=None,
):
    self.raw = raw
    self.text = text
    #: One of `KINDS`, or `routine`, `section`, `label`.
    self.kind = kind
    self.unit = unit
    self.scope = tuple(scope)
    self.name = name
    self.parameters = tuple(parameters)
    self.result = result
    self.indirect = indirect
    #: The scope as written; the split is not reversible (classes join with `_$_`,
    #: procedures with `_`).
    self.raw_scope = raw_scope
    #: True when a checksum replaced the parameter list, so empty `parameters` means
    #: "not recorded" rather than "none".
    self.elided = elided
    #: The name and signature as written, since an elided list cannot be expanded.
    self.raw_signature = raw_signature
    #: For a `WRPR`, the entry index and forwarded-to method as spelled text.
    self.wrapped = wrapped

qualified property

qualified

UNIT.CLASS.NAME, the part a caller usually wants.

detect

detect(name)

Whether name is one this reads.

The $ in a Free Pascal symbol is not legal in a C identifier on most targets, and the separators are distinctive enough that the shape alone is a fair test -- but this parses anyway, because the parse is what establishes that the parts rejoin to the input.

Source code in src/demangle/schemes/pascal/_parser.py
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
def detect(name):
    """Whether `name` is one this reads.

    The `$` in a Free Pascal symbol is not legal in a C identifier on most targets, and
    the separators are distinctive enough that the shape alone is a fair test -- but this
    parses anyway, because the parse is what establishes that the parts rejoin to the
    input.
    """
    if not name or "$" not in name:
        return False
    try:
        parse_pascal_symbol(name)
    except DemangleFailure:
        return False
    return True

parse_pascal_symbol

parse_pascal_symbol(name)

Parse name, returning a PascalSymbol, or raise DemangleFailure.

Source code in src/demangle/schemes/pascal/_parser.py
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
391
392
393
def parse_pascal_symbol(name):
    """Parse `name`, returning a `PascalSymbol`, or raise `DemangleFailure`."""
    found = _LABEL.match(name)
    if found is not None:
        unit, label = found.groups()
        return PascalSymbol(name, f"assembler label {label} in {unit}", "label", unit=unit, name=label)

    found = _SECTION.match(name)
    if found is not None:
        which, unit = found.groups()
        word = "initialisation" if which == "INIT" else "finalisation"
        return PascalSymbol(name, f"{word} of {unit}", "section", unit=unit, name=which)

    indirect = name.endswith(_INDIRECT)
    body = name[: -len(_INDIRECT)] if indirect else name

    table = None
    for ending, description in _TABLES.items():
        if body.endswith(ending):
            table = description
            body = body[: -len(ending)]
            break

    kind = None
    found = _KIND_PATTERN.match(body)
    if found is not None:
        kind = found.group(1)
        body = body[found.end() :]

    at = body.find("_$$_")
    if at < 0:
        raise DemangleFailure("no unit-and-name separator")
    left, suffix = body[:at], body[at + 4 :]
    if not left or not suffix:
        raise DemangleFailure("empty unit or name")

    scope_at = left.find("$_$")
    if scope_at >= 0:
        unit, raw_scope = left[:scope_at], left[scope_at + 3 :]
    else:
        unit, raw_scope = left, ""
    if not unit:
        raise DemangleFailure("empty unit")

    if kind == "WRPR":
        wrapper = _parse_wrapper(name, unit, suffix, indirect)
        if wrapper is None:
            raise DemangleFailure("not a wrapper name")
        return wrapper

    # A method with nothing else around it leaves an empty last piece; anything there
    # instead is the chain of enclosing procedures, joined with a bare `_`.
    scope = [piece for piece in raw_scope.split("_$_") if piece] if raw_scope else []

    # `P$` keeps a program's symbols apart from a unit of the same name.
    is_program = unit.startswith("P$")
    spelled_unit = unit[2:] if is_program else unit
    qualified = ".".join([spelled_unit, *scope])

    if kind is None:
        name_part, parameters, result = _split_signature(suffix)
        elided = suffix[len(name_part) + 1 :] if parameters is None else None
        if _rejoin(name_part, parameters, result, elided) != suffix:
            raise DemangleFailure("the signature does not read back")
        text = _spell_signature(f"{qualified}.{spell_routine_name(name_part)}", parameters, result)
        symbol = PascalSymbol(
            name,
            text,
            "routine",
            unit=unit,
            scope=scope,
            name=name_part,
            parameters=parameters if parameters is not None else (),
            result=result,
            indirect=indirect,
            raw_scope=raw_scope,
            raw_signature=suffix,
            elided=parameters is None,
        )
    else:
        symbol = PascalSymbol(
            name,
            f"{KINDS[kind]} {qualified}.{suffix}",
            kind,
            unit=unit,
            scope=scope,
            name=suffix,
            indirect=indirect,
            raw_scope=raw_scope,
            raw_signature=suffix,
        )

    if table is not None:
        symbol.text = f"{table} for {symbol.text}"
    if is_program:
        symbol.text = "program " + symbol.text
    if indirect:
        symbol.text += " (indirect reference)"
    return symbol

parse

parse(
    mangled, builder, limits=DEFAULT_LIMITS, options=None
)

Parse a Free Pascal symbol into builder.

Source code in src/demangle/schemes/pascal/__init__.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=None):
    """Parse a Free Pascal symbol into `builder`."""
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)

    try:
        symbol = parse_pascal_symbol(mangled)
    except DemangleFailure as error:
        raise NotMangledError(mangled, str(error)) from error

    if len(symbol.text) > limits.max_output:
        raise LimitExceeded(mangled, "output length", limits.max_output)

    if _wants_structure(builder):
        return nodes.build(symbol)
    return builder.raw(symbol.text)

Parser

Reading Free Pascal's symbol names.

Free Pascal builds a symbol out of $-delimited parts in make_mangledname (compiler/symdef.pas), and nothing in the toolchain reads one back. So the grammar here is a transcription of that function and of the three others that feed it, and the correctness argument is the one Go and Nim use: the parts this splits out, rejoined with the separators the compiler uses, must reproduce the symbol exactly.

The shape, from the compiler's own comment:

[<kind> "_$"] <unit or "P$"program> ["$_$" <scope>] ["_$$_" <name and parameters>]
  • kind is one of nine markers saying the symbol is a table rather than code -- VMT, RTTI, INIT, IID, IIDSTR, RESSTR, WRPR -- or that it is data rather than a routine: U for a variable and TC for a typed constant.
  • scope is the classes and records the thing is nested in, joined by _$_, and then any enclosing procedures. A method's scope ends in a bare _$_, which is why TWIDGET_$__$$_AREA has that run of underscores in it.
  • name and parameters is the routine name, then $ and a type per parameter, then $$ and the result type.

Case does not come back. Pascal is case-insensitive and the compiler upper-cases identifiers before it mangles them, so Add and ADD are one symbol. Nothing can undo that, and this does not pretend to.

A long parameter list does not come back either. Over about 12 characters the compiler replaces the whole list with $crc and a checksum, and over 100 characters it does the same to a scope with $CRC. Both are reported as what they are rather than guessed at.

spell_routine_name

spell_routine_name(name)

$plus is operator +; $class_constructor is what it says.

Source code in src/demangle/schemes/pascal/_parser.py
238
239
240
241
242
243
244
245
246
247
def spell_routine_name(name):
    """`$plus` is `operator +`; `$class_constructor` is what it says."""
    if not name.startswith("$"):
        return name
    inner = name[1:]
    if inner in OPERATORS:
        return f"operator {OPERATORS[inner]}"
    if inner in CLASS_ROUTINES:
        return CLASS_ROUTINES[inner]
    return name

Nodes

demangle.schemes.pascal.nodes

The Free Pascal symbol tree.

The same parts-in-order shape as the other schemes: a node is the fragments it was built from, so rendering concatenates and the tree cannot spell a symbol differently from the text path.

What a caller wants from one of these is the unit, the class, and the signature -- none of which survives splitting the spelling, because a parameter type can be a generic whose own name holds a $ and a ..

Unit

Unit(parts)

Bases: _Pascal

The unit, or the program, the symbol belongs to.

Source code in src/demangle/schemes/pascal/nodes.py
20
21
22
23
24
25
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

PascalName

PascalName(parts)

Bases: _Pascal

A class, record or routine name.

Source code in src/demangle/schemes/pascal/nodes.py
20
21
22
23
24
25
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Parameters

Parameters(parts)

Bases: _Pascal

The parameter list, as the mangling recorded it.

Source code in src/demangle/schemes/pascal/nodes.py
20
21
22
23
24
25
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Symbol

Symbol(parts, pascal_kind)

Bases: _Pascal

A whole Free Pascal symbol.

pascal_kind is what the mangling said this is -- routine, or one of the nine markers, or section or label -- which the spelling alone does not always say.

Source code in src/demangle/schemes/pascal/nodes.py
76
77
78
def __init__(self, parts, pascal_kind):
    super().__init__(parts)
    self.pascal_kind = pascal_kind

build

build(symbol)

Assemble the tree for a parsed PascalSymbol.

Source code in src/demangle/schemes/pascal/nodes.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def build(symbol):
    """Assemble the tree for a parsed `PascalSymbol`."""
    from ._parser import spell_routine_name

    if symbol.kind in ("label", "section"):
        return Symbol([symbol.text], symbol.kind)

    if symbol.kind == "WRPR" and symbol.wrapped is not None:
        index, implementation, tail = symbol.wrapped
        parts = ["interface wrapper for ", Unit([symbol.unit])]
        for piece in symbol.scope:
            parts.extend((".", PascalName([piece])))
        parts.extend((f" #{index}: ", PascalName([implementation]), tail))
        if symbol.indirect:
            parts.append(" (indirect reference)")
        return Symbol(parts, symbol.kind)

    # The lead (`vmt for `, the kind word, `program `) is found by searching for the name
    # as the spelling writes it (without a program's `P$`) and with the scope attached,
    # so a unit name that is also a word in the lead cannot match the wrong place.
    spelled_unit = symbol.unit[2:] if symbol.unit.startswith("P$") else symbol.unit
    qualified = ".".join([spelled_unit, *symbol.scope])
    at = symbol.text.find(qualified) if qualified else -1
    lead = symbol.text[:at] if at > 0 else ""
    parts = [lead] if lead else []
    parts.append(Unit([spelled_unit]))
    for piece in symbol.scope:
        parts.extend((".", PascalName([piece])))
    if symbol.name:
        parts.extend((".", PascalName([spell_routine_name(symbol.name)])))
    if symbol.kind == "routine":
        if symbol.elided:
            parts.append("(<parameters elided by the compiler>)")
        elif symbol.parameters:
            inner = []
            for at, parameter in enumerate(symbol.parameters):
                if at:
                    inner.append(", ")
                inner.append(PascalName([parameter]))
            parts.extend(("(", Parameters(inner), ")"))
        if symbol.result is not None:
            parts.extend((": ", PascalName([symbol.result])))
    if symbol.indirect:
        parts.append(" (indirect reference)")
    return Symbol(parts, symbol.kind)

Pre-Itanium C++

demangle.schemes.gnuv2

Pre-Itanium C++: GNU g++ before 3.0, cfront/ARM, Lucid, HP aCC and EDG.

Five manglings, one demangler, because that is how libiberty implements them: one cplus-dem.c under five style flags. See _parser.py for the port and for what is measured against the reference's own vectors.

Detection is the hard part, and the reason this scheme is offered almost last. A GNU v2 symbol is an ordinary C identifier with a __ somewhere in it -- AtEnd__13ivRubberGroup is a name a C compiler would have accepted -- so there is no prefix to key on, and a scheme that guessed would rewrite other people's symbols into plausible lies. Three things keep it honest:

  • it is offered after every other scheme except CodeWarrior, Itanium included, so it only ever sees names nothing else claimed;
  • detect runs the whole parse rather than a shape test, and a name that does not parse end to end is not claimed;
  • a reading that decodes nothing -- one where the demangler consumed the name and gave back what it was given -- is refused, because that is not evidence of anything.

Measured over every corpus in tests/conformance/ and over the symbol tables of the system's own C libraries: see tests/test_gnuv2.py, which pins the count both ways.

A caller who knows which compiler built the binary should say so, with GnuV2Options(style=...): the five styles read the same bytes differently and the name does not say which one wrote it. style="gnu" is the default, being the one most likely to be met.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="gnuv2",
    node_kinds=("name", "parameters", "symbol", "type"),
    detect=detect,
    parse=parse,
    description="Pre-Itanium C++ (g++ 2.x, cfront/ARM, Lucid, HP aCC, EDG)",
    options_type=GnuV2Options,
    aliases=("gnu-v2", "cfront", "cplus-dem"),
    symbol_table_decorations=True,
    priority=290,
)

The scheme as the registry holds it, registered when this package is imported.

STYLES module-attribute

STYLES = ('auto', 'gnu', 'lucid', 'arm', 'hp', 'edg')

The demangling styles this shares its code with, spelled as c++filt --format= does.

auto is the reference's auto_demangling, which is GNU's reading plus EDG's parameterised-type prefixes; it is not a search over the other four.

GnuV2Symbol

GnuV2Symbol(
    text,
    style,
    qualified_name,
    parameters,
    arguments_text,
    return_type,
    qualifiers,
    special,
    suffix,
    evidence,
)

What one of these names says, spelled and in pieces.

text is the whole of it, and the rest is what the parts-in-order tree is built from. parameters is None where the name encodes no argument list -- a static data member, a virtual table -- which is not the same as an empty one.

Source code in src/demangle/schemes/gnuv2/_parser.py
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
def __init__(
    self,
    text,
    style,
    qualified_name,
    parameters,
    arguments_text,
    return_type,
    qualifiers,
    special,
    suffix,
    evidence,
):
    self.text = text
    self.style = style
    self.qualified_name = qualified_name
    self.parameters = parameters
    #: What the argument list contributed to `text`, or ""; `nodes.build` slices at it.
    self.arguments_text = arguments_text
    self.return_type = return_type
    self.qualifiers = qualifiers
    self.special = special
    self.suffix = suffix
    #: `class`, `qualified`, `template`, `operator`, `structor`, `special`; empty
    #: is no evidence to `detect`. `unmarked` alone (arguments with no `F` and no
    #: class before them) is evidence against.
    self.evidence = evidence

detect

detect(name, style='gnu')

Whether name is a pre-Itanium C++ symbol, decided by reading it.

There is no prefix that says so, so the whole name is parsed and the answer is whether it parsed. Screened first, so that the great majority of symbols cost a substring search and nothing else.

Source code in src/demangle/schemes/gnuv2/__init__.py
214
215
216
217
218
219
220
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
246
def detect(name, style="gnu"):
    """Whether `name` is a pre-Itanium C++ symbol, decided by reading it.

    There is no prefix that says so, so the whole name is parsed and the answer is
    whether it parsed. Screened first, so that the great majority of symbols cost a
    substring search and nothing else.
    """
    # The cheap necessary condition from `_screen`, inline, before any call.
    if "__" not in name and not (name[:1] == "_" and ("$" in name or "." in name)):
        return False
    if name.startswith(("_Z", "__Z")):
        # Itanium refusals fall through here, and `__Z...` is full of `__` separators;
        # no g++ 2.x name opens with `_Z`.
        return False
    if name.startswith(("_R", "__R")) and name[name.index("R") + 1 : name.index("R") + 2].isupper():
        # Rust v0 refusals, likewise: `_R` and a capital is a name reserved to the
        # implementation, and every one met here was a damaged Rust symbol.
        return False
    if (
        name[:2] == "__"
        and style == "gnu"
        and name[2:3].isalpha()
        and name[2] not in _SPECIAL_OPENINGS
        and name.find("__", 3) < 0
        and not name.startswith("__imp_")
    ):
        # `__libc_start_main`: after `__` and a letter that opens no special form,
        # constructor or DLL import, g++ 2.x's reading needs a second `__` to split at.
        return False
    if len(name) > _DETECT_MAX or not _screen(name):
        return False
    symbol = _reads(name, style)
    return symbol is not None and _decoded(name, symbol) and _plausible(symbol)

parse

parse(
    mangled,
    builder,
    limits=DEFAULT_LIMITS,
    options=DEFAULT_OPTIONS,
)

Parse a pre-Itanium C++ name into builder.

Source code in src/demangle/schemes/gnuv2/__init__.py
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
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=DEFAULT_OPTIONS):
    """Parse a pre-Itanium C++ name into `builder`."""
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)
    if options is None:
        options = DEFAULT_OPTIONS

    try:
        symbol = demangle_gnuv2(mangled, style=options.style, params=options.params, ansi=options.ansi)
    except DemangleFailure as error:
        raise ParseError(mangled, None, str(error)) from error
    except RecursionError as error:
        raise LimitExceeded(mangled, "recursion depth", limits.max_depth) from error

    if not _decoded(mangled, symbol):
        # The same bar `detect` holds a name to.
        raise ParseError(mangled, None, "the name decodes to itself")
    if len(symbol.text) > limits.max_output:
        raise LimitExceeded(mangled, "output length", limits.max_output)

    if _wants_structure(builder):
        return nodes.build(symbol)
    return builder.raw(symbol.text)

demangle_gnuv2

demangle_gnuv2(
    mangled, style="gnu", params=True, ansi=True
)

Read one pre-Itanium C++ name. Raises DemangleFailure where it is not one.

An unknown style is the caller's mistake rather than the name's, and raises ValueError.

Source code in src/demangle/schemes/gnuv2/_parser.py
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
def demangle_gnuv2(mangled, style="gnu", params=True, ansi=True):
    """Read one pre-Itanium C++ name. Raises `DemangleFailure` where it is not one.

    An unknown `style` is the caller's mistake rather than the name's, and raises
    `ValueError`.
    """
    if style not in STYLES:
        raise ValueError(f"unknown style {style!r}; expected one of {', '.join(STYLES)}")
    work = _Work(style, params=params, ansi=ansi)
    try:
        text = _internal_demangle(work, mangled)
    except RecursionError as error:
        raise _Overflow("nested too deeply") from error
    if text is None:
        raise DemangleFailure(f"not a {style} C++ name")
    capture = work.capture
    return GnuV2Symbol(
        text=text,
        style=style,
        qualified_name=(capture["name"] if capture else text),
        parameters=(tuple(capture["args"]) if capture else None),
        arguments_text=(capture["text"] if capture else ""),
        return_type=work.return_type,
        qualifiers=work.trailing,
        special=work.special,
        suffix=work.suffix,
        evidence=frozenset(work.evidence),
    )

Options

demangle.schemes.gnuv2.options

Which of the five pre-Itanium manglings to read, and how much of the name to spell.

The reference is one demangler with a style flag, not five demanglers, and the flag has to be supplied: AtEnd__13ivRubberGroup is a valid name under every one of them and they do not all read it the same way. There is no marker in the name to key on, which is why this is an option rather than something detection could work out.

DMGL_PARAMS and DMGL_ANSI are the reference's other two flags and are carried for the same reason: they change what comes out, and the vectors in demangle-expected pin both settings of the first.

DEFAULT_OPTIONS module-attribute

DEFAULT_OPTIONS = GnuV2Options()

What demangle() uses when a caller names no options: GNU's reading, spelled in full.

GnuV2Options dataclass

GnuV2Options(style='gnu', params=True, ansi=True)

How to read a pre-Itanium C++ name.

style class-attribute instance-attribute
style = 'gnu'

Which compiler wrote it: gnu, lucid, arm, hp, edg, or auto.

gnu is g++ before 3.0 and the default, being the one of the five that a binary in the wild is most likely to hold. arm is the encoding the Annotated Reference Manual describes, which cfront and its descendants emit; lucid is Lucid's lcc; hp is HP aCC; edg is the Edison Design Group front end. auto is the reference's auto_demangling: GNU's reading plus EDG's parameterised-type prefixes, and not a search over the other four.

params class-attribute instance-attribute
params = True

Spell the argument list, and the qualifiers that follow it.

DMGL_PARAMS. False gives what c++filt -p prints: ivTSolver::AddAlignment rather than ivTSolver::AddAlignment(unsigned int, ivInteractor *, ivTGlue *).

ansi class-attribute instance-attribute
ansi = True

Spell ANSI C++ qualifiers -- const, volatile, __restrict -- on types.

DMGL_ANSI. False drops them, which is what a demangler predating the standard did.

Parser

The pre-Itanium C++ demangler, ported from libiberty's cplus-dem.c.

One algorithm, five styles. GNU g++ before 3.0, Lucid's lcc, the ARM/Annotated Reference Manual encoding that cfront and its descendants emit, HP's aCC, and EDG's front end all mangle names the same way up to a handful of flag-guarded differences, and libiberty demangles all five with one body of code under GNU_DEMANGLING, LUCID_DEMANGLING, ARM_DEMANGLING, HP_DEMANGLING and EDG_DEMANGLING. This is that code, transcribed.

The reference is GCC 8.3.0's libiberty/cplus-dem.c -- the last release before the pre-v3 demangler was dropped -- and the transcription is deliberately literal: the function names are its function names, the control flow is its control flow, and the quirks are kept rather than tidied. demangle_template returning failure for an empty argument list, demangle_template_value_parm returning -1 where every caller then treats -1 as success, do_type clearing the buffer its caller handed it: all of those are the reference's behaviour, they are all observable in its own test vectors, and a cleaner port would be a different demangler.

The two structural things worth knowing before reading it:

  • It builds text by prepending. There is no tree. demangle_class prepends foo:: to what has been built so far, which is how AtEnd__13ivRubberGroup becomes ivRubberGroup::AtEnd(void) with the class arriving after the function name has already been written. _Buf below is the reference's string, with the same prepend/append/truncate operations, because several places index into the buffer (work.temp_start) or cut a character off the end (the extra comma in an ARM template argument list).

  • It is stateful across the whole name. _Work is the reference's work_stuff: remembered types for Tn back-references, the squangling B and K vectors, the constructor and destructor counters that demangle_prefix sets and demangle_class consumes, and temp_start, the offset in the buffer where a class's template arguments began. iterate_demangle_function snapshots the whole of it, because GNU mangling is genuinely ambiguous -- there is no token marking where a function name ends and its signature begins, only a run of __ that might be either -- so the demangler guesses, and backtracks when the guess does not parse.

Scored against the reference's own vectors: libiberty/testsuite/demangle-expected from the same tree, every case marked --format=gnu, --format=lucid, --format=arm or --format=hp, under both settings of DMGL_PARAMS. See tests/conformance/.

Nodes

demangle.schemes.gnuv2.nodes

The pre-Itanium C++ tree.

The same parts-in-order shape as the Go, Nim, D and JNI trees: a node is the fragments it was built from, so rendering concatenates and the tree cannot spell a symbol differently from the text path.

The reference builds text by prepending and appending into one buffer, and there is no point pretending otherwise -- so the structure here is sliced out of the finished spelling at boundaries the parser recorded while it ran: where the function's name ended, and exactly what the argument list contributed. Everything else stays as literal text, in the position the demangler wrote it.

Where the slice does not add up -- the recorded name is not found where it should be -- the whole spelling is emitted as one name part rather than a structure that might not be true. tests/test_gnuv2.py pins how many of libiberty's own vectors take each path.

GnuV2Name

GnuV2Name(parts)

Bases: _GnuV2

The entity's name with its scope: ivTSolver::AddAlignment.

Template arguments belong to the component they qualify, so they are inside this and not split out: foo::bar<int> is one name.

Source code in src/demangle/schemes/gnuv2/nodes.py
26
27
28
29
30
31
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Type

Type(parts)

Bases: _GnuV2

One parameter type, spelled.

Source code in src/demangle/schemes/gnuv2/nodes.py
26
27
28
29
30
31
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Parameters

Parameters(parts)

Bases: _GnuV2

The argument list, parentheses included, with each type a Type inside it.

Source code in src/demangle/schemes/gnuv2/nodes.py
26
27
28
29
30
31
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Symbol

Symbol(parts)

Bases: _GnuV2

A whole pre-Itanium C++ symbol.

Source code in src/demangle/schemes/gnuv2/nodes.py
26
27
28
29
30
31
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

build

build(symbol)

Assemble the tree for a parsed GnuV2Symbol.

Falls back to one flat name part whenever the recorded boundaries do not line up with the spelling, so render() is the spelling in every case.

Source code in src/demangle/schemes/gnuv2/nodes.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def build(symbol):
    """Assemble the tree for a parsed `GnuV2Symbol`.

    Falls back to one flat `name` part whenever the recorded boundaries do not line up
    with the spelling, so `render()` is the spelling in every case.
    """
    text = symbol.text
    name = symbol.qualified_name
    arguments = symbol.arguments_text

    if name:
        anchor = name + arguments
        at = text.find(anchor)
        if at >= 0:
            middle = _parameters(arguments, symbol.parameters)
            if middle is not None:
                head, tail = text[:at], text[at + len(anchor) :]
                parts = []
                if head:
                    parts.append(head)
                parts.append(GnuV2Name([name]))
                parts.extend(middle)
                if tail:
                    parts.append(tail)
                return Symbol(parts)

    return Symbol([GnuV2Name([text])])

CodeWarrior

demangle.schemes.codewarrior

Metrowerks CodeWarrior C++.

The other pre-Itanium C++ mangling, and a scheme of its own rather than a sixth style of gnuv2 because libiberty never read it: cplus-dem.c has no CodeWarrior flag, demangle-expected has no vectors for it, and binutils has never demangled one. What built with it: Nintendo GameCube and Wii titles, Palm OS, BeOS, classic Mac OS, and a great deal of other embedded work -- which is why the reference is a decompilation project's tool, encounter/cwdemangle, rather than a compiler vendor's.

It looks like the ARM encoding and is not. A template argument list is written out literally in the symbol, single_ptr<10CModelData>, so finding where the name ends means counting brackets before looking for the __; a pointer-to-member carries two hidden parameters whose spelling says whether the member function is const; and the type spelling is the reference's own -- const char*, not char const *.

Detection has the same problem gnuv2 has and answers it the same way: a CodeWarrior symbol is an ordinary C identifier with a __ in it, so detect parses the whole name rather than testing a prefix, and this scheme is offered last -- after everything with a marker and after gnuv2, which goes first because a name valid under both should go to the commoner mangling. A __dt that the gnu style does not know falls through to this scheme, so __dt__6CActorFv is read here rather than as a function called __dt. Measured over every corpus in tests/conformance/ and over the system's own C libraries: see tests/test_codewarrior.py, which pins the count both ways.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="codewarrior",
    node_kinds=("name", "parameters", "symbol", "type"),
    detect=detect,
    parse=parse,
    description="Metrowerks CodeWarrior C++ symbol names",
    options_type=CodeWarriorOptions,
    aliases=("cw", "metrowerks", "mwcc"),
    symbol_table_decorations=True,
    priority=300,
)

The scheme as the registry holds it, registered when this package is imported.

CodeWarriorSymbol

CodeWarriorSymbol(
    text,
    qualified_name,
    parameters,
    arguments_text,
    return_type,
    static_variable,
)

What one of these names says, spelled and in pieces.

Source code in src/demangle/schemes/codewarrior/_parser.py
410
411
412
413
414
415
416
417
418
def __init__(self, text, qualified_name, parameters, arguments_text, return_type, static_variable):
    self.text = text
    self.qualified_name = qualified_name
    self.parameters = parameters
    #: Exactly what the argument list contributed to `text`, parentheses included.
    self.arguments_text = arguments_text
    self.return_type = return_type
    #: The function-local static this symbol is the storage or the guard for, if any.
    self.static_variable = static_variable

detect

detect(name)

Whether name is a CodeWarrior symbol, decided by reading it.

There is no prefix that says so, so the whole name is parsed and the answer is whether it parsed. Screened first, so that the great majority of symbols cost a substring search and nothing else.

Source code in src/demangle/schemes/codewarrior/__init__.py
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
def detect(name):
    """Whether `name` is a CodeWarrior symbol, decided by reading it.

    There is no prefix that says so, so the whole name is parsed and the answer is
    whether it parsed. Screened first, so that the great majority of symbols cost a
    substring search and nothing else.
    """
    # Every name the reference reads has a `__` in it, and a special name (`__dt__...`)
    # a second one after its own: `__libc_start_main` is neither.
    if "__" not in name:
        return False
    if name[:2] == "__" and name.find("__", 2) < 0 and name[2:4] != "op":
        return False
    if len(name) > _DETECT_MAX or not _screen(name):
        return False
    # Itanium and Rust v0 refusals fall through to here; `__RTTI__` is CodeWarrior's own.
    if _OTHER_SCHEME.match(name) and not name.startswith("__RTTI__"):
        return False
    try:
        symbol = demangle_codewarrior(name)
    except DemangleFailure:
        return False
    except RecursionError:
        return False
    return symbol.text != name and _plausible(symbol)

parse

parse(
    mangled,
    builder,
    limits=DEFAULT_LIMITS,
    options=DEFAULT_OPTIONS,
)

Parse a CodeWarrior symbol into builder.

Source code in src/demangle/schemes/codewarrior/__init__.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=DEFAULT_OPTIONS):
    """Parse a CodeWarrior symbol into `builder`."""
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)
    if options is None:
        options = DEFAULT_OPTIONS

    try:
        symbol = demangle_codewarrior(
            mangled,
            omit_empty_parameters=options.omit_empty_parameters,
            mw_extensions=options.mw_extensions,
        )
    except DemangleFailure as error:
        raise ParseError(mangled, None, str(error)) from error
    except RecursionError as error:
        raise LimitExceeded(mangled, "recursion depth", limits.max_depth) from error

    if symbol.text == mangled:
        raise ParseError(mangled, None, "the name decodes to itself")
    if len(symbol.text) > limits.max_output:
        raise LimitExceeded(mangled, "output length", limits.max_output)

    if _wants_structure(builder):
        return nodes.build(symbol)
    return builder.raw(symbol.text)

demangle_codewarrior

demangle_codewarrior(
    mangled, omit_empty_parameters=True, mw_extensions=False
)

Read one CodeWarrior symbol. Raises DemangleFailure where it is not one.

Source code in src/demangle/schemes/codewarrior/_parser.py
424
425
426
427
428
429
430
431
432
433
def demangle_codewarrior(mangled, omit_empty_parameters=True, mw_extensions=False):
    """Read one CodeWarrior symbol. Raises `DemangleFailure` where it is not one."""
    options = _Options(omit_empty_parameters, mw_extensions)
    try:
        return _demangle(mangled, options)
    except RecursionError as error:
        raise DemangleFailure("nested too deeply") from error
    except (IndexError, ValueError) as error:
        # The reference returns None where a slice runs off the end; this says so.
        raise DemangleFailure(str(error) or "malformed name") from error

Options

demangle.schemes.codewarrior.options

How much of a CodeWarrior name to spell, and whether to read Metrowerks' extensions.

Both are the reference's own options, kept because they change what comes out and the vectors in cwdemangle's test module pin both settings of each.

DEFAULT_OPTIONS module-attribute

DEFAULT_OPTIONS = CodeWarriorOptions()

What demangle() uses when a caller names no options: the reference's own defaults.

CodeWarriorOptions dataclass

CodeWarriorOptions(
    omit_empty_parameters=True, mw_extensions=False
)

How to spell a CodeWarrior name.

omit_empty_parameters class-attribute instance-attribute
omit_empty_parameters = True

Write () for a function that takes none, rather than (void).

omit_empty_parameters. True is the reference's default and what its CLI prints.

mw_extensions class-attribute instance-attribute
mw_extensions = False

Read Metrowerks' extension types, __int128 and __vec2x32float__.

mw_extensions. Off by default, and that is the reference's choice rather than a conservative reading of it: the two are spelled 1 and 2, which collide with a template argument literal, so a name carrying either cannot always be told from one carrying a small integer. Turning this on is a caller saying they know which.

Parser

The Metrowerks CodeWarrior demangler, ported from encounter/cwdemangle.

CodeWarrior is the other pre-Itanium C++ mangling, and the reason it is a scheme of its own rather than a sixth style of gnuv2 is that libiberty never read it: cplus-dem.c has no CodeWarrior flag, demangle-expected has no vectors for it, and binutils has never demangled one. It is what Nintendo GameCube and Wii titles, and a great deal of other 1990s and 2000s embedded work, were built with -- which is why the reference is a decompilation project's tool rather than a compiler vendor's.

The reference is encounter/cwdemangle, dedicated to the public domain under CC0-1.0. This is a transcription of its demangle, with its function names kept.

The encoding looks like ARM's and is not:

  • a template argument list is written literally, single_ptr<10CModelData>, brackets and commas and all, rather than being encoded -- so finding where the name ends and the signature begins means counting < and > before looking for the __;
  • Q2 introduces a qualified name whose component count is a single digit, and each component is length-prefixed and may itself carry a template argument list;
  • a pointer-to-member is M<class>F followed by two hidden parameters, PCvPv or PCvPCv, and which of the two decides whether the member function is const;
  • the spelling is the reference's own: const char* with the * against the type, and void (*)(const char*, void*) built from a pre/post pair around the declarator.

Two options are the reference's: omit_empty_parameters, which is on by default and turns (void) into (), and mw_extensions, which is off by default because the Metrowerks extension types __int128 and __vec2x32float__ are spelled 1 and 2 and collide with template argument literals -- the reference says so, and this keeps its default rather than guessing.

Nodes

demangle.schemes.codewarrior.nodes

The CodeWarrior tree.

The same parts-in-order shape as the Go, Nim, D, JNI and pre-Itanium C++ trees: a node is the fragments it was built from, so rendering concatenates and the tree cannot spell a symbol differently from the text path.

The reference builds one string out of a pre/post pair, so the structure here is sliced out of the finished spelling at the boundaries the parser recorded -- where the qualified name ended, and exactly what the argument list contributed. Where the slice does not add up, the whole spelling becomes one name part rather than a structure that might not be true.

CodeWarriorName

CodeWarriorName(parts)

Bases: _CodeWarrior

The entity's name with its scope: rstl::basic_string<wchar_t>::mNull.

Source code in src/demangle/schemes/codewarrior/nodes.py
22
23
24
25
26
27
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Type

Type(parts)

Bases: _CodeWarrior

One parameter type, spelled.

Source code in src/demangle/schemes/codewarrior/nodes.py
22
23
24
25
26
27
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Parameters

Parameters(parts)

Bases: _CodeWarrior

The argument list, with each type a Type inside it.

Source code in src/demangle/schemes/codewarrior/nodes.py
22
23
24
25
26
27
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Symbol

Symbol(parts)

Bases: _CodeWarrior

A whole CodeWarrior symbol.

Source code in src/demangle/schemes/codewarrior/nodes.py
22
23
24
25
26
27
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

build

build(symbol)

Assemble the tree for a parsed CodeWarriorSymbol.

Source code in src/demangle/schemes/codewarrior/nodes.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def build(symbol):
    """Assemble the tree for a parsed `CodeWarriorSymbol`."""
    text = symbol.text
    name = symbol.qualified_name
    arguments = symbol.arguments_text

    if name:
        at = text.find(name)
        if at >= 0:
            head, tail = text[:at], text[at + len(name) :]
            middle = _parameters(name, arguments, symbol.parameters)
            if middle is not None:
                parts = []
                if head:
                    parts.append(head)
                parts.extend(middle)
                if tail:
                    parts.append(tail)
                return Symbol(parts)

    return Symbol([CodeWarriorName([text])])

Delphi / C++Builder

demangle.schemes.delphi

Borland/Embarcadero Delphi and C++Builder symbol names.

Delphi packages (BPLs) and C++Builder objects use the same mangling: @Unit@Class@Method then $ and a type encoding. Free Pascal's $-delimited scheme is a different one and is not read here.

There is no Delphi compiler on the platforms this package is developed on, so the grammar is a transcription of Embarcadero's own unmangler -- unmangle.c in the C++Builder RTL, the same code tdump -um runs -- via the comments and control flow preserved in that file. Spelling is what that unmangler prints, including the C++ :: qualifier, because that is the output a Delphi-built PE's exports are compared against by the toolchain.

tests/conformance/delphi-tdump.txt is a dump of the real tdump.exe -q -um over the export tables of real BPLs and C++Builder DLLs; its expected column is what that unmangler printed, and every readable entry is replayed. delphi-real-world.txt is a per-kind sample of the same dump. Independently, a reading must consume the whole symbol -- a parse that cannot account for the bytes is refused.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="delphi",
    node_kinds=("name", "parameters", "symbol"),
    detect=detect,
    parse=parse,
    description="Borland/Embarcadero Delphi and C++Builder symbol mangling",
    aliases=("borland", "bcc", "c++builder", "embarcadero"),
    first_characters="@",
    priority=35,
)

The scheme as the registry holds it, registered when this package is imported.

DelphiSymbol

DelphiSymbol(raw, text, kind)

One parsed Delphi/C++Builder symbol.

Source code in src/demangle/schemes/delphi/_parser.py
156
157
158
159
def __init__(self, raw, text, kind):
    self.raw = raw
    self.text = text
    self.kind = kind

detect

detect(name)

Whether name is a Delphi/C++Builder export, not MSVC @name@N.

@__swiftmacro_ is Swift's macro mangling and is left for that scheme: the two share a first character, and guessing would rewrite a Swift name.

A leading @@ without a $ encoding is __linkproc__ in the unmangler. Real BPL exports of that shape are a single identifier (@@InitExe). @@bug@@x is not, and is left unclaimed so a broken plugin's fixture is not rewritten. A qualified data name such as @System@Var has no $ and is still claimed.

@LOCAL@ and @GUARD@ are left for the CodeWarrior scheme, which is where they come from: Wii CodeWarrior spells a function-local static @LOCAL@<function>@<variable> and its guard @GUARD@.... Both parse here as a unit called LOCAL or GUARD, which is a name Borland never wrote.

A ? is refused for the same reason and a stronger one: no Borland production writes one -- there is none in any of the 11,363 recorded exports -- and it is the MSVC marker, so a name carrying one that reaches this scheme is a piece of somebody else's. This scheme copies characters through rather than checking an alphabet, so it would read the @?0??define_lambda@@YAHXZ@QBE@XZ left over when demangle_text tokenises an MSVC lambda name at its angle brackets as a Delphi declaration built out of half an MSVC symbol. Asked for by language it is still read; what this decides is whether to claim a name nobody said was Delphi's.

For the same reason the alphabet is checked as a whole rather than one forbidden character at a time. This scheme declares @ as its first character and is offered every symbol that starts with one, so it must not claim what no Borland tool wrote: a demangled Swift type such as @convention(block) (Swift.Int) -> Swift.UInt, whose @ markers would read as this grammar's qualifier separator, or @feat.00 and @comp.id, which every COFF object from MSVC and clang-cl carries. Reading either yields a name that is neither the symbol nor a declaration, the one answer this package treats as worse than leaving a name alone.

_SYMBOL_CHARACTERS is what the 11,363 readable recorded exports are made of and nothing else. See the module docstring for why that is the whole alphabet: a name is @-delimited qualifiers -- Pascal identifiers -- with $, %, & and # as the encoding's own markers. A . is not among them, which is what turns @feat.00 away.

Source code in src/demangle/schemes/delphi/_parser.py
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
def detect(name):
    """Whether `name` is a Delphi/C++Builder export, not MSVC `@name@N`.

    `@__swiftmacro_` is Swift's macro mangling and is left for that scheme: the two
    share a first character, and guessing would rewrite a Swift name.

    A leading `@@` without a `$` encoding is `__linkproc__` in the unmangler. Real BPL
    exports of that shape are a single identifier (`@@InitExe`). `@@bug@@x` is not, and
    is left unclaimed so a broken plugin's fixture is not rewritten. A qualified data
    name such as `@System@Var` has no `$` and is still claimed.

    `@LOCAL@` and `@GUARD@` are left for the CodeWarrior scheme, which is where they come
    from: Wii CodeWarrior spells a function-local static `@LOCAL@<function>@<variable>`
    and its guard `@GUARD@...`. Both parse here as a unit called `LOCAL` or `GUARD`,
    which is a name Borland never wrote.

    A `?` is refused for the same reason and a stronger one: no Borland production
    writes one -- there is none in any of the 11,363 recorded exports -- and it is *the*
    MSVC marker, so a name carrying one that reaches this scheme is a piece of somebody
    else's. This scheme copies characters through rather than checking an alphabet, so
    it would read the `@?0??define_lambda@@YAHXZ@QBE@XZ` left over when `demangle_text`
    tokenises an MSVC lambda name at its angle brackets as a Delphi declaration built
    out of half an MSVC symbol. Asked for by language it is still read; what this
    decides is whether to claim a name nobody said was Delphi's.

    For the same reason the alphabet is checked as a whole rather than one forbidden
    character at a time. This scheme declares `@` as its first character and is offered
    every symbol that starts with one, so it must not claim what no Borland tool wrote:
    a demangled Swift type such as `@convention(block) (Swift.Int) -> Swift.UInt`, whose
    `@` markers would read as this grammar's qualifier separator, or `@feat.00` and
    `@comp.id`, which every COFF object from MSVC and clang-cl carries. Reading either
    yields a name that is neither the symbol nor a declaration, the one answer this
    package treats as worse than leaving a name alone.

    `_SYMBOL_CHARACTERS` is what the 11,363 readable recorded exports are made of and
    nothing else. See the module docstring for why that is the whole alphabet: a name is
    `@`-delimited qualifiers -- Pascal identifiers -- with `$`, `%`, `&` and `#` as the
    encoding's own markers. A `.` is not among them, which is what turns `@feat.00`
    away.
    """
    if not name or name[0] != "@" or name.startswith("@__swift") or _MSVC_FASTCALL.match(name):
        return False
    if not _SYMBOL_CHARACTERS.fullmatch(name):
        return False
    if "?" in name:
        return False
    if name.startswith(("@LOCAL@", "@GUARD@")):
        return False
    if name.startswith("@@") and "$" not in name and not _LINKPROC_BARE.match(name):
        return False
    if not any(ch.isalpha() for ch in name):
        return False
    try:
        parse_delphi_symbol(name)
    except (DemangleFailure, LimitExceeded):
        return False
    return True

parse_delphi_symbol

parse_delphi_symbol(name, limits=DEFAULT_LIMITS)

Parse name, returning a DelphiSymbol, or raise DemangleFailure.

A name that exceeds limits raises LimitExceeded instead.

Source code in src/demangle/schemes/delphi/_parser.py
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
def parse_delphi_symbol(name, limits=DEFAULT_LIMITS):
    """Parse `name`, returning a `DelphiSymbol`, or raise `DemangleFailure`.

    A name that exceeds `limits` raises `LimitExceeded` instead.
    """
    if not name or name[0] != "@":
        raise DemangleFailure("not a Delphi mangled name")
    if _MSVC_FASTCALL.match(name):
        raise DemangleFailure("MSVC fastcall decoration")
    if not any(char.isalpha() for char in name):
        raise DemangleFailure("no identifier")
    if len(name) > limits.max_input:
        raise LimitExceeded(name, "input length", limits.max_input)
    parser = _Parser(name[1:], name, limits)
    try:
        text = parser.finish(do_args=True)
    except RecursionError as error:
        raise LimitExceeded(name, "recursion depth", limits.max_depth) from error
    if not text:
        raise DemangleFailure("empty result")
    if len(text) > limits.max_output:
        raise LimitExceeded(name, "output length", limits.max_output)
    return DelphiSymbol(name, text, parser.kind or KIND_DATA)

parse

parse(
    mangled, builder, limits=DEFAULT_LIMITS, options=None
)

Parse a Delphi/C++Builder symbol into builder.

Source code in src/demangle/schemes/delphi/__init__.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=None):
    """Parse a Delphi/C++Builder symbol into `builder`."""
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)

    try:
        symbol = parse_delphi_symbol(mangled, limits)
    except DemangleFailure as error:
        raise NotMangledError(mangled, str(error)) from error

    if len(symbol.text) > limits.max_output:
        raise LimitExceeded(mangled, "output length", limits.max_output)

    if _wants_structure(builder):
        return nodes.build(symbol)
    return builder.raw(symbol.text)

Parser

Reading Borland/Embarcadero mangled names.

Transcribed from unmangle.c in the C++Builder RTL -- the unmangler TDUMP, the linker and the debugger all run -- as ported with its comments intact. The control flow is that file's, not a reconstruction from examples.

A name is @-delimited qualifiers, then optionally $ and a type. Calling conventions are extra q letters after the function marker: qqr is __fastcall (Delphi's register), qqs is __stdcall, qqc is __cdecl. Types are single letters, or a decimal length followed by an @-qualified name. tN is a back-reference to an earlier parameter, 1-based in base 36.

Microsoft's 32-bit __fastcall C decoration is @name@bytes with a decimal count and no $. It is refused here, because it is not this scheme.

Nodes

demangle.schemes.delphi.nodes

The Delphi/C++Builder symbol tree.

Left-to-right like Go and Free Pascal: a node is its fragments in order, rendering concatenates, and the tree cannot spell a symbol differently from the text path.

What a caller wants is the unit, the class, and the encoded parameters -- none of which survives splitting the spelling, because a length-prefixed type can itself hold @ and % template markup.

DelphiName

DelphiName(parts)

Bases: _Delphi

A unit, class, or routine name.

Source code in src/demangle/schemes/delphi/nodes.py
19
20
21
22
23
24
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Parameters

Parameters(parts)

Bases: _Delphi

The parameter list, as the mangling recorded it.

Source code in src/demangle/schemes/delphi/nodes.py
19
20
21
22
23
24
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Symbol

Symbol(parts, delphi_kind)

Bases: _Delphi

A whole Delphi/C++Builder symbol.

delphi_kind is what the mangling said this is -- function, constructor, data, and so on -- which the spelling alone does not always say.

Source code in src/demangle/schemes/delphi/nodes.py
67
68
69
def __init__(self, parts, delphi_kind):
    super().__init__(parts)
    self.delphi_kind = delphi_kind

build

build(symbol)

Assemble the tree for a parsed DelphiSymbol.

Source code in src/demangle/schemes/delphi/nodes.py
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
98
def build(symbol):
    """Assemble the tree for a parsed `DelphiSymbol`."""
    text = symbol.text
    open_paren = text.find("(")
    if text.startswith("()", open_paren) and text.endswith("operator ", 0, open_paren):
        # `operator ()` is a name, and the parameter list is the next `(`.
        open_paren = text.find("(", open_paren + 2)
    if open_paren < 0:
        return Symbol([DelphiName([text])], symbol.kind)
    head, rest = text[:open_paren], text[open_paren:]
    close = rest.rfind(")")
    if close < 0:
        return Symbol([DelphiName([text])], symbol.kind)
    params = rest[1:close]
    tail = rest[close + 1 :]
    parts = [DelphiName([head]), "("]
    if params:
        inner = []
        for at, piece in enumerate(_top_level_parameters(params)):
            if at:
                inner.append(", ")
            inner.append(DelphiName([piece]))
        parts.append(Parameters(inner))
    parts.append(")")
    if tail:
        parts.append(tail)
    return Symbol(parts, symbol.kind)

Ada / GNAT

demangle.schemes.ada

Ada, as GNAT encodes it.

The last of the pre-Itanium formats libiberty still carries, reached as c++filt --format=gnat and dropped from nothing: when the GNU v2, lucid, ARM and HP styles were removed from the default, gnat stayed. Narrow but concentrated -- avionics, rail, defence -- and the reference is ada_demangle in libiberty/cplus-dem.c, with GCC's own exp_dbug.ads documenting the encoding normatively.

Detection is the whole difficulty, and it is settled by measurement rather than argument. An Ada symbol carries no types and no marker: yz__qrs is a package and a subprogram, and it is also exactly what a C program writes. Parsing the name and claiming whatever parses -- what a demangler for a scheme with a marker can afford -- reads 797 names from the other schemes' corpora and 6,764 real symbols from this machine's own libraries as Ada. That is not detection, it is a coin toss with a confident voice.

So a name is claimed only when it carries something GNAT wrote and a C compiler would not: the _ada_ prefix, an O-operator, a TK task suffix, a P/N protected subprogram, a stream S[RWIO], a controlled D[FA], an X body-nested marker, a ___elabb-style special name, a _B/_E entry body, or an overload number -- and the whole name has to be accounted for, because several of the reference's suffixes stop reading and abandon the rest. Measured under that rule: 0 claims over every other checked-in corpus, and 0 over 339,117 symbols from this machine's libraries.

The cost is that a name with no such marker -- yz__qrs, and four of the 34 reference vectors -- is not auto-detected (x_E is not counted: the reference itself declines it). It demangles when a caller says language="ada", which is the same bargain the Go scheme makes and for the same reason: failing to claim a name returns it unchanged, which is what an unreadable name does anyway, while claiming someone else's rewrites it into a plausible lie. tests/test_ada.py pins both numbers, so neither can drift quietly.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="ada",
    node_kinds=("attribute", "component", "symbol"),
    detect=detect,
    parse=parse,
    description="Ada symbol names as GNAT encodes them (c++filt --format=gnat)",
    aliases=("gnat",),
    symbol_table_decorations=True,
    priority=280,
)

The scheme as the registry holds it, registered when this package is imported.

AdaSymbol

AdaSymbol(text, parts, evidence, unread='')

One demangled GNAT name.

text is the whole spelling. parts are the dot-separated components in order, which is what the tree is built from: an Ada name is a path and nothing else, and splitting text on . would be wrong the moment a component is .":=" or the entity carries a .Finalize.

Source code in src/demangle/schemes/ada/_parser.py
 99
100
101
102
103
104
105
106
107
def __init__(self, text, parts, evidence, unread=""):
    self.text = text
    self.parts = tuple(parts)
    #: What the reference stopped without reading: several suffixes abandon the
    #: rest (`...controllerDF__2` drops the `__2`). `detect` declines such names.
    self.unread = unread
    #: Which GNAT-specific encodings this name carried; empty for one
    #: indistinguishable from a C name. `detect` reads this.
    self.evidence = frozenset(evidence)

demangle_ada

demangle_ada(mangled)

Read a GNAT-encoded name, or raise DemangleFailure.

A transcription of ada_demangle, following its control flow rather than re-deriving the grammar: the loop, the ordered suffix tests, and every goto unknown. Comments name the reference's own.

Source code in src/demangle/schemes/ada/_parser.py
121
122
123
124
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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
211
212
213
214
215
216
217
218
219
220
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
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
def demangle_ada(mangled):
    """Read a GNAT-encoded name, or raise `DemangleFailure`.

    A transcription of `ada_demangle`, following its control flow rather than
    re-deriving the grammar: the loop, the ordered suffix tests, and every `goto
    unknown`. Comments name the reference's own.
    """
    if not mangled:
        raise DemangleFailure("empty name")

    evidence = set()
    text = mangled
    if text.startswith(_LIBRARY_PREFIX):
        # "Discard leading _ada_, which is used for library level subprograms."
        text = text[len(_LIBRARY_PREFIX) :]
        evidence.add("library-prefix")

    # "All ada unit names are lower-case."
    if not text or not _is_lower(text[0]):
        raise DemangleFailure("an Ada unit name starts with a lower-case letter")

    out = []
    parts = []
    at = 0
    size = len(text)

    def rest(offset=0):
        """The character at `at + offset`, or "" past the end -- the reference's `p[n]`.

        The C reads one past the last character freely, because the string is NUL
        terminated and `p[1] == 0` is a meaningful test. Returning "" keeps those tests
        readable here without a length check at every one.
        """
        index = at + offset
        return text[index] if index < size else ""

    while True:
        start = at
        if _is_lower(rest()):
            # "An identifier, which is always lower case."
            at += 1
            # `_` continues the identifier only before a lower-case letter or digit, so
            # `x_E` leaves `_E` for the exception-marker test.
            while (
                _is_lower(rest()) or _is_digit(rest()) or (rest() == "_" and (_is_lower(rest(1)) or _is_digit(rest(1))))
            ):
                at += 1
            out.append(text[start:at])
            parts.append(text[start:at])
        elif rest() == "O":
            # "An operator name."
            for key in _OPERATOR_KEYS:
                if text.startswith(key, at):
                    at += len(key)
                    spelled = f'"{_OPERATORS[key]}"'
                    out.append(spelled)
                    parts.append(spelled)
                    evidence.add("operator")
                    break
            else:
                raise DemangleFailure("not a GNAT operator name")
        else:
            # "Not a GNAT encoding."
            raise DemangleFailure("not a GNAT encoding")

        # Suffixes: sequential `if`s in the reference, two of which can fire for one
        # name. Order and independence change readings; keep both.
        if rest() == "T" and rest(1) == "K":
            evidence.add("task")
            if rest(2) == "B" and rest(3) == "":
                # "Subprogram for task body."
                at += 3
                break
            if rest(2) == "_" and rest(3) == "_":
                # "Inner declarations in a task."
                at += 4
                out.append(".")
                continue
            raise DemangleFailure("not a GNAT task encoding")

        if rest() == "E" and rest(1) == "":
            # "Exception name." -- the reference declines these.
            raise DemangleFailure("an exception name is not demangled")

        if rest() in ("P", "N") and rest(1) == "":
            # "Protected type subprogram."
            evidence.add("protected")
            at += 1
            break

        if rest() in ("N", "S") and rest(1) == "":
            # "Enumerated type name table." Only `S` can reach here: `N` broke above.
            raise DemangleFailure("an enumerated type name table is not demangled")

        if rest() == "X":
            # "Body nested."
            evidence.add("body-nested")
            at += 1
            while rest() in ("n", "b"):
                at += 1

        if rest() == "S" and rest(1) != "" and rest(2) in ("_", ""):
            # "Stream operations."
            name = _STREAM.get(rest(1))
            if name is None:
                raise DemangleFailure("not a GNAT stream operation")
            at += 2
            out.append(name)
            parts.append(name)
            evidence.add("stream")
        elif rest() == "D":
            # "Controlled type operation."
            name = _CONTROLLED.get(rest(1))
            if name is None:
                raise DemangleFailure("not a GNAT controlled-type operation")
            out.append(name)
            parts.append(name)
            evidence.add("controlled")
            at += 2
            break

        if rest() == "_":
            if rest(1) == "_":
                # "Standard separator.  Handled first."
                at += 2
                if _is_digit(rest()):
                    # "Overloading number."
                    evidence.add("overload")
                    at += 1
                    while _is_digit(rest()) or (rest() == "_" and _is_digit(rest(1))):
                        at += 1
                    if rest() == "X":
                        evidence.add("body-nested")
                        at += 1
                        while rest() in ("n", "b"):
                            at += 1
                elif rest() == "_" and rest(1) != "_":
                    # "Special names."
                    for key in _SPECIAL_KEYS:
                        if text.startswith(key, at):
                            at += len(key)
                            out.append(_SPECIAL[key])
                            parts.append(_SPECIAL[key])
                            evidence.add("special")
                            break
                    else:
                        raise DemangleFailure("not a GNAT special name")
                    break
                else:
                    out.append(".")
                    continue
            elif rest(1) in ("B", "E"):
                # "Entry Body or barrier Evaluation."
                evidence.add("entry-body")
                at += 2
                while _is_digit(rest()):
                    at += 1
                if rest() == "s" and rest(1) == "":
                    at += 1
                    break
                raise DemangleFailure("not a GNAT entry body")
            else:
                raise DemangleFailure("not a GNAT separator")

        if rest() == "." and _is_digit(rest(1)):
            # "Nested subprogram."
            evidence.add("nested-subprogram")
            at += 2
            while _is_digit(rest()):
                at += 1

        if rest() == "":
            # "End of mangled name."
            break
        raise DemangleFailure("trailing characters after a complete GNAT name")

    return AdaSymbol("".join(out), parts, evidence, text[at:])

detect

detect(name)

Whether name is a GNAT symbol, decided by reading it and by what it carries.

Two conditions, and the second is the one that matters. The name has to parse, and it has to hold at least one encoding GNAT writes and a C compiler does not -- see the module docstring for why, and for what the alternative measured at.

Source code in src/demangle/schemes/ada/__init__.py
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
def detect(name):
    """Whether `name` is a GNAT symbol, decided by reading it *and* by what it carries.

    Two conditions, and the second is the one that matters. The name has to parse, and
    it has to hold at least one encoding GNAT writes and a C compiler does not -- see
    the module docstring for why, and for what the alternative measured at.
    """
    # Cheapest reject first: a GNAT name holds a `__` or starts `_ada_` (all 34 of the
    # reference's vectors have a `__`).
    if "__" not in name and not name.startswith(_LIBRARY_PREFIX):
        return False
    if not name or len(name) > _DETECT_MAX:
        return False
    library = name.startswith(_LIBRARY_PREFIX)
    head = name[5:6] if library else name[:1]
    if not ("a" <= head <= "z"):
        return False
    if not _MAY_CARRY_EVIDENCE.search(name):
        return False
    try:
        symbol = demangle_ada(name)
    except DemangleFailure:
        return False
    if not symbol.evidence:
        return False
    # `parse` stays faithful to the reference and tolerates trailing text; claiming such
    # a name is decided here.
    return not symbol.unread or _TRAILING_OVERLOAD.fullmatch(symbol.unread) is not None

parse

parse(
    mangled, builder, limits=DEFAULT_LIMITS, options=None
)

Parse a GNAT symbol into builder.

No second detection test. parse is reached either because detect claimed the name or because a caller named the language, and in the second case the caller has said what it is -- which is how yz__qrs, correctly not auto-detected, is still readable on request.

Source code in src/demangle/schemes/ada/__init__.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=None):
    """Parse a GNAT symbol into `builder`.

    No second detection test. `parse` is reached either because `detect` claimed the
    name or because a caller named the language, and in the second case the caller has
    said what it is -- which is how `yz__qrs`, correctly not auto-detected, is still
    readable on request.
    """
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)

    try:
        symbol = demangle_ada(mangled)
    except DemangleFailure as error:
        raise NotMangledError(mangled, str(error)) from error

    if len(symbol.text) > limits.max_output:
        raise LimitExceeded(mangled, "output length", limits.max_output)

    if _wants_structure(builder):
        return nodes.build(symbol)
    return builder.raw(symbol.text)

Nodes

demangle.schemes.ada.nodes

The Ada symbol tree.

The same parts-in-order shape as the Go, Nim, D and JNI trees: a node is the fragments it was built from, so rendering concatenates and the tree cannot spell a symbol differently from the text path.

What the structure is for is the one question the spelling cannot answer. An Ada name is a dotted path, but text.split(".") is wrong the moment a component is an operator -- oper."+", and system.finalization_root.":=" has a . inside a component -- or the entity carries a .Finalize. The parts here were separated while reading, so they are the components the parser actually found.

Component

Component(parts)

Bases: _Ada

One element of the path: a unit, a subprogram, or a quoted operator name.

Source code in src/demangle/schemes/ada/nodes.py
22
23
24
25
26
27
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Attribute

Attribute(parts)

Bases: _Ada

What the entity is, where GNAT encoded that rather than leaving it implicit.

'Size, 'Elab_Body, 'Read, .Finalize. Written by the compiler rather than the programmer, and carrying its own leading punctuation because the reference does -- an attribute joins with ' and a controlled-type operation with ..

Source code in src/demangle/schemes/ada/nodes.py
22
23
24
25
26
27
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Symbol

Symbol(parts)

Bases: _Ada

A whole GNAT symbol.

Source code in src/demangle/schemes/ada/nodes.py
22
23
24
25
26
27
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

build

build(symbol)

Assemble the tree for a parsed AdaSymbol.

Source code in src/demangle/schemes/ada/nodes.py
77
78
79
80
81
82
83
84
85
86
87
def build(symbol):
    """Assemble the tree for a parsed `AdaSymbol`."""
    parts = []
    for at, part in enumerate(symbol.parts):
        if part.startswith(_ATTRIBUTE_STARTS):
            parts.append(Attribute([part]))
            continue
        if at:
            parts.append(".")
        parts.append(Component([part]))
    return Symbol(parts)

JNI

demangle.schemes.jni

JNI native method names.

A Java method declared native is called through a C function whose name encodes the method's class, its name, and -- where it is overloaded -- its parameter types. Android apps ship these by the thousand, and the usual way to read one is by eye.

The encoding is in the JNI specification's Design Overview under "Resolving Native Method Names", which is unusually good fortune: every other scheme here had to be transcribed from a reference implementation, and this one is written down normatively. See _parser.py for what it says and for the one thing it does not preserve.

Reference: https://docs.oracle.com/en/java/javase/17/docs/specs/jni/design.html

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="jni",
    node_kinds=("name", "parameters", "path", "symbol"),
    detect=detect,
    parse=parse,
    description="JNI native method names (Java_pkg_Class_method)",
    aliases=("java",),
    first_characters="J",
    priority=15,
)

The scheme as the registry holds it, registered when this package is imported.

JniSymbol

JniSymbol(raw, declaring, method, parameters)

One parsed JNI symbol: its dotted path, and its parameter types where it has any.

Source code in src/demangle/schemes/jni/_parser.py
73
74
75
76
77
78
79
80
81
def __init__(self, raw, declaring, method, parameters):
    self.raw = raw
    self.declaring = declaring
    self.method = method
    #: `None` where the symbol carries no signature (only an overloaded native does);
    #: `()` would say the method takes nothing.
    self.parameters = parameters
    joined = f"{declaring}.{method}" if declaring else method
    self.text = joined if parameters is None else f"{joined}({', '.join(parameters)})"

descriptor_types

descriptor_types(text)

The parameter types of a JVM descriptor's argument list, spelled as source.

text is the unescaped argument run -- no brackets and no return type, which is what JNI mangles. Ljava/lang/String;I[B is java.lang.String, int, byte[].

Source code in src/demangle/schemes/jni/_parser.py
124
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
def descriptor_types(text):
    """The parameter types of a JVM descriptor's argument list, spelled as source.

    `text` is the *unescaped* argument run -- no brackets and no return type, which is
    what JNI mangles. `Ljava/lang/String;I[B` is `java.lang.String`, `int`, `byte[]`.
    """
    found = []
    at = 0
    while at < len(text):
        arrays = 0
        while at < len(text) and text[at] == "[":
            arrays += 1
            at += 1
        if at >= len(text):
            raise DemangleFailure("array descriptor with no element type")
        char = text[at]
        at += 1
        if char == "V":
            # `V` is a return type only (JVMS 4.3.2), never a parameter or array element.
            raise DemangleFailure("void is not a parameter type")
        if char in PRIMITIVES:
            spelled = PRIMITIVES[char]
        elif char == "L":
            end = text.find(";", at)
            if end < 0:
                raise DemangleFailure("class descriptor with no terminator")
            spelled = text[at:end].replace("/", ".")
            if not spelled:
                raise DemangleFailure("class descriptor with no name")
            at = end + 1
        else:
            raise DemangleFailure(f"unknown type descriptor {char!r}")
        found.append(spelled + "[]" * arrays)
    return tuple(found)

parse_jni_symbol

parse_jni_symbol(name, limits=DEFAULT_LIMITS)

Parse name, returning a JniSymbol, or raise DemangleFailure.

Source code in src/demangle/schemes/jni/_parser.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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
211
212
213
214
215
216
def parse_jni_symbol(name, limits=DEFAULT_LIMITS):
    """Parse `name`, returning a `JniSymbol`, or raise `DemangleFailure`."""
    if not name.startswith(PREFIX):
        raise DemangleFailure("not a JNI native method name")
    body = name[len(PREFIX) :]
    if not body:
        raise DemangleFailure("nothing after the prefix")

    # Not simply the first `__`: a `/` before an escape also writes one
    # (`com/example/Foo/π` is `com_example_Foo__003c0`). The real separator is
    # followed by a JVM descriptor, so each candidate is tried in turn.
    path = None
    parameters = None
    at = body.find("__")
    while at >= 0:
        tail_at = at + 2
        if (
            tail_at < len(body)
            and body[tail_at] not in PRIMITIVES
            and body[tail_at] != "L"
            and not body.startswith(("_3", "_0"), tail_at)
        ):
            # A descriptor opens on an ASCII type code or an escaped array bracket.
            # `_0` is retained for callers escaping an ASCII type code explicitly.
            # Unicode names preceded by `/` also make `__`, but their digit-led
            # suffix cannot be a signature; avoid copying/decoding each full tail.
            at = body.find("__", at + 1)
            continue
        head, tail = body[:at], body[tail_at:]
        if head:
            try:
                head_path = unescape(head)
                if "//" in head_path:
                    # An empty component already decoded in the prefix cannot be
                    # repaired by any later separator. The final path check refuses it.
                    break
                # An empty tail is valid: an overloaded method taking no arguments.
                candidate = descriptor_types(unescape(tail))
            except DemangleFailure:
                candidate = None
            if candidate is not None:
                path, parameters = head_path, candidate
                break
        at = body.find("__", at + 1)
    if path is None:
        path = unescape(body)
        if "//" in path:
            # A non-separator `__` leaves no empty component, so an empty one means the
            # `__` was the separator and its tail was not a descriptor.
            raise DemangleFailure("no component between separators")
    declaring, _, method = path.rpartition("/")
    if not method:
        raise DemangleFailure("no method name")
    if not declaring:
        # A native method is always in a class; otherwise any C `Java_helper` is claimed.
        raise DemangleFailure("no declaring class")
    return JniSymbol(name, declaring.replace("/", "."), method, parameters)

detect

detect(name)

Whether name is a JNI native method symbol.

The prefix is distinctive but not proof: a C function may be called Java_helper, and claiming one would rewrite a name this library was not sure about. So the name must also decode: a declaring class as well as a method -- one separator at least, which Java_helper does not have -- and, where a signature is present, one that is a valid JVM descriptor. Java_C_m__Q is refused because Q names no type, and Java_C_m__ because the separator promises a signature that is not there.

A malformed escape is not a refusal, because it cannot be told from the thing it would be malformed as: _0abc is _0 and three hex digits, and it is also a / followed by 0abc. The mangling gives / no escape of its own, so the second reading is always available and the first is never certain.

Source code in src/demangle/schemes/jni/__init__.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def detect(name):
    """Whether `name` is a JNI native method symbol.

    The prefix is distinctive but not proof: a C function may be called `Java_helper`,
    and claiming one would rewrite a name this library was not sure about. So the name
    must also *decode*: a declaring class as well as a method -- one separator at least,
    which `Java_helper` does not have -- and, where a signature is present, one that is
    a valid JVM descriptor. `Java_C_m__Q` is refused because `Q` names no type, and
    `Java_C_m__` because the separator promises a signature that is not there.

    A malformed *escape* is not a refusal, because it cannot be told from the thing it
    would be malformed as: `_0abc` is `_0` and three hex digits, and it is also a `/`
    followed by `0abc`. The mangling gives `/` no escape of its own, so the second
    reading is always available and the first is never certain.
    """
    if not name.startswith(PREFIX):
        return False
    try:
        return parse_jni_symbol(name) is not None
    except DemangleFailure:
        return False

parse

parse(
    mangled, builder, limits=DEFAULT_LIMITS, options=None
)

Parse a JNI symbol into builder.

Source code in src/demangle/schemes/jni/__init__.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=None):
    """Parse a JNI symbol into `builder`."""
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)
    if not mangled.startswith(PREFIX):
        raise NotMangledError(mangled, "not a JNI native method name")
    try:
        symbol = parse_jni_symbol(mangled, limits)
    except DemangleFailure as failure:
        raise ParseError(mangled, 0, str(failure)) from None
    if len(symbol.text) > limits.max_output:
        raise LimitExceeded(mangled, "output length", limits.max_output)
    if _wants_structure(builder):
        return nodes.build(symbol)
    return builder.raw(symbol.text)

Nodes

demangle.schemes.jni.nodes

The JNI symbol tree.

The same parts-in-order shape as the Go, Nim and D trees: a node is the fragments it was built from, so rendering concatenates and the tree cannot spell a symbol differently from the text path.

What the structure is for is the one question splitting the spelling cannot answer: a parameter type may itself contain a . -- java.lang.String is three of them -- so text.split(", ") on the argument list is right and split(".") on the path is not. The parameters node hands the types over already separated.

Path

Path(parts)

Bases: _Jni

The package and class the method was declared in, as far as the name says.

Both are joined with the same character the mangling uses for a package separator, so the two cannot be told apart; this is the whole of what precedes the method.

Source code in src/demangle/schemes/jni/nodes.py
21
22
23
24
25
26
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

JniName

JniName(parts)

Bases: _Jni

The method's own name, with its escapes undone.

Source code in src/demangle/schemes/jni/nodes.py
21
22
23
24
25
26
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Parameters

Parameters(parts)

Bases: _Jni

The parameter types, present only where the method is overloaded.

Source code in src/demangle/schemes/jni/nodes.py
21
22
23
24
25
26
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Symbol

Symbol(parts)

Bases: _Jni

A whole JNI native method symbol.

Source code in src/demangle/schemes/jni/nodes.py
21
22
23
24
25
26
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

build

build(symbol)

Assemble the tree for a parsed JniSymbol.

Source code in src/demangle/schemes/jni/nodes.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def build(symbol):
    """Assemble the tree for a parsed `JniSymbol`."""
    parts = []
    if symbol.declaring:
        parts.extend((Path([symbol.declaring]), "."))
    parts.append(JniName([symbol.method]))
    if symbol.parameters is not None:
        inner = []
        for at, parameter in enumerate(symbol.parameters):
            if at:
                inner.append(", ")
            inner.append(JniName([parameter]))
        parts.extend(("(", Parameters(inner), ")"))
    return Symbol(parts)

Objective-C

demangle.schemes.objc

Objective-C symbol names.

Objective-C is barely mangled, and what mangling there is comes from the compiler rather than from the language: clang and GCC each write the runtime's data structures into symbols of their own devising, and only the GNU-family method form is a mangling in the sense the other schemes here use the word. So the rules are transcribed from clang's lib/AST/Mangle.cpp, lib/CodeGen/CGObjCMac.cpp and lib/CodeGen/CGObjCGNU.cpp, and from the symbols GCC's own front end left in the shipped libobjc.a.

Four families are read:

-[NSString stringWithFormat:]     the Apple runtimes, and what every crash log shows
_i_NSString__stringWithFormat_    the GNU family: GCC's runtime and GNUstep's
_OBJC_CLASS_$_NSString            Apple's non-fragile ABI
.objc_class_name_NSString         Apple's fragile ABI, and the GNU runtimes' own

Clang's block invocation functions (___<len><method>_block_invoke, __block_literal_global, __block_descriptor) are also read. The length prefix before a method is verified against the method's text; a mismatched count is refused rather than guessed at.

Correctness rests on re-assembly, as it does for Go, Nim and Free Pascal, and on agreement with what the compiler emitted for declarations this package wrote: 445 of 445 symbols clang produced for Objective-C written the way Objective-C is written, across the macOS, fragile and GNUstep ABIs. Over a corpus built to put underscores inside class names, category names and selectors -- the collision clang's own source warns about -- 396 of 422, and Symbol.ambiguous marks every name where more than one reading exists.

The GNU-family method mangling is not injective. clang says so where it writes it: "This is the mangling we've always used on the GNU runtimes, but it has obvious collisions in the face of underscores within class names, category names, and selectors." A : and a separator are both written _, so _i_A_B_c is -[A(B) c] and -[A_B c] and -[A(B_c) ] alike. Every reading that re-mangles to the symbol is found; the one preferred is the one needing no category, because a method outside a category leaves the field empty and its two separators fall together into a visible doubled underscore.

Two GNUstep forms are refused rather than guessed at. .objc_category_FooBar joins the class and category names with no separator at all, and nothing can say where one ends. .objc_selector_<name>_<types> is split at the rightmost underscore whose left half is a selector, which is right unless the type encoding names a struct.

PLUGIN module-attribute

PLUGIN = LanguagePlugin(
    name="objc",
    node_kinds=("name", "path", "symbol"),
    detect=detect,
    parse=parse,
    description="Objective-C method, class and runtime symbol names",
    aliases=("objective-c", "objectivec"),
    priority=30,
    first_characters="-+_.lL",
)

The scheme as the registry holds it, registered when this package is imported.

DemangleFailure

Bases: Exception

Raised when a name is not one this reads.

ObjcSymbol

ObjcSymbol(
    raw,
    text,
    kind,
    *,
    runtime="",
    class_name=None,
    category=None,
    selector=None,
    ivar=None,
    block=None,
    ambiguous=False,
    label="",
)

One Objective-C symbol.

text is the readable spelling. The remaining fields are what the mangling said, and raw is the symbol exactly as it came in, so a caller can rejoin. label is the words a runtime data symbol's spelling opens with, Objective-C class say, and empty for a method or a block.

Source code in src/demangle/schemes/objc/_parser.py
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
def __init__(
    self,
    raw,
    text,
    kind,
    *,
    runtime="",
    class_name=None,
    category=None,
    selector=None,
    ivar=None,
    block=None,
    ambiguous=False,
    label="",
):
    self.raw = raw
    self.text = text
    self.kind = kind
    self.runtime = runtime
    self.class_name = class_name
    self.category = category
    self.selector = selector
    self.ivar = ivar
    #: `(index, parent)` for a block invocation function, else None.
    self.block = block
    #: Whether another reading of a GNU-family method name re-mangles to the same
    #: symbol. Never true for the Apple form.
    self.ambiguous = ambiguous
    self.label = label

detect

detect(name)

Whether name is one this reads, and distinctive enough to claim unasked.

The screen is only there to keep the parse off every symbol in a binary; what actually claims a name is the parse succeeding. A form that says objc somewhere in it is worth trying, and so are the two that do not: -[/+[, and the _i_/_c_ method mangling.

The markers are tested one by one rather than through any(...) over _SCREEN_MARKERS. This plugin declares _ among its first characters, so it is offered every underscore-prefixed symbol in a binary -- 85% of the benchmark corpus -- and the generator would cost six interpreter frames per name to run five membership tests that are each a single C-level scan. Written out it is the same five scans in the same short-circuiting order, and no frames at all: 71,778 calls fewer over the cold corpus. _SCREEN_MARKERS stays as the documentation of what the screen is.

Source code in src/demangle/schemes/objc/_parser.py
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
def detect(name):
    """Whether `name` is one this reads, and distinctive enough to claim unasked.

    The screen is only there to keep the parse off every symbol in a binary; what
    actually claims a name is the parse succeeding. A form that says `objc` somewhere in
    it is worth trying, and so are the two that do not: `-[`/`+[`, and the `_i_`/`_c_`
    method mangling.

    The markers are tested one by one rather than through `any(...)` over
    `_SCREEN_MARKERS`. This plugin declares `_` among its first characters, so it is
    offered every underscore-prefixed symbol in a binary -- 85% of the benchmark corpus
    -- and the generator would cost six interpreter frames per name to run five
    membership tests that are each a single C-level scan. Written out it is the same
    five scans in the same short-circuiting order, and no frames at all: 71,778 calls
    fewer over the cold corpus. `_SCREEN_MARKERS` stays as the documentation of what the
    screen is.
    """
    if not name:
        return False
    if name[0] in "-+":
        return _apple_method(name) is not None
    if (
        "objc" not in name
        and "OBJC" not in name
        and "_block_invoke" not in name
        and "block_literal" not in name
        and "block_descriptor" not in name
        and not _method_prefixed(name)
    ):
        return False
    try:
        return parse_objc_symbol(name) is not None
    except DemangleFailure:
        return False

gnu_method_readings

gnu_method_readings(name, limit=None)

Every (class, category, selector) that re-mangles to name, best first.

The mangling writes _ for both a separator and a :, and does nothing to mark which is which, so a symbol can have several readings and all of them are equally well-formed. Every split is tried and only those that re-mangle to the input survive; what orders them is the one structural signal the mangling does carry.

A method that is not in a category has an empty category field, and the two separators around it fall together into a doubled underscore. That is visible, and it is the common case, so a reading that needs no category is preferred over one that does. Within each group the leftmost split wins, which is the shortest class name -- the reading a class whose name holds no underscore produces.

limit stops the search once that many readings are in hand. The caller that wants a spelling passes 2, because all it needs is the best reading and whether a second exists; enumerating the rest is work nobody reads.

Measured against the compiler, over symbols clang emitted for declarations this package generated: 131 of 131 correct when class, category and selector are ordinary identifiers without underscores, which is how Objective-C is conventionally written; 96 of 122 on a corpus built to put underscores in all three, where 65 of the 122 have more than one reading at all. ObjcSymbol.ambiguous says which those are.

Source code in src/demangle/schemes/objc/_parser.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
211
212
213
214
215
216
217
218
219
220
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
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
def gnu_method_readings(name, limit=None):
    """Every `(class, category, selector)` that re-mangles to `name`, best first.

    The mangling writes `_` for both a separator and a `:`, and does nothing to mark
    which is which, so a symbol can have several readings and all of them are equally
    well-formed. Every split is tried and only those that re-mangle to the input
    survive; what orders them is the one structural signal the mangling does carry.

    A method that is not in a category has an *empty* category field, and the two
    separators around it fall together into a doubled underscore. That is visible, and
    it is the common case, so a reading that needs no category is preferred over one
    that does. Within each group the leftmost split wins, which is the shortest class
    name -- the reading a class whose name holds no underscore produces.

    `limit` stops the search once that many readings are in hand. The caller that wants
    a spelling passes 2, because all it needs is the best reading and whether a second
    exists; enumerating the rest is work nobody reads.

    Measured against the compiler, over symbols clang emitted for declarations this
    package generated: 131 of 131 correct when class, category and selector are ordinary
    identifiers without underscores, which is how Objective-C is conventionally written;
    96 of 122 on a corpus built to put underscores in all three, where 65 of the 122 have
    more than one reading at all. `ObjcSymbol.ambiguous` says which those are.
    """
    if not (name.startswith("_i_") or name.startswith("_c_")):
        return []
    body = name[3:]
    if not body:
        return []

    # The selector for a tail is read once per position, not once per (class, category)
    # pair; recomputing it there would make the search cubic.
    marks = [index for index, char in enumerate(body) if char == "_"][:_MAX_SEPARATORS]
    tails = {}

    def selector_after(mark):
        """The selector spelled by everything past `mark`, or None. Memoised."""
        found = tails.get(mark)
        if found is None:
            tail = body[mark + 1 :]
            selector = _unmangle_selector(tail)
            # `mangle_gnu_method(...) == name` reduces to this: a candidate's class and
            # category are slices of `body`, so they re-mangle by construction and only
            # the selector can disagree.
            if selector is not None and _remangle_selector(selector) != tail:
                selector = None
            found = tails[mark] = (selector,)
        return found[0]

    # Once a prefix is not an identifier, no longer prefix is either.
    stop = len(body)
    for mark in marks:
        if mark and not _IDENTIFIER.match(body[:mark]):
            stop = mark
            break

    readings = []

    def take(class_name, category, selector):
        readings.append((class_name, category or None, selector))
        return limit is not None and len(readings) >= limit

    # Pass one: no category, preferred and cheap (no pair to search).
    for mark in marks:
        if mark == 0 or mark >= stop:
            continue
        if mark + 1 >= len(body) or body[mark + 1] != "_":
            continue
        selector = selector_after(mark + 1)
        if selector is not None and take(body[:mark], "", selector):
            return readings

    # Pass two: readings that need a category, over the already-bounded `marks`.
    for class_end in marks:
        if class_end == 0 or class_end >= stop:
            continue
        class_name = body[:class_end]
        for category_end in marks:
            if category_end <= class_end + 1:
                continue
            category = body[class_end + 1 : category_end]
            if not _IDENTIFIER.match(category):
                continue
            selector = selector_after(category_end)
            if selector is not None and take(class_name, category, selector):
                return readings
    return readings

mangle_gnu_method

mangle_gnu_method(
    is_class_method, class_name, category, selector
)

The GNU-family symbol for a method, from clang's mangleObjCMethodName.

Written out because it is what makes reading one checkable: a reading is only accepted when re-mangling it reproduces the symbol.

Source code in src/demangle/schemes/objc/_parser.py
139
140
141
142
143
144
145
146
147
148
def mangle_gnu_method(is_class_method, class_name, category, selector):
    """The GNU-family symbol for a method, from clang's `mangleObjCMethodName`.

    Written out because it is what makes reading one checkable: a reading is only
    accepted when re-mangling it reproduces the symbol.
    """
    slots = selector.split(":")
    # Each slot's `:` becomes `_`; `split` leaves a trailing empty piece for the last one.
    body = "".join(f"{slot}_" for slot in slots[:-1]) if slots[-1] == "" else selector
    return f"{'_c_' if is_class_method else '_i_'}{class_name}_{category}_{body}"

parse_objc_symbol

parse_objc_symbol(name)

Parse name, returning an ObjcSymbol, or raise DemangleFailure.

A Mach-O symbol table carries decoration the assembler added and the compiler did not write: one leading underscore on every symbol, and l or L before that on a private label. Each is stripped in turn and the stripped form is tried first.

Trying it first rather than second is what gets ___cfunc_block_invoke right. Both readings parse: __ is the block prefix, so the outer name is either _cfunc with no Mach-O underscore or cfunc with one. The second is the answer on Darwin, where these symbols come from, and on ELF the stripped form does not begin with __ at all and so does not parse -- which leaves the unstripped reading, correctly. A C function genuinely called _cfunc in an ELF object is the one case the two are indistinguishable, and it reads as cfunc.

Source code in src/demangle/schemes/objc/_parser.py
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
def parse_objc_symbol(name):
    """Parse `name`, returning an `ObjcSymbol`, or raise `DemangleFailure`.

    A Mach-O symbol table carries decoration the assembler added and the compiler did
    not write: one leading underscore on every symbol, and `l` or `L` before that on a
    private label. Each is stripped in turn and the stripped form is tried *first*.

    Trying it first rather than second is what gets `___cfunc_block_invoke` right. Both
    readings parse: `__` is the block prefix, so the outer name is either `_cfunc` with
    no Mach-O underscore or `cfunc` with one. The second is the answer on Darwin, where
    these symbols come from, and on ELF the stripped form does not begin with `__` at
    all and so does not parse -- which leaves the unstripped reading, correctly. A C
    function genuinely called `_cfunc` in an ELF object is the one case the two are
    indistinguishable, and it reads as `cfunc`.
    """
    if not name:
        raise DemangleFailure("empty name")
    if name.startswith(_CXX_BLOCK):
        # A block in a C++ function: the C++ scheme has the production and spells it
        # (`invocation function for block in foo()`); this one would not.
        raise DemangleFailure("a C++ block invocation function, not an Objective-C one")
    for candidate in _candidates(name):
        for reader in _READERS:
            try:
                found = reader(candidate)
            except DemangleFailure:
                continue
            if found is not None:
                found.raw = name
                return found
    raise DemangleFailure("not an Objective-C symbol")

parse

parse(
    mangled, builder, limits=DEFAULT_LIMITS, options=None
)

Parse an Objective-C symbol into builder.

Source code in src/demangle/schemes/objc/__init__.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def parse(mangled, builder, limits=DEFAULT_LIMITS, options=None):
    """Parse an Objective-C symbol into `builder`."""
    if not mangled:
        raise NotMangledError(mangled, "empty name")
    if len(mangled) > limits.max_input:
        raise LimitExceeded(mangled, "input length", limits.max_input)

    try:
        symbol = parse_objc_symbol(mangled)
    except DemangleFailure as error:
        raise NotMangledError(mangled, str(error)) from error

    if len(symbol.text) > limits.max_output:
        raise LimitExceeded(mangled, "output length", limits.max_output)

    if _wants_structure(builder):
        return nodes.build(symbol)
    return builder.raw(symbol.text)

Parser

Objective-C symbol names, as the compiler writes them.

Objective-C is barely mangled, and what mangling there is comes from clang rather than from a language specification. So the rules here are transcribed from clang's own sources -- lib/AST/Mangle.cpp for method and block names, lib/CodeGen/CGObjCMac.cpp for the Apple runtime's data symbols and lib/CodeGen/CGObjCGNU.cpp for the GNUstep runtime's -- and correctness rests on re-assembly, as it does for Go, Nim and Free Pascal: the parts this splits out, rejoined with the compiler's own separators, must reproduce the symbol exactly.

Three runtimes emit three different sets of names and this reads all three:

-[NSString stringWithFormat:]              Apple, and the readable form everywhere
_i_NSString__stringWithFormat_             GNUstep and the GCC runtime
_OBJC_CLASS_$_NSString                     Apple, non-fragile ABI
_OBJC_CLASS_NSString                       Apple fragile ABI, and GNUstep
.objc_class_name_NSString                  Apple fragile ABI

The GNU-family method mangling is not injective, and clang says so where it writes it: "This is the mangling we've always used on the GNU runtimes, but it has obvious collisions in the face of underscores within class names, category names, and selectors." _i_A_B_c is -[A(B) c] if A is the class, and -[A_B c]... is impossible to tell apart from it. This reads the leftmost split that re-mangles, and Method.ambiguous says when another reading exists.

decode_type_encoding

decode_type_encoding(text)

An Objective-C type encoding, with the GNU runtime's substitutions undone.

Source code in src/demangle/schemes/objc/_parser.py
500
501
502
503
504
def decode_type_encoding(text):
    """An Objective-C type encoding, with the GNU runtime's substitutions undone."""
    for written, meant in _ENCODING_SUBSTITUTIONS.items():
        text = text.replace(written, meant)
    return text

Nodes

demangle.schemes.objc.nodes

The Objective-C symbol tree.

The same parts-in-order shape as the other schemes: a node is the fragments it was built from, so rendering concatenates and the tree cannot spell a symbol differently from the text path.

What a caller wants from one of these is the class, the category and the selector, which is exactly what splitting -[NSString(Extra) stringWithFormat:] back out of a string gets wrong -- a category name can hold a bracket in no runtime, but a selector holds colons and spaces, and a block's parent is a whole method with brackets of its own.

ClassName

ClassName(parts)

Bases: _Objc

The class, or the protocol, the symbol belongs to.

Source code in src/demangle/schemes/objc/nodes.py
21
22
23
24
25
26
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Category

Category(parts)

Bases: _Objc

The category, when the symbol names one.

Source code in src/demangle/schemes/objc/nodes.py
21
22
23
24
25
26
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Selector

Selector(parts)

Bases: _Objc

The selector, colons and all.

Source code in src/demangle/schemes/objc/nodes.py
21
22
23
24
25
26
def __init__(self, parts):
    self.parts = tuple(parts)
    total = 0
    for part in self.parts:
        total += len(part) if isinstance(part, str) else part.size
    self.size = total

Symbol

Symbol(parts, objc_kind, runtime='', ambiguous=False)

Bases: _Objc

A whole Objective-C symbol.

objc_kind is what the mangling said this is -- instance method, class method, class, category, ivar, selector, block, reference, encoding or label -- which the spelling alone does not always say.

runtime is which family wrote it: apple, gnu, or empty where the form is shared. ambiguous is set when the mangling admits more than one reading and this is the preferred one rather than the only one.

Source code in src/demangle/schemes/objc/nodes.py
82
83
84
85
86
def __init__(self, parts, objc_kind, runtime="", ambiguous=False):
    super().__init__(parts)
    self.objc_kind = objc_kind
    self.runtime = runtime
    self.ambiguous = ambiguous

build

build(symbol)

Assemble the tree for a parsed ObjcSymbol.

The pieces are placed by searching the spelling for them rather than by rebuilding it, so the tree renders to the same string the text path produces however the spelling is worded. A piece that is not in the spelling -- a block's class, say, which is inside the parent it names -- simply is not a node.

Source code in src/demangle/schemes/objc/nodes.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def build(symbol):
    """Assemble the tree for a parsed `ObjcSymbol`.

    The pieces are placed by *searching the spelling for them* rather than by rebuilding
    it, so the tree renders to the same string the text path produces however the
    spelling is worded. A piece that is not in the spelling -- a block's class, say,
    which is inside the parent it names -- simply is not a node.
    """
    text = symbol.text
    pieces = []
    for value, factory in (
        (symbol.class_name, ClassName),
        (symbol.category, Category),
        (symbol.selector, Selector),
    ):
        if value:
            pieces.append((value, factory))

    # Searched for after the label, which can hold the name: `Objective-C selector b`.
    at = len(symbol.label) if pieces else 0
    parts = [text[:at]] if at else []
    for value, factory in pieces:
        found = text.find(value, at)
        if found < 0:
            continue
        if found > at:
            parts.append(text[at:found])
        parts.append(factory([value]))
        at = found + len(value)
    if at < len(text):
        parts.append(text[at:])
    if not parts:
        parts = [text]
    return Symbol(parts, symbol.kind, runtime=symbol.runtime, ambiguous=symbol.ambiguous)