diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index d5fe7e2..aad69af 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -3195,6 +3195,98 @@ fn fn_has_decorator(stmt: Map, name: String) -> Bool { false } +// cg_entry_seam — emit the guards and the entry injection for a decorated fn. +// Extracted so it can be emitted into the WRAPPER when a construct injects at +// exit (the body then lives in a static helper) and inline otherwise. +// +// Guards run FIRST and may refuse — a non-zero return short-circuits the fn and +// becomes its result. A refused call must not report a crossing, so guards +// precede injection. EVERY guard runs (stacking @authenticate @authorize +// applies both); injection is topmost-wins, matching the VBD role convention, +// because a role is singular and a refusal is not. +fn cg_entry_seam(stmt: Map, fn_name: String) -> Void { + let gdl = stmt["decorators"] + let n_gdl: Int = native_list_len(gdl) + let gi = 0 + while gi < n_gdl { + let gd = native_list_get(gdl, gi) + let gdn: String = gd["name"] + let g_target: String = decorator_guard(gdn) + if !str_eq(g_target, "") { + emit_line(" { el_val_t __g = " + g_target + "(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit(gdn) + ")); if (__g) return __g; }") + } + let gi = gi + 1 + } + let idl = stmt["decorators"] + let n_idl: Int = native_list_len(idl) + let di = 0 + let did_inject: Bool = false + while di < n_idl { + if !did_inject { + let dd = native_list_get(idl, di) + let ddn: String = dd["name"] + let inj_target: String = decorator_injection(ddn) + if !str_eq(inj_target, "") { + emit_line(" " + inj_target + "(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit(ddn) + "));") + let did_inject = true + } + } + let di = di + 1 + } +} + +// cg_exit_target / cg_exit_construct — the first construct on this fn that +// injects at exit, or "" if none. +fn cg_exit_target(stmt: Map) -> String { + let xdl = stmt["decorators"] + let n_xdl: Int = native_list_len(xdl) + let xi = 0 + let found: String = "" + while xi < n_xdl { + if str_eq(found, "") { + let xd = native_list_get(xdl, xi) + let xdn: String = xd["name"] + let xt: String = decorator_exit(xdn) + if !str_eq(xt, "") { let found = xt } + } + let xi = xi + 1 + } + found +} + +fn cg_exit_construct(stmt: Map) -> String { + let xdl = stmt["decorators"] + let n_xdl: Int = native_list_len(xdl) + let xi = 0 + let found: String = "" + while xi < n_xdl { + if str_eq(found, "") { + let xd = native_list_get(xdl, xi) + let xdn: String = xd["name"] + let xt: String = decorator_exit(xdn) + if !str_eq(xt, "") { let found = xdn } + } + let xi = xi + 1 + } + found +} + +// params_to_call_args — "a, b, c" from the param list, for the wrapper's call +// into the body helper. +fn params_to_call_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 + pn + let i = i + 1 + } + out +} + fn cg_fn(stmt: Map) -> Void { let fn_name: String = stmt["name"] // Skip El's `fn main()` - C provides its own main() for top-level stmts @@ -3216,53 +3308,25 @@ fn cg_fn(stmt: Map) -> Void { // Seed the per-function int-name set so the `+` codegen can dispatch // arithmetic vs concat on type-annotated identifiers. build_int_names_for_params(params) - emit_line("el_val_t " + fn_name + "(" + params_c + ") {") // ── API-reshape decorator-seam: auto-emit at the decorated-fn boundary ── - // Every @manager/@accessor fn gets ONE injected call to engram_boundary_beat - // at entry — interoception (chrono tick) + telemetry (afferent counter) + - // strengthen (self-activity) + a dharma bus event — so a decorated op - // self-reports with ZERO hand-written instrumentation in its body. (VBD role - // = the topmost decorator; write it topmost when stacking with @route.) - // The beat carries the CONSTRUCT that caused it, not only the fn that beat. - // Without it the graph accumulates boundary events with no way to attribute - // them to the decorator responsible — so no construct can ever be measured, - // and "is this decorator earning its keep" stays an argument instead of a - // query. One parameter is the whole difference. - // Guards run FIRST and may refuse — a non-zero return short-circuits the fn - // and becomes its result. A refused call must not beat, so guards precede - // injection. EVERY guard runs (stacking @authenticate @authorize applies - // both), unlike injection where the topmost construct wins. - let gdl = stmt["decorators"] - let n_gdl: Int = native_list_len(gdl) - let gi = 0 - while gi < n_gdl { - let gd = native_list_get(gdl, gi) - let gdn: String = gd["name"] - let g_target: String = decorator_guard(gdn) - if !str_eq(g_target, "") { - emit_line(" { el_val_t __g = " + g_target + "(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit(gdn) + ")); if (__g) return __g; }") - } - let gi = gi + 1 - } - - // Codegen no longer knows which constructs inject. It reads what the - // program declared (see scan_declared_decorators). Topmost decorator wins, - // matching the VBD role convention. - let idl = stmt["decorators"] - let n_idl: Int = native_list_len(idl) - let di = 0 - let did_inject: Bool = false - while di < n_idl { - if !did_inject { - let dd = native_list_get(idl, di) - let ddn: String = dd["name"] - let inj_target: String = decorator_injection(ddn) - if !str_eq(inj_target, "") { - emit_line(" " + inj_target + "(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit(ddn) + "));") - let did_inject = true - } - } - let di = di + 1 + // A decorated op self-reports with ZERO hand-written instrumentation in its + // body. What it reports, and whether it may refuse, is DECLARED by the + // construct — codegen reads it (see scan_declared_decorators). + // + // When a construct injects at exit, the body moves into a static helper and + // the visible fn becomes a wrapper, so EARLY RETURNS still pass through the + // exit injection. Emitting it only before the fall-through return would + // silently miss every early return — which is precisely the class of + // failure this seam exists to remove. Fns with no exit construct emit + // 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, "") + if has_exit { + emit_line("static el_val_t __el_body_" + fn_name + "(" + params_c + ") {") + } else { + emit_line("el_val_t " + fn_name + "(" + params_c + ") {") + cg_entry_seam(stmt, fn_name) } // Seed declared with parameter names so reassignment works let decl = native_list_empty() @@ -3286,6 +3350,17 @@ fn cg_fn(stmt: Map) -> Void { el_release(final_decl) emit_line(" return 0;") emit_line("}") + // 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_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);") + emit_line(" return __r;") + emit_line("}") + } emit_blank() } @@ -4139,6 +4214,24 @@ fn decorator_guard(name: String) -> String { state_get("__dec_guard_" + name) } +// An EXIT injection runs after the fn returns and receives the result: +// target(, , ) +// +// @decorator("injects_at_exit", "persist_now") +// fn durable() {} +// +// This is `hold`'s after-crossing face. §6 of the design records 62 +// persist-after-mutate sites and 9-of-9 failed index-after-append sites — every +// one an obligation that decayed into "remember to do this after you mutate." +// An obligation a human must remember is not an obligation. +fn declare_exit(name: String, exits: String) -> Void { + state_set("__dec_exit_" + name, exits) +} + +fn decorator_exit(name: String) -> String { + state_get("__dec_exit_" + 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 @@ -4151,6 +4244,8 @@ fn scan_declared_decorators(tokens: [Any]) -> Void { let pending_target: String = "" let has_pending_g: Bool = false let pending_guard: String = "" + let has_pending_x: Bool = false + let pending_exit: String = "" let pos: Int = 0 let going: Bool = true while going { @@ -4197,6 +4292,10 @@ fn scan_declared_decorators(tokens: [Any]) -> Void { let has_pending_g = true let pending_guard = native_list_get(args, 1) } + if str_eq(dkind, "injects_at_exit") { + let has_pending_x = true + let pending_exit = native_list_get(args, 1) + } } } let pos = p @@ -4211,6 +4310,10 @@ fn scan_declared_decorators(tokens: [Any]) -> Void { declare_guard(fname, pending_guard) let has_pending_g = false } + if has_pending_x { + declare_exit(fname, pending_exit) + let has_pending_x = false + } let pos = pos + 2 } else { let pos = pos + 1 diff --git a/lang/tests/native/test_compiler.el b/lang/tests/native/test_compiler.el index fce476b..2ed9731 100644 --- a/lang/tests/native/test_compiler.el +++ b/lang/tests/native/test_compiler.el @@ -842,3 +842,36 @@ test "undeclared-guard-emits-nothing" { let out: String = compile_capture(src) assert !str_contains(out, "if (__g)"), "an undeclared construct guards nothing" } + +// ── Declared constructs: exit injection and composition ────────────────────── + +test "declared-exit-wraps-body-so-early-returns-pass-through" { + let src: String = "@decorator(\"injects_at_exit\", \"persist_now\")\nfn durable() {}\n@durable\nfn mutate(k: String) -> Int { if str_eq(k, \"bad\") { return 0 } return 1 }" + let out: String = compile_capture(src) + assert str_contains(out, "static el_val_t __el_body_mutate"), "body moves into a helper" + assert str_contains(out, "persist_now(EL_STR(\"mutate\")"), "exit injection runs" + assert str_contains(out, "return __r;"), "the wrapper returns the body's result" +} + +test "declared-exit-receives-the-result" { + let src: String = "@decorator(\"injects_at_exit\", \"persist_now\")\nfn durable() {}\n@durable\nfn f() -> Int { return 1 }" + let out: String = compile_capture(src) + assert str_contains(out, "__r);"), "the exit target is handed what the fn returned" +} + +test "no-exit-construct-emits-no-wrapper" { + let src: String = "@manager\nfn f() -> Int { return 1 }" + let out: String = compile_capture(src) + assert !str_contains(out, "__el_body_"), "fns without an exit construct are unwrapped, byte for byte as before" +} + +test "constructs-compose-guard-entry-exit" { + let src: String = "@decorator(\"guards_at_entry\", \"my_auth\")\nfn authenticate() {}\n@decorator(\"injects_at_exit\", \"persist_now\")\nfn durable() {}\n@authenticate\n@durable\n@manager\nfn op() -> Int { return 1 }" + let out: String = compile_capture(src) + let g: Int = str_index_of(out, "my_auth(") + let b: Int = str_index_of(out, "engram_boundary_beat(EL_STR(\"op\")") + let x: Int = str_index_of(out, "persist_now(") + assert g < b, "guard before entry injection" + assert b < x, "entry injection before exit injection" + assert x >= 0, "three independent constructs compose on one fn" +}