gelang 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +535 -0
  4. package/bin/ge.js +112 -0
  5. package/package.json +62 -0
  6. package/python/pyeffic/__init__.py +9 -0
  7. package/python/pyeffic/__main__.py +6 -0
  8. package/python/pyeffic/analyzer.py +464 -0
  9. package/python/pyeffic/apisurface.py +238 -0
  10. package/python/pyeffic/autoselect.py +327 -0
  11. package/python/pyeffic/backends.py +87 -0
  12. package/python/pyeffic/bench.py +233 -0
  13. package/python/pyeffic/cli.py +184 -0
  14. package/python/pyeffic/compiler.py +421 -0
  15. package/python/pyeffic/config.py +383 -0
  16. package/python/pyeffic/dartgen.py +441 -0
  17. package/python/pyeffic/deploy.py +586 -0
  18. package/python/pyeffic/diagnostics.py +194 -0
  19. package/python/pyeffic/difftest.py +424 -0
  20. package/python/pyeffic/downloader.py +307 -0
  21. package/python/pyeffic/emitters/__init__.py +11 -0
  22. package/python/pyeffic/emitters/base.py +2359 -0
  23. package/python/pyeffic/emitters/cpp.py +266 -0
  24. package/python/pyeffic/emitters/csharp.py +342 -0
  25. package/python/pyeffic/emitters/dart.py +349 -0
  26. package/python/pyeffic/emitters/go.py +388 -0
  27. package/python/pyeffic/emitters/kotlin.py +314 -0
  28. package/python/pyeffic/emitters/rust.py +314 -0
  29. package/python/pyeffic/emitters/zig.py +411 -0
  30. package/python/pyeffic/ffi.py +49 -0
  31. package/python/pyeffic/frontends/__init__.py +94 -0
  32. package/python/pyeffic/frontends/hybrid.py +709 -0
  33. package/python/pyeffic/frontends/typescript.py +965 -0
  34. package/python/pyeffic/ge_cli.py +1148 -0
  35. package/python/pyeffic/golden.py +348 -0
  36. package/python/pyeffic/idents.py +206 -0
  37. package/python/pyeffic/modules.py +220 -0
  38. package/python/pyeffic/packer.py +222 -0
  39. package/python/pyeffic/pipeline.py +797 -0
  40. package/python/pyeffic/reactgen.py +966 -0
  41. package/python/pyeffic/researcher.py +177 -0
  42. package/python/pyeffic/scaffold.py +397 -0
  43. package/python/pyeffic/stdlib.py +246 -0
  44. package/python/pyeffic/styling.py +220 -0
  45. package/python/pyeffic/templates/desktop_gui/README.md +106 -0
  46. package/python/pyeffic/templates/desktop_gui/app/__init__.py +0 -0
  47. package/python/pyeffic/templates/desktop_gui/app/core/__init__.py +0 -0
  48. package/python/pyeffic/templates/desktop_gui/app/core/add.ge.py +13 -0
  49. package/python/pyeffic/templates/desktop_gui/app/core/factorial.ge.py +20 -0
  50. package/python/pyeffic/templates/desktop_gui/app/core/fibonacci.ge.py +25 -0
  51. package/python/pyeffic/templates/desktop_gui/app/core/gcd.ge.py +19 -0
  52. package/python/pyeffic/templates/desktop_gui/app/core/is_prime.ge.py +24 -0
  53. package/python/pyeffic/templates/desktop_gui/app/core/multiply.ge.py +13 -0
  54. package/python/pyeffic/templates/desktop_gui/app/core/power.ge.py +25 -0
  55. package/python/pyeffic/templates/desktop_gui/app/main.ge.py +49 -0
  56. package/python/pyeffic/templates/desktop_gui/app/memory/__init__.py +0 -0
  57. package/python/pyeffic/templates/desktop_gui/app/memory/buffer.ge.py +26 -0
  58. package/python/pyeffic/templates/desktop_gui/app/memory/limits.ge.py +47 -0
  59. package/python/pyeffic/templates/desktop_gui/app/memory/state.ge.py +44 -0
  60. package/python/pyeffic/templates/desktop_gui/app/ui/__init__.py +0 -0
  61. package/python/pyeffic/templates/desktop_gui/app/ui/layout.ge.py +64 -0
  62. package/python/pyeffic/templates/desktop_gui/app/ui/render.ge.py +87 -0
  63. package/python/pyeffic/templates/desktop_gui/app/ui/theme.ge.py +147 -0
  64. package/python/pyeffic/templates/desktop_gui/app/ui/widgets.ge.py +105 -0
  65. package/python/pyeffic/templates/desktop_gui/desktop/__init__.py +1 -0
  66. package/python/pyeffic/templates/desktop_gui/desktop/main.ge.py +258 -0
  67. package/python/pyeffic/templates/desktop_gui/ge.toml +16 -0
  68. package/python/pyeffic/templates/desktop_gui/tests/__init__.py +0 -0
  69. package/python/pyeffic/templates/desktop_gui/tests/ge_loader.py +76 -0
  70. package/python/pyeffic/templates/desktop_gui/tests/test_app.py +173 -0
  71. package/python/pyeffic/templates/web_react/README.md +115 -0
  72. package/python/pyeffic/templates/web_react/app/__init__.py +0 -0
  73. package/python/pyeffic/templates/web_react/app/core/__init__.py +0 -0
  74. package/python/pyeffic/templates/web_react/app/core/add.ge.py +9 -0
  75. package/python/pyeffic/templates/web_react/app/core/factorial.ge.py +16 -0
  76. package/python/pyeffic/templates/web_react/app/core/fibonacci.ge.py +21 -0
  77. package/python/pyeffic/templates/web_react/app/core/is_prime.ge.py +20 -0
  78. package/python/pyeffic/templates/web_react/app/core/multiply.ge.py +9 -0
  79. package/python/pyeffic/templates/web_react/app/main.ge.py +25 -0
  80. package/python/pyeffic/templates/web_react/app/memory/__init__.py +0 -0
  81. package/python/pyeffic/templates/web_react/app/memory/buffer.ge.py +25 -0
  82. package/python/pyeffic/templates/web_react/app/memory/limits.ge.py +51 -0
  83. package/python/pyeffic/templates/web_react/ge.toml +23 -0
  84. package/python/pyeffic/templates/web_react/tests/__init__.py +0 -0
  85. package/python/pyeffic/templates/web_react/tests/ge_loader.py +68 -0
  86. package/python/pyeffic/templates/web_react/tests/test_app.py +105 -0
  87. package/python/pyeffic/templates/web_react/ui/main.ge.ui +33 -0
  88. package/python/pyeffic/templates/web_react/web/__init__.py +0 -0
  89. package/python/pyeffic/templates/web_react/web/server.ge.py +78 -0
  90. package/python/pyeffic/ts2py.py +657 -0
  91. package/python/pyeffic/typecheck.py +232 -0
  92. package/python/pyeffic/ui.py +154 -0
  93. package/python/pyeffic/ui_dsl.py +618 -0
  94. package/python/pyeffic/widgets.py +87 -0
  95. package/scripts/README.md +42 -0
  96. package/scripts/check-toolchains.py +85 -0
