Files
el/el-compiler/runtime/el_runtime.c
T
Will Anderson 5c05ce9b99 self-host the el compiler
Today's milestone: dist/platform/elc compiles itself byte-for-byte to
itself (stage1 == stage2 == stage3 verified). The compiler is now a
real binary in the world.

What landed
- Spec rewrite (language.md) to truth — every feature marked
  implemented / planned / not-in-this-language with no fiction.
- C runtime extension: 51 new builtins. JSON parser + accessors,
  time, UUID, env, in-process state K/V, float formatting + math,
  string ops (index_of, split, char_at, char_code, pad_left/right,
  format), list ops (push, push_front, join, range), bool_to_str.
  Runtime grew 631 → 1611 lines, header 171 → 247.
- Codegen fix: transform_implicit_return lifts a function's bare
  trailing expression into an explicit return. Without it, lex(),
  parse(), and every other implicit-return function returned 0/nil
  and the whole pipeline produced empty C output.
- Codegen fix: index expressions dispatch on AST kind. obj["literal"]
  → el_get_field (map), arr[i] → el_list_get (list). Same Index node
  in the parser, two different runtime calls.
- Codegen fix: skip emitting fn main() (collides with C main()) and
  honor parsed return-type annotations so Void functions don't get
  return-wrapped (return println(x) is a C type error).
- Parser: capture return-type identifier from -> Ret annotations.
- Lexer: + vessel keyword, + % operator, + \r escape.
- Runtime fix: el_list_append now allocates a fresh list rather than
  realloc'ing the input. Realloc moved blocks made caller pointers
  dangle, which was inserting garbage values into declared lists and
  causing strcmp segfaults. Persistent allocation eliminates the
  whole class of use-after-free at modest memory cost.

Bootstrap path
- One-shot Python helper translated elc-combined.el to C and
  produced stage1. Helper is disposable; not committed.
- stage1 compiles elc-combined.el → stage2.c which cc compiles to
  stage2; stage2 compiles elc-combined.el → stage3.c. stage2.c and
  stage3.c are byte-identical. Closure proven.
- New elc installed at dist/platform/elc; old broken binary
  preserved as dist/platform/elc.legacy.
- dist/platform/elc.c is the canonical generated source.
- elvm and the bytecode pipeline are no longer on the critical path.

Known gap
- The `+` operator's heuristic dispatch still picks string concat
  when both operands are Idents with no literal anchor. Self-hosting
  works because the compiler source is careful, but `fn add(a:Int,
  b:Int) { a + b }` will not do arithmetic until codegen reads the
  parsed type annotations to dispatch. Fix is wiring; not done here.

Tested
- tiny / lextest / whiletest / map+field / array build all run.
- cgi-studio (1037 lines real El) compiles to C cleanly. Link fails
  only because runtime is missing fs_list, json_encode, llm_*; those
  are scheduled batches.
- Three-stage closure (stage1 vs stage2 vs stage3) byte-identical.
2026-04-30 13:10:29 -05:00

1621 lines
55 KiB
C

