gelang 0.1.0
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.
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/bin/ge.js +112 -0
- package/package.json +62 -0
- package/python/pyeffic/__init__.py +9 -0
- package/python/pyeffic/__main__.py +6 -0
- package/python/pyeffic/analyzer.py +464 -0
- package/python/pyeffic/apisurface.py +238 -0
- package/python/pyeffic/autoselect.py +327 -0
- package/python/pyeffic/backends.py +87 -0
- package/python/pyeffic/bench.py +233 -0
- package/python/pyeffic/cli.py +184 -0
- package/python/pyeffic/compiler.py +421 -0
- package/python/pyeffic/config.py +383 -0
- package/python/pyeffic/dartgen.py +441 -0
- package/python/pyeffic/deploy.py +586 -0
- package/python/pyeffic/diagnostics.py +194 -0
- package/python/pyeffic/difftest.py +424 -0
- package/python/pyeffic/downloader.py +307 -0
- package/python/pyeffic/emitters/__init__.py +11 -0
- package/python/pyeffic/emitters/base.py +2359 -0
- package/python/pyeffic/emitters/cpp.py +266 -0
- package/python/pyeffic/emitters/csharp.py +342 -0
- package/python/pyeffic/emitters/dart.py +349 -0
- package/python/pyeffic/emitters/go.py +388 -0
- package/python/pyeffic/emitters/kotlin.py +314 -0
- package/python/pyeffic/emitters/rust.py +314 -0
- package/python/pyeffic/emitters/zig.py +411 -0
- package/python/pyeffic/ffi.py +49 -0
- package/python/pyeffic/frontends/__init__.py +94 -0
- package/python/pyeffic/frontends/hybrid.py +709 -0
- package/python/pyeffic/frontends/typescript.py +965 -0
- package/python/pyeffic/ge_cli.py +1148 -0
- package/python/pyeffic/golden.py +348 -0
- package/python/pyeffic/idents.py +206 -0
- package/python/pyeffic/modules.py +220 -0
- package/python/pyeffic/packer.py +222 -0
- package/python/pyeffic/pipeline.py +797 -0
- package/python/pyeffic/reactgen.py +966 -0
- package/python/pyeffic/researcher.py +177 -0
- package/python/pyeffic/scaffold.py +397 -0
- package/python/pyeffic/stdlib.py +246 -0
- package/python/pyeffic/styling.py +220 -0
- package/python/pyeffic/templates/desktop_gui/README.md +106 -0
- package/python/pyeffic/templates/desktop_gui/app/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/add.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/factorial.ge.py +20 -0
- package/python/pyeffic/templates/desktop_gui/app/core/fibonacci.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/core/gcd.ge.py +19 -0
- package/python/pyeffic/templates/desktop_gui/app/core/is_prime.ge.py +24 -0
- package/python/pyeffic/templates/desktop_gui/app/core/multiply.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/power.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/main.ge.py +49 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/buffer.ge.py +26 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/limits.ge.py +47 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/state.ge.py +44 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/layout.ge.py +64 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/render.ge.py +87 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/theme.ge.py +147 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/widgets.ge.py +105 -0
- package/python/pyeffic/templates/desktop_gui/desktop/__init__.py +1 -0
- package/python/pyeffic/templates/desktop_gui/desktop/main.ge.py +258 -0
- package/python/pyeffic/templates/desktop_gui/ge.toml +16 -0
- package/python/pyeffic/templates/desktop_gui/tests/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/tests/ge_loader.py +76 -0
- package/python/pyeffic/templates/desktop_gui/tests/test_app.py +173 -0
- package/python/pyeffic/templates/web_react/README.md +115 -0
- package/python/pyeffic/templates/web_react/app/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/add.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/core/factorial.ge.py +16 -0
- package/python/pyeffic/templates/web_react/app/core/fibonacci.ge.py +21 -0
- package/python/pyeffic/templates/web_react/app/core/is_prime.ge.py +20 -0
- package/python/pyeffic/templates/web_react/app/core/multiply.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/main.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/memory/buffer.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/limits.ge.py +51 -0
- package/python/pyeffic/templates/web_react/ge.toml +23 -0
- package/python/pyeffic/templates/web_react/tests/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/tests/ge_loader.py +68 -0
- package/python/pyeffic/templates/web_react/tests/test_app.py +105 -0
- package/python/pyeffic/templates/web_react/ui/main.ge.ui +33 -0
- package/python/pyeffic/templates/web_react/web/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/web/server.ge.py +78 -0
- package/python/pyeffic/ts2py.py +657 -0
- package/python/pyeffic/typecheck.py +232 -0
- package/python/pyeffic/ui.py +154 -0
- package/python/pyeffic/ui_dsl.py +618 -0
- package/python/pyeffic/widgets.py +87 -0
- package/scripts/README.md +42 -0
- package/scripts/check-toolchains.py +85 -0
package/bin/ge.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GE launcher — finds a Python 3.10+ interpreter and runs the GE compiler.
|
|
4
|
+
*
|
|
5
|
+
* The npm package bundles the compiler source, so no `pip install` is
|
|
6
|
+
* needed. This shim exists only to locate Python and set PYTHONPATH.
|
|
7
|
+
*
|
|
8
|
+
* npx gelang build main.ge --run
|
|
9
|
+
* npx gelang doctor
|
|
10
|
+
* npx gelang create myapp --template desktop-gui -y
|
|
11
|
+
*/
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const { spawnSync } = require("child_process");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
|
|
18
|
+
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
19
|
+
const PY_SRC = path.join(PKG_ROOT, "python");
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Python discovery
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const MIN_MAJOR = 3;
|
|
26
|
+
const MIN_MINOR = 10;
|
|
27
|
+
|
|
28
|
+
function pythonCandidates() {
|
|
29
|
+
const out = [];
|
|
30
|
+
if (process.env.GE_PYTHON) out.push([process.env.GE_PYTHON, []]);
|
|
31
|
+
if (process.platform === "win32") {
|
|
32
|
+
out.push(["py", ["-3"]]);
|
|
33
|
+
out.push(["python3", []]);
|
|
34
|
+
out.push(["python", []]);
|
|
35
|
+
} else {
|
|
36
|
+
out.push(["python3", []]);
|
|
37
|
+
out.push(["python", []]);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function probe(cmd, preArgs) {
|
|
43
|
+
const r = spawnSync(cmd, [...preArgs, "-c",
|
|
44
|
+
"import sys;print('%d.%d'%sys.version_info[:2])"],
|
|
45
|
+
{ encoding: "utf8" });
|
|
46
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
47
|
+
const m = r.stdout.trim().match(/^(\d+)\.(\d+)$/);
|
|
48
|
+
if (!m) return null;
|
|
49
|
+
return { cmd, preArgs, major: +m[1], minor: +m[2] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findPython() {
|
|
53
|
+
for (const [cmd, preArgs] of pythonCandidates()) {
|
|
54
|
+
const info = probe(cmd, preArgs);
|
|
55
|
+
if (!info) continue;
|
|
56
|
+
const ok = info.major > MIN_MAJOR ||
|
|
57
|
+
(info.major === MIN_MAJOR && info.minor >= MIN_MINOR);
|
|
58
|
+
if (ok) return info;
|
|
59
|
+
// remember the first too-old interpreter so the error can be specific
|
|
60
|
+
if (!findPython._tooOld) findPython._tooOld = info;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Main
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
function main() {
|
|
70
|
+
if (!fs.existsSync(path.join(PY_SRC, "pyeffic"))) {
|
|
71
|
+
console.error(
|
|
72
|
+
"GE: bundled compiler source is missing from this install.\n" +
|
|
73
|
+
" Expected: " + path.join(PY_SRC, "pyeffic") + "\n" +
|
|
74
|
+
" Reinstall the package, or run `npm rebuild gelang`."
|
|
75
|
+
);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const py = findPython();
|
|
80
|
+
if (!py) {
|
|
81
|
+
const old = findPython._tooOld;
|
|
82
|
+
console.error(
|
|
83
|
+
"\nGE needs Python " + MIN_MAJOR + "." + MIN_MINOR + " or newer.\n" +
|
|
84
|
+
(old
|
|
85
|
+
? " Found: " + old.cmd + " (" + old.major + "." + old.minor + ")\n"
|
|
86
|
+
: " Found: nothing on PATH\n") +
|
|
87
|
+
"\nInstall Python:\n" +
|
|
88
|
+
" Windows https://www.python.org/downloads/ (or: winget install Python.Python.3.12)\n" +
|
|
89
|
+
" macOS brew install python@3.12\n" +
|
|
90
|
+
" Debian sudo apt install python3\n" +
|
|
91
|
+
"\nThen run: npx gelang doctor\n"
|
|
92
|
+
);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const env = { ...process.env };
|
|
97
|
+
env.PYTHONPATH = env.PYTHONPATH
|
|
98
|
+
? PY_SRC + path.delimiter + env.PYTHONPATH
|
|
99
|
+
: PY_SRC;
|
|
100
|
+
// keep the user's own bytecode cache out of the package directory
|
|
101
|
+
env.PYTHONDONTWRITEBYTECODE = env.PYTHONDONTWRITEBYTECODE || "1";
|
|
102
|
+
|
|
103
|
+
const args = [...py.preArgs, "-m", "pyeffic.ge_cli", ...process.argv.slice(2)];
|
|
104
|
+
const r = spawnSync(py.cmd, args, { stdio: "inherit", env });
|
|
105
|
+
if (r.error) {
|
|
106
|
+
console.error("GE: failed to start Python: " + r.error.message);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
return r.status === null ? 1 : r.status;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
process.exit(main());
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gelang",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One typed source language, compiled to Rust, C++, C#, Zig, Go, or Kotlin — chosen per function. Generates Flutter and React UIs from one DSL.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ge",
|
|
7
|
+
"gelang",
|
|
8
|
+
"transpiler",
|
|
9
|
+
"compiler",
|
|
10
|
+
"polyglot",
|
|
11
|
+
"rust",
|
|
12
|
+
"cpp",
|
|
13
|
+
"csharp",
|
|
14
|
+
"zig",
|
|
15
|
+
"golang",
|
|
16
|
+
"kotlin",
|
|
17
|
+
"flutter",
|
|
18
|
+
"react",
|
|
19
|
+
"typescript",
|
|
20
|
+
"python"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://github.com/Zrald1/gelang",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/Zrald1/gelang.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/Zrald1/gelang/issues"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"author": "Zrald",
|
|
32
|
+
"bin": {
|
|
33
|
+
"ge": "bin/ge.js",
|
|
34
|
+
"gelang": "bin/ge.js"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"bin/",
|
|
38
|
+
"python/",
|
|
39
|
+
"scripts/check-toolchains.py",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"CHANGELOG.md"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"prepublishOnly": "node scripts/build-npm.js",
|
|
46
|
+
"build": "node scripts/build-npm.js",
|
|
47
|
+
"doctor": "node bin/ge.js doctor",
|
|
48
|
+
"test": "node scripts/test.js"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=16"
|
|
52
|
+
},
|
|
53
|
+
"os": [
|
|
54
|
+
"darwin",
|
|
55
|
+
"linux",
|
|
56
|
+
"win32"
|
|
57
|
+
],
|
|
58
|
+
"cpu": [
|
|
59
|
+
"x64",
|
|
60
|
+
"arm64"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""pyeffic - Python to C++/Rust efficiency-driven transpiler.
|
|
2
|
+
|
|
3
|
+
Write Python. pyeffic analyzes each function, researches whether C++ or Rust
|
|
4
|
+
is the better backend for that workload, emits native code, compiles it, and
|
|
5
|
+
runs it. Unsupported Python constructs fall back to embedded CPython so the
|
|
6
|
+
full language is always covered (just not always accelerated).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""Parse Python source into typed function units and classify features.
|
|
2
|
+
|
|
3
|
+
Uses Python's ast module. Each top-level function (and methods of classes that
|
|
4
|
+
look like plain data containers) becomes a `FuncUnit` with:
|
|
5
|
+
- typed signature (from PEP 484 hints; untyped params default to f64)
|
|
6
|
+
- a feature profile used by the researcher to pick a backend
|
|
7
|
+
- a `supported` flag: True if it lives in the transpilable subset.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
# Types we understand. Anything else -> unsupported (falls back to CPython).
|
|
15
|
+
SCALAR_TYPES = {"int", "float", "bool", "str"}
|
|
16
|
+
CONTAINER_TYPES = {"list", "tuple", "range", "dict", "set"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class FuncUnit:
|
|
21
|
+
name: str
|
|
22
|
+
lineno: int
|
|
23
|
+
params: list[tuple[str, str]] # (name, pytype) pytype is normalized
|
|
24
|
+
ret_type: str
|
|
25
|
+
features: set[str] = field(default_factory=set)
|
|
26
|
+
supported: bool = True
|
|
27
|
+
unsupported_reasons: list[str] = field(default_factory=list)
|
|
28
|
+
body: ast.FunctionDef | None = None
|
|
29
|
+
source: str = ""
|
|
30
|
+
backend: str = "rust" # filled in by researcher
|
|
31
|
+
emitted: str = "" # filled in by emitter
|
|
32
|
+
ffi_export: bool = False # filled in by ffi.tag_ffi
|
|
33
|
+
forced_backend: str | None = None # set by @rust/@cpp/@dart decorator
|
|
34
|
+
class_name: str = "" # set for methods: the owning class name
|
|
35
|
+
is_method: bool = False # True if this is a class method
|
|
36
|
+
# Production: track container element types for heterogeneous containers
|
|
37
|
+
# e.g. {"vals": "str"} means vals is list[str] -> element type is str
|
|
38
|
+
param_elem_types: dict[str, str] = field(default_factory=dict)
|
|
39
|
+
# dict key/value types: {"d": ("str", "int")} means d is dict[str, int]
|
|
40
|
+
param_dict_types: dict[str, tuple[str, str]] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
def mark_unsupported(self, reason: str) -> None:
|
|
43
|
+
self.supported = False
|
|
44
|
+
self.unsupported_reasons.append(reason)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ClassUnit:
|
|
49
|
+
"""A class definition collected from source."""
|
|
50
|
+
name: str
|
|
51
|
+
lineno: int
|
|
52
|
+
fields: list[tuple[str, str]] = field(default_factory=list) # (name, type)
|
|
53
|
+
methods: list[str] = field(default_factory=list) # method names
|
|
54
|
+
constructor_params: list[tuple[str, str]] = field(default_factory=list)
|
|
55
|
+
source: str = ""
|
|
56
|
+
bases: list[str] = field(default_factory=list) # parent class names
|
|
57
|
+
properties: list[str] = field(default_factory=list) # @property method names
|
|
58
|
+
static_methods: list[str] = field(default_factory=list) # @staticmethod names
|
|
59
|
+
class_methods: list[str] = field(default_factory=list) # @classmethod names
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _norm_type(node: ast.AST | None) -> str:
|
|
63
|
+
"""Normalize an annotation node to a simple type string we track."""
|
|
64
|
+
if node is None:
|
|
65
|
+
return "int" # default for untyped numerics
|
|
66
|
+
if isinstance(node, ast.Name):
|
|
67
|
+
return node.id
|
|
68
|
+
if isinstance(node, ast.Constant) and node.value is None:
|
|
69
|
+
return "None"
|
|
70
|
+
if isinstance(node, ast.Subscript):
|
|
71
|
+
# list[int], list[float], list[str], etc.
|
|
72
|
+
base = _norm_type(node.value)
|
|
73
|
+
return base
|
|
74
|
+
if isinstance(node, ast.BinOp): # PEP 604 int | None etc.
|
|
75
|
+
return "any"
|
|
76
|
+
return "any"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _container_elem_type(node: ast.AST | None) -> str:
|
|
80
|
+
"""Extract the element type from a container annotation like list[str]."""
|
|
81
|
+
if node is None:
|
|
82
|
+
return "int"
|
|
83
|
+
if isinstance(node, ast.Subscript):
|
|
84
|
+
# list[str] -> str, dict[str, int] -> str (key type)
|
|
85
|
+
sl = node.slice
|
|
86
|
+
if isinstance(sl, ast.Tuple) and sl.elts:
|
|
87
|
+
return _norm_type(sl.elts[0])
|
|
88
|
+
return _norm_type(sl)
|
|
89
|
+
return "int"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _dict_kv_types(node: ast.AST | None) -> tuple[str, str]:
|
|
93
|
+
"""Extract key and value types from a dict annotation like dict[str, int]."""
|
|
94
|
+
if node is None:
|
|
95
|
+
return ("str", "int")
|
|
96
|
+
if isinstance(node, ast.Subscript):
|
|
97
|
+
sl = node.slice
|
|
98
|
+
if isinstance(sl, ast.Tuple) and len(sl.elts) == 2:
|
|
99
|
+
return (_norm_type(sl.elts[0]), _norm_type(sl.elts[1]))
|
|
100
|
+
if isinstance(sl, ast.Name):
|
|
101
|
+
return ("str", _norm_type(sl))
|
|
102
|
+
return ("str", "int")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _classify_features(func: FuncUnit, fn: ast.FunctionDef) -> None:
|
|
106
|
+
"""Walk the function body to record workload features and support."""
|
|
107
|
+
for node in ast.walk(fn):
|
|
108
|
+
# --- feature signals ---
|
|
109
|
+
if isinstance(node, ast.For):
|
|
110
|
+
if isinstance(node.iter, ast.Call) and getattr(node.iter.func, "id", None) == "range":
|
|
111
|
+
func.features.add("numeric_loop")
|
|
112
|
+
else:
|
|
113
|
+
func.features.add("container_iter")
|
|
114
|
+
if isinstance(node, (ast.List, ast.ListComp)):
|
|
115
|
+
func.features.add("list_alloc")
|
|
116
|
+
if isinstance(node, ast.Subscript):
|
|
117
|
+
func.features.add("indexing")
|
|
118
|
+
if isinstance(node, ast.Call):
|
|
119
|
+
fname = getattr(node.func, "id", None)
|
|
120
|
+
if fname in {"sum", "min", "max", "abs", "pow", "round"}:
|
|
121
|
+
func.features.add("math_builtin")
|
|
122
|
+
elif fname == "len":
|
|
123
|
+
func.features.add("len_builtin")
|
|
124
|
+
elif fname == "print":
|
|
125
|
+
func.features.add("io_print")
|
|
126
|
+
elif fname == "append" or (isinstance(node.func, ast.Attribute) and node.func.attr == "append"):
|
|
127
|
+
func.features.add("list_append")
|
|
128
|
+
elif fname in {"enumerate", "zip", "map", "filter", "sorted", "reversed", "any", "all"}:
|
|
129
|
+
func.features.add("iter_builtin")
|
|
130
|
+
elif fname in {"bin", "hex", "oct", "chr", "ord", "repr", "hash", "format", "divmod"}:
|
|
131
|
+
func.features.add("conv_builtin")
|
|
132
|
+
elif fname == "isinstance":
|
|
133
|
+
func.features.add("type_check")
|
|
134
|
+
elif fname == "input":
|
|
135
|
+
func.features.add("io_input")
|
|
136
|
+
elif fname == "range":
|
|
137
|
+
func.features.add("range_call")
|
|
138
|
+
elif fname in ("ge_inline", "ge_raw"):
|
|
139
|
+
func.features.add("inline_code")
|
|
140
|
+
# Security: mark that this function uses inline code injection
|
|
141
|
+
# This is a known RCE vector — callers should validate the source
|
|
142
|
+
if isinstance(node, (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod, ast.Pow, ast.FloorDiv)):
|
|
143
|
+
func.features.add("arithmetic")
|
|
144
|
+
if isinstance(node, ast.Compare):
|
|
145
|
+
func.features.add("comparison")
|
|
146
|
+
if isinstance(node, ast.While):
|
|
147
|
+
func.features.add("while_loop")
|
|
148
|
+
if isinstance(node, ast.ClassDef):
|
|
149
|
+
func.features.add("class_use")
|
|
150
|
+
|
|
151
|
+
# --- unsupported constructs ---
|
|
152
|
+
if isinstance(node, ast.Call):
|
|
153
|
+
fname = getattr(node.func, "id", None)
|
|
154
|
+
if fname in {"eval", "exec", "compile", "globals", "locals", "vars", "dir", "getattr", "setattr", "delattr", "hasattr", "type"}:
|
|
155
|
+
func.mark_unsupported(f"{fname}() dynamic call")
|
|
156
|
+
if isinstance(node, (ast.Yield, ast.YieldFrom)):
|
|
157
|
+
pass # generators are now supported (lowered to list-building functions)
|
|
158
|
+
if isinstance(node, ast.Lambda):
|
|
159
|
+
pass # lambdas are now supported by emitters
|
|
160
|
+
if isinstance(node, (ast.AsyncFunctionDef, ast.Await)):
|
|
161
|
+
func.mark_unsupported("async")
|
|
162
|
+
if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom):
|
|
163
|
+
func.mark_unsupported("imports inside function")
|
|
164
|
+
if isinstance(node, ast.Global) or isinstance(node, ast.Nonlocal):
|
|
165
|
+
func.mark_unsupported("global/nonlocal")
|
|
166
|
+
if isinstance(node, ast.With):
|
|
167
|
+
pass # with statements are now supported by emitters
|
|
168
|
+
if isinstance(node, ast.Attribute) and node.attr in {"__class__", "__dict__", "__mro__"}:
|
|
169
|
+
func.mark_unsupported("runtime introspection")
|
|
170
|
+
if isinstance(node, ast.Starred):
|
|
171
|
+
pass # *args now supported (limited)
|
|
172
|
+
# del, raise, assert, match are now supported by emitters
|
|
173
|
+
if isinstance(node, ast.Delete):
|
|
174
|
+
func.features.add("del_stmt")
|
|
175
|
+
if isinstance(node, ast.Raise):
|
|
176
|
+
func.features.add("raise_stmt")
|
|
177
|
+
if isinstance(node, ast.Assert):
|
|
178
|
+
func.features.add("assert_stmt")
|
|
179
|
+
if isinstance(node, ast.Match):
|
|
180
|
+
func.features.add("match_stmt")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def parse_source(source: str) -> list[FuncUnit]:
|
|
184
|
+
"""Parse source into a list of FuncUnits. Backward-compatible API."""
|
|
185
|
+
units, _classes = parse_source_full(source)
|
|
186
|
+
return units
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def collect_constants(source: str) -> dict[str, object]:
|
|
190
|
+
"""Extract module-level constant assignments (NAME: type = value).
|
|
191
|
+
|
|
192
|
+
Returns a dict mapping constant name to its literal value.
|
|
193
|
+
Only simple scalar constants (int, float, bool, str) are collected.
|
|
194
|
+
These are inlined into native code by the emitter.
|
|
195
|
+
"""
|
|
196
|
+
tree = ast.parse(source)
|
|
197
|
+
constants: dict[str, object] = {}
|
|
198
|
+
for node in tree.body:
|
|
199
|
+
# AnnAssign: NAME: type = value (annotated assignment)
|
|
200
|
+
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
201
|
+
if node.value is not None and isinstance(node.value, ast.Constant):
|
|
202
|
+
if isinstance(node.value.value, (int, float, bool, str)):
|
|
203
|
+
constants[node.target.id] = node.value.value
|
|
204
|
+
# Assign: NAME = value (plain assignment)
|
|
205
|
+
elif isinstance(node, ast.Assign):
|
|
206
|
+
for target in node.targets:
|
|
207
|
+
if isinstance(target, ast.Name) and isinstance(node.value, ast.Constant):
|
|
208
|
+
if isinstance(node.value.value, (int, float, bool, str)):
|
|
209
|
+
constants[target.id] = node.value.value
|
|
210
|
+
return constants
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def collect_preamble(source: str) -> dict[str, str]:
|
|
214
|
+
"""Extract module-level ge_preamble("backend", "raw code") calls.
|
|
215
|
+
|
|
216
|
+
Returns a dict mapping backend name to raw code string.
|
|
217
|
+
This code is injected at file scope, after the prelude but before
|
|
218
|
+
function definitions. Used for platform-specific code like Win32 GUI
|
|
219
|
+
that requires file-scope function definitions.
|
|
220
|
+
|
|
221
|
+
Example GE source:
|
|
222
|
+
ge_preamble("cpp", '#include <windows.h>\\nvoid win32_helper() { ... }')
|
|
223
|
+
|
|
224
|
+
The C++ emitter will insert this code after includes, before functions.
|
|
225
|
+
"""
|
|
226
|
+
tree = ast.parse(source)
|
|
227
|
+
preamble: dict[str, str] = {}
|
|
228
|
+
for node in tree.body:
|
|
229
|
+
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
|
|
230
|
+
call = node.value
|
|
231
|
+
fname = getattr(call.func, "id", None)
|
|
232
|
+
if fname == "ge_preamble" and len(call.args) >= 2:
|
|
233
|
+
if (isinstance(call.args[0], ast.Constant)
|
|
234
|
+
and isinstance(call.args[0].value, str)
|
|
235
|
+
and isinstance(call.args[1], ast.Constant)
|
|
236
|
+
and isinstance(call.args[1].value, str)):
|
|
237
|
+
backend = call.args[0].value
|
|
238
|
+
code = call.args[1].value
|
|
239
|
+
# accumulate: multiple preamble calls for same backend are concatenated
|
|
240
|
+
if backend in preamble:
|
|
241
|
+
preamble[backend] += "\n" + code
|
|
242
|
+
else:
|
|
243
|
+
preamble[backend] = code
|
|
244
|
+
return preamble
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def parse_source_full(source: str) -> tuple[list[FuncUnit], list[ClassUnit]]:
|
|
248
|
+
"""Parse source into FuncUnits and ClassUnits.
|
|
249
|
+
|
|
250
|
+
Returns (function_units, class_units) so emitters can emit struct definitions.
|
|
251
|
+
"""
|
|
252
|
+
tree = ast.parse(source)
|
|
253
|
+
units: list[FuncUnit] = []
|
|
254
|
+
classes: list[ClassUnit] = []
|
|
255
|
+
src_lines = source.splitlines()
|
|
256
|
+
|
|
257
|
+
# pre-scan: collect class names so function params can use class types
|
|
258
|
+
_class_names: set[str] = set()
|
|
259
|
+
for node in tree.body:
|
|
260
|
+
if isinstance(node, ast.ClassDef):
|
|
261
|
+
_class_names.add(node.name)
|
|
262
|
+
|
|
263
|
+
for node in tree.body:
|
|
264
|
+
if isinstance(node, ast.FunctionDef):
|
|
265
|
+
params: list[tuple[str, str]] = []
|
|
266
|
+
defaults = node.args.defaults
|
|
267
|
+
# defaults align to the last N args
|
|
268
|
+
n_defaults = len(defaults)
|
|
269
|
+
n_args = len(node.args.args)
|
|
270
|
+
param_elem_types: dict[str, str] = {}
|
|
271
|
+
param_dict_types: dict[str, tuple[str, str]] = {}
|
|
272
|
+
for i, arg in enumerate(node.args.args):
|
|
273
|
+
ptype = _norm_type(arg.annotation)
|
|
274
|
+
if ptype not in SCALAR_TYPES and ptype not in CONTAINER_TYPES and ptype not in _class_names:
|
|
275
|
+
ptype = "any"
|
|
276
|
+
# if no annotation but has a default, infer from default
|
|
277
|
+
if ptype == "any" and i >= n_args - n_defaults:
|
|
278
|
+
default_idx = i - (n_args - n_defaults)
|
|
279
|
+
d = defaults[default_idx]
|
|
280
|
+
if isinstance(d, ast.Constant):
|
|
281
|
+
if isinstance(d.value, bool):
|
|
282
|
+
ptype = "bool"
|
|
283
|
+
elif isinstance(d.value, int):
|
|
284
|
+
ptype = "int"
|
|
285
|
+
elif isinstance(d.value, float):
|
|
286
|
+
ptype = "float"
|
|
287
|
+
elif isinstance(d.value, str):
|
|
288
|
+
ptype = "str"
|
|
289
|
+
# track container element type for heterogeneous containers
|
|
290
|
+
if ptype in CONTAINER_TYPES and arg.annotation is not None:
|
|
291
|
+
elem_t = _container_elem_type(arg.annotation)
|
|
292
|
+
if elem_t not in SCALAR_TYPES and elem_t not in CONTAINER_TYPES:
|
|
293
|
+
elem_t = "int"
|
|
294
|
+
param_elem_types[arg.arg] = elem_t
|
|
295
|
+
# track dict key/value types for heterogeneous dicts
|
|
296
|
+
if ptype == "dict":
|
|
297
|
+
key_t, val_t = _dict_kv_types(arg.annotation)
|
|
298
|
+
if key_t not in SCALAR_TYPES and key_t not in CONTAINER_TYPES:
|
|
299
|
+
key_t = "str"
|
|
300
|
+
if val_t not in SCALAR_TYPES and val_t not in CONTAINER_TYPES:
|
|
301
|
+
val_t = "int"
|
|
302
|
+
param_dict_types[arg.arg] = (key_t, val_t)
|
|
303
|
+
params.append((arg.arg, ptype))
|
|
304
|
+
# handle *args (vararg) — typed as list
|
|
305
|
+
if node.args.vararg:
|
|
306
|
+
ptype = _norm_type(node.args.vararg.annotation)
|
|
307
|
+
if ptype not in SCALAR_TYPES and ptype not in CONTAINER_TYPES:
|
|
308
|
+
ptype = "list"
|
|
309
|
+
params.append((node.args.vararg.arg, ptype))
|
|
310
|
+
ret = _norm_type(node.returns)
|
|
311
|
+
if ret not in SCALAR_TYPES and ret not in CONTAINER_TYPES and ret != "None" and ret not in _class_names:
|
|
312
|
+
ret = "any"
|
|
313
|
+
unit = FuncUnit(
|
|
314
|
+
name=node.name,
|
|
315
|
+
lineno=node.lineno,
|
|
316
|
+
params=params,
|
|
317
|
+
ret_type=ret,
|
|
318
|
+
body=node,
|
|
319
|
+
source="\n".join(src_lines[node.lineno - 1 : node.end_lineno]),
|
|
320
|
+
param_elem_types=param_elem_types,
|
|
321
|
+
param_dict_types=param_dict_types,
|
|
322
|
+
)
|
|
323
|
+
# parse @rust/@cpp/@dart/@csharp/@zig/@go decorators for per-function backend selection
|
|
324
|
+
for dec in node.decorator_list:
|
|
325
|
+
dec_name = None
|
|
326
|
+
if isinstance(dec, ast.Name):
|
|
327
|
+
dec_name = dec.id
|
|
328
|
+
elif isinstance(dec, ast.Call) and isinstance(dec.func, ast.Name):
|
|
329
|
+
dec_name = dec.func.id
|
|
330
|
+
if dec_name in ("rust", "cpp", "dart", "csharp", "zig", "go", "kotlin"):
|
|
331
|
+
unit.forced_backend = dec_name
|
|
332
|
+
unit.backend = dec_name
|
|
333
|
+
if dec_name == "dart":
|
|
334
|
+
# @dart functions stay in Dart — not compiled to native
|
|
335
|
+
# but still tracked for Dart code generation
|
|
336
|
+
unit.supported = False
|
|
337
|
+
unit.unsupported_reasons = ["dart backend (stays in Dart)"]
|
|
338
|
+
# type sanity: any 'any' -> unsupported unless it's None ret
|
|
339
|
+
for _, t in params:
|
|
340
|
+
if t == "any":
|
|
341
|
+
unit.mark_unsupported("untyped/complex parameter")
|
|
342
|
+
if ret == "any":
|
|
343
|
+
unit.mark_unsupported("untyped/complex return")
|
|
344
|
+
_classify_features(unit, node)
|
|
345
|
+
if not unit.features:
|
|
346
|
+
unit.features.add("plain")
|
|
347
|
+
units.append(unit)
|
|
348
|
+
elif isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
349
|
+
# imports are fine at module level; ignored by transpiler
|
|
350
|
+
continue
|
|
351
|
+
elif isinstance(node, ast.ClassDef):
|
|
352
|
+
# collect base class names
|
|
353
|
+
base_names: list[str] = []
|
|
354
|
+
for base in node.bases:
|
|
355
|
+
if isinstance(base, ast.Name):
|
|
356
|
+
base_names.append(base.id)
|
|
357
|
+
# collect class fields (annotated assignments without value)
|
|
358
|
+
class_fields: list[tuple[str, str]] = []
|
|
359
|
+
constructor_params: list[tuple[str, str]] = []
|
|
360
|
+
property_names: list[str] = []
|
|
361
|
+
static_method_names: list[str] = []
|
|
362
|
+
class_method_names: list[str] = []
|
|
363
|
+
for item in node.body:
|
|
364
|
+
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
|
365
|
+
ft = _norm_type(item.annotation)
|
|
366
|
+
if ft not in SCALAR_TYPES and ft not in CONTAINER_TYPES:
|
|
367
|
+
ft = "any"
|
|
368
|
+
class_fields.append((item.target.id, ft))
|
|
369
|
+
# collect constructor params if __init__ exists
|
|
370
|
+
for item in node.body:
|
|
371
|
+
if isinstance(item, ast.FunctionDef) and item.name == "__init__":
|
|
372
|
+
for arg in item.args.args:
|
|
373
|
+
if arg.arg == "self":
|
|
374
|
+
continue
|
|
375
|
+
ptype = _norm_type(arg.annotation)
|
|
376
|
+
if ptype not in SCALAR_TYPES and ptype not in CONTAINER_TYPES:
|
|
377
|
+
ptype = "any"
|
|
378
|
+
constructor_params.append((arg.arg, ptype))
|
|
379
|
+
# detect @property, @staticmethod, @classmethod
|
|
380
|
+
for item in node.body:
|
|
381
|
+
if isinstance(item, ast.FunctionDef):
|
|
382
|
+
for dec in item.decorator_list:
|
|
383
|
+
dec_name = None
|
|
384
|
+
if isinstance(dec, ast.Name):
|
|
385
|
+
dec_name = dec.id
|
|
386
|
+
elif isinstance(dec, ast.Call) and isinstance(dec.func, ast.Name):
|
|
387
|
+
dec_name = dec.func.id
|
|
388
|
+
if dec_name == "property":
|
|
389
|
+
property_names.append(item.name)
|
|
390
|
+
elif dec_name == "staticmethod":
|
|
391
|
+
static_method_names.append(item.name)
|
|
392
|
+
elif dec_name == "classmethod":
|
|
393
|
+
class_method_names.append(item.name)
|
|
394
|
+
# register the class
|
|
395
|
+
cls_unit = ClassUnit(
|
|
396
|
+
name=node.name,
|
|
397
|
+
lineno=node.lineno,
|
|
398
|
+
fields=class_fields,
|
|
399
|
+
methods=[],
|
|
400
|
+
constructor_params=constructor_params,
|
|
401
|
+
source="\n".join(src_lines[node.lineno - 1 : node.end_lineno]),
|
|
402
|
+
bases=base_names,
|
|
403
|
+
properties=property_names,
|
|
404
|
+
static_methods=static_method_names,
|
|
405
|
+
class_methods=class_method_names,
|
|
406
|
+
)
|
|
407
|
+
classes.append(cls_unit)
|
|
408
|
+
# emit class methods as standalone functions with `self` param
|
|
409
|
+
for item in node.body:
|
|
410
|
+
if isinstance(item, ast.FunctionDef):
|
|
411
|
+
# skip __init__ (handled by constructor emission)
|
|
412
|
+
if item.name == "__init__":
|
|
413
|
+
continue
|
|
414
|
+
# check decorators
|
|
415
|
+
is_static = item.name in static_method_names
|
|
416
|
+
is_class = item.name in class_method_names
|
|
417
|
+
is_prop = item.name in property_names
|
|
418
|
+
cls_unit.methods.append(item.name)
|
|
419
|
+
params: list[tuple[str, str]] = []
|
|
420
|
+
if not is_static and not is_class:
|
|
421
|
+
# add self as first param (typed as the class name)
|
|
422
|
+
# use _self instead of self (self is reserved in Rust)
|
|
423
|
+
params.append(("_self", node.name))
|
|
424
|
+
elif is_class:
|
|
425
|
+
# classmethod: first param is the class
|
|
426
|
+
params.append(("_cls", node.name))
|
|
427
|
+
for arg in item.args.args:
|
|
428
|
+
if arg.arg in ("self", "cls"):
|
|
429
|
+
continue
|
|
430
|
+
ptype = _norm_type(arg.annotation)
|
|
431
|
+
if ptype not in SCALAR_TYPES and ptype not in CONTAINER_TYPES and ptype != node.name:
|
|
432
|
+
ptype = "any"
|
|
433
|
+
params.append((arg.arg, ptype))
|
|
434
|
+
ret = _norm_type(item.returns)
|
|
435
|
+
if ret not in SCALAR_TYPES and ret not in CONTAINER_TYPES and ret != "None" and ret != node.name:
|
|
436
|
+
ret = "any"
|
|
437
|
+
sub = FuncUnit(
|
|
438
|
+
name=f"{node.name}.{item.name}",
|
|
439
|
+
lineno=item.lineno,
|
|
440
|
+
params=params,
|
|
441
|
+
ret_type=ret,
|
|
442
|
+
body=item,
|
|
443
|
+
source="\n".join(src_lines[item.lineno - 1 : item.end_lineno]),
|
|
444
|
+
class_name=node.name,
|
|
445
|
+
is_method=True,
|
|
446
|
+
)
|
|
447
|
+
_classify_features(sub, item)
|
|
448
|
+
if not sub.features:
|
|
449
|
+
sub.features.add("plain")
|
|
450
|
+
units.append(sub)
|
|
451
|
+
else:
|
|
452
|
+
# module-level code that isn't a function -> unsupported, runs in CPython
|
|
453
|
+
units.append(
|
|
454
|
+
FuncUnit(
|
|
455
|
+
name=f"<module@{getattr(node, 'lineno', 0)}>",
|
|
456
|
+
lineno=getattr(node, "lineno", 0),
|
|
457
|
+
params=[],
|
|
458
|
+
ret_type="None",
|
|
459
|
+
supported=False,
|
|
460
|
+
unsupported_reasons=["module-level non-function code"],
|
|
461
|
+
source=ast.get_source_segment(source, node) or "",
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
return units, classes
|