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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Zig backend spec + program assembly.
|
|
2
|
+
|
|
3
|
+
Zig compiles to native code with no runtime. Exports C ABI functions with
|
|
4
|
+
the `export` keyword. Interops seamlessly with C/C++/Rust via the C ABI.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
|
|
10
|
+
from .base import Emitter, Spec
|
|
11
|
+
from ..analyzer import FuncUnit
|
|
12
|
+
|
|
13
|
+
SPEC = Spec(
|
|
14
|
+
name="zig",
|
|
15
|
+
types={"int": "i64", "float": "f64", "bool": "bool", "str": "[]const u8", "None": "void"},
|
|
16
|
+
list_type="[]const i64",
|
|
17
|
+
list_param_type="[]const i64",
|
|
18
|
+
list_elem_type="i64",
|
|
19
|
+
borrow_list_arg=False,
|
|
20
|
+
range_call="({lo}..{hi})",
|
|
21
|
+
range_step_call="({lo}..{hi})",
|
|
22
|
+
len_call="@as(i64, @intCast({x}.len))",
|
|
23
|
+
list_len="@as(i64, @intCast({x}.len))",
|
|
24
|
+
print_int='std.io.getStdOut().writer().print("{{d}}\\n", .{{{v}}}) catch unreachable',
|
|
25
|
+
print_float='std.io.getStdOut().writer().print("{{d}}\\n", .{{{v}}}) catch unreachable',
|
|
26
|
+
print_str='std.io.getStdOut().writer().print("{{s}}\\n", .{{{v}}}) catch unreachable',
|
|
27
|
+
print_bool='std.io.getStdOut().writer().print("{}\\n", .{{{v}}}) catch unreachable',
|
|
28
|
+
print_generic='std.io.getStdOut().writer().print("{{any}}\\n", .{{{v}}}) catch unreachable',
|
|
29
|
+
int_cast="@as(i64, {x})",
|
|
30
|
+
float_cast="@as(f64, {x})",
|
|
31
|
+
float_div="(@as(f64, {l}) / @as(f64, {r}))",
|
|
32
|
+
floor_div="@divFloor({l}, {r})",
|
|
33
|
+
sum_call="blk: {{ var s: i64 = 0; for ({it}) |v| {{ s += v; }} break :blk s; }}",
|
|
34
|
+
abs_int="@intCast(if ({x} < 0) -{x} else {x})",
|
|
35
|
+
abs_float="@abs({x})",
|
|
36
|
+
min_call="@min({it}...)",
|
|
37
|
+
max_call="@max({it}...)",
|
|
38
|
+
pow_call="std.math.pow(i64, {l}, {r})",
|
|
39
|
+
append_call="{x}.append({v}) catch unreachable",
|
|
40
|
+
index_call="{x}[@as(usize, @intCast({i}))]",
|
|
41
|
+
comment="//",
|
|
42
|
+
fn_template="{sig} {{\n{body}\n}}",
|
|
43
|
+
main_template="",
|
|
44
|
+
var_decl_template="var {target}: {nt} = {val};",
|
|
45
|
+
ffi_prefix="export fn ",
|
|
46
|
+
indent=" ",
|
|
47
|
+
str_concat="{l} ++ {r}",
|
|
48
|
+
str_len="{x}.len",
|
|
49
|
+
str_index="{x}[{i}]",
|
|
50
|
+
str_slice="{x}[{start}..{end}]",
|
|
51
|
+
str_slice_start="{x}[{start}..]",
|
|
52
|
+
str_slice_end="{x}[..{end}]",
|
|
53
|
+
list_concat="blk: {{ var t = std.ArrayList(i64).init(std.heap.page_allocator); t.appendSlice({l}) catch unreachable; t.appendSlice({r}) catch unreachable; break :blk t.toOwnedSlice() catch unreachable; }}",
|
|
54
|
+
foreach_template="for ({iter}) |{var}|",
|
|
55
|
+
try_template="// try/except limited in Zig\n// {body}\n// {handler}",
|
|
56
|
+
struct_template="const {name} = struct {{\n{fields}\n}};",
|
|
57
|
+
struct_field_template=" {type}: {name},",
|
|
58
|
+
struct_new_template="fn {name}_new({params}) {name} {{\n{body}\n}}",
|
|
59
|
+
dict_type="std.StringHashMap({V})",
|
|
60
|
+
dict_get="{d}.get({k}).?",
|
|
61
|
+
dict_set="{d}.put({k}, {v}) catch unreachable",
|
|
62
|
+
dict_contains="{d}.contains({k})",
|
|
63
|
+
tuple_type="[2]{T}",
|
|
64
|
+
tuple_get="{t}[{i}]",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ZigEmitter(Emitter):
|
|
69
|
+
def __init__(self, spec):
|
|
70
|
+
super().__init__(spec)
|
|
71
|
+
self.mutated: set[str] = set()
|
|
72
|
+
|
|
73
|
+
def _try_stmt(self, node, ind):
|
|
74
|
+
"""Zig doesn't have try/catch — emit body directly, comment handler."""
|
|
75
|
+
self.lines.append(f"{ind}// try/except: Zig has no exceptions")
|
|
76
|
+
for s in node.body:
|
|
77
|
+
self.stmt(s)
|
|
78
|
+
if node.handlers:
|
|
79
|
+
self.lines.append(f"{ind}// except handler (not emitted in Zig):")
|
|
80
|
+
for s in node.handlers[0].body:
|
|
81
|
+
save = self.lines
|
|
82
|
+
self.lines = []
|
|
83
|
+
self.stmt(s)
|
|
84
|
+
for line in self.lines:
|
|
85
|
+
save.append(f"{ind}// {line.strip()}")
|
|
86
|
+
self.lines = save
|
|
87
|
+
if node.finalbody:
|
|
88
|
+
for s in node.finalbody:
|
|
89
|
+
self.stmt(s)
|
|
90
|
+
|
|
91
|
+
def expr(self, node): # type: ignore[override]
|
|
92
|
+
# Zig requires @rem for signed integer modulo
|
|
93
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mod):
|
|
94
|
+
left = self.expr(node.left)
|
|
95
|
+
right = self.expr(node.right)
|
|
96
|
+
return f"@rem({left}, {right})"
|
|
97
|
+
return super().expr(node)
|
|
98
|
+
|
|
99
|
+
def _find_mutated_vars(self, body: list) -> set[str]:
|
|
100
|
+
"""Scan AST body for variables that are mutated (AugAssign, re-assign, append, subscript assign)."""
|
|
101
|
+
mutated: set[str] = set()
|
|
102
|
+
for node in ast.walk(ast.Module(body=body, type_ignores=[])):
|
|
103
|
+
if isinstance(node, ast.AugAssign):
|
|
104
|
+
if isinstance(node.target, ast.Name):
|
|
105
|
+
mutated.add(node.target.id)
|
|
106
|
+
elif isinstance(node, ast.Assign):
|
|
107
|
+
for t in node.targets:
|
|
108
|
+
if isinstance(t, ast.Name):
|
|
109
|
+
mutated.add(t.id)
|
|
110
|
+
elif isinstance(t, ast.Subscript) and isinstance(t.value, ast.Name):
|
|
111
|
+
mutated.add(t.value.id)
|
|
112
|
+
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
|
|
113
|
+
if node.func.attr == "append" and isinstance(node.func.value, ast.Name):
|
|
114
|
+
mutated.add(node.func.value.id)
|
|
115
|
+
return mutated
|
|
116
|
+
|
|
117
|
+
def emit(self, unit: FuncUnit) -> str:
|
|
118
|
+
"""Override emit to track mutated variables and make mutable param copies."""
|
|
119
|
+
self.mutated = self._find_mutated_vars(unit.body)
|
|
120
|
+
# Detect mutated list parameters (append, subscript assign, etc.)
|
|
121
|
+
self.mutated_list_params: set[str] = set()
|
|
122
|
+
for pname, ptype in unit.params:
|
|
123
|
+
if ptype == "list" and pname in self.mutated:
|
|
124
|
+
self.mutated_list_params.add(pname)
|
|
125
|
+
# Zig parameters are const — make mutable copies for any scalar param that's mutated
|
|
126
|
+
# List params that are mutated need ArrayList conversion
|
|
127
|
+
mutated_params = []
|
|
128
|
+
for pname, ptype in unit.params:
|
|
129
|
+
if pname in self.mutated and ptype != "list":
|
|
130
|
+
mutated_params.append(pname)
|
|
131
|
+
# We'll insert mutable copies as the first lines of the body
|
|
132
|
+
self._zig_mut_copies = mutated_params
|
|
133
|
+
self._zig_list_conversions = list(self.mutated_list_params)
|
|
134
|
+
result = super().emit(unit)
|
|
135
|
+
if mutated_params or self.mutated_list_params:
|
|
136
|
+
# Insert mutable copies and list conversions right after the opening brace
|
|
137
|
+
lines = result.split("\n")
|
|
138
|
+
insert_lines = []
|
|
139
|
+
for pname in mutated_params:
|
|
140
|
+
insert_lines.append(f" var {pname}_ = {pname};")
|
|
141
|
+
for pname in self.mutated_list_params:
|
|
142
|
+
# Convert []const i64 to ArrayList for mutation
|
|
143
|
+
insert_lines.append(f" var {pname}_list = std.ArrayList(i64).init(std.heap.page_allocator);")
|
|
144
|
+
insert_lines.append(f" for ({pname}) |item| {{ {pname}_list.append(item) catch unreachable; }}")
|
|
145
|
+
for i, line in enumerate(lines):
|
|
146
|
+
if "{" in line and "fn" in lines[i]:
|
|
147
|
+
for j, il in enumerate(insert_lines):
|
|
148
|
+
lines.insert(i + 1 + j, il)
|
|
149
|
+
break
|
|
150
|
+
result = "\n".join(lines)
|
|
151
|
+
# Replace param references with mutable version
|
|
152
|
+
import re
|
|
153
|
+
for pname in mutated_params:
|
|
154
|
+
old = result
|
|
155
|
+
result = re.sub(r'\b' + pname + r'\b', pname + "_", result)
|
|
156
|
+
result = re.sub(r'\b' + pname + r'_: ', pname + ': ', result, count=1)
|
|
157
|
+
result = result.replace(f"var {pname}_ = {pname}_;", f"var {pname}_ = {pname};")
|
|
158
|
+
# Replace list param references with ArrayList.items where indexing, or the list itself for append
|
|
159
|
+
for pname in self.mutated_list_params:
|
|
160
|
+
# Replace append calls: pname.append(v) -> pname_list.append(v)
|
|
161
|
+
result = re.sub(
|
|
162
|
+
r'\b' + pname + r'\.append\(',
|
|
163
|
+
pname + '_list.append(',
|
|
164
|
+
result
|
|
165
|
+
)
|
|
166
|
+
# Replace indexing: pname[i] -> pname_list.items[i]
|
|
167
|
+
result = re.sub(
|
|
168
|
+
r'\b' + pname + r'\[',
|
|
169
|
+
pname + '_list.items[',
|
|
170
|
+
result
|
|
171
|
+
)
|
|
172
|
+
# Replace len(pname) -> pname_list.items.len
|
|
173
|
+
result = re.sub(
|
|
174
|
+
r'\b' + pname + r'\.len\b',
|
|
175
|
+
pname + '_list.items.len',
|
|
176
|
+
result
|
|
177
|
+
)
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
def _is_mutated(self, var: str) -> bool:
|
|
181
|
+
return var in self.mutated
|
|
182
|
+
|
|
183
|
+
def _decl_keyword(self, var: str) -> str:
|
|
184
|
+
"""Return 'var' for mutated variables, 'const' for unmutated."""
|
|
185
|
+
return "var" if self._is_mutated(var) else "const"
|
|
186
|
+
|
|
187
|
+
def stmt(self, node): # type: ignore[override]
|
|
188
|
+
"""Override to use const/var based on mutation tracking."""
|
|
189
|
+
ind = self.spec.indent * self.indent_lvl
|
|
190
|
+
# Zig requires non-void return values to be used or discarded with `_ = `
|
|
191
|
+
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
|
|
192
|
+
fname = getattr(node.value.func, "id", None)
|
|
193
|
+
if fname != "print":
|
|
194
|
+
self.lines.append(f"{ind}_ = {self.expr(node.value)};")
|
|
195
|
+
return
|
|
196
|
+
if isinstance(node, ast.AnnAssign) and node.value is not None:
|
|
197
|
+
target = node.target.id if isinstance(node.target, ast.Name) else self.expr(node.target)
|
|
198
|
+
t = self._ann_type(node.annotation) if isinstance(node.target, ast.Name) else self.infer_type(node.value)
|
|
199
|
+
if isinstance(node.target, ast.Name):
|
|
200
|
+
self.var_types[target] = t
|
|
201
|
+
self.declared.add(target)
|
|
202
|
+
nt = self.py_to_native(t)
|
|
203
|
+
kw = self._decl_keyword(target)
|
|
204
|
+
self.lines.append(f"{ind}{kw} {target}: {nt} = {self.expr(node.value)};")
|
|
205
|
+
return
|
|
206
|
+
if isinstance(node, ast.Assign):
|
|
207
|
+
val = self.expr(node.value)
|
|
208
|
+
for target in node.targets:
|
|
209
|
+
if isinstance(target, ast.Name):
|
|
210
|
+
t = self.infer_type(node.value)
|
|
211
|
+
self.var_types[target.id] = t
|
|
212
|
+
nt = self.py_to_native(t)
|
|
213
|
+
if target.id in self.declared:
|
|
214
|
+
self.lines.append(f"{ind}{target.id} = {val};")
|
|
215
|
+
else:
|
|
216
|
+
self.declared.add(target.id)
|
|
217
|
+
kw = self._decl_keyword(target.id)
|
|
218
|
+
self.lines.append(f"{ind}{kw} {target.id}: {nt} = {val};")
|
|
219
|
+
elif isinstance(target, ast.Subscript):
|
|
220
|
+
target_type = self.infer_type(target.value)
|
|
221
|
+
if target_type == "dict":
|
|
222
|
+
key = self.expr(target.slice)
|
|
223
|
+
d = self.expr(target.value)
|
|
224
|
+
self.lines.append(f"{ind}{self.spec.dict_set.format(d=d, k=key, v=val)};")
|
|
225
|
+
else:
|
|
226
|
+
self.lines.append(f"{ind}{self.expr(target)} = {val};")
|
|
227
|
+
else:
|
|
228
|
+
self.lines.append(f"{ind}// unsupported assign target")
|
|
229
|
+
return
|
|
230
|
+
super().stmt(node)
|
|
231
|
+
|
|
232
|
+
def for_loop(self, node, ind): # type: ignore[override]
|
|
233
|
+
var = node.target.id if isinstance(node.target, ast.Name) else "_"
|
|
234
|
+
it = node.iter
|
|
235
|
+
if isinstance(it, ast.Call) and getattr(it.func, "id", None) == "range":
|
|
236
|
+
args = it.args
|
|
237
|
+
if len(args) == 1:
|
|
238
|
+
lo, hi = "0", self.expr(args[0])
|
|
239
|
+
else:
|
|
240
|
+
lo, hi = self.expr(args[0]), self.expr(args[1])
|
|
241
|
+
self.var_types[var] = "int"
|
|
242
|
+
# loop variables are always mutated (incremented)
|
|
243
|
+
self.lines.append(f"{ind}var {var}: i64 = {lo};")
|
|
244
|
+
self.lines.append(f"{ind}while ({var} < {hi}) : ({var} += 1) {{")
|
|
245
|
+
else:
|
|
246
|
+
iter_s = self.expr(it)
|
|
247
|
+
self.var_types[var] = "long"
|
|
248
|
+
self.lines.append(f"{ind}for ({iter_s}) |{var}| {{")
|
|
249
|
+
self.indent_lvl += 1
|
|
250
|
+
for s in node.body:
|
|
251
|
+
self.stmt(s)
|
|
252
|
+
self.indent_lvl -= 1
|
|
253
|
+
self.lines.append(f"{ind}}}")
|
|
254
|
+
|
|
255
|
+
def signature(self, unit: FuncUnit) -> str:
|
|
256
|
+
emit_name = unit.name.replace(".", "_")
|
|
257
|
+
params = []
|
|
258
|
+
for pname, ptype in unit.params:
|
|
259
|
+
nt = self.param_native_type(ptype)
|
|
260
|
+
params.append(f"{pname}: {nt}")
|
|
261
|
+
ret = self.py_to_native(unit.ret_type) if unit.ret_type != "None" else "void"
|
|
262
|
+
param_str = ", ".join(params)
|
|
263
|
+
if ret == "void":
|
|
264
|
+
return f"fn {emit_name}({param_str}) void"
|
|
265
|
+
return f"fn {emit_name}({param_str}) {ret}"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _entry_unit(units: list[FuncUnit], entry: str | None) -> FuncUnit:
|
|
269
|
+
if entry:
|
|
270
|
+
for u in units:
|
|
271
|
+
if u.name == entry:
|
|
272
|
+
return u
|
|
273
|
+
for u in units:
|
|
274
|
+
if u.name == "main":
|
|
275
|
+
return u
|
|
276
|
+
return units[0]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _emit_structs_zig(classes: list) -> str:
|
|
280
|
+
"""Emit Zig struct definitions and constructors for ClassUnits."""
|
|
281
|
+
out: list[str] = []
|
|
282
|
+
for cls in classes:
|
|
283
|
+
fields_str = ""
|
|
284
|
+
for fname, ftype in cls.fields:
|
|
285
|
+
nt = SPEC.types.get(ftype, SPEC.types.get("float"))
|
|
286
|
+
if ftype == "list":
|
|
287
|
+
nt = "[]const i64"
|
|
288
|
+
fields_str += f" {fname}: {nt},\n"
|
|
289
|
+
out.append(f"const {cls.name} = struct {{\n{fields_str}}};\n")
|
|
290
|
+
if cls.constructor_params:
|
|
291
|
+
params_str = ", ".join(
|
|
292
|
+
f"{pname}: {SPEC.types.get(ptype, 'f64')}"
|
|
293
|
+
for pname, ptype in cls.constructor_params
|
|
294
|
+
)
|
|
295
|
+
init_lines = "\n".join(
|
|
296
|
+
f" .{fname} = {fname}," for fname, _ in cls.constructor_params
|
|
297
|
+
)
|
|
298
|
+
out.append(f"fn {cls.name}_new({params_str}) {cls.name} {{\n"
|
|
299
|
+
f" return .{{\n{init_lines}\n }};\n}}\n")
|
|
300
|
+
else:
|
|
301
|
+
out.append(f"fn {cls.name}_new() {cls.name} {{\n"
|
|
302
|
+
f" return .{{}};\n}}\n")
|
|
303
|
+
return "\n".join(out)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def emit_zig(units: list[FuncUnit], entry: str | None,
|
|
307
|
+
library_mode: bool = False,
|
|
308
|
+
extern_fns: list[FuncUnit] | None = None,
|
|
309
|
+
classes: list | None = None,
|
|
310
|
+
constants: dict | None = None,
|
|
311
|
+
preamble: dict | None = None) -> tuple[str, dict[str, str]]:
|
|
312
|
+
"""Return (full_program_source, {func_name: emitted_source}).
|
|
313
|
+
|
|
314
|
+
In library_mode: emit `export fn` for C ABI FFI.
|
|
315
|
+
extern_fns: functions from other backends that this Zig code calls.
|
|
316
|
+
"""
|
|
317
|
+
emitter = ZigEmitter(SPEC)
|
|
318
|
+
emitter.library_mode = library_mode
|
|
319
|
+
emitter.constants = constants or {}
|
|
320
|
+
emitted: dict[str, str] = {}
|
|
321
|
+
fns: list[str] = []
|
|
322
|
+
|
|
323
|
+
# register class names
|
|
324
|
+
if classes:
|
|
325
|
+
for cls in classes:
|
|
326
|
+
emitter.class_names.add(cls.name)
|
|
327
|
+
emitter.class_fields[cls.name] = cls.fields
|
|
328
|
+
emitter.class_bases[cls.name] = cls.bases
|
|
329
|
+
emitter.class_properties[cls.name] = cls.properties
|
|
330
|
+
emitter.class_static_methods[cls.name] = cls.static_methods
|
|
331
|
+
|
|
332
|
+
prelude = (
|
|
333
|
+
'const std = @import("std");\n\n'
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# emit extern declarations for cross-backend calls
|
|
337
|
+
if extern_fns:
|
|
338
|
+
decls = []
|
|
339
|
+
for u in extern_fns:
|
|
340
|
+
if not u.supported:
|
|
341
|
+
continue
|
|
342
|
+
params = ", ".join(
|
|
343
|
+
f"{'[]const i64' if ptype == 'list' else SPEC.types.get(ptype, 'f64')} {pname}"
|
|
344
|
+
for pname, ptype in u.params)
|
|
345
|
+
ret = SPEC.types.get(u.ret_type, "void") if u.ret_type != "None" else "void"
|
|
346
|
+
decls.append(f'extern fn {u.name}({params}) {ret};')
|
|
347
|
+
prelude += "\n// cross-backend declarations\n" + "\n".join(decls) + "\n\n"
|
|
348
|
+
|
|
349
|
+
# emit struct definitions
|
|
350
|
+
if classes:
|
|
351
|
+
struct_code = _emit_structs_zig(classes)
|
|
352
|
+
if struct_code:
|
|
353
|
+
prelude += struct_code + "\n"
|
|
354
|
+
|
|
355
|
+
# build function return type lookup for type inference
|
|
356
|
+
for u in units:
|
|
357
|
+
if u.supported:
|
|
358
|
+
emitter.func_return_types[u.name] = u.ret_type
|
|
359
|
+
|
|
360
|
+
if library_mode:
|
|
361
|
+
for u in units:
|
|
362
|
+
if not u.supported:
|
|
363
|
+
continue
|
|
364
|
+
if u.is_method and u.name.endswith(".__init__"):
|
|
365
|
+
continue
|
|
366
|
+
code = emitter.emit(u)
|
|
367
|
+
if emitter.unsupported_emissions:
|
|
368
|
+
u.supported = False
|
|
369
|
+
u.unsupported_reasons.extend(emitter.unsupported_emissions)
|
|
370
|
+
continue
|
|
371
|
+
# prefix with `export` for C ABI
|
|
372
|
+
code = code.replace(f"fn {u.name.replace('.', '_')}(", f"export fn {u.name.replace('.', '_')}(")
|
|
373
|
+
emitted[u.name] = code
|
|
374
|
+
fns.append(code)
|
|
375
|
+
return prelude + "\n".join(fns) + "\n", emitted
|
|
376
|
+
|
|
377
|
+
entry_u = _entry_unit(units, entry)
|
|
378
|
+
for u in units:
|
|
379
|
+
if u.is_method and u.name.endswith(".__init__"):
|
|
380
|
+
continue
|
|
381
|
+
code = emitter.emit(u)
|
|
382
|
+
if emitter.unsupported_emissions:
|
|
383
|
+
u.supported = False
|
|
384
|
+
u.unsupported_reasons.extend(emitter.unsupported_emissions)
|
|
385
|
+
continue
|
|
386
|
+
emitted[u.name] = code
|
|
387
|
+
fns.append(code)
|
|
388
|
+
|
|
389
|
+
if entry_u.name != "main":
|
|
390
|
+
if entry_u.ret_type == "None":
|
|
391
|
+
wrapper = "pub fn main() void {\n " + entry_u.name + "();\n}\n"
|
|
392
|
+
else:
|
|
393
|
+
wrapper = f"pub fn main() void {{\n _ = {entry_u.name}();\n}}\n"
|
|
394
|
+
fns.append(wrapper)
|
|
395
|
+
else:
|
|
396
|
+
# Zig main() must return void (or u8 for exit code)
|
|
397
|
+
# Rename the user's main() to __ge_main() and add a wrapper
|
|
398
|
+
if entry_u.ret_type != "None":
|
|
399
|
+
fns[-1] = fns[-1].replace(f"fn {entry_u.name}(", "fn __ge_main(")
|
|
400
|
+
wrapper = "pub fn main() void {\n _ = __ge_main();\n}\n"
|
|
401
|
+
fns.append(wrapper)
|
|
402
|
+
else:
|
|
403
|
+
fns[-1] = fns[-1].replace(f"fn {entry_u.name}(", "pub fn main(")
|
|
404
|
+
|
|
405
|
+
program = prelude + "\n".join(fns) + "\n"
|
|
406
|
+
# inject stdlib runtime
|
|
407
|
+
from ..stdlib import get_runtime
|
|
408
|
+
runtime = get_runtime("zig")
|
|
409
|
+
if runtime:
|
|
410
|
+
program = runtime + "\n" + program
|
|
411
|
+
return program, emitted
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""FFI export layer: pick C-ABI-exportable functions and map names.
|
|
2
|
+
|
|
3
|
+
Only functions whose params and return are scalars (int/float/bool) can cross
|
|
4
|
+
the C ABI safely. List/str functions stay internal to the library (callable by
|
|
5
|
+
exported functions, but not directly from Dart). This module tags each FuncUnit
|
|
6
|
+
with `ffi_export` and provides C-ABI name mapping (snake_case -> camelCase for
|
|
7
|
+
Dart).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .analyzer import FuncUnit
|
|
12
|
+
|
|
13
|
+
SCALAR_FFI = {"int", "float", "bool"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_ffi_exportable(u: FuncUnit) -> bool:
|
|
17
|
+
if not u.supported:
|
|
18
|
+
return False
|
|
19
|
+
if u.name == "main":
|
|
20
|
+
return False
|
|
21
|
+
for _, t in u.params:
|
|
22
|
+
if t not in SCALAR_FFI:
|
|
23
|
+
return False
|
|
24
|
+
if u.ret_type not in SCALAR_FFI and u.ret_type != "None":
|
|
25
|
+
return False
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def tag_ffi(units: list[FuncUnit]) -> None:
|
|
30
|
+
for u in units:
|
|
31
|
+
u.ffi_export = is_ffi_exportable(u) # type: ignore[attr-defined]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def c_name(name: str) -> str:
|
|
35
|
+
"""C-ABI symbol name (kept as-is, snake_case)."""
|
|
36
|
+
return name
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def dart_name(name: str) -> str:
|
|
40
|
+
"""camelCase Dart binding name."""
|
|
41
|
+
parts = name.split("_")
|
|
42
|
+
return parts[0] + "".join(p.capitalize() for p in parts[1:])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# C-ABI / Dart FFI type mapping
|
|
46
|
+
DART_FFI_TYPE = {"int": "Int64", "float": "Double", "bool": "Bool", "None": "Void"}
|
|
47
|
+
NATIVE_C_TYPE = {"int": "int64_t", "float": "double", "bool": "bool", "None": "void"}
|
|
48
|
+
RUST_C_TYPE = {"int": "i64", "float": "f64", "bool": "bool", "None": "()"}
|
|
49
|
+
DART_NATIVE_TYPE = {"int": "int", "float": "double", "bool": "bool", "None": "void"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""GE frontends — source language variants that all lower to the same IR.
|
|
2
|
+
|
|
3
|
+
GE follows the N-frontends x M-backends architecture: each source language
|
|
4
|
+
has a frontend that produces the shared typed IR (`FuncUnit` + Python AST),
|
|
5
|
+
and each target language has a backend (emitter) that consumes it.
|
|
6
|
+
|
|
7
|
+
main.ge hybrid -> hybrid frontend -> IR
|
|
8
|
+
foo.ge.py Python-like -> python frontend -> IR
|
|
9
|
+
foo.ge.ts TypeScript -> typescript frontend -> IR
|
|
10
|
+
|
|
|
11
|
+
+--------------------------------+
|
|
12
|
+
|
|
|
13
|
+
rust / cpp / csharp / zig / go / kotlin emitters
|
|
14
|
+
|
|
15
|
+
`.ge` is the canonical extension. A single `.ge` file may contain both
|
|
16
|
+
Python-flavoured and TypeScript-flavoured definitions; the hybrid frontend
|
|
17
|
+
classifies each top-level chunk and lowers it, so a project can mix styles
|
|
18
|
+
per function without splitting files.
|
|
19
|
+
|
|
20
|
+
The single-flavour extensions still work and are useful when a whole module
|
|
21
|
+
is one style:
|
|
22
|
+
|
|
23
|
+
foo.ge.py every chunk is Python-flavoured
|
|
24
|
+
foo.ge.ts every chunk is TypeScript-flavoured
|
|
25
|
+
|
|
26
|
+
Adding a source language means adding a frontend here; nothing downstream
|
|
27
|
+
changes.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
#: Canonical GE extension — flavour is detected per chunk.
|
|
34
|
+
HYBRID_SUFFIXES = (".ge",)
|
|
35
|
+
|
|
36
|
+
#: Source variants that use the TypeScript-flavoured frontend.
|
|
37
|
+
#: `.ge.ts` is canonical; `.ts.ge.py` is accepted for compatibility with
|
|
38
|
+
#: projects scaffolded before the extension was settled.
|
|
39
|
+
TYPESCRIPT_SUFFIXES = (".ge.ts", ".ts.ge.py")
|
|
40
|
+
|
|
41
|
+
#: Source variants that use the Python-flavoured frontend.
|
|
42
|
+
PYTHON_SUFFIXES = (".ge.py",)
|
|
43
|
+
|
|
44
|
+
#: Longest-first so `.ge.py` is not mistaken for `.ge`, and `.ts.ge.py` is
|
|
45
|
+
#: not mistaken for `.ge.py`.
|
|
46
|
+
ALL_SUFFIXES = tuple(sorted(
|
|
47
|
+
PYTHON_SUFFIXES + TYPESCRIPT_SUFFIXES + HYBRID_SUFFIXES,
|
|
48
|
+
key=len, reverse=True))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _match_suffix(name: str, suffixes: tuple[str, ...]) -> str | None:
|
|
52
|
+
"""Return the longest suffix in `suffixes` that `name` ends with."""
|
|
53
|
+
lowered = name.lower()
|
|
54
|
+
for suffix in sorted(suffixes, key=len, reverse=True):
|
|
55
|
+
if lowered.endswith(suffix):
|
|
56
|
+
return suffix
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def frontend_for(path: Path | str) -> str:
|
|
61
|
+
"""Return the frontend name for a source path.
|
|
62
|
+
|
|
63
|
+
One of "hybrid" (.ge), "typescript" (.ge.ts), or "python" (.ge.py).
|
|
64
|
+
"""
|
|
65
|
+
name = Path(path).name
|
|
66
|
+
if _match_suffix(name, TYPESCRIPT_SUFFIXES):
|
|
67
|
+
return "typescript"
|
|
68
|
+
if _match_suffix(name, PYTHON_SUFFIXES):
|
|
69
|
+
return "python"
|
|
70
|
+
if _match_suffix(name, HYBRID_SUFFIXES):
|
|
71
|
+
return "hybrid"
|
|
72
|
+
return "python"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_ge_source(path: Path | str) -> bool:
|
|
76
|
+
"""True if the path looks like a GE source file in any supported flavour."""
|
|
77
|
+
return _match_suffix(Path(path).name, ALL_SUFFIXES) is not None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def python_flavour_of(path: Path | str) -> str:
|
|
81
|
+
"""Return the .ge.py style name for a path, whichever flavour it is.
|
|
82
|
+
|
|
83
|
+
Used so a project keeps a single canonical module name regardless of
|
|
84
|
+
which flavour the author chose:
|
|
85
|
+
|
|
86
|
+
app/memory.ge -> app/memory.ge.py
|
|
87
|
+
app/memory.ge.ts -> app/memory.ge.py
|
|
88
|
+
app/memory.ge.py -> app/memory.ge.py
|
|
89
|
+
"""
|
|
90
|
+
p = Path(path)
|
|
91
|
+
suffix = _match_suffix(p.name, ALL_SUFFIXES)
|
|
92
|
+
if suffix is None:
|
|
93
|
+
return str(p)
|
|
94
|
+
return str(p.with_name(p.name[: -len(suffix)] + ".ge.py"))
|