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:
+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