demangle 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- demangle/__init__.py +103 -0
- demangle/_signature.py +623 -0
- demangle/api.py +749 -0
- demangle/cli.py +521 -0
- demangle/core/__init__.py +20 -0
- demangle/core/ast.py +899 -0
- demangle/core/builder.py +160 -0
- demangle/core/cache.py +70 -0
- demangle/core/decorations.py +73 -0
- demangle/core/errors.py +75 -0
- demangle/core/limits.py +50 -0
- demangle/core/plugin.py +98 -0
- demangle/core/reader.py +247 -0
- demangle/core/registry.py +221 -0
- demangle/core/spelling.py +470 -0
- demangle/core/style.py +192 -0
- demangle/filter.py +226 -0
- demangle/py.typed +0 -0
- demangle/schemes/__init__.py +7 -0
- demangle/schemes/ada/__init__.py +175 -0
- demangle/schemes/ada/_parser.py +313 -0
- demangle/schemes/ada/nodes.py +89 -0
- demangle/schemes/codewarrior/__init__.py +215 -0
- demangle/schemes/codewarrior/_parser.py +583 -0
- demangle/schemes/codewarrior/nodes.py +131 -0
- demangle/schemes/codewarrior/options.py +33 -0
- demangle/schemes/d/__init__.py +118 -0
- demangle/schemes/d/_parser.py +1616 -0
- demangle/schemes/d/nodes.py +93 -0
- demangle/schemes/delphi/__init__.py +79 -0
- demangle/schemes/delphi/_parser.py +897 -0
- demangle/schemes/delphi/nodes.py +128 -0
- demangle/schemes/gnuv2/__init__.py +325 -0
- demangle/schemes/gnuv2/_parser.py +2487 -0
- demangle/schemes/gnuv2/nodes.py +145 -0
- demangle/schemes/gnuv2/options.py +54 -0
- demangle/schemes/go/__init__.py +127 -0
- demangle/schemes/go/_parser.py +278 -0
- demangle/schemes/go/nodes.py +138 -0
- demangle/schemes/itanium/__init__.py +61 -0
- demangle/schemes/itanium/options.py +332 -0
- demangle/schemes/itanium/parser.py +5456 -0
- demangle/schemes/itanium/substitutions.py +362 -0
- demangle/schemes/itanium/tables.py +376 -0
- demangle/schemes/jni/__init__.py +96 -0
- demangle/schemes/jni/_parser.py +226 -0
- demangle/schemes/jni/nodes.py +91 -0
- demangle/schemes/msvc/__init__.py +291 -0
- demangle/schemes/msvc/_parser.py +2024 -0
- demangle/schemes/msvc/nodes.py +450 -0
- demangle/schemes/msvc/options.py +140 -0
- demangle/schemes/nim/__init__.py +81 -0
- demangle/schemes/nim/_parser.py +470 -0
- demangle/schemes/nim/nodes.py +81 -0
- demangle/schemes/objc/__init__.py +119 -0
- demangle/schemes/objc/_parser.py +823 -0
- demangle/schemes/objc/nodes.py +122 -0
- demangle/schemes/pascal/__init__.py +85 -0
- demangle/schemes/pascal/_parser.py +428 -0
- demangle/schemes/pascal/nodes.py +131 -0
- demangle/schemes/rust/__init__.py +244 -0
- demangle/schemes/rust/_dispatch.py +85 -0
- demangle/schemes/rust/_legacy.py +266 -0
- demangle/schemes/rust/_v0.py +1867 -0
- demangle/schemes/rust/nodes.py +194 -0
- demangle/schemes/rust/options.py +36 -0
- demangle/schemes/swift/__init__.py +247 -0
- demangle/schemes/swift/_demangler.py +3224 -0
- demangle/schemes/swift/_node.py +644 -0
- demangle/schemes/swift/_old_demangler.py +1525 -0
- demangle/schemes/swift/_printer.py +2737 -0
- demangle/schemes/swift/_punycode.py +116 -0
- demangle/schemes/swift/nodes.py +143 -0
- demangle/schemes/swift/options.py +155 -0
- demangle/schemes/swift/resolve.py +577 -0
- demangle/schemes/swift/symbolic.py +201 -0
- demangle-0.3.0.dist-info/METADATA +382 -0
- demangle-0.3.0.dist-info/RECORD +82 -0
- demangle-0.3.0.dist-info/WHEEL +4 -0
- demangle-0.3.0.dist-info/entry_points.txt +2 -0
- demangle-0.3.0.dist-info/licenses/LICENSE +21 -0
- demangle-0.3.0.dist-info/licenses/NOTICE +178 -0
demangle/api.py
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
"""The public API.
|
|
2
|
+
|
|
3
|
+
Deliberately small. A handful of functions cover what callers actually do, and every
|
|
4
|
+
one of them is stable ground that later versions can build on without breaking:
|
|
5
|
+
|
|
6
|
+
demangle(name) the readable spelling, or the name unchanged -- never raises
|
|
7
|
+
demangle_strict(name) the readable spelling -- raises
|
|
8
|
+
parse(name) a tree to inspect -- raises
|
|
9
|
+
detect(name) which scheme, if any
|
|
10
|
+
demangle_all(names) the batch form, cached
|
|
11
|
+
|
|
12
|
+
demangle_type(enc, language=...) a bare type encoding, spelled -- raises
|
|
13
|
+
parse_type(enc, language=...) a bare type encoding, as a tree -- raises
|
|
14
|
+
|
|
15
|
+
Each of those has a `...b` form taking and returning bytes, because a symbol table holds
|
|
16
|
+
bytes rather than text.
|
|
17
|
+
|
|
18
|
+
The split between `demangle` and `demangle_strict` is the important one. A tool
|
|
19
|
+
labelling every symbol in a binary meets far more non-mangled names than mangled ones,
|
|
20
|
+
and treating that as an error would mean an exception per symbol; it wants the name
|
|
21
|
+
back. A tool demangling one name a user typed wants to be told what was wrong with it.
|
|
22
|
+
Serving both from one function -- with a sentinel, or a flag -- makes the common case
|
|
23
|
+
carry the uncommon one's error handling, so they are two functions.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from collections.abc import Iterable, Iterator
|
|
27
|
+
from typing import Any, NoReturn
|
|
28
|
+
|
|
29
|
+
from .core import registry as _registry
|
|
30
|
+
from .core import style as _style_module
|
|
31
|
+
from .core.ast import Node, builder_for
|
|
32
|
+
from .core.cache import MISSING, BoundedCache
|
|
33
|
+
from .core.decorations import VERSION_SEPARATOR, split_decorations
|
|
34
|
+
from .core.errors import (
|
|
35
|
+
DemanglingError,
|
|
36
|
+
LimitExceeded,
|
|
37
|
+
NotMangledError,
|
|
38
|
+
ParseError,
|
|
39
|
+
reraise_if_operational,
|
|
40
|
+
)
|
|
41
|
+
from .core.limits import DEFAULT_LIMITS, Limits
|
|
42
|
+
from .core.registry import candidates, get, names
|
|
43
|
+
from .core.style import DEFAULT_STYLE, Style, available_styles, get_style
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"cache_clear",
|
|
47
|
+
"cache_stats",
|
|
48
|
+
"demangle",
|
|
49
|
+
"demangle_all",
|
|
50
|
+
"demangle_strict",
|
|
51
|
+
"demangle_type",
|
|
52
|
+
"demangleb",
|
|
53
|
+
"demangleb_strict",
|
|
54
|
+
"demangleb_type",
|
|
55
|
+
"detect",
|
|
56
|
+
"detectb",
|
|
57
|
+
"languages",
|
|
58
|
+
"node_kinds",
|
|
59
|
+
"parse",
|
|
60
|
+
"parse_type",
|
|
61
|
+
"parseb",
|
|
62
|
+
"parseb_type",
|
|
63
|
+
"style",
|
|
64
|
+
"styles",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
#: Symbol tables repeat names relentlessly -- one binary can name `std::allocator<char>`
|
|
68
|
+
#: thousands of times -- so memoisation is worth more here than any micro-optimisation.
|
|
69
|
+
#:
|
|
70
|
+
#: Keyed by everything that changes the answer, `limits` included. Leaving the limits
|
|
71
|
+
#: out is not merely a missed bound: one caller passing tight limits would poison the
|
|
72
|
+
#: entry for every other caller of that name in the process, and `demangle()` cannot
|
|
73
|
+
#: report it because it never raises. `Limits` is a frozen slots dataclass, so it
|
|
74
|
+
#: hashes by value and two equal limit sets share a cache entry.
|
|
75
|
+
_CACHE = BoundedCache(max_size=16384)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
#: Emptied whenever a style or a language is registered. Replacing `llvm`, or replacing
|
|
79
|
+
#: a whole scheme, used to leave every name demangled beforehand still answering from
|
|
80
|
+
#: cache with the older spelling.
|
|
81
|
+
#:
|
|
82
|
+
#: Done by notification rather than by folding a generation counter into the key,
|
|
83
|
+
#: because the key is built once per `demangle()` call and that is the hottest path in
|
|
84
|
+
#: the package. Asking two modules "have you changed" there measured 58% slower on the
|
|
85
|
+
#: warm path than clearing a cache on the rare occasion one has.
|
|
86
|
+
_style_module.notify_on_change(_CACHE.clear)
|
|
87
|
+
_registry.notify_on_change(_CACHE.clear)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _refuse_non_string(mangled):
|
|
91
|
+
"""Report a non-`str` argument as the caller's mistake it is.
|
|
92
|
+
|
|
93
|
+
Bytes used to be handed straight back, unchanged and unremarked, so a tool reading
|
|
94
|
+
an ELF string table -- where names *are* bytes -- saw every symbol come back exactly
|
|
95
|
+
as it went in and concluded the library did not work. The strict entry points were
|
|
96
|
+
worse: they reached the registry's first-character screen and raised
|
|
97
|
+
`TypeError: 'in <string>' requires string as left operand, not int`, which names
|
|
98
|
+
neither the problem nor the fix, and which the documented "raises only
|
|
99
|
+
`DemanglingError`" contract said could not happen.
|
|
100
|
+
"""
|
|
101
|
+
if isinstance(mangled, _BYTES_LIKE):
|
|
102
|
+
raise TypeError(f"expected str, got {type(mangled).__name__}; symbol tables hold bytes, so use demangleb()")
|
|
103
|
+
raise TypeError(f"expected str, got {type(mangled).__name__}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _resolve(language):
|
|
107
|
+
"""Pick the plugin for `language`, or None to mean "detect"."""
|
|
108
|
+
if language is None:
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
return get(language)
|
|
112
|
+
except (KeyError, TypeError):
|
|
113
|
+
raise ValueError(f"unknown language {language!r}; known languages are {names()}") from None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _parse_with(plugin, mangled, builder, limits, style):
|
|
117
|
+
"""Run one plugin, handling any symbol-table decoration around the name.
|
|
118
|
+
|
|
119
|
+
Splitting here rather than in each parser keeps the grammars free of ELF and
|
|
120
|
+
toolchain conventions, and means a new scheme inherits the behaviour by setting one
|
|
121
|
+
flag instead of reimplementing it.
|
|
122
|
+
"""
|
|
123
|
+
options = style.options_for(plugin.name)
|
|
124
|
+
decoration = ""
|
|
125
|
+
if plugin.symbol_table_decorations:
|
|
126
|
+
mangled, decoration = split_decorations(mangled)
|
|
127
|
+
|
|
128
|
+
if options is None:
|
|
129
|
+
handle = plugin.parse(mangled, builder, limits)
|
|
130
|
+
else:
|
|
131
|
+
handle = plugin.parse(mangled, builder, limits, options)
|
|
132
|
+
|
|
133
|
+
return builder.decorated(handle, decoration) if decoration else handle
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _refuse_unhashable(language, limits) -> NoReturn:
|
|
137
|
+
"""Name the argument that could not go into the cache key, as a `ValueError`.
|
|
138
|
+
|
|
139
|
+
Only reached once a lookup has raised `TypeError`, so one of the two is unhashable;
|
|
140
|
+
the name is a `str` and a style's name is one too.
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
hash(language)
|
|
144
|
+
except TypeError:
|
|
145
|
+
raise ValueError(f"unknown language {language!r}; known languages are {names()}") from None
|
|
146
|
+
try:
|
|
147
|
+
hash(limits)
|
|
148
|
+
except TypeError:
|
|
149
|
+
raise ValueError(f"unhashable limits {limits!r}; pass a Limits instance") from None
|
|
150
|
+
raise TypeError("arguments to demangle() must be hashable")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def demangle(
|
|
154
|
+
mangled: str,
|
|
155
|
+
*,
|
|
156
|
+
language: str | None = None,
|
|
157
|
+
style: str | Style | None = DEFAULT_STYLE,
|
|
158
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
159
|
+
) -> str:
|
|
160
|
+
"""Return the readable spelling of `mangled`, or `mangled` unchanged.
|
|
161
|
+
|
|
162
|
+
Never raises for any *string*: a name this library cannot read comes back exactly as
|
|
163
|
+
it went in, because a wrong expansion is worse than a mangled name -- it matches
|
|
164
|
+
neither the original symbol nor the real declaration, so it corrupts every
|
|
165
|
+
downstream lookup that trusted it. That promise is about the name, not about the
|
|
166
|
+
argument's type: passing something that is not a `str` is a mistake in the calling
|
|
167
|
+
code and is reported as one.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
mangled: the symbol name. Any string; need not be mangled.
|
|
171
|
+
language: force a scheme by name, or None to detect.
|
|
172
|
+
style: output spelling policy -- `"llvm"` (default) or `"gnu"`.
|
|
173
|
+
limits: resource bounds for the parse.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The demangled name, or `mangled` unchanged.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
TypeError: `mangled` is not a `str`. Use `demangleb()` for bytes.
|
|
180
|
+
"""
|
|
181
|
+
if not isinstance(mangled, str):
|
|
182
|
+
_refuse_non_string(mangled)
|
|
183
|
+
if not mangled:
|
|
184
|
+
return mangled
|
|
185
|
+
resolved_style = get_style(style)
|
|
186
|
+
# A caller may hand in a `Style` object rather than a name, and two different objects
|
|
187
|
+
# can carry the same name -- so keying on the name alone served one of them the
|
|
188
|
+
# other's spelling. A style holds a builder and a mapping of per-language options,
|
|
189
|
+
# neither of which hashes by value, so it cannot go into the key itself; a call that
|
|
190
|
+
# passes one is simply not cached. That is the rare path. The common one is a name,
|
|
191
|
+
# and it stays a four-element tuple. The test is for the name -- `None` or a `str` --
|
|
192
|
+
# rather than for a `Style`, so a subclass of one carrying a different builder under
|
|
193
|
+
# the same name takes the rare path too, instead of being served whatever was cached
|
|
194
|
+
# under that name first. `__class__ is str` rather than `isinstance`, and the
|
|
195
|
+
# registry's flag rather than its `_load()`, because this is the hottest line in the
|
|
196
|
+
# package: the two calls together cost the warm path a sixth of its time.
|
|
197
|
+
#
|
|
198
|
+
# Warmed before the cache is touched: the first `candidates()`/`get()` loads the
|
|
199
|
+
# registry, and loading registers plugins, which clears the cache -- wiping the miss
|
|
200
|
+
# just recorded if the `get` runs first.
|
|
201
|
+
if not _registry._loaded:
|
|
202
|
+
_registry._load()
|
|
203
|
+
if style is not None and style.__class__ is not str:
|
|
204
|
+
key = None
|
|
205
|
+
else:
|
|
206
|
+
key = (mangled, language, resolved_style.name, limits)
|
|
207
|
+
# The lookup hashes the key once. An argument that cannot be hashed -- a list
|
|
208
|
+
# for `limits`, say -- surfaces there as a `TypeError`, and is reported as the
|
|
209
|
+
# bad argument it is. Reported from the failure rather than checked for in
|
|
210
|
+
# advance, because `Limits` is a frozen dataclass whose hash is computed from
|
|
211
|
+
# its fields every time: checking it first would hash it twice on every warm
|
|
212
|
+
# call, which is the call the cache exists to make cheap.
|
|
213
|
+
try:
|
|
214
|
+
cached = _CACHE.get(key)
|
|
215
|
+
except TypeError:
|
|
216
|
+
_refuse_unhashable(language, limits)
|
|
217
|
+
if cached is not MISSING:
|
|
218
|
+
return cached
|
|
219
|
+
|
|
220
|
+
builder = resolved_style.spelling_builder
|
|
221
|
+
plugin = None if language is None else _resolve(language)
|
|
222
|
+
if plugin is not None:
|
|
223
|
+
tried, base = (plugin,), None
|
|
224
|
+
else:
|
|
225
|
+
tried, base = candidates(mangled), _undecorated(mangled)
|
|
226
|
+
|
|
227
|
+
for candidate in tried:
|
|
228
|
+
try:
|
|
229
|
+
# `_claims` written out. It is the same two tests in the same order, and its
|
|
230
|
+
# `try` is redundant *here*: a `detect` that throws is caught by this loop's
|
|
231
|
+
# own handler, which calls the same `reraise_if_operational` and then tries
|
|
232
|
+
# the next scheme, exactly as returning False would have. What the call cost
|
|
233
|
+
# was an interpreter frame for every candidate a name is offered to -- around
|
|
234
|
+
# seven per name on the benchmark corpus, six of them on a symbol that is not
|
|
235
|
+
# mangled at all, which is most of a real symbol table. `detect()` keeps the
|
|
236
|
+
# helper: it is not the hot path and one implementation of the rule should be
|
|
237
|
+
# readable somewhere.
|
|
238
|
+
if (
|
|
239
|
+
plugin is None
|
|
240
|
+
and not candidate.detect(mangled)
|
|
241
|
+
and (base is None or not candidate.symbol_table_decorations or not candidate.detect(base))
|
|
242
|
+
):
|
|
243
|
+
continue
|
|
244
|
+
handle = _parse_with(candidate, mangled, builder, limits, resolved_style)
|
|
245
|
+
except LimitExceeded:
|
|
246
|
+
# A limit is not a "this name is not mine". The scheme claimed it and then
|
|
247
|
+
# ran out of the budget the caller set, which says the name is expensive --
|
|
248
|
+
# not that some other scheme should be handed the same text. Offering it on
|
|
249
|
+
# is how `_ZN11Expressions2f2ILi1EEEvPApsT__i`, an Itanium name that spends
|
|
250
|
+
# more substitutions than a tightened budget allows, came back as
|
|
251
|
+
# `_ZN11Expressions2f2ILi1EEEvPApsT(int)`: the pre-Itanium scheme reading the
|
|
252
|
+
# mangling itself as an identifier and the trailing `i` as a parameter. A
|
|
253
|
+
# declaration that names nothing is the one answer this library treats as
|
|
254
|
+
# worse than no answer, and a caller who *lowers* a limit is defending
|
|
255
|
+
# against hostile input, which is the last place to start guessing.
|
|
256
|
+
break
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
reraise_if_operational(exc)
|
|
259
|
+
# Try the next scheme. Detection is a cheap prefix test and is allowed to be
|
|
260
|
+
# wrong, and a third-party plugin is allowed to be buggy -- the registry
|
|
261
|
+
# already takes care not to let a broken plugin bring the library down, and
|
|
262
|
+
# keeping the `detect` call inside this `try` is what stops it doing so here.
|
|
263
|
+
continue
|
|
264
|
+
# Recorded here rather than through a helper: `demangle` is the hot entry
|
|
265
|
+
# point, and this is a conditional and a call either way.
|
|
266
|
+
result = builder.spell(handle)
|
|
267
|
+
return result if key is None else _CACHE.put(key, result)
|
|
268
|
+
|
|
269
|
+
return mangled if key is None else _CACHE.put(key, mangled)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def demangle_strict(
|
|
273
|
+
mangled: str,
|
|
274
|
+
*,
|
|
275
|
+
language: str | None = None,
|
|
276
|
+
style: str | Style | None = DEFAULT_STYLE,
|
|
277
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
278
|
+
) -> str:
|
|
279
|
+
"""Return the readable spelling of `mangled`, raising when it cannot be read.
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
NotMangledError: the name matches no known scheme.
|
|
283
|
+
ParseError: the name has a known prefix but does not follow the grammar.
|
|
284
|
+
LimitExceeded: a resource bound was hit.
|
|
285
|
+
"""
|
|
286
|
+
resolved_style = get_style(style)
|
|
287
|
+
builder = resolved_style.spelling_builder
|
|
288
|
+
return builder.spell(_parse_handle(mangled, builder, language, resolved_style, limits))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def parse(
|
|
292
|
+
mangled: str,
|
|
293
|
+
*,
|
|
294
|
+
language: str | None = None,
|
|
295
|
+
style: str | Style | None = DEFAULT_STYLE,
|
|
296
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
297
|
+
) -> Node:
|
|
298
|
+
"""Parse `mangled` into a tree.
|
|
299
|
+
|
|
300
|
+
Use this when the *parts* matter -- the namespace, the template arguments, the
|
|
301
|
+
parameter types -- rather than the spelling. The result is a `core.ast.Node`
|
|
302
|
+
supporting `.walk()`, `.find(kind)` and `.spell()`.
|
|
303
|
+
|
|
304
|
+
Raises the same errors as `demangle_strict`.
|
|
305
|
+
|
|
306
|
+
Note:
|
|
307
|
+
Every scheme returns a tree, but the kinds differ with what each language has to
|
|
308
|
+
say. C++ trees carry declarator shape -- pointers, references, parameter lists --
|
|
309
|
+
because C++ types wrap the name they declare. Rust has no declarator syntax, so
|
|
310
|
+
its trees carry path structure instead: `symbol`, `path`, `impl`, `namespace`.
|
|
311
|
+
`name`, `template` and `literal` mean the same thing in all three.
|
|
312
|
+
"""
|
|
313
|
+
resolved = get_style(style)
|
|
314
|
+
return _parse_handle(mangled, builder_for(resolved), language, resolved, limits)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def demangle_type(
|
|
318
|
+
mangled: str,
|
|
319
|
+
*,
|
|
320
|
+
language: str,
|
|
321
|
+
style: str | Style | None = DEFAULT_STYLE,
|
|
322
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
323
|
+
) -> str:
|
|
324
|
+
"""Return the readable spelling of a bare *type* encoding.
|
|
325
|
+
|
|
326
|
+
`Pi` is `int*`, `PEAX` is `void *`, `SaySiG` is `[Swift.Int]`. This is what a
|
|
327
|
+
`typeinfo` name, an RTTI type descriptor and a Swift metadata typeref carry: a type
|
|
328
|
+
on its own, with none of the surrounding symbol a name has.
|
|
329
|
+
|
|
330
|
+
`language` is required, and cannot be made optional. A whole symbol says which
|
|
331
|
+
scheme it belongs to -- `_Z`, `?`, `$s` -- but a type encoding says nothing at all:
|
|
332
|
+
`i` is a valid Itanium type, a valid Swift type, and a valid C identifier, so there
|
|
333
|
+
is no evidence to detect on. Guessing would mean reading plain C symbols as types,
|
|
334
|
+
which is why every reference puts this behind a flag of its own -- `c++filt -t`,
|
|
335
|
+
libiberty's `DMGL_TYPES`, `UnDecorateSymbolName`'s `UNDNAME_TYPE_ONLY`.
|
|
336
|
+
|
|
337
|
+
Raises rather than passing the name through, unlike `demangle()`. A caller who named
|
|
338
|
+
the scheme is asking a question about one encoding they already believe is one, not
|
|
339
|
+
labelling a table of symbols, and wants to be told when it is not.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
mangled: the type encoding.
|
|
343
|
+
language: which scheme to read it as -- required. See `languages()`.
|
|
344
|
+
style: output spelling policy -- `"llvm"` (default) or `"gnu"`.
|
|
345
|
+
limits: resource bounds for the parse.
|
|
346
|
+
|
|
347
|
+
Raises:
|
|
348
|
+
ValueError: `language` is unknown, or names a scheme with no type grammar.
|
|
349
|
+
NotMangledError: the encoding is empty.
|
|
350
|
+
ParseError: the encoding does not follow the scheme's type grammar.
|
|
351
|
+
LimitExceeded: a resource bound was hit.
|
|
352
|
+
"""
|
|
353
|
+
resolved = get_style(style)
|
|
354
|
+
builder = resolved.spelling_builder
|
|
355
|
+
return builder.spell(_parse_type_handle(mangled, builder, language, resolved, limits))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def parse_type(
|
|
359
|
+
mangled: str,
|
|
360
|
+
*,
|
|
361
|
+
language: str,
|
|
362
|
+
style: str | Style | None = DEFAULT_STYLE,
|
|
363
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
364
|
+
) -> Node:
|
|
365
|
+
"""Parse a bare *type* encoding into a tree.
|
|
366
|
+
|
|
367
|
+
`demangle_type()` is to `demangle_strict()` as this is to `parse()`: same input,
|
|
368
|
+
same required `language`, same errors -- a `core.ast.Node` instead of a string.
|
|
369
|
+
"""
|
|
370
|
+
resolved = get_style(style)
|
|
371
|
+
return _parse_type_handle(mangled, builder_for(resolved), language, resolved, limits)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _parse_type_handle(mangled, builder, language, style, limits):
|
|
375
|
+
if not isinstance(mangled, str):
|
|
376
|
+
_refuse_non_string(mangled)
|
|
377
|
+
plugin = _resolve(language)
|
|
378
|
+
if plugin is None:
|
|
379
|
+
raise ValueError("demangle_type needs a language; a type encoding carries no marker to detect on")
|
|
380
|
+
if plugin.parse_type is None:
|
|
381
|
+
raise ValueError(f"{plugin.name} has no type grammar of its own; {_type_languages()} do")
|
|
382
|
+
options = style.options_for(plugin.name)
|
|
383
|
+
try:
|
|
384
|
+
if options is None:
|
|
385
|
+
return plugin.parse_type(mangled, builder, limits)
|
|
386
|
+
return plugin.parse_type(mangled, builder, limits, options)
|
|
387
|
+
except RecursionError as exc:
|
|
388
|
+
raise _depth_exceeded(mangled, limits) from exc
|
|
389
|
+
except DemanglingError:
|
|
390
|
+
raise
|
|
391
|
+
except Exception as exc:
|
|
392
|
+
reraise_if_operational(exc)
|
|
393
|
+
# Same contract as `_parse_handle`: these entry points raise `DemanglingError`
|
|
394
|
+
# and nothing else, so a defect in a plugin is wrapped rather than let out as an
|
|
395
|
+
# `AttributeError` no caller can reasonably catch. The original is chained.
|
|
396
|
+
raise ParseError(mangled, None, f"{plugin.name} type parser failed: {exc!r}") from exc
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def style(base: str | Style | None = DEFAULT_STYLE, /, **languages: Any) -> Style:
|
|
400
|
+
"""Compose a style for one call: a named one, with some per-language changes.
|
|
401
|
+
|
|
402
|
+
The two named styles say how to spell a name; what a caller usually wants to vary is
|
|
403
|
+
how *much* of it to spell. Every peer tool composes that at the call site --
|
|
404
|
+
`llvm-undname --no-calling-convention`, `UnDecorateSymbolName`'s mask -- and this is
|
|
405
|
+
the same thing without a global registration:
|
|
406
|
+
|
|
407
|
+
>>> import demangle
|
|
408
|
+
>>> narrow = demangle.style("llvm", msvc={"calling_convention": False})
|
|
409
|
+
>>> demangle.demangle("?f@@YAXH@Z", style=narrow)
|
|
410
|
+
'void f(int)'
|
|
411
|
+
>>> demangle.demangle("?f@@YAXH@Z")
|
|
412
|
+
'void __cdecl f(int)'
|
|
413
|
+
|
|
414
|
+
Each keyword is a language name; each value is that language's options object or a
|
|
415
|
+
mapping of the fields to change. `languages()` lists the names, and each scheme's
|
|
416
|
+
`options` module documents what it accepts.
|
|
417
|
+
|
|
418
|
+
The result is an object rather than a registered name, and that is what keeps it to
|
|
419
|
+
one call: `demangle()` does not cache a call that passes a style object, so a
|
|
420
|
+
narrower spelling asked for here is never served to a caller asking for the default.
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
ValueError: `base` is not a known style, or a language named has no options.
|
|
424
|
+
"""
|
|
425
|
+
return get_style(base).with_options(**languages)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def node_kinds(language: str | None = None) -> tuple[str, ...]:
|
|
429
|
+
"""Every `kind` a `parse()` tree can hold, sorted. One scheme's, or all of them.
|
|
430
|
+
|
|
431
|
+
The vocabulary to switch on, published rather than left to be read out of the source:
|
|
432
|
+
|
|
433
|
+
>>> import demangle
|
|
434
|
+
>>> demangle.node_kinds("d")
|
|
435
|
+
('name', 'path', 'symbol')
|
|
436
|
+
|
|
437
|
+
What each kind *means* differs with what the language has to say. A C++ tree carries
|
|
438
|
+
declarator shape -- `pointer`, `array`, `function` -- because a C++ type wraps the
|
|
439
|
+
name it declares. Rust has no declarator syntax, so its trees carry path structure
|
|
440
|
+
instead: `symbol`, `path`, `impl`, `namespace`. `name`, `template` and `literal` mean
|
|
441
|
+
the same thing wherever they appear.
|
|
442
|
+
|
|
443
|
+
Raises:
|
|
444
|
+
ValueError: `language` is not a known scheme.
|
|
445
|
+
"""
|
|
446
|
+
if language is not None:
|
|
447
|
+
return tuple(sorted(_resolve(language).node_kinds))
|
|
448
|
+
from .core.registry import available
|
|
449
|
+
|
|
450
|
+
return tuple(sorted({kind for plugin in available() for kind in plugin.node_kinds}))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _type_languages():
|
|
454
|
+
"""The schemes that read a bare type, for the error that says one does not."""
|
|
455
|
+
return ", ".join(sorted(name for name in names() if get(name).parse_type is not None))
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _parse_handle(mangled, builder, language, style, limits):
|
|
459
|
+
if not isinstance(mangled, str):
|
|
460
|
+
_refuse_non_string(mangled)
|
|
461
|
+
if not mangled:
|
|
462
|
+
raise NotMangledError(mangled, "empty name")
|
|
463
|
+
plugin = _resolve(language)
|
|
464
|
+
if plugin is not None:
|
|
465
|
+
try:
|
|
466
|
+
return _parse_with(plugin, mangled, builder, limits, style)
|
|
467
|
+
except RecursionError as exc:
|
|
468
|
+
raise _depth_exceeded(mangled, limits) from exc
|
|
469
|
+
|
|
470
|
+
first_error = None
|
|
471
|
+
base = _undecorated(mangled)
|
|
472
|
+
for candidate in candidates(mangled):
|
|
473
|
+
try:
|
|
474
|
+
if not _claims(candidate, mangled, base):
|
|
475
|
+
continue
|
|
476
|
+
return _parse_with(candidate, mangled, builder, limits, style)
|
|
477
|
+
except RecursionError as exc:
|
|
478
|
+
if first_error is None:
|
|
479
|
+
first_error = _depth_exceeded(mangled, limits)
|
|
480
|
+
first_error.__cause__ = exc
|
|
481
|
+
except LimitExceeded as exc:
|
|
482
|
+
# Raised, not remembered: see the note in `demangle`. The next candidate
|
|
483
|
+
# would be reading a name this one has already claimed, and the answer it
|
|
484
|
+
# gives is a reading of the mangling rather than of the name.
|
|
485
|
+
raise exc
|
|
486
|
+
except DemanglingError as exc:
|
|
487
|
+
# Keep the first failure: it came from the highest-priority plugin that
|
|
488
|
+
# claimed the name, so it is the most likely to be the useful diagnostic.
|
|
489
|
+
if first_error is None:
|
|
490
|
+
first_error = exc
|
|
491
|
+
except Exception as exc:
|
|
492
|
+
reraise_if_operational(exc)
|
|
493
|
+
# A plugin raised something that is not a demangling failure -- a defect in
|
|
494
|
+
# it, or in this package. The documented contract is that these entry points
|
|
495
|
+
# raise `DemanglingError` and nothing else, so it is wrapped rather than
|
|
496
|
+
# allowed to escape as an `AttributeError` a caller cannot reasonably catch.
|
|
497
|
+
# The original is chained, so the bug is still diagnosable.
|
|
498
|
+
if first_error is None:
|
|
499
|
+
first_error = ParseError(mangled, None, f"{candidate.name} parser failed: {exc!r}")
|
|
500
|
+
first_error.__cause__ = exc
|
|
501
|
+
if first_error is not None:
|
|
502
|
+
raise first_error
|
|
503
|
+
raise NotMangledError(mangled)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _depth_exceeded(mangled, limits):
|
|
507
|
+
"""A `RecursionError` from a parser, reported as the bound it is.
|
|
508
|
+
|
|
509
|
+
Two bounds govern how deep a name may nest, and `max_depth` is only one of them. The
|
|
510
|
+
other is the interpreter's own stack, and it is the *lower* of the two in practice:
|
|
511
|
+
a production costs several Python frames, so at the default recursion limit of 1000
|
|
512
|
+
an Itanium name gives out around 141 levels of nested template, 164 of `decltype`,
|
|
513
|
+
197 of function type and 493 of pointer -- all of them under the default
|
|
514
|
+
`max_depth` of 256, let alone `RELAXED_LIMITS`' 2048.
|
|
515
|
+
|
|
516
|
+
Which of the two binds first therefore depends on the shape of the name and on how
|
|
517
|
+
deep the caller's own stack already was. Both are the same fact -- this name nests
|
|
518
|
+
further than this process will follow -- so both are reported the same way. Before
|
|
519
|
+
this, one arrived as `LimitExceeded` and the other as
|
|
520
|
+
`ParseError: itanium parser failed: RecursionError(...)`, which reads as a defect in
|
|
521
|
+
the parser rather than a bound doing its job, and leaks an implementation detail
|
|
522
|
+
into a message a caller was meant to be able to act on.
|
|
523
|
+
|
|
524
|
+
A caller who needs the deeper limits to be reachable can raise
|
|
525
|
+
`sys.setrecursionlimit()`; on the versions this package supports, a Python-to-Python
|
|
526
|
+
call does not consume the C stack, so that is safer than it once was.
|
|
527
|
+
"""
|
|
528
|
+
return LimitExceeded(mangled, "recursion depth", limits.max_depth)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _undecorated(mangled):
|
|
532
|
+
"""The name with any ELF version suffix taken off, or None if it carries none.
|
|
533
|
+
|
|
534
|
+
Asked once per name rather than once per candidate. The split depends on nothing but
|
|
535
|
+
the name, and a symbol table is mostly names that carry no decoration at all -- so
|
|
536
|
+
the answer for the first plugin that asks is the answer for all of them, and it is
|
|
537
|
+
almost always "there is nothing to take off".
|
|
538
|
+
|
|
539
|
+
`split_decorations` is what defines the split and stays the place it is written; this
|
|
540
|
+
is its answer read directly off the same `find`, because building and unpacking a
|
|
541
|
+
two-tuple through a second interpreter frame is a per-name cost for a question whose
|
|
542
|
+
answer is `None` for nearly every symbol. The separator is imported rather than
|
|
543
|
+
spelled again, so the two cannot drift apart.
|
|
544
|
+
"""
|
|
545
|
+
version = mangled.find(VERSION_SEPARATOR)
|
|
546
|
+
return mangled[:version] if version > 0 else None
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _claims(plugin, mangled, base):
|
|
550
|
+
"""Whether `plugin` recognises `mangled`, ignoring any symbol-table decoration.
|
|
551
|
+
|
|
552
|
+
The raw name is tried first because this runs on every symbol a caller offers, and
|
|
553
|
+
in a real binary most of them are not mangled at all. A decoration is a *suffix*, so
|
|
554
|
+
a prefix test sees straight through it and `base` -- what `_undecorated` made of the
|
|
555
|
+
name, or None -- is only consulted when the cheap test has already failed.
|
|
556
|
+
|
|
557
|
+
Never raises. `detect` belongs to a plugin that may be a third party's, and the
|
|
558
|
+
registry already declines to let a broken one take the library down; this is the
|
|
559
|
+
other half of that, since a `detect` that throws would otherwise escape through
|
|
560
|
+
`demangle()`, which is documented never to raise.
|
|
561
|
+
"""
|
|
562
|
+
try:
|
|
563
|
+
if plugin.detect(mangled):
|
|
564
|
+
return True
|
|
565
|
+
return base is not None and plugin.symbol_table_decorations and plugin.detect(base)
|
|
566
|
+
except Exception as exc:
|
|
567
|
+
reraise_if_operational(exc)
|
|
568
|
+
return False
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def detect(mangled: str) -> str | None:
|
|
572
|
+
"""Name the scheme `mangled` appears to use, or None.
|
|
573
|
+
|
|
574
|
+
A prefix test only -- it reports what the name looks like, not that it will parse.
|
|
575
|
+
Never raises: like `demangle()`, it is called on every symbol in a table.
|
|
576
|
+
"""
|
|
577
|
+
if not isinstance(mangled, str):
|
|
578
|
+
# `detect` is offered every symbol in a table and answers None for anything it
|
|
579
|
+
# does not recognise, so a wrong type is answered the same way rather than
|
|
580
|
+
# raised: a caller looping over a table wants a verdict, not an exception.
|
|
581
|
+
return None
|
|
582
|
+
if not mangled:
|
|
583
|
+
return None
|
|
584
|
+
base = _undecorated(mangled)
|
|
585
|
+
for plugin in candidates(mangled):
|
|
586
|
+
if _claims(plugin, mangled, base):
|
|
587
|
+
return plugin.name
|
|
588
|
+
return None
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
#: How a symbol table's bytes become a `str` and back again.
|
|
592
|
+
#:
|
|
593
|
+
#: A mangled name is read out of an object file, where it is a run of bytes ending at a
|
|
594
|
+
#: NUL and nothing else -- not text in any declared encoding. Almost all of them are
|
|
595
|
+
#: ASCII, but not all: a raw identifier can carry anything the assembler accepted, and a
|
|
596
|
+
#: truncated symbol table can cut a name mid-character.
|
|
597
|
+
#:
|
|
598
|
+
#: `surrogateescape` is what makes the round trip total. Every byte that is not valid
|
|
599
|
+
#: UTF-8 is parked in a lone surrogate, and encoding back with the same handler restores
|
|
600
|
+
#: exactly the byte that went in. So a name this package cannot read comes back out of
|
|
601
|
+
#: `demangleb` byte for byte, which is the same promise `demangle` makes for a `str`.
|
|
602
|
+
_BYTES_ENCODING = "utf-8"
|
|
603
|
+
_BYTES_ERRORS = "surrogateescape"
|
|
604
|
+
|
|
605
|
+
#: The types `demangleb` accepts. `memoryview` is included because that is what a caller
|
|
606
|
+
#: slicing a mapped object file has in hand, and copying it to ask a question would be a
|
|
607
|
+
#: strange thing to make them do.
|
|
608
|
+
_BYTES_LIKE = (bytes, bytearray, memoryview)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _decode(mangled):
|
|
612
|
+
"""Bytes in, `str` out, or a `TypeError` naming what was actually passed."""
|
|
613
|
+
if isinstance(mangled, _BYTES_LIKE):
|
|
614
|
+
return bytes(mangled).decode(_BYTES_ENCODING, _BYTES_ERRORS)
|
|
615
|
+
raise TypeError(f"expected bytes, got {type(mangled).__name__}")
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def demangleb(
|
|
619
|
+
mangled: bytes,
|
|
620
|
+
*,
|
|
621
|
+
language: str | None = None,
|
|
622
|
+
style: str | Style | None = DEFAULT_STYLE,
|
|
623
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
624
|
+
) -> bytes:
|
|
625
|
+
"""`demangle()` over bytes, returning bytes.
|
|
626
|
+
|
|
627
|
+
The form to use when the names come from a symbol table, which is where they usually
|
|
628
|
+
do: an ELF or Mach-O string table holds bytes, and a tool that has read one should
|
|
629
|
+
not have to guess an encoding to ask what a name says.
|
|
630
|
+
|
|
631
|
+
Like `demangle()`, never raises for a name it cannot read -- it hands the bytes back
|
|
632
|
+
exactly as they arrived, including any that are not valid UTF-8. Raises `TypeError`
|
|
633
|
+
if given something that is not bytes-like, which is a mistake in the calling code
|
|
634
|
+
rather than a property of the symbol.
|
|
635
|
+
|
|
636
|
+
>>> import demangle
|
|
637
|
+
>>> demangle.demangleb(b"_ZN3foo3barEv")
|
|
638
|
+
b'foo::bar()'
|
|
639
|
+
"""
|
|
640
|
+
return demangle(_decode(mangled), language=language, style=style, limits=limits).encode(
|
|
641
|
+
_BYTES_ENCODING, _BYTES_ERRORS
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def demangleb_strict(
|
|
646
|
+
mangled: bytes,
|
|
647
|
+
*,
|
|
648
|
+
language: str | None = None,
|
|
649
|
+
style: str | Style | None = DEFAULT_STYLE,
|
|
650
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
651
|
+
) -> bytes:
|
|
652
|
+
"""`demangle_strict()` over bytes, returning bytes. Raises what it raises."""
|
|
653
|
+
return demangle_strict(_decode(mangled), language=language, style=style, limits=limits).encode(
|
|
654
|
+
_BYTES_ENCODING, _BYTES_ERRORS
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def parseb(
|
|
659
|
+
mangled: bytes,
|
|
660
|
+
*,
|
|
661
|
+
language: str | None = None,
|
|
662
|
+
style: str | Style | None = DEFAULT_STYLE,
|
|
663
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
664
|
+
) -> Node:
|
|
665
|
+
"""`parse()` over bytes. The tree it returns spells `str`, as every tree does."""
|
|
666
|
+
return parse(_decode(mangled), language=language, style=style, limits=limits)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def demangleb_type(
|
|
670
|
+
mangled: bytes,
|
|
671
|
+
*,
|
|
672
|
+
language: str,
|
|
673
|
+
style: str | Style | None = DEFAULT_STYLE,
|
|
674
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
675
|
+
) -> bytes:
|
|
676
|
+
"""`demangle_type()` over bytes, returning bytes.
|
|
677
|
+
|
|
678
|
+
A type encoding is read out of a binary as often as a symbol is -- an Itanium
|
|
679
|
+
`typeinfo` name sits in `.rodata` and an MSVC type descriptor's name in `.rdata` --
|
|
680
|
+
so it gets the same bytes form the rest of the package has. Raises what
|
|
681
|
+
`demangle_type()` raises.
|
|
682
|
+
"""
|
|
683
|
+
return demangle_type(_decode(mangled), language=language, style=style, limits=limits).encode(
|
|
684
|
+
_BYTES_ENCODING, _BYTES_ERRORS
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def parseb_type(
|
|
689
|
+
mangled: bytes,
|
|
690
|
+
*,
|
|
691
|
+
language: str,
|
|
692
|
+
style: str | Style | None = DEFAULT_STYLE,
|
|
693
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
694
|
+
) -> Node:
|
|
695
|
+
"""`parse_type()` over bytes. The tree it returns spells `str`, as every tree does."""
|
|
696
|
+
return parse_type(_decode(mangled), language=language, style=style, limits=limits)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def detectb(mangled: bytes) -> str | None:
|
|
700
|
+
"""`detect()` over bytes. The scheme's name is a `str`; it is this package's own."""
|
|
701
|
+
if not isinstance(mangled, _BYTES_LIKE):
|
|
702
|
+
return None
|
|
703
|
+
return detect(bytes(mangled).decode(_BYTES_ENCODING, _BYTES_ERRORS))
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def demangle_all(
|
|
707
|
+
names_: Iterable[str],
|
|
708
|
+
*,
|
|
709
|
+
language: str | None = None,
|
|
710
|
+
style: str | Style | None = DEFAULT_STYLE,
|
|
711
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
712
|
+
) -> Iterator[str]:
|
|
713
|
+
"""Demangle an iterable of names, yielding results in order.
|
|
714
|
+
|
|
715
|
+
Returns a generator, so a caller streaming a large symbol table never holds more
|
|
716
|
+
than one result at a time beyond what it keeps itself. Shares the module cache,
|
|
717
|
+
which is where the real gain is: symbol tables repeat names heavily.
|
|
718
|
+
|
|
719
|
+
Arguments are validated before the generator is created, so a bad `language` or
|
|
720
|
+
`style` is reported at the call rather than at the first `next()`.
|
|
721
|
+
"""
|
|
722
|
+
_resolve(language)
|
|
723
|
+
get_style(style)
|
|
724
|
+
|
|
725
|
+
def _stream():
|
|
726
|
+
for name in names_:
|
|
727
|
+
yield demangle(name, language=language, style=style, limits=limits)
|
|
728
|
+
|
|
729
|
+
return _stream()
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def languages() -> list[str]:
|
|
733
|
+
"""Registered language names, including any third-party plugins."""
|
|
734
|
+
return names()
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def styles() -> list[str]:
|
|
738
|
+
"""Registered output style names."""
|
|
739
|
+
return available_styles()
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def cache_clear() -> None:
|
|
743
|
+
"""Empty the module-level result cache."""
|
|
744
|
+
_CACHE.clear()
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def cache_stats() -> dict[str, Any]:
|
|
748
|
+
"""Hit rate and occupancy of the result cache."""
|
|
749
|
+
return _CACHE.stats
|