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,421 @@
1
+ """Compile emitted Rust/C++ sources with the detected toolchain."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import sys
6
+ import subprocess
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+
10
+ from .config import CompilerInfo, Config
11
+
12
+ _EXE = ".exe" if os.name == "nt" else ""
13
+ _LIB = ".dll" if os.name == "nt" else (".so" if os.name != "darwin" else ".dylib")
14
+
15
+
16
+ @dataclass
17
+ class CompileResult:
18
+ ok: bool
19
+ exe: Path | None
20
+ log: str
21
+
22
+
23
+ # ---- Android cross-compilation ----
24
+
25
+ ANDROID_ABIS = [
26
+ ("aarch64-linux-android", "arm64-v8a", "aarch64-linux-android"),
27
+ ("armv7-linux-androideabi", "armeabi-v7a", "armv7a-linux-androideabi"),
28
+ ("x86_64-linux-android", "x86_64", "x86_64-linux-android"),
29
+ ]
30
+ ANDROID_API_LEVEL = "24" # Android 7.0+ — covers 99%+ of devices
31
+
32
+
33
+ def _find_ndk() -> Path | None:
34
+ """Find the latest Android NDK installation."""
35
+ sdk = Path(os.environ.get("LOCALAPPDATA", "")) / "Android" / "Sdk" / "ndk"
36
+ if not sdk.exists():
37
+ return None
38
+ versions = sorted(sdk.iterdir(), reverse=True)
39
+ return versions[0] if versions else None
40
+
41
+
42
+ def _ndk_linker(ndk: Path, rust_target: str) -> str:
43
+ """Get the NDK linker path for a given Rust target."""
44
+ bin_dir = ndk / "toolchains" / "llvm" / "prebuilt" / "windows-x86_64" / "bin"
45
+ # map rust target to NDK clang prefix
46
+ prefix_map = {
47
+ "aarch64-linux-android": f"aarch64-linux-android{ANDROID_API_LEVEL}",
48
+ "armv7-linux-androideabi": f"armv7a-linux-androideabi{ANDROID_API_LEVEL}",
49
+ "x86_64-linux-android": f"x86_64-linux-android{ANDROID_API_LEVEL}",
50
+ }
51
+ prefix = prefix_map.get(rust_target, rust_target)
52
+ linker = bin_dir / f"{prefix}-clang.cmd"
53
+ return str(linker) if linker.exists() else str(bin_dir / f"{prefix}-clang")
54
+
55
+
56
+ def _ndk_cpp_compiler(ndk: Path, rust_target: str) -> str:
57
+ """Get the NDK C++ compiler path for a given Rust target."""
58
+ bin_dir = ndk / "toolchains" / "llvm" / "prebuilt" / "windows-x86_64" / "bin"
59
+ prefix_map = {
60
+ "aarch64-linux-android": f"aarch64-linux-android{ANDROID_API_LEVEL}",
61
+ "armv7-linux-androideabi": f"armv7a-linux-androideabi{ANDROID_API_LEVEL}",
62
+ "x86_64-linux-android": f"x86_64-linux-android{ANDROID_API_LEVEL}",
63
+ }
64
+ prefix = prefix_map.get(rust_target, rust_target)
65
+ cpp = bin_dir / f"{prefix}-clang++.cmd"
66
+ return str(cpp) if cpp.exists() else str(bin_dir / f"{prefix}-clang++")
67
+
68
+
69
+ @dataclass
70
+ class AndroidCompileResult:
71
+ ok: bool
72
+ libs: dict[str, Path] = field(default_factory=dict) # abi -> .so path
73
+ logs: list[str] = field(default_factory=list)
74
+
75
+
76
+ def compile_rust_android(src: Path, cfg: Config, info: CompilerInfo,
77
+ lib_basename: str = "ge_logic",
78
+ out_dir: Path | None = None) -> AndroidCompileResult:
79
+ """Cross-compile a Rust source to Android .so files for all ABIs.
80
+
81
+ Requires: rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
82
+ And: Android NDK installed.
83
+ """
84
+ if not info.rustc:
85
+ return AndroidCompileResult(False, logs=["rustc not found"])
86
+ ndk = _find_ndk()
87
+ if not ndk:
88
+ return AndroidCompileResult(False, logs=["Android NDK not found in SDK"])
89
+
90
+ if out_dir is None:
91
+ out_dir = src.parent / "android_libs"
92
+ out_dir.mkdir(parents=True, exist_ok=True)
93
+
94
+ result = AndroidCompileResult(ok=True)
95
+ for rust_target, abi, _ in ANDROID_ABIS:
96
+ abi_dir = out_dir / abi
97
+ abi_dir.mkdir(exist_ok=True)
98
+ out = abi_dir / f"lib{lib_basename}.so"
99
+ linker = _ndk_linker(ndk, rust_target)
100
+ cmd = [info.rustc, "--target", rust_target,
101
+ "-C", f"opt-level={cfg.opt_level}", "-A", "warnings",
102
+ "-C", f"linker={linker}",
103
+ "--crate-type", "cdylib", "-o", str(out), str(src)]
104
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
105
+ log = (r.stdout or "") + (r.stderr or "")
106
+ if r.returncode != 0:
107
+ result.ok = False
108
+ result.logs.append(f"[{abi}] FAIL: {log[:300]}")
109
+ else:
110
+ result.libs[abi] = out
111
+ result.logs.append(f"[{abi}] OK -> {out.name} ({out.stat().st_size} bytes)")
112
+ return result
113
+
114
+
115
+ def compile_mixed_android(rust_src: Path, cpp_src: Path | None,
116
+ cfg: Config, info: CompilerInfo,
117
+ lib_basename: str = "ge_logic",
118
+ out_dir: Path | None = None) -> AndroidCompileResult:
119
+ """Cross-compile Rust + C++ to Android .so files, linked into one library.
120
+
121
+ For each ABI:
122
+ 1. Compile C++ to object file using NDK clang++
123
+ 2. Compile Rust cdylib, linking the C++ object file in
124
+ """
125
+ if not info.rustc:
126
+ return AndroidCompileResult(False, logs=["rustc not found"])
127
+ ndk = _find_ndk()
128
+ if not ndk:
129
+ return AndroidCompileResult(False, logs=["Android NDK not found in SDK"])
130
+
131
+ if out_dir is None:
132
+ out_dir = rust_src.parent / "android_libs"
133
+ out_dir.mkdir(parents=True, exist_ok=True)
134
+
135
+ result = AndroidCompileResult(ok=True)
136
+ for rust_target, abi, _ in ANDROID_ABIS:
137
+ abi_dir = out_dir / abi
138
+ abi_dir.mkdir(exist_ok=True)
139
+ out = abi_dir / f"lib{lib_basename}.so"
140
+
141
+ # 1) compile C++ to object file (if provided)
142
+ cpp_obj = None
143
+ if cpp_src and cpp_src.exists():
144
+ cpp_obj = abi_dir / f"{cpp_src.stem}.o"
145
+ cpp_compiler = _ndk_cpp_compiler(ndk, rust_target)
146
+ cpp_cmd = [cpp_compiler, "-O2", "-std=c++17", "-w", "-c",
147
+ "-o", str(cpp_obj), str(cpp_src)]
148
+ r_cpp = subprocess.run(cpp_cmd, capture_output=True, text=True,
149
+ timeout=120, encoding="utf-8", errors="replace")
150
+ if r_cpp.returncode != 0:
151
+ result.ok = False
152
+ result.logs.append(f"[{abi}] C++ FAIL: {(r_cpp.stderr or '')[:300]}")
153
+ continue
154
+
155
+ # 2) compile Rust cdylib, linking C++ object
156
+ linker = _ndk_linker(ndk, rust_target)
157
+ cmd = [info.rustc, "--target", rust_target,
158
+ "-C", f"opt-level={cfg.opt_level}", "-A", "warnings",
159
+ "-C", f"linker={linker}",
160
+ "--crate-type", "cdylib", "-o", str(out), str(rust_src)]
161
+ if cpp_obj:
162
+ cmd += ["-C", f"link-arg={cpp_obj}"]
163
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=120,
164
+ encoding="utf-8", errors="replace")
165
+ log = (r.stdout or "") + (r.stderr or "")
166
+ if r.returncode != 0:
167
+ result.ok = False
168
+ result.logs.append(f"[{abi}] Rust FAIL: {log[:300]}")
169
+ else:
170
+ result.libs[abi] = out
171
+ result.logs.append(f"[{abi}] OK -> {out.name} ({out.stat().st_size} bytes)")
172
+ return result
173
+
174
+
175
+ def compile_rust(src: Path, cfg: Config, info: CompilerInfo,
176
+ shared: bool = False, lib_basename: str = "pyeffic_logic") -> CompileResult:
177
+ if not info.rustc:
178
+ return CompileResult(False, None, "rustc not found on PATH")
179
+ # detect if source uses Win32 API (needs user32/gdi32 linking on Windows)
180
+ src_text = src.read_text(encoding="utf-8", errors="replace")
181
+ needs_win32 = ("windows.h" in src_text or "user32" in src_text
182
+ or "CreateWindow" in src_text or "WinMain" in src_text
183
+ or "GetMessage" in src_text or "WndProc" in src_text)
184
+ if shared:
185
+ out = cfg.target_dir / f"{lib_basename}{_LIB}"
186
+ cmd = [info.rustc, "-C", f"opt-level={cfg.opt_level}", "-A", "warnings",
187
+ "--crate-type", "cdylib", "-o", str(out), str(src)]
188
+ else:
189
+ out = cfg.bin_dir() / (src.stem + _EXE)
190
+ cmd = [info.rustc, "-C", f"opt-level={cfg.opt_level}", "-A", "warnings",
191
+ "-o", str(out), str(src)]
192
+ # link Windows GUI libraries when Win32 API is used
193
+ if needs_win32 and sys.platform == "win32":
194
+ cmd += ["-C", "link-arg=user32.lib", "-C", "link-arg=gdi32.lib"]
195
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
196
+ log = (r.stdout or "") + (r.stderr or "")
197
+ if r.returncode != 0:
198
+ return CompileResult(False, None, log)
199
+ return CompileResult(True, out, log)
200
+
201
+
202
+ def compile_cpp(src: Path, cfg: Config, info: CompilerInfo,
203
+ shared: bool = False, lib_basename: str = "pyeffic_logic") -> CompileResult:
204
+ if not info.cpp:
205
+ return CompileResult(False, None, "no C++ compiler (g++/clang++/cl) found on PATH")
206
+ # detect if source uses Win32 API (needs user32/gdi32 linking)
207
+ src_text = src.read_text(encoding="utf-8", errors="replace")
208
+ needs_win32 = ("#include <windows.h>" in src_text or "#include <wingdi.h>" in src_text
209
+ or "WinMain" in src_text or "CreateWindow" in src_text)
210
+ if shared:
211
+ out = cfg.target_dir / f"{lib_basename}{_LIB}"
212
+ if info.cpp_kind == "msvc":
213
+ cmd = [info.cpp, f"/O{cfg.opt_level}", "/EHsc", "/std:c++17", "/LD",
214
+ f"/Fe:{out}", str(src)]
215
+ if needs_win32:
216
+ cmd[-1:-1] = ["user32.lib", "gdi32.lib"]
217
+ else:
218
+ cmd = [info.cpp, f"-O{cfg.opt_level}", "-std=c++17", "-w", "-shared",
219
+ "-fPIC", "-o", str(out), str(src)]
220
+ if needs_win32:
221
+ cmd.extend(["-luser32", "-lgdi32"])
222
+ else:
223
+ out = cfg.bin_dir() / (src.stem + _EXE)
224
+ if info.cpp_kind == "msvc":
225
+ cmd = [info.cpp, f"/O{cfg.opt_level}", "/EHsc", "/std:c++17",
226
+ f"/Fe:{out}", str(src)]
227
+ if needs_win32:
228
+ cmd[-1:-1] = ["user32.lib", "gdi32.lib"]
229
+ else:
230
+ cmd = [info.cpp, f"-O{cfg.opt_level}", "-std=c++17", "-w",
231
+ "-o", str(out), str(src)]
232
+ if needs_win32:
233
+ cmd.extend(["-luser32", "-lgdi32"])
234
+ cmd = [c for c in cmd if c] # remove empty strings
235
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
236
+ log = (r.stdout or "") + (r.stderr or "")
237
+ if r.returncode != 0:
238
+ return CompileResult(False, None, log)
239
+ return CompileResult(True, out, log)
240
+
241
+
242
+ # ---- C# compilation via .NET NativeAOT ----
243
+
244
+ def compile_csharp(src: Path, cfg: Config, info: CompilerInfo,
245
+ shared: bool = False, lib_basename: str = "pyeffic_logic") -> CompileResult:
246
+ """Compile C# source to a native library via .NET NativeAOT.
247
+
248
+ Requires .NET 8+ SDK with NativeAOT workload. Produces a shared library
249
+ that exports C ABI functions via [UnmanagedCallersOnly].
250
+ """
251
+ if not info.dotnet:
252
+ return CompileResult(False, None, "dotnet not found on PATH (need .NET 8+ SDK)")
253
+
254
+ # Create a temporary project for NativeAOT compilation
255
+ project_dir = src.parent / f"{lib_basename}_csharp_project"
256
+ project_dir.mkdir(parents=True, exist_ok=True)
257
+
258
+ # write csproj
259
+ csproj = project_dir / f"{lib_basename}.csproj"
260
+ output_type = "Library" if shared else "Exe"
261
+ # Use NativeAOT only for shared libraries (FFI); for executables, use regular build
262
+ # NativeAOT requires the MSVC C++ workload which may not be installed
263
+ publish_aot = "true" if shared else "false"
264
+ csproj.write_text(
265
+ f"""<Project Sdk="Microsoft.NET.Sdk">
266
+ <PropertyGroup>
267
+ <TargetFramework>net8.0</TargetFramework>
268
+ <Nullable>enable</Nullable>
269
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
270
+ <PublishAot>{publish_aot}</PublishAot>
271
+ <OutputType>{output_type}</OutputType>
272
+ <AssemblyName>{lib_basename}</AssemblyName>
273
+ </PropertyGroup>
274
+ </Project>
275
+ """, encoding="utf-8")
276
+
277
+ # copy source into project
278
+ cs_file = project_dir / f"{lib_basename}.cs"
279
+ cs_file.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
280
+
281
+ if shared:
282
+ out = cfg.target_dir / f"{lib_basename}{_LIB}"
283
+ cmd = [info.dotnet, "publish", str(csproj.resolve()), "-c", "Release",
284
+ "-r", _get_rid(), "-o", str((project_dir / "publish").resolve()),
285
+ "/p:NativeLib=Shared", "/p:SelfContained=true"]
286
+ else:
287
+ out = project_dir / "publish" / f"{lib_basename}{_EXE}"
288
+ cmd = [info.dotnet, "publish", str(csproj.resolve()), "-c", "Release",
289
+ "-o", str((project_dir / "publish").resolve())]
290
+
291
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=300,
292
+ cwd=str(project_dir))
293
+ log = (r.stdout or "") + (r.stderr or "")
294
+ if r.returncode != 0:
295
+ return CompileResult(False, None, log)
296
+
297
+ # find the produced binary
298
+ publish_dir = project_dir / "publish"
299
+ if shared:
300
+ produced = list(publish_dir.glob(f"{lib_basename}*{_LIB}"))
301
+ if not produced:
302
+ produced = list(publish_dir.glob(f"{lib_basename}*"))
303
+ if produced:
304
+ import shutil as sh
305
+ sh.copy2(produced[0], out)
306
+ return CompileResult(True, out, log)
307
+ return CompileResult(False, None, f"published but no library found in {publish_dir}\n{log}")
308
+ else:
309
+ exe = publish_dir / f"{lib_basename}{_EXE}"
310
+ if exe.exists():
311
+ return CompileResult(True, exe, log)
312
+ return CompileResult(False, None, f"published but no exe found in {publish_dir}\n{log}")
313
+
314
+
315
+ def _get_rid() -> str:
316
+ """Get the .NET Runtime Identifier for the current platform."""
317
+ import platform
318
+ sys_name = platform.system()
319
+ machine = platform.machine().lower()
320
+ if sys_name == "Windows":
321
+ return "win-x64" if "64" in machine else "win-x86"
322
+ if sys_name == "Darwin":
323
+ return "osx-arm64" if "arm" in machine or "aarch64" in machine else "osx-x64"
324
+ return "linux-x64" if "64" in machine else "linux-x86"
325
+
326
+
327
+ # ---- Zig compilation ----
328
+
329
+ def compile_zig(src: Path, cfg: Config, info: CompilerInfo,
330
+ shared: bool = False, lib_basename: str = "pyeffic_logic") -> CompileResult:
331
+ """Compile Zig source to a native binary or shared library.
332
+
333
+ Zig compiles with no runtime and exports C ABI functions natively.
334
+ """
335
+ if not info.zig:
336
+ return CompileResult(False, None, "zig not found on PATH")
337
+
338
+ # Zig uses ReleaseFast/ReleaseSafe/ReleaseSmall, not -O3
339
+ opt_mode = "ReleaseFast" if cfg.opt_level in ("3", "2") else "Debug"
340
+
341
+ if shared:
342
+ out = cfg.target_dir / f"{lib_basename}{_LIB}"
343
+ cmd = [info.zig, "build-lib", str(src), "-dynamic", "-O", opt_mode,
344
+ "-femit-bin=" + str(out)]
345
+ else:
346
+ out = cfg.bin_dir() / (src.stem + _EXE)
347
+ cmd = [info.zig, "build-exe", str(src), "-O", opt_mode,
348
+ "-femit-bin=" + str(out)]
349
+
350
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
351
+ log = (r.stdout or "") + (r.stderr or "")
352
+ if r.returncode != 0:
353
+ return CompileResult(False, None, log)
354
+ return CompileResult(True, out, log)
355
+
356
+
357
+ # ---- Go compilation (cgo shared library) ----
358
+
359
+ def compile_go(src: Path, cfg: Config, info: CompilerInfo,
360
+ shared: bool = False, lib_basename: str = "pyeffic_logic") -> CompileResult:
361
+ """Compile Go source to a native shared library via cgo.
362
+
363
+ Go exports C ABI functions with //export directive. Requires Go 1.21+.
364
+ """
365
+ if not info.go:
366
+ return CompileResult(False, None, "go not found on PATH")
367
+
368
+ # Go 1.16+ requires a go.mod file for module mode
369
+ go_mod = src.parent / "go.mod"
370
+ if not go_mod.exists():
371
+ go_mod.write_text("module pyeffic\n\ngo 1.21\n", encoding="utf-8")
372
+
373
+ if shared:
374
+ out = cfg.target_dir / f"{lib_basename}{_LIB}"
375
+ env = os.environ.copy()
376
+ env["CGO_ENABLED"] = "1"
377
+ # full output path: go runs with cwd=src.parent, so a bare name would
378
+ # land next to the source instead of in the target directory
379
+ cmd = [info.go, "build", "-buildmode=c-shared",
380
+ "-o", str(out.resolve()), str(src.resolve())]
381
+ else:
382
+ out = cfg.bin_dir() / (src.stem + _EXE)
383
+ env = os.environ.copy()
384
+ cmd = [info.go, "build", "-o", str(out.resolve()), str(src.resolve())]
385
+
386
+ # Go requires running from the directory containing the source
387
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=120, env=env,
388
+ cwd=str(src.parent))
389
+ log = (r.stdout or "") + (r.stderr or "")
390
+ if r.returncode != 0:
391
+ return CompileResult(False, None, log)
392
+ return CompileResult(True, out, log)
393
+
394
+
395
+ # ---- Kotlin compilation (Kotlin/Native) ----
396
+
397
+ def compile_kotlin(src: Path, cfg: Config, info: CompilerInfo,
398
+ shared: bool = False, lib_basename: str = "pyeffic_logic") -> CompileResult:
399
+ """Compile Kotlin source to a native library via Kotlin/Native.
400
+
401
+ Kotlin/Native compiles Kotlin to a native shared library with @CName
402
+ exports for C ABI FFI. Requires Kotlin compiler (kotlinc) with native target.
403
+ """
404
+ if not info.kotlinc:
405
+ return CompileResult(False, None, "kotlinc not found on PATH")
406
+
407
+ if shared:
408
+ out = cfg.target_dir / f"{lib_basename}{_LIB}"
409
+ # Kotlin/Native produces shared libraries with -produce dynamic
410
+ cmd = [info.kotlinc, "-produce", "dynamic", "-opt",
411
+ "-output", str(out), str(src)]
412
+ else:
413
+ out = cfg.bin_dir() / (src.stem + _EXE)
414
+ cmd = [info.kotlinc, "-produce", "program", "-opt",
415
+ "-output", str(out), str(src)]
416
+
417
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
418
+ log = (r.stdout or "") + (r.stderr or "")
419
+ if r.returncode != 0:
420
+ return CompileResult(False, None, log)
421
+ return CompileResult(True, out, log)