diff --git a/lang/dist/platform/elc b/lang/dist/platform/elc index 8af8093..f1cba65 100755 Binary files a/lang/dist/platform/elc and b/lang/dist/platform/elc differ diff --git a/lang/dist/platform/elc-new b/lang/dist/platform/elc-new index 8af8093..f1cba65 100755 Binary files a/lang/dist/platform/elc-new and b/lang/dist/platform/elc-new differ diff --git a/lang/el-compiler/runtime/el_runtime.c b/lang/el-compiler/runtime/el_runtime.c index d3d2aaa..df25f4a 100644 --- a/lang/el-compiler/runtime/el_runtime.c +++ b/lang/el-compiler/runtime/el_runtime.c @@ -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 ────────────────────────────────────────────────────── */ diff --git a/lang/el-compiler/runtime/el_runtime.h b/lang/el-compiler/runtime/el_runtime.h index 99cd89d..12ad99b 100644 --- a/lang/el-compiler/runtime/el_runtime.h +++ b/lang/el-compiler/runtime/el_runtime.h @@ -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 ────────────────────────────────────────────────────── */ diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index ca231ed..d8a78f6 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -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 { 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 { 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 { 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, indent: String, declared: [String]) -> [Strin cg_stmts(try_body, indent, native_list_clone(declared)) return declared } + + // assert , — 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) -> 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) -> 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], 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 ") + 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], 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], 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 { diff --git a/lang/el-compiler/src/compiler.el b/lang/el-compiler/src/compiler.el index ab7fded..a4f5fdc 100644 --- a/lang/el-compiler/src/compiler.el +++ b/lang/el-compiler/src/compiler.el @@ -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] = 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--. 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] []") + println("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] [--test] []") 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 { diff --git a/lang/el-compiler/src/parser.el b/lang/el-compiler/src/parser.el index 9c7c3e5..025ac90 100644 --- a/lang/el-compiler/src/parser.el +++ b/lang/el-compiler/src/parser.el @@ -1684,6 +1684,28 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map { }, p) } + // assert [ , ] + // 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`