gelang 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/bin/ge.js +112 -0
- package/package.json +62 -0
- package/python/pyeffic/__init__.py +9 -0
- package/python/pyeffic/__main__.py +6 -0
- package/python/pyeffic/analyzer.py +464 -0
- package/python/pyeffic/apisurface.py +238 -0
- package/python/pyeffic/autoselect.py +327 -0
- package/python/pyeffic/backends.py +87 -0
- package/python/pyeffic/bench.py +233 -0
- package/python/pyeffic/cli.py +184 -0
- package/python/pyeffic/compiler.py +421 -0
- package/python/pyeffic/config.py +383 -0
- package/python/pyeffic/dartgen.py +441 -0
- package/python/pyeffic/deploy.py +586 -0
- package/python/pyeffic/diagnostics.py +194 -0
- package/python/pyeffic/difftest.py +424 -0
- package/python/pyeffic/downloader.py +307 -0
- package/python/pyeffic/emitters/__init__.py +11 -0
- package/python/pyeffic/emitters/base.py +2359 -0
- package/python/pyeffic/emitters/cpp.py +266 -0
- package/python/pyeffic/emitters/csharp.py +342 -0
- package/python/pyeffic/emitters/dart.py +349 -0
- package/python/pyeffic/emitters/go.py +388 -0
- package/python/pyeffic/emitters/kotlin.py +314 -0
- package/python/pyeffic/emitters/rust.py +314 -0
- package/python/pyeffic/emitters/zig.py +411 -0
- package/python/pyeffic/ffi.py +49 -0
- package/python/pyeffic/frontends/__init__.py +94 -0
- package/python/pyeffic/frontends/hybrid.py +709 -0
- package/python/pyeffic/frontends/typescript.py +965 -0
- package/python/pyeffic/ge_cli.py +1148 -0
- package/python/pyeffic/golden.py +348 -0
- package/python/pyeffic/idents.py +206 -0
- package/python/pyeffic/modules.py +220 -0
- package/python/pyeffic/packer.py +222 -0
- package/python/pyeffic/pipeline.py +797 -0
- package/python/pyeffic/reactgen.py +966 -0
- package/python/pyeffic/researcher.py +177 -0
- package/python/pyeffic/scaffold.py +397 -0
- package/python/pyeffic/stdlib.py +246 -0
- package/python/pyeffic/styling.py +220 -0
- package/python/pyeffic/templates/desktop_gui/README.md +106 -0
- package/python/pyeffic/templates/desktop_gui/app/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/add.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/factorial.ge.py +20 -0
- package/python/pyeffic/templates/desktop_gui/app/core/fibonacci.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/core/gcd.ge.py +19 -0
- package/python/pyeffic/templates/desktop_gui/app/core/is_prime.ge.py +24 -0
- package/python/pyeffic/templates/desktop_gui/app/core/multiply.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/power.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/main.ge.py +49 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/buffer.ge.py +26 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/limits.ge.py +47 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/state.ge.py +44 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/layout.ge.py +64 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/render.ge.py +87 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/theme.ge.py +147 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/widgets.ge.py +105 -0
- package/python/pyeffic/templates/desktop_gui/desktop/__init__.py +1 -0
- package/python/pyeffic/templates/desktop_gui/desktop/main.ge.py +258 -0
- package/python/pyeffic/templates/desktop_gui/ge.toml +16 -0
- package/python/pyeffic/templates/desktop_gui/tests/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/tests/ge_loader.py +76 -0
- package/python/pyeffic/templates/desktop_gui/tests/test_app.py +173 -0
- package/python/pyeffic/templates/web_react/README.md +115 -0
- package/python/pyeffic/templates/web_react/app/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/add.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/core/factorial.ge.py +16 -0
- package/python/pyeffic/templates/web_react/app/core/fibonacci.ge.py +21 -0
- package/python/pyeffic/templates/web_react/app/core/is_prime.ge.py +20 -0
- package/python/pyeffic/templates/web_react/app/core/multiply.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/main.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/memory/buffer.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/limits.ge.py +51 -0
- package/python/pyeffic/templates/web_react/ge.toml +23 -0
- package/python/pyeffic/templates/web_react/tests/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/tests/ge_loader.py +68 -0
- package/python/pyeffic/templates/web_react/tests/test_app.py +105 -0
- package/python/pyeffic/templates/web_react/ui/main.ge.ui +33 -0
- package/python/pyeffic/templates/web_react/web/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/web/server.ge.py +78 -0
- package/python/pyeffic/ts2py.py +657 -0
- package/python/pyeffic/typecheck.py +232 -0
- package/python/pyeffic/ui.py +154 -0
- package/python/pyeffic/ui_dsl.py +618 -0
- package/python/pyeffic/widgets.py +87 -0
- package/scripts/README.md +42 -0
- package/scripts/check-toolchains.py +85 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
"""TypeScript to Python transpiler for the GE language subset.
|
|
2
|
+
|
|
3
|
+
Users can write GE source in either Python or TypeScript. Both convert to
|
|
4
|
+
Rust/C++/Dart via the same pipeline:
|
|
5
|
+
|
|
6
|
+
TypeScript (.ts) → Python source → [existing GE pipeline] → Rust/C++/Dart
|
|
7
|
+
Python (.ge.py) → [existing GE pipeline] → Rust/C++/Dart
|
|
8
|
+
|
|
9
|
+
Supported TS subset:
|
|
10
|
+
- Functions with typed params and return types
|
|
11
|
+
- let/const variable declarations with types
|
|
12
|
+
- if/else, for, while control flow
|
|
13
|
+
- Arithmetic, comparison, logical operators
|
|
14
|
+
- Array literals, indexing, .length, .push()
|
|
15
|
+
- Widget DSL (build() function with widget constructors)
|
|
16
|
+
- CSS-like style strings
|
|
17
|
+
- console.log() → print()
|
|
18
|
+
- Math.idiv(a, b) → a // b (integer division, GE extension)
|
|
19
|
+
|
|
20
|
+
Type mappings:
|
|
21
|
+
number → int (GE default: integer arithmetic for financial calc)
|
|
22
|
+
int → int, float → float, string → str, boolean → bool, void → None
|
|
23
|
+
T[] → list, Widget → None
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import List, Optional
|
|
30
|
+
|
|
31
|
+
# ---- Type mappings ----
|
|
32
|
+
TS_TO_PY_TYPES = {
|
|
33
|
+
"number": "int",
|
|
34
|
+
"int": "int",
|
|
35
|
+
"float": "float",
|
|
36
|
+
"string": "str",
|
|
37
|
+
"boolean": "bool",
|
|
38
|
+
"bool": "bool",
|
|
39
|
+
"void": "None",
|
|
40
|
+
"Widget": "None",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Widget names (for DSL translation: object args → keyword args)
|
|
44
|
+
WIDGETS = {"Text", "Column", "Row", "Container", "ElevatedButton",
|
|
45
|
+
"SizedBox", "Divider", "Expanded"}
|
|
46
|
+
|
|
47
|
+
# camelCase → snake_case mapping for widget kwargs
|
|
48
|
+
CAMEL_TO_SNAKE = {
|
|
49
|
+
"onClick": "on_click",
|
|
50
|
+
"children": "children",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Positional arg keys (first positional in Python widget DSL)
|
|
54
|
+
POSITIONAL_KEYS = {"children", "text", "label"}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Token:
|
|
59
|
+
kind: str
|
|
60
|
+
value: str
|
|
61
|
+
line: int
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---- Tokenizer ----
|
|
65
|
+
|
|
66
|
+
_MASTER_RE = re.compile(r'''
|
|
67
|
+
(?P<COMMENT_LINE>//[^\n]*)
|
|
68
|
+
| (?P<COMMENT_BLOCK>/\*.*?\*/)
|
|
69
|
+
| (?P<TEMPLATE>`(?:[^`\\]|\\.)*`)
|
|
70
|
+
| (?P<STRING>"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')
|
|
71
|
+
| (?P<NUMBER>\d+\.\d+|\d+)
|
|
72
|
+
| (?P<OP>===|!==|==|!=|<=|>=|&&|\|\||\+\+|--|\+=|-=|[+\-*/%<>=!&|])
|
|
73
|
+
| (?P<PUNCT>[(){}\[\];:,.])
|
|
74
|
+
| (?P<IDENT>[A-Za-z_][A-Za-z0-9_]*)
|
|
75
|
+
| (?P<SKIP>\s+)
|
|
76
|
+
| (?P<UNKNOWN>.)
|
|
77
|
+
''', re.VERBOSE | re.DOTALL)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def tokenize(source: str) -> List[Token]:
|
|
81
|
+
tokens = []
|
|
82
|
+
line = 1
|
|
83
|
+
for m in _MASTER_RE.finditer(source):
|
|
84
|
+
kind = m.lastgroup
|
|
85
|
+
value = m.group()
|
|
86
|
+
line += value.count('\n')
|
|
87
|
+
if kind in ("SKIP", "COMMENT_LINE", "COMMENT_BLOCK"):
|
|
88
|
+
continue
|
|
89
|
+
if kind == "TEMPLATE":
|
|
90
|
+
# treat template strings like regular strings
|
|
91
|
+
kind = "STRING"
|
|
92
|
+
if kind == "UNKNOWN":
|
|
93
|
+
continue
|
|
94
|
+
tokens.append(Token(kind, value, line - value.count('\n')))
|
|
95
|
+
tokens.append(Token("EOF", "", line))
|
|
96
|
+
return tokens
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---- Parser (recursive descent → Python source) ----
|
|
100
|
+
|
|
101
|
+
class TSParser:
|
|
102
|
+
def __init__(self, tokens: List[Token]):
|
|
103
|
+
self.tokens = tokens
|
|
104
|
+
self.pos = 0
|
|
105
|
+
self.lines: List[str] = []
|
|
106
|
+
self.indent = 0
|
|
107
|
+
|
|
108
|
+
def emit(self, line: str = ""):
|
|
109
|
+
self.lines.append(" " * self.indent + line)
|
|
110
|
+
|
|
111
|
+
# --- token helpers ---
|
|
112
|
+
def peek(self, offset=0) -> Optional[Token]:
|
|
113
|
+
i = self.pos + offset
|
|
114
|
+
return self.tokens[i] if i < len(self.tokens) else None
|
|
115
|
+
|
|
116
|
+
def consume(self) -> Token:
|
|
117
|
+
t = self.tokens[self.pos]
|
|
118
|
+
self.pos += 1
|
|
119
|
+
return t
|
|
120
|
+
|
|
121
|
+
def match(self, value: str) -> bool:
|
|
122
|
+
t = self.peek()
|
|
123
|
+
return t is not None and t.value == value
|
|
124
|
+
|
|
125
|
+
def match_kind(self, kind: str) -> bool:
|
|
126
|
+
t = self.peek()
|
|
127
|
+
return t is not None and t.kind == kind
|
|
128
|
+
|
|
129
|
+
def accept(self, value: str) -> Optional[Token]:
|
|
130
|
+
if self.match(value):
|
|
131
|
+
return self.consume()
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def expect(self, value: str) -> Token:
|
|
135
|
+
if not self.match(value):
|
|
136
|
+
t = self.peek()
|
|
137
|
+
raise SyntaxError(
|
|
138
|
+
f"Expected '{value}', got '{t.value if t else 'EOF'}' at line {t.line if t else '?'}")
|
|
139
|
+
return self.consume()
|
|
140
|
+
|
|
141
|
+
# --- program ---
|
|
142
|
+
def parse_program(self) -> str:
|
|
143
|
+
while self.peek() and self.peek().kind != "EOF":
|
|
144
|
+
if self.match("import"):
|
|
145
|
+
self.parse_import()
|
|
146
|
+
elif self.match("function"):
|
|
147
|
+
self.parse_function()
|
|
148
|
+
elif self.peek().value in ("let", "const"):
|
|
149
|
+
self.parse_module_var()
|
|
150
|
+
else:
|
|
151
|
+
# skip unknown module-level code
|
|
152
|
+
self.consume()
|
|
153
|
+
return "\n".join(self.lines)
|
|
154
|
+
|
|
155
|
+
def parse_import(self):
|
|
156
|
+
self.consume() # import
|
|
157
|
+
if self.accept("{"):
|
|
158
|
+
names = []
|
|
159
|
+
while not self.match("}"):
|
|
160
|
+
names.append(self.consume().value)
|
|
161
|
+
self.accept(",")
|
|
162
|
+
self.expect("}")
|
|
163
|
+
self.expect("from")
|
|
164
|
+
mod = self.consume().value.strip('"\'`')
|
|
165
|
+
self.accept(";")
|
|
166
|
+
if "widgets" in mod:
|
|
167
|
+
self.emit(f"from pyeffic.widgets import {', '.join(names)}")
|
|
168
|
+
else:
|
|
169
|
+
# default import — skip
|
|
170
|
+
self.consume()
|
|
171
|
+
self.expect("from")
|
|
172
|
+
self.consume()
|
|
173
|
+
self.accept(";")
|
|
174
|
+
|
|
175
|
+
def parse_module_var(self):
|
|
176
|
+
self.consume() # let/const
|
|
177
|
+
name = self.consume().value
|
|
178
|
+
vtype = ""
|
|
179
|
+
if self.accept(":"):
|
|
180
|
+
vtype = self.parse_type()
|
|
181
|
+
self.expect("=")
|
|
182
|
+
val = self.parse_expr()
|
|
183
|
+
self.accept(";")
|
|
184
|
+
if vtype:
|
|
185
|
+
self.emit(f"{name}: {vtype} = {val}")
|
|
186
|
+
else:
|
|
187
|
+
self.emit(f"{name} = {val}")
|
|
188
|
+
|
|
189
|
+
# --- functions ---
|
|
190
|
+
def parse_function(self):
|
|
191
|
+
self.consume() # function
|
|
192
|
+
name = self.consume().value
|
|
193
|
+
self.expect("(")
|
|
194
|
+
params = self.parse_params()
|
|
195
|
+
self.expect(")")
|
|
196
|
+
ret_type = "None"
|
|
197
|
+
if self.accept(":"):
|
|
198
|
+
ret_type = self.parse_type()
|
|
199
|
+
self.expect("{")
|
|
200
|
+
self.emit(f"def {name}({params}) -> {ret_type}:")
|
|
201
|
+
self.indent += 1
|
|
202
|
+
self.parse_stmts()
|
|
203
|
+
self.indent -= 1
|
|
204
|
+
self.expect("}")
|
|
205
|
+
self.emit() # blank line after function
|
|
206
|
+
|
|
207
|
+
def parse_params(self) -> str:
|
|
208
|
+
params = []
|
|
209
|
+
while not self.match(")"):
|
|
210
|
+
name = self.consume().value
|
|
211
|
+
ptype = ""
|
|
212
|
+
if self.accept(":"):
|
|
213
|
+
ptype = self.parse_type()
|
|
214
|
+
if ptype:
|
|
215
|
+
params.append(f"{name}: {ptype}")
|
|
216
|
+
else:
|
|
217
|
+
params.append(name)
|
|
218
|
+
self.accept(",")
|
|
219
|
+
return ", ".join(params)
|
|
220
|
+
|
|
221
|
+
def parse_type(self) -> str:
|
|
222
|
+
base = self.consume().value
|
|
223
|
+
py_type = TS_TO_PY_TYPES.get(base, base)
|
|
224
|
+
# handle T[] (array type)
|
|
225
|
+
while self.accept("["):
|
|
226
|
+
self.expect("]")
|
|
227
|
+
py_type = "list"
|
|
228
|
+
return py_type
|
|
229
|
+
|
|
230
|
+
# --- statements ---
|
|
231
|
+
def parse_stmts(self):
|
|
232
|
+
while not self.match("}") and self.peek().kind != "EOF":
|
|
233
|
+
self.parse_stmt()
|
|
234
|
+
|
|
235
|
+
def parse_stmt(self):
|
|
236
|
+
t = self.peek()
|
|
237
|
+
if t is None or t.kind == "EOF":
|
|
238
|
+
return
|
|
239
|
+
if t.value in ("let", "const"):
|
|
240
|
+
self.parse_var_decl()
|
|
241
|
+
elif t.value == "if":
|
|
242
|
+
self.parse_if()
|
|
243
|
+
elif t.value == "for":
|
|
244
|
+
self.parse_for()
|
|
245
|
+
elif t.value == "while":
|
|
246
|
+
self.parse_while()
|
|
247
|
+
elif t.value == "return":
|
|
248
|
+
self.parse_return()
|
|
249
|
+
else:
|
|
250
|
+
# expression statement or assignment
|
|
251
|
+
expr = self.parse_expr()
|
|
252
|
+
if self.match("="):
|
|
253
|
+
self.consume()
|
|
254
|
+
val = self.parse_expr()
|
|
255
|
+
self.emit(f"{expr} = {val}")
|
|
256
|
+
else:
|
|
257
|
+
self.emit(expr)
|
|
258
|
+
self.accept(";")
|
|
259
|
+
|
|
260
|
+
def parse_var_decl(self):
|
|
261
|
+
self.consume() # let/const
|
|
262
|
+
name = self.consume().value
|
|
263
|
+
vtype = ""
|
|
264
|
+
if self.accept(":"):
|
|
265
|
+
vtype = self.parse_type()
|
|
266
|
+
self.expect("=")
|
|
267
|
+
val = self.parse_expr()
|
|
268
|
+
self.accept(";")
|
|
269
|
+
if vtype:
|
|
270
|
+
self.emit(f"{name}: {vtype} = {val}")
|
|
271
|
+
else:
|
|
272
|
+
self.emit(f"{name} = {val}")
|
|
273
|
+
|
|
274
|
+
def parse_if(self):
|
|
275
|
+
self.consume() # if
|
|
276
|
+
self.expect("(")
|
|
277
|
+
cond = self.parse_expr()
|
|
278
|
+
self.expect(")")
|
|
279
|
+
self.expect("{")
|
|
280
|
+
self.emit(f"if {cond}:")
|
|
281
|
+
self.indent += 1
|
|
282
|
+
self.parse_stmts()
|
|
283
|
+
self.indent -= 1
|
|
284
|
+
self.expect("}")
|
|
285
|
+
while self.match("else"):
|
|
286
|
+
self.consume() # else
|
|
287
|
+
if self.match("if"):
|
|
288
|
+
self.consume() # if
|
|
289
|
+
self.expect("(")
|
|
290
|
+
cond = self.parse_expr()
|
|
291
|
+
self.expect(")")
|
|
292
|
+
self.expect("{")
|
|
293
|
+
self.emit(f"elif {cond}:")
|
|
294
|
+
self.indent += 1
|
|
295
|
+
self.parse_stmts()
|
|
296
|
+
self.indent -= 1
|
|
297
|
+
self.expect("}")
|
|
298
|
+
else:
|
|
299
|
+
self.expect("{")
|
|
300
|
+
self.emit("else:")
|
|
301
|
+
self.indent += 1
|
|
302
|
+
self.parse_stmts()
|
|
303
|
+
self.indent -= 1
|
|
304
|
+
self.expect("}")
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
def parse_for(self):
|
|
308
|
+
self.consume() # for
|
|
309
|
+
self.expect("(")
|
|
310
|
+
# parse init: let i = start
|
|
311
|
+
var = None
|
|
312
|
+
start = "0"
|
|
313
|
+
if self.peek().value in ("let", "const"):
|
|
314
|
+
self.consume()
|
|
315
|
+
var = self.consume().value
|
|
316
|
+
self.accept(":")
|
|
317
|
+
if self.match_kind("IDENT"):
|
|
318
|
+
self.parse_type() # consume type annotation
|
|
319
|
+
self.expect("=")
|
|
320
|
+
start = self.parse_expr()
|
|
321
|
+
elif self.peek().kind == "IDENT":
|
|
322
|
+
var = self.consume().value
|
|
323
|
+
self.accept("=")
|
|
324
|
+
start = self.parse_expr()
|
|
325
|
+
self.expect(";")
|
|
326
|
+
# parse condition: var < end or var <= end
|
|
327
|
+
cond_var = self.consume().value
|
|
328
|
+
op = self.consume().value
|
|
329
|
+
end = self.parse_expr()
|
|
330
|
+
self.expect(";")
|
|
331
|
+
# parse update: var++ or var += n
|
|
332
|
+
self.consume() # var name
|
|
333
|
+
step = "1"
|
|
334
|
+
if self.accept("++"):
|
|
335
|
+
pass
|
|
336
|
+
elif self.accept("--"):
|
|
337
|
+
step = "-1"
|
|
338
|
+
elif self.accept("+="):
|
|
339
|
+
step = self.parse_expr()
|
|
340
|
+
elif self.accept("-="):
|
|
341
|
+
step = "-" + self.parse_expr()
|
|
342
|
+
self.expect(")")
|
|
343
|
+
self.expect("{")
|
|
344
|
+
|
|
345
|
+
# build range
|
|
346
|
+
if op == "<=":
|
|
347
|
+
end_expr = f"{end} + 1"
|
|
348
|
+
else:
|
|
349
|
+
end_expr = end
|
|
350
|
+
|
|
351
|
+
# handle .length in condition: i < arr.length → len(arr)
|
|
352
|
+
if step != "1":
|
|
353
|
+
self.emit(f"for {var} in range({start}, {end_expr}, {step}):")
|
|
354
|
+
else:
|
|
355
|
+
self.emit(f"for {var} in range({start}, {end_expr}):")
|
|
356
|
+
self.indent += 1
|
|
357
|
+
self.parse_stmts()
|
|
358
|
+
self.indent -= 1
|
|
359
|
+
self.expect("}")
|
|
360
|
+
|
|
361
|
+
def parse_while(self):
|
|
362
|
+
self.consume() # while
|
|
363
|
+
self.expect("(")
|
|
364
|
+
cond = self.parse_expr()
|
|
365
|
+
self.expect(")")
|
|
366
|
+
self.expect("{")
|
|
367
|
+
self.emit(f"while {cond}:")
|
|
368
|
+
self.indent += 1
|
|
369
|
+
self.parse_stmts()
|
|
370
|
+
self.indent -= 1
|
|
371
|
+
self.expect("}")
|
|
372
|
+
|
|
373
|
+
def parse_return(self):
|
|
374
|
+
self.consume() # return
|
|
375
|
+
if self.match(";") or self.match("}"):
|
|
376
|
+
self.emit("return")
|
|
377
|
+
else:
|
|
378
|
+
val = self.parse_expr()
|
|
379
|
+
self.emit(f"return {val}")
|
|
380
|
+
self.accept(";")
|
|
381
|
+
|
|
382
|
+
# --- expressions (recursive descent) ---
|
|
383
|
+
def parse_expr(self) -> str:
|
|
384
|
+
return self.parse_or()
|
|
385
|
+
|
|
386
|
+
def parse_or(self) -> str:
|
|
387
|
+
left = self.parse_and()
|
|
388
|
+
while self.match("||"):
|
|
389
|
+
self.consume()
|
|
390
|
+
right = self.parse_and()
|
|
391
|
+
left = f"{left} or {right}"
|
|
392
|
+
return left
|
|
393
|
+
|
|
394
|
+
def parse_and(self) -> str:
|
|
395
|
+
left = self.parse_not()
|
|
396
|
+
while self.match("&&"):
|
|
397
|
+
self.consume()
|
|
398
|
+
right = self.parse_not()
|
|
399
|
+
left = f"{left} and {right}"
|
|
400
|
+
return left
|
|
401
|
+
|
|
402
|
+
def parse_not(self) -> str:
|
|
403
|
+
if self.match("!"):
|
|
404
|
+
self.consume()
|
|
405
|
+
return f"not {self.parse_not()}"
|
|
406
|
+
return self.parse_comparison()
|
|
407
|
+
|
|
408
|
+
def parse_comparison(self) -> str:
|
|
409
|
+
left = self.parse_add()
|
|
410
|
+
while self.peek() and self.peek().value in ("==", "===", "!=", "!==", "<", ">", "<=", ">="):
|
|
411
|
+
op = self.consume().value
|
|
412
|
+
op = "==" if op == "===" else ("!=" if op == "!==" else op)
|
|
413
|
+
right = self.parse_add()
|
|
414
|
+
left = f"{left} {op} {right}"
|
|
415
|
+
return left
|
|
416
|
+
|
|
417
|
+
def parse_add(self) -> str:
|
|
418
|
+
left = self.parse_mul()
|
|
419
|
+
while self.peek() and self.peek().value in ("+", "-"):
|
|
420
|
+
op = self.consume().value
|
|
421
|
+
right = self.parse_mul()
|
|
422
|
+
left = f"{left} {op} {right}"
|
|
423
|
+
return left
|
|
424
|
+
|
|
425
|
+
def parse_mul(self) -> str:
|
|
426
|
+
left = self.parse_unary()
|
|
427
|
+
while self.peek() and self.peek().value in ("*", "/", "%"):
|
|
428
|
+
op = self.consume().value
|
|
429
|
+
right = self.parse_unary()
|
|
430
|
+
left = f"{left} {op} {right}"
|
|
431
|
+
return left
|
|
432
|
+
|
|
433
|
+
def parse_unary(self) -> str:
|
|
434
|
+
if self.match("-"):
|
|
435
|
+
self.consume()
|
|
436
|
+
return f"-{self.parse_unary()}"
|
|
437
|
+
if self.match("+"):
|
|
438
|
+
self.consume()
|
|
439
|
+
return self.parse_unary()
|
|
440
|
+
return self.parse_postfix()
|
|
441
|
+
|
|
442
|
+
def parse_postfix(self) -> str:
|
|
443
|
+
expr = self.parse_primary()
|
|
444
|
+
while True:
|
|
445
|
+
if self.match("["):
|
|
446
|
+
self.consume()
|
|
447
|
+
idx = self.parse_expr()
|
|
448
|
+
self.expect("]")
|
|
449
|
+
expr = f"{expr}[{idx}]"
|
|
450
|
+
elif self.match("."):
|
|
451
|
+
self.consume()
|
|
452
|
+
member = self.consume().value
|
|
453
|
+
if self.match("("):
|
|
454
|
+
self.consume() # (
|
|
455
|
+
if member == "length":
|
|
456
|
+
# arr.length() — unusual but handle it
|
|
457
|
+
self.expect(")")
|
|
458
|
+
expr = f"len({expr})"
|
|
459
|
+
elif member == "push":
|
|
460
|
+
args = self.parse_args()
|
|
461
|
+
self.expect(")")
|
|
462
|
+
expr = f"{expr}.append({args})"
|
|
463
|
+
else:
|
|
464
|
+
args = self.parse_args()
|
|
465
|
+
self.expect(")")
|
|
466
|
+
expr = self._method_call(expr, member, args)
|
|
467
|
+
else:
|
|
468
|
+
if member == "length":
|
|
469
|
+
expr = f"len({expr})"
|
|
470
|
+
else:
|
|
471
|
+
expr = f"{expr}.{member}"
|
|
472
|
+
elif self.match("("):
|
|
473
|
+
self.consume()
|
|
474
|
+
if expr in WIDGETS or expr == "Action":
|
|
475
|
+
args = self.parse_widget_args(expr)
|
|
476
|
+
else:
|
|
477
|
+
args = self.parse_args()
|
|
478
|
+
self.expect(")")
|
|
479
|
+
expr = self._function_call(expr, args)
|
|
480
|
+
else:
|
|
481
|
+
break
|
|
482
|
+
return expr
|
|
483
|
+
|
|
484
|
+
def _method_call(self, obj: str, method: str, args: str) -> str:
|
|
485
|
+
"""Translate special method calls."""
|
|
486
|
+
if obj == "Math":
|
|
487
|
+
if method == "idiv":
|
|
488
|
+
# Math.idiv(a, b) → (a // b)
|
|
489
|
+
parts = [p.strip() for p in args.split(",", 1)]
|
|
490
|
+
if len(parts) == 2:
|
|
491
|
+
return f"({parts[0]} // {parts[1]})"
|
|
492
|
+
if method == "floor":
|
|
493
|
+
return f"int({args})"
|
|
494
|
+
if method == "abs":
|
|
495
|
+
return f"abs({args})"
|
|
496
|
+
if method == "max":
|
|
497
|
+
return f"max({args})"
|
|
498
|
+
if method == "min":
|
|
499
|
+
return f"min({args})"
|
|
500
|
+
if method == "sqrt":
|
|
501
|
+
return f"({args} ** 0.5)"
|
|
502
|
+
return f"{obj}.{method}({args})"
|
|
503
|
+
|
|
504
|
+
def _function_call(self, name: str, args: str) -> str:
|
|
505
|
+
"""Translate special function calls."""
|
|
506
|
+
if name == "console":
|
|
507
|
+
# console.log(...) → print(...)
|
|
508
|
+
# strip the .log part — console was parsed as name, log as method
|
|
509
|
+
pass
|
|
510
|
+
return f"{name}({args})"
|
|
511
|
+
|
|
512
|
+
def parse_args(self) -> str:
|
|
513
|
+
args = []
|
|
514
|
+
while not self.match(")") and self.peek().kind != "EOF":
|
|
515
|
+
args.append(self.parse_expr())
|
|
516
|
+
self.accept(",")
|
|
517
|
+
return ", ".join(args)
|
|
518
|
+
|
|
519
|
+
def parse_widget_args(self, widget_name: str) -> str:
|
|
520
|
+
"""Parse widget constructor args: { key: value } → key=value."""
|
|
521
|
+
# if not an object literal, use regular args
|
|
522
|
+
if not self.match("{"):
|
|
523
|
+
return self.parse_args()
|
|
524
|
+
|
|
525
|
+
self.consume() # {
|
|
526
|
+
kwargs = []
|
|
527
|
+
positional = None
|
|
528
|
+
|
|
529
|
+
while not self.match("}") and self.peek().kind != "EOF":
|
|
530
|
+
key = self.consume().value
|
|
531
|
+
self.expect(":")
|
|
532
|
+
val = self.parse_expr()
|
|
533
|
+
|
|
534
|
+
# convert camelCase to snake_case
|
|
535
|
+
py_key = CAMEL_TO_SNAKE.get(key, key)
|
|
536
|
+
|
|
537
|
+
# handle positional args
|
|
538
|
+
if key in POSITIONAL_KEYS:
|
|
539
|
+
positional = val
|
|
540
|
+
else:
|
|
541
|
+
kwargs.append(f"{py_key}={val}")
|
|
542
|
+
|
|
543
|
+
self.accept(",")
|
|
544
|
+
|
|
545
|
+
self.expect("}")
|
|
546
|
+
|
|
547
|
+
parts = []
|
|
548
|
+
if positional is not None:
|
|
549
|
+
parts.append(positional)
|
|
550
|
+
parts.extend(kwargs)
|
|
551
|
+
return ", ".join(parts)
|
|
552
|
+
|
|
553
|
+
def parse_primary(self) -> str:
|
|
554
|
+
t = self.peek()
|
|
555
|
+
if t is None or t.kind == "EOF":
|
|
556
|
+
raise SyntaxError("Unexpected end of input")
|
|
557
|
+
|
|
558
|
+
if t.kind == "NUMBER":
|
|
559
|
+
self.consume()
|
|
560
|
+
# ensure float literals have a decimal point in Python
|
|
561
|
+
if "." not in t.value:
|
|
562
|
+
return t.value # integer literal
|
|
563
|
+
return t.value
|
|
564
|
+
|
|
565
|
+
if t.kind == "STRING":
|
|
566
|
+
self.consume()
|
|
567
|
+
return t.value # already has quotes
|
|
568
|
+
|
|
569
|
+
if t.value == "true":
|
|
570
|
+
self.consume()
|
|
571
|
+
return "True"
|
|
572
|
+
if t.value == "false":
|
|
573
|
+
self.consume()
|
|
574
|
+
return "False"
|
|
575
|
+
if t.value in ("null", "undefined"):
|
|
576
|
+
self.consume()
|
|
577
|
+
return "None"
|
|
578
|
+
|
|
579
|
+
if t.value == "(":
|
|
580
|
+
self.consume()
|
|
581
|
+
expr = self.parse_expr()
|
|
582
|
+
self.expect(")")
|
|
583
|
+
return f"({expr})"
|
|
584
|
+
|
|
585
|
+
if t.value == "[":
|
|
586
|
+
self.consume()
|
|
587
|
+
elements = []
|
|
588
|
+
while not self.match("]") and self.peek().kind != "EOF":
|
|
589
|
+
elements.append(self.parse_expr())
|
|
590
|
+
self.accept(",")
|
|
591
|
+
self.expect("]")
|
|
592
|
+
return "[" + ", ".join(elements) + "]"
|
|
593
|
+
|
|
594
|
+
if t.kind == "IDENT":
|
|
595
|
+
self.consume()
|
|
596
|
+
name = t.value
|
|
597
|
+
# handle console.log → print
|
|
598
|
+
if name == "console":
|
|
599
|
+
if self.match("."):
|
|
600
|
+
self.consume()
|
|
601
|
+
method = self.consume().value
|
|
602
|
+
if method == "log":
|
|
603
|
+
self.expect("(")
|
|
604
|
+
args = self.parse_args()
|
|
605
|
+
self.expect(")")
|
|
606
|
+
return f"print({args})"
|
|
607
|
+
# handle Math.* calls
|
|
608
|
+
if name == "Math":
|
|
609
|
+
if self.match("."):
|
|
610
|
+
self.consume()
|
|
611
|
+
method = self.consume().value
|
|
612
|
+
if self.match("("):
|
|
613
|
+
self.consume()
|
|
614
|
+
args = self.parse_args()
|
|
615
|
+
self.expect(")")
|
|
616
|
+
return self._method_call("Math", method, args)
|
|
617
|
+
return name
|
|
618
|
+
|
|
619
|
+
raise SyntaxError(f"Unexpected token: {t.value} at line {t.line}")
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ---- Public API ----
|
|
623
|
+
|
|
624
|
+
def transpile(ts_source: str) -> str:
|
|
625
|
+
"""Transpile TypeScript source to Python source (GE subset).
|
|
626
|
+
|
|
627
|
+
Delegates to the canonical TypeScript frontend
|
|
628
|
+
(pyeffic.frontends.typescript) so there is a single lowering
|
|
629
|
+
implementation. Falls back to the legacy parser in this module if the
|
|
630
|
+
frontend rejects the input (e.g. widget-DSL syntax), which keeps older
|
|
631
|
+
sources working.
|
|
632
|
+
"""
|
|
633
|
+
try:
|
|
634
|
+
from .frontends.typescript import ts_to_python
|
|
635
|
+
return ts_to_python(ts_source)
|
|
636
|
+
except Exception:
|
|
637
|
+
tokens = tokenize(ts_source)
|
|
638
|
+
parser = TSParser(tokens)
|
|
639
|
+
return parser.parse_program()
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def transpile_file(ts_path, force: bool = False) -> "Path":
|
|
643
|
+
"""Transpile a .ts file to a .ge.py file. Returns the Python file path.
|
|
644
|
+
|
|
645
|
+
If a .ge.py with the same stem already exists (e.g. user has both
|
|
646
|
+
myrent.ts and myrent.ge.py), the transpiled output goes to myrent.ts.ge.py
|
|
647
|
+
to avoid clobbering the hand-written Python source.
|
|
648
|
+
"""
|
|
649
|
+
from pathlib import Path
|
|
650
|
+
ts_path = Path(ts_path)
|
|
651
|
+
py_source = transpile(ts_path.read_text(encoding="utf-8"))
|
|
652
|
+
py_path = ts_path.with_suffix(".ge.py")
|
|
653
|
+
if py_path.exists() and not force:
|
|
654
|
+
# don't clobber a hand-written .ge.py — use .ts.ge.py instead
|
|
655
|
+
py_path = ts_path.with_suffix(".ts.ge.py")
|
|
656
|
+
py_path.write_text(py_source, encoding="utf-8")
|
|
657
|
+
return py_path
|