unidecompiler-simulator 0.1.1__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.
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: unidecompiler-simulator
3
+ Version: 0.1.1
4
+ Summary: A decoupled generic IR simulator for unidecompiler
5
+ Author-email: Wker <1670133844@qq.com>
6
+ License-Expression: AGPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/Wker666/unidecompiler
8
+ Project-URL: Repository, https://github.com/Wker666/unidecompiler
9
+ Project-URL: Issues, https://github.com/Wker666/unidecompiler/issues
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: unidecompiler<0.2.0,>=0.1.1
13
+
14
+ # unidecompiler-simulator
15
+
16
+ `unidecompiler-simulator` is a library for bounded execution of
17
+ `unidecompiler` generic IR. It depends on the decompiler core but the core does
18
+ not depend on, import, or know about this package.
19
+
20
+ The simulator owns frames, control flow, calls, limits, exceptions, and the
21
+ execution trace. A frontend may expose an optional `simulation_adapter`
22
+ attribute for frontend-specific function lookup and runtime facts. The adapter
23
+ may answer individual operations, but it must not execute functions, interpret
24
+ instructions, recover control flow, or return executable callbacks.
25
+
26
+ Generic IR execution is available without a frontend:
27
+
28
+ ```python
29
+ from unidecompiler_simulator import SimulationEngine
30
+
31
+ result = SimulationEngine().simulate_function(module, function, args=(1, 2))
32
+ ```
33
+
34
+ Artifact execution gives an adapter the decoded frontend-owned payload only for
35
+ function lookup and runtime hooks:
36
+
37
+ ```python
38
+ result = SimulationEngine().simulate_artifact(
39
+ data,
40
+ "sample.bytecode",
41
+ query={"class": "Example", "method": "run"},
42
+ )
43
+ ```
44
+
45
+ The `unidecompiler-cli` package hosts the command-line interface and accepts a
46
+ frontend-owned function query and JSON arguments:
47
+
48
+ ```sh
49
+ unidecompiler simulate sample.bytecode --function 'Example.run' --args '[1, 2]'
50
+ ```
51
+
52
+ For the Lua bubble-sort fixture, compile the source before invoking the
53
+ simulator. The Lua frontend adapter supplies only Lua function lookup and
54
+ runtime value facts; the simulator still executes the lifted generic IR.
55
+
56
+ ```sh
57
+ luac -o bubble_sort.luac simulator_projects/source/arithmetic.lua
58
+ unidecompiler simulate bubble_sort.luac --frontend lua --function bubble_sort --args '[[5, 1, 4, 2, 8]]'
59
+ ```
60
+
61
+ The completed result contains `"values": [[1, 2, 4, 5, 8]]`.
62
+
63
+ Built-in frontend adapters accept the following frontend-owned function queries:
64
+
65
+ | Frontend | Query |
66
+ | --- | --- |
67
+ | Python `.pyc` | Unique function name, for example `arithmetic` |
68
+ | JVM `.class` | Unique method name or `Class.method`, for example `Sample.add` |
69
+ | .NET assembly | Unique method name or `Type.Method`, for example `Probe.Add` |
70
+ | WebAssembly | Function name or `$funcN`, for example `add` or `$func0` |
71
+ | Lua chunk | Unique function name, for example `bubble_sort` |
72
+
73
+ Ambiguous queries are rejected by the relevant frontend adapter. The simulator
74
+ does not choose among overloads or perform frontend-specific name recovery.
75
+
76
+ Execution is bounded and in-memory. Unknown operations, unsupported IR, and
77
+ unsafe runtime behavior stop with a structured result instead of guessing.
78
+
79
+ Applications may inject an `ExternalEnvironment` for unresolved named calls.
80
+ The environment receives only `ExternalCallRequest` data and returns an
81
+ `ExternalCallResult`; it never receives IR, frames, adapters, or execution
82
+ control. Python-file loading is intentionally owned by `unidecompiler-cli`,
83
+ not this library.
84
+
85
+ The simulator does not resolve `Global` expressions as function names. Any
86
+ frontend-specific function lookup or dynamic call target must be provided by
87
+ the optional adapter and must resolve back to a `FunctionIR` owned by the
88
+ current lifted module.
89
+
90
+ The simulator intentionally does not execute frontend bytecode, frontend
91
+ opcode tables, or core `Effect` objects directly. Core is responsible for
92
+ lifting VM-neutral effects into generic IR; this package executes that generic
93
+ IR. Keeping that boundary strict prevents a language-specific opcode switch
94
+ from leaking into the simulator and keeps every frontend replaceable.
@@ -0,0 +1,81 @@
1
+ # unidecompiler-simulator
2
+
3
+ `unidecompiler-simulator` is a library for bounded execution of
4
+ `unidecompiler` generic IR. It depends on the decompiler core but the core does
5
+ not depend on, import, or know about this package.
6
+
7
+ The simulator owns frames, control flow, calls, limits, exceptions, and the
8
+ execution trace. A frontend may expose an optional `simulation_adapter`
9
+ attribute for frontend-specific function lookup and runtime facts. The adapter
10
+ may answer individual operations, but it must not execute functions, interpret
11
+ instructions, recover control flow, or return executable callbacks.
12
+
13
+ Generic IR execution is available without a frontend:
14
+
15
+ ```python
16
+ from unidecompiler_simulator import SimulationEngine
17
+
18
+ result = SimulationEngine().simulate_function(module, function, args=(1, 2))
19
+ ```
20
+
21
+ Artifact execution gives an adapter the decoded frontend-owned payload only for
22
+ function lookup and runtime hooks:
23
+
24
+ ```python
25
+ result = SimulationEngine().simulate_artifact(
26
+ data,
27
+ "sample.bytecode",
28
+ query={"class": "Example", "method": "run"},
29
+ )
30
+ ```
31
+
32
+ The `unidecompiler-cli` package hosts the command-line interface and accepts a
33
+ frontend-owned function query and JSON arguments:
34
+
35
+ ```sh
36
+ unidecompiler simulate sample.bytecode --function 'Example.run' --args '[1, 2]'
37
+ ```
38
+
39
+ For the Lua bubble-sort fixture, compile the source before invoking the
40
+ simulator. The Lua frontend adapter supplies only Lua function lookup and
41
+ runtime value facts; the simulator still executes the lifted generic IR.
42
+
43
+ ```sh
44
+ luac -o bubble_sort.luac simulator_projects/source/arithmetic.lua
45
+ unidecompiler simulate bubble_sort.luac --frontend lua --function bubble_sort --args '[[5, 1, 4, 2, 8]]'
46
+ ```
47
+
48
+ The completed result contains `"values": [[1, 2, 4, 5, 8]]`.
49
+
50
+ Built-in frontend adapters accept the following frontend-owned function queries:
51
+
52
+ | Frontend | Query |
53
+ | --- | --- |
54
+ | Python `.pyc` | Unique function name, for example `arithmetic` |
55
+ | JVM `.class` | Unique method name or `Class.method`, for example `Sample.add` |
56
+ | .NET assembly | Unique method name or `Type.Method`, for example `Probe.Add` |
57
+ | WebAssembly | Function name or `$funcN`, for example `add` or `$func0` |
58
+ | Lua chunk | Unique function name, for example `bubble_sort` |
59
+
60
+ Ambiguous queries are rejected by the relevant frontend adapter. The simulator
61
+ does not choose among overloads or perform frontend-specific name recovery.
62
+
63
+ Execution is bounded and in-memory. Unknown operations, unsupported IR, and
64
+ unsafe runtime behavior stop with a structured result instead of guessing.
65
+
66
+ Applications may inject an `ExternalEnvironment` for unresolved named calls.
67
+ The environment receives only `ExternalCallRequest` data and returns an
68
+ `ExternalCallResult`; it never receives IR, frames, adapters, or execution
69
+ control. Python-file loading is intentionally owned by `unidecompiler-cli`,
70
+ not this library.
71
+
72
+ The simulator does not resolve `Global` expressions as function names. Any
73
+ frontend-specific function lookup or dynamic call target must be provided by
74
+ the optional adapter and must resolve back to a `FunctionIR` owned by the
75
+ current lifted module.
76
+
77
+ The simulator intentionally does not execute frontend bytecode, frontend
78
+ opcode tables, or core `Effect` objects directly. Core is responsible for
79
+ lifting VM-neutral effects into generic IR; this package executes that generic
80
+ IR. Keeping that boundary strict prevents a language-specific opcode switch
81
+ from leaking into the simulator and keeps every frontend replaceable.
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "unidecompiler-simulator"
7
+ version = "0.1.1"
8
+ description = "A decoupled generic IR simulator for unidecompiler"
9
+ readme = "README.md"
10
+ license = "AGPL-3.0-or-later"
11
+ authors = [{ name = "Wker", email = "1670133844@qq.com" }]
12
+ requires-python = ">=3.11"
13
+ dependencies = ["unidecompiler>=0.1.1,<0.2.0"]
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/Wker666/unidecompiler"
17
+ Repository = "https://github.com/Wker666/unidecompiler"
18
+ Issues = "https://github.com/Wker666/unidecompiler/issues"
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,53 @@
1
+ """Decoupled, bounded execution of unidecompiler generic IR."""
2
+
3
+ from unidecompiler_simulator.adapters import (
4
+ CallRequest,
5
+ IntrinsicCall,
6
+ NotHandled,
7
+ ResolvedFunction,
8
+ SimulationTarget,
9
+ SimulationTargetCandidate,
10
+ SimulationAdapter,
11
+ )
12
+ from unidecompiler_simulator.engine import (
13
+ SimulationEngine,
14
+ SimulationCancellation,
15
+ SimulationEvent,
16
+ SimulationLimits,
17
+ SimulationResult,
18
+ SimulationStatus,
19
+ SimulationTargetListing,
20
+ )
21
+ from unidecompiler_simulator.environment import (
22
+ ExternalCallRequest,
23
+ ExternalCallResult,
24
+ ExternalCallStatus,
25
+ ExternalEnvironment,
26
+ ExternalFunction,
27
+ )
28
+ from unidecompiler_simulator.values import ObjectValue, SliceValue, TableValue
29
+
30
+ __all__ = [
31
+ "NotHandled",
32
+ "CallRequest",
33
+ "IntrinsicCall",
34
+ "ObjectValue",
35
+ "SliceValue",
36
+ "TableValue",
37
+ "ResolvedFunction",
38
+ "SimulationAdapter",
39
+ "SimulationTarget",
40
+ "SimulationTargetCandidate",
41
+ "SimulationEngine",
42
+ "SimulationCancellation",
43
+ "SimulationEvent",
44
+ "SimulationLimits",
45
+ "SimulationResult",
46
+ "SimulationStatus",
47
+ "SimulationTargetListing",
48
+ "ExternalCallRequest",
49
+ "ExternalCallResult",
50
+ "ExternalCallStatus",
51
+ "ExternalEnvironment",
52
+ "ExternalFunction",
53
+ ]
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Protocol, runtime_checkable
5
+
6
+ from unidecompiler.core.ir import FunctionIR, ModuleIR
7
+ from unidecompiler.plugins import FrontendModule
8
+
9
+
10
+ class _NotHandled:
11
+ def __repr__(self) -> str:
12
+ return "NotHandled"
13
+
14
+
15
+ NotHandled = _NotHandled()
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class CallRequest:
20
+ """Data-only call request passed to an optional frontend adapter."""
21
+
22
+ callee: object
23
+ args: tuple[object, ...]
24
+ keywords: tuple[tuple[str, object], ...] = ()
25
+ context: object | None = None
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class SimulationTargetCandidate:
30
+ """A frontend-owned lookup query suitable for presenting to a host."""
31
+
32
+ label: str
33
+ query: object
34
+
35
+ def __post_init__(self) -> None:
36
+ if not self.label:
37
+ raise ValueError("simulation target label must not be empty")
38
+ if callable(self.query):
39
+ raise TypeError("simulation target query cannot be executable")
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class SimulationTarget:
44
+ """A verified generic function target with an opaque frontend query."""
45
+
46
+ label: str
47
+ query: object
48
+ function_index: int
49
+ params: tuple[str, ...]
50
+
51
+ def __post_init__(self) -> None:
52
+ if not self.label:
53
+ raise ValueError("simulation target label must not be empty")
54
+ if self.function_index < 0:
55
+ raise ValueError("simulation target function_index must be non-negative")
56
+ if not all(isinstance(param, str) for param in self.params):
57
+ raise TypeError("simulation target parameters must be strings")
58
+ if callable(self.query):
59
+ raise TypeError("simulation target query cannot be executable")
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class IntrinsicCall:
64
+ """A data-only request for a simulator-owned pure runtime operation."""
65
+
66
+ name: str
67
+ bit_width: int | None = None
68
+
69
+ def __post_init__(self) -> None:
70
+ if not self.name:
71
+ raise ValueError("intrinsic name must not be empty")
72
+ if self.bit_width is not None and self.bit_width <= 0:
73
+ raise ValueError("intrinsic bit_width must be positive")
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class ResolvedFunction:
78
+ """A frontend-selected generic function plus an opaque identity token.
79
+
80
+ The simulator validates that ``function`` belongs to the lifted module and
81
+ owns all execution. ``context`` is only passed back to adapter hooks; it is
82
+ never called, interpreted, or used as control flow by the simulator.
83
+ """
84
+
85
+ function: FunctionIR
86
+ context: object | None = None
87
+ identifier: str = ""
88
+
89
+ def __post_init__(self) -> None:
90
+ if callable(self.context):
91
+ raise TypeError("resolved function context cannot be executable")
92
+
93
+
94
+ @runtime_checkable
95
+ class SimulationAdapter(Protocol):
96
+ """Optional frontend runtime facts, never a frontend execution engine.
97
+
98
+ Implementations may define any subset of the operation methods. Missing
99
+ methods are treated as ``NotHandled``. They must not expose execute/run/
100
+ step/eval methods or return executable callbacks.
101
+ """
102
+
103
+ frontend_id: str
104
+
105
+ def resolve_function(
106
+ self,
107
+ query: object,
108
+ decoded_module: FrontendModule,
109
+ lifted_module: ModuleIR,
110
+ ) -> ResolvedFunction | _NotHandled: ...
111
+
112
+ def list_simulation_targets(
113
+ self,
114
+ decoded_module: FrontendModule,
115
+ lifted_module: ModuleIR,
116
+ ) -> tuple[SimulationTargetCandidate, ...] | _NotHandled: ...
117
+
118
+
119
+ def adapter_for(plugin: object) -> SimulationAdapter | None:
120
+ adapter = getattr(plugin, "simulation_adapter", None)
121
+ if adapter is None:
122
+ return None
123
+ if isinstance(adapter, type):
124
+ adapter = adapter()
125
+ frontend_id = getattr(adapter, "frontend_id", None)
126
+ if not isinstance(frontend_id, str) or not frontend_id:
127
+ raise TypeError("simulation_adapter must declare a non-empty frontend_id")
128
+ if not callable(getattr(adapter, "resolve_function", None)):
129
+ raise TypeError("simulation_adapter must provide resolve_function")
130
+ forbidden = {"execute_function", "run", "step", "eval", "interpret", "next_instruction"}
131
+ exposed = forbidden.intersection(name for name in dir(adapter) if not name.startswith("__"))
132
+ if exposed:
133
+ names = ", ".join(sorted(exposed))
134
+ raise TypeError(f"simulation_adapter exposes forbidden execution methods: {names}")
135
+ return adapter # type: ignore[return-value]
136
+
137
+
138
+ def call_adapter(adapter: object | None, name: str, *args: Any) -> object:
139
+ if adapter is None:
140
+ return NotHandled
141
+ method = getattr(adapter, name, None)
142
+ if method is None:
143
+ return NotHandled
144
+ result = method(*args)
145
+ if callable(result) and not isinstance(result, ResolvedFunction):
146
+ raise TypeError(f"adapter operation {name!r} returned an executable callback")
147
+ return result