@@ -0,0 +1,797 @@
1
+ """End-to-end pipelines.
2
+
3
+ `build` — transpile to a native executable (console).
4
+ `build_flutter`— transpile logic to a C-ABI shared lib + generate a Flutter
5
+ project (Dart FFI bindings + widget UI) from the same Python.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ from .analyzer import FuncUnit, parse_source, parse_source_full, ClassUnit
13
+ from .compiler import CompileResult, compile_cpp, compile_rust, compile_csharp, compile_zig, compile_go, compile_kotlin
14
+ from .config import CompilerInfo, Config, detect_compilers
15
+ from .dartgen import generate_bindings, generate_main, generate_pubspec
16
+ from .diagnostics import ErrorReporter, report_compile_error, report_missing_entry
17
+ from .emitters import emit_cpp, emit_rust, emit_dart_functions, emit_csharp, emit_zig, emit_go, emit_kotlin
18
+ from . import ffi as ffi_mod
19
+ from .researcher import Decision, decide_program
20
+ from .typecheck import check_source
21
+ from .ui import parse_ui, collect_state
22
+
23
+
24
+ @dataclass
25
+ class UnitReport:
26
+ unit: FuncUnit
27
+ decision: Decision | None = None
28
+ emitted: str = ""
29
+ source_file: Path | None = None
30
+
31
+
32
+ @dataclass
33
+ class BuildReport:
34
+ source: Path
35
+ units: list[UnitReport] = field(default_factory=list)
36
+ program_decision: Decision | None = None
37
+ compilers: CompilerInfo | None = None
38
+ rust_program: str = ""
39
+ cpp_program: str = ""
40
+ csharp_program: str = ""
41
+ zig_program: str = ""
42
+ go_program: str = ""
43
+ kotlin_program: str = ""
44
+ rust_src: Path | None = None
45
+ cpp_src: Path | None = None
46
+ csharp_src: Path | None = None
47
+ zig_src: Path | None = None
48
+ go_src: Path | None = None
49
+ kotlin_src: Path | None = None
50
+ rust_compile: CompileResult | None = None
51
+ cpp_compile: CompileResult | None = None
52
+ csharp_compile: CompileResult | None = None
53
+ zig_compile: CompileResult | None = None
54
+ go_compile: CompileResult | None = None
55
+ kotlin_compile: CompileResult | None = None
56
+ errors: ErrorReporter = field(default_factory=ErrorReporter)
57
+ #: source frontend that produced the IR ("python" or "typescript")
58
+ frontend: str = "python"
59
+ #: functions renamed because their name is a target-language keyword
60
+ renamed_functions: dict[str, str] = field(default_factory=dict)
61
+
62
+
63
+ @dataclass
64
+ class NativeMixedReport:
65
+ """Result of a mixed-backend native build.
66
+
67
+ GE's native backends interoperate through the C ABI, so a desktop app can
68
+ put the memory-owning shell in Rust and hot code/rendering in C++ and
69
+ still ship one executable.
70
+ """
71
+ source: Path
72
+ entry: str | None = None
73
+ #: backend -> number of functions emitted
74
+ group_sizes: dict[str, int] = field(default_factory=dict)
75
+ #: backend -> emitted source path
76
+ srcs: dict[str, Path] = field(default_factory=dict)
77
+ #: companion object files linked into the executable
78
+ objects: dict[str, Path] = field(default_factory=dict)
79
+ exe: Path | None = None
80
+ compile_ok: bool = False
81
+ compile_log: str = ""
82
+ errors: ErrorReporter = field(default_factory=ErrorReporter)
83
+ frontend: str = "python"
84
+
85
+
86
+
87
+ def build(source_path: Path, cfg: Config, entry: str | None = None) -> BuildReport:
88
+ source = source_path.read_text(encoding="utf-8")
89
+ # lower non-Python source flavours (.ge hybrid, .ge.ts) before analysis
90
+ from .frontends import frontend_for
91
+ from .frontends.typescript import TypeScriptSyntaxError
92
+ from .modules import _lower_source
93
+ frontend = frontend_for(source_path)
94
+ lowered_source = source
95
+ if frontend != "python":
96
+ try:
97
+ lowered_source = _lower_source(source, source_path)
98
+ except TypeScriptSyntaxError as exc:
99
+ report = BuildReport(source=source_path, compilers=detect_compilers())
100
+ report.errors.error("GE010", str(exc), file=str(source_path.name),
101
+ line=exc.line)
102
+ return report
103
+ # resolve imports (merge imported functions into the compilation unit)
104
+ from .modules import resolve_imports, get_last_constants, get_last_preamble
105
+ units, classes, import_warnings = resolve_imports(source, source_path)
106
+ constants = get_last_constants()
107
+ preamble = get_last_preamble()
108
+ info = detect_compilers()
109
+ report = BuildReport(source=source_path, compilers=info)
110
+ report.frontend = frontend
111
+
112
+ # report import warnings
113
+ for w in import_warnings:
114
+ report.errors.warning("GE001", w, file=str(source_path.name))
115
+
116
+ # report unsupported features with structured diagnostics
117
+ src_lines = lowered_source.splitlines()
118
+ for u in units:
119
+ if not u.supported and u.unsupported_reasons:
120
+ reason = u.unsupported_reasons[0]
121
+ source_line = src_lines[u.lineno - 1] if 0 < u.lineno <= len(src_lines) else ""
122
+ # module-level code is a warning (functions still compile)
123
+ if "module-level" in reason:
124
+ report.errors.warning("GE001", f"unsupported: {reason}",
125
+ file=str(source_path.name), line=u.lineno,
126
+ source_line=source_line)
127
+ # @dart functions are intentionally not compiled to native — info only
128
+ elif "dart backend" in reason:
129
+ pass # not an error, the dart emitter handles these
130
+ # build() is a UI function, not meant for native compilation
131
+ elif u.name == "build":
132
+ pass # UI function, handled by Flutter generator
133
+ else:
134
+ report.errors.error("GE001", f"unsupported: {reason}",
135
+ file=str(source_path.name), line=u.lineno,
136
+ source_line=source_line)
137
+
138
+ # run type checker before emission (on the lowered Python form)
139
+ entry_name = entry or "main"
140
+ type_errors = check_source(lowered_source, file=str(source_path.name),
141
+ entry=entry_name, classes=classes, units=units)
142
+ for d in type_errors.diagnostics:
143
+ src_line = src_lines[d.line - 1] if 0 < d.line <= len(src_lines) else ""
144
+ if d.severity == "error":
145
+ report.errors.error(d.code, d.message, file=d.file, line=d.line,
146
+ column=d.column, source_line=src_line)
147
+ else:
148
+ report.errors.warning(d.code, d.message, file=d.file, line=d.line,
149
+ column=d.column, source_line=src_line)
150
+
151
+ supported = [u for u in units if u.supported and u.name != "build"]
152
+ prog_dec, per_fn = decide_program(units, cfg.do_research, cfg.research_timeout, cfg.force_backend)
153
+ report.program_decision = prog_dec
154
+
155
+ for u, d in zip(units, per_fn):
156
+ rep = UnitReport(unit=u, decision=d)
157
+ u.backend = d.backend
158
+ report.units.append(rep)
159
+
160
+ # Rename any function whose name is a reserved word in a target language.
161
+ # Done here, on the shared IR, so definitions, call sites and the
162
+ # cross-backend `extern "C"` declarations all agree on one symbol.
163
+ from .idents import rename_reserved_functions
164
+ active_backends = {u.backend for u in supported if u.backend}
165
+ if prog_dec and prog_dec.backend:
166
+ active_backends.add(prog_dec.backend)
167
+ renamed = rename_reserved_functions(supported, active_backends)
168
+ if renamed:
169
+ report.renamed_functions = renamed
170
+ for old_name, new_name in renamed.items():
171
+ report.errors.warning(
172
+ "GE011",
173
+ f"renamed '{old_name}' -> '{new_name}': reserved word in "
174
+ f"a target language",
175
+ file=str(source_path.name))
176
+
177
+ if not supported:
178
+ return report
179
+
180
+ # check for missing entry point
181
+ if entry:
182
+ if not any(u.name == entry for u in supported):
183
+ report.errors.error("GE008", f"entry point '{entry}' not found",
184
+ file=str(source_path.name))
185
+ return report
186
+
187
+ backend = prog_dec.backend
188
+ if backend == "rust":
189
+ prog, emitted_map = emit_rust(supported, entry, classes=classes, constants=constants, preamble=preamble)
190
+ report.rust_program = prog
191
+ report.rust_src = cfg.out("backend", "rust_main.rs")
192
+ report.rust_src.write_text(prog, encoding="utf-8")
193
+ elif backend == "cpp":
194
+ prog, emitted_map = emit_cpp(supported, entry, classes=classes, constants=constants, preamble=preamble)
195
+ report.cpp_program = prog
196
+ report.cpp_src = cfg.out("backend", "cpp_main.cpp")
197
+ report.cpp_src.write_text(prog, encoding="utf-8")
198
+ elif backend == "csharp":
199
+ prog, emitted_map = emit_csharp(supported, entry, classes=classes, constants=constants, preamble=preamble)
200
+ report.csharp_program = prog
201
+ report.csharp_src = cfg.out("backend", "csharp_main.cs")
202
+ report.csharp_src.write_text(prog, encoding="utf-8")
203
+ elif backend == "zig":
204
+ prog, emitted_map = emit_zig(supported, entry, classes=classes, constants=constants, preamble=preamble)
205
+ report.zig_program = prog
206
+ report.zig_src = cfg.out("backend", "zig_main.zig")
207
+ report.zig_src.write_text(prog, encoding="utf-8")
208
+ elif backend == "go":
209
+ prog, emitted_map = emit_go(supported, entry, classes=classes, constants=constants, preamble=preamble)
210
+ report.go_program = prog
211
+ report.go_src = cfg.out("backend", "go_main.go")
212
+ report.go_src.write_text(prog, encoding="utf-8")
213
+ elif backend == "kotlin":
214
+ prog, emitted_map = emit_kotlin(supported, entry, classes=classes, constants=constants, preamble=preamble)
215
+ report.kotlin_program = prog
216
+ report.kotlin_src = cfg.out("backend", "kotlin_main.kt")
217
+ report.kotlin_src.write_text(prog, encoding="utf-8")
218
+ else:
219
+ # default to rust
220
+ prog, emitted_map = emit_rust(supported, entry, classes=classes, constants=constants, preamble=preamble)
221
+ report.rust_program = prog
222
+ report.rust_src = cfg.out("backend", "rust_main.rs")
223
+ report.rust_src.write_text(prog, encoding="utf-8")
224
+
225
+ for u in supported:
226
+ for r in report.units:
227
+ if r.unit is u:
228
+ r.emitted = emitted_map.get(u.name, "")
229
+
230
+ # Production: report functions that became unsupported during emission
231
+ # (e.g. the emitter found a construct it couldn't translate)
232
+ for u in supported:
233
+ if not u.supported and u.unsupported_reasons:
234
+ for reason in u.unsupported_reasons:
235
+ source_line = src_lines[u.lineno - 1] if 0 < u.lineno <= len(src_lines) else ""
236
+ report.errors.error("GE009", f"emission failed: {reason}",
237
+ file=str(source_path.name), line=u.lineno,
238
+ source_line=source_line)
239
+
240
+ if report.rust_src:
241
+ report.rust_compile = compile_rust(report.rust_src, cfg, info)
242
+ if not report.rust_compile.ok:
243
+ d = report_compile_error("Rust", report.rust_compile.log)
244
+ report.errors.error(d.code, d.message, file=str(source_path.name), line=d.line)
245
+ if report.cpp_src:
246
+ report.cpp_compile = compile_cpp(report.cpp_src, cfg, info)
247
+ if not report.cpp_compile.ok:
248
+ d = report_compile_error("C++", report.cpp_compile.log)
249
+ report.errors.error(d.code, d.message, file=str(source_path.name), line=d.line)
250
+ if report.csharp_src:
251
+ report.csharp_compile = compile_csharp(report.csharp_src, cfg, info)
252
+ if not report.csharp_compile.ok:
253
+ d = report_compile_error("C#", report.csharp_compile.log)
254
+ report.errors.error(d.code, d.message, file=str(source_path.name), line=d.line)
255
+ if report.zig_src:
256
+ report.zig_compile = compile_zig(report.zig_src, cfg, info)
257
+ if not report.zig_compile.ok:
258
+ d = report_compile_error("Zig", report.zig_compile.log)
259
+ report.errors.error(d.code, d.message, file=str(source_path.name), line=d.line)
260
+ if report.go_src:
261
+ report.go_compile = compile_go(report.go_src, cfg, info)
262
+ if not report.go_compile.ok:
263
+ d = report_compile_error("Go", report.go_compile.log)
264
+ report.errors.error(d.code, d.message, file=str(source_path.name), line=d.line)
265
+ if report.kotlin_src:
266
+ report.kotlin_compile = compile_kotlin(report.kotlin_src, cfg, info)
267
+ if not report.kotlin_compile.ok:
268
+ d = report_compile_error("Kotlin", report.kotlin_compile.log)
269
+ report.errors.error(d.code, d.message, file=str(source_path.name), line=d.line)
270
+
271
+ return report
272
+
273
+
274
+ # ---------------------------------------------------------------------------
275
+ # Flutter pipeline
276
+ # ---------------------------------------------------------------------------
277
+
278
+ @dataclass
279
+ class FlutterReport:
280
+ source: Path
281
+ project_dir: Path
282
+ backend: str = "rust"
283
+ # multi-backend: one lib per native backend
284
+ lib_srcs: dict[str, Path] = field(default_factory=dict) # backend -> .rs/.cpp source
285
+ lib_binaries: dict[str, Path] = field(default_factory=dict) # backend -> .dll
286
+ lib_compiles: dict[str, CompileResult] = field(default_factory=dict) # backend -> result
287
+ # legacy single-backend fields (for backward compat)
288
+ lib_src: Path | None = None
289
+ lib_binary: Path | None = None
290
+ lib_compile: CompileResult | None = None
291
+ bindings_dart: Path | None = None
292
+ main_dart: Path | None = None
293
+ pubspec: Path | None = None
294
+ ffi_exports: list[str] = field(default_factory=list)
295
+ state_fields: list[str] = field(default_factory=list)
296
+ ui_tree: dict | None = None
297
+ errors: list[str] = field(default_factory=list)
298
+ # per-function backend mapping (function name -> backend)
299
+ func_backends: dict[str, str] = field(default_factory=dict)
300
+ # @dart functions transpiled to Dart source (merged into main.dart)
301
+ dart_source: str = ""
302
+
303
+
304
+ def build_native_mixed(source_path: Path, cfg: Config,
305
+ entry: str | None = None) -> NativeMixedReport:
306
+ """Build one native executable from a mix of backends.
307
+
308
+ The entry module (and everything it imports) is split by backend:
309
+
310
+ * Rust functions become the program's Rust source. The entry point
311
+ lives here, so this group is compiled last and produces the exe.
312
+ * Every other native backend (C++ today) is emitted in library mode
313
+ with `extern "C"` exports and compiled to an object file.
314
+ * The Rust link step pulls those objects in, so both groups end up in
315
+ one binary with no shared library to deploy.
316
+
317
+ This is what `ge build <entry> --target desktop` uses when the program
318
+ mixes backends, e.g. a Rust shell over C++ rendering.
319
+ """
320
+ import ast as _ast
321
+ from .modules import resolve_imports, get_last_constants, get_last_preamble
322
+ from .emitters import emit_cpp, emit_rust
323
+ from .frontends import frontend_for
324
+ from .frontends.typescript import TypeScriptSyntaxError
325
+
326
+ report = NativeMixedReport(source=source_path, entry=entry)
327
+ info = detect_compilers()
328
+
329
+ source = source_path.read_text(encoding="utf-8")
330
+ frontend = frontend_for(source_path)
331
+ report.frontend = frontend
332
+ if frontend != "python":
333
+ from .modules import _lower_source
334
+ try:
335
+ _lower_source(source, source_path)
336
+ except TypeScriptSyntaxError as exc:
337
+ report.errors.error("GE010", str(exc), file=str(source_path.name),
338
+ line=exc.line)
339
+ return report
340
+
341
+ units, classes, import_warnings = resolve_imports(source, source_path)
342
+ constants = get_last_constants()
343
+ preamble = get_last_preamble()
344
+ for w in import_warnings:
345
+ report.errors.warning("GE001", w, file=str(source_path.name))
346
+
347
+ # report unsupported units before doing any work
348
+ for u in units:
349
+ if not u.supported and u.unsupported_reasons:
350
+ reason = u.unsupported_reasons[0]
351
+ if u.name == "build":
352
+ continue
353
+ if "module-level" in reason:
354
+ # docstrings / ge_preamble calls at file scope are expected
355
+ report.errors.warning("GE001", f"unsupported: {reason}",
356
+ file=str(source_path.name), line=u.lineno)
357
+ continue
358
+ report.errors.error("GE001", f"unsupported: {reason}",
359
+ file=str(source_path.name), line=u.lineno)
360
+
361
+ supported = [u for u in units if u.supported and u.body is not None]
362
+
363
+ # assign a backend to every function: explicit decorator wins, otherwise
364
+ # the config default, otherwise auto-selection.
365
+ from .autoselect import select_backend
366
+ groups: dict[str, list[FuncUnit]] = {}
367
+ for u in supported:
368
+ if u.forced_backend in ("rust", "cpp", "csharp", "zig", "go", "kotlin"):
369
+ backend = u.forced_backend
370
+ elif cfg.force_backend:
371
+ backend = cfg.force_backend
372
+ elif u.name == "main":
373
+ # the entry point must live in the backend that links the exe
374
+ backend = "rust"
375
+ else:
376
+ backend, _reasons = select_backend(u)
377
+ groups.setdefault(backend, []).append(u)
378
+
379
+ if not groups:
380
+ report.errors.error("GE008", "no compilable functions found",
381
+ file=str(source_path.name))
382
+ return report
383
+
384
+ # rename reserved-word functions before any emitter runs, so the C ABI
385
+ # symbol is identical on both sides of a cross-backend call
386
+ from .idents import rename_reserved_functions
387
+ renamed = rename_reserved_functions(supported, set(groups))
388
+ if renamed:
389
+ report.renamed_functions = renamed
390
+ # grouping keys are unchanged (backend), but unit names moved
391
+ for old_name, new_name in renamed.items():
392
+ report.errors.warning(
393
+ "GE011",
394
+ f"renamed '{old_name}' -> '{new_name}': reserved word in "
395
+ f"a target language",
396
+ file=str(source_path.name))
397
+
398
+ # the executable-producing backend: prefer rust, then cpp
399
+ exe_backend = "rust" if "rust" in groups else next(iter(groups))
400
+ companion_backends = [b for b in groups if b != exe_backend]
401
+
402
+ # ---- cross-backend call analysis -----------------------------------
403
+ # A companion backend may call into the entry backend and vice versa.
404
+ # Each side needs `extern "C"` declarations for the functions it calls,
405
+ # and the callee side needs its symbols exported with C linkage.
406
+ def _called_from(group_units: list[FuncUnit],
407
+ candidates: set[str]) -> list[FuncUnit]:
408
+ """Functions from `candidates` that `group_units` actually calls."""
409
+ found: list[FuncUnit] = []
410
+ seen: set[str] = set()
411
+ for u in group_units:
412
+ for node in _ast.walk(u.body):
413
+ if isinstance(node, _ast.Call) and isinstance(node.func, _ast.Name):
414
+ fname = node.func.id
415
+ if fname in candidates and fname not in seen:
416
+ seen.add(fname)
417
+ target = name_to_unit.get(fname)
418
+ if target is not None:
419
+ found.append(target)
420
+ return found
421
+
422
+ name_to_unit = {u.name: u for u in supported}
423
+ entry_names = {u.name for u in groups.get(exe_backend, [])}
424
+ companion_names: set[str] = set()
425
+ for b in companion_backends:
426
+ companion_names.update(u.name for u in groups.get(b, []))
427
+
428
+ # Raw preamble code (ge_preamble) can also call across the boundary —
429
+ # e.g. a Rust shell's WndProc calling C++ helpers — so scan it too.
430
+ def _names_in_preamble(backend: str, candidates: set[str]) -> set[str]:
431
+ import re as _re
432
+ text = preamble.get(backend, "")
433
+ if not text:
434
+ return set()
435
+ hits: set[str] = set()
436
+ for name in candidates:
437
+ if _re.search(rf"\b{_re.escape(name)}\s*\(", text):
438
+ hits.add(name)
439
+ return hits
440
+
441
+ entry_preamble_names = _names_in_preamble(exe_backend, companion_names)
442
+
443
+ companion_calls_entry: list[FuncUnit] = []
444
+ entry_calls_companion: list[FuncUnit] = []
445
+ for b in companion_backends:
446
+ companion_preamble_names = _names_in_preamble(b, entry_names)
447
+ companion_calls_entry.extend(
448
+ name_to_unit[n] for n in companion_preamble_names
449
+ if n in name_to_unit)
450
+ companion_calls_entry.extend(
451
+ _called_from(groups.get(b, []), entry_names))
452
+ entry_calls_companion.extend(
453
+ _called_from(groups.get(exe_backend, []), companion_names))
454
+ entry_calls_companion.extend(
455
+ name_to_unit[n] for n in entry_preamble_names if n in name_to_unit)
456
+ # de-duplicate while keeping order
457
+ companion_calls_entry = list({u.name: u for u in companion_calls_entry}.values())
458
+ entry_calls_companion = list({u.name: u for u in entry_calls_companion}.values())
459
+
460
+ out_dir = cfg.backend_dir()
461
+
462
+ # ---- 1) companion backends -> object files -------------------------
463
+ for backend in companion_backends:
464
+ group = groups[backend]
465
+ report.group_sizes[backend] = len(group)
466
+ if backend == "cpp":
467
+ prog, _ = emit_cpp(group, entry=None, library_mode=True,
468
+ extern_fns=companion_calls_entry or [],
469
+ classes=classes,
470
+ constants=constants, preamble=preamble)
471
+ src = out_dir / "mixed_companion.cpp"
472
+ src.write_text(prog, encoding="utf-8")
473
+ report.srcs["cpp"] = src
474
+
475
+ if not info.cpp:
476
+ report.errors.error("GE005", "no C++ compiler found",
477
+ file=str(source_path.name))
478
+ return report
479
+ obj = cfg.obj_dir() / "mixed_companion.o"
480
+ if info.cpp_kind == "msvc":
481
+ cmd = [info.cpp, "/O2", "/EHsc", "/std:c++17", "/c",
482
+ f"/Fo{obj}", str(src)]
483
+ else:
484
+ cmd = [info.cpp, "-O2", "-std=c++17", "-w", "-c",
485
+ "-o", str(obj), str(src)]
486
+ import subprocess as _sp
487
+ r = _sp.run(cmd, capture_output=True, text=True, timeout=300)
488
+ if r.returncode != 0:
489
+ report.compile_log = (r.stdout or "") + (r.stderr or "")
490
+ report.errors.error("GE005", "C++ companion compilation failed",
491
+ file=str(src.name))
492
+ return report
493
+ report.objects["cpp"] = obj
494
+ else:
495
+ report.errors.warning(
496
+ "GE001", f"mixed build: backend '{backend}' is not yet "
497
+ f"linkable as a companion; its functions are skipped",
498
+ file=str(source_path.name))
499
+
500
+ # ---- 2) executable backend ----------------------------------------
501
+ group = groups[exe_backend]
502
+ report.group_sizes[exe_backend] = len(group)
503
+
504
+ if exe_backend == "rust":
505
+ prog, _ = emit_rust(group, entry=entry, classes=classes,
506
+ constants=constants, preamble=preamble,
507
+ extern_fns=entry_calls_companion or None,
508
+ export_fns=[u.name for u in companion_calls_entry] or None)
509
+ src = out_dir / "mixed_main.rs"
510
+ src.write_text(prog, encoding="utf-8")
511
+ report.srcs["rust"] = src
512
+
513
+ if not info.rustc:
514
+ report.errors.error("GE005", "rustc not found on PATH",
515
+ file=str(source_path.name))
516
+ return report
517
+ exe = cfg.bin_dir() / ("mixed_main.exe" if _is_windows() else "mixed_main")
518
+ cmd = [info.rustc, "-C", f"opt-level={cfg.opt_level}", "-A", "warnings",
519
+ "-o", str(exe), str(src)]
520
+ for obj in report.objects.values():
521
+ cmd += ["-C", f"link-arg={obj}"]
522
+ if _is_windows():
523
+ # Win32 GUI + audio + HTTP are used by the desktop templates
524
+ for lib in ("user32.lib", "gdi32.lib", "winmm.lib", "winhttp.lib"):
525
+ cmd += ["-C", f"link-arg={lib}"]
526
+ import subprocess as _sp
527
+ r = _sp.run(cmd, capture_output=True, text=True, timeout=600)
528
+ report.compile_log = (r.stdout or "") + (r.stderr or "")
529
+ report.compile_ok = r.returncode == 0
530
+ report.exe = exe if report.compile_ok else None
531
+ if not report.compile_ok:
532
+ report.errors.error("GE005", "Rust link failed",
533
+ file=str(src.name))
534
+ else:
535
+ report.errors.warning(
536
+ "GE001", f"mixed build: entry backend '{exe_backend}' has no "
537
+ f"executable path; nothing linked",
538
+ file=str(source_path.name))
539
+
540
+ return report
541
+
542
+
543
+ def _is_windows() -> bool:
544
+ import sys
545
+ return sys.platform == "win32"
546
+
547
+
548
+ def build_flutter(source_path: Path, cfg: Config, app_name: str = "pyeffic_app",
549
+ lib_basename: str = "pyeffic_logic") -> FlutterReport:
550
+ source = source_path.read_text(encoding="utf-8")
551
+ info = detect_compilers()
552
+ # A Flutter project is always a mobile-target artifact, whatever the
553
+ # caller's config says:
554
+ # build/mobile/<app>/backend/ native sources
555
+ # build/mobile/<app>/ui/ generated Dart UI
556
+ # build/mobile/<app>/lib/ compiled library
557
+ project_dir = cfg.out_dir / "mobile" / app_name
558
+ project_dir.mkdir(parents=True, exist_ok=True)
559
+ (project_dir / "backend").mkdir(exist_ok=True)
560
+ (project_dir / "ui").mkdir(exist_ok=True)
561
+ (project_dir / "lib").mkdir(exist_ok=True)
562
+ report = FlutterReport(source=source_path, project_dir=project_dir)
563
+
564
+ # 1) parse logic + tag FFI (with import resolution)
565
+ from .modules import resolve_imports, get_last_constants, get_last_preamble
566
+ units, classes, import_warnings = resolve_imports(source, source_path)
567
+ constants = get_last_constants()
568
+ preamble = get_last_preamble()
569
+ for w in import_warnings:
570
+ report.errors.append(w)
571
+ logic_units = [u for u in units if u.name != "build"]
572
+ ffi_mod.tag_ffi(logic_units)
573
+ supported = [u for u in logic_units if u.supported]
574
+
575
+ # 2) pick default backend for unannotated functions
576
+ prog_dec, _ = decide_program(logic_units, cfg.do_research, cfg.research_timeout,
577
+ cfg.force_backend)
578
+ report.backend = prog_dec.backend
579
+
580
+ # 3) assign backends: use forced_backend if set, else auto-select per function
581
+ from .autoselect import select_backend
582
+ for u in supported:
583
+ if u.forced_backend:
584
+ backend = u.forced_backend
585
+ elif cfg.force_backend:
586
+ backend = cfg.force_backend
587
+ else:
588
+ # auto-select best backend for this function
589
+ backend, _reasons = select_backend(u)
590
+ # all native backends (not dart — dart stays in Dart)
591
+ if backend in ("rust", "cpp", "csharp", "zig", "go", "kotlin"):
592
+ report.func_backends[u.name] = backend
593
+ u.backend = backend
594
+
595
+ # 4) split functions by backend and emit each group
596
+ backend_groups: dict[str, list] = {}
597
+ for u in supported:
598
+ b = report.func_backends.get(u.name)
599
+ if b and b in ("rust", "cpp", "csharp", "zig", "go", "kotlin"):
600
+ backend_groups.setdefault(b, []).append(u)
601
+
602
+ # detect cross-backend calls: which functions from backend A are called by backend B
603
+ import ast as _ast
604
+ all_names = {u.name: u for u in supported}
605
+ backend_name_sets = {b: {u.name for u in group} for b, group in backend_groups.items()}
606
+
607
+ # find cross-backend calls: for each backend, which functions from OTHER backends does it call?
608
+ cross_calls: dict[str, list[FuncUnit]] = {} # backend -> list of FuncUnits it calls from other backends
609
+ for backend, group_units in backend_groups.items():
610
+ called = []
611
+ other_names = set()
612
+ for other_b, names in backend_name_sets.items():
613
+ if other_b != backend:
614
+ other_names.update(names)
615
+ for u in group_units:
616
+ for node in _ast.walk(u.body):
617
+ if isinstance(node, _ast.Call) and isinstance(node.func, _ast.Name):
618
+ fname = node.func.id
619
+ if fname in other_names and all_names.get(fname):
620
+ if all_names[fname] not in called:
621
+ called.append(all_names[fname])
622
+ cross_calls[backend] = called
623
+
624
+ # for backward compat with existing Rust/C++ specific linking logic
625
+ cpp_called_from_rust = cross_calls.get("rust", [])
626
+ rust_called_from_cpp = cross_calls.get("cpp", [])
627
+
628
+ multi = len(backend_groups) > 1
629
+
630
+ # emit each backend group with cross-backend extern declarations
631
+ for backend, group_units in backend_groups.items():
632
+ lib_name = f"{lib_basename}_{backend}" if multi else lib_basename
633
+ extern_fns = cross_calls.get(backend, [])
634
+ if backend == "rust":
635
+ export_names = [u.name for u in group_units
636
+ if any(u.name in {c.name for c in cross_calls.get(b, [])}
637
+ for b in backend_groups if b != "rust")] if multi else None
638
+ prog, _ = emit_rust(group_units, entry=None, library_mode=True,
639
+ extern_fns=extern_fns if extern_fns else None,
640
+ export_fns=export_names, classes=classes, constants=constants, preamble=preamble)
641
+ src_path = project_dir / "backend" / f"{lib_name}.rs"
642
+ src_path.write_text(prog, encoding="utf-8")
643
+ report.lib_srcs["rust"] = src_path
644
+ elif backend == "cpp":
645
+ prog, _ = emit_cpp(group_units, entry=None, library_mode=True,
646
+ extern_fns=extern_fns if extern_fns else None,
647
+ classes=classes, constants=constants, preamble=preamble)
648
+ src_path = project_dir / "backend" / f"{lib_name}.cpp"
649
+ src_path.write_text(prog, encoding="utf-8")
650
+ report.lib_srcs["cpp"] = src_path
651
+ elif backend == "csharp":
652
+ prog, _ = emit_csharp(group_units, entry=None, library_mode=True,
653
+ classes=classes, constants=constants, preamble=preamble)
654
+ src_path = project_dir / "backend" / f"{lib_name}.cs"
655
+ src_path.write_text(prog, encoding="utf-8")
656
+ report.lib_srcs["csharp"] = src_path
657
+ elif backend == "zig":
658
+ prog, _ = emit_zig(group_units, entry=None, library_mode=True,
659
+ classes=classes, constants=constants, preamble=preamble)
660
+ src_path = project_dir / "backend" / f"{lib_name}.zig"
661
+ src_path.write_text(prog, encoding="utf-8")
662
+ report.lib_srcs["zig"] = src_path
663
+ elif backend == "go":
664
+ prog, _ = emit_go(group_units, entry=None, library_mode=True,
665
+ classes=classes, constants=constants, preamble=preamble)
666
+ src_path = project_dir / "backend" / f"{lib_name}.go"
667
+ src_path.write_text(prog, encoding="utf-8")
668
+ report.lib_srcs["go"] = src_path
669
+ elif backend == "kotlin":
670
+ prog, _ = emit_kotlin(group_units, entry=None, library_mode=True,
671
+ classes=classes, constants=constants, preamble=preamble)
672
+ src_path = project_dir / "backend" / f"{lib_name}.kt"
673
+ src_path.write_text(prog, encoding="utf-8")
674
+ report.lib_srcs["kotlin"] = src_path
675
+
676
+ # compile: if multi-backend, compile C++ to object file and link into Rust DLL
677
+ if multi and "rust" in backend_groups and "cpp" in backend_groups:
678
+ # compile C++ to object file
679
+ cpp_src = report.lib_srcs["cpp"]
680
+ cpp_obj = cpp_src.with_suffix(".obj" if info.cpp_kind == "msvc" else ".o")
681
+ if info.cpp_kind == "msvc":
682
+ cmd = [info.cpp, "/O2", "/EHsc", "/std:c++17", "/c",
683
+ f"/Fo:{cpp_obj}", str(cpp_src)]
684
+ else:
685
+ # on Windows, clang++ targets MSVC and doesn't support -fPIC
686
+ import os as _os
687
+ is_windows = _os.name == "nt"
688
+ cmd = [info.cpp, "-O2", "-std=c++17", "-w", "-c"]
689
+ if not is_windows:
690
+ cmd += ["-fPIC"]
691
+ cmd += ["-o", str(cpp_obj), str(cpp_src)]
692
+ import subprocess
693
+ r = subprocess.run(cmd, capture_output=True, timeout=120,
694
+ text=True, encoding="utf-8", errors="replace")
695
+ cpp_ok = r.returncode == 0
696
+ report.lib_compiles["cpp"] = CompileResult(cpp_ok, cpp_obj if cpp_ok else None,
697
+ (r.stdout or "") + (r.stderr or ""))
698
+
699
+ # compile Rust cdylib, linking the C++ object file
700
+ rust_src = report.lib_srcs["rust"]
701
+ rust_lib_name = lib_basename # single DLL: use the base name
702
+ rust_out = rust_src.parent / f"{rust_lib_name}.dll"
703
+ link_arg = str(cpp_obj) if cpp_ok else ""
704
+ cmd = [info.rustc, "-C", f"opt-level={cfg.opt_level}", "-A", "warnings",
705
+ "--crate-type", "cdylib"]
706
+ if link_arg:
707
+ cmd += ["-C", f"link-arg={link_arg}"]
708
+ cmd += ["-o", str(rust_out), str(rust_src)]
709
+ r = subprocess.run(cmd, capture_output=True, timeout=120,
710
+ text=True, encoding="utf-8", errors="replace")
711
+ rust_ok = r.returncode == 0
712
+ report.lib_compiles["rust"] = CompileResult(rust_ok, rust_out if rust_ok else None,
713
+ (r.stdout or "") + (r.stderr or ""))
714
+ if rust_ok:
715
+ report.lib_binaries["rust"] = rust_out
716
+ # single DLL — use base name for bindings
717
+ report.func_backends = {name: "rust" for name in report.func_backends}
718
+ else:
719
+ # single backend: compile normally
720
+ for backend, group_units in backend_groups.items():
721
+ lib_name = f"{lib_basename}_{backend}" if multi else lib_basename
722
+ if backend == "rust":
723
+ cr = compile_rust(report.lib_srcs["rust"], cfg, info,
724
+ shared=True, lib_basename=lib_name)
725
+ report.lib_compiles["rust"] = cr
726
+ if cr.ok and cr.exe:
727
+ report.lib_binaries["rust"] = cr.exe
728
+ elif backend == "cpp":
729
+ cr = compile_cpp(report.lib_srcs["cpp"], cfg, info,
730
+ shared=True, lib_basename=lib_name)
731
+ report.lib_compiles["cpp"] = cr
732
+ if cr.ok and cr.exe:
733
+ report.lib_binaries["cpp"] = cr.exe
734
+
735
+ # backward compat: set legacy fields from the primary backend
736
+ if report.lib_srcs:
737
+ primary = "rust" if "rust" in report.lib_srcs else "cpp"
738
+ report.lib_src = report.lib_srcs.get(primary)
739
+ report.lib_binary = report.lib_binaries.get(primary)
740
+ report.lib_compile = report.lib_compiles.get(primary)
741
+
742
+ # 5) parse UI + generate Dart
743
+ try:
744
+ tree = parse_ui(source)
745
+ report.ui_tree = tree
746
+ report.state_fields = sorted(collect_state(tree))
747
+ except Exception as e:
748
+ report.errors.append(f"UI parse failed: {e}")
749
+ tree = {"kind": "Column", "children": [
750
+ {"kind": "Text", "text": f"UI parse error: {e}", "style": "body", "state": ""}], "align": "center"}
751
+
752
+ ffi_units = [u for u in supported if getattr(u, "ffi_export", False)]
753
+ report.ffi_exports = [u.name for u in ffi_units]
754
+
755
+ # collect @dart functions and transpile them to Dart source
756
+ dart_units = [u for u in logic_units if u.forced_backend == "dart" and u.body is not None]
757
+ dart_source = ""
758
+ if dart_units:
759
+ dart_source = emit_dart_functions(dart_units)
760
+ report.dart_source = dart_source
761
+
762
+ # generate bindings with multi-backend support
763
+ # when multi-backend is linked into a single DLL, all functions load from lib_basename
764
+ if multi and "rust" in backend_groups and "cpp" in backend_groups:
765
+ # single DLL — all functions load from lib_basename
766
+ bindings = generate_bindings(ffi_units, lib_basename, None, None)
767
+ else:
768
+ lib_names = {}
769
+ for b in backend_groups:
770
+ lib_names[b] = f"{lib_basename}_{b}" if multi else lib_basename
771
+ bindings = generate_bindings(ffi_units, lib_basename, report.func_backends, lib_names)
772
+ report.bindings_dart = project_dir / "lib" / "bindings.dart"
773
+ report.bindings_dart.write_text(bindings, encoding="utf-8")
774
+ # also copy to ui/ folder for structured output
775
+ (project_dir / "ui" / "bindings.dart").write_text(bindings, encoding="utf-8")
776
+
777
+ main_dart = generate_main(tree, ffi_units, app_title=app_name,
778
+ dart_source=dart_source)
779
+ report.main_dart = project_dir / "lib" / "main.dart"
780
+ report.main_dart.write_text(main_dart, encoding="utf-8")
781
+ # also copy to ui/ folder for structured output
782
+ (project_dir / "ui" / "main.dart").write_text(main_dart, encoding="utf-8")
783
+
784
+ pubspec = generate_pubspec(app_name)
785
+ report.pubspec = project_dir / "pubspec.yaml"
786
+ report.pubspec.write_text(pubspec, encoding="utf-8")
787
+
788
+ # place the compiled libs next to the Dart project root for easy loading
789
+ for b, binary in report.lib_binaries.items():
790
+ if binary:
791
+ target = project_dir / binary.name
792
+ try:
793
+ target.write_bytes(binary.read_bytes())
794
+ except Exception:
795
+ pass
796
+
797
+ return report