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,965 @@
1
+ """TypeScript-flavoured GE frontend.
2
+
3
+ Lowers a strictly-typed TypeScript subset into the same IR the Python-like
4
+ frontend produces, so every existing backend (Rust/C++/C#/Zig/Go/Kotlin)
5
+ works unchanged.
6
+
7
+ Design (following foundry-transpile / smelt):
8
+ TypeScript source --tokenize--> --parse--> Python source --ast.parse--> IR
9
+
10
+ Emitting Python source and reusing `ast.parse` keeps one single source of
11
+ truth for the IR and guarantees both frontends behave identically from the
12
+ analyzer onward.
13
+
14
+ Supported subset
15
+ ----------------
16
+ function f(a: number, b: string): number { ... }
17
+ let x: number = 1; const Y: string = "s";
18
+ if / else if / else
19
+ while (cond) { ... }
20
+ for (let i: number = 0; i < n; i = i + 1) { ... }
21
+ return expr;
22
+ console.log(x); -> print(x)
23
+ gePreamble("cpp", "...") -> ge_preamble(...)
24
+ geInline / geRaw -> ge_inline / ge_raw
25
+ operators: + - * / % ** === !== == != < <= > >= && || ! & |
26
+ types: number string boolean void any number[] T[]
27
+ template literals: `Hi ${name}` -> f"Hi {name}"
28
+ arrays: [1, 2, 3], indexing a[i], arr.length -> len(arr)
29
+
30
+ Anything outside the subset raises `TypeScriptSyntaxError` with a line
31
+ number, so failures are loud rather than silently wrong.
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import re
36
+ from dataclasses import dataclass
37
+
38
+ from ..analyzer import FuncUnit, parse_source_full, collect_constants, collect_preamble
39
+
40
+
41
+ class TypeScriptSyntaxError(Exception):
42
+ """Raised when the input is outside the supported TypeScript subset."""
43
+
44
+ def __init__(self, message: str, line: int = 0, col: int = 0):
45
+ self.line = line
46
+ self.col = col
47
+ loc = f" (line {line})" if line else ""
48
+ super().__init__(f"TypeScript frontend{loc}: {message}")
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Tokenizer
53
+ # ---------------------------------------------------------------------------
54
+
55
+ KEYWORDS = {
56
+ "function", "let", "const", "var", "if", "else", "while", "for",
57
+ "return", "true", "false", "null", "undefined", "export", "import",
58
+ "from", "interface", "type", "new", "break", "continue", "class",
59
+ }
60
+
61
+ # longest-first so `===` wins over `==`
62
+ OPERATORS = [
63
+ "===", "!==", "**=", "...", "=>",
64
+ "==", "!=", "<=", ">=", "&&", "||", "++", "--",
65
+ "+=", "-=", "*=", "/=", "%=", "**",
66
+ "+", "-", "*", "/", "%", "<", ">", "=", "!", "(", ")", "{", "}",
67
+ "[", "]", ";", ",", ":", ".", "?", "&", "|", "@",
68
+ ]
69
+
70
+ _NUM_RE = re.compile(r"(?:0[xX][0-9a-fA-F]+)|(?:\d+\.\d+)|(?:\d+)")
71
+ _IDENT_RE = re.compile(r"[A-Za-z_$][A-Za-z0-9_$]*")
72
+
73
+
74
+ _ESCAPES = {
75
+ "n": "\n", "t": "\t", "r": "\r", "0": "\0",
76
+ "\\": "\\", '"': '"', "'": "'", "`": "`", "$": "$",
77
+ }
78
+
79
+
80
+ def _decode_escape(ch: str) -> str:
81
+ """Decode a single backslash escape from a TS string/template."""
82
+ return _ESCAPES.get(ch, ch)
83
+
84
+
85
+ @dataclass
86
+ class Token:
87
+ kind: str # ident | number | string | template | op | eof
88
+ value: str
89
+ line: int
90
+ col: int
91
+
92
+ def __repr__(self) -> str: # pragma: no cover - debug aid
93
+ return f"Token({self.kind},{self.value!r}@{self.line})"
94
+
95
+
96
+ def tokenize(src: str) -> list[Token]:
97
+ """Turn TypeScript source into a token stream."""
98
+ tokens: list[Token] = []
99
+ i = 0
100
+ line = 1
101
+ col = 1
102
+ n = len(src)
103
+
104
+ def advance(k: int) -> None:
105
+ nonlocal i, line, col
106
+ for ch in src[i:i + k]:
107
+ if ch == "\n":
108
+ line += 1
109
+ col = 1
110
+ else:
111
+ col += 1
112
+ i += k
113
+
114
+ while i < n:
115
+ ch = src[i]
116
+
117
+ # whitespace
118
+ if ch in " \t\r\n":
119
+ advance(1)
120
+ continue
121
+
122
+ # line comment
123
+ if src.startswith("//", i):
124
+ while i < n and src[i] != "\n":
125
+ advance(1)
126
+ continue
127
+
128
+ # block comment
129
+ if src.startswith("/*", i):
130
+ end = src.find("*/", i + 2)
131
+ if end == -1:
132
+ raise TypeScriptSyntaxError("unterminated block comment", line, col)
133
+ advance(end + 2 - i)
134
+ continue
135
+
136
+ # string literal
137
+ if ch in "\"'":
138
+ quote = ch
139
+ start_line, start_col = line, col
140
+ advance(1)
141
+ buf: list[str] = []
142
+ while i < n and src[i] != quote:
143
+ if src[i] == "\\" and i + 1 < n:
144
+ buf.append(_decode_escape(src[i + 1]))
145
+ advance(2)
146
+ continue
147
+ if src[i] == "\n":
148
+ raise TypeScriptSyntaxError("unterminated string", start_line, start_col)
149
+ buf.append(src[i])
150
+ advance(1)
151
+ if i >= n:
152
+ raise TypeScriptSyntaxError("unterminated string", start_line, start_col)
153
+ advance(1)
154
+ tokens.append(Token("string", "".join(buf), start_line, start_col))
155
+ continue
156
+
157
+ # template literal
158
+ if ch == "`":
159
+ start_line, start_col = line, col
160
+ advance(1)
161
+ buf = []
162
+ depth = 0
163
+ while i < n:
164
+ c = src[i]
165
+ if c == "\\" and i + 1 < n:
166
+ # keep escapes verbatim: template content is either raw
167
+ # target-language code (intrinsics) or an f-string body,
168
+ # and in both cases the escape must survive to the emitter
169
+ buf.append(src[i:i + 2])
170
+ advance(2)
171
+ continue
172
+ if c == "$" and i + 1 < n and src[i + 1] == "{":
173
+ depth += 1
174
+ buf.append("${")
175
+ advance(2)
176
+ continue
177
+ if c == "}" and depth > 0:
178
+ depth -= 1
179
+ buf.append("}")
180
+ advance(1)
181
+ continue
182
+ if c == "`" and depth == 0:
183
+ break
184
+ buf.append(c)
185
+ advance(1)
186
+ if i >= n:
187
+ raise TypeScriptSyntaxError("unterminated template literal", start_line, start_col)
188
+ advance(1)
189
+ tokens.append(Token("template", "".join(buf), start_line, start_col))
190
+ continue
191
+
192
+ # number
193
+ m = _NUM_RE.match(src, i)
194
+ if m:
195
+ tokens.append(Token("number", m.group(0), line, col))
196
+ advance(len(m.group(0)))
197
+ continue
198
+
199
+ # identifier / keyword
200
+ m = _IDENT_RE.match(src, i)
201
+ if m:
202
+ tokens.append(Token("ident", m.group(0), line, col))
203
+ advance(len(m.group(0)))
204
+ continue
205
+
206
+ # operator
207
+ for op in OPERATORS:
208
+ if src.startswith(op, i):
209
+ tokens.append(Token("op", op, line, col))
210
+ advance(len(op))
211
+ break
212
+ else:
213
+ raise TypeScriptSyntaxError(f"unexpected character {ch!r}", line, col)
214
+
215
+ tokens.append(Token("eof", "", line, col))
216
+ return tokens
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # Type mapping
221
+ # ---------------------------------------------------------------------------
222
+
223
+ TS_TO_GE_TYPE = {
224
+ "number": "int",
225
+ "string": "str",
226
+ "boolean": "bool",
227
+ "void": "None",
228
+ "any": "any",
229
+ "null": "None",
230
+ "undefined": "None",
231
+ }
232
+
233
+ #: Intrinsic names that map to GE compiler intrinsics.
234
+ INTRINSICS = {
235
+ "gePreamble": "ge_preamble",
236
+ "geInline": "ge_inline",
237
+ "geRaw": "ge_raw",
238
+ }
239
+
240
+
241
+ def map_type(ts_type: str) -> str:
242
+ """Map a TypeScript type annotation onto a GE type name."""
243
+ t = ts_type.strip()
244
+ if t.endswith("[]"):
245
+ return "list"
246
+ if t.startswith("Array<") and t.endswith(">"):
247
+ return "list"
248
+ return TS_TO_GE_TYPE.get(t, t)
249
+
250
+
251
+ # ---------------------------------------------------------------------------
252
+ # Parser -> Python source
253
+ # ---------------------------------------------------------------------------
254
+
255
+ def _strip_outer_parens(text: str) -> str:
256
+ """Drop a single pair of parens that wraps the whole expression.
257
+
258
+ `(a + b)` -> `a + b`, but `(a) + (b)` is left alone because the first
259
+ paren does not match the last one.
260
+ """
261
+ if len(text) < 2 or not text.startswith("(") or not text.endswith(")"):
262
+ return text
263
+ depth = 0
264
+ for i, ch in enumerate(text):
265
+ if ch == "(":
266
+ depth += 1
267
+ elif ch == ")":
268
+ depth -= 1
269
+ if depth == 0:
270
+ # closes before the end -> the outer parens do not wrap it all
271
+ return text if i != len(text) - 1 else text[1:-1]
272
+ return text
273
+
274
+
275
+ def _py_str(value: str) -> str:
276
+ """Emit a Python double-quoted string literal."""
277
+ escaped = (value
278
+ .replace("\\", "\\\\")
279
+ .replace('"', '\\"')
280
+ .replace("\n", "\\n")
281
+ .replace("\t", "\\t"))
282
+ return f'"{escaped}"'
283
+
284
+
285
+ def _py_raw_string(value: str) -> str:
286
+ """Emit raw target-language code as a Python triple-quoted string.
287
+
288
+ Used for gePreamble/geInline/geRaw payloads, which must reach the
289
+ emitter byte-for-byte. Escapes are kept minimal so the code reads the
290
+ same in the lowered form as it did in the source.
291
+ """
292
+ body = value.replace("\\", "\\\\")
293
+ # a literal triple quote would terminate the string early
294
+ body = body.replace('"""', '\\"\\"\\"')
295
+ if body.endswith('"'):
296
+ body += "\\"
297
+ return f'"""{body}"""'
298
+
299
+ class Parser:
300
+ """Recursive-descent parser producing Python source text."""
301
+
302
+ def __init__(self, tokens: list[Token]):
303
+ self.toks = tokens
304
+ self.pos = 0
305
+ self.indent = 0
306
+ self.lines: list[str] = []
307
+ self._imported_types: set[str] = set()
308
+
309
+ # -- token helpers ----------------------------------------------------
310
+ @property
311
+ def cur(self) -> Token:
312
+ return self.toks[self.pos]
313
+
314
+ def peek(self, offset: int = 1) -> Token:
315
+ idx = min(self.pos + offset, len(self.toks) - 1)
316
+ return self.toks[idx]
317
+
318
+ def at_op(self, *ops: str) -> bool:
319
+ return self.cur.kind == "op" and self.cur.value in ops
320
+
321
+ def at_ident(self, *names: str) -> bool:
322
+ return self.cur.kind == "ident" and self.cur.value in names
323
+
324
+ def next(self) -> Token:
325
+ tok = self.cur
326
+ self.pos += 1
327
+ return tok
328
+
329
+ def expect_op(self, op: str) -> Token:
330
+ if not self.at_op(op):
331
+ raise TypeScriptSyntaxError(
332
+ f"expected {op!r} but found {self.cur.value!r}",
333
+ self.cur.line, self.cur.col)
334
+ return self.next()
335
+
336
+ def expect_ident(self, name: str | None = None) -> Token:
337
+ if self.cur.kind != "ident":
338
+ raise TypeScriptSyntaxError(
339
+ f"expected identifier but found {self.cur.value!r}",
340
+ self.cur.line, self.cur.col)
341
+ if name is not None and self.cur.value != name:
342
+ raise TypeScriptSyntaxError(
343
+ f"expected {name!r} but found {self.cur.value!r}",
344
+ self.cur.line, self.cur.col)
345
+ return self.next()
346
+
347
+ # -- output helpers ---------------------------------------------------
348
+ def emit(self, text: str) -> None:
349
+ self.lines.append(" " * self.indent + text)
350
+
351
+ def emit_blank(self) -> None:
352
+ self.lines.append("")
353
+
354
+ # -- program ----------------------------------------------------------
355
+ def parse_program(self) -> str:
356
+ """Parse a whole file into Python source."""
357
+ while self.cur.kind != "eof":
358
+ self.parse_top_level()
359
+ return "\n".join(self.lines) + "\n"
360
+
361
+ def parse_top_level(self) -> None:
362
+ tok = self.cur
363
+
364
+ if self.at_op("@"):
365
+ self.parse_decorated_function()
366
+ return
367
+ if self.at_ident("import"):
368
+ self.parse_import()
369
+ return
370
+ if self.at_ident("export"):
371
+ self.next()
372
+ return
373
+ if self.at_ident("interface", "type"):
374
+ self.skip_declaration()
375
+ return
376
+ if self.at_ident("const", "let", "var"):
377
+ self.parse_var_decl()
378
+ return
379
+ if self.at_ident("function"):
380
+ self.parse_function()
381
+ return
382
+ if tok.kind == "op" and tok.value == ";":
383
+ self.next()
384
+ return
385
+
386
+ # bare expression statement (e.g. a top-level gePreamble call)
387
+ expr = self.parse_expression()
388
+ self.expect_op(";")
389
+ self.emit(expr)
390
+ self.emit_blank()
391
+
392
+ def parse_decorators(self) -> list[str]:
393
+ """Collect `@cpp` / `@rust` / ... decorators and return their names."""
394
+ names: list[str] = []
395
+ while self.at_op("@"):
396
+ self.next()
397
+ names.append(self.expect_ident().value)
398
+ return names
399
+
400
+ def parse_decorated_function(self) -> None:
401
+ """`@cpp export function f() {}` -> `@cpp` + `def f():`."""
402
+ decorators = self.parse_decorators()
403
+ if self.at_ident("export"):
404
+ self.next()
405
+ if not self.at_ident("function"):
406
+ raise TypeScriptSyntaxError(
407
+ "decorator must precede a function declaration",
408
+ self.cur.line, self.cur.col)
409
+ for d in decorators:
410
+ self.emit(f"@{d}")
411
+ self.parse_function()
412
+
413
+ def parse_import(self) -> None:
414
+ """`import { a, b } from "./mod";` -> `from mod import a, b`."""
415
+ self.expect_ident("import")
416
+ names: list[str] = []
417
+ if self.at_op("{"):
418
+ self.next()
419
+ while not self.at_op("}"):
420
+ names.append(self.expect_ident().value)
421
+ if self.at_op(","):
422
+ self.next()
423
+ self.expect_op("}")
424
+ else:
425
+ names.append(self.expect_ident().value)
426
+ self.expect_ident("from")
427
+ if self.cur.kind not in ("string", "template"):
428
+ raise TypeScriptSyntaxError(
429
+ "import path must be a string literal", self.cur.line, self.cur.col)
430
+ module = self.next().value
431
+ self.expect_op(";")
432
+ # widget imports come from the GE widget library, not a project module
433
+ if "widgets" in module:
434
+ module = "pyeffic.widgets"
435
+ else:
436
+ # ./x -> x, ../y -> y, ../../a/b -> a.b
437
+ # GE's module resolver searches the source dir and its parents,
438
+ # so dropping the relative prefix is enough.
439
+ while module.startswith("./") or module.startswith("../"):
440
+ module = module[2:] if module.startswith("./") else module[3:]
441
+ module = module.replace("/", ".")
442
+ self.emit(f"from {module} import {', '.join(names)}")
443
+ self.emit_blank()
444
+
445
+ def skip_declaration(self) -> None:
446
+ """Skip an interface/type declaration (types only, no code)."""
447
+ depth = 0
448
+ while self.cur.kind != "eof":
449
+ if self.at_op("{"):
450
+ depth += 1
451
+ elif self.at_op("}"):
452
+ depth -= 1
453
+ if depth == 0:
454
+ self.next()
455
+ if self.at_op(";"):
456
+ self.next()
457
+ return
458
+ elif self.at_op(";") and depth == 0:
459
+ self.next()
460
+ return
461
+ self.next()
462
+
463
+ # -- declarations -----------------------------------------------------
464
+ def parse_type_annotation(self) -> str:
465
+ """Parse `: Type` (with optional generic/array parts)."""
466
+ self.expect_op(":")
467
+ parts: list[str] = []
468
+ angle = 0
469
+ bracket = 0
470
+ while self.cur.kind != "eof":
471
+ if self.at_op("<"):
472
+ angle += 1
473
+ elif self.at_op(">"):
474
+ angle -= 1
475
+ elif self.at_op("["):
476
+ bracket += 1
477
+ elif self.at_op("]"):
478
+ if bracket == 0:
479
+ break # this ] closes an enclosing subscript, not the type
480
+ bracket -= 1
481
+ if angle == 0 and bracket == 0 and self.at_op(",", ")", "=", ";", "{"):
482
+ break
483
+ parts.append(self.next().value)
484
+ return map_type("".join(parts))
485
+
486
+ def parse_var_decl(self) -> None:
487
+ """`let x: number = 1;` -> `x: int = 1`."""
488
+ self.next() # let / const / var
489
+ name = self.expect_ident().value
490
+ ge_type = self.parse_type_annotation() if self.at_op(":") else None
491
+ if self.at_op("="):
492
+ self.next()
493
+ value = self.parse_expression()
494
+ else:
495
+ value = None
496
+ self.expect_op(";")
497
+ if ge_type:
498
+ self.emit(f"{name}: {ge_type} = {value if value is not None else self._zero_for(ge_type)}")
499
+ elif value is not None:
500
+ self.emit(f"{name} = {value}")
501
+
502
+ @staticmethod
503
+ def _zero_for(ge_type: str) -> str:
504
+ return {"int": "0", "float": "0.0", "bool": "False", "str": '""'}.get(ge_type, "None")
505
+
506
+ def parse_function(self) -> None:
507
+ """`function f(a: number): number { ... }` -> `def f(a: int) -> int:`."""
508
+ self.expect_ident("function")
509
+ name = self.expect_ident().value
510
+ self.expect_op("(")
511
+ params: list[str] = []
512
+ while not self.at_op(")"):
513
+ pname = self.expect_ident().value
514
+ ptype = self.parse_type_annotation() if self.at_op(":") else "any"
515
+ params.append(f"{pname}: {ptype}")
516
+ if self.at_op(","):
517
+ self.next()
518
+ self.expect_op(")")
519
+ ret = self.parse_type_annotation() if self.at_op(":") else "None"
520
+ self.expect_op("{")
521
+
522
+ sig = f"def {name}({', '.join(params)})"
523
+ if ret != "None":
524
+ sig += f" -> {ret}"
525
+ self.emit(sig + ":")
526
+ self.indent += 1
527
+ self.parse_block()
528
+ self.indent -= 1
529
+ if not self.lines or self.lines[-1].strip() != "":
530
+ self.emit_blank()
531
+
532
+ # -- statements -------------------------------------------------------
533
+ def parse_block(self) -> None:
534
+ """Parse statements until the matching `}`."""
535
+ while not self.at_op("}"):
536
+ if self.cur.kind == "eof":
537
+ raise TypeScriptSyntaxError("unexpected end of file in block", self.cur.line)
538
+ self.parse_statement()
539
+ self.expect_op("}")
540
+
541
+ def parse_statement(self) -> None:
542
+ if self.at_op(";"):
543
+ self.next()
544
+ return
545
+ if self.at_ident("let", "const", "var"):
546
+ self.parse_var_decl()
547
+ return
548
+ if self.at_ident("return"):
549
+ self.next()
550
+ if self.at_op(";"):
551
+ self.next()
552
+ self.emit("return")
553
+ else:
554
+ value = self.parse_expression()
555
+ self.expect_op(";")
556
+ self.emit(f"return {value}")
557
+ return
558
+ if self.at_ident("if"):
559
+ self.parse_if()
560
+ return
561
+ if self.at_ident("while"):
562
+ self.parse_while()
563
+ return
564
+ if self.at_ident("for"):
565
+ self.parse_for()
566
+ return
567
+ if self.at_ident("break"):
568
+ self.next()
569
+ self.expect_op(";")
570
+ self.emit("break")
571
+ return
572
+ if self.at_ident("continue"):
573
+ self.next()
574
+ self.expect_op(";")
575
+ self.emit("continue")
576
+ return
577
+ if self.at_ident("function"):
578
+ self.parse_function()
579
+ return
580
+
581
+ # expression statement (assignment or call)
582
+ self.parse_expression_statement()
583
+
584
+ def parse_expression_statement(self) -> None:
585
+ start = self.pos
586
+ expr = self.parse_expression()
587
+ if self.at_op("=") or self.at_op("+=", "-=", "*=", "/=", "%="):
588
+ op = self.next().value
589
+ rhs = self.parse_expression()
590
+ self.expect_op(";")
591
+ if op == "=":
592
+ self.emit(f"{expr} = {rhs}")
593
+ else:
594
+ self.emit(f"{expr} {op[0]}= {rhs}")
595
+ return
596
+ self.expect_op(";")
597
+ # re-parse the call for print() rewriting
598
+ self.pos = start
599
+ expr = self.parse_expression()
600
+ self.expect_op(";")
601
+ self.emit(expr)
602
+
603
+ def parse_if(self) -> None:
604
+ self.expect_ident("if")
605
+ self.expect_op("(")
606
+ cond = self.parse_expression()
607
+ self.expect_op(")")
608
+ self.expect_op("{")
609
+ self.emit(f"if {cond}:")
610
+ self.indent += 1
611
+ self.parse_block()
612
+ self.indent -= 1
613
+
614
+ while self.at_ident("else"):
615
+ self.next()
616
+ if self.at_ident("if"):
617
+ self.next()
618
+ self.expect_op("(")
619
+ cond2 = self.parse_expression()
620
+ self.expect_op(")")
621
+ self.expect_op("{")
622
+ self.emit(f"elif {cond2}:")
623
+ self.indent += 1
624
+ self.parse_block()
625
+ self.indent -= 1
626
+ else:
627
+ self.expect_op("{")
628
+ self.emit("else:")
629
+ self.indent += 1
630
+ self.parse_block()
631
+ self.indent -= 1
632
+ break
633
+
634
+ def parse_while(self) -> None:
635
+ self.expect_ident("while")
636
+ self.expect_op("(")
637
+ cond = self.parse_expression()
638
+ self.expect_op(")")
639
+ self.expect_op("{")
640
+ self.emit(f"while {cond}:")
641
+ self.indent += 1
642
+ self.parse_block()
643
+ self.indent -= 1
644
+
645
+ def parse_for(self) -> None:
646
+ """Classic C-style for -> while (keeps semantics simple and correct)."""
647
+ self.expect_ident("for")
648
+ self.expect_op("(")
649
+
650
+ # init
651
+ if self.at_ident("let", "const", "var"):
652
+ self.next()
653
+ name = self.expect_ident().value
654
+ ge_type = self.parse_type_annotation() if self.at_op(":") else "int"
655
+ self.expect_op("=")
656
+ value = self.parse_expression()
657
+ self.emit(f"{name}: {ge_type} = {value}")
658
+ elif self.at_op(";"):
659
+ pass
660
+ else:
661
+ expr = self.parse_expression()
662
+ if self.at_op("="):
663
+ self.next()
664
+ rhs = self.parse_expression()
665
+ self.emit(f"{expr} = {rhs}")
666
+ self.expect_op(";")
667
+
668
+ # condition
669
+ cond = self.parse_expression()
670
+ self.expect_op(";")
671
+
672
+ # update
673
+ update_parts: list[str] = []
674
+ while not self.at_op(")"):
675
+ target = self.parse_expression()
676
+ if self.at_op("++"):
677
+ self.next()
678
+ update_parts.append(f"{target} = {target} + 1")
679
+ elif self.at_op("--"):
680
+ self.next()
681
+ update_parts.append(f"{target} = {target} - 1")
682
+ elif self.at_op("="):
683
+ self.next()
684
+ rhs = self.parse_expression()
685
+ update_parts.append(f"{target} = {rhs}")
686
+ elif self.at_op("+=", "-=", "*=", "/="):
687
+ op = self.next().value
688
+ rhs = self.parse_expression()
689
+ update_parts.append(f"{target} {op[0]}= {rhs}")
690
+ if self.at_op(","):
691
+ self.next()
692
+ self.expect_op(")")
693
+ self.expect_op("{")
694
+
695
+ self.emit(f"while {cond}:")
696
+ self.indent += 1
697
+ self.parse_block()
698
+ for part in update_parts:
699
+ self.emit(part)
700
+ self.indent -= 1
701
+
702
+ # -- expressions ------------------------------------------------------
703
+ def parse_expression(self) -> str:
704
+ return _strip_outer_parens(self.parse_binary(0))
705
+
706
+ _PRECEDENCE = {
707
+ "||": 1, "&&": 2, "|": 3, "&": 4,
708
+ "==": 5, "!=": 5, "===": 5, "!==": 5,
709
+ "<": 6, "<=": 6, ">": 6, ">=": 6,
710
+ "+": 7, "-": 7,
711
+ "*": 8, "/": 8, "%": 8,
712
+ "**": 9,
713
+ }
714
+
715
+ _BINOP_MAP = {
716
+ "===": "==", "!==": "!=",
717
+ "&&": "and", "||": "or",
718
+ "&": "and", "|": "or",
719
+ }
720
+
721
+ def parse_binary(self, min_prec: int) -> str:
722
+ left = self.parse_unary()
723
+ while True:
724
+ if self.cur.kind != "op":
725
+ break
726
+ op = self.cur.value
727
+ prec = self._PRECEDENCE.get(op)
728
+ if prec is None or prec < min_prec:
729
+ break
730
+ self.next()
731
+ right = self.parse_binary(prec + 1)
732
+ py_op = self._BINOP_MAP.get(op, op)
733
+ left = f"({left} {py_op} {right})"
734
+ return left
735
+
736
+ def parse_unary(self) -> str:
737
+ if self.at_op("@"):
738
+ # `@name(args)` — a call to a named .ge block. Lowered to a plain
739
+ # call; the hybrid frontend rewrites it to the target function.
740
+ self.next()
741
+ name = self.expect_ident().value
742
+ if not self.at_op("("):
743
+ raise TypeScriptSyntaxError(
744
+ "expected '(' after @block reference", self.cur.line, self.cur.col)
745
+ self.next()
746
+ args: list[str] = []
747
+ while not self.at_op(")"):
748
+ args.append(self.parse_expression())
749
+ if self.at_op(","):
750
+ self.next()
751
+ self.expect_op(")")
752
+ return f"{name}({', '.join(args)})"
753
+ if self.at_op("!"):
754
+ self.next()
755
+ return f"(not {self.parse_unary()})"
756
+ if self.at_op("-"):
757
+ self.next()
758
+ return f"(-{self.parse_unary()})"
759
+ if self.at_op("+"):
760
+ self.next()
761
+ return self.parse_unary()
762
+ return self.parse_postfix()
763
+
764
+ def parse_postfix(self) -> str:
765
+ expr = self.parse_primary()
766
+ while True:
767
+ if self.at_op("."):
768
+ self.next()
769
+ attr = self.expect_ident().value
770
+ if attr == "length":
771
+ expr = f"len({expr})"
772
+ elif attr == "push":
773
+ # handled as a call below
774
+ expr = f"{expr}.append"
775
+ else:
776
+ expr = f"{expr}.{attr}"
777
+ elif self.at_op("["):
778
+ self.next()
779
+ idx = self.parse_expression()
780
+ self.expect_op("]")
781
+ expr = f"{expr}[{idx}]"
782
+ elif self.at_op("("):
783
+ self.next()
784
+ args: list[str] = []
785
+ while not self.at_op(")"):
786
+ args.append(self.parse_expression())
787
+ if self.at_op(","):
788
+ self.next()
789
+ self.expect_op(")")
790
+ expr = self._apply_call(expr, args)
791
+ else:
792
+ break
793
+ return expr
794
+
795
+ def _apply_call(self, callee: str, args: list[str]) -> str:
796
+ """Rewrite known TS APIs onto their GE/Python equivalents."""
797
+ joined = ", ".join(args)
798
+ if callee == "console.log":
799
+ return f"print({joined})"
800
+ if callee in INTRINSICS:
801
+ return f"{INTRINSICS[callee]}({joined})"
802
+ if callee == "Math.floor":
803
+ return f"int({joined})"
804
+ if callee == "Math.abs":
805
+ return f"abs({joined})"
806
+ if callee == "Math.max":
807
+ return f"max({joined})"
808
+ if callee == "Math.min":
809
+ return f"min({joined})"
810
+ if callee == "Math.pow":
811
+ return f"pow({joined})"
812
+ if callee == "Math.idiv":
813
+ if len(args) == 2:
814
+ return f"({args[0]} // {args[1]})"
815
+ return f"int({joined})"
816
+ if callee == "Number":
817
+ return f"int({joined})"
818
+ if callee == "String":
819
+ return f"str({joined})"
820
+ if callee.endswith(".push"):
821
+ base = callee[: -len(".push")]
822
+ return f"{base}.append({joined})"
823
+ if callee.endswith(".toString"):
824
+ base = callee[: -len(".toString")]
825
+ return f"str({base})"
826
+ return f"{callee}({joined})"
827
+
828
+ def parse_primary(self) -> str:
829
+ tok = self.cur
830
+
831
+ if tok.kind == "number":
832
+ self.next()
833
+ return tok.value
834
+ if tok.kind == "string":
835
+ self.next()
836
+ return _py_str(tok.value)
837
+ if tok.kind == "template":
838
+ self.next()
839
+ return self._template_to_fstring(tok.value)
840
+
841
+ if self.at_ident("true"):
842
+ self.next()
843
+ return "True"
844
+ if self.at_ident("false"):
845
+ self.next()
846
+ return "False"
847
+ if self.at_ident("null", "undefined"):
848
+ self.next()
849
+ return "None"
850
+
851
+ if self.at_op("("):
852
+ self.next()
853
+ expr = self.parse_expression()
854
+ self.expect_op(")")
855
+ return f"({expr})"
856
+
857
+ if self.at_op("["):
858
+ self.next()
859
+ items: list[str] = []
860
+ while not self.at_op("]"):
861
+ items.append(self.parse_expression())
862
+ if self.at_op(","):
863
+ self.next()
864
+ self.expect_op("]")
865
+ return "[" + ", ".join(items) + "]"
866
+
867
+ if self.at_ident("new"):
868
+ self.next()
869
+ ctor = self.expect_ident().value
870
+ self.expect_op("(")
871
+ args: list[str] = []
872
+ while not self.at_op(")"):
873
+ args.append(self.parse_expression())
874
+ if self.at_op(","):
875
+ self.next()
876
+ self.expect_op(")")
877
+ return f"{ctor}({', '.join(args)})"
878
+
879
+ if tok.kind == "ident":
880
+ self.next()
881
+ # intrinsics take raw target-language code; parse their arguments
882
+ # specially so template literals survive verbatim
883
+ if tok.value in INTRINSICS and self.at_op("("):
884
+ return self._parse_intrinsic_call(tok.value)
885
+ return tok.value
886
+
887
+ raise TypeScriptSyntaxError(
888
+ f"unexpected token {tok.value!r} in expression", tok.line, tok.col)
889
+
890
+ def _parse_intrinsic_call(self, ts_name: str) -> str:
891
+ """Parse gePreamble/geInline/geRaw arguments, keeping raw code intact."""
892
+ self.expect_op("(")
893
+ args: list[str] = []
894
+ while not self.at_op(")"):
895
+ if self.cur.kind == "template":
896
+ raw = self.next().value
897
+ args.append(_py_raw_string(raw))
898
+ else:
899
+ args.append(self.parse_expression())
900
+ if self.at_op(","):
901
+ self.next()
902
+ self.expect_op(")")
903
+ return f"{INTRINSICS[ts_name]}({', '.join(args)})"
904
+
905
+ @staticmethod
906
+ def _template_to_fstring(raw: str) -> str:
907
+ """`Hi ${name}` -> f"Hi {name}".
908
+
909
+ Backslash escapes are left alone: TS and Python agree on \\n, \\t,
910
+ \\r, \\\\ and \\", so the body survives the round trip unchanged.
911
+ Only unescaped double quotes need protecting, since the literal is
912
+ emitted with double quotes.
913
+ """
914
+ out: list[str] = []
915
+ i = 0
916
+ n = len(raw)
917
+ while i < n:
918
+ c = raw[i]
919
+ if c == "\\" and i + 1 < n:
920
+ out.append(raw[i:i + 2])
921
+ i += 2
922
+ continue
923
+ if c == '"':
924
+ out.append('\\"')
925
+ i += 1
926
+ continue
927
+ out.append(c)
928
+ i += 1
929
+ body = "".join(out)
930
+ body = re.sub(r"\$\{([^}]*)\}", lambda m: "{" + m.group(1) + "}", body)
931
+ return f'f"{body}"'
932
+
933
+
934
+ # ---------------------------------------------------------------------------
935
+ # Public API
936
+ # ---------------------------------------------------------------------------
937
+
938
+ def ts_to_python(source: str) -> str:
939
+ """Lower TypeScript-flavoured GE source into equivalent Python source."""
940
+ tokens = tokenize(source)
941
+ parser = Parser(tokens)
942
+ return parser.parse_program()
943
+
944
+
945
+ def parse_ts_source_full(source: str) -> tuple[list[FuncUnit], list]:
946
+ """Parse TypeScript-flavoured GE source into the shared IR.
947
+
948
+ Returns (function_units, class_units) exactly like the Python frontend.
949
+ """
950
+ python_source = ts_to_python(source)
951
+ try:
952
+ return parse_source_full(python_source)
953
+ except SyntaxError as exc: # pragma: no cover - defensive
954
+ raise TypeScriptSyntaxError(
955
+ f"lowered source is not valid Python: {exc.msg}", exc.lineno or 0) from exc
956
+
957
+
958
+ def collect_ts_constants(source: str) -> dict:
959
+ """Collect module-level constants from TypeScript-flavoured source."""
960
+ return collect_constants(ts_to_python(source))
961
+
962
+
963
+ def collect_ts_preamble(source: str) -> dict:
964
+ """Collect gePreamble(...) blocks from TypeScript-flavoured source."""
965
+ return collect_preamble(ts_to_python(source))