Files
will.anderson 254cbe0ac2
El SDK CI - dev / build-and-test (pull_request) Successful in 3m22s
fix: add __-prefixed runtime primitives expected by El compiler
The El compiler generates calls to __-prefixed C primitives from within
El stdlib compiled code (e.g. __println, __str_len, __json_get, etc).
These were absent from el_runtime.c, causing linker failures when
building el-install, elb, or epm with the current compiler.

Add 46 __-prefixed aliases/implementations in el_runtime.c covering:
- I/O: __println, __print, __readline
- String: __str_len, __str_cmp, __str_ncmp, __str_alloc, __str_set_char,
  __str_concat_raw, __str_slice_raw, __str_char_at, plus numeric converters
- FS: __fs_read, __fs_write, __fs_exists, __fs_mkdir, __fs_list_raw, etc
- HTTP: __http_do, __http_do_map, __http_serve, __http_serve_v2,
  __http_response, __http_sse_* (weak stubs)
- JSON: __json_get, __json_set, __json_parse_map, __json_stringify_val, etc
- State, env, exec, uuid, sha256, args
2026-05-06 20:36:49 -05:00

1208 lines
44 KiB
C

/*
* el_seed.c — El language seed runtime: minimal C OS boundary
*
* This file exposes all OS-boundary primitives that El programs need, under
* the __ prefix convention. It is self-contained: all allocators, arena
* management, and el_request_start / el_request_end are defined here.
*
* Threading: __thread_create / __thread_join use dlsym(RTLD_DEFAULT) to look
* up El function symbols at runtime. This is the foundation of El's parallelism.
*
* Link: cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
* -o <out> <prog>.c el_seed.c
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "el_seed.h"
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <math.h>
#include <time.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
#include <dlfcn.h>
#include <curl/curl.h>
/* ── Private allocator ───────────────────────────────────────────────────── */
/*
* el_seed.c carries its own arena for per-request allocation tracking.
* The arena is reset at el_request_start / el_request_end, which are defined
* below and delegate to seed_request_start / seed_request_end.
*/
#define SEED_ARENA_INITIAL 512
typedef struct {
char** ptrs;
size_t count;
size_t cap;
} SeedArena;
static _Thread_local SeedArena _seed_arena = {NULL, 0, 0};
static _Thread_local int _seed_arena_on = 0;
static void seed_arena_track(char* p) {
if (!_seed_arena_on || !p) return;
if (_seed_arena.count >= _seed_arena.cap) {
size_t nc = _seed_arena.cap == 0 ? SEED_ARENA_INITIAL : _seed_arena.cap * 2;
char** g = realloc(_seed_arena.ptrs, nc * sizeof(char*));
if (!g) return;
_seed_arena.ptrs = g;
_seed_arena.cap = nc;
}
_seed_arena.ptrs[_seed_arena.count++] = p;
}
static void seed_request_start(void) {
_seed_arena.count = 0;
_seed_arena_on = 1;
}
static void seed_request_end(void) {
_seed_arena_on = 0;
for (size_t i = 0; i < _seed_arena.count; i++) free(_seed_arena.ptrs[i]);
_seed_arena.count = 0;
}
/* el_request_start / el_request_end — formerly defined in el_runtime.c.
* Now self-contained in el_seed.c, delegating to the seed arena. */
void el_request_start(void) { seed_request_start(); }
void el_request_end(void) { seed_request_end(); }
/* Persistent alloc — bypasses arena (state, engram internals). */
static char* seed_strdup_persist(const char* s) {
if (!s) return strdup("");
return strdup(s);
}
static char* seed_strdup(const char* s) {
char* p = strdup(s ? s : "");
seed_arena_track(p);
return p;
}
static char* seed_strbuf(size_t n) {
char* p = malloc(n + 1);
if (!p) { fputs("el_seed: out of memory\n", stderr); exit(1); }
p[0] = '\0';
seed_arena_track(p);
return p;
}
static el_val_t seed_wrap_str(char* s) { return EL_STR(s); }
/* ── String primitives ───────────────────────────────────────────────────── */
el_val_t __str_len(el_val_t s) {
const char* p = EL_CSTR(s);
if (!p) return 0;
return (el_val_t)strlen(p);
}
el_val_t __str_char_at(el_val_t s, el_val_t i) {
const char* p = EL_CSTR(s);
if (!p) return 0;
int64_t len = (int64_t)strlen(p);
int64_t idx = (int64_t)i;
if (idx < 0 || idx >= len) return 0;
return (el_val_t)(unsigned char)p[idx];
}
el_val_t __str_alloc(el_val_t n) {
int64_t sz = (int64_t)n;
if (sz < 0) sz = 0;
char* buf = seed_strbuf((size_t)sz);
memset(buf, 0, (size_t)sz + 1);
return seed_wrap_str(buf);
}
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) {
char* p = (char*)(uintptr_t)s;
if (!p) return s;
int64_t len = (int64_t)strlen(p);
int64_t idx = (int64_t)i;
if (idx < 0 || idx >= len) return s;
p[idx] = (char)(unsigned char)(int64_t)c;
return s;
}
el_val_t __str_cmp(el_val_t a, el_val_t b) {
const char* sa = EL_CSTR(a);
const char* sb = EL_CSTR(b);
if (!sa) sa = "";
if (!sb) sb = "";
return (el_val_t)strcmp(sa, sb);
}
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) {
const char* sa = EL_CSTR(a);
const char* sb = EL_CSTR(b);
if (!sa) sa = "";
if (!sb) sb = "";
return (el_val_t)strncmp(sa, sb, (size_t)(int64_t)n);
}
el_val_t __str_concat_raw(el_val_t a, el_val_t b) {
const char* sa = EL_CSTR(a);
const char* sb = EL_CSTR(b);
if (!sa) sa = "";
if (!sb) sb = "";
size_t la = strlen(sa), lb = strlen(sb);
char* out = seed_strbuf(la + lb);
memcpy(out, sa, la);
memcpy(out + la, sb, lb);
out[la + lb] = '\0';
return seed_wrap_str(out);
}
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) {
const char* p = EL_CSTR(s);
if (!p) return seed_wrap_str(seed_strdup(""));
int64_t len = (int64_t)strlen(p);
int64_t st = (int64_t)start;
int64_t en = (int64_t)end;
if (st < 0) st = 0;
if (en > len) en = len;
if (st >= en) return seed_wrap_str(seed_strdup(""));
int64_t sz = en - st;
char* out = seed_strbuf((size_t)sz);
memcpy(out, p + st, (size_t)sz);
out[sz] = '\0';
return seed_wrap_str(out);
}
el_val_t __int_to_str(el_val_t n) {
char buf[32];
snprintf(buf, sizeof(buf), "%lld", (long long)(int64_t)n);
return seed_wrap_str(seed_strdup(buf));
}
el_val_t __str_to_int(el_val_t s) {
const char* p = EL_CSTR(s);
if (!p) return 0;
return (el_val_t)atoll(p);
}
el_val_t __float_to_str(el_val_t f) {
char buf[64];
snprintf(buf, sizeof(buf), "%g", el_to_float(f));
return seed_wrap_str(seed_strdup(buf));
}
el_val_t __str_to_float(el_val_t s) {
const char* p = EL_CSTR(s);
if (!p) return el_from_float(0.0);
return el_from_float(strtod(p, NULL));
}
/* ── I/O ─────────────────────────────────────────────────────────────────── */
void __println(el_val_t s) {
const char* p = EL_CSTR(s);
puts(p ? p : "");
}
void __print(el_val_t s) {
const char* p = EL_CSTR(s);
if (p) fputs(p, stdout);
}
el_val_t __readline(void) {
char buf[4096];
if (!fgets(buf, sizeof(buf), stdin)) return seed_wrap_str(seed_strdup(""));
size_t len = strlen(buf);
if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0';
return seed_wrap_str(seed_strdup(buf));
}
/* ── Filesystem ──────────────────────────────────────────────────────────── */
el_val_t __fs_read(el_val_t path) {
const char* p = EL_CSTR(path);
if (!p) return seed_wrap_str(seed_strdup(""));
FILE* f = fopen(p, "rb");
if (!f) return seed_wrap_str(seed_strdup(""));
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
if (sz < 0) { fclose(f); return seed_wrap_str(seed_strdup("")); }
char* buf = seed_strbuf((size_t)sz);
size_t got = fread(buf, 1, (size_t)sz, f);
buf[got] = '\0';
fclose(f);
return seed_wrap_str(buf);
}
el_val_t __fs_write(el_val_t path, el_val_t content) {
const char* p = EL_CSTR(path);
const char* c = EL_CSTR(content);
if (!p || !c) return 0;
FILE* f = fopen(p, "wb");
if (!f) return 0;
size_t n = strlen(c);
size_t w = fwrite(c, 1, n, f);
fclose(f);
return w == n ? 1 : 0;
}
el_val_t __fs_exists(el_val_t path) {
const char* p = EL_CSTR(path);
if (!p || !*p) return 0;
struct stat st;
return (el_val_t)(stat(p, &st) == 0 ? 1 : 0);
}
el_val_t __fs_list_raw(el_val_t path) {
const char* p = EL_CSTR(path);
if (!p) return seed_wrap_str(seed_strdup(""));
DIR* d = opendir(p);
if (!d) return seed_wrap_str(seed_strdup(""));
/* Build newline-separated list of filenames. */
size_t cap = 4096, len = 0;
char* buf = malloc(cap);
if (!buf) { closedir(d); return seed_wrap_str(seed_strdup("")); }
buf[0] = '\0';
struct dirent* e;
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue;
size_t nlen = strlen(e->d_name);
while (len + nlen + 2 >= cap) {
cap *= 2;
char* g = realloc(buf, cap);
if (!g) { free(buf); closedir(d); return seed_wrap_str(seed_strdup("")); }
buf = g;
}
if (len > 0) buf[len++] = '\n';
memcpy(buf + len, e->d_name, nlen);
len += nlen;
buf[len] = '\0';
}
closedir(d);
seed_arena_track(buf);
return seed_wrap_str(buf);
}
el_val_t __fs_mkdir(el_val_t path) {
const char* p = EL_CSTR(path);
if (!p || !*p) return 0;
size_t n = strlen(p);
char* buf = malloc(n + 1);
if (!buf) return 0;
memcpy(buf, p, n + 1);
for (size_t i = 1; i <= n; i++) {
if (buf[i] == '/' || buf[i] == '\0') {
char saved = buf[i];
buf[i] = '\0';
if (buf[0] != '\0') {
if (mkdir(buf, 0755) != 0 && errno != EEXIST) {
struct stat st;
if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) {
free(buf); return 0;
}
}
}
buf[i] = saved;
}
}
free(buf);
return 1;
}
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) {
const char* p = EL_CSTR(path);
const char* b = EL_CSTR(bytes);
int64_t sz = (int64_t)n;
if (!p || !b || sz < 0) return 0;
FILE* f = fopen(p, "wb");
if (!f) return 0;
size_t written = (sz > 0) ? fwrite(b, 1, (size_t)sz, f) : 0;
int ok1 = (fflush(f) == 0);
int ok2 = (fclose(f) == 0);
if (!ok1 || !ok2 || written != (size_t)sz) { remove(p); return 0; }
return 1;
}
/* ── HTTP client ─────────────────────────────────────────────────────────── */
typedef struct {
char* data;
size_t len;
size_t cap;
} SeedHttpBuf;
static void seed_httpbuf_init(SeedHttpBuf* b) {
b->cap = 1024; b->len = 0;
b->data = malloc(b->cap);
if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); }
b->data[0] = '\0';
}
static void seed_httpbuf_append(SeedHttpBuf* b, const void* src, size_t n) {
while (b->len + n + 1 > b->cap) b->cap *= 2;
b->data = realloc(b->data, b->cap);
if (!b->data) { fputs("el_seed: 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 seed_http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) {
size_t n = size * nmemb;
seed_httpbuf_append((SeedHttpBuf*)ud, ptr, n);
return n;
}
/* Build a curl_slist from a JSON object string of header name:value pairs. */
static struct curl_slist* seed_headers_from_json(const char* hj) {
struct curl_slist* h = NULL;
if (!hj || !*hj) return NULL;
/* Walk key:value pairs at depth 1. Simple parser — same logic as json_find_key
* in el_runtime.c but adapted for building curl headers. */
const char* p = hj;
while (*p && *p != '{') p++;
if (*p == '{') p++;
while (*p) {
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++;
if (*p == '}' || *p == '\0') break;
if (*p != '"') break;
/* Parse key */
p++;
const char* ks = p;
while (*p && *p != '"') { if (*p == '\\') p++; p++; }
size_t klen = (size_t)(p - ks);
if (*p == '"') p++;
/* Skip : */
while (*p == ' ' || *p == ':') p++;
/* Parse value */
if (*p != '"') break;
p++;
const char* vs = p;
while (*p && *p != '"') { if (*p == '\\') p++; p++; }
size_t vlen = (size_t)(p - vs);
if (*p == '"') p++;
/* Build "Key: Value" header line */
size_t line_len = klen + 2 + vlen + 1;
char* line = malloc(line_len);
if (line) {
memcpy(line, ks, klen);
memcpy(line + klen, ": ", 2);
memcpy(line + klen + 2, vs, vlen);
line[klen + 2 + vlen] = '\0';
h = curl_slist_append(h, line);
free(line);
}
}
return h;
}
static el_val_t seed_http_error_json(const char* msg) {
if (!msg) msg = "unknown error";
size_t n = strlen(msg) * 6 + 20;
char* buf = seed_strbuf(n);
/* Simple escape: replace " with \" */
char* d = buf;
*d++ = '{'; *d++ = '"'; *d++ = 'e'; *d++ = 'r'; *d++ = 'r';
*d++ = 'o'; *d++ = 'r'; *d++ = '"'; *d++ = ':'; *d++ = '"';
for (const char* s = msg; *s; s++) {
if (*s == '"' || *s == '\\') *d++ = '\\';
*d++ = *s;
}
*d++ = '"'; *d++ = '}'; *d = '\0';
return seed_wrap_str(buf);
}
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_json, el_val_t timeout_ms) {
const char* m = EL_CSTR(method);
const char* u = EL_CSTR(url);
const char* b = EL_CSTR(body);
const char* hj = EL_CSTR(headers_json);
int64_t tms = (int64_t)timeout_ms;
if (tms <= 0) tms = 60000;
if (!u || !*u) return seed_http_error_json("empty url");
CURL* c = curl_easy_init();
if (!c) return seed_http_error_json("curl_easy_init failed");
SeedHttpBuf rb; seed_httpbuf_init(&rb);
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
curl_easy_setopt(c, CURLOPT_URL, u);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_write_cb);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, (long)tms);
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0");
struct curl_slist* hdrs = seed_headers_from_json(hj);
if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs);
if (m && strcmp(m, "POST") == 0) {
curl_easy_setopt(c, CURLOPT_POST, 1L);
curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : "");
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0));
} else if (m && strcmp(m, "PUT") == 0) {
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PUT");
if (b) {
curl_easy_setopt(c, CURLOPT_POSTFIELDS, b);
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b));
}
} else if (m && strcmp(m, "DELETE") == 0) {
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE");
} else if (m && strcmp(m, "PATCH") == 0) {
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH");
if (b) {
curl_easy_setopt(c, CURLOPT_POSTFIELDS, b);
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b));
}
}
/* GET is the default */
CURLcode rc = curl_easy_perform(c);
if (hdrs) curl_slist_free_all(hdrs);
curl_easy_cleanup(c);
if (rc != CURLE_OK) {
free(rb.data);
const char* em = errbuf[0] ? errbuf : curl_easy_strerror(rc);
return seed_http_error_json(em);
}
seed_arena_track(rb.data);
return seed_wrap_str(rb.data);
}
static size_t seed_http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) {
return fwrite(ptr, size, nmemb, (FILE*)ud);
}
el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_json, el_val_t out_path) {
const char* m = EL_CSTR(method);
const char* u = EL_CSTR(url);
const char* b = EL_CSTR(body);
const char* hj = EL_CSTR(headers_json);
const char* op = EL_CSTR(out_path);
if (!u || !*u || !op || !*op) return 0;
FILE* f = fopen(op, "wb");
if (!f) return 0;
CURL* c = curl_easy_init();
if (!c) { fclose(f); remove(op); return 0; }
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
curl_easy_setopt(c, CURLOPT_URL, u);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_file_write_cb);
curl_easy_setopt(c, CURLOPT_WRITEDATA, f);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 60000L);
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0");
struct curl_slist* hdrs = seed_headers_from_json(hj);
if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs);
if (m && strcmp(m, "POST") == 0) {
curl_easy_setopt(c, CURLOPT_POST, 1L);
curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : "");
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0));
}
CURLcode rc = curl_easy_perform(c);
if (hdrs) curl_slist_free_all(hdrs);
curl_easy_cleanup(c);
int ok1 = (fflush(f) == 0);
int ok2 = (fclose(f) == 0);
if (rc != CURLE_OK || !ok1 || !ok2) { remove(op); return 0; }
return 1;
}
/* ── HTTP server ─────────────────────────────────────────────────────────── */
/* Delegate to el_runtime.c's http_serve / http_serve_v2 via the existing
* http_set_handler mechanism. */
void __http_serve(el_val_t port, el_val_t handler_name) {
http_serve(port, handler_name);
}
void __http_serve_v2(el_val_t port, el_val_t handler_name) {
http_serve_v2(port, handler_name);
}
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
return http_response(status, headers_json, body);
}
/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */
/*
* Thread-local file descriptor stashed by http_worker_v2 before calling the
* El handler. El SSE builtins read this to get the raw socket fd.
*
* Lifecycle:
* http_worker_v2 sets _tl_http_conn_fd = fd (via el_seed_set_http_conn_fd)
* El handler calls __http_conn_fd() → receives that fd
* El handler calls __http_sse_open(fd) → sends SSE headers, keeps fd open
* El handler calls __http_sse_send(fd, data) → writes "data: ...\n\n"
* El handler calls __http_sse_close(fd) → closes the fd
* El handler returns "__sse__" sentinel → http_worker_v2 does NOT close fd
*
* The -1 value means no current connection (guard against misuse outside
* a handler context).
*/
static __thread int _tl_http_conn_fd = -1;
/* Called by el_runtime.c's http_worker_v2 — not part of the El ABI. */
void el_seed_set_http_conn_fd(int fd) {
_tl_http_conn_fd = fd;
}
/* __http_conn_fd() — returns the raw fd for the current HTTP connection.
* Valid only inside an http_serve_v2 handler before it returns. */
el_val_t __http_conn_fd(void) {
return EL_INT(_tl_http_conn_fd);
}
/* __http_sse_open(fd) — sends SSE response headers on fd, keeping it open.
* Returns 1 on success, 0 on write failure. */
el_val_t __http_sse_open(el_val_t conn_id) {
int fd = (int)(int64_t)conn_id;
if (fd < 0) return 0;
static const char sse_headers[] =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/event-stream\r\n"
"Cache-Control: no-cache\r\n"
"Connection: keep-alive\r\n"
"Access-Control-Allow-Origin: *\r\n"
"\r\n";
size_t n = sizeof(sse_headers) - 1; /* exclude NUL */
size_t sent = 0;
while (sent < n) {
ssize_t w = write(fd, sse_headers + sent, n - sent);
if (w <= 0) return 0;
sent += (size_t)w;
}
return 1;
}
/* __http_sse_send(fd, data) — writes one SSE event frame: "data: <data>\n\n".
* data must not contain newlines. Returns 1 on success, 0 on client disconnect. */
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) {
int fd = (int)(int64_t)conn_id;
if (fd < 0) return 0;
const char* s = EL_CSTR(data);
if (!s) s = "";
/* Build "data: <s>\n\n" in a single buffer for one write call. */
size_t prefix_len = 6; /* "data: " */
size_t slen = strlen(s);
size_t total = prefix_len + slen + 2; /* + "\n\n" */
char* buf = malloc(total + 1);
if (!buf) return 0;
memcpy(buf, "data: ", 6);
memcpy(buf + 6, s, slen);
buf[6 + slen] = '\n';
buf[6 + slen + 1] = '\n';
buf[total] = '\0';
size_t sent = 0;
int ok = 1;
while (sent < total) {
ssize_t w = write(fd, buf + sent, total - sent);
if (w <= 0) { ok = 0; break; }
sent += (size_t)w;
}
free(buf);
return ok ? 1 : 0;
}
/* __http_sse_close(fd) — closes the SSE connection fd. */
el_val_t __http_sse_close(el_val_t conn_id) {
int fd = (int)(int64_t)conn_id;
if (fd < 0) return 0;
close(fd);
return 1;
}
/* ── Threading ───────────────────────────────────────────────────────────── */
/*
* Design:
* Static ElThread table (max EL_SEED_MAX_THREADS entries).
* __thread_create: pick a free slot, store fn_name + arg, launch pthread.
* Worker: dlsym(RTLD_DEFAULT, fn_name) → call as el_val_t fn(el_val_t).
* Store result string in slot.result.
* __thread_join: pthread_join, return stored result string.
*
* Thread handle is the slot index (0..EL_SEED_MAX_THREADS-1).
* Returns -1 on failure (no slots, dlsym failure, pthread_create failure).
*
* Each slot is guarded by its own mutex so join/create on different handles
* never contend.
*/
#define EL_SEED_MAX_THREADS 64
typedef el_val_t (*ElFn1)(el_val_t);
typedef struct {
int in_use;
char* fn_name;
char* arg;
char* result;
pthread_t tid;
int done; /* set to 1 by worker before exit */
} ElThread;
static ElThread _el_threads[EL_SEED_MAX_THREADS];
static pthread_mutex_t _el_thread_mu = PTHREAD_MUTEX_INITIALIZER;
typedef struct {
int slot;
} ElThreadArg;
static void* el_thread_worker(void* raw) {
ElThreadArg* ta = (ElThreadArg*)raw;
int slot = ta->slot;
free(ta);
ElThread* t = &_el_threads[slot];
/* Resolve the El function symbol in the running binary. */
void* sym = dlsym(RTLD_DEFAULT, t->fn_name);
if (!sym) {
pthread_mutex_lock(&_el_thread_mu);
t->result = seed_strdup_persist("");
t->done = 1;
pthread_mutex_unlock(&_el_thread_mu);
return NULL;
}
ElFn1 fn = (ElFn1)sym;
/* Call the El function with the string argument. */
el_val_t arg_val = EL_STR(t->arg);
el_val_t ret = fn(arg_val);
/* Persist the result string. */
const char* rs = EL_CSTR(ret);
char* stored = seed_strdup_persist(rs ? rs : "");
pthread_mutex_lock(&_el_thread_mu);
t->result = stored;
t->done = 1;
pthread_mutex_unlock(&_el_thread_mu);
return NULL;
}
el_val_t __thread_create(el_val_t fn_name, el_val_t arg) {
const char* fname = EL_CSTR(fn_name);
const char* astr = EL_CSTR(arg);
if (!fname || !*fname) return (el_val_t)(int64_t)-1;
if (!astr) astr = "";
pthread_mutex_lock(&_el_thread_mu);
int slot = -1;
for (int i = 0; i < EL_SEED_MAX_THREADS; i++) {
if (!_el_threads[i].in_use) { slot = i; break; }
}
if (slot < 0) {
pthread_mutex_unlock(&_el_thread_mu);
return (el_val_t)(int64_t)-1;
}
ElThread* t = &_el_threads[slot];
t->in_use = 1;
t->done = 0;
t->fn_name = seed_strdup_persist(fname);
t->arg = seed_strdup_persist(astr);
t->result = NULL;
pthread_mutex_unlock(&_el_thread_mu);
ElThreadArg* ta = malloc(sizeof(ElThreadArg));
if (!ta) {
pthread_mutex_lock(&_el_thread_mu);
free(t->fn_name); free(t->arg);
t->in_use = 0;
pthread_mutex_unlock(&_el_thread_mu);
return (el_val_t)(int64_t)-1;
}
ta->slot = slot;
if (pthread_create(&t->tid, NULL, el_thread_worker, ta) != 0) {
free(ta);
pthread_mutex_lock(&_el_thread_mu);
free(t->fn_name); free(t->arg);
t->in_use = 0;
pthread_mutex_unlock(&_el_thread_mu);
return (el_val_t)(int64_t)-1;
}
return (el_val_t)(int64_t)slot;
}
el_val_t __thread_join(el_val_t tid) {
int64_t slot = (int64_t)tid;
if (slot < 0 || slot >= EL_SEED_MAX_THREADS) return seed_wrap_str(seed_strdup(""));
ElThread* t = &_el_threads[slot];
pthread_mutex_lock(&_el_thread_mu);
if (!t->in_use) {
pthread_mutex_unlock(&_el_thread_mu);
return seed_wrap_str(seed_strdup(""));
}
pthread_mutex_unlock(&_el_thread_mu);
/* Wait for thread to finish. */
pthread_join(t->tid, NULL);
pthread_mutex_lock(&_el_thread_mu);
char* res = t->result ? t->result : "";
char* copy = seed_strdup(res); /* arena-tracked for caller lifetime */
free(t->fn_name);
free(t->arg);
if (t->result) { free(t->result); t->result = NULL; }
t->fn_name = NULL; t->arg = NULL;
t->in_use = 0;
t->done = 0;
pthread_mutex_unlock(&_el_thread_mu);
return seed_wrap_str(copy);
}
/* ── Mutex pool ──────────────────────────────────────────────────────────── */
#define EL_SEED_MAX_MUTEXES 256
static pthread_mutex_t _el_mutexes[EL_SEED_MAX_MUTEXES];
static int _el_mutex_init[EL_SEED_MAX_MUTEXES];
static int _el_mutex_used[EL_SEED_MAX_MUTEXES];
static pthread_mutex_t _el_mutex_pool_mu = PTHREAD_MUTEX_INITIALIZER;
el_val_t __mutex_new(void) {
pthread_mutex_lock(&_el_mutex_pool_mu);
int slot = -1;
for (int i = 0; i < EL_SEED_MAX_MUTEXES; i++) {
if (!_el_mutex_used[i]) { slot = i; break; }
}
if (slot < 0) {
pthread_mutex_unlock(&_el_mutex_pool_mu);
return (el_val_t)(int64_t)-1;
}
_el_mutex_used[slot] = 1;
if (!_el_mutex_init[slot]) {
pthread_mutex_init(&_el_mutexes[slot], NULL);
_el_mutex_init[slot] = 1;
}
pthread_mutex_unlock(&_el_mutex_pool_mu);
return (el_val_t)(int64_t)slot;
}
void __mutex_lock(el_val_t m) {
int64_t slot = (int64_t)m;
if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return;
if (!_el_mutex_init[slot]) return;
pthread_mutex_lock(&_el_mutexes[slot]);
}
void __mutex_unlock(el_val_t m) {
int64_t slot = (int64_t)m;
if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return;
if (!_el_mutex_init[slot]) return;
pthread_mutex_unlock(&_el_mutexes[slot]);
}
/* ── Subprocess ──────────────────────────────────────────────────────────── */
el_val_t __exec(el_val_t cmd) {
const char* c = EL_CSTR(cmd);
if (!c) return seed_wrap_str(seed_strdup(""));
FILE* f = popen(c, "r");
if (!f) return seed_wrap_str(seed_strdup(""));
size_t cap = 4096, len = 0;
char* buf = malloc(cap);
if (!buf) { pclose(f); return seed_wrap_str(seed_strdup("")); }
char tmp[4096];
while (fgets(tmp, sizeof(tmp), f)) {
size_t n = strlen(tmp);
while (len + n + 1 > cap) {
cap *= 2;
char* g = realloc(buf, cap);
if (!g) { free(buf); pclose(f); return seed_wrap_str(seed_strdup("")); }
buf = g;
}
memcpy(buf + len, tmp, n);
len += n;
buf[len] = '\0';
}
pclose(f);
seed_arena_track(buf);
return seed_wrap_str(buf);
}
void __exec_bg(el_val_t cmd) {
const char* c = EL_CSTR(cmd);
if (!c) return;
/* Fork-free: run in background via system() with & appended. */
size_t n = strlen(c);
char* s = malloc(n + 4);
if (!s) return;
memcpy(s, c, n);
memcpy(s + n, " &", 3);
system(s);
free(s);
}
/* ── Environment and process ─────────────────────────────────────────────── */
el_val_t __env_get(el_val_t key) {
const char* k = EL_CSTR(key);
if (!k) return seed_wrap_str(seed_strdup(""));
const char* v = getenv(k);
return seed_wrap_str(seed_strdup(v ? v : ""));
}
void __exit_program(el_val_t code) {
exit((int)(int64_t)code);
}
/* ── args_json ────────────────────────────────────────────────────────────── */
static int _seed_argc = 0;
static char** _seed_argv = NULL;
void el_seed_init_args(int argc, char** argv) {
_seed_argc = argc;
_seed_argv = argv;
}
el_val_t __args_json(void) {
/* Return ["arg1","arg2",...] as a JSON string. Skip argv[0] (program name). */
size_t cap = 256, len = 0;
char* buf = malloc(cap);
if (!buf) return seed_wrap_str(seed_strdup("[]"));
buf[len++] = '[';
int first = 1;
for (int i = 1; i < _seed_argc; i++) {
const char* a = _seed_argv[i];
/* Estimate: each arg needs at most strlen*6+4 bytes (worst case escape) */
size_t need = strlen(a) * 6 + 8;
while (len + need + 2 > cap) {
cap *= 2;
char* g = realloc(buf, cap);
if (!g) { free(buf); return seed_wrap_str(seed_strdup("[]")); }
buf = g;
}
if (!first) buf[len++] = ',';
first = 0;
buf[len++] = '"';
for (const char* p = a; *p; p++) {
unsigned char c = (unsigned char)*p;
if (c == '"') { buf[len++] = '\\'; buf[len++] = '"'; }
else if (c == '\\') { buf[len++] = '\\'; buf[len++] = '\\'; }
else if (c == '\n') { buf[len++] = '\\'; buf[len++] = 'n'; }
else if (c == '\r') { buf[len++] = '\\'; buf[len++] = 'r'; }
else if (c == '\t') { buf[len++] = '\\'; buf[len++] = 't'; }
else buf[len++] = (char)c;
}
buf[len++] = '"';
}
buf[len++] = ']';
buf[len] = '\0';
seed_arena_track(buf);
return seed_wrap_str(buf);
}
/* ── Time ────────────────────────────────────────────────────────────────── */
el_val_t __time_now_ns(void) {
struct timespec ts;
if (clock_gettime(CLOCK_REALTIME, &ts) == 0) {
int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec;
return (el_val_t)ns;
}
struct timeval tv;
gettimeofday(&tv, NULL);
return (el_val_t)((int64_t)tv.tv_sec * 1000000000LL + (int64_t)tv.tv_usec * 1000LL);
}
void __sleep_ms(el_val_t ms) {
int64_t m = (int64_t)ms;
if (m < 0) m = 0;
struct timespec ts;
ts.tv_sec = (time_t)(m / 1000LL);
ts.tv_nsec = (long)((m % 1000LL) * 1000000LL);
nanosleep(&ts, NULL);
}
/* ── UUID ────────────────────────────────────────────────────────────────── */
static int _seed_uuid_seeded = 0;
el_val_t __uuid_v4(void) {
if (!_seed_uuid_seeded) {
srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_seed_uuid_seeded);
_seed_uuid_seeded = 1;
}
unsigned char b[16];
for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff);
b[6] = (b[6] & 0x0f) | 0x40; /* version 4 */
b[8] = (b[8] & 0x3f) | 0x80; /* RFC 4122 variant */
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 seed_wrap_str(seed_strdup(buf));
}
/* ── Math ────────────────────────────────────────────────────────────────── */
el_val_t __sqrt_f(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); }
el_val_t __log_f(el_val_t f) { return el_from_float(log10(el_to_float(f))); }
el_val_t __ln_f(el_val_t f) { return el_from_float(log(el_to_float(f))); }
el_val_t __sin_f(el_val_t f) { return el_from_float(sin(el_to_float(f))); }
el_val_t __cos_f(el_val_t f) { return el_from_float(cos(el_to_float(f))); }
el_val_t __pi_f(void) { return el_from_float(3.14159265358979323846); }
/* ── JSON — thin wrappers around el_runtime.c implementations ────────────── */
el_val_t __json_get(el_val_t json, el_val_t key) { return json_get(json, key); }
el_val_t __json_get_raw(el_val_t json_str, el_val_t key) { return json_get_raw(json_str, key); }
el_val_t __json_parse(el_val_t s) { return json_parse(s); }
el_val_t __json_stringify(el_val_t v) { return json_stringify(v); }
el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); }
el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); }
el_val_t __json_array_len(el_val_t json_str) { return json_array_len(json_str); }
el_val_t __json_array_get(el_val_t json_str, el_val_t index) { return json_array_get(json_str, index); }
el_val_t __json_array_get_string(el_val_t json_str, el_val_t index) { return json_array_get_string(json_str, index); }
el_val_t __json_get_string(el_val_t json_str, el_val_t key) { return json_get_string(json_str, key); }
el_val_t __json_get_int(el_val_t json_str, el_val_t key) { return json_get_int(json_str, key); }
el_val_t __json_get_float(el_val_t json_str, el_val_t key) { return json_get_float(json_str, key); }
el_val_t __json_get_bool(el_val_t json_str, el_val_t key) { return json_get_bool(json_str, key); }
el_val_t __json_set(el_val_t json_str, el_val_t key, el_val_t value) { return json_set(json_str, key, value); }
/* ── State K/V — thin wrappers ───────────────────────────────────────────── */
el_val_t __state_set(el_val_t key, el_val_t value) { return state_set(key, value); }
el_val_t __state_get(el_val_t key) { return state_get(key); }
el_val_t __state_del(el_val_t key) { return state_del(key); }
el_val_t __state_keys(void) { return state_keys(); }
/* ── HTML/URL — thin wrappers ────────────────────────────────────────────── */
el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json) {
return el_html_sanitize(input_html, allowlist_json);
}
el_val_t __url_encode(el_val_t s) { return url_encode(s); }
el_val_t __url_decode(el_val_t s) { return url_decode(s); }
/* ── Engram — thin wrappers ──────────────────────────────────────────────── */
el_val_t __engram_node(el_val_t content, el_val_t node_type, el_val_t salience) {
return engram_node(content, node_type, salience);
}
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) {
return engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags);
}
el_val_t __engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
el_val_t salience, el_val_t certainty, el_val_t confidence,
el_val_t status, el_val_t tags, el_val_t layer_id) {
return engram_node_layered(content, node_type, label, salience, certainty, confidence,
status, tags, layer_id);
}
el_val_t __engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
el_val_t transparent, el_val_t injectable) {
return engram_add_layer(name, priority, suppressible, transparent, injectable);
}
el_val_t __engram_remove_layer(el_val_t layer_id) { return engram_remove_layer(layer_id); }
el_val_t __engram_list_layers(void) { return engram_list_layers(); }
el_val_t __engram_get_node(el_val_t id) { return engram_get_node(id); }
void __engram_strengthen(el_val_t node_id) { engram_strengthen(node_id); }
void __engram_forget(el_val_t node_id) { engram_forget(node_id); }
el_val_t __engram_node_count(void) { return engram_node_count(); }
el_val_t __engram_search(el_val_t query, el_val_t limit) { return engram_search(query, limit); }
el_val_t __engram_scan_nodes(el_val_t limit, el_val_t offset) { return engram_scan_nodes(limit, offset); }
void __engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) {
engram_connect(from_id, to_id, weight, relation);
}
el_val_t __engram_edge_between(el_val_t from_id, el_val_t to_id) {
return engram_edge_between(from_id, to_id);
}
el_val_t __engram_neighbors(el_val_t node_id) { return engram_neighbors(node_id); }
el_val_t __engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) {
return engram_neighbors_filtered(node_id, max_depth, direction);
}
el_val_t __engram_edge_count(void) { return engram_edge_count(); }
el_val_t __engram_activate(el_val_t query, el_val_t depth) { return engram_activate(query, depth); }
el_val_t __engram_save(el_val_t path) { return engram_save(path); }
el_val_t __engram_load(el_val_t path) { return engram_load(path); }
el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); }
el_val_t __engram_search_json(el_val_t query, el_val_t limit) {
return engram_search_json(query, limit);
}
el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset) {
return engram_scan_nodes_json(limit, offset);
}
el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset) {
return engram_scan_nodes_by_type_json(node_type, limit, offset);
}
el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) {
return engram_neighbors_json(node_id, max_depth, direction);
}
el_val_t __engram_activate_json(el_val_t query, el_val_t depth) {
return engram_activate_json(query, depth);
}
el_val_t __engram_stats_json(void) { return engram_stats_json(); }
el_val_t __engram_list_layers_json(void) { return engram_list_layers_json(); }
el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth) {
return engram_compile_layered_json(intent, depth);
}
/* ── Cryptographic hashing ────────────────────────────────────────────────── */
/*
* SHA-256 — self-contained implementation (no OpenSSL dependency).
* Based on Brad Conte's public-domain reference implementation.
*/
typedef struct {
uint8_t data[64];
uint32_t datalen;
uint64_t bitlen;
uint32_t state[8];
} _seed_sha256_ctx;
static const uint32_t _seed_sha256_k[64] = {
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
};
#define _SEED_ROTR32(x,n) (((x)>>(n))|((x)<<(32-(n))))
#define _SEED_CH(x,y,z) (((x)&(y))^(~(x)&(z)))
#define _SEED_MAJ(x,y,z) (((x)&(y))^((x)&(z))^((y)&(z)))
#define _SEED_EP0(x) (_SEED_ROTR32(x,2)^_SEED_ROTR32(x,13)^_SEED_ROTR32(x,22))
#define _SEED_EP1(x) (_SEED_ROTR32(x,6)^_SEED_ROTR32(x,11)^_SEED_ROTR32(x,25))
#define _SEED_SIG0(x) (_SEED_ROTR32(x,7)^_SEED_ROTR32(x,18)^((x)>>3))
#define _SEED_SIG1(x) (_SEED_ROTR32(x,17)^_SEED_ROTR32(x,19)^((x)>>10))
static void _seed_sha256_transform(_seed_sha256_ctx* ctx, const uint8_t* data) {
uint32_t a,b,c,d,e,f,g,h,t1,t2,m[64];
for (int i=0,j=0; i<16; i++,j+=4)
m[i]=(uint32_t)(data[j]<<24)|(data[j+1]<<16)|(data[j+2]<<8)|data[j+3];
for (int i=16; i<64; i++)
m[i]=_SEED_SIG1(m[i-2])+m[i-7]+_SEED_SIG0(m[i-15])+m[i-16];
a=ctx->state[0]; b=ctx->state[1]; c=ctx->state[2]; d=ctx->state[3];
e=ctx->state[4]; f=ctx->state[5]; g=ctx->state[6]; h=ctx->state[7];
for (int i=0; i<64; i++) {
t1=h+_SEED_EP1(e)+_SEED_CH(e,f,g)+_seed_sha256_k[i]+m[i];
t2=_SEED_EP0(a)+_SEED_MAJ(a,b,c);
h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2;
}
ctx->state[0]+=a; ctx->state[1]+=b; ctx->state[2]+=c; ctx->state[3]+=d;
ctx->state[4]+=e; ctx->state[5]+=f; ctx->state[6]+=g; ctx->state[7]+=h;
}
static void _seed_sha256_init(_seed_sha256_ctx* ctx) {
ctx->datalen=0; ctx->bitlen=0;
ctx->state[0]=0x6a09e667; ctx->state[1]=0xbb67ae85;
ctx->state[2]=0x3c6ef372; ctx->state[3]=0xa54ff53a;
ctx->state[4]=0x510e527f; ctx->state[5]=0x9b05688c;
ctx->state[6]=0x1f83d9ab; ctx->state[7]=0x5be0cd19;
}
static void _seed_sha256_update(_seed_sha256_ctx* ctx, const uint8_t* data, size_t len) {
for (size_t i=0; i<len; i++) {
ctx->data[ctx->datalen++] = data[i];
if (ctx->datalen==64) { _seed_sha256_transform(ctx,ctx->data); ctx->bitlen+=512; ctx->datalen=0; }
}
}
static void _seed_sha256_final(_seed_sha256_ctx* ctx, uint8_t hash[32]) {
uint32_t i=ctx->datalen;
ctx->data[i++]=0x80;
if (ctx->datalen<56) { while(i<56) ctx->data[i++]=0; }
else { while(i<64) ctx->data[i++]=0; _seed_sha256_transform(ctx,ctx->data); memset(ctx->data,0,56); }
ctx->bitlen+=ctx->datalen*8;
ctx->data[63]=(uint8_t)(ctx->bitlen); ctx->data[62]=(uint8_t)(ctx->bitlen>>8);
ctx->data[61]=(uint8_t)(ctx->bitlen>>16); ctx->data[60]=(uint8_t)(ctx->bitlen>>24);
ctx->data[59]=(uint8_t)(ctx->bitlen>>32); ctx->data[58]=(uint8_t)(ctx->bitlen>>40);
ctx->data[57]=(uint8_t)(ctx->bitlen>>48); ctx->data[56]=(uint8_t)(ctx->bitlen>>56);
_seed_sha256_transform(ctx,ctx->data);
for (i=0; i<4; i++) {
hash[i] =(uint8_t)(ctx->state[0]>>(24-i*8));
hash[i+4] =(uint8_t)(ctx->state[1]>>(24-i*8));
hash[i+8] =(uint8_t)(ctx->state[2]>>(24-i*8));
hash[i+12] =(uint8_t)(ctx->state[3]>>(24-i*8));
hash[i+16] =(uint8_t)(ctx->state[4]>>(24-i*8));
hash[i+20] =(uint8_t)(ctx->state[5]>>(24-i*8));
hash[i+24] =(uint8_t)(ctx->state[6]>>(24-i*8));
hash[i+28] =(uint8_t)(ctx->state[7]>>(24-i*8));
}
}
el_val_t __sha256_hex(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) s = "";
_seed_sha256_ctx ctx;
_seed_sha256_init(&ctx);
_seed_sha256_update(&ctx, (const uint8_t*)s, strlen(s));
uint8_t digest[32];
_seed_sha256_final(&ctx, digest);
static const char hex[] = "0123456789abcdef";
char* out = malloc(65);
if (!out) return EL_STR("");
for (int i=0; i<32; i++) {
out[i*2] = hex[(digest[i]>>4)&0xf];
out[i*2+1] = hex[digest[i]&0xf];
}
out[64] = '\0';
return EL_STR(out);
}