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.
Files changed (96) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +535 -0
  4. package/bin/ge.js +112 -0
  5. package/package.json +62 -0
  6. package/python/pyeffic/__init__.py +9 -0
  7. package/python/pyeffic/__main__.py +6 -0
  8. package/python/pyeffic/analyzer.py +464 -0
  9. package/python/pyeffic/apisurface.py +238 -0
  10. package/python/pyeffic/autoselect.py +327 -0
  11. package/python/pyeffic/backends.py +87 -0
  12. package/python/pyeffic/bench.py +233 -0
  13. package/python/pyeffic/cli.py +184 -0
  14. package/python/pyeffic/compiler.py +421 -0
  15. package/python/pyeffic/config.py +383 -0
  16. package/python/pyeffic/dartgen.py +441 -0
  17. package/python/pyeffic/deploy.py +586 -0
  18. package/python/pyeffic/diagnostics.py +194 -0
  19. package/python/pyeffic/difftest.py +424 -0
  20. package/python/pyeffic/downloader.py +307 -0
  21. package/python/pyeffic/emitters/__init__.py +11 -0
  22. package/python/pyeffic/emitters/base.py +2359 -0
  23. package/python/pyeffic/emitters/cpp.py +266 -0
  24. package/python/pyeffic/emitters/csharp.py +342 -0
  25. package/python/pyeffic/emitters/dart.py +349 -0
  26. package/python/pyeffic/emitters/go.py +388 -0
  27. package/python/pyeffic/emitters/kotlin.py +314 -0
  28. package/python/pyeffic/emitters/rust.py +314 -0
  29. package/python/pyeffic/emitters/zig.py +411 -0
  30. package/python/pyeffic/ffi.py +49 -0
  31. package/python/pyeffic/frontends/__init__.py +94 -0
  32. package/python/pyeffic/frontends/hybrid.py +709 -0
  33. package/python/pyeffic/frontends/typescript.py +965 -0
  34. package/python/pyeffic/ge_cli.py +1148 -0
  35. package/python/pyeffic/golden.py +348 -0
  36. package/python/pyeffic/idents.py +206 -0
  37. package/python/pyeffic/modules.py +220 -0
  38. package/python/pyeffic/packer.py +222 -0
  39. package/python/pyeffic/pipeline.py +797 -0
  40. package/python/pyeffic/reactgen.py +966 -0
  41. package/python/pyeffic/researcher.py +177 -0
  42. package/python/pyeffic/scaffold.py +397 -0
  43. package/python/pyeffic/stdlib.py +246 -0
  44. package/python/pyeffic/styling.py +220 -0
  45. package/python/pyeffic/templates/desktop_gui/README.md +106 -0
  46. package/python/pyeffic/templates/desktop_gui/app/__init__.py +0 -0
  47. package/python/pyeffic/templates/desktop_gui/app/core/__init__.py +0 -0
  48. package/python/pyeffic/templates/desktop_gui/app/core/add.ge.py +13 -0
  49. package/python/pyeffic/templates/desktop_gui/app/core/factorial.ge.py +20 -0
  50. package/python/pyeffic/templates/desktop_gui/app/core/fibonacci.ge.py +25 -0
  51. package/python/pyeffic/templates/desktop_gui/app/core/gcd.ge.py +19 -0
  52. package/python/pyeffic/templates/desktop_gui/app/core/is_prime.ge.py +24 -0
  53. package/python/pyeffic/templates/desktop_gui/app/core/multiply.ge.py +13 -0
  54. package/python/pyeffic/templates/desktop_gui/app/core/power.ge.py +25 -0
  55. package/python/pyeffic/templates/desktop_gui/app/main.ge.py +49 -0
  56. package/python/pyeffic/templates/desktop_gui/app/memory/__init__.py +0 -0
  57. package/python/pyeffic/templates/desktop_gui/app/memory/buffer.ge.py +26 -0
  58. package/python/pyeffic/templates/desktop_gui/app/memory/limits.ge.py +47 -0
  59. package/python/pyeffic/templates/desktop_gui/app/memory/state.ge.py +44 -0
  60. package/python/pyeffic/templates/desktop_gui/app/ui/__init__.py +0 -0
  61. package/python/pyeffic/templates/desktop_gui/app/ui/layout.ge.py +64 -0
  62. package/python/pyeffic/templates/desktop_gui/app/ui/render.ge.py +87 -0
  63. package/python/pyeffic/templates/desktop_gui/app/ui/theme.ge.py +147 -0
  64. package/python/pyeffic/templates/desktop_gui/app/ui/widgets.ge.py +105 -0
  65. package/python/pyeffic/templates/desktop_gui/desktop/__init__.py +1 -0
  66. package/python/pyeffic/templates/desktop_gui/desktop/main.ge.py +258 -0
  67. package/python/pyeffic/templates/desktop_gui/ge.toml +16 -0
  68. package/python/pyeffic/templates/desktop_gui/tests/__init__.py +0 -0
  69. package/python/pyeffic/templates/desktop_gui/tests/ge_loader.py +76 -0
  70. package/python/pyeffic/templates/desktop_gui/tests/test_app.py +173 -0
  71. package/python/pyeffic/templates/web_react/README.md +115 -0
  72. package/python/pyeffic/templates/web_react/app/__init__.py +0 -0
  73. package/python/pyeffic/templates/web_react/app/core/__init__.py +0 -0
  74. package/python/pyeffic/templates/web_react/app/core/add.ge.py +9 -0
  75. package/python/pyeffic/templates/web_react/app/core/factorial.ge.py +16 -0
  76. package/python/pyeffic/templates/web_react/app/core/fibonacci.ge.py +21 -0
  77. package/python/pyeffic/templates/web_react/app/core/is_prime.ge.py +20 -0
  78. package/python/pyeffic/templates/web_react/app/core/multiply.ge.py +9 -0
  79. package/python/pyeffic/templates/web_react/app/main.ge.py +25 -0
  80. package/python/pyeffic/templates/web_react/app/memory/__init__.py +0 -0
  81. package/python/pyeffic/templates/web_react/app/memory/buffer.ge.py +25 -0
  82. package/python/pyeffic/templates/web_react/app/memory/limits.ge.py +51 -0
  83. package/python/pyeffic/templates/web_react/ge.toml +23 -0
  84. package/python/pyeffic/templates/web_react/tests/__init__.py +0 -0
  85. package/python/pyeffic/templates/web_react/tests/ge_loader.py +68 -0
  86. package/python/pyeffic/templates/web_react/tests/test_app.py +105 -0
  87. package/python/pyeffic/templates/web_react/ui/main.ge.ui +33 -0
  88. package/python/pyeffic/templates/web_react/web/__init__.py +0 -0
  89. package/python/pyeffic/templates/web_react/web/server.ge.py +78 -0
  90. package/python/pyeffic/ts2py.py +657 -0
  91. package/python/pyeffic/typecheck.py +232 -0
  92. package/python/pyeffic/ui.py +154 -0
  93. package/python/pyeffic/ui_dsl.py +618 -0
  94. package/python/pyeffic/widgets.py +87 -0
  95. package/scripts/README.md +42 -0
  96. 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,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -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