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:
@@ -3254,6 +3254,84 @@ fn cg_exit_target(stmt: Map<String, Any>) -> String {
|
||||
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 {
|
||||
let xdl = stmt["decorators"]
|
||||
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.
|
||||
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<String, Any>) -> 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(<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
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user