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:
@@ -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);")
|
||||
|
||||
Reference in New Issue
Block a user