diff --git a/dist/platform/elc b/dist/platform/elc index 37a9e73..fb7502c 100755 Binary files a/dist/platform/elc and b/dist/platform/elc differ diff --git a/el-compiler/src/codegen-js.el b/el-compiler/src/codegen-js.el index 7374fe8..882f796 100644 --- a/el-compiler/src/codegen-js.el +++ b/el-compiler/src/codegen-js.el @@ -439,19 +439,36 @@ fn js_cg_expr(expr: Map) -> String { // El's `obj.foo` becomes JS `obj["foo"]` — works on plain objects // (maps) and on JS objects with prototype. el_get_field is a // runtime helper for callers that want EL_NULL on missing keys. + // + // If the object is a Try (nil-propagation postfix `obj?`), use JS + // optional chaining: `obj?.["foo"]` — returns undefined (null) if obj + // is null/undefined, otherwise accesses the field. let obj = expr["object"] let field: String = expr["field"] + let obj_kind: String = obj["expr"] + if str_eq(obj_kind, "Try") { + let inner = obj["inner"] + let inner_c: String = js_cg_expr(inner) + return "(" + inner_c + ")?.[" + js_str_lit(field) + "] ?? null" + } let obj_c: String = js_cg_expr(obj) return "el_get_field(" + obj_c + ", " + js_str_lit(field) + ")" } if kind == "Index" { // Map vs list dispatch on the index expression kind, same as C. + // If the object is a Try (nil-propagation), use JS optional indexing. let obj = expr["object"] let idx = expr["index"] let obj_c: String = js_cg_expr(obj) let idx_c: String = js_cg_expr(idx) let idx_kind: String = idx["expr"] + let obj_kind: String = obj["expr"] + if str_eq(obj_kind, "Try") { + let inner = obj["inner"] + let inner_c: String = js_cg_expr(inner) + return "(" + inner_c + ")?.[" + idx_c + "] ?? null" + } if str_eq(idx_kind, "Str") { return "el_get_field(" + obj_c + ", " + idx_c + ")" } @@ -491,6 +508,12 @@ fn js_cg_expr(expr: Map) -> String { } if kind == "Try" { + // Postfix `?` — nil-propagation guard. + // When used as `expr?.field` the Field handler above intercepts and + // emits `(expr)?.["field"]`. Here, a bare `expr?` (not followed by + // field/index access) passes through to the inner expression unchanged + // (it acts as an identity but marks the value as "nil-propagating" for + // its caller). This matches the C backend's current behavior. let inner = expr["inner"] return js_cg_expr(inner) } diff --git a/examples/browser-auth.el b/examples/browser-auth.el new file mode 100644 index 0000000..214b73d --- /dev/null +++ b/examples/browser-auth.el @@ -0,0 +1,165 @@ +// browser-auth.el -- El-compiled auth flow using Supabase via native_js_call +// +// Compile: elc --target=js --bundle examples/browser-auth.el > auth.js +// (requires el_runtime.js in the same directory as browser-auth.el) +// +// Demonstrates: +// - @async functions with DOM interaction +// - native_js_call to invoke third-party library methods (Supabase) +// - DOM bridge: dom_get_element, dom_get_value, dom_set_text, dom_add_class +// dom_remove_class, dom_show, dom_hide, dom_is_null +// - window_set to expose El functions to the browser global scope +// - local_storage_set/get for session hints +// - set_timeout for transient UI state +// - state_set/get for component state +// +// Expected HTML elements: +// #acct-email-input -- email text input +// #send-link-btn -- submit button +// #auth-message -- status message container +// #auth-form -- the form to hide after success +// +// Supabase client is expected on window.supabase (loaded from Supabase CDN +// via a separate script tag before auth.js). + +// ── UI helpers ───────────────────────────────────────────────────────────── + +fn show_message(text: String, is_error: Bool) -> Void { + let msg_el = dom_get_element("auth-message") + if !dom_is_null(msg_el) { + dom_set_text(msg_el, text) + dom_remove_class(msg_el, "hidden") + if is_error { + dom_add_class(msg_el, "error") + dom_remove_class(msg_el, "success") + } else { + dom_add_class(msg_el, "success") + dom_remove_class(msg_el, "error") + } + } +} + +fn set_button_loading(loading: Bool) -> Void { + let btn = dom_get_element("send-link-btn") + if !dom_is_null(btn) { + if loading { + dom_set_text(btn, "Sending...") + dom_set_attr(btn, "disabled", "true") + } else { + dom_set_text(btn, "Send Magic Link") + dom_remove_attr(btn, "disabled") + } + } +} + +fn clear_message() -> Void { + let msg_el = dom_get_element("auth-message") + if !dom_is_null(msg_el) { + dom_add_class(msg_el, "hidden") + dom_set_text(msg_el, "") + } +} + +// ── Email validation ─────────────────────────────────────────────────────── + +fn is_valid_email(email: String) -> Bool { + let trimmed: String = str_trim(email) + if str_len(trimmed) < 5 { return false } + let at_pos: Int = str_index_of(trimmed, "@") + if at_pos < 1 { return false } + let dot_pos: Int = str_index_of(trimmed, ".") + if dot_pos < at_pos + 2 { return false } + return true +} + +// ── Auth flow ────────────────────────────────────────────────────────────── + +@async +fn send_magic_link() -> Void { + let email_el = dom_get_element("acct-email-input") + if dom_is_null(email_el) { + show_message("Could not find email input", true) + return null + } + + let email: String = str_trim(dom_get_value(email_el)) + + if !is_valid_email(email) { + show_message("Please enter a valid email address", true) + return null + } + + clear_message() + set_button_loading(true) + state_set("auth_email", email) + + // Call Supabase via native_js_call. + // window.supabase.auth.signInWithOtp({ email: "..." }) returns a Promise. + let supabase = window_get("supabase") + if dom_is_null(supabase) { + show_message("Auth service not available", true) + set_button_loading(false) + return null + } + + let auth = native_js_call(supabase, "auth", []) + let payload = { "email": email } + let result = native_js_call(auth, "signInWithOtp", [payload]) + + // Await the promise via native_js_call on the result object + let error = native_js_call(result, "then", [check_auth_result]) + set_button_loading(false) +} + +fn check_auth_result(resp: Any) -> Void { + let err = resp["error"] + if !dom_is_null(err) { + let msg: String = err["message"] + show_message("Error: " + msg, true) + return null + } + let email: String = state_get("auth_email") + local_storage_set("auth_pending_email", email) + show_message("Magic link sent! Check your inbox for " + email, false) + let form = dom_get_element("auth-form") + if !dom_is_null(form) { + dom_hide(form) + } +} + +// ── Keyboard support ─────────────────────────────────────────────────────── + +fn handle_email_keydown(event: Any) -> Void { + let key: String = dom_get_prop(event, "key") + if str_eq(key, "Enter") { + send_magic_link() + } +} + +// ── Initialization ───────────────────────────────────────────────────────── + +fn init_auth() -> Void { + let email_el = dom_get_element("acct-email-input") + if !dom_is_null(email_el) { + // Pre-fill from local storage if a pending send was interrupted + let pending: String = local_storage_get("auth_pending_email") + if !str_eq(pending, "") { + dom_set_value(email_el, pending) + } + dom_listen(email_el, "keydown", handle_email_keydown) + } + let btn = dom_get_element("send-link-btn") + if !dom_is_null(btn) { + dom_listen(btn, "click", send_magic_link) + } + state_set("auth_initialized", "true") +} + +fn main() -> Void { + // Expose send_magic_link globally so inline event handlers can call it + window_set("sendMagicLink", send_magic_link) + window_set("initAuth", init_auth) + + // Run init when DOM is ready + window_on_load(init_auth) +} diff --git a/spec/codegen-js.md b/spec/codegen-js.md index 7770811..b60e750 100644 --- a/spec/codegen-js.md +++ b/spec/codegen-js.md @@ -1,6 +1,6 @@ # El JavaScript Backend (codegen-js) -**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. +**Status:** Phase 4 complete. ~80% language coverage. Core runtime (~70 builtins) implemented including full DOM bridge (extended), localStorage, timers, window navigation, window export helpers, native_js escape hatches, and @async/await support. Enum::Variant match patterns fully implemented in parser and both codegens. TypeDef parser bug fixed (= before { consumed correctly). `?` nil-propagation compiles to JS optional chaining for field/index access. `--bundle` flag produces self-contained IIFE output for direct `