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,618 @@
1
+ """GE UI DSL — a declarative UI designer language.
2
+
3
+ This is a separate language from the major backends (C++, Rust, Dart, C#,
4
+ Zig, Go). It uses a simple, readable syntax to describe UI screens that
5
+ compile to Flutter widgets. It can be combined with any backend when
6
+ building a complete application.
7
+
8
+ Syntax example (.ge.ui file):
9
+
10
+ Screen "MyRent Dashboard" {
11
+ state: total_rent: int = 0
12
+ state: total_paid: int = 0
13
+ state: outstanding: int = 0
14
+
15
+ Column {
16
+ padding: 16
17
+ children:
18
+ Text "Total Rent" style="headline"
19
+ Text state="total_rent" style="value"
20
+ Divider
21
+ Text "Total Paid" style="headline"
22
+ Text state="total_paid" style="value"
23
+ SizedBox height=20
24
+ ElevatedButton "Refresh" on_click=Action(call="compute_total_rent")
25
+ }
26
+ }
27
+
28
+ The DSL supports:
29
+ - Screen definitions with title and state declarations
30
+ - Container widgets: Column, Row, Container, Expanded, SizedBox
31
+ - Display widgets: Text, Divider, Icon
32
+ - Interactive widgets: ElevatedButton, TextField
33
+ - Style strings: "fontSize:20; bold:true; color:#1565C0"
34
+ - State bindings: state="variable_name"
35
+ - Actions: on_click=Action(call="function_name", args=[...], update="state_var")
36
+ - Named styles: headline, value, title, subtitle, error
37
+
38
+ The parser produces a tree of WidgetNode dicts that can be converted to
39
+ Flutter Dart code by the dartgen module.
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import re
44
+ from dataclasses import dataclass, field
45
+ from typing import Any
46
+
47
+
48
+ @dataclass
49
+ class WidgetNode:
50
+ """A node in the UI widget tree."""
51
+ kind: str
52
+ children: list["WidgetNode"] = field(default_factory=list)
53
+ props: dict[str, Any] = field(default_factory=dict)
54
+ style_dict: dict[str, Any] = field(default_factory=dict)
55
+ state: str | None = None
56
+ text: str | None = None
57
+ label: str | None = None
58
+ action: dict[str, Any] | None = None
59
+
60
+
61
+ @dataclass
62
+ class StateVar:
63
+ """A state variable declaration."""
64
+ name: str
65
+ type: str
66
+ default: Any = None
67
+
68
+
69
+ @dataclass
70
+ class Screen:
71
+ """A UI screen definition."""
72
+ title: str
73
+ state_vars: list[StateVar] = field(default_factory=list)
74
+ root: WidgetNode | None = None
75
+
76
+
77
+ # ---- Tokenizer ----
78
+
79
+ class Token:
80
+ def __init__(self, kind: str, value: str, line: int):
81
+ self.kind = kind
82
+ self.value = value
83
+ self.line = line
84
+
85
+ def __repr__(self):
86
+ return f"Token({self.kind}, {self.value!r}, line={self.line})"
87
+
88
+
89
+ def tokenize(source: str) -> list[Token]:
90
+ """Tokenize the UI DSL source."""
91
+ tokens: list[Token] = []
92
+ line = 1
93
+ i = 0
94
+ while i < len(source):
95
+ c = source[i]
96
+ if c == "\n":
97
+ line += 1
98
+ i += 1
99
+ continue
100
+ if c in " \t\r":
101
+ i += 1
102
+ continue
103
+ if c == "#": # comment
104
+ while i < len(source) and source[i] != "\n":
105
+ i += 1
106
+ continue
107
+ if source.startswith('"""', i) or source.startswith("'''", i):
108
+ # module/block docstring — skip to the closing triple quote
109
+ quote = source[i:i + 3]
110
+ end = source.find(quote, i + 3)
111
+ if end == -1:
112
+ end = len(source)
113
+ line += source.count("\n", i, end)
114
+ i = end + 3
115
+ continue
116
+ if c == "{":
117
+ tokens.append(Token("LBRACE", "{", line))
118
+ i += 1
119
+ continue
120
+ if c == "}":
121
+ tokens.append(Token("RBRACE", "}", line))
122
+ i += 1
123
+ continue
124
+ if c == ":":
125
+ tokens.append(Token("COLON", ":", line))
126
+ i += 1
127
+ continue
128
+ if c == "=":
129
+ tokens.append(Token("EQUALS", "=", line))
130
+ i += 1
131
+ continue
132
+ if c == "[":
133
+ tokens.append(Token("LBRACKET", "[", line))
134
+ i += 1
135
+ continue
136
+ if c == "]":
137
+ tokens.append(Token("RBRACKET", "]", line))
138
+ i += 1
139
+ continue
140
+ if c == ",":
141
+ tokens.append(Token("COMMA", ",", line))
142
+ i += 1
143
+ continue
144
+ if c == "(":
145
+ tokens.append(Token("LPAREN", "(", line))
146
+ i += 1
147
+ continue
148
+ if c == ")":
149
+ tokens.append(Token("RPAREN", ")", line))
150
+ i += 1
151
+ continue
152
+ if c == '"':
153
+ # string literal
154
+ j = i + 1
155
+ while j < len(source) and source[j] != '"':
156
+ if source[j] == "\\":
157
+ j += 1
158
+ j += 1
159
+ tokens.append(Token("STRING", source[i + 1:j], line))
160
+ i = j + 1
161
+ continue
162
+ if c.isdigit() or (c == "-" and i + 1 < len(source) and source[i + 1].isdigit()):
163
+ # number literal
164
+ j = i + 1
165
+ while j < len(source) and (source[j].isdigit() or source[j] == "."):
166
+ j += 1
167
+ tokens.append(Token("NUMBER", source[i:j], line))
168
+ i = j
169
+ continue
170
+ if c.isalpha() or c == "_":
171
+ # identifier or keyword
172
+ j = i + 1
173
+ while j < len(source) and (source[j].isalnum() or source[j] in "_."):
174
+ j += 1
175
+ word = source[i:j]
176
+ tokens.append(Token("IDENT", word, line))
177
+ i = j
178
+ continue
179
+ raise SyntaxError(f"Unexpected character {c!r} at line {line}")
180
+
181
+ tokens.append(Token("EOF", "", line))
182
+ return tokens
183
+
184
+
185
+ # ---- Parser ----
186
+
187
+ CONTAINER_WIDGETS = {"Column", "Row", "Container", "Expanded", "SizedBox",
188
+ "ListView", "Stack", "Padding", "Center", "Card",
189
+ "Scaffold", "AppBar"}
190
+ LEAF_WIDGETS = {"Text", "Divider", "Icon", "ElevatedButton", "TextButton",
191
+ "TextField", "Image", "CircularProgressIndicator",
192
+ "Switch", "Checkbox", "Slider"}
193
+ ALL_WIDGETS = CONTAINER_WIDGETS | LEAF_WIDGETS
194
+
195
+
196
+ class Parser:
197
+ def __init__(self, tokens: list[Token]):
198
+ self.tokens = tokens
199
+ self.pos = 0
200
+
201
+ def peek(self) -> Token:
202
+ return self.tokens[self.pos]
203
+
204
+ def next(self) -> Token:
205
+ t = self.tokens[self.pos]
206
+ self.pos += 1
207
+ return t
208
+
209
+ def expect(self, kind: str) -> Token:
210
+ t = self.next()
211
+ if t.kind != kind:
212
+ raise SyntaxError(f"Expected {kind} but got {t.kind} ({t.value!r}) at line {t.line}")
213
+ return t
214
+
215
+ def parse_screen(self) -> Screen:
216
+ """Parse a screen definition.
217
+
218
+ Both forms are accepted:
219
+ Screen "My App" { ... }
220
+ Window { title: "My App" ... }
221
+ """
222
+ self.expect("IDENT") # "Screen" / "Window"
223
+ title = ""
224
+ if self.peek().kind == "STRING":
225
+ title = self.next().value
226
+ screen = Screen(title=title)
227
+ self.expect("LBRACE")
228
+
229
+ while self.peek().kind != "RBRACE":
230
+ t = self.peek()
231
+ if t.kind == "IDENT" and t.value in ("title", "name"):
232
+ self.next() # consume "title" / "name"
233
+ self.expect("COLON")
234
+ screen.title = self.expect("STRING").value
235
+ elif t.kind == "IDENT" and t.value == "state":
236
+ self.next() # consume "state"
237
+ self.expect("COLON")
238
+ state_var = self.parse_state_decl()
239
+ screen.state_vars.append(state_var)
240
+ elif t.kind == "IDENT" and t.value in ALL_WIDGETS:
241
+ widget = self.parse_widget()
242
+ screen.root = widget
243
+ elif t.kind == "IDENT" and t.value == "children":
244
+ self.next() # consume "children"
245
+ self.expect("COLON")
246
+ # parse child widgets until we hit something that's not a widget
247
+ while self.peek().kind == "IDENT" and self.peek().value in ALL_WIDGETS:
248
+ child = self.parse_widget()
249
+ if screen.root is None:
250
+ screen.root = WidgetNode(kind="Column")
251
+ screen.root.children.append(child)
252
+ else:
253
+ raise SyntaxError(f"Unexpected token {t.value!r} at line {t.line}")
254
+
255
+ self.expect("RBRACE")
256
+ return screen
257
+
258
+ def parse_state_decl(self) -> StateVar:
259
+ """Parse a state variable declaration: name: type = default"""
260
+ name_tok = self.expect("IDENT")
261
+ self.expect("COLON")
262
+ type_tok = self.expect("IDENT")
263
+ default = None
264
+ if self.peek().kind == "EQUALS":
265
+ self.next()
266
+ dt = self.next()
267
+ if dt.kind == "NUMBER":
268
+ default = int(dt.value) if "." not in dt.value else float(dt.value)
269
+ elif dt.kind == "STRING":
270
+ default = dt.value
271
+ elif dt.kind == "IDENT":
272
+ if dt.value == "true":
273
+ default = True
274
+ elif dt.value == "false":
275
+ default = False
276
+ else:
277
+ default = dt.value
278
+ return StateVar(name=name_tok.value, type=type_tok.value, default=default)
279
+
280
+ def parse_widget(self) -> WidgetNode:
281
+ """Parse a widget definition."""
282
+ kind_tok = self.expect("IDENT")
283
+ node = WidgetNode(kind=kind_tok.value)
284
+
285
+ # handle text/label directly after widget name: Text "Hello" or Button "Click"
286
+ if self.peek().kind == "STRING":
287
+ text_tok = self.next()
288
+ if kind_tok.value == "Text":
289
+ node.text = text_tok.value
290
+ elif kind_tok.value in ("ElevatedButton", "TextButton"):
291
+ node.label = text_tok.value
292
+ else:
293
+ node.props["text"] = text_tok.value
294
+
295
+ # parse properties until we see children or RBRACE
296
+ while self.peek().kind == "IDENT":
297
+ prop_tok = self.peek()
298
+ if prop_tok.value in ALL_WIDGETS:
299
+ # this is a child widget, not a property
300
+ break
301
+ if prop_tok.value == "children":
302
+ self.next()
303
+ self.expect("COLON")
304
+ while self.peek().kind == "IDENT" and self.peek().value in ALL_WIDGETS:
305
+ child = self.parse_widget()
306
+ node.children.append(child)
307
+ break
308
+ # parse property: name = value or name: value
309
+ self.next() # consume property name
310
+ if self.peek().kind == "COLON":
311
+ self.next()
312
+ elif self.peek().kind == "EQUALS":
313
+ self.next()
314
+ else:
315
+ # property without value (e.g., "Divider")
316
+ node.props[prop_tok.value] = True
317
+ continue
318
+
319
+ val_tok = self.next()
320
+ if val_tok.kind == "STRING":
321
+ if prop_tok.value == "style":
322
+ from .styling import parse_style
323
+ node.style_dict = parse_style(val_tok.value)
324
+ elif prop_tok.value == "state":
325
+ node.state = val_tok.value
326
+ elif prop_tok.value == "text":
327
+ node.text = val_tok.value
328
+ elif prop_tok.value == "label":
329
+ node.label = val_tok.value
330
+ elif prop_tok.value == "on_click":
331
+ node.action = {"call": val_tok.value}
332
+ else:
333
+ node.props[prop_tok.value] = val_tok.value
334
+ elif val_tok.kind == "NUMBER":
335
+ if "." in val_tok.value:
336
+ node.props[prop_tok.value] = float(val_tok.value)
337
+ else:
338
+ node.props[prop_tok.value] = int(val_tok.value)
339
+ elif val_tok.kind == "IDENT":
340
+ if val_tok.value == "Action":
341
+ # parse Action(call="fn", args=[...], update="state")
342
+ node.action = self.parse_action()
343
+ elif val_tok.value in ("true", "false"):
344
+ node.props[prop_tok.value] = (val_tok.value == "true")
345
+ else:
346
+ node.props[prop_tok.value] = val_tok.value
347
+
348
+ # parse body block if present
349
+ if self.peek().kind == "LBRACE":
350
+ self.next() # consume {
351
+ while self.peek().kind != "RBRACE":
352
+ if self.peek().kind == "IDENT" and self.peek().value in ALL_WIDGETS:
353
+ child = self.parse_widget()
354
+ node.children.append(child)
355
+ elif self.peek().kind == "IDENT" and self.peek().value == "children":
356
+ self.next()
357
+ self.expect("COLON")
358
+ while self.peek().kind == "IDENT" and self.peek().value in ALL_WIDGETS:
359
+ child = self.parse_widget()
360
+ node.children.append(child)
361
+ elif self.peek().kind == "STRING":
362
+ # bare string in body — treat as Text widget
363
+ text_tok = self.next()
364
+ child = WidgetNode(kind="Text", text=text_tok.value)
365
+ node.children.append(child)
366
+ elif self.peek().kind == "IDENT":
367
+ # property inside body block: name: value or name = value
368
+ prop_tok = self.next()
369
+ if self.peek().kind == "COLON":
370
+ self.next()
371
+ elif self.peek().kind == "EQUALS":
372
+ self.next()
373
+ else:
374
+ node.props[prop_tok.value] = True
375
+ continue
376
+ val_tok = self.next()
377
+ if val_tok.kind == "STRING":
378
+ if prop_tok.value == "style":
379
+ from .styling import parse_style
380
+ node.style_dict = parse_style(val_tok.value)
381
+ elif prop_tok.value == "state":
382
+ node.state = val_tok.value
383
+ elif prop_tok.value == "text":
384
+ node.text = val_tok.value
385
+ elif prop_tok.value == "label":
386
+ node.label = val_tok.value
387
+ elif prop_tok.value == "on_click":
388
+ node.action = {"call": val_tok.value}
389
+ else:
390
+ node.props[prop_tok.value] = val_tok.value
391
+ elif val_tok.kind == "NUMBER":
392
+ if "." in val_tok.value:
393
+ node.props[prop_tok.value] = float(val_tok.value)
394
+ else:
395
+ node.props[prop_tok.value] = int(val_tok.value)
396
+ elif val_tok.kind == "IDENT":
397
+ if val_tok.value == "Action":
398
+ node.action = self.parse_action()
399
+ elif val_tok.value in ("true", "false"):
400
+ node.props[prop_tok.value] = (val_tok.value == "true")
401
+ else:
402
+ node.props[prop_tok.value] = val_tok.value
403
+ else:
404
+ raise SyntaxError(f"Unexpected token in widget body: {self.peek().value!r}")
405
+ self.expect("RBRACE")
406
+
407
+ return node
408
+
409
+ def parse_action(self) -> dict[str, Any]:
410
+ """Parse an Action(...) expression."""
411
+ self.expect("LPAREN")
412
+ action: dict[str, Any] = {"args": []}
413
+ while self.peek().kind != "RPAREN":
414
+ prop = self.expect("IDENT")
415
+ self.expect("EQUALS")
416
+ val = self.next()
417
+ if val.kind == "STRING":
418
+ action[prop.value] = val.value
419
+ elif val.kind == "NUMBER":
420
+ action[prop.value] = int(val.value) if "." not in val.value else float(val.value)
421
+ elif val.kind == "LBRACKET":
422
+ # parse array
423
+ while self.peek().kind != "RBRACKET":
424
+ v = self.next()
425
+ if v.kind == "STRING":
426
+ action["args"].append(v.value)
427
+ elif v.kind == "NUMBER":
428
+ action["args"].append(int(v.value) if "." not in v.value else float(v.value))
429
+ if self.peek().kind == "COMMA":
430
+ self.next()
431
+ self.expect("RBRACKET")
432
+ if self.peek().kind == "COMMA":
433
+ self.next()
434
+ self.expect("RPAREN")
435
+ return action
436
+
437
+
438
+ def parse_ui_dsl(source: str) -> Screen:
439
+ """Parse a .ge.ui file and return a Screen definition."""
440
+ tokens = tokenize(source)
441
+ parser = Parser(tokens)
442
+ return parser.parse_screen()
443
+
444
+
445
+ def parse_ui_file(path) -> Screen:
446
+ """Parse a .ge.ui file from disk."""
447
+ from pathlib import Path
448
+ p = Path(path)
449
+ source = p.read_text(encoding="utf-8")
450
+ return parse_ui_dsl(source)
451
+
452
+
453
+ def collect_state_from_screen(screen: Screen) -> dict[str, str]:
454
+ """Collect all state variables referenced in the screen."""
455
+ states: dict[str, str] = {}
456
+ for sv in screen.state_vars:
457
+ states[sv.name] = sv.type
458
+ if screen.root:
459
+ _collect_state_recursive(screen.root, states)
460
+ return states
461
+
462
+
463
+ def _collect_state_recursive(node: WidgetNode, states: dict[str, str]) -> None:
464
+ if node.state:
465
+ states[node.state] = "int" # default type
466
+ if node.action and "update" in node.action:
467
+ states[node.action["update"]] = "int"
468
+ for child in node.children:
469
+ _collect_state_recursive(child, states)
470
+
471
+
472
+ # ---- Flutter code generation from UI DSL ----
473
+
474
+ def ui_dsl_to_flutter(screen: Screen) -> str:
475
+ """Convert a parsed UI DSL Screen to Flutter Dart code.
476
+
477
+ This generates a complete Flutter widget that can be merged into
478
+ main.dart alongside the existing UI generation.
479
+ """
480
+ lines: list[str] = []
481
+ lines.append("// AUTO-GENERATED from GE UI DSL — do not edit by hand.")
482
+ lines.append("import 'package:flutter/material.dart';")
483
+ lines.append("import 'bindings.dart' as ffi;")
484
+ lines.append("")
485
+
486
+ # generate state class
487
+ class_name = screen.title.replace(" ", "") + "Screen"
488
+ lines.append(f"class {class_name} extends StatefulWidget {{")
489
+ lines.append(f" const {class_name}({{super.key}});")
490
+ lines.append(f" @override")
491
+ lines.append(f" State<{class_name}> createState() => _{class_name}State();")
492
+ lines.append(f"}}")
493
+ lines.append("")
494
+
495
+ # generate state class body
496
+ lines.append(f"class _{class_name}State extends State<{class_name}> {{")
497
+ for sv in screen.state_vars:
498
+ dart_type = _dart_type(sv.type)
499
+ default_val = _dart_default(sv.default, sv.type)
500
+ lines.append(f" {dart_type} {sv.name} = {default_val};")
501
+ lines.append("")
502
+
503
+ lines.append(" @override")
504
+ lines.append(" Widget build(BuildContext context) {")
505
+ lines.append(" return Scaffold(")
506
+ lines.append(f" appBar: AppBar(title: const Text('{screen.title}')),")
507
+ lines.append(" body: " + _widget_to_flutter(screen.root, 8))
508
+ lines.append(" );")
509
+ lines.append(" }")
510
+ lines.append("}")
511
+
512
+ return "\n".join(lines) + "\n"
513
+
514
+
515
+ def _dart_type(ge_type: str) -> str:
516
+ return {"int": "int", "float": "double", "bool": "bool",
517
+ "str": "String", "string": "String"}.get(ge_type, "int")
518
+
519
+
520
+ def _dart_default(val: Any, ge_type: str) -> str:
521
+ if val is None:
522
+ return {"int": "0", "float": "0.0", "bool": "false",
523
+ "str": "''", "string": "''"}.get(ge_type, "0")
524
+ if isinstance(val, str):
525
+ return f"'{val}'"
526
+ return str(val)
527
+
528
+
529
+ def _widget_to_flutter(node: WidgetNode | None, indent: int) -> str:
530
+ """Convert a widget node to Flutter Dart code."""
531
+ if node is None:
532
+ return "const SizedBox.shrink()"
533
+ ind = " " * indent
534
+ kind = node.kind
535
+
536
+ if kind == "Column":
537
+ children = ",\n".join(_widget_to_flutter(c, indent + 6) for c in node.children)
538
+ return f"Column(\n{ind} crossAxisAlignment: CrossAxisAlignment.start,\n{ind} children: [\n{children},\n{ind} ],\n{ind})"
539
+ elif kind == "Row":
540
+ children = ",\n".join(_widget_to_flutter(c, indent + 6) for c in node.children)
541
+ return f"Row(\n{ind} children: [\n{children},\n{ind} ],\n{ind})"
542
+ elif kind == "Text":
543
+ if node.state:
544
+ return f"Text('$\\{{{node.state}\\}}', style: {_style_to_flutter(node.style_dict)})"
545
+ elif node.text:
546
+ return f"Text('{node.text}', style: {_style_to_flutter(node.style_dict)})"
547
+ return "Text('')"
548
+ elif kind == "Divider":
549
+ return "const Divider()"
550
+ elif kind == "SizedBox":
551
+ h = node.props.get("height", "")
552
+ w = node.props.get("width", "")
553
+ if h and w:
554
+ return f"const SizedBox(height: {h}, width: {w})"
555
+ elif h:
556
+ return f"const SizedBox(height: {h})"
557
+ elif w:
558
+ return f"const SizedBox(width: {w})"
559
+ return "const SizedBox.shrink()"
560
+ elif kind == "ElevatedButton":
561
+ label = node.label or node.text or "Click"
562
+ if node.action:
563
+ call = node.action.get("call", "")
564
+ update = node.action.get("update", "")
565
+ if update:
566
+ return f"ElevatedButton(onPressed: () {{ setState(() {{ {update} = ffi.{call}(); }}); }}, child: const Text('{label}'))"
567
+ else:
568
+ return f"ElevatedButton(onPressed: () {{ ffi.{call}(); }}, child: const Text('{label}'))"
569
+ return f"ElevatedButton(onPressed: () {{}}, child: const Text('{label}'))"
570
+ elif kind == "Container":
571
+ padding = node.style_dict.get("padding", 0)
572
+ child = _widget_to_flutter(node.children[0] if node.children else None, indent + 2)
573
+ return f"Container(padding: const EdgeInsets.all({padding}), child: {child})"
574
+ elif kind == "Expanded":
575
+ child = _widget_to_flutter(node.children[0] if node.children else None, indent + 2)
576
+ return f"Expanded(child: {child})"
577
+ elif kind == "Card":
578
+ child = _widget_to_flutter(node.children[0] if node.children else None, indent + 2)
579
+ return f"Card(child: {child})"
580
+ elif kind == "Center":
581
+ child = _widget_to_flutter(node.children[0] if node.children else None, indent + 2)
582
+ return f"Center(child: {child})"
583
+ elif kind == "Padding":
584
+ padding = node.props.get("padding", 16)
585
+ child = _widget_to_flutter(node.children[0] if node.children else None, indent + 2)
586
+ return f"Padding(padding: const EdgeInsets.all({padding}), child: {child})"
587
+ else:
588
+ return f"// Unknown widget: {kind}"
589
+
590
+
591
+ def _style_to_flutter(style_dict: dict[str, Any]) -> str:
592
+ """Convert a style dict to a Flutter TextStyle."""
593
+ if not style_dict:
594
+ return "const TextStyle()"
595
+ parts = []
596
+ if "fontSize" in style_dict:
597
+ parts.append(f"fontSize: {style_dict['fontSize']}")
598
+ if style_dict.get("bold"):
599
+ parts.append("fontWeight: FontWeight.bold")
600
+ if "color" in style_dict:
601
+ color = style_dict["color"]
602
+ if color.startswith("#"):
603
+ parts.append(f"color: const Color(0xFF{color[1:]})")
604
+ else:
605
+ parts.append(f"color: Colors.{_map_color(color)}")
606
+ if not parts:
607
+ return "const TextStyle()"
608
+ return f"TextStyle({', '.join(parts)})"
609
+
610
+
611
+ def _map_color(color: str) -> str:
612
+ """Map a named color to a Flutter Colors name."""
613
+ mapping = {
614
+ "red": "red", "blue": "blue", "green": "green",
615
+ "black": "black", "white": "white", "grey": "grey",
616
+ "orange": "orange", "purple": "purple", "teal": "teal",
617
+ }
618
+ return mapping.get(color, "black")