compiler: capability-kind enforcement (cgi / service / utility)

Capability becomes a compile-time structural property, not a runtime
convention. A program's top-level block determines what runtime
primitives it may call; the codegen rejects forbidden calls with
#error directives so cc fails with a clear message.

Three kinds:
  cgi      — full self-formation. All primitives.
  service  — bounded. Cannot call self-formation primitives:
             llm_call_agentic, llm_register_tool, dharma_emit,
             dharma_field. Single-turn LLM calls allowed.
  utility  — default (no top-level block). No DHARMA, no LLM.
             Pure compute + I/O.

Deep claim: the binary either CAN or CANNOT do a thing. There is no
runtime check, no opt-in, no override. A weather service compiled
with `service { ... }` is structurally incapable of becoming Neuron.
Sponsors of services know exactly what they're vouching for.

Implementation
- Lexer: `service` keyword.
- Parser: parse_service_block parallels parse_cgi_block. Produces
  ServiceBlock AST with name/sponsor/domain.
- Codegen entry: scans top-level for cgi/service blocks, sets
  __program_kind state ("cgi" / "service" / "utility"). Rejects
  programs declaring both kinds.
- cg_expr Call: cap_check_call(fn_name) per emission. Records
  violations in __cap_violations CSV. emit_cap_violations() writes
  one #error per violation at end of generated C.
- Helpers: is_self_formation_call, is_dharma_call, is_llm_call.

Tests verified:
  cgi + llm_call_agentic        → compiles ✓
  service + llm_call_agentic    → cc fails with capability violation
                                  for 'service' on 'llm_call_agentic'
  service + llm_call (1-turn)   → compiles ✓
  utility + dharma_send         → cc fails with capability violation
                                  for 'utility' on 'dharma_send'
  utility + http/json/state     → compiles + runs ✓ ("got: world")
  cgi + dharma_emit (manager)   → compiles ✓ (VBD also enforced)
  cgi + dharma_emit (engine)    → cc fails with VBD violation

Three-stage closure: stage1.c == stage2.c (byte-identical).
Engram rebuilt against new compiler — daemon on :8742 healthy,
{"node_count":0,"edge_count":0}.

