lang: give cross-cutting concerns an owner instead of a convention
El's units of encapsulation are the function and the module. Neither can hold
a concern that belongs to the process, so each one had been expressed the only
way it could be -- as a convention: call this at every site. Conventions of
that shape do not hold. Measured here: zero process-identity guards at any
layer, 20 environment variables each with its default written inline at the
read site, 62 persist call sites, 10 per-route auth checks. One absence, four
times.
Step 0 first, because the premise was wrong. El was believed to have no
middleware or effect mechanism. It has one, and it is already load-bearing:
codegen injects engram_boundary_beat at the entry of every @manager/@accessor
fn, decorators take arguments and stack, dharma_emit from a non-@manager fn is
a #error, and the cgi block injects el_cgi_init at the head of main(). So the
correct move was not to invent a mechanism but to generalize the seam that
already existed. The real gap is narrower and is now recorded: the seam is
prologue-only and its callee is a fixed builtin.
Adds a `program` block -- the third program-level declarative block. cgi and
service declare what a program may do; program declares what it is.
program "engram" {
singleton: "engram"
env ENGRAM_BIND: String = ":8742"
env GUIDE_PORT: Int = "8771"
}
singleton takes an exclusive flock before any user statement runs and refuses a
second start, reporting the holder's pid. It is a lock rather than a pidfile so
the kernel releases it on death including SIGKILL -- no stale state, and so no
"delete the lock file to get unstuck" ritual, which would itself be a
convention. It reports the pid because "already running" is not actionable; a
pid is. That is the direct answer to a stale process surviving a pkill and
going on answering probes.
env entries resolve once at startup -- environment wins, declaration supplies
the fallback -- and validate as a whole, reporting every problem at once rather
than costing one restart per variable. config("X") for an undeclared X is
fatal, because an advisory schema is just another convention. Programs without
a program block are unaffected, so migration is per-program.
Only one keyword is added. `config` and `env` could not become keywords -- both
are real identifiers in the tree -- so the block's fields are read as
identifier token values by its own parse loop and stay usable everywhere else.
The init function is emitted at the block site and called from main() rather
than inlined into main(). The live backend is codegen_streaming, which emits in
source order and cannot hold the entry list alive until main(); this way only a
single bool has to survive.
Also fixes: config() was defined in el_runtime.c but never prototyped in
el_runtime.h, so any el program calling it failed to compile under C99.
Spec: section 18 documents what shipped. Section 9 is corrected -- it claimed
decorators had no structural meaning, which has not been true for some time.
Section 19 designs durability-as-an-epilogue-effect and route authorization
and states plainly why neither is implemented here: both land in files under
concurrent modification, and the prerequisite for both is lifting the seam
from prologue-only to prologue/epilogue.
Self-hosting fixpoint verified byte-identical.
This commit is contained in:
@@ -3265,6 +3265,7 @@ fn is_top_level_decl(stmt: Map<String, Any>) -> Bool {
|
||||
if kind == "EnumDef" { return true }
|
||||
if kind == "Import" { return true }
|
||||
if kind == "CgiBlock" { return true }
|
||||
if kind == "ProgramBlock" { return true }
|
||||
if kind == "ExternFn" { return true }
|
||||
false
|
||||
}
|
||||
@@ -3277,6 +3278,55 @@ fn cgi_arg(value: String, has_value: Bool) -> String {
|
||||
return "EL_NULL"
|
||||
}
|
||||
|
||||
// -- Program block: cross-cutting concerns injected at the process boundary ----
|
||||
//
|
||||
// emit_program_init — emit the `static void __el_program_init(void)` that
|
||||
// carries a program's declared cross-cutting concerns. Called from main()
|
||||
// BEFORE any user statement runs, so the guarantees hold for the whole process
|
||||
// rather than depending on each call site remembering to ask for them.
|
||||
//
|
||||
// This is emitted at the point the `program` block is encountered, not buffered
|
||||
// until main(). The streaming backend emits in source order and cannot hold a
|
||||
// declaration's entry list alive until main(); emitting a named function here
|
||||
// and calling it from main() means only a single bool has to survive.
|
||||
//
|
||||
// Order matters and is deliberate:
|
||||
// 1. singleton FIRST — if another instance already holds the lock, refuse and
|
||||
// exit before touching configuration, ports, or any data directory.
|
||||
// 2. config declarations — resolve env-or-default, one declaration per entry.
|
||||
// 3. validate LAST — report EVERY missing/ill-typed entry at once, then exit.
|
||||
fn el_bool_arg(b: Bool) -> String {
|
||||
if b { return "EL_INT(1)" }
|
||||
return "EL_INT(0)"
|
||||
}
|
||||
|
||||
fn emit_program_init(stmt: Map<String, Any>) -> Void {
|
||||
let pname: String = stmt["name"]
|
||||
emit_line("static void __el_program_init(void) {")
|
||||
let has_singleton: Bool = stmt["has_singleton"]
|
||||
if has_singleton {
|
||||
let sid: String = stmt["singleton"]
|
||||
emit_line(" el_singleton_acquire(EL_STR(" + c_str_lit(sid) + "));")
|
||||
}
|
||||
let entries = stmt["entries"]
|
||||
let n: Int = native_list_len(entries)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let e = native_list_get(entries, i)
|
||||
let ename: String = e["name"]
|
||||
let etype: String = e["etype"]
|
||||
let edefault: String = e["default"]
|
||||
let has_default: Bool = e["has_default"]
|
||||
let erequired: Bool = e["required"]
|
||||
let arg_def: String = cgi_arg(edefault, has_default)
|
||||
emit_line(" el_config_declare(EL_STR(" + c_str_lit(ename) + "), EL_STR(" + c_str_lit(etype) + "), " + arg_def + ", " + el_bool_arg(has_default) + ", " + el_bool_arg(erequired) + ");")
|
||||
let i = i + 1
|
||||
}
|
||||
emit_line(" el_config_validate(EL_STR(" + c_str_lit(pname) + "));")
|
||||
emit_line("}")
|
||||
emit_blank()
|
||||
}
|
||||
|
||||
// -- VBD role enforcement ------------------------------------------------------
|
||||
//
|
||||
// Scan a function body for direct calls to DHARMA-restricted builtins
|
||||
@@ -3599,6 +3649,20 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// Program block: emit the cross-cutting init function before the user's
|
||||
// functions so main() can call it (see emit_program_init).
|
||||
let prog_have: Bool = false
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk4: String = stmt["stmt"]
|
||||
if str_eq(sk4, "ProgramBlock") {
|
||||
emit_program_init(stmt)
|
||||
let prog_have = true
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Function definitions
|
||||
let i = 0
|
||||
while i < n {
|
||||
@@ -3617,6 +3681,9 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
|
||||
// with the C-side parameters when fn main()'s body is folded in below.
|
||||
emit_line("int main(int _argc, char** _argv) {")
|
||||
emit_line(" el_runtime_init_args(_argc, _argv);")
|
||||
if prog_have {
|
||||
emit_line(" __el_program_init();")
|
||||
}
|
||||
if cgi_count >= 1 {
|
||||
let cname: String = cgi_block["name"]
|
||||
let cdid: String = cgi_block["dharma_id"]
|
||||
@@ -4210,6 +4277,7 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
// Fix: copy the values out BEFORE the release (strings, so no dangling reference)
|
||||
// and emit from these. No search, so the failure mode is removed rather than moved.
|
||||
let cgi_have: Bool = false
|
||||
let prog_have: Bool = false
|
||||
let cgi_name_v: String = ""
|
||||
let cgi_did_v: String = ""
|
||||
let cgi_prin_v: String = ""
|
||||
@@ -4331,6 +4399,14 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
// These are no-ops in codegen (forward decls already emitted)
|
||||
// — except a CgiBlock, whose declared identity must survive
|
||||
// this release to be emitted as a compiled constant.
|
||||
// A ProgramBlock's cross-cutting declarations are
|
||||
// emitted HERE, as a named init function, because the
|
||||
// streaming backend cannot hold the entry list alive
|
||||
// until main(). Only the bool survives.
|
||||
if str_eq(sk, "ProgramBlock") {
|
||||
emit_program_init(stmt)
|
||||
let prog_have = true
|
||||
}
|
||||
if str_eq(sk, "CgiBlock") {
|
||||
let cgi_have = true
|
||||
let cgi_name_v = stmt["name"]
|
||||
@@ -4477,6 +4553,13 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
let kind2: String = state_get("__program_kind")
|
||||
emit_line("int main(int _argc, char** _argv) {")
|
||||
emit_line(" el_runtime_init_args(_argc, _argv);")
|
||||
// Cross-cutting concerns declared by a `program` block run BEFORE anything
|
||||
// else — a singleton violation must refuse the start before this process
|
||||
// touches a port or a data directory, and configuration must be validated
|
||||
// before the first read of it rather than at each read site.
|
||||
if prog_have {
|
||||
emit_line(" __el_program_init();")
|
||||
}
|
||||
|
||||
// cgi init if needed
|
||||
let ns2: Int = native_list_len(sigs)
|
||||
|
||||
Reference in New Issue
Block a user