give a construct its after-crossing face, and let constructs compose
§6 records 62 persist-after-mutate sites, 10 auth-per-route, and
index-after-append that failed at 9 of 9 — every one an obligation at a
crossing that decayed into "remember to do this afterwards." An obligation a
human must remember is not an obligation, and the 9-of-9 figure is what that
costs.
@decorator("injects_at_exit", "persist_now")
fn durable() {}
The body moves into a static helper and the visible fn becomes a wrapper, so
EARLY RETURNS pass through the exit injection. Emitting it only before the
fall-through return would have silently missed every early return — the exact
failure class this seam exists to remove. Fns with no exit construct emit
byte-identically to before.
Three independent constructs now compose on one fn, none known to the compiler:
el_val_t mutate(el_val_t k) {
{ el_val_t __g = my_auth(EL_STR("mutate"), EL_STR("authenticate")); if (__g) return __g; }
engram_boundary_beat(EL_STR("mutate"), EL_STR("manager"));
el_val_t __r = __el_body_mutate(k);
persist_now(EL_STR("mutate"), EL_STR("durable"), __r);
return __r;
}
Guard, then entry, then body, then exit. §5.2 asked whether `hold` is one
construct or two; the implementation answers one construct with two faces,
selected by declared kind rather than by two mechanisms.
Verified: existing output byte-identical, compiler self-hosts byte-identically,
early returns pass through the exit, ordering holds under composition. 98/98
native compiler tests pass.
This commit is contained in:
+149
-46
@@ -3195,6 +3195,98 @@ fn fn_has_decorator(stmt: Map<String, Any>, 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<String, Any>, 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, Any>) -> 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, Any>) -> 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<String, Any>) -> 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<String, Any>) -> 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<String, Any>) -> 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(<fn>, <construct>, <result>)
|
||||
//
|
||||
// @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
|
||||
|
||||
Reference in New Issue
Block a user