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