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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user