unidecompiler 0.1.0__tar.gz
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.
- unidecompiler-0.1.0/PKG-INFO +16 -0
- unidecompiler-0.1.0/README.md +9 -0
- unidecompiler-0.1.0/pyproject.toml +14 -0
- unidecompiler-0.1.0/setup.cfg +4 -0
- unidecompiler-0.1.0/src/unidecompiler/__init__.py +24 -0
- unidecompiler-0.1.0/src/unidecompiler/analysis.py +324 -0
- unidecompiler-0.1.0/src/unidecompiler/backends/__init__.py +2 -0
- unidecompiler-0.1.0/src/unidecompiler/backends/base.py +22 -0
- unidecompiler-0.1.0/src/unidecompiler/backends/pseudocode.py +906 -0
- unidecompiler-0.1.0/src/unidecompiler/core/__init__.py +2 -0
- unidecompiler-0.1.0/src/unidecompiler/core/ast.py +297 -0
- unidecompiler-0.1.0/src/unidecompiler/core/astify.py +542 -0
- unidecompiler-0.1.0/src/unidecompiler/core/cfg.py +209 -0
- unidecompiler-0.1.0/src/unidecompiler/core/diagnostics.py +73 -0
- unidecompiler-0.1.0/src/unidecompiler/core/effects.py +1328 -0
- unidecompiler-0.1.0/src/unidecompiler/core/function_assembly.py +91 -0
- unidecompiler-0.1.0/src/unidecompiler/core/ir.py +353 -0
- unidecompiler-0.1.0/src/unidecompiler/core/low_level_cfg_structuring.py +5073 -0
- unidecompiler-0.1.0/src/unidecompiler/core/rawcfg.py +163 -0
- unidecompiler-0.1.0/src/unidecompiler/core/reporting.py +56 -0
- unidecompiler-0.1.0/src/unidecompiler/core/ssa.py +692 -0
- unidecompiler-0.1.0/src/unidecompiler/core/stack_machine.py +148 -0
- unidecompiler-0.1.0/src/unidecompiler/core/structuring.py +243 -0
- unidecompiler-0.1.0/src/unidecompiler/core/vm_bytecode.py +46 -0
- unidecompiler-0.1.0/src/unidecompiler/core/vm_effect_table.py +59 -0
- unidecompiler-0.1.0/src/unidecompiler/core/vm_function.py +1671 -0
- unidecompiler-0.1.0/src/unidecompiler/core/vm_hints.py +37 -0
- unidecompiler-0.1.0/src/unidecompiler/core/vm_module.py +21 -0
- unidecompiler-0.1.0/src/unidecompiler/core/vm_operands.py +38 -0
- unidecompiler-0.1.0/src/unidecompiler/core/vm_region.py +2739 -0
- unidecompiler-0.1.0/src/unidecompiler/core/vm_structures.py +123 -0
- unidecompiler-0.1.0/src/unidecompiler/engine.py +254 -0
- unidecompiler-0.1.0/src/unidecompiler/input_sources.py +93 -0
- unidecompiler-0.1.0/src/unidecompiler/plugin_registry.py +158 -0
- unidecompiler-0.1.0/src/unidecompiler/plugins.py +50 -0
- unidecompiler-0.1.0/src/unidecompiler.egg-info/PKG-INFO +16 -0
- unidecompiler-0.1.0/src/unidecompiler.egg-info/SOURCES.txt +37 -0
- unidecompiler-0.1.0/src/unidecompiler.egg-info/dependency_links.txt +1 -0
- unidecompiler-0.1.0/src/unidecompiler.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unidecompiler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small universal bytecode decompiler experiment.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# unidecompiler
|
|
9
|
+
|
|
10
|
+
`unidecompiler` is the frontend-neutral core for universal bytecode
|
|
11
|
+
decompilation. It owns thin-IR lifting, recovery, generic IR, diagnostics, AST
|
|
12
|
+
generation, and the stable `DecompilerEngine` facade used by CLI, GUI, and
|
|
13
|
+
frontend plugin packages.
|
|
14
|
+
|
|
15
|
+
Frontend plugins decode VM-specific formats and submit neutral bytecode facts;
|
|
16
|
+
they do not perform source-structure recovery.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# unidecompiler
|
|
2
|
+
|
|
3
|
+
`unidecompiler` is the frontend-neutral core for universal bytecode
|
|
4
|
+
decompilation. It owns thin-IR lifting, recovery, generic IR, diagnostics, AST
|
|
5
|
+
generation, and the stable `DecompilerEngine` facade used by CLI, GUI, and
|
|
6
|
+
frontend plugin packages.
|
|
7
|
+
|
|
8
|
+
Frontend plugins decode VM-specific formats and submit neutral bytecode facts;
|
|
9
|
+
they do not perform source-structure recovery.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "unidecompiler"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A small universal bytecode decompiler experiment."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
[tool.setuptools.packages.find]
|
|
14
|
+
where = ["src"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""VM-neutral bytecode decompilation library.
|
|
2
|
+
|
|
3
|
+
Use :class:`unidecompiler.plugin_registry.FrontendRegistry` to supply or
|
|
4
|
+
discover frontend plugins. Command-line hosting lives in ``unidecompiler-cli``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from unidecompiler.engine import (
|
|
8
|
+
BytecodeInstruction,
|
|
9
|
+
DecompileResult,
|
|
10
|
+
DecompilerEngine,
|
|
11
|
+
FunctionResult,
|
|
12
|
+
PseudocodeDocument,
|
|
13
|
+
PseudocodeRange,
|
|
14
|
+
)
|
|
15
|
+
from unidecompiler.analysis import BytecodeControlFlowInstruction, BrowseEntry, BrowseIndex, ControlFlowBlock, ControlFlowEdge, FunctionControlFlow, Reference, Symbol, SymbolIndex
|
|
16
|
+
from unidecompiler.plugins import FrontendDecodeError, FrontendModule, FrontendPlugin, FrontendVersionSupport
|
|
17
|
+
from unidecompiler.plugin_registry import FrontendRegistrationError
|
|
18
|
+
|
|
19
|
+
__all__ = (
|
|
20
|
+
"BytecodeInstruction", "DecompileResult", "DecompilerEngine", "FrontendDecodeError",
|
|
21
|
+
"FrontendModule", "FrontendPlugin", "FrontendVersionSupport", "FrontendRegistrationError", "FunctionResult",
|
|
22
|
+
"PseudocodeDocument", "PseudocodeRange",
|
|
23
|
+
"BytecodeControlFlowInstruction", "BrowseEntry", "BrowseIndex", "ControlFlowBlock", "ControlFlowEdge", "FunctionControlFlow", "Reference", "Symbol", "SymbolIndex",
|
|
24
|
+
)
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""VM-neutral, read-only semantic indexes for decompiler hosts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, fields, is_dataclass
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from unidecompiler.core.ast import (
|
|
8
|
+
AstExpr, CallExpr, ConstExpr, FunctionDecl, GetAttrExpr, GlobalRef,
|
|
9
|
+
ModuleDecl, NewObjectExpr, StoreAttrStmt, VarRef,
|
|
10
|
+
)
|
|
11
|
+
from unidecompiler.core.cfg import build_cfg
|
|
12
|
+
from unidecompiler.core.ir import FunctionIR, SourceRef
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SymbolKind = Literal["function", "parameter"]
|
|
16
|
+
ReferenceKind = Literal["call", "global-read", "parameter-read"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Symbol:
|
|
21
|
+
id: str
|
|
22
|
+
name: str
|
|
23
|
+
kind: SymbolKind
|
|
24
|
+
function_id: str
|
|
25
|
+
source: SourceRef | None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Reference:
|
|
30
|
+
id: str
|
|
31
|
+
name: str
|
|
32
|
+
kind: ReferenceKind
|
|
33
|
+
function_id: str
|
|
34
|
+
source: SourceRef | None
|
|
35
|
+
target_ids: tuple[str, ...] = ()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class SymbolIndex:
|
|
40
|
+
symbols: tuple[Symbol, ...] = ()
|
|
41
|
+
references: tuple[Reference, ...] = ()
|
|
42
|
+
|
|
43
|
+
def definitions(self, name: str) -> tuple[Symbol, ...]:
|
|
44
|
+
return tuple(symbol for symbol in self.symbols if symbol.name == name)
|
|
45
|
+
|
|
46
|
+
def usages(self, symbol_id: str) -> tuple[Reference, ...]:
|
|
47
|
+
return tuple(reference for reference in self.references if symbol_id in reference.target_ids)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
BrowseKind = Literal["constant", "string", "type", "member", "global"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class BrowseEntry:
|
|
55
|
+
"""A generic recovered-AST fact, with provenance when it is available."""
|
|
56
|
+
|
|
57
|
+
id: str
|
|
58
|
+
kind: BrowseKind
|
|
59
|
+
name: str
|
|
60
|
+
value: str
|
|
61
|
+
function_id: str
|
|
62
|
+
source: SourceRef | None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class BrowseIndex:
|
|
67
|
+
entries: tuple[BrowseEntry, ...] = ()
|
|
68
|
+
|
|
69
|
+
def by_kind(self, kind: BrowseKind) -> tuple[BrowseEntry, ...]:
|
|
70
|
+
return tuple(entry for entry in self.entries if entry.kind == kind)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class ControlFlowBlock:
|
|
75
|
+
id: str
|
|
76
|
+
statement_count: int
|
|
77
|
+
terminator: str | None
|
|
78
|
+
source: SourceRef | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class BytecodeControlFlowInstruction:
|
|
83
|
+
"""Neutral control-transfer facts retained for read-only presentation."""
|
|
84
|
+
|
|
85
|
+
offset: int
|
|
86
|
+
source: SourceRef
|
|
87
|
+
flow: Literal["conditional", "unconditional", "multiway"] | None
|
|
88
|
+
targets: tuple[int, ...] = ()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class ControlFlowEdge:
|
|
93
|
+
source: str
|
|
94
|
+
target: str
|
|
95
|
+
kind: str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class FunctionControlFlow:
|
|
100
|
+
function_id: str
|
|
101
|
+
entry: str | None
|
|
102
|
+
blocks: tuple[ControlFlowBlock, ...]
|
|
103
|
+
edges: tuple[ControlFlowEdge, ...]
|
|
104
|
+
diagnostics: tuple[str, ...] = ()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def build_control_flow_index(
|
|
108
|
+
functions: tuple[tuple[str, FunctionIR], ...],
|
|
109
|
+
bytecode: tuple[tuple[str, tuple[BytecodeControlFlowInstruction, ...]], ...] = (),
|
|
110
|
+
) -> tuple[FunctionControlFlow, ...]:
|
|
111
|
+
bytecode_by_function = dict(bytecode)
|
|
112
|
+
output: list[FunctionControlFlow] = []
|
|
113
|
+
for function_id, function in functions:
|
|
114
|
+
cfg = build_cfg(function)
|
|
115
|
+
structured = FunctionControlFlow(
|
|
116
|
+
function_id=function_id,
|
|
117
|
+
entry=cfg.entry,
|
|
118
|
+
blocks=tuple(ControlFlowBlock(
|
|
119
|
+
id=block.id,
|
|
120
|
+
statement_count=len(block.statements),
|
|
121
|
+
terminator=None if block.terminator is None else type(block.terminator).__name__,
|
|
122
|
+
source=next((statement.source for statement in block.statements if statement.source is not None), None),
|
|
123
|
+
) for block in function.blocks),
|
|
124
|
+
edges=tuple(ControlFlowEdge(edge.source, edge.target, edge.kind) for edge in cfg.edges),
|
|
125
|
+
diagnostics=cfg.diagnostics,
|
|
126
|
+
)
|
|
127
|
+
projected = _bytecode_control_flow(function_id, bytecode_by_function.get(function_id, ()))
|
|
128
|
+
output.append(projected if projected is not None else structured)
|
|
129
|
+
return tuple(output)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _bytecode_control_flow(
|
|
133
|
+
function_id: str,
|
|
134
|
+
instructions: tuple[BytecodeControlFlowInstruction, ...],
|
|
135
|
+
) -> FunctionControlFlow | None:
|
|
136
|
+
if not instructions or not any(instruction.targets for instruction in instructions):
|
|
137
|
+
return None
|
|
138
|
+
if any(instruction.targets and instruction.flow is None for instruction in instructions):
|
|
139
|
+
return None
|
|
140
|
+
ordered = tuple(sorted(instructions, key=lambda instruction: instruction.offset))
|
|
141
|
+
offsets = tuple(instruction.offset for instruction in ordered)
|
|
142
|
+
by_offset = {instruction.offset: instruction for instruction in ordered}
|
|
143
|
+
leaders = {offsets[0]}
|
|
144
|
+
for index, instruction in enumerate(ordered):
|
|
145
|
+
leaders.update(target for target in instruction.targets if target in by_offset)
|
|
146
|
+
if instruction.targets and index + 1 < len(ordered):
|
|
147
|
+
leaders.add(offsets[index + 1])
|
|
148
|
+
ordered_leaders = tuple(sorted(leaders))
|
|
149
|
+
leader_for_offset = {
|
|
150
|
+
offset: max(leader for leader in ordered_leaders if leader <= offset)
|
|
151
|
+
for offset in offsets
|
|
152
|
+
}
|
|
153
|
+
blocks: list[ControlFlowBlock] = []
|
|
154
|
+
edges: list[ControlFlowEdge] = []
|
|
155
|
+
for index, leader in enumerate(ordered_leaders):
|
|
156
|
+
next_leader = ordered_leaders[index + 1] if index + 1 < len(ordered_leaders) else None
|
|
157
|
+
members = tuple(item for item in ordered if item.offset >= leader and (next_leader is None or item.offset < next_leader))
|
|
158
|
+
if not members:
|
|
159
|
+
continue
|
|
160
|
+
last = members[-1]
|
|
161
|
+
block_id = f"offset_{leader}"
|
|
162
|
+
terminator = _presentation_terminator(last, next_leader)
|
|
163
|
+
blocks.append(ControlFlowBlock(block_id, len(members), terminator, members[0].source))
|
|
164
|
+
for target in last.targets:
|
|
165
|
+
target_leader = leader_for_offset.get(target)
|
|
166
|
+
if target_leader is not None:
|
|
167
|
+
edges.append(ControlFlowEdge(block_id, f"offset_{target_leader}", "branch" if last.flow == "conditional" else "jump"))
|
|
168
|
+
if last.flow == "conditional" and next_leader is not None:
|
|
169
|
+
edges.append(ControlFlowEdge(block_id, f"offset_{next_leader}", "fallthrough"))
|
|
170
|
+
elif not last.targets and next_leader is not None:
|
|
171
|
+
edges.append(ControlFlowEdge(block_id, f"offset_{next_leader}", "fallthrough"))
|
|
172
|
+
if len(blocks) <= 1 or not edges:
|
|
173
|
+
return None
|
|
174
|
+
return FunctionControlFlow(
|
|
175
|
+
function_id=function_id,
|
|
176
|
+
entry=blocks[0].id,
|
|
177
|
+
blocks=tuple(blocks),
|
|
178
|
+
edges=tuple(edges),
|
|
179
|
+
diagnostics=("Bytecode CFG shown from VM-neutral control hints",),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _presentation_terminator(
|
|
184
|
+
instruction: BytecodeControlFlowInstruction,
|
|
185
|
+
next_leader: int | None,
|
|
186
|
+
) -> str:
|
|
187
|
+
if instruction.flow == "conditional":
|
|
188
|
+
return "Branch"
|
|
189
|
+
if instruction.flow == "unconditional":
|
|
190
|
+
return "Jump"
|
|
191
|
+
if instruction.flow == "multiway":
|
|
192
|
+
return "Switch"
|
|
193
|
+
return "Return" if next_leader is None else "Fallthrough"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_symbol_index(
|
|
197
|
+
module: ModuleDecl,
|
|
198
|
+
functions: tuple[tuple[str, str, SourceRef | None], ...],
|
|
199
|
+
) -> SymbolIndex:
|
|
200
|
+
"""Build only facts provable from the generic AST; never infer VM semantics."""
|
|
201
|
+
ast_functions = tuple(_walk_functions(module.functions))
|
|
202
|
+
if len(ast_functions) != len(functions):
|
|
203
|
+
raise ValueError("function result and AST function order differ")
|
|
204
|
+
|
|
205
|
+
symbols: list[Symbol] = []
|
|
206
|
+
references: list[Reference] = []
|
|
207
|
+
function_symbols: dict[str, list[str]] = {}
|
|
208
|
+
parameter_symbols: dict[tuple[str, str], str] = {}
|
|
209
|
+
for function, (function_id, name, source) in zip(ast_functions, functions, strict=True):
|
|
210
|
+
symbol_id = f"{function_id}:function"
|
|
211
|
+
symbols.append(Symbol(symbol_id, name, "function", function_id, source))
|
|
212
|
+
function_symbols.setdefault(name, []).append(symbol_id)
|
|
213
|
+
for parameter in function.params:
|
|
214
|
+
parameter_id = f"{function_id}:parameter:{parameter}"
|
|
215
|
+
symbols.append(Symbol(parameter_id, parameter, "parameter", function_id, function.source or source))
|
|
216
|
+
parameter_symbols[function_id, parameter] = parameter_id
|
|
217
|
+
|
|
218
|
+
for function, (function_id, _name, _source) in zip(ast_functions, functions, strict=True):
|
|
219
|
+
ordinal = 0
|
|
220
|
+
for node in _walk_nodes(function.body):
|
|
221
|
+
if isinstance(node, CallExpr):
|
|
222
|
+
callee_name = _call_name(node)
|
|
223
|
+
if callee_name is not None:
|
|
224
|
+
references.append(Reference(
|
|
225
|
+
id=f"{function_id}:reference:{ordinal}",
|
|
226
|
+
name=callee_name,
|
|
227
|
+
kind="call",
|
|
228
|
+
function_id=function_id,
|
|
229
|
+
source=node.source,
|
|
230
|
+
target_ids=tuple(function_symbols.get(callee_name, ())),
|
|
231
|
+
))
|
|
232
|
+
ordinal += 1
|
|
233
|
+
elif isinstance(node, GlobalRef):
|
|
234
|
+
references.append(Reference(
|
|
235
|
+
id=f"{function_id}:reference:{ordinal}",
|
|
236
|
+
name=node.name,
|
|
237
|
+
kind="global-read",
|
|
238
|
+
function_id=function_id,
|
|
239
|
+
source=node.source,
|
|
240
|
+
))
|
|
241
|
+
ordinal += 1
|
|
242
|
+
elif isinstance(node, VarRef):
|
|
243
|
+
parameter_id = parameter_symbols.get((function_id, node.name))
|
|
244
|
+
if parameter_id is not None:
|
|
245
|
+
references.append(Reference(
|
|
246
|
+
id=f"{function_id}:reference:{ordinal}",
|
|
247
|
+
name=node.name,
|
|
248
|
+
kind="parameter-read",
|
|
249
|
+
function_id=function_id,
|
|
250
|
+
source=node.source,
|
|
251
|
+
target_ids=(parameter_id,),
|
|
252
|
+
))
|
|
253
|
+
ordinal += 1
|
|
254
|
+
return SymbolIndex(tuple(symbols), tuple(references))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def build_browse_index(
|
|
258
|
+
module: ModuleDecl,
|
|
259
|
+
functions: tuple[tuple[str, str, SourceRef | None], ...],
|
|
260
|
+
) -> BrowseIndex:
|
|
261
|
+
"""Index only facts explicitly represented by the generic recovered AST.
|
|
262
|
+
|
|
263
|
+
The index intentionally avoids frontend metadata and does not guess source
|
|
264
|
+
language semantics. Occurrences remain separate because each one may have
|
|
265
|
+
a different source location for host navigation.
|
|
266
|
+
"""
|
|
267
|
+
ast_functions = tuple(_walk_functions(module.functions))
|
|
268
|
+
if len(ast_functions) != len(functions):
|
|
269
|
+
raise ValueError("function result and AST function order differ")
|
|
270
|
+
|
|
271
|
+
entries: list[BrowseEntry] = []
|
|
272
|
+
for function, (function_id, _name, _source) in zip(ast_functions, functions, strict=True):
|
|
273
|
+
ordinal = 0
|
|
274
|
+
for node in _walk_nodes(function.body):
|
|
275
|
+
kind: BrowseKind | None = None
|
|
276
|
+
name = ""
|
|
277
|
+
value = ""
|
|
278
|
+
source = getattr(node, "source", None)
|
|
279
|
+
if isinstance(node, ConstExpr):
|
|
280
|
+
kind = "string" if isinstance(node.value, str) else "constant"
|
|
281
|
+
name = node.value if isinstance(node.value, str) else type(node.value).__name__
|
|
282
|
+
value = repr(node.value)
|
|
283
|
+
elif isinstance(node, NewObjectExpr):
|
|
284
|
+
kind, name, value = "type", node.type_name, node.type_name
|
|
285
|
+
elif isinstance(node, (GetAttrExpr, StoreAttrStmt)):
|
|
286
|
+
kind, name, value = "member", node.attr, node.attr
|
|
287
|
+
elif isinstance(node, GlobalRef):
|
|
288
|
+
kind, name, value = "global", node.name, node.name
|
|
289
|
+
elif isinstance(node, AstExpr) and node.type.name != "unknown":
|
|
290
|
+
kind, name, value = "type", node.type.name, node.type.name
|
|
291
|
+
if kind is None:
|
|
292
|
+
continue
|
|
293
|
+
entries.append(BrowseEntry(
|
|
294
|
+
id=f"{function_id}:browse:{ordinal}",
|
|
295
|
+
kind=kind,
|
|
296
|
+
name=str(name),
|
|
297
|
+
value=str(value),
|
|
298
|
+
function_id=function_id,
|
|
299
|
+
source=source,
|
|
300
|
+
))
|
|
301
|
+
ordinal += 1
|
|
302
|
+
return BrowseIndex(tuple(entries))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _walk_functions(functions: tuple[FunctionDecl, ...]):
|
|
306
|
+
for function in functions:
|
|
307
|
+
yield function
|
|
308
|
+
yield from _walk_functions(function.nested_functions)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _walk_nodes(value):
|
|
312
|
+
if isinstance(value, tuple | list):
|
|
313
|
+
for item in value:
|
|
314
|
+
yield from _walk_nodes(item)
|
|
315
|
+
elif is_dataclass(value):
|
|
316
|
+
yield value
|
|
317
|
+
for field in fields(value):
|
|
318
|
+
yield from _walk_nodes(getattr(value, field.name))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _call_name(call: CallExpr) -> str | None:
|
|
322
|
+
if isinstance(call.callee, (GlobalRef, VarRef)):
|
|
323
|
+
return call.callee.name
|
|
324
|
+
return None
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
from unidecompiler.core.ir import ModuleIR
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class OutputArtifact:
|
|
11
|
+
kind: str
|
|
12
|
+
text: str
|
|
13
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Backend(Protocol):
|
|
17
|
+
id: str
|
|
18
|
+
display_name: str
|
|
19
|
+
|
|
20
|
+
def emit(self, module: ModuleIR) -> OutputArtifact:
|
|
21
|
+
"""Emit a user-facing artifact from Universal IR."""
|
|
22
|
+
|