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.
This commit is contained in:
bigmerge
2026-08-17 08:08:37 -05:00
parent 4f7568b07f
commit 2bed8483f7
+132 -1
View File
@@ -3254,6 +3254,84 @@ fn cg_exit_target(stmt: Map<String, Any>) -> String {
found found
} }
fn cg_wrap_target(stmt: Map<String, Any>) -> 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, Any>) -> 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, Any>) -> String { fn cg_exit_construct(stmt: Map<String, Any>) -> String {
let xdl = stmt["decorators"] let xdl = stmt["decorators"]
let n_xdl: Int = native_list_len(xdl) let n_xdl: Int = native_list_len(xdl)
@@ -3321,7 +3399,10 @@ fn cg_fn(stmt: Map<String, Any>) -> Void {
// exactly as before, byte for byte. // exactly as before, byte for byte.
let exit_target: String = cg_exit_target(stmt) let exit_target: String = cg_exit_target(stmt)
let exit_construct: String = cg_exit_construct(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 { if has_exit {
emit_line("static el_val_t __el_body_" + fn_name + "(" + params_c + ") {") emit_line("static el_val_t __el_body_" + fn_name + "(" + params_c + ") {")
} else { } else {
@@ -3353,11 +3434,30 @@ fn cg_fn(stmt: Map<String, Any>) -> Void {
// The wrapper: guards, entry injection, the body call, then the exit // The wrapper: guards, entry injection, the body call, then the exit
// injection, which receives the result so it can observe what the fn // injection, which receives the result so it can observe what the fn
// actually returned. // 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 { if has_exit {
emit_line("el_val_t " + fn_name + "(" + params_c + ") {") emit_line("el_val_t " + fn_name + "(" + params_c + ") {")
cg_entry_seam(stmt, fn_name) cg_entry_seam(stmt, fn_name)
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) + ");") 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(" " + exit_target + "(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit(exit_construct) + "), __r);")
}
emit_line(" return __r;") emit_line(" return __r;")
emit_line("}") emit_line("}")
} }
@@ -4232,6 +4332,27 @@ fn decorator_exit(name: String) -> String {
state_get("__dec_exit_" + name) 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(<fn>, <construct>, 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 // scan_declared_decorators token-level pre-pass registering every construct
// the program declares. Runs once per module alongside scan_routes, because // 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 // 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 pending_guard: String = ""
let has_pending_x: Bool = false let has_pending_x: Bool = false
let pending_exit: String = "" let pending_exit: String = ""
let has_pending_w: Bool = false
let pending_wrap: String = ""
let pos: Int = 0 let pos: Int = 0
let going: Bool = true let going: Bool = true
while going { while going {
@@ -4296,6 +4419,10 @@ fn scan_declared_decorators(tokens: [Any]) -> Void {
let has_pending_x = true let has_pending_x = true
let pending_exit = native_list_get(args, 1) 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 let pos = p
@@ -4314,6 +4441,10 @@ fn scan_declared_decorators(tokens: [Any]) -> Void {
declare_exit(fname, pending_exit) declare_exit(fname, pending_exit)
let has_pending_x = false let has_pending_x = false
} }
if has_pending_w {
declare_wrap(fname, pending_wrap)
let has_pending_w = false
}
let pos = pos + 2 let pos = pos + 2
} else { } else {
let pos = pos + 1 let pos = pos + 1