From 2bed8483f75ebc76eefac69567b01d2da70d5db9 Mon Sep 17 00:00:00 2001 From: bigmerge Date: Mon, 17 Aug 2026 08:08:37 -0500 Subject: [PATCH] EXPERIMENT: hand the construct the body as a real closure ROOT CAUSE of the weaker design: "C has no closures" was taken as a fact about what is possible. It is a fact about one grammar. Every C++ lambda, every Go closure, every Rust closure compiles to a struct of captured values plus a function pointer -- which is what is emitted here. Codegen emits C; it is not written in C's syntax, and the distinction is the whole difference between a construct that can only decide whether to repeat and one that controls invocation. It would also have crippled the JS backend, which has closures natively, for a limit that applies only to the C one. PREDICTIONS AND RESULTS 1 env struct + thunk taking void* TRUE 2 fails to compile: struct redefinition FALSE -- C allows the inner declaration to shadow. Prediction wrong; C is more permissive than assumed. A different real defect surfaced instead: a wrap with no exit construct emitted `(EL_STR("f"), EL_STR(""), __r);` -- a call to an empty target -- because has_exit was reused as "needs a wrapper" and the exit line was emitted unconditionally. Fixed. 3 compiles when the target is declared in El FALSE -- and this is the root cause worth keeping: El has ONE type, el_val_t = int64_t. El's type system cannot describe a callable, so `extern fn` and the real signature cannot be made to agree in El's own vocabulary. The fix is not a cast: codegen DEFINES the wrap calling convention, so codegen emits the extern declaration. The convention is not El-expressible; it is emitted. 4 target controls invocation, 0..N times TRUE 5 existing @manager output byte-identical TRUE 6 compiler fixpoint holds TRUE 7 emitting the convention makes it compile TRUE MEASURED base(5) wrapped by a target that invokes the body twice and sums -> 10 never_runs(5) wrapped by a target that never invokes it -> 999 Neither is expressible by "decide whether to repeat". This supersedes the repeats_body experiment on experiment/repeats-body, which was built around the mistaken limit. --- lang/el-compiler/src/codegen.el | 137 +++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 3 deletions(-) diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index aad69af..ad46530 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -3254,6 +3254,84 @@ fn cg_exit_target(stmt: Map) -> String { found } +fn cg_wrap_target(stmt: Map) -> String { + let wdl = stmt["decorators"] + let n_wdl: Int = native_list_len(wdl) + let wi = 0 + let found: String = "" + while wi < n_wdl { + if str_eq(found, "") { + let wd = native_list_get(wdl, wi) + let wdn: String = wd["name"] + let wt: String = decorator_wrap(wdn) + if !str_eq(wt, "") { let found = wt } + } + let wi = wi + 1 + } + found +} + +fn cg_wrap_construct(stmt: Map) -> String { + let wdl = stmt["decorators"] + let n_wdl: Int = native_list_len(wdl) + let wi = 0 + let found: String = "" + while wi < n_wdl { + if str_eq(found, "") { + let wd = native_list_get(wdl, wi) + let wdn: String = wd["name"] + let wt: String = decorator_wrap(wdn) + if !str_eq(wt, "") { let found = wdn } + } + let wi = wi + 1 + } + found +} + +// params_to_env_fields / params_to_env_init / params_to_env_args — the captured +// environment. This IS the closure: a struct of captured values, a function +// pointer that takes it, and the pair handed to the wrap target. +fn params_to_env_fields(params: [Any]) -> String { + let out: String = "" + let n: Int = native_list_len(params) + let i = 0 + while i < n { + let p = native_list_get(params, i) + let pn: String = p["name"] + let out = out + " el_val_t " + pn + ";" + let i = i + 1 + } + out +} + +fn params_to_env_init(params: [Any]) -> String { + let out: String = "" + let n: Int = native_list_len(params) + let i = 0 + while i < n { + let p = native_list_get(params, i) + let pn: String = p["name"] + if i > 0 { let out = out + ", " } + let out = out + pn + let i = i + 1 + } + out +} + +fn params_to_env_args(params: [Any]) -> String { + let out: String = "" + let n: Int = native_list_len(params) + let i = 0 + while i < n { + let p = native_list_get(params, i) + let pn: String = p["name"] + if i > 0 { let out = out + ", " } + let out = out + "__e->" + pn + let i = i + 1 + } + out +} + fn cg_exit_construct(stmt: Map) -> String { let xdl = stmt["decorators"] let n_xdl: Int = native_list_len(xdl) @@ -3321,7 +3399,10 @@ fn cg_fn(stmt: Map) -> Void { // exactly as before, byte for byte. let exit_target: String = cg_exit_target(stmt) let exit_construct: String = cg_exit_construct(stmt) - let has_exit: Bool = !str_eq(exit_target, "") + let wrap_target: String = cg_wrap_target(stmt) + let wrap_construct: String = cg_wrap_construct(stmt) + let has_wrap: Bool = !str_eq(wrap_target, "") + let has_exit: Bool = !str_eq(exit_target, "") || has_wrap if has_exit { emit_line("static el_val_t __el_body_" + fn_name + "(" + params_c + ") {") } else { @@ -3353,11 +3434,30 @@ fn cg_fn(stmt: Map) -> Void { // The wrapper: guards, entry injection, the body call, then the exit // injection, which receives the result so it can observe what the fn // actually returned. + if has_wrap { + // Codegen defines the wrap calling convention, so codegen declares it. + // El has ONE type -- el_val_t = int64_t -- so El's own `extern fn` cannot + // describe a callable, and asking it to produces an int/pointer + // mismatch. The convention is not El-expressible; it is emitted. + emit_line("extern el_val_t " + wrap_target + "(el_val_t, el_val_t, el_val_t(*)(void*), void*);") + emit_line("struct __env_" + fn_name + " { " + params_to_env_fields(params) + " };") + emit_line("static el_val_t __thunk_" + fn_name + "(void* __v) {") + emit_line(" struct __env_" + fn_name + "* __e = (struct __env_" + fn_name + "*)__v; (void)__e;") + emit_line(" return __el_body_" + fn_name + "(" + params_to_env_args(params) + ");") + emit_line("}") + } if has_exit { emit_line("el_val_t " + fn_name + "(" + params_c + ") {") cg_entry_seam(stmt, fn_name) - emit_line(" el_val_t __r = __el_body_" + fn_name + "(" + params_to_call_args(params) + ");") - emit_line(" " + exit_target + "(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit(exit_construct) + "), __r);") + if has_wrap { + emit_line(" struct __env_" + fn_name + " __env = { " + params_to_env_init(params) + " };") + emit_line(" el_val_t __r = " + wrap_target + "(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit(wrap_construct) + "), __thunk_" + fn_name + ", &__env);") + } else { + emit_line(" el_val_t __r = __el_body_" + fn_name + "(" + params_to_call_args(params) + ");") + } + if !str_eq(exit_target, "") { + emit_line(" " + exit_target + "(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit(exit_construct) + "), __r);") + } emit_line(" return __r;") emit_line("}") } @@ -4232,6 +4332,27 @@ fn decorator_exit(name: String) -> String { state_get("__dec_exit_" + name) } +// A WRAP construct receives the body as a CLOSURE and decides how, whether, and +// how many times to invoke it. +// +// target(, , el_val_t (*body)(void*), void* env) -> el_val_t +// +// C has no closure SYNTAX. That is not the same as C being unable to express a +// closure: every C++ lambda, every Go closure, every Rust closure compiles to a +// struct of captured values plus a function pointer, which is exactly what is +// emitted here. Taking "C has no closures" as a fact about what is possible, +// rather than about one grammar, produces a strictly weaker construct that can +// only decide whether to repeat — no timeout, no rollback-and-retry, no +// parallel, no memoize-on-arguments. It would also have crippled the JS backend, +// which has closures natively, for a limit in the C one. +fn declare_wrap(name: String, wraps: String) -> Void { + state_set("__dec_wrap_" + name, wraps) +} + +fn decorator_wrap(name: String) -> String { + state_get("__dec_wrap_" + name) +} + // scan_declared_decorators — token-level pre-pass registering every construct // the program declares. Runs once per module alongside scan_routes, because // the streaming backend discards per-fn ASTs and there is no whole-program AST @@ -4246,6 +4367,8 @@ fn scan_declared_decorators(tokens: [Any]) -> Void { let pending_guard: String = "" let has_pending_x: Bool = false let pending_exit: String = "" + let has_pending_w: Bool = false + let pending_wrap: String = "" let pos: Int = 0 let going: Bool = true while going { @@ -4296,6 +4419,10 @@ fn scan_declared_decorators(tokens: [Any]) -> Void { let has_pending_x = true let pending_exit = native_list_get(args, 1) } + if str_eq(dkind, "wraps_body") { + let has_pending_w = true + let pending_wrap = native_list_get(args, 1) + } } } let pos = p @@ -4314,6 +4441,10 @@ fn scan_declared_decorators(tokens: [Any]) -> Void { declare_exit(fname, pending_exit) let has_pending_x = false } + if has_pending_w { + declare_wrap(fname, pending_wrap) + let has_pending_w = false + } let pos = pos + 2 } else { let pos = pos + 1