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,177 @@
|
|
|
1
|
+
"""Backend selection: web research + static heuristics.
|
|
2
|
+
|
|
3
|
+
For each function the researcher builds a query describing the workload, runs a
|
|
4
|
+
real web search (DuckDuckGo HTML endpoint, no API key required), and scores
|
|
5
|
+
C++ vs Rust from the snippets. Static heuristics are always computed as a
|
|
6
|
+
reliable backbone; web results nudge the score. If the network is unavailable
|
|
7
|
+
or `do_research` is off, heuristics alone decide.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from .analyzer import FuncUnit
|
|
17
|
+
|
|
18
|
+
# Static heuristic weights. Positive => favors Rust, negative => favors C++.
|
|
19
|
+
HEURISTICS: dict[str, int] = {
|
|
20
|
+
"numeric_loop": +2, # Rust bounds-checked SIMD-friendly loops
|
|
21
|
+
"arithmetic": +1,
|
|
22
|
+
"indexing": +1, # Rust safe indexing
|
|
23
|
+
"list_alloc": +1, # Vec is ergonomic
|
|
24
|
+
"list_append": +1,
|
|
25
|
+
"container_iter": 0,
|
|
26
|
+
"math_builtin": 0,
|
|
27
|
+
"len_builtin": 0,
|
|
28
|
+
"comparison": 0,
|
|
29
|
+
"while_loop": 0,
|
|
30
|
+
"io_print": -1, # C++ iostream often lighter for trivial IO
|
|
31
|
+
"class_use": -2, # C++ OOP ergonomics
|
|
32
|
+
"plain": 0,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
RUST_POS = re.compile(r"\b(rust)\b", re.I)
|
|
36
|
+
CPP_POS = re.compile(r"\b(c\+\+|cpp|std::)\b", re.I)
|
|
37
|
+
RUST_PERF = re.compile(r"rust.{0,40}(faster|safer|zero.cost|simd|bounds)", re.I)
|
|
38
|
+
CPP_PERF = re.compile(r"(c\+\+|cpp).{0,40}(faster|stl|template|simd|legacy)", re.I)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Decision:
|
|
43
|
+
backend: str # "rust" | "cpp"
|
|
44
|
+
rust_score: float
|
|
45
|
+
cpp_score: float
|
|
46
|
+
reasons: list[str]
|
|
47
|
+
web_used: bool
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _query_for(unit: FuncUnit) -> str:
|
|
51
|
+
feats = sorted(unit.features)
|
|
52
|
+
keywords = {
|
|
53
|
+
"numeric_loop": "numeric loop SIMD",
|
|
54
|
+
"arithmetic": "arithmetic kernel",
|
|
55
|
+
"indexing": "array indexing",
|
|
56
|
+
"list_alloc": "dynamic array allocation",
|
|
57
|
+
"list_append": "vector append",
|
|
58
|
+
"container_iter": "container iteration",
|
|
59
|
+
"math_builtin": "math builtins",
|
|
60
|
+
"io_print": "console IO",
|
|
61
|
+
"class_use": "object oriented class",
|
|
62
|
+
"comparison": "comparisons",
|
|
63
|
+
"while_loop": "while loop",
|
|
64
|
+
"plain": "plain function",
|
|
65
|
+
}
|
|
66
|
+
terms = [keywords.get(f, f) for f in feats[:3]]
|
|
67
|
+
return "rust vs c++ performance " + " ".join(terms)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _ddg_search(query: str, timeout: float) -> list[str]:
|
|
71
|
+
"""Fetch DuckDuckGo lite results and return snippet strings.
|
|
72
|
+
|
|
73
|
+
Uses the lite endpoint (plain HTML, no JS) with a POST request, which is
|
|
74
|
+
the most scraping-friendly surface DuckDuckGo offers.
|
|
75
|
+
"""
|
|
76
|
+
data = urllib.parse.urlencode({"q": query, "kl": "us-en"}).encode()
|
|
77
|
+
req = urllib.request.Request(
|
|
78
|
+
"https://lite.duckduckgo.com/lite/",
|
|
79
|
+
data=data,
|
|
80
|
+
headers={
|
|
81
|
+
"User-Agent": "Mozilla/5.0 (pyeffic/0.1)",
|
|
82
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
try:
|
|
86
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
87
|
+
html = r.read().decode("utf-8", errors="ignore")
|
|
88
|
+
except Exception:
|
|
89
|
+
return []
|
|
90
|
+
# snippets live in <td class='result-snippet'>...</td>
|
|
91
|
+
snippets = re.findall(r"<td class='result-snippet'>(.*?)</td>", html, re.S)
|
|
92
|
+
clean = [re.sub(r"<[^>]+>", " ", s) for s in snippets]
|
|
93
|
+
return [s.strip() for s in clean if s.strip()][:8]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def decide(unit: FuncUnit, do_research: bool, timeout: float, force: str | None) -> Decision:
|
|
97
|
+
if force in ("rust", "cpp", "csharp", "zig", "go", "kotlin"):
|
|
98
|
+
return Decision(backend=force, rust_score=0, cpp_score=0,
|
|
99
|
+
reasons=[f"forced backend={force}"], web_used=False)
|
|
100
|
+
|
|
101
|
+
# Use the new auto-selector for intelligent multi-backend selection
|
|
102
|
+
from .autoselect import select_backend
|
|
103
|
+
backend, reasons = select_backend(unit)
|
|
104
|
+
|
|
105
|
+
web_used = False
|
|
106
|
+
if do_research:
|
|
107
|
+
q = _query_for(unit)
|
|
108
|
+
snippets = _ddg_search(q, timeout)
|
|
109
|
+
if snippets:
|
|
110
|
+
web_used = True
|
|
111
|
+
# web research can adjust the selection
|
|
112
|
+
r_hits = sum(1 for s in snippets if RUST_POS.search(s))
|
|
113
|
+
c_hits = sum(1 for s in snippets if CPP_POS.search(s))
|
|
114
|
+
r_perf = sum(1 for s in snippets if RUST_PERF.search(s))
|
|
115
|
+
c_perf = sum(1 for s in snippets if CPP_PERF.search(s))
|
|
116
|
+
reasons.append(f"web: rust_hits={r_hits} perf={r_perf}, cpp_hits={c_hits} perf={c_perf} (query='{q}')")
|
|
117
|
+
# if web research strongly favors C++ over Rust, override
|
|
118
|
+
if c_hits + c_perf > r_hits + r_perf + 2 and backend == "rust":
|
|
119
|
+
backend = "cpp"
|
|
120
|
+
reasons.append(f"web override: cpp favored -> {backend}")
|
|
121
|
+
else:
|
|
122
|
+
reasons.append("web: no results, auto-select only")
|
|
123
|
+
|
|
124
|
+
return Decision(backend=backend, rust_score=0, cpp_score=0, reasons=reasons, web_used=web_used)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def decide_program(units: list[FuncUnit], do_research: bool, timeout: float,
|
|
128
|
+
force: str | None) -> tuple[Decision, list[Decision]]:
|
|
129
|
+
"""Pick ONE backend for the whole program (so the call graph stays intact).
|
|
130
|
+
|
|
131
|
+
Returns (program_decision, per_function_decisions_for_reporting).
|
|
132
|
+
Uses the auto-selector to pick the best backend for the whole program
|
|
133
|
+
based on aggregated function characteristics.
|
|
134
|
+
"""
|
|
135
|
+
per_fn: list[Decision] = []
|
|
136
|
+
any_web = False
|
|
137
|
+
all_reasons: list[str] = []
|
|
138
|
+
# aggregate backend scores across all functions
|
|
139
|
+
from .autoselect import score_backend, DEFAULT_PREFERENCE
|
|
140
|
+
agg_scores = {b: 0.0 for b in DEFAULT_PREFERENCE}
|
|
141
|
+
|
|
142
|
+
for u in units:
|
|
143
|
+
if not u.supported:
|
|
144
|
+
per_fn.append(Decision("cpython", 0, 0, ["unsupported -> cpython fallback"], False))
|
|
145
|
+
continue
|
|
146
|
+
d = decide(u, do_research, timeout, force)
|
|
147
|
+
per_fn.append(d)
|
|
148
|
+
any_web = any_web or d.web_used
|
|
149
|
+
all_reasons.append(f"[{u.name}] " + "; ".join(d.reasons))
|
|
150
|
+
# aggregate scores from auto-selector
|
|
151
|
+
if not force:
|
|
152
|
+
scores = score_backend(u)
|
|
153
|
+
for b in DEFAULT_PREFERENCE:
|
|
154
|
+
agg_scores[b] += scores[b].score
|
|
155
|
+
|
|
156
|
+
if force in ("rust", "cpp", "csharp", "zig", "go", "kotlin"):
|
|
157
|
+
prog = Decision(force, 0, 0, [f"forced backend={force}"], False)
|
|
158
|
+
else:
|
|
159
|
+
# pick the backend with the highest aggregate score
|
|
160
|
+
best_backend = "rust"
|
|
161
|
+
best_score = 0.0
|
|
162
|
+
for b in DEFAULT_PREFERENCE:
|
|
163
|
+
if agg_scores[b] > best_score:
|
|
164
|
+
best_score = agg_scores[b]
|
|
165
|
+
best_backend = b
|
|
166
|
+
if best_score <= 0:
|
|
167
|
+
best_backend = "rust" # default tie-break
|
|
168
|
+
score_summary = ", ".join(f"{b}={agg_scores[b]:.1f}" for b in DEFAULT_PREFERENCE
|
|
169
|
+
if agg_scores[b] != 0)
|
|
170
|
+
prog = Decision(
|
|
171
|
+
backend=best_backend,
|
|
172
|
+
rust_score=agg_scores.get("rust", 0),
|
|
173
|
+
cpp_score=agg_scores.get("cpp", 0),
|
|
174
|
+
reasons=[f"auto-select aggregate: {score_summary} -> {best_backend}"] + all_reasons,
|
|
175
|
+
web_used=any_web,
|
|
176
|
+
)
|
|
177
|
+
return prog, per_fn
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""GE project scaffolding — `ge create` command.
|
|
2
|
+
|
|
3
|
+
Generates a properly structured GE project with:
|
|
4
|
+
app/ — main application entry (shared logic)
|
|
5
|
+
desktop/ — desktop platform wrapper (Windows/Linux/macOS)
|
|
6
|
+
web/ — web platform wrapper
|
|
7
|
+
mobile/ — mobile platform wrapper (Android/iOS)
|
|
8
|
+
backend/ — native backend source (Rust/C++/C#/Zig/Go/Kotlin)
|
|
9
|
+
ui/ — .ge.ui declarative UI definitions
|
|
10
|
+
tests/ — test scripts
|
|
11
|
+
|
|
12
|
+
The user is prompted for:
|
|
13
|
+
1. App name
|
|
14
|
+
2. Platforms (desktop/web/mobile/all) — default: all
|
|
15
|
+
3. Backend language (all/rust/cpp/csharp/zig/go/kotlin) — default: all
|
|
16
|
+
4. UI backend (all/dart) — default: all (dart only for now)
|
|
17
|
+
|
|
18
|
+
Non-interactive mode is supported via CLI flags.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
ALL_BACKENDS = ["rust", "cpp", "csharp", "zig", "go", "kotlin"]
|
|
26
|
+
ALL_PLATFORMS = ["desktop", "web", "mobile"]
|
|
27
|
+
#: Template name -> directory under pyeffic/templates/
|
|
28
|
+
TEMPLATE_DIRS = {
|
|
29
|
+
"desktop-gui": "desktop_gui",
|
|
30
|
+
"web-react": "web_react",
|
|
31
|
+
}
|
|
32
|
+
ALL_TEMPLATES = ["default"] + list(TEMPLATE_DIRS)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _sanitize_name(name: str) -> str:
|
|
36
|
+
"""Convert app name to a valid package name (lowercase, underscores)."""
|
|
37
|
+
s = re.sub(r"[^a-zA-Z0-9_]", "_", name.strip())
|
|
38
|
+
if s and s[0].isdigit():
|
|
39
|
+
s = "_" + s
|
|
40
|
+
return s.lower() or "ge_app"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _prompt(message: str, default: str, choices: list[str] | None = None) -> str:
|
|
44
|
+
"""Interactive prompt with default and optional choices."""
|
|
45
|
+
hint = f" [{default}]" if default else ""
|
|
46
|
+
choice_hint = ""
|
|
47
|
+
if choices:
|
|
48
|
+
choice_hint = f" ({'/'.join(choices)})"
|
|
49
|
+
while True:
|
|
50
|
+
val = input(f"{message}{choice_hint}{hint}: ").strip()
|
|
51
|
+
if not val:
|
|
52
|
+
return default
|
|
53
|
+
if choices and val not in choices:
|
|
54
|
+
print(f" Invalid choice. Options: {', '.join(choices)}")
|
|
55
|
+
continue
|
|
56
|
+
return val
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _prompt_multi(message: str, default: str, choices: list[str]) -> list[str]:
|
|
60
|
+
"""Prompt for comma-separated multi-select. 'all' selects everything."""
|
|
61
|
+
while True:
|
|
62
|
+
val = input(f"{message} (choices: {', '.join(choices)}, or 'all') [{default}]: ").strip()
|
|
63
|
+
if not val:
|
|
64
|
+
val = default
|
|
65
|
+
if val == "all":
|
|
66
|
+
return choices[:]
|
|
67
|
+
parts = [p.strip() for p in val.split(",") if p.strip()]
|
|
68
|
+
invalid = [p for p in parts if p not in choices]
|
|
69
|
+
if invalid:
|
|
70
|
+
print(f" Invalid: {', '.join(invalid)}. Options: {', '.join(choices)}")
|
|
71
|
+
continue
|
|
72
|
+
return parts
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Template files
|
|
76
|
+
|
|
77
|
+
_APP_MAIN_TEMPLATE = '''"""GE application — {app_name}
|
|
78
|
+
|
|
79
|
+
This is the main application logic. Platform-specific code (desktop/web/mobile)
|
|
80
|
+
imports from here. Write your core GE functions in this file.
|
|
81
|
+
"""
|
|
82
|
+
from __future__ import annotations
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def greet(name: str) -> str:
|
|
86
|
+
return f"Hello, {{name}}! Welcome to {app_name}."
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main() -> None:
|
|
90
|
+
print(greet("World"))
|
|
91
|
+
'''
|
|
92
|
+
|
|
93
|
+
_DESKTOP_MAIN_TEMPLATE = '''"""Desktop entry point for {app_name}.
|
|
94
|
+
|
|
95
|
+
Builds the desktop application using the shared app logic.
|
|
96
|
+
Run with: ge build desktop/main.ge.py --run
|
|
97
|
+
"""
|
|
98
|
+
from __future__ import annotations
|
|
99
|
+
from app.main import greet, main
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def desktop_main() -> None:
|
|
103
|
+
print("=== {app_name} Desktop ===")
|
|
104
|
+
main()
|
|
105
|
+
'''
|
|
106
|
+
|
|
107
|
+
_WEB_MAIN_TEMPLATE = '''"""Web entry point for {app_name}.
|
|
108
|
+
|
|
109
|
+
Builds the web application using the shared app logic.
|
|
110
|
+
Run with: ge build web/main.ge.py
|
|
111
|
+
"""
|
|
112
|
+
from __future__ import annotations
|
|
113
|
+
from app.main import greet, main
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def web_main() -> None:
|
|
117
|
+
print("=== {app_name} Web ===")
|
|
118
|
+
main()
|
|
119
|
+
'''
|
|
120
|
+
|
|
121
|
+
_MOBILE_MAIN_TEMPLATE = '''"""Mobile entry point for {app_name}.
|
|
122
|
+
|
|
123
|
+
Builds the mobile (Android/iOS) application using the shared app logic.
|
|
124
|
+
Run with: ge flutter mobile/main.ge.py --app-name {app_name}
|
|
125
|
+
"""
|
|
126
|
+
from __future__ import annotations
|
|
127
|
+
from app.main import greet, main
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def mobile_main() -> None:
|
|
131
|
+
print("=== {app_name} Mobile ===")
|
|
132
|
+
main()
|
|
133
|
+
'''
|
|
134
|
+
|
|
135
|
+
_UI_TEMPLATE = '''"""GE UI definition for {app_name}.
|
|
136
|
+
|
|
137
|
+
Define your UI declaratively using the .ge.ui DSL.
|
|
138
|
+
This file is parsed by the GE UI parser and generates Flutter widgets.
|
|
139
|
+
"""
|
|
140
|
+
Window {{
|
|
141
|
+
title: "{app_name}"
|
|
142
|
+
Column {{
|
|
143
|
+
Text {{ text: "Welcome to {app_name}", style: "headline" }}
|
|
144
|
+
Text {{ text: "Built with GE", style: "body" }}
|
|
145
|
+
}}
|
|
146
|
+
}}
|
|
147
|
+
'''
|
|
148
|
+
|
|
149
|
+
_BACKEND_TEMPLATE = '''"""GE backend functions for {app_name}.
|
|
150
|
+
|
|
151
|
+
These functions are compiled to native code via the selected backend(s).
|
|
152
|
+
Use @rust, @cpp, @csharp, @zig, @go, or @kotlin decorators to force a backend.
|
|
153
|
+
Without a decorator, GE auto-selects the best backend.
|
|
154
|
+
"""
|
|
155
|
+
from __future__ import annotations
|
|
156
|
+
from pyeffic.backends import rust, cpp, csharp, zig, go, kotlin
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def compute_sum(a: int, b: int) -> int:
|
|
160
|
+
return a + b
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def compute_product(a: int, b: int) -> int:
|
|
164
|
+
return a * b
|
|
165
|
+
'''
|
|
166
|
+
|
|
167
|
+
_TEST_TEMPLATE = '''"""Tests for {app_name}.
|
|
168
|
+
|
|
169
|
+
Run with: python -m unittest discover tests
|
|
170
|
+
"""
|
|
171
|
+
import unittest
|
|
172
|
+
import sys
|
|
173
|
+
from pathlib import Path
|
|
174
|
+
|
|
175
|
+
# Add project root to path
|
|
176
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TestAppFunctions(unittest.TestCase):
|
|
180
|
+
def test_greet(self):
|
|
181
|
+
from app.main import greet
|
|
182
|
+
self.assertEqual(greet("World"), "Hello, World! Welcome to {app_name}.")
|
|
183
|
+
|
|
184
|
+
def test_compute_sum(self):
|
|
185
|
+
from backend.main import compute_sum
|
|
186
|
+
self.assertEqual(compute_sum(3, 4), 7)
|
|
187
|
+
|
|
188
|
+
def test_compute_product(self):
|
|
189
|
+
from backend.main import compute_product
|
|
190
|
+
self.assertEqual(compute_product(3, 4), 12)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
unittest.main()
|
|
195
|
+
'''
|
|
196
|
+
|
|
197
|
+
_GE_CONFIG_TEMPLATE = '''# GE project configuration for {app_name}
|
|
198
|
+
[project]
|
|
199
|
+
name = "{app_name}"
|
|
200
|
+
version = "0.1.0"
|
|
201
|
+
description = "A GE application"
|
|
202
|
+
|
|
203
|
+
[build]
|
|
204
|
+
# Default backend for all functions (auto selects best per function)
|
|
205
|
+
backend = "auto"
|
|
206
|
+
# Platforms to build for
|
|
207
|
+
platforms = [{platforms}]
|
|
208
|
+
|
|
209
|
+
[backends]
|
|
210
|
+
# Which backends to generate code for (default: all)
|
|
211
|
+
languages = [{backends}]
|
|
212
|
+
|
|
213
|
+
[ui]
|
|
214
|
+
# UI backend (dart for Flutter)
|
|
215
|
+
backend = "dart"
|
|
216
|
+
'''
|
|
217
|
+
|
|
218
|
+
_README_TEMPLATE = '''# {app_name}
|
|
219
|
+
|
|
220
|
+
A GE application built with the [GE programming language](https://github.com/user/ge).
|
|
221
|
+
|
|
222
|
+
## Structure
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
{app_name}/
|
|
226
|
+
app/ — main application logic (shared)
|
|
227
|
+
desktop/ — desktop platform entry
|
|
228
|
+
web/ — web platform entry
|
|
229
|
+
mobile/ — mobile platform entry
|
|
230
|
+
backend/ — native backend functions
|
|
231
|
+
ui/ — .ge.ui declarative UI definitions
|
|
232
|
+
tests/ — test scripts
|
|
233
|
+
ge.toml — GE project config
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Build
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Build for desktop
|
|
240
|
+
ge build desktop/main.ge.py --run
|
|
241
|
+
|
|
242
|
+
# Build for web
|
|
243
|
+
ge build web/main.ge.py
|
|
244
|
+
|
|
245
|
+
# Build Flutter app (mobile/desktop)
|
|
246
|
+
ge flutter mobile/main.ge.py --app-name {app_name}
|
|
247
|
+
|
|
248
|
+
# Build all backends
|
|
249
|
+
ge build app/main.ge.py --backend auto
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Test
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
python -m unittest discover tests
|
|
256
|
+
```
|
|
257
|
+
'''
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def create_project(
|
|
261
|
+
name: str,
|
|
262
|
+
out_dir: Path | str = ".",
|
|
263
|
+
platforms: list[str] | None = None,
|
|
264
|
+
backends: list[str] | None = None,
|
|
265
|
+
interactive: bool = True,
|
|
266
|
+
template: str = "default",
|
|
267
|
+
) -> Path:
|
|
268
|
+
"""Create a new GE project with proper structure.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
name: app name
|
|
272
|
+
out_dir: output directory
|
|
273
|
+
platforms: list of platforms (desktop/web/mobile), or None for all
|
|
274
|
+
backends: list of backend languages, or None for all
|
|
275
|
+
interactive: if True, prompt for missing values
|
|
276
|
+
template: "default" or "desktop-gui"
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Path to the created project directory.
|
|
280
|
+
"""
|
|
281
|
+
if interactive:
|
|
282
|
+
name = _prompt("App name", _sanitize_name(name))
|
|
283
|
+
if template == "default":
|
|
284
|
+
template = _prompt("Template", "default", ALL_TEMPLATES)
|
|
285
|
+
if platforms is None:
|
|
286
|
+
platforms = _prompt_multi("Platforms", "all", ALL_PLATFORMS)
|
|
287
|
+
if backends is None:
|
|
288
|
+
backends = _prompt_multi("Backend languages", "all", ALL_BACKENDS)
|
|
289
|
+
else:
|
|
290
|
+
name = _sanitize_name(name)
|
|
291
|
+
if platforms is None:
|
|
292
|
+
platforms = ALL_PLATFORMS[:]
|
|
293
|
+
if backends is None:
|
|
294
|
+
backends = ALL_BACKENDS[:]
|
|
295
|
+
|
|
296
|
+
if template in TEMPLATE_DIRS:
|
|
297
|
+
return _create_template_project(template, name, out_dir)
|
|
298
|
+
|
|
299
|
+
out = Path(out_dir) / name
|
|
300
|
+
if out.exists():
|
|
301
|
+
raise FileExistsError(f"Directory already exists: {out}")
|
|
302
|
+
|
|
303
|
+
# Create directory structure
|
|
304
|
+
dirs = ["app", "backend", "ui", "tests"]
|
|
305
|
+
for p in platforms:
|
|
306
|
+
dirs.append(p)
|
|
307
|
+
for d in dirs:
|
|
308
|
+
(out / d).mkdir(parents=True, exist_ok=True)
|
|
309
|
+
|
|
310
|
+
# Write files
|
|
311
|
+
(out / "app" / "main.ge.py").write_text(
|
|
312
|
+
_APP_MAIN_TEMPLATE.format(app_name=name), encoding="utf-8")
|
|
313
|
+
(out / "app" / "__init__.py").write_text("", encoding="utf-8")
|
|
314
|
+
|
|
315
|
+
if "desktop" in platforms:
|
|
316
|
+
(out / "desktop" / "main.ge.py").write_text(
|
|
317
|
+
_DESKTOP_MAIN_TEMPLATE.format(app_name=name), encoding="utf-8")
|
|
318
|
+
if "web" in platforms:
|
|
319
|
+
(out / "web" / "main.ge.py").write_text(
|
|
320
|
+
_WEB_MAIN_TEMPLATE.format(app_name=name), encoding="utf-8")
|
|
321
|
+
if "mobile" in platforms:
|
|
322
|
+
(out / "mobile" / "main.ge.py").write_text(
|
|
323
|
+
_MOBILE_MAIN_TEMPLATE.format(app_name=name), encoding="utf-8")
|
|
324
|
+
|
|
325
|
+
(out / "backend" / "main.ge.py").write_text(
|
|
326
|
+
_BACKEND_TEMPLATE.format(app_name=name), encoding="utf-8")
|
|
327
|
+
(out / "backend" / "__init__.py").write_text("", encoding="utf-8")
|
|
328
|
+
|
|
329
|
+
(out / "ui" / "main.ge.ui").write_text(
|
|
330
|
+
_UI_TEMPLATE.format(app_name=name), encoding="utf-8")
|
|
331
|
+
|
|
332
|
+
(out / "tests" / "test_app.py").write_text(
|
|
333
|
+
_TEST_TEMPLATE.format(app_name=name), encoding="utf-8")
|
|
334
|
+
(out / "tests" / "__init__.py").write_text("", encoding="utf-8")
|
|
335
|
+
|
|
336
|
+
platforms_str = ", ".join(f'"{p}"' for p in platforms)
|
|
337
|
+
backends_str = ", ".join(f'"{b}"' for b in backends)
|
|
338
|
+
(out / "ge.toml").write_text(
|
|
339
|
+
_GE_CONFIG_TEMPLATE.format(app_name=name, platforms=platforms_str, backends=backends_str),
|
|
340
|
+
encoding="utf-8")
|
|
341
|
+
|
|
342
|
+
(out / "README.md").write_text(
|
|
343
|
+
_README_TEMPLATE.format(app_name=name), encoding="utf-8")
|
|
344
|
+
|
|
345
|
+
return out
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def create_project_noninteractive(
|
|
349
|
+
name: str,
|
|
350
|
+
out_dir: Path | str = ".",
|
|
351
|
+
platforms: list[str] | None = None,
|
|
352
|
+
backends: list[str] | None = None,
|
|
353
|
+
template: str = "default",
|
|
354
|
+
) -> Path:
|
|
355
|
+
"""Create a project without any prompts."""
|
|
356
|
+
return create_project(name, out_dir, platforms, backends, interactive=False,
|
|
357
|
+
template=template)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _create_template_project(template: str, name: str,
|
|
361
|
+
out_dir: Path | str = ".") -> Path:
|
|
362
|
+
"""Create a project from a directory under pyeffic/templates/.
|
|
363
|
+
|
|
364
|
+
Each template is a real file tree (not inline strings) so it stays
|
|
365
|
+
readable, diffable, and independently editable. Every occurrence of
|
|
366
|
+
`{app_name}` in a template file is replaced with the project name.
|
|
367
|
+
|
|
368
|
+
Registered templates:
|
|
369
|
+
desktop-gui Rust shell + C++ core + C++ UI (native Win32)
|
|
370
|
+
web-react React 19 + TypeScript SPA + Rust backend
|
|
371
|
+
"""
|
|
372
|
+
dirname = TEMPLATE_DIRS.get(template)
|
|
373
|
+
if dirname is None:
|
|
374
|
+
raise ValueError(f"unknown template: {template}")
|
|
375
|
+
template_root = Path(__file__).parent / "templates" / dirname
|
|
376
|
+
if not template_root.is_dir():
|
|
377
|
+
raise FileNotFoundError(f"template directory missing: {template_root}")
|
|
378
|
+
|
|
379
|
+
out = Path(out_dir) / name
|
|
380
|
+
if out.exists():
|
|
381
|
+
raise FileExistsError(f"Directory already exists: {out}")
|
|
382
|
+
|
|
383
|
+
for src in sorted(template_root.rglob("*")):
|
|
384
|
+
if src.is_dir():
|
|
385
|
+
continue
|
|
386
|
+
rel = src.relative_to(template_root)
|
|
387
|
+
dst = out / rel
|
|
388
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
389
|
+
content = src.read_text(encoding="utf-8")
|
|
390
|
+
dst.write_text(content.replace("{app_name}", name), encoding="utf-8")
|
|
391
|
+
|
|
392
|
+
return out
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _create_desktop_gui_project(name: str, out_dir: Path | str = ".") -> Path:
|
|
396
|
+
"""Backwards-compatible alias for the desktop-gui template."""
|
|
397
|
+
return _create_template_project("desktop-gui", name, out_dir)
|