diff --git a/dist/platform/elc b/dist/platform/elc index c52f4fe..722711c 100755 Binary files a/dist/platform/elc and b/dist/platform/elc differ diff --git a/dist/platform/elc.c b/dist/platform/elc.c index 785e7ae..60eefad 100644 --- a/dist/platform/elc.c +++ b/dist/platform/elc.c @@ -1650,7 +1650,7 @@ el_val_t cg_expr(el_val_t expr) { } if (str_eq(kind, EL_STR("Float"))) { el_val_t v = el_get_field(expr, EL_STR("value")); - return v; + return el_str_concat(el_str_concat(EL_STR("el_from_float("), v), EL_STR(")")); } if (str_eq(kind, EL_STR("Str"))) { el_val_t v = el_get_field(expr, EL_STR("value")); diff --git a/dist/platform/elc.prev2 b/dist/platform/elc.prev2 new file mode 100755 index 0000000..c52f4fe Binary files /dev/null and b/dist/platform/elc.prev2 differ diff --git a/el-compiler/runtime/el_runtime.c b/el-compiler/runtime/el_runtime.c index 8aee619..e5b0365 100644 --- a/el-compiler/runtime/el_runtime.c +++ b/el-compiler/runtime/el_runtime.c @@ -6,7 +6,9 @@ * On any 64-bit system sizeof(pointer) <= sizeof(int64_t), so this is safe. * * Compile with: - * cc -std=c11 -I -o .c el_runtime.c + * cc -std=c11 -I -lcurl -lpthread -o .c el_runtime.c + * + * Link requirements: -lcurl (HTTP client + LLM), -lpthread (HTTP server). */ #include "el_runtime.h" @@ -20,6 +22,16 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* ── Internal allocators ─────────────────────────────────────────────────── */ @@ -341,20 +353,470 @@ el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { return mapv; } -/* ── HTTP stubs ──────────────────────────────────────────────────────────── */ +/* ── 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) { - (void)url; - return el_wrap_str(el_strdup("")); + return http_do("GET", EL_CSTR(url), NULL, NULL); } el_val_t http_post(el_val_t url, el_val_t body) { - (void)url; (void)body; - return el_wrap_str(el_strdup("")); + 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 : ""); + pthread_mutex_unlock(&_http_handler_mu); +} + +static http_handler_fn http_lookup_active(void) { + http_handler_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler) { + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, _http_active_handler) == 0) { + out = _http_handlers[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Auto-detect Content-Type from response body. */ +static const char* http_detect_content_type(const char* body) { + if (!body) return "text/plain; charset=utf-8"; + const char* p = body; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (strncasecmp(p, "= cap) { + if (cap >= 1024 * 1024) { free(buf); return -1; } + cap *= 2; + buf = realloc(buf, cap); + if (!buf) return -1; + } + ssize_t n = recv(fd, buf + len, cap - len - 1, 0); + if (n <= 0) { free(buf); return -1; } + len += (size_t)n; + buf[len] = '\0'; + if (strstr(buf, "\r\n\r\n")) break; + } + /* Parse request line */ + char* sp1 = strchr(buf, ' '); + if (!sp1) { free(buf); return -1; } + *sp1 = '\0'; + *out_method = el_strdup(buf); + char* path_start = sp1 + 1; + char* sp2 = strchr(path_start, ' '); + if (!sp2) { free(*out_method); *out_method = NULL; free(buf); return -1; } + *sp2 = '\0'; + *out_path = el_strdup(path_start); + char* hdr_end = strstr(sp2 + 1, "\r\n\r\n"); + /* Find Content-Length */ + long content_length = 0; + char* hp = sp2 + 1; + while (hp < hdr_end) { + char* line_end = strstr(hp, "\r\n"); + if (!line_end || line_end >= hdr_end) break; + if (strncasecmp(hp, "Content-Length:", 15) == 0) { + content_length = strtol(hp + 15, NULL, 10); + if (content_length < 0) content_length = 0; + if (content_length > 64 * 1024 * 1024) content_length = 64 * 1024 * 1024; + } + hp = line_end + 2; + } + /* Body: any bytes already read past hdr_end, plus more recv */ + char* body_start = hdr_end + 4; + size_t body_have = (buf + len) - body_start; + char* body = malloc((size_t)content_length + 1); + if (!body) { free(*out_method); free(*out_path); *out_method=NULL; *out_path=NULL; free(buf); return -1; } + if ((long)body_have > content_length) body_have = (size_t)content_length; + if (body_have > 0) memcpy(body, body_start, body_have); + while ((long)body_have < content_length) { + ssize_t n = recv(fd, body + body_have, (size_t)content_length - body_have, 0); + if (n <= 0) break; + body_have += (size_t)n; + } + body[body_have] = '\0'; + *out_body = body; + free(buf); + return 0; +} + +static void http_send_response(int fd, const char* body) { + if (!body) body = ""; + const char* ct = http_detect_content_type(body); + size_t blen = strlen(body); + char header[512]; + int hl = snprintf(header, sizeof(header), + "HTTP/1.1 200 OK\r\n" + "Content-Type: %s\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n", ct, blen); + if (hl < 0) return; + /* Best-effort full writes */ + const char* p = header; size_t left = (size_t)hl; + while (left > 0) { + ssize_t w = send(fd, p, left, 0); + if (w <= 0) return; + p += w; left -= (size_t)w; + } + p = body; left = blen; + while (left > 0) { + ssize_t w = send(fd, p, left, 0); + if (w <= 0) return; + p += w; left -= (size_t)w; + } +} + +typedef struct { + int fd; +} HttpWorkerArg; + +static void* http_worker(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL; + if (http_read_request(fd, &method, &path, &body) == 0) { + http_handler_fn h = http_lookup_active(); + char* response = NULL; + if (h) { + el_val_t r = h(EL_STR(method), EL_STR(path), EL_STR(body)); + const char* rs = EL_CSTR(r); + response = el_strdup(rs ? rs : ""); + } else { + response = el_strdup("el-runtime: no http handler registered (call http_set_handler)"); + } + http_send_response(fd, response); + free(response); + } + free(method); free(path); free(body); + close(fd); + /* release a slot */ + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; } void http_serve(el_val_t port, el_val_t handler) { - (void)port; (void)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 ──────────────────────────────────────────────────────────── */ @@ -386,6 +848,21 @@ el_val_t fs_write(el_val_t pathv, el_val_t contentv) { 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) { @@ -420,18 +897,9 @@ el_val_t json_get(el_val_t jsonv, el_val_t keyv) { } /* ── Float bit-cast helpers ──────────────────────────────────────────────── */ - -static inline double el_to_float(el_val_t v) { - union { int64_t i; double f; } u; - u.i = (int64_t)v; - return u.f; -} - -static inline el_val_t el_from_float(double f) { - union { double f; int64_t i; } u; - u.f = f; - return (el_val_t)u.i; -} +/* `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) ─────────────────────────────────────── */ /* @@ -449,11 +917,7 @@ static inline el_val_t el_from_float(double f) { * types separately. This matches the rest of el_val_t's type-erased model. */ -typedef struct { - const char* p; - const char* end; - int err; -} JsonParser; +/* JsonParser struct is forward-declared near the HTTP/Engram section. */ static void jp_skip_ws(JsonParser* jp) { while (jp->p < jp->end) { @@ -608,11 +1072,8 @@ el_val_t json_parse(el_val_t sv) { * from small integers without a separate type tag. */ -typedef struct { - char* buf; - size_t len; - size_t cap; -} JsonBuf; +/* 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; @@ -1525,46 +1986,1068 @@ el_val_t dharma_peers(void) { return el_list_empty(); } -/* ── Engram local graph stubs ────────────────────────────────────────────── */ +/* ── 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; +} + +/* 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); - if (!c) c = "(null)"; - if (!nt) nt = "(null)"; - char buf[512]; - snprintf(buf, sizeof(buf), "[engram] node: %s (type=%s salience=%lld)", c, nt, (long long)salience); - puts(buf); - return el_wrap_str(el_strdup("stub-node-id")); + 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_activate(el_val_t query, el_val_t depth) { - const char* q = EL_CSTR(query); - if (!q) q = "(null)"; - char buf[512]; - snprintf(buf, sizeof(buf), "[engram] activate: %s depth=%lld", q, (long long)depth); - puts(buf); - return el_list_empty(); +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)); } -void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { - const char* f = EL_CSTR(from_id); - const char* t = EL_CSTR(to_id); - const char* r = EL_CSTR(relation); - if (!f) f = "(null)"; - if (!t) t = "(null)"; - if (!r) r = "(null)"; - char buf[512]; - snprintf(buf, sizeof(buf), "[engram] connect: %s -[%s]-> %s weight=%lld", f, r, t, (long long)weight); - puts(buf); +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* id = EL_CSTR(node_id); - if (!id) id = "(null)"; - char buf[256]; - snprintf(buf, sizeof(buf), "[engram] strengthen: %s", id); - puts(buf); + 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; +} + +/* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */ +/* + * All LLM builtins call https://api.anthropic.com/v1/messages with the API + * key from env ANTHROPIC_API_KEY. Default model is "claude-sonnet-4-5" + * when the supplied model is empty/null. + * + * `llm_call_agentic` is currently implemented as a single-turn fallback + * delegating to `llm_call_system` — TODO: implement the full multi-turn + * tool_use / tool_result loop. Programs that need agentic tool dispatch + * should register a tool handler via state_set("tools/", json) and + * loop themselves until the model emits stop_reason=end_turn. + */ + +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 ────────────────────────────────────────────── diff --git a/el-compiler/runtime/el_runtime.h b/el-compiler/runtime/el_runtime.h index acfa263..d747679 100644 --- a/el-compiler/runtime/el_runtime.h +++ b/el-compiler/runtime/el_runtime.h @@ -22,6 +22,15 @@ * EL_STR(s) cast string literal to el_val_t * EL_CSTR(v) cast el_val_t back to const char* * EL_INT(v) identity — el_val_t is already int64_t + * + * Link requirements: + * -lcurl — required for the HTTP client (http_get, http_post, llm_*). + * -lpthread — required for the HTTP server (one detached thread per + * connection, capped at 64 concurrent). + * + * Canonical compile command: + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el-compiler/runtime/el_runtime.c */ #pragma once @@ -36,6 +45,22 @@ typedef int64_t el_val_t; #define EL_INT(v) (v) #define EL_NULL ((el_val_t)0) +/* Float values share the el_val_t (int64) slot via a bit-cast. + * The codegen emits Float literals as `el_from_float()` so the + * underlying bits represent the IEEE 754 double. Float-aware builtins + * (math, format, json) round-trip via these helpers. */ +static inline double el_to_float(el_val_t v) { + union { int64_t i; double f; } u; + u.i = (int64_t)v; + return u.f; +} + +static inline el_val_t el_from_float(double f) { + union { double f; int64_t i; } u; + u.f = f; + return (el_val_t)u.i; +} + #ifdef __cplusplus extern "C" { #endif @@ -88,12 +113,18 @@ el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value); el_val_t http_get(el_val_t url); el_val_t http_post(el_val_t url, el_val_t body); +el_val_t http_post_json(el_val_t url, el_val_t json_body); +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map); +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map); +el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header); void http_serve(el_val_t port, el_val_t handler); +void http_set_handler(el_val_t name); /* ── Filesystem ──────────────────────────────────────────────────────────── */ el_val_t fs_read(el_val_t path); el_val_t fs_write(el_val_t path, el_val_t content); +el_val_t fs_list(el_val_t path); /* ── JSON ────────────────────────────────────────────────────────────────── */ @@ -206,9 +237,33 @@ el_val_t dharma_peers(void); * network-wide across all connected CGI graphs. */ el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); -el_val_t engram_activate(el_val_t query, el_val_t depth); -void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +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); +el_val_t engram_get_node(el_val_t id); void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); + +/* ── LLM (Anthropic API client) ───────────────────────────────────────────── + * All functions call https://api.anthropic.com/v1/messages with the API key + * from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */ + +el_val_t llm_call(el_val_t model, el_val_t prompt); +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt); +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools); +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64); +el_val_t llm_models(void); /* ── args() ───────────────────────────────────────────────────────────────── * Provides access to command-line arguments passed to the program. diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index 4a4923e..6d0821c 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -108,8 +108,12 @@ fn cg_expr(expr: Map) -> String { } if kind == "Float" { + // Wrap Float literals in el_from_float() so the bit pattern is + // preserved through the el_val_t (int64) slot. Without this, + // implicit double→int64 conversion in C truncates `0.8` to `0` + // when passed to a builtin that expects el_val_t. let v: String = expr["value"] - return v + return "el_from_float(" + v + ")" } if kind == "Str" { diff --git a/elc-combined.el b/elc-combined.el index aae8375..7763947 100644 --- a/elc-combined.el +++ b/elc-combined.el @@ -1,5 +1,4 @@ // elc-combined.el — El self-hosting compiler, single-file bootstrap edition -// Inlines lexer + parser + codegen — CLI entry at end. // lexer.el — el self-hosting lexer // @@ -536,7 +535,6 @@ fn lex(source: String) -> [Map] { let tokens = native_list_append(tokens, make_tok("Eof", "")) tokens } - // parser.el — el self-hosting recursive descent parser // // Consumes the token list produced by lexer.el and builds a list of AST @@ -1320,7 +1318,6 @@ fn parse(tokens: [Map]) -> [Map] { } stmts } - // codegen.el — El compiler C source code generator // // Input: list of AST statement maps (from parser.el) @@ -1431,8 +1428,12 @@ fn cg_expr(expr: Map) -> String { } if kind == "Float" { + // Wrap Float literals in el_from_float() so the bit pattern is + // preserved through the el_val_t (int64) slot. Without this, + // implicit double→int64 conversion in C truncates `0.8` to `0` + // when passed to a builtin that expects el_val_t. let v: String = expr["value"] - return v + return "el_from_float(" + v + ")" } if kind == "Str" { @@ -2128,7 +2129,6 @@ fn codegen(stmts: [Map], source: String) -> String { // Return empty string — output was streamed via println "" } - // compiler.el — el self-hosting compiler pipeline // // Wires lexer -> parser -> codegen into a single compile() function.