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.
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/bin/ge.js +112 -0
- package/package.json +62 -0
- package/python/pyeffic/__init__.py +9 -0
- package/python/pyeffic/__main__.py +6 -0
- package/python/pyeffic/analyzer.py +464 -0
- package/python/pyeffic/apisurface.py +238 -0
- package/python/pyeffic/autoselect.py +327 -0
- package/python/pyeffic/backends.py +87 -0
- package/python/pyeffic/bench.py +233 -0
- package/python/pyeffic/cli.py +184 -0
- package/python/pyeffic/compiler.py +421 -0
- package/python/pyeffic/config.py +383 -0
- package/python/pyeffic/dartgen.py +441 -0
- package/python/pyeffic/deploy.py +586 -0
- package/python/pyeffic/diagnostics.py +194 -0
- package/python/pyeffic/difftest.py +424 -0
- package/python/pyeffic/downloader.py +307 -0
- package/python/pyeffic/emitters/__init__.py +11 -0
- package/python/pyeffic/emitters/base.py +2359 -0
- package/python/pyeffic/emitters/cpp.py +266 -0
- package/python/pyeffic/emitters/csharp.py +342 -0
- package/python/pyeffic/emitters/dart.py +349 -0
- package/python/pyeffic/emitters/go.py +388 -0
- package/python/pyeffic/emitters/kotlin.py +314 -0
- package/python/pyeffic/emitters/rust.py +314 -0
- package/python/pyeffic/emitters/zig.py +411 -0
- package/python/pyeffic/ffi.py +49 -0
- package/python/pyeffic/frontends/__init__.py +94 -0
- package/python/pyeffic/frontends/hybrid.py +709 -0
- package/python/pyeffic/frontends/typescript.py +965 -0
- package/python/pyeffic/ge_cli.py +1148 -0
- package/python/pyeffic/golden.py +348 -0
- package/python/pyeffic/idents.py +206 -0
- package/python/pyeffic/modules.py +220 -0
- package/python/pyeffic/packer.py +222 -0
- package/python/pyeffic/pipeline.py +797 -0
- package/python/pyeffic/reactgen.py +966 -0
- package/python/pyeffic/researcher.py +177 -0
- package/python/pyeffic/scaffold.py +397 -0
- package/python/pyeffic/stdlib.py +246 -0
- package/python/pyeffic/styling.py +220 -0
- package/python/pyeffic/templates/desktop_gui/README.md +106 -0
- package/python/pyeffic/templates/desktop_gui/app/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/add.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/factorial.ge.py +20 -0
- package/python/pyeffic/templates/desktop_gui/app/core/fibonacci.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/core/gcd.ge.py +19 -0
- package/python/pyeffic/templates/desktop_gui/app/core/is_prime.ge.py +24 -0
- package/python/pyeffic/templates/desktop_gui/app/core/multiply.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/power.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/main.ge.py +49 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/buffer.ge.py +26 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/limits.ge.py +47 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/state.ge.py +44 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/layout.ge.py +64 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/render.ge.py +87 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/theme.ge.py +147 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/widgets.ge.py +105 -0
- package/python/pyeffic/templates/desktop_gui/desktop/__init__.py +1 -0
- package/python/pyeffic/templates/desktop_gui/desktop/main.ge.py +258 -0
- package/python/pyeffic/templates/desktop_gui/ge.toml +16 -0
- package/python/pyeffic/templates/desktop_gui/tests/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/tests/ge_loader.py +76 -0
- package/python/pyeffic/templates/desktop_gui/tests/test_app.py +173 -0
- package/python/pyeffic/templates/web_react/README.md +115 -0
- package/python/pyeffic/templates/web_react/app/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/add.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/core/factorial.ge.py +16 -0
- package/python/pyeffic/templates/web_react/app/core/fibonacci.ge.py +21 -0
- package/python/pyeffic/templates/web_react/app/core/is_prime.ge.py +20 -0
- package/python/pyeffic/templates/web_react/app/core/multiply.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/main.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/memory/buffer.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/limits.ge.py +51 -0
- package/python/pyeffic/templates/web_react/ge.toml +23 -0
- package/python/pyeffic/templates/web_react/tests/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/tests/ge_loader.py +68 -0
- package/python/pyeffic/templates/web_react/tests/test_app.py +105 -0
- package/python/pyeffic/templates/web_react/ui/main.ge.ui +33 -0
- package/python/pyeffic/templates/web_react/web/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/web/server.ge.py +78 -0
- package/python/pyeffic/ts2py.py +657 -0
- package/python/pyeffic/typecheck.py +232 -0
- package/python/pyeffic/ui.py +154 -0
- package/python/pyeffic/ui_dsl.py +618 -0
- package/python/pyeffic/widgets.py +87 -0
- package/scripts/README.md +42 -0
- 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
|