gelang 0.1.3 → 0.1.4

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 CHANGED
@@ -4,6 +4,59 @@ All notable changes to GE. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.1.4] — 2026-09-13
8
+
9
+ Found by `scripts/e2e.py`, a new harness that compiles and runs 118
10
+ programs on every installed backend and compares stdout with CPython.
11
+
12
+ ### Fixed
13
+
14
+ - **Wrong output.** `print(True)` printed lowercase `true` on Rust, C++,
15
+ Go, Zig and Kotlin; list printing used each target's native format
16
+ (`[1 2 3]` in Go, `{ 1, 2, 3 }` in Zig). Both now match Python.
17
+ - **Dict iteration** walked `(key, value)` pairs instead of keys, so
18
+ `for k in d: d[k]` failed to compile on every backend.
19
+ - **List slicing** was marked unsupported, which dropped the whole
20
+ function and surfaced as the misleading `main function not found`.
21
+ `xs[a:b]`, `xs[a:]`, `xs[:b]` and `xs[:]` are implemented everywhere.
22
+ - **`str.split` / `str.join`** had no implementation at all.
23
+ - **`str.replace`** had no implementation at all.
24
+ - **`min(a, b)` / `max(a, b)`** emitted a bare call no target provides.
25
+ - **`abs()`** on a literal was ambiguous in Rust, untyped in Zig, and
26
+ absent for `int64` in Go.
27
+ - **Default arguments** were rejected outright. A call may now supply
28
+ between `n_required` and `len(params)` arguments.
29
+ - **`set` annotations** became `f64`, because `set` was missing from the
30
+ native type mapping. Added `set_type` per backend.
31
+ - **Lists of strings** could not be expressed: only Rust and C++ had a
32
+ `{T}` placeholder in `list_type`.
33
+ - **C++ `std::sqrt`** appeared before `#include <cmath>`.
34
+ - **Kotlin** used `Math.pow` (no such class in Kotlin/Native) and a
35
+ C-style ternary (Kotlin has no `?:`).
36
+ - **C++ `upper()` / `lower()`** emitted a bare comment, so the program
37
+ compiled but did nothing.
38
+ - **Rust string concatenation** moved the left operand.
39
+ - **Emitter crashes**: when the entry function was rejected, zig, go,
40
+ kotlin and csharp raised `IndexError` instead of reporting the reason.
41
+ - **`ge tools check`** disagreed with `ge doctor` because `CompilerInfo`
42
+ field names (`dotnet`, `kotlinc`) differ from the public toolchain
43
+ names.
44
+ - **Zig** emitted a raw newline inside a string literal, and
45
+ `_mark_unsupported` produced a C-style comment, which Zig rejects.
46
+
47
+ ### Added
48
+
49
+ - `scripts/e2e.py` — end-to-end tests against real toolchains.
50
+ - `ge doctor` — runtime and toolchain check with install hints.
51
+
52
+ ### Known limitations
53
+
54
+ - Zig models lists as fixed slices, so `list.append` is rejected with a
55
+ clear diagnostic rather than compiling. Switching the backend to
56
+ `std.ArrayList` needs an allocator threaded through every signature.
57
+ - Tuples are supported at arity 2; other arities are rejected with a
58
+ diagnostic instead of emitting broken code.
59
+
7
60
  ## [Unreleased]
8
61
 
9
62
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gelang",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "One typed source language, compiled to Rust, C++, C#, Zig, Go, or Kotlin — chosen per function. Generates Flutter and React UIs from one DSL.",
5
5
  "keywords": [
6
6
  "ge",
@@ -38,6 +38,11 @@ class FuncUnit:
38
38
  param_elem_types: dict[str, str] = field(default_factory=dict)
39
39
  # dict key/value types: {"d": ("str", "int")} means d is dict[str, int]
40
40
  param_dict_types: dict[str, tuple[str, str]] = field(default_factory=dict)
41
+ # Number of leading parameters without a default. Calls may
42
+ # supply between n_required_params and len(params) arguments.
43
+ n_required_params: int = 0
44
+ # Default expressions per parameter name, as source text.
45
+ param_defaults: dict[str, str] = field(default_factory=dict)
41
46
 
42
47
  def mark_unsupported(self, reason: str) -> None:
43
48
  self.supported = False
@@ -310,6 +315,19 @@ def parse_source_full(source: str) -> tuple[list[FuncUnit], list[ClassUnit]]:
310
315
  ret = _norm_type(node.returns)
311
316
  if ret not in SCALAR_TYPES and ret not in CONTAINER_TYPES and ret != "None" and ret not in _class_names:
312
317
  ret = "any"
318
+ # Default arguments: a call may supply between n_required and
319
+ # len(params) arguments; the rest are filled in at the call site.
320
+ _args = node.args.args
321
+ _n_defaults = len(node.args.defaults)
322
+ _n_required = len(_args) - _n_defaults
323
+ _param_defaults: dict[str, str] = {}
324
+ for _i, _a in enumerate(_args):
325
+ if _i >= _n_required:
326
+ _d = node.args.defaults[_i - _n_required]
327
+ try:
328
+ _param_defaults[_a.arg] = ast.unparse(_d)
329
+ except Exception:
330
+ _param_defaults[_a.arg] = ""
313
331
  unit = FuncUnit(
314
332
  name=node.name,
315
333
  lineno=node.lineno,
@@ -319,6 +337,8 @@ def parse_source_full(source: str) -> tuple[list[FuncUnit], list[ClassUnit]]:
319
337
  source="\n".join(src_lines[node.lineno - 1 : node.end_lineno]),
320
338
  param_elem_types=param_elem_types,
321
339
  param_dict_types=param_dict_types,
340
+ n_required_params=_n_required,
341
+ param_defaults=_param_defaults,
322
342
  )
323
343
  # parse @rust/@cpp/@dart/@csharp/@zig/@go decorators for per-function backend selection
324
344
  for dec in node.decorator_list:
@@ -293,10 +293,22 @@ def get_tools_status() -> dict[str, dict]:
293
293
  """Get status of all known toolchains."""
294
294
  from .config import detect_compilers
295
295
  detected = detect_compilers()
296
+ # CompilerInfo field names differ from the public toolchain names
297
+ # (`dotnet` backs C#, `kotlinc` backs Kotlin), so map them explicitly.
298
+ fields = {
299
+ "rust": "rustc",
300
+ "cpp": "cpp",
301
+ "csharp": "dotnet",
302
+ "zig": "zig",
303
+ "go": "go",
304
+ "kotlin": "kotlinc",
305
+ "dart": "dart",
306
+ }
296
307
  status = {}
297
308
  for name in ["rust", "cpp", "csharp", "zig", "go", "kotlin", "dart"]:
298
- available = is_toolchain_available(name) or getattr(detected, name, None) is not None
299
- path = get_toolchain_path(name) or getattr(detected, name, None)
309
+ detected_path = getattr(detected, fields.get(name, name), None)
310
+ available = is_toolchain_available(name) or detected_path is not None
311
+ path = get_toolchain_path(name) or detected_path
300
312
  downloadable = name in DOWNLOAD_URLS
301
313
  status[name] = {
302
314
  "available": bool(available),
@@ -48,6 +48,31 @@ class Spec:
48
48
  var_decl_template: str = "{nt} {target} = {val};" # typed variable declaration
49
49
  ffi_prefix: str = "" # decoration for C-ABI exports in library mode
50
50
  indent: str = " "
51
+ # Type-aware exponentiation. Integer `**` and float `**` need different
52
+ # target syntax in most languages; when these are empty the emitter falls
53
+ # back to pow_call.
54
+ pow_int: str = ""
55
+ pow_float: str = ""
56
+ # two-argument min(a, b) / max(a, b). Most targets have no plain
57
+ # min/max function, so these carry the target-specific spelling.
58
+ min2_call: str = ""
59
+ max2_call: str = ""
60
+ # List slicing. `{x}` is the list, `{start}` and `{stop}` are integer
61
+ # bounds. list_copy is the full-slice form (`xs[:]`), which Python
62
+ # defines as a shallow copy.
63
+ # str.split(sep) / sep.join(list). Empty means unsupported.
64
+ str_split: str = ""
65
+ str_join: str = ""
66
+ list_slice: str = ""
67
+ list_copy: str = ""
68
+ # Iterating a dict in Python yields its keys. `dict_keys` is an
69
+ # expression producing those keys; `foreach_dict_template` is the
70
+ # loop head to use with it (Go ranges a map differently).
71
+ dict_keys: str = ""
72
+ foreach_dict_template: str = ""
73
+ # Printing a sequence. Python renders lists as `[1, 2, 3]`, so every
74
+ # backend must match that rather than its own native format.
75
+ print_list: str = ""
51
76
  # string operations
52
77
  str_concat: str = "{l} + {r}" # string concatenation
53
78
  str_len: str = "{x}.length()" # string length (C++ style)
@@ -74,6 +99,7 @@ class Spec:
74
99
  dict_contains: str = "({d}.find({k}) != {d}.end())"
75
100
  # tuple support
76
101
  tuple_type: str = "std::tuple<{T}>"
102
+ set_type: str = "" # native set type
77
103
  tuple_get: str = "std::get<{i}>({t})"
78
104
  # list length (separate from len_call which may be used for strings)
79
105
  list_len: str = "{x}.size()"
@@ -90,6 +116,9 @@ class Emitter:
90
116
  self.params: set[str] = set() # param names (already borrowed if list)
91
117
  self.library_mode: bool = False # emit C-ABI exports instead of a main()
92
118
  self.extern_names: set[str] = set() # extern "C" fns from other backends
119
+ # {name: ([param names], {param: default source text})} so a call site
120
+ # that omits defaults can fill them in.
121
+ self.func_signatures: dict[str, tuple[list[str], dict[str, str]]] = {}
93
122
  self.force_extern_c: bool = False # force extern "C" on all functions (multi-backend)
94
123
  self.export_names: set[str] = set() # Rust fns called from C++ — export them
95
124
  self.class_names: set[str] = set() # known class names for constructor calls
@@ -179,6 +208,10 @@ class Emitter:
179
208
  return self.spec.dict_type.format(V=self.spec.list_elem_type)
180
209
  if t == "tuple":
181
210
  return self.spec.tuple_type.format(T=self.spec.list_elem_type)
211
+ if t == "set":
212
+ if self.spec.set_type:
213
+ return self.spec.set_type
214
+ return self.spec.list_type.format(T=self.spec.list_elem_type)
182
215
  # class type — use the class name directly
183
216
  if t in self.class_names:
184
217
  return t
@@ -220,6 +253,12 @@ class Emitter:
220
253
  return self.spec.dict_type.format(V=self.spec.list_elem_type)
221
254
  if t == "tuple":
222
255
  return self.spec.tuple_type.format(T=self.spec.list_elem_type)
256
+ if t == "set":
257
+ if self.spec.set_type:
258
+ if self.spec.name == "cpp":
259
+ return f"const {self.spec.set_type}&"
260
+ return self.spec.set_type
261
+ return self.spec.list_param_type
223
262
  # class type — pass by value (or const ref in C++)
224
263
  if t in self.class_names:
225
264
  if self.spec.name == "cpp":
@@ -334,6 +373,12 @@ class Emitter:
334
373
  self.var_types[target] = t
335
374
  self.declared.add(target)
336
375
  nt = self.py_to_native(t)
376
+ if t == "list":
377
+ # a bare `list` annotation carries no element type; take it
378
+ # from the value so `xs = "a,b".split(",")` is a string list
379
+ elem = self._list_elem_of_value(node.value)
380
+ if elem and elem != self.spec.list_elem_type:
381
+ nt = self.spec.list_type.format(T=elem)
337
382
  if self.spec.name == "rust":
338
383
  self.lines.append(f"{ind}let mut {target}: {nt} = {self.expr(node.value)};")
339
384
  else:
@@ -754,7 +799,12 @@ class Emitter:
754
799
  msg = f"{self.current_func}: {feature}"
755
800
  if msg not in self.unsupported_emissions:
756
801
  self.unsupported_emissions.append(msg)
757
- return f"/*unsupported: {feature}*/"
802
+ # Use the backend's own comment syntax: a C-style comment is not
803
+ # valid in Zig, and the placeholder still has to parse.
804
+ marker = getattr(self.spec, "comment", "//") or "//"
805
+ if marker.startswith("/*"):
806
+ return f"/*unsupported: {feature}*/"
807
+ return f"{marker} unsupported: {feature}"
758
808
 
759
809
  def _ident(self, name: str) -> str:
760
810
  """Escape a local/parameter name that collides with a target keyword.
@@ -858,12 +908,21 @@ class Emitter:
858
908
  self._for_zip(node, ind)
859
909
  return
860
910
  else:
861
- # iterate a container (list)
911
+ # iterate a container
862
912
  iter_s = self.expr(it)
863
- elem_type = self._iter_elem_type(it)
864
- self.var_types[var] = elem_type
865
- head = self.spec.foreach_template.format(var=var, iter=iter_s,
866
- etype=self.spec.list_elem_type)
913
+ if self.infer_type(it) == "dict" and self.spec.dict_keys:
914
+ # Python iterates dict keys, not (key, value) pairs
915
+ iter_s = self.spec.dict_keys.format(x=iter_s)
916
+ tmpl = (self.spec.foreach_dict_template
917
+ or self.spec.foreach_template)
918
+ self.var_types[var] = "str"
919
+ head = tmpl.format(var=var, iter=iter_s,
920
+ etype=self.spec.list_elem_type)
921
+ else:
922
+ elem_type = self._iter_elem_type(it)
923
+ self.var_types[var] = elem_type
924
+ head = self.spec.foreach_template.format(
925
+ var=var, iter=iter_s, etype=self.spec.list_elem_type)
867
926
  self.lines.append(f"{ind}{head} {{")
868
927
  self.indent_lvl += 1
869
928
  for s in node.body:
@@ -991,7 +1050,7 @@ class Emitter:
991
1050
  if isinstance(node.op, ast.FloorDiv):
992
1051
  return self.spec.floor_div.format(l=left, r=right)
993
1052
  if isinstance(node.op, ast.Pow):
994
- return self.spec.pow_call.format(l=left, r=right)
1053
+ return self._pow(left, right, node)
995
1054
  if isinstance(node.op, ast.Add):
996
1055
  lt = self.infer_type(node.left)
997
1056
  rt = self.infer_type(node.right)
@@ -1079,8 +1138,11 @@ class Emitter:
1079
1138
  orelse = self.expr(node.orelse)
1080
1139
  if self.spec.name == "rust":
1081
1140
  return f"if {cond} {{ {body} }} else {{ {orelse} }}"
1082
- if self.spec.name in ("cpp", "csharp", "kotlin"):
1141
+ if self.spec.name in ("cpp", "csharp"):
1083
1142
  return f"({cond} ? {body} : {orelse})"
1143
+ if self.spec.name == "kotlin":
1144
+ # Kotlin has no ?: operator — it uses an if expression
1145
+ return f"(if ({cond}) {body} else {orelse})"
1084
1146
  if self.spec.name == "go":
1085
1147
  return f"(func() int64 {{ if {cond} {{ return {body} }}; return {orelse} }}())"
1086
1148
  if self.spec.name == "zig":
@@ -1104,9 +1166,18 @@ class Emitter:
1104
1166
  if stop:
1105
1167
  return self.spec.str_slice_end.format(x=base, end=stop)
1106
1168
  return base # full slice s[:] = s
1107
- # list slicing — not fully supported, mark as unsupported
1108
- if start and stop:
1109
- return self._mark_unsupported(f"list slice {base}[{start}:{stop}]")
1169
+ # list slicing — xs[a:b], xs[a:], xs[:b], xs[:]
1170
+ if self.spec.list_slice:
1171
+ length = self.spec.len_call.format(x=base)
1172
+ if not start:
1173
+ start = self.spec.int_cast.format(x="0")
1174
+ if not stop:
1175
+ stop = length
1176
+ if not sl.lower and not sl.upper:
1177
+ if self.spec.list_copy:
1178
+ return self.spec.list_copy.format(x=base)
1179
+ return self.spec.list_slice.format(
1180
+ x=base, start=start, stop=stop)
1110
1181
  return self._mark_unsupported(f"list slice {base}")
1111
1182
  idx = self.expr(node.slice)
1112
1183
  if base_type == "dict":
@@ -1135,17 +1206,22 @@ class Emitter:
1135
1206
  return self.spec.index_call.format(x=base, i=idx)
1136
1207
  if isinstance(node, ast.List):
1137
1208
  elems = ", ".join(self.expr(e) for e in node.elts)
1209
+ # a literal of strings needs a string element type, not the
1210
+ # backend's default integer element
1211
+ elem = self._list_elem_of_value(node) or self.spec.list_elem_type
1138
1212
  if self.spec.name == "rust":
1139
1213
  return f"vec![{elems}]"
1140
1214
  if self.spec.name == "go":
1141
- return f"[]{self.spec.list_elem_type}{{{elems}}}"
1215
+ return f"[]{elem}{{{elems}}}"
1142
1216
  if self.spec.name == "kotlin":
1143
1217
  return f"mutableListOf({elems})"
1144
1218
  if self.spec.name == "zig":
1145
1219
  return f"&[_]i64{{ {elems} }}"
1146
1220
  if self.spec.name == "csharp":
1147
- return f"new {self.spec.list_type}{{{elems}}}"
1148
- return f"std::vector<{self.spec.list_elem_type}>{{{elems}}}"
1221
+ lt = self.spec.list_type.format(T=elem)
1222
+ return f"new {lt}{{{elems}}}"
1223
+ lt = self.spec.list_type.format(T=elem)
1224
+ return f"{lt}{{{elems}}}"
1149
1225
  if isinstance(node, ast.ListComp):
1150
1226
  return self._list_comp(node)
1151
1227
  if isinstance(node, ast.Dict):
@@ -1186,15 +1262,36 @@ class Emitter:
1186
1262
  if self.spec.name == "csharp":
1187
1263
  return f"new HashSet<long>{{{elems}}}"
1188
1264
  if self.spec.name == "go":
1189
- return f"map[int64_t]struct{{}}{{}}"
1265
+ # Go has no set type; GE uses map[elem]bool. Go rejects
1266
+ # duplicate *constant* keys at compile time, so literals are
1267
+ # deduplicated here (a runtime set would collapse them anyway).
1268
+ seen: list[str] = []
1269
+ for e in node.elts:
1270
+ rendered = self.expr(e)
1271
+ if rendered not in seen:
1272
+ seen.append(rendered)
1273
+ if not seen:
1274
+ return "map[int64]bool{}"
1275
+ pairs = ", ".join(f"{v}: true" for v in seen)
1276
+ return f"map[int64]bool{{{pairs}}}"
1190
1277
  if self.spec.name == "kotlin":
1191
1278
  return f"hashSetOf({elems})"
1279
+ if self.spec.name == "zig":
1280
+ # no allocator-backed set in the Zig runtime
1281
+ return self._mark_unsupported("set literal (Zig)")
1192
1282
  return f"std::set<int64_t>{{{elems}}}"
1193
1283
  if isinstance(node, ast.SetComp):
1194
1284
  return self._set_comp(node)
1195
1285
  if isinstance(node, ast.DictComp):
1196
1286
  return self._dict_comp(node)
1197
1287
  if isinstance(node, ast.Tuple):
1288
+ # GE models tuples as fixed-width pairs. Anything else would need
1289
+ # a per-arity native type (Go structs, Kotlin Triple, ...), so it
1290
+ # is rejected loudly rather than emitted as broken code.
1291
+ if len(node.elts) != 2:
1292
+ return self._mark_unsupported(
1293
+ f"tuple of {len(node.elts)} elements "
1294
+ f"(only 2-element tuples are supported)")
1198
1295
  elems = ", ".join(self.expr(e) for e in node.elts)
1199
1296
  if self.spec.name == "rust":
1200
1297
  return f"({elems})"
@@ -1273,10 +1370,16 @@ class Emitter:
1273
1370
  if fname == "sum":
1274
1371
  return self.spec.sum_call.format(it=self.expr(node.args[0]))
1275
1372
  if fname in ("min", "max"):
1276
- tmpl = self.spec.min_call if fname == "min" else self.spec.max_call
1373
+ is_min = fname == "min"
1374
+ tmpl = self.spec.min_call if is_min else self.spec.max_call
1277
1375
  raw = [self.expr(a) for a in node.args]
1278
1376
  if len(raw) == 1:
1279
1377
  return tmpl.format(it=raw[0])
1378
+ if len(raw) == 2:
1379
+ pair = (self.spec.min2_call if is_min
1380
+ else self.spec.max2_call)
1381
+ if pair:
1382
+ return pair.format(a=raw[0], b=raw[1])
1280
1383
  return f"{fname}({', '.join(raw)})"
1281
1384
  if fname == "pow":
1282
1385
  return self.spec.pow_call.format(l=self.expr(node.args[0]), r=self.expr(node.args[1]))
@@ -1584,8 +1687,26 @@ class Emitter:
1584
1687
  return self._stdlib_call("ge_log", node.args)
1585
1688
  if fname == "exp":
1586
1689
  return self._stdlib_call("ge_exp", node.args)
1690
+ if (isinstance(node.func, ast.Attribute) and node.func.attr == "split"
1691
+ and len(node.args) == 1):
1692
+ if not self.spec.str_split:
1693
+ return self._mark_unsupported(
1694
+ f"str.split (not available on the {self.spec.name} backend)")
1695
+ return self.spec.str_split.format(
1696
+ x=self.expr(node.func.value), sep=self.expr(node.args[0]))
1697
+ if (isinstance(node.func, ast.Attribute) and node.func.attr == "join"
1698
+ and len(node.args) == 1):
1699
+ if not self.spec.str_join:
1700
+ return self._mark_unsupported(
1701
+ f"str.join (not available on the {self.spec.name} backend)")
1702
+ return self.spec.str_join.format(
1703
+ x=self.expr(node.args[0]), sep=self.expr(node.func.value))
1587
1704
  if isinstance(node.func, ast.Attribute) and node.func.attr == "append":
1588
1705
  base = self.expr(node.func.value)
1706
+ if not self.spec.append_call:
1707
+ return self._mark_unsupported(
1708
+ f"list.append (the {self.spec.name} backend models lists "
1709
+ f"as fixed slices)")
1589
1710
  return self.spec.append_call.format(x=base, v=self.expr(node.args[0]))
1590
1711
  # super().method(args) -> ParentClass_method(_self, args)
1591
1712
  if (isinstance(node.func, ast.Attribute) and
@@ -1620,7 +1741,7 @@ class Emitter:
1620
1741
  if self.spec.name == "rust":
1621
1742
  return f"{base}.to_uppercase()"
1622
1743
  if self.spec.name == "cpp":
1623
- return f"/*str.upper()*/"
1744
+ return f"geStrUpper({base})"
1624
1745
  if self.spec.name == "csharp":
1625
1746
  return f"{base}.ToUpper()"
1626
1747
  if self.spec.name == "go":
@@ -1631,6 +1752,8 @@ class Emitter:
1631
1752
  if method == "lower":
1632
1753
  if self.spec.name == "rust":
1633
1754
  return f"{base}.to_lowercase()"
1755
+ if self.spec.name == "cpp":
1756
+ return f"geStrLower({base})"
1634
1757
  if self.spec.name == "csharp":
1635
1758
  return f"{base}.ToLower()"
1636
1759
  if self.spec.name == "go":
@@ -1638,6 +1761,19 @@ class Emitter:
1638
1761
  if self.spec.name == "kotlin":
1639
1762
  return f"{base}.lowercase()"
1640
1763
  return f"{base}.lower()"
1764
+ if method == "replace" and len(node.args) == 2:
1765
+ a = self.expr(node.args[0])
1766
+ b = self.expr(node.args[1])
1767
+ if self.spec.name == "rust":
1768
+ return f"{base}.replace({a}.as_str(), {b}.as_str())"
1769
+ if self.spec.name == "cpp":
1770
+ return f"geStrReplace({base}, {a}, {b})"
1771
+ if self.spec.name == "csharp":
1772
+ return f"{base}.Replace({a}, {b})"
1773
+ if self.spec.name == "go":
1774
+ return f"strings.ReplaceAll({base}, {a}, {b})"
1775
+ if self.spec.name == "kotlin":
1776
+ return f"{base}.replace({a}, {b})"
1641
1777
  if method == "strip":
1642
1778
  if self.spec.name == "rust":
1643
1779
  return f"{base}.trim().to_string()"
@@ -1674,12 +1810,36 @@ class Emitter:
1674
1810
  # **kwargs — not supported, skip
1675
1811
  continue
1676
1812
  args.append(self.expr(kw.value))
1813
+ args = self._fill_defaults(fname, args, node)
1677
1814
  call_str = f"{fname}({', '.join(args)})"
1678
1815
  # wrap extern "C" calls in unsafe block (Rust requires this)
1679
1816
  if fname in self.extern_names and self.spec.name == "rust":
1680
1817
  return f"unsafe {{ {call_str} }}"
1681
1818
  return call_str
1682
1819
 
1820
+ def _fill_defaults(self, fname: str, args: list[str],
1821
+ node: ast.Call) -> list[str]:
1822
+ """Append default arguments a call site omitted.
1823
+
1824
+ `def f(a, b=10)` called as `f(5)` must emit `f(5, 10)` in the target
1825
+ language, which has no notion of Python default parameters.
1826
+ """
1827
+ if node.keywords:
1828
+ return args
1829
+ sig = self.func_signatures.get(fname)
1830
+ if not sig:
1831
+ return args
1832
+ names, defaults = sig
1833
+ if len(args) >= len(names):
1834
+ return args
1835
+ filled = list(args)
1836
+ for name in names[len(filled):]:
1837
+ d = defaults.get(name)
1838
+ if d is None or d == "":
1839
+ break
1840
+ filled.append(d)
1841
+ return filled
1842
+
1683
1843
  def _str_call(self, arg: ast.AST) -> str:
1684
1844
  """Convert a value to string."""
1685
1845
  t = self.infer_type(arg)
@@ -2201,6 +2361,8 @@ class Emitter:
2201
2361
  return self.spec.print_float.format(v=v)
2202
2362
  if t == "bool":
2203
2363
  return self.spec.print_bool.format(v=v)
2364
+ if t in ("list", "tuple", "set") and self.spec.print_list:
2365
+ return self.spec.print_list.format(v=v)
2204
2366
  # fallback to heuristics on the source text
2205
2367
  if any(c in v for c in ".") and not v.startswith('"'):
2206
2368
  return self.spec.print_float.format(v=v)
@@ -2226,6 +2388,50 @@ class Emitter:
2226
2388
  return base if base in ("list", "dict", "tuple", "set") else "list"
2227
2389
  return "int"
2228
2390
 
2391
+ def _list_elem_of_value(self, node: ast.AST) -> str:
2392
+ """Native element type of a list-producing expression, or "".
2393
+
2394
+ A bare `list` annotation says nothing about the element type, so it is
2395
+ taken from the value: `"a,b".split(",")` yields strings, not ints.
2396
+ """
2397
+ if (isinstance(node, ast.Call)
2398
+ and isinstance(node.func, ast.Attribute)):
2399
+ attr = node.func.attr
2400
+ if attr in ("split", "splitlines", "keys"):
2401
+ return self.spec.types.get("str") or self._native_elem_type("str")
2402
+ if attr == "values":
2403
+ return self.spec.list_elem_type
2404
+ if attr in ("copy", "sorted", "reverse"):
2405
+ return self._list_elem_of_value(node.func.value) or ""
2406
+ if attr == "split" or attr == "items":
2407
+ return self.spec.types.get("str", "String")
2408
+ if isinstance(node, ast.List):
2409
+ types = [self.infer_type(e) for e in node.elts]
2410
+ types = [t for t in types if t]
2411
+ if types and all(t == types[0] for t in types):
2412
+ return self._native_elem_type(types[0])
2413
+ if isinstance(node, ast.ListComp):
2414
+ return self._native_elem_type(self.infer_type(node.elt))
2415
+ if isinstance(node, ast.Subscript):
2416
+ return self._list_elem_of_value(node.value)
2417
+ return ""
2418
+
2419
+ def _pow(self, left: str, right: str, node: ast.AST) -> str:
2420
+ """Render `a ** b`.
2421
+
2422
+ Integer and float exponentiation need different target syntax, and a
2423
+ bare literal like `2 ** 10` is ambiguous in Rust, so the operand type
2424
+ decides which template is used.
2425
+ """
2426
+ lt = self.infer_type(node.left)
2427
+ rt = self.infer_type(node.right)
2428
+ is_float = lt == "float" or rt == "float"
2429
+ if is_float and self.spec.pow_float:
2430
+ return self.spec.pow_float.format(l=left, r=right)
2431
+ if not is_float and self.spec.pow_int:
2432
+ return self.spec.pow_int.format(l=left, r=right)
2433
+ return self.spec.pow_call.format(l=left, r=right)
2434
+
2229
2435
  def infer_type(self, node: ast.AST) -> str:
2230
2436
  if isinstance(node, ast.Constant):
2231
2437
  if isinstance(node.value, bool):
@@ -12,6 +12,10 @@ SPEC = Spec(
12
12
  list_type="std::vector<{T}>",
13
13
  list_param_type="std::vector<int64_t>&",
14
14
  list_elem_type="int64_t",
15
+ str_split="geStrSplit({x}, {sep})",
16
+ str_join="geStrJoin({x}, {sep})",
17
+ list_slice="std::vector<int64_t>({x}.begin() + ({start}), {x}.begin() + ({stop}))",
18
+ list_copy="std::vector<int64_t>({x})",
15
19
  borrow_list_arg=False,
16
20
  range_call="for_range({lo}, {hi})",
17
21
  range_step_call="for_range({lo}, {hi}, {step})",
@@ -20,8 +24,9 @@ SPEC = Spec(
20
24
  print_int='std::cout << ({v}) << std::endl',
21
25
  print_float='std::cout << ({v}) << std::endl',
22
26
  print_str='std::cout << ({v}) << std::endl',
23
- print_bool='std::cout << ({v}) << std::endl',
27
+ print_bool='std::cout << (({v}) ? "True" : "False") << std::endl',
24
28
  print_generic='std::cout << ({v}) << std::endl',
29
+ print_list='std::cout << geListStr({v}) << std::endl',
25
30
  int_cast="static_cast<int64_t>({x})",
26
31
  float_cast="static_cast<double>({x})",
27
32
  float_div="(static_cast<double>({l}) / static_cast<double>({r}))",
@@ -29,9 +34,13 @@ SPEC = Spec(
29
34
  sum_call="std::accumulate({it}.begin(), {it}.end(), 0LL)",
30
35
  abs_int="std::abs({x})",
31
36
  abs_float="std::fabs({x})",
37
+ min2_call="std::min({a}, {b})",
38
+ max2_call="std::max({a}, {b})",
32
39
  min_call="*std::min_element({it}.begin(), {it}.end())",
33
40
  max_call="*std::max_element({it}.begin(), {it}.end())",
34
41
  pow_call="std::pow({l}, {r})",
42
+ pow_int="static_cast<int64_t>(std::pow(static_cast<double>({l}), static_cast<double>({r})))",
43
+ pow_float="std::pow({l}, {r})",
35
44
  append_call="{x}.push_back({v})",
36
45
  index_call="{x}[{i}]",
37
46
  comment="//",
@@ -51,8 +60,10 @@ SPEC = Spec(
51
60
  struct_field_template=" {type} {name};",
52
61
  struct_new_template="{name} {name}_new({params}) {{\n{body}\n}}",
53
62
  dict_type="std::map<std::string, {V}>",
63
+ set_type="std::set<int64_t>",
54
64
  dict_get="{d}.at({k})",
55
65
  dict_set="{d}[{k}] = {v}",
66
+ dict_keys="geDictKeys({x})",
56
67
  dict_contains="({d}.find({k}) != {d}.end())",
57
68
  tuple_type="std::tuple<{T}, {T}>",
58
69
  tuple_get="std::get<{i}>({t})",
@@ -80,8 +91,13 @@ class CppEmitter(Emitter):
80
91
  )
81
92
  else:
82
93
  iter_s = self.expr(it)
83
- self.var_types[var] = "int"
84
- self.lines.append(f"{ind}for (auto& {var} : {iter_s}) {{")
94
+ if self.infer_type(it) == "dict":
95
+ self.var_types[var] = "str"
96
+ self.lines.append(
97
+ f"{ind}for (auto {var} : geDictKeys({iter_s})) {{")
98
+ else:
99
+ self.var_types[var] = "int"
100
+ self.lines.append(f"{ind}for (auto& {var} : {iter_s}) {{")
85
101
  self.indent_lvl += 1
86
102
  for s in node.body:
87
103
  self.stmt(s)
@@ -135,6 +151,11 @@ def emit_cpp(units: list[FuncUnit], entry: str | None,
135
151
  constants: dict | None = None,
136
152
  preamble: dict | None = None) -> tuple[str, dict[str, str]]:
137
153
  emitter = CppEmitter(SPEC)
154
+ emitter.func_signatures = {
155
+ u.name: ([p for p, _t in u.params],
156
+ dict(getattr(u, 'param_defaults', {})))
157
+ for u in units
158
+ }
138
159
  emitter.library_mode = library_mode
139
160
  emitter.constants = constants or {}
140
161
  # in multi-backend mode, force extern "C" on all functions so Rust can link
@@ -160,6 +181,8 @@ def emit_cpp(units: list[FuncUnit], entry: str | None,
160
181
  "#include <cmath>\n"
161
182
  "#include <numeric>\n"
162
183
  "#include <algorithm>\n"
184
+ "#include <set>\n"
185
+ "#include <cctype>\n"
163
186
  "#include <string>\n\n"
164
187
  "using std::cout;\n"
165
188
  "using std::endl;\n\n"
@@ -14,9 +14,13 @@ from ..analyzer import FuncUnit
14
14
  SPEC = Spec(
15
15
  name="csharp",
16
16
  types={"int": "long", "float": "double", "bool": "bool", "str": "string", "None": "void"},
17
- list_type="List<long>",
17
+ list_type="List<{T}>",
18
18
  list_param_type="List<long>",
19
19
  list_elem_type="long",
20
+ str_split="{x}.Split({sep}).ToList()",
21
+ str_join="string.Join({sep}, {x})",
22
+ list_slice="{x}.GetRange((int)({start}), (int)(({stop}) - ({start})))",
23
+ list_copy="new List<long>({x})",
20
24
  borrow_list_arg=False,
21
25
  range_call="({lo}..{hi})",
22
26
  range_step_call="({lo}..{hi}).Step({step})",
@@ -27,6 +31,7 @@ SPEC = Spec(
27
31
  print_str='Console.WriteLine({v})',
28
32
  print_bool='Console.WriteLine({v})',
29
33
  print_generic='Console.WriteLine({v})',
34
+ print_list='Console.WriteLine("[" + string.Join(", ", {v}) + "]")',
30
35
  int_cast="(long)({x})",
31
36
  float_cast="(double)({x})",
32
37
  float_div="((double)({l}) / (double)({r}))",
@@ -34,9 +39,13 @@ SPEC = Spec(
34
39
  sum_call="{it}.Sum()",
35
40
  abs_int="Math.Abs({x})",
36
41
  abs_float="Math.Abs({x})",
42
+ min2_call="Math.Min({a}, {b})",
43
+ max2_call="Math.Max({a}, {b})",
37
44
  min_call="{it}.Min()",
38
45
  max_call="{it}.Max()",
39
46
  pow_call="Math.Pow({l}, {r})",
47
+ pow_int="(long)Math.Pow({l}, {r})",
48
+ pow_float="Math.Pow({l}, {r})",
40
49
  append_call="{x}.Add({v})",
41
50
  index_call="{x}[(int)({i})]",
42
51
  comment="//",
@@ -57,8 +66,10 @@ SPEC = Spec(
57
66
  struct_field_template=" public {type} {name};",
58
67
  struct_new_template="static {name} {name}_New({params}) {{\n{body}\n}}",
59
68
  dict_type="Dictionary<string, {V}>",
69
+ set_type="HashSet<long>",
60
70
  dict_get="{d}[{k}]",
61
71
  dict_set="{d}[{k}] = {v}",
72
+ dict_keys="{x}.Keys",
62
73
  dict_contains="{d}.ContainsKey({k})",
63
74
  tuple_type="({T}, {T})",
64
75
  tuple_get="{t}.Item{i1}",
@@ -108,8 +119,12 @@ class CSharpEmitter(Emitter):
108
119
  self.lines.append(f"{ind}for (long {var} = {lo}; {var} < {hi}; {var}++) {{")
109
120
  else:
110
121
  iter_s = self.expr(it)
111
- self.var_types[var] = "long"
112
- self.lines.append(f"{ind}foreach (var {var} in {iter_s}) {{")
122
+ if self.infer_type(it) == "dict":
123
+ self.var_types[var] = "str"
124
+ self.lines.append(f"{ind}foreach (var {var} in {iter_s}.Keys) {{")
125
+ else:
126
+ self.var_types[var] = "long"
127
+ self.lines.append(f"{ind}foreach (var {var} in {iter_s}) {{")
113
128
  self.indent_lvl += 1
114
129
  for s in node.body:
115
130
  self.stmt(s)
@@ -133,9 +148,15 @@ class CSharpEmitter(Emitter):
133
148
  def stmt(self, node): # type: ignore[override]
134
149
  # escape C# keywords in variable declarations
135
150
  if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
151
+ # bare `list` gets its element type from the value
152
+ _ann = self._ann_type(node.annotation)
153
+ _elem = (self._list_elem_of_value(node.value)
154
+ if (_ann == "list" and node.value is not None) else "")
136
155
  name = self._esc(node.target.id)
137
156
  ann_type = self._ann_type(node.annotation)
138
157
  nt = self.py_to_native(ann_type)
158
+ if _elem and _elem != self.spec.list_elem_type:
159
+ nt = self.spec.list_type.format(T=_elem)
139
160
  if node.value is not None:
140
161
  val = self.expr(node.value)
141
162
  self.lines.append(f"{self.spec.indent * self.indent_lvl}{nt} {name} = {val};")
@@ -227,6 +248,11 @@ def emit_csharp(units: list[FuncUnit], entry: str | None,
227
248
  extern_fns: functions from other backends that this C# code calls.
228
249
  """
229
250
  emitter = CSharpEmitter(SPEC)
251
+ emitter.func_signatures = {
252
+ u.name: ([p for p, _t in u.params],
253
+ dict(getattr(u, 'param_defaults', {})))
254
+ for u in units
255
+ }
230
256
  emitter.library_mode = library_mode
231
257
  emitter.constants = constants or {}
232
258
  emitted: dict[str, str] = {}
@@ -317,6 +343,9 @@ def emit_csharp(units: list[FuncUnit], entry: str | None,
317
343
  for line in fn.split("\n"):
318
344
  indented_fns.append(" " + line)
319
345
  indented_fns.append(wrapper.rstrip())
346
+ elif not fns:
347
+ # entry rejected during emission; see the pipeline diagnostic
348
+ return prelude, emitted
320
349
  else:
321
350
  # rename main to Main for C# entry point
322
351
  # C# Main must return void or int (32-bit), not long
@@ -13,9 +13,13 @@ from ..analyzer import FuncUnit
13
13
  SPEC = Spec(
14
14
  name="go",
15
15
  types={"int": "int64", "float": "float64", "bool": "bool", "str": "string", "None": ""},
16
- list_type="[]int64",
16
+ list_type="[]{T}",
17
17
  list_param_type="[]int64",
18
18
  list_elem_type="int64",
19
+ str_split="strings.Split({x}, {sep})",
20
+ str_join="strings.Join({x}, {sep})",
21
+ list_slice="append([]int64(nil), {x}[{start}:{stop}]...)",
22
+ list_copy="append([]int64(nil), {x}...)",
19
23
  borrow_list_arg=False,
20
24
  range_call="({lo}..{hi})",
21
25
  range_step_call="({lo}..{hi})",
@@ -24,18 +28,23 @@ SPEC = Spec(
24
28
  print_int='fmt.Println({v})',
25
29
  print_float='fmt.Println({v})',
26
30
  print_str='fmt.Println({v})',
27
- print_bool='fmt.Println({v})',
31
+ print_bool='fmt.Println(geBoolStr({v}))',
28
32
  print_generic='fmt.Println({v})',
33
+ print_list='fmt.Println(geListStr({v}))',
29
34
  int_cast="int64({x})",
30
35
  float_cast="float64({x})",
31
36
  float_div="(float64({l}) / float64({r}))",
32
37
  floor_div="({l} / {r})",
33
38
  sum_call="func() int64 {{ var s int64; for _, v := range {it} {{ s += v }}; return s }}()",
34
- abs_int="abs({x})",
39
+ abs_int="geAbsInt({x})",
35
40
  abs_float="math.Abs({x})",
41
+ min2_call="geMin2({a}, {b})",
42
+ max2_call="geMax2({a}, {b})",
36
43
  min_call="func() int64 {{ var m int64; for i, v := range {it} {{ if i == 0 || v < m {{ m = v }} }}; return m }}()",
37
44
  max_call="func() int64 {{ var m int64; for i, v := range {it} {{ if i == 0 || v > m {{ m = v }} }}; return m }}()",
38
45
  pow_call="int64(math.Pow(float64({l}), float64({r})))",
46
+ pow_int="int64(math.Pow(float64({l}), float64({r})))",
47
+ pow_float="math.Pow({l}, {r})",
39
48
  append_call="{x} = append({x}, {v})",
40
49
  index_call="{x}[{i}]",
41
50
  comment="//",
@@ -58,8 +67,11 @@ SPEC = Spec(
58
67
  struct_field_template="\t{type} {name}",
59
68
  struct_new_template="func {name}_New({params}) {name} {{\n{body}\n}}",
60
69
  dict_type="map[string]{V}",
70
+ set_type="map[int64]bool",
61
71
  dict_get="{d}[{k}]",
62
72
  dict_set="{d}[{k}] = {v}",
73
+ dict_keys="{x}",
74
+ foreach_dict_template="for {var} := range {iter}",
63
75
  dict_contains="dictContains({d}, {k})",
64
76
  tuple_type="struct {{ a {T}; b {T} }}",
65
77
  tuple_get="{t}.{field}",
@@ -157,8 +169,13 @@ class GoEmitter(Emitter):
157
169
  self.lines.append(f"{ind} {var_a} := {it_a}[__i]; {var_b} := {it_b}[__i]")
158
170
  else:
159
171
  iter_s = self.expr(it)
160
- self.var_types[var] = "long"
161
- self.lines.append(f"{ind}for _, {var} := range {iter_s} {{")
172
+ if self.infer_type(it) == "dict":
173
+ # range over a map yields keys
174
+ self.var_types[var] = "str"
175
+ self.lines.append(f"{ind}for {var} := range {iter_s} {{")
176
+ else:
177
+ self.var_types[var] = "long"
178
+ self.lines.append(f"{ind}for _, {var} := range {iter_s} {{")
162
179
  self.indent_lvl += 1
163
180
  for s in node.body:
164
181
  self.stmt(s)
@@ -186,6 +203,10 @@ class GoEmitter(Emitter):
186
203
  name = node.target.id
187
204
  ann_type = self._ann_type(node.annotation)
188
205
  nt = self.py_to_native(ann_type)
206
+ if ann_type == "list" and node.value is not None:
207
+ elem = self._list_elem_of_value(node.value)
208
+ if elem and elem != self.spec.list_elem_type:
209
+ nt = self.spec.list_type.format(T=elem)
189
210
  if node.value is not None:
190
211
  val = self.expr(node.value)
191
212
  self.lines.append(f"{self.spec.indent * self.indent_lvl}var {name} {nt} = {val}")
@@ -211,7 +232,8 @@ class GoEmitter(Emitter):
211
232
  def _ann_type(self, node: ast.AST) -> str:
212
233
  if isinstance(node, ast.Name):
213
234
  t = node.id
214
- if t in ("int", "float", "bool", "str", "list", "dict", "tuple"):
235
+ if t in ("int", "float", "bool", "str", "list", "dict",
236
+ "tuple", "set"):
215
237
  return t
216
238
  if t in self.class_names:
217
239
  return t
@@ -276,6 +298,11 @@ def emit_go(units: list[FuncUnit], entry: str | None,
276
298
  extern_fns: functions from other backends that this Go code calls.
277
299
  """
278
300
  emitter = GoEmitter(SPEC)
301
+ emitter.func_signatures = {
302
+ u.name: ([p for p, _t in u.params],
303
+ dict(getattr(u, 'param_defaults', {})))
304
+ for u in units
305
+ }
279
306
  emitter.library_mode = library_mode
280
307
  emitter.constants = constants or {}
281
308
  emitted: dict[str, str] = {}
@@ -294,6 +321,7 @@ def emit_go(units: list[FuncUnit], entry: str | None,
294
321
  "package main\n\n"
295
322
  'import "fmt"\n'
296
323
  'import "strings"\n'
324
+ 'import "strconv"\n'
297
325
  'import "sort"\n'
298
326
  'import "os"\n'
299
327
  'import "math"\n'
@@ -369,6 +397,10 @@ def emit_go(units: list[FuncUnit], entry: str | None,
369
397
  else:
370
398
  wrapper = f"func main() {{\n\t_ = {entry_u.name}()\n}}\n"
371
399
  fns.append(wrapper)
400
+ elif not fns:
401
+ # the entry function was rejected during emission; the pipeline
402
+ # already holds the reason, so return what we have
403
+ return prelude + "\n".join(fns), emitted
372
404
  else:
373
405
  # Go main() must have no arguments and no return values
374
406
  # Rename the user's main() to __ge_main() and add a wrapper
@@ -14,9 +14,13 @@ from ..analyzer import FuncUnit
14
14
  SPEC = Spec(
15
15
  name="kotlin",
16
16
  types={"int": "Long", "float": "Double", "bool": "Boolean", "str": "String", "None": "Unit"},
17
- list_type="MutableList<Long>",
17
+ list_type="MutableList<{T}>",
18
18
  list_param_type="MutableList<Long>",
19
19
  list_elem_type="Long",
20
+ str_split="{x}.split({sep}).toMutableList()",
21
+ str_join="{x}.joinToString({sep})",
22
+ list_slice="{x}.subList(({start}).toInt(), ({stop}).toInt()).toMutableList()",
23
+ list_copy="{x}.toMutableList()",
20
24
  borrow_list_arg=False,
21
25
  range_call="({lo}..{hi})",
22
26
  range_step_call="({lo}..{hi} step {step})",
@@ -25,8 +29,9 @@ SPEC = Spec(
25
29
  print_int='println({v})',
26
30
  print_float='println({v})',
27
31
  print_str='println({v})',
28
- print_bool='println({v})',
32
+ print_bool='println(if ({v}) "True" else "False")',
29
33
  print_generic='println({v})',
34
+ print_list='println({v}.joinToString(", ", "[", "]"))',
30
35
  int_cast="{x}.toLong()",
31
36
  float_cast="{x}.toDouble()",
32
37
  float_div="({l}.toDouble() / {r}.toDouble())",
@@ -34,9 +39,13 @@ SPEC = Spec(
34
39
  sum_call="{it}.sum()",
35
40
  abs_int="abs({x})",
36
41
  abs_float="abs({x})",
42
+ min2_call="minOf({a}, {b})",
43
+ max2_call="maxOf({a}, {b})",
37
44
  min_call="{it}.minOrNull()!!",
38
45
  max_call="{it}.maxOrNull()!!",
39
- pow_call="Math.pow({l}.toDouble(), {r}.toDouble()).toLong()",
46
+ pow_call="{l}.toDouble().pow({r}.toDouble()).toLong()",
47
+ pow_int="{l}.toDouble().pow({r}.toDouble()).toLong()",
48
+ pow_float="{l}.pow({r})",
40
49
  append_call="{x}.add({v})",
41
50
  index_call="{x}[{i}.toInt()]",
42
51
  comment="//",
@@ -58,8 +67,10 @@ SPEC = Spec(
58
67
  struct_field_template=" val {type}: {name},",
59
68
  struct_new_template="fun {name}_new({params}): {name} {{\n{body}\n}}",
60
69
  dict_type="HashMap<String, {V}>",
61
- dict_get="{d}[{k}]",
70
+ set_type="MutableSet<Long>",
71
+ dict_get="{d}[{k}]!!",
62
72
  dict_set="{d}[{k}] = {v}",
73
+ dict_keys="{x}.keys",
63
74
  dict_contains="{d}.containsKey({k})",
64
75
  tuple_type="Pair<{T}, {T}>",
65
76
  tuple_get="{t}.{field}",
@@ -146,8 +157,12 @@ class KotlinEmitter(Emitter):
146
157
  self.lines.append(f"{ind}for ({var} in {lo} until {hi}) {{")
147
158
  else:
148
159
  iter_s = self.expr(it)
149
- self.var_types[var] = "long"
150
- self.lines.append(f"{ind}for ({var} in {iter_s}) {{")
160
+ if self.infer_type(it) == "dict":
161
+ self.var_types[var] = "str"
162
+ self.lines.append(f"{ind}for ({var} in {iter_s}.keys) {{")
163
+ else:
164
+ self.var_types[var] = "long"
165
+ self.lines.append(f"{ind}for ({var} in {iter_s}) {{")
151
166
  self.indent_lvl += 1
152
167
  for s in node.body:
153
168
  self.stmt(s)
@@ -217,6 +232,11 @@ def emit_kotlin(units: list[FuncUnit], entry: str | None,
217
232
  extern_fns: functions from other backends that this Kotlin code calls.
218
233
  """
219
234
  emitter = KotlinEmitter(SPEC)
235
+ emitter.func_signatures = {
236
+ u.name: ([p for p, _t in u.params],
237
+ dict(getattr(u, 'param_defaults', {})))
238
+ for u in units
239
+ }
220
240
  emitter.library_mode = library_mode
221
241
  emitter.constants = constants or {}
222
242
  emitted: dict[str, str] = {}
@@ -295,6 +315,9 @@ def emit_kotlin(units: list[FuncUnit], entry: str | None,
295
315
  else:
296
316
  wrapper = f"fun main() {{\n {entry_u.name}()\n}}\n"
297
317
  fns.append(wrapper)
318
+ elif not fns:
319
+ # entry rejected during emission; see the pipeline diagnostic
320
+ return prelude + "\n".join(fns), emitted
298
321
  else:
299
322
  # Kotlin main() must return Unit (void)
300
323
  # Rename the user's main() to __ge_main() and add a wrapper
@@ -12,6 +12,10 @@ SPEC = Spec(
12
12
  list_type="Vec<{T}>",
13
13
  list_param_type="&[i64]",
14
14
  list_elem_type="i64",
15
+ str_split="{x}.split({sep}.as_str()).map(|s| s.to_string()).collect::<Vec<String>>()",
16
+ str_join="{x}.join({sep}.as_str())",
17
+ list_slice="{x}[({start}) as usize..({stop}) as usize].to_vec()",
18
+ list_copy="{x}.clone()",
15
19
  borrow_list_arg=True,
16
20
  range_call="({lo}..{hi})",
17
21
  range_step_call="({lo}..{hi}).step_by({step} as usize)",
@@ -20,25 +24,32 @@ SPEC = Spec(
20
24
  print_int='println!("{{}}", {v})',
21
25
  print_float='println!("{{}}", {v})',
22
26
  print_str='println!("{{}}", {v})',
23
- print_bool='println!("{{}}", {v})',
27
+ print_bool='println!("{{}}", if {v} {{ "True" }} else {{ "False" }})',
24
28
  print_generic='println!("{{:?}}", {v})',
29
+ print_list='println!("{{:?}}", {v})',
25
30
  int_cast="{x} as i64",
26
31
  float_cast="{x} as f64",
27
32
  float_div="({l} as f64 / {r} as f64)",
28
33
  floor_div="({l} / {r})",
29
34
  sum_call="{it}.iter().sum::<i64>()",
30
- abs_int="{x}.abs()",
35
+ abs_int="i64::abs({x})",
31
36
  abs_float="{x}.abs()",
37
+ min2_call="std::cmp::min({a}, {b})",
38
+ max2_call="std::cmp::max({a}, {b})",
32
39
  min_call="{it}.iter().min().unwrap()",
33
40
  max_call="{it}.iter().max().unwrap()",
34
41
  pow_call="{l}.pow({r} as u32)",
42
+ pow_int="i64::pow({l}, {r} as u32)",
43
+ pow_float="f64::powf({l}, {r})",
35
44
  append_call="{x}.push({v})",
36
45
  index_call="{x}[{i} as usize]",
37
46
  comment="//",
38
47
  fn_template="{sig} {{\n{body}\n}}",
39
48
  main_template="",
40
49
  ffi_prefix="#[no_mangle]\npub extern \"C\" ",
41
- str_concat="{l} + &{r}",
50
+ # `String + &String` is not Add in Rust and `a + b` moves `a`, so a
51
+ # format! is used instead — always correct, and it accepts &str too.
52
+ str_concat="format!(\"{{}}{{}}\", {l}, {r})",
42
53
  str_len="{x}.len() as i64",
43
54
  str_index="{x}.as_bytes()[{i} as usize] as i64",
44
55
  str_slice="{x}[{start}..{end}].to_string()",
@@ -52,8 +63,11 @@ SPEC = Spec(
52
63
  struct_field_template=" {type}: {name},",
53
64
  struct_new_template="fn {name}_new({params}) -> {name} {{\n{body}\n}}",
54
65
  dict_type="std::collections::HashMap<String, {V}>",
66
+ set_type="std::collections::HashSet<i64>",
55
67
  dict_get="{d}.get(&{k}).copied().unwrap_or(0)",
56
68
  dict_set="{d}.insert({k}, {v})",
69
+ dict_keys="{x}.keys().cloned().collect::<Vec<String>>()",
70
+ foreach_dict_template="for {var} in {iter}",
57
71
  dict_contains="{d}.contains_key(&{k})",
58
72
  tuple_type="({T}, {T})",
59
73
  tuple_get="{t}.{i}",
@@ -188,6 +202,11 @@ def emit_rust(units: list[FuncUnit], entry: str | None,
188
202
  constants: module-level constants to inline (name -> value).
189
203
  """
190
204
  emitter = RustEmitter(SPEC)
205
+ emitter.func_signatures = {
206
+ u.name: ([p for p, _t in u.params],
207
+ dict(getattr(u, 'param_defaults', {})))
208
+ for u in units
209
+ }
191
210
  emitter.library_mode = library_mode
192
211
  emitter.export_names = set(export_fns) if export_fns else set()
193
212
  emitter.constants = constants or {}
@@ -13,9 +13,11 @@ from ..analyzer import FuncUnit
13
13
  SPEC = Spec(
14
14
  name="zig",
15
15
  types={"int": "i64", "float": "f64", "bool": "bool", "str": "[]const u8", "None": "void"},
16
- list_type="[]const i64",
16
+ list_type="[]const {T}",
17
17
  list_param_type="[]const i64",
18
18
  list_elem_type="i64",
19
+ list_slice="{x}[@intCast({start})..@intCast({stop})]",
20
+ list_copy="{x}",
19
21
  borrow_list_arg=False,
20
22
  range_call="({lo}..{hi})",
21
23
  range_step_call="({lo}..{hi})",
@@ -24,19 +26,26 @@ SPEC = Spec(
24
26
  print_int='std.io.getStdOut().writer().print("{{d}}\\n", .{{{v}}}) catch unreachable',
25
27
  print_float='std.io.getStdOut().writer().print("{{d}}\\n", .{{{v}}}) catch unreachable',
26
28
  print_str='std.io.getStdOut().writer().print("{{s}}\\n", .{{{v}}}) catch unreachable',
27
- print_bool='std.io.getStdOut().writer().print("{}\\n", .{{{v}}}) catch unreachable',
29
+ print_bool='std.io.getStdOut().writer().print("{{s}}\\n", .{{if ({v}) "True" else "False"}}) catch unreachable',
28
30
  print_generic='std.io.getStdOut().writer().print("{{any}}\\n", .{{{v}}}) catch unreachable',
31
+ print_list='gePrintList({v})',
29
32
  int_cast="@as(i64, {x})",
30
33
  float_cast="@as(f64, {x})",
31
34
  float_div="(@as(f64, {l}) / @as(f64, {r}))",
32
35
  floor_div="@divFloor({l}, {r})",
33
36
  sum_call="blk: {{ var s: i64 = 0; for ({it}) |v| {{ s += v; }} break :blk s; }}",
34
- abs_int="@intCast(if ({x} < 0) -{x} else {x})",
37
+ abs_int="@as(i64, @intCast(if ({x} < 0) -{x} else {x}))",
35
38
  abs_float="@abs({x})",
39
+ min2_call="@min({a}, {b})",
40
+ max2_call="@max({a}, {b})",
36
41
  min_call="@min({it}...)",
37
42
  max_call="@max({it}...)",
38
43
  pow_call="std.math.pow(i64, {l}, {r})",
39
- append_call="{x}.append({v}) catch unreachable",
44
+ pow_int="std.math.pow(i64, {l}, {r})",
45
+ pow_float="std.math.pow(f64, {l}, {r})",
46
+ # Zig lists are `[]const i64` slices and cannot grow; list.append
47
+ # is rejected with a clear diagnostic rather than emitting invalid Zig.
48
+ append_call="",
40
49
  index_call="{x}[@as(usize, @intCast({i}))]",
41
50
  comment="//",
42
51
  fn_template="{sig} {{\n{body}\n}}",
@@ -194,6 +203,7 @@ class ZigEmitter(Emitter):
194
203
  self.lines.append(f"{ind}_ = {self.expr(node.value)};")
195
204
  return
196
205
  if isinstance(node, ast.AnnAssign) and node.value is not None:
206
+ # bare `list` gets its element type from the value
197
207
  target = node.target.id if isinstance(node.target, ast.Name) else self.expr(node.target)
198
208
  t = self._ann_type(node.annotation) if isinstance(node.target, ast.Name) else self.infer_type(node.value)
199
209
  if isinstance(node.target, ast.Name):
@@ -244,8 +254,16 @@ class ZigEmitter(Emitter):
244
254
  self.lines.append(f"{ind}while ({var} < {hi}) : ({var} += 1) {{")
245
255
  else:
246
256
  iter_s = self.expr(it)
247
- self.var_types[var] = "long"
248
- self.lines.append(f"{ind}for ({iter_s}) |{var}| {{")
257
+ if self.infer_type(it) == "dict":
258
+ # Zig has no allocator here, so walk the map's key iterator
259
+ # instead of materialising a key list.
260
+ self.var_types[var] = "str"
261
+ self.lines.append(f"{ind}var __keys_{var} = {iter_s}.keyIterator();")
262
+ self.lines.append(f"{ind}while (__keys_{var}.next()) |__kp_{var}| {{")
263
+ self.lines.append(f"{ind} const {var} = __kp_{var}.*;")
264
+ else:
265
+ self.var_types[var] = "long"
266
+ self.lines.append(f"{ind}for ({iter_s}) |{var}| {{")
249
267
  self.indent_lvl += 1
250
268
  for s in node.body:
251
269
  self.stmt(s)
@@ -315,6 +333,11 @@ def emit_zig(units: list[FuncUnit], entry: str | None,
315
333
  extern_fns: functions from other backends that this Zig code calls.
316
334
  """
317
335
  emitter = ZigEmitter(SPEC)
336
+ emitter.func_signatures = {
337
+ u.name: ([p for p, _t in u.params],
338
+ dict(getattr(u, 'param_defaults', {})))
339
+ for u in units
340
+ }
318
341
  emitter.library_mode = library_mode
319
342
  emitter.constants = constants or {}
320
343
  emitted: dict[str, str] = {}
@@ -392,6 +415,10 @@ def emit_zig(units: list[FuncUnit], entry: str | None,
392
415
  else:
393
416
  wrapper = f"pub fn main() void {{\n _ = {entry_u.name}();\n}}\n"
394
417
  fns.append(wrapper)
418
+ elif not fns:
419
+ # the entry function was rejected during emission; the pipeline
420
+ # already has the reason, so emit a stub rather than crashing
421
+ return prelude, emitted
395
422
  else:
396
423
  # Zig main() must return void (or u8 for exit code)
397
424
  # Rename the user's main() to __ge_main() and add a wrapper
@@ -156,6 +156,64 @@ fn ge_exp(x: f64) -> f64 { x.exp() }
156
156
  ''',
157
157
  "cpp": '''
158
158
  // GE Math runtime (C++)
159
+ std::string geStrUpper(std::string s) {
160
+ for (auto& c : s) c = (char)std::toupper((unsigned char)c);
161
+ return s;
162
+ }
163
+
164
+ std::string geStrLower(std::string s) {
165
+ for (auto& c : s) c = (char)std::tolower((unsigned char)c);
166
+ return s;
167
+ }
168
+
169
+ std::string geStrReplace(std::string s, const std::string& from,
170
+ const std::string& to) {
171
+ if (from.empty()) return s;
172
+ size_t pos = 0;
173
+ while ((pos = s.find(from, pos)) != std::string::npos) {
174
+ s.replace(pos, from.size(), to);
175
+ pos += to.size();
176
+ }
177
+ return s;
178
+ }
179
+
180
+ std::vector<std::string> geStrSplit(const std::string& s, const std::string& sep) {
181
+ std::vector<std::string> out;
182
+ if (sep.empty()) { out.push_back(s); return out; }
183
+ size_t pos = 0, found;
184
+ while ((found = s.find(sep, pos)) != std::string::npos) {
185
+ out.push_back(s.substr(pos, found - pos));
186
+ pos = found + sep.size();
187
+ }
188
+ out.push_back(s.substr(pos));
189
+ return out;
190
+ }
191
+
192
+ std::string geStrJoin(const std::vector<std::string>& v, const std::string& sep) {
193
+ std::string out;
194
+ for (size_t i = 0; i < v.size(); ++i) {
195
+ if (i) out += sep;
196
+ out += v[i];
197
+ }
198
+ return out;
199
+ }
200
+
201
+ std::vector<std::string> geDictKeys(const std::map<std::string, int64_t>& d) {
202
+ std::vector<std::string> ks;
203
+ ks.reserve(d.size());
204
+ for (const auto& kv : d) ks.push_back(kv.first);
205
+ return ks;
206
+ }
207
+
208
+ std::string geListStr(const std::vector<int64_t>& v) {
209
+ std::string s = "[";
210
+ for (size_t i = 0; i < v.size(); ++i) {
211
+ if (i) s += ", ";
212
+ s += std::to_string(v[i]);
213
+ }
214
+ return s + "]";
215
+ }
216
+
159
217
  double ge_sqrt(double x) { return std::sqrt(x); }
160
218
  double ge_floor(double x) { return std::floor(x); }
161
219
  double ge_ceil(double x) { return std::ceil(x); }
@@ -180,6 +238,42 @@ static double GeExp(double x) { return System.Math.Exp(x); }
180
238
  ''',
181
239
  "go": '''
182
240
  // GE Math runtime (Go)
241
+ func geAbsInt(x int64) int64 {
242
+ if x < 0 {
243
+ return -x
244
+ }
245
+ return x
246
+ }
247
+
248
+ func geMin2(a, b int64) int64 {
249
+ if a < b {
250
+ return a
251
+ }
252
+ return b
253
+ }
254
+
255
+ func geMax2(a, b int64) int64 {
256
+ if a > b {
257
+ return a
258
+ }
259
+ return b
260
+ }
261
+
262
+ func geListStr(v []int64) string {
263
+ parts := make([]string, len(v))
264
+ for i, x := range v {
265
+ parts[i] = strconv.FormatInt(x, 10)
266
+ }
267
+ return "[" + strings.Join(parts, ", ") + "]"
268
+ }
269
+
270
+ func geBoolStr(b bool) string {
271
+ if b {
272
+ return "True"
273
+ }
274
+ return "False"
275
+ }
276
+
183
277
  func geSqrt(x float64) float64 { return math.Sqrt(x) }
184
278
  func geFloor(x float64) float64 { return math.Floor(x) }
185
279
  func geCeil(x float64) float64 { return math.Ceil(x) }
@@ -205,6 +299,16 @@ fun ge_exp(x: Double): Double = exp(x)
205
299
  ''',
206
300
  "zig": '''
207
301
  // GE Math runtime (Zig)
302
+ fn gePrintList(v: []const i64) void {
303
+ const w = std.io.getStdOut().writer();
304
+ w.print("[", .{}) catch unreachable;
305
+ for (v, 0..) |x, i| {
306
+ if (i > 0) w.print(", ", .{}) catch unreachable;
307
+ w.print("{d}", .{x}) catch unreachable;
308
+ }
309
+ w.print("]\\n", .{}) catch unreachable;
310
+ }
311
+
208
312
  fn ge_sqrt(x: f64) f64 { return @sqrt(x); }
209
313
  fn ge_floor(x: f64) f64 { return @floor(x); }
210
314
  fn ge_ceil(x: f64) f64 { return @ceil(x); }
@@ -34,6 +34,8 @@ class TypeChecker:
34
34
  self.file = file
35
35
  self.errors = ErrorReporter()
36
36
  self.func_sigs: dict[str, tuple[list[str], str]] = {} # name -> (param_types, ret_type)
37
+
38
+ self.func_required: dict[str, int] = {}
37
39
  self.func_names: set[str] = set()
38
40
  self.class_names: set[str] = set()
39
41
 
@@ -49,6 +51,8 @@ class TypeChecker:
49
51
  if u.supported and u.body:
50
52
  param_types = [t for _, t in u.params]
51
53
  self.func_sigs[u.name] = (param_types, u.ret_type)
54
+ self.func_required[u.name] = getattr(
55
+ u, "n_required_params", len(param_types)) or len(param_types)
52
56
  self.func_names.add(u.name)
53
57
  # collect class names
54
58
  if classes:
@@ -97,16 +101,25 @@ class TypeChecker:
97
101
  # check if calling a known function with wrong number of args
98
102
  if fname in self.func_sigs:
99
103
  param_types, _ = self.func_sigs[fname]
100
- if len(node.args) != len(param_types):
104
+ n_req = self.func_required.get(fname, len(param_types))
105
+ n_given = len(node.args)
106
+ if n_given < n_req or n_given > len(param_types):
107
+ if n_req == len(param_types):
108
+ expect = f"{len(param_types)} args"
109
+ else:
110
+ expect = (f"between {n_req} and "
111
+ f"{len(param_types)} args")
101
112
  self.errors.error(
102
113
  "GE002",
103
- f"function '{fname}' expects {len(param_types)} args, "
104
- f"got {len(node.args)}",
114
+ f"function '{fname}' expects {expect}, "
115
+ f"got {n_given}",
105
116
  file=self.file, line=node.lineno,
106
117
  )
107
118
  # check for undefined function calls
108
119
  elif fname not in ("print", "len", "range", "abs", "sum", "min", "max",
109
120
  "float", "int", "str", "bool", "dict", "list", "tuple",
121
+ "round", "sorted", "reversed", "enumerate", "zip",
122
+ "any", "all", "pow", "divmod", "ord", "chr",
110
123
  "ge_inline", "ge_raw", "ge_preamble"):
111
124
  if fname not in self.class_names:
112
125
  self.errors.error(