let a construct declare its own meaning instead of the emitter knowing it

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.
This commit is contained in:
bigmerge
2026-08-17 07:50:56 -05:00
parent dcaa77d77b
commit 5718943f2e
2 changed files with 168 additions and 5 deletions
+135 -5
View File
@@ -3228,12 +3228,24 @@ 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.
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<String, Any>]) -> 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(<fn>), 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<String, Any>], 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<String, Any>] = 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);")
+33
View File
@@ -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"
}