let a construct refuse, not only observe

@authenticate (6 uses), @authorize (3), @rate_limit (3) and @validate (2)
parsed, attached, and compiled to nothing. Fourteen applications that read as
protection and emitted no instruction — a function decorated @authenticate
compiled byte-identically to an undecorated one.

The missing capability was not authentication. It was that a construct could
observe a boundary but never refuse one. injects_at_entry discards the target's
result; there was no form in which a construct could say no.

    @decorator("guards_at_entry", "my_auth")
    fn authenticate() {}

    @authenticate
    @authorize
    fn handler() -> String { ... }

emits, at entry:

    { el_val_t __g = my_auth(EL_STR("handler"), EL_STR("authenticate")); if (__g) return __g; }
    { el_val_t __g = my_roles(EL_STR("handler"), EL_STR("authorize")); if (__g) return __g; }

Guards precede injections because a refused call must not report a crossing,
and every guard runs where the topmost injecting construct wins — refusal is
not a role, so it does not follow the role convention.

The compiler still knows nothing about auth. The program points the construct
at its own function, which is where that decision belongs.

Verified: existing @manager/@accessor output byte-identical, compiler
self-hosts byte-identically, guards stack in declaration order and emit before
the beat. 94/94 native compiler tests pass.
This commit is contained in:
bigmerge
2026-08-17 07:53:10 -05:00
parent 5718943f2e
commit 60737b0305
3 changed files with 88 additions and 0 deletions
+50
View File
@@ -3228,6 +3228,23 @@ fn cg_fn(stmt: Map<String, Any>) -> Void {
// 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.
@@ -4099,6 +4116,29 @@ fn decorator_injection(name: String) -> String {
state_get("__dec_inject_" + name)
}
// A GUARD is an injection that may refuse. The declared target is called at
// entry with the same (fn, construct) pair; a non-zero return short-circuits
// the decorated fn and becomes its result.
//
// @decorator("guards_at_entry", "my_auth_check")
// fn authenticate() {}
//
// @authenticate
// fn handler() -> String { ... } // my_auth_check runs first and may refuse
//
// This is what @authenticate (6 uses), @authorize (3), @rate_limit (3) and
// @validate (2) needed and never had. They parsed, attached, and compiled to
// nothing fourteen applications that read as protection and emitted no
// instruction. The compiler still knows nothing about authentication: the
// program points the construct at its own function.
fn declare_guard(name: String, guards: String) -> Void {
state_set("__dec_guard_" + name, guards)
}
fn decorator_guard(name: String) -> String {
state_get("__dec_guard_" + 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
@@ -4109,6 +4149,8 @@ fn scan_declared_decorators(tokens: [Any]) -> Void {
let total: Int = native_list_len(tokens) / 2
let has_pending: Bool = false
let pending_target: String = ""
let has_pending_g: Bool = false
let pending_guard: String = ""
let pos: Int = 0
let going: Bool = true
while going {
@@ -4151,6 +4193,10 @@ fn scan_declared_decorators(tokens: [Any]) -> Void {
let has_pending = true
let pending_target = native_list_get(args, 1)
}
if str_eq(dkind, "guards_at_entry") {
let has_pending_g = true
let pending_guard = native_list_get(args, 1)
}
}
}
let pos = p
@@ -4161,6 +4207,10 @@ fn scan_declared_decorators(tokens: [Any]) -> Void {
declare_decorator(fname, pending_target)
let has_pending = false
}
if has_pending_g {
declare_guard(fname, pending_guard)
let has_pending_g = false
}
let pos = pos + 2
} else {
let pos = pos + 1