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