Files
el/lang/runtime/el_seed.c
T
Neuron 5503e1d9a4 organ: el gets a speaker, and fetches the voice from the engram
El could turn meaning into samples and could not make a sound. Every path
from those samples to the air ran outside the language, through a 939-line
Swift program that shelled out to afplay, so the voice was not a capability
of El or of Neuron but a separate binary standing next to them.

Two things land here.

The speaker. el_audio_darwin.m is a CoreAudio AudioQueue realizer in its own
translation unit, declared in el_runtime.h, deliberately not a patch to
el_runtime.c — acquiring a device must not mean editing the middle of the
language, the same rule the realizer registry follows for modalities. It
takes samples straight out of memory, so nothing is written to disk and no
process is spawned between the intent to speak and the sound. The async half
(play/stop/playing/played_frames) exists because barge-in means stopping on
the spot, and a blocking play cannot be interrupted. el_peripheral_null.c is
the same entry points everywhere else, so El that speaks links anywhere and
truthfully reports having no speaker.

The voice. organ_voice_fetch asks the engram for a voice region by query and
reads the geometry off the node that comes back. A voice is not a JSON file
next to the code; it is a memory, and the organ retrieves it the way anything
retrieves a memory. An absent region returns empty rather than a plausible
default, because a caller must be able to tell 'this is how they sound' from
'I never heard them'.

Underneath both: __str_set_char bounds-checked writes against strlen(), which
is 0 for the zero-filled buffer __str_alloc hands back, so every write was
rejected and every El-authored WAV in this repo was 55,244 bytes of silence
that reported ok=true. Byte buffers now carry their capacity in a side table;
text keeps the exact strlen behaviour it had. This is why nobody noticed El
was mute.

Measured: voice fetched from the engram reads f0=137 f0_end=116 kf=1269
f1=500 f2=2093 f3=3531, matching the 30s LPC measurement; render is 20160
samples at 16 kHz; both the rendered utterance and an own-core tone played
aloud through CoreAudio with no Swift and no afplay in the chain.
2026-08-16 16:27:30 -05:00

