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