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 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})"