0fa9e749e1
Three changes that turned the runtime into something Engram-the-server
can actually run on top of.
1. engram_*_json accessors. The runtime's engram_get_node/search/scan/
neighbors/activate return ElList/ElMap; passing those through
json_stringify hit the type-erasure wall (an ElList* has no header
that distinguishes it from a string pointer). Added pre-serialized
sibling builtins:
engram_get_node_json(id) -> JSON object
engram_search_json(query, limit) -> JSON array of node objects
engram_scan_nodes_json(limit, offset)
engram_neighbors_json(node_id, max_depth, direction)
engram_activate_json(query, depth)
engram_stats_json()
Each walks the typed C structures and serializes directly, reusing
the existing engram_emit_node_json / engram_emit_edge_json helpers
from the snapshot path.
2. http_set_handler now falls back to dlsym(RTLD_DEFAULT, name) when
the named handler isn't already in the C-level registry. El programs
that define `fn handle_request(method, path, body) -> String` can
register themselves just by calling http_set_handler("handle_request").
No C glue required. Verified live on a real El server.
3. Codegen: extended int-typed dispatch on `+` to handle Calls. New
helper is_int_call recognizes a known-int-returning builtin set:
str_len, str_index_of, str_to_int, str_char_code, native_list_len,
el_list_len, len, json_get_int, json_array_len, engram_node_count,
engram_edge_count, time_now, time_now_utc, time_diff, time_add,
time_from_parts, el_abs/max/min, float_to_int. With this,
`pos + str_len(needle)` compiles to integer arithmetic instead of
string concat. The earlier limitation noted in the previous commit
(Ident + Call returning Int) is now closed.
Also: el_to_float / el_from_float moved to el_runtime.h as static
inlines so generated programs can use them. Eliminates the unused
inline definitions that were duplicating in the .c file.
Closure verified: stage1 vs stage2 byte-identical against the new
runtime. dist/platform/elc rebuilt; .prev4 preserved.
Engram server (engram/src/server.el) end-to-end:
POST /api/nodes ×3 → 3 UUIDs returned
POST /api/edges ×2 → linkage made
GET /api/stats → {"node_count":3,"edge_count":2}
GET /api/search?q=spreading&limit=5 → 1 hit, full node JSON
POST /api/activate {"query":"Hebbian","depth":3}
→ seed node @ hop 0, strength 0.8
→ 1-hop neighbor @ strength 0.392 (= 0.8 × 0.7 weight × 0.7 decay)
GET /api/neighbors/<id>?depth=2 → {node, edge, hops} triple
POST /api/save → {"ok":true,"path":"..."}
Server stays alive across all routes.
Snapshot save/load on restart still TODO — server starts with 0 nodes
even when a snapshot exists; investigation pending.
3310 lines
121 KiB
C
3310 lines
121 KiB
C
/*
|
|
* 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<runtime-dir> -lcurl -lpthread -o <prog> <prog>.c el_runtime.c
|
|
*
|
|
* Link requirements: -lcurl (HTTP client + LLM), -lpthread (HTTP server).
|
|
*/
|
|
|
|
#include "el_runtime.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
#include <math.h>
|
|
#include <time.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <dlfcn.h> /* dlsym for http_set_handler fallback */
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <pthread.h>
|
|
#include <curl/curl.h>
|
|
|
|
/* ── 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, "<!DOCTYPE", 9) == 0) return "text/html; charset=utf-8";
|
|
if (strncasecmp(p, "<html", 5) == 0) return "text/html; charset=utf-8";
|
|
if (*p == '{' || *p == '[') return "application/json; charset=utf-8";
|
|
return "text/plain; charset=utf-8";
|
|
}
|
|
|
|
/* Read the full HTTP request from a connection: request line, headers, body. */
|
|
static int http_read_request(int fd, char** out_method, char** out_path,
|
|
char** out_body) {
|
|
*out_method = NULL; *out_path = NULL; *out_body = NULL;
|
|
/* Read headers until \r\n\r\n */
|
|
size_t cap = 4096, len = 0;
|
|
char* buf = malloc(cap);
|
|
if (!buf) return -1;
|
|
while (1) {
|
|
if (len + 1 >= 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);
|
|
}
|
|
|
|
/* ── DHARMA network stubs ───────────────────────────────────────────────────
|
|
* Stub implementations for all dharma_* and engram_* builtins.
|
|
* Each stub prints a descriptive line to stdout so calls are visible in tests.
|
|
* Full implementations are provided by the DHARMA runtime linked at deploy. */
|
|
|
|
el_val_t dharma_connect(el_val_t cgi_id) {
|
|
const char* id = EL_CSTR(cgi_id);
|
|
if (!id) id = "(null)";
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf), "[dharma] connect: %s", id);
|
|
puts(buf);
|
|
/* Return a synthetic channel ID of the form "ch:<cgi_id>" */
|
|
char ch[272];
|
|
snprintf(ch, sizeof(ch), "ch:%s", id);
|
|
return el_wrap_str(el_strdup(ch));
|
|
}
|
|
|
|
el_val_t dharma_send(el_val_t channel, el_val_t content) {
|
|
const char* ch = EL_CSTR(channel);
|
|
const char* msg = EL_CSTR(content);
|
|
if (!ch) ch = "(null)";
|
|
if (!msg) msg = "(null)";
|
|
char buf[1024];
|
|
snprintf(buf, sizeof(buf), "[dharma] send on %s: %s", ch, msg);
|
|
puts(buf);
|
|
return el_wrap_str(el_strdup(""));
|
|
}
|
|
|
|
el_val_t dharma_activate(el_val_t query) {
|
|
const char* q = EL_CSTR(query);
|
|
if (!q) q = "(null)";
|
|
char buf[512];
|
|
snprintf(buf, sizeof(buf), "[dharma] activate: %s", q);
|
|
puts(buf);
|
|
return el_list_empty();
|
|
}
|
|
|
|
void dharma_emit(el_val_t event_type, el_val_t payload) {
|
|
const char* et = EL_CSTR(event_type);
|
|
const char* pay = EL_CSTR(payload);
|
|
if (!et) et = "(null)";
|
|
if (!pay) pay = "(null)";
|
|
char buf[1024];
|
|
snprintf(buf, sizeof(buf), "[dharma] emit: %s %s", et, pay);
|
|
puts(buf);
|
|
}
|
|
|
|
el_val_t dharma_field(el_val_t event_type) {
|
|
const char* et = EL_CSTR(event_type);
|
|
if (!et) et = "(null)";
|
|
char buf[512];
|
|
snprintf(buf, sizeof(buf), "[dharma] field: %s", et);
|
|
puts(buf);
|
|
return el_map_new(0);
|
|
}
|
|
|
|
void dharma_strengthen(el_val_t cgi_id, el_val_t weight) {
|
|
const char* id = EL_CSTR(cgi_id);
|
|
if (!id) id = "(null)";
|
|
/* weight is encoded as el_val_t; print as integer (float encoding TBD) */
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf), "[dharma] strengthen: %s +%lld", id, (long long)weight);
|
|
puts(buf);
|
|
}
|
|
|
|
el_val_t dharma_relationship(el_val_t cgi_id) {
|
|
const char* id = EL_CSTR(cgi_id);
|
|
if (!id) id = "(null)";
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf), "[dharma] relationship: %s", id);
|
|
puts(buf);
|
|
return 0; /* 0.0 — no prior relationship in stub mode */
|
|
}
|
|
|
|
el_val_t dharma_peers(void) {
|
|
puts("[dharma] peers");
|
|
return el_list_empty();
|
|
}
|
|
|
|
/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */
|
|
/*
|
|
* 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));
|
|
}
|
|
|
|
/* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */
|
|
/*
|
|
* All LLM builtins call https://api.anthropic.com/v1/messages with the API
|
|
* key from env ANTHROPIC_API_KEY. Default model is "claude-sonnet-4-5"
|
|
* when the supplied model is empty/null.
|
|
*
|
|
* `llm_call_agentic` is currently implemented as a single-turn fallback
|
|
* delegating to `llm_call_system` — TODO: implement the full multi-turn
|
|
* tool_use / tool_result loop. Programs that need agentic tool dispatch
|
|
* should register a tool handler via state_set("tools/<name>", json) and
|
|
* loop themselves until the model emits stop_reason=end_turn.
|
|
*/
|
|
|
|
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);
|
|
}
|
|
|
|
el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools) {
|
|
/* TODO: full multi-turn tool_use / tool_result loop. For now we delegate
|
|
* to llm_call_system and ignore the tools list — programs needing real
|
|
* agentic dispatch should drive the loop themselves with raw http_post. */
|
|
(void)tools;
|
|
return llm_call_system(model, system, user);
|
|
}
|
|
|
|
/* 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); }
|