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.
Files changed (39) hide show
  1. unidecompiler-0.1.0/PKG-INFO +16 -0
  2. unidecompiler-0.1.0/README.md +9 -0
  3. unidecompiler-0.1.0/pyproject.toml +14 -0
  4. unidecompiler-0.1.0/setup.cfg +4 -0
  5. unidecompiler-0.1.0/src/unidecompiler/__init__.py +24 -0
  6. unidecompiler-0.1.0/src/unidecompiler/analysis.py +324 -0
  7. unidecompiler-0.1.0/src/unidecompiler/backends/__init__.py +2 -0
  8. unidecompiler-0.1.0/src/unidecompiler/backends/base.py +22 -0
  9. unidecompiler-0.1.0/src/unidecompiler/backends/pseudocode.py +906 -0
  10. unidecompiler-0.1.0/src/unidecompiler/core/__init__.py +2 -0
  11. unidecompiler-0.1.0/src/unidecompiler/core/ast.py +297 -0
  12. unidecompiler-0.1.0/src/unidecompiler/core/astify.py +542 -0
  13. unidecompiler-0.1.0/src/unidecompiler/core/cfg.py +209 -0
  14. unidecompiler-0.1.0/src/unidecompiler/core/diagnostics.py +73 -0
  15. unidecompiler-0.1.0/src/unidecompiler/core/effects.py +1328 -0
  16. unidecompiler-0.1.0/src/unidecompiler/core/function_assembly.py +91 -0
  17. unidecompiler-0.1.0/src/unidecompiler/core/ir.py +353 -0
  18. unidecompiler-0.1.0/src/unidecompiler/core/low_level_cfg_structuring.py +5073 -0
  19. unidecompiler-0.1.0/src/unidecompiler/core/rawcfg.py +163 -0
  20. unidecompiler-0.1.0/src/unidecompiler/core/reporting.py +56 -0
  21. unidecompiler-0.1.0/src/unidecompiler/core/ssa.py +692 -0
  22. unidecompiler-0.1.0/src/unidecompiler/core/stack_machine.py +148 -0
  23. unidecompiler-0.1.0/src/unidecompiler/core/structuring.py +243 -0
  24. unidecompiler-0.1.0/src/unidecompiler/core/vm_bytecode.py +46 -0
  25. unidecompiler-0.1.0/src/unidecompiler/core/vm_effect_table.py +59 -0
  26. unidecompiler-0.1.0/src/unidecompiler/core/vm_function.py +1671 -0
  27. unidecompiler-0.1.0/src/unidecompiler/core/vm_hints.py +37 -0
  28. unidecompiler-0.1.0/src/unidecompiler/core/vm_module.py +21 -0
  29. unidecompiler-0.1.0/src/unidecompiler/core/vm_operands.py +38 -0
  30. unidecompiler-0.1.0/src/unidecompiler/core/vm_region.py +2739 -0
  31. unidecompiler-0.1.0/src/unidecompiler/core/vm_structures.py +123 -0
  32. unidecompiler-0.1.0/src/unidecompiler/engine.py +254 -0
  33. unidecompiler-0.1.0/src/unidecompiler/input_sources.py +93 -0
  34. unidecompiler-0.1.0/src/unidecompiler/plugin_registry.py +158 -0
  35. unidecompiler-0.1.0/src/unidecompiler/plugins.py +50 -0
  36. unidecompiler-0.1.0/src/unidecompiler.egg-info/PKG-INFO +16 -0
  37. unidecompiler-0.1.0/src/unidecompiler.egg-info/SOURCES.txt +37 -0
  38. unidecompiler-0.1.0/src/unidecompiler.egg-info/dependency_links.txt +1 -0
  39. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ """Output backend interfaces and built-in backend adapters."""
2
+
@@ -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
+