Add --test mode to elc with Assert stmt and full native test suite passing

Implement compile_test() entry point that emits a C test harness instead
of a normal program. Test blocks (previously skipped) now compile to
static functions with per-assertion pass/fail tracking. Assert statement
added to parser and codegen. Runtime extended with now_ns, fs_list_json,
json_build_object, json_build_array, json_escape_string, state_has,
state_get_or. Fix float negation codegen, float equality comparisons,
time_to_parts return type (JSON string), time_format empty-fmt, json_set
raw-value semantics, state_keys JSON array return. All 310 native tests
pass across 9 suites (core, text, string, math, env, state, json, time, fs).
This commit is contained in:
Will Anderson
2026-05-06 14:33:47 -05:00
parent 6ced0f8009
commit ec889e1e53
7 changed files with 460 additions and 28 deletions
+185 -16
View File
@@ -2053,6 +2053,52 @@ el_val_t fs_list(el_val_t pathv) {
return lst;
}
/* fs_list_json — return directory entries as a JSON array of strings.
* Returns "[]" for missing or non-directory paths. Excludes "." and "..". */
el_val_t fs_list_json(el_val_t pathv) {
const char* path = EL_CSTR(pathv);
if (!path) return EL_STR("[]");
DIR* d = opendir(path);
if (!d) return EL_STR("[]");
/* Collect entries first so we can build the JSON in one pass. */
char** names = NULL;
size_t count = 0, cap = 0;
struct dirent* e;
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue;
if (count >= cap) {
cap = cap ? cap * 2 : 16;
names = realloc(names, cap * sizeof(char*));
if (!names) { closedir(d); return EL_STR("[]"); }
}
names[count++] = strdup(e->d_name);
}
closedir(d);
/* Build JSON array. */
size_t sz = 3; /* "[]" + NUL */
for (size_t i = 0; i < count; i++) sz += strlen(names[i]) * 2 + 6; /* conservative */
char* buf = malloc(sz);
if (!buf) { for (size_t i = 0; i < count; i++) free(names[i]); free(names); return EL_STR("[]"); }
size_t pos = 0;
buf[pos++] = '[';
for (size_t i = 0; i < count; i++) {
if (i > 0) buf[pos++] = ',';
buf[pos++] = '"';
for (const char* p = names[i]; *p; p++) {
if (*p == '"' || *p == '\\') buf[pos++] = '\\';
else if (*p == '\n') { buf[pos++] = '\\'; buf[pos++] = 'n'; continue; }
else if (*p == '\t') { buf[pos++] = '\\'; buf[pos++] = 't'; continue; }
buf[pos++] = *p;
}
buf[pos++] = '"';
free(names[i]);
}
free(names);
buf[pos++] = ']';
buf[pos] = '\0';
return el_wrap_str(buf);
}
/* fs_exists — true iff stat(path) succeeds. Symlinks are followed. */
el_val_t fs_exists(el_val_t pathv) {
const char* path = EL_CSTR(pathv);
@@ -3304,14 +3350,20 @@ el_val_t json_get_raw(el_val_t json_str, el_val_t key) {
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);
/* raw_val is the JSON value as-is (already encoded by the caller).
* If it looks like a plain (non-JSON) string, wrap it as a JSON string.
* Convention: callers pass pre-encoded values like "\"bob\"" for strings,
* "42" for numbers, "true"/"false" for booleans. */
const char* raw_val = EL_CSTR(value);
if (!k) k = "";
if (!raw_val) raw_val = "null";
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_puts(&b, raw_val);
jb_putc(&b, '}');
return el_wrap_str(b.buf);
}
@@ -3325,7 +3377,7 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) {
memcpy(b.buf + b.len, json, prefix);
b.len += prefix;
b.buf[b.len] = '\0';
jb_emit_value(&b, value);
jb_puts(&b, raw_val);
jb_puts(&b, end);
return el_wrap_str(b.buf);
}
@@ -3356,7 +3408,7 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) {
if (!empty) jb_putc(&b, ',');
jb_emit_escaped(&b, k);
jb_putc(&b, ':');
jb_emit_value(&b, value);
jb_puts(&b, raw_val);
/* Append from close_idx onward */
jb_puts(&b, json + close_idx);
return el_wrap_str(b.buf);
@@ -3437,6 +3489,87 @@ el_val_t json_array_get_string(el_val_t json_str, el_val_t index) {
return el_wrap_str(parsed);
}
/* json_escape_string — escape a string value for embedding in JSON.
* Returns the escaped content WITHOUT surrounding quotes.
* "say \"hello\"" -> "say \\\"hello\\\"" */
el_val_t json_escape_string(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return el_wrap_str(el_strdup(""));
size_t n = strlen(s);
/* Worst case: every char needs a 2-char escape. */
char* out = malloc(n * 2 + 1);
if (!out) return el_wrap_str(el_strdup(""));
size_t j = 0;
for (size_t i = 0; i < n; i++) {
unsigned char c = (unsigned char)s[i];
if (c == '"') { out[j++] = '\\'; out[j++] = '"'; }
else if (c == '\\') { out[j++] = '\\'; out[j++] = '\\'; }
else if (c == '\n') { out[j++] = '\\'; out[j++] = 'n'; }
else if (c == '\r') { out[j++] = '\\'; out[j++] = 'r'; }
else if (c == '\t') { out[j++] = '\\'; out[j++] = 't'; }
else { out[j++] = (char)c; }
}
out[j] = '\0';
el_val_t result = el_wrap_str(el_strdup(out));
free(out);
return result;
}
/* json_build_object — build a JSON object from a flat key-value list.
* kvs is [key0, val0, key1, val1, ...]. Values are raw JSON (pass
* strings as "\"value\"" or use json_escape_string). */
el_val_t json_build_object(el_val_t kvs) {
el_val_t list = kvs;
int64_t n = el_list_len(list);
JsonBuf b; jb_init(&b);
jb_putc(&b, '{');
int first = 1;
for (int64_t i = 0; i + 1 < n; i += 2) {
el_val_t k = el_list_get(list, (el_val_t)i);
el_val_t v = el_list_get(list, (el_val_t)(i + 1));
const char* ks = EL_CSTR(k);
const char* vs = EL_CSTR(v);
if (!ks || !vs) continue;
if (!first) jb_putc(&b, ',');
first = 0;
jb_putc(&b, '"');
jb_puts(&b, ks);
jb_puts(&b, "\":\"");
/* escape the value string */
size_t vn = strlen(vs);
for (size_t j = 0; j < vn; j++) {
unsigned char c = (unsigned char)vs[j];
if (c == '"') { jb_putc(&b, '\\'); jb_putc(&b, '"'); }
else if (c == '\\') { jb_putc(&b, '\\'); jb_putc(&b, '\\'); }
else if (c == '\n') { jb_putc(&b, '\\'); jb_putc(&b, 'n'); }
else if (c == '\r') { jb_putc(&b, '\\'); jb_putc(&b, 'r'); }
else if (c == '\t') { jb_putc(&b, '\\'); jb_putc(&b, 't'); }
else { jb_putc(&b, (char)c); }
}
jb_putc(&b, '"');
}
jb_putc(&b, '}');
return el_wrap_str(b.buf);
}
/* json_build_array — build a JSON array from a list of raw JSON values.
* items is ["\"alpha\"", "\"beta\"", "42", "true", ...]. */
el_val_t json_build_array(el_val_t items) {
el_val_t list = items;
int64_t n = el_list_len(list);
JsonBuf b; jb_init(&b);
jb_putc(&b, '[');
for (int64_t i = 0; i < n; i++) {
el_val_t v = el_list_get(list, (el_val_t)i);
const char* vs = EL_CSTR(v);
if (!vs) continue;
if (i > 0) jb_putc(&b, ',');
jb_puts(&b, vs);
}
jb_putc(&b, ']');
return el_wrap_str(b.buf);
}
/* ── Time ────────────────────────────────────────────────────────────────── */
el_val_t time_now(void) {
@@ -3458,7 +3591,7 @@ el_val_t time_format(el_val_t ts, el_val_t fmt) {
struct tm tm;
gmtime_r(&s, &tm);
const char* fmt_str = EL_CSTR(fmt);
if (!fmt_str || strcmp(fmt_str, "ISO") == 0) {
if (!fmt_str || *fmt_str == '\0' || 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,
@@ -3477,15 +3610,13 @@ el_val_t time_to_parts(el_val_t ts) {
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;
/* Return a JSON string so callers can use json_get to extract fields. */
char buf[256];
snprintf(buf, sizeof(buf),
"{\"year\":%d,\"month\":%d,\"day\":%d,\"hour\":%d,\"minute\":%d,\"second\":%d,\"ms\":%d}",
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));
}
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz) {
@@ -3591,6 +3722,12 @@ el_val_t now(void) {
return el_now_instant();
}
/* now_ns — return current Unix time as nanoseconds (Int).
* Thin wrapper over el_now_instant for use in test timing. */
el_val_t now_ns(void) {
return el_now_instant();
}
/* unix_seconds(n) — Instant from a Unix-epoch second count.
* unix_millis(n) Instant from a Unix-epoch millisecond count. */
el_val_t unix_seconds(el_val_t n) {
@@ -4818,12 +4955,44 @@ el_val_t state_del(el_val_t key) {
el_val_t state_keys(void) {
pthread_mutex_lock(&_state_mu);
el_val_t lst = el_list_empty();
/* Build a JSON array string: ["key1","key2",...] */
JsonBuf b; jb_init(&b);
jb_putc(&b, '[');
for (size_t i = 0; i < _state_count; i++) {
lst = el_list_append(lst, el_wrap_str(el_strdup(_state_entries[i].key)));
if (i > 0) jb_putc(&b, ',');
jb_putc(&b, '"');
jb_emit_escaped(&b, _state_entries[i].key);
jb_putc(&b, '"');
}
jb_putc(&b, ']');
pthread_mutex_unlock(&_state_mu);
return el_wrap_str(b.buf);
}
/* Returns 1 (true) if the key is present in the state store, else 0 (false). */
el_val_t state_has(el_val_t key) {
const char* k = EL_CSTR(key);
if (!k) return 0;
pthread_mutex_lock(&_state_mu);
StateEntry* e = state_find(k);
int found = (e != NULL) ? 1 : 0;
pthread_mutex_unlock(&_state_mu);
return (el_val_t)found;
}
/* Returns the value for key, or default_val if the key is absent. */
el_val_t state_get_or(el_val_t key, el_val_t default_val) {
const char* k = EL_CSTR(key);
if (!k) return default_val;
pthread_mutex_lock(&_state_mu);
StateEntry* e = state_find(k);
if (e) {
char* copy = el_strdup(e->value);
pthread_mutex_unlock(&_state_mu);
return el_wrap_str(copy);
}
pthread_mutex_unlock(&_state_mu);
return lst;
return default_val;
}
/* ── Float formatting ────────────────────────────────────────────────────── */
+7
View File
@@ -231,6 +231,7 @@ el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
el_val_t fs_read(el_val_t path);
el_val_t fs_write(el_val_t path, el_val_t content);
el_val_t fs_list(el_val_t path);
el_val_t fs_list_json(el_val_t path);
el_val_t fs_exists(el_val_t path);
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
@@ -260,6 +261,9 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
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 json_escape_string(el_val_t sv);
el_val_t json_build_object(el_val_t kvs);
el_val_t json_build_array(el_val_t items);
/* ── Time ────────────────────────────────────────────────────────────────── */
@@ -272,6 +276,7 @@ el_val_t time_to_parts(el_val_t ts);
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
el_val_t now_ns(void);
/* ── Instant + Duration: first-class temporal types ──────────────────────────
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
@@ -428,6 +433,8 @@ 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 state_has(el_val_t key);
el_val_t state_get_or(el_val_t key, el_val_t default_val);
/* ── Float formatting ────────────────────────────────────────────────────── */