A bug found and fixed during testing: cap_record_violation had
`csv = ","` (bare assignment, not valid in El) instead of
`let csv = ","`. Without the let, the leading comma never made
it into the accumulator, off-by-one'ing the kind extraction so
"service" appeared as "ervice" in error messages. Pattern
fixed; this confirms once more that El requires `let X = ...`
for all rebindings (codegen converts to assignment when X is
already declared).
This commit is contained in:
Will Anderson
2026-04-30 14:18:17 -05:00
parent 12d5e7777e
commit 5adc05aa48
7 changed files with 617 additions and 3 deletions
BIN
View File
Binary file not shown.
+209 -1
View File
@@ -53,6 +53,12 @@ el_val_t params_to_c(el_val_t params);
el_val_t transform_implicit_return(el_val_t body);
el_val_t is_int_name(el_val_t name);
el_val_t is_int_call(el_val_t call_expr);
el_val_t cap_record_violation(el_val_t kind, el_val_t fn_name);
el_val_t is_self_formation_call(el_val_t fn_name);
el_val_t is_dharma_call(el_val_t fn_name);
el_val_t is_llm_call(el_val_t fn_name);
el_val_t cap_check_call(el_val_t fn_name);
el_val_t emit_cap_violations(void);
el_val_t add_int_name(el_val_t name);
el_val_t build_int_names_for_params(el_val_t params);
el_val_t cg_fn(el_val_t stmt);
@@ -409,6 +415,9 @@ el_val_t keyword_kind(el_val_t word) {
if (str_eq(word, EL_STR("cgi"))) {
return EL_STR("Cgi");
}
if (str_eq(word, EL_STR("service"))) {
return EL_STR("Service");
}
if (str_eq(word, EL_STR("manager"))) {
return EL_STR("Manager");
}
@@ -1562,6 +1571,43 @@ el_val_t parse_stmt(el_val_t tokens, el_val_t pos) {
p = expect(tokens, p, EL_STR("RBrace"));
return make_result(el_map_new(10, "stmt", EL_STR("CgiBlock"), "name", name, "dharma_id", dharma_id, "principal", principal, "network", network, "engram", engram, "has_dharma_id", has_dharma_id, "has_principal", has_principal, "has_network", has_network, "has_engram", has_engram), p);
}
if (str_eq(k, EL_STR("Service"))) {
el_val_t p = (pos + 1);
el_val_t name = tok_value(tokens, p);
p = (p + 1);
p = expect(tokens, p, EL_STR("LBrace"));
el_val_t sponsor = EL_STR("");
el_val_t domain = EL_STR("");
el_val_t running = 1;
while (running) {
el_val_t k2 = tok_kind(tokens, p);
if (str_eq(k2, EL_STR("RBrace"))) {
running = 0;
} else {
if (str_eq(k2, EL_STR("Eof"))) {
running = 0;
} else {
el_val_t fname = tok_value(tokens, p);
p = (p + 1);
p = expect(tokens, p, EL_STR("Colon"));
el_val_t fval = tok_value(tokens, p);
p = (p + 1);
if (str_eq(fname, EL_STR("sponsor"))) {
sponsor = fval;
}
if (str_eq(fname, EL_STR("domain"))) {
domain = fval;
}
el_val_t k3 = tok_kind(tokens, p);
if (str_eq(k3, EL_STR("Comma"))) {
p = (p + 1);
}
}
}
}
p = expect(tokens, p, EL_STR("RBrace"));
return make_result(el_map_new(4, "stmt", EL_STR("ServiceBlock"), "name", name, "sponsor", sponsor, "domain", domain), p);
}
el_val_t r = parse_expr(tokens, pos);
el_val_t val = el_get_field(r, EL_STR("node"));
el_val_t p = el_get_field(r, EL_STR("pos"));
@@ -1924,6 +1970,7 @@ el_val_t cg_expr(el_val_t expr) {
}
if (str_eq(func_kind, EL_STR("Ident"))) {
el_val_t fn_name = el_get_field(func, EL_STR("name"));
cap_check_call(fn_name);
return el_str_concat(el_str_concat(el_str_concat(fn_name, EL_STR("(")), args_c), EL_STR(")"));
}
if (str_eq(func_kind, EL_STR("Field"))) {
@@ -2401,6 +2448,141 @@ el_val_t is_int_call(el_val_t call_expr) {
return 0;
}
el_val_t cap_record_violation(el_val_t kind, el_val_t fn_name) {
el_val_t csv = state_get(EL_STR("__cap_violations"));
if (str_eq(csv, EL_STR(""))) {
csv = EL_STR(",");
}
el_val_t entry = el_str_concat(el_str_concat(kind, EL_STR(":")), fn_name);
el_val_t key = el_str_concat(el_str_concat(EL_STR(","), entry), EL_STR(","));
if (str_contains(csv, key)) {
return 1;
}
state_set(EL_STR("__cap_violations"), el_str_concat(el_str_concat(csv, entry), EL_STR(",")));
return 1;
return 0;
}
el_val_t is_self_formation_call(el_val_t fn_name) {
if (str_eq(fn_name, EL_STR("llm_call_agentic"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("llm_register_tool"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("dharma_emit"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("dharma_field"))) {
return 1;
}
return 0;
return 0;
}
el_val_t is_dharma_call(el_val_t fn_name) {
if (str_eq(fn_name, EL_STR("dharma_connect"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("dharma_send"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("dharma_activate"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("dharma_emit"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("dharma_field"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("dharma_strengthen"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("dharma_relationship"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("dharma_peers"))) {
return 1;
}
return 0;
return 0;
}
el_val_t is_llm_call(el_val_t fn_name) {
if (str_eq(fn_name, EL_STR("llm_call"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("llm_call_system"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("llm_call_agentic"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("llm_vision"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("llm_register_tool"))) {
return 1;
}
if (str_eq(fn_name, EL_STR("llm_models"))) {
return 1;
}
return 0;
return 0;
}
el_val_t cap_check_call(el_val_t fn_name) {
el_val_t kind = state_get(EL_STR("__program_kind"));
if (str_eq(kind, EL_STR("cgi"))) {
return 1;
}
if (str_eq(kind, EL_STR("service"))) {
if (is_self_formation_call(fn_name)) {
cap_record_violation(EL_STR("service"), fn_name);
return 0;
}
return 1;
}
if (is_dharma_call(fn_name)) {
cap_record_violation(EL_STR("utility"), fn_name);
return 0;
}
if (is_llm_call(fn_name)) {
cap_record_violation(EL_STR("utility"), fn_name);
return 0;
}
return 1;
return 0;
}
el_val_t emit_cap_violations(void) {
el_val_t csv = state_get(EL_STR("__cap_violations"));
if (str_eq(csv, EL_STR(""))) {
return 0;
}
if (str_eq(csv, EL_STR(","))) {
return 0;
}
el_val_t n = str_len(csv);
el_val_t i = 1;
while (i < n) {
el_val_t next_comma = str_index_of(str_slice(csv, i, n), EL_STR(","));
if (next_comma < 0) {
return 0;
}
el_val_t entry = str_slice(csv, i, (i + next_comma));
el_val_t colon = str_index_of(entry, EL_STR(":"));
if (colon > 0) {
el_val_t kind = str_slice(entry, 0, colon);
el_val_t fn_name = str_slice(entry, (colon + 1), str_len(entry));
emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("#error \"capability violation: '"), kind), EL_STR("' programs may not call '")), fn_name), EL_STR("' (self-formation primitive — only 'cgi' programs may use it)\"")));
}
i = ((i + next_comma) + 1);
}
return 0;
}
el_val_t add_int_name(el_val_t name) {
el_val_t csv = state_get(EL_STR("__int_names"));
if (str_eq(csv, EL_STR(""))) {
@@ -2691,6 +2873,8 @@ el_val_t codegen(el_val_t stmts, el_val_t source) {
el_val_t n_top = native_list_len(stmts);
el_val_t cgi_count = 0;
el_val_t cgi_block = el_map_new(1, "stmt", EL_STR("None"));
el_val_t svc_count = 0;
el_val_t svc_block = el_map_new(1, "stmt", EL_STR("None"));
el_val_t ti = 0;
while (ti < n_top) {
el_val_t s = native_list_get(stmts, ti);
@@ -2701,11 +2885,34 @@ el_val_t codegen(el_val_t stmts, el_val_t source) {
cgi_block = s;
}
}
if (str_eq(sk, EL_STR("ServiceBlock"))) {
svc_count = (svc_count + 1);
if (svc_count == 1) {
svc_block = s;
}
}
ti = (ti + 1);
}
if (cgi_count > 1) {
emit_line(EL_STR("#error \"El: multiple cgi blocks in program (only one allowed)\""));
}
if (svc_count > 1) {
emit_line(EL_STR("#error \"El: multiple service blocks in program (only one allowed)\""));
}
if (cgi_count >= 1) {
if (svc_count >= 1) {
emit_line(EL_STR("#error \"El: program declares both cgi and service blocks (mutually exclusive — pick one)\""));
}
}
el_val_t kind = EL_STR("utility");
if (cgi_count >= 1) {
kind = EL_STR("cgi");
}
if (svc_count >= 1) {
kind = EL_STR("service");
}
state_set(EL_STR("__program_kind"), kind);
state_set(EL_STR("__cap_violations"), EL_STR(""));
emit_line(EL_STR("#include <stdint.h>"));
emit_line(EL_STR("#include <stdlib.h>"));
emit_line(EL_STR("#include \"el_runtime.h\""));
@@ -2714,7 +2921,7 @@ el_val_t codegen(el_val_t stmts, el_val_t source) {
el_val_t i = 0;
while (i < n) {
el_val_t stmt = native_list_get(stmts, i);
el_val_t kind = el_get_field(stmt, EL_STR("stmt"));
kind = el_get_field(stmt, EL_STR("stmt"));
if (str_eq(kind, EL_STR("FnDef"))) {
el_val_t fn_name = el_get_field(stmt, EL_STR("name"));
if (!str_eq(fn_name, EL_STR("main"))) {
@@ -2769,6 +2976,7 @@ el_val_t codegen(el_val_t stmts, el_val_t source) {
emit_line(EL_STR(" return 0;"));
emit_line(EL_STR("}"));
emit_blank();
emit_cap_violations();
return EL_STR("");
return 0;
}
Vendored Executable
BIN
View File
Binary file not shown.