let a construct declare what may not cross it

The other half of a boundary: not what runs when something crosses, but what
may not cross at all. It was two string literals in vbd_is_restricted_name and
one #error in cg_fn — one prohibition, uneditable without a compiler release.

    @decorator("prohibits_outside", "raw_sql")
    fn repository() {}

    fn sneaky() -> Int { raw_sql("DROP") }
    // #error "boundary violation: raw_sql may only be called from an
    //          @repository fn, but 'sneaky' is not one"

The recursive matcher is parameterised through a state key rather than by
threading an argument through every branch of the walk — the mechanism codegen
already uses for __match_counter and __if_expr_counter. Each prohibition is
checked in its own turn, so the owning construct is known by construction and
the diagnostic names it instead of hardcoding one rule's wording.

PREDICTIONS AND RESULTS
  1 the 3 duplicated uniqueness rules are textually identical    TRUE
  2 a declared prohibition reproduces @manager's #error          TRUE
  3 existing output byte-identical                               TRUE
  4 a program can declare its own prohibition                    TRUE
  5 fixpoint holds                                               TRUE

I misread result 2 on first pass: a @manager fn calling dharma_emit still
emitted one #error, which looked like a failure. It is the CAPABILITY-tier rule
at codegen.el:2578, a separate prohibition system, and it fires identically on
the pre-change compiler.

MEASURED DEFECTS STILL OPEN
  - two independent prohibition systems (VBD constructs, capability tiers);
    only the first is declarable
  - 3 uniqueness rules written 6 times, once per codegen path, kept in sync by
    hand and identical today

102/102 native compiler tests pass, compiler self-hosts byte-identically.
This commit is contained in:
bigmerge
2026-08-17 08:15:27 -05:00
parent 7d01608a9d
commit 1b324a071f
2 changed files with 101 additions and 5 deletions
+73 -5
View File
@@ -3378,9 +3378,25 @@ fn cg_fn(stmt: Map<String, Any>) -> 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<String, Any>) -> 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
+28
View File
@@ -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"
}