1668 lines
63 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 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>
/* el_runtime.c bridge prototypes.
*
* A block of __-prefixed wrappers further down in this file (http serving,
* JSON access, key-val state, URL/HTML escaping, and the whole engram_*
* node/edge/layer/search surface -- 51 symbols in total) delegate to
* unprefixed counterparts that are implemented in el_runtime.c, not here.
* Porting them into native el_seed.c or El has not happened yet.
* tools/install.sh compiles el_seed.c and el_runtime.c as separate objects
* and archives both into libel.a, so the symbols are always present at link
* time. el_seed.c alone was just missing the prototypes, which made even a
* standalone -c compile of this one file fail on a toolchain that now treats
* an implicit function declaration as a hard error under C11.
*
* A plain include of el_runtime.h was tried first and rejected: it redefines
* el_to_float and el_from_float, which el_seed.h already provides. Narrow
* prototypes, copied verbatim from el_runtime.h, avoid that collision without
* pulling in the rest of the retiring runtime header.
*/
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
void http_serve(el_val_t port, el_val_t handler);
void http_serve_v2(el_val_t port, el_val_t handler);
el_val_t json_get(el_val_t json, el_val_t key);
el_val_t json_get_string(el_val_t json_str, el_val_t key);
el_val_t json_get_int(el_val_t json_str, el_val_t key);
el_val_t json_get_float(el_val_t json_str, el_val_t key);
el_val_t json_get_bool(el_val_t json_str, el_val_t key);
el_val_t json_get_raw(el_val_t json_str, el_val_t key);
el_val_t json_parse(el_val_t s);
el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
el_val_t json_stringify(el_val_t v);
el_val_t json_array_len(el_val_t json_str);
el_val_t json_array_get(el_val_t json_str, el_val_t index);
el_val_t json_array_get_string(el_val_t json_str, el_val_t index);
el_val_t state_set(el_val_t key, el_val_t value);
el_val_t state_get(el_val_t key);
el_val_t state_del(el_val_t key);
el_val_t state_keys(void);
el_val_t url_encode(el_val_t s);
el_val_t url_decode(el_val_t s);
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t 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);
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);
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);
el_val_t engram_remove_layer(el_val_t layer_id);
el_val_t engram_list_layers(void);
el_val_t engram_list_layers_json(void);
el_val_t engram_get_node(el_val_t id);
el_val_t engram_get_node_json(el_val_t id);
el_val_t engram_get_node_by_label(el_val_t label);
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_edge_count(void);
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
el_val_t engram_search(el_val_t query, el_val_t limit);
el_val_t engram_search_json(el_val_t query, el_val_t limit);
el_val_t engram_activate(el_val_t query, el_val_t depth);
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
el_val_t engram_stats_json(void);
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_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
el_val_t engram_load(el_val_t path);
el_val_t engram_save(el_val_t path);
/* ── 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;
}
/* Defined in el_runtime.c. The string-length cache there keys on pointer +
* generation; anything that frees or mutates a runtime string must bump the
* generation or a reused address could return a stale length. Weak so this
* file still links on its own. */
__attribute__((weak)) void el_str_cache_flush(void);
/* Byte-buffer capacity registry (defined below, next to the string
* primitives). The arena frees the pointers it tracked, so any capacity
* entry for those addresses must go with them — otherwise a later malloc
* reusing the address would inherit a stale width. */
static void seed_cap_drop(const char* p);
static void seed_request_end(void) {
_seed_arena_on = 0;
for (size_t i = 0; i < _seed_arena.count; i++) {
seed_cap_drop(_seed_arena.ptrs[i]);
free(_seed_arena.ptrs[i]);
}
_seed_arena.count = 0;
if (el_str_cache_flush) el_str_cache_flush(); /* freed pointers may be reused */
}
/* 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); }
/* ── Byte-buffer capacity registry ────────────────────────────────────────────
* A String produced by __str_alloc is a fixed-size BYTE BUFFER, not text. Its
* length is the capacity it was asked for; strlen() is meaningless on it,
* because the buffer is zero-filled and binary content (PCM audio, RIFF
* headers, image rasters) contains NUL bytes by nature.
*
* Before this registry existed, __str_set_char bounds-checked the write index
* against strlen(p). For a freshly __str_alloc'd buffer strlen(p) == 0, so the
* check `idx >= len` rejected EVERY index and the function was a total no-op:
* every El program that built bytes this way wrote a file of pure zeros and
* still saw a success return. That is why El's own-core WAV writer emitted
* 55,244 silent bytes with a correct-looking header length and no header.
*
* The fix cannot be "trust the index", because that removes the bound. It also
* cannot be a length header stored behind the pointer, because __str_set_char
* accepts any String — including a string literal in .rodata, where reading the
* bytes preceding the pointer is undefined and may fault. So capacity is kept
* in a side table keyed by the pointer itself: allocation registers, the arena
* sweep unregisters, and anything not registered keeps the exact strlen
* behaviour it had before. Text semantics are unchanged; byte buffers gain the
* bound they always should have had. */
typedef struct {
char* ptr; /* NULL = empty slot, (char*)1 = tombstone */
size_t cap;
} SeedCapEntry;
#define SEED_CAP_TOMB ((char*)1)
static _Thread_local SeedCapEntry* _seed_cap = NULL;
static _Thread_local size_t _seed_cap_mask = 0; /* table size - 1 */
static _Thread_local size_t _seed_cap_used = 0; /* live + tombstoned */
static size_t seed_cap_hash(const char* p) {
uintptr_t h = (uintptr_t)p >> 4; /* malloc alignment: low bits are dead */
h *= (uintptr_t)0x9E3779B97F4A7C15ull;
return (size_t)(h >> 32);
}
static void seed_cap_put(char* p, size_t cap);
static void seed_cap_grow(void) {
size_t old_size = _seed_cap_mask ? _seed_cap_mask + 1 : 0;
SeedCapEntry* old = _seed_cap;
size_t new_size = old_size ? old_size * 2 : 256;
SeedCapEntry* fresh = calloc(new_size, sizeof(SeedCapEntry));
if (!fresh) return; /* out of memory: keep old table */
_seed_cap = fresh;
_seed_cap_mask = new_size - 1;
_seed_cap_used = 0;
for (size_t i = 0; i < old_size; i++) {
if (old[i].ptr && old[i].ptr != SEED_CAP_TOMB) seed_cap_put(old[i].ptr, old[i].cap);
}
free(old);
}
static void seed_cap_put(char* p, size_t cap) {
if (!p) return;
if (!_seed_cap || (_seed_cap_used + 1) * 4 >= (_seed_cap_mask + 1) * 3) {
seed_cap_grow();
if (!_seed_cap) return;
}
size_t i = seed_cap_hash(p) & _seed_cap_mask;
size_t first_free = (size_t)-1;
for (;;) {
char* e = _seed_cap[i].ptr;
if (e == p) { _seed_cap[i].cap = cap; return; } /* address reused */
if (e == SEED_CAP_TOMB && first_free == (size_t)-1) first_free = i;
if (!e) {
if (first_free != (size_t)-1) i = first_free; else _seed_cap_used++;
_seed_cap[i].ptr = p;
_seed_cap[i].cap = cap;
return;
}
i = (i + 1) & _seed_cap_mask;
}
}
/* Capacity of a registered byte buffer, or -1 when the pointer is not one. */
static int64_t seed_cap_get(const char* p) {
if (!p || !_seed_cap) return -1;
size_t i = seed_cap_hash(p) & _seed_cap_mask;
for (;;) {
char* e = _seed_cap[i].ptr;
if (!e) return -1;
if (e == (char*)p) return (int64_t)_seed_cap[i].cap;
i = (i + 1) & _seed_cap_mask;
}
}
static void seed_cap_drop(const char* p) {
if (!p || !_seed_cap) return;
size_t i = seed_cap_hash(p) & _seed_cap_mask;
for (;;) {
char* e = _seed_cap[i].ptr;
if (!e) return;
if (e == (char*)p) { _seed_cap[i].ptr = SEED_CAP_TOMB; return; }
i = (i + 1) & _seed_cap_mask;
}
}
/* Effective addressable length of a String: its buffer capacity when it is a
* byte buffer, otherwise strlen. */
static int64_t seed_addressable_len(const char* p) {
int64_t cap = seed_cap_get(p);
return cap >= 0 ? cap : (int64_t)strlen(p);
}
/* ── 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 = seed_addressable_len(p); /* capacity for byte buffers */
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);
seed_cap_put(buf, (size_t)sz); /* this is a byte buffer of width sz */
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 = seed_addressable_len(p); /* capacity for byte buffers */
int64_t idx = (int64_t)i;
if (idx < 0 || idx >= len) return s;
p[idx] = (char)(unsigned char)(int64_t)c;
if (el_str_cache_flush) el_str_cache_flush(); /* in-place write can move the NUL */
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;
}
/* stderr counterpart of println. Flushed immediately: a disclosure line is only
* worth anything if it lands before the thing it discloses happens. */
void eprintln(el_val_t s) {
const char* p = EL_CSTR(s);
fputs(p ? p : "", stderr);
fputc('\n', stderr);
fflush(stderr);
}
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]);
}
/* ── Channels ─────────────────────────────────────────────────────────────── *
* Buffered MPMC channel backed by a mutex + condvar + circular buffer.
* Ported from the pre-restructure el_runtime.c (b2aac4b) — runtime/channel.el
* has always called these five primitives, but they were never carried
* forward into el_seed.c when el_runtime.c was consolidated onto the
* canonical release copy. Native channels were silently unlinkable on dev
* until this port.
*
* __channel_new(capacity) -> Int (handle)
* __channel_send(ch, msg) — blocks if full (capacity > 0) or never (unbounded)
* __channel_recv(ch) -> String — blocks until a message is available
* __channel_try_recv(ch) -> String — non-blocking, returns "" if empty
* __channel_close(ch) — signal no more sends; recv drains remaining
*
* Bounded channels (cap > 0): circular buffer, sender blocks when full.
* Unbounded channels (cap == 0): dynamic array, sender never blocks.
*/
#define EL_CHANNEL_MAX 64
#define EL_CHANNEL_BUF 1024
typedef struct {
char** buf;
int cap; /* 0 = unbounded (grows dynamically) */
int head, tail, count;
int dyn_cap; /* allocated slots for unbounded mode */
int closed;
pthread_mutex_t mu;
pthread_cond_t not_empty;
pthread_cond_t not_full;
} ElChannel;
static ElChannel _channels[EL_CHANNEL_MAX];
static int _channel_count = 0;
static pthread_mutex_t _channel_alloc_mu = PTHREAD_MUTEX_INITIALIZER;
el_val_t __channel_new(el_val_t capacity_v) {
int cap = (int)(int64_t)capacity_v;
if (cap < 0) cap = 0;
pthread_mutex_lock(&_channel_alloc_mu);
if (_channel_count >= EL_CHANNEL_MAX) {
pthread_mutex_unlock(&_channel_alloc_mu);
fprintf(stderr, "[__channel_new] channel table full\n");
return EL_INT(-1);
}
int slot = _channel_count++;
pthread_mutex_unlock(&_channel_alloc_mu);
ElChannel* ch = &_channels[slot];
memset(ch, 0, sizeof(*ch));
ch->cap = cap;
ch->closed = 0;
ch->head = 0;
ch->tail = 0;
ch->count = 0;
if (cap > 0) {
/* Bounded: fixed circular buffer. */
ch->buf = (char**)malloc((size_t)cap * sizeof(char*));
ch->dyn_cap = cap;
} else {
/* Unbounded: start with EL_CHANNEL_BUF slots, grow as needed. */
ch->buf = (char**)malloc(EL_CHANNEL_BUF * sizeof(char*));
ch->dyn_cap = EL_CHANNEL_BUF;
}
if (!ch->buf) {
fprintf(stderr, "[__channel_new] out of memory\n");
return EL_INT(-1);
}
pthread_mutex_init(&ch->mu, NULL);
pthread_cond_init(&ch->not_empty, NULL);
pthread_cond_init(&ch->not_full, NULL);
return EL_INT(slot);
}
el_val_t __channel_send(el_val_t ch_v, el_val_t msg_v) {
int slot = (int)(int64_t)ch_v;
if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR("");
ElChannel* ch = &_channels[slot];
const char* msg = EL_CSTR(msg_v);
if (!msg) msg = "";
char* copy = strdup(msg); /* channel owns the string */
pthread_mutex_lock(&ch->mu);
if (ch->closed) {
/* Send on closed channel is a no-op (drop the message). */
pthread_mutex_unlock(&ch->mu);
free(copy);
return EL_STR("");
}
if (ch->cap > 0) {
/* Bounded: block while full. */
while (ch->count >= ch->cap && !ch->closed) {
pthread_cond_wait(&ch->not_full, &ch->mu);
}
if (ch->closed) {
pthread_mutex_unlock(&ch->mu);
free(copy);
return EL_STR("");
}
ch->buf[ch->tail] = copy;
ch->tail = (ch->tail + 1) % ch->cap;
ch->count++;
} else {
/* Unbounded: grow the buffer if needed. */
if (ch->count >= ch->dyn_cap) {
int new_cap = ch->dyn_cap * 2;
char** grown = (char**)realloc(ch->buf, (size_t)new_cap * sizeof(char*));
if (!grown) {
pthread_mutex_unlock(&ch->mu);
free(copy);
fprintf(stderr, "[__channel_send] out of memory growing channel\n");
return EL_STR("");
}
/* The circular buffer may have wrapped. Linearise it first.
* In unbounded mode head is always 0 (we append at tail, drain
* from head), so a simple memmove isn't needed — but if the
* buffer did wrap (tail < head after growth), we need to fix up.
* Simplest safe path: if tail wrapped, move the head..old_cap
* segment to new_cap..new_cap+(old_cap-head). */
if (ch->tail < ch->head) {
/* Wrapped: [head..old_cap) is the front, [0..tail) is the back. */
int front = ch->dyn_cap - ch->head;
memmove(grown + ch->dyn_cap, grown + ch->head, (size_t)front * sizeof(char*));
ch->head = ch->dyn_cap;
}
ch->buf = grown;
ch->dyn_cap = new_cap;
}
ch->buf[ch->tail] = copy;
ch->tail = (ch->tail + 1) % ch->dyn_cap;
ch->count++;
}
pthread_cond_signal(&ch->not_empty);
pthread_mutex_unlock(&ch->mu);
return EL_STR("");
}
el_val_t __channel_recv(el_val_t ch_v) {
int slot = (int)(int64_t)ch_v;
if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR("");
ElChannel* ch = &_channels[slot];
pthread_mutex_lock(&ch->mu);
/* Block until there is a message or the channel is closed and drained. */
while (ch->count == 0 && !ch->closed) {
pthread_cond_wait(&ch->not_empty, &ch->mu);
}
if (ch->count == 0) {
/* Closed and empty — signal EOF. */
pthread_mutex_unlock(&ch->mu);
return EL_STR("");
}
int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap;
char* msg = ch->buf[ch->head];
ch->head = (ch->head + 1) % buf_cap;
ch->count--;
pthread_cond_signal(&ch->not_full);
pthread_mutex_unlock(&ch->mu);
/* Hand the string to the arena so it is freed after the request. */
seed_arena_track(msg);
return EL_STR(msg);
}
el_val_t __channel_try_recv(el_val_t ch_v) {
int slot = (int)(int64_t)ch_v;
if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR("");
ElChannel* ch = &_channels[slot];
pthread_mutex_lock(&ch->mu);
if (ch->count == 0) {
pthread_mutex_unlock(&ch->mu);
return EL_STR("");
}
int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap;
char* msg = ch->buf[ch->head];
ch->head = (ch->head + 1) % buf_cap;
ch->count--;
pthread_cond_signal(&ch->not_full);
pthread_mutex_unlock(&ch->mu);
seed_arena_track(msg);
return EL_STR(msg);
}
el_val_t __channel_close(el_val_t ch_v) {
int slot = (int)(int64_t)ch_v;
if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR("");
ElChannel* ch = &_channels[slot];
pthread_mutex_lock(&ch->mu);
ch->closed = 1;
/* Wake all blocked recvers and senders so they can observe the close. */
pthread_cond_broadcast(&ch->not_empty);
pthread_cond_broadcast(&ch->not_full);
pthread_mutex_unlock(&ch->mu);
return EL_STR("");
}
/* ── 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_get_node_by_label(el_val_t label) { return engram_get_node_by_label(label); }
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_edges_json(el_val_t limit, el_val_t offset);
el_val_t __engram_edges_json(el_val_t limit, el_val_t offset) {
return engram_edges_json(limit, offset);
}
el_val_t engram_pool_stats_json(void);
el_val_t __engram_pool_stats_json(void) { return engram_pool_stats_json(); }
el_val_t el_alloc_count(void);
el_val_t el_alloc_bytes(void);
el_val_t el_peak_rss(void);
el_val_t __el_alloc_count(void) { return el_alloc_count(); }
el_val_t __el_alloc_bytes(void) { return el_alloc_bytes(); }
el_val_t __el_peak_rss(void) { return el_peak_rss(); }
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);
}
/* Forward decls for el_runtime.c symbols this file wraps. el_seed.c does not
* include el_runtime.h (documented in lang/AGENTS.md), so each wrapped symbol
* needs a prototype here or clang treats it as an implicit declaration (error
* under C99+) and the ABI mis-truncates the el_val_t return. */
el_val_t engram_op_assert_json(el_val_t node_id, el_val_t depth);
el_val_t engram_node_full_in(el_val_t purview, 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);
void engram_connect_in(el_val_t purview, el_val_t from_id, el_val_t to_id,
el_val_t weight, el_val_t relation);
el_val_t __engram_stats_json(void) { return engram_stats_json(); }
el_val_t __engram_op_assert_json(el_val_t node_id, el_val_t depth) { return engram_op_assert_json(node_id, depth); }
el_val_t __engram_node_full_in(el_val_t purview, 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_in(purview, content, node_type, label, salience, importance, confidence, tier, tags);
}
void __engram_connect_in(el_val_t purview, el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) {
engram_connect_in(purview, from_id, to_id, weight, relation);
}
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);
}