diff --git a/el-compiler/src/codegen-js.el b/el-compiler/src/codegen-js.el index 924329a..6cd27ea 100644 --- a/el-compiler/src/codegen-js.el +++ b/el-compiler/src/codegen-js.el @@ -562,6 +562,13 @@ fn js_cg_expr(expr: Map) -> String { return js_cg_match(expr) } + // Lambda (anonymous function literal): fn(params) -> RetType { body } + // Emitted as a JS arrow function expression: (params) => { body }. + // Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... }) + if kind == "Lambda" { + return js_cg_lambda(expr) + } + "null" } @@ -641,6 +648,65 @@ fn js_cg_match(expr: Map) -> String { str_join(parts, "") } +// ── Lambda codegen ──────────────────────────────────────────────────────────── +// +// Anonymous function literals: fn(params) -> RetType { body } +// +// Strategy: emit the lambda as a hoisted JS function declaration with a +// generated name (__lambda_N), then return the name as the expression value. +// This works because JS function declarations are hoisted within their scope, +// so the generated name is valid at any use site within the same function or +// module. The emitted code looks like: +// +// function __lambda_1(event) { dom_hide(spinner); } +// ... +// dom_listen(btn, "click", __lambda_1); +// +// This approach is clean, debuggable, and avoids any need for a string-buffer +// mode in the codegen. + +fn js_next_lambda_id() -> String { + let csv: String = state_get("__js_lambda_counter") + let n = 0 + if !str_eq(csv, "") { + let n = str_to_int(csv) + } + let n = n + 1 + state_set("__js_lambda_counter", native_int_to_str(n)) + native_int_to_str(n) +} + +fn js_cg_lambda(expr: Map) -> String { + let params = expr["params"] + let body = expr["body"] + let ret_type: String = expr["ret_type"] + let id: String = js_next_lambda_id() + let lambda_name: String = "__lambda_" + id + let params_str: String = js_params_str(params) + // Emit the function definition immediately into the output stream. + // It will appear before the statement containing this expression. + js_emit_line("function " + lambda_name + "(" + params_str + ") {") + let decl = native_list_empty() + let np: Int = native_list_len(params) + let pi = 0 + while pi < np { + let param = native_list_get(params, pi) + let pname: String = param["name"] + let decl = native_list_append(decl, pname) + let pi = pi + 1 + } + let body_xformed = body + if !str_eq(ret_type, "Void") { + let body_xformed = js_transform_implicit_return(body) + } + js_build_int_names_for_params(params) + js_cg_stmts(body_xformed, " ", decl) + js_emit_line("}") + js_emit_blank() + // Return the function name as the expression value. + lambda_name +} + // ── Variable scope tracking ─────────────────────────────────────────────────── // // El allows `let x = ...` to redeclare in the same scope. JS would throw @@ -971,6 +1037,7 @@ fn codegen_js_inner(stmts: [Map], source: String, bundle_mode: Bool state_set("__js_int_names", "") state_set("__js_match_counter", "") state_set("__js_async_fns", "") + state_set("__js_lambda_counter", "") // Preamble: in bundle mode, inline the runtime and wrap in IIFE. // In module mode, emit a single import that side-effects globalThis. diff --git a/el-compiler/src/parser.el b/el-compiler/src/parser.el index ffe63b3..ca2de82 100644 --- a/el-compiler/src/parser.el +++ b/el-compiler/src/parser.el @@ -279,6 +279,30 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { return r } + // Anonymous function literal (lambda): fn(params) -> RetType { body } + // Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... }) + // Produces a Lambda expression node (distinct from a named FnDef statement). + if k == "Fn" { + let p = pos + 1 + let r = parse_params(tokens, p) + let params = r["params"] + let p = r["pos"] + let ret_type = "" + let k2 = tok_kind(tokens, p) + if k2 == "Arrow" { + let p = p + 1 + let kt = tok_kind(tokens, p) + if kt == "Ident" { + let ret_type = tok_value(tokens, p) + } + let p = skip_type(tokens, p) + } + let r2 = parse_block(tokens, p) + let body = r2["stmts"] + let p = r2["pos"] + return make_result({ "expr": "Lambda", "params": params, "body": body, "ret_type": ret_type }, p) + } + // Unary not if k == "Not" { let r = parse_primary(tokens, pos + 1)