/* * el_runtime.c — El language C runtime implementation * * All functions use el_val_t (= int64_t) as the universal value type. * Strings are transported as their pointer address cast to int64_t. * On any 64-bit system sizeof(pointer) <= sizeof(int64_t), so this is safe. * * Compile with: * cc -std=c11 -I -lcurl -lpthread -o .c el_runtime.c * * Link requirements: -lcurl (HTTP client + LLM), -lpthread (HTTP server). */ #include "el_runtime.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* dlsym for http_set_handler fallback */ #include #include #include #include #include /* ── Internal allocators ─────────────────────────────────────────────────── */ static char* el_strdup(const char* s) { if (!s) return strdup(""); return strdup(s); } static char* el_strbuf(size_t n) { char* p = malloc(n + 1); if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } p[0] = '\0'; return p; } /* Wrap an allocated C string as el_val_t */ static el_val_t el_wrap_str(char* s) { return EL_STR(s); } /* ── I/O ──────────────────────────────────────────────────────────────────── */ void println(el_val_t s) { const char* str = EL_CSTR(s); if (str) puts(str); else puts(""); } void print(el_val_t s) { const char* str = EL_CSTR(s); if (str) fputs(str, stdout); } el_val_t readline(void) { char buf[4096]; if (!fgets(buf, sizeof(buf), stdin)) return el_wrap_str(el_strdup("")); size_t len = strlen(buf); if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; return el_wrap_str(el_strdup(buf)); } /* ── String builtins ─────────────────────────────────────────────────────── */ el_val_t el_str_concat(el_val_t av, el_val_t bv) { const char* a = EL_CSTR(av); const char* b = EL_CSTR(bv); if (!a) a = ""; if (!b) b = ""; size_t la = strlen(a); size_t lb = strlen(b); char* out = el_strbuf(la + lb); memcpy(out, a, la); memcpy(out + la, b, lb); out[la + lb] = '\0'; return el_wrap_str(out); } el_val_t str_eq(el_val_t av, el_val_t bv) { const char* a = EL_CSTR(av); const char* b = EL_CSTR(bv); if (!a || !b) return (el_val_t)(a == b); return (el_val_t)(strcmp(a, b) == 0); } el_val_t str_starts_with(el_val_t sv, el_val_t prefv) { const char* s = EL_CSTR(sv); const char* prefix = EL_CSTR(prefv); if (!s || !prefix) return 0; size_t lp = strlen(prefix); return (el_val_t)(strncmp(s, prefix, lp) == 0); } el_val_t str_ends_with(el_val_t sv, el_val_t sufv) { const char* s = EL_CSTR(sv); const char* suffix = EL_CSTR(sufv); if (!s || !suffix) return 0; size_t ls = strlen(s); size_t lsuf = strlen(suffix); if (lsuf > ls) return 0; return (el_val_t)(strcmp(s + ls - lsuf, suffix) == 0); } el_val_t str_len(el_val_t sv) { const char* s = EL_CSTR(sv); if (!s) return 0; return (el_val_t)strlen(s); } el_val_t str_concat(el_val_t a, el_val_t b) { return el_str_concat(a, b); } el_val_t int_to_str(el_val_t n) { char buf[32]; snprintf(buf, sizeof(buf), "%lld", (long long)n); return el_wrap_str(el_strdup(buf)); } el_val_t str_to_int(el_val_t sv) { const char* s = EL_CSTR(sv); if (!s) return 0; return (el_val_t)atoll(s); } el_val_t str_slice(el_val_t sv, el_val_t start, el_val_t end) { const char* s = EL_CSTR(sv); if (!s) return el_wrap_str(el_strdup("")); int64_t len = (int64_t)strlen(s); if (start < 0) start = 0; if (end > len) end = len; if (start >= end) return el_wrap_str(el_strdup("")); int64_t sz = end - start; char* out = el_strbuf((size_t)sz); memcpy(out, s + start, (size_t)sz); out[sz] = '\0'; return el_wrap_str(out); } el_val_t str_contains(el_val_t sv, el_val_t subv) { const char* s = EL_CSTR(sv); const char* sub = EL_CSTR(subv); if (!s || !sub) return 0; return (el_val_t)(strstr(s, sub) != NULL); } el_val_t str_replace(el_val_t sv, el_val_t fromv, el_val_t tov) { const char* s = EL_CSTR(sv); const char* from = EL_CSTR(fromv); const char* to = EL_CSTR(tov); if (!s || !from || !to) return el_wrap_str(el_strdup(s ? s : "")); size_t ls = strlen(s); size_t lf = strlen(from); size_t lt = strlen(to); if (lf == 0) return el_wrap_str(el_strdup(s)); size_t count = 0; const char* p = s; while ((p = strstr(p, from)) != NULL) { count++; p += lf; } size_t out_sz = ls + count * lt + 1; char* out = el_strbuf(out_sz); char* dst = out; p = s; const char* found; while ((found = strstr(p, from)) != NULL) { size_t chunk = (size_t)(found - p); memcpy(dst, p, chunk); dst += chunk; memcpy(dst, to, lt); dst += lt; p = found + lf; } strcpy(dst, p); return el_wrap_str(out); } el_val_t str_to_upper(el_val_t sv) { const char* s = EL_CSTR(sv); if (!s) return el_wrap_str(el_strdup("")); size_t n = strlen(s); char* out = el_strbuf(n); for (size_t i = 0; i < n; i++) out[i] = (char)toupper((unsigned char)s[i]); out[n] = '\0'; return el_wrap_str(out); } el_val_t str_to_lower(el_val_t sv) { const char* s = EL_CSTR(sv); if (!s) return el_wrap_str(el_strdup("")); size_t n = strlen(s); char* out = el_strbuf(n); for (size_t i = 0; i < n; i++) out[i] = (char)tolower((unsigned char)s[i]); out[n] = '\0'; return el_wrap_str(out); } el_val_t str_trim(el_val_t sv) { const char* s = EL_CSTR(sv); if (!s) return el_wrap_str(el_strdup("")); while (*s && isspace((unsigned char)*s)) s++; size_t n = strlen(s); while (n > 0 && isspace((unsigned char)s[n - 1])) n--; char* out = el_strbuf(n); memcpy(out, s, n); out[n] = '\0'; return el_wrap_str(out); } /* ── Math ────────────────────────────────────────────────────────────────── */ el_val_t el_abs(el_val_t n) { return n < 0 ? -n : n; } el_val_t el_max(el_val_t a, el_val_t b) { return a > b ? a : b; } el_val_t el_min(el_val_t a, el_val_t b) { return a < b ? a : b; } /* ── List ────────────────────────────────────────────────────────────────── */ /* * Dynamic array header: * int64_t capacity * int64_t length * el_val_t elems[] */ typedef struct { int64_t capacity; int64_t length; el_val_t elems[1]; } ElList; static ElList* list_alloc(int64_t cap) { ElList* lst = malloc(sizeof(ElList) + (size_t)(cap > 1 ? cap - 1 : 0) * sizeof(el_val_t)); if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } lst->capacity = cap; lst->length = 0; return lst; } el_val_t el_list_empty(void) { return EL_STR(list_alloc(4)); } el_val_t el_list_new(el_val_t count, ...) { ElList* lst = list_alloc(count > 0 ? count : 4); va_list ap; va_start(ap, count); for (int64_t i = 0; i < count; i++) { lst->elems[i] = va_arg(ap, el_val_t); } va_end(ap); lst->length = count; return EL_STR(lst); } el_val_t el_list_len(el_val_t listv) { ElList* lst = (ElList*)(uintptr_t)listv; if (!lst) return 0; return lst->length; } el_val_t el_list_get(el_val_t listv, el_val_t index) { ElList* lst = (ElList*)(uintptr_t)listv; if (!lst) return 0; if (index < 0 || index >= lst->length) return 0; return lst->elems[index]; } el_val_t el_list_append(el_val_t listv, el_val_t elem) { /* Always allocate a fresh list rather than realloc'ing the input. * El callers commonly hold a stale pointer to the original list (e.g. * cg_if_stmt passes `declared` to two successive cg_stmts calls; the * first call may realloc the underlying block, leaving the second * with a dangling pointer). Persistent allocation eliminates that * whole class of use-after-free at modest memory cost. */ ElList* old = (ElList*)(uintptr_t)listv; int64_t old_len = old ? old->length : 0; int64_t new_cap = old_len + 1; if (new_cap < 4) new_cap = 4; ElList* new_lst = malloc(sizeof(ElList) + (size_t)(new_cap - 1) * sizeof(el_val_t)); if (!new_lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } new_lst->capacity = new_cap; new_lst->length = old_len + 1; if (old && old_len > 0) { memcpy(new_lst->elems, old->elems, (size_t)old_len * sizeof(el_val_t)); } new_lst->elems[old_len] = elem; return EL_STR(new_lst); } /* ── Map ─────────────────────────────────────────────────────────────────── */ typedef struct { int64_t count; el_val_t* keys; el_val_t* values; } ElMap; el_val_t el_map_new(el_val_t pair_count, ...) { ElMap* m = malloc(sizeof(ElMap)); if (!m) { fputs("el_runtime: out of memory\n", stderr); exit(1); } m->count = pair_count; m->keys = malloc(sizeof(el_val_t) * (size_t)(pair_count > 0 ? pair_count : 1)); m->values = malloc(sizeof(el_val_t) * (size_t)(pair_count > 0 ? pair_count : 1)); va_list ap; va_start(ap, pair_count); for (int64_t i = 0; i < pair_count; i++) { m->keys[i] = va_arg(ap, el_val_t); m->values[i] = va_arg(ap, el_val_t); } va_end(ap); return EL_STR(m); } static ElMap* as_map(el_val_t v) { return (ElMap*)(uintptr_t)v; } el_val_t el_map_get(el_val_t mapv, el_val_t keyv) { ElMap* m = as_map(mapv); const char* key = EL_CSTR(keyv); if (!m || !key) return 0; for (int64_t i = 0; i < m->count; i++) { const char* k = EL_CSTR(m->keys[i]); if (k && strcmp(k, key) == 0) return m->values[i]; } return 0; } el_val_t el_get_field(el_val_t mapv, el_val_t keyv) { return el_map_get(mapv, keyv); } el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { ElMap* m = as_map(mapv); const char* key = EL_CSTR(keyv); if (!m) return 0; for (int64_t i = 0; i < m->count; i++) { const char* k = EL_CSTR(m->keys[i]); if (k && strcmp(k, key) == 0) { m->values[i] = value; return mapv; } } int64_t nc = m->count + 1; m->keys = realloc(m->keys, sizeof(el_val_t) * (size_t)nc); m->values = realloc(m->values, sizeof(el_val_t) * (size_t)nc); m->keys[m->count] = keyv; m->values[m->count] = value; m->count = nc; return mapv; } /* ── Batch 2/3 forward decls (defined later in JSON section) ────────────── */ typedef struct JsonBuf JsonBuf; typedef struct JsonParser JsonParser; static void jb_init(JsonBuf* b); static void jb_putc(JsonBuf* b, char c); static void jb_puts(JsonBuf* b, const char* s); static void jb_emit_escaped(JsonBuf* b, const char* s); static int looks_like_string(el_val_t v); static const char* json_find_key(const char* s, const char* key); static const char* json_skip_value(const char* p); static char* jp_parse_string_raw(JsonParser* jp); /* Struct definitions are visible here because batch 2/3 helpers above use * them by value; the bodies (jb_init, etc.) appear in the JSON section. */ struct JsonBuf { char* buf; size_t len; size_t cap; }; struct JsonParser { const char* p; const char* end; int err; }; /* ── Batch 2: Real HTTP (libcurl client + POSIX-socket server) ───────────── */ /* * Client: blocking libcurl easy-handle calls. Errors are returned as a JSON * fragment {"error":"..."} so callers can detect via str_starts_with("{") / * json_get_string("error", ...). * * Server: bind/listen/accept loop on a TCP socket. Each accepted connection * is handled in its own pthread (detached). A semaphore-style counter caps * concurrent in-flight connections at HTTP_MAX_CONNS (64). When the cap is * reached, accept() blocks until a worker exits. This prevents runaway * thread creation under high load. * * Handler dispatch: El does not expose first-class function references at * the runtime layer, so the second argument to http_serve(port, handler) is * treated as a string name (or any el_val_t — the runtime ignores its * value and uses the registry). Callers register a C-level handler via * * extern void el_runtime_register_handler(const char* name, * el_val_t (*fn)(el_val_t, * el_val_t, * el_val_t)); * * and select the active handler by calling http_set_handler("name") from * El, or by setting it directly through the C registry. If no handler is * registered, the server replies with a 200 carrying a default message so * the loop is observable. */ /* ── HTTP client write-callback buffer ───────────────────────────────────── */ typedef struct { char* data; size_t len; size_t cap; } HttpBuf; static void httpbuf_init(HttpBuf* b) { b->cap = 1024; b->len = 0; b->data = malloc(b->cap); if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } b->data[0] = '\0'; } static void httpbuf_append(HttpBuf* b, const void* src, size_t n) { if (b->len + n + 1 > b->cap) { while (b->len + n + 1 > b->cap) b->cap *= 2; b->data = realloc(b->data, b->cap); if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } } memcpy(b->data + b->len, src, n); b->len += n; b->data[b->len] = '\0'; } static size_t http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { size_t n = size * nmemb; httpbuf_append((HttpBuf*)ud, ptr, n); return n; } /* JSON-escape an arbitrary C string into an allocated buffer. */ static char* json_escape_alloc(const char* s) { if (!s) return el_strdup(""); JsonBuf b; jb_init(&b); for (const char* p = s; *p; p++) { unsigned char c = (unsigned char)*p; switch (c) { case '"': jb_puts(&b, "\\\""); break; case '\\': jb_puts(&b, "\\\\"); break; case '\n': jb_puts(&b, "\\n"); break; case '\r': jb_puts(&b, "\\r"); break; case '\t': jb_puts(&b, "\\t"); break; default: if (c < 0x20) { char tmp[8]; snprintf(tmp, sizeof(tmp), "\\u%04x", c); jb_puts(&b, tmp); } else jb_putc(&b, (char)c); } } return b.buf; } static el_val_t http_error_json(const char* msg) { char* esc = json_escape_alloc(msg ? msg : "unknown error"); char* buf = el_strbuf(strlen(esc) + 16); sprintf(buf, "{\"error\":\"%s\"}", esc); free(esc); return el_wrap_str(buf); } /* Internal: do a libcurl request; takes optional body/headers. */ static el_val_t http_do(const char* method, const char* url, const char* body, struct curl_slist* extra_headers) { if (!url || !*url) return http_error_json("empty url"); CURL* c = curl_easy_init(); if (!c) return http_error_json("curl_easy_init failed"); HttpBuf rb; httpbuf_init(&rb); char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; curl_easy_setopt(c, CURLOPT_URL, url); curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(c, CURLOPT_TIMEOUT, 60L); curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); if (method && strcmp(method, "POST") == 0) { curl_easy_setopt(c, CURLOPT_POST, 1L); curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); } CURLcode rc = curl_easy_perform(c); curl_easy_cleanup(c); if (rc != CURLE_OK) { free(rb.data); const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); return http_error_json(m); } return el_wrap_str(rb.data); } el_val_t http_get(el_val_t url) { return http_do("GET", EL_CSTR(url), NULL, NULL); } el_val_t http_post(el_val_t url, el_val_t body) { return http_do("POST", EL_CSTR(url), EL_CSTR(body), NULL); } el_val_t http_post_json(el_val_t url, el_val_t json_body) { struct curl_slist* h = NULL; h = curl_slist_append(h, "Content-Type: application/json"); el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h); curl_slist_free_all(h); return r; } /* Build a curl_slist from an ElMap of name -> value strings. */ static struct curl_slist* headers_from_map(el_val_t headers_map) { struct curl_slist* h = NULL; ElMap* m = as_map(headers_map); if (!m) return NULL; for (int64_t i = 0; i < m->count; i++) { const char* k = EL_CSTR(m->keys[i]); const char* v = EL_CSTR(m->values[i]); if (!k || !v) continue; size_t n = strlen(k) + strlen(v) + 4; char* line = malloc(n); if (!line) continue; snprintf(line, n, "%s: %s", k, v); h = curl_slist_append(h, line); free(line); } return h; } el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map) { struct curl_slist* h = headers_from_map(headers_map); el_val_t r = http_do("GET", EL_CSTR(url), NULL, h); if (h) curl_slist_free_all(h); return r; } el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map) { struct curl_slist* h = headers_from_map(headers_map); el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(body), h); if (h) curl_slist_free_all(h); return r; } el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header) { struct curl_slist* h = NULL; h = curl_slist_append(h, "Content-Type: application/x-www-form-urlencoded"); const char* a = EL_CSTR(auth_header); if (a && *a) { size_t n = strlen(a) + 32; char* line = malloc(n); snprintf(line, n, "Authorization: %s", a); h = curl_slist_append(h, line); free(line); } el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(form_body), h); curl_slist_free_all(h); return r; } /* ── HTTP server (POSIX sockets + pthreads) ──────────────────────────────── */ #define HTTP_MAX_CONNS 64 typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body); typedef struct { char* name; http_handler_fn fn; } HttpHandlerEntry; static HttpHandlerEntry _http_handlers[32]; static size_t _http_handler_count = 0; static char* _http_active_handler = NULL; static pthread_mutex_t _http_handler_mu = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t _http_conn_mu = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t _http_conn_cv = PTHREAD_COND_INITIALIZER; static int _http_conn_active = 0; /* Public C-level API: register a handler by name. Programs that want El * `http_serve` to dispatch into their handler call this from main() before * http_serve. Not declared in the header to keep the public API minimal — * extern lookup works since C symbols are global. */ void el_runtime_register_handler(const char* name, http_handler_fn fn); void el_runtime_register_handler(const char* name, http_handler_fn fn) { if (!name || !fn) return; pthread_mutex_lock(&_http_handler_mu); for (size_t i = 0; i < _http_handler_count; i++) { if (strcmp(_http_handlers[i].name, name) == 0) { _http_handlers[i].fn = fn; pthread_mutex_unlock(&_http_handler_mu); return; } } if (_http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { _http_handlers[_http_handler_count].name = el_strdup(name); _http_handlers[_http_handler_count].fn = fn; _http_handler_count++; } pthread_mutex_unlock(&_http_handler_mu); } void http_set_handler(el_val_t name) { const char* n = EL_CSTR(name); pthread_mutex_lock(&_http_handler_mu); free(_http_active_handler); _http_active_handler = el_strdup(n ? n : ""); /* If the name is not yet in the registry, try dlsym lookup against * the running binary's symbol table. Every El `fn name(...)` compiles * to a global C symbol with that exact name, so El programs can self- * register their own handlers just by calling http_set_handler("name"). */ if (n && *n) { int found = 0; for (size_t i = 0; i < _http_handler_count; i++) { if (strcmp(_http_handlers[i].name, n) == 0) { found = 1; break; } } if (!found) { void* sym = dlsym(RTLD_DEFAULT, n); if (sym && _http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { _http_handlers[_http_handler_count].name = el_strdup(n); _http_handlers[_http_handler_count].fn = (http_handler_fn)sym; _http_handler_count++; } } } pthread_mutex_unlock(&_http_handler_mu); } static http_handler_fn http_lookup_active(void) { http_handler_fn out = NULL; pthread_mutex_lock(&_http_handler_mu); if (_http_active_handler) { for (size_t i = 0; i < _http_handler_count; i++) { if (strcmp(_http_handlers[i].name, _http_active_handler) == 0) { out = _http_handlers[i].fn; break; } } } pthread_mutex_unlock(&_http_handler_mu); return out; } /* Auto-detect Content-Type from response body. */ static const char* http_detect_content_type(const char* body) { if (!body) return "text/plain; charset=utf-8"; const char* p = body; while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; if (strncasecmp(p, "= cap) { if (cap >= 1024 * 1024) { free(buf); return -1; } cap *= 2; buf = realloc(buf, cap); if (!buf) return -1; } ssize_t n = recv(fd, buf + len, cap - len - 1, 0); if (n <= 0) { free(buf); return -1; } len += (size_t)n; buf[len] = '\0'; if (strstr(buf, "\r\n\r\n")) break; } /* Parse request line */ char* sp1 = strchr(buf, ' '); if (!sp1) { free(buf); return -1; } *sp1 = '\0'; *out_method = el_strdup(buf); char* path_start = sp1 + 1; char* sp2 = strchr(path_start, ' '); if (!sp2) { free(*out_method); *out_method = NULL; free(buf); return -1; } *sp2 = '\0'; *out_path = el_strdup(path_start); char* hdr_end = strstr(sp2 + 1, "\r\n\r\n"); /* Find Content-Length */ long content_length = 0; char* hp = sp2 + 1; while (hp < hdr_end) { char* line_end = strstr(hp, "\r\n"); if (!line_end || line_end >= hdr_end) break; if (strncasecmp(hp, "Content-Length:", 15) == 0) { content_length = strtol(hp + 15, NULL, 10); if (content_length < 0) content_length = 0; if (content_length > 64 * 1024 * 1024) content_length = 64 * 1024 * 1024; } hp = line_end + 2; } /* Body: any bytes already read past hdr_end, plus more recv */ char* body_start = hdr_end + 4; size_t body_have = (buf + len) - body_start; char* body = malloc((size_t)content_length + 1); if (!body) { free(*out_method); free(*out_path); *out_method=NULL; *out_path=NULL; free(buf); return -1; } if ((long)body_have > content_length) body_have = (size_t)content_length; if (body_have > 0) memcpy(body, body_start, body_have); while ((long)body_have < content_length) { ssize_t n = recv(fd, body + body_have, (size_t)content_length - body_have, 0); if (n <= 0) break; body_have += (size_t)n; } body[body_have] = '\0'; *out_body = body; free(buf); return 0; } static void http_send_response(int fd, const char* body) { if (!body) body = ""; const char* ct = http_detect_content_type(body); size_t blen = strlen(body); char header[512]; int hl = snprintf(header, sizeof(header), "HTTP/1.1 200 OK\r\n" "Content-Type: %s\r\n" "Content-Length: %zu\r\n" "Connection: close\r\n" "\r\n", ct, blen); if (hl < 0) return; /* Best-effort full writes */ const char* p = header; size_t left = (size_t)hl; while (left > 0) { ssize_t w = send(fd, p, left, 0); if (w <= 0) return; p += w; left -= (size_t)w; } p = body; left = blen; while (left > 0) { ssize_t w = send(fd, p, left, 0); if (w <= 0) return; p += w; left -= (size_t)w; } } typedef struct { int fd; } HttpWorkerArg; static void* http_worker(void* arg) { HttpWorkerArg* a = (HttpWorkerArg*)arg; int fd = a->fd; free(a); char *method = NULL, *path = NULL, *body = NULL; if (http_read_request(fd, &method, &path, &body) == 0) { http_handler_fn h = http_lookup_active(); char* response = NULL; if (h) { el_val_t r = h(EL_STR(method), EL_STR(path), EL_STR(body)); const char* rs = EL_CSTR(r); response = el_strdup(rs ? rs : ""); } else { response = el_strdup("el-runtime: no http handler registered (call http_set_handler)"); } http_send_response(fd, response); free(response); } free(method); free(path); free(body); close(fd); /* release a slot */ pthread_mutex_lock(&_http_conn_mu); _http_conn_active--; pthread_cond_signal(&_http_conn_cv); pthread_mutex_unlock(&_http_conn_mu); return NULL; } void http_serve(el_val_t port, el_val_t handler) { /* If `handler` looks like a string name, register it as the active handler. */ const char* hname = EL_CSTR(handler); if (hname && looks_like_string(handler)) { http_set_handler(handler); } int p = (int)port; if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return; } int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { perror("socket"); return; } int yes = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons((uint16_t)p); if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); close(sock); return; } if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; } fprintf(stderr, "[http] listening on 0.0.0.0:%d\n", p); while (1) { struct sockaddr_in cli; socklen_t clen = sizeof(cli); int cfd = accept(sock, (struct sockaddr*)&cli, &clen); if (cfd < 0) { if (errno == EINTR) continue; perror("accept"); break; } pthread_mutex_lock(&_http_conn_mu); while (_http_conn_active >= HTTP_MAX_CONNS) { pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); } _http_conn_active++; pthread_mutex_unlock(&_http_conn_mu); HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); if (!arg) { close(cfd); continue; } arg->fd = cfd; pthread_t tid; if (pthread_create(&tid, NULL, http_worker, arg) != 0) { close(cfd); free(arg); pthread_mutex_lock(&_http_conn_mu); _http_conn_active--; pthread_cond_signal(&_http_conn_cv); pthread_mutex_unlock(&_http_conn_mu); continue; } pthread_detach(tid); } close(sock); } /* ── Filesystem ──────────────────────────────────────────────────────────── */ el_val_t fs_read(el_val_t pathv) { const char* path = EL_CSTR(pathv); if (!path) return el_wrap_str(el_strdup("")); FILE* f = fopen(path, "rb"); if (!f) return el_wrap_str(el_strdup("")); fseek(f, 0, SEEK_END); long sz = ftell(f); rewind(f); char* buf = el_strbuf((size_t)sz); size_t got = fread(buf, 1, (size_t)sz, f); buf[got] = '\0'; fclose(f); return el_wrap_str(buf); } el_val_t fs_write(el_val_t pathv, el_val_t contentv) { const char* path = EL_CSTR(pathv); const char* content = EL_CSTR(contentv); if (!path || !content) return 0; FILE* f = fopen(path, "wb"); if (!f) return 0; size_t n = strlen(content); size_t written = fwrite(content, 1, n, f); fclose(f); return written == n ? 1 : 0; } el_val_t fs_list(el_val_t pathv) { const char* path = EL_CSTR(pathv); el_val_t lst = el_list_empty(); if (!path) return lst; DIR* d = opendir(path); if (!d) return lst; struct dirent* e; while ((e = readdir(d)) != NULL) { if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; lst = el_list_append(lst, el_wrap_str(el_strdup(e->d_name))); } closedir(d); return lst; } /* ── JSON ────────────────────────────────────────────────────────────────── */ el_val_t json_get(el_val_t jsonv, el_val_t keyv) { const char* json = EL_CSTR(jsonv); const char* key = EL_CSTR(keyv); if (!json || !key) return el_wrap_str(el_strdup("")); size_t klen = strlen(key); char* pattern = el_strbuf(klen + 4); snprintf(pattern, klen + 5, "\"%s\":", key); const char* p = strstr(json, pattern); free(pattern); if (!p) return el_wrap_str(el_strdup("")); p += strlen(key) + 3; /* skip "key": */ while (*p == ' ' || *p == '\t' || *p == '\n') p++; if (*p == '"') { p++; const char* start = p; while (*p && !(*p == '"' && *(p-1) != '\\')) p++; size_t len = (size_t)(p - start); char* out = el_strbuf(len); memcpy(out, start, len); out[len] = '\0'; return el_wrap_str(out); } const char* start = p; while (*p && *p != ',' && *p != '}' && *p != ']' && *p != '\n') p++; size_t len = (size_t)(p - start); char* out = el_strbuf(len); memcpy(out, start, len); out[len] = '\0'; return el_wrap_str(out); } /* ── Float bit-cast helpers ──────────────────────────────────────────────── */ /* `el_to_float` and `el_from_float` are exposed in el_runtime.h as static * inlines so generated programs (which #include the header) can call them * for Float literals. No definitions are needed here. */ /* ── JSON parser (recursive descent) ─────────────────────────────────────── */ /* * Parsed JSON representation: * - object -> ElMap (keys & values are el_val_t) * - array -> ElList * - string -> EL_STR-wrapped char* (allocated) * - number -> int (el_val_t) if integer, otherwise el_from_float(double) * - true -> 1 * - false -> 0 * - null -> EL_NULL (0) * * Note: there is no runtime type tag — parsed numbers cannot be * distinguished from booleans by the runtime alone. The codegen tracks * types separately. This matches the rest of el_val_t's type-erased model. */ /* JsonParser struct is forward-declared near the HTTP/Engram section. */ static void jp_skip_ws(JsonParser* jp) { while (jp->p < jp->end) { char c = *jp->p; if (c == ' ' || c == '\t' || c == '\n' || c == '\r') jp->p++; else break; } } static el_val_t jp_parse_value(JsonParser* jp); /* Parse a JSON string literal (the opening " has NOT yet been consumed). */ static char* jp_parse_string_raw(JsonParser* jp) { if (jp->p >= jp->end || *jp->p != '"') { jp->err = 1; return el_strdup(""); } jp->p++; size_t cap = 32, len = 0; char* out = malloc(cap); if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } while (jp->p < jp->end && *jp->p != '"') { char c = *jp->p++; if (c == '\\' && jp->p < jp->end) { char esc = *jp->p++; switch (esc) { case '"': c = '"'; break; case '\\': c = '\\'; break; case '/': c = '/'; break; case 'b': c = '\b'; break; case 'f': c = '\f'; break; case 'n': c = '\n'; break; case 'r': c = '\r'; break; case 't': c = '\t'; break; case 'u': { /* Skip 4 hex digits; emit '?' as a placeholder */ for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++; c = '?'; break; } default: c = esc; break; } } if (len + 1 >= cap) { cap *= 2; out = realloc(out, cap); if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } } out[len++] = c; } if (jp->p < jp->end && *jp->p == '"') jp->p++; else jp->err = 1; out[len] = '\0'; return out; } static el_val_t jp_parse_number(JsonParser* jp) { const char* start = jp->p; int is_float = 0; if (jp->p < jp->end && (*jp->p == '-' || *jp->p == '+')) jp->p++; while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; if (jp->p < jp->end && *jp->p == '.') { is_float = 1; jp->p++; while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; } if (jp->p < jp->end && (*jp->p == 'e' || *jp->p == 'E')) { is_float = 1; jp->p++; if (jp->p < jp->end && (*jp->p == '+' || *jp->p == '-')) jp->p++; while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; } size_t n = (size_t)(jp->p - start); char buf[64]; if (n >= sizeof(buf)) n = sizeof(buf) - 1; memcpy(buf, start, n); buf[n] = '\0'; if (is_float) return el_from_float(strtod(buf, NULL)); return (el_val_t)strtoll(buf, NULL, 10); } static el_val_t jp_parse_array(JsonParser* jp) { if (jp->p < jp->end && *jp->p == '[') jp->p++; el_val_t lst = el_list_empty(); jp_skip_ws(jp); if (jp->p < jp->end && *jp->p == ']') { jp->p++; return lst; } while (jp->p < jp->end) { jp_skip_ws(jp); el_val_t v = jp_parse_value(jp); lst = el_list_append(lst, v); jp_skip_ws(jp); if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } if (jp->p < jp->end && *jp->p == ']') { jp->p++; break; } jp->err = 1; break; } return lst; } static el_val_t jp_parse_object(JsonParser* jp) { if (jp->p < jp->end && *jp->p == '{') jp->p++; el_val_t m = el_map_new(0); jp_skip_ws(jp); if (jp->p < jp->end && *jp->p == '}') { jp->p++; return m; } while (jp->p < jp->end) { jp_skip_ws(jp); char* key = jp_parse_string_raw(jp); jp_skip_ws(jp); if (jp->p < jp->end && *jp->p == ':') jp->p++; else { jp->err = 1; free(key); break; } jp_skip_ws(jp); el_val_t v = jp_parse_value(jp); m = el_map_set(m, EL_STR(key), v); jp_skip_ws(jp); if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } if (jp->p < jp->end && *jp->p == '}') { jp->p++; break; } jp->err = 1; break; } return m; } static el_val_t jp_parse_value(JsonParser* jp) { jp_skip_ws(jp); if (jp->p >= jp->end) { jp->err = 1; return EL_NULL; } char c = *jp->p; if (c == '"') return el_wrap_str(jp_parse_string_raw(jp)); if (c == '{') return jp_parse_object(jp); if (c == '[') return jp_parse_array(jp); if (c == '-' || isdigit((unsigned char)c)) return jp_parse_number(jp); if (c == 't' && jp->p + 4 <= jp->end && strncmp(jp->p, "true", 4) == 0) { jp->p += 4; return 1; } if (c == 'f' && jp->p + 5 <= jp->end && strncmp(jp->p, "false", 5) == 0) { jp->p += 5; return 0; } if (c == 'n' && jp->p + 4 <= jp->end && strncmp(jp->p, "null", 4) == 0) { jp->p += 4; return EL_NULL; } jp->err = 1; return EL_NULL; } el_val_t json_parse(el_val_t sv) { const char* s = EL_CSTR(sv); if (!s) return EL_NULL; JsonParser jp = { .p = s, .end = s + strlen(s), .err = 0 }; el_val_t v = jp_parse_value(&jp); if (jp.err) return EL_NULL; return v; } /* ── JSON stringify ──────────────────────────────────────────────────────── */ /* * Stringify policy: el_val_t is type-erased, so we cannot perfectly * round-trip arbitrary values. We use these heuristics: * - If value is an ElList pointer (in the heap range), serialize as array. * - If value is an ElMap pointer, serialize as object. * - If value looks like a printable string pointer, serialize as string. * - Otherwise serialize as integer. * This is best-effort. Programs that need exact control should build the * string directly. A pointer test is the cheapest way to disambiguate * from small integers without a separate type tag. */ /* JsonBuf struct is forward-declared near the HTTP section so HTTP helpers * can use it. Its definition appears there. */ static void jb_init(JsonBuf* b) { b->cap = 64; b->len = 0; b->buf = malloc(b->cap); if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } b->buf[0] = '\0'; } static void jb_reserve(JsonBuf* b, size_t add) { if (b->len + add + 1 > b->cap) { while (b->len + add + 1 > b->cap) b->cap *= 2; b->buf = realloc(b->buf, b->cap); if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } } } static void jb_putc(JsonBuf* b, char c) { jb_reserve(b, 1); b->buf[b->len++] = c; b->buf[b->len] = '\0'; } static void jb_puts(JsonBuf* b, const char* s) { size_t n = strlen(s); jb_reserve(b, n); memcpy(b->buf + b->len, s, n); b->len += n; b->buf[b->len] = '\0'; } static void jb_emit_escaped(JsonBuf* b, const char* s) { jb_putc(b, '"'); for (; *s; s++) { unsigned char c = (unsigned char)*s; switch (c) { case '"': jb_puts(b, "\\\""); break; case '\\': jb_puts(b, "\\\\"); break; case '\b': jb_puts(b, "\\b"); break; case '\f': jb_puts(b, "\\f"); break; case '\n': jb_puts(b, "\\n"); break; case '\r': jb_puts(b, "\\r"); break; case '\t': jb_puts(b, "\\t"); break; default: if (c < 0x20) { char tmp[8]; snprintf(tmp, sizeof(tmp), "\\u%04x", c); jb_puts(b, tmp); } else { jb_putc(b, (char)c); } break; } } jb_putc(b, '"'); } /* Heuristic: is this el_val_t likely a pointer to an ElList? * We can't fully verify, but pointers are large addresses, integers small. * Treat values whose magnitude exceeds 2^32 as potential pointers and * sniff by reading the header conservatively. * * Simpler heuristic: if the value reads as a printable string, treat as * string; otherwise as integer. Lists/Maps are encoded as struct pointers, * which have leading binary bytes — so they won't look like strings. */ static int looks_like_string(el_val_t v) { if (v == 0) return 0; /* Treat plausible heap addresses as candidates */ uintptr_t p = (uintptr_t)v; /* Small integers (positive and negative) are not pointers */ if ((int64_t)v >= -1000000 && (int64_t)v <= 1000000) return 0; if (p < 0x1000) return 0; /* Sniff first bytes for printable */ const unsigned char* s = (const unsigned char*)p; for (int i = 0; i < 16; i++) { unsigned char c = s[i]; if (c == '\0') return i > 0; /* terminated string */ if (c < 0x09 || (c > 0x0d && c < 0x20) || c >= 0x7f) return 0; } return 1; /* 16+ printable bytes — call it a string */ } static void jb_emit_value(JsonBuf* b, el_val_t v); static void jb_emit_int(JsonBuf* b, int64_t n) { char tmp[32]; snprintf(tmp, sizeof(tmp), "%lld", (long long)n); jb_puts(b, tmp); } static void jb_emit_value(JsonBuf* b, el_val_t v) { if (v == EL_NULL) { jb_puts(b, "null"); return; } if (looks_like_string(v)) { jb_emit_escaped(b, EL_CSTR(v)); return; } jb_emit_int(b, (int64_t)v); } el_val_t json_stringify(el_val_t v) { JsonBuf b; jb_init(&b); jb_emit_value(&b, v); return el_wrap_str(b.buf); } /* ── JSON substring accessors ────────────────────────────────────────────── */ /* * These walk the raw JSON string looking for "key": at the top level (depth 1) * of an object. They handle escaped quotes, nested objects/arrays, and * whitespace around the colon. */ /* Find "key": at object-depth == 1 inside the JSON object string `s`. * Returns pointer to the first byte of the value, or NULL. */ static const char* json_find_key(const char* s, const char* key) { if (!s || !key) return NULL; size_t klen = strlen(key); int depth = 0; int in_str = 0; int escape = 0; const char* p = s; while (*p) { char c = *p; if (in_str) { if (escape) { escape = 0; } else if (c == '\\') { escape = 1; } else if (c == '"') { /* End of string. If we're at depth 1, check if this was a key. */ p++; if (depth == 1) { /* The string just ended at p-1. Check if it matches key * and is followed by a colon. We need to backtrack to find * the start of this string and compare. */ } in_str = 0; continue; } p++; continue; } if (c == '"') { /* Start of a string literal */ const char* str_start = p + 1; const char* q = str_start; int e = 0; while (*q) { if (e) { e = 0; q++; continue; } if (*q == '\\') { e = 1; q++; continue; } if (*q == '"') break; q++; } size_t slen = (size_t)(q - str_start); const char* after = (*q == '"') ? q + 1 : q; /* If at depth 1 and matches key and followed by ':' -> got it */ if (depth == 1 && slen == klen && strncmp(str_start, key, klen) == 0) { const char* r = after; while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; if (*r == ':') { r++; while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; return r; } } p = after; continue; } if (c == '{' || c == '[') depth++; else if (c == '}' || c == ']') depth--; p++; } return NULL; } /* Skip a JSON value starting at p; return pointer past the value end. */ static const char* json_skip_value(const char* p) { if (!p || !*p) return p; while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; if (*p == '"') { p++; int e = 0; while (*p) { if (e) { e = 0; p++; continue; } if (*p == '\\') { e = 1; p++; continue; } if (*p == '"') { p++; break; } p++; } return p; } if (*p == '{' || *p == '[') { char open = *p; char close = (open == '{') ? '}' : ']'; int depth = 0; int in_str = 0; int e = 0; while (*p) { char c = *p; if (in_str) { if (e) { e = 0; } else if (c == '\\') { e = 1; } else if (c == '"') in_str = 0; p++; continue; } if (c == '"') { in_str = 1; p++; continue; } if (c == open) depth++; else if (c == close) { depth--; p++; if (depth == 0) return p; continue; } p++; } return p; } /* scalar: number, true/false/null */ while (*p && *p != ',' && *p != '}' && *p != ']' && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; return p; } el_val_t json_get_string(el_val_t json_str, el_val_t key) { const char* json = EL_CSTR(json_str); const char* k = EL_CSTR(key); const char* p = json_find_key(json, k); if (!p || *p != '"') return el_wrap_str(el_strdup("")); p++; JsonParser jp = { .p = p - 1, .end = json + (json ? strlen(json) : 0), .err = 0 }; char* parsed = jp_parse_string_raw(&jp); if (jp.err) { free(parsed); return el_wrap_str(el_strdup("")); } return el_wrap_str(parsed); } el_val_t json_get_int(el_val_t json_str, el_val_t key) { const char* json = EL_CSTR(json_str); const char* k = EL_CSTR(key); const char* p = json_find_key(json, k); if (!p) return 0; if (*p == '"' || *p == '{' || *p == '[') return 0; return (el_val_t)strtoll(p, NULL, 10); } el_val_t json_get_float(el_val_t json_str, el_val_t key) { const char* json = EL_CSTR(json_str); const char* k = EL_CSTR(key); const char* p = json_find_key(json, k); if (!p) return 0; if (*p == '"' || *p == '{' || *p == '[') return 0; return el_from_float(strtod(p, NULL)); } el_val_t json_get_bool(el_val_t json_str, el_val_t key) { const char* json = EL_CSTR(json_str); const char* k = EL_CSTR(key); const char* p = json_find_key(json, k); if (!p) return 0; if (strncmp(p, "true", 4) == 0) return 1; return 0; } el_val_t json_get_raw(el_val_t json_str, el_val_t key) { const char* json = EL_CSTR(json_str); const char* k = EL_CSTR(key); const char* p = json_find_key(json, k); if (!p) return el_wrap_str(el_strdup("")); const char* end = json_skip_value(p); size_t n = (size_t)(end - p); char* out = el_strbuf(n); memcpy(out, p, n); out[n] = '\0'; return el_wrap_str(out); } el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { const char* json = EL_CSTR(json_str); const char* k = EL_CSTR(key); if (!k) k = ""; if (!json || !*json) { /* Build a fresh object */ JsonBuf b; jb_init(&b); jb_putc(&b, '{'); jb_emit_escaped(&b, k); jb_putc(&b, ':'); jb_emit_value(&b, value); jb_putc(&b, '}'); return el_wrap_str(b.buf); } const char* existing = json_find_key(json, k); JsonBuf b; jb_init(&b); if (existing) { const char* end = json_skip_value(existing); /* Copy [json .. existing) */ size_t prefix = (size_t)(existing - json); jb_reserve(&b, prefix); memcpy(b.buf + b.len, json, prefix); b.len += prefix; b.buf[b.len] = '\0'; jb_emit_value(&b, value); jb_puts(&b, end); return el_wrap_str(b.buf); } /* Insert before closing '}'. Find last '}' */ size_t jl = strlen(json); if (jl == 0) { free(b.buf); return el_wrap_str(el_strdup("{}")); } /* Find last '}' from the end */ ssize_t close_idx = -1; for (ssize_t i = (ssize_t)jl - 1; i >= 0; i--) { if (json[i] == '}') { close_idx = i; break; } } if (close_idx < 0) { free(b.buf); return el_wrap_str(el_strdup(json)); } /* Determine if object is empty: scan between last '{' and '}' for non-ws */ int empty = 1; for (ssize_t i = close_idx - 1; i >= 0; i--) { char c = json[i]; if (c == '{') break; if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { empty = 0; break; } } /* Copy json[0..close_idx) */ jb_reserve(&b, (size_t)close_idx); memcpy(b.buf + b.len, json, (size_t)close_idx); b.len += (size_t)close_idx; b.buf[b.len] = '\0'; if (!empty) jb_putc(&b, ','); jb_emit_escaped(&b, k); jb_putc(&b, ':'); jb_emit_value(&b, value); /* Append from close_idx onward */ jb_puts(&b, json + close_idx); return el_wrap_str(b.buf); } el_val_t json_array_len(el_val_t json_str) { const char* s = EL_CSTR(json_str); if (!s) return 0; while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; if (*s != '[') return 0; s++; while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; if (*s == ']') return 0; int64_t count = 0; while (*s) { const char* end = json_skip_value(s); if (end == s) break; count++; s = end; while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; if (*s == ',') { s++; continue; } if (*s == ']' || *s == '\0') break; } return (el_val_t)count; } /* ── Time ────────────────────────────────────────────────────────────────── */ el_val_t time_now(void) { struct timeval tv; gettimeofday(&tv, NULL); int64_t ms = (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; return (el_val_t)ms; } el_val_t time_now_utc(void) { return time_now(); } el_val_t time_format(el_val_t ts, el_val_t fmt) { int64_t ms = (int64_t)ts; time_t s = (time_t)(ms / 1000); int msec = (int)(ms % 1000); if (msec < 0) { msec += 1000; s -= 1; } struct tm tm; gmtime_r(&s, &tm); const char* fmt_str = EL_CSTR(fmt); if (!fmt_str || strcmp(fmt_str, "ISO") == 0) { char buf[64]; snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, msec); return el_wrap_str(el_strdup(buf)); } char buf[256]; if (strftime(buf, sizeof(buf), fmt_str, &tm) == 0) buf[0] = '\0'; return el_wrap_str(el_strdup(buf)); } el_val_t time_to_parts(el_val_t ts) { int64_t ms = (int64_t)ts; time_t s = (time_t)(ms / 1000); int msec = (int)(ms % 1000); if (msec < 0) { msec += 1000; s -= 1; } struct tm tm; gmtime_r(&s, &tm); el_val_t m = el_map_new(0); m = el_map_set(m, EL_STR(el_strdup("year")), (el_val_t)(tm.tm_year + 1900)); m = el_map_set(m, EL_STR(el_strdup("month")), (el_val_t)(tm.tm_mon + 1)); m = el_map_set(m, EL_STR(el_strdup("day")), (el_val_t)tm.tm_mday); m = el_map_set(m, EL_STR(el_strdup("hour")), (el_val_t)tm.tm_hour); m = el_map_set(m, EL_STR(el_strdup("minute")), (el_val_t)tm.tm_min); m = el_map_set(m, EL_STR(el_strdup("second")), (el_val_t)tm.tm_sec); m = el_map_set(m, EL_STR(el_strdup("ms")), (el_val_t)msec); return m; } el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz) { (void)tz; int64_t s = (int64_t)secs; int64_t n = (int64_t)ns; int64_t ms = s * 1000LL + n / 1000000LL; return (el_val_t)ms; } el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit) { const char* u = EL_CSTR(unit); int64_t cur = (int64_t)ts; int64_t d = (int64_t)n; int64_t add_ms = d; if (u) { if (strcmp(u, "ms") == 0) add_ms = d; else if (strcmp(u, "sec") == 0) add_ms = d * 1000LL; else if (strcmp(u, "min") == 0) add_ms = d * 60000LL; else if (strcmp(u, "hour") == 0) add_ms = d * 3600000LL; else if (strcmp(u, "day") == 0) add_ms = d * 86400000LL; } return (el_val_t)(cur + add_ms); } el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit) { int64_t d = (int64_t)ts2 - (int64_t)ts1; const char* u = EL_CSTR(unit); if (!u || strcmp(u, "ms") == 0) return (el_val_t)d; if (strcmp(u, "sec") == 0) return (el_val_t)(d / 1000LL); if (strcmp(u, "min") == 0) return (el_val_t)(d / 60000LL); if (strcmp(u, "hour") == 0) return (el_val_t)(d / 3600000LL); if (strcmp(u, "day") == 0) return (el_val_t)(d / 86400000LL); return (el_val_t)d; } /* ── UUID v4 ─────────────────────────────────────────────────────────────── */ static int _el_uuid_seeded = 0; static void _el_uuid_seed(void) { if (!_el_uuid_seeded) { srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_el_uuid_seeded); _el_uuid_seeded = 1; } } el_val_t uuid_new(void) { _el_uuid_seed(); unsigned char b[16]; for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); /* Version 4 */ b[6] = (b[6] & 0x0f) | 0x40; /* RFC 4122 variant */ b[8] = (b[8] & 0x3f) | 0x80; char buf[37]; snprintf(buf, sizeof(buf), "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]); return el_wrap_str(el_strdup(buf)); } el_val_t uuid_v4(void) { return uuid_new(); } /* ── Environment ─────────────────────────────────────────────────────────── */ el_val_t env(el_val_t key) { const char* k = EL_CSTR(key); if (!k) return el_wrap_str(el_strdup("")); const char* v = getenv(k); return el_wrap_str(el_strdup(v ? v : "")); } /* ── In-process state K/V ────────────────────────────────────────────────── */ typedef struct { char* key; char* value; } StateEntry; static StateEntry* _state_entries = NULL; static size_t _state_count = 0; static size_t _state_cap = 0; static StateEntry* state_find(const char* key) { for (size_t i = 0; i < _state_count; i++) { if (strcmp(_state_entries[i].key, key) == 0) return &_state_entries[i]; } return NULL; } el_val_t state_set(el_val_t key, el_val_t value) { const char* k = EL_CSTR(key); const char* v = EL_CSTR(value); if (!k) return 0; if (!v) v = ""; StateEntry* e = state_find(k); if (e) { free(e->value); e->value = el_strdup(v); return 1; } if (_state_count >= _state_cap) { size_t nc = _state_cap == 0 ? 16 : _state_cap * 2; _state_entries = realloc(_state_entries, nc * sizeof(StateEntry)); if (!_state_entries) { fputs("el_runtime: out of memory\n", stderr); exit(1); } _state_cap = nc; } _state_entries[_state_count].key = el_strdup(k); _state_entries[_state_count].value = el_strdup(v); _state_count++; return 1; } el_val_t state_get(el_val_t key) { const char* k = EL_CSTR(key); if (!k) return el_wrap_str(el_strdup("")); StateEntry* e = state_find(k); return el_wrap_str(el_strdup(e ? e->value : "")); } el_val_t state_del(el_val_t key) { const char* k = EL_CSTR(key); if (!k) return 0; for (size_t i = 0; i < _state_count; i++) { if (strcmp(_state_entries[i].key, k) == 0) { free(_state_entries[i].key); free(_state_entries[i].value); for (size_t j = i + 1; j < _state_count; j++) { _state_entries[j - 1] = _state_entries[j]; } _state_count--; return 1; } } return 1; } el_val_t state_keys(void) { el_val_t lst = el_list_empty(); for (size_t i = 0; i < _state_count; i++) { lst = el_list_append(lst, el_wrap_str(el_strdup(_state_entries[i].key))); } return lst; } /* ── Float formatting ────────────────────────────────────────────────────── */ el_val_t float_to_str(el_val_t f) { char buf[64]; snprintf(buf, sizeof(buf), "%g", el_to_float(f)); return el_wrap_str(el_strdup(buf)); } el_val_t int_to_float(el_val_t n) { return el_from_float((double)(int64_t)n); } el_val_t float_to_int(el_val_t f) { return (el_val_t)(int64_t)el_to_float(f); } el_val_t format_float(el_val_t f, el_val_t decimals) { int d = (int)(int64_t)decimals; if (d < 0) d = 0; if (d > 30) d = 30; char buf[128]; snprintf(buf, sizeof(buf), "%.*f", d, el_to_float(f)); return el_wrap_str(el_strdup(buf)); } el_val_t decimal_round(el_val_t f, el_val_t decimals) { int d = (int)(int64_t)decimals; if (d < 0) d = 0; if (d > 15) d = 15; double mul = pow(10.0, (double)d); double v = el_to_float(f); double r = (v >= 0.0 ? floor(v * mul + 0.5) : -floor(-v * mul + 0.5)) / mul; return el_from_float(r); } el_val_t str_to_float(el_val_t s) { const char* str = EL_CSTR(s); if (!str) return el_from_float(0.0); return el_from_float(strtod(str, NULL)); } /* ── Math (Float-aware) ──────────────────────────────────────────────────── */ el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); } el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); } el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); } el_val_t math_pi(void) { return el_from_float(3.141592653589793238462643383279502884); } /* ── String additions ────────────────────────────────────────────────────── */ el_val_t str_index_of(el_val_t s, el_val_t sub) { const char* str = EL_CSTR(s); const char* sb = EL_CSTR(sub); if (!str || !sb) return -1; const char* hit = strstr(str, sb); if (!hit) return -1; return (el_val_t)(int64_t)(hit - str); } el_val_t str_split(el_val_t s, el_val_t sep) { const char* str = EL_CSTR(s); const char* sp = EL_CSTR(sep); el_val_t lst = el_list_empty(); if (!str) return lst; if (!sp || !*sp) { lst = el_list_append(lst, el_wrap_str(el_strdup(str))); return lst; } size_t lp = strlen(sp); const char* p = str; const char* hit; while ((hit = strstr(p, sp)) != NULL) { size_t n = (size_t)(hit - p); char* out = el_strbuf(n); memcpy(out, p, n); out[n] = '\0'; lst = el_list_append(lst, el_wrap_str(out)); p = hit + lp; } lst = el_list_append(lst, el_wrap_str(el_strdup(p))); return lst; } el_val_t str_char_at(el_val_t s, el_val_t i) { const char* str = EL_CSTR(s); int64_t idx = (int64_t)i; if (!str) return el_wrap_str(el_strdup("")); int64_t n = (int64_t)strlen(str); if (idx < 0 || idx >= n) return el_wrap_str(el_strdup("")); char buf[2]; buf[0] = str[idx]; buf[1] = '\0'; return el_wrap_str(el_strdup(buf)); } el_val_t str_char_code(el_val_t s, el_val_t i) { const char* str = EL_CSTR(s); int64_t idx = (int64_t)i; if (!str) return 0; int64_t n = (int64_t)strlen(str); if (idx < 0 || idx >= n) return 0; return (el_val_t)(unsigned char)str[idx]; } static el_val_t str_pad(const char* s, int64_t width, const char* pad, int left) { if (!s) s = ""; if (!pad || !*pad) pad = " "; int64_t lp = (int64_t)strlen(pad); int64_t ls = (int64_t)strlen(s); if (ls >= width) return el_wrap_str(el_strdup(s)); int64_t need = width - ls; char* out = el_strbuf((size_t)width); if (left) { for (int64_t i = 0; i < need; i++) out[i] = pad[i % lp]; memcpy(out + need, s, (size_t)ls); } else { memcpy(out, s, (size_t)ls); for (int64_t i = 0; i < need; i++) out[ls + i] = pad[i % lp]; } out[width] = '\0'; return el_wrap_str(out); } el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad) { return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 1); } el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad) { return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 0); } el_val_t str_format(el_val_t template, el_val_t data) { const char* tpl = EL_CSTR(template); if (!tpl) return el_wrap_str(el_strdup("")); JsonBuf b; jb_init(&b); const char* p = tpl; while (*p) { if (*p == '{') { const char* q = p + 1; while (*q && *q != '}') q++; if (*q == '}') { size_t klen = (size_t)(q - p - 1); char keybuf[256]; if (klen < sizeof(keybuf)) { memcpy(keybuf, p + 1, klen); keybuf[klen] = '\0'; el_val_t v = el_map_get(data, EL_STR(keybuf)); if (v != 0 && looks_like_string(v)) { jb_puts(&b, EL_CSTR(v)); p = q + 1; continue; } else if (v != 0) { jb_emit_int(&b, (int64_t)v); p = q + 1; continue; } } /* Unknown key — leave {key} verbatim */ jb_reserve(&b, klen + 2); memcpy(b.buf + b.len, p, klen + 2); b.len += klen + 2; b.buf[b.len] = '\0'; p = q + 1; continue; } } jb_putc(&b, *p); p++; } return el_wrap_str(b.buf); } el_val_t str_lower(el_val_t s) { return str_to_lower(s); } el_val_t str_upper(el_val_t s) { return str_to_upper(s); } /* ── List additions ──────────────────────────────────────────────────────── */ el_val_t list_push(el_val_t list, el_val_t elem) { return el_list_append(list, elem); } el_val_t list_push_front(el_val_t listv, el_val_t elem) { ElList* lst = (ElList*)(uintptr_t)listv; if (!lst) { el_val_t nl = el_list_empty(); return el_list_append(nl, elem); } /* Append to grow capacity, then shift right */ listv = el_list_append(listv, elem); lst = (ElList*)(uintptr_t)listv; for (int64_t i = lst->length - 1; i > 0; i--) { lst->elems[i] = lst->elems[i - 1]; } lst->elems[0] = elem; return EL_STR(lst); } el_val_t list_join(el_val_t listv, el_val_t sep) { ElList* lst = (ElList*)(uintptr_t)listv; const char* sp = EL_CSTR(sep); if (!sp) sp = ""; if (!lst || lst->length == 0) return el_wrap_str(el_strdup("")); JsonBuf b; jb_init(&b); for (int64_t i = 0; i < lst->length; i++) { if (i > 0) jb_puts(&b, sp); el_val_t v = lst->elems[i]; if (v == 0) continue; if (looks_like_string(v)) { jb_puts(&b, EL_CSTR(v)); } else { char tmp[32]; snprintf(tmp, sizeof(tmp), "%lld", (long long)v); jb_puts(&b, tmp); } } return el_wrap_str(b.buf); } el_val_t list_range(el_val_t start, el_val_t end) { int64_t a = (int64_t)start; int64_t b = (int64_t)end; el_val_t lst = el_list_empty(); for (int64_t i = a; i < b; i++) lst = el_list_append(lst, (el_val_t)i); return lst; } /* ── Bool helpers ────────────────────────────────────────────────────────── */ el_val_t bool_to_str(el_val_t b) { return el_wrap_str(el_strdup(b ? "true" : "false")); } /* ── Process ─────────────────────────────────────────────────────────────── */ void exit_program(el_val_t code) { exit((int)code); } /* ── args() — command-line argument access ────────────────────────────────── * Compiled El programs call args() to get a list of CLI arguments. * Call el_runtime_init_args(argc, argv) at the start of C main() to populate. * The args list excludes argv[0] (the program name). */ static el_val_t _el_args_list = 0; void el_runtime_init_args(int argc, char** argv) { _el_args_list = el_list_empty(); for (int i = 1; i < argc; i++) { _el_args_list = el_list_append(_el_args_list, EL_STR(argv[i])); } } el_val_t args(void) { if (!_el_args_list) _el_args_list = el_list_empty(); return _el_args_list; } /* ── CGI identity ──────────────────────────────────────────────────────────── * Called once at program start by the generated main() of a cgi {} program. * Stores CGI identity so dharma_* builtins can reference it. */ static const char* _el_cgi_name = NULL; static const char* _el_cgi_dharma_id = NULL; static const char* _el_cgi_principal = NULL; static const char* _el_cgi_network = NULL; static const char* _el_cgi_engram = NULL; void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, el_val_t network, el_val_t engram) { _el_cgi_name = EL_CSTR(name); _el_cgi_dharma_id = EL_CSTR(dharma_id); _el_cgi_principal = EL_CSTR(principal); _el_cgi_network = EL_CSTR(network) ? EL_CSTR(network) : "dharma-mainnet"; _el_cgi_engram = EL_CSTR(engram) ? EL_CSTR(engram) : "http://localhost:8742"; printf("[cgi] identity: name=%s dharma_id=%s principal=%s network=%s engram=%s\n", _el_cgi_name ? _el_cgi_name : "(unset)", _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unset)", _el_cgi_principal ? _el_cgi_principal : "(unset)", _el_cgi_network, _el_cgi_engram); } /* ── Batch 3: Engram in-process graph store ──────────────────────────────── */ /* * Single global EngramStore allocated lazily on first call. All node and * edge content strings are owned (strdup'd) by the store. Linear arrays * with doubling capacity for both nodes and edges. * * Activation algorithm (engram_activate): * 1. Find seed nodes whose content/label/tags contain query (case-insens). * 2. BFS up to `depth` hops along outgoing+incoming edges from each seed. * 3. activation = seed.salience * product(edge_weights) * 0.7^hops * 4. If reached by multiple paths, take max activation. * 5. epistemic_confidence = activation * node.confidence * 6. Filter: epistemic_confidence >= 0.2 * 7. Sort descending by activation_strength. */ typedef struct EngramNode { char* id; char* content; char* node_type; char* label; char* tier; char* tags; char* metadata; double salience; double importance; double confidence; int64_t activation_count; int64_t last_activated; int64_t created_at; int64_t updated_at; } EngramNode; typedef struct EngramEdge { char* id; char* from_id; char* to_id; char* relation; char* metadata; double weight; double confidence; int64_t created_at; int64_t updated_at; int64_t last_fired; } EngramEdge; typedef struct EngramStore { EngramNode* nodes; int64_t node_count; int64_t node_capacity; EngramEdge* edges; int64_t edge_count; int64_t edge_capacity; } EngramStore; static EngramStore* engram_global = NULL; static EngramStore* engram_get(void) { if (engram_global) return engram_global; engram_global = calloc(1, sizeof(EngramStore)); if (!engram_global) { fputs("el_runtime: out of memory\n", stderr); exit(1); } engram_global->node_capacity = 16; engram_global->nodes = calloc((size_t)engram_global->node_capacity, sizeof(EngramNode)); engram_global->edge_capacity = 16; engram_global->edges = calloc((size_t)engram_global->edge_capacity, sizeof(EngramEdge)); return engram_global; } static int64_t engram_now_ms(void) { struct timeval tv; gettimeofday(&tv, NULL); return (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; } static EngramNode* engram_find_node(const char* id) { if (!id) return NULL; EngramStore* g = engram_get(); for (int64_t i = 0; i < g->node_count; i++) { if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return &g->nodes[i]; } return NULL; } static int64_t engram_find_node_index(const char* id) { if (!id) return -1; EngramStore* g = engram_get(); for (int64_t i = 0; i < g->node_count; i++) { if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return i; } return -1; } static void engram_grow_nodes(void) { EngramStore* g = engram_get(); if (g->node_count < g->node_capacity) return; int64_t nc = g->node_capacity * 2; g->nodes = realloc(g->nodes, (size_t)nc * sizeof(EngramNode)); if (!g->nodes) { fputs("el_runtime: out of memory\n", stderr); exit(1); } memset(g->nodes + g->node_capacity, 0, (size_t)(nc - g->node_capacity) * sizeof(EngramNode)); g->node_capacity = nc; } static void engram_grow_edges(void) { EngramStore* g = engram_get(); if (g->edge_count < g->edge_capacity) return; int64_t nc = g->edge_capacity * 2; g->edges = realloc(g->edges, (size_t)nc * sizeof(EngramEdge)); if (!g->edges) { fputs("el_runtime: out of memory\n", stderr); exit(1); } memset(g->edges + g->edge_capacity, 0, (size_t)(nc - g->edge_capacity) * sizeof(EngramEdge)); g->edge_capacity = nc; } /* Build a fresh UUID string. Reuses uuid_new but takes the underlying char*. */ static char* engram_new_id(void) { el_val_t v = uuid_new(); const char* s = EL_CSTR(v); return el_strdup(s ? s : ""); } /* Convert a node into an ElMap of its fields. */ static el_val_t engram_node_to_map(const EngramNode* n) { el_val_t m = el_map_new(0); m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(n->id ? n->id : ""))); m = el_map_set(m, EL_STR(el_strdup("content")), EL_STR(el_strdup(n->content ? n->content : ""))); m = el_map_set(m, EL_STR(el_strdup("node_type")), EL_STR(el_strdup(n->node_type ? n->node_type : ""))); m = el_map_set(m, EL_STR(el_strdup("label")), EL_STR(el_strdup(n->label ? n->label : ""))); m = el_map_set(m, EL_STR(el_strdup("tier")), EL_STR(el_strdup(n->tier ? n->tier : "Working"))); m = el_map_set(m, EL_STR(el_strdup("tags")), EL_STR(el_strdup(n->tags ? n->tags : ""))); m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(n->metadata ? n->metadata : "{}"))); m = el_map_set(m, EL_STR(el_strdup("salience")), el_from_float(n->salience)); m = el_map_set(m, EL_STR(el_strdup("importance")), el_from_float(n->importance)); m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(n->confidence)); m = el_map_set(m, EL_STR(el_strdup("activation_count")), (el_val_t)n->activation_count); m = el_map_set(m, EL_STR(el_strdup("last_activated")), (el_val_t)n->last_activated); m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)n->created_at); m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)n->updated_at); return m; } /* (Node JSON serialization is provided by `engram_emit_node_json` further * down in the persistence section — reused by the *_json builtins below.) */ static void engram_emit_node_json(JsonBuf* b, const EngramNode* n); static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e); /* Salience may arrive either as a float bit-pattern or as a small integer * (e.g. 1, meaning 1.0). Heuristic: if interpreted as double it's in * [0.0, 100.0] use it; otherwise treat as int and convert. */ static double engram_decode_score(el_val_t v) { double f = el_to_float(v); if (!isnan(f) && !isinf(f) && f >= 0.0 && f <= 100.0) return f; int64_t n = (int64_t)v; return (double)n; } static char* engram_first_n_chars(const char* s, size_t n) { if (!s) return el_strdup(""); size_t l = strlen(s); if (l > n) l = n; char* out = el_strbuf(l); memcpy(out, s, l); out[l] = '\0'; return out; } el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { EngramStore* g = engram_get(); engram_grow_nodes(); EngramNode* n = &g->nodes[g->node_count]; memset(n, 0, sizeof(*n)); n->id = engram_new_id(); const char* c = EL_CSTR(content); const char* nt = EL_CSTR(node_type); n->content = el_strdup(c ? c : ""); n->node_type = el_strdup(nt && *nt ? nt : "Memory"); n->label = engram_first_n_chars(c, 60); n->tier = el_strdup("Working"); n->tags = el_strdup(""); n->metadata = el_strdup("{}"); n->salience = engram_decode_score(salience); if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0; n->activation_count = 0; int64_t now = engram_now_ms(); n->last_activated = now; n->created_at = now; n->updated_at = now; g->node_count++; return el_wrap_str(el_strdup(n->id)); } el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, el_val_t salience, el_val_t importance, el_val_t confidence, el_val_t tier, el_val_t tags) { EngramStore* g = engram_get(); engram_grow_nodes(); EngramNode* n = &g->nodes[g->node_count]; memset(n, 0, sizeof(*n)); n->id = engram_new_id(); const char* c = EL_CSTR(content); const char* nt = EL_CSTR(node_type); const char* lb = EL_CSTR(label); const char* ti = EL_CSTR(tier); const char* tg = EL_CSTR(tags); n->content = el_strdup(c ? c : ""); n->node_type = el_strdup(nt && *nt ? nt : "Memory"); n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); n->tier = el_strdup(ti && *ti ? ti : "Working"); n->tags = el_strdup(tg ? tg : ""); n->metadata = el_strdup("{}"); n->salience = engram_decode_score(salience); n->importance = engram_decode_score(importance); n->confidence = engram_decode_score(confidence); if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; int64_t now = engram_now_ms(); n->last_activated = now; n->created_at = now; n->updated_at = now; g->node_count++; return el_wrap_str(el_strdup(n->id)); } el_val_t engram_get_node(el_val_t id) { const char* sid = EL_CSTR(id); EngramNode* n = engram_find_node(sid); if (!n) return el_map_new(0); return engram_node_to_map(n); } void engram_strengthen(el_val_t node_id) { const char* sid = EL_CSTR(node_id); EngramNode* n = engram_find_node(sid); if (!n) return; n->salience += 0.05; if (n->salience > 1.0) n->salience = 1.0; n->activation_count++; n->last_activated = engram_now_ms(); n->updated_at = n->last_activated; } void engram_forget(el_val_t node_id) { const char* sid = EL_CSTR(node_id); if (!sid) return; EngramStore* g = engram_get(); int64_t idx = engram_find_node_index(sid); if (idx < 0) return; /* Free node strings */ EngramNode* n = &g->nodes[idx]; free(n->id); free(n->content); free(n->node_type); free(n->label); free(n->tier); free(n->tags); free(n->metadata); /* Shift remaining nodes down */ for (int64_t i = idx + 1; i < g->node_count; i++) { g->nodes[i - 1] = g->nodes[i]; } g->node_count--; memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); /* Remove all incident edges */ int64_t w = 0; for (int64_t r = 0; r < g->edge_count; r++) { EngramEdge* e = &g->edges[r]; int incident = (e->from_id && strcmp(e->from_id, sid) == 0) || (e->to_id && strcmp(e->to_id, sid) == 0); if (incident) { free(e->id); free(e->from_id); free(e->to_id); free(e->relation); free(e->metadata); } else { if (w != r) g->edges[w] = g->edges[r]; w++; } } g->edge_count = w; } el_val_t engram_node_count(void) { return (el_val_t)engram_get()->node_count; } static int istr_contains(const char* hay, const char* needle) { if (!hay || !needle || !*needle) return 0; size_t nl = strlen(needle); for (const char* p = hay; *p; p++) { if (strncasecmp(p, needle, nl) == 0) return 1; } return 0; } el_val_t engram_search(el_val_t query, el_val_t limit) { EngramStore* g = engram_get(); const char* q = EL_CSTR(query); int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; el_val_t lst = el_list_empty(); if (!q || !*q) return lst; int64_t found = 0; for (int64_t i = 0; i < g->node_count && found < lim; i++) { EngramNode* n = &g->nodes[i]; if (istr_contains(n->content, q) || istr_contains(n->label, q) || istr_contains(n->tags, q)) { lst = el_list_append(lst, engram_node_to_map(n)); found++; } } return lst; } /* Sort node indices by salience desc (small N, insertion sort is fine). */ static void engram_sort_indices_by_salience(int64_t* arr, int64_t n, const EngramNode* nodes) { for (int64_t i = 1; i < n; i++) { int64_t key = arr[i]; double ks = nodes[key].salience; int64_t j = i - 1; while (j >= 0 && nodes[arr[j]].salience < ks) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } } el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset) { EngramStore* g = engram_get(); int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; int64_t off = (int64_t)offset; if (off < 0) off = 0; el_val_t lst = el_list_empty(); if (g->node_count == 0) return lst; int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); if (!idx) return lst; for (int64_t i = 0; i < g->node_count; i++) idx[i] = i; engram_sort_indices_by_salience(idx, g->node_count, g->nodes); int64_t end = off + lim; if (end > g->node_count) end = g->node_count; for (int64_t i = off; i < end; i++) { lst = el_list_append(lst, engram_node_to_map(&g->nodes[idx[i]])); } free(idx); return lst; } void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { EngramStore* g = engram_get(); const char* f = EL_CSTR(from_id); const char* t = EL_CSTR(to_id); const char* r = EL_CSTR(relation); if (!f || !t) return; 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(f); e->to_id = el_strdup(t); e->relation = el_strdup(r && *r ? r : "associate"); e->metadata = el_strdup("{}"); e->weight = engram_decode_score(weight); if (e->weight <= 0.0 || e->weight > 1.0) e->weight = 0.5; e->confidence = 1.0; int64_t now = engram_now_ms(); e->created_at = now; e->updated_at = now; e->last_fired = 0; g->edge_count++; } el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id) { EngramStore* g = engram_get(); const char* f = EL_CSTR(from_id); const char* t = EL_CSTR(to_id); if (!f || !t) return 0; 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, f) == 0 && strcmp(e->to_id, t) == 0) return 1; } return 0; } /* Reserved helper: edge -> ElMap. Kept around for future builtins. */ static el_val_t engram_edge_to_map(const EngramEdge* e) __attribute__((unused)); static el_val_t engram_edge_to_map(const EngramEdge* e) { el_val_t m = el_map_new(0); m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(e->id ? e->id : ""))); m = el_map_set(m, EL_STR(el_strdup("from_id")), EL_STR(el_strdup(e->from_id ? e->from_id : ""))); m = el_map_set(m, EL_STR(el_strdup("to_id")), EL_STR(el_strdup(e->to_id ? e->to_id : ""))); m = el_map_set(m, EL_STR(el_strdup("relation")), EL_STR(el_strdup(e->relation ? e->relation : ""))); m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(e->metadata ? e->metadata : "{}"))); m = el_map_set(m, EL_STR(el_strdup("weight")), el_from_float(e->weight)); m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(e->confidence)); m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)e->created_at); m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)e->updated_at); m = el_map_set(m, EL_STR(el_strdup("last_fired")), (el_val_t)e->last_fired); return m; } el_val_t engram_neighbors(el_val_t node_id) { EngramStore* g = engram_get(); const char* sid = EL_CSTR(node_id); el_val_t lst = el_list_empty(); if (!sid) return lst; for (int64_t i = 0; i < g->edge_count; i++) { EngramEdge* e = &g->edges[i]; const char* other = NULL; if (e->from_id && strcmp(e->from_id, sid) == 0) other = e->to_id; else if (e->to_id && strcmp(e->to_id, sid) == 0) other = e->from_id; if (!other) continue; EngramNode* n = engram_find_node(other); if (n) lst = el_list_append(lst, engram_node_to_map(n)); } return lst; } el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { EngramStore* g = engram_get(); const char* sid = EL_CSTR(node_id); int64_t md = (int64_t)max_depth; if (md <= 0) md = 1; const char* dir = EL_CSTR(direction); /* "out" | "in" | "both" (default) */ el_val_t lst = el_list_empty(); if (!sid || g->node_count == 0) return lst; int64_t start = engram_find_node_index(sid); if (start < 0) return lst; /* BFS with depth tracking */ int64_t* visited = calloc((size_t)g->node_count, sizeof(int64_t)); int64_t* queue = calloc((size_t)g->node_count, sizeof(int64_t)); int64_t* depths = calloc((size_t)g->node_count, sizeof(int64_t)); if (!visited || !queue || !depths) { free(visited); free(queue); free(depths); return lst; } int64_t qh = 0, qt = 0; queue[qt++] = start; visited[start] = 1; depths[start] = 0; while (qh < qt) { int64_t cur = queue[qh++]; const char* cur_id = g->nodes[cur].id; int64_t cur_depth = depths[cur]; if (cur_depth >= md) continue; for (int64_t i = 0; i < g->edge_count; i++) { EngramEdge* e = &g->edges[i]; const char* other = NULL; int outgoing = e->from_id && strcmp(e->from_id, cur_id) == 0; int incoming = e->to_id && strcmp(e->to_id, cur_id) == 0; if (dir && strcmp(dir, "out") == 0 && !outgoing) continue; if (dir && strcmp(dir, "in") == 0 && !incoming) continue; if (outgoing) other = e->to_id; else if (incoming) other = e->from_id; else continue; int64_t oi = engram_find_node_index(other); if (oi < 0 || visited[oi]) continue; visited[oi] = 1; depths[oi] = cur_depth + 1; queue[qt++] = oi; } } /* Emit all visited except the seed */ for (int64_t i = 0; i < g->node_count; i++) { if (visited[i] && i != start) { lst = el_list_append(lst, engram_node_to_map(&g->nodes[i])); } } free(visited); free(queue); free(depths); return lst; } el_val_t engram_edge_count(void) { return (el_val_t)engram_get()->edge_count; } /* Spreading activation. Returns ElList of {node, activation_strength, hops}. */ el_val_t engram_activate(el_val_t query, el_val_t depth) { EngramStore* g = engram_get(); const char* q = EL_CSTR(query); int64_t max_depth = (int64_t)depth; if (max_depth <= 0) max_depth = 2; el_val_t out = el_list_empty(); if (!q || g->node_count == 0) return out; /* Per-node activation tracking. */ double* best_activation = calloc((size_t)g->node_count, sizeof(double)); int64_t* best_hops = calloc((size_t)g->node_count, sizeof(int64_t)); int* reached = calloc((size_t)g->node_count, sizeof(int)); if (!best_activation || !best_hops || !reached) { free(best_activation); free(best_hops); free(reached); return out; } /* Find seeds */ typedef struct { int64_t idx; double act; } SeedEntry; SeedEntry* seeds = malloc((size_t)g->node_count * sizeof(SeedEntry)); int64_t seed_count = 0; if (!seeds) { free(best_activation); free(best_hops); free(reached); return out; } for (int64_t i = 0; i < g->node_count; i++) { EngramNode* n = &g->nodes[i]; if (istr_contains(n->content, q) || istr_contains(n->label, q) || istr_contains(n->tags, q)) { seeds[seed_count].idx = i; seeds[seed_count].act = n->salience; seed_count++; best_activation[i] = n->salience; best_hops[i] = 0; reached[i] = 1; } } /* BFS from each seed. We'll maintain a queue of (node_idx, depth, act). */ typedef struct { int64_t idx; int64_t hops; double act; } Frontier; Frontier* fr = malloc((size_t)(g->node_count * (max_depth + 1)) * sizeof(Frontier) + 16 * sizeof(Frontier)); if (!fr) { free(best_activation); free(best_hops); free(reached); free(seeds); return out; } int64_t fhead = 0, ftail = 0; int64_t fcap = (int64_t)((size_t)(g->node_count * (max_depth + 1)) + 16); for (int64_t s = 0; s < seed_count; s++) { if (ftail >= fcap) break; fr[ftail].idx = seeds[s].idx; fr[ftail].hops = 0; fr[ftail].act = seeds[s].act; ftail++; } const double DECAY = 0.7; while (fhead < ftail) { Frontier f = fr[fhead++]; if (f.hops >= max_depth) continue; const char* cur_id = g->nodes[f.idx].id; for (int64_t ei = 0; ei < g->edge_count; ei++) { EngramEdge* e = &g->edges[ei]; const char* other = NULL; if (e->from_id && strcmp(e->from_id, cur_id) == 0) other = e->to_id; else if (e->to_id && strcmp(e->to_id, cur_id) == 0) other = e->from_id; else continue; int64_t oi = engram_find_node_index(other); if (oi < 0) continue; double new_act = f.act * e->weight * DECAY; int64_t new_hops = f.hops + 1; if (!reached[oi] || new_act > best_activation[oi]) { best_activation[oi] = new_act; best_hops[oi] = new_hops; reached[oi] = 1; if (ftail < fcap) { fr[ftail].idx = oi; fr[ftail].hops = new_hops; fr[ftail].act = new_act; ftail++; } } } } /* Collect, filter by epistemic_confidence >= 0.2, sort desc by activation. */ typedef struct { int64_t idx; double act; double epist; int64_t hops; } Result; Result* results = malloc((size_t)g->node_count * sizeof(Result)); int64_t rcount = 0; if (!results) { free(best_activation); free(best_hops); free(reached); free(seeds); free(fr); return out; } for (int64_t i = 0; i < g->node_count; i++) { if (!reached[i]) continue; double epist = best_activation[i] * g->nodes[i].confidence; if (epist < 0.2) continue; results[rcount].idx = i; results[rcount].act = best_activation[i]; results[rcount].epist = epist; results[rcount].hops = best_hops[i]; rcount++; } /* Insertion sort by act desc. */ for (int64_t i = 1; i < rcount; i++) { Result key = results[i]; int64_t j = i - 1; while (j >= 0 && results[j].act < key.act) { results[j + 1] = results[j]; j--; } results[j + 1] = key; } for (int64_t i = 0; i < rcount; i++) { el_val_t entry = el_map_new(0); entry = el_map_set(entry, EL_STR(el_strdup("node")), engram_node_to_map(&g->nodes[results[i].idx])); entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), el_from_float(results[i].act)); entry = el_map_set(entry, EL_STR(el_strdup("epistemic_confidence")), el_from_float(results[i].epist)); entry = el_map_set(entry, EL_STR(el_strdup("hops")), (el_val_t)results[i].hops); out = el_list_append(out, entry); } free(best_activation); free(best_hops); free(reached); free(seeds); free(fr); free(results); return out; } /* ── Engram persistence (JSON snapshot) ─────────────────────────────────── */ static void engram_emit_node_json(JsonBuf* b, const EngramNode* n) { jb_putc(b, '{'); jb_puts(b, "\"id\":"); jb_emit_escaped(b, n->id ? n->id : ""); jb_puts(b, ",\"content\":"); jb_emit_escaped(b, n->content ? n->content : ""); jb_puts(b, ",\"node_type\":"); jb_emit_escaped(b, n->node_type ? n->node_type : ""); jb_puts(b, ",\"label\":"); jb_emit_escaped(b, n->label ? n->label : ""); jb_puts(b, ",\"tier\":"); jb_emit_escaped(b, n->tier ? n->tier : "Working"); jb_puts(b, ",\"tags\":"); jb_emit_escaped(b, n->tags ? n->tags : ""); jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, n->metadata ? n->metadata : "{}"); char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"salience\":%g", n->salience); jb_puts(b, tmp); snprintf(tmp, sizeof(tmp), ",\"importance\":%g", n->importance); jb_puts(b, tmp); snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", n->confidence); jb_puts(b, tmp); snprintf(tmp, sizeof(tmp), ",\"activation_count\":%lld", (long long)n->activation_count); jb_puts(b, tmp); snprintf(tmp, sizeof(tmp), ",\"last_activated\":%lld", (long long)n->last_activated); jb_puts(b, tmp); snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)n->created_at); jb_puts(b, tmp); snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)n->updated_at); jb_puts(b, tmp); jb_putc(b, '}'); } static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e) { jb_putc(b, '{'); jb_puts(b, "\"id\":"); jb_emit_escaped(b, e->id ? e->id : ""); jb_puts(b, ",\"from_id\":"); jb_emit_escaped(b, e->from_id ? e->from_id : ""); jb_puts(b, ",\"to_id\":"); jb_emit_escaped(b, e->to_id ? e->to_id : ""); jb_puts(b, ",\"relation\":"); jb_emit_escaped(b, e->relation ? e->relation : ""); jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, e->metadata ? e->metadata : "{}"); char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"weight\":%g", e->weight); jb_puts(b, tmp); snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", e->confidence); jb_puts(b, tmp); snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)e->created_at); jb_puts(b, tmp); snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)e->updated_at); jb_puts(b, tmp); snprintf(tmp, sizeof(tmp), ",\"last_fired\":%lld", (long long)e->last_fired); jb_puts(b, tmp); jb_putc(b, '}'); } el_val_t engram_save(el_val_t path) { const char* p = EL_CSTR(path); if (!p || !*p) return 0; EngramStore* g = engram_get(); JsonBuf b; jb_init(&b); jb_puts(&b, "{\"nodes\":["); for (int64_t i = 0; i < g->node_count; i++) { if (i > 0) jb_putc(&b, ','); engram_emit_node_json(&b, &g->nodes[i]); } jb_puts(&b, "],\"edges\":["); for (int64_t i = 0; i < g->edge_count; i++) { if (i > 0) jb_putc(&b, ','); engram_emit_edge_json(&b, &g->edges[i]); } jb_puts(&b, "]}"); FILE* f = fopen(p, "wb"); if (!f) { free(b.buf); return 0; } size_t w = fwrite(b.buf, 1, b.len, f); fclose(f); int ok = (w == b.len); free(b.buf); return ok ? 1 : 0; } /* Helper: extract a string field from a JSON object substring. */ static char* eg_get_str_field(const char* obj, const char* key) { const char* p = json_find_key(obj, key); if (!p) return el_strdup(""); if (*p != '"') return el_strdup(""); JsonParser jp = { .p = p, .end = p + strlen(p), .err = 0 }; char* out = jp_parse_string_raw(&jp); if (jp.err) { free(out); return el_strdup(""); } return out; } static double eg_get_num_field(const char* obj, const char* key) { const char* p = json_find_key(obj, key); if (!p || *p == '"' || *p == '{' || *p == '[') return 0.0; return strtod(p, NULL); } static int64_t eg_get_int_field(const char* obj, const char* key) { const char* p = json_find_key(obj, key); if (!p || *p == '"' || *p == '{' || *p == '[') return 0; return strtoll(p, NULL, 10); } /* Iterate the top-level nodes/edges arrays in a saved snapshot. */ static const char* eg_skip_ws(const char* p) { while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; return p; } el_val_t engram_load(el_val_t path) { const char* p = EL_CSTR(path); if (!p || !*p) return 0; FILE* f = fopen(p, "rb"); if (!f) return 0; fseek(f, 0, SEEK_END); long sz = ftell(f); rewind(f); if (sz <= 0) { fclose(f); return 0; } char* data = malloc((size_t)sz + 1); if (!data) { fclose(f); return 0; } size_t got = fread(data, 1, (size_t)sz, f); fclose(f); data[got] = '\0'; /* Reset store */ EngramStore* g = engram_get(); for (int64_t i = 0; i < g->node_count; i++) { free(g->nodes[i].id); free(g->nodes[i].content); free(g->nodes[i].node_type); free(g->nodes[i].label); free(g->nodes[i].tier); free(g->nodes[i].tags); free(g->nodes[i].metadata); } g->node_count = 0; for (int64_t i = 0; i < g->edge_count; i++) { free(g->edges[i].id); free(g->edges[i].from_id); free(g->edges[i].to_id); free(g->edges[i].relation); free(g->edges[i].metadata); } g->edge_count = 0; /* Walk nodes array */ const char* nodes_p = json_find_key(data, "nodes"); if (nodes_p) { nodes_p = eg_skip_ws(nodes_p); if (*nodes_p == '[') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); while (*nodes_p && *nodes_p != ']') { if (*nodes_p != '{') { nodes_p++; continue; } const char* end = json_skip_value(nodes_p); size_t n = (size_t)(end - nodes_p); char* obj = malloc(n + 1); memcpy(obj, nodes_p, n); obj[n] = '\0'; engram_grow_nodes(); EngramNode* nn = &g->nodes[g->node_count]; memset(nn, 0, sizeof(*nn)); nn->id = eg_get_str_field(obj, "id"); nn->content = eg_get_str_field(obj, "content"); nn->node_type = eg_get_str_field(obj, "node_type"); nn->label = eg_get_str_field(obj, "label"); nn->tier = eg_get_str_field(obj, "tier"); nn->tags = eg_get_str_field(obj, "tags"); nn->metadata = eg_get_str_field(obj, "metadata"); if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = el_strdup("{}"); } nn->salience = eg_get_num_field(obj, "salience"); nn->importance = eg_get_num_field(obj, "importance"); nn->confidence = eg_get_num_field(obj, "confidence"); nn->activation_count = eg_get_int_field(obj, "activation_count"); nn->last_activated = eg_get_int_field(obj, "last_activated"); nn->created_at = eg_get_int_field(obj, "created_at"); nn->updated_at = eg_get_int_field(obj, "updated_at"); g->node_count++; free(obj); nodes_p = end; nodes_p = eg_skip_ws(nodes_p); if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); } } } } /* Walk edges array */ const char* edges_p = json_find_key(data, "edges"); if (edges_p) { edges_p = eg_skip_ws(edges_p); if (*edges_p == '[') { edges_p++; edges_p = eg_skip_ws(edges_p); while (*edges_p && *edges_p != ']') { if (*edges_p != '{') { edges_p++; continue; } const char* end = json_skip_value(edges_p); size_t n = (size_t)(end - edges_p); char* obj = malloc(n + 1); memcpy(obj, edges_p, n); obj[n] = '\0'; engram_grow_edges(); EngramEdge* ee = &g->edges[g->edge_count]; memset(ee, 0, sizeof(*ee)); ee->id = eg_get_str_field(obj, "id"); ee->from_id = eg_get_str_field(obj, "from_id"); ee->to_id = eg_get_str_field(obj, "to_id"); ee->relation = eg_get_str_field(obj, "relation"); ee->metadata = eg_get_str_field(obj, "metadata"); if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup("{}"); } ee->weight = eg_get_num_field(obj, "weight"); ee->confidence = eg_get_num_field(obj, "confidence"); ee->created_at = eg_get_int_field(obj, "created_at"); ee->updated_at = eg_get_int_field(obj, "updated_at"); ee->last_fired = eg_get_int_field(obj, "last_fired"); g->edge_count++; free(obj); edges_p = end; edges_p = eg_skip_ws(edges_p); if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); } } } } free(data); return 1; } /* ── Engram JSON-string accessors ───────────────────────────────────────── * These return pre-serialized JSON strings so callers (especially HTTP * handlers) don't have to round-trip ElList/ElMap through json_stringify * — which can't reliably distinguish those structures from raw pointers * due to el_val_t's type erasure. The runtime knows the real C types and * can serialize directly. */ el_val_t engram_get_node_json(el_val_t id) { const char* sid = EL_CSTR(id); EngramNode* n = engram_find_node(sid); if (!n) return el_wrap_str(el_strdup("{}")); JsonBuf b; jb_init(&b); engram_emit_node_json(&b, n); return el_wrap_str(b.buf); } el_val_t engram_search_json(el_val_t query, el_val_t limit) { EngramStore* g = engram_get(); const char* q = EL_CSTR(query); int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; JsonBuf b; jb_init(&b); jb_putc(&b, '['); int first = 1; int64_t found = 0; if (q && *q) { for (int64_t i = 0; i < g->node_count && found < lim; i++) { EngramNode* n = &g->nodes[i]; if (istr_contains(n->content, q) || istr_contains(n->label, q) || istr_contains(n->tags, q)) { if (!first) jb_putc(&b, ','); engram_emit_node_json(&b, n); first = 0; found++; } } } jb_putc(&b, ']'); return el_wrap_str(b.buf); } el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) { EngramStore* g = engram_get(); int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; int64_t off = (int64_t)offset; if (off < 0) off = 0; JsonBuf b; jb_init(&b); jb_putc(&b, '['); if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } for (int64_t i = 0; i < g->node_count; i++) idx[i] = i; engram_sort_indices_by_salience(idx, g->node_count, g->nodes); int64_t end = off + lim; if (end > g->node_count) end = g->node_count; int first = 1; for (int64_t i = off; i < end; i++) { if (!first) jb_putc(&b, ','); engram_emit_node_json(&b, &g->nodes[idx[i]]); first = 0; } free(idx); jb_putc(&b, ']'); return el_wrap_str(b.buf); } el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { /* Re-implement here directly so we serialize without going through * the ElList path. Walks BFS to max_depth, emits {node, edge, hops} * triples. */ EngramStore* g = engram_get(); const char* sid = EL_CSTR(node_id); int64_t depth = (int64_t)max_depth; if (depth <= 0) depth = 1; const char* dir = EL_CSTR(direction); if (!dir) dir = "both"; int allow_out = (strcmp(dir, "out") == 0) || (strcmp(dir, "both") == 0); int allow_in = (strcmp(dir, "in") == 0) || (strcmp(dir, "both") == 0); JsonBuf b; jb_init(&b); jb_putc(&b, '['); if (!sid || !*sid) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } /* Frontier of (node_id, hops). Cap to a sane size. */ char** frontier = calloc(1024, sizeof(char*)); int64_t* frontier_h = calloc(1024, sizeof(int64_t)); int64_t fc = 0; char** visited = calloc(1024, sizeof(char*)); int64_t vc = 0; if (!frontier || !frontier_h || !visited) { free(frontier); free(frontier_h); free(visited); jb_putc(&b, ']'); return el_wrap_str(b.buf); } frontier[fc] = el_strdup(sid); frontier_h[fc] = 0; fc++; visited[vc++] = el_strdup(sid); int first = 1; while (fc > 0) { char* cur = frontier[0]; int64_t h = frontier_h[0]; for (int64_t k = 1; k < fc; k++) { frontier[k-1] = frontier[k]; frontier_h[k-1] = frontier_h[k]; } fc--; if (h >= depth) { free(cur); continue; } for (int64_t i = 0; i < g->edge_count; i++) { EngramEdge* e = &g->edges[i]; const char* peer = NULL; if (allow_out && e->from_id && strcmp(e->from_id, cur) == 0) peer = e->to_id; else if (allow_in && e->to_id && strcmp(e->to_id, cur) == 0) peer = e->from_id; if (!peer) continue; int seen = 0; for (int64_t v = 0; v < vc; v++) { if (strcmp(visited[v], peer) == 0) { seen = 1; break; } } if (seen) continue; EngramNode* n = engram_find_node(peer); if (!n) continue; if (!first) jb_putc(&b, ','); jb_puts(&b, "{\"node\":"); engram_emit_node_json(&b, n); jb_puts(&b, ",\"edge\":"); engram_emit_edge_json(&b, e); char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(h + 1)); jb_puts(&b, tmp); first = 0; if (vc < 1024) visited[vc++] = el_strdup(peer); if (fc < 1024 && h + 1 < depth) { frontier[fc] = el_strdup(peer); frontier_h[fc] = h + 1; fc++; } } free(cur); } for (int64_t i = 0; i < fc; i++) free(frontier[i]); for (int64_t i = 0; i < vc; i++) free(visited[i]); free(frontier); free(frontier_h); free(visited); jb_putc(&b, ']'); return el_wrap_str(b.buf); } el_val_t engram_activate_json(el_val_t query, el_val_t depth) { /* Run the existing engram_activate to get the ElList of result maps, * then walk that list and serialize each entry into JSON manually. * We have the raw nodes via engram_find_node, so we can re-emit * directly without trusting json_stringify on the ElMap. */ el_val_t lst = engram_activate(query, depth); ElList* arr = (ElList*)(uintptr_t)lst; JsonBuf b; jb_init(&b); jb_putc(&b, '['); if (arr) { for (int64_t i = 0; i < arr->length; i++) { ElMap* entry = (ElMap*)(uintptr_t)arr->elems[i]; if (!entry) continue; /* The entry map has keys: "node" (ElMap), "activation_strength" * (Float bit-pattern), "hops" (Int). Read them from the map * directly using el_map_get with EL_STR keys. */ el_val_t node_map = el_map_get(arr->elems[i], EL_STR("node")); el_val_t strength_v = el_map_get(arr->elems[i], EL_STR("activation_strength")); el_val_t hops_v = el_map_get(arr->elems[i], EL_STR("hops")); /* Look up the underlying EngramNode by id field of the map */ el_val_t id_v = el_map_get(node_map, EL_STR("id")); const char* id_s = EL_CSTR(id_v); EngramNode* n = id_s ? engram_find_node(id_s) : NULL; if (i > 0) jb_putc(&b, ','); jb_puts(&b, "{\"node\":"); if (n) { engram_emit_node_json(&b, n); } else { jb_puts(&b, "{}"); } char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"activation_strength\":%g", el_to_float(strength_v)); jb_puts(&b, tmp); snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(int64_t)hops_v); jb_puts(&b, tmp); } } jb_putc(&b, ']'); return el_wrap_str(b.buf); } el_val_t engram_stats_json(void) { EngramStore* g = engram_get(); char buf[128]; snprintf(buf, sizeof(buf), "{\"node_count\":%lld,\"edge_count\":%lld}", (long long)g->node_count, (long long)g->edge_count); 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` 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"; static const char* LLM_API_URL = "https://api.anthropic.com/v1/messages"; static const char* LLM_VERSION = "2023-06-01"; static const char* llm_resolve_model(const char* m) { if (!m || !*m) return LLM_DEFAULT_MODEL; return m; } /* Make an Anthropic /v1/messages request with the given JSON body. Returns * the assistant's first text content as an owned string, or a JSON error * fragment on transport failure. */ static el_val_t llm_request(const char* json_body) { const char* api_key = getenv("ANTHROPIC_API_KEY"); if (!api_key || !*api_key) { return http_error_json("ANTHROPIC_API_KEY not set"); } struct curl_slist* h = NULL; h = curl_slist_append(h, "Content-Type: application/json"); { size_t n = strlen(api_key) + 16; char* line = malloc(n); snprintf(line, n, "x-api-key: %s", api_key); h = curl_slist_append(h, line); free(line); } { size_t n = strlen(LLM_VERSION) + 32; char* line = malloc(n); snprintf(line, n, "anthropic-version: %s", LLM_VERSION); h = curl_slist_append(h, line); free(line); } el_val_t resp = http_do("POST", LLM_API_URL, json_body, h); curl_slist_free_all(h); return resp; } /* Extract concatenated assistant text from an Anthropic /v1/messages * response. The response shape is: * {"content":[{"type":"text","text":"..."}, ...], ...} * If parsing fails, returns the raw response so the caller can inspect. */ static el_val_t llm_extract_text(el_val_t resp_val) { const char* resp = EL_CSTR(resp_val); if (!resp || !*resp) return el_wrap_str(el_strdup("")); /* If error JSON, propagate as-is. */ if (resp[0] == '{' && strstr(resp, "\"error\"")) { return el_wrap_str(el_strdup(resp)); } /* Find "content":[ ... ] */ const char* p = json_find_key(resp, "content"); if (!p) return el_wrap_str(el_strdup(resp)); while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; if (*p != '[') return el_wrap_str(el_strdup(resp)); p++; JsonBuf out; jb_init(&out); 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* type_p = json_find_key(obj, "type"); if (type_p && *type_p == '"') { JsonParser jp = { .p = type_p, .end = type_p + strlen(type_p), .err = 0 }; char* type_s = jp_parse_string_raw(&jp); if (!jp.err && type_s && strcmp(type_s, "text") == 0) { const char* tp = json_find_key(obj, "text"); if (tp && *tp == '"') { JsonParser jp2 = { .p = tp, .end = tp + strlen(tp), .err = 0 }; char* text_s = jp_parse_string_raw(&jp2); if (!jp2.err && text_s) jb_puts(&out, text_s); free(text_s); } } free(type_s); } free(obj); p = end; } return el_wrap_str(out.buf); } el_val_t llm_call(el_val_t model, el_val_t prompt) { const char* m = llm_resolve_model(EL_CSTR(model)); const char* u = EL_CSTR(prompt); if (!u) u = ""; char* esc_user = json_escape_alloc(u); JsonBuf b; jb_init(&b); jb_putc(&b, '{'); jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, m); jb_puts(&b, ",\"max_tokens\":4096"); jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); free(esc_user); el_val_t resp = llm_request(b.buf); free(b.buf); return llm_extract_text(resp); } el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) { const char* m = llm_resolve_model(EL_CSTR(model)); const char* s = EL_CSTR(system_prompt); if (!s) s = ""; const char* u = EL_CSTR(user_prompt); if (!u) u = ""; char* esc_sys = json_escape_alloc(s); char* esc_user = json_escape_alloc(u); JsonBuf b; jb_init(&b); jb_putc(&b, '{'); jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, m); jb_puts(&b, ",\"max_tokens\":4096"); if (*s) { jb_puts(&b, ",\"system\":\""); jb_puts(&b, esc_sys); jb_puts(&b, "\""); } jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); free(esc_sys); free(esc_user); el_val_t resp = llm_request(b.buf); free(b.buf); 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) { /* 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). */ static char* base64_encode(const unsigned char* src, size_t n) { static const char tbl[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; size_t out_len = 4 * ((n + 2) / 3); char* out = malloc(out_len + 1); if (!out) return NULL; size_t o = 0; for (size_t i = 0; i < n;) { uint32_t v = 0; int got = 0; v |= (uint32_t)src[i++] << 16; got++; if (i < n) { v |= (uint32_t)src[i++] << 8; got++; } if (i < n) { v |= (uint32_t)src[i++]; got++; } out[o++] = tbl[(v >> 18) & 0x3f]; out[o++] = tbl[(v >> 12) & 0x3f]; out[o++] = (got > 1) ? tbl[(v >> 6) & 0x3f] : '='; out[o++] = (got > 2) ? tbl[v & 0x3f] : '='; } out[o] = '\0'; return out; } el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64) { const char* m = llm_resolve_model(EL_CSTR(model)); const char* s = EL_CSTR(system); if (!s) s = ""; const char* u = EL_CSTR(prompt); if (!u) u = ""; const char* img = EL_CSTR(image_url_or_b64); if (!img) img = ""; /* Choose source mode */ char* image_block = NULL; if (strncasecmp(img, "http://", 7) == 0 || strncasecmp(img, "https://", 8) == 0) { char* esc_url = json_escape_alloc(img); size_t n = strlen(esc_url) + 128; image_block = malloc(n); snprintf(image_block, n, "{\"type\":\"image\",\"source\":{\"type\":\"url\",\"url\":\"%s\"}}", esc_url); free(esc_url); } else if (strncmp(img, "data:", 5) == 0) { /* Inline data URL: split media-type and base64 */ const char* semi = strchr(img + 5, ';'); const char* comma = strchr(img + 5, ','); char media[64] = "image/png"; if (semi && comma && semi < comma) { size_t ml = (size_t)(semi - (img + 5)); if (ml >= sizeof(media)) ml = sizeof(media) - 1; memcpy(media, img + 5, ml); media[ml] = '\0'; } const char* b64 = comma ? comma + 1 : ""; char* esc_media = json_escape_alloc(media); char* esc_b64 = json_escape_alloc(b64); size_t n = strlen(esc_media) + strlen(esc_b64) + 192; image_block = malloc(n); snprintf(image_block, n, "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," "\"media_type\":\"%s\",\"data\":\"%s\"}}", esc_media, esc_b64); free(esc_media); free(esc_b64); } else if (*img) { /* Treat as file path: read, base64-encode, attach. */ FILE* f = fopen(img, "rb"); if (!f) { char err[256]; snprintf(err, sizeof(err), "cannot open image: %s", img); return http_error_json(err); } fseek(f, 0, SEEK_END); long sz = ftell(f); rewind(f); if (sz <= 0) { fclose(f); return http_error_json("empty image file"); } unsigned char* buf = malloc((size_t)sz); if (!buf) { fclose(f); return http_error_json("oom"); } size_t got = fread(buf, 1, (size_t)sz, f); fclose(f); char* b64 = base64_encode(buf, got); free(buf); if (!b64) return http_error_json("base64 encode failed"); const char* media = "image/png"; size_t ilen = strlen(img); if (ilen >= 4) { if (strcasecmp(img + ilen - 4, ".jpg") == 0 || (ilen >= 5 && strcasecmp(img + ilen - 5, ".jpeg") == 0)) media = "image/jpeg"; else if (strcasecmp(img + ilen - 4, ".gif") == 0) media = "image/gif"; else if (strcasecmp(img + ilen - 4, ".webp") == 0) media = "image/webp"; } char* esc_b64 = json_escape_alloc(b64); free(b64); size_t n = strlen(esc_b64) + 192; image_block = malloc(n); snprintf(image_block, n, "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," "\"media_type\":\"%s\",\"data\":\"%s\"}}", media, esc_b64); free(esc_b64); } char* esc_sys = json_escape_alloc(s); char* esc_user = json_escape_alloc(u); JsonBuf b; jb_init(&b); jb_putc(&b, '{'); jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, m); jb_puts(&b, ",\"max_tokens\":4096"); if (*s) { jb_puts(&b, ",\"system\":\""); jb_puts(&b, esc_sys); jb_puts(&b, "\""); } jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":["); if (image_block) { jb_puts(&b, image_block); jb_putc(&b, ','); } jb_puts(&b, "{\"type\":\"text\",\"text\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}]}"); free(esc_sys); free(esc_user); free(image_block); el_val_t resp = llm_request(b.buf); free(b.buf); return llm_extract_text(resp); } el_val_t llm_models(void) { el_val_t lst = el_list_empty(); lst = el_list_append(lst, el_wrap_str(el_strdup("claude-sonnet-4-5"))); lst = el_list_append(lst, el_wrap_str(el_strdup("claude-opus-4-7"))); lst = el_list_append(lst, el_wrap_str(el_strdup("claude-haiku-4-5"))); return lst; } /* ── Native VM builtin aliases ────────────────────────────────────────────── * El source files use native_* names (El VM builtins). * When compiled to C, these map directly to el_* runtime functions. */ el_val_t native_list_get(el_val_t list, el_val_t index) { return el_list_get(list, index); } el_val_t native_list_len(el_val_t list) { return el_list_len(list); } el_val_t native_list_append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); } el_val_t native_list_empty(void) { return el_list_empty(); } el_val_t native_string_chars(el_val_t sv) { const char* s = EL_CSTR(sv); el_val_t result = el_list_empty(); if (!s) return result; while (*s) { char buf[2]; buf[0] = *s; buf[1] = '\0'; result = el_list_append(result, EL_STR(strdup(buf))); s++; } return result; } el_val_t native_int_to_str(el_val_t n) { return int_to_str(n); } /* ── Method-call shorthand aliases ────────────────────────────────────────── * Short names that result from the method-call convention: * myList.append(x) → append(myList, x) * myList.len() → len(myList) * myList.get(i) → get(myList, i) * myMap.map_get(k) → map_get(myMap, k) * myMap.map_set(k,v) → map_set(myMap, k, v) */ el_val_t append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); } el_val_t len(el_val_t list) { return el_list_len(list); } el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); } el_val_t map_get(el_val_t map, el_val_t key) { return el_map_get(map, key); } el_val_t map_set(el_val_t map, el_val_t key, el_val_t value) { return el_map_set(map, key, value); }