diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index ad46530..68bf3db 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -3378,9 +3378,25 @@ fn cg_fn(stmt: Map) -> Void { // from @manager-decorated functions. Surface violations to the C compiler // via #error directives emitted before the function definition. Read the // decorator LIST so the role may be stacked with other decorators. - if vbd_has_restricted_call(body) { - if !fn_has_decorator(stmt, "manager") { - emit_line("#error \"VBD violation: dharma_emit/dharma_field called from non-@manager fn '" + fn_name + "'\"") + // Check each DECLARED prohibition in turn. The owning construct is known by + // construction, so the diagnostic names it rather than hardcoding one rule. + let pcs: String = prohibiting_constructs() + if !str_eq(pcs, "") { + let pc_list = str_split(pcs, ",") + let n_pc: Int = native_list_len(pc_list) + let pci = 0 + while pci < n_pc { + let pc: String = str_trim(native_list_get(pc_list, pci)) + if !str_eq(pc, "") { + state_set("__prohibit_active", prohibition_names(pc)) + if vbd_has_restricted_call(body) { + if !fn_has_decorator(stmt, pc) { + emit_line("#error \"boundary violation: " + prohibition_names(pc) + " may only be called from an @" + pc + " fn, but '" + fn_name + "' is not one\"") + } + } + state_set("__prohibit_active", "") + } + let pci = pci + 1 } } // Seed the per-function int-name set so the `+` codegen can dispatch @@ -3561,8 +3577,15 @@ fn emit_program_init(stmt: Map) -> Void { // (dharma_emit, dharma_field). These may only appear inside @manager fns. fn vbd_is_restricted_name(name: String) -> Bool { - if str_eq(name, "dharma_emit") { return true } - if str_eq(name, "dharma_field") { return true } + let active: String = state_get("__prohibit_active") + if str_eq(active, "") { return false } + let parts = str_split(active, ",") + let n: Int = native_list_len(parts) + let i = 0 + while i < n { + if str_eq(str_trim(native_list_get(parts, i)), name) { return true } + let i = i + 1 + } false } @@ -4353,6 +4376,40 @@ fn decorator_wrap(name: String) -> String { state_get("__dec_wrap_" + name) } +// A PROHIBITION is the other half of a boundary: not what runs when something +// crosses, but what may not cross at all. +// +// @decorator("prohibits_outside", "dharma_emit,dharma_field") +// fn manager() {} +// +// Calls to those names may then appear ONLY inside a fn carrying @manager. +// Before this the rule lived as two string literals in vbd_is_restricted_name +// and one #error in cg_fn -- one prohibition, uneditable without a compiler +// release, and unrankable because a #error is a string handed to cpp, which +// knows nothing about El. +// +// The construct list is kept in one state key so the recursive walker can be +// parameterised without threading an argument through every branch of it -- +// the same mechanism codegen already uses for __match_counter and +// __if_expr_counter. +fn declare_prohibition(construct: String, names_csv: String) -> Void { + let known: String = state_get("__prohibit_constructs") + if str_eq(known, "") { + state_set("__prohibit_constructs", construct) + } else { + state_set("__prohibit_constructs", known + "," + construct) + } + state_set("__prohibit_names_" + construct, names_csv) +} + +fn prohibition_names(construct: String) -> String { + state_get("__prohibit_names_" + construct) +} + +fn prohibiting_constructs() -> String { + state_get("__prohibit_constructs") +} + // 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 @@ -4360,6 +4417,7 @@ fn decorator_wrap(name: String) -> String { fn scan_declared_decorators(tokens: [Any]) -> Void { declare_decorator("manager", "engram_boundary_beat") declare_decorator("accessor", "engram_boundary_beat") + declare_prohibition("manager", "dharma_emit,dharma_field") let total: Int = native_list_len(tokens) / 2 let has_pending: Bool = false let pending_target: String = "" @@ -4369,6 +4427,8 @@ fn scan_declared_decorators(tokens: [Any]) -> Void { let pending_exit: String = "" let has_pending_w: Bool = false let pending_wrap: String = "" + let has_pending_p: Bool = false + let pending_prohibit: String = "" let pos: Int = 0 let going: Bool = true while going { @@ -4423,6 +4483,10 @@ fn scan_declared_decorators(tokens: [Any]) -> Void { let has_pending_w = true let pending_wrap = native_list_get(args, 1) } + if str_eq(dkind, "prohibits_outside") { + let has_pending_p = true + let pending_prohibit = native_list_get(args, 1) + } } } let pos = p @@ -4445,6 +4509,10 @@ fn scan_declared_decorators(tokens: [Any]) -> Void { declare_wrap(fname, pending_wrap) let has_pending_w = false } + if has_pending_p { + declare_prohibition(fname, pending_prohibit) + let has_pending_p = false + } let pos = pos + 2 } else { let pos = pos + 1 diff --git a/lang/tests/native/test_compiler.el b/lang/tests/native/test_compiler.el index 2ed9731..ed4ee1b 100644 --- a/lang/tests/native/test_compiler.el +++ b/lang/tests/native/test_compiler.el @@ -875,3 +875,31 @@ test "constructs-compose-guard-entry-exit" { assert b < x, "entry injection before exit injection" assert x >= 0, "three independent constructs compose on one fn" } + +// ── Declared constructs: wraps and prohibitions ────────────────────────────── + +test "declared-wrap-emits-closure-and-convention" { + let src: String = "@decorator(\"wraps_body\", \"with_timeout\")\nfn timed() {}\n@timed\nfn slow(k: Int) -> Int { return 9 }" + let out: String = compile_capture(src) + assert str_contains(out, "struct __env_slow"), "captured environment is emitted" + assert str_contains(out, "__thunk_slow(void* __v)"), "a thunk taking void* is emitted" + assert str_contains(out, "extern el_val_t with_timeout(el_val_t, el_val_t, el_val_t(*)(void*), void*);"), "codegen emits the calling convention — El's single type cannot describe a callable" +} + +test "declared-prohibition-fires-outside-the-boundary" { + let src: String = "@decorator(\"prohibits_outside\", \"raw_sql\")\nfn repository() {}\nfn sneaky() -> Int { raw_sql(\"DROP\") return 1 }" + let out: String = compile_capture(src) + assert str_contains(out, "raw_sql may only be called from an @repository fn"), "a program-declared prohibition is enforced" +} + +test "declared-prohibition-permits-inside-the-boundary" { + let src: String = "@decorator(\"prohibits_outside\", \"raw_sql\")\nfn repository() {}\n@repository\nfn allowed() -> Int { raw_sql(\"SELECT\") return 1 }" + let out: String = compile_capture(src) + assert !str_contains(out, "raw_sql may only be called"), "the owning construct permits the call" +} + +test "seeded-vbd-prohibition-still-enforced" { + let src: String = "fn leaky() -> Int { dharma_emit(\"x\", \"y\") return 1 }" + let out: String = compile_capture(src) + assert str_contains(out, "may only be called from an @manager fn"), "the compiled-in core prohibition survives being declared rather than branched" +}