add DOM bridge, async/await, window export, and native_js to JS target
- el_runtime.js: add 19 dom_* builtins (browser-only, throw in Node), window_set/window_get for exposing El functions to the browser global scope, and native_js/native_js_call escape hatches for third-party libs - codegen-js.el: destructure all new builtins in generated preamble; add @async decorator support that emits async function + await at call sites for known-async HTTP builtins and user-declared @async functions; pre- registration pass ensures forward calls to @async functions get await - spec/codegen-js.md: mark Phase 3 (DOM bridge) implemented, document @async approach and its limitations, update builtin table and status - examples/browser-counter.el: canonical example showing dom_get_element, dom_set_text, dom_is_null, window_set, and state_set/get
This commit is contained in:
@@ -522,6 +522,152 @@ function math_sin(f) { return Math.sin(f); }
|
||||
function math_cos(f) { return Math.cos(f); }
|
||||
function math_pi() { return Math.PI; }
|
||||
|
||||
// ── DOM bridge (browser-only) ──────────────────────────────────────────────
|
||||
//
|
||||
// These functions wrap the browser DOM API. Each throws a descriptive error
|
||||
// when called from a Node environment, mirroring the pattern used by fs_*
|
||||
// in browser mode.
|
||||
|
||||
function _ensureBrowser(name) {
|
||||
if (IS_NODE) {
|
||||
throw new Error(`${name}: not supported in Node runtime — DOM is browser-only`);
|
||||
}
|
||||
}
|
||||
|
||||
function dom_get_element(id) {
|
||||
_ensureBrowser('dom_get_element');
|
||||
return document.getElementById(String(id));
|
||||
}
|
||||
|
||||
function dom_get_value(el) {
|
||||
_ensureBrowser('dom_get_value');
|
||||
return el == null ? '' : String(el.value ?? '');
|
||||
}
|
||||
|
||||
function dom_set_value(el, v) {
|
||||
_ensureBrowser('dom_set_value');
|
||||
if (el != null) el.value = String(v);
|
||||
}
|
||||
|
||||
function dom_get_text(el) {
|
||||
_ensureBrowser('dom_get_text');
|
||||
return el == null ? '' : String(el.textContent ?? '');
|
||||
}
|
||||
|
||||
function dom_set_text(el, text) {
|
||||
_ensureBrowser('dom_set_text');
|
||||
if (el != null) el.textContent = String(text);
|
||||
}
|
||||
|
||||
function dom_set_prop(el, prop, val) {
|
||||
_ensureBrowser('dom_set_prop');
|
||||
if (el != null) el[String(prop)] = val;
|
||||
}
|
||||
|
||||
function dom_get_prop(el, prop) {
|
||||
_ensureBrowser('dom_get_prop');
|
||||
if (el == null) return null;
|
||||
const v = el[String(prop)];
|
||||
return v === undefined ? null : v;
|
||||
}
|
||||
|
||||
function dom_set_style(el, prop, val) {
|
||||
_ensureBrowser('dom_set_style');
|
||||
if (el != null) el.style[String(prop)] = String(val);
|
||||
}
|
||||
|
||||
function dom_add_class(el, cls) {
|
||||
_ensureBrowser('dom_add_class');
|
||||
if (el != null) el.classList.add(String(cls));
|
||||
}
|
||||
|
||||
function dom_remove_class(el, cls) {
|
||||
_ensureBrowser('dom_remove_class');
|
||||
if (el != null) el.classList.remove(String(cls));
|
||||
}
|
||||
|
||||
function dom_show(el) {
|
||||
_ensureBrowser('dom_show');
|
||||
if (el != null) el.style.display = '';
|
||||
}
|
||||
|
||||
function dom_hide(el) {
|
||||
_ensureBrowser('dom_hide');
|
||||
if (el != null) el.style.display = 'none';
|
||||
}
|
||||
|
||||
function dom_listen(el, event, handler) {
|
||||
_ensureBrowser('dom_listen');
|
||||
if (el != null) el.addEventListener(String(event), handler);
|
||||
}
|
||||
|
||||
function dom_query(selector) {
|
||||
_ensureBrowser('dom_query');
|
||||
return document.querySelector(String(selector));
|
||||
}
|
||||
|
||||
function dom_query_all(selector) {
|
||||
_ensureBrowser('dom_query_all');
|
||||
return Array.from(document.querySelectorAll(String(selector)));
|
||||
}
|
||||
|
||||
function dom_create(tag) {
|
||||
_ensureBrowser('dom_create');
|
||||
return document.createElement(String(tag));
|
||||
}
|
||||
|
||||
function dom_append(parent, child) {
|
||||
_ensureBrowser('dom_append');
|
||||
if (parent != null && child != null) parent.appendChild(child);
|
||||
}
|
||||
|
||||
function dom_remove(el) {
|
||||
_ensureBrowser('dom_remove');
|
||||
if (el != null) el.remove();
|
||||
}
|
||||
|
||||
function dom_is_null(el) {
|
||||
return el === null || el === undefined;
|
||||
}
|
||||
|
||||
// ── Window export helpers ──────────────────────────────────────────────────
|
||||
//
|
||||
// Expose El functions to the browser's global scope so they can be called
|
||||
// from inline event handlers (onclick="increment()") or by external JS.
|
||||
// In Node mode, writes to globalThis so the same pattern works in tests.
|
||||
|
||||
function window_set(name, val) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window[String(name)] = val;
|
||||
} else if (typeof globalThis !== 'undefined') {
|
||||
globalThis[String(name)] = val;
|
||||
}
|
||||
}
|
||||
|
||||
function window_get(name) {
|
||||
if (typeof window !== 'undefined') {
|
||||
const v = window[String(name)];
|
||||
return v === undefined ? null : v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── native_js escape hatch ─────────────────────────────────────────────────
|
||||
//
|
||||
// Evaluate arbitrary JS from El source. Intended for calling third-party
|
||||
// browser libraries (Supabase, Stripe, etc.) until proper El bindings exist.
|
||||
// Use sparingly — this bypasses El's type system entirely.
|
||||
|
||||
function native_js(code) {
|
||||
// eslint-disable-next-line no-eval
|
||||
return eval(String(code));
|
||||
}
|
||||
|
||||
function native_js_call(obj, method, args) {
|
||||
if (obj == null) throw new Error('native_js_call: object is null');
|
||||
return obj[String(method)](...(Array.isArray(args) ? args : []));
|
||||
}
|
||||
|
||||
// ── Stubs for not-yet-supported features ───────────────────────────────────
|
||||
//
|
||||
// These compile but throw when called. See spec/codegen-js.md §7.
|
||||
@@ -632,6 +778,15 @@ const __el = {
|
||||
// Float / math
|
||||
float_to_str, int_to_float, float_to_int, format_float, decimal_round,
|
||||
str_to_float, math_sqrt, math_log, math_ln, math_sin, math_cos, math_pi,
|
||||
// DOM bridge (browser-only)
|
||||
dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,
|
||||
dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,
|
||||
dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,
|
||||
dom_append, dom_remove, dom_is_null,
|
||||
// Window export helpers
|
||||
window_set, window_get,
|
||||
// native_js escape hatch
|
||||
native_js, native_js_call,
|
||||
// CGI / DHARMA / Engram / LLM (stubs)
|
||||
el_cgi_init,
|
||||
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
|
||||
@@ -676,4 +831,11 @@ export {
|
||||
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
|
||||
engram_node, engram_search, engram_activate,
|
||||
llm_call, llm_call_system,
|
||||
// DOM bridge
|
||||
dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,
|
||||
dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,
|
||||
dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,
|
||||
dom_append, dom_remove, dom_is_null,
|
||||
// Window / native_js
|
||||
window_set, window_get, native_js, native_js_call,
|
||||
};
|
||||
|
||||
@@ -86,6 +86,37 @@ fn js_binop(op: String) -> String {
|
||||
op
|
||||
}
|
||||
|
||||
// ── Async function tracking ───────────────────────────────────────────────────
|
||||
//
|
||||
// Functions decorated with @async are recorded here. Any call to a known-async
|
||||
// builtin (http_get, http_post, http_post_json) or to a user-declared @async
|
||||
// function gets an `await` prefix in generated JS.
|
||||
//
|
||||
// Known-async builtins — these return Promise<T> in el_runtime.js.
|
||||
fn js_is_async_builtin(name: String) -> Bool {
|
||||
if str_eq(name, "http_get") { return true }
|
||||
if str_eq(name, "http_post") { return true }
|
||||
if str_eq(name, "http_post_json") { return true }
|
||||
if str_eq(name, "http_get_with_headers") { return true }
|
||||
if str_eq(name, "http_post_with_headers") { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn js_register_async_fn(name: String) -> Bool {
|
||||
let csv: String = state_get("__js_async_fns")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__js_async_fns", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn js_is_async_fn(name: String) -> Bool {
|
||||
let csv: String = state_get("__js_async_fns")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
// ── Int-name tracking (mirrors codegen.el) ────────────────────────────────────
|
||||
|
||||
fn js_is_int_name(name: String) -> Bool {
|
||||
@@ -377,7 +408,14 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
|
||||
|
||||
if func_kind == "Ident" {
|
||||
let fn_name: String = func["name"]
|
||||
return fn_name + "(" + args_c + ")"
|
||||
let call_expr: String = fn_name + "(" + args_c + ")"
|
||||
if js_is_async_builtin(fn_name) {
|
||||
return "await " + call_expr
|
||||
}
|
||||
if js_is_async_fn(fn_name) {
|
||||
return "await " + call_expr
|
||||
}
|
||||
return call_expr
|
||||
}
|
||||
|
||||
if func_kind == "Field" {
|
||||
@@ -785,16 +823,30 @@ fn js_cg_fn(stmt: Map<String, Any>) -> Void {
|
||||
let params = stmt["params"]
|
||||
let body = stmt["body"]
|
||||
let ret_type: String = stmt["ret_type"]
|
||||
let decorator: String = stmt["decorator"]
|
||||
let params_str: String = js_params_str(params)
|
||||
js_build_int_names_for_params(params)
|
||||
|
||||
// Special-case `fn main` — emit as a regular function and call it
|
||||
// at module bottom (after all top-level statements). This matches
|
||||
// the C backend's behavior where `fn main` is the entry point.
|
||||
if fn_name == "main" {
|
||||
js_emit_line("function main(" + params_str + ") {")
|
||||
// Detect @async decorator — emit `async function` and register the name
|
||||
// so call sites for this function get `await` prefixed automatically.
|
||||
// When the decorator field is absent, el_get_field returns null; str_eq
|
||||
// handles null safely (returns false), so no special nil-check is needed.
|
||||
if str_eq(decorator, "async") {
|
||||
js_register_async_fn(fn_name)
|
||||
if fn_name == "main" {
|
||||
js_emit_line("async function main(" + params_str + ") {")
|
||||
} else {
|
||||
js_emit_line("async function " + fn_name + "(" + params_str + ") {")
|
||||
}
|
||||
} else {
|
||||
js_emit_line("function " + fn_name + "(" + params_str + ") {")
|
||||
// Special-case `fn main` — emit as a regular function and call it
|
||||
// at module bottom (after all top-level statements). This matches
|
||||
// the C backend's behavior where `fn main` is the entry point.
|
||||
if fn_name == "main" {
|
||||
js_emit_line("function main(" + params_str + ") {")
|
||||
} else {
|
||||
js_emit_line("function " + fn_name + "(" + params_str + ") {")
|
||||
}
|
||||
}
|
||||
|
||||
let decl = native_list_empty()
|
||||
@@ -839,6 +891,7 @@ fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
|
||||
// Reset per-compile state.
|
||||
state_set("__js_int_names", "")
|
||||
state_set("__js_match_counter", "")
|
||||
state_set("__js_async_fns", "")
|
||||
|
||||
// Preamble: inline the runtime via a single import that side-effects
|
||||
// globalThis. The runtime path is resolved relative to the generated
|
||||
@@ -868,12 +921,34 @@ fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
|
||||
js_emit_line(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,")
|
||||
js_emit_line(" engram_node, engram_search, engram_activate,")
|
||||
js_emit_line(" llm_call, llm_call_system,")
|
||||
js_emit_line(" dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,")
|
||||
js_emit_line(" dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,")
|
||||
js_emit_line(" dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,")
|
||||
js_emit_line(" dom_append, dom_remove, dom_is_null,")
|
||||
js_emit_line(" window_set, window_get, native_js, native_js_call,")
|
||||
js_emit_line("} = globalThis.__el;")
|
||||
js_emit_blank()
|
||||
|
||||
// Function definitions
|
||||
// Pre-registration pass: scan all FnDefs for @async decorators so that
|
||||
// forward calls to @async functions get `await` even if the callee is
|
||||
// defined after the caller.
|
||||
let n: Int = native_list_len(stmts)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk: String = stmt["stmt"]
|
||||
if str_eq(sk, "FnDef") {
|
||||
let dec: String = stmt["decorator"]
|
||||
if str_eq(dec, "async") {
|
||||
let aname: String = stmt["name"]
|
||||
js_register_async_fn(aname)
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Function definitions
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
if js_is_fndef(stmt) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// browser-counter.el — canonical browser DOM bridge example
|
||||
//
|
||||
// Compile with: elc --target=js examples/browser-counter.el > counter.js
|
||||
//
|
||||
// Then include in an HTML page that has a <span id="count-display"> element.
|
||||
// The page can call window.increment() from any onclick handler, e.g.:
|
||||
// <button onclick="increment()">+1</button>
|
||||
//
|
||||
// On load the display is initialised to "0". Each call to increment()
|
||||
// adds 1 and updates the display text.
|
||||
//
|
||||
// Demonstrates:
|
||||
// - dom_get_element to locate a DOM node by id
|
||||
// - dom_set_text to update visible text content
|
||||
// - dom_is_null to guard against missing elements
|
||||
// - window_set to expose an El function for inline event handlers
|
||||
// - state_set/get for in-memory counter state (survives calls, resets
|
||||
// on page reload — same semantics as the C state_* API)
|
||||
|
||||
fn init() -> Void {
|
||||
state_set("counter", 0)
|
||||
let display = dom_get_element("count-display")
|
||||
if !dom_is_null(display) {
|
||||
dom_set_text(display, "0")
|
||||
}
|
||||
}
|
||||
|
||||
fn increment() -> Void {
|
||||
let current = str_to_int(state_get("counter"))
|
||||
let next = current + 1
|
||||
state_set("counter", next)
|
||||
let display = dom_get_element("count-display")
|
||||
if !dom_is_null(display) {
|
||||
dom_set_text(display, int_to_str(next))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
init()
|
||||
window_set("increment", increment)
|
||||
}
|
||||
+49
-9
@@ -1,6 +1,6 @@
|
||||
# El JavaScript Backend (codegen-js)
|
||||
|
||||
**Status:** scaffolded. Hello-world compiles and runs. ~50% language coverage. Core runtime (~30 builtins) implemented. CGI / DHARMA / LLM / Engram intentionally stubbed.
|
||||
**Status:** Phase 3 complete. Hello-world compiles and runs. ~50% language coverage. Core runtime (~50 builtins) implemented including full DOM bridge, window export helpers, native_js escape hatches, and @async/await support. CGI / DHARMA / LLM / Engram intentionally stubbed.
|
||||
|
||||
**Authoritative files**
|
||||
|
||||
@@ -78,6 +78,9 @@ Same function names as `el_runtime.c` wherever possible, so codegen-js can emit
|
||||
| `args` | `args()` returns `process.argv.slice(2)` in Node, `[]` in browser |
|
||||
| `state_*` | In-memory `Map` keyed by string |
|
||||
| `env` | `process.env[k]` in Node, throws in browser |
|
||||
| DOM bridge (Phase 3) | `dom_get_element`, `dom_get_value`, `dom_set_value`, `dom_get_text`, `dom_set_text`, `dom_set_prop`, `dom_get_prop`, `dom_set_style`, `dom_add_class`, `dom_remove_class`, `dom_show`, `dom_hide`, `dom_listen`, `dom_query`, `dom_query_all`, `dom_create`, `dom_append`, `dom_remove`, `dom_is_null` (browser-only; throw in Node) |
|
||||
| Window export | `window_set(name, val)`, `window_get(name)` — expose/retrieve values on `window` (or `globalThis` in Node) |
|
||||
| native_js escape hatch | `native_js(code)` — evaluates a JS expression via `eval`; `native_js_call(obj, method, args)` — calls a method on an object. Use for third-party browser libraries until proper bindings exist |
|
||||
|
||||
### Stubbed (throw at runtime)
|
||||
|
||||
@@ -128,17 +131,54 @@ The runtime auto-detects via `typeof window === 'undefined'`.
|
||||
|
||||
---
|
||||
|
||||
## 5. The async problem (the big deferred decision)
|
||||
## 5. The async problem
|
||||
|
||||
`fetch()` is async. The C backend's `http_get(url)` is synchronous and returns the body string directly. El source was written assuming sync. Three options:
|
||||
|
||||
1. **Pretend it's sync from El's POV; use synchronous XHR (browser) or `child_process.execSync('curl …')` (Node).** Bad: synchronous XHR is deprecated and frozen on the main thread; `execSync` is a hack.
|
||||
2. **Make every `http_*` builtin in the JS runtime return a `Promise`, and rewrite codegen-js to insert `await` everywhere.** This requires turning every El function that transitively calls a network builtin into an `async fn` in JS. Doable, but invasive — the El AST does not currently mark async-ness.
|
||||
3. **Compile El's call sites with implicit await; compile-time taint tracking marks every fn that transitively calls a network builtin as `async`. Generated JS uses `async function` and `await`.** This is the right answer long-term.
|
||||
1. **Pretend it's sync from El's POV; use synchronous XHR (browser) or `child_process.execSync('curl ...')` (Node).** Bad: synchronous XHR is deprecated and frozen on the main thread; `execSync` is a hack.
|
||||
2. **Make every `http_*` builtin in the JS runtime return a `Promise`, and rewrite codegen-js to insert `await` everywhere.** This requires turning every El function that transitively calls a network builtin into an `async fn` in JS. Doable, but invasive.
|
||||
3. **Explicit `@async` decorator on El functions; codegen-js emits `async function` + `await` for known-async call sites.** This is the approach implemented.
|
||||
|
||||
**Decision for this scaffold:** option 3, but only the runtime side is implemented. `http_get` in `el_runtime.js` returns a `Promise<string>`. `codegen-js.el` does NOT yet emit `async`/`await`. Calling `http_get` from compiled El will return a Promise that the El program will treat as a string (which produces `"[object Promise]"`). This is documented and accepted for the scaffold; the compile-time taint pass is a follow-up.
|
||||
**Decision:** option 3, with an explicit opt-in decorator. `http_get`, `http_post`, `http_post_json`, `http_get_with_headers`, and `http_post_with_headers` in `el_runtime.js` return `Promise<string>`. `codegen-js.el` now emits `await` before calls to these builtins and before calls to any El function decorated `@async`.
|
||||
|
||||
For now, programs that don't touch HTTP work correctly. That covers `el-ui/runtime` (which only manipulates the DOM and a graph), most of cgi-studio's pure UI components, and all hello-world style programs.
|
||||
### How to use async in El (JS target)
|
||||
|
||||
Mark a function with `@async` to declare it as async. Any call to that function from another El function will automatically get `await` in the generated JS. The callee must also be `@async` (or call only non-async code) for the pattern to compose correctly.
|
||||
|
||||
```el
|
||||
@async
|
||||
fn fetch_user(id: String) -> String {
|
||||
http_get("https://api.example.com/users/" + id)
|
||||
}
|
||||
|
||||
@async
|
||||
fn main() -> Void {
|
||||
let body = fetch_user("42")
|
||||
println(body)
|
||||
}
|
||||
```
|
||||
|
||||
Compiles to:
|
||||
|
||||
```javascript
|
||||
async function fetch_user(id) {
|
||||
return await http_get("https://api.example.com/users/" + id);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let body = await fetch_user("42");
|
||||
println(body);
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
- `@async` is a JS-target-only convention. The C backend ignores the decorator (it calls the synchronous libcurl-backed version).
|
||||
- Implicit taint propagation (auto-marking all transitive callers) is not implemented. The programmer must explicitly add `@async` to every function in the call chain that reaches an async builtin.
|
||||
- Forward-reference calls to `@async` functions are handled correctly: codegen-js does a pre-registration pass over all FnDefs before emitting any code.
|
||||
|
||||
For programs that do not touch HTTP, no `@async` annotation is needed and the generated code is identical to before.
|
||||
|
||||
---
|
||||
|
||||
@@ -201,9 +241,9 @@ This is the real-world test. `el-ui/runtime/src/` is currently 5 hand-written `.
|
||||
|
||||
1. **Phase 1 — Hello-world** (this scaffold). Done.
|
||||
2. **Phase 2 — language coverage.** Get codegen-js to ~95% parity with codegen.el for non-network features. Specifically: `match`, struct/enum field access, `?`-propagation, full `for`-over-list, complete unary/binary operators, lexical closures (the C backend doesn't have these but we'll need them for el-ui's component model).
|
||||
3. **Phase 3 — DOM bridge.** Add `dom_*` builtins to el_runtime.js: `dom_create_element`, `dom_set_text`, `dom_append_child`, `dom_query`, `dom_listen`, etc. These are Node-as-El builtins for the browser; the C backend will add a stub set that errors. Source-shareable El UI code becomes possible.
|
||||
3. **Phase 3 — DOM bridge.** IMPLEMENTED. `dom_*` builtins added to `el_runtime.js`: full set covering element lookup, text/value/property manipulation, class and style control, event listeners, query selectors, element creation and insertion, and `dom_is_null` for null-guarding. Also added: `window_set`/`window_get` for exposing El functions to the browser global scope, and `native_js`/`native_js_call` escape hatches for calling third-party browser libraries. `codegen-js.el` preamble updated to destructure all new names. Canonical example: `examples/browser-counter.el`.
|
||||
4. **Phase 4 — Component class lowering.** El doesn't have classes; el-ui's `Component` is a JS class. Decide: extend El with a `component` keyword that compiles to JS class + C struct? Or have el-ui authors define components as `fn render_<name>(state) -> String` and provide a small bootstrap. The latter is the lower-impact path.
|
||||
5. **Phase 5 — Async taint pass.** Implement compile-time async tracking so `http_get` and friends produce `await fetch()` correctly. Required before authoring code that fetches data.
|
||||
5. **Phase 5 — Async taint pass.** PARTIALLY IMPLEMENTED. `@async` decorator on El functions causes codegen-js to emit `async function` + `await` at call sites. Known async builtins (`http_get`, `http_post`, `http_post_json`, `http_get_with_headers`, `http_post_with_headers`) also get implicit `await`. Remaining gap: implicit taint propagation — programmers must manually annotate every function in the call chain. Full compile-time taint tracking (automatically marking all transitive callers) is a follow-up.
|
||||
6. **Phase 6 — Port `el-ui/runtime/`.** Translate the 5 JS files to El, compile to JS, swap in. Run el-ui's existing tests. Iterate.
|
||||
7. **Phase 7 — Port cgi-studio UI.** Larger surface area; same pattern.
|
||||
8. **Phase 8 — Marketplace plugins.** Open the door for third-party UI El.
|
||||
|
||||
Reference in New Issue
Block a user