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.
@@ -0,0 +1,472 @@
1
+ """JavaScript / TypeScript-lite → EML.
2
+
3
+ Phase E2.5 — third decompiler. Wraps :mod:`esprima` (pure Python, no
4
+ native deps) and lowers the same shape we accept in Python and C: a
5
+ function declaration (or an arrow function bound at module scope)
6
+ with a parameter list and a body that ends in a single returned
7
+ expression.
8
+
9
+ What we accept in this revision:
10
+
11
+ - ``function f(x, y) { return <expr>; }``
12
+ - ``const f = (x, y) => <expr>;`` and ``= (x) => { return ...; }``
13
+ - ``var`` / ``let`` / ``const`` local declarations → ``let`` bindings
14
+ - ``Math.exp(...)``, ``Math.sin(...)``, ``Math.PI``, etc.
15
+ - Numeric literals (``1.5``, ``2e-3``) and unary ``+``/``-``
16
+ - JS arithmetic operators with ECMAScript precedence
17
+ - ``cond ? then : else`` ternaries (lowered branch-free)
18
+
19
+ What we don't (yet):
20
+
21
+ - ``if`` / ``for`` / ``while`` / ``switch`` (E3)
22
+ - Object literals, destructuring, classes, ``this`` (E3+)
23
+ - String operations, template literals, regular expressions
24
+ - TypeScript type annotations are *tolerated* (esprima parses
25
+ plain JS) — pass ``--lang javascript`` for ``.ts`` files that
26
+ don't actually use generics or annotations.
27
+
28
+ Anything else raises :class:`EFrogError` pointing at later phases.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import os
34
+ from dataclasses import dataclass
35
+ from typing import List, Optional, Tuple
36
+
37
+ import esprima
38
+
39
+ from .python import (
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_javascript_source(
54
+ source: str,
55
+ *,
56
+ source_path: Optional[str] = None,
57
+ module_name_override: Optional[str] = None,
58
+ ) -> DecompiledModule:
59
+ try:
60
+ tree = esprima.parseScript(source, {"loc": True})
61
+ except esprima.Error as exc:
62
+ raise EFrogError(f"JavaScript parse error: {exc}") from exc
63
+
64
+ module_name = (
65
+ module_name_override
66
+ or _module_name_from_path(source_path)
67
+ or "module"
68
+ )
69
+ mod = DecompiledModule(name=module_name, source_path=source_path)
70
+
71
+ for node in tree.body:
72
+ decl = _as_function_declaration(node)
73
+ if decl is None:
74
+ # Top-level statements we don't decompile (imports, side-effects)
75
+ # are silently skipped — same convention as the Python decompiler.
76
+ continue
77
+ mod.functions.append(_decompile_function(decl, mod))
78
+
79
+ if not mod.functions:
80
+ raise EFrogError(
81
+ "no top-level function declarations found — nothing to decompile"
82
+ )
83
+ return mod
84
+
85
+
86
+ def decompile_javascript_file(path: str) -> DecompiledModule:
87
+ with open(path, "r", encoding="utf-8") as f:
88
+ return decompile_javascript_source(f.read(), source_path=path)
89
+
90
+
91
+ # ── Helpers: top-level recognition ──────────────────────────────────
92
+
93
+
94
+ @dataclass
95
+ class _FuncShape:
96
+ name: str
97
+ params: List[object]
98
+ body: object # BlockStatement or expression node
99
+ body_is_expr: bool
100
+
101
+
102
+ @dataclass
103
+ class _Ctx:
104
+ mod: DecompiledModule
105
+
106
+
107
+ def _module_name_from_path(path: Optional[str]) -> Optional[str]:
108
+ if not path:
109
+ return None
110
+ stem = os.path.splitext(os.path.basename(path))[0]
111
+ return stem or None
112
+
113
+
114
+ def _as_function_declaration(node) -> Optional[_FuncShape]:
115
+ """Return a :class:`_FuncShape` if `node` is a top-level function we
116
+ can decompile (declaration form or `const NAME = (…) => …`)."""
117
+ t = node.type
118
+ if t == "FunctionDeclaration":
119
+ return _FuncShape(
120
+ name=node.id.name,
121
+ params=node.params,
122
+ body=node.body,
123
+ body_is_expr=False,
124
+ )
125
+ if t == "VariableDeclaration":
126
+ if len(node.declarations) != 1:
127
+ return None
128
+ d = node.declarations[0]
129
+ if d.id.type != "Identifier" or d.init is None:
130
+ return None
131
+ init_t = d.init.type
132
+ if init_t == "ArrowFunctionExpression":
133
+ return _FuncShape(
134
+ name=d.id.name,
135
+ params=d.init.params,
136
+ body=d.init.body,
137
+ body_is_expr=(d.init.body.type != "BlockStatement"),
138
+ )
139
+ if init_t == "FunctionExpression":
140
+ return _FuncShape(
141
+ name=d.id.name,
142
+ params=d.init.params,
143
+ body=d.init.body,
144
+ body_is_expr=False,
145
+ )
146
+ return None
147
+
148
+
149
+ # ── Function decompilation ──────────────────────────────────────────
150
+
151
+
152
+ def _decompile_function(shape: _FuncShape, mod: DecompiledModule) -> DecompiledFunction:
153
+ args: List[Tuple[str, str]] = []
154
+ for p in shape.params:
155
+ if p.type != "Identifier":
156
+ # Default values, rest params, destructuring patterns —
157
+ # all out of scope for the math-substrate slice.
158
+ raise EFrogError(
159
+ f"unsupported param node `{p.type}` in JS decompiler"
160
+ )
161
+ args.append((p.name, "Real"))
162
+
163
+ ctx = _Ctx(mod=mod)
164
+ if shape.body_is_expr:
165
+ let_bindings: List[Tuple[str, str]] = []
166
+ body_expr = _expr(shape.body, 0, ctx)
167
+ else:
168
+ let_bindings, body_expr = _decompile_body(shape.body, ctx)
169
+
170
+ chain = _chain_order(body_expr)
171
+ for _, e in let_bindings:
172
+ chain = max(chain, _chain_order(e))
173
+
174
+ return DecompiledFunction(
175
+ name=shape.name,
176
+ args=args,
177
+ return_type="Real",
178
+ chain_order=chain,
179
+ let_bindings=let_bindings,
180
+ body_expr=body_expr,
181
+ )
182
+
183
+
184
+ def _decompile_body(block, ctx: _Ctx) -> Tuple[List[Tuple[str, str]], str]:
185
+ if block is None or block.type != "BlockStatement":
186
+ raise EFrogError("function body must be a block")
187
+
188
+ let_bindings: List[Tuple[str, str]] = []
189
+ return_expr: Optional[str] = None
190
+ seen_return = False
191
+
192
+ for stmt in block.body:
193
+ if seen_return:
194
+ raise EFrogError("statements after return not supported in JS")
195
+
196
+ t = stmt.type
197
+ if t == "VariableDeclaration":
198
+ for d in stmt.declarations:
199
+ if d.id.type != "Identifier":
200
+ raise EFrogError(
201
+ "destructured declarations unsupported in JS"
202
+ )
203
+ if d.init is None:
204
+ raise EFrogError(
205
+ f"local `{d.id.name}` declared without initializer"
206
+ )
207
+ let_bindings.append((d.id.name, _expr(d.init, 0, ctx)))
208
+ continue
209
+
210
+ if t == "ExpressionStatement":
211
+ expr = stmt.expression
212
+ if expr.type != "AssignmentExpression":
213
+ raise EFrogError(
214
+ f"top-level `{expr.type}` statement unsupported in JS"
215
+ )
216
+ if expr.left.type != "Identifier":
217
+ raise EFrogError("only assignments to a name supported in JS")
218
+ if expr.operator == "=":
219
+ let_bindings.append((expr.left.name, _expr(expr.right, 0, ctx)))
220
+ elif expr.operator in _COMPOUND_ASSIGN:
221
+ op = _COMPOUND_ASSIGN[expr.operator]
222
+ rhs = _expr(expr.right, 0, ctx)
223
+ let_bindings.append(
224
+ (expr.left.name, f"{expr.left.name} {op} {rhs}")
225
+ )
226
+ else:
227
+ raise EFrogError(
228
+ f"compound assign `{expr.operator}` unsupported in JS"
229
+ )
230
+ continue
231
+
232
+ if t == "ReturnStatement":
233
+ if stmt.argument is None:
234
+ raise EFrogError("bare `return;` not supported in JS")
235
+ return_expr = _expr(stmt.argument, 0, ctx)
236
+ seen_return = True
237
+ continue
238
+
239
+ raise EFrogError(
240
+ f"`{t}` not supported in JS decompiler "
241
+ f"(if/for/while go to E3+)"
242
+ )
243
+
244
+ if return_expr is None:
245
+ raise EFrogError("function has no `return` statement")
246
+ return let_bindings, return_expr
247
+
248
+
249
+ _COMPOUND_ASSIGN = {
250
+ "+=": "+", "-=": "-", "*=": "*", "/=": "/", "%=": "%",
251
+ }
252
+
253
+
254
+ # ── Expression printer ──────────────────────────────────────────────
255
+
256
+
257
+ # Subset of ECMA-262 binary operator precedence, restricted to what
258
+ # the math substrate accepts. Higher binds tighter.
259
+ _JS_PREC = {
260
+ "+": 10, "-": 10,
261
+ "*": 20, "/": 20, "%": 20,
262
+ "**": 30,
263
+ }
264
+
265
+ _JS_OP = {
266
+ "+": "+", "-": "-",
267
+ "*": "*", "/": "/", "%": "%",
268
+ "**": "^",
269
+ }
270
+
271
+
272
+ # JS `Math.<member>` → EML callable name. Anything not listed errors.
273
+ _MATH_MEMBER = {
274
+ "exp": "exp",
275
+ "log": "log",
276
+ "log2": "log2",
277
+ "log10": "log10",
278
+ "sin": "sin",
279
+ "cos": "cos",
280
+ "tan": "tan",
281
+ "asin": "asin",
282
+ "acos": "acos",
283
+ "atan": "atan",
284
+ "sinh": "sinh",
285
+ "cosh": "cosh",
286
+ "tanh": "tanh",
287
+ "asinh": "asinh",
288
+ "acosh": "acosh",
289
+ "atanh": "atanh",
290
+ "sqrt": "sqrt",
291
+ "pow": "pow",
292
+ "abs": "abs",
293
+ "min": "min",
294
+ "max": "max",
295
+ "floor": "floor",
296
+ "ceil": "ceil",
297
+ "round": "round",
298
+ }
299
+
300
+ _MATH_CONSTS = {
301
+ "PI": 3.141592653589793,
302
+ "E": 2.718281828459045,
303
+ "LN2": 0.6931471805599453,
304
+ "LN10": 2.302585092994046,
305
+ "LOG2E": 1.4426950408889634,
306
+ "LOG10E": 0.4342944819032518,
307
+ "SQRT2": 1.4142135623730951,
308
+ "SQRT1_2": 0.7071067811865476,
309
+ }
310
+
311
+
312
+ def _expr(node, parent_prec: int, ctx: _Ctx) -> str:
313
+ t = node.type
314
+
315
+ if t == "Literal":
316
+ return _emit_literal(node)
317
+
318
+ if t == "Identifier":
319
+ return node.name
320
+
321
+ if t == "UnaryExpression":
322
+ if not node.prefix:
323
+ raise EFrogError("postfix unary operators unsupported in JS")
324
+ if node.operator == "-":
325
+ inner = _expr(node.argument, _JS_PREC["*"], ctx)
326
+ return _wrap(f"-{inner}", _JS_PREC["*"], parent_prec)
327
+ if node.operator == "+":
328
+ return _expr(node.argument, parent_prec, ctx)
329
+ if node.operator == "!":
330
+ ctx.mod.needs_step01 = True
331
+ return f"(1.0 - {_condition_expr(node.argument, ctx)})"
332
+ raise EFrogError(f"unary `{node.operator}` unsupported in JS")
333
+
334
+ if t in ("BinaryExpression", "LogicalExpression"):
335
+ op = node.operator
336
+ if op not in _JS_OP:
337
+ raise EFrogError(f"binary `{op}` unsupported in JS")
338
+ prec = _JS_PREC[op]
339
+ # `**` is right-associative in ECMAScript; everything else left-assoc.
340
+ if op == "**":
341
+ left = _expr(node.left, prec + 1, ctx)
342
+ right = _expr(node.right, prec, ctx)
343
+ else:
344
+ left = _expr(node.left, prec, ctx)
345
+ right = _expr(node.right, prec + 1, ctx)
346
+ return _wrap(f"{left} {_JS_OP[op]} {right}", prec, parent_prec)
347
+
348
+ if t == "CallExpression":
349
+ return _emit_call(node, ctx)
350
+
351
+ if t == "MemberExpression":
352
+ const = _member_constant(node)
353
+ if const is not None:
354
+ return _emit_float(const)
355
+ raise EFrogError(
356
+ f"member access `{_describe_member(node)}` unsupported in JS"
357
+ )
358
+
359
+ if t == "ConditionalExpression":
360
+ cond = _condition_expr(node.test, ctx)
361
+ then = _expr(node.consequent, 0, ctx)
362
+ otherwise = _expr(node.alternate, 0, ctx)
363
+ ctx.mod.needs_step01 = True
364
+ return f"lerp({otherwise}, {then}, {cond})"
365
+
366
+ raise EFrogError(f"`{t}` unsupported in JS decompiler")
367
+
368
+
369
+ def _wrap(s: str, my_prec: int, parent_prec: int) -> str:
370
+ return f"({s})" if my_prec < parent_prec else s
371
+
372
+
373
+ def _emit_literal(node) -> str:
374
+ v = node.value
375
+ if isinstance(v, bool):
376
+ return "1.0" if v else "0.0"
377
+ if isinstance(v, (int, float)):
378
+ return _emit_float(float(v))
379
+ raise EFrogError(f"unsupported literal: {v!r}")
380
+
381
+
382
+ def _member_constant(node) -> Optional[float]:
383
+ if (
384
+ getattr(node, "object", None) is not None
385
+ and node.object.type == "Identifier"
386
+ and node.object.name == "Math"
387
+ and not node.computed
388
+ and node.property.type == "Identifier"
389
+ ):
390
+ return _MATH_CONSTS.get(node.property.name)
391
+ return None
392
+
393
+
394
+ def _describe_member(node) -> str:
395
+ obj = node.object
396
+ obj_name = obj.name if obj.type == "Identifier" else "<expr>"
397
+ prop = node.property
398
+ prop_name = prop.name if prop.type == "Identifier" else "<expr>"
399
+ return f"{obj_name}.{prop_name}"
400
+
401
+
402
+ def _emit_call(node, ctx: _Ctx) -> str:
403
+ name = _resolve_call_name(node.callee)
404
+ if name is None:
405
+ raise EFrogError(
406
+ f"unrecognised call target `{_describe_callee(node.callee)}`"
407
+ )
408
+ if name not in _BUILTIN_FUNCS:
409
+ raise EFrogError(
410
+ f"call to non-math function `{name}` unsupported in JS"
411
+ )
412
+ eml_name = _PYTHON_TO_EML_FN.get(name, name)
413
+ args = ", ".join(_expr(a, 0, ctx) for a in node.arguments)
414
+ return f"{eml_name}({args})"
415
+
416
+
417
+ def _resolve_call_name(callee) -> Optional[str]:
418
+ if callee.type == "Identifier":
419
+ return callee.name
420
+ if (
421
+ callee.type == "MemberExpression"
422
+ and not callee.computed
423
+ and callee.object.type == "Identifier"
424
+ and callee.object.name == "Math"
425
+ and callee.property.type == "Identifier"
426
+ ):
427
+ return _MATH_MEMBER.get(callee.property.name)
428
+ return None
429
+
430
+
431
+ def _describe_callee(callee) -> str:
432
+ if callee.type == "Identifier":
433
+ return callee.name
434
+ if callee.type == "MemberExpression":
435
+ return _describe_member(callee)
436
+ return f"<{callee.type}>"
437
+
438
+
439
+ # ── Conditional → branch-free selector ──────────────────────────────
440
+
441
+
442
+ _CMP_TEMPLATE = {
443
+ ">": "step01(({left}) - ({right}))",
444
+ ">=": "step01(({left}) - ({right}))",
445
+ "<": "step01(({right}) - ({left}))",
446
+ "<=": "step01(({right}) - ({left}))",
447
+ }
448
+
449
+
450
+ def _condition_expr(node, ctx: _Ctx) -> str:
451
+ t = node.type
452
+ if t == "BinaryExpression" and node.operator in _CMP_TEMPLATE:
453
+ ctx.mod.needs_step01 = True
454
+ left = _expr(node.left, 0, ctx)
455
+ right = _expr(node.right, 0, ctx)
456
+ return _CMP_TEMPLATE[node.operator].format(left=left, right=right)
457
+
458
+ if t == "LogicalExpression":
459
+ if node.operator == "&&":
460
+ return f"(({_condition_expr(node.left, ctx)}) * ({_condition_expr(node.right, ctx)}))"
461
+ if node.operator == "||":
462
+ inv_l = f"(1.0 - {_condition_expr(node.left, ctx)})"
463
+ inv_r = f"(1.0 - {_condition_expr(node.right, ctx)})"
464
+ return f"(1.0 - ({inv_l} * {inv_r}))"
465
+
466
+ if t == "UnaryExpression" and node.operator == "!":
467
+ return f"(1.0 - {_condition_expr(node.argument, ctx)})"
468
+
469
+ raise EFrogError(
470
+ f"can't encode JS condition `{t}` branch-free; "
471
+ f"rewrite as <, <=, >, >=, &&, ||, !"
472
+ )