runtime + compiler: dharma, match, cgi blocks, VBD, agentic LLM
Two parallel agent sweeps closing the remaining structural gaps.
== Compiler completions ==
- match codegen: lowers Match into GCC/Clang statement-expression
({ ... }). Patterns: Wildcard, Binding, LitInt (==), LitStr
(str_eq), LitBool. Per-match unique label via state counter.
Verified: classify(0)→"zero", classify(1)→"one", classify(7)→"other".
- cgi block parsing: `cgi "name" { dharma_id, principal, network,
engram }` → CgiBlock AST node → el_cgi_init() emitted as the first
call in main() after el_runtime_init_args. Multiple cgi blocks per
program emit a #error directive. Missing optional fields → EL_NULL.
- VBD compile-time enforcement: parser attaches `decorator: <name>`
to FnDef. Codegen recursively walks fn bodies (Call/BinOp/Not/Neg/
Field/Index/Try/Array/Map/If/For/Match plus Let/Return/Expr/While/
For). If a non-@manager function calls dharma_emit or dharma_field,
emit `#error "VBD violation: ... fn '<name>'"` before the function
body. Verified: @engine fn calling dharma_emit → cc fails with the
message. @manager fn calling dharma_emit → compiles clean.
Three-stage closure: stage1.c == stage2.c == stage3.c (2791 lines
each, byte-identical). dist/platform/elc rebuilt at 165 KB; .prev5
preserved.
== Runtime completions ==
- Real dharma_* primitives, no more stubs. Channel registry,
request/response over HTTP, network-wide spreading activation,
fire-and-forget event emission, blocking dharma_field with
pthread_cond_timedwait (30s default), Hebbian relationship
weights stored as Engram edges between dharma:self and
dharma:peer:<id>, sorted-by-weight peer list. URL/ID arrays
snapshotted before network I/O so mutexes never block on socket.
- New public C contract: el_runtime_dharma_event_arrive(type, payload,
source) — application HTTP handler calls this when /dharma/event
arrives, runtime broadcasts on _dharma_event_cv. Keeps the HTTP
server generic; events flow through the application's router.
- llm_call_agentic real multi-turn loop. Tool registry (mutex-
protected, dlsym-resolved, mirroring http_set_handler). Loop:
build request with tools+messages → POST → dispatch on stop_reason.
end_turn → return text. max_tokens → text + "[truncated]". tool_use
→ walk content[], call registered handler per block, build
tool_result message, append to conversation, loop. Iteration cap
10. Tools not registered return {"error":"tool not registered: X"}
with is_error: true.
- New builtin: llm_register_tool(name, handler_fn_name).
Compile clean: cc -std=c11 -Wall -Wextra -c → zero warnings, zero
errors. Smoke test exercises every new dharma_* primitive +
llm_register_tool round-trip.
Runtime grew 3309→4079 lines (.c, ~155 KB), 312→342 lines (.h).
== Integration ==
Engram rebuilt against the new runtime: 130 KB binary, daemon
swapped on :8742 cleanly, /health and /api/stats both returning
correctly under launchd. No regressions.
== Status of "planned" items in language.md ==
- match codegen → IMPLEMENTED
- cgi block parsing → IMPLEMENTED
- VBD enforcement → IMPLEMENTED
- % operator → IMPLEMENTED (earlier today)
- vessel keyword → lexed (codegen uses package compatible)
- activate construct → still planned (low priority; engram_activate
builtin covers the use case for now)
- sealed block → still planned
- dharma_emit fanout parallelization → potential future work, current
serial behavior matches spec
This commit is contained in:
@@ -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:<cgi_id>" */
|
||||
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
|
||||
* "<id>@<url>" where <url> is the peer's Engram-exposed daemon.
|
||||
*
|
||||
* Channels are logical handles cached per-cgi: `dharma_connect` is
|
||||
* idempotent and returns "ch:<cgi_id>". 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:<cgi_id>"). 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 @<url> suffix */
|
||||
char* base_id; /* registry-id portion (before @) for relationship lookup */
|
||||
char* url; /* resolved transport URL */
|
||||
char* channel_id; /* "ch:<cgi_id>" */
|
||||
} 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 "<id>@<url>" → (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:<base_id>". 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/<name>", 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). */
|
||||
|
||||
@@ -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
|
||||
* "<registry-id>@<transport-url>" e.g. "ntn-genesis@http://localhost:7770"
|
||||
* If the @<url> portion is omitted, transport defaults to
|
||||
* "http://localhost:7770" (the local CGI daemon assumption).
|
||||
*
|
||||
* Wire protocol (all peers expose):
|
||||
* POST <url>/dharma/recv { channel, from, content } → response body
|
||||
* POST <url>/dharma/event { type, payload, source, timestamp }
|
||||
* POST <url>/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 <name>(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. */
|
||||
|
||||
@@ -433,9 +433,82 @@ fn cg_expr(expr: Map<String, Any>) -> 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, Any>) -> 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<String, Any>, 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<String, Any>) -> 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<String, Any>) -> 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<String, Any>) -> 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<String, Any>]) -> 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<String, Any>], 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<String, Any> = { "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 <stdint.h>")
|
||||
emit_line("#include <stdlib.h>")
|
||||
@@ -851,6 +1096,23 @@ fn codegen(stmts: [Map<String, Any>], 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 {
|
||||
|
||||
@@ -736,12 +736,93 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user