From 5718943f2e4d98bdee836530a2919e9a1e868522 Mon Sep 17 00:00:00 2001 From: bigmerge Date: Mon, 17 Aug 2026 07:50:56 -0500 Subject: [PATCH] let a construct declare its own meaning instead of the emitter knowing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit codegen called fn_has_decorator for exactly three names — manager, accessor, route. Twelve others parsed, attached as {name,args}, and compiled to nothing, including four that look like protection: @authenticate (6 uses), @authorize (3), @rate_limit (3), @validate (2). The cause was not that the branches were untidy. A construct had nothing to BE, so its meaning had nowhere to live except the emitter, and every construct was therefore a compiler edit. A name -> injection table would have moved the enumeration twenty lines up without removing it. So the construct now carries its own meaning: @decorator("injects_at_entry", "engram_boundary_beat") fn audited() {} @audited fn risky_op() -> Int { ... } // gets the beat, attributed to "audited" scan_declared_decorators is a token-level pre-pass beside scan_routes, forced by streaming codegen having no whole-program AST. manager and accessor are seeded as the compiled-in core — the fixedSelf shape from substrate.go: a complete fallback exists, declaration is enrichment. This is the injection half of the seam only. The prohibition half (@manager's #error on dharma_emit) stays hardcoded, because "which calls may appear inside this boundary" is a query over program structure and there is nothing yet to ask. Verified three ways: emitted C for existing @manager/@accessor code is byte-identical to the hardcoded path; a construct with a name the compiler has never heard of injects correctly; the compiler self-hosts byte-identically. 90/90 native compiler tests pass. --- lang/el-compiler/src/codegen.el | 140 +++++++++++++++++++++++++++-- lang/tests/native/test_compiler.el | 33 +++++++ 2 files changed, 168 insertions(+), 5 deletions(-) diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index 7603b36..89e2ee9 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -3228,12 +3228,24 @@ 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. - if fn_has_decorator(stmt, "manager") { - emit_line(" engram_boundary_beat(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit("manager") + "));") - } else { - if fn_has_decorator(stmt, "accessor") { - emit_line(" engram_boundary_beat(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit("accessor") + "));") + // 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 } // Seed declared with parameter names so reassignment works let decl = native_list_empty() @@ -4046,6 +4058,119 @@ fn program_has_routes(recs: [Map]) -> Bool { native_list_len(recs) > 0 } +// ── Declared constructs ────────────────────────────────────────────────────── +// +// A construct declares its own meaning. Codegen READS the declaration instead +// of knowing the construct: +// +// @decorator("injects_at_entry", "engram_boundary_beat") +// fn manager() {} +// +// Any fn decorated @manager then gets +// engram_boundary_beat(EL_STR(), EL_STR("manager")); +// injected at entry. Adding a construct is a declaration IN THE PROGRAM. It +// does not touch the compiler. +// +// WHY: codegen called fn_has_decorator for exactly three names — manager, +// accessor, route. Twelve others parsed, attached as {name,args}, and compiled +// to nothing, including four that look like protection: @authenticate (6 uses), +// @authorize (3), @rate_limit (3), @validate (2). The meaning of a construct +// had nowhere to live except the emitter, so every construct was a compiler +// edit and an undeclared one was silently inert. A lookup table of +// name -> injection would have moved the enumeration twenty lines up, not +// removed it; the class only goes away when the construct itself carries its +// meaning. +// +// manager and accessor are seeded below as the compiled-in core — a seed the +// periphery is declared against, the same shape as fixedSelf in substrate.go: +// a complete fallback exists, declaration is enrichment. A program may declare +// its own constructs and may redeclare these. +// +// This is the injection half of the seam. The prohibition half (@manager's +// #error on dharma_emit) is still hardcoded, because "which calls may appear +// inside this boundary" is a query over program structure and there is nothing +// yet to ask. + +fn declare_decorator(name: String, injects: String) -> Void { + state_set("__dec_inject_" + name, injects) +} + +fn decorator_injection(name: String) -> String { + state_get("__dec_inject_" + 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 +// to walk. +fn scan_declared_decorators(tokens: [Any]) -> Void { + declare_decorator("manager", "engram_boundary_beat") + declare_decorator("accessor", "engram_boundary_beat") + let total: Int = native_list_len(tokens) / 2 + let has_pending: Bool = false + let pending_target: String = "" + let pos: Int = 0 + let going: Bool = true + while going { + if pos >= total { + let going = false + } else { + let k: String = tok_kind(tokens, pos) + if str_eq(k, "Eof") { + let going = false + } else { + if str_eq(k, "At") { + let dname: String = tok_value(tokens, pos + 1) + let p: Int = pos + 2 + let args: [String] = native_list_empty() + let ka: String = tok_kind(tokens, p) + if str_eq(ka, "LParen") { + let p = p + 1 + let running: Bool = true + while running { + let kd: String = tok_kind(tokens, p) + if str_eq(kd, "RParen") { + let running = false + } else { + if str_eq(kd, "Eof") { + let running = false + } else { + if str_eq(kd, "Str") { + let args = native_list_append(args, tok_value(tokens, p)) + } + let p = p + 1 + } + } + } + if str_eq(tok_kind(tokens, p), "RParen") { let p = p + 1 } + } + if str_eq(dname, "decorator") { + if native_list_len(args) >= 2 { + let dkind: String = native_list_get(args, 0) + if str_eq(dkind, "injects_at_entry") { + let has_pending = true + let pending_target = native_list_get(args, 1) + } + } + } + let pos = p + } else { + if str_eq(k, "Fn") { + let fname: String = tok_value(tokens, pos + 1) + if has_pending { + declare_decorator(fname, pending_target) + let has_pending = false + } + let pos = pos + 2 + } else { + let pos = pos + 1 + } + } + } + } + } +} + // route_method_guard — C boolean prefix guarding on HTTP method, or "" for none. fn route_method_guard(method: String) -> String { if str_eq(method, "") { return "" } @@ -4208,6 +4333,11 @@ fn codegen_streaming(tokens: [Any], sigs: [Map], source: String) -> // tokens below via refcount, like `sigs`). If any exist, forward-declare the // generated dispatcher NOW so hand-written fns (e.g. handle_request) may call // it before its definition is emitted after the fn-emit loop. + // Declared constructs: register what the program says its decorators mean, + // before any fn is emitted. Must precede the fn-emit loop — cg_fn reads the + // registry to decide what, if anything, a decorator injects. + scan_declared_decorators(tokens) + let route_records: [Map] = scan_routes(tokens) if program_has_routes(route_records) { emit_line("el_val_t el_route_dispatch(el_val_t method, el_val_t clean, el_val_t path, el_val_t body);") diff --git a/lang/tests/native/test_compiler.el b/lang/tests/native/test_compiler.el index 4f68f78..bf12fb0 100644 --- a/lang/tests/native/test_compiler.el +++ b/lang/tests/native/test_compiler.el @@ -772,3 +772,36 @@ test "decorator-authenticate-compiles-to-nothing" { let bare: String = compile_capture("fn f() -> Int { return 1 }") assert str_eq(out, bare), "KNOWN DEFECT: @authenticate emits identical C to no decorator at all" } + +// ── Declared constructs ────────────────────────────────────────────────────── +// +// A construct declares its own meaning and codegen reads it. Adding a +// construct is a declaration in the program; it does not touch the compiler. + +test "declared-construct-injects-without-compiler-knowledge" { + let src: String = "@decorator(\"injects_at_entry\", \"engram_boundary_beat\")\nfn audited() {}\n@audited\nfn risky() -> Int { return 7 }" + let out: String = compile_capture(src) + assert str_contains(out, "engram_boundary_beat(EL_STR(\"risky\")"), "a program-declared construct injects" + assert str_contains(out, "EL_STR(\"audited\")"), "the beat is attributed to the declared construct" +} + +test "declared-construct-name-unknown-to-codegen" { + // The name is arbitrary. Nothing in the compiler mentions it. + let src: String = "@decorator(\"injects_at_entry\", \"engram_boundary_beat\")\nfn zzq_unlikely_name() {}\n@zzq_unlikely_name\nfn f() -> Int { return 1 }" + let out: String = compile_capture(src) + assert str_contains(out, "EL_STR(\"zzq_unlikely_name\")"), "an arbitrary construct name works" +} + +test "undeclared-construct-still-injects-nothing" { + let src: String = "@nobody_declared_this\nfn f() -> Int { return 1 }" + let out: String = compile_capture(src) + assert !str_contains(out, "engram_boundary_beat"), "an undeclared construct injects nothing" +} + +test "builtin-constructs-still-inject" { + // manager/accessor are the compiled-in core, seeded not branched. + let src: String = "@manager\nfn m() -> Int { return 1 }\n@accessor\nfn a() -> Int { return 2 }" + let out: String = compile_capture(src) + assert str_contains(out, "EL_STR(\"manager\")"), "seeded manager still injects" + assert str_contains(out, "EL_STR(\"accessor\")"), "seeded accessor still injects" +}