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:
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -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 ────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────── */
|
||||
|
||||
|
||||
+212
-11
@@ -144,6 +144,53 @@ fn c_str_lit(s: String) -> String {
|
||||
"\"" + c_escape(s) + "\""
|
||||
}
|
||||
|
||||
// sanitize_test_name — convert a test name string to a valid C identifier fragment.
|
||||
// "int-to-str" -> "int_to_str", "lex empty" -> "lex_empty"
|
||||
fn sanitize_test_name(name: String) -> String {
|
||||
let n: Int = str_len(name)
|
||||
let i: Int = 0
|
||||
let out: String = ""
|
||||
while i < n {
|
||||
let code: Int = str_char_code(name, i)
|
||||
// a-z: 97-122, A-Z: 65-90, 0-9: 48-57 — keep; everything else -> '_'
|
||||
if code >= 97 {
|
||||
if code <= 122 {
|
||||
let out = out + str_char_at(name, i)
|
||||
} else {
|
||||
let out = out + "_"
|
||||
}
|
||||
} else {
|
||||
if code >= 65 {
|
||||
if code <= 90 {
|
||||
let out = out + str_char_at(name, i)
|
||||
} else {
|
||||
if code >= 48 {
|
||||
if code <= 57 {
|
||||
let out = out + str_char_at(name, i)
|
||||
} else {
|
||||
let out = out + "_"
|
||||
}
|
||||
} else {
|
||||
let out = out + "_"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if code >= 48 {
|
||||
if code <= 57 {
|
||||
let out = out + str_char_at(name, i)
|
||||
} else {
|
||||
let out = out + "_"
|
||||
}
|
||||
} else {
|
||||
let out = out + "_"
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// -- Type mapping --------------------------------------------------------------
|
||||
|
||||
fn el_type_to_c(type_str: String) -> String {
|
||||
@@ -437,6 +484,14 @@ fn cg_expr(expr: Map<String, Any>) -> String {
|
||||
|
||||
if kind == "Neg" {
|
||||
let inner = expr["inner"]
|
||||
let inner_kind: String = inner["expr"]
|
||||
// Float literal negation: emit el_from_float(-n) so the IEEE 754 sign
|
||||
// bit is set correctly. Arithmetic negation of the int64 bit pattern
|
||||
// (the el_val_t representation) produces garbage, not -f.
|
||||
if str_eq(inner_kind, "Float") {
|
||||
let fval: String = inner["value"]
|
||||
return "el_from_float(-" + fval + ")"
|
||||
}
|
||||
let inner_c: String = cg_expr(inner)
|
||||
return "(-" + inner_c + ")"
|
||||
}
|
||||
@@ -762,6 +817,14 @@ fn cg_expr(expr: Map<String, Any>) -> String {
|
||||
return "(" + left_c + " == " + right_c + ")"
|
||||
}
|
||||
}
|
||||
// Float literal or negative float literal: use plain == (bit-equal
|
||||
// el_val_t comparison). This handles `r0 == 3.0`, `neg == -3.0`, etc.
|
||||
if is_float_expr(left) {
|
||||
return "(" + left_c + " == " + right_c + ")"
|
||||
}
|
||||
if is_float_expr(right) {
|
||||
return "(" + left_c + " == " + right_c + ")"
|
||||
}
|
||||
if left_kind == "Str" {
|
||||
return "str_eq(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
@@ -813,6 +876,13 @@ fn cg_expr(expr: Map<String, Any>) -> String {
|
||||
return "(" + left_c + " != " + right_c + ")"
|
||||
}
|
||||
}
|
||||
// Float-typed operands use plain != (bit-equal comparison).
|
||||
if is_float_expr(left) {
|
||||
return "(" + left_c + " != " + right_c + ")"
|
||||
}
|
||||
if is_float_expr(right) {
|
||||
return "(" + left_c + " != " + right_c + ")"
|
||||
}
|
||||
if left_kind == "Str" {
|
||||
return "!str_eq(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
@@ -1508,6 +1578,26 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
cg_stmts(try_body, indent, native_list_clone(declared))
|
||||
return declared
|
||||
}
|
||||
|
||||
// assert <cond> , <msg> — test harness assertion
|
||||
if kind == "Assert" {
|
||||
let cond_node = stmt["cond"]
|
||||
let msg_node = stmt["msg"]
|
||||
let c_cond: String = cg_expr(cond_node)
|
||||
let c_msg: String = ""
|
||||
let msg_kind: String = msg_node["expr"]
|
||||
if str_eq(msg_kind, "Str") {
|
||||
let raw_msg: String = msg_node["value"]
|
||||
let c_msg = "\"" + c_escape(raw_msg) + "\""
|
||||
} else {
|
||||
let c_msg = "EL_STR_PTR(" + cg_expr(msg_node) + ")"
|
||||
}
|
||||
emit_line(indent + "if (!(" + c_cond + ")) {")
|
||||
emit_line(indent + " __el_test_fail(__el_cur_test, " + c_msg + "); __el_fail++;")
|
||||
emit_line(indent + "} else { __el_pass++; }")
|
||||
return declared
|
||||
}
|
||||
|
||||
declared
|
||||
}
|
||||
|
||||
@@ -2150,6 +2240,19 @@ fn is_int_expr(expr: Map<String, Any>) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// is_float_expr — true when expr is (or evaluates to) a Float-typed value.
|
||||
// Used in EqEq/NotEq codegen to avoid str_eq on float values.
|
||||
fn is_float_expr(expr: Map<String, Any>) -> Bool {
|
||||
let k: String = expr["expr"]
|
||||
if str_eq(k, "Float") { return true }
|
||||
if str_eq(k, "Neg") {
|
||||
let inner = expr["inner"]
|
||||
let ik: String = inner["expr"]
|
||||
if str_eq(ik, "Float") { return true }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// -- Capability-kind enforcement ----------------------------------------------
|
||||
//
|
||||
// A program's top-level block (cgi / service / none) determines which
|
||||
@@ -3481,6 +3584,25 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
let si = si + 1
|
||||
}
|
||||
|
||||
// In test mode: collect test function names for harness main().
|
||||
let test_is_mode: Bool = false
|
||||
let tmode_str: String = state_get("__test_mode")
|
||||
if str_eq(tmode_str, "1") { let test_is_mode = true }
|
||||
let test_names: [String] = native_list_empty()
|
||||
let test_c_names: [String] = native_list_empty()
|
||||
|
||||
// Emit test harness preamble (counters, fail printer) when in test mode.
|
||||
if test_is_mode {
|
||||
emit_line("#include <stdio.h>")
|
||||
emit_blank()
|
||||
emit_line("static int __el_pass = 0, __el_fail = 0;")
|
||||
emit_line("static const char *__el_cur_test = \"(none)\";")
|
||||
emit_line("static void __el_test_fail(const char *test, const char *msg) {")
|
||||
emit_line(" fprintf(stderr, \"FAIL %-40s %s\\n\", test, msg);")
|
||||
emit_line("}")
|
||||
emit_blank()
|
||||
}
|
||||
|
||||
// Streaming parse-emit loop.
|
||||
// For each parsed stmt:
|
||||
// - FnDef (not main): emit immediately via cg_fn, release AST
|
||||
@@ -3501,17 +3623,65 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
let stream_running = false
|
||||
} else {
|
||||
if str_eq(k, "Test") {
|
||||
// Skip test "name" { ... } blocks entirely.
|
||||
// The self-hosted compiler does not implement test execution.
|
||||
// Without this skip, the body `{ ... }` would be parsed as a Map
|
||||
// literal, building a huge AST with O(n²) string allocation that
|
||||
// causes OOM on any file containing test blocks.
|
||||
let p: Int = pos + 1
|
||||
let k_name: String = tok_kind(tokens, p)
|
||||
if str_eq(k_name, "Str") { let p = p + 1 }
|
||||
let k_body: String = tok_kind(tokens, p)
|
||||
if str_eq(k_body, "LBrace") { let p = skip_to_rbrace(tokens, p) }
|
||||
let pos = p
|
||||
if test_is_mode {
|
||||
// Compile test "name" { ... } block into a static void __el_test_NAME() function.
|
||||
let p: Int = pos + 1
|
||||
let test_name: String = "unnamed"
|
||||
if str_eq(tok_kind(tokens, p), "Str") {
|
||||
let test_name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
}
|
||||
let fn_c_name: String = "__el_test_" + sanitize_test_name(test_name)
|
||||
let test_names = native_list_append(test_names, test_name)
|
||||
let test_c_names = native_list_append(test_c_names, fn_c_name)
|
||||
// Emit the test function header.
|
||||
emit_line("static void " + fn_c_name + "(void) {")
|
||||
emit_line(" __el_cur_test = \"" + c_escape(test_name) + "\";")
|
||||
// Skip the opening LBrace and parse body statements.
|
||||
if str_eq(tok_kind(tokens, p), "LBrace") { let p = p + 1 }
|
||||
let body_decl: [String] = native_list_empty()
|
||||
let body_done: Bool = false
|
||||
while !body_done {
|
||||
let bk: String = tok_kind(tokens, p)
|
||||
if str_eq(bk, "RBrace") {
|
||||
let body_done = true
|
||||
} else {
|
||||
if str_eq(bk, "Eof") {
|
||||
let body_done = true
|
||||
} else {
|
||||
let br = parse_one(tokens, p)
|
||||
let bstmt = br["node"]
|
||||
let np: Int = br["pos"]
|
||||
el_release(br)
|
||||
if np > p {
|
||||
let body_arena: Any = el_arena_push()
|
||||
let body_decl = cg_stmt(bstmt, " ", body_decl)
|
||||
el_arena_pop(body_arena)
|
||||
el_release(bstmt)
|
||||
let p = np
|
||||
} else {
|
||||
let p = p + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip past closing RBrace.
|
||||
if str_eq(tok_kind(tokens, p), "RBrace") { let p = p + 1 }
|
||||
el_release(body_decl)
|
||||
emit_line("}")
|
||||
emit_blank()
|
||||
let pos = p
|
||||
} else {
|
||||
// Non-test mode: skip test blocks entirely to avoid OOM.
|
||||
// Without this skip, the body `{ ... }` would be parsed as a Map
|
||||
// literal, building a huge AST with O(n²) string allocation.
|
||||
let p: Int = pos + 1
|
||||
let k_name: String = tok_kind(tokens, p)
|
||||
if str_eq(k_name, "Str") { let p = p + 1 }
|
||||
let k_body: String = tok_kind(tokens, p)
|
||||
if str_eq(k_body, "LBrace") { let p = skip_to_rbrace(tokens, p) }
|
||||
let pos = p
|
||||
}
|
||||
} else {
|
||||
let r = parse_one(tokens, pos)
|
||||
let stmt = r["node"]
|
||||
@@ -3575,6 +3745,37 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
// Tokens fully consumed by the streaming loop — release now to free peak heap.
|
||||
el_release(tokens)
|
||||
|
||||
if test_is_mode {
|
||||
// Test mode: emit test harness main() that calls each collected test function.
|
||||
// Discard El's main body and top-level exec stmts (not needed in test harness).
|
||||
el_release(el_main_body)
|
||||
el_release(toplevel_exec_stmts)
|
||||
el_release(toplevel_let_names)
|
||||
el_release(sigs)
|
||||
|
||||
let test_arena_mark: Any = el_arena_push()
|
||||
emit_line("int main(int _argc, char **_argv) {")
|
||||
emit_line(" el_runtime_init_args(_argc, _argv);")
|
||||
let ti: Int = 0
|
||||
let tn: Int = native_list_len(test_c_names)
|
||||
while ti < tn {
|
||||
let tc_name: String = native_list_get(test_c_names, ti)
|
||||
emit_line(" " + tc_name + "();")
|
||||
let ti = ti + 1
|
||||
}
|
||||
emit_line(" printf(\"%d passed, %d failed\\n\", __el_pass, __el_fail);")
|
||||
emit_line(" return __el_fail;")
|
||||
emit_line("}")
|
||||
el_arena_pop(test_arena_mark)
|
||||
el_release(test_names)
|
||||
el_release(test_c_names)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Release test tracking lists (empty in non-test mode).
|
||||
el_release(test_names)
|
||||
el_release(test_c_names)
|
||||
|
||||
// Library detection: no fn main and no top-level executable stmts
|
||||
let is_library: Bool = false
|
||||
if !has_el_main {
|
||||
|
||||
@@ -41,6 +41,20 @@ fn compile(source: String) -> String {
|
||||
""
|
||||
}
|
||||
|
||||
// compile_test — like compile() but sets __test_mode so codegen_streaming
|
||||
// compiles test { } blocks instead of skipping them, and emits the test
|
||||
// harness main() instead of the normal int main().
|
||||
fn compile_test(source: String) -> String {
|
||||
state_set("__test_mode", "1")
|
||||
let top_mark: Any = el_arena_push()
|
||||
let tokens: [Any] = lex(source)
|
||||
let sigs: [Map<String, Any>] = scan_fn_sigs(tokens)
|
||||
codegen_streaming(tokens, sigs, source)
|
||||
el_arena_pop(top_mark)
|
||||
state_set("__test_mode", "")
|
||||
""
|
||||
}
|
||||
|
||||
// compile_js — full pipeline (JS target, module mode): source string -> JS source string
|
||||
fn compile_js(source: String) -> String {
|
||||
let tokens: [Any] = lex(source)
|
||||
@@ -159,6 +173,18 @@ fn detect_obfuscate(argv: [String]) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Detect --test flag in argv.
|
||||
fn detect_test(argv: [String]) -> Bool {
|
||||
let n: Int = native_list_len(argv)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let a: String = native_list_get(argv, i)
|
||||
if str_eq(a, "--test") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Build a unique temp file path: /tmp/elc-<pid>-<timestamp>.<suffix>
|
||||
fn make_temp_path(suffix: String) -> String {
|
||||
let pid: Int = getpid_now()
|
||||
@@ -488,6 +514,7 @@ fn main() -> Void {
|
||||
let do_bundle: Bool = detect_bundle(argv)
|
||||
let do_minify: Bool = detect_minify(argv)
|
||||
let do_obfuscate: Bool = detect_obfuscate(argv)
|
||||
let do_test: Bool = detect_test(argv)
|
||||
// --obfuscate implies --minify: obfuscating unminified code is pointless.
|
||||
if do_obfuscate {
|
||||
let do_minify = true
|
||||
@@ -495,7 +522,7 @@ fn main() -> Void {
|
||||
let positional: [String] = strip_flags(argv)
|
||||
let argc: Int = native_list_len(positional)
|
||||
if argc < 1 {
|
||||
println("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]")
|
||||
println("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] [--test] <source.el> [<output>]")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
@@ -532,6 +559,12 @@ fn main() -> Void {
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// --test mode: compile with test harness (C target only).
|
||||
if do_test {
|
||||
compile_test(source)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Standard path (no post-processing).
|
||||
let out: String = ""
|
||||
if do_bundle {
|
||||
|
||||
@@ -1684,6 +1684,28 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
}, p)
|
||||
}
|
||||
|
||||
// assert <cond_expr> [ , <msg_expr> ]
|
||||
// The message is optional — if the next token after the condition is not a
|
||||
// Comma, emit an empty string placeholder so the test still works.
|
||||
if k == "Assert" {
|
||||
let p: Int = pos + 1
|
||||
let cond_r = parse_expr(tokens, p)
|
||||
let cond_node = cond_r["node"]
|
||||
let p: Int = cond_r["pos"]
|
||||
el_release(cond_r)
|
||||
let after_k: String = tok_kind(tokens, p)
|
||||
if str_eq(after_k, "Comma") {
|
||||
let p = p + 1
|
||||
let msg_r = parse_expr(tokens, p)
|
||||
let msg_node = msg_r["node"]
|
||||
let p: Int = msg_r["pos"]
|
||||
el_release(msg_r)
|
||||
return make_result({ "stmt": "Assert", "cond": cond_node, "msg": msg_node }, p)
|
||||
}
|
||||
// No message — use empty string placeholder.
|
||||
return make_result({ "stmt": "Assert", "cond": cond_node, "msg": { "expr": "Str", "value": "" } }, p)
|
||||
}
|
||||
|
||||
// Bare reassignment: `name = expr`. Handled BEFORE the expression
|
||||
// fallback so we don't drop the assign on the floor and emit three
|
||||
// orphan expressions (the original silent-miscompile bug). El's `let`
|
||||
|
||||
Reference in New Issue
Block a user