diff --git a/dist/platform/elc b/dist/platform/elc index 88d4d3c..a4ed6f4 100755 Binary files a/dist/platform/elc and b/dist/platform/elc differ diff --git a/dist/platform/elc.c b/dist/platform/elc.c index cfdb4f2..23d69ba 100644 --- a/dist/platform/elc.c +++ b/dist/platform/elc.c @@ -39,6 +39,8 @@ el_val_t emit_line(el_val_t line); el_val_t emit_blank(void); el_val_t binop_to_c(el_val_t op); el_val_t cg_expr(el_val_t expr); +el_val_t next_match_id(void); +el_val_t cg_match(el_val_t expr); el_val_t list_contains(el_val_t lst, el_val_t s); el_val_t cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared); el_val_t strip_outer_parens(el_val_t s); @@ -56,6 +58,10 @@ el_val_t build_int_names_for_params(el_val_t params); el_val_t cg_fn(el_val_t stmt); el_val_t is_fndef(el_val_t stmt); el_val_t is_top_level_decl(el_val_t stmt); +el_val_t cgi_arg(el_val_t value, el_val_t has_value); +el_val_t vbd_is_restricted_name(el_val_t name); +el_val_t vbd_expr_has_restricted_call(el_val_t expr); +el_val_t vbd_has_restricted_call(el_val_t stmts); el_val_t codegen(el_val_t stmts, el_val_t source); el_val_t compile(el_val_t source); @@ -1491,8 +1497,70 @@ el_val_t parse_stmt(el_val_t tokens, el_val_t pos) { } if (str_eq(k, EL_STR("At"))) { el_val_t p = (pos + 1); + el_val_t dec_name = tok_value(tokens, p); p = (p + 1); - return parse_stmt(tokens, p); + el_val_t r = parse_stmt(tokens, p); + el_val_t inner = el_get_field(r, EL_STR("node")); + el_val_t p2 = el_get_field(r, EL_STR("pos")); + el_val_t inner_kind = el_get_field(inner, EL_STR("stmt")); + if (str_eq(inner_kind, EL_STR("FnDef"))) { + el_val_t with_dec = el_map_new(6, "stmt", EL_STR("FnDef"), "name", el_get_field(inner, EL_STR("name")), "params", el_get_field(inner, EL_STR("params")), "body", el_get_field(inner, EL_STR("body")), "ret_type", el_get_field(inner, EL_STR("ret_type")), "decorator", dec_name); + return make_result(with_dec, p2); + } + return r; + } + if (str_eq(k, EL_STR("Cgi"))) { + 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 dharma_id = EL_STR(""); + el_val_t principal = EL_STR(""); + el_val_t network = EL_STR(""); + el_val_t engram = EL_STR(""); + el_val_t has_dharma_id = 0; + el_val_t has_principal = 0; + el_val_t has_network = 0; + el_val_t has_engram = 0; + 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("dharma_id"))) { + dharma_id = fval; + has_dharma_id = 1; + } + if (str_eq(fname, EL_STR("principal"))) { + principal = fval; + has_principal = 1; + } + if (str_eq(fname, EL_STR("network"))) { + network = fval; + has_network = 1; + } + if (str_eq(fname, EL_STR("engram"))) { + engram = fval; + has_engram = 1; + } + 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(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); } el_val_t r = parse_expr(tokens, pos); el_val_t val = el_get_field(r, EL_STR("node")); @@ -1930,10 +1998,78 @@ el_val_t cg_expr(el_val_t expr) { el_val_t cond_c = cg_expr(cond); return el_str_concat(el_str_concat(EL_STR("/* if-expr */ (("), cond_c), EL_STR(") ? (el_val_t)1 : (el_val_t)0)")); } + if (str_eq(kind, EL_STR("Match"))) { + return cg_match(expr); + } return EL_STR("EL_NULL"); return 0; } +el_val_t next_match_id(void) { + el_val_t csv = state_get(EL_STR("__match_counter")); + el_val_t n = 0; + if (!str_eq(csv, EL_STR(""))) { + n = str_to_int(csv); + } + n = (n + 1); + state_set(EL_STR("__match_counter"), native_int_to_str(n)); + return native_int_to_str(n); + return 0; +} + +el_val_t cg_match(el_val_t expr) { + el_val_t subject = el_get_field(expr, EL_STR("subject")); + el_val_t arms = el_get_field(expr, EL_STR("arms")); + el_val_t subj_c = cg_expr(subject); + el_val_t id = next_match_id(); + el_val_t subj_var = el_str_concat(EL_STR("_match_subj_"), id); + el_val_t result_var = el_str_concat(EL_STR("_match_result_"), id); + el_val_t done_label = el_str_concat(EL_STR("_match_done_"), id); + el_val_t out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("({ el_val_t "), subj_var), EL_STR(" = ")), subj_c), EL_STR("; el_val_t ")), result_var), EL_STR(" = 0; ")); + el_val_t n = native_list_len(arms); + el_val_t i = 0; + while (i < n) { + el_val_t arm = native_list_get(arms, i); + el_val_t pat = el_get_field(arm, EL_STR("pattern")); + el_val_t body = el_get_field(arm, EL_STR("body")); + el_val_t pkind = el_get_field(pat, EL_STR("pattern")); + el_val_t body_c = cg_expr(body); + if (str_eq(pkind, EL_STR("Wildcard"))) { + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("{ ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")); + } else { + if (str_eq(pkind, EL_STR("Binding"))) { + el_val_t bname = el_get_field(pat, EL_STR("name")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("{ el_val_t ")), bname), EL_STR(" = ")), subj_var), EL_STR("; ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")); + } else { + if (str_eq(pkind, EL_STR("LitInt"))) { + el_val_t v = el_get_field(pat, EL_STR("value")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("if (")), subj_var), EL_STR(" == ")), v), EL_STR(") { ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")); + } else { + if (str_eq(pkind, EL_STR("LitStr"))) { + el_val_t v = el_get_field(pat, EL_STR("value")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("if (str_eq(")), subj_var), EL_STR(", EL_STR(")), c_str_lit(v)), EL_STR("))) { ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")); + } else { + if (str_eq(pkind, EL_STR("LitBool"))) { + el_val_t v = el_get_field(pat, EL_STR("value")); + el_val_t bv = EL_STR("0"); + if (str_eq(v, EL_STR("true"))) { + bv = EL_STR("1"); + } + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("if (")), subj_var), EL_STR(" == ")), bv), EL_STR(") { ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")); + } else { + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("{ ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")); + } + } + } + } + } + i = (i + 1); + } + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, done_label), EL_STR(":; ")), result_var), EL_STR("; })")); + return out; + return 0; +} + el_val_t list_contains(el_val_t lst, el_val_t s) { el_val_t n = native_list_len(lst); el_val_t i = 0; @@ -2025,6 +2161,9 @@ el_val_t cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared) { if (str_eq(kind, EL_STR("Import"))) { return declared; } + if (str_eq(kind, EL_STR("CgiBlock"))) { + return declared; + } return declared; return 0; } @@ -2304,6 +2443,12 @@ el_val_t cg_fn(el_val_t stmt) { el_val_t body = el_get_field(stmt, EL_STR("body")); el_val_t ret_type = el_get_field(stmt, EL_STR("ret_type")); el_val_t params_c = params_to_c(params); + el_val_t decorator = el_get_field(stmt, EL_STR("decorator")); + if (vbd_has_restricted_call(body)) { + if (!str_eq(decorator, EL_STR("manager"))) { + emit_line(el_str_concat(el_str_concat(EL_STR("#error \"VBD violation: dharma_emit/dharma_field called from non-@manager fn '"), fn_name), EL_STR("'\""))); + } + } build_int_names_for_params(params); emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_val_t "), fn_name), EL_STR("(")), params_c), EL_STR(") {"))); el_val_t decl = native_list_empty(); @@ -2346,11 +2491,221 @@ el_val_t is_top_level_decl(el_val_t stmt) { if (str_eq(kind, EL_STR("Import"))) { return 1; } + if (str_eq(kind, EL_STR("CgiBlock"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t cgi_arg(el_val_t value, el_val_t has_value) { + if (has_value) { + return el_str_concat(el_str_concat(EL_STR("EL_STR("), c_str_lit(value)), EL_STR(")")); + } + return EL_STR("EL_NULL"); + return 0; +} + +el_val_t vbd_is_restricted_name(el_val_t name) { + if (str_eq(name, EL_STR("dharma_emit"))) { + return 1; + } + if (str_eq(name, EL_STR("dharma_field"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t vbd_expr_has_restricted_call(el_val_t expr) { + el_val_t kind = el_get_field(expr, EL_STR("expr")); + if (str_eq(kind, EL_STR("Call"))) { + el_val_t func = el_get_field(expr, EL_STR("func")); + el_val_t fk = el_get_field(func, EL_STR("expr")); + if (str_eq(fk, EL_STR("Ident"))) { + el_val_t fname = el_get_field(func, EL_STR("name")); + if (vbd_is_restricted_name(fname)) { + return 1; + } + } + if (vbd_expr_has_restricted_call(func)) { + return 1; + } + el_val_t args = el_get_field(expr, EL_STR("args")); + el_val_t an = native_list_len(args); + el_val_t ai = 0; + while (ai < an) { + el_val_t a = native_list_get(args, ai); + if (vbd_expr_has_restricted_call(a)) { + return 1; + } + ai = (ai + 1); + } + return 0; + } + if (str_eq(kind, EL_STR("BinOp"))) { + el_val_t l = el_get_field(expr, EL_STR("left")); + el_val_t r = el_get_field(expr, EL_STR("right")); + if (vbd_expr_has_restricted_call(l)) { + return 1; + } + if (vbd_expr_has_restricted_call(r)) { + return 1; + } + return 0; + } + if (str_eq(kind, EL_STR("Not"))) { + return vbd_expr_has_restricted_call(el_get_field(expr, EL_STR("inner"))); + } + if (str_eq(kind, EL_STR("Neg"))) { + return vbd_expr_has_restricted_call(el_get_field(expr, EL_STR("inner"))); + } + if (str_eq(kind, EL_STR("Field"))) { + return vbd_expr_has_restricted_call(el_get_field(expr, EL_STR("object"))); + } + if (str_eq(kind, EL_STR("Index"))) { + if (vbd_expr_has_restricted_call(el_get_field(expr, EL_STR("object")))) { + return 1; + } + if (vbd_expr_has_restricted_call(el_get_field(expr, EL_STR("index")))) { + return 1; + } + return 0; + } + if (str_eq(kind, EL_STR("Try"))) { + return vbd_expr_has_restricted_call(el_get_field(expr, EL_STR("inner"))); + } + if (str_eq(kind, EL_STR("Array"))) { + el_val_t elems = el_get_field(expr, EL_STR("elems")); + el_val_t n = native_list_len(elems); + el_val_t i = 0; + while (i < n) { + el_val_t e = native_list_get(elems, i); + if (vbd_expr_has_restricted_call(e)) { + return 1; + } + i = (i + 1); + } + return 0; + } + if (str_eq(kind, EL_STR("Map"))) { + el_val_t pairs = el_get_field(expr, EL_STR("pairs")); + el_val_t n = native_list_len(pairs); + el_val_t i = 0; + while (i < n) { + el_val_t pair = native_list_get(pairs, i); + el_val_t v = el_get_field(pair, EL_STR("value")); + if (vbd_expr_has_restricted_call(v)) { + return 1; + } + i = (i + 1); + } + return 0; + } + if (str_eq(kind, EL_STR("If"))) { + if (vbd_expr_has_restricted_call(el_get_field(expr, EL_STR("cond")))) { + return 1; + } + if (vbd_has_restricted_call(el_get_field(expr, EL_STR("then")))) { + return 1; + } + if (vbd_has_restricted_call(el_get_field(expr, EL_STR("else")))) { + return 1; + } + return 0; + } + if (str_eq(kind, EL_STR("For"))) { + if (vbd_expr_has_restricted_call(el_get_field(expr, EL_STR("list")))) { + return 1; + } + if (vbd_has_restricted_call(el_get_field(expr, EL_STR("body")))) { + return 1; + } + return 0; + } + if (str_eq(kind, EL_STR("Match"))) { + if (vbd_expr_has_restricted_call(el_get_field(expr, EL_STR("subject")))) { + return 1; + } + el_val_t arms = el_get_field(expr, EL_STR("arms")); + el_val_t n = native_list_len(arms); + el_val_t i = 0; + while (i < n) { + el_val_t arm = native_list_get(arms, i); + el_val_t body = el_get_field(arm, EL_STR("body")); + if (vbd_expr_has_restricted_call(body)) { + return 1; + } + i = (i + 1); + } + return 0; + } + return 0; + return 0; +} + +el_val_t vbd_has_restricted_call(el_val_t stmts) { + el_val_t n = native_list_len(stmts); + el_val_t i = 0; + while (i < n) { + el_val_t s = native_list_get(stmts, i); + el_val_t sk = el_get_field(s, EL_STR("stmt")); + if (str_eq(sk, EL_STR("Let"))) { + if (vbd_expr_has_restricted_call(el_get_field(s, EL_STR("value")))) { + return 1; + } + } + if (str_eq(sk, EL_STR("Return"))) { + if (vbd_expr_has_restricted_call(el_get_field(s, EL_STR("value")))) { + return 1; + } + } + if (str_eq(sk, EL_STR("Expr"))) { + if (vbd_expr_has_restricted_call(el_get_field(s, EL_STR("value")))) { + return 1; + } + } + if (str_eq(sk, EL_STR("While"))) { + if (vbd_expr_has_restricted_call(el_get_field(s, EL_STR("cond")))) { + return 1; + } + if (vbd_has_restricted_call(el_get_field(s, EL_STR("body")))) { + return 1; + } + } + if (str_eq(sk, EL_STR("For"))) { + if (vbd_expr_has_restricted_call(el_get_field(s, EL_STR("list")))) { + return 1; + } + if (vbd_has_restricted_call(el_get_field(s, EL_STR("body")))) { + return 1; + } + } + i = (i + 1); + } return 0; return 0; } 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 ti = 0; + while (ti < n_top) { + el_val_t s = native_list_get(stmts, ti); + el_val_t sk = el_get_field(s, EL_STR("stmt")); + if (str_eq(sk, EL_STR("CgiBlock"))) { + cgi_count = (cgi_count + 1); + if (cgi_count == 1) { + cgi_block = s; + } + } + ti = (ti + 1); + } + if (cgi_count > 1) { + emit_line(EL_STR("#error \"El: multiple cgi blocks in program (only one allowed)\"")); + } emit_line(EL_STR("#include ")); emit_line(EL_STR("#include ")); emit_line(EL_STR("#include \"el_runtime.h\"")); @@ -2381,6 +2736,23 @@ el_val_t codegen(el_val_t stmts, el_val_t source) { } emit_line(EL_STR("int main(int argc, char** argv) {")); emit_line(EL_STR(" el_runtime_init_args(argc, argv);")); + if (cgi_count >= 1) { + el_val_t cname = el_get_field(cgi_block, EL_STR("name")); + el_val_t cdid = el_get_field(cgi_block, EL_STR("dharma_id")); + el_val_t cprin = el_get_field(cgi_block, EL_STR("principal")); + el_val_t cnet = el_get_field(cgi_block, EL_STR("network")); + el_val_t ceng = el_get_field(cgi_block, EL_STR("engram")); + el_val_t has_did = el_get_field(cgi_block, EL_STR("has_dharma_id")); + el_val_t has_prin = el_get_field(cgi_block, EL_STR("has_principal")); + el_val_t has_net = el_get_field(cgi_block, EL_STR("has_network")); + el_val_t has_eng = el_get_field(cgi_block, EL_STR("has_engram")); + el_val_t arg_name = el_str_concat(el_str_concat(EL_STR("EL_STR("), c_str_lit(cname)), EL_STR(")")); + el_val_t arg_did = cgi_arg(cdid, has_did); + el_val_t arg_prin = cgi_arg(cprin, has_prin); + el_val_t arg_net = cgi_arg(cnet, has_net); + el_val_t arg_eng = cgi_arg(ceng, has_eng); + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" el_cgi_init("), arg_name), EL_STR(", ")), arg_did), EL_STR(", ")), arg_prin), EL_STR(", ")), arg_net), EL_STR(", ")), arg_eng), EL_STR(");"))); + } el_val_t main_decl = native_list_empty(); i = 0; while (i < n) { diff --git a/dist/platform/elc.prev5 b/dist/platform/elc.prev5 new file mode 100755 index 0000000..88d4d3c Binary files /dev/null and b/dist/platform/elc.prev5 differ diff --git a/el-compiler/runtime/el_runtime.c b/el-compiler/runtime/el_runtime.c index bf60cd6..bcc7a99 100644 --- a/el-compiler/runtime/el_runtime.c +++ b/el-compiler/runtime/el_runtime.c @@ -1926,84 +1926,6 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, _el_cgi_engram); } -/* ── DHARMA network stubs ─────────────────────────────────────────────────── - * Stub implementations for all dharma_* and engram_* builtins. - * Each stub prints a descriptive line to stdout so calls are visible in tests. - * Full implementations are provided by the DHARMA runtime linked at deploy. */ - -el_val_t dharma_connect(el_val_t cgi_id) { - const char* id = EL_CSTR(cgi_id); - if (!id) id = "(null)"; - char buf[256]; - snprintf(buf, sizeof(buf), "[dharma] connect: %s", id); - puts(buf); - /* Return a synthetic channel ID of the form "ch:" */ - char ch[272]; - snprintf(ch, sizeof(ch), "ch:%s", id); - return el_wrap_str(el_strdup(ch)); -} - -el_val_t dharma_send(el_val_t channel, el_val_t content) { - const char* ch = EL_CSTR(channel); - const char* msg = EL_CSTR(content); - if (!ch) ch = "(null)"; - if (!msg) msg = "(null)"; - char buf[1024]; - snprintf(buf, sizeof(buf), "[dharma] send on %s: %s", ch, msg); - puts(buf); - return el_wrap_str(el_strdup("")); -} - -el_val_t dharma_activate(el_val_t query) { - const char* q = EL_CSTR(query); - if (!q) q = "(null)"; - char buf[512]; - snprintf(buf, sizeof(buf), "[dharma] activate: %s", q); - puts(buf); - return el_list_empty(); -} - -void dharma_emit(el_val_t event_type, el_val_t payload) { - const char* et = EL_CSTR(event_type); - const char* pay = EL_CSTR(payload); - if (!et) et = "(null)"; - if (!pay) pay = "(null)"; - char buf[1024]; - snprintf(buf, sizeof(buf), "[dharma] emit: %s %s", et, pay); - puts(buf); -} - -el_val_t dharma_field(el_val_t event_type) { - const char* et = EL_CSTR(event_type); - if (!et) et = "(null)"; - char buf[512]; - snprintf(buf, sizeof(buf), "[dharma] field: %s", et); - puts(buf); - return el_map_new(0); -} - -void dharma_strengthen(el_val_t cgi_id, el_val_t weight) { - const char* id = EL_CSTR(cgi_id); - if (!id) id = "(null)"; - /* weight is encoded as el_val_t; print as integer (float encoding TBD) */ - char buf[256]; - snprintf(buf, sizeof(buf), "[dharma] strengthen: %s +%lld", id, (long long)weight); - puts(buf); -} - -el_val_t dharma_relationship(el_val_t cgi_id) { - const char* id = EL_CSTR(cgi_id); - if (!id) id = "(null)"; - char buf[256]; - snprintf(buf, sizeof(buf), "[dharma] relationship: %s", id); - puts(buf); - return 0; /* 0.0 — no prior relationship in stub mode */ -} - -el_val_t dharma_peers(void) { - puts("[dharma] peers"); - return el_list_empty(); -} /* ── Batch 3: Engram in-process graph store ──────────────────────────────── */ /* @@ -2981,17 +2903,526 @@ el_val_t engram_stats_json(void) { return el_wrap_str(el_strdup(buf)); } +/* ── DHARMA network ───────────────────────────────────────────────────────── + * Real implementation. Peers are addressed by `dharma_id` — either bare + * (e.g. "ntn-genesis", transport defaults to http://localhost:7770) or + * "@" where is the peer's Engram-exposed daemon. + * + * Channels are logical handles cached per-cgi: `dharma_connect` is + * idempotent and returns "ch:". The channel registry below tracks + * every cgi_id we've connected to and its resolved transport URL. + * + * Relationship weights live in the local Engram graph: edges of type + * "dharma-relation" between a synthetic local node ("dharma:self") and + * synthetic peer nodes ("dharma:peer:"). Hebbian increments + * accumulate in EngramEdge.weight, clamped to [0.0, 1.0]. + * + * Events arrive over HTTP via the application's request handler, which is + * expected to call el_runtime_dharma_event_arrive() when it sees a + * /dharma/event POST. dharma_field() blocks on a per-event-type queue. + */ + +#define DHARMA_DEFAULT_URL "http://localhost:7770" + +/* Channel registry — one entry per known peer. */ +typedef struct DharmaChannel { + char* cgi_id; /* full dharma_id including any @ suffix */ + char* base_id; /* registry-id portion (before @) for relationship lookup */ + char* url; /* resolved transport URL */ + char* channel_id; /* "ch:" */ +} DharmaChannel; + +static DharmaChannel* _dharma_channels = NULL; +static size_t _dharma_channel_count = 0; +static size_t _dharma_channel_cap = 0; +static pthread_mutex_t _dharma_channel_mu = PTHREAD_MUTEX_INITIALIZER; + +/* Event queue — per-type linked list. dharma_field blocks on _dharma_event_cv. */ +typedef struct DharmaEvent { + char* event_type; + char* payload; + char* source; + int64_t timestamp; + struct DharmaEvent* next; +} DharmaEvent; + +static DharmaEvent* _dharma_event_head = NULL; +static DharmaEvent* _dharma_event_tail = NULL; +static pthread_mutex_t _dharma_event_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _dharma_event_cv = PTHREAD_COND_INITIALIZER; + +/* Split "@" → (base_id, url). If no "@", base_id = full, url = default. + * Returned strings are heap-allocated; caller must free. */ +static void dharma_parse_id(const char* full, char** out_base, char** out_url) { + if (!full) full = ""; + const char* at = strchr(full, '@'); + if (at) { + size_t bn = (size_t)(at - full); + char* b = malloc(bn + 1); + memcpy(b, full, bn); b[bn] = '\0'; + *out_base = b; + *out_url = el_strdup(at + 1); + if (!**out_url) { free(*out_url); *out_url = el_strdup(DHARMA_DEFAULT_URL); } + } else { + *out_base = el_strdup(full); + *out_url = el_strdup(DHARMA_DEFAULT_URL); + } +} + +/* Find existing channel by full cgi_id. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_find_channel_locked(const char* cgi_id) { + if (!cgi_id) return NULL; + for (size_t i = 0; i < _dharma_channel_count; i++) { + if (_dharma_channels[i].cgi_id && + strcmp(_dharma_channels[i].cgi_id, cgi_id) == 0) { + return &_dharma_channels[i]; + } + } + return NULL; +} + +/* Add a new channel entry. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_add_channel_locked(const char* cgi_id) { + if (_dharma_channel_count >= _dharma_channel_cap) { + size_t nc = _dharma_channel_cap ? _dharma_channel_cap * 2 : 8; + _dharma_channels = realloc(_dharma_channels, nc * sizeof(DharmaChannel)); + if (!_dharma_channels) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(_dharma_channels + _dharma_channel_cap, 0, + (nc - _dharma_channel_cap) * sizeof(DharmaChannel)); + _dharma_channel_cap = nc; + } + DharmaChannel* ch = &_dharma_channels[_dharma_channel_count++]; + char* base = NULL; char* url = NULL; + dharma_parse_id(cgi_id, &base, &url); + ch->cgi_id = el_strdup(cgi_id ? cgi_id : ""); + ch->base_id = base; + ch->url = url; + size_t cn = strlen(ch->cgi_id) + 4; + ch->channel_id = malloc(cn); + snprintf(ch->channel_id, cn, "ch:%s", ch->cgi_id); + return ch; +} + +el_val_t dharma_connect(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(id); + if (!ch) ch = dharma_add_channel_locked(id); + char* out = el_strdup(ch->channel_id); + pthread_mutex_unlock(&_dharma_channel_mu); + return el_wrap_str(out); +} + +/* Build an error JSON body — same shape http_error_json uses. */ +static el_val_t dharma_error_json(const char* msg) { + return http_error_json(msg); +} + +el_val_t dharma_send(el_val_t channel, el_val_t content) { + const char* ch_id = EL_CSTR(channel); + const char* msg = EL_CSTR(content); + if (!ch_id || strncmp(ch_id, "ch:", 3) != 0) { + return dharma_error_json("invalid channel"); + } + const char* peer_id = ch_id + 3; + /* Look up channel; if unknown (caller fabricated), auto-register. */ + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(peer_id); + if (!ch) ch = dharma_add_channel_locked(peer_id); + char* url = el_strdup(ch->url); + pthread_mutex_unlock(&_dharma_channel_mu); + /* Build /dharma/recv body. */ + const char* from = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + char* esc_ch = json_escape_alloc(ch_id); + char* esc_from = json_escape_alloc(from); + char* esc_msg = json_escape_alloc(msg ? msg : ""); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"channel\":\""); jb_puts(&b, esc_ch); + jb_puts(&b, "\",\"from\":\""); jb_puts(&b, esc_from); + jb_puts(&b, "\",\"content\":\""); jb_puts(&b, esc_msg); + jb_puts(&b, "\"}"); + free(esc_ch); free(esc_from); free(esc_msg); + size_t ul = strlen(url) + 16; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/recv", url); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); + free(b.buf); free(full_url); free(url); + return resp; +} + +el_val_t dharma_activate(el_val_t query) { + const char* q = EL_CSTR(query); + if (!q) q = ""; + el_val_t out = el_list_empty(); + char* esc_q = json_escape_alloc(q); + JsonBuf body; jb_init(&body); + jb_puts(&body, "{\"query\":\""); jb_puts(&body, esc_q); jb_puts(&body, "\"}"); + free(esc_q); + + /* Snapshot the channel list under lock so we can iterate without + * holding the mutex during network I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + char** ids = calloc(n ? n : 1, sizeof(char*)); + char** bases = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) { + urls[i] = el_strdup(_dharma_channels[i].url); + ids[i] = el_strdup(_dharma_channels[i].cgi_id); + bases[i] = el_strdup(_dharma_channels[i].base_id); + } + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/api/activate", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, body.buf, h); + curl_slist_free_all(h); + free(full_url); + const char* rs = EL_CSTR(resp); + if (!rs || !*rs) continue; + if (rs[0] == '{' && strstr(rs, "\"error\"")) continue; + + /* Look up relationship weight (attenuation). */ + double rel_weight = 1.0; + { + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", bases[i]); + EngramStore* g = engram_get(); + for (int64_t k = 0; k < g->edge_count; k++) { + EngramEdge* e = &g->edges[k]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + rel_weight = e->weight; + break; + } + } + } + + /* Iterate the response array. Expect either a top-level array + * or an object whose "results" field is an array. */ + const char* arr = rs; + while (*arr == ' ' || *arr == '\t' || *arr == '\n' || *arr == '\r') arr++; + char* arr_owned = NULL; + if (*arr == '{') { + el_val_t r = json_get_raw(EL_STR(rs), EL_STR("results")); + const char* rr = EL_CSTR(r); + if (rr && *rr == '[') { + arr_owned = el_strdup(rr); + arr = arr_owned; + } else { + continue; + } + } + if (*arr != '[') { free(arr_owned); continue; } + const char* p = arr + 1; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + while (*p && *p != ']') { + const char* end = json_skip_value(p); + size_t en = (size_t)(end - p); + char* obj = el_strbuf(en); + memcpy(obj, p, en); obj[en] = '\0'; + + /* Pull activation_strength if present, else 1.0. */ + el_val_t act_v = json_get_float(EL_STR(obj), EL_STR("activation_strength")); + double act = el_to_float(act_v); + if (!(act > 0.0 && act <= 100.0)) act = 1.0; + double final_act = act * rel_weight; + + el_val_t entry = el_map_new(0); + /* node = the inner JSON if present, else the entire obj. */ + el_val_t node_raw = json_get_raw(EL_STR(obj), EL_STR("node")); + const char* nr = EL_CSTR(node_raw); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + (nr && *nr) ? node_raw : EL_STR(el_strdup(obj))); + entry = el_map_set(entry, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(ids[i]))); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(final_act)); + out = el_list_append(out, entry); + free(obj); + p = end; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + } + free(arr_owned); + } + for (size_t i = 0; i < n; i++) { free(urls[i]); free(ids[i]); free(bases[i]); } + free(urls); free(ids); free(bases); + free(body.buf); + return out; +} + +void dharma_emit(el_val_t event_type, el_val_t payload) { + const char* et = EL_CSTR(event_type); + const char* pay = EL_CSTR(payload); + if (!et) et = ""; + if (!pay) pay = ""; + const char* src = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + int64_t ts = engram_now_ms(); + + char* esc_et = json_escape_alloc(et); + char* esc_pay = json_escape_alloc(pay); + char* esc_src = json_escape_alloc(src); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"type\":\""); jb_puts(&b, esc_et); + jb_puts(&b, "\",\"payload\":\""); jb_puts(&b, esc_pay); + jb_puts(&b, "\",\"source\":\""); jb_puts(&b, esc_src); + jb_puts(&b, "\",\"timestamp\":"); jb_emit_int(&b, ts); + jb_putc(&b, '}'); + free(esc_et); free(esc_pay); free(esc_src); + + /* Snapshot URLs to avoid holding the channel mutex during I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) urls[i] = el_strdup(_dharma_channels[i].url); + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/event", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", full_url, b.buf, h); + (void)r; /* fire-and-forget — emit is not synchronous */ + curl_slist_free_all(h); + free(full_url); + } + for (size_t i = 0; i < n; i++) free(urls[i]); + free(urls); + free(b.buf); +} + +void el_runtime_dharma_event_arrive(const char* event_type, const char* payload, + const char* source) { + DharmaEvent* ev = calloc(1, sizeof(DharmaEvent)); + if (!ev) return; + ev->event_type = el_strdup(event_type ? event_type : ""); + ev->payload = el_strdup(payload ? payload : ""); + ev->source = el_strdup(source ? source : ""); + ev->timestamp = engram_now_ms(); + ev->next = NULL; + pthread_mutex_lock(&_dharma_event_mu); + if (_dharma_event_tail) _dharma_event_tail->next = ev; + else _dharma_event_head = ev; + _dharma_event_tail = ev; + pthread_cond_broadcast(&_dharma_event_cv); + pthread_mutex_unlock(&_dharma_event_mu); +} + +el_val_t dharma_field(el_val_t event_type) { + const char* et = EL_CSTR(event_type); + if (!et) et = ""; + + /* Compute deadline: now + 30 seconds. */ + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += 30; + + DharmaEvent* found = NULL; + pthread_mutex_lock(&_dharma_event_mu); + while (1) { + /* Scan queue for matching type; pop and return first match. */ + DharmaEvent* prev = NULL; + DharmaEvent* cur = _dharma_event_head; + while (cur) { + if (cur->event_type && strcmp(cur->event_type, et) == 0) { + if (prev) prev->next = cur->next; + else _dharma_event_head = cur->next; + if (_dharma_event_tail == cur) _dharma_event_tail = prev; + cur->next = NULL; + found = cur; + break; + } + prev = cur; cur = cur->next; + } + if (found) break; + int rc = pthread_cond_timedwait(&_dharma_event_cv, &_dharma_event_mu, &deadline); + if (rc == ETIMEDOUT) break; + } + pthread_mutex_unlock(&_dharma_event_mu); + + if (!found) return el_map_new(0); + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("type")), + EL_STR(el_strdup(found->event_type ? found->event_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("payload")), + EL_STR(el_strdup(found->payload ? found->payload : ""))); + m = el_map_set(m, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(found->source ? found->source : ""))); + m = el_map_set(m, EL_STR(el_strdup("timestamp")), (el_val_t)found->timestamp); + free(found->event_type); free(found->payload); free(found->source); free(found); + return m; +} + +/* Locate (or create) the local "dharma:self" node and the synthetic peer + * node "dharma:peer:". Returns the index of the dharma-relation + * edge, or -1 if not found. If `create` is non-zero, ensure the nodes + * and edge exist (creating them as needed) and return the edge index. */ +static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int create) { + if (!peer_base || !*peer_base) return -1; + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", peer_base); + + /* Look for the edge first. */ + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + return i; + } + } + if (!create) return -1; + + /* Ensure self node exists. We use a fixed id (not engram_new_id) so + * subsequent calls reuse the same one. */ + if (!engram_find_node(self_id)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(self_id); + n->content = el_strdup(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)"); + n->node_type = el_strdup("DharmaSelf"); + n->label = el_strdup("dharma:self"); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + g->node_count++; + } + if (!engram_find_node(peer_node)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(peer_node); + n->content = el_strdup(peer_base); + n->node_type = el_strdup("DharmaPeer"); + n->label = el_strdup(peer_node); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + g->node_count++; + } + /* Create the edge with weight 0.0 — caller will increment. */ + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(self_id); + e->to_id = el_strdup(peer_node); + e->relation = el_strdup("dharma-relation"); + e->metadata = el_strdup("{}"); + e->weight = 0.0; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; e->updated_at = now; + int64_t idx = g->edge_count; + g->edge_count++; + return idx; +} + +void dharma_strengthen(el_val_t cgi_id, el_val_t weight) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return; + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 1); + free(base); + if (ei < 0) return; + EngramStore* g = engram_get(); + double inc = engram_decode_score(weight); + if (!(inc >= 0.0)) inc = 0.0; + double w = g->edges[ei].weight + inc; + if (w < 0.0) w = 0.0; + if (w > 1.0) w = 1.0; + g->edges[ei].weight = w; + g->edges[ei].updated_at = engram_now_ms(); + g->edges[ei].last_fired = g->edges[ei].updated_at; +} + +el_val_t dharma_relationship(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_from_float(0.0); + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 0); + free(base); + if (ei < 0) return el_from_float(0.0); + EngramStore* g = engram_get(); + return el_from_float(g->edges[ei].weight); +} + +el_val_t dharma_peers(void) { + /* Walk dharma-relation edges out of "dharma:self", weight > 0, sort desc. */ + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + typedef struct { char* peer_base; double weight; } PeerEntry; + PeerEntry* peers = malloc((size_t)(g->edge_count + 1) * sizeof(PeerEntry)); + int64_t pcount = 0; + if (!peers) return el_list_empty(); + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (!e->from_id || !e->to_id) continue; + if (strcmp(e->from_id, self_id) != 0) continue; + if (!e->relation || strcmp(e->relation, "dharma-relation") != 0) continue; + if (e->weight <= 0.0) continue; + const char* prefix = "dharma:peer:"; + size_t pl = strlen(prefix); + if (strncmp(e->to_id, prefix, pl) != 0) continue; + peers[pcount].peer_base = el_strdup(e->to_id + pl); + peers[pcount].weight = e->weight; + pcount++; + } + /* Sort desc by weight. */ + for (int64_t i = 1; i < pcount; i++) { + PeerEntry key = peers[i]; + int64_t j = i - 1; + while (j >= 0 && peers[j].weight < key.weight) { + peers[j + 1] = peers[j]; j--; + } + peers[j + 1] = key; + } + el_val_t out = el_list_empty(); + for (int64_t i = 0; i < pcount; i++) { + out = el_list_append(out, EL_STR(peers[i].peer_base)); + } + free(peers); + return out; +} + /* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */ /* * All LLM builtins call https://api.anthropic.com/v1/messages with the API * key from env ANTHROPIC_API_KEY. Default model is "claude-sonnet-4-5" * when the supplied model is empty/null. * - * `llm_call_agentic` is currently implemented as a single-turn fallback - * delegating to `llm_call_system` — TODO: implement the full multi-turn - * tool_use / tool_result loop. Programs that need agentic tool dispatch - * should register a tool handler via state_set("tools/", json) and - * loop themselves until the model emits stop_reason=end_turn. + * `llm_call_agentic` runs a real multi-turn tool_use/tool_result loop. + * Tool handlers are registered with `llm_register_tool(name, fn_name)`, + * which dlsym()s the named symbol. Each tool handler has the C signature + * el_val_t handler(el_val_t input_json); + * and returns a JSON-string el_val_t result. Iteration is capped at 10. */ static const char* LLM_DEFAULT_MODEL = "claude-sonnet-4-5"; @@ -3121,12 +3552,351 @@ el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_p return llm_extract_text(resp); } +/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */ + +typedef el_val_t (*llm_tool_fn)(el_val_t input); + +typedef struct LlmToolEntry { + char* name; + llm_tool_fn fn; +} LlmToolEntry; + +static LlmToolEntry _llm_tools[64]; +static size_t _llm_tool_count = 0; +static pthread_mutex_t _llm_tool_mu = PTHREAD_MUTEX_INITIALIZER; + +static llm_tool_fn llm_tool_lookup(const char* name) { + if (!name) return NULL; + llm_tool_fn fn = NULL; + pthread_mutex_lock(&_llm_tool_mu); + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, name) == 0) { fn = _llm_tools[i].fn; break; } + } + pthread_mutex_unlock(&_llm_tool_mu); + return fn; +} + +void llm_register_tool(el_val_t name, el_val_t handler_fn_name) { + const char* nm = EL_CSTR(name); + const char* sym = EL_CSTR(handler_fn_name); + if (!nm || !*nm || !sym || !*sym) return; + void* p = dlsym(RTLD_DEFAULT, sym); + if (!p) { + fprintf(stderr, "[llm_register_tool] symbol not found: %s\n", sym); + return; + } + pthread_mutex_lock(&_llm_tool_mu); + /* Replace existing entry by name. */ + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, nm) == 0) { + _llm_tools[i].fn = (llm_tool_fn)p; + pthread_mutex_unlock(&_llm_tool_mu); + return; + } + } + if (_llm_tool_count < sizeof(_llm_tools) / sizeof(_llm_tools[0])) { + _llm_tools[_llm_tool_count].name = el_strdup(nm); + _llm_tools[_llm_tool_count].fn = (llm_tool_fn)p; + _llm_tool_count++; + } + pthread_mutex_unlock(&_llm_tool_mu); +} + +/* Serialize the El `tools` list into the JSON `tools:[...]` field expected + * by the Anthropic API. Each tool is an ElMap with name/description/ + * input_schema. input_schema is treated as either a JSON-object string + * (passed through verbatim) or a missing field (substitute {}). */ +static void llm_emit_tools_json(JsonBuf* b, el_val_t tools_list) { + jb_putc(b, '['); + ElList* lst = (ElList*)(uintptr_t)tools_list; + int64_t n = lst ? lst->length : 0; + for (int64_t i = 0; i < n; i++) { + if (i > 0) jb_putc(b, ','); + ElMap* tm = as_map(lst->elems[i]); + const char* name = ""; + const char* desc = ""; + const char* schema = "{}"; + if (tm) { + for (int64_t k = 0; k < tm->count; k++) { + const char* key = EL_CSTR(tm->keys[k]); + const char* val = EL_CSTR(tm->values[k]); + if (!key || !val) continue; + if (strcmp(key, "name") == 0) name = val; + else if (strcmp(key, "description") == 0) desc = val; + else if (strcmp(key, "input_schema") == 0) schema = val; + } + } + char* esc_name = json_escape_alloc(name); + char* esc_desc = json_escape_alloc(desc); + jb_puts(b, "{\"name\":\""); jb_puts(b, esc_name); + jb_puts(b, "\",\"description\":\""); jb_puts(b, esc_desc); + jb_puts(b, "\",\"input_schema\":"); jb_puts(b, schema && *schema ? schema : "{}"); + jb_putc(b, '}'); + free(esc_name); free(esc_desc); + } + jb_putc(b, ']'); +} + +/* Walk the assistant `content` array and emit each block back into b, + * preserving the verbatim JSON of every block — used to re-include the + * assistant turn in the next request. */ +static void llm_emit_content_blocks(JsonBuf* b, const char* resp) { + const char* p = json_find_key(resp, "content"); + jb_putc(b, '['); + if (!p) { jb_putc(b, ']'); return; } + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { jb_putc(b, ']'); return; } + p++; + int first = 1; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + if (!first) jb_putc(b, ','); + first = 0; + size_t n = (size_t)(end - p); + jb_reserve(b, n); + memcpy(b->buf + b->len, p, n); + b->len += n; + b->buf[b->len] = '\0'; + p = end; + } + jb_putc(b, ']'); +} + +/* Concatenate all "text" blocks from a response. Returns owned string. */ +static char* llm_concat_text_blocks(const char* resp) { + JsonBuf out; jb_init(&out); + if (!resp) return out.buf; + const char* p = json_find_key(resp, "content"); + if (!p) return out.buf; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return out.buf; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* tp = json_find_key(obj, "type"); + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* tname = jp_parse_string_raw(&jp); + if (!jp.err && tname && strcmp(tname, "text") == 0) { + const char* xp = json_find_key(obj, "text"); + if (xp && *xp == '"') { + JsonParser jp2 = { .p = xp, .end = xp + strlen(xp), .err = 0 }; + char* txt = jp_parse_string_raw(&jp2); + if (!jp2.err && txt) jb_puts(&out, txt); + free(txt); + } + } + free(tname); + } + free(obj); + p = end; + } + return out.buf; +} + +/* Build tool_result message blocks for every tool_use in a response. + * Appends to `b` an array element for each tool_use; caller wraps. */ +static int llm_build_tool_results(JsonBuf* b, const char* resp) { + int any = 0; + const char* p = json_find_key(resp, "content"); + if (!p) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return 0; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + + const char* tp = json_find_key(obj, "type"); + char* type_s = NULL; + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + type_s = jp_parse_string_raw(&jp); + } + if (type_s && strcmp(type_s, "tool_use") == 0) { + /* Extract id, name, input. */ + char* id_s = NULL; char* name_s = NULL; + const char* idp = json_find_key(obj, "id"); + if (idp && *idp == '"') { + JsonParser jp = { .p = idp, .end = idp + strlen(idp), .err = 0 }; + id_s = jp_parse_string_raw(&jp); + } + const char* np = json_find_key(obj, "name"); + if (np && *np == '"') { + JsonParser jp = { .p = np, .end = np + strlen(np), .err = 0 }; + name_s = jp_parse_string_raw(&jp); + } + el_val_t input_raw = json_get_raw(EL_STR(obj), EL_STR("input")); + const char* input_s = EL_CSTR(input_raw); + if (!input_s || !*input_s) input_s = "{}"; + + llm_tool_fn fn = llm_tool_lookup(name_s ? name_s : ""); + char* result = NULL; + int is_error = 0; + if (!fn) { + size_t en = strlen(name_s ? name_s : "(null)") + 64; + result = malloc(en); + snprintf(result, en, "{\"error\":\"tool not registered: %s\"}", + name_s ? name_s : "(null)"); + is_error = 1; + } else { + el_val_t out = fn(EL_STR(input_s)); + const char* os = EL_CSTR(out); + result = el_strdup(os ? os : ""); + } + + if (any) jb_putc(b, ','); + char* esc_id = json_escape_alloc(id_s ? id_s : ""); + char* esc_res = json_escape_alloc(result ? result : ""); + jb_puts(b, "{\"type\":\"tool_result\",\"tool_use_id\":\""); + jb_puts(b, esc_id); + jb_puts(b, "\",\"content\":\""); + jb_puts(b, esc_res); + jb_puts(b, "\""); + if (is_error) jb_puts(b, ",\"is_error\":true"); + jb_putc(b, '}'); + free(esc_id); free(esc_res); free(result); + free(id_s); free(name_s); + any = 1; + } + free(type_s); + free(obj); + p = end; + } + return any; +} + el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools) { - /* TODO: full multi-turn tool_use / tool_result loop. For now we delegate - * to llm_call_system and ignore the tools list — programs needing real - * agentic dispatch should drive the loop themselves with raw http_post. */ - (void)tools; - return llm_call_system(model, system, user); + /* Empty tools list → degrade to plain system call. */ + ElList* tl = (ElList*)(uintptr_t)tools; + if (!tl || tl->length == 0) { + return llm_call_system(model, system, user); + } + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* sys_p = EL_CSTR(system); if (!sys_p) sys_p = ""; + const char* usr_p = EL_CSTR(user); if (!usr_p) usr_p = ""; + + /* Build the static parts: tools JSON and system prompt — these don't + * change across iterations. */ + JsonBuf tools_buf; jb_init(&tools_buf); + llm_emit_tools_json(&tools_buf, tools); + char* esc_sys = json_escape_alloc(sys_p); + + /* messages array, accumulated as a mutable JSON fragment (no surrounding + * brackets — emitted at request time). */ + JsonBuf msgs; jb_init(&msgs); + /* First user message. */ + char* esc_user = json_escape_alloc(usr_p); + jb_puts(&msgs, "{\"role\":\"user\",\"content\":\""); + jb_puts(&msgs, esc_user); + jb_puts(&msgs, "\"}"); + free(esc_user); + + char* last_text = el_strdup(""); + el_val_t final_out = 0; + int reached_cap = 1; + + for (int iter = 0; iter < 10; iter++) { + /* Build request body. */ + JsonBuf body; jb_init(&body); + jb_putc(&body, '{'); + jb_puts(&body, "\"model\":"); jb_emit_escaped(&body, m); + jb_puts(&body, ",\"max_tokens\":4096"); + if (*sys_p) { + jb_puts(&body, ",\"system\":\""); + jb_puts(&body, esc_sys); + jb_puts(&body, "\""); + } + jb_puts(&body, ",\"tools\":"); + jb_puts(&body, tools_buf.buf); + jb_puts(&body, ",\"messages\":["); + jb_puts(&body, msgs.buf); + jb_puts(&body, "]}"); + + el_val_t resp_v = llm_request(body.buf); + free(body.buf); + const char* resp = EL_CSTR(resp_v); + if (!resp || !*resp) { + final_out = http_error_json("empty response"); + reached_cap = 0; + break; + } + if (resp[0] == '{' && strstr(resp, "\"error\"") && + !json_find_key(resp, "content")) { + final_out = el_wrap_str(el_strdup(resp)); + reached_cap = 0; + break; + } + + /* Update last_text from this response. */ + free(last_text); + last_text = llm_concat_text_blocks(resp); + + /* Inspect stop_reason. */ + el_val_t sr_v = json_get_string(EL_STR(resp), EL_STR("stop_reason")); + const char* sr = EL_CSTR(sr_v); if (!sr) sr = ""; + + if (strcmp(sr, "end_turn") == 0) { + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + if (strcmp(sr, "max_tokens") == 0) { + size_t ln = strlen(last_text) + 16; + char* out = malloc(ln); + snprintf(out, ln, "%s\n[truncated]", last_text); + final_out = el_wrap_str(out); + reached_cap = 0; + break; + } + if (strcmp(sr, "tool_use") != 0) { + /* Unexpected stop reason; return the text we have. */ + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + + /* Append the assistant turn (raw content blocks) to messages. */ + JsonBuf ab; jb_init(&ab); + jb_puts(&ab, ",{\"role\":\"assistant\",\"content\":"); + llm_emit_content_blocks(&ab, resp); + jb_putc(&ab, '}'); + jb_puts(&msgs, ab.buf); + free(ab.buf); + + /* Build tool_result message. */ + JsonBuf tr; jb_init(&tr); + jb_puts(&tr, ",{\"role\":\"user\",\"content\":["); + int any = llm_build_tool_results(&tr, resp); + jb_puts(&tr, "]}"); + if (any) { + jb_puts(&msgs, tr.buf); + } + free(tr.buf); + } + + if (reached_cap) { + size_t ln = strlen(last_text) + 32; + char* out = malloc(ln); + snprintf(out, ln, "[loop_cap_reached]\n%s", last_text); + final_out = el_wrap_str(out); + } + free(last_text); + free(esc_sys); + free(tools_buf.buf); + free(msgs.buf); + return final_out; } /* base64-encode arbitrary bytes (returns owned C string). */ diff --git a/el-compiler/runtime/el_runtime.h b/el-compiler/runtime/el_runtime.h index c471cef..da4d968 100644 --- a/el-compiler/runtime/el_runtime.h +++ b/el-compiler/runtime/el_runtime.h @@ -219,8 +219,22 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, /* ── DHARMA network builtins ───────────────────────────────────────────────── * Available to CGI programs (declared with a `cgi {}` block). - * Stubs print descriptive output and return empty/zero values. - * Full implementations are linked from the DHARMA runtime at deploy time. */ + * + * Peers are addressed by `dharma_id` of the form + * "@" e.g. "ntn-genesis@http://localhost:7770" + * If the @ portion is omitted, transport defaults to + * "http://localhost:7770" (the local CGI daemon assumption). + * + * Wire protocol (all peers expose): + * POST /dharma/recv { channel, from, content } → response body + * POST /dharma/event { type, payload, source, timestamp } + * POST /api/activate { query } → list of nodes + * + * Hosting application's responsibility: an El program with a `cgi {}` block + * runs http_serve() with its own request handler; that handler should route + * "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so + * incoming events feed dharma_field() queues. The runtime itself does not + * intercept any /dharma path. */ el_val_t dharma_connect(el_val_t cgi_id); el_val_t dharma_send(el_val_t channel, el_val_t content); @@ -231,6 +245,14 @@ void dharma_strengthen(el_val_t cgi_id, el_val_t weight); el_val_t dharma_relationship(el_val_t cgi_id); el_val_t dharma_peers(void); +/* Public C API: called by an El program's HTTP handler when a /dharma/event + * request arrives. Pushes onto the per-event-type queue and signals any + * pending dharma_field() blockers. All three arguments must be NUL-terminated + * C strings (or NULL — then treated as empty). */ +void el_runtime_dharma_event_arrive(const char* event_type, + const char* payload, + const char* source); + /* ── Engram local graph primitives ─────────────────────────────────────────── * Operate on the CGI's local Engram knowledge graph. * `engram_activate` queries the local graph only; `dharma_activate` is @@ -275,6 +297,14 @@ el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_va el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64); el_val_t llm_models(void); +/* Register a tool handler by name. The handler is looked up via dlsym + * (mirroring http_set_handler), so any El `fn (input)` compiles to + * a global C symbol that this function can locate at runtime. + * Handler signature: `el_val_t handler(el_val_t input_json)` — receives + * the tool input as a JSON-string el_val_t and returns a JSON-string + * el_val_t result. Used by llm_call_agentic. */ +void llm_register_tool(el_val_t name, el_val_t handler_fn_name); + /* ── args() ───────────────────────────────────────────────────────────────── * Provides access to command-line arguments passed to the program. * Populated by el_runtime_init_args() before main() runs. */ diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index a31eaf5..69886fe 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -433,9 +433,82 @@ fn cg_expr(expr: Map) -> String { return "/* if-expr */ ((" + cond_c + ") ? (el_val_t)1 : (el_val_t)0)" } + if kind == "Match" { + return cg_match(expr) + } + "EL_NULL" } +// ── Match codegen ───────────────────────────────────────────────────────────── +// +// Lower a match expression to a GCC/Clang statement-expression. +// A unique label suffix is allocated per match via state_set("__match_counter"). + +fn next_match_id() -> String { + let csv: String = state_get("__match_counter") + let n = 0 + if !str_eq(csv, "") { + let n = str_to_int(csv) + } + let n = n + 1 + state_set("__match_counter", native_int_to_str(n)) + native_int_to_str(n) +} + +fn cg_match(expr: Map) -> String { + let subject = expr["subject"] + let arms = expr["arms"] + let subj_c: String = cg_expr(subject) + let id: String = next_match_id() + let subj_var: String = "_match_subj_" + id + let result_var: String = "_match_result_" + id + let done_label: String = "_match_done_" + id + let out: String = "({ el_val_t " + subj_var + " = " + subj_c + "; el_val_t " + result_var + " = 0; " + let n: Int = native_list_len(arms) + let i = 0 + while i < n { + let arm = native_list_get(arms, i) + let pat = arm["pattern"] + let body = arm["body"] + let pkind: String = pat["pattern"] + let body_c: String = cg_expr(body) + if str_eq(pkind, "Wildcard") { + let out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } else { + if str_eq(pkind, "Binding") { + let bname: String = pat["name"] + let out = out + "{ el_val_t " + bname + " = " + subj_var + "; " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } else { + if str_eq(pkind, "LitInt") { + let v: String = pat["value"] + let out = out + "if (" + subj_var + " == " + v + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } else { + if str_eq(pkind, "LitStr") { + let v: String = pat["value"] + let out = out + "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } else { + if str_eq(pkind, "LitBool") { + let v: String = pat["value"] + let bv = "0" + if str_eq(v, "true") { + let bv = "1" + } + let out = out + "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } else { + // unknown pattern → wildcard + let out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } + } + } + } + } + let i = i + 1 + } + let out = out + done_label + ":; " + result_var + "; })" + out +} + // ── Variable scope tracking ─────────────────────────────────────────────────── // // El allows `let x = expr` to both declare and reassign x in the same scope. @@ -538,6 +611,7 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin if kind == "TypeDef" { return declared } if kind == "EnumDef" { return declared } if kind == "Import" { return declared } + if kind == "CgiBlock" { return declared } declared } @@ -767,6 +841,15 @@ fn cg_fn(stmt: Map) -> Void { let body = stmt["body"] let ret_type: String = stmt["ret_type"] let params_c: String = params_to_c(params) + // VBD role enforcement: dharma_emit / dharma_field may only be called + // from @manager-decorated functions. Surface violations to the C compiler + // via #error directives emitted before the function definition. + let decorator: String = stmt["decorator"] + if vbd_has_restricted_call(body) { + if !str_eq(decorator, "manager") { + emit_line("#error \"VBD violation: dharma_emit/dharma_field called from non-@manager fn '" + fn_name + "'\"") + } + } // Seed the per-function int-name set so the `+` codegen can dispatch // arithmetic vs concat on type-annotated identifiers. build_int_names_for_params(params) @@ -808,12 +891,174 @@ fn is_top_level_decl(stmt: Map) -> Bool { if kind == "TypeDef" { return true } if kind == "EnumDef" { return true } if kind == "Import" { return true } + if kind == "CgiBlock" { return true } + false +} + +// Format a string-or-EL_NULL argument for el_cgi_init. +fn cgi_arg(value: String, has_value: Bool) -> String { + if has_value { + return "EL_STR(" + c_str_lit(value) + ")" + } + return "EL_NULL" +} + +// ── VBD role enforcement ────────────────────────────────────────────────────── +// +// Scan a function body for direct calls to DHARMA-restricted builtins +// (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 } + false +} + +fn vbd_expr_has_restricted_call(expr: Map) -> Bool { + let kind: String = expr["expr"] + if str_eq(kind, "Call") { + let func = expr["func"] + let fk: String = func["expr"] + if str_eq(fk, "Ident") { + let fname: String = func["name"] + if vbd_is_restricted_name(fname) { return true } + } + if vbd_expr_has_restricted_call(func) { return true } + let args = expr["args"] + let an: Int = native_list_len(args) + let ai = 0 + while ai < an { + let a = native_list_get(args, ai) + if vbd_expr_has_restricted_call(a) { return true } + let ai = ai + 1 + } + return false + } + if str_eq(kind, "BinOp") { + let l = expr["left"] + let r = expr["right"] + if vbd_expr_has_restricted_call(l) { return true } + if vbd_expr_has_restricted_call(r) { return true } + return false + } + if str_eq(kind, "Not") { + return vbd_expr_has_restricted_call(expr["inner"]) + } + if str_eq(kind, "Neg") { + return vbd_expr_has_restricted_call(expr["inner"]) + } + if str_eq(kind, "Field") { + return vbd_expr_has_restricted_call(expr["object"]) + } + if str_eq(kind, "Index") { + if vbd_expr_has_restricted_call(expr["object"]) { return true } + if vbd_expr_has_restricted_call(expr["index"]) { return true } + return false + } + if str_eq(kind, "Try") { + return vbd_expr_has_restricted_call(expr["inner"]) + } + if str_eq(kind, "Array") { + let elems = expr["elems"] + let n: Int = native_list_len(elems) + let i = 0 + while i < n { + let e = native_list_get(elems, i) + if vbd_expr_has_restricted_call(e) { return true } + let i = i + 1 + } + return false + } + if str_eq(kind, "Map") { + let pairs = expr["pairs"] + let n: Int = native_list_len(pairs) + let i = 0 + while i < n { + let pair = native_list_get(pairs, i) + let v = pair["value"] + if vbd_expr_has_restricted_call(v) { return true } + let i = i + 1 + } + return false + } + if str_eq(kind, "If") { + if vbd_expr_has_restricted_call(expr["cond"]) { return true } + if vbd_has_restricted_call(expr["then"]) { return true } + if vbd_has_restricted_call(expr["else"]) { return true } + return false + } + if str_eq(kind, "For") { + if vbd_expr_has_restricted_call(expr["list"]) { return true } + if vbd_has_restricted_call(expr["body"]) { return true } + return false + } + if str_eq(kind, "Match") { + if vbd_expr_has_restricted_call(expr["subject"]) { return true } + let arms = expr["arms"] + let n: Int = native_list_len(arms) + let i = 0 + while i < n { + let arm = native_list_get(arms, i) + let body = arm["body"] + if vbd_expr_has_restricted_call(body) { return true } + let i = i + 1 + } + return false + } + false +} + +fn vbd_has_restricted_call(stmts: [Map]) -> Bool { + let n: Int = native_list_len(stmts) + let i = 0 + while i < n { + let s = native_list_get(stmts, i) + let sk: String = s["stmt"] + if str_eq(sk, "Let") { + if vbd_expr_has_restricted_call(s["value"]) { return true } + } + if str_eq(sk, "Return") { + if vbd_expr_has_restricted_call(s["value"]) { return true } + } + if str_eq(sk, "Expr") { + if vbd_expr_has_restricted_call(s["value"]) { return true } + } + if str_eq(sk, "While") { + if vbd_expr_has_restricted_call(s["cond"]) { return true } + if vbd_has_restricted_call(s["body"]) { return true } + } + if str_eq(sk, "For") { + if vbd_expr_has_restricted_call(s["list"]) { return true } + if vbd_has_restricted_call(s["body"]) { return true } + } + let i = i + 1 + } false } // ── Entry point ──────────────────────────────────────────────────────────────── fn codegen(stmts: [Map], source: String) -> String { + // Detect cgi blocks: at most one allowed. Emit a #error if more than one. + let n_top: Int = native_list_len(stmts) + let cgi_count = 0 + let cgi_block: Map = { "stmt": "None" } + let ti = 0 + while ti < n_top { + let s = native_list_get(stmts, ti) + let sk: String = s["stmt"] + if str_eq(sk, "CgiBlock") { + let cgi_count = cgi_count + 1 + if cgi_count == 1 { + let cgi_block = s + } + } + let ti = ti + 1 + } + if cgi_count > 1 { + emit_line("#error \"El: multiple cgi blocks in program (only one allowed)\"") + } + // Preamble emit_line("#include ") emit_line("#include ") @@ -851,6 +1096,23 @@ fn codegen(stmts: [Map], source: String) -> String { // main() emit_line("int main(int argc, char** argv) {") emit_line(" el_runtime_init_args(argc, argv);") + if cgi_count >= 1 { + let cname: String = cgi_block["name"] + let cdid: String = cgi_block["dharma_id"] + let cprin: String = cgi_block["principal"] + let cnet: String = cgi_block["network"] + let ceng: String = cgi_block["engram"] + let has_did: Bool = cgi_block["has_dharma_id"] + let has_prin: Bool = cgi_block["has_principal"] + let has_net: Bool = cgi_block["has_network"] + let has_eng: Bool = cgi_block["has_engram"] + let arg_name: String = "EL_STR(" + c_str_lit(cname) + ")" + let arg_did: String = cgi_arg(cdid, has_did) + let arg_prin: String = cgi_arg(cprin, has_prin) + let arg_net: String = cgi_arg(cnet, has_net) + let arg_eng: String = cgi_arg(ceng, has_eng) + emit_line(" el_cgi_init(" + arg_name + ", " + arg_did + ", " + arg_prin + ", " + arg_net + ", " + arg_eng + ");") + } let main_decl = native_list_empty() let i = 0 while i < n { diff --git a/el-compiler/src/parser.el b/el-compiler/src/parser.el index d1339a8..d194392 100644 --- a/el-compiler/src/parser.el +++ b/el-compiler/src/parser.el @@ -736,12 +736,93 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p) } - // @decorator — skip and parse next stmt + // @decorator — capture decorator name and attach to following stmt if k == "At" { let p = pos + 1 - // skip decorator name + let dec_name = tok_value(tokens, p) let p = p + 1 - return parse_stmt(tokens, p) + let r = parse_stmt(tokens, p) + let inner = r["node"] + let p2 = r["pos"] + let inner_kind: String = inner["stmt"] + if str_eq(inner_kind, "FnDef") { + let with_dec = { + "stmt": "FnDef", + "name": inner["name"], + "params": inner["params"], + "body": inner["body"], + "ret_type": inner["ret_type"], + "decorator": dec_name + } + return make_result(with_dec, p2) + } + return r + } + + // cgi block: cgi "name" { field: "val", ... } + if k == "Cgi" { + let p = pos + 1 + let name = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "LBrace") + let dharma_id = "" + let principal = "" + let network = "" + let engram = "" + let has_dharma_id = false + let has_principal = false + let has_network = false + let has_engram = false + let running = true + while running { + let k2 = tok_kind(tokens, p) + if k2 == "RBrace" { + let running = false + } else { + if k2 == "Eof" { + let running = false + } else { + let fname = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "Colon") + let fval = tok_value(tokens, p) + let p = p + 1 + if str_eq(fname, "dharma_id") { + let dharma_id = fval + let has_dharma_id = true + } + if str_eq(fname, "principal") { + let principal = fval + let has_principal = true + } + if str_eq(fname, "network") { + let network = fval + let has_network = true + } + if str_eq(fname, "engram") { + let engram = fval + let has_engram = true + } + let k3 = tok_kind(tokens, p) + if k3 == "Comma" { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RBrace") + return make_result({ + "stmt": "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) } // bare expression or if/match statement diff --git a/elc-combined.el b/elc-combined.el index 90a46b8..7886ad6 100644 --- a/elc-combined.el +++ b/elc-combined.el @@ -1272,12 +1272,93 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p) } - // @decorator — skip and parse next stmt + // @decorator — capture decorator name and attach to following stmt if k == "At" { let p = pos + 1 - // skip decorator name + let dec_name = tok_value(tokens, p) let p = p + 1 - return parse_stmt(tokens, p) + let r = parse_stmt(tokens, p) + let inner = r["node"] + let p2 = r["pos"] + let inner_kind: String = inner["stmt"] + if str_eq(inner_kind, "FnDef") { + let with_dec = { + "stmt": "FnDef", + "name": inner["name"], + "params": inner["params"], + "body": inner["body"], + "ret_type": inner["ret_type"], + "decorator": dec_name + } + return make_result(with_dec, p2) + } + return r + } + + // cgi block: cgi "name" { field: "val", ... } + if k == "Cgi" { + let p = pos + 1 + let name = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "LBrace") + let dharma_id = "" + let principal = "" + let network = "" + let engram = "" + let has_dharma_id = false + let has_principal = false + let has_network = false + let has_engram = false + let running = true + while running { + let k2 = tok_kind(tokens, p) + if k2 == "RBrace" { + let running = false + } else { + if k2 == "Eof" { + let running = false + } else { + let fname = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "Colon") + let fval = tok_value(tokens, p) + let p = p + 1 + if str_eq(fname, "dharma_id") { + let dharma_id = fval + let has_dharma_id = true + } + if str_eq(fname, "principal") { + let principal = fval + let has_principal = true + } + if str_eq(fname, "network") { + let network = fval + let has_network = true + } + if str_eq(fname, "engram") { + let engram = fval + let has_engram = true + } + let k3 = tok_kind(tokens, p) + if k3 == "Comma" { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RBrace") + return make_result({ + "stmt": "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) } // bare expression or if/match statement @@ -1752,9 +1833,82 @@ fn cg_expr(expr: Map) -> String { return "/* if-expr */ ((" + cond_c + ") ? (el_val_t)1 : (el_val_t)0)" } + if kind == "Match" { + return cg_match(expr) + } + "EL_NULL" } +// ── Match codegen ───────────────────────────────────────────────────────────── +// +// Lower a match expression to a GCC/Clang statement-expression. +// A unique label suffix is allocated per match via state_set("__match_counter"). + +fn next_match_id() -> String { + let csv: String = state_get("__match_counter") + let n = 0 + if !str_eq(csv, "") { + let n = str_to_int(csv) + } + let n = n + 1 + state_set("__match_counter", native_int_to_str(n)) + native_int_to_str(n) +} + +fn cg_match(expr: Map) -> String { + let subject = expr["subject"] + let arms = expr["arms"] + let subj_c: String = cg_expr(subject) + let id: String = next_match_id() + let subj_var: String = "_match_subj_" + id + let result_var: String = "_match_result_" + id + let done_label: String = "_match_done_" + id + let out: String = "({ el_val_t " + subj_var + " = " + subj_c + "; el_val_t " + result_var + " = 0; " + let n: Int = native_list_len(arms) + let i = 0 + while i < n { + let arm = native_list_get(arms, i) + let pat = arm["pattern"] + let body = arm["body"] + let pkind: String = pat["pattern"] + let body_c: String = cg_expr(body) + if str_eq(pkind, "Wildcard") { + let out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } else { + if str_eq(pkind, "Binding") { + let bname: String = pat["name"] + let out = out + "{ el_val_t " + bname + " = " + subj_var + "; " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } else { + if str_eq(pkind, "LitInt") { + let v: String = pat["value"] + let out = out + "if (" + subj_var + " == " + v + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } else { + if str_eq(pkind, "LitStr") { + let v: String = pat["value"] + let out = out + "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } else { + if str_eq(pkind, "LitBool") { + let v: String = pat["value"] + let bv = "0" + if str_eq(v, "true") { + let bv = "1" + } + let out = out + "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } else { + // unknown pattern → wildcard + let out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + } + } + } + } + } + let i = i + 1 + } + let out = out + done_label + ":; " + result_var + "; })" + out +} + // ── Variable scope tracking ─────────────────────────────────────────────────── // // El allows `let x = expr` to both declare and reassign x in the same scope. @@ -1857,6 +2011,7 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin if kind == "TypeDef" { return declared } if kind == "EnumDef" { return declared } if kind == "Import" { return declared } + if kind == "CgiBlock" { return declared } declared } @@ -2086,6 +2241,15 @@ fn cg_fn(stmt: Map) -> Void { let body = stmt["body"] let ret_type: String = stmt["ret_type"] let params_c: String = params_to_c(params) + // VBD role enforcement: dharma_emit / dharma_field may only be called + // from @manager-decorated functions. Surface violations to the C compiler + // via #error directives emitted before the function definition. + let decorator: String = stmt["decorator"] + if vbd_has_restricted_call(body) { + if !str_eq(decorator, "manager") { + emit_line("#error \"VBD violation: dharma_emit/dharma_field called from non-@manager fn '" + fn_name + "'\"") + } + } // Seed the per-function int-name set so the `+` codegen can dispatch // arithmetic vs concat on type-annotated identifiers. build_int_names_for_params(params) @@ -2127,12 +2291,174 @@ fn is_top_level_decl(stmt: Map) -> Bool { if kind == "TypeDef" { return true } if kind == "EnumDef" { return true } if kind == "Import" { return true } + if kind == "CgiBlock" { return true } + false +} + +// Format a string-or-EL_NULL argument for el_cgi_init. +fn cgi_arg(value: String, has_value: Bool) -> String { + if has_value { + return "EL_STR(" + c_str_lit(value) + ")" + } + return "EL_NULL" +} + +// ── VBD role enforcement ────────────────────────────────────────────────────── +// +// Scan a function body for direct calls to DHARMA-restricted builtins +// (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 } + false +} + +fn vbd_expr_has_restricted_call(expr: Map) -> Bool { + let kind: String = expr["expr"] + if str_eq(kind, "Call") { + let func = expr["func"] + let fk: String = func["expr"] + if str_eq(fk, "Ident") { + let fname: String = func["name"] + if vbd_is_restricted_name(fname) { return true } + } + if vbd_expr_has_restricted_call(func) { return true } + let args = expr["args"] + let an: Int = native_list_len(args) + let ai = 0 + while ai < an { + let a = native_list_get(args, ai) + if vbd_expr_has_restricted_call(a) { return true } + let ai = ai + 1 + } + return false + } + if str_eq(kind, "BinOp") { + let l = expr["left"] + let r = expr["right"] + if vbd_expr_has_restricted_call(l) { return true } + if vbd_expr_has_restricted_call(r) { return true } + return false + } + if str_eq(kind, "Not") { + return vbd_expr_has_restricted_call(expr["inner"]) + } + if str_eq(kind, "Neg") { + return vbd_expr_has_restricted_call(expr["inner"]) + } + if str_eq(kind, "Field") { + return vbd_expr_has_restricted_call(expr["object"]) + } + if str_eq(kind, "Index") { + if vbd_expr_has_restricted_call(expr["object"]) { return true } + if vbd_expr_has_restricted_call(expr["index"]) { return true } + return false + } + if str_eq(kind, "Try") { + return vbd_expr_has_restricted_call(expr["inner"]) + } + if str_eq(kind, "Array") { + let elems = expr["elems"] + let n: Int = native_list_len(elems) + let i = 0 + while i < n { + let e = native_list_get(elems, i) + if vbd_expr_has_restricted_call(e) { return true } + let i = i + 1 + } + return false + } + if str_eq(kind, "Map") { + let pairs = expr["pairs"] + let n: Int = native_list_len(pairs) + let i = 0 + while i < n { + let pair = native_list_get(pairs, i) + let v = pair["value"] + if vbd_expr_has_restricted_call(v) { return true } + let i = i + 1 + } + return false + } + if str_eq(kind, "If") { + if vbd_expr_has_restricted_call(expr["cond"]) { return true } + if vbd_has_restricted_call(expr["then"]) { return true } + if vbd_has_restricted_call(expr["else"]) { return true } + return false + } + if str_eq(kind, "For") { + if vbd_expr_has_restricted_call(expr["list"]) { return true } + if vbd_has_restricted_call(expr["body"]) { return true } + return false + } + if str_eq(kind, "Match") { + if vbd_expr_has_restricted_call(expr["subject"]) { return true } + let arms = expr["arms"] + let n: Int = native_list_len(arms) + let i = 0 + while i < n { + let arm = native_list_get(arms, i) + let body = arm["body"] + if vbd_expr_has_restricted_call(body) { return true } + let i = i + 1 + } + return false + } + false +} + +fn vbd_has_restricted_call(stmts: [Map]) -> Bool { + let n: Int = native_list_len(stmts) + let i = 0 + while i < n { + let s = native_list_get(stmts, i) + let sk: String = s["stmt"] + if str_eq(sk, "Let") { + if vbd_expr_has_restricted_call(s["value"]) { return true } + } + if str_eq(sk, "Return") { + if vbd_expr_has_restricted_call(s["value"]) { return true } + } + if str_eq(sk, "Expr") { + if vbd_expr_has_restricted_call(s["value"]) { return true } + } + if str_eq(sk, "While") { + if vbd_expr_has_restricted_call(s["cond"]) { return true } + if vbd_has_restricted_call(s["body"]) { return true } + } + if str_eq(sk, "For") { + if vbd_expr_has_restricted_call(s["list"]) { return true } + if vbd_has_restricted_call(s["body"]) { return true } + } + let i = i + 1 + } false } // ── Entry point ──────────────────────────────────────────────────────────────── fn codegen(stmts: [Map], source: String) -> String { + // Detect cgi blocks: at most one allowed. Emit a #error if more than one. + let n_top: Int = native_list_len(stmts) + let cgi_count = 0 + let cgi_block: Map = { "stmt": "None" } + let ti = 0 + while ti < n_top { + let s = native_list_get(stmts, ti) + let sk: String = s["stmt"] + if str_eq(sk, "CgiBlock") { + let cgi_count = cgi_count + 1 + if cgi_count == 1 { + let cgi_block = s + } + } + let ti = ti + 1 + } + if cgi_count > 1 { + emit_line("#error \"El: multiple cgi blocks in program (only one allowed)\"") + } + // Preamble emit_line("#include ") emit_line("#include ") @@ -2170,6 +2496,23 @@ fn codegen(stmts: [Map], source: String) -> String { // main() emit_line("int main(int argc, char** argv) {") emit_line(" el_runtime_init_args(argc, argv);") + if cgi_count >= 1 { + let cname: String = cgi_block["name"] + let cdid: String = cgi_block["dharma_id"] + let cprin: String = cgi_block["principal"] + let cnet: String = cgi_block["network"] + let ceng: String = cgi_block["engram"] + let has_did: Bool = cgi_block["has_dharma_id"] + let has_prin: Bool = cgi_block["has_principal"] + let has_net: Bool = cgi_block["has_network"] + let has_eng: Bool = cgi_block["has_engram"] + let arg_name: String = "EL_STR(" + c_str_lit(cname) + ")" + let arg_did: String = cgi_arg(cdid, has_did) + let arg_prin: String = cgi_arg(cprin, has_prin) + let arg_net: String = cgi_arg(cnet, has_net) + let arg_eng: String = cgi_arg(ceng, has_eng) + emit_line(" el_cgi_init(" + arg_name + ", " + arg_did + ", " + arg_prin + ", " + arg_net + ", " + arg_eng + ");") + } let main_decl = native_list_empty() let i = 0 while i < n {