/*
* el_runtime.c — El language C runtime implementation
*
* All functions use el_val_t (= int64_t) as the universal value type.
* Strings are transported as their pointer address cast to int64_t.
* On any 64-bit system sizeof(pointer) <= sizeof(int64_t), so this is safe.
*
* Compile with:
* cc -std=c11 -I<runtime-dir> -o <prog> <prog>.c el_runtime.c
*/
#include "el_runtime.h"
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include <time.h>
#include <sys/time.h>
/* ── Internal allocators ─────────────────────────────────────────────────── */
static char* el_strdup(const char* s) {
if (!s) return strdup("");
return strdup(s);
}
static char* el_strbuf(size_t n) {
char* p = malloc(n + 1);
if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
p[0] = '\0';
return p;
}
/* Wrap an allocated C string as el_val_t */
static el_val_t el_wrap_str(char* s) {
return EL_STR(s);
}
/* ── I/O ──────────────────────────────────────────────────────────────────── */
void println(el_val_t s) {
const char* str = EL_CSTR(s);
if (str) puts(str);
else puts("");
}
void print(el_val_t s) {
const char* str = EL_CSTR(s);
if (str) fputs(str, stdout);
}
el_val_t readline(void) {
char buf[4096];
if (!fgets(buf, sizeof(buf), stdin)) return el_wrap_str(el_strdup(""));
size_t len = strlen(buf);
if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0';
return el_wrap_str(el_strdup(buf));
}
/* ── String builtins ─────────────────────────────────────────────────────── */
el_val_t el_str_concat(el_val_t av, el_val_t bv) {
const char* a = EL_CSTR(av);
const char* b = EL_CSTR(bv);
if (!a) a = "";
if (!b) b = "";
size_t la = strlen(a);
size_t lb = strlen(b);
char* out = el_strbuf(la + lb);
memcpy(out, a, la);
memcpy(out + la, b, lb);
out[la + lb] = '\0';
return el_wrap_str(out);
}
el_val_t str_eq(el_val_t av, el_val_t bv) {
const char* a = EL_CSTR(av);
const char* b = EL_CSTR(bv);
if (!a || !b) return (el_val_t)(a == b);
return (el_val_t)(strcmp(a, b) == 0);
}
el_val_t str_starts_with(el_val_t sv, el_val_t prefv) {
const char* s = EL_CSTR(sv);
const char* prefix = EL_CSTR(prefv);
if (!s || !prefix) return 0;
size_t lp = strlen(prefix);
return (el_val_t)(strncmp(s, prefix, lp) == 0);
}
el_val_t str_ends_with(el_val_t sv, el_val_t sufv) {
const char* s = EL_CSTR(sv);
const char* suffix = EL_CSTR(sufv);
if (!s || !suffix) return 0;
size_t ls = strlen(s);
size_t lsuf = strlen(suffix);
if (lsuf > ls) return 0;
return (el_val_t)(strcmp(s + ls - lsuf, suffix) == 0);
}
el_val_t str_len(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return 0;
return (el_val_t)strlen(s);
}
el_val_t str_concat(el_val_t a, el_val_t b) {
return el_str_concat(a, b);
}
el_val_t int_to_str(el_val_t n) {
char buf[32];
snprintf(buf, sizeof(buf), "%lld", (long long)n);
return el_wrap_str(el_strdup(buf));
}
el_val_t str_to_int(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return 0;
return (el_val_t)atoll(s);
}
el_val_t str_slice(el_val_t sv, el_val_t start, el_val_t end) {
const char* s = EL_CSTR(sv);
if (!s) return el_wrap_str(el_strdup(""));
int64_t len = (int64_t)strlen(s);
if (start < 0) start = 0;
if (end > len) end = len;
if (start >= end) return el_wrap_str(el_strdup(""));
int64_t sz = end - start;
char* out = el_strbuf((size_t)sz);
memcpy(out, s + start, (size_t)sz);
out[sz] = '\0';
return el_wrap_str(out);
}
el_val_t str_contains(el_val_t sv, el_val_t subv) {
const char* s = EL_CSTR(sv);
const char* sub = EL_CSTR(subv);
if (!s || !sub) return 0;
return (el_val_t)(strstr(s, sub) != NULL);
}
el_val_t str_replace(el_val_t sv, el_val_t fromv, el_val_t tov) {
const char* s = EL_CSTR(sv);
const char* from = EL_CSTR(fromv);
const char* to = EL_CSTR(tov);
if (!s || !from || !to) return el_wrap_str(el_strdup(s ? s : ""));
size_t ls = strlen(s);
size_t lf = strlen(from);
size_t lt = strlen(to);
if (lf == 0) return el_wrap_str(el_strdup(s));
size_t count = 0;
const char* p = s;
while ((p = strstr(p, from)) != NULL) { count++; p += lf; }
size_t out_sz = ls + count * lt + 1;
char* out = el_strbuf(out_sz);
char* dst = out;
p = s;
const char* found;
while ((found = strstr(p, from)) != NULL) {
size_t chunk = (size_t)(found - p);
memcpy(dst, p, chunk); dst += chunk;
memcpy(dst, to, lt); dst += lt;
p = found + lf;
}
strcpy(dst, p);
return el_wrap_str(out);
}
el_val_t str_to_upper(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return el_wrap_str(el_strdup(""));
size_t n = strlen(s);
char* out = el_strbuf(n);
for (size_t i = 0; i < n; i++) out[i] = (char)toupper((unsigned char)s[i]);
out[n] = '\0';
return el_wrap_str(out);
}
el_val_t str_to_lower(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return el_wrap_str(el_strdup(""));
size_t n = strlen(s);
char* out = el_strbuf(n);
for (size_t i = 0; i < n; i++) out[i] = (char)tolower((unsigned char)s[i]);
out[n] = '\0';
return el_wrap_str(out);
}
el_val_t str_trim(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return el_wrap_str(el_strdup(""));
while (*s && isspace((unsigned char)*s)) s++;
size_t n = strlen(s);
while (n > 0 && isspace((unsigned char)s[n - 1])) n--;
char* out = el_strbuf(n);
memcpy(out, s, n);
out[n] = '\0';
return el_wrap_str(out);
}
/* ── Math ────────────────────────────────────────────────────────────────── */
el_val_t el_abs(el_val_t n) { return n < 0 ? -n : n; }
el_val_t el_max(el_val_t a, el_val_t b) { return a > b ? a : b; }
el_val_t el_min(el_val_t a, el_val_t b) { return a < b ? a : b; }
/* ── List ────────────────────────────────────────────────────────────────── */
/*
* Dynamic array header:
* int64_t capacity
* int64_t length
* el_val_t elems[]
*/
typedef struct {
int64_t capacity;
int64_t length;
el_val_t elems[1];
} ElList;
static ElList* list_alloc(int64_t cap) {
ElList* lst = malloc(sizeof(ElList) + (size_t)(cap > 1 ? cap - 1 : 0) * sizeof(el_val_t));
if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
lst->capacity = cap;
lst->length = 0;
return lst;
}
el_val_t el_list_empty(void) {
return EL_STR(list_alloc(4));
}
el_val_t el_list_new(el_val_t count, ...) {
ElList* lst = list_alloc(count > 0 ? count : 4);
va_list ap;
va_start(ap, count);
for (int64_t i = 0; i < count; i++) {
lst->elems[i] = va_arg(ap, el_val_t);
}
va_end(ap);
lst->length = count;
return EL_STR(lst);
}
el_val_t el_list_len(el_val_t listv) {
ElList* lst = (ElList*)(uintptr_t)listv;
if (!lst) return 0;
return lst->length;
}
el_val_t el_list_get(el_val_t listv, el_val_t index) {
ElList* lst = (ElList*)(uintptr_t)listv;
if (!lst) return 0;
if (index < 0 || index >= lst->length) return 0;
return lst->elems[index];
}
el_val_t el_list_append(el_val_t listv, el_val_t elem) {
/* Always allocate a fresh list rather than realloc'ing the input.
* El callers commonly hold a stale pointer to the original list (e.g.
* cg_if_stmt passes `declared` to two successive cg_stmts calls; the
* first call may realloc the underlying block, leaving the second
* with a dangling pointer). Persistent allocation eliminates that
* whole class of use-after-free at modest memory cost. */
ElList* old = (ElList*)(uintptr_t)listv;
int64_t old_len = old ? old->length : 0;
int64_t new_cap = old_len + 1;
if (new_cap < 4) new_cap = 4;
ElList* new_lst = malloc(sizeof(ElList) + (size_t)(new_cap - 1) * sizeof(el_val_t));
if (!new_lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
new_lst->capacity = new_cap;
new_lst->length = old_len + 1;
if (old && old_len > 0) {
memcpy(new_lst->elems, old->elems, (size_t)old_len * sizeof(el_val_t));
}
new_lst->elems[old_len] = elem;
return EL_STR(new_lst);
}
/* ── Map ─────────────────────────────────────────────────────────────────── */
typedef struct {
int64_t count;
el_val_t* keys;
el_val_t* values;
} ElMap;
el_val_t el_map_new(el_val_t pair_count, ...) {
ElMap* m = malloc(sizeof(ElMap));
if (!m) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
m->count = pair_count;
m->keys = malloc(sizeof(el_val_t) * (size_t)(pair_count > 0 ? pair_count : 1));
m->values = malloc(sizeof(el_val_t) * (size_t)(pair_count > 0 ? pair_count : 1));
va_list ap;
va_start(ap, pair_count);
for (int64_t i = 0; i < pair_count; i++) {
m->keys[i] = va_arg(ap, el_val_t);
m->values[i] = va_arg(ap, el_val_t);
}
va_end(ap);
return EL_STR(m);
}
static ElMap* as_map(el_val_t v) { return (ElMap*)(uintptr_t)v; }
el_val_t el_map_get(el_val_t mapv, el_val_t keyv) {
ElMap* m = as_map(mapv);
const char* key = EL_CSTR(keyv);
if (!m || !key) return 0;
for (int64_t i = 0; i < m->count; i++) {
const char* k = EL_CSTR(m->keys[i]);
if (k && strcmp(k, key) == 0) return m->values[i];
}
return 0;
}
el_val_t el_get_field(el_val_t mapv, el_val_t keyv) {
return el_map_get(mapv, keyv);
}
el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) {
ElMap* m = as_map(mapv);
const char* key = EL_CSTR(keyv);
if (!m) return 0;
for (int64_t i = 0; i < m->count; i++) {
const char* k = EL_CSTR(m->keys[i]);
if (k && strcmp(k, key) == 0) { m->values[i] = value; return mapv; }
}
int64_t nc = m->count + 1;
m->keys = realloc(m->keys, sizeof(el_val_t) * (size_t)nc);
m->values = realloc(m->values, sizeof(el_val_t) * (size_t)nc);
m->keys[m->count] = keyv;
m->values[m->count] = value;
m->count = nc;
return mapv;
}
/* ── HTTP stubs ──────────────────────────────────────────────────────────── */
el_val_t http_get(el_val_t url) {
(void)url;
return el_wrap_str(el_strdup(""));
}
el_val_t http_post(el_val_t url, el_val_t body) {
(void)url; (void)body;
return el_wrap_str(el_strdup(""));
}
void http_serve(el_val_t port, el_val_t handler) {
(void)port; (void)handler;
}
/* ── Filesystem ──────────────────────────────────────────────────────────── */
el_val_t fs_read(el_val_t pathv) {
const char* path = EL_CSTR(pathv);
if (!path) return el_wrap_str(el_strdup(""));
FILE* f = fopen(path, "rb");
if (!f) return el_wrap_str(el_strdup(""));
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
char* buf = el_strbuf((size_t)sz);
size_t got = fread(buf, 1, (size_t)sz, f);
buf[got] = '\0';
fclose(f);
return el_wrap_str(buf);
}
el_val_t fs_write(el_val_t pathv, el_val_t contentv) {
const char* path = EL_CSTR(pathv);
const char* content = EL_CSTR(contentv);
if (!path || !content) return 0;
FILE* f = fopen(path, "wb");
if (!f) return 0;
size_t n = strlen(content);
size_t written = fwrite(content, 1, n, f);
fclose(f);
return written == n ? 1 : 0;
}
/* ── JSON ────────────────────────────────────────────────────────────────── */
el_val_t json_get(el_val_t jsonv, el_val_t keyv) {
const char* json = EL_CSTR(jsonv);
const char* key = EL_CSTR(keyv);
if (!json || !key) return el_wrap_str(el_strdup(""));
size_t klen = strlen(key);
char* pattern = el_strbuf(klen + 4);
snprintf(pattern, klen + 5, "\"%s\":", key);
const char* p = strstr(json, pattern);
free(pattern);
if (!p) return el_wrap_str(el_strdup(""));
p += strlen(key) + 3; /* skip "key": */
while (*p == ' ' || *p == '\t' || *p == '\n') p++;
if (*p == '"') {
p++;
const char* start = p;
while (*p && !(*p == '"' && *(p-1) != '\\')) p++;
size_t len = (size_t)(p - start);
char* out = el_strbuf(len);
memcpy(out, start, len);
out[len] = '\0';
return el_wrap_str(out);
}
const char* start = p;
while (*p && *p != ',' && *p != '}' && *p != ']' && *p != '\n') p++;
size_t len = (size_t)(p - start);
char* out = el_strbuf(len);
memcpy(out, start, len);
out[len] = '\0';
return el_wrap_str(out);
}
/* ── Float bit-cast helpers ──────────────────────────────────────────────── */
static inline double el_to_float(el_val_t v) {
union { int64_t i; double f; } u;
u.i = (int64_t)v;
return u.f;
}
static inline el_val_t el_from_float(double f) {
union { double f; int64_t i; } u;
u.f = f;
return (el_val_t)u.i;
}
/* ── JSON parser (recursive descent) ─────────────────────────────────────── */
/*
* Parsed JSON representation:
* - object -> ElMap (keys & values are el_val_t)
* - array -> ElList
* - string -> EL_STR-wrapped char* (allocated)
* - number -> int (el_val_t) if integer, otherwise el_from_float(double)
* - true -> 1
* - false -> 0
* - null -> EL_NULL (0)
*
* Note: there is no runtime type tag — parsed numbers cannot be
* distinguished from booleans by the runtime alone. The codegen tracks
* types separately. This matches the rest of el_val_t's type-erased model.
*/
typedef struct {
const char* p;
const char* end;
int err;
} JsonParser;
static void jp_skip_ws(JsonParser* jp) {
while (jp->p < jp->end) {
char c = *jp->p;
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') jp->p++;
else break;
}
}
static el_val_t jp_parse_value(JsonParser* jp);
/* Parse a JSON string literal (the opening " has NOT yet been consumed). */
static char* jp_parse_string_raw(JsonParser* jp) {
if (jp->p >= jp->end || *jp->p != '"') { jp->err = 1; return el_strdup(""); }
jp->p++;
size_t cap = 32, len = 0;
char* out = malloc(cap);
if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
while (jp->p < jp->end && *jp->p != '"') {
char c = *jp->p++;
if (c == '\\' && jp->p < jp->end) {
char esc = *jp->p++;
switch (esc) {
case '"': c = '"'; break;
case '\\': c = '\\'; break;
case '/': c = '/'; break;
case 'b': c = '\b'; break;
case 'f': c = '\f'; break;
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'u': {
/* Skip 4 hex digits; emit '?' as a placeholder */
for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++;
c = '?';
break;
}
default: c = esc; break;
}
}
if (len + 1 >= cap) {
cap *= 2;
out = realloc(out, cap);
if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
}
out[len++] = c;
}
if (jp->p < jp->end && *jp->p == '"') jp->p++;
else jp->err = 1;
out[len] = '\0';
return out;
}
static el_val_t jp_parse_number(JsonParser* jp) {
const char* start = jp->p;
int is_float = 0;
if (jp->p < jp->end && (*jp->p == '-' || *jp->p == '+')) jp->p++;
while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++;
if (jp->p < jp->end && *jp->p == '.') {
is_float = 1; jp->p++;
while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++;
}
if (jp->p < jp->end && (*jp->p == 'e' || *jp->p == 'E')) {
is_float = 1; jp->p++;
if (jp->p < jp->end && (*jp->p == '+' || *jp->p == '-')) jp->p++;
while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++;
}
size_t n = (size_t)(jp->p - start);
char buf[64];
if (n >= sizeof(buf)) n = sizeof(buf) - 1;
memcpy(buf, start, n);
buf[n] = '\0';
if (is_float) return el_from_float(strtod(buf, NULL));
return (el_val_t)strtoll(buf, NULL, 10);
}
static el_val_t jp_parse_array(JsonParser* jp) {
if (jp->p < jp->end && *jp->p == '[') jp->p++;
el_val_t lst = el_list_empty();
jp_skip_ws(jp);
if (jp->p < jp->end && *jp->p == ']') { jp->p++; return lst; }
while (jp->p < jp->end) {
jp_skip_ws(jp);
el_val_t v = jp_parse_value(jp);
lst = el_list_append(lst, v);
jp_skip_ws(jp);
if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; }
if (jp->p < jp->end && *jp->p == ']') { jp->p++; break; }
jp->err = 1;
break;
}
return lst;
}
static el_val_t jp_parse_object(JsonParser* jp) {
if (jp->p < jp->end && *jp->p == '{') jp->p++;
el_val_t m = el_map_new(0);
jp_skip_ws(jp);
if (jp->p < jp->end && *jp->p == '}') { jp->p++; return m; }
while (jp->p < jp->end) {
jp_skip_ws(jp);
char* key = jp_parse_string_raw(jp);
jp_skip_ws(jp);
if (jp->p < jp->end && *jp->p == ':') jp->p++;
else { jp->err = 1; free(key); break; }
jp_skip_ws(jp);
el_val_t v = jp_parse_value(jp);
m = el_map_set(m, EL_STR(key), v);
jp_skip_ws(jp);
if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; }
if (jp->p < jp->end && *jp->p == '}') { jp->p++; break; }
jp->err = 1;
break;
}
return m;
}
static el_val_t jp_parse_value(JsonParser* jp) {
jp_skip_ws(jp);
if (jp->p >= jp->end) { jp->err = 1; return EL_NULL; }
char c = *jp->p;
if (c == '"') return el_wrap_str(jp_parse_string_raw(jp));
if (c == '{') return jp_parse_object(jp);
if (c == '[') return jp_parse_array(jp);
if (c == '-' || isdigit((unsigned char)c)) return jp_parse_number(jp);
if (c == 't' && jp->p + 4 <= jp->end && strncmp(jp->p, "true", 4) == 0) { jp->p += 4; return 1; }
if (c == 'f' && jp->p + 5 <= jp->end && strncmp(jp->p, "false", 5) == 0) { jp->p += 5; return 0; }
if (c == 'n' && jp->p + 4 <= jp->end && strncmp(jp->p, "null", 4) == 0) { jp->p += 4; return EL_NULL; }
jp->err = 1;
return EL_NULL;
}
el_val_t json_parse(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return EL_NULL;
JsonParser jp = { .p = s, .end = s + strlen(s), .err = 0 };
el_val_t v = jp_parse_value(&jp);
if (jp.err) return EL_NULL;
return v;
}
/* ── JSON stringify ──────────────────────────────────────────────────────── */
/*
* Stringify policy: el_val_t is type-erased, so we cannot perfectly
* round-trip arbitrary values. We use these heuristics:
* - If value is an ElList pointer (in the heap range), serialize as array.
* - If value is an ElMap pointer, serialize as object.
* - If value looks like a printable string pointer, serialize as string.
* - Otherwise serialize as integer.
* This is best-effort. Programs that need exact control should build the
* string directly. A pointer test is the cheapest way to disambiguate
* from small integers without a separate type tag.
*/
typedef struct {
char* buf;
size_t len;
size_t cap;
} JsonBuf;
static void jb_init(JsonBuf* b) {
b->cap = 64; b->len = 0;
b->buf = malloc(b->cap);
if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
b->buf[0] = '\0';
}
static void jb_reserve(JsonBuf* b, size_t add) {
if (b->len + add + 1 > b->cap) {
while (b->len + add + 1 > b->cap) b->cap *= 2;
b->buf = realloc(b->buf, b->cap);
if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
}
}
static void jb_putc(JsonBuf* b, char c) {
jb_reserve(b, 1);
b->buf[b->len++] = c;
b->buf[b->len] = '\0';
}
static void jb_puts(JsonBuf* b, const char* s) {
size_t n = strlen(s);
jb_reserve(b, n);
memcpy(b->buf + b->len, s, n);
b->len += n;
b->buf[b->len] = '\0';
}
static void jb_emit_escaped(JsonBuf* b, const char* s) {
jb_putc(b, '"');
for (; *s; s++) {
unsigned char c = (unsigned char)*s;
switch (c) {
case '"': jb_puts(b, "\\\""); break;
case '\\': jb_puts(b, "\\\\"); break;
case '\b': jb_puts(b, "\\b"); break;
case '\f': jb_puts(b, "\\f"); break;
case '\n': jb_puts(b, "\\n"); break;
case '\r': jb_puts(b, "\\r"); break;
case '\t': jb_puts(b, "\\t"); break;
default:
if (c < 0x20) {
char tmp[8];
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
jb_puts(b, tmp);
} else {
jb_putc(b, (char)c);
}
break;
}
}
jb_putc(b, '"');
}
/* Heuristic: is this el_val_t likely a pointer to an ElList?
* We can't fully verify, but pointers are large addresses, integers small.
* Treat values whose magnitude exceeds 2^32 as potential pointers and
* sniff by reading the header conservatively.
*
* Simpler heuristic: if the value reads as a printable string, treat as
* string; otherwise as integer. Lists/Maps are encoded as struct pointers,
* which have leading binary bytes — so they won't look like strings. */
static int looks_like_string(el_val_t v) {
if (v == 0) return 0;
/* Treat plausible heap addresses as candidates */
uintptr_t p = (uintptr_t)v;
/* Small integers (positive and negative) are not pointers */
if ((int64_t)v >= -1000000 && (int64_t)v <= 1000000) return 0;
if (p < 0x1000) return 0;
/* Sniff first bytes for printable */
const unsigned char* s = (const unsigned char*)p;
for (int i = 0; i < 16; i++) {
unsigned char c = s[i];
if (c == '\0') return i > 0; /* terminated string */
if (c < 0x09 || (c > 0x0d && c < 0x20) || c >= 0x7f) return 0;
}
return 1; /* 16+ printable bytes — call it a string */
}
static void jb_emit_value(JsonBuf* b, el_val_t v);
static void jb_emit_int(JsonBuf* b, int64_t n) {
char tmp[32];
snprintf(tmp, sizeof(tmp), "%lld", (long long)n);
jb_puts(b, tmp);
}
static void jb_emit_value(JsonBuf* b, el_val_t v) {
if (v == EL_NULL) { jb_puts(b, "null"); return; }
if (looks_like_string(v)) {
jb_emit_escaped(b, EL_CSTR(v));
return;
}
jb_emit_int(b, (int64_t)v);
}
el_val_t json_stringify(el_val_t v) {
JsonBuf b; jb_init(&b);
jb_emit_value(&b, v);
return el_wrap_str(b.buf);
}
/* ── JSON substring accessors ────────────────────────────────────────────── */
/*
* These walk the raw JSON string looking for "key": at the top level (depth 1)
* of an object. They handle escaped quotes, nested objects/arrays, and
* whitespace around the colon.
*/
/* Find "key": at object-depth == 1 inside the JSON object string `s`.
* Returns pointer to the first byte of the value, or NULL. */
static const char* json_find_key(const char* s, const char* key) {
if (!s || !key) return NULL;
size_t klen = strlen(key);
int depth = 0;
int in_str = 0;
int escape = 0;
const char* p = s;
while (*p) {
char c = *p;
if (in_str) {
if (escape) { escape = 0; }
else if (c == '\\') { escape = 1; }
else if (c == '"') {
/* End of string. If we're at depth 1, check if this was a key. */
p++;
if (depth == 1) {
/* The string just ended at p-1. Check if it matches key
* and is followed by a colon. We need to backtrack to find
* the start of this string and compare. */
}
in_str = 0;
continue;
}
p++;
continue;
}
if (c == '"') {
/* Start of a string literal */
const char* str_start = p + 1;
const char* q = str_start;
int e = 0;
while (*q) {
if (e) { e = 0; q++; continue; }
if (*q == '\\') { e = 1; q++; continue; }
if (*q == '"') break;
q++;
}
size_t slen = (size_t)(q - str_start);
const char* after = (*q == '"') ? q + 1 : q;
/* If at depth 1 and matches key and followed by ':' -> got it */
if (depth == 1 && slen == klen && strncmp(str_start, key, klen) == 0) {
const char* r = after;
while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++;
if (*r == ':') {
r++;
while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++;
return r;
}
}
p = after;
continue;
}
if (c == '{' || c == '[') depth++;
else if (c == '}' || c == ']') depth--;
p++;
}
return NULL;
}
/* Skip a JSON value starting at p; return pointer past the value end. */
static const char* json_skip_value(const char* p) {
if (!p || !*p) return p;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
if (*p == '"') {
p++;
int e = 0;
while (*p) {
if (e) { e = 0; p++; continue; }
if (*p == '\\') { e = 1; p++; continue; }
if (*p == '"') { p++; break; }
p++;
}
return p;
}
if (*p == '{' || *p == '[') {
char open = *p;
char close = (open == '{') ? '}' : ']';
int depth = 0;
int in_str = 0;
int e = 0;
while (*p) {
char c = *p;
if (in_str) {
if (e) { e = 0; }
else if (c == '\\') { e = 1; }
else if (c == '"') in_str = 0;
p++;
continue;
}
if (c == '"') { in_str = 1; p++; continue; }
if (c == open) depth++;
else if (c == close) { depth--; p++; if (depth == 0) return p; continue; }
p++;
}
return p;
}
/* scalar: number, true/false/null */
while (*p && *p != ',' && *p != '}' && *p != ']' &&
*p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++;
return p;
}
el_val_t json_get_string(el_val_t json_str, el_val_t key) {
const char* json = EL_CSTR(json_str);
const char* k = EL_CSTR(key);
const char* p = json_find_key(json, k);
if (!p || *p != '"') return el_wrap_str(el_strdup(""));
p++;
JsonParser jp = { .p = p - 1, .end = json + (json ? strlen(json) : 0), .err = 0 };
char* parsed = jp_parse_string_raw(&jp);
if (jp.err) { free(parsed); return el_wrap_str(el_strdup("")); }
return el_wrap_str(parsed);
}
el_val_t json_get_int(el_val_t json_str, el_val_t key) {
const char* json = EL_CSTR(json_str);
const char* k = EL_CSTR(key);
const char* p = json_find_key(json, k);
if (!p) return 0;
if (*p == '"' || *p == '{' || *p == '[') return 0;
return (el_val_t)strtoll(p, NULL, 10);
}
el_val_t json_get_float(el_val_t json_str, el_val_t key) {
const char* json = EL_CSTR(json_str);
const char* k = EL_CSTR(key);
const char* p = json_find_key(json, k);
if (!p) return 0;
if (*p == '"' || *p == '{' || *p == '[') return 0;
return el_from_float(strtod(p, NULL));
}
el_val_t json_get_bool(el_val_t json_str, el_val_t key) {
const char* json = EL_CSTR(json_str);
const char* k = EL_CSTR(key);
const char* p = json_find_key(json, k);
if (!p) return 0;
if (strncmp(p, "true", 4) == 0) return 1;
return 0;
}
el_val_t json_get_raw(el_val_t json_str, el_val_t key) {
const char* json = EL_CSTR(json_str);
const char* k = EL_CSTR(key);
const char* p = json_find_key(json, k);
if (!p) return el_wrap_str(el_strdup(""));
const char* end = json_skip_value(p);
size_t n = (size_t)(end - p);
char* out = el_strbuf(n);
memcpy(out, p, n);
out[n] = '\0';
return el_wrap_str(out);
}
el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) {
const char* json = EL_CSTR(json_str);
const char* k = EL_CSTR(key);
if (!k) k = "";
if (!json || !*json) {
/* Build a fresh object */
JsonBuf b; jb_init(&b);
jb_putc(&b, '{');
jb_emit_escaped(&b, k);
jb_putc(&b, ':');
jb_emit_value(&b, value);
jb_putc(&b, '}');
return el_wrap_str(b.buf);
}
const char* existing = json_find_key(json, k);
JsonBuf b; jb_init(&b);
if (existing) {
const char* end = json_skip_value(existing);
/* Copy [json .. existing) */
size_t prefix = (size_t)(existing - json);
jb_reserve(&b, prefix);
memcpy(b.buf + b.len, json, prefix);
b.len += prefix;
b.buf[b.len] = '\0';
jb_emit_value(&b, value);
jb_puts(&b, end);
return el_wrap_str(b.buf);
}
/* Insert before closing '}'. Find last '}' */
size_t jl = strlen(json);
if (jl == 0) { free(b.buf); return el_wrap_str(el_strdup("{}")); }
/* Find last '}' from the end */
ssize_t close_idx = -1;
for (ssize_t i = (ssize_t)jl - 1; i >= 0; i--) {
if (json[i] == '}') { close_idx = i; break; }
}
if (close_idx < 0) {
free(b.buf);
return el_wrap_str(el_strdup(json));
}
/* Determine if object is empty: scan between last '{' and '}' for non-ws */
int empty = 1;
for (ssize_t i = close_idx - 1; i >= 0; i--) {
char c = json[i];
if (c == '{') break;
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { empty = 0; break; }
}
/* Copy json[0..close_idx) */
jb_reserve(&b, (size_t)close_idx);
memcpy(b.buf + b.len, json, (size_t)close_idx);
b.len += (size_t)close_idx;
b.buf[b.len] = '\0';
if (!empty) jb_putc(&b, ',');
jb_emit_escaped(&b, k);
jb_putc(&b, ':');
jb_emit_value(&b, value);
/* Append from close_idx onward */
jb_puts(&b, json + close_idx);
return el_wrap_str(b.buf);
}
el_val_t json_array_len(el_val_t json_str) {
const char* s = EL_CSTR(json_str);
if (!s) return 0;
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
if (*s != '[') return 0;
s++;
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
if (*s == ']') return 0;
int64_t count = 0;
while (*s) {
const char* end = json_skip_value(s);
if (end == s) break;
count++;
s = end;
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
if (*s == ',') { s++; continue; }
if (*s == ']' || *s == '\0') break;
}
return (el_val_t)count;
}
/* ── Time ────────────────────────────────────────────────────────────────── */
el_val_t time_now(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
int64_t ms = (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL;
return (el_val_t)ms;
}
el_val_t time_now_utc(void) {
return time_now();
}
el_val_t time_format(el_val_t ts, el_val_t fmt) {
int64_t ms = (int64_t)ts;
time_t s = (time_t)(ms / 1000);
int msec = (int)(ms % 1000);
if (msec < 0) { msec += 1000; s -= 1; }
struct tm tm;
gmtime_r(&s, &tm);
const char* fmt_str = EL_CSTR(fmt);
if (!fmt_str || strcmp(fmt_str, "ISO") == 0) {
char buf[64];
snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec, msec);
return el_wrap_str(el_strdup(buf));
}
char buf[256];
if (strftime(buf, sizeof(buf), fmt_str, &tm) == 0) buf[0] = '\0';
return el_wrap_str(el_strdup(buf));
}
el_val_t time_to_parts(el_val_t ts) {
int64_t ms = (int64_t)ts;
time_t s = (time_t)(ms / 1000);
int msec = (int)(ms % 1000);
if (msec < 0) { msec += 1000; s -= 1; }
struct tm tm;
gmtime_r(&s, &tm);
el_val_t m = el_map_new(0);
m = el_map_set(m, EL_STR(el_strdup("year")), (el_val_t)(tm.tm_year + 1900));
m = el_map_set(m, EL_STR(el_strdup("month")), (el_val_t)(tm.tm_mon + 1));
m = el_map_set(m, EL_STR(el_strdup("day")), (el_val_t)tm.tm_mday);
m = el_map_set(m, EL_STR(el_strdup("hour")), (el_val_t)tm.tm_hour);
m = el_map_set(m, EL_STR(el_strdup("minute")), (el_val_t)tm.tm_min);
m = el_map_set(m, EL_STR(el_strdup("second")), (el_val_t)tm.tm_sec);
m = el_map_set(m, EL_STR(el_strdup("ms")), (el_val_t)msec);
return m;
}
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz) {
(void)tz;
int64_t s = (int64_t)secs;
int64_t n = (int64_t)ns;
int64_t ms = s * 1000LL + n / 1000000LL;
return (el_val_t)ms;
}
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit) {
const char* u = EL_CSTR(unit);
int64_t cur = (int64_t)ts;
int64_t d = (int64_t)n;
int64_t add_ms = d;
if (u) {
if (strcmp(u, "ms") == 0) add_ms = d;
else if (strcmp(u, "sec") == 0) add_ms = d * 1000LL;
else if (strcmp(u, "min") == 0) add_ms = d * 60000LL;
else if (strcmp(u, "hour") == 0) add_ms = d * 3600000LL;
else if (strcmp(u, "day") == 0) add_ms = d * 86400000LL;
}
return (el_val_t)(cur + add_ms);
}
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit) {
int64_t d = (int64_t)ts2 - (int64_t)ts1;
const char* u = EL_CSTR(unit);
if (!u || strcmp(u, "ms") == 0) return (el_val_t)d;
if (strcmp(u, "sec") == 0) return (el_val_t)(d / 1000LL);
if (strcmp(u, "min") == 0) return (el_val_t)(d / 60000LL);
if (strcmp(u, "hour") == 0) return (el_val_t)(d / 3600000LL);
if (strcmp(u, "day") == 0) return (el_val_t)(d / 86400000LL);
return (el_val_t)d;
}
/* ── UUID v4 ─────────────────────────────────────────────────────────────── */
static int _el_uuid_seeded = 0;
static void _el_uuid_seed(void) {
if (!_el_uuid_seeded) {
srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_el_uuid_seeded);
_el_uuid_seeded = 1;
}
}
el_val_t uuid_new(void) {
_el_uuid_seed();
unsigned char b[16];
for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff);
/* Version 4 */
b[6] = (b[6] & 0x0f) | 0x40;
/* RFC 4122 variant */
b[8] = (b[8] & 0x3f) | 0x80;
char buf[37];
snprintf(buf, sizeof(buf),
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
b[0], b[1], b[2], b[3],
b[4], b[5],
b[6], b[7],
b[8], b[9],
b[10], b[11], b[12], b[13], b[14], b[15]);
return el_wrap_str(el_strdup(buf));
}
el_val_t uuid_v4(void) { return uuid_new(); }
/* ── Environment ─────────────────────────────────────────────────────────── */
el_val_t env(el_val_t key) {
const char* k = EL_CSTR(key);
if (!k) return el_wrap_str(el_strdup(""));
const char* v = getenv(k);
return el_wrap_str(el_strdup(v ? v : ""));
}
/* ── In-process state K/V ────────────────────────────────────────────────── */
typedef struct {
char* key;
char* value;
} StateEntry;
static StateEntry* _state_entries = NULL;
static size_t _state_count = 0;
static size_t _state_cap = 0;
static StateEntry* state_find(const char* key) {
for (size_t i = 0; i < _state_count; i++) {
if (strcmp(_state_entries[i].key, key) == 0) return &_state_entries[i];
}
return NULL;
}
el_val_t state_set(el_val_t key, el_val_t value) {
const char* k = EL_CSTR(key);
const char* v = EL_CSTR(value);
if (!k) return 0;
if (!v) v = "";
StateEntry* e = state_find(k);
if (e) {
free(e->value);
e->value = el_strdup(v);
return 1;
}
if (_state_count >= _state_cap) {
size_t nc = _state_cap == 0 ? 16 : _state_cap * 2;
_state_entries = realloc(_state_entries, nc * sizeof(StateEntry));
if (!_state_entries) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
_state_cap = nc;
}
_state_entries[_state_count].key = el_strdup(k);
_state_entries[_state_count].value = el_strdup(v);
_state_count++;
return 1;
}
el_val_t state_get(el_val_t key) {
const char* k = EL_CSTR(key);
if (!k) return el_wrap_str(el_strdup(""));
StateEntry* e = state_find(k);
return el_wrap_str(el_strdup(e ? e->value : ""));
}
el_val_t state_del(el_val_t key) {
const char* k = EL_CSTR(key);
if (!k) return 0;
for (size_t i = 0; i < _state_count; i++) {
if (strcmp(_state_entries[i].key, k) == 0) {
free(_state_entries[i].key);
free(_state_entries[i].value);
for (size_t j = i + 1; j < _state_count; j++) {
_state_entries[j - 1] = _state_entries[j];
}
_state_count--;
return 1;
}
}
return 1;
}
el_val_t state_keys(void) {
el_val_t lst = el_list_empty();
for (size_t i = 0; i < _state_count; i++) {
lst = el_list_append(lst, el_wrap_str(el_strdup(_state_entries[i].key)));
}
return lst;
}
/* ── Float formatting ────────────────────────────────────────────────────── */
el_val_t float_to_str(el_val_t f) {
char buf[64];
snprintf(buf, sizeof(buf), "%g", el_to_float(f));
return el_wrap_str(el_strdup(buf));
}
el_val_t int_to_float(el_val_t n) {
return el_from_float((double)(int64_t)n);
}
el_val_t float_to_int(el_val_t f) {
return (el_val_t)(int64_t)el_to_float(f);
}
el_val_t format_float(el_val_t f, el_val_t decimals) {
int d = (int)(int64_t)decimals;
if (d < 0) d = 0;
if (d > 30) d = 30;
char buf[128];
snprintf(buf, sizeof(buf), "%.*f", d, el_to_float(f));
return el_wrap_str(el_strdup(buf));
}
el_val_t decimal_round(el_val_t f, el_val_t decimals) {
int d = (int)(int64_t)decimals;
if (d < 0) d = 0;
if (d > 15) d = 15;
double mul = pow(10.0, (double)d);
double v = el_to_float(f);
double r = (v >= 0.0 ? floor(v * mul + 0.5) : -floor(-v * mul + 0.5)) / mul;
return el_from_float(r);
}
el_val_t str_to_float(el_val_t s) {
const char* str = EL_CSTR(s);
if (!str) return el_from_float(0.0);
return el_from_float(strtod(str, NULL));
}
/* ── Math (Float-aware) ──────────────────────────────────────────────────── */
el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); }
el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); }
el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); }
el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); }
el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); }
el_val_t math_pi(void) { return el_from_float(3.141592653589793238462643383279502884); }
/* ── String additions ────────────────────────────────────────────────────── */
el_val_t str_index_of(el_val_t s, el_val_t sub) {
const char* str = EL_CSTR(s);
const char* sb = EL_CSTR(sub);
if (!str || !sb) return -1;
const char* hit = strstr(str, sb);
if (!hit) return -1;
return (el_val_t)(int64_t)(hit - str);
}
el_val_t str_split(el_val_t s, el_val_t sep) {
const char* str = EL_CSTR(s);
const char* sp = EL_CSTR(sep);
el_val_t lst = el_list_empty();
if (!str) return lst;
if (!sp || !*sp) {
lst = el_list_append(lst, el_wrap_str(el_strdup(str)));
return lst;
}
size_t lp = strlen(sp);
const char* p = str;
const char* hit;
while ((hit = strstr(p, sp)) != NULL) {
size_t n = (size_t)(hit - p);
char* out = el_strbuf(n);
memcpy(out, p, n);
out[n] = '\0';
lst = el_list_append(lst, el_wrap_str(out));
p = hit + lp;
}
lst = el_list_append(lst, el_wrap_str(el_strdup(p)));
return lst;
}
el_val_t str_char_at(el_val_t s, el_val_t i) {
const char* str = EL_CSTR(s);
int64_t idx = (int64_t)i;
if (!str) return el_wrap_str(el_strdup(""));
int64_t n = (int64_t)strlen(str);
if (idx < 0 || idx >= n) return el_wrap_str(el_strdup(""));
char buf[2];
buf[0] = str[idx];
buf[1] = '\0';
return el_wrap_str(el_strdup(buf));
}
el_val_t str_char_code(el_val_t s, el_val_t i) {
const char* str = EL_CSTR(s);
int64_t idx = (int64_t)i;
if (!str) return 0;
int64_t n = (int64_t)strlen(str);
if (idx < 0 || idx >= n) return 0;
return (el_val_t)(unsigned char)str[idx];
}
static el_val_t str_pad(const char* s, int64_t width, const char* pad, int left) {
if (!s) s = "";
if (!pad || !*pad) pad = " ";
int64_t lp = (int64_t)strlen(pad);
int64_t ls = (int64_t)strlen(s);
if (ls >= width) return el_wrap_str(el_strdup(s));
int64_t need = width - ls;
char* out = el_strbuf((size_t)width);
if (left) {
for (int64_t i = 0; i < need; i++) out[i] = pad[i % lp];
memcpy(out + need, s, (size_t)ls);
} else {
memcpy(out, s, (size_t)ls);
for (int64_t i = 0; i < need; i++) out[ls + i] = pad[i % lp];
}
out[width] = '\0';
return el_wrap_str(out);
}
el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad) {
return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 1);
}
el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad) {
return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 0);
}
el_val_t str_format(el_val_t template, el_val_t data) {
const char* tpl = EL_CSTR(template);
if (!tpl) return el_wrap_str(el_strdup(""));
JsonBuf b; jb_init(&b);
const char* p = tpl;
while (*p) {
if (*p == '{') {
const char* q = p + 1;
while (*q && *q != '}') q++;
if (*q == '}') {
size_t klen = (size_t)(q - p - 1);
char keybuf[256];
if (klen < sizeof(keybuf)) {
memcpy(keybuf, p + 1, klen);
keybuf[klen] = '\0';
el_val_t v = el_map_get(data, EL_STR(keybuf));
if (v != 0 && looks_like_string(v)) {
jb_puts(&b, EL_CSTR(v));
p = q + 1;
continue;
} else if (v != 0) {
jb_emit_int(&b, (int64_t)v);
p = q + 1;
continue;
}
}
/* Unknown key — leave {key} verbatim */
jb_reserve(&b, klen + 2);
memcpy(b.buf + b.len, p, klen + 2);
b.len += klen + 2;
b.buf[b.len] = '\0';
p = q + 1;
continue;
}
}
jb_putc(&b, *p);
p++;
}
return el_wrap_str(b.buf);
}
el_val_t str_lower(el_val_t s) { return str_to_lower(s); }
el_val_t str_upper(el_val_t s) { return str_to_upper(s); }
/* ── List additions ──────────────────────────────────────────────────────── */
el_val_t list_push(el_val_t list, el_val_t elem) {
return el_list_append(list, elem);
}
el_val_t list_push_front(el_val_t listv, el_val_t elem) {
ElList* lst = (ElList*)(uintptr_t)listv;
if (!lst) {
el_val_t nl = el_list_empty();
return el_list_append(nl, elem);
}
/* Append to grow capacity, then shift right */
listv = el_list_append(listv, elem);
lst = (ElList*)(uintptr_t)listv;
for (int64_t i = lst->length - 1; i > 0; i--) {
lst->elems[i] = lst->elems[i - 1];
}
lst->elems[0] = elem;
return EL_STR(lst);
}
el_val_t list_join(el_val_t listv, el_val_t sep) {
ElList* lst = (ElList*)(uintptr_t)listv;
const char* sp = EL_CSTR(sep);
if (!sp) sp = "";
if (!lst || lst->length == 0) return el_wrap_str(el_strdup(""));
JsonBuf b; jb_init(&b);
for (int64_t i = 0; i < lst->length; i++) {
if (i > 0) jb_puts(&b, sp);
el_val_t v = lst->elems[i];
if (v == 0) continue;
if (looks_like_string(v)) {
jb_puts(&b, EL_CSTR(v));
} else {
char tmp[32];
snprintf(tmp, sizeof(tmp), "%lld", (long long)v);
jb_puts(&b, tmp);
}
}
return el_wrap_str(b.buf);
}
el_val_t list_range(el_val_t start, el_val_t end) {
int64_t a = (int64_t)start;
int64_t b = (int64_t)end;
el_val_t lst = el_list_empty();
for (int64_t i = a; i < b; i++) lst = el_list_append(lst, (el_val_t)i);
return lst;
}
/* ── Bool helpers ────────────────────────────────────────────────────────── */
el_val_t bool_to_str(el_val_t b) {
return el_wrap_str(el_strdup(b ? "true" : "false"));
}
/* ── Process ─────────────────────────────────────────────────────────────── */
void exit_program(el_val_t code) {
exit((int)code);
}
/* ── args() — command-line argument access ──────────────────────────────────
* Compiled El programs call args() to get a list of CLI arguments.
* Call el_runtime_init_args(argc, argv) at the start of C main() to populate.
* The args list excludes argv[0] (the program name). */
static el_val_t _el_args_list = 0;
void el_runtime_init_args(int argc, char** argv) {
_el_args_list = el_list_empty();
for (int i = 1; i < argc; i++) {
_el_args_list = el_list_append(_el_args_list, EL_STR(argv[i]));
}
}
el_val_t args(void) {
if (!_el_args_list) _el_args_list = el_list_empty();
return _el_args_list;
}
/* ── CGI identity ────────────────────────────────────────────────────────────
* Called once at program start by the generated main() of a cgi {} program.
* Stores CGI identity so dharma_* builtins can reference it. */
static const char* _el_cgi_name = NULL;
static const char* _el_cgi_dharma_id = NULL;
static const char* _el_cgi_principal = NULL;
static const char* _el_cgi_network = NULL;
static const char* _el_cgi_engram = NULL;
void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
el_val_t network, el_val_t engram) {
_el_cgi_name = EL_CSTR(name);
_el_cgi_dharma_id = EL_CSTR(dharma_id);
_el_cgi_principal = EL_CSTR(principal);
_el_cgi_network = EL_CSTR(network) ? EL_CSTR(network) : "dharma-mainnet";
_el_cgi_engram = EL_CSTR(engram) ? EL_CSTR(engram) : "http://localhost:8742";
printf("[cgi] identity: name=%s dharma_id=%s principal=%s network=%s engram=%s\n",
_el_cgi_name ? _el_cgi_name : "(unset)",
_el_cgi_dharma_id ? _el_cgi_dharma_id : "(unset)",
_el_cgi_principal ? _el_cgi_principal : "(unset)",
_el_cgi_network,
_el_cgi_engram);
}
/* ── DHARMA network stubs ───────────────────────────────────────────────────
* Stub implementations for all dharma_* and engram_* builtins.
* Each stub prints a descriptive line to stdout so calls are visible in tests.
* Full implementations are provided by the DHARMA runtime linked at deploy. */
el_val_t dharma_connect(el_val_t cgi_id) {
const char* id = EL_CSTR(cgi_id);
if (!id) id = "(null)";
char buf[256];
snprintf(buf, sizeof(buf), "[dharma] connect: %s", id);
puts(buf);
/* Return a synthetic channel ID of the form "ch:<cgi_id>" */
char ch[272];
snprintf(ch, sizeof(ch), "ch:%s", id);
return el_wrap_str(el_strdup(ch));
}
el_val_t dharma_send(el_val_t channel, el_val_t content) {
const char* ch = EL_CSTR(channel);
const char* msg = EL_CSTR(content);
if (!ch) ch = "(null)";
if (!msg) msg = "(null)";
char buf[1024];
snprintf(buf, sizeof(buf), "[dharma] send on %s: %s", ch, msg);
puts(buf);
return el_wrap_str(el_strdup(""));
}
el_val_t dharma_activate(el_val_t query) {
const char* q = EL_CSTR(query);
if (!q) q = "(null)";
char buf[512];
snprintf(buf, sizeof(buf), "[dharma] activate: %s", q);
puts(buf);
return el_list_empty();
}
void dharma_emit(el_val_t event_type, el_val_t payload) {
const char* et = EL_CSTR(event_type);
const char* pay = EL_CSTR(payload);
if (!et) et = "(null)";
if (!pay) pay = "(null)";
char buf[1024];
snprintf(buf, sizeof(buf), "[dharma] emit: %s %s", et, pay);
puts(buf);
}
el_val_t dharma_field(el_val_t event_type) {
const char* et = EL_CSTR(event_type);
if (!et) et = "(null)";
char buf[512];
snprintf(buf, sizeof(buf), "[dharma] field: %s", et);
puts(buf);
return el_map_new(0);
}
void dharma_strengthen(el_val_t cgi_id, el_val_t weight) {
const char* id = EL_CSTR(cgi_id);
if (!id) id = "(null)";
/* weight is encoded as el_val_t; print as integer (float encoding TBD) */
char buf[256];
snprintf(buf, sizeof(buf), "[dharma] strengthen: %s +%lld", id, (long long)weight);
puts(buf);
}
el_val_t dharma_relationship(el_val_t cgi_id) {
const char* id = EL_CSTR(cgi_id);
if (!id) id = "(null)";
char buf[256];
snprintf(buf, sizeof(buf), "[dharma] relationship: %s", id);
puts(buf);
return 0; /* 0.0 — no prior relationship in stub mode */
}
el_val_t dharma_peers(void) {
puts("[dharma] peers");
return el_list_empty();
}
/* ── Engram local graph stubs ────────────────────────────────────────────── */
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) {
const char* c = EL_CSTR(content);
const char* nt = EL_CSTR(node_type);
if (!c) c = "(null)";
if (!nt) nt = "(null)";
char buf[512];
snprintf(buf, sizeof(buf), "[engram] node: %s (type=%s salience=%lld)", c, nt, (long long)salience);
puts(buf);
return el_wrap_str(el_strdup("stub-node-id"));
}
el_val_t engram_activate(el_val_t query, el_val_t depth) {
const char* q = EL_CSTR(query);
if (!q) q = "(null)";
char buf[512];
snprintf(buf, sizeof(buf), "[engram] activate: %s depth=%lld", q, (long long)depth);
puts(buf);
return el_list_empty();
}
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) {
const char* f = EL_CSTR(from_id);
const char* t = EL_CSTR(to_id);
const char* r = EL_CSTR(relation);
if (!f) f = "(null)";
if (!t) t = "(null)";
if (!r) r = "(null)";
char buf[512];
snprintf(buf, sizeof(buf), "[engram] connect: %s -[%s]-> %s weight=%lld", f, r, t, (long long)weight);
puts(buf);
}
void engram_strengthen(el_val_t node_id) {
const char* id = EL_CSTR(node_id);
if (!id) id = "(null)";
char buf[256];
snprintf(buf, sizeof(buf), "[engram] strengthen: %s", id);
puts(buf);
}
/* ── Native VM builtin aliases ──────────────────────────────────────────────
* El source files use native_* names (El VM builtins).
* When compiled to C, these map directly to el_* runtime functions. */
el_val_t native_list_get(el_val_t list, el_val_t index) {
return el_list_get(list, index);
}
el_val_t native_list_len(el_val_t list) {
return el_list_len(list);
}
el_val_t native_list_append(el_val_t list, el_val_t elem) {
return el_list_append(list, elem);
}
el_val_t native_list_empty(void) {
return el_list_empty();
}
el_val_t native_string_chars(el_val_t sv) {
const char* s = EL_CSTR(sv);
el_val_t result = el_list_empty();
if (!s) return result;
while (*s) {
char buf[2];
buf[0] = *s;
buf[1] = '\0';
result = el_list_append(result, EL_STR(strdup(buf)));
s++;
}
return result;
}
el_val_t native_int_to_str(el_val_t n) {
return int_to_str(n);
}
/* ── Method-call shorthand aliases ──────────────────────────────────────────
* Short names that result from the method-call convention:
* myList.append(x) → append(myList, x)
* myList.len() → len(myList)
* myList.get(i) → get(myList, i)
* myMap.map_get(k) → map_get(myMap, k)
* myMap.map_set(k,v) → map_set(myMap, k, v) */
el_val_t append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); }
el_val_t len(el_val_t list) { return el_list_len(list); }
el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); }
el_val_t map_get(el_val_t map, el_val_t key) { return el_map_get(map, key); }
el_val_t map_set(el_val_t map, el_val_t key, el_val_t value) { return el_map_set(map, key, value); }