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_cos(f) { return Math.cos(f); }
|
||||||
function math_pi() { return Math.PI; }
|
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 ───────────────────────────────────
|
// ── Stubs for not-yet-supported features ───────────────────────────────────
|
||||||
//
|
//
|
||||||
// These compile but throw when called. See spec/codegen-js.md §7.
|
// These compile but throw when called. See spec/codegen-js.md §7.
|
||||||
@@ -632,6 +778,15 @@ const __el = {
|
|||||||
// Float / math
|
// Float / math
|
||||||
float_to_str, int_to_float, float_to_int, format_float, decimal_round,
|
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,
|
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)
|
// CGI / DHARMA / Engram / LLM (stubs)
|
||||||
el_cgi_init,
|
el_cgi_init,
|
||||||
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
|
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,
|
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
|
||||||
engram_node, engram_search, engram_activate,
|
engram_node, engram_search, engram_activate,
|
||||||
llm_call, llm_call_system,
|
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
|
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) ────────────────────────────────────
|
// ── Int-name tracking (mirrors codegen.el) ────────────────────────────────────
|
||||||
|
|
||||||
fn js_is_int_name(name: String) -> Bool {
|
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" {
|
if func_kind == "Ident" {
|
||||||
let fn_name: String = func["name"]
|
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" {
|
if func_kind == "Field" {
|
||||||
@@ -785,9 +823,22 @@ fn js_cg_fn(stmt: Map<String, Any>) -> Void {
|
|||||||
let params = stmt["params"]
|
let params = stmt["params"]
|
||||||
let body = stmt["body"]
|
let body = stmt["body"]
|
||||||
let ret_type: String = stmt["ret_type"]
|
let ret_type: String = stmt["ret_type"]
|
||||||
|
let decorator: String = stmt["decorator"]
|
||||||
let params_str: String = js_params_str(params)
|
let params_str: String = js_params_str(params)
|
||||||
js_build_int_names_for_params(params)
|
js_build_int_names_for_params(params)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
// Special-case `fn main` — emit as a regular function and call it
|
// Special-case `fn main` — emit as a regular function and call it
|
||||||
// at module bottom (after all top-level statements). This matches
|
// at module bottom (after all top-level statements). This matches
|
||||||
// the C backend's behavior where `fn main` is the entry point.
|
// the C backend's behavior where `fn main` is the entry point.
|
||||||
@@ -796,6 +847,7 @@ fn js_cg_fn(stmt: Map<String, Any>) -> Void {
|
|||||||
} else {
|
} else {
|
||||||
js_emit_line("function " + fn_name + "(" + params_str + ") {")
|
js_emit_line("function " + fn_name + "(" + params_str + ") {")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let decl = native_list_empty()
|
let decl = native_list_empty()
|
||||||
let np: Int = native_list_len(params)
|
let np: Int = native_list_len(params)
|
||||||
@@ -839,6 +891,7 @@ fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
|
|||||||
// Reset per-compile state.
|
// Reset per-compile state.
|
||||||
state_set("__js_int_names", "")
|
state_set("__js_int_names", "")
|
||||||
state_set("__js_match_counter", "")
|
state_set("__js_match_counter", "")
|
||||||
|
state_set("__js_async_fns", "")
|
||||||
|
|
||||||
// Preamble: inline the runtime via a single import that side-effects
|
// Preamble: inline the runtime via a single import that side-effects
|
||||||
// globalThis. The runtime path is resolved relative to the generated
|
// 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(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,")
|
||||||
js_emit_line(" engram_node, engram_search, engram_activate,")
|
js_emit_line(" engram_node, engram_search, engram_activate,")
|
||||||
js_emit_line(" llm_call, llm_call_system,")
|
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_line("} = globalThis.__el;")
|
||||||
js_emit_blank()
|
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 n: Int = native_list_len(stmts)
|
||||||
let i = 0
|
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 {
|
while i < n {
|
||||||
let stmt = native_list_get(stmts, i)
|
let stmt = native_list_get(stmts, i)
|
||||||
if js_is_fndef(stmt) {
|
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)
|
# 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**
|
**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 |
|
| `args` | `args()` returns `process.argv.slice(2)` in Node, `[]` in browser |
|
||||||
| `state_*` | In-memory `Map` keyed by string |
|
| `state_*` | In-memory `Map` keyed by string |
|
||||||
| `env` | `process.env[k]` in Node, throws in browser |
|
| `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)
|
### 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:
|
`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.
|
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.
|
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. **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.
|
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.
|
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).
|
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.
|
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.
|
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.
|
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.
|
8. **Phase 8 — Marketplace plugins.** Open the door for third-party UI El.
|
||||||
|
|||||||
Reference in New Issue
Block a user