efrog 0.4.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.
- efrog/__init__.py +46 -0
- efrog/cli.py +232 -0
- efrog/decompilers/__init__.py +1 -0
- efrog/decompilers/c.py +339 -0
- efrog/decompilers/javascript.py +472 -0
- efrog/decompilers/matlab.py +526 -0
- efrog/decompilers/python.py +737 -0
- efrog/decompilers/rust.py +659 -0
- efrog/genome.py +340 -0
- efrog/lean.py +302 -0
- efrog/mic.py +188 -0
- efrog/optimize.py +386 -0
- efrog/profile.py +146 -0
- efrog/verify.py +384 -0
- efrog-0.4.0.dist-info/METADATA +192 -0
- efrog-0.4.0.dist-info/RECORD +19 -0
- efrog-0.4.0.dist-info/WHEEL +5 -0
- efrog-0.4.0.dist-info/entry_points.txt +2 -0
- efrog-0.4.0.dist-info/top_level.txt +1 -0
efrog/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""eFrog — universal decompiler for the EML substrate.
|
|
2
|
+
|
|
3
|
+
Forge compiles math into everything.
|
|
4
|
+
eFrog compiles everything into math.
|
|
5
|
+
|
|
6
|
+
Phase E1 ships the Python AST decompiler. Later phases add C / Rust /
|
|
7
|
+
JS / MATLAB / CUDA / shaders / Verilog / Solidity, then symbolic
|
|
8
|
+
extraction with loops + conditionals, then live-audio decomposition.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.4.0a0"
|
|
12
|
+
|
|
13
|
+
from .decompilers.c import decompile_c_source
|
|
14
|
+
from .decompilers.javascript import decompile_javascript_source
|
|
15
|
+
from .decompilers.matlab import decompile_matlab_source
|
|
16
|
+
from .decompilers.python import decompile_python_source
|
|
17
|
+
from .decompilers.rust import decompile_rust_source
|
|
18
|
+
from .genome import classify_function, classify_module, render_genome
|
|
19
|
+
from .lean import render_lean
|
|
20
|
+
from .mic import decompose_buffer, decompose_microphone, render_mic
|
|
21
|
+
from .optimize import optimize_function, optimize_module, render_optimize
|
|
22
|
+
from .profile import profile_module, render_profile
|
|
23
|
+
from .verify import render_verify, verify_python_file
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"__version__",
|
|
27
|
+
"decompile_python_source",
|
|
28
|
+
"decompile_c_source",
|
|
29
|
+
"decompile_javascript_source",
|
|
30
|
+
"decompile_matlab_source",
|
|
31
|
+
"decompile_rust_source",
|
|
32
|
+
"profile_module",
|
|
33
|
+
"render_profile",
|
|
34
|
+
"verify_python_file",
|
|
35
|
+
"render_verify",
|
|
36
|
+
"classify_module",
|
|
37
|
+
"classify_function",
|
|
38
|
+
"render_genome",
|
|
39
|
+
"render_lean",
|
|
40
|
+
"optimize_module",
|
|
41
|
+
"optimize_function",
|
|
42
|
+
"render_optimize",
|
|
43
|
+
"decompose_microphone",
|
|
44
|
+
"decompose_buffer",
|
|
45
|
+
"render_mic",
|
|
46
|
+
]
|
efrog/cli.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""eFrog CLI entry point. E1: Python. E2: + C, --profile, --verify."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .decompilers.c import decompile_c_file
|
|
12
|
+
from .decompilers.javascript import decompile_javascript_file
|
|
13
|
+
from .decompilers.matlab import decompile_matlab_file
|
|
14
|
+
from .decompilers.python import EFrogError, decompile_python_file
|
|
15
|
+
from .decompilers.rust import decompile_rust_file
|
|
16
|
+
from .genome import render_genome
|
|
17
|
+
from .lean import render_lean
|
|
18
|
+
from .mic import decompose_microphone, render_mic
|
|
19
|
+
from .optimize import optimize_module, render_optimize
|
|
20
|
+
from .profile import render_profile
|
|
21
|
+
from .verify import render_verify, verify_python_file
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_LANG_BY_EXT = {
|
|
25
|
+
".py": "python",
|
|
26
|
+
".c": "c",
|
|
27
|
+
".js": "javascript",
|
|
28
|
+
".mjs": "javascript",
|
|
29
|
+
".ts": "javascript", # TS is parsed as JS — annotations not yet handled
|
|
30
|
+
".rs": "rust",
|
|
31
|
+
".m": "matlab",
|
|
32
|
+
# Future-phase stubs — the dispatcher knows the language but the
|
|
33
|
+
# decompiler isn't shipped yet. They get a clear error message
|
|
34
|
+
# instead of silently falling back to Python.
|
|
35
|
+
".cpp": "cpp",
|
|
36
|
+
".cu": "cuda",
|
|
37
|
+
".m": "matlab",
|
|
38
|
+
".hlsl": "hlsl",
|
|
39
|
+
".glsl": "glsl",
|
|
40
|
+
".v": "verilog",
|
|
41
|
+
".sv": "systemverilog",
|
|
42
|
+
".sol": "solidity",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _detect_language(path: str, override: str | None) -> str:
|
|
47
|
+
if override:
|
|
48
|
+
return override
|
|
49
|
+
ext = os.path.splitext(path)[1].lower()
|
|
50
|
+
return _LANG_BY_EXT.get(ext, "")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
54
|
+
parser = argparse.ArgumentParser(
|
|
55
|
+
prog="efrog",
|
|
56
|
+
description=(
|
|
57
|
+
"eFrog — extract EML from any source. "
|
|
58
|
+
"E2.5 supports Python / C / JavaScript / Rust / MATLAB; "
|
|
59
|
+
"E3+ adds shaders / Verilog / Solidity."
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument("input", nargs="?", help="source file (omit when using --mic)")
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"-o", "--output",
|
|
65
|
+
help="write EML to this path (default: stdout)",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--lang",
|
|
69
|
+
help="force a language instead of inferring from extension",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--module",
|
|
73
|
+
help="override the emitted `module name;` (default: filename stem)",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--version",
|
|
77
|
+
action="version",
|
|
78
|
+
version=f"efrog {__version__}",
|
|
79
|
+
)
|
|
80
|
+
# Hidden-for-now flags that point at future phases. Surfaced so
|
|
81
|
+
# `--help` is honest about what's coming, but they error politely.
|
|
82
|
+
parser.add_argument("--profile", action="store_true",
|
|
83
|
+
help="print per-function chain order, transcendental "
|
|
84
|
+
"count, node count, drift risk")
|
|
85
|
+
parser.add_argument("--verify", action="store_true",
|
|
86
|
+
help="round-trip the extracted EML through a Python "
|
|
87
|
+
"shim and compare numerically against the original "
|
|
88
|
+
"(E3 partial — Lean proofs land in full E3)")
|
|
89
|
+
parser.add_argument("--samples", type=int, default=100,
|
|
90
|
+
help="number of random input samples for --verify "
|
|
91
|
+
"(default 100)")
|
|
92
|
+
parser.add_argument("--seed", type=int, default=42,
|
|
93
|
+
help="RNG seed for --verify sampling (default 42)")
|
|
94
|
+
parser.add_argument("--mic", action="store_true",
|
|
95
|
+
help="capture a short audio buffer from the default "
|
|
96
|
+
"input device and emit a single-sine EML fit "
|
|
97
|
+
"(E4 scaffolding — multi-tone + envelope land later)")
|
|
98
|
+
parser.add_argument("--mic-duration", type=float, default=0.5,
|
|
99
|
+
help="seconds of audio to capture (default 0.5)")
|
|
100
|
+
parser.add_argument("--mic-rate", type=int, default=44_100,
|
|
101
|
+
help="sample rate in Hz (default 44100)")
|
|
102
|
+
parser.add_argument("--genome", action="store_true",
|
|
103
|
+
help="classify each decompiled function against a small "
|
|
104
|
+
"bundled corpus of canonical math patterns "
|
|
105
|
+
"(gaussian, sigmoid, softplus, polynomial, …)")
|
|
106
|
+
parser.add_argument("--genome-top", type=int, default=5,
|
|
107
|
+
help="how many genome matches to print per function "
|
|
108
|
+
"(default 5)")
|
|
109
|
+
parser.add_argument("--lean", action="store_true",
|
|
110
|
+
help="emit a Lean 4 theorem skeleton for each "
|
|
111
|
+
"decompiled function (Mathlib-style; bodies "
|
|
112
|
+
"are sorry-bodied scaffolds)")
|
|
113
|
+
parser.add_argument("--optimize", action="store_true",
|
|
114
|
+
help="run a conservative algebraic simplifier "
|
|
115
|
+
"(x+0, x*1, exp(0), x**2 → x*x, …) before "
|
|
116
|
+
"emitting EML")
|
|
117
|
+
parser.add_argument("--optimize-report", action="store_true",
|
|
118
|
+
help="print the per-function rewrite count "
|
|
119
|
+
"alongside --optimize output")
|
|
120
|
+
|
|
121
|
+
args = parser.parse_args(argv)
|
|
122
|
+
|
|
123
|
+
if args.mic:
|
|
124
|
+
try:
|
|
125
|
+
result = decompose_microphone(
|
|
126
|
+
duration=args.mic_duration,
|
|
127
|
+
sample_rate=args.mic_rate,
|
|
128
|
+
)
|
|
129
|
+
except RuntimeError as exc:
|
|
130
|
+
print(f"efrog: {exc}", file=sys.stderr)
|
|
131
|
+
return 1
|
|
132
|
+
out_text = render_mic(result)
|
|
133
|
+
if args.output:
|
|
134
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
135
|
+
f.write(out_text)
|
|
136
|
+
else:
|
|
137
|
+
sys.stdout.write(out_text)
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
if not args.input:
|
|
141
|
+
parser.error("input file required (or use --mic in E4)")
|
|
142
|
+
|
|
143
|
+
path = args.input
|
|
144
|
+
if not os.path.isfile(path):
|
|
145
|
+
print(f"efrog: no such file: {path}", file=sys.stderr)
|
|
146
|
+
return 1
|
|
147
|
+
|
|
148
|
+
lang = _detect_language(path, args.lang)
|
|
149
|
+
decompiler = _DECOMPILERS.get(lang)
|
|
150
|
+
if decompiler is None:
|
|
151
|
+
if not lang:
|
|
152
|
+
print(
|
|
153
|
+
f"efrog: can't infer language from {path!r}; pass --lang",
|
|
154
|
+
file=sys.stderr,
|
|
155
|
+
)
|
|
156
|
+
return 1
|
|
157
|
+
print(
|
|
158
|
+
f"efrog: language `{lang}` ships in a later phase. "
|
|
159
|
+
f"E2.5 supports python, c, javascript/typescript, rust, and matlab. "
|
|
160
|
+
f"See ROADMAP.md.",
|
|
161
|
+
file=sys.stderr,
|
|
162
|
+
)
|
|
163
|
+
return 2
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
mod = decompiler(path)
|
|
167
|
+
except EFrogError as exc:
|
|
168
|
+
print(f"efrog: {exc}", file=sys.stderr)
|
|
169
|
+
return 1
|
|
170
|
+
|
|
171
|
+
if args.module:
|
|
172
|
+
mod.name = args.module
|
|
173
|
+
|
|
174
|
+
if args.optimize:
|
|
175
|
+
reports = optimize_module(mod)
|
|
176
|
+
if args.optimize_report:
|
|
177
|
+
sys.stderr.write(render_optimize(reports))
|
|
178
|
+
|
|
179
|
+
if args.profile:
|
|
180
|
+
sys.stdout.write(render_profile(mod))
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
if args.genome:
|
|
184
|
+
sys.stdout.write(render_genome(mod, top_k=args.genome_top))
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
if args.lean:
|
|
188
|
+
out_text = render_lean(mod)
|
|
189
|
+
if args.output:
|
|
190
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
191
|
+
f.write(out_text)
|
|
192
|
+
else:
|
|
193
|
+
sys.stdout.write(out_text)
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
if args.verify:
|
|
197
|
+
# --verify is currently Python-only because the round-trip
|
|
198
|
+
# needs to run the original source. C round-trip lands when
|
|
199
|
+
# we can host a tiny Tiny C interpreter or shell out to gcc.
|
|
200
|
+
if lang != "python":
|
|
201
|
+
print(
|
|
202
|
+
f"efrog: --verify currently supports python only "
|
|
203
|
+
f"(your file is {lang}); the C round-trip needs a runtime",
|
|
204
|
+
file=sys.stderr,
|
|
205
|
+
)
|
|
206
|
+
return 2
|
|
207
|
+
result = verify_python_file(
|
|
208
|
+
path, n_samples=args.samples, seed=args.seed
|
|
209
|
+
)
|
|
210
|
+
sys.stdout.write(render_verify(result))
|
|
211
|
+
return 0 if result.passed else 1
|
|
212
|
+
|
|
213
|
+
eml = mod.to_eml()
|
|
214
|
+
if args.output:
|
|
215
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
216
|
+
f.write(eml)
|
|
217
|
+
else:
|
|
218
|
+
sys.stdout.write(eml)
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
_DECOMPILERS = {
|
|
223
|
+
"python": decompile_python_file,
|
|
224
|
+
"c": decompile_c_file,
|
|
225
|
+
"javascript": decompile_javascript_file,
|
|
226
|
+
"rust": decompile_rust_file,
|
|
227
|
+
"matlab": decompile_matlab_file,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Per-language decompilers. E1 ships Python; E2 adds C/Rust/JS/MATLAB."""
|
efrog/decompilers/c.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""C → EML.
|
|
2
|
+
|
|
3
|
+
Phase E2 introduces C support — the second decompiler. We wrap
|
|
4
|
+
:mod:`pycparser` (pure Python, no native deps) and walk the same
|
|
5
|
+
shape we accept in Python: a function declaration with a parameter
|
|
6
|
+
list and a body that evaluates a single expression returned at the
|
|
7
|
+
end.
|
|
8
|
+
|
|
9
|
+
What we accept in E2:
|
|
10
|
+
|
|
11
|
+
- ``double f(double x, double y) { return <expr>; }``
|
|
12
|
+
- ``float`` and ``double`` parameter types (mapped to ``Real``)
|
|
13
|
+
- ``int`` parameters (mapped to ``Real`` — signed-as-real)
|
|
14
|
+
- Local declarations like ``double dx = x - mu;`` → ``let`` bindings
|
|
15
|
+
- ``math.h`` calls (``exp``, ``log``, ``sin``, …, ``fabs``, ``pow``)
|
|
16
|
+
- Arithmetic operators with C precedence
|
|
17
|
+
- Numeric literals (``1.5``, ``2.0``, ``1e-3``, ``42``)
|
|
18
|
+
- ``M_PI`` / ``M_E`` constants from ``math.h`` (inlined)
|
|
19
|
+
|
|
20
|
+
What we don't (yet):
|
|
21
|
+
|
|
22
|
+
- Pointers, arrays, structs (E2.5+)
|
|
23
|
+
- ``if`` / ``for`` / ``while`` (E3 — symbolic execution + unrolling)
|
|
24
|
+
- Macros (we strip ``#``-lines but don't expand them)
|
|
25
|
+
- Function calls to anything outside ``math.h``
|
|
26
|
+
|
|
27
|
+
Anything else raises :class:`EFrogError` pointing at later phases.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import re
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import List, Optional, Tuple
|
|
36
|
+
|
|
37
|
+
from pycparser import CParser, c_ast
|
|
38
|
+
|
|
39
|
+
from .python import ( # reuse the EML-side dataclasses + estimator
|
|
40
|
+
DecompiledFunction,
|
|
41
|
+
DecompiledModule,
|
|
42
|
+
EFrogError,
|
|
43
|
+
_BUILTIN_FUNCS,
|
|
44
|
+
_PYTHON_TO_EML_FN,
|
|
45
|
+
_chain_order,
|
|
46
|
+
_emit_float,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── Public entry points ──────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def decompile_c_source(
|
|
54
|
+
source: str,
|
|
55
|
+
*,
|
|
56
|
+
source_path: Optional[str] = None,
|
|
57
|
+
module_name_override: Optional[str] = None,
|
|
58
|
+
) -> DecompiledModule:
|
|
59
|
+
cleaned = _strip_preprocessor(source)
|
|
60
|
+
parser = CParser()
|
|
61
|
+
try:
|
|
62
|
+
# parse() returns a FileAST. Synthetic <stdin> filename for nicer errors.
|
|
63
|
+
ast_root = parser.parse(cleaned, filename=source_path or "<stdin>")
|
|
64
|
+
except Exception as exc: # pycparser raises ParseError, base Exception is safest
|
|
65
|
+
raise EFrogError(f"C parse error: {exc}") from exc
|
|
66
|
+
|
|
67
|
+
module_name = (
|
|
68
|
+
module_name_override
|
|
69
|
+
or _module_name_from_path(source_path)
|
|
70
|
+
or "module"
|
|
71
|
+
)
|
|
72
|
+
mod = DecompiledModule(name=module_name, source_path=source_path)
|
|
73
|
+
|
|
74
|
+
for ext in ast_root.ext:
|
|
75
|
+
if isinstance(ext, c_ast.FuncDef):
|
|
76
|
+
mod.functions.append(_decompile_funcdef(ext))
|
|
77
|
+
else:
|
|
78
|
+
# Top-level decls/typedefs/forward-decls — silently skip in E2.
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
if not mod.functions:
|
|
82
|
+
raise EFrogError("no function definitions found — nothing to decompile")
|
|
83
|
+
return mod
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def decompile_c_file(path: str) -> DecompiledModule:
|
|
87
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
88
|
+
return decompile_c_source(f.read(), source_path=path)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ── Preprocessor + module-name helpers ──────────────────────────────
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
_PREPROC_LINE = re.compile(r"^\s*#.*$", re.MULTILINE)
|
|
95
|
+
# Strip `/* ... */` block comments and `// ...` line comments —
|
|
96
|
+
# pycparser does NOT accept either by design.
|
|
97
|
+
_BLOCK_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
|
|
98
|
+
_LINE_COMMENT = re.compile(r"//[^\n]*")
|
|
99
|
+
|
|
100
|
+
# Standard math.h constants we know how to inline.
|
|
101
|
+
_C_CONSTANTS = {
|
|
102
|
+
"M_PI": 3.141592653589793,
|
|
103
|
+
"M_E": 2.718281828459045,
|
|
104
|
+
"M_TAU": 6.283185307179586,
|
|
105
|
+
"M_SQRT2": 1.4142135623730951,
|
|
106
|
+
"M_LN2": 0.6931471805599453,
|
|
107
|
+
"M_LN10": 2.302585092994046,
|
|
108
|
+
"M_LOG2E": 1.4426950408889634,
|
|
109
|
+
"M_LOG10E": 0.4342944819032518,
|
|
110
|
+
"M_PI_2": 1.5707963267948966,
|
|
111
|
+
"M_PI_4": 0.7853981633974483,
|
|
112
|
+
"M_1_PI": 0.3183098861837907,
|
|
113
|
+
"M_2_PI": 0.6366197723675814,
|
|
114
|
+
"M_SQRT1_2": 0.7071067811865476,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _strip_preprocessor(source: str) -> str:
|
|
119
|
+
"""Remove `#`-lines and C comments so pycparser is happy with
|
|
120
|
+
bare C. We don't expand macros — callers that need them should
|
|
121
|
+
pre-process with ``cpp`` first."""
|
|
122
|
+
s = _BLOCK_COMMENT.sub(" ", source)
|
|
123
|
+
s = _LINE_COMMENT.sub("", s)
|
|
124
|
+
s = _PREPROC_LINE.sub("", s)
|
|
125
|
+
return s
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _module_name_from_path(path: Optional[str]) -> Optional[str]:
|
|
129
|
+
if not path:
|
|
130
|
+
return None
|
|
131
|
+
return os.path.splitext(os.path.basename(path))[0] or None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── Function decompilation ──────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _decompile_funcdef(node: c_ast.FuncDef) -> DecompiledFunction:
|
|
138
|
+
decl = node.decl
|
|
139
|
+
name = decl.name
|
|
140
|
+
func_decl = decl.type
|
|
141
|
+
if not isinstance(func_decl, c_ast.FuncDecl):
|
|
142
|
+
raise EFrogError(f"`{name}` is not a function definition")
|
|
143
|
+
|
|
144
|
+
args = _decode_params(func_decl.args)
|
|
145
|
+
return_type = _emit_type(func_decl.type)
|
|
146
|
+
|
|
147
|
+
let_bindings, body_expr = _decompile_body(node.body)
|
|
148
|
+
|
|
149
|
+
chain = _chain_order(body_expr)
|
|
150
|
+
for _, e in let_bindings:
|
|
151
|
+
chain = max(chain, _chain_order(e))
|
|
152
|
+
|
|
153
|
+
return DecompiledFunction(
|
|
154
|
+
name=name,
|
|
155
|
+
args=args,
|
|
156
|
+
return_type=return_type,
|
|
157
|
+
chain_order=chain,
|
|
158
|
+
let_bindings=let_bindings,
|
|
159
|
+
body_expr=body_expr,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _decode_params(params: Optional[c_ast.ParamList]) -> List[Tuple[str, str]]:
|
|
164
|
+
if params is None or params.params is None:
|
|
165
|
+
return []
|
|
166
|
+
out: List[Tuple[str, str]] = []
|
|
167
|
+
for p in params.params:
|
|
168
|
+
# `f(void)` is represented by pycparser as a single Typename
|
|
169
|
+
# entry with an `IdentifierType(['void'])` — skip it cleanly.
|
|
170
|
+
if isinstance(p, c_ast.Typename):
|
|
171
|
+
t = p.type
|
|
172
|
+
if (
|
|
173
|
+
isinstance(t, c_ast.TypeDecl)
|
|
174
|
+
and isinstance(t.type, c_ast.IdentifierType)
|
|
175
|
+
and t.type.names == ["void"]
|
|
176
|
+
):
|
|
177
|
+
continue
|
|
178
|
+
raise EFrogError(
|
|
179
|
+
f"abstract param `{type(p).__name__}` unsupported in E2"
|
|
180
|
+
)
|
|
181
|
+
if not isinstance(p, c_ast.Decl):
|
|
182
|
+
raise EFrogError(f"unsupported param node: {type(p).__name__}")
|
|
183
|
+
out.append((p.name or "_", _emit_type(p.type)))
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _emit_type(t: c_ast.Node) -> str:
|
|
188
|
+
"""Map a C type node to the EML type name."""
|
|
189
|
+
if isinstance(t, c_ast.TypeDecl):
|
|
190
|
+
return _emit_type(t.type)
|
|
191
|
+
if isinstance(t, c_ast.IdentifierType):
|
|
192
|
+
names = " ".join(t.names)
|
|
193
|
+
return _C_TYPE_TO_EML.get(names, names)
|
|
194
|
+
if isinstance(t, c_ast.PtrDecl):
|
|
195
|
+
raise EFrogError("pointer / array params unsupported in E2")
|
|
196
|
+
raise EFrogError(f"unsupported C type: {type(t).__name__}")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
_C_TYPE_TO_EML = {
|
|
200
|
+
"double": "Real",
|
|
201
|
+
"float": "Real",
|
|
202
|
+
"long double": "Real",
|
|
203
|
+
"int": "Real",
|
|
204
|
+
"long": "Real",
|
|
205
|
+
"long long": "Real",
|
|
206
|
+
"short": "Real",
|
|
207
|
+
"signed": "Real",
|
|
208
|
+
"unsigned int": "Real",
|
|
209
|
+
"unsigned long": "Real",
|
|
210
|
+
"unsigned": "Real",
|
|
211
|
+
"void": "Unit",
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _decompile_body(
|
|
216
|
+
block: c_ast.Compound,
|
|
217
|
+
) -> Tuple[List[Tuple[str, str]], str]:
|
|
218
|
+
if block is None or block.block_items is None:
|
|
219
|
+
raise EFrogError("function body is empty")
|
|
220
|
+
|
|
221
|
+
let_bindings: List[Tuple[str, str]] = []
|
|
222
|
+
return_expr: Optional[str] = None
|
|
223
|
+
seen_return = False
|
|
224
|
+
|
|
225
|
+
for item in block.block_items:
|
|
226
|
+
if seen_return:
|
|
227
|
+
raise EFrogError("statements after return not supported")
|
|
228
|
+
|
|
229
|
+
if isinstance(item, c_ast.Decl):
|
|
230
|
+
if item.init is None:
|
|
231
|
+
raise EFrogError(f"local `{item.name}` declared without initializer")
|
|
232
|
+
let_bindings.append((item.name, _expr(item.init)))
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
if isinstance(item, c_ast.Assignment):
|
|
236
|
+
if not isinstance(item.lvalue, c_ast.ID):
|
|
237
|
+
raise EFrogError("only assignments to a name supported in E2")
|
|
238
|
+
if item.op == "=":
|
|
239
|
+
let_bindings.append((item.lvalue.name, _expr(item.rvalue)))
|
|
240
|
+
elif item.op in _COMPOUND_ASSIGN:
|
|
241
|
+
op = _COMPOUND_ASSIGN[item.op]
|
|
242
|
+
rhs = _expr(item.rvalue)
|
|
243
|
+
let_bindings.append(
|
|
244
|
+
(item.lvalue.name, f"{item.lvalue.name} {op} {rhs}")
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
raise EFrogError(f"compound assign `{item.op}` unsupported")
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
if isinstance(item, c_ast.Return):
|
|
251
|
+
if item.expr is None:
|
|
252
|
+
raise EFrogError("bare `return;` not supported")
|
|
253
|
+
return_expr = _expr(item.expr)
|
|
254
|
+
seen_return = True
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
raise EFrogError(
|
|
258
|
+
f"{type(item).__name__} not supported in E2 "
|
|
259
|
+
f"(if/for/while go to E3+)"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if return_expr is None:
|
|
263
|
+
raise EFrogError("function has no `return` statement")
|
|
264
|
+
return let_bindings, return_expr
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
_COMPOUND_ASSIGN = {
|
|
268
|
+
"+=": "+", "-=": "-", "*=": "*", "/=": "/", "%=": "%",
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ── Expression printer ──────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
_C_BIN_PREC = {
|
|
276
|
+
"+": 10, "-": 10,
|
|
277
|
+
"*": 20, "/": 20, "%": 20,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_C_BIN_OP = {
|
|
281
|
+
"+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _expr(node: c_ast.Node, parent_prec: int = 0) -> str:
|
|
286
|
+
if isinstance(node, c_ast.Constant):
|
|
287
|
+
return _emit_constant(node)
|
|
288
|
+
if isinstance(node, c_ast.ID):
|
|
289
|
+
if node.name in _C_CONSTANTS:
|
|
290
|
+
return _emit_float(_C_CONSTANTS[node.name])
|
|
291
|
+
return node.name
|
|
292
|
+
if isinstance(node, c_ast.UnaryOp):
|
|
293
|
+
if node.op == "-":
|
|
294
|
+
inner = _expr(node.expr, _C_BIN_PREC["*"])
|
|
295
|
+
return _wrap(f"-{inner}", _C_BIN_PREC["*"], parent_prec)
|
|
296
|
+
if node.op == "+":
|
|
297
|
+
return _expr(node.expr, parent_prec)
|
|
298
|
+
raise EFrogError(f"unary `{node.op}` unsupported in E2")
|
|
299
|
+
if isinstance(node, c_ast.BinaryOp):
|
|
300
|
+
if node.op not in _C_BIN_OP:
|
|
301
|
+
raise EFrogError(f"binary `{node.op}` unsupported in E2")
|
|
302
|
+
prec = _C_BIN_PREC[node.op]
|
|
303
|
+
left = _expr(node.left, prec)
|
|
304
|
+
right = _expr(node.right, prec + 1) # left-assoc
|
|
305
|
+
return _wrap(f"{left} {_C_BIN_OP[node.op]} {right}", prec, parent_prec)
|
|
306
|
+
if isinstance(node, c_ast.Cast):
|
|
307
|
+
# `(double) x` etc. — strip the cast, keep the expression.
|
|
308
|
+
return _expr(node.expr, parent_prec)
|
|
309
|
+
if isinstance(node, c_ast.FuncCall):
|
|
310
|
+
return _emit_call(node)
|
|
311
|
+
raise EFrogError(f"{type(node).__name__} unsupported in C decompiler")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _wrap(s: str, my_prec: int, parent_prec: int) -> str:
|
|
315
|
+
return f"({s})" if my_prec < parent_prec else s
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _emit_constant(node: c_ast.Constant) -> str:
|
|
319
|
+
v = node.value
|
|
320
|
+
# Strip C numeric suffixes: 1.0f, 2.5L, 42u, 1e3F …
|
|
321
|
+
cleaned = re.sub(r"[uUlLfF]+$", "", v)
|
|
322
|
+
try:
|
|
323
|
+
f = float(cleaned)
|
|
324
|
+
except ValueError as exc:
|
|
325
|
+
raise EFrogError(f"can't parse C literal {v!r}: {exc}") from exc
|
|
326
|
+
return _emit_float(f)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _emit_call(node: c_ast.FuncCall) -> str:
|
|
330
|
+
if not isinstance(node.name, c_ast.ID):
|
|
331
|
+
raise EFrogError("call through non-name target unsupported in E2")
|
|
332
|
+
name = node.name.name
|
|
333
|
+
if name not in _BUILTIN_FUNCS:
|
|
334
|
+
raise EFrogError(
|
|
335
|
+
f"call to non-math function `{name}` unsupported in E2"
|
|
336
|
+
)
|
|
337
|
+
eml_name = _PYTHON_TO_EML_FN.get(name, name)
|
|
338
|
+
args = ", ".join(_expr(a) for a in (node.args.exprs if node.args else []))
|
|
339
|
+
return f"{eml_name}({args})"
|