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,348 @@
|
|
|
1
|
+
"""Golden lowering corpus — proves emitted code stays put.
|
|
2
|
+
|
|
3
|
+
Every emitter change is a risk: generated code can churn silently while still
|
|
4
|
+
compiling and still producing the right answer. This module makes that churn
|
|
5
|
+
visible.
|
|
6
|
+
|
|
7
|
+
For each case under `tests/golden/<case>/`:
|
|
8
|
+
|
|
9
|
+
main.ge the source
|
|
10
|
+
rust.golden expected emitted Rust
|
|
11
|
+
cpp.golden expected emitted C++
|
|
12
|
+
csharp.golden ...
|
|
13
|
+
zig.golden
|
|
14
|
+
go.golden
|
|
15
|
+
kotlin.golden
|
|
16
|
+
expected.out what the program must print
|
|
17
|
+
|
|
18
|
+
Two independent checks:
|
|
19
|
+
|
|
20
|
+
lowering emitted code == committed golden, byte for byte
|
|
21
|
+
behaviour compiling and running the golden prints expected.out
|
|
22
|
+
|
|
23
|
+
The lowering check proves we still emit the code we reviewed. The behaviour
|
|
24
|
+
check proves that code still does what it did. Together they catch a
|
|
25
|
+
regression that keeps compiling.
|
|
26
|
+
|
|
27
|
+
This is the model bento uses for its TypeScript-to-Go corpus, and it is the
|
|
28
|
+
same idea as Kotlin's `apiDump`/`apiCheck` applied to emitted code.
|
|
29
|
+
|
|
30
|
+
Usage
|
|
31
|
+
-----
|
|
32
|
+
ge golden --check fail on any drift
|
|
33
|
+
ge golden --update regenerate goldens (review the diff, then commit)
|
|
34
|
+
ge golden --check -v show which cases drifted
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import subprocess
|
|
39
|
+
import sys
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
from .config import Config, detect_compilers
|
|
44
|
+
|
|
45
|
+
BACKENDS = ("rust", "cpp", "csharp", "zig", "go", "kotlin")
|
|
46
|
+
|
|
47
|
+
_EMITTERS = {
|
|
48
|
+
"rust": "emit_rust",
|
|
49
|
+
"cpp": "emit_cpp",
|
|
50
|
+
"csharp": "emit_csharp",
|
|
51
|
+
"zig": "emit_zig",
|
|
52
|
+
"go": "emit_go",
|
|
53
|
+
"kotlin": "emit_kotlin",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def golden_root() -> Path:
|
|
58
|
+
"""Where the corpus lives, relative to the repo root."""
|
|
59
|
+
here = Path(__file__).resolve().parent.parent
|
|
60
|
+
return here / "tests" / "golden"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Emission
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def emit_unit(source_path: Path, backend: str) -> str:
|
|
68
|
+
"""Emit the whole compilation unit for one backend.
|
|
69
|
+
|
|
70
|
+
Uses the same import resolution and IR rename path as a real build, so a
|
|
71
|
+
golden reflects what a user would actually get.
|
|
72
|
+
"""
|
|
73
|
+
from .emitters import (emit_cpp, emit_csharp, emit_go, emit_kotlin,
|
|
74
|
+
emit_rust, emit_zig)
|
|
75
|
+
from .idents import rename_reserved_functions
|
|
76
|
+
from .modules import get_last_constants, get_last_preamble, resolve_imports
|
|
77
|
+
|
|
78
|
+
source = source_path.read_text(encoding="utf-8")
|
|
79
|
+
units, classes, _warnings = resolve_imports(source, source_path)
|
|
80
|
+
supported = [u for u in units if u.supported and u.body is not None]
|
|
81
|
+
|
|
82
|
+
# pin every function to the requested backend so the golden is stable
|
|
83
|
+
for u in supported:
|
|
84
|
+
u.forced_backend = backend
|
|
85
|
+
rename_reserved_functions(supported, {backend})
|
|
86
|
+
|
|
87
|
+
emit_fn = {
|
|
88
|
+
"rust": emit_rust, "cpp": emit_cpp, "csharp": emit_csharp,
|
|
89
|
+
"zig": emit_zig, "go": emit_go, "kotlin": emit_kotlin,
|
|
90
|
+
}[backend]
|
|
91
|
+
prog, _emitted = emit_fn(supported, entry="main", classes=classes,
|
|
92
|
+
constants=get_last_constants(),
|
|
93
|
+
preamble=get_last_preamble())
|
|
94
|
+
return prog
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _normalise(text: str) -> str:
|
|
98
|
+
"""Line endings normalised so goldens are platform-independent."""
|
|
99
|
+
return text.replace("\r\n", "\n").replace("\r", "\n")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Results
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class GoldenProblem:
|
|
108
|
+
case: str
|
|
109
|
+
kind: str # "lowering" | "behaviour" | "missing" | "unexpected"
|
|
110
|
+
detail: str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class GoldenReport:
|
|
115
|
+
cases: list[Path] = field(default_factory=list)
|
|
116
|
+
problems: list[GoldenProblem] = field(default_factory=list)
|
|
117
|
+
updated: list[str] = field(default_factory=list)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def ok(self) -> bool:
|
|
121
|
+
return not self.problems
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def checked(self) -> int:
|
|
125
|
+
return len(self.cases)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Case discovery
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def find_cases(root: Path | None = None) -> list[Path]:
|
|
133
|
+
"""Every golden case directory (one that contains a source file)."""
|
|
134
|
+
root = root or golden_root()
|
|
135
|
+
if not root.is_dir():
|
|
136
|
+
return []
|
|
137
|
+
out: list[Path] = []
|
|
138
|
+
for d in sorted(root.iterdir()):
|
|
139
|
+
if not d.is_dir():
|
|
140
|
+
continue
|
|
141
|
+
for name in ("main.ge", "main.ge.py", "main.ge.ts"):
|
|
142
|
+
if (d / name).is_file():
|
|
143
|
+
out.append(d)
|
|
144
|
+
break
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def case_source(case_dir: Path) -> Path:
|
|
149
|
+
for name in ("main.ge", "main.ge.py", "main.ge.ts"):
|
|
150
|
+
p = case_dir / name
|
|
151
|
+
if p.is_file():
|
|
152
|
+
return p
|
|
153
|
+
raise FileNotFoundError(f"no source in {case_dir}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Check / update
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def _golden_path(case_dir: Path, backend: str) -> Path:
|
|
161
|
+
return case_dir / f"{backend}.golden"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def check_case(case_dir: Path, backends: tuple[str, ...] = BACKENDS,
|
|
165
|
+
run: bool = True) -> list[GoldenProblem]:
|
|
166
|
+
"""Compare emitted code with the committed goldens for one case."""
|
|
167
|
+
problems: list[GoldenProblem] = []
|
|
168
|
+
src = case_source(case_dir)
|
|
169
|
+
name = case_dir.name
|
|
170
|
+
|
|
171
|
+
for backend in backends:
|
|
172
|
+
gpath = _golden_path(case_dir, backend)
|
|
173
|
+
try:
|
|
174
|
+
emitted = _normalise(emit_unit(src, backend))
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
problems.append(GoldenProblem(
|
|
177
|
+
name, "lowering", f"{backend}: emission failed: {exc}"))
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
if not gpath.exists():
|
|
181
|
+
problems.append(GoldenProblem(
|
|
182
|
+
name, "missing", f"{backend}.golden is absent "
|
|
183
|
+
f"(run: ge golden --update)"))
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
expected = _normalise(gpath.read_text(encoding="utf-8"))
|
|
187
|
+
if emitted != expected:
|
|
188
|
+
problems.append(GoldenProblem(
|
|
189
|
+
name, "lowering",
|
|
190
|
+
f"{backend}.golden drifted — review with `ge golden --update` "
|
|
191
|
+
f"then commit the diff"))
|
|
192
|
+
return problems
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def update_case(case_dir: Path, backends: tuple[str, ...] = BACKENDS) -> list[str]:
|
|
196
|
+
"""Regenerate the goldens for one case. Returns the files written."""
|
|
197
|
+
src = case_source(case_dir)
|
|
198
|
+
written: list[str] = []
|
|
199
|
+
for backend in backends:
|
|
200
|
+
try:
|
|
201
|
+
emitted = _normalise(emit_unit(src, backend))
|
|
202
|
+
except Exception:
|
|
203
|
+
continue
|
|
204
|
+
gpath = _golden_path(case_dir, backend)
|
|
205
|
+
old = gpath.read_text(encoding="utf-8") if gpath.exists() else None
|
|
206
|
+
if _normalise(old or "") != emitted:
|
|
207
|
+
gpath.write_text(emitted, encoding="utf-8")
|
|
208
|
+
written.append(f"{case_dir.name}/{backend}.golden")
|
|
209
|
+
return written
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _check_behaviour(case_dir: Path, timeout: int = 90) -> list[GoldenProblem]:
|
|
213
|
+
"""Compile and run the source; it must print expected.out."""
|
|
214
|
+
expected_path = case_dir / "expected.out"
|
|
215
|
+
if not expected_path.exists():
|
|
216
|
+
return [GoldenProblem(case_dir.name, "missing",
|
|
217
|
+
"expected.out is absent")]
|
|
218
|
+
expected = _normalise(expected_path.read_text(encoding="utf-8")).strip()
|
|
219
|
+
|
|
220
|
+
from .pipeline import build
|
|
221
|
+
from .config import Config
|
|
222
|
+
|
|
223
|
+
with __import__("tempfile").TemporaryDirectory() as td:
|
|
224
|
+
cfg = Config(out_dir=Path(td), target="desktop", do_research=False)
|
|
225
|
+
try:
|
|
226
|
+
report = build(case_source(case_dir), cfg, entry="main")
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
return [GoldenProblem(case_dir.name, "behaviour",
|
|
229
|
+
f"build raised: {exc}")]
|
|
230
|
+
if report.errors.has_errors():
|
|
231
|
+
first = report.errors.diagnostics[0]
|
|
232
|
+
return [GoldenProblem(
|
|
233
|
+
case_dir.name, "behaviour",
|
|
234
|
+
f"build failed: {getattr(first, 'message', first)}")]
|
|
235
|
+
|
|
236
|
+
cr = report.rust_compile or report.cpp_compile
|
|
237
|
+
if cr is None or not cr.ok or not cr.exe:
|
|
238
|
+
return [GoldenProblem(case_dir.name, "behaviour",
|
|
239
|
+
"no runnable binary produced")]
|
|
240
|
+
try:
|
|
241
|
+
proc = subprocess.run([str(cr.exe)], capture_output=True,
|
|
242
|
+
text=True, timeout=timeout)
|
|
243
|
+
except subprocess.TimeoutExpired:
|
|
244
|
+
return [GoldenProblem(case_dir.name, "behaviour",
|
|
245
|
+
f"timed out after {timeout}s")]
|
|
246
|
+
got = _normalise(proc.stdout).strip()
|
|
247
|
+
if got != expected:
|
|
248
|
+
return [GoldenProblem(
|
|
249
|
+
case_dir.name, "behaviour",
|
|
250
|
+
f"output changed\n expected: {expected!r}\n"
|
|
251
|
+
f" actual : {got!r}")]
|
|
252
|
+
return []
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def check(root: Path | None = None, backends: tuple[str, ...] = BACKENDS,
|
|
256
|
+
behaviour: bool = True) -> GoldenReport:
|
|
257
|
+
"""Check every case."""
|
|
258
|
+
report = GoldenReport()
|
|
259
|
+
for case_dir in find_cases(root):
|
|
260
|
+
report.cases.append(case_dir)
|
|
261
|
+
report.problems.extend(check_case(case_dir, backends))
|
|
262
|
+
if behaviour:
|
|
263
|
+
report.problems.extend(_check_behaviour(case_dir))
|
|
264
|
+
return report
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def update(root: Path | None = None,
|
|
268
|
+
backends: tuple[str, ...] = BACKENDS) -> GoldenReport:
|
|
269
|
+
"""Regenerate every golden."""
|
|
270
|
+
report = GoldenReport()
|
|
271
|
+
for case_dir in find_cases(root):
|
|
272
|
+
report.cases.append(case_dir)
|
|
273
|
+
report.updated.extend(update_case(case_dir, backends))
|
|
274
|
+
return report
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def available(backends: tuple[str, ...] = BACKENDS) -> tuple[str, ...]:
|
|
278
|
+
"""Backends whose toolchain is installed (behaviour checks need these)."""
|
|
279
|
+
info = detect_compilers()
|
|
280
|
+
have = {
|
|
281
|
+
"rust": bool(info.rustc), "cpp": bool(info.cpp),
|
|
282
|
+
"csharp": bool(info.dotnet), "zig": bool(info.zig),
|
|
283
|
+
"go": bool(info.go), "kotlin": bool(info.kotlinc),
|
|
284
|
+
}
|
|
285
|
+
return tuple(b for b in backends if have.get(b))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
# CLI
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
def main(argv: list[str] | None = None) -> int:
|
|
293
|
+
import argparse
|
|
294
|
+
|
|
295
|
+
p = argparse.ArgumentParser(
|
|
296
|
+
prog="ge golden",
|
|
297
|
+
description="Check or regenerate the golden lowering corpus")
|
|
298
|
+
p.add_argument("--check", action="store_true",
|
|
299
|
+
help="fail if emitted code differs from the committed golden")
|
|
300
|
+
p.add_argument("--update", action="store_true",
|
|
301
|
+
help="regenerate goldens from the current emitters")
|
|
302
|
+
p.add_argument("--no-behaviour", action="store_true",
|
|
303
|
+
help="skip the compile-and-run check")
|
|
304
|
+
p.add_argument("--backends", default=None,
|
|
305
|
+
help="comma-separated subset (default: all)")
|
|
306
|
+
p.add_argument("-v", "--verbose", action="store_true")
|
|
307
|
+
args = p.parse_args(argv)
|
|
308
|
+
|
|
309
|
+
backends = BACKENDS
|
|
310
|
+
if args.backends:
|
|
311
|
+
backends = tuple(b.strip() for b in args.backends.split(",") if b.strip())
|
|
312
|
+
|
|
313
|
+
root = golden_root()
|
|
314
|
+
if not root.is_dir():
|
|
315
|
+
print(f"error: no golden corpus at {root}", file=sys.stderr)
|
|
316
|
+
return 2
|
|
317
|
+
|
|
318
|
+
if args.update:
|
|
319
|
+
report = update(root, backends)
|
|
320
|
+
print("GE golden corpus — update")
|
|
321
|
+
print(f" cases : {report.checked}")
|
|
322
|
+
print(f" written: {len(report.updated)}")
|
|
323
|
+
for name in report.updated:
|
|
324
|
+
print(f" {name}")
|
|
325
|
+
if not report.updated:
|
|
326
|
+
print(" (no changes)")
|
|
327
|
+
print()
|
|
328
|
+
print("review the diff, then commit it")
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
report = check(root, backends, behaviour=not args.no_behaviour)
|
|
332
|
+
print("GE golden corpus — check")
|
|
333
|
+
print(f" cases : {report.checked}")
|
|
334
|
+
print(f" backends: {', '.join(backends)}")
|
|
335
|
+
print()
|
|
336
|
+
|
|
337
|
+
if report.ok:
|
|
338
|
+
print("all goldens match and every case still prints its expected output")
|
|
339
|
+
return 0
|
|
340
|
+
|
|
341
|
+
print(f"{len(report.problems)} problem(s):")
|
|
342
|
+
for prob in report.problems:
|
|
343
|
+
print(f" [{prob.kind}] {prob.case}: {prob.detail}")
|
|
344
|
+
return 1
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
sys.exit(main())
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Identifier safety across target languages.
|
|
2
|
+
|
|
3
|
+
GE identifiers are Python names, so they may collide with a target
|
|
4
|
+
language's reserved words (`double` is a C++ type, `base` is a C# keyword,
|
|
5
|
+
`match` is a Rust keyword, ...).
|
|
6
|
+
|
|
7
|
+
Two kinds of identifier need different treatment:
|
|
8
|
+
|
|
9
|
+
* **Function names** can cross the C ABI (Rust calling a C++ function), so
|
|
10
|
+
every side must agree on the symbol. These are renamed once in the shared
|
|
11
|
+
IR by `rename_reserved_functions`, before any emitter runs.
|
|
12
|
+
* **Locals and parameters** never cross the ABI, so each backend escapes
|
|
13
|
+
them in its own idiom (`@base` in C#, `r#match` in Rust, `base_` in C++
|
|
14
|
+
which has no escape syntax).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .analyzer import FuncUnit
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Reserved word sets
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
CPP_RESERVED = {
|
|
25
|
+
# keywords
|
|
26
|
+
"alignas", "alignof", "and", "and_eq", "asm", "auto", "bitand", "bitor",
|
|
27
|
+
"bool", "break", "case", "catch", "char", "char8_t", "char16_t",
|
|
28
|
+
"char32_t", "class", "compl", "concept", "const", "consteval",
|
|
29
|
+
"constexpr", "constinit", "const_cast", "continue", "co_await",
|
|
30
|
+
"co_return", "co_yield", "decltype", "default", "delete", "do", "double",
|
|
31
|
+
"dynamic_cast", "else", "enum", "explicit", "export", "extern", "false",
|
|
32
|
+
"float", "for", "friend", "goto", "if", "inline", "int", "long",
|
|
33
|
+
"mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr",
|
|
34
|
+
"operator", "or", "or_eq", "private", "protected", "public", "register",
|
|
35
|
+
"reinterpret_cast", "requires", "return", "short", "signed", "sizeof",
|
|
36
|
+
"static", "static_assert", "static_cast", "struct", "switch", "template",
|
|
37
|
+
"this", "thread_local", "throw", "true", "try", "typedef", "typeid",
|
|
38
|
+
"typename", "union", "unsigned", "using", "virtual", "void", "volatile",
|
|
39
|
+
"wchar_t", "while", "xor", "xor_eq",
|
|
40
|
+
# core library type names: a function with one of these names would
|
|
41
|
+
# collide with the type itself
|
|
42
|
+
"std", "string", "vector", "map", "tuple", "pair", "set",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
RUST_RESERVED = {
|
|
46
|
+
"as", "break", "const", "continue", "crate", "dyn", "else", "enum",
|
|
47
|
+
"extern", "false", "fn", "for", "if", "impl", "in", "let", "loop",
|
|
48
|
+
"match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
|
|
49
|
+
"static", "struct", "super", "trait", "true", "type", "unsafe", "use",
|
|
50
|
+
"where", "while", "async", "await", "abstract", "become", "box", "do",
|
|
51
|
+
"final", "macro", "override", "priv", "try", "typeof", "unsized",
|
|
52
|
+
"virtual", "yield",
|
|
53
|
+
# core prelude type names
|
|
54
|
+
"String", "Vec", "Box", "Option", "Result", "Some", "None", "Ok", "Err",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
CSHARP_RESERVED = {
|
|
58
|
+
"abstract", "as", "base", "bool", "break", "byte", "case", "catch",
|
|
59
|
+
"char", "checked", "class", "const", "continue", "decimal", "default",
|
|
60
|
+
"delegate", "do", "double", "else", "enum", "event", "explicit",
|
|
61
|
+
"extern", "false", "finally", "fixed", "float", "for", "foreach", "goto",
|
|
62
|
+
"if", "implicit", "in", "int", "interface", "internal", "is", "lock",
|
|
63
|
+
"long", "namespace", "new", "null", "object", "operator", "out",
|
|
64
|
+
"override", "params", "private", "protected", "public", "readonly",
|
|
65
|
+
"ref", "return", "sbyte", "sealed", "short", "sizeof", "stackalloc",
|
|
66
|
+
"static", "string", "struct", "switch", "this", "throw", "true", "try",
|
|
67
|
+
"typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using",
|
|
68
|
+
"virtual", "void", "volatile", "while",
|
|
69
|
+
# core library type names
|
|
70
|
+
"Console", "Math", "Convert", "String", "Object",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
GO_RESERVED = {
|
|
74
|
+
"break", "case", "chan", "const", "continue", "default", "defer",
|
|
75
|
+
"else", "fallthrough", "for", "func", "go", "goto", "if", "import",
|
|
76
|
+
"interface", "map", "package", "range", "return", "select", "struct",
|
|
77
|
+
"switch", "type", "var",
|
|
78
|
+
# core prelude type names
|
|
79
|
+
"int", "int64", "float64", "string", "bool", "byte", "rune",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
KOTLIN_RESERVED = {
|
|
83
|
+
"as", "break", "class", "continue", "do", "else", "false", "for", "fun",
|
|
84
|
+
"if", "in", "interface", "is", "null", "object", "package", "return",
|
|
85
|
+
"super", "this", "throw", "true", "try", "typealias", "typeof", "val",
|
|
86
|
+
"var", "when", "while",
|
|
87
|
+
# core prelude type names
|
|
88
|
+
"Int", "Long", "Double", "String", "Boolean", "List", "Array", "Unit",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
ZIG_RESERVED = {
|
|
92
|
+
"align", "allowzero", "and", "anyframe", "anytype", "asm", "async",
|
|
93
|
+
"await", "break", "catch", "comptime", "const", "continue", "defer",
|
|
94
|
+
"else", "enum", "errdefer", "error", "export", "extern", "fn", "for",
|
|
95
|
+
"if", "inline", "noalias", "nosuspend", "opaque", "or", "orelse",
|
|
96
|
+
"packed", "pub", "resume", "return", "linksection", "struct", "suspend",
|
|
97
|
+
"switch", "test", "threadlocal", "try", "union", "unreachable",
|
|
98
|
+
"usingnamespace", "var", "volatile", "while",
|
|
99
|
+
# core prelude type names
|
|
100
|
+
"i64", "f64", "bool", "u8", "usize", "std",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#: backend name -> reserved words
|
|
104
|
+
RESERVED: dict[str, set[str]] = {
|
|
105
|
+
"cpp": CPP_RESERVED,
|
|
106
|
+
"rust": RUST_RESERVED,
|
|
107
|
+
"csharp": CSHARP_RESERVED,
|
|
108
|
+
"go": GO_RESERVED,
|
|
109
|
+
"kotlin": KOTLIN_RESERVED,
|
|
110
|
+
"zig": ZIG_RESERVED,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#: how each backend escapes a local/parameter name
|
|
114
|
+
ESCAPE_STYLE = {
|
|
115
|
+
"csharp": "prefix_at", # @base
|
|
116
|
+
"rust": "prefix_r", # r#match
|
|
117
|
+
"cpp": "suffix", # base_ (no escape syntax in C++)
|
|
118
|
+
"go": "suffix",
|
|
119
|
+
"kotlin": "suffix",
|
|
120
|
+
"zig": "suffix",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def is_reserved(name: str, backend) -> bool:
|
|
125
|
+
"""True if `name` is reserved by the given backend (or any of several)."""
|
|
126
|
+
if isinstance(backend, str):
|
|
127
|
+
return name in RESERVED.get(backend, set())
|
|
128
|
+
return any(name in RESERVED.get(b, set()) for b in backend)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def escape_local(name: str, backend: str) -> str:
|
|
132
|
+
"""Escape a local/parameter name in the backend's idiom."""
|
|
133
|
+
if not is_reserved(name, backend):
|
|
134
|
+
return name
|
|
135
|
+
style = ESCAPE_STYLE.get(backend, "suffix")
|
|
136
|
+
if style == "prefix_at":
|
|
137
|
+
return "@" + name
|
|
138
|
+
if style == "prefix_r":
|
|
139
|
+
return "r#" + name
|
|
140
|
+
return name + "_"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def safe_function_name(name: str, backends) -> str:
|
|
144
|
+
"""A function name that is safe in every one of `backends`.
|
|
145
|
+
|
|
146
|
+
Function symbols can cross the C ABI, so the rename has to be decided
|
|
147
|
+
once, for all backends at the same time.
|
|
148
|
+
"""
|
|
149
|
+
for backend in backends:
|
|
150
|
+
if is_reserved(name, backend):
|
|
151
|
+
return name + "_"
|
|
152
|
+
return name
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# IR-level rename
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def rename_reserved_functions(units: list[FuncUnit], backends) -> dict[str, str]:
|
|
160
|
+
"""Rename functions whose names collide with a target keyword.
|
|
161
|
+
|
|
162
|
+
Applies the rename to definitions *and* call sites across every unit, so
|
|
163
|
+
the whole program — including cross-backend `extern "C"` declarations —
|
|
164
|
+
agrees on one symbol. Returns the {old: new} map for reporting.
|
|
165
|
+
"""
|
|
166
|
+
import ast
|
|
167
|
+
|
|
168
|
+
# `main` is the entry point: emitters resolve it by name, so it is never
|
|
169
|
+
# renamed. (It is not in the reserved sets either.)
|
|
170
|
+
collisions = {u.name for u in units
|
|
171
|
+
if u.name != "main" and is_reserved(u.name, backends)}
|
|
172
|
+
if not collisions:
|
|
173
|
+
return {}
|
|
174
|
+
mapping = {name: safe_function_name(name, backends) for name in collisions}
|
|
175
|
+
|
|
176
|
+
# rename definitions
|
|
177
|
+
for u in units:
|
|
178
|
+
if u.name in mapping:
|
|
179
|
+
u.name = mapping[u.name]
|
|
180
|
+
|
|
181
|
+
# rename call sites and any reference
|
|
182
|
+
class _Renamer(ast.NodeTransformer):
|
|
183
|
+
def visit_Name(self, node: ast.Name) -> ast.AST:
|
|
184
|
+
if node.id in mapping:
|
|
185
|
+
node.id = mapping[node.id]
|
|
186
|
+
return node
|
|
187
|
+
|
|
188
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST:
|
|
189
|
+
if node.name in mapping:
|
|
190
|
+
node.name = mapping[node.name]
|
|
191
|
+
self.generic_visit(node)
|
|
192
|
+
return node
|
|
193
|
+
|
|
194
|
+
renamer = _Renamer()
|
|
195
|
+
for u in units:
|
|
196
|
+
body = u.body
|
|
197
|
+
if body is None:
|
|
198
|
+
continue
|
|
199
|
+
# FuncUnit.body is the function node in some paths and the statement
|
|
200
|
+
# list in others; normalise before rewriting.
|
|
201
|
+
if isinstance(body, ast.AST):
|
|
202
|
+
u.body = renamer.visit(body)
|
|
203
|
+
elif isinstance(body, list):
|
|
204
|
+
u.body = [renamer.visit(stmt) for stmt in body]
|
|
205
|
+
|
|
206
|
+
return mapping
|