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,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