gelang 0.1.6 → 0.1.7
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 +38 -0
- package/package.json +1 -1
- package/python/pyeffic/dartgen.py +4 -3
- package/python/pyeffic/emitters/base.py +65 -6
- package/python/pyeffic/emitters/cpp.py +1 -1
- package/python/pyeffic/emitters/csharp.py +1 -1
- package/python/pyeffic/emitters/go.py +1 -1
- package/python/pyeffic/emitters/kotlin.py +1 -1
- package/python/pyeffic/emitters/rust.py +9 -6
- package/python/pyeffic/emitters/zig.py +7 -1
- package/python/pyeffic/ge_cli.py +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,44 @@ 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.7] — 2026-09-13
|
|
8
|
+
|
|
9
|
+
Found while building a real application (a Flutter AI keyboard with a Rust
|
|
10
|
+
memory layer and a C# core) against the published package.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Rust string parameters moved on first use.** `fn f(text: str)` emitted
|
|
15
|
+
`text: String`, so a parameter used twice failed with E0382. Parameters are
|
|
16
|
+
now `&str` and call sites borrow, which is the documented Rust idiom
|
|
17
|
+
("prefer the borrowed type over borrowing the owned type"); a `str`
|
|
18
|
+
returning function converts back to an owned `String` on the way out.
|
|
19
|
+
- **`s[i]` returned a code point, not a character.** Python's `s[i]` is a
|
|
20
|
+
one-character *string*, so `if s[0] == "a"` compared an integer with a
|
|
21
|
+
string and failed to compile on every backend.
|
|
22
|
+
- **Zig could not compare strings.** `a == b` on two `[]const u8` is invalid;
|
|
23
|
+
equality now goes through `std.mem.eql`.
|
|
24
|
+
- **`if len(s) < n` produced invalid Rust.** An unparenthesised cast made
|
|
25
|
+
`x.len() as i64 < 2` parse as a generic argument list.
|
|
26
|
+
- **Rust string slicing and `contains`** needed `usize` ranges and `&str`
|
|
27
|
+
patterns respectively.
|
|
28
|
+
- **Zig `int(3.9)`** was rejected (`@as(i64, 3.9)` cannot coerce); it now
|
|
29
|
+
uses `@intFromFloat`.
|
|
30
|
+
- **Zig read-only list parameters** were `*std.ArrayList`, which forced the
|
|
31
|
+
caller to declare the list `var`. They are now `*const std.ArrayList`.
|
|
32
|
+
- **Module-level string constants** were typed as integers, so `A + B` on two
|
|
33
|
+
string constants emitted integer addition.
|
|
34
|
+
- **`ge analyze` did not know about imports.** Every call across a module
|
|
35
|
+
boundary was reported as `GE007 called but not defined`; the resolved units
|
|
36
|
+
are now passed to the type checker.
|
|
37
|
+
- **Generated Flutter** had three analyzer warnings: an unused FFI handle and
|
|
38
|
+
a deprecated `withOpacity`. `flutter analyze` now reports no issues.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- `examples/` is gitignored. It holds experiment projects built against the
|
|
43
|
+
published package to exercise the compiler, not part of the release.
|
|
44
|
+
|
|
7
45
|
## [Unreleased]
|
|
8
46
|
|
|
9
47
|
### Added
|
package/package.json
CHANGED
|
@@ -72,9 +72,9 @@ def generate_bindings(ffi_units: list[FuncUnit], lib_basename: str,
|
|
|
72
72
|
if multi:
|
|
73
73
|
# multi-backend: one DynamicLibrary per backend
|
|
74
74
|
for b in sorted(backends_used):
|
|
75
|
-
lines.append(f" late final DynamicLibrary _lib_{b};")
|
|
75
|
+
lines.append(f" // ignore: unused_field\n late final DynamicLibrary _lib_{b};")
|
|
76
76
|
else:
|
|
77
|
-
lines.append(" late final DynamicLibrary _lib;")
|
|
77
|
+
lines.append(" // ignore: unused_field\n late final DynamicLibrary _lib;")
|
|
78
78
|
|
|
79
79
|
for u in ffi_units:
|
|
80
80
|
lines.append(f" late final {dart_name(u.name)}Dart {dart_name(u.name)};")
|
|
@@ -306,6 +306,7 @@ class HomePage extends StatefulWidget {{
|
|
|
306
306
|
}}
|
|
307
307
|
|
|
308
308
|
class _HomePageState extends State<HomePage> {{
|
|
309
|
+
// ignore: unused_field — the UI-only path has no native exports yet
|
|
309
310
|
final NativeLib _lib = NativeLib();
|
|
310
311
|
|
|
311
312
|
{state_decls}
|
|
@@ -353,7 +354,7 @@ class GeTagged extends StatelessWidget {{
|
|
|
353
354
|
}},
|
|
354
355
|
child: Container(
|
|
355
356
|
decoration: BoxDecoration(
|
|
356
|
-
border: Border.all(color: Colors.blue.
|
|
357
|
+
border: Border.all(color: Colors.blue.withValues(alpha: 0.3), width: 1),
|
|
357
358
|
),
|
|
358
359
|
child: child,
|
|
359
360
|
),
|
|
@@ -51,6 +51,9 @@ class Spec:
|
|
|
51
51
|
# Type-aware exponentiation. Integer `**` and float `**` need different
|
|
52
52
|
# target syntax in most languages; when these are empty the emitter falls
|
|
53
53
|
# back to pow_call.
|
|
54
|
+
# Converting a float to int needs a different spelling on some
|
|
55
|
+
# targets (Zig rejects @as(i64, 3.9)); empty falls back to int_cast.
|
|
56
|
+
int_cast_float: str = ""
|
|
54
57
|
pow_int: str = ""
|
|
55
58
|
pow_float: str = ""
|
|
56
59
|
# two-argument min(a, b) / max(a, b). Most targets have no plain
|
|
@@ -164,6 +167,9 @@ class Emitter:
|
|
|
164
167
|
# reset unsupported tracking for this function
|
|
165
168
|
self.unsupported_emissions = []
|
|
166
169
|
self.current_func = unit.name
|
|
170
|
+
# declared return type, so a borrowed &str can be converted back to an
|
|
171
|
+
# owned String on the way out (Rust idiom: borrow in, own out)
|
|
172
|
+
self.current_ret_type = unit.ret_type
|
|
167
173
|
# load container element types for this function's params
|
|
168
174
|
self.param_elem_types = getattr(unit, 'param_elem_types', {})
|
|
169
175
|
self.param_dict_types = getattr(unit, 'param_dict_types', {})
|
|
@@ -268,6 +274,9 @@ class Emitter:
|
|
|
268
274
|
return self.spec.dict_type.format(V=self.spec.list_elem_type)
|
|
269
275
|
if t == "tuple":
|
|
270
276
|
return self.spec.tuple_type.format(T=self.spec.list_elem_type)
|
|
277
|
+
if t == "str" and self.spec.name == "rust":
|
|
278
|
+
# borrowed: an owned String parameter would be moved on first use
|
|
279
|
+
return "&str"
|
|
271
280
|
if t == "set":
|
|
272
281
|
if self.spec.set_type:
|
|
273
282
|
if self.spec.name == "cpp":
|
|
@@ -330,6 +339,14 @@ class Emitter:
|
|
|
330
339
|
and node.value.id in self.mutated_params
|
|
331
340
|
and self.var_types.get(node.value.id) == "list"):
|
|
332
341
|
self.lines.append(f"{ind}return (*{node.value.id}).clone();")
|
|
342
|
+
elif (self.spec.name == "rust"
|
|
343
|
+
and self.current_ret_type == "str"
|
|
344
|
+
and isinstance(node.value, ast.Name)
|
|
345
|
+
and node.value.id in self.params):
|
|
346
|
+
# str parameters are borrowed &str, but a str-returning
|
|
347
|
+
# function must hand back an owned String
|
|
348
|
+
self.lines.append(
|
|
349
|
+
f"{ind}return {self.expr(node.value)}.to_string();")
|
|
333
350
|
else:
|
|
334
351
|
self.lines.append(f"{ind}return {self.expr(node.value)};")
|
|
335
352
|
elif isinstance(node, ast.Assign):
|
|
@@ -1114,7 +1131,8 @@ class Emitter:
|
|
|
1114
1131
|
elif self.spec.name == "go":
|
|
1115
1132
|
contains = f"contains({right}, {cur})"
|
|
1116
1133
|
elif self.spec.name == "kotlin":
|
|
1117
|
-
|
|
1134
|
+
# contains() takes a Pattern, not a String
|
|
1135
|
+
contains = f"{right}.contains({cur}.as_str())"
|
|
1118
1136
|
else:
|
|
1119
1137
|
contains = f"/*contains*/"
|
|
1120
1138
|
if isinstance(op, ast.NotIn):
|
|
@@ -1124,7 +1142,8 @@ class Emitter:
|
|
|
1124
1142
|
else:
|
|
1125
1143
|
# string contains
|
|
1126
1144
|
if self.spec.name == "rust":
|
|
1127
|
-
|
|
1145
|
+
# contains() takes a Pattern, not a String
|
|
1146
|
+
contains = f"{right}.contains({cur}.as_str())"
|
|
1128
1147
|
elif self.spec.name == "cpp":
|
|
1129
1148
|
contains = f"({right}.find({cur}) != std::string::npos)"
|
|
1130
1149
|
elif self.spec.name == "csharp":
|
|
@@ -1143,7 +1162,17 @@ class Emitter:
|
|
|
1143
1162
|
else:
|
|
1144
1163
|
sym = self.cmp_symbol(op)
|
|
1145
1164
|
right = self.expr(comp)
|
|
1146
|
-
|
|
1165
|
+
right_type = self.infer_type(comp)
|
|
1166
|
+
# Zig cannot compare slices with ==; string equality goes
|
|
1167
|
+
# through std.mem.eql.
|
|
1168
|
+
if (self.spec.name == "zig"
|
|
1169
|
+
and (left_type == "str" or right_type == "str")
|
|
1170
|
+
and isinstance(op, (ast.Eq, ast.NotEq))):
|
|
1171
|
+
eql = f"std.mem.eql(u8, {cur}, {right})"
|
|
1172
|
+
parts.append(f"(!{eql})" if isinstance(op, ast.NotEq)
|
|
1173
|
+
else eql)
|
|
1174
|
+
else:
|
|
1175
|
+
parts.append(f"({cur} {sym} {right})")
|
|
1147
1176
|
cur = right
|
|
1148
1177
|
return "(" + " && ".join(parts) + ")"
|
|
1149
1178
|
if isinstance(node, ast.IfExp):
|
|
@@ -1423,7 +1452,11 @@ class Emitter:
|
|
|
1423
1452
|
if fname == "pow":
|
|
1424
1453
|
return self.spec.pow_call.format(l=self.expr(node.args[0]), r=self.expr(node.args[1]))
|
|
1425
1454
|
if fname == "int":
|
|
1426
|
-
|
|
1455
|
+
arg = node.args[0]
|
|
1456
|
+
# Zig rejects @as(i64, 3.9); a float argument needs its own form
|
|
1457
|
+
if self.infer_type(arg) == "float" and self.spec.int_cast_float:
|
|
1458
|
+
return f"({self.spec.int_cast_float.format(x=self.expr(arg))})"
|
|
1459
|
+
return f"({self.spec.int_cast.format(x=self.expr(arg))})"
|
|
1427
1460
|
if fname == "float":
|
|
1428
1461
|
return f"({self.spec.float_cast.format(x=self.expr(node.args[0]))})"
|
|
1429
1462
|
if fname == "str":
|
|
@@ -1941,6 +1974,16 @@ class Emitter:
|
|
|
1941
1974
|
t = self.infer_type(node)
|
|
1942
1975
|
if t == "list":
|
|
1943
1976
|
return "&" + s
|
|
1977
|
+
if t == "str" and self.spec.name == "rust":
|
|
1978
|
+
# a str param is &str; a String local or literal must be borrowed
|
|
1979
|
+
if isinstance(node, ast.Name) and node.id in self.params:
|
|
1980
|
+
return s # already a &str parameter
|
|
1981
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
1982
|
+
# a literal is already &'static str; emitting String::from
|
|
1983
|
+
# here would need an extra borrow and an allocation
|
|
1984
|
+
return '"' + node.value.replace("\\", "\\\\").replace(
|
|
1985
|
+
'"', '\\"').replace("\n", "\\n") + '"'
|
|
1986
|
+
return "&" + s
|
|
1944
1987
|
return s
|
|
1945
1988
|
|
|
1946
1989
|
def _stdlib_call(self, ge_name: str, args: list[ast.AST]) -> str:
|
|
@@ -2518,7 +2561,22 @@ class Emitter:
|
|
|
2518
2561
|
if isinstance(node.value, str):
|
|
2519
2562
|
return "str"
|
|
2520
2563
|
if isinstance(node, ast.Name):
|
|
2521
|
-
|
|
2564
|
+
if node.id in self.var_types:
|
|
2565
|
+
return self.var_types[node.id]
|
|
2566
|
+
# module-level constants are inlined, not declared, so their type
|
|
2567
|
+
# has to be recovered from the collected value — otherwise
|
|
2568
|
+
# `GREETING + NAME` is treated as integer addition
|
|
2569
|
+
if node.id in self.constants:
|
|
2570
|
+
v = self.constants[node.id]
|
|
2571
|
+
if isinstance(v, bool):
|
|
2572
|
+
return "bool"
|
|
2573
|
+
if isinstance(v, int):
|
|
2574
|
+
return "int"
|
|
2575
|
+
if isinstance(v, float):
|
|
2576
|
+
return "float"
|
|
2577
|
+
if isinstance(v, str):
|
|
2578
|
+
return "str"
|
|
2579
|
+
return "int"
|
|
2522
2580
|
if isinstance(node, ast.BinOp):
|
|
2523
2581
|
lt = self.infer_type(node.left)
|
|
2524
2582
|
rt = self.infer_type(node.right)
|
|
@@ -2548,7 +2606,8 @@ class Emitter:
|
|
|
2548
2606
|
# slicing a string returns a string, slicing a list returns a list
|
|
2549
2607
|
return base_type if base_type in ("str", "list") else "list"
|
|
2550
2608
|
if base_type == "str":
|
|
2551
|
-
|
|
2609
|
+
# Python's s[i] is a one-character string, not a code point
|
|
2610
|
+
return "str"
|
|
2552
2611
|
if base_type == "dict":
|
|
2553
2612
|
return "int" # dict values are integers
|
|
2554
2613
|
if base_type == "tuple":
|
|
@@ -54,7 +54,7 @@ SPEC = Spec(
|
|
|
54
54
|
ffi_prefix='extern "C" ',
|
|
55
55
|
str_concat="{l} + {r}",
|
|
56
56
|
str_len="static_cast<int64_t>({x}.length())",
|
|
57
|
-
str_index="
|
|
57
|
+
str_index="std::string(1, ({x})[({i})])",
|
|
58
58
|
str_slice="{x}.substr({start}, {len})",
|
|
59
59
|
str_slice_start="{x}.substr({start})",
|
|
60
60
|
str_slice_end="{x}.substr(0, {end})",
|
|
@@ -64,7 +64,7 @@ SPEC = Spec(
|
|
|
64
64
|
indent=" ",
|
|
65
65
|
str_concat="{l} + {r}",
|
|
66
66
|
str_len="{x}.Length",
|
|
67
|
-
str_index="(
|
|
67
|
+
str_index="({x})[(int)({i})].ToString()",
|
|
68
68
|
str_slice="{x}.Substring({start}, {len})",
|
|
69
69
|
str_slice_start="{x}.Substring({start})",
|
|
70
70
|
str_slice_end="{x}.Substring(0, {end})",
|
|
@@ -61,7 +61,7 @@ SPEC = Spec(
|
|
|
61
61
|
indent=" ",
|
|
62
62
|
str_concat="{l} + {r}",
|
|
63
63
|
str_len="{x}.length.toLong()",
|
|
64
|
-
str_index="{x}[{i}.toInt()].
|
|
64
|
+
str_index="({x})[({i}).toInt()].toString()",
|
|
65
65
|
str_slice="{x}.substring({start}.toInt(), {end}.toInt())",
|
|
66
66
|
str_slice_start="{x}.substring({start}.toInt())",
|
|
67
67
|
str_slice_end="{x}.substring(0, {end}.toInt())",
|
|
@@ -24,7 +24,9 @@ SPEC = Spec(
|
|
|
24
24
|
borrow_list_arg=True,
|
|
25
25
|
range_call="({lo}..{hi})",
|
|
26
26
|
range_step_call="({lo}..{hi}).step_by({step} as usize)",
|
|
27
|
-
|
|
27
|
+
# parenthesised: an unparenthesised cast makes `x.len() as i64 < 2`
|
|
28
|
+
# parse as a generic argument list, not a comparison
|
|
29
|
+
len_call="({x}.len() as i64)",
|
|
28
30
|
list_len="{x}.len() as i64",
|
|
29
31
|
print_int='println!("{{}}", {v})',
|
|
30
32
|
print_float='println!("{{}}", {v})',
|
|
@@ -55,11 +57,12 @@ SPEC = Spec(
|
|
|
55
57
|
# `String + &String` is not Add in Rust and `a + b` moves `a`, so a
|
|
56
58
|
# format! is used instead — always correct, and it accepts &str too.
|
|
57
59
|
str_concat="format!(\"{{}}{{}}\", {l}, {r})",
|
|
58
|
-
str_len="{x}.len() as i64",
|
|
59
|
-
str_index="{x}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
str_len="({x}.len() as i64)",
|
|
61
|
+
str_index="{x}[({i}) as usize..].chars().next().unwrap_or(' ').to_string()",
|
|
62
|
+
# ranges must be usize, and slicing a String needs a panic-free bound
|
|
63
|
+
str_slice="{x}[({start}) as usize..({end}) as usize].to_string()",
|
|
64
|
+
str_slice_start="{x}[({start}) as usize..].to_string()",
|
|
65
|
+
str_slice_end="{x}[..({end}) as usize].to_string()",
|
|
63
66
|
list_concat="{{ let mut t = {l}; t.extend({r}.iter()); t }}",
|
|
64
67
|
condition_parens=False,
|
|
65
68
|
# .cloned() so the loop variable is owned: iterating a &[i64]
|
|
@@ -32,6 +32,7 @@ SPEC = Spec(
|
|
|
32
32
|
list_reverse="std.mem.reverse(i64, {x}.items)",
|
|
33
33
|
print_list='gePrintList({v}.items)',
|
|
34
34
|
int_cast="@as(i64, {x})",
|
|
35
|
+
int_cast_float="@as(i64, @intFromFloat({x}))",
|
|
35
36
|
float_cast="@as(f64, {x})",
|
|
36
37
|
float_div="(@as(f64, {l}) / @as(f64, {r}))",
|
|
37
38
|
floor_div="@divFloor({l}, {r})",
|
|
@@ -55,7 +56,7 @@ SPEC = Spec(
|
|
|
55
56
|
indent=" ",
|
|
56
57
|
str_concat="{l} ++ {r}",
|
|
57
58
|
str_len="{x}.len",
|
|
58
|
-
str_index="{x}[{i}]",
|
|
59
|
+
str_index="({x})[@as(usize, @intCast({i}))..][0..1]",
|
|
59
60
|
str_slice="{x}[{start}..{end}]",
|
|
60
61
|
str_slice_start="{x}[{start}..]",
|
|
61
62
|
str_slice_end="{x}[..{end}]",
|
|
@@ -269,6 +270,11 @@ class ZigEmitter(Emitter):
|
|
|
269
270
|
params = []
|
|
270
271
|
for pname, ptype in unit.params:
|
|
271
272
|
nt = self.param_native_type(ptype, pname)
|
|
273
|
+
# A parameter this function does not mutate is borrowed const, so
|
|
274
|
+
# the caller may keep its own binding `const`.
|
|
275
|
+
if (ptype == "list" and nt.startswith("*std.ArrayList")
|
|
276
|
+
and pname not in self.mutated_list_params):
|
|
277
|
+
nt = "*const " + nt[1:]
|
|
272
278
|
params.append(f"{pname}: {nt}")
|
|
273
279
|
ret = self.py_to_native(unit.ret_type) if unit.ret_type != "None" else "void"
|
|
274
280
|
param_str = ", ".join(params)
|
package/python/pyeffic/ge_cli.py
CHANGED
|
@@ -238,8 +238,12 @@ def cmd_analyze(args) -> int:
|
|
|
238
238
|
print(f" Import resolution error: {e}")
|
|
239
239
|
units, classes, warnings = [], [], []
|
|
240
240
|
|
|
241
|
-
# Run type checker
|
|
242
|
-
|
|
241
|
+
# Run type checker. The resolved units are passed in so that functions
|
|
242
|
+
# brought in by an import are known — otherwise every call across a module
|
|
243
|
+
# boundary is reported as GE007 "called but not defined".
|
|
244
|
+
reporter = check_source(source, file=str(source_path), entry=entry,
|
|
245
|
+
units=units or None,
|
|
246
|
+
classes=classes or None)
|
|
243
247
|
|
|
244
248
|
# Print diagnostics
|
|
245
249
|
errors = [d for d in reporter.diagnostics if d.severity == "error"]
|