From 60737b030547b3901da53b06b4c406907ce5df54 Mon Sep 17 00:00:00 2001 From: bigmerge Date: Mon, 17 Aug 2026 07:53:10 -0500 Subject: [PATCH] let a construct refuse, not only observe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @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. --- lang/el-compiler/src/codegen.el | 50 ++++++++++++++++++++++++++++++ lang/spec/language.md | 1 + lang/tests/native/test_compiler.el | 37 ++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index 89e2ee9..d5fe7e2 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -3228,6 +3228,23 @@ fn cg_fn(stmt: Map) -> 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 diff --git a/lang/spec/language.md b/lang/spec/language.md index bdf82e5..ab23710 100644 --- a/lang/spec/language.md +++ b/lang/spec/language.md @@ -461,6 +461,7 @@ The `@` token followed by an identifier attaches a decorator to the next `FnDef` |---|---| | `@manager` | Permits calls to `dharma_emit` / `dharma_field`. Calling either from a non-`@manager` fn emits a `#error` into the generated C — a compile-time failure, not a lint. | | `@manager`, `@accessor` | Codegen injects one call to `engram_boundary_beat(, )` at function entry, where `` is the decorator that caused the beat. The decorated op self-reports (chrono tick, afferent counter, self-activity strengthen, dharma bus event carrying `{"construct":"..."}`) with **zero** hand-written instrumentation in its body. Without the construct argument the graph accumulates boundary events with no attribution, so no construct can be measured. | +| `@decorator(kind, target)` | **Declares a construct.** The decorated `fn`'s name becomes a usable decorator whose meaning is `target`. Codegen reads the declaration; it does not know the construct. Adding a construct is a declaration in the program, not a compiler edit. Two kinds exist: `"injects_at_entry"` calls `target(, )` at entry, result discarded — this is what `@manager`/`@accessor` are, seeded as the compiled-in core. `"guards_at_entry"` calls `target(, )` at entry and a **non-zero return short-circuits the decorated fn and becomes its result**. Guards run before injections (a refused call must not report a crossing) and *every* guard on a fn runs, whereas the topmost injecting construct wins. The compiler knows nothing about authentication, rate limiting or validation: the program points the construct at its own function. | | `@route(path, method, …)` | Records a route into a generated dispatch table. | Decorators with no registered meaning are accepted and ignored. diff --git a/lang/tests/native/test_compiler.el b/lang/tests/native/test_compiler.el index bf12fb0..fce476b 100644 --- a/lang/tests/native/test_compiler.el +++ b/lang/tests/native/test_compiler.el @@ -805,3 +805,40 @@ test "builtin-constructs-still-inject" { assert str_contains(out, "EL_STR(\"manager\")"), "seeded manager still injects" assert str_contains(out, "EL_STR(\"accessor\")"), "seeded accessor still injects" } + +// ── Declared constructs: guards ────────────────────────────────────────────── +// +// A guard is an injection that may refuse. Non-zero return short-circuits the +// decorated fn. This is what @authenticate/@authorize/@rate_limit/@validate +// needed and never had — fourteen applications that read as protection and +// emitted no instruction. + +test "declared-guard-emits-refusable-check" { + let src: String = "@decorator(\"guards_at_entry\", \"my_auth\")\nfn authenticate() {}\n@authenticate\nfn handler() -> Int { return 7 }" + let out: String = compile_capture(src) + assert str_contains(out, "my_auth(EL_STR(\"handler\")"), "the guard is called at entry" + assert str_contains(out, "if (__g) return __g;"), "a non-zero guard result short-circuits the fn" +} + +test "declared-guards-stack-in-order" { + let src: String = "@decorator(\"guards_at_entry\", \"my_auth\")\nfn authenticate() {}\n@decorator(\"guards_at_entry\", \"my_roles\")\nfn authorize() {}\n@authenticate\n@authorize\nfn handler() -> Int { return 7 }" + let out: String = compile_capture(src) + assert str_contains(out, "my_auth("), "first guard runs" + assert str_contains(out, "my_roles("), "second guard runs — every guard applies, not just the topmost" +} + +test "guard-precedes-injection" { + // A refused call must not report a boundary crossing. + let src: String = "@decorator(\"guards_at_entry\", \"my_auth\")\nfn authenticate() {}\n@authenticate\n@manager\nfn handler() -> Int { return 7 }" + 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(\"handler\")") + assert g < b, "the guard is emitted before the beat" + assert g >= 0, "guard present" +} + +test "undeclared-guard-emits-nothing" { + let src: String = "@not_a_declared_guard\nfn handler() -> Int { return 7 }" + let out: String = compile_capture(src) + assert !str_contains(out, "if (__g)"), "an undeclared construct guards nothing" +}