gelang 0.1.4 → 0.1.6
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 +107 -48
- package/package.json +1 -1
- package/python/pyeffic/emitters/base.py +100 -14
- package/python/pyeffic/emitters/cpp.py +5 -0
- package/python/pyeffic/emitters/csharp.py +9 -0
- package/python/pyeffic/emitters/go.py +8 -1
- package/python/pyeffic/emitters/kotlin.py +5 -0
- package/python/pyeffic/emitters/rust.py +8 -1
- package/python/pyeffic/emitters/zig.py +98 -76
- package/python/pyeffic/ge_cli.py +111 -5
- package/python/pyeffic/stdlib.py +111 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,113 @@ 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
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Hybrid `.ge` frontend** — one file may mix Python-flavoured and
|
|
12
|
+
TypeScript-flavoured definitions. Flavour is detected per chunk.
|
|
13
|
+
- **Named blocks** — `<name> ... </name>` gives a chunk a name; `@name(...)`
|
|
14
|
+
calls it from anywhere in the file, in either flavour, forward or backward.
|
|
15
|
+
- **Mixed-backend native linking** — `ge build` emits each non-entry backend
|
|
16
|
+
in library mode with `extern "C"` exports, compiles it to an object, and
|
|
17
|
+
links one executable. No build script, no hand-written FFI.
|
|
18
|
+
- **`ge diff`** — differential testing. Runs the same program through CPython
|
|
19
|
+
and every installed backend, comparing output byte-for-byte.
|
|
20
|
+
- **`ge golden`** — golden lowering corpus. Emitted code must match the
|
|
21
|
+
committed golden, and compiling it must still print the expected output.
|
|
22
|
+
- **`ge api-check`** — committed dumps of the CLI, diagnostics, and intrinsic
|
|
23
|
+
surfaces. Drift fails the check.
|
|
24
|
+
- **`ge doctor`** — runtime and toolchain check with per-platform install
|
|
25
|
+
hints. `--json` for CI.
|
|
26
|
+
- **`ge react`** — generates a React 19 + TypeScript + Vite project from a
|
|
27
|
+
`.ge.ui` file, one component per file.
|
|
28
|
+
- **Organized build output** — `build/{desktop,web,mobile}/{backend,obj,bin}`.
|
|
29
|
+
- **npm distribution** — `npx gelang` runs the compiler with bundled source,
|
|
30
|
+
no `pip install` required.
|
|
31
|
+
- **`--target` buckets** — `desktop`, `web`, `mobile`.
|
|
32
|
+
- **Target-keyword safety** — identifiers that collide with a target
|
|
33
|
+
language's reserved words are escaped (`@base` in C#, `r#match` in Rust)
|
|
34
|
+
or renamed consistently across the ABI (`double` → `double_`).
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- C# reserved words in parameter positions emitted invalid code (`base`).
|
|
39
|
+
- Function names colliding with target keywords broke compilation
|
|
40
|
+
(`double` in C++, `match` in Rust).
|
|
41
|
+
- `from pyeffic.backends import rust` was resolved as user code, pulling the
|
|
42
|
+
compiler's own sources into the program.
|
|
43
|
+
- `ge_build`/`build_flutter` used an undefined `preamble` variable.
|
|
44
|
+
- Go backend wrote binaries next to the source instead of `build/<target>/bin`.
|
|
45
|
+
- Module resolver matched a sibling file for dotted imports
|
|
46
|
+
(`app.main` resolved to `desktop/main.ge`).
|
|
47
|
+
- `.ge.ui` parser rejected files with a leading docstring.
|
|
48
|
+
- `.ge.ui` parser only accepted `Screen "title" {}`, not `Window { title: }`.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- Canonical extension is `.ge`. `.ge.py` and `.ge.ts` remain supported for
|
|
53
|
+
single-flavour files; `.ts.ge.py` is accepted as a legacy alias.
|
|
54
|
+
- Build root defaults to `build/` (was `ge_build/`).
|
|
55
|
+
- Package name on npm is `gelang`.
|
|
56
|
+
## [0.1.6] — 2026-09-13
|
|
57
|
+
|
|
58
|
+
### Zig is now a full backend
|
|
59
|
+
|
|
60
|
+
Zig modelled a GE list as a `[]const i64` slice, which cannot grow, so
|
|
61
|
+
`append`, `sort`, `split`, list comprehensions and `min`/`max` over a list
|
|
62
|
+
were either rejected or emitted Zig that did not compile. Research into
|
|
63
|
+
Zig's allocator conventions settled the design: a generated program runs
|
|
64
|
+
start to finish, so a single **arena** is the right fit — not
|
|
65
|
+
`page_allocator` (a syscall per allocation) and not
|
|
66
|
+
`GeneralPurposeAllocator` (a debugging allocator).
|
|
67
|
+
|
|
68
|
+
Lists are now `std.ArrayList(T)` over an arena that `main()` releases:
|
|
69
|
+
|
|
70
|
+
- literals become `geListFrom(elem, &[_]elem{...})`; an empty literal is
|
|
71
|
+
`geListNew(elem)`
|
|
72
|
+
- append, sort, reverse, index, len, iteration, slicing and printing all work
|
|
73
|
+
- a list parameter is `*std.ArrayList`, so mutation in the callee is visible
|
|
74
|
+
to the caller, matching Python's reference semantics. Call sites pass
|
|
75
|
+
`&name`; an rvalue argument is bound to a temporary first.
|
|
76
|
+
- passing a list to a mutating callee marks the local `var` (Zig rejects an
|
|
77
|
+
unmutated `var`)
|
|
78
|
+
|
|
79
|
+
Also implemented for Zig: `str.split`, `str.join`, `str.replace`,
|
|
80
|
+
`str.upper`, `str.lower` (all arena-allocated), sets as
|
|
81
|
+
`AutoHashMap(elem, void)` with a deduplicating literal, and dict literals
|
|
82
|
+
over the arena.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- Iterating a Rust list yields `&i64`, so `for x in xs: if x == n` did not
|
|
87
|
+
compile. The loop now uses `.cloned()`, which yields owned values for both
|
|
88
|
+
`i64` and `String`. Python semantics are unchanged.
|
|
89
|
+
- Go rejects `int64(3.9)` for an untyped constant; `int()` now truncates
|
|
90
|
+
through `math.Trunc`, matching Python.
|
|
91
|
+
- `list.sort()` and `list.reverse()` had no implementation and fell through
|
|
92
|
+
to `xs.sort()`, valid only in Rust and Kotlin.
|
|
93
|
+
- `ge analyze` parsed hybrid `.ge` files as plain Python, reporting a syntax
|
|
94
|
+
error on the first block tag, and never lowered `.ge.ts` at all.
|
|
95
|
+
- `ge react` wrote to `web/` instead of the documented `build/web/`.
|
|
96
|
+
- `ge flutter` handed a `.ge.ui` file to the module resolver, which tried to
|
|
97
|
+
parse `Window {` as Python.
|
|
98
|
+
- The return type of `str.upper()` and friends was not inferred, so `print`
|
|
99
|
+
used the generic format — a Zig `[]const u8` rendered as a byte array.
|
|
100
|
+
- `scripts/e2e.py` aborted the whole run with `UnicodeEncodeError` when a
|
|
101
|
+
compiler diagnostic contained a character the console codec cannot encode.
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
|
|
105
|
+
- `scripts/e2e.py` — 118 end-to-end cases that compile and run real programs
|
|
106
|
+
on every installed backend and compare stdout with CPython.
|
|
107
|
+
- `str_upper`, `str_lower`, `str_replace`, `set_len` and `dict_len` on the
|
|
108
|
+
shared `Spec`, so every backend expresses these the same way.
|
|
109
|
+
|
|
110
|
+
### Known limitations
|
|
111
|
+
|
|
112
|
+
- Tuples are supported at arity 2; other arities are rejected with
|
|
113
|
+
`error[GE009]` rather than emitting broken code.
|
|
7
114
|
## [0.1.4] — 2026-09-13
|
|
8
115
|
|
|
9
116
|
Found by `scripts/e2e.py`, a new harness that compiles and runs 118
|
|
@@ -57,55 +164,7 @@ programs on every installed backend and compares stdout with CPython.
|
|
|
57
164
|
- Tuples are supported at arity 2; other arities are rejected with a
|
|
58
165
|
diagnostic instead of emitting broken code.
|
|
59
166
|
|
|
60
|
-
## [Unreleased]
|
|
61
|
-
|
|
62
|
-
### Added
|
|
63
167
|
|
|
64
|
-
- **Hybrid `.ge` frontend** — one file may mix Python-flavoured and
|
|
65
|
-
TypeScript-flavoured definitions. Flavour is detected per chunk.
|
|
66
|
-
- **Named blocks** — `<name> ... </name>` gives a chunk a name; `@name(...)`
|
|
67
|
-
calls it from anywhere in the file, in either flavour, forward or backward.
|
|
68
|
-
- **Mixed-backend native linking** — `ge build` emits each non-entry backend
|
|
69
|
-
in library mode with `extern "C"` exports, compiles it to an object, and
|
|
70
|
-
links one executable. No build script, no hand-written FFI.
|
|
71
|
-
- **`ge diff`** — differential testing. Runs the same program through CPython
|
|
72
|
-
and every installed backend, comparing output byte-for-byte.
|
|
73
|
-
- **`ge golden`** — golden lowering corpus. Emitted code must match the
|
|
74
|
-
committed golden, and compiling it must still print the expected output.
|
|
75
|
-
- **`ge api-check`** — committed dumps of the CLI, diagnostics, and intrinsic
|
|
76
|
-
surfaces. Drift fails the check.
|
|
77
|
-
- **`ge doctor`** — runtime and toolchain check with per-platform install
|
|
78
|
-
hints. `--json` for CI.
|
|
79
|
-
- **`ge react`** — generates a React 19 + TypeScript + Vite project from a
|
|
80
|
-
`.ge.ui` file, one component per file.
|
|
81
|
-
- **Organized build output** — `build/{desktop,web,mobile}/{backend,obj,bin}`.
|
|
82
|
-
- **npm distribution** — `npx gelang` runs the compiler with bundled source,
|
|
83
|
-
no `pip install` required.
|
|
84
|
-
- **`--target` buckets** — `desktop`, `web`, `mobile`.
|
|
85
|
-
- **Target-keyword safety** — identifiers that collide with a target
|
|
86
|
-
language's reserved words are escaped (`@base` in C#, `r#match` in Rust)
|
|
87
|
-
or renamed consistently across the ABI (`double` → `double_`).
|
|
88
|
-
|
|
89
|
-
### Fixed
|
|
90
|
-
|
|
91
|
-
- C# reserved words in parameter positions emitted invalid code (`base`).
|
|
92
|
-
- Function names colliding with target keywords broke compilation
|
|
93
|
-
(`double` in C++, `match` in Rust).
|
|
94
|
-
- `from pyeffic.backends import rust` was resolved as user code, pulling the
|
|
95
|
-
compiler's own sources into the program.
|
|
96
|
-
- `ge_build`/`build_flutter` used an undefined `preamble` variable.
|
|
97
|
-
- Go backend wrote binaries next to the source instead of `build/<target>/bin`.
|
|
98
|
-
- Module resolver matched a sibling file for dotted imports
|
|
99
|
-
(`app.main` resolved to `desktop/main.ge`).
|
|
100
|
-
- `.ge.ui` parser rejected files with a leading docstring.
|
|
101
|
-
- `.ge.ui` parser only accepted `Screen "title" {}`, not `Window { title: }`.
|
|
102
|
-
|
|
103
|
-
### Changed
|
|
104
|
-
|
|
105
|
-
- Canonical extension is `.ge`. `.ge.py` and `.ge.ts` remain supported for
|
|
106
|
-
single-flavour files; `.ts.ge.py` is accepted as a legacy alias.
|
|
107
|
-
- Build root defaults to `build/` (was `ge_build/`).
|
|
108
|
-
- Package name on npm is `gelang`.
|
|
109
168
|
|
|
110
169
|
## [0.1.0] — 2026-09-12
|
|
111
170
|
|
package/package.json
CHANGED
|
@@ -63,6 +63,15 @@ class Spec:
|
|
|
63
63
|
# str.split(sep) / sep.join(list). Empty means unsupported.
|
|
64
64
|
str_split: str = ""
|
|
65
65
|
str_join: str = ""
|
|
66
|
+
# str.upper() / str.lower(). Empty falls back to the per-backend
|
|
67
|
+
# branch in the dispatcher.
|
|
68
|
+
str_upper: str = ""
|
|
69
|
+
str_lower: str = ""
|
|
70
|
+
# str.replace(a, b). Empty falls back to the generic method call.
|
|
71
|
+
str_replace: str = ""
|
|
72
|
+
# in-place list.sort() / list.reverse()
|
|
73
|
+
list_sort: str = ""
|
|
74
|
+
list_reverse: str = ""
|
|
66
75
|
list_slice: str = ""
|
|
67
76
|
list_copy: str = ""
|
|
68
77
|
# Iterating a dict in Python yields its keys. `dict_keys` is an
|
|
@@ -100,6 +109,8 @@ class Spec:
|
|
|
100
109
|
# tuple support
|
|
101
110
|
tuple_type: str = "std::tuple<{T}>"
|
|
102
111
|
set_type: str = "" # native set type
|
|
112
|
+
set_len: str = "" # len(set)
|
|
113
|
+
dict_len: str = "" # len(dict)
|
|
103
114
|
tuple_get: str = "std::get<{i}>({t})"
|
|
104
115
|
# list length (separate from len_call which may be used for strings)
|
|
105
116
|
list_len: str = "{x}.size()"
|
|
@@ -230,6 +241,10 @@ class Emitter:
|
|
|
230
241
|
elem_t = self._native_elem_type(self.param_elem_types[pname])
|
|
231
242
|
if self.spec.name == "rust":
|
|
232
243
|
return f"&[{elem_t}]"
|
|
244
|
+
if self.spec.name == "zig":
|
|
245
|
+
# a list parameter is always a pointer to the caller's
|
|
246
|
+
# ArrayList so mutation is visible to it
|
|
247
|
+
return f"*std.ArrayList({elem_t})"
|
|
233
248
|
return self.spec.list_type.format(T=elem_t)
|
|
234
249
|
return self.spec.list_param_type
|
|
235
250
|
if t == "dict":
|
|
@@ -1216,7 +1231,10 @@ class Emitter:
|
|
|
1216
1231
|
if self.spec.name == "kotlin":
|
|
1217
1232
|
return f"mutableListOf({elems})"
|
|
1218
1233
|
if self.spec.name == "zig":
|
|
1219
|
-
|
|
1234
|
+
# Zig lists are growable ArrayLists; build one from the literal
|
|
1235
|
+
if not node.elts:
|
|
1236
|
+
return f"geListNew({elem})"
|
|
1237
|
+
return f"geListFrom({elem}, &[_]{elem}{{ {elems} }})"
|
|
1220
1238
|
if self.spec.name == "csharp":
|
|
1221
1239
|
lt = self.spec.list_type.format(T=elem)
|
|
1222
1240
|
return f"new {lt}{{{elems}}}"
|
|
@@ -1247,11 +1265,20 @@ class Emitter:
|
|
|
1247
1265
|
for k, v in zip(node.keys, node.values))
|
|
1248
1266
|
return f"hashMapOf({pairs})"
|
|
1249
1267
|
if self.spec.name == "zig":
|
|
1250
|
-
# Zig
|
|
1251
|
-
#
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1268
|
+
# Zig has no map literal; build one with the arena allocator.
|
|
1269
|
+
# An empty literal needs no block at all, and a block whose
|
|
1270
|
+
# local is never mutated is rejected by Zig.
|
|
1271
|
+
if not node.keys:
|
|
1272
|
+
return (f"std.StringHashMap({self.spec.list_elem_type})"
|
|
1273
|
+
f".init(ge_alloc)")
|
|
1274
|
+
puts = "\n".join(
|
|
1275
|
+
f" __m.put({self.expr(k)}, {self.expr(v)}) catch unreachable;"
|
|
1276
|
+
for k, v in zip(node.keys, node.values))
|
|
1277
|
+
return (f"blk: {{\n"
|
|
1278
|
+
f" var __m = std.StringHashMap({self.spec.list_elem_type}).init(ge_alloc);\n"
|
|
1279
|
+
f"{puts}\n"
|
|
1280
|
+
f" break :blk __m;\n"
|
|
1281
|
+
f"}}")
|
|
1255
1282
|
return f"{{{pairs}}}"
|
|
1256
1283
|
if isinstance(node, ast.Set):
|
|
1257
1284
|
elems = ", ".join(self.expr(e) for e in node.elts)
|
|
@@ -1277,8 +1304,16 @@ class Emitter:
|
|
|
1277
1304
|
if self.spec.name == "kotlin":
|
|
1278
1305
|
return f"hashSetOf({elems})"
|
|
1279
1306
|
if self.spec.name == "zig":
|
|
1280
|
-
# no
|
|
1281
|
-
|
|
1307
|
+
# Zig has no set type; GE uses AutoHashMap(elem, void) over
|
|
1308
|
+
# the arena allocator
|
|
1309
|
+
if not node.elts:
|
|
1310
|
+
return "geSetNew(i64)"
|
|
1311
|
+
seen: list[str] = []
|
|
1312
|
+
for e in node.elts:
|
|
1313
|
+
rendered = self.expr(e)
|
|
1314
|
+
if rendered not in seen:
|
|
1315
|
+
seen.append(rendered)
|
|
1316
|
+
return f"geSetFrom(i64, &[_]i64{{ {', '.join(seen)} }})"
|
|
1282
1317
|
return f"std::set<int64_t>{{{elems}}}"
|
|
1283
1318
|
if isinstance(node, ast.SetComp):
|
|
1284
1319
|
return self._set_comp(node)
|
|
@@ -1362,6 +1397,10 @@ class Emitter:
|
|
|
1362
1397
|
return self.spec.str_len.format(x=self.expr(arg))
|
|
1363
1398
|
if t == "list":
|
|
1364
1399
|
return self.spec.list_len.format(x=self.expr(arg))
|
|
1400
|
+
if t == "set" and self.spec.set_len:
|
|
1401
|
+
return self.spec.set_len.format(x=self.expr(arg))
|
|
1402
|
+
if t == "dict" and self.spec.dict_len:
|
|
1403
|
+
return self.spec.dict_len.format(x=self.expr(arg))
|
|
1365
1404
|
return self.spec.len_call.format(x=self.expr(arg))
|
|
1366
1405
|
if fname == "abs":
|
|
1367
1406
|
t = self.infer_type(node.args[0])
|
|
@@ -1738,6 +1777,8 @@ class Emitter:
|
|
|
1738
1777
|
if base_type == "str" or (isinstance(node.func.value, ast.Constant) and isinstance(node.func.value.value, str)):
|
|
1739
1778
|
method = node.func.attr
|
|
1740
1779
|
if method == "upper":
|
|
1780
|
+
if self.spec.str_upper:
|
|
1781
|
+
return self.spec.str_upper.format(x=base)
|
|
1741
1782
|
if self.spec.name == "rust":
|
|
1742
1783
|
return f"{base}.to_uppercase()"
|
|
1743
1784
|
if self.spec.name == "cpp":
|
|
@@ -1750,6 +1791,8 @@ class Emitter:
|
|
|
1750
1791
|
return f"{base}.uppercase()"
|
|
1751
1792
|
return f"{base}.upper()"
|
|
1752
1793
|
if method == "lower":
|
|
1794
|
+
if self.spec.str_lower:
|
|
1795
|
+
return self.spec.str_lower.format(x=base)
|
|
1753
1796
|
if self.spec.name == "rust":
|
|
1754
1797
|
return f"{base}.to_lowercase()"
|
|
1755
1798
|
if self.spec.name == "cpp":
|
|
@@ -1764,10 +1807,10 @@ class Emitter:
|
|
|
1764
1807
|
if method == "replace" and len(node.args) == 2:
|
|
1765
1808
|
a = self.expr(node.args[0])
|
|
1766
1809
|
b = self.expr(node.args[1])
|
|
1810
|
+
if self.spec.str_replace:
|
|
1811
|
+
return self.spec.str_replace.format(x=base, a=a, b=b)
|
|
1767
1812
|
if self.spec.name == "rust":
|
|
1768
1813
|
return f"{base}.replace({a}.as_str(), {b}.as_str())"
|
|
1769
|
-
if self.spec.name == "cpp":
|
|
1770
|
-
return f"geStrReplace({base}, {a}, {b})"
|
|
1771
1814
|
if self.spec.name == "csharp":
|
|
1772
1815
|
return f"{base}.Replace({a}, {b})"
|
|
1773
1816
|
if self.spec.name == "go":
|
|
@@ -1784,6 +1827,18 @@ class Emitter:
|
|
|
1784
1827
|
if self.spec.name == "kotlin":
|
|
1785
1828
|
return f"{base}.trim()"
|
|
1786
1829
|
return f"{base}.strip()"
|
|
1830
|
+
# in-place list operations: xs.sort() / xs.reverse()
|
|
1831
|
+
if (isinstance(node.func, ast.Attribute)
|
|
1832
|
+
and node.func.attr in ("sort", "reverse")
|
|
1833
|
+
and not node.args):
|
|
1834
|
+
list_base = self.expr(node.func.value)
|
|
1835
|
+
tpl = (self.spec.list_sort if node.func.attr == "sort"
|
|
1836
|
+
else self.spec.list_reverse)
|
|
1837
|
+
if tpl:
|
|
1838
|
+
return tpl.format(x=list_base)
|
|
1839
|
+
return self._mark_unsupported(
|
|
1840
|
+
f"list.{node.func.attr} (not available on the "
|
|
1841
|
+
f"{self.spec.name} backend)")
|
|
1787
1842
|
# generic method call
|
|
1788
1843
|
return f"{base}.{node.func.attr}({', '.join(args)})"
|
|
1789
1844
|
# generic (user) function call: borrow list args where appropriate
|
|
@@ -1802,6 +1857,20 @@ class Emitter:
|
|
|
1802
1857
|
else:
|
|
1803
1858
|
args.append(f"&mut {self.expr(a)}")
|
|
1804
1859
|
continue
|
|
1860
|
+
if self.spec.name == "zig" and self.infer_type(a) == "list":
|
|
1861
|
+
# Zig list params are *std.ArrayList, so the caller passes a
|
|
1862
|
+
# pointer to its own list (mutations must be visible).
|
|
1863
|
+
if isinstance(a, ast.Name):
|
|
1864
|
+
args.append(f"&{self.expr(a)}")
|
|
1865
|
+
else:
|
|
1866
|
+
# a literal or call result is an rvalue and cannot be
|
|
1867
|
+
# addressed; bind it to a temporary first
|
|
1868
|
+
tmp = f"__ge_arg{i}_{len(self.lines)}"
|
|
1869
|
+
self.lines.append(
|
|
1870
|
+
f"{self.spec.indent * self.indent_lvl}"
|
|
1871
|
+
f"var {tmp} = {self.expr(a)};")
|
|
1872
|
+
args.append(f"&{tmp}")
|
|
1873
|
+
continue
|
|
1805
1874
|
args.append(self._arg(a))
|
|
1806
1875
|
# handle keyword arguments: map to positional by reordering
|
|
1807
1876
|
# (native languages don't support keyword args, so we just append them)
|
|
@@ -2153,11 +2222,17 @@ class Emitter:
|
|
|
2153
2222
|
f"for _, {var} := range {iter_src} {{ __v = append(__v, int64({elem})) }}; "
|
|
2154
2223
|
f"return __v; }}()")
|
|
2155
2224
|
if self.spec.name == "zig":
|
|
2225
|
+
# build an ArrayList (the GE list type), not a slice
|
|
2226
|
+
et = self.spec.list_elem_type
|
|
2156
2227
|
if lo is not None:
|
|
2157
|
-
return (f"blk: {{ var __v =
|
|
2158
|
-
f"var {var}: i64 = {lo}; while ({var} < {hi}) : ({var} += 1) {{
|
|
2159
|
-
f"
|
|
2160
|
-
|
|
2228
|
+
return (f"blk: {{ var __v = geListNew({et}); "
|
|
2229
|
+
f"var {var}: i64 = {lo}; while ({var} < {hi}) : ({var} += 1) {{ "
|
|
2230
|
+
f"__v.append(@as({et}, @intCast({elem}))) catch unreachable; }} "
|
|
2231
|
+
f"break :blk __v; }}")
|
|
2232
|
+
return (f"blk: {{ var __v = geListNew({et}); "
|
|
2233
|
+
f"for ({iter_src}.items) |{var}| {{ "
|
|
2234
|
+
f"__v.append(@as({et}, @intCast({elem}))) catch unreachable; }} "
|
|
2235
|
+
f"break :blk __v; }}")
|
|
2161
2236
|
if self.spec.name == "kotlin":
|
|
2162
2237
|
if lo is not None:
|
|
2163
2238
|
return (f"run {{ val __v = mutableListOf<Long>(); "
|
|
@@ -2525,6 +2600,17 @@ class Emitter:
|
|
|
2525
2600
|
key = f"{node.func.value.id}.{node.func.attr}"
|
|
2526
2601
|
if key in self.func_return_types:
|
|
2527
2602
|
return self.func_return_types[key]
|
|
2603
|
+
# str methods that return a string. Without this, print() falls
|
|
2604
|
+
# back to the generic format and a Zig []const u8 renders as a
|
|
2605
|
+
# byte array.
|
|
2606
|
+
if (isinstance(node.func, ast.Attribute)
|
|
2607
|
+
and self.infer_type(node.func.value) == "str"
|
|
2608
|
+
and node.func.attr in ("upper", "lower", "strip", "replace",
|
|
2609
|
+
"join", "title", "capitalize")):
|
|
2610
|
+
return "str"
|
|
2611
|
+
if (isinstance(node.func, ast.Attribute)
|
|
2612
|
+
and node.func.attr in ("split", "splitlines")):
|
|
2613
|
+
return "list"
|
|
2528
2614
|
if isinstance(node, ast.Set):
|
|
2529
2615
|
return "set"
|
|
2530
2616
|
if isinstance(node, ast.SetComp):
|
|
@@ -12,7 +12,12 @@ 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
|
+
list_sort="std::sort({x}.begin(), {x}.end())",
|
|
16
|
+
list_reverse="std::reverse({x}.begin(), {x}.end())",
|
|
15
17
|
str_split="geStrSplit({x}, {sep})",
|
|
18
|
+
str_upper="geStrUpper({x})",
|
|
19
|
+
str_lower="geStrLower({x})",
|
|
20
|
+
str_replace="geStrReplace({x}, {a}, {b})",
|
|
16
21
|
str_join="geStrJoin({x}, {sep})",
|
|
17
22
|
list_slice="std::vector<int64_t>({x}.begin() + ({start}), {x}.begin() + ({stop}))",
|
|
18
23
|
list_copy="std::vector<int64_t>({x})",
|
|
@@ -17,7 +17,12 @@ SPEC = Spec(
|
|
|
17
17
|
list_type="List<{T}>",
|
|
18
18
|
list_param_type="List<long>",
|
|
19
19
|
list_elem_type="long",
|
|
20
|
+
list_sort="{x}.Sort()",
|
|
21
|
+
list_reverse="{x}.Reverse()",
|
|
20
22
|
str_split="{x}.Split({sep}).ToList()",
|
|
23
|
+
str_upper="{x}.ToUpper()",
|
|
24
|
+
str_lower="{x}.ToLower()",
|
|
25
|
+
str_replace="{x}.Replace({a}, {b})",
|
|
21
26
|
str_join="string.Join({sep}, {x})",
|
|
22
27
|
list_slice="{x}.GetRange((int)({start}), (int)(({stop}) - ({start})))",
|
|
23
28
|
list_copy="new List<long>({x})",
|
|
@@ -26,6 +31,10 @@ SPEC = Spec(
|
|
|
26
31
|
range_step_call="({lo}..{hi}).Step({step})",
|
|
27
32
|
len_call="{x}.Length",
|
|
28
33
|
list_len="{x}.Count",
|
|
34
|
+
# Dictionary and HashSet expose Count, not Length; len()
|
|
35
|
+
# fell back to len_call and emitted an invalid member.
|
|
36
|
+
dict_len="{x}.Count",
|
|
37
|
+
set_len="{x}.Count",
|
|
29
38
|
print_int='Console.WriteLine({v})',
|
|
30
39
|
print_float='Console.WriteLine({v})',
|
|
31
40
|
print_str='Console.WriteLine({v})',
|
|
@@ -16,7 +16,12 @@ SPEC = Spec(
|
|
|
16
16
|
list_type="[]{T}",
|
|
17
17
|
list_param_type="[]int64",
|
|
18
18
|
list_elem_type="int64",
|
|
19
|
+
list_sort="sort.Slice({x}, func(i, j int) bool {{ return {x}[i] < {x}[j] }})",
|
|
20
|
+
list_reverse="sort.Slice({x}, func(i, j int) bool {{ return i > j }})",
|
|
19
21
|
str_split="strings.Split({x}, {sep})",
|
|
22
|
+
str_upper="strings.ToUpper({x})",
|
|
23
|
+
str_lower="strings.ToLower({x})",
|
|
24
|
+
str_replace="strings.ReplaceAll({x}, {a}, {b})",
|
|
20
25
|
str_join="strings.Join({x}, {sep})",
|
|
21
26
|
list_slice="append([]int64(nil), {x}[{start}:{stop}]...)",
|
|
22
27
|
list_copy="append([]int64(nil), {x}...)",
|
|
@@ -31,7 +36,9 @@ SPEC = Spec(
|
|
|
31
36
|
print_bool='fmt.Println(geBoolStr({v}))',
|
|
32
37
|
print_generic='fmt.Println({v})',
|
|
33
38
|
print_list='fmt.Println(geListStr({v}))',
|
|
34
|
-
|
|
39
|
+
# Go rejects int64(3.9) for an untyped constant; math.Trunc works
|
|
40
|
+
# for both constants and variables and matches Python int().
|
|
41
|
+
int_cast="int64(math.Trunc({x}))",
|
|
35
42
|
float_cast="float64({x})",
|
|
36
43
|
float_div="(float64({l}) / float64({r}))",
|
|
37
44
|
floor_div="({l} / {r})",
|
|
@@ -17,7 +17,12 @@ SPEC = Spec(
|
|
|
17
17
|
list_type="MutableList<{T}>",
|
|
18
18
|
list_param_type="MutableList<Long>",
|
|
19
19
|
list_elem_type="Long",
|
|
20
|
+
list_sort="{x}.sort()",
|
|
21
|
+
list_reverse="{x}.reverse()",
|
|
20
22
|
str_split="{x}.split({sep}).toMutableList()",
|
|
23
|
+
str_upper="{x}.uppercase()",
|
|
24
|
+
str_lower="{x}.lowercase()",
|
|
25
|
+
str_replace="{x}.replace({a}, {b})",
|
|
21
26
|
str_join="{x}.joinToString({sep})",
|
|
22
27
|
list_slice="{x}.subList(({start}).toInt(), ({stop}).toInt()).toMutableList()",
|
|
23
28
|
list_copy="{x}.toMutableList()",
|
|
@@ -12,7 +12,12 @@ SPEC = Spec(
|
|
|
12
12
|
list_type="Vec<{T}>",
|
|
13
13
|
list_param_type="&[i64]",
|
|
14
14
|
list_elem_type="i64",
|
|
15
|
+
list_sort="{x}.sort()",
|
|
16
|
+
list_reverse="{x}.reverse()",
|
|
15
17
|
str_split="{x}.split({sep}.as_str()).map(|s| s.to_string()).collect::<Vec<String>>()",
|
|
18
|
+
str_upper="{x}.to_uppercase()",
|
|
19
|
+
str_lower="{x}.to_lowercase()",
|
|
20
|
+
str_replace="{x}.replace({a}.as_str(), {b}.as_str())",
|
|
16
21
|
str_join="{x}.join({sep}.as_str())",
|
|
17
22
|
list_slice="{x}[({start}) as usize..({stop}) as usize].to_vec()",
|
|
18
23
|
list_copy="{x}.clone()",
|
|
@@ -57,7 +62,9 @@ SPEC = Spec(
|
|
|
57
62
|
str_slice_end="{x}[..{end}].to_string()",
|
|
58
63
|
list_concat="{{ let mut t = {l}; t.extend({r}.iter()); t }}",
|
|
59
64
|
condition_parens=False,
|
|
60
|
-
|
|
65
|
+
# .cloned() so the loop variable is owned: iterating a &[i64]
|
|
66
|
+
# otherwise yields &i64 and `x == 5` does not compile.
|
|
67
|
+
foreach_template="for {var} in {iter}.iter().cloned()",
|
|
61
68
|
try_template="// try/except not natively supported in Rust\n// {body}\n// {handler}",
|
|
62
69
|
struct_template="#[derive(Clone)]\nstruct {name} {{\n{fields}\n}}",
|
|
63
70
|
struct_field_template=" {type}: {name},",
|
|
@@ -13,40 +13,40 @@ 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="
|
|
17
|
-
list_param_type="
|
|
16
|
+
list_type="std.ArrayList({T})",
|
|
17
|
+
list_param_type="*std.ArrayList(i64)",
|
|
18
18
|
list_elem_type="i64",
|
|
19
|
-
list_slice="{x}
|
|
20
|
-
list_copy="{x}",
|
|
19
|
+
list_slice="geListSlice(i64, {x}.items, @as(usize, @intCast({start})), @as(usize, @intCast({stop})))",
|
|
20
|
+
list_copy="geListCopy(i64, {x}.items)",
|
|
21
21
|
borrow_list_arg=False,
|
|
22
22
|
range_call="({lo}..{hi})",
|
|
23
23
|
range_step_call="({lo}..{hi})",
|
|
24
|
-
len_call="@as(i64, @intCast({x}.len))",
|
|
25
|
-
list_len="@as(i64, @intCast({x}.len))",
|
|
24
|
+
len_call="@as(i64, @intCast({x}.items.len))",
|
|
25
|
+
list_len="@as(i64, @intCast({x}.items.len))",
|
|
26
26
|
print_int='std.io.getStdOut().writer().print("{{d}}\\n", .{{{v}}}) catch unreachable',
|
|
27
27
|
print_float='std.io.getStdOut().writer().print("{{d}}\\n", .{{{v}}}) catch unreachable',
|
|
28
28
|
print_str='std.io.getStdOut().writer().print("{{s}}\\n", .{{{v}}}) catch unreachable',
|
|
29
29
|
print_bool='std.io.getStdOut().writer().print("{{s}}\\n", .{{if ({v}) "True" else "False"}}) catch unreachable',
|
|
30
30
|
print_generic='std.io.getStdOut().writer().print("{{any}}\\n", .{{{v}}}) catch unreachable',
|
|
31
|
-
|
|
31
|
+
list_sort="std.mem.sort(i64, {x}.items, {{}}, std.sort.asc(i64))",
|
|
32
|
+
list_reverse="std.mem.reverse(i64, {x}.items)",
|
|
33
|
+
print_list='gePrintList({v}.items)',
|
|
32
34
|
int_cast="@as(i64, {x})",
|
|
33
35
|
float_cast="@as(f64, {x})",
|
|
34
36
|
float_div="(@as(f64, {l}) / @as(f64, {r}))",
|
|
35
37
|
floor_div="@divFloor({l}, {r})",
|
|
36
|
-
sum_call="blk: {{ var s: i64 = 0; for ({it}) |v| {{ s += v; }} break :blk s; }}",
|
|
38
|
+
sum_call="blk: {{ var s: i64 = 0; for ({it}.items) |v| {{ s += v; }} break :blk s; }}",
|
|
37
39
|
abs_int="@as(i64, @intCast(if ({x} < 0) -{x} else {x}))",
|
|
38
40
|
abs_float="@abs({x})",
|
|
39
41
|
min2_call="@min({a}, {b})",
|
|
40
42
|
max2_call="@max({a}, {b})",
|
|
41
|
-
min_call="
|
|
42
|
-
max_call="
|
|
43
|
+
min_call="blk: {{ var __m = {it}.items[0]; for ({it}.items) |v| {{ if (v < __m) __m = v; }} break :blk __m; }}",
|
|
44
|
+
max_call="blk: {{ var __m = {it}.items[0]; for ({it}.items) |v| {{ if (v > __m) __m = v; }} break :blk __m; }}",
|
|
43
45
|
pow_call="std.math.pow(i64, {l}, {r})",
|
|
44
46
|
pow_int="std.math.pow(i64, {l}, {r})",
|
|
45
47
|
pow_float="std.math.pow(f64, {l}, {r})",
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
append_call="",
|
|
49
|
-
index_call="{x}[@as(usize, @intCast({i}))]",
|
|
48
|
+
append_call="{x}.append({v}) catch unreachable",
|
|
49
|
+
index_call="{x}.items[@as(usize, @intCast({i}))]",
|
|
50
50
|
comment="//",
|
|
51
51
|
fn_template="{sig} {{\n{body}\n}}",
|
|
52
52
|
main_template="",
|
|
@@ -59,16 +59,24 @@ SPEC = Spec(
|
|
|
59
59
|
str_slice="{x}[{start}..{end}]",
|
|
60
60
|
str_slice_start="{x}[{start}..]",
|
|
61
61
|
str_slice_end="{x}[..{end}]",
|
|
62
|
-
list_concat="
|
|
63
|
-
|
|
62
|
+
list_concat="geListConcat(i64, {l}.items, {r}.items)",
|
|
63
|
+
str_split="geStrSplit({x}, {sep})",
|
|
64
|
+
str_join="geStrJoin({x}.items, {sep})",
|
|
65
|
+
str_upper="geStrUpper({x})",
|
|
66
|
+
str_lower="geStrLower({x})",
|
|
67
|
+
str_replace="geStrReplace({x}, {a}, {b})",
|
|
68
|
+
foreach_template="for ({iter}.items) |{var}|",
|
|
64
69
|
try_template="// try/except limited in Zig\n// {body}\n// {handler}",
|
|
65
70
|
struct_template="const {name} = struct {{\n{fields}\n}};",
|
|
66
71
|
struct_field_template=" {type}: {name},",
|
|
67
72
|
struct_new_template="fn {name}_new({params}) {name} {{\n{body}\n}}",
|
|
68
73
|
dict_type="std.StringHashMap({V})",
|
|
74
|
+
set_type="std.AutoHashMap(i64, void)",
|
|
69
75
|
dict_get="{d}.get({k}).?",
|
|
70
76
|
dict_set="{d}.put({k}, {v}) catch unreachable",
|
|
71
77
|
dict_contains="{d}.contains({k})",
|
|
78
|
+
set_len="@as(i64, @intCast({x}.count()))",
|
|
79
|
+
dict_len="@as(i64, @intCast({x}.count()))",
|
|
72
80
|
tuple_type="[2]{T}",
|
|
73
81
|
tuple_get="{t}[{i}]",
|
|
74
82
|
)
|
|
@@ -121,69 +129,49 @@ class ZigEmitter(Emitter):
|
|
|
121
129
|
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
|
|
122
130
|
if node.func.attr == "append" and isinstance(node.func.value, ast.Name):
|
|
123
131
|
mutated.add(node.func.value.id)
|
|
132
|
+
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
133
|
+
# passing a list to a callee that mutates it counts as a
|
|
134
|
+
# mutation here, so the local must be `var`
|
|
135
|
+
for idx in self.func_mutated_params.get(node.func.id, ()):
|
|
136
|
+
if idx < len(node.args):
|
|
137
|
+
arg = node.args[idx]
|
|
138
|
+
if isinstance(arg, ast.Name):
|
|
139
|
+
mutated.add(arg.id)
|
|
124
140
|
return mutated
|
|
125
141
|
|
|
126
142
|
def emit(self, unit: FuncUnit) -> str:
|
|
127
|
-
"""
|
|
143
|
+
"""Emit a function.
|
|
144
|
+
|
|
145
|
+
A list parameter is a `*std.ArrayList(i64)`, so no conversion is
|
|
146
|
+
needed: the callee mutates the caller's list directly. Only scalar
|
|
147
|
+
parameters that are reassigned need a mutable local, because Zig
|
|
148
|
+
parameters are immutable.
|
|
149
|
+
"""
|
|
128
150
|
self.mutated = self._find_mutated_vars(unit.body)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
for pname, ptype in unit.params:
|
|
138
|
-
if pname in self.mutated and ptype != "list":
|
|
139
|
-
mutated_params.append(pname)
|
|
140
|
-
# We'll insert mutable copies as the first lines of the body
|
|
141
|
-
self._zig_mut_copies = mutated_params
|
|
142
|
-
self._zig_list_conversions = list(self.mutated_list_params)
|
|
151
|
+
self.mutated_list_params = {
|
|
152
|
+
pname for pname, ptype in unit.params
|
|
153
|
+
if ptype == "list" and pname in self.mutated
|
|
154
|
+
}
|
|
155
|
+
self.mutated_params = {
|
|
156
|
+
pname for pname, ptype in unit.params
|
|
157
|
+
if pname in self.mutated and ptype != "list"
|
|
158
|
+
}
|
|
143
159
|
result = super().emit(unit)
|
|
144
|
-
if
|
|
145
|
-
# Insert mutable copies and list conversions right after the opening brace
|
|
160
|
+
if self.mutated_params:
|
|
146
161
|
lines = result.split("\n")
|
|
147
|
-
|
|
148
|
-
for pname in mutated_params:
|
|
149
|
-
insert_lines.append(f" var {pname}_ = {pname};")
|
|
150
|
-
for pname in self.mutated_list_params:
|
|
151
|
-
# Convert []const i64 to ArrayList for mutation
|
|
152
|
-
insert_lines.append(f" var {pname}_list = std.ArrayList(i64).init(std.heap.page_allocator);")
|
|
153
|
-
insert_lines.append(f" for ({pname}) |item| {{ {pname}_list.append(item) catch unreachable; }}")
|
|
162
|
+
insert = [f" var {p}_ = {p};" for p in sorted(self.mutated_params)]
|
|
154
163
|
for i, line in enumerate(lines):
|
|
155
|
-
if "
|
|
156
|
-
for j, il in enumerate(
|
|
164
|
+
if "fn" in line and "{" in line:
|
|
165
|
+
for j, il in enumerate(insert):
|
|
157
166
|
lines.insert(i + 1 + j, il)
|
|
158
167
|
break
|
|
159
168
|
result = "\n".join(lines)
|
|
160
|
-
# Replace param references with mutable version
|
|
161
169
|
import re
|
|
162
|
-
for pname in mutated_params:
|
|
163
|
-
|
|
164
|
-
result = re.sub(r
|
|
165
|
-
result =
|
|
166
|
-
|
|
167
|
-
# Replace list param references with ArrayList.items where indexing, or the list itself for append
|
|
168
|
-
for pname in self.mutated_list_params:
|
|
169
|
-
# Replace append calls: pname.append(v) -> pname_list.append(v)
|
|
170
|
-
result = re.sub(
|
|
171
|
-
r'\b' + pname + r'\.append\(',
|
|
172
|
-
pname + '_list.append(',
|
|
173
|
-
result
|
|
174
|
-
)
|
|
175
|
-
# Replace indexing: pname[i] -> pname_list.items[i]
|
|
176
|
-
result = re.sub(
|
|
177
|
-
r'\b' + pname + r'\[',
|
|
178
|
-
pname + '_list.items[',
|
|
179
|
-
result
|
|
180
|
-
)
|
|
181
|
-
# Replace len(pname) -> pname_list.items.len
|
|
182
|
-
result = re.sub(
|
|
183
|
-
r'\b' + pname + r'\.len\b',
|
|
184
|
-
pname + '_list.items.len',
|
|
185
|
-
result
|
|
186
|
-
)
|
|
170
|
+
for pname in self.mutated_params:
|
|
171
|
+
result = re.sub(r"\b" + pname + r"\b", pname + "_", result)
|
|
172
|
+
result = re.sub(r"\b" + pname + r"_: ", pname + ": ", result, count=1)
|
|
173
|
+
result = result.replace(f"var {pname}_ = {pname}_;",
|
|
174
|
+
f"var {pname}_ = {pname};")
|
|
187
175
|
return result
|
|
188
176
|
|
|
189
177
|
def _is_mutated(self, var: str) -> bool:
|
|
@@ -210,6 +198,12 @@ class ZigEmitter(Emitter):
|
|
|
210
198
|
self.var_types[target] = t
|
|
211
199
|
self.declared.add(target)
|
|
212
200
|
nt = self.py_to_native(t)
|
|
201
|
+
if t == "list":
|
|
202
|
+
# a bare `list` annotation carries no element type; take it
|
|
203
|
+
# from the value so `"a,b".split(",")` is a string list
|
|
204
|
+
elem = self._list_elem_of_value(node.value)
|
|
205
|
+
if elem and elem != self.spec.list_elem_type:
|
|
206
|
+
nt = self.spec.list_type.format(T=elem)
|
|
213
207
|
kw = self._decl_keyword(target)
|
|
214
208
|
self.lines.append(f"{ind}{kw} {target}: {nt} = {self.expr(node.value)};")
|
|
215
209
|
return
|
|
@@ -263,7 +257,7 @@ class ZigEmitter(Emitter):
|
|
|
263
257
|
self.lines.append(f"{ind} const {var} = __kp_{var}.*;")
|
|
264
258
|
else:
|
|
265
259
|
self.var_types[var] = "long"
|
|
266
|
-
self.lines.append(f"{ind}for ({iter_s}) |{var}| {{")
|
|
260
|
+
self.lines.append(f"{ind}for ({iter_s}.items) |{var}| {{")
|
|
267
261
|
self.indent_lvl += 1
|
|
268
262
|
for s in node.body:
|
|
269
263
|
self.stmt(s)
|
|
@@ -274,7 +268,7 @@ class ZigEmitter(Emitter):
|
|
|
274
268
|
emit_name = unit.name.replace(".", "_")
|
|
275
269
|
params = []
|
|
276
270
|
for pname, ptype in unit.params:
|
|
277
|
-
nt = self.param_native_type(ptype)
|
|
271
|
+
nt = self.param_native_type(ptype, pname)
|
|
278
272
|
params.append(f"{pname}: {nt}")
|
|
279
273
|
ret = self.py_to_native(unit.ret_type) if unit.ret_type != "None" else "void"
|
|
280
274
|
param_str = ", ".join(params)
|
|
@@ -321,6 +315,23 @@ def _emit_structs_zig(classes: list) -> str:
|
|
|
321
315
|
return "\n".join(out)
|
|
322
316
|
|
|
323
317
|
|
|
318
|
+
def _precompute_mutated_params(emitter, units) -> None:
|
|
319
|
+
"""Record which parameters each function mutates.
|
|
320
|
+
|
|
321
|
+
The emitter normally learns this as it emits each unit, so a function
|
|
322
|
+
defined after its caller would not be known yet. Doing it up front makes
|
|
323
|
+
call-site handling independent of definition order.
|
|
324
|
+
"""
|
|
325
|
+
for u in units:
|
|
326
|
+
if u.body is None:
|
|
327
|
+
continue
|
|
328
|
+
mutated = emitter._find_mutated_vars(u.body.body)
|
|
329
|
+
indices = {i for i, (pname, _pt) in enumerate(u.params)
|
|
330
|
+
if pname in mutated}
|
|
331
|
+
if indices:
|
|
332
|
+
emitter.func_mutated_params[u.name] = indices
|
|
333
|
+
|
|
334
|
+
|
|
324
335
|
def emit_zig(units: list[FuncUnit], entry: str | None,
|
|
325
336
|
library_mode: bool = False,
|
|
326
337
|
extern_fns: list[FuncUnit] | None = None,
|
|
@@ -340,6 +351,7 @@ def emit_zig(units: list[FuncUnit], entry: str | None,
|
|
|
340
351
|
}
|
|
341
352
|
emitter.library_mode = library_mode
|
|
342
353
|
emitter.constants = constants or {}
|
|
354
|
+
_precompute_mutated_params(emitter, units)
|
|
343
355
|
emitted: dict[str, str] = {}
|
|
344
356
|
fns: list[str] = []
|
|
345
357
|
|
|
@@ -411,9 +423,13 @@ def emit_zig(units: list[FuncUnit], entry: str | None,
|
|
|
411
423
|
|
|
412
424
|
if entry_u.name != "main":
|
|
413
425
|
if entry_u.ret_type == "None":
|
|
414
|
-
wrapper = "pub fn main() void {\n
|
|
426
|
+
wrapper = ("pub fn main() void {\n"
|
|
427
|
+
" defer _ = ge_arena.deinit();\n"
|
|
428
|
+
" " + entry_u.name + "();\n}\n")
|
|
415
429
|
else:
|
|
416
|
-
wrapper = f"pub fn main() void {{\n
|
|
430
|
+
wrapper = (f"pub fn main() void {{\n"
|
|
431
|
+
f" defer _ = ge_arena.deinit();\n"
|
|
432
|
+
f" _ = {entry_u.name}();\n}}\n")
|
|
417
433
|
fns.append(wrapper)
|
|
418
434
|
elif not fns:
|
|
419
435
|
# the entry function was rejected during emission; the pipeline
|
|
@@ -421,13 +437,19 @@ def emit_zig(units: list[FuncUnit], entry: str | None,
|
|
|
421
437
|
return prelude, emitted
|
|
422
438
|
else:
|
|
423
439
|
# Zig main() must return void (or u8 for exit code)
|
|
424
|
-
# Rename the user's main() to __ge_main() and add a wrapper
|
|
440
|
+
# Rename the user's main() to __ge_main() and add a wrapper that also
|
|
441
|
+
# releases the arena.
|
|
425
442
|
if entry_u.ret_type != "None":
|
|
426
443
|
fns[-1] = fns[-1].replace(f"fn {entry_u.name}(", "fn __ge_main(")
|
|
427
|
-
wrapper = "pub fn main() void {\n
|
|
428
|
-
|
|
444
|
+
wrapper = ("pub fn main() void {\n"
|
|
445
|
+
" defer _ = ge_arena.deinit();\n"
|
|
446
|
+
" _ = __ge_main();\n}\n")
|
|
429
447
|
else:
|
|
430
|
-
fns[-1] = fns[-1].replace(f"fn {entry_u.name}(", "
|
|
448
|
+
fns[-1] = fns[-1].replace(f"fn {entry_u.name}(", "fn __ge_main(")
|
|
449
|
+
wrapper = ("pub fn main() void {\n"
|
|
450
|
+
" defer _ = ge_arena.deinit();\n"
|
|
451
|
+
" __ge_main();\n}\n")
|
|
452
|
+
fns.append(wrapper)
|
|
431
453
|
|
|
432
454
|
program = prelude + "\n".join(fns) + "\n"
|
|
433
455
|
# inject stdlib runtime
|
package/python/pyeffic/ge_cli.py
CHANGED
|
@@ -211,11 +211,27 @@ def cmd_analyze(args) -> int:
|
|
|
211
211
|
return 1
|
|
212
212
|
|
|
213
213
|
source = source_path.read_text(encoding="utf-8")
|
|
214
|
+
original_source = source
|
|
214
215
|
entry = args.entry or "main"
|
|
215
216
|
|
|
217
|
+
# Lower non-Python flavours (.ge hybrid, .ge.ts) before analysis, exactly
|
|
218
|
+
# as build() does. Without this, `ge analyze` parses a hybrid file as
|
|
219
|
+
# plain Python and reports a syntax error on the first block tag.
|
|
220
|
+
# resolve_imports lowers internally, so it gets the original source.
|
|
221
|
+
from .frontends import frontend_for
|
|
222
|
+
from .frontends.typescript import TypeScriptSyntaxError
|
|
223
|
+
from .modules import _lower_source
|
|
224
|
+
|
|
225
|
+
if frontend_for(source_path) != "python":
|
|
226
|
+
try:
|
|
227
|
+
source = _lower_source(source, source_path)
|
|
228
|
+
except TypeScriptSyntaxError as exc:
|
|
229
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
230
|
+
return 1
|
|
231
|
+
|
|
216
232
|
# Resolve imports first
|
|
217
233
|
try:
|
|
218
|
-
units, classes, warnings = resolve_imports(
|
|
234
|
+
units, classes, warnings = resolve_imports(original_source, source_path)
|
|
219
235
|
for w in warnings:
|
|
220
236
|
print(f" warning: {w}")
|
|
221
237
|
except Exception as e:
|
|
@@ -411,8 +427,96 @@ def _print_flutter(report) -> None:
|
|
|
411
427
|
print(f" ! {e}")
|
|
412
428
|
|
|
413
429
|
|
|
430
|
+
def _ui_node_to_tree(node) -> dict:
|
|
431
|
+
"""Convert a .ge.ui WidgetNode into the dict dartgen.generate_main wants."""
|
|
432
|
+
if node is None:
|
|
433
|
+
return {"kind": "Column", "children": [], "align": "center"}
|
|
434
|
+
d: dict = {"kind": node.kind}
|
|
435
|
+
if node.children:
|
|
436
|
+
d["children"] = [_ui_node_to_tree(c) for c in node.children]
|
|
437
|
+
child = node.props.get("child")
|
|
438
|
+
if child is not None and hasattr(child, "kind"):
|
|
439
|
+
d["child"] = _ui_node_to_tree(child)
|
|
440
|
+
if node.text is not None:
|
|
441
|
+
d["text"] = node.text
|
|
442
|
+
elif "text" in node.props:
|
|
443
|
+
d["text"] = node.props["text"]
|
|
444
|
+
if node.label is not None:
|
|
445
|
+
d["label"] = node.label
|
|
446
|
+
elif "label" in node.props:
|
|
447
|
+
d["label"] = node.props["label"]
|
|
448
|
+
if node.state:
|
|
449
|
+
d["state"] = node.state
|
|
450
|
+
style = node.props.get("style") or node.style_dict.get("style")
|
|
451
|
+
if style:
|
|
452
|
+
d["style"] = style
|
|
453
|
+
for key in ("align", "padding", "color", "hint"):
|
|
454
|
+
if key in node.props:
|
|
455
|
+
d[key] = node.props[key]
|
|
456
|
+
if node.action:
|
|
457
|
+
d["action"] = node.action
|
|
458
|
+
return d
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _flutter_from_ui(args, ui_path: Path) -> int:
|
|
462
|
+
"""`ge flutter foo.ge.ui` — generate a Flutter project from the UI DSL."""
|
|
463
|
+
from .ui_dsl import parse_ui_file
|
|
464
|
+
from .dartgen import generate_main, generate_pubspec
|
|
465
|
+
from .config import Config
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
screen = parse_ui_file(ui_path)
|
|
469
|
+
except SyntaxError as e:
|
|
470
|
+
print(f"error: invalid .ge.ui — {e}", file=sys.stderr)
|
|
471
|
+
return 1
|
|
472
|
+
|
|
473
|
+
app_name = args.app_name or ui_path.stem
|
|
474
|
+
cfg = Config(out_dir=Path(args.out_dir), target="mobile")
|
|
475
|
+
project_dir = cfg.target_dir / app_name
|
|
476
|
+
lib_dir = project_dir / "lib"
|
|
477
|
+
lib_dir.mkdir(parents=True, exist_ok=True)
|
|
478
|
+
|
|
479
|
+
tree = _ui_node_to_tree(screen.root)
|
|
480
|
+
title = screen.title or app_name
|
|
481
|
+
main_dart = generate_main(tree, [], app_title=title)
|
|
482
|
+
|
|
483
|
+
(lib_dir / "main.dart").write_text(main_dart, encoding="utf-8")
|
|
484
|
+
# generate_main always imports bindings.dart; a UI-only project has no
|
|
485
|
+
# FFI exports, so emit the stub rather than leaving a dangling import.
|
|
486
|
+
from .dartgen import generate_bindings
|
|
487
|
+
(lib_dir / "bindings.dart").write_text(
|
|
488
|
+
generate_bindings([], app_name), encoding="utf-8")
|
|
489
|
+
(project_dir / "pubspec.yaml").write_text(
|
|
490
|
+
generate_pubspec(app_name), encoding="utf-8")
|
|
491
|
+
|
|
492
|
+
print(GE_BANNER)
|
|
493
|
+
print()
|
|
494
|
+
print(f"== GE flutter app: {project_dir} ==")
|
|
495
|
+
print(f"Title : {title}")
|
|
496
|
+
print(f"Dart files : {lib_dir / 'main.dart'}")
|
|
497
|
+
print(f"\nNext: cd {project_dir} && flutter pub get && flutter run")
|
|
498
|
+
|
|
499
|
+
dart = _find_dart()
|
|
500
|
+
if dart:
|
|
501
|
+
r = subprocess.run([dart, "analyze", str(lib_dir)],
|
|
502
|
+
capture_output=True, text=True, timeout=120)
|
|
503
|
+
out = ((r.stdout or "") + (r.stderr or "")).strip()
|
|
504
|
+
print(out[:1200] if out else "(analyzer: no output)")
|
|
505
|
+
print("Dart analysis: " + ("PASS" if r.returncode == 0 else "issues found"))
|
|
506
|
+
else:
|
|
507
|
+
print("\n(dart not found — install Flutter to validate generated Dart)")
|
|
508
|
+
return 0
|
|
509
|
+
|
|
510
|
+
|
|
414
511
|
def cmd_flutter(args) -> int:
|
|
415
|
-
|
|
512
|
+
src_path = Path(args.file)
|
|
513
|
+
# A .ge.ui file is a UI definition, not GE source. `ge flutter` used to
|
|
514
|
+
# hand it to the module resolver, which tried to parse `Window {` as
|
|
515
|
+
# Python and failed. Generate the Dart UI directly instead.
|
|
516
|
+
if src_path.suffix == ".ui":
|
|
517
|
+
return _flutter_from_ui(args, src_path)
|
|
518
|
+
|
|
519
|
+
source = _maybe_transpile_ts(src_path)
|
|
416
520
|
# a Flutter app is a mobile artifact, so it always lands in build/mobile/
|
|
417
521
|
cfg = Config(out_dir=Path(args.out_dir), target="mobile",
|
|
418
522
|
force_backend=None if args.backend == "auto" else args.backend,
|
|
@@ -502,12 +606,14 @@ def cmd_react(args) -> int:
|
|
|
502
606
|
return 1
|
|
503
607
|
|
|
504
608
|
app_name = args.app_name or ui_path.stem.replace(".ge", "")
|
|
505
|
-
#
|
|
609
|
+
# Output goes under the target-aware build root so every artifact of a
|
|
610
|
+
# project lives in one place: build/web/frontend. An explicit -o wins.
|
|
506
611
|
if args.out_dir:
|
|
507
612
|
out_dir = Path(args.out_dir)
|
|
508
613
|
else:
|
|
509
|
-
|
|
510
|
-
|
|
614
|
+
from .config import Config
|
|
615
|
+
cfg = Config(target="web")
|
|
616
|
+
out_dir = cfg.target_dir / "frontend"
|
|
511
617
|
|
|
512
618
|
files = generate_react_app(screen, app_name)
|
|
513
619
|
written = write_react_app(files, out_dir, force=args.force)
|
package/python/pyeffic/stdlib.py
CHANGED
|
@@ -298,17 +298,125 @@ fun ge_log(x: Double): Double = ln(x)
|
|
|
298
298
|
fun ge_exp(x: Double): Double = exp(x)
|
|
299
299
|
''',
|
|
300
300
|
"zig": '''
|
|
301
|
-
// GE
|
|
302
|
-
|
|
301
|
+
// GE runtime (Zig)
|
|
302
|
+
//
|
|
303
|
+
// GE lists are growable, so they need an allocator. A generated program
|
|
304
|
+
// runs start-to-finish, so a single arena is the right fit: it amortises
|
|
305
|
+
// the page-grain cost across many small lists and everything is released
|
|
306
|
+
// in one call. (std.heap.page_allocator alone would issue a syscall per
|
|
307
|
+
// allocation, and GeneralPurposeAllocator is a debugging allocator.)
|
|
308
|
+
var ge_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
309
|
+
const ge_alloc = ge_arena.allocator();
|
|
310
|
+
|
|
311
|
+
fn geListNew(comptime T: type) std.ArrayList(T) {
|
|
312
|
+
return std.ArrayList(T).init(ge_alloc);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fn geListFrom(comptime T: type, items: []const T) std.ArrayList(T) {
|
|
316
|
+
var l = std.ArrayList(T).init(ge_alloc);
|
|
317
|
+
l.appendSlice(items) catch unreachable;
|
|
318
|
+
return l;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fn geListCopy(comptime T: type, src: []const T) std.ArrayList(T) {
|
|
322
|
+
return geListFrom(T, src);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
fn geListSlice(comptime T: type, src: []const T, lo: usize, hi: usize) std.ArrayList(T) {
|
|
326
|
+
var l = std.ArrayList(T).init(ge_alloc);
|
|
327
|
+
l.appendSlice(src[lo..hi]) catch unreachable;
|
|
328
|
+
return l;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
fn geListConcat(comptime T: type, a: []const T, b: []const T) std.ArrayList(T) {
|
|
332
|
+
var l = std.ArrayList(T).init(ge_alloc);
|
|
333
|
+
l.appendSlice(a) catch unreachable;
|
|
334
|
+
l.appendSlice(b) catch unreachable;
|
|
335
|
+
return l;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Python renders a sequence as [1, 2, 3] and quotes strings with '.
|
|
339
|
+
fn gePrintList(v: anytype) void {
|
|
303
340
|
const w = std.io.getStdOut().writer();
|
|
304
341
|
w.print("[", .{}) catch unreachable;
|
|
305
342
|
for (v, 0..) |x, i| {
|
|
306
343
|
if (i > 0) w.print(", ", .{}) catch unreachable;
|
|
307
|
-
|
|
344
|
+
switch (@typeInfo(@TypeOf(x))) {
|
|
345
|
+
.pointer => w.print("'{s}'", .{x}) catch unreachable,
|
|
346
|
+
.float => w.print("{d}", .{x}) catch unreachable,
|
|
347
|
+
.bool => w.print("{s}", .{if (x) "True" else "False"}) catch unreachable,
|
|
348
|
+
else => w.print("{d}", .{x}) catch unreachable,
|
|
349
|
+
}
|
|
308
350
|
}
|
|
309
351
|
w.print("]\\n", .{}) catch unreachable;
|
|
310
352
|
}
|
|
311
353
|
|
|
354
|
+
fn geSetNew(comptime T: type) std.AutoHashMap(T, void) {
|
|
355
|
+
return std.AutoHashMap(T, void).init(ge_alloc);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn geSetFrom(comptime T: type, items: []const T) std.AutoHashMap(T, void) {
|
|
359
|
+
var s = std.AutoHashMap(T, void).init(ge_alloc);
|
|
360
|
+
for (items) |x| s.put(x, {{}}) catch unreachable;
|
|
361
|
+
return s;
|
|
362
|
+
}
|
|
363
|
+
fn geStrSplit(s: []const u8, sep: []const u8) std.ArrayList([]const u8) {
|
|
364
|
+
var l = std.ArrayList([]const u8).init(ge_alloc);
|
|
365
|
+
if (sep.len == 0) {
|
|
366
|
+
l.append(s) catch unreachable;
|
|
367
|
+
return l;
|
|
368
|
+
}
|
|
369
|
+
var it = std.mem.splitSequence(u8, s, sep);
|
|
370
|
+
while (it.next()) |part| l.append(part) catch unreachable;
|
|
371
|
+
return l;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
fn geStrJoin(parts: []const []const u8, sep: []const u8) []const u8 {
|
|
375
|
+
var total: usize = 0;
|
|
376
|
+
for (parts, 0..) |p, i| {
|
|
377
|
+
total += p.len;
|
|
378
|
+
if (i > 0) total += sep.len;
|
|
379
|
+
}
|
|
380
|
+
const buf = ge_alloc.alloc(u8, total) catch unreachable;
|
|
381
|
+
var pos: usize = 0;
|
|
382
|
+
for (parts, 0..) |p, i| {
|
|
383
|
+
if (i > 0) {
|
|
384
|
+
@memcpy(buf[pos..][0..sep.len], sep);
|
|
385
|
+
pos += sep.len;
|
|
386
|
+
}
|
|
387
|
+
@memcpy(buf[pos..][0..p.len], p);
|
|
388
|
+
pos += p.len;
|
|
389
|
+
}
|
|
390
|
+
return buf;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
fn geStrReplace(s: []const u8, from: []const u8, to: []const u8) []const u8 {
|
|
394
|
+
if (from.len == 0) return s;
|
|
395
|
+
var out = std.ArrayList(u8).init(ge_alloc);
|
|
396
|
+
var i: usize = 0;
|
|
397
|
+
while (i < s.len) {
|
|
398
|
+
if (i + from.len <= s.len and std.mem.eql(u8, s[i..][0..from.len], from)) {
|
|
399
|
+
out.appendSlice(to) catch unreachable;
|
|
400
|
+
i += from.len;
|
|
401
|
+
} else {
|
|
402
|
+
out.append(s[i]) catch unreachable;
|
|
403
|
+
i += 1;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return out.items;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
fn geStrUpper(s: []const u8) []const u8 {
|
|
410
|
+
const buf = ge_alloc.alloc(u8, s.len) catch unreachable;
|
|
411
|
+
for (s, 0..) |c, i| buf[i] = std.ascii.toUpper(c);
|
|
412
|
+
return buf;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
fn geStrLower(s: []const u8) []const u8 {
|
|
416
|
+
const buf = ge_alloc.alloc(u8, s.len) catch unreachable;
|
|
417
|
+
for (s, 0..) |c, i| buf[i] = std.ascii.toLower(c);
|
|
418
|
+
return buf;
|
|
419
|
+
}
|
|
312
420
|
fn ge_sqrt(x: f64) f64 { return @sqrt(x); }
|
|
313
421
|
fn ge_floor(x: f64) f64 { return @floor(x); }
|
|
314
422
|
fn ge_ceil(x: f64) f64 { return @ceil(x); }
|