gelang 0.1.3 → 0.1.5
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 +112 -0
- package/package.json +1 -1
- package/python/pyeffic/analyzer.py +20 -0
- package/python/pyeffic/downloader.py +14 -2
- package/python/pyeffic/emitters/base.py +319 -27
- package/python/pyeffic/emitters/cpp.py +31 -3
- package/python/pyeffic/emitters/csharp.py +37 -3
- package/python/pyeffic/emitters/go.py +46 -7
- package/python/pyeffic/emitters/kotlin.py +34 -6
- package/python/pyeffic/emitters/rust.py +30 -4
- package/python/pyeffic/emitters/zig.py +122 -73
- package/python/pyeffic/ge_cli.py +111 -5
- package/python/pyeffic/stdlib.py +213 -1
- package/python/pyeffic/typecheck.py +16 -3
|
@@ -13,31 +13,40 @@ from ..analyzer import FuncUnit
|
|
|
13
13
|
SPEC = Spec(
|
|
14
14
|
name="zig",
|
|
15
15
|
types={"int": "i64", "float": "f64", "bool": "bool", "str": "[]const u8", "None": "void"},
|
|
16
|
-
list_type="
|
|
17
|
-
list_param_type="
|
|
16
|
+
list_type="std.ArrayList({T})",
|
|
17
|
+
list_param_type="*std.ArrayList(i64)",
|
|
18
18
|
list_elem_type="i64",
|
|
19
|
+
list_slice="geListSlice(i64, {x}.items, @as(usize, @intCast({start})), @as(usize, @intCast({stop})))",
|
|
20
|
+
list_copy="geListCopy(i64, {x}.items)",
|
|
19
21
|
borrow_list_arg=False,
|
|
20
22
|
range_call="({lo}..{hi})",
|
|
21
23
|
range_step_call="({lo}..{hi})",
|
|
22
|
-
len_call="@as(i64, @intCast({x}.len))",
|
|
23
|
-
list_len="@as(i64, @intCast({x}.len))",
|
|
24
|
+
len_call="@as(i64, @intCast({x}.items.len))",
|
|
25
|
+
list_len="@as(i64, @intCast({x}.items.len))",
|
|
24
26
|
print_int='std.io.getStdOut().writer().print("{{d}}\\n", .{{{v}}}) catch unreachable',
|
|
25
27
|
print_float='std.io.getStdOut().writer().print("{{d}}\\n", .{{{v}}}) catch unreachable',
|
|
26
28
|
print_str='std.io.getStdOut().writer().print("{{s}}\\n", .{{{v}}}) catch unreachable',
|
|
27
|
-
print_bool='std.io.getStdOut().writer().print("{}\\n", .{{{v}}}) catch unreachable',
|
|
29
|
+
print_bool='std.io.getStdOut().writer().print("{{s}}\\n", .{{if ({v}) "True" else "False"}}) catch unreachable',
|
|
28
30
|
print_generic='std.io.getStdOut().writer().print("{{any}}\\n", .{{{v}}}) catch unreachable',
|
|
31
|
+
list_sort="std.mem.sort(i64, {x}.items, {{}}, std.sort.asc(i64))",
|
|
32
|
+
list_reverse="std.mem.reverse(i64, {x}.items)",
|
|
33
|
+
print_list='gePrintList({v}.items)',
|
|
29
34
|
int_cast="@as(i64, {x})",
|
|
30
35
|
float_cast="@as(f64, {x})",
|
|
31
36
|
float_div="(@as(f64, {l}) / @as(f64, {r}))",
|
|
32
37
|
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})",
|
|
38
|
+
sum_call="blk: {{ var s: i64 = 0; for ({it}.items) |v| {{ s += v; }} break :blk s; }}",
|
|
39
|
+
abs_int="@as(i64, @intCast(if ({x} < 0) -{x} else {x}))",
|
|
35
40
|
abs_float="@abs({x})",
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
min2_call="@min({a}, {b})",
|
|
42
|
+
max2_call="@max({a}, {b})",
|
|
43
|
+
min_call="blk: {{ var __m = {it}.items[0]; for ({it}.items) |v| {{ if (v < __m) __m = v; }} break :blk __m; }}",
|
|
44
|
+
max_call="blk: {{ var __m = {it}.items[0]; for ({it}.items) |v| {{ if (v > __m) __m = v; }} break :blk __m; }}",
|
|
38
45
|
pow_call="std.math.pow(i64, {l}, {r})",
|
|
46
|
+
pow_int="std.math.pow(i64, {l}, {r})",
|
|
47
|
+
pow_float="std.math.pow(f64, {l}, {r})",
|
|
39
48
|
append_call="{x}.append({v}) catch unreachable",
|
|
40
|
-
index_call="{x}[@as(usize, @intCast({i}))]",
|
|
49
|
+
index_call="{x}.items[@as(usize, @intCast({i}))]",
|
|
41
50
|
comment="//",
|
|
42
51
|
fn_template="{sig} {{\n{body}\n}}",
|
|
43
52
|
main_template="",
|
|
@@ -50,16 +59,24 @@ SPEC = Spec(
|
|
|
50
59
|
str_slice="{x}[{start}..{end}]",
|
|
51
60
|
str_slice_start="{x}[{start}..]",
|
|
52
61
|
str_slice_end="{x}[..{end}]",
|
|
53
|
-
list_concat="
|
|
54
|
-
|
|
62
|
+
list_concat="geListConcat(i64, {l}.items, {r}.items)",
|
|
63
|
+
str_split="geStrSplit({x}, {sep})",
|
|
64
|
+
str_join="geStrJoin({x}.items, {sep})",
|
|
65
|
+
str_upper="geStrUpper({x})",
|
|
66
|
+
str_lower="geStrLower({x})",
|
|
67
|
+
str_replace="geStrReplace({x}, {a}, {b})",
|
|
68
|
+
foreach_template="for ({iter}.items) |{var}|",
|
|
55
69
|
try_template="// try/except limited in Zig\n// {body}\n// {handler}",
|
|
56
70
|
struct_template="const {name} = struct {{\n{fields}\n}};",
|
|
57
71
|
struct_field_template=" {type}: {name},",
|
|
58
72
|
struct_new_template="fn {name}_new({params}) {name} {{\n{body}\n}}",
|
|
59
73
|
dict_type="std.StringHashMap({V})",
|
|
74
|
+
set_type="std.AutoHashMap(i64, void)",
|
|
60
75
|
dict_get="{d}.get({k}).?",
|
|
61
76
|
dict_set="{d}.put({k}, {v}) catch unreachable",
|
|
62
77
|
dict_contains="{d}.contains({k})",
|
|
78
|
+
set_len="@as(i64, @intCast({x}.count()))",
|
|
79
|
+
dict_len="@as(i64, @intCast({x}.count()))",
|
|
63
80
|
tuple_type="[2]{T}",
|
|
64
81
|
tuple_get="{t}[{i}]",
|
|
65
82
|
)
|
|
@@ -112,69 +129,49 @@ class ZigEmitter(Emitter):
|
|
|
112
129
|
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
|
|
113
130
|
if node.func.attr == "append" and isinstance(node.func.value, ast.Name):
|
|
114
131
|
mutated.add(node.func.value.id)
|
|
132
|
+
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
133
|
+
# passing a list to a callee that mutates it counts as a
|
|
134
|
+
# mutation here, so the local must be `var`
|
|
135
|
+
for idx in self.func_mutated_params.get(node.func.id, ()):
|
|
136
|
+
if idx < len(node.args):
|
|
137
|
+
arg = node.args[idx]
|
|
138
|
+
if isinstance(arg, ast.Name):
|
|
139
|
+
mutated.add(arg.id)
|
|
115
140
|
return mutated
|
|
116
141
|
|
|
117
142
|
def emit(self, unit: FuncUnit) -> str:
|
|
118
|
-
"""
|
|
143
|
+
"""Emit a function.
|
|
144
|
+
|
|
145
|
+
A list parameter is a `*std.ArrayList(i64)`, so no conversion is
|
|
146
|
+
needed: the callee mutates the caller's list directly. Only scalar
|
|
147
|
+
parameters that are reassigned need a mutable local, because Zig
|
|
148
|
+
parameters are immutable.
|
|
149
|
+
"""
|
|
119
150
|
self.mutated = self._find_mutated_vars(unit.body)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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)
|
|
151
|
+
self.mutated_list_params = {
|
|
152
|
+
pname for pname, ptype in unit.params
|
|
153
|
+
if ptype == "list" and pname in self.mutated
|
|
154
|
+
}
|
|
155
|
+
self.mutated_params = {
|
|
156
|
+
pname for pname, ptype in unit.params
|
|
157
|
+
if pname in self.mutated and ptype != "list"
|
|
158
|
+
}
|
|
134
159
|
result = super().emit(unit)
|
|
135
|
-
if
|
|
136
|
-
# Insert mutable copies and list conversions right after the opening brace
|
|
160
|
+
if self.mutated_params:
|
|
137
161
|
lines = result.split("\n")
|
|
138
|
-
|
|
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; }}")
|
|
162
|
+
insert = [f" var {p}_ = {p};" for p in sorted(self.mutated_params)]
|
|
145
163
|
for i, line in enumerate(lines):
|
|
146
|
-
if "
|
|
147
|
-
for j, il in enumerate(
|
|
164
|
+
if "fn" in line and "{" in line:
|
|
165
|
+
for j, il in enumerate(insert):
|
|
148
166
|
lines.insert(i + 1 + j, il)
|
|
149
167
|
break
|
|
150
168
|
result = "\n".join(lines)
|
|
151
|
-
# Replace param references with mutable version
|
|
152
169
|
import re
|
|
153
|
-
for pname in mutated_params:
|
|
154
|
-
|
|
155
|
-
result = re.sub(r
|
|
156
|
-
result =
|
|
157
|
-
|
|
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
|
-
)
|
|
170
|
+
for pname in self.mutated_params:
|
|
171
|
+
result = re.sub(r"\b" + pname + r"\b", pname + "_", result)
|
|
172
|
+
result = re.sub(r"\b" + pname + r"_: ", pname + ": ", result, count=1)
|
|
173
|
+
result = result.replace(f"var {pname}_ = {pname}_;",
|
|
174
|
+
f"var {pname}_ = {pname};")
|
|
178
175
|
return result
|
|
179
176
|
|
|
180
177
|
def _is_mutated(self, var: str) -> bool:
|
|
@@ -194,12 +191,19 @@ class ZigEmitter(Emitter):
|
|
|
194
191
|
self.lines.append(f"{ind}_ = {self.expr(node.value)};")
|
|
195
192
|
return
|
|
196
193
|
if isinstance(node, ast.AnnAssign) and node.value is not None:
|
|
194
|
+
# bare `list` gets its element type from the value
|
|
197
195
|
target = node.target.id if isinstance(node.target, ast.Name) else self.expr(node.target)
|
|
198
196
|
t = self._ann_type(node.annotation) if isinstance(node.target, ast.Name) else self.infer_type(node.value)
|
|
199
197
|
if isinstance(node.target, ast.Name):
|
|
200
198
|
self.var_types[target] = t
|
|
201
199
|
self.declared.add(target)
|
|
202
200
|
nt = self.py_to_native(t)
|
|
201
|
+
if t == "list":
|
|
202
|
+
# a bare `list` annotation carries no element type; take it
|
|
203
|
+
# from the value so `"a,b".split(",")` is a string list
|
|
204
|
+
elem = self._list_elem_of_value(node.value)
|
|
205
|
+
if elem and elem != self.spec.list_elem_type:
|
|
206
|
+
nt = self.spec.list_type.format(T=elem)
|
|
203
207
|
kw = self._decl_keyword(target)
|
|
204
208
|
self.lines.append(f"{ind}{kw} {target}: {nt} = {self.expr(node.value)};")
|
|
205
209
|
return
|
|
@@ -244,8 +248,16 @@ class ZigEmitter(Emitter):
|
|
|
244
248
|
self.lines.append(f"{ind}while ({var} < {hi}) : ({var} += 1) {{")
|
|
245
249
|
else:
|
|
246
250
|
iter_s = self.expr(it)
|
|
247
|
-
self.
|
|
248
|
-
|
|
251
|
+
if self.infer_type(it) == "dict":
|
|
252
|
+
# Zig has no allocator here, so walk the map's key iterator
|
|
253
|
+
# instead of materialising a key list.
|
|
254
|
+
self.var_types[var] = "str"
|
|
255
|
+
self.lines.append(f"{ind}var __keys_{var} = {iter_s}.keyIterator();")
|
|
256
|
+
self.lines.append(f"{ind}while (__keys_{var}.next()) |__kp_{var}| {{")
|
|
257
|
+
self.lines.append(f"{ind} const {var} = __kp_{var}.*;")
|
|
258
|
+
else:
|
|
259
|
+
self.var_types[var] = "long"
|
|
260
|
+
self.lines.append(f"{ind}for ({iter_s}.items) |{var}| {{")
|
|
249
261
|
self.indent_lvl += 1
|
|
250
262
|
for s in node.body:
|
|
251
263
|
self.stmt(s)
|
|
@@ -256,7 +268,7 @@ class ZigEmitter(Emitter):
|
|
|
256
268
|
emit_name = unit.name.replace(".", "_")
|
|
257
269
|
params = []
|
|
258
270
|
for pname, ptype in unit.params:
|
|
259
|
-
nt = self.param_native_type(ptype)
|
|
271
|
+
nt = self.param_native_type(ptype, pname)
|
|
260
272
|
params.append(f"{pname}: {nt}")
|
|
261
273
|
ret = self.py_to_native(unit.ret_type) if unit.ret_type != "None" else "void"
|
|
262
274
|
param_str = ", ".join(params)
|
|
@@ -303,6 +315,23 @@ def _emit_structs_zig(classes: list) -> str:
|
|
|
303
315
|
return "\n".join(out)
|
|
304
316
|
|
|
305
317
|
|
|
318
|
+
def _precompute_mutated_params(emitter, units) -> None:
|
|
319
|
+
"""Record which parameters each function mutates.
|
|
320
|
+
|
|
321
|
+
The emitter normally learns this as it emits each unit, so a function
|
|
322
|
+
defined after its caller would not be known yet. Doing it up front makes
|
|
323
|
+
call-site handling independent of definition order.
|
|
324
|
+
"""
|
|
325
|
+
for u in units:
|
|
326
|
+
if u.body is None:
|
|
327
|
+
continue
|
|
328
|
+
mutated = emitter._find_mutated_vars(u.body.body)
|
|
329
|
+
indices = {i for i, (pname, _pt) in enumerate(u.params)
|
|
330
|
+
if pname in mutated}
|
|
331
|
+
if indices:
|
|
332
|
+
emitter.func_mutated_params[u.name] = indices
|
|
333
|
+
|
|
334
|
+
|
|
306
335
|
def emit_zig(units: list[FuncUnit], entry: str | None,
|
|
307
336
|
library_mode: bool = False,
|
|
308
337
|
extern_fns: list[FuncUnit] | None = None,
|
|
@@ -315,8 +344,14 @@ def emit_zig(units: list[FuncUnit], entry: str | None,
|
|
|
315
344
|
extern_fns: functions from other backends that this Zig code calls.
|
|
316
345
|
"""
|
|
317
346
|
emitter = ZigEmitter(SPEC)
|
|
347
|
+
emitter.func_signatures = {
|
|
348
|
+
u.name: ([p for p, _t in u.params],
|
|
349
|
+
dict(getattr(u, 'param_defaults', {})))
|
|
350
|
+
for u in units
|
|
351
|
+
}
|
|
318
352
|
emitter.library_mode = library_mode
|
|
319
353
|
emitter.constants = constants or {}
|
|
354
|
+
_precompute_mutated_params(emitter, units)
|
|
320
355
|
emitted: dict[str, str] = {}
|
|
321
356
|
fns: list[str] = []
|
|
322
357
|
|
|
@@ -388,19 +423,33 @@ def emit_zig(units: list[FuncUnit], entry: str | None,
|
|
|
388
423
|
|
|
389
424
|
if entry_u.name != "main":
|
|
390
425
|
if entry_u.ret_type == "None":
|
|
391
|
-
wrapper = "pub fn main() void {\n
|
|
426
|
+
wrapper = ("pub fn main() void {\n"
|
|
427
|
+
" defer _ = ge_arena.deinit();\n"
|
|
428
|
+
" " + entry_u.name + "();\n}\n")
|
|
392
429
|
else:
|
|
393
|
-
wrapper = f"pub fn main() void {{\n
|
|
430
|
+
wrapper = (f"pub fn main() void {{\n"
|
|
431
|
+
f" defer _ = ge_arena.deinit();\n"
|
|
432
|
+
f" _ = {entry_u.name}();\n}}\n")
|
|
394
433
|
fns.append(wrapper)
|
|
434
|
+
elif not fns:
|
|
435
|
+
# the entry function was rejected during emission; the pipeline
|
|
436
|
+
# already has the reason, so emit a stub rather than crashing
|
|
437
|
+
return prelude, emitted
|
|
395
438
|
else:
|
|
396
439
|
# Zig main() must return void (or u8 for exit code)
|
|
397
|
-
# Rename the user's main() to __ge_main() and add a wrapper
|
|
440
|
+
# Rename the user's main() to __ge_main() and add a wrapper that also
|
|
441
|
+
# releases the arena.
|
|
398
442
|
if entry_u.ret_type != "None":
|
|
399
443
|
fns[-1] = fns[-1].replace(f"fn {entry_u.name}(", "fn __ge_main(")
|
|
400
|
-
wrapper = "pub fn main() void {\n
|
|
401
|
-
|
|
444
|
+
wrapper = ("pub fn main() void {\n"
|
|
445
|
+
" defer _ = ge_arena.deinit();\n"
|
|
446
|
+
" _ = __ge_main();\n}\n")
|
|
402
447
|
else:
|
|
403
|
-
fns[-1] = fns[-1].replace(f"fn {entry_u.name}(", "
|
|
448
|
+
fns[-1] = fns[-1].replace(f"fn {entry_u.name}(", "fn __ge_main(")
|
|
449
|
+
wrapper = ("pub fn main() void {\n"
|
|
450
|
+
" defer _ = ge_arena.deinit();\n"
|
|
451
|
+
" __ge_main();\n}\n")
|
|
452
|
+
fns.append(wrapper)
|
|
404
453
|
|
|
405
454
|
program = prelude + "\n".join(fns) + "\n"
|
|
406
455
|
# inject stdlib runtime
|
package/python/pyeffic/ge_cli.py
CHANGED
|
@@ -211,11 +211,27 @@ def cmd_analyze(args) -> int:
|
|
|
211
211
|
return 1
|
|
212
212
|
|
|
213
213
|
source = source_path.read_text(encoding="utf-8")
|
|
214
|
+
original_source = source
|
|
214
215
|
entry = args.entry or "main"
|
|
215
216
|
|
|
217
|
+
# Lower non-Python flavours (.ge hybrid, .ge.ts) before analysis, exactly
|
|
218
|
+
# as build() does. Without this, `ge analyze` parses a hybrid file as
|
|
219
|
+
# plain Python and reports a syntax error on the first block tag.
|
|
220
|
+
# resolve_imports lowers internally, so it gets the original source.
|
|
221
|
+
from .frontends import frontend_for
|
|
222
|
+
from .frontends.typescript import TypeScriptSyntaxError
|
|
223
|
+
from .modules import _lower_source
|
|
224
|
+
|
|
225
|
+
if frontend_for(source_path) != "python":
|
|
226
|
+
try:
|
|
227
|
+
source = _lower_source(source, source_path)
|
|
228
|
+
except TypeScriptSyntaxError as exc:
|
|
229
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
230
|
+
return 1
|
|
231
|
+
|
|
216
232
|
# Resolve imports first
|
|
217
233
|
try:
|
|
218
|
-
units, classes, warnings = resolve_imports(
|
|
234
|
+
units, classes, warnings = resolve_imports(original_source, source_path)
|
|
219
235
|
for w in warnings:
|
|
220
236
|
print(f" warning: {w}")
|
|
221
237
|
except Exception as e:
|
|
@@ -411,8 +427,96 @@ def _print_flutter(report) -> None:
|
|
|
411
427
|
print(f" ! {e}")
|
|
412
428
|
|
|
413
429
|
|
|
430
|
+
def _ui_node_to_tree(node) -> dict:
|
|
431
|
+
"""Convert a .ge.ui WidgetNode into the dict dartgen.generate_main wants."""
|
|
432
|
+
if node is None:
|
|
433
|
+
return {"kind": "Column", "children": [], "align": "center"}
|
|
434
|
+
d: dict = {"kind": node.kind}
|
|
435
|
+
if node.children:
|
|
436
|
+
d["children"] = [_ui_node_to_tree(c) for c in node.children]
|
|
437
|
+
child = node.props.get("child")
|
|
438
|
+
if child is not None and hasattr(child, "kind"):
|
|
439
|
+
d["child"] = _ui_node_to_tree(child)
|
|
440
|
+
if node.text is not None:
|
|
441
|
+
d["text"] = node.text
|
|
442
|
+
elif "text" in node.props:
|
|
443
|
+
d["text"] = node.props["text"]
|
|
444
|
+
if node.label is not None:
|
|
445
|
+
d["label"] = node.label
|
|
446
|
+
elif "label" in node.props:
|
|
447
|
+
d["label"] = node.props["label"]
|
|
448
|
+
if node.state:
|
|
449
|
+
d["state"] = node.state
|
|
450
|
+
style = node.props.get("style") or node.style_dict.get("style")
|
|
451
|
+
if style:
|
|
452
|
+
d["style"] = style
|
|
453
|
+
for key in ("align", "padding", "color", "hint"):
|
|
454
|
+
if key in node.props:
|
|
455
|
+
d[key] = node.props[key]
|
|
456
|
+
if node.action:
|
|
457
|
+
d["action"] = node.action
|
|
458
|
+
return d
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _flutter_from_ui(args, ui_path: Path) -> int:
|
|
462
|
+
"""`ge flutter foo.ge.ui` — generate a Flutter project from the UI DSL."""
|
|
463
|
+
from .ui_dsl import parse_ui_file
|
|
464
|
+
from .dartgen import generate_main, generate_pubspec
|
|
465
|
+
from .config import Config
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
screen = parse_ui_file(ui_path)
|
|
469
|
+
except SyntaxError as e:
|
|
470
|
+
print(f"error: invalid .ge.ui — {e}", file=sys.stderr)
|
|
471
|
+
return 1
|
|
472
|
+
|
|
473
|
+
app_name = args.app_name or ui_path.stem
|
|
474
|
+
cfg = Config(out_dir=Path(args.out_dir), target="mobile")
|
|
475
|
+
project_dir = cfg.target_dir / app_name
|
|
476
|
+
lib_dir = project_dir / "lib"
|
|
477
|
+
lib_dir.mkdir(parents=True, exist_ok=True)
|
|
478
|
+
|
|
479
|
+
tree = _ui_node_to_tree(screen.root)
|
|
480
|
+
title = screen.title or app_name
|
|
481
|
+
main_dart = generate_main(tree, [], app_title=title)
|
|
482
|
+
|
|
483
|
+
(lib_dir / "main.dart").write_text(main_dart, encoding="utf-8")
|
|
484
|
+
# generate_main always imports bindings.dart; a UI-only project has no
|
|
485
|
+
# FFI exports, so emit the stub rather than leaving a dangling import.
|
|
486
|
+
from .dartgen import generate_bindings
|
|
487
|
+
(lib_dir / "bindings.dart").write_text(
|
|
488
|
+
generate_bindings([], app_name), encoding="utf-8")
|
|
489
|
+
(project_dir / "pubspec.yaml").write_text(
|
|
490
|
+
generate_pubspec(app_name), encoding="utf-8")
|
|
491
|
+
|
|
492
|
+
print(GE_BANNER)
|
|
493
|
+
print()
|
|
494
|
+
print(f"== GE flutter app: {project_dir} ==")
|
|
495
|
+
print(f"Title : {title}")
|
|
496
|
+
print(f"Dart files : {lib_dir / 'main.dart'}")
|
|
497
|
+
print(f"\nNext: cd {project_dir} && flutter pub get && flutter run")
|
|
498
|
+
|
|
499
|
+
dart = _find_dart()
|
|
500
|
+
if dart:
|
|
501
|
+
r = subprocess.run([dart, "analyze", str(lib_dir)],
|
|
502
|
+
capture_output=True, text=True, timeout=120)
|
|
503
|
+
out = ((r.stdout or "") + (r.stderr or "")).strip()
|
|
504
|
+
print(out[:1200] if out else "(analyzer: no output)")
|
|
505
|
+
print("Dart analysis: " + ("PASS" if r.returncode == 0 else "issues found"))
|
|
506
|
+
else:
|
|
507
|
+
print("\n(dart not found — install Flutter to validate generated Dart)")
|
|
508
|
+
return 0
|
|
509
|
+
|
|
510
|
+
|
|
414
511
|
def cmd_flutter(args) -> int:
|
|
415
|
-
|
|
512
|
+
src_path = Path(args.file)
|
|
513
|
+
# A .ge.ui file is a UI definition, not GE source. `ge flutter` used to
|
|
514
|
+
# hand it to the module resolver, which tried to parse `Window {` as
|
|
515
|
+
# Python and failed. Generate the Dart UI directly instead.
|
|
516
|
+
if src_path.suffix == ".ui":
|
|
517
|
+
return _flutter_from_ui(args, src_path)
|
|
518
|
+
|
|
519
|
+
source = _maybe_transpile_ts(src_path)
|
|
416
520
|
# a Flutter app is a mobile artifact, so it always lands in build/mobile/
|
|
417
521
|
cfg = Config(out_dir=Path(args.out_dir), target="mobile",
|
|
418
522
|
force_backend=None if args.backend == "auto" else args.backend,
|
|
@@ -502,12 +606,14 @@ def cmd_react(args) -> int:
|
|
|
502
606
|
return 1
|
|
503
607
|
|
|
504
608
|
app_name = args.app_name or ui_path.stem.replace(".ge", "")
|
|
505
|
-
#
|
|
609
|
+
# Output goes under the target-aware build root so every artifact of a
|
|
610
|
+
# project lives in one place: build/web/frontend. An explicit -o wins.
|
|
506
611
|
if args.out_dir:
|
|
507
612
|
out_dir = Path(args.out_dir)
|
|
508
613
|
else:
|
|
509
|
-
|
|
510
|
-
|
|
614
|
+
from .config import Config
|
|
615
|
+
cfg = Config(target="web")
|
|
616
|
+
out_dir = cfg.target_dir / "frontend"
|
|
511
617
|
|
|
512
618
|
files = generate_react_app(screen, app_name)
|
|
513
619
|
written = write_react_app(files, out_dir, force=args.force)
|