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