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,232 @@
1
+ """Basic type checker for GE source.
2
+
3
+ Runs before code emission to catch type errors early, producing structured
4
+ diagnostics instead of letting the native compiler fail with confusing errors.
5
+
6
+ Checks:
7
+ - Parameter types are annotated (GE003)
8
+ - Return type is annotated
9
+ - No mixing of int and float in arithmetic without explicit cast
10
+ - Function calls reference defined functions with matching signatures
11
+ - List operations are only on list-typed values
12
+ - Boolean conditions are bool-typed
13
+ - Return statements match the declared return type
14
+
15
+ This is NOT a full Hindley-Milner type system. It's a pragmatic checker that
16
+ catches the most common errors before the native compiler sees them.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import ast
21
+ from .analyzer import FuncUnit, parse_source
22
+ from .diagnostics import ErrorReporter, Diagnostic
23
+
24
+
25
+ # types we track
26
+ SCALAR_TYPES = {"int", "float", "bool", "str"}
27
+ CONTAINER_TYPES = {"list"}
28
+
29
+
30
+ class TypeChecker:
31
+ """Walks the AST of each function and checks type consistency."""
32
+
33
+ def __init__(self, file: str = ""):
34
+ self.file = file
35
+ self.errors = ErrorReporter()
36
+ self.func_sigs: dict[str, tuple[list[str], str]] = {} # name -> (param_types, ret_type)
37
+ self.func_names: set[str] = set()
38
+ self.class_names: set[str] = set()
39
+
40
+ def check(self, units: list[FuncUnit], classes: list | None = None,
41
+ entry: str = "") -> ErrorReporter:
42
+ """Check all functions and return the error reporter."""
43
+ # UI widget names are known (used in build() functions)
44
+ ui_widgets = {"Text", "Column", "Row", "Container", "ElevatedButton",
45
+ "SizedBox", "Divider", "Expanded", "Action", "build"}
46
+ self.func_names.update(ui_widgets)
47
+ # first pass: collect function signatures
48
+ for u in units:
49
+ if u.supported and u.body:
50
+ param_types = [t for _, t in u.params]
51
+ self.func_sigs[u.name] = (param_types, u.ret_type)
52
+ self.func_names.add(u.name)
53
+ # collect class names
54
+ if classes:
55
+ for cls in classes:
56
+ self.class_names.add(cls.name)
57
+
58
+ # second pass: check each function body (skip UI build() functions)
59
+ for u in units:
60
+ if u.supported and u.body and u.name != "build":
61
+ self._check_function(u)
62
+
63
+ # check entry point exists
64
+ if entry and entry not in self.func_names:
65
+ self.errors.error("GE008",
66
+ f"entry point '{entry}' not found in source",
67
+ file=self.file)
68
+
69
+ return self.errors
70
+
71
+ def _check_function(self, unit: FuncUnit) -> None:
72
+ """Check a single function body for type errors."""
73
+ # check for untyped parameters (look at raw AST annotations)
74
+ for arg in unit.body.args.args:
75
+ if arg.annotation is None:
76
+ self.errors.error("GE003",
77
+ f"parameter '{arg.arg}' has no type annotation — GE requires typed parameters",
78
+ file=self.file, line=arg.lineno)
79
+ # check for untyped return (except main)
80
+ if unit.body.returns is None and unit.name != "main":
81
+ self.errors.error("GE003",
82
+ f"function '{unit.name}' has no return type annotation — GE requires typed returns",
83
+ file=self.file, line=unit.body.lineno)
84
+
85
+ var_types: dict[str, str] = {}
86
+ # seed params
87
+ for pname, ptype in unit.params:
88
+ var_types[pname] = ptype
89
+
90
+ for stmt in ast.walk(unit.body):
91
+ self._check_node(stmt, unit, var_types)
92
+
93
+ def _check_node(self, node: ast.AST, unit: FuncUnit,
94
+ var_types: dict[str, str]) -> None:
95
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
96
+ fname = node.func.id
97
+ # check if calling a known function with wrong number of args
98
+ if fname in self.func_sigs:
99
+ param_types, _ = self.func_sigs[fname]
100
+ if len(node.args) != len(param_types):
101
+ self.errors.error(
102
+ "GE002",
103
+ f"function '{fname}' expects {len(param_types)} args, "
104
+ f"got {len(node.args)}",
105
+ file=self.file, line=node.lineno,
106
+ )
107
+ # check for undefined function calls
108
+ elif fname not in ("print", "len", "range", "abs", "sum", "min", "max",
109
+ "float", "int", "str", "bool", "dict", "list", "tuple",
110
+ "ge_inline", "ge_raw", "ge_preamble"):
111
+ if fname not in self.class_names:
112
+ self.errors.error(
113
+ "GE007",
114
+ f"function '{fname}' is called but not defined",
115
+ file=self.file, line=node.lineno,
116
+ )
117
+
118
+ if isinstance(node, ast.Return) and node.value is not None:
119
+ ret_type = self._infer_type(node.value, var_types)
120
+ if unit.ret_type != "None" and ret_type != "any":
121
+ if unit.ret_type == "int" and ret_type == "float":
122
+ self.errors.warning(
123
+ "GE002",
124
+ f"returning float from int function '{unit.name}' — "
125
+ f"possible truncation",
126
+ file=self.file, line=node.lineno,
127
+ )
128
+ elif unit.ret_type == "float" and ret_type == "int":
129
+ pass # int -> float is fine (widening)
130
+ elif unit.ret_type not in (ret_type, "any", "list") and ret_type not in ("any", "list"):
131
+ if unit.ret_type in SCALAR_TYPES and ret_type in SCALAR_TYPES:
132
+ self.errors.warning(
133
+ "GE002",
134
+ f"return type mismatch in '{unit.name}': "
135
+ f"declared '{unit.ret_type}', returning '{ret_type}'",
136
+ file=self.file, line=node.lineno,
137
+ )
138
+
139
+ if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
140
+ if node.value is not None:
141
+ ann_type = self._ann_type(node.annotation)
142
+ val_type = self._infer_type(node.value, var_types)
143
+ if ann_type in SCALAR_TYPES and val_type in SCALAR_TYPES:
144
+ if ann_type == "int" and val_type == "float":
145
+ self.errors.warning(
146
+ "GE002",
147
+ f"assigning float to int variable '{node.target.id}' — "
148
+ f"possible truncation",
149
+ file=self.file, line=node.lineno,
150
+ )
151
+ elif ann_type != val_type and ann_type != "float":
152
+ self.errors.error(
153
+ "GE002",
154
+ f"type mismatch: cannot assign '{val_type}' to '{ann_type}' variable '{node.target.id}'",
155
+ file=self.file, line=node.lineno,
156
+ )
157
+
158
+ def _infer_type(self, node: ast.AST, var_types: dict[str, str]) -> str:
159
+ if isinstance(node, ast.Constant):
160
+ if isinstance(node.value, bool):
161
+ return "bool"
162
+ if isinstance(node.value, int):
163
+ return "int"
164
+ if isinstance(node.value, float):
165
+ return "float"
166
+ if isinstance(node.value, str):
167
+ return "str"
168
+ if isinstance(node, ast.Name):
169
+ return var_types.get(node.id, "any")
170
+ if isinstance(node, ast.BinOp):
171
+ lt = self._infer_type(node.left, var_types)
172
+ rt = self._infer_type(node.right, var_types)
173
+ if isinstance(node.op, ast.Div):
174
+ return "float"
175
+ if "float" in (lt, rt):
176
+ return "float"
177
+ return "int"
178
+ if isinstance(node, ast.List):
179
+ return "list"
180
+ if isinstance(node, ast.Dict):
181
+ return "dict"
182
+ if isinstance(node, ast.Tuple):
183
+ return "tuple"
184
+ if isinstance(node, ast.JoinedStr):
185
+ return "str"
186
+ if isinstance(node, ast.Subscript):
187
+ base = self._infer_type(node.value, var_types)
188
+ if isinstance(node.slice, ast.Slice):
189
+ return base if base in ("str", "list") else "any"
190
+ if base == "str":
191
+ return "int"
192
+ if base == "dict":
193
+ return "any"
194
+ return "int"
195
+ if isinstance(node, ast.Call):
196
+ fname = getattr(node.func, "id", None)
197
+ if fname == "len":
198
+ return "int"
199
+ if fname == "float":
200
+ return "float"
201
+ if fname == "int":
202
+ return "int"
203
+ if fname in self.func_sigs:
204
+ _, ret = self.func_sigs[fname]
205
+ return ret
206
+ return "any"
207
+
208
+ def _ann_type(self, node: ast.AST) -> str:
209
+ if isinstance(node, ast.Name):
210
+ t = node.id
211
+ return t if t in ("int", "float", "bool", "str", "list", "dict", "tuple") else "any"
212
+ if isinstance(node, ast.Subscript):
213
+ base = getattr(node.value, "id", "")
214
+ if base == "dict":
215
+ return "dict"
216
+ if base == "tuple":
217
+ return "tuple"
218
+ return "list"
219
+ return "any"
220
+
221
+
222
+ def check_source(source: str, file: str = "", entry: str = "",
223
+ classes: list | None = None,
224
+ units: list[FuncUnit] | None = None) -> ErrorReporter:
225
+ """Parse and type-check a GE source file. Returns the error reporter.
226
+
227
+ If `units` is provided, use them instead of parsing (for import resolution).
228
+ """
229
+ if units is None:
230
+ units = parse_source(source)
231
+ checker = TypeChecker(file=file)
232
+ return checker.check(units, classes=classes, entry=entry)
@@ -0,0 +1,154 @@
1
+ """Interpret the `build()` function's AST into a widget tree.
2
+
3
+ Does NOT execute the Python — it walks the AST of `build()` and evaluates
4
+ widget-constructor calls with literal/nested args into a plain dict tree. This
5
+ is safe (no arbitrary code execution) and deterministic.
6
+
7
+ Tree node shapes:
8
+ {"kind": "Text", "text": str, "style": str, "state": str}
9
+ {"kind": "Column"|"Row", "children": [node,...], "align": str}
10
+ {"kind": "Container", "child": node|None, "padding": int, "color": str}
11
+ {"kind": "Button", "label": str, "action": action_node|None}
12
+ {"kind": "Action", "call": str, "args": [literal,...], "update": str}
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import ast
17
+
18
+ from .styling import parse_style
19
+
20
+ WIDGETS = {"Text", "Column", "Row", "Container", "ElevatedButton",
21
+ "SizedBox", "Divider", "Expanded"}
22
+
23
+
24
+ class UIParseError(Exception):
25
+ pass
26
+
27
+
28
+ def _lit(node: ast.AST):
29
+ """Evaluate a literal/constant node."""
30
+ if isinstance(node, ast.Constant):
31
+ return node.value
32
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
33
+ return -_lit(node.operand)
34
+ raise UIParseError(f"expected a literal, got {type(node).__name__}")
35
+
36
+
37
+ def _parse_action_or_list(on_click):
38
+ """on_click can be a single Action dict or a list of Action dicts."""
39
+ if on_click is None:
40
+ return None
41
+ if isinstance(on_click, list):
42
+ return on_click
43
+ return [on_click]
44
+
45
+
46
+ def _eval(node: ast.AST):
47
+ """Evaluate a widget expression node into a tree dict."""
48
+ if isinstance(node, ast.Constant):
49
+ return node.value
50
+ if isinstance(node, ast.List):
51
+ return [_eval(e) for e in node.elts]
52
+ if isinstance(node, ast.Call):
53
+ fname = getattr(node.func, "id", None)
54
+ if fname == "Action":
55
+ kw = {k.arg: _lit(k.value) if isinstance(k.value, ast.Constant) else _eval(k.value)
56
+ for k in node.keywords}
57
+ args = [_eval(a) for a in node.args]
58
+ call = kw.get("call") or (args[0] if args else "")
59
+ arglist = kw.get("args") or (args[1] if len(args) > 1 else [])
60
+ update = kw.get("update") or (args[2] if len(args) > 2 else "")
61
+ return {"kind": "Action", "call": call, "args": arglist, "update": update}
62
+ if fname in WIDGETS:
63
+ kw = {k.arg: (_lit(k.value) if isinstance(k.value, ast.Constant) else _eval(k.value))
64
+ for k in node.keywords}
65
+ pos = [_eval(a) for a in node.args]
66
+ if fname == "Text":
67
+ text = kw.get("text") or (pos[0] if pos else "")
68
+ style_raw = kw.get("style", "body")
69
+ return {"kind": "Text", "text": text,
70
+ "style": style_raw, "style_dict": parse_style(style_raw),
71
+ "state": kw.get("state", "")}
72
+ if fname in ("Column", "Row"):
73
+ children = kw.get("children") or (pos[0] if pos else [])
74
+ style_raw = kw.get("style", "")
75
+ return {"kind": fname, "children": children,
76
+ "align": kw.get("align", "center"),
77
+ "style_dict": parse_style(style_raw)}
78
+ if fname == "Container":
79
+ child = kw.get("child") or (pos[0] if pos else None)
80
+ style_raw = kw.get("style", "")
81
+ return {"kind": "Container", "child": child,
82
+ "padding": kw.get("padding", 8), "color": kw.get("color", ""),
83
+ "style_dict": parse_style(style_raw)}
84
+ if fname == "ElevatedButton":
85
+ label = kw.get("label") or (pos[0] if pos else "")
86
+ style_raw = kw.get("style", "")
87
+ # on_click can be a single Action or a list of Actions
88
+ on_click = kw.get("on_click")
89
+ action = _parse_action_or_list(on_click)
90
+ return {"kind": "Button", "label": label, "action": action,
91
+ "style_dict": parse_style(style_raw)}
92
+ if fname == "SizedBox":
93
+ return {"kind": "SizedBox",
94
+ "height": kw.get("height", 0),
95
+ "width": kw.get("width", 0),
96
+ "style_dict": parse_style(kw.get("style", ""))}
97
+ if fname == "Divider":
98
+ return {"kind": "Divider",
99
+ "style_dict": parse_style(kw.get("style", ""))}
100
+ if fname == "Expanded":
101
+ child = kw.get("child") or (pos[0] if pos else None)
102
+ return {"kind": "Expanded", "child": child,
103
+ "flex": kw.get("flex", 1),
104
+ "style_dict": parse_style(kw.get("style", ""))}
105
+ raise UIParseError(f"unsupported call: {fname}")
106
+ raise UIParseError(f"unsupported expression: {type(node).__name__}")
107
+
108
+
109
+ def parse_ui(source: str) -> dict:
110
+ """Parse a Python source string, find `build()`, return its widget tree."""
111
+ tree = ast.parse(source)
112
+ build_fn = None
113
+ for node in tree.body:
114
+ if isinstance(node, ast.FunctionDef) and node.name == "build":
115
+ build_fn = node
116
+ break
117
+ if build_fn is None:
118
+ raise UIParseError("no build() function found in UI source")
119
+ # build() must be a single return of a widget tree
120
+ ret = None
121
+ for stmt in build_fn.body:
122
+ if isinstance(stmt, ast.Return):
123
+ ret = stmt
124
+ break
125
+ if ret is None or ret.value is None:
126
+ raise UIParseError("build() must return a widget tree")
127
+ return _eval(ret.value)
128
+
129
+
130
+ def collect_state(tree: dict) -> set[str]:
131
+ """Find all state field names referenced (Text state= / Action update=)."""
132
+ fields: set[str] = set()
133
+
134
+ def walk(n):
135
+ if not isinstance(n, dict):
136
+ return
137
+ k = n.get("kind")
138
+ if k == "Text" and n.get("state"):
139
+ fields.add(n["state"])
140
+ if k == "Button" and isinstance(n.get("action"), list):
141
+ for act in n["action"]:
142
+ upd = act.get("update")
143
+ if upd:
144
+ fields.add(upd)
145
+ for key in ("children", "child"):
146
+ v = n.get(key)
147
+ if isinstance(v, list):
148
+ for c in v:
149
+ walk(c)
150
+ elif isinstance(v, dict):
151
+ walk(v)
152
+
153
+ walk(tree)
154
+ return fields