Merge PR #4: perf: 81% RSS reduction in elc compiler + --test mode

This commit is contained in:
Will Anderson
2026-05-06 14:35:54 -05:00
9 changed files with 1749 additions and 343 deletions
+3 -3
View File
@@ -308,7 +308,7 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes")))) {
return route_create_node(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes")) || str_eq(clean, EL_STR("/nodes/list")) || str_eq(clean, EL_STR("/api/nodes/list")))) {
if (str_eq(method, EL_STR("GET")) && (((str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes"))) || str_eq(clean, EL_STR("/nodes/list"))) || str_eq(clean, EL_STR("/api/nodes/list")))) {
return route_scan_nodes(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/edges")) || str_eq(clean, EL_STR("/edges")))) {
@@ -351,8 +351,8 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
return 0;
}
int main(int argc, char** argv) {
el_runtime_init_args(argc, argv);
int main(int _argc, char** _argv) {
el_runtime_init_args(_argc, _argv);
bind_str = env(EL_STR("ENGRAM_BIND"));
if (str_eq(bind_str, EL_STR(""))) {
bind_str = EL_STR(":8742");
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+262 -50
View File
@@ -42,7 +42,9 @@
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
#ifdef HAVE_CURL
#include <curl/curl.h>
#endif
/* ── Internal allocators ─────────────────────────────────────────────────── */
@@ -739,6 +741,39 @@ struct JsonParser {
* the loop is observable.
*/
/* ── JSON error helper (used by HTTP, PQ, crypto stubs) ─────────────────── */
/* JSON-escape an arbitrary C string into an allocated buffer. */
static char* json_escape_alloc(const char* s) {
if (!s) return el_strdup("");
JsonBuf b; jb_init(&b);
for (const char* p = s; *p; p++) {
unsigned char c = (unsigned char)*p;
switch (c) {
case '"': jb_puts(&b, "\\\""); break;
case '\\': jb_puts(&b, "\\\\"); 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);
}
}
return b.buf;
}
static el_val_t http_error_json(const char* msg) {
char* esc = json_escape_alloc(msg ? msg : "unknown error");
char* buf = el_strbuf(strlen(esc) + 16);
sprintf(buf, "{\"error\":\"%s\"}", esc);
free(esc);
return el_wrap_str(buf);
}
#ifdef HAVE_CURL
/* ── HTTP client write-callback buffer ───────────────────────────────────── */
typedef struct {
@@ -772,36 +807,6 @@ static size_t http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) {
return n;
}
/* JSON-escape an arbitrary C string into an allocated buffer. */
static char* json_escape_alloc(const char* s) {
if (!s) return el_strdup("");
JsonBuf b; jb_init(&b);
for (const char* p = s; *p; p++) {
unsigned char c = (unsigned char)*p;
switch (c) {
case '"': jb_puts(&b, "\\\""); break;
case '\\': jb_puts(&b, "\\\\"); 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);
}
}
return b.buf;
}
static el_val_t http_error_json(const char* msg) {
char* esc = json_escape_alloc(msg ? msg : "unknown error");
char* buf = el_strbuf(strlen(esc) + 16);
sprintf(buf, "{\"error\":\"%s\"}", esc);
free(esc);
return el_wrap_str(buf);
}
/* HTTP timeout (ms) — read once from EL_HTTP_TIMEOUT_MS, default 60000.
* Applied via CURLOPT_TIMEOUT_MS on every libcurl request. */
static long _el_http_timeout_ms = -1;
@@ -1009,6 +1014,7 @@ el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el
if (h) curl_slist_free_all(h);
return r;
}
#endif /* HAVE_CURL */
/* ── HTTP server (POSIX sockets + pthreads) ──────────────────────────────── */
@@ -2047,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);
@@ -3298,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);
}
@@ -3319,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);
}
@@ -3350,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);
@@ -3431,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) {
@@ -3452,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,
@@ -3471,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) {
@@ -3585,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) {
@@ -4812,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 ────────────────────────────────────────────────────── */
@@ -7315,8 +7490,10 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di
free(frontier); free(frontier_h); free(visited);
jb_putc(&b, ']'); return el_wrap_str(b.buf);
}
frontier[fc] = el_strdup(sid); frontier_h[fc] = 0; fc++;
visited[vc++] = el_strdup(sid);
/* Use plain strdup (not el_strdup) so arena doesn't track these pointers.
* The BFS loop manually frees them below arena would double-free them. */
frontier[fc] = strdup(sid); frontier_h[fc] = 0; fc++;
visited[vc++] = strdup(sid);
int first = 1;
while (fc > 0) {
@@ -7345,8 +7522,8 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di
char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(h + 1));
jb_puts(&b, tmp);
first = 0;
if (vc < 1024) visited[vc++] = el_strdup(peer);
if (fc < 1024 && h + 1 < depth) { frontier[fc] = el_strdup(peer); frontier_h[fc] = h + 1; fc++; }
if (vc < 1024) visited[vc++] = strdup(peer);
if (fc < 1024 && h + 1 < depth) { frontier[fc] = strdup(peer); frontier_h[fc] = h + 1; fc++; }
}
free(cur);
}
@@ -7586,6 +7763,7 @@ el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) {
return el_wrap_str(b.buf);
}
#ifdef HAVE_CURL
/* ── DHARMA network ─────────────────────────────────────────────────────────
* Real implementation. Peers are addressed by `dharma_id` either bare
* (e.g. "ntn-genesis", transport defaults to http://localhost:7770) or
@@ -8097,6 +8275,7 @@ el_val_t dharma_peers(void) {
free(peers);
return out;
}
#endif /* HAVE_CURL — DHARMA network */
/* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */
/*
@@ -8111,6 +8290,7 @@ el_val_t dharma_peers(void) {
* and returns a JSON-string el_val_t result. Iteration is capped at 10.
*/
#ifdef HAVE_CURL
static const char* LLM_DEFAULT_MODEL = "claude-sonnet-4-5";
static const char* LLM_API_URL = "https://api.anthropic.com/v1/messages";
static const char* LLM_VERSION = "2023-06-01";
@@ -8796,6 +8976,7 @@ el_val_t llm_models(void) {
lst = el_list_append(lst, el_wrap_str(el_strdup("claude-haiku-4-5")));
return lst;
}
#endif /* HAVE_CURL */
/* ── Native VM builtin aliases ──────────────────────────────────────────────
* El source files use native_* names (El VM builtins).
@@ -9986,6 +10167,7 @@ el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_
#endif /* __has_include(<openssl/evp.h>) */
#ifdef HAVE_CURL
/* ────────────────────────────────────────────────────────────────────────────
* OTLP/HTTP observability logs, traces, metrics
*
@@ -10344,6 +10526,8 @@ el_val_t emit_event(el_val_t name_v, el_val_t duration_ms_v) {
return trace_span_end(h);
}
#endif /* HAVE_CURL — OTLP */
/* ── Threading seed primitives ───────────────────────────────────────────────
* __thread_create(fn_name, arg) -> Int spawn El fn in a pthread, return tid
* __thread_join(tid) -> String join thread, return result string
@@ -10798,6 +10982,7 @@ el_val_t config(el_val_t key_v) {
return el_wrap_str(el_strdup(val));
}
#ifdef HAVE_CURL
/* http_patch — HTTP PATCH request with Content-Type: application/json.
* Returns the response body (same error convention as http_post_json). */
el_val_t http_patch(el_val_t url_v, el_val_t body_v) {
@@ -10913,6 +11098,7 @@ el_val_t http_get_engram(el_val_t url_v, el_val_t key_v) {
}
return el_wrap_str(rb.data);
}
#endif /* HAVE_CURL */
/* str_to_bytes — encode a string as a JSON array of unsigned byte values.
* "hello" -> "[104,101,108,108,111]"
@@ -10991,3 +11177,29 @@ el_val_t hash_sha256(el_val_t sv) {
return el_hex_encode(digest, 32);
}
#ifndef HAVE_CURL
/* ── HAVE_CURL=0 stubs — compile without -lcurl for the elc CLI binary. ───── *
* These return a JSON error string so El programs get a clear message if they
* call HTTP/LLM functions in a curl-less build. */
static el_val_t _no_curl_err(void) {
return el_wrap_str(el_strdup("{\"error\":\"not built with HAVE_CURL\"}"));
}
el_val_t http_get(el_val_t url) { (void)url; return _no_curl_err(); }
el_val_t http_post(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); }
el_val_t http_post_json(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); }
el_val_t http_get_with_headers(el_val_t url, el_val_t h) { (void)url; (void)h; return _no_curl_err(); }
el_val_t http_post_with_headers(el_val_t url, el_val_t b, el_val_t h) { (void)url; (void)b; (void)h; return _no_curl_err(); }
el_val_t http_post_form_auth(el_val_t url, el_val_t b, el_val_t a) { (void)url; (void)b; (void)a; return _no_curl_err(); }
el_val_t http_delete(el_val_t url) { (void)url; return _no_curl_err(); }
el_val_t http_patch(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); }
el_val_t http_get_to_file(el_val_t url, el_val_t h, el_val_t p) { (void)url; (void)h; (void)p; return _no_curl_err(); }
el_val_t http_post_to_file(el_val_t url, el_val_t b, el_val_t h, el_val_t p) { (void)url; (void)b; (void)h; (void)p; return _no_curl_err(); }
el_val_t http_post_engram(el_val_t url, el_val_t k, el_val_t b) { (void)url; (void)k; (void)b; return _no_curl_err(); }
el_val_t http_get_engram(el_val_t url, el_val_t k) { (void)url; (void)k; return _no_curl_err(); }
el_val_t llm_call(el_val_t m, el_val_t p) { (void)m; (void)p; return _no_curl_err(); }
el_val_t llm_call_system(el_val_t m, el_val_t s, el_val_t u) { (void)m; (void)s; (void)u; return _no_curl_err(); }
el_val_t llm_call_agentic(el_val_t m, el_val_t s, el_val_t u, el_val_t t) { (void)m; (void)s; (void)u; (void)t; return _no_curl_err(); }
el_val_t llm_vision(el_val_t m, el_val_t s, el_val_t p, el_val_t i) { (void)m; (void)s; (void)p; (void)i; return _no_curl_err(); }
el_val_t llm_models(void) { return el_list_empty(); }
void llm_register_tool(el_val_t n, el_val_t f) { (void)n; (void)f; }
#endif /* !HAVE_CURL */
+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 ────────────────────────────────────────────────────── */
+660 -19
View File
@@ -38,10 +38,13 @@ fn is_hex_digit_byte(b: Int) -> Bool {
}
fn c_escape(s: String) -> String {
// Use index-based byte scanning via str_char_code(s, i) and str_char_at(s, i).
// This avoids native_string_chars + str_join, which corrupts high-byte (>= 0x80)
// characters because list_join's looks_like_string heuristic rejects strings
// whose first byte is >= 0x7F and emits them as decimal pointer values instead.
// Batch ASCII chars using str_slice instead of str_char_at per byte.
// Track clean_start: the beginning of the current run of bytes that need
// no escaping. On each special byte, flush the accumulated clean run via
// str_slice, then append the escape. This reduces parts-list appends from
// O(N) to O(K) where K = number of special bytes << N for normal strings.
//
// Special bytes: '"'=34, '\\'=92, '\n'=10, '\r'=13, '\t'=9, any byte>=128.
//
// IMPORTANT: after a \xNN hex escape, if the next byte is a hex digit
// (0-9, a-f, A-F), we emit `""` to split the C string literal so the C
@@ -51,46 +54,75 @@ fn c_escape(s: String) -> String {
let total: Int = str_len(s)
let parts: [String] = native_list_empty()
let i: Int = 0
let clean_start: Int = 0
let prev_was_hex_escape: Bool = false
while i < total {
let bval: Int = str_char_code(s, i)
// If the previous token was a \xNN escape and the current byte is a
// hex digit, insert an empty string literal ("") to break the escape.
// Handle the hex-escape split case first: if prev was \xNN and this
// byte is a hex digit, we must flush the clean run and insert "".
// (At this point clean_start == i since the previous special byte
// already reset it, so flush is a no-op unless something is pending.)
if prev_was_hex_escape {
if is_hex_digit_byte(bval) {
// Flush any accumulated clean bytes before the split marker.
if clean_start < i {
let parts = native_list_append(parts, str_slice(s, clean_start, i))
}
let parts = native_list_append(parts, "\"\"")
let clean_start = i
}
}
let prev_was_hex_escape = false
if bval == 34 {
// 34 = '"'
// 34 = '"' — flush clean run, then escape
if clean_start < i {
let parts = native_list_append(parts, str_slice(s, clean_start, i))
}
let parts = native_list_append(parts, "\\\"")
let clean_start = i + 1
} else {
if bval == 92 {
// 92 = '\\'
if clean_start < i {
let parts = native_list_append(parts, str_slice(s, clean_start, i))
}
let parts = native_list_append(parts, "\\\\")
let clean_start = i + 1
} else {
if bval == 10 {
// 10 = '\n'
if clean_start < i {
let parts = native_list_append(parts, str_slice(s, clean_start, i))
}
let parts = native_list_append(parts, "\\n")
let clean_start = i + 1
} else {
if bval == 13 {
// 13 = '\r'
if clean_start < i {
let parts = native_list_append(parts, str_slice(s, clean_start, i))
}
let parts = native_list_append(parts, "\\r")
let clean_start = i + 1
} else {
if bval == 9 {
// 9 = '\t'
if clean_start < i {
let parts = native_list_append(parts, str_slice(s, clean_start, i))
}
let parts = native_list_append(parts, "\\t")
let clean_start = i + 1
} else {
if bval >= 128 {
// Escape non-ASCII bytes (>= 0x80) as \xNN so
// Clang does not misinterpret multi-byte UTF-8
// sequences in C string literals.
// Non-ASCII: flush, then \xNN
if clean_start < i {
let parts = native_list_append(parts, str_slice(s, clean_start, i))
}
let parts = native_list_append(parts, "\\x" + byte_to_hex2(bval))
let prev_was_hex_escape = true
} else {
let parts = native_list_append(parts, str_char_at(s, i))
let clean_start = i + 1
}
// else: plain ASCII extends the current clean run (no append)
}
}
}
@@ -98,13 +130,67 @@ fn c_escape(s: String) -> String {
}
let i = i + 1
}
str_join(parts, "")
// Flush the final clean run if any
if clean_start < total {
let parts = native_list_append(parts, str_slice(s, clean_start, total))
}
let result: String = str_join(parts, "")
// parts list fully consumed release to free peak heap.
el_release(parts)
result
}
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 {
@@ -398,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 + ")"
}
@@ -723,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 + ")"
}
@@ -774,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 + ")"
}
@@ -813,6 +922,8 @@ fn cg_expr(expr: Map<String, Any>) -> String {
let i = i + 1
}
let args_c: String = str_join(args_parts, ", ")
// args_parts list fully consumed release to free peak heap.
el_release(args_parts)
if func_kind == "Ident" {
let fn_name: String = func["name"]
@@ -917,7 +1028,10 @@ fn cg_expr(expr: Map<String, Any>) -> String {
let items_parts = native_list_append(items_parts, elem_c)
let i = i + 1
}
return "el_list_new(" + native_int_to_str(n) + ", " + str_join(items_parts, ", ") + ")"
let items_joined: String = str_join(items_parts, ", ")
// items_parts fully consumed release to free peak heap.
el_release(items_parts)
return "el_list_new(" + native_int_to_str(n) + ", " + items_joined + ")"
}
if kind == "Map" {
@@ -938,7 +1052,10 @@ fn cg_expr(expr: Map<String, Any>) -> String {
let items_parts = native_list_append(items_parts, c_str_lit(key) + ", " + val_c)
let i = i + 1
}
return "el_map_new(" + native_int_to_str(n) + ", " + str_join(items_parts, ", ") + ")"
let items_joined: String = str_join(items_parts, ", ")
// items_parts fully consumed release to free peak heap.
el_release(items_parts)
return "el_map_new(" + native_int_to_str(n) + ", " + items_joined + ")"
}
if kind == "Try" {
@@ -1041,7 +1158,10 @@ fn cg_match(expr: Map<String, Any>) -> String {
let i = i + 1
}
let parts = native_list_append(parts, done_label + ":; " + result_var + "; })")
str_join(parts, "")
let result: String = str_join(parts, "")
// parts list fully consumed release to free peak heap.
el_release(parts)
result
}
// Lower a match statement (used for side effects, not as an expression) to a
@@ -1210,7 +1330,10 @@ fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
}
let i = i + 1
}
str_join(parts, "")
let result: String = str_join(parts, "")
// parts list fully consumed release to free peak heap.
el_release(parts)
result
}
fn cg_if_expr(expr: Map<String, Any>) -> String {
@@ -1455,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
}
@@ -1546,7 +1689,11 @@ fn cg_stmts(stmts: [Map<String, Any>], indent: String, declared: [String]) -> [S
let decl = declared
while i < n {
let stmt = native_list_get(stmts, i)
// Per-statement arena scope: free intermediate strings (str_concat
// fragments, cg_expr results) after each statement is emitted.
let s_mark: Any = el_arena_push()
let decl = cg_stmt(stmt, indent, decl)
el_arena_pop(s_mark)
let i = i + 1
}
decl
@@ -1570,7 +1717,10 @@ fn params_to_c(params: [Map<String, Any>]) -> String {
let parts = native_list_append(parts, decl)
let i = i + 1
}
str_join(parts, ", ")
let result: String = str_join(parts, ", ")
// parts list fully consumed release to free peak heap.
el_release(parts)
result
}
// Transform a function body so that an implicit-return final expression
@@ -2090,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
@@ -2539,6 +2702,9 @@ fn builtin_arity(name: String) -> Int {
if str_eq(name, "__channel_recv") { return 1 }
if str_eq(name, "__channel_try_recv") { return 1 }
if str_eq(name, "__channel_close") { return 1 }
// Arena mark/restore builtins
if str_eq(name, "el_arena_push") { return 0 }
if str_eq(name, "el_arena_pop") { return 1 }
// -1 sentinel: variadic / unknown / user-defined -> no check.
return -1
}
@@ -2775,7 +2941,8 @@ fn cg_fn(stmt: Map<String, Any>) -> Void {
if !str_eq(ret_type, "Void") {
let body_xformed = transform_implicit_return(body)
}
cg_stmts(body_xformed, " ", decl)
let final_decl = cg_stmts(body_xformed, " ", decl)
el_release(final_decl)
emit_line(" return 0;")
emit_line("}")
emit_blank()
@@ -3249,3 +3416,477 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
// Return empty string - output was streamed via println
""
}
// Streaming codegen (JIT function-at-a-time)
//
// codegen_streaming is a memory-efficient alternative to codegen().
// Instead of receiving the full parsed AST, it receives the raw token list
// and a pre-scanned signature list (from scan_fn_sigs in parser.el).
//
// Pipeline:
// 1. Scan phase (already done by caller): scan_fn_sigs(tokens) -> sigs
// 2. Emit preamble using sigs (no full AST needed)
// 3. For each top-level statement:
// parse_one(tokens, pos) -> { node, pos }
// cg_decl_streaming(node) <- emit C for this one decl
// el_release(node) <- discard AST immediately
//
// Peak memory: O(one function's AST) instead of O(whole program AST).
//
// Entry point: codegen_streaming(tokens, sigs, source) -> String
// cg_decl_streaming emit C for a single top-level declaration.
// Handles FnDef, ExternFn, TypeDef, EnumDef, Import, CgiBlock, ServiceBlock.
// Top-level Let statements go into the main() body, not here.
// Top-level executable statements (non-fn, non-let, non-decl) are
// accumulated into state and emitted later in main().
fn cg_decl_streaming(stmt: Map<String, Any>) -> Void {
let sk: String = stmt["stmt"]
if str_eq(sk, "FnDef") {
cg_fn(stmt)
return
}
// All other top-level decl kinds are either no-ops (Import, TypeDef,
// EnumDef, ExternFn forward decl already emitted) or capability markers
// (CgiBlock, ServiceBlock already handled in preamble).
// Top-level Lets are also no-ops here (file-scope slots already emitted).
// Executable top-level stmts (Expr, Return, etc.) are accumulated in state.
if !str_eq(sk, "FnDef") {
if !is_top_level_decl(stmt) {
if !str_eq(sk, "Let") {
// This is an executable top-level statement.
// We can't emit it into main() yet because we haven't started
// emitting main(). Accumulate in state as a list index.
// We'll collect these into a list and emit after all fns.
state_set("__streaming_has_toplevel_stmts", "1")
}
}
}
}
// emit_streaming_preamble emit #includes, forward decls, and file-scope lets
// using the pre-scanned signature data (no full AST).
fn emit_streaming_preamble(sigs: [Map<String, Any>], source: String) -> Void {
let n: Int = native_list_len(sigs)
// Detect program kind from sigs
let cgi_count: Int = 0
let svc_count: Int = 0
let i: Int = 0
while i < n {
let sig = native_list_get(sigs, i)
let sk: String = sig["kind"]
if str_eq(sk, "cgi_block") { let cgi_count = cgi_count + 1 }
if str_eq(sk, "service_block") { let svc_count = svc_count + 1 }
let i = i + 1
}
if cgi_count > 1 {
emit_line("#error \"El: multiple cgi blocks in program (only one allowed)\"")
}
if svc_count > 1 {
emit_line("#error \"El: multiple service blocks in program (only one allowed)\"")
}
if cgi_count >= 1 {
if svc_count >= 1 {
emit_line("#error \"El: program declares both cgi and service blocks (mutually exclusive - pick one)\"")
}
}
let kind: String = "utility"
if cgi_count >= 1 { let kind = "cgi" }
if svc_count >= 1 { let kind = "service" }
state_set("__program_kind", kind)
state_set("__cap_violations", "")
state_set("__arity_violations", "")
state_set("__time_violations", "")
emit_line("#include <stdint.h>")
emit_line("#include <stdlib.h>")
emit_line("#include \"el_runtime.h\"")
emit_blank()
// Forward declarations use pre-computed params_c strings from scan.
let i = 0
while i < n {
let sig = native_list_get(sigs, i)
let sk: String = sig["kind"]
if str_eq(sk, "fn") {
let fn_name: String = sig["name"]
if !str_eq(fn_name, "main") {
let params_c: String = sig["params_c"]
emit_line("el_val_t " + fn_name + "(" + params_c + ");")
}
}
if str_eq(sk, "extern_fn") {
let fn_name: String = sig["name"]
let params_c: String = sig["params_c"]
emit_line("el_val_t " + fn_name + "(" + params_c + ");")
}
let i = i + 1
}
emit_blank()
// File-scope let slots
let has_toplevel_lets: Bool = false
let i = 0
while i < n {
let sig = native_list_get(sigs, i)
let sk: String = sig["kind"]
if str_eq(sk, "toplevel_let") {
let name: String = sig["name"]
let ltype: String = sig["ltype"]
if str_eq(ltype, "Int") { add_int_name(name) }
emit_line("el_val_t " + name + ";")
let has_toplevel_lets = true
}
let i = i + 1
}
if has_toplevel_lets { emit_blank() }
}
// codegen_streaming JIT function-at-a-time compiler backend.
// tokens: flat token list from lex()
// sigs: pre-scanned signature list from scan_fn_sigs(tokens)
// source: original source string (for string literal lookup)
fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) -> String {
let total_tokens: Int = native_list_len(tokens) / 2
// Emit preamble (forward decls, file-scope lets, #includes)
// Arena scope: free intermediate strings built during preamble emission.
let preamble_mark: Any = el_arena_push()
emit_streaming_preamble(sigs, source)
el_arena_pop(preamble_mark)
// Detect whether there is a fn main() and whether there are top-level
// executable stmts (for library detection) from sigs.
let has_el_main: Bool = false
let ns: Int = native_list_len(sigs)
let si: Int = 0
while si < ns {
let sig = native_list_get(sigs, si)
let sk2: String = sig["kind"]
if str_eq(sk2, "fn") {
let fn_name_chk: String = sig["name"]
if str_eq(fn_name_chk, "main") { let has_el_main = true }
}
let si = si + 1
}
// Collect top-level let names for seeding main()'s declared set.
let toplevel_let_names: [String] = native_list_empty()
let si = 0
while si < ns {
let sig = native_list_get(sigs, si)
let sk2: String = sig["kind"]
if str_eq(sk2, "toplevel_let") {
let tname: String = sig["name"]
let toplevel_let_names = native_list_append(toplevel_let_names, tname)
}
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
// - Others: accumulate only fn-main body and top-level executable stmts
// (these are small in count relative to fn bodies)
let pos: Int = 0
let el_main_body: [Map<String, Any>] = native_list_empty()
let toplevel_exec_stmts: [Map<String, Any>] = native_list_empty()
let has_toplevel_exec: Bool = false
let stream_running: Bool = true
while stream_running {
if pos >= total_tokens {
let stream_running = false
} else {
let k: String = tok_kind(tokens, pos)
if str_eq(k, "Eof") {
let stream_running = false
} else {
if str_eq(k, "Test") {
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"]
let new_pos: Int = r["pos"]
el_release(r)
// Guard against infinite loops
if new_pos <= pos {
el_release(stmt)
let pos = pos + 1
} else {
let sk: String = stmt["stmt"]
if str_eq(sk, "FnDef") {
let fn_name2: String = stmt["name"]
if str_eq(fn_name2, "main") {
// Capture main() body for later
let body = stmt["body"]
let bn: Int = native_list_len(body)
let bi: Int = 0
while bi < bn {
let el_main_body = native_list_append(el_main_body, native_list_get(body, bi))
let bi = bi + 1
}
el_release(stmt)
} else {
// Emit immediately this is the JIT core
// Arena scope: free all intermediate strings (str_concat,
// int_to_str, cg_expr fragments) after each function.
let fn_arena_mark: Any = el_arena_push()
cg_fn(stmt)
el_release(stmt)
el_arena_pop(fn_arena_mark)
}
} else {
if is_top_level_decl(stmt) {
// Import, TypeDef, EnumDef, CgiBlock, ServiceBlock, ExternFn
// These are no-ops in codegen (forward decls already emitted)
el_release(stmt)
} else {
if str_eq(sk, "Let") {
// Top-level let: file-scope slot already declared.
// Keep for main() init these are few and small.
let toplevel_exec_stmts = native_list_append(toplevel_exec_stmts, stmt)
let has_toplevel_exec = true
} else {
// Executable top-level stmt (rare)
let toplevel_exec_stmts = native_list_append(toplevel_exec_stmts, stmt)
let has_toplevel_exec = true
}
}
}
let pos = new_pos
}
}
}
}
}
// 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 {
if !has_toplevel_exec {
let is_library = true
}
}
if is_library { return "" }
// Emit main() wrap in arena scope to free intermediate strings.
let main_arena_mark: Any = el_arena_push()
let kind2: String = state_get("__program_kind")
emit_line("int main(int _argc, char** _argv) {")
emit_line(" el_runtime_init_args(_argc, _argv);")
// cgi init if needed
let ns2: Int = native_list_len(sigs)
let si2: Int = 0
while si2 < ns2 {
let sig2 = native_list_get(sigs, si2)
let sk3: String = sig2["kind"]
if str_eq(sk3, "cgi_block") {
// We need the full cgi_block data it was parsed by scan_fn_sigs
// but scan only stored the name. For cgi_init we need dharma_id etc.
// Since cgi blocks are rare and small, they end up in toplevel_exec_stmts.
// Find the CgiBlock in toplevel_exec_stmts.
let tes_n: Int = native_list_len(toplevel_exec_stmts)
let tes_i: Int = 0
while tes_i < tes_n {
let tes = native_list_get(toplevel_exec_stmts, tes_i)
let tes_k: String = tes["stmt"]
if str_eq(tes_k, "CgiBlock") {
let cname2: String = tes["name"]
let cdid2: String = tes["dharma_id"]
let cprin2: String = tes["principal"]
let cnet2: String = tes["network"]
let ceng2: String = tes["engram"]
let has_did2: Bool = tes["has_dharma_id"]
let has_prin2: Bool = tes["has_principal"]
let has_net2: Bool = tes["has_network"]
let has_eng2: Bool = tes["has_engram"]
let arg_name2: String = "EL_STR(" + c_str_lit(cname2) + ")"
let arg_did2: String = cgi_arg(cdid2, has_did2)
let arg_prin2: String = cgi_arg(cprin2, has_prin2)
let arg_net2: String = cgi_arg(cnet2, has_net2)
let arg_eng2: String = cgi_arg(ceng2, has_eng2)
emit_line(" el_cgi_init(" + arg_name2 + ", " + arg_did2 + ", " + arg_prin2 + ", " + arg_net2 + ", " + arg_eng2 + ");")
}
let tes_i = tes_i + 1
}
}
let si2 = si2 + 1
}
// sigs fully consumed release to free peak heap.
el_release(sigs)
// Seed declared set with top-level let names
let main_decl2: [String] = native_list_empty()
let tln: Int = native_list_len(toplevel_let_names)
let tli: Int = 0
while tli < tln {
let main_decl2 = native_list_append(main_decl2, native_list_get(toplevel_let_names, tli))
let tli = tli + 1
}
// toplevel_let_names fully consumed release to free peak heap.
el_release(toplevel_let_names)
// Emit top-level executable stmts (lets and others) into main()
// Per-statement arena scope mirrors el_main_body: frees intermediate strings
// (str_concat fragments from cg_expr) after each statement, preventing O(n²)
// accumulation when many stmts are present (e.g. from unrecognized constructs).
let tes_n2: Int = native_list_len(toplevel_exec_stmts)
let tes_i2: Int = 0
while tes_i2 < tes_n2 {
let tes2 = native_list_get(toplevel_exec_stmts, tes_i2)
let tes_k2: String = tes2["stmt"]
if !str_eq(tes_k2, "CgiBlock") {
if !str_eq(tes_k2, "ServiceBlock") {
let tes_mark: Any = el_arena_push()
let main_decl2 = cg_stmt(tes2, " ", main_decl2)
el_arena_pop(tes_mark)
}
}
let tes_i2 = tes_i2 + 1
}
// toplevel_exec_stmts fully consumed release to free peak heap.
el_release(toplevel_exec_stmts)
// Emit fn main() body per-statement arena scope frees intermediate strings.
let mn: Int = native_list_len(el_main_body)
let mi: Int = 0
while mi < mn {
let mstmt = native_list_get(el_main_body, mi)
let stmt_mark: Any = el_arena_push()
let main_decl2 = cg_stmt(mstmt, " ", main_decl2)
el_arena_pop(stmt_mark)
let mi = mi + 1
}
// el_main_body and main_decl2 fully consumed release to free peak heap.
el_release(el_main_body)
el_release(main_decl2)
emit_line(" return 0;")
emit_line("}")
emit_blank()
emit_cap_violations()
emit_arity_violations()
emit_time_violations()
el_arena_pop(main_arena_mark)
""
}
+55 -10
View File
@@ -20,18 +20,44 @@ import "codegen.el"
import "codegen-js.el"
// compile full pipeline (C target): source string -> C source string
// Uses JIT function-at-a-time streaming: parse one decl emit C discard AST.
// Peak memory is O(one function's AST) instead of O(whole program AST).
fn compile(source: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
// Token list is no longer needed after parsing release it to free memory
// before codegen allocates its own working data on large source files.
el_release(tokens)
codegen(stmts, source)
// Top-level arena scope: activates the string arena before lex() so that
// ALL strdup allocations (token strings, sig strings, codegen fragments)
// are tracked and freed on pop. Without this, lex() and scan_fn_sigs()
// run before any push, leaving _tl_arena_active=0 and leaking every
// token string. Also prevents inner pop(mark=0) calls from deactivating
// the arena between per-function scopes.
let top_mark: Any = el_arena_push()
let tokens: [Any] = lex(source)
// Fast pre-scan: collect fn signatures + program kind without building
// full expression ASTs. O(tokens) time, minimal allocation.
let sigs: [Map<String, Any>] = scan_fn_sigs(tokens)
// Stream parse-emit: parse one decl at a time, emit C, discard.
// All output written to stdout via println before pop.
codegen_streaming(tokens, sigs, source)
el_arena_pop(top_mark)
""
}
// 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: [Map<String, Any>] = lex(source)
let tokens: [Any] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
// Token list is no longer needed after parsing release it to free memory.
el_release(tokens)
@@ -41,7 +67,7 @@ fn compile_js(source: String) -> String {
// compile_js_with_bundle JS target in bundle mode.
// Reads el_runtime.js from runtime_path and inlines it inside an IIFE.
fn compile_js_with_bundle(source: String, runtime_path: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let tokens: [Any] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
el_release(tokens)
let runtime_content: String = fs_read(runtime_path)
@@ -147,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()
@@ -476,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
@@ -483,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)
}
@@ -501,7 +540,7 @@ fn main() -> Void {
// (without inlining imports) and write out a .elh file alongside the .c.
if do_emit_header {
let raw_source: String = fs_read(src_path)
let hdr_tokens: [Map<String, Any>] = lex(raw_source)
let hdr_tokens: [Any] = lex(raw_source)
let hdr_stmts: [Map<String, Any>] = parse(hdr_tokens)
el_release(hdr_tokens)
let hdr_path: String = str_slice(src_path, 0, str_len(src_path) - 3) + ".elh"
@@ -520,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 {
+317 -231
View File
@@ -7,11 +7,50 @@
//
// Entry point: fn lex(source: String) -> [Map<String, Any>]
//
// Uses native_string_chars to split the source into a chars list,
// then indexes it with native_list_get - avoids O(N-) string cloning.
// Performance: the hot lexer loop uses str_char_code (returns Int) instead of
// str_char_at (returns strdup'd String) for character classification.
// For a 400KB source, str_char_at allocates ~400K × 16B = ~6.4MB of temporary
// strings for the `ch` variable alone. str_char_code avoids all that.
// -- Character helpers ---------------------------------------------------------
// -- Character helpers (Int-based, no string allocation) ----------------------
// These operate on char codes (from str_char_code) instead of str_char_at,
// eliminating one strdup per character in the hot lexer loop.
fn is_digit_code(c: Int) -> Bool {
// '0'=48 .. '9'=57
if c >= 48 {
if c <= 57 { return true }
}
false
}
fn is_alpha_code(c: Int) -> Bool {
// 'A'=65..'Z'=90, 'a'=97..'z'=122
if c >= 65 {
if c <= 90 { return true }
}
if c >= 97 {
if c <= 122 { return true }
}
false
}
fn is_alnum_or_underscore_code(c: Int) -> Bool {
if is_digit_code(c) { return true }
if is_alpha_code(c) { return true }
if c == 95 { return true } // '_'
false
}
fn is_ws_code(c: Int) -> Bool {
if c == 32 { return true } // ' '
if c == 9 { return true } // '\t'
if c == 10 { return true } // '\n'
if c == 13 { return true } // '\r'
false
}
// Legacy String-based helpers kept for scan_interp helpers that use str_char_at.
fn lex_is_digit(ch: String) -> Bool {
if ch == "0" { return true }
if ch == "1" { return true }
@@ -97,8 +136,11 @@ fn lex_is_whitespace(ch: String) -> Bool {
false
}
fn make_tok(kind: String, value: String) -> Map<String, Any> {
{ "kind": kind, "value": value }
// tok_append append a (kind, value) pair to a flat token list.
// Returns the updated list. Gamma combines flat-list + char-code for max savings.
fn tok_append(tokens: [Any], kind: String, value: String) -> [Any] {
let tokens = native_list_append(tokens, kind)
native_list_append(tokens, value)
}
// -- Keyword lookup ------------------------------------------------------------
@@ -157,45 +199,43 @@ fn keyword_kind(word: String) -> String {
// scan_digits - advance i while chars[i] is a digit
// Returns { "text": ..., "pos": i }
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
fn scan_digits(src: String, start: Int, total: Int) -> Map<String, Any> {
let i = start
let parts: [String] = native_list_empty()
let running = true
while running {
if i >= total {
let running = false
} else {
let ch: String = native_list_get(chars, i)
if lex_is_digit(ch) {
let parts = native_list_append(parts, ch)
let c: Int = str_char_code(src, i)
if is_digit_code(c) {
let i = i + 1
} else {
let running = false
}
}
}
{ "text": str_join(parts, ""), "pos": i }
// Use str_slice instead of building a parts list O(1) allocation, O(n) copy.
{ "text": str_slice(src, start, i), "pos": i }
}
// scan_ident - advance i while chars[i] is alphanumeric or underscore
fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
fn scan_ident(src: String, start: Int, total: Int) -> Map<String, Any> {
let i = start
let parts: [String] = native_list_empty()
let running = true
while running {
if i >= total {
let running = false
} else {
let ch: String = native_list_get(chars, i)
if is_alnum_or_underscore(ch) {
let parts = native_list_append(parts, ch)
let c: Int = str_char_code(src, i)
if is_alnum_or_underscore_code(c) {
let i = i + 1
} else {
let running = false
}
}
}
{ "text": str_join(parts, ""), "pos": i }
// Use str_slice instead of building a parts list O(1) allocation, O(n) copy.
{ "text": str_slice(src, start, i), "pos": i }
}
// -- Code-bearing string detection + comment strip ----------------------------
@@ -208,34 +248,16 @@ fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
// looks_like_code - heuristic gate so we only strip strings that actually
// embed JS or CSS. Plain prose, hex blobs, JSON, etc. pass through verbatim.
fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool {
let nchars: [String] = native_string_chars(needle)
let nlen: Int = native_list_len(nchars)
fn substr_at(src: String, start: Int, total: Int, needle: String) -> Bool {
let nlen: Int = str_len(needle)
if start + nlen > total { return false }
let i = 0
let matched = true
while i < nlen {
let a: String = native_list_get(chars, start + i)
let b: String = native_list_get(nchars, i)
if a == b { let i = i + 1 } else { let matched = false; let i = nlen }
}
matched
// Use str_slice comparison instead of char-by-char loop.
str_eq(str_slice(src, start, start + nlen), needle)
}
fn str_has(s: String, needle: String) -> Bool {
let chars: [String] = native_string_chars(s)
let total: Int = native_list_len(chars)
let i = 0
let found = false
while i < total {
if substr_at(chars, i, total, needle) {
let found = true
let i = total
} else {
let i = i + 1
}
}
found
// Use the built-in str_contains which is implemented in native C O(n) single pass.
str_contains(s, needle)
}
fn looks_like_code(s: String) -> Bool {
@@ -254,8 +276,7 @@ fn looks_like_code(s: String) -> Bool {
// comment opener: if the char immediately before '/' is ':', emit the '/'
// literally and advance one position.
fn strip_code_comments(s: String) -> String {
let chars: [String] = native_string_chars(s)
let total: Int = native_list_len(chars)
let total: Int = str_len(s)
let out_parts: [String] = native_list_empty()
let i = 0
let in_squote = false
@@ -263,7 +284,7 @@ fn strip_code_comments(s: String) -> String {
let in_btick = false
let prev = ""
while i < total {
let ch: String = native_list_get(chars, i)
let ch: String = str_char_at(s, i)
let in_js_string = false
if in_squote { let in_js_string = true }
if in_dquote { let in_js_string = true }
@@ -275,7 +296,7 @@ fn strip_code_comments(s: String) -> String {
let out_parts = native_list_append(out_parts, ch)
let next_i = i + 1
if next_i < total {
let nc: String = native_list_get(chars, next_i)
let nc: String = str_char_at(s, next_i)
let out_parts = native_list_append(out_parts, nc)
let prev = nc
let i = next_i + 1
@@ -304,7 +325,7 @@ fn strip_code_comments(s: String) -> String {
let next_i = i + 1
let next_ch = ""
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
let next_ch: String = str_char_at(s, next_i)
}
if ch == "/" {
@@ -323,7 +344,7 @@ fn strip_code_comments(s: String) -> String {
if i >= total {
let scanning = false
} else {
let lc: String = native_list_get(chars, i)
let lc: String = str_char_at(s, i)
if lc == "\n" {
let scanning = false
} else {
@@ -342,11 +363,11 @@ fn strip_code_comments(s: String) -> String {
if i >= total {
let scanning2 = false
} else {
let bc: String = native_list_get(chars, i)
let bc: String = str_char_at(s, i)
if bc == "*" {
let after = i + 1
if after < total {
let nc2: String = native_list_get(chars, after)
let nc2: String = str_char_at(s, after)
if nc2 == "/" {
let i = after + 1
let scanning2 = false
@@ -402,7 +423,7 @@ fn strip_code_comments(s: String) -> String {
// scan_string - scan a quoted string literal, handling \" escapes.
// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close }
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
fn scan_string(src: String, start: Int, total: Int) -> Map<String, Any> {
let i = start
let parts: [String] = native_list_empty()
let running = true
@@ -410,12 +431,12 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
if i >= total {
let running = false
} else {
let ch: String = native_list_get(chars, i)
let ch: String = str_char_at(src, i)
if ch == "\\" {
// escape: peek next char
let next_i = i + 1
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
let next_ch: String = str_char_at(src, next_i)
if next_ch == "\"" {
let parts = native_list_append(parts, "\"")
let i = next_i + 1
@@ -465,19 +486,17 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
// scan_interp_brace - scan from `start` (the char after `${`) to the matching
// `}`, tracking brace depth so inner braces (e.g. fn calls, map literals) are
// handled correctly. Returns { "text": inner_source, "pos": i_after_close }.
fn scan_interp_brace(chars: [String], start: Int, total: Int) -> Map<String, Any> {
fn scan_interp_brace(src: String, start: Int, total: Int) -> Map<String, Any> {
let i = start
let parts: [String] = native_list_empty()
let depth = 1
let running = true
while running {
if i >= total {
let running = false
} else {
let ch: String = native_list_get(chars, i)
let ch: String = str_char_at(src, i)
if ch == "{" {
let depth = depth + 1
let parts = native_list_append(parts, ch)
let i = i + 1
} else {
if ch == "}" {
@@ -487,33 +506,33 @@ fn scan_interp_brace(chars: [String], start: Int, total: Int) -> Map<String, Any
let i = i + 1
let running = false
} else {
let parts = native_list_append(parts, ch)
let i = i + 1
}
} else {
let parts = native_list_append(parts, ch)
let i = i + 1
}
}
}
}
{ "text": str_join(parts, ""), "pos": i }
// Use str_slice instead of parts list the inner source is a contiguous substring.
{ "text": str_slice(src, start, i - 1), "pos": i }
}
// interp_tokens_append_all - copy every token from src into dst, skipping the
// trailing Eof sentinel that lex() always appends. Returns the updated dst list.
fn interp_tokens_append_all(dst: [Map<String, Any>], src: [Map<String, Any>]) -> [Map<String, Any>] {
// interp_tokens_append_all - copy every (kind, value) pair from flat src list
// into flat dst list, skipping the trailing Eof pair that lex() always appends.
fn interp_tokens_append_all(dst: [Any], src: [Any]) -> [Any] {
let src_len: Int = native_list_len(src)
let j = 0
let result = dst
while j < src_len {
let tok: Map<String, Any> = native_list_get(src, j)
let tk: String = tok["kind"]
if tk == "Eof" {
let kind: String = native_list_get(src, j)
if kind == "Eof" {
let j = src_len
} else {
let result = native_list_append(result, tok)
let j = j + 1
let val: String = native_list_get(src, j + 1)
let result = native_list_append(result, kind)
let result = native_list_append(result, val)
let j = j + 2
}
}
result
@@ -536,10 +555,17 @@ fn interp_tokens_append_all(dst: [Map<String, Any>], src: [Map<String, Any>]) ->
//
// Supported escape sequences: \" \n \t \r \\ \$ (literal dollar sign).
// Nested quotes inside ${} are not supported; use a variable instead.
fn scan_interp_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
//
// Performance: uses str_char_code (Int) for all character dispatch, eliminating
// per-character strdup. Plain runs are batched into str_slice segments instead
// of accumulating single-char strings, reducing list appends from O(N) to O(K)
// where K = number of escape/special chars in the literal.
// Char codes: '\' = 92, '"' = 34, '$' = 36, '{' = 123
fn scan_interp_string(src: String, start: Int, total: Int) -> Map<String, Any> {
let i = start
let out_tokens: [Map<String, Any>] = native_list_empty()
let cur_part: [String] = native_list_empty()
let out_tokens: [Any] = native_list_empty()
let cur_parts: [String] = native_list_empty()
let clean_start = start
let has_interp = false
let need_plus = false
let running = true
@@ -548,39 +574,55 @@ fn scan_interp_string(chars: [String], start: Int, total: Int) -> Map<String, An
if i >= total {
let running = false
} else {
let ch: String = native_list_get(chars, i)
let c: Int = str_char_code(src, i)
if ch == "\\" {
// Escape sequence
if c == 92 {
// '\\' = 92 escape sequence: flush clean run, append resolved char
if clean_start < i {
let cur_parts = native_list_append(cur_parts, str_slice(src, clean_start, i))
}
let next_i = i + 1
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
if next_ch == "$" {
// \$ => literal '$' (escape for interpolation syntax)
let cur_part = native_list_append(cur_part, "$")
let nc: Int = str_char_code(src, next_i)
if nc == 36 {
// '\$' => literal '$' (36 = '$')
let cur_parts = native_list_append(cur_parts, "$")
let clean_start = next_i + 1
let i = next_i + 1
} else {
if next_ch == "\"" {
let cur_part = native_list_append(cur_part, "\"")
if nc == 34 {
// '\"' => literal '"' (34 = '"')
let cur_parts = native_list_append(cur_parts, "\"")
let clean_start = next_i + 1
let i = next_i + 1
} else {
if next_ch == "n" {
let cur_part = native_list_append(cur_part, "\n")
if nc == 110 {
// '\n' (110 = 'n')
let cur_parts = native_list_append(cur_parts, "\n")
let clean_start = next_i + 1
let i = next_i + 1
} else {
if next_ch == "t" {
let cur_part = native_list_append(cur_part, "\t")
if nc == 116 {
// '\t' (116 = 't')
let cur_parts = native_list_append(cur_parts, "\t")
let clean_start = next_i + 1
let i = next_i + 1
} else {
if next_ch == "r" {
let cur_part = native_list_append(cur_part, "\r")
if nc == 114 {
// '\r' (114 = 'r')
let cur_parts = native_list_append(cur_parts, "\r")
let clean_start = next_i + 1
let i = next_i + 1
} else {
if next_ch == "\\" {
let cur_part = native_list_append(cur_part, "\\")
if nc == 92 {
// '\\' (92)
let cur_parts = native_list_append(cur_parts, "\\")
let clean_start = next_i + 1
let i = next_i + 1
} else {
let cur_part = native_list_append(cur_part, next_ch)
// Unknown escape: emit the escaped char verbatim
let cur_parts = native_list_append(cur_parts, str_slice(src, next_i, next_i + 1))
let clean_start = next_i + 1
let i = next_i + 1
}
}
@@ -589,75 +631,85 @@ fn scan_interp_string(chars: [String], start: Int, total: Int) -> Map<String, An
}
}
} else {
let i = i + 1
let clean_start = next_i
let i = next_i
}
} else {
if ch == "\"" {
// Closing quote - stop scanning
if c == 34 {
// '"' = 34 — closing quote: flush clean run, stop
if clean_start < i {
let cur_parts = native_list_append(cur_parts, str_slice(src, clean_start, i))
}
let i = i + 1
let clean_start = i
let running = false
} else {
if ch == "$" {
// Check for ${ (start of interpolation)
if c == 36 {
// '$' = 36 possible interpolation start
let next_i = i + 1
let is_interp = false
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
if next_ch == "{" {
let nc2: Int = str_char_code(src, next_i)
if nc2 == 123 {
// '{' = 123
let is_interp = true
}
}
if is_interp {
// Flush the accumulated literal part (if non-empty)
let part_len: Int = native_list_len(cur_part)
if clean_start < i {
let cur_parts = native_list_append(cur_parts, str_slice(src, clean_start, i))
}
let part_len: Int = native_list_len(cur_parts)
if part_len > 0 {
let part_text = str_join(cur_part, "")
let part_text = str_join(cur_parts, "")
if need_plus {
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
let out_tokens = tok_append(out_tokens, "Plus", "+")
}
let clean_part = part_text
if looks_like_code(part_text) {
let clean_part = strip_code_comments(part_text)
}
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_part))
let out_tokens = tok_append(out_tokens, "Str", clean_part)
let need_plus = true
}
let cur_part = native_list_empty()
let cur_parts = native_list_empty()
let has_interp = true
// Scan brace-balanced expression source
let brace_result = scan_interp_brace(chars, next_i + 1, total)
let brace_result = scan_interp_brace(src, next_i + 1, total)
let expr_src: String = brace_result["text"]
let new_i: Int = brace_result["pos"]
let i = new_i
let clean_start = new_i
// Re-lex the expression and inline the tokens.
// Wrap in ( ) so that operators inside ${} (e.g.
// age + 1) are parsed as a grouped sub-expression
// rather than merging with the surrounding concat
// Plus tokens at the wrong precedence level.
let inner_toks: [Map<String, Any>] = lex(expr_src)
let inner_toks: [Any] = lex(expr_src)
let inner_len: Int = native_list_len(inner_toks)
if need_plus {
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
let out_tokens = tok_append(out_tokens, "Plus", "+")
}
// Empty interpolation ${} => empty string segment
if inner_len <= 1 {
let out_tokens = native_list_append(out_tokens, make_tok("Str", ""))
// inner_len <= 2 = only the Eof pair (kind="Eof", value="")
if inner_len <= 2 {
let out_tokens = tok_append(out_tokens, "Str", "")
} else {
let out_tokens = native_list_append(out_tokens, make_tok("LParen", "("))
let out_tokens = tok_append(out_tokens, "LParen", "(")
let out_tokens = interp_tokens_append_all(out_tokens, inner_toks)
let out_tokens = native_list_append(out_tokens, make_tok("RParen", ")"))
let out_tokens = tok_append(out_tokens, "RParen", ")")
}
let need_plus = true
} else {
// Plain '$' not followed by '{' - treat as literal
let cur_part = native_list_append(cur_part, "$")
// Plain '$' not followed by '{' - treat as literal, continue clean run
let i = i + 1
}
} else {
let cur_part = native_list_append(cur_part, ch)
// Plain char extends clean run, no append needed
let i = i + 1
}
}
@@ -666,8 +718,11 @@ fn scan_interp_string(chars: [String], start: Int, total: Int) -> Map<String, An
}
// Flush remaining literal segment and build final token list
let part_text = str_join(cur_part, "")
let part_len: Int = native_list_len(cur_part)
if clean_start < i {
let cur_parts = native_list_append(cur_parts, str_slice(src, clean_start, i))
}
let part_len: Int = native_list_len(cur_parts)
let part_text = str_join(cur_parts, "")
if has_interp {
// Interpolated string: only emit trailing segment if non-empty
if part_len > 0 {
@@ -676,9 +731,9 @@ fn scan_interp_string(chars: [String], start: Int, total: Int) -> Map<String, An
let clean_part = strip_code_comments(part_text)
}
if need_plus {
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
let out_tokens = tok_append(out_tokens, "Plus", "+")
}
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_part))
let out_tokens = tok_append(out_tokens, "Str", clean_part)
}
} else {
// Plain string with no interpolation - same behaviour as old scan_string
@@ -686,42 +741,51 @@ fn scan_interp_string(chars: [String], start: Int, total: Int) -> Map<String, An
if looks_like_code(part_text) {
let clean_text = strip_code_comments(part_text)
}
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_text))
let out_tokens = tok_append(out_tokens, "Str", clean_text)
}
{ "tokens": out_tokens, "pos": i }
}
// -- Main lexer ----------------------------------------------------------------
// Char code constants (avoids strdup for single-char comparison)
// '/' = 47, '"' = 34, '0'-'9' = 48-57, 'a'-'z' = 97-122, 'A'-'Z' = 65-90
// '_' = 95, ' '=32, '\t'=9, '\n'=10, '\r'=13
// '=' = 61, '!' = 33, '<' = 60, '>' = 62, '&' = 38, '|' = 124
// '-' = 45, ':' = 58, '+' = 43, '*' = 42, '%' = 37
// '(' = 40, ')' = 41, '{' = 123, '}' = 125, '[' = 91, ']' = 93
// ',' = 44, '.' = 46, ';' = 59, '@' = 64, '?' = 63
fn lex(source: String) -> [Map<String, Any>] {
let chars: [String] = native_string_chars(source)
let total: Int = native_list_len(chars)
let tokens: [Map<String, Any>] = native_list_empty()
fn lex(source: String) -> [Any] {
// Use str_char_code (returns Int) instead of str_char_at (returns strdup String)
// for all character classification in the hot loop. For a 400KB source,
// str_char_at allocates ~400K × 16B = ~6.4MB of temporary strings.
let total: Int = str_len(source)
let tokens: [Any] = native_list_empty()
let i: Int = 0
while i < total {
let ch: String = native_list_get(chars, i)
let c: Int = str_char_code(source, i)
// Skip whitespace
if lex_is_whitespace(ch) {
// Skip whitespace (space=32, tab=9, newline=10, CR=13)
if is_ws_code(c) {
let i = i + 1
} else {
// Line comments: //
if ch == "/" {
// Line comments: // (slash=47)
if c == 47 {
let next_i = i + 1
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
if next_ch == "/" {
// skip to end of line
let nc: Int = str_char_code(source, next_i)
if nc == 47 {
// skip to end of line (newline=10)
let i = i + 2
let running2 = true
while running2 {
if i >= total {
let running2 = false
} else {
let lch: String = native_list_get(chars, i)
if lch == "\n" {
let lc: Int = str_char_code(source, i)
if lc == 10 {
let running2 = false
} else {
let i = i + 1
@@ -729,232 +793,254 @@ fn lex(source: String) -> [Map<String, Any>] {
}
}
} else {
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
let tokens = tok_append(tokens, "Slash", "/")
let i = i + 1
}
} else {
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
let tokens = tok_append(tokens, "Slash", "/")
let i = i + 1
}
} else {
// String literal (plain or interpolated with ${expr} syntax).
// scan_interp_string handles both cases: plain strings emit a
// single Str token; interpolated strings emit a flat token
// sequence (Str Plus expr-tokens Plus Str ...) that the parser
// naturally assembles into a BinOp concat tree.
if ch == "\"" {
let interp_result = scan_interp_string(chars, i + 1, total)
let interp_toks: [Map<String, Any>] = interp_result["tokens"]
// String literal: '"' = 34
if c == 34 {
let interp_result = scan_interp_string(source, i + 1, total)
let interp_toks: [Any] = interp_result["tokens"]
let new_pos: Int = interp_result["pos"]
let tokens = interp_tokens_append_all(tokens, interp_toks)
let i = new_pos
} else {
// Number literal
if lex_is_digit(ch) {
let result = scan_digits(chars, i, total)
// Number literal: '0'-'9' = 48-57
if is_digit_code(c) {
let result = scan_digits(source, i, total)
let num_text: String = result["text"]
let new_pos: Int = result["pos"]
// check for float (dot followed by digit)
// check for float (dot=46 followed by digit)
if new_pos < total {
let dot_ch: String = native_list_get(chars, new_pos)
if dot_ch == "." {
let dc: Int = str_char_code(source, new_pos)
if dc == 46 {
let after_dot = new_pos + 1
if after_dot < total {
let after_dot_ch: String = native_list_get(chars, after_dot)
if lex_is_digit(after_dot_ch) {
let frac_result = scan_digits(chars, after_dot, total)
let adc: Int = str_char_code(source, after_dot)
if is_digit_code(adc) {
let frac_result = scan_digits(source, after_dot, total)
let frac_text: String = frac_result["text"]
let frac_pos: Int = frac_result["pos"]
let tokens = native_list_append(tokens, make_tok("Float", num_text + "." + frac_text))
let tokens = tok_append(tokens, "Float", num_text + "." + frac_text)
let i = frac_pos
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let tokens = tok_append(tokens, "Int", num_text)
let i = new_pos
}
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let tokens = tok_append(tokens, "Int", num_text)
let i = new_pos
}
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let tokens = tok_append(tokens, "Int", num_text)
let i = new_pos
}
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let tokens = tok_append(tokens, "Int", num_text)
let i = new_pos
}
} else {
// Identifier or keyword
if lex_is_alpha(ch) || ch == "_" {
let result = scan_ident(chars, i, total)
// Identifier or keyword: alpha or '_'=95
if is_alpha_code(c) || c == 95 {
let result = scan_ident(source, i, total)
let word: String = result["text"]
let new_pos: Int = result["pos"]
let kw = keyword_kind(word)
if kw == "" {
let tokens = native_list_append(tokens, make_tok("Ident", word))
let tokens = tok_append(tokens, "Ident", word)
} else {
let tokens = native_list_append(tokens, make_tok(kw, word))
let tokens = tok_append(tokens, kw, word)
}
let i = new_pos
} else {
// Multi-char and single-char operators/delimiters
let peek_i = i + 1
let peek_ch = ""
let peek_c: Int = -1
if peek_i < total {
let peek_ch: String = native_list_get(chars, peek_i)
let peek_c: Int = str_char_code(source, peek_i)
}
if ch == "=" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("EqEq", "=="))
if c == 61 {
// '=' = 61
if peek_c == 61 {
let tokens = tok_append(tokens, "EqEq", "==")
let i = i + 2
} else {
if peek_ch == ">" {
let tokens = native_list_append(tokens, make_tok("FatArrow", "=>"))
if peek_c == 62 {
// '>' = 62
let tokens = tok_append(tokens, "FatArrow", "=>")
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Eq", "="))
let tokens = tok_append(tokens, "Eq", "=")
let i = i + 1
}
}
} else {
if ch == "!" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("NotEq", "!="))
if c == 33 {
// '!' = 33
if peek_c == 61 {
let tokens = tok_append(tokens, "NotEq", "!=")
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Not", "!"))
let tokens = tok_append(tokens, "Not", "!")
let i = i + 1
}
} else {
if ch == "<" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("LtEq", "<="))
if c == 60 {
// '<' = 60
if peek_c == 61 {
let tokens = tok_append(tokens, "LtEq", "<=")
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Lt", "<"))
let tokens = tok_append(tokens, "Lt", "<")
let i = i + 1
}
} else {
if ch == ">" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("GtEq", ">="))
if c == 62 {
// '>' = 62
if peek_c == 61 {
let tokens = tok_append(tokens, "GtEq", ">=")
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Gt", ">"))
let tokens = tok_append(tokens, "Gt", ">")
let i = i + 1
}
} else {
if ch == "&" {
if peek_ch == "&" {
let tokens = native_list_append(tokens, make_tok("And", "&&"))
if c == 38 {
// '&' = 38
if peek_c == 38 {
let tokens = tok_append(tokens, "And", "&&")
let i = i + 2
} else {
let i = i + 1
}
} else {
if ch == "|" {
if peek_ch == "|" {
let tokens = native_list_append(tokens, make_tok("Or", "||"))
if c == 124 {
// '|' = 124
if peek_c == 124 {
let tokens = tok_append(tokens, "Or", "||")
let i = i + 2
} else {
if peek_ch == ">" {
let tokens = native_list_append(tokens, make_tok("PipeOp", "|>"))
if peek_c == 62 {
// '>' = 62
let tokens = tok_append(tokens, "PipeOp", "|>")
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Pipe", "|"))
let tokens = tok_append(tokens, "Pipe", "|")
let i = i + 1
}
}
} else {
if ch == "-" {
if peek_ch == ">" {
let tokens = native_list_append(tokens, make_tok("Arrow", "->"))
if c == 45 {
// '-' = 45
if peek_c == 62 {
// '>' = 62
let tokens = tok_append(tokens, "Arrow", "->")
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Minus", "-"))
let tokens = tok_append(tokens, "Minus", "-")
let i = i + 1
}
} else {
if ch == ":" {
if peek_ch == ":" {
let tokens = native_list_append(tokens, make_tok("ColonColon", "::"))
if c == 58 {
// ':' = 58
if peek_c == 58 {
let tokens = tok_append(tokens, "ColonColon", "::")
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Colon", ":"))
let tokens = tok_append(tokens, "Colon", ":")
let i = i + 1
}
} else {
if ch == "+" {
let tokens = native_list_append(tokens, make_tok("Plus", "+"))
if c == 43 {
// '+' = 43
let tokens = tok_append(tokens, "Plus", "+")
let i = i + 1
} else {
if ch == "*" {
let tokens = native_list_append(tokens, make_tok("Star", "*"))
if c == 42 {
// '*' = 42
let tokens = tok_append(tokens, "Star", "*")
let i = i + 1
} else {
if ch == "%" {
let tokens = native_list_append(tokens, make_tok("Percent", "%"))
if c == 37 {
// '%' = 37
let tokens = tok_append(tokens, "Percent", "%")
let i = i + 1
} else {
if ch == "(" {
let tokens = native_list_append(tokens, make_tok("LParen", "("))
if c == 40 {
// '(' = 40
let tokens = tok_append(tokens, "LParen", "(")
let i = i + 1
} else {
if ch == ")" {
let tokens = native_list_append(tokens, make_tok("RParen", ")"))
if c == 41 {
// ')' = 41
let tokens = tok_append(tokens, "RParen", ")")
let i = i + 1
} else {
if ch == "{" {
let tokens = native_list_append(tokens, make_tok("LBrace", "{"))
if c == 123 {
// '{' = 123
let tokens = tok_append(tokens, "LBrace", "{")
let i = i + 1
} else {
if ch == "}" {
let tokens = native_list_append(tokens, make_tok("RBrace", "}"))
if c == 125 {
// '}' = 125
let tokens = tok_append(tokens, "RBrace", "}")
let i = i + 1
} else {
if ch == "[" {
let tokens = native_list_append(tokens, make_tok("LBracket", "["))
if c == 91 {
// '[' = 91
let tokens = tok_append(tokens, "LBracket", "[")
let i = i + 1
} else {
if ch == "]" {
let tokens = native_list_append(tokens, make_tok("RBracket", "]"))
if c == 93 {
// ']' = 93
let tokens = tok_append(tokens, "RBracket", "]")
let i = i + 1
} else {
if ch == "," {
let tokens = native_list_append(tokens, make_tok("Comma", ","))
if c == 44 {
// ',' = 44
let tokens = tok_append(tokens, "Comma", ",")
let i = i + 1
} else {
if ch == "." {
// Check for ..= (inclusive range) before .. (exclusive range) before single .
if c == 46 {
// '.' = 46: check for ..= or ..
let peek2_i = i + 2
let peek2_ch = ""
let peek2_c: Int = -1
if peek2_i < total {
let peek2_ch: String = native_list_get(chars, peek2_i)
let peek2_c: Int = str_char_code(source, peek2_i)
}
if peek_ch == "." {
if peek2_ch == "=" {
let tokens = native_list_append(tokens, make_tok("DotDotEq", "..="))
if peek_c == 46 {
// '..' prefix
if peek2_c == 61 {
// '..=' = 46 46 61
let tokens = tok_append(tokens, "DotDotEq", "..=")
let i = i + 3
} else {
let tokens = native_list_append(tokens, make_tok("DotDot", ".."))
let tokens = tok_append(tokens, "DotDot", "..")
let i = i + 2
}
} else {
let tokens = native_list_append(tokens, make_tok("Dot", "."))
let tokens = tok_append(tokens, "Dot", ".")
let i = i + 1
}
} else {
if ch == ";" {
let tokens = native_list_append(tokens, make_tok("Semicolon", ";"))
if c == 59 {
// ';' = 59
let tokens = tok_append(tokens, "Semicolon", ";")
let i = i + 1
} else {
if ch == "@" {
let tokens = native_list_append(tokens, make_tok("At", "@"))
if c == 64 {
// '@' = 64
let tokens = tok_append(tokens, "At", "@")
let i = i + 1
} else {
if ch == "?" {
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
if c == 63 {
// '?' = 63
let tokens = tok_append(tokens, "QuestionMark", "?")
let i = i + 1
} else {
// unknown char - skip
@@ -988,6 +1074,6 @@ fn lex(source: String) -> [Map<String, Any>] {
}
}
let tokens = native_list_append(tokens, make_tok("Eof", ""))
let tokens = tok_append(tokens, "Eof", "")
tokens
}
+445 -30
View File
@@ -9,25 +9,28 @@
// The token list is passed as a parameter to all parse functions.
// native_list_get is used to index into it without cloning.
//
// Entry point: fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>]
// Entry point: fn parse(tokens: [Any]) -> [Map<String, Any>]
// -- Token access helpers ------------------------------------------------------
// Tokens is a flat [Any] list: tokens[2*i] = kind, tokens[2*i+1] = value.
// This avoids one ElMap allocation per token (~112B each), saving ~4MB on large
// programs. All callers use these helpers -- only these three need updating.
fn tok_at(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
native_list_get(tokens, pos)
fn tok_at(tokens: [Any], pos: Int) -> Map<String, Any> {
let kind: String = native_list_get(tokens, pos * 2)
let value: String = native_list_get(tokens, pos * 2 + 1)
{ "kind": kind, "value": value }
}
fn tok_kind(tokens: [Map<String, Any>], pos: Int) -> String {
let t = native_list_get(tokens, pos)
t["kind"]
fn tok_kind(tokens: [Any], pos: Int) -> String {
native_list_get(tokens, pos * 2)
}
fn tok_value(tokens: [Map<String, Any>], pos: Int) -> String {
let t = native_list_get(tokens, pos)
t["value"]
fn tok_value(tokens: [Any], pos: Int) -> String {
native_list_get(tokens, pos * 2 + 1)
}
fn expect(tokens: [Map<String, Any>], pos: Int, kind: String) -> Int {
fn expect(tokens: [Any], pos: Int, kind: String) -> Int {
let k = tok_kind(tokens, pos)
if k == kind {
return pos + 1
@@ -46,7 +49,7 @@ fn make_result(node: Map<String, Any>, pos: Int) -> Map<String, Any> {
// Skips over a type annotation, returning the new position.
// Types can be: Ident, [Type], Map<K,V>, Type?, Type<Type,...>
fn skip_type(tokens: [Map<String, Any>], pos: Int) -> Int {
fn skip_type(tokens: [Any], pos: Int) -> Int {
let k = tok_kind(tokens, pos)
// Array type: [Type]
if k == "LBracket" {
@@ -103,7 +106,7 @@ fn skip_type(tokens: [Map<String, Any>], pos: Int) -> Int {
// -- Parameter list ------------------------------------------------------------
// Parses (name: Type, name: Type, ...) - returns { "params": [...], "pos": ... }
fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_params(tokens: [Any], pos: Int) -> Map<String, Any> {
let p = expect(tokens, pos, "LParen")
let params: [Map<String, Any>] = native_list_empty()
let running = true
@@ -292,7 +295,7 @@ fn is_void_element(name: String) -> Bool {
// Collect tokens as text content until we hit Lt, LBrace, Eof, or a
// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p }
fn parse_html_text_tokens(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> {
let parts: [String] = native_list_empty()
let p = pos
let running = true
@@ -322,7 +325,7 @@ fn parse_html_text_tokens(tokens: [Map<String, Any>], pos: Int) -> Map<String, A
// Parse an attribute list: (attrname | attrname="val" | attrname={expr})*
// Stops at Gt or Slash (for self-closing />).
fn parse_html_attrs(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_html_attrs(tokens: [Any], pos: Int) -> Map<String, Any> {
let attrs: [Map<String, Any>] = native_list_empty()
let p = pos
let running = true
@@ -355,6 +358,8 @@ fn parse_html_attrs(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_expr(tokens, p + 1)
let val_node = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let p = expect(tokens, p, "RBrace")
let attrs = native_list_append(attrs, { "name": attr_name, "kind": "dynamic", "value": val_node })
} else {
@@ -374,7 +379,7 @@ fn parse_html_attrs(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
// Parse the children of an HTML element until we see the closing tag </tag>
// or EOF. Returns { "children": [...], "pos": p_after_closing_tag }
fn parse_html_children(tokens: [Map<String, Any>], pos: Int, parent_tag: String) -> Map<String, Any> {
fn parse_html_children(tokens: [Any], pos: Int, parent_tag: String) -> Map<String, Any> {
let children: [Map<String, Any>] = native_list_empty()
let p = pos
let running = true
@@ -423,6 +428,8 @@ fn parse_html_children(tokens: [Map<String, Any>], pos: Int, parent_tag: String)
let r = parse_html_element(tokens, p)
let child = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let children = native_list_append(children, child)
}
}
@@ -442,6 +449,8 @@ fn parse_html_children(tokens: [Map<String, Any>], pos: Int, parent_tag: String)
state_set("__no_block_expr", prev_no_block)
let list_expr = r_list["node"]
let p = r_list["pos"]
// r_list result map fully consumed release to free peak heap.
el_release(r_list)
// expect "as"
let p = expect(tokens, p, "As")
// item variable name
@@ -453,6 +462,8 @@ fn parse_html_children(tokens: [Map<String, Any>], pos: Int, parent_tag: String)
let r_body = parse_html_each_body(tokens, p)
let body_children = r_body["children"]
let p = r_body["pos"]
// r_body result map fully consumed release to free peak heap.
el_release(r_body)
let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
let children = native_list_append(children, each_node)
} else {
@@ -473,6 +484,8 @@ fn parse_html_children(tokens: [Map<String, Any>], pos: Int, parent_tag: String)
let r = parse_expr(tokens, p + 1)
let interp_val = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let p = expect(tokens, p, "RBrace")
// Check if the expr is a call to raw()
let is_raw_call = false
@@ -501,6 +514,8 @@ fn parse_html_children(tokens: [Map<String, Any>], pos: Int, parent_tag: String)
let r_text = parse_html_text_tokens(tokens, p)
let text_str: String = r_text["text"]
let p = r_text["pos"]
// r_text result map fully consumed release to free peak heap.
el_release(r_text)
let text_trimmed: String = str_trim(text_str)
if !str_eq(text_trimmed, "") {
let children = native_list_append(children, { "html": "Text", "text": text_trimmed })
@@ -514,14 +529,14 @@ fn parse_html_children(tokens: [Map<String, Any>], pos: Int, parent_tag: String)
// Parse body of {#each} until {/each}. Mirrors parse_html_children but
// stops at the {/each} sentinel rather than a closing element tag.
fn parse_html_each_body(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_html_each_body(tokens: [Any], pos: Int) -> Map<String, Any> {
parse_html_children(tokens, pos, "__each__")
}
// Parse a single HTML element: <tag attrs> children </tag>
// or self-closing: <tag attrs/>
// Pos points to the Lt token.
fn parse_html_element(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_html_element(tokens: [Any], pos: Int) -> Map<String, Any> {
let p = pos
// consume <
let p = expect(tokens, p, "Lt")
@@ -532,6 +547,8 @@ fn parse_html_element(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any>
let r_attrs = parse_html_attrs(tokens, p)
let attrs = r_attrs["attrs"]
let p = r_attrs["pos"]
// r_attrs result map fully consumed release to free peak heap.
el_release(r_attrs)
// check for self-closing /> or void element
let k = tok_kind(tokens, p)
let self_closing = false
@@ -552,13 +569,15 @@ fn parse_html_element(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any>
let r_children = parse_html_children(tokens, p, tag_name)
let children = r_children["children"]
let p = r_children["pos"]
// r_children result map fully consumed release to free peak heap.
el_release(r_children)
make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": children, "self_closing": false }, p)
}
// Entry point for HTML template parsing.
// Pos points to Lt (or Lt Not for <!doctype>).
// May parse an optional <!doctype html> prefix followed by the root element.
fn parse_html_template(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_html_template(tokens: [Any], pos: Int) -> Map<String, Any> {
let p = pos
// Check for <!doctype html>
let doctype = false
@@ -589,6 +608,8 @@ fn parse_html_template(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any>
let r = parse_html_element(tokens, p)
let root = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let root_with_doctype = root
if doctype {
let root_with_doctype = { "html": root["html"], "tag": root["tag"], "attrs": root["attrs"], "children": root["children"], "self_closing": root["self_closing"], "doctype": true }
@@ -596,7 +617,7 @@ fn parse_html_template(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any>
make_result({ "expr": "HtmlTemplate", "root": root_with_doctype }, p)
}
fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_primary(tokens: [Any], pos: Int) -> Map<String, Any> {
let k = tok_kind(tokens, pos)
let v = tok_value(tokens, pos)
@@ -646,6 +667,8 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_expr(tokens, pos + 1)
let node = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let p = expect(tokens, p, "RParen")
return make_result(node, p)
}
@@ -666,6 +689,8 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_expr(tokens, p)
let elem = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let elems = native_list_append(elems, elem)
let k3 = tok_kind(tokens, p)
if k3 == "Comma" {
@@ -711,6 +736,8 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_expr(tokens, new_p)
let val_node = r["node"]
let new_p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let pair = { "key": key, "value": val_node }
let pairs = native_list_append(pairs, pair)
let k3 = tok_kind(tokens, new_p)
@@ -757,6 +784,8 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_params(tokens, p)
let params = r["params"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let ret_type = ""
let k2 = tok_kind(tokens, p)
if k2 == "Arrow" {
@@ -770,6 +799,8 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r2 = parse_block(tokens, p)
let body = r2["stmts"]
let p = r2["pos"]
// r2 result map fully consumed release to free peak heap.
el_release(r2)
return make_result({ "expr": "Lambda", "params": params, "body": body, "ret_type": ret_type }, p)
}
@@ -778,6 +809,8 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_primary(tokens, pos + 1)
let inner = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
return make_result({ "expr": "Not", "inner": inner }, p)
}
@@ -786,6 +819,8 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_primary(tokens, pos + 1)
let inner = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
return make_result({ "expr": "Neg", "inner": inner }, p)
}
@@ -819,7 +854,7 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
make_result({ "expr": "Nil" }, pos + 1)
}
fn parse_if(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_if(tokens: [Any], pos: Int) -> Map<String, Any> {
let p = expect(tokens, pos, "If")
// Suppress Map-literal parsing in the cond so a stray `{` (the start
// of the then-block) isn't gobbled as a Map.
@@ -829,9 +864,13 @@ fn parse_if(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
state_set("__no_block_expr", prev_no_block)
let cond = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let r2 = parse_block(tokens, p)
let then_stmts = r2["stmts"]
let p = r2["pos"]
// r2 result map fully consumed release to free peak heap.
el_release(r2)
let has_else = false
let else_stmts: [Map<String, Any>] = native_list_empty()
let k2 = tok_kind(tokens, p)
@@ -843,19 +882,23 @@ fn parse_if(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r3 = parse_if(tokens, p)
let nested = r3["node"]
let p = r3["pos"]
// r3 result map fully consumed release to free peak heap.
el_release(r3)
let else_stmts = native_list_append(else_stmts, { "stmt": "Expr", "value": nested })
let has_else = true
} else {
let r3 = parse_block(tokens, p)
let else_stmts = r3["stmts"]
let p = r3["pos"]
// r3 result map fully consumed release to free peak heap.
el_release(r3)
let has_else = true
}
}
make_result({ "expr": "If", "cond": cond, "then": then_stmts, "else": else_stmts, "has_else": has_else }, p)
}
fn parse_match(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_match(tokens: [Any], pos: Int) -> Map<String, Any> {
let p = expect(tokens, pos, "Match")
let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1")
@@ -863,6 +906,8 @@ fn parse_match(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
state_set("__no_block_expr", prev_no_block)
let subject = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let p = expect(tokens, p, "LBrace")
let arms: [Map<String, Any>] = native_list_empty()
let running = true
@@ -878,10 +923,14 @@ fn parse_match(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r2 = parse_pattern(tokens, p)
let pattern = r2["node"]
let p = r2["pos"]
// r2 result map fully consumed release to free peak heap.
el_release(r2)
let p = expect(tokens, p, "FatArrow")
let r3 = parse_expr(tokens, p)
let body = r3["node"]
let p = r3["pos"]
// r3 result map fully consumed release to free peak heap.
el_release(r3)
let arm = { "pattern": pattern, "body": body }
let arms = native_list_append(arms, arm)
let k2 = tok_kind(tokens, p)
@@ -895,7 +944,7 @@ fn parse_match(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
make_result({ "expr": "Match", "subject": subject, "arms": arms }, p)
}
fn parse_pattern(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_pattern(tokens: [Any], pos: Int) -> Map<String, Any> {
let k = tok_kind(tokens, pos)
if k == "Ident" {
let v = tok_value(tokens, pos)
@@ -924,7 +973,7 @@ fn parse_pattern(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
make_result({ "pattern": "Wildcard" }, pos + 1)
}
fn parse_for_expr(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_for_expr(tokens: [Any], pos: Int) -> Map<String, Any> {
let p = expect(tokens, pos, "For")
let item_name = tok_value(tokens, p)
let p = p + 1
@@ -935,13 +984,17 @@ fn parse_for_expr(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
state_set("__no_block_expr", prev_no_block)
let list_expr = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let r2 = parse_block(tokens, p)
let body = r2["stmts"]
let p = r2["pos"]
// r2 result map fully consumed release to free peak heap.
el_release(r2)
make_result({ "expr": "For", "item": item_name, "list": list_expr, "body": body }, p)
}
fn parse_block(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_block(tokens: [Any], pos: Int) -> Map<String, Any> {
let p = expect(tokens, pos, "LBrace")
let stmts: [Map<String, Any>] = native_list_empty()
let running = true
@@ -956,6 +1009,8 @@ fn parse_block(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_stmt(tokens, p)
let stmt = r["node"]
let new_p: Int = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let stmts = native_list_append(stmts, stmt)
// Non-progress guard: a malformed input (e.g. `||` that
// dragged the parser into Map-literal mode partway through
@@ -998,10 +1053,12 @@ fn is_duration_unit(name: String) -> Bool {
false
}
fn parse_postfix(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_postfix(tokens: [Any], pos: Int) -> Map<String, Any> {
let r = parse_primary(tokens, pos)
let node = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
// Postfix duration literal: `<Int>.<unit>` where <unit> is one of
// nanos | millis | seconds | minutes | hours | days (each with an
@@ -1043,6 +1100,8 @@ fn parse_postfix(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r2 = parse_expr(tokens, p)
let arg = r2["node"]
let p = r2["pos"]
// r2 result map fully consumed release to free peak heap.
el_release(r2)
let args = native_list_append(args, arg)
let k3 = tok_kind(tokens, p)
if k3 == "Comma" {
@@ -1063,6 +1122,8 @@ fn parse_postfix(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r2 = parse_expr(tokens, p + 1)
let idx = r2["node"]
let p = r2["pos"]
// r2 result map fully consumed release to free peak heap.
el_release(r2)
let p = expect(tokens, p, "RBracket")
let node = { "expr": "Index", "object": node, "index": idx }
} else {
@@ -1115,10 +1176,12 @@ fn is_binop(kind: String) -> Bool {
false
}
fn parse_binop(tokens: [Map<String, Any>], pos: Int, min_prec: Int) -> Map<String, Any> {
fn parse_binop(tokens: [Any], pos: Int, min_prec: Int) -> Map<String, Any> {
let r = parse_postfix(tokens, pos)
let left = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let running = true
while running {
let k = tok_kind(tokens, p)
@@ -1129,6 +1192,8 @@ fn parse_binop(tokens: [Map<String, Any>], pos: Int, min_prec: Int) -> Map<Strin
let r2 = parse_binop(tokens, p + 1, prec + 1)
let right = r2["node"]
let p = r2["pos"]
// r2 result map fully consumed release to free peak heap.
el_release(r2)
let left = { "expr": "BinOp", "op": op, "left": left, "right": right }
} else {
let running = false
@@ -1140,13 +1205,13 @@ fn parse_binop(tokens: [Map<String, Any>], pos: Int, min_prec: Int) -> Map<Strin
make_result(left, p)
}
fn parse_expr(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_expr(tokens: [Any], pos: Int) -> Map<String, Any> {
parse_binop(tokens, pos, 1)
}
// -- Statement parsing ---------------------------------------------------------
fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
let k = tok_kind(tokens, pos)
// let binding
@@ -1171,6 +1236,8 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_expr(tokens, p)
let val = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
return make_result({ "stmt": "Let", "name": name, "value": val, "type": ltype }, p)
}
@@ -1187,6 +1254,8 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_expr(tokens, p)
let val = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
return make_result({ "stmt": "Return", "value": val }, p)
}
@@ -1201,6 +1270,8 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_params(tokens, p)
let params = r["params"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let ret_type = ""
let k3: String = tok_kind(tokens, p)
if str_eq(k3, "Arrow") {
@@ -1221,6 +1292,8 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_params(tokens, p)
let params = r["params"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
// return type annotation: -> Type. Capture the leading identifier
// so codegen can distinguish Void-returning functions from value-
// returning ones. Anything not "Void" is treated as a value type.
@@ -1237,6 +1310,8 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r2 = parse_block(tokens, p)
let body = r2["stmts"]
let p = r2["pos"]
// r2 result map fully consumed release to free peak heap.
el_release(r2)
return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, p)
}
@@ -1360,9 +1435,13 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
state_set("__no_block_expr", prev_no_block)
let cond = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let r2 = parse_block(tokens, p)
let body = r2["stmts"]
let p = r2["pos"]
// r2 result map fully consumed release to free peak heap.
el_release(r2)
return make_result({ "stmt": "While", "cond": cond, "body": body }, p)
}
@@ -1388,6 +1467,8 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
state_set("__no_block_expr", prev_no_block)
let start_expr = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
// Check for range operator: .. (exclusive) or ..= (inclusive)
let range_k = tok_kind(tokens, p)
if range_k == "DotDot" {
@@ -1396,9 +1477,11 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r2 = parse_expr(tokens, p)
let end_expr = r2["node"]
let p = r2["pos"]
el_release(r2)
let r3 = parse_block(tokens, p)
let body = r3["stmts"]
let p = r3["pos"]
el_release(r3)
return make_result({ "stmt": "ForRange", "var": item_name, "start": start_expr, "end": end_expr, "inclusive": false, "body": body }, p)
}
if range_k == "DotDotEq" {
@@ -1407,9 +1490,11 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r2 = parse_expr(tokens, p)
let end_expr = r2["node"]
let p = r2["pos"]
el_release(r2)
let r3 = parse_block(tokens, p)
let body = r3["stmts"]
let p = r3["pos"]
el_release(r3)
return make_result({ "stmt": "ForRange", "var": item_name, "start": start_expr, "end": end_expr, "inclusive": true, "body": body }, p)
}
// No range operator: regular for-in (list iteration)
@@ -1417,6 +1502,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r2 = parse_block(tokens, p)
let body = r2["stmts"]
let p = r2["pos"]
el_release(r2)
return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p)
}
@@ -1428,6 +1514,8 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r_try = parse_block(tokens, p)
let try_body = r_try["stmts"]
let p = r_try["pos"]
// r_try result map fully consumed release to free peak heap.
el_release(r_try)
let catch_name = "err"
let k2 = tok_kind(tokens, p)
if str_eq(k2, "Catch") {
@@ -1449,6 +1537,8 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r_catch = parse_block(tokens, p)
let catch_body = r_catch["stmts"]
let p = r_catch["pos"]
// r_catch result map fully consumed release to free peak heap.
el_release(r_catch)
return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": catch_body }, p)
}
return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": native_list_empty() }, p)
@@ -1472,6 +1562,8 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
"ret_type": inner["ret_type"],
"decorator": dec_name
}
// r result map fully consumed release to free peak heap.
el_release(r)
return make_result(with_dec, p2)
}
return r
@@ -1592,6 +1684,28 @@ fn parse_stmt(tokens: [Map<String, 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`
@@ -1606,6 +1720,8 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_expr(tokens, p)
let val = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
return make_result({ "stmt": "Assign", "name": name, "value": val }, p)
}
}
@@ -1614,13 +1730,16 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let r = parse_expr(tokens, pos)
let val = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
make_result({ "stmt": "Expr", "value": val }, p)
}
// -- Top-level parse ------------------------------------------------------------
fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
let total: Int = native_list_len(tokens)
fn parse(tokens: [Any]) -> [Map<String, Any>] {
// Flat list: 2 entries per token, so divide by 2 for token count.
let total: Int = native_list_len(tokens) / 2
let stmts: [Map<String, Any>] = native_list_empty()
let pos: Int = 0
let running = true
@@ -1635,6 +1754,8 @@ fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
let r = parse_stmt(tokens, pos)
let stmt = r["node"]
let new_pos: Int = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let stmts = native_list_append(stmts, stmt)
// Guard against infinite loops - if pos didn't advance, force it
if new_pos <= pos {
@@ -1647,3 +1768,297 @@ fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
}
stmts
}
// -- Streaming parse helpers ---------------------------------------------------
// parse_one parse exactly one top-level statement at position `pos`.
// Returns { "node": stmt_map, "pos": new_pos }.
// Enables the streaming compiler pipeline (parse one emit C discard).
fn parse_one(tokens: [Any], pos: Int) -> Map<String, Any> {
parse_stmt(tokens, pos)
}
// skip_to_rbrace advance past a balanced { ... } block.
// On entry, pos must point at the LBrace token.
// Returns the position of the token AFTER the matching RBrace.
fn skip_to_rbrace(tokens: [Any], pos: Int) -> Int {
let total: Int = native_list_len(tokens) / 2
let p: Int = pos + 1
let depth: Int = 1
let going: Bool = true
while going {
if p >= total {
let going = false
} else {
let kk: String = tok_kind(tokens, p)
if str_eq(kk, "Eof") {
let going = false
} else {
if str_eq(kk, "LBrace") {
let depth = depth + 1
let p = p + 1
} else {
if str_eq(kk, "RBrace") {
let depth = depth - 1
let p = p + 1
if depth <= 0 {
let going = false
}
} else {
let p = p + 1
}
}
}
}
}
p
}
// is_stmt_start_kind true if `k` is a token kind that can start a new
// top-level statement (used to find safe stopping points during token skips).
fn is_stmt_start_kind(k: String) -> Bool {
if str_eq(k, "Fn") { return true }
if str_eq(k, "Let") { return true }
if str_eq(k, "Extern") { return true }
if str_eq(k, "Cgi") { return true }
if str_eq(k, "Service") { return true }
if str_eq(k, "Type") { return true }
if str_eq(k, "Enum") { return true }
if str_eq(k, "Import") { return true }
if str_eq(k, "From") { return true }
if str_eq(k, "Eof") { return true }
false
}
// skip_expr_to_stmt_boundary skip tokens from `pos` until we reach a
// token that could start a new top-level statement, staying depth-aware
// so that braces inside expressions don't fool us.
fn skip_expr_to_stmt_boundary(tokens: [Any], pos: Int) -> Int {
let total: Int = native_list_len(tokens) / 2
let p: Int = pos
let depth: Int = 0
let going: Bool = true
while going {
if p >= total {
let going = false
} else {
let kk: String = tok_kind(tokens, p)
if str_eq(kk, "Eof") {
let going = false
} else {
if str_eq(kk, "LBrace") {
let depth = depth + 1
let p = p + 1
} else {
if str_eq(kk, "RBrace") {
if depth <= 0 {
let going = false
} else {
let depth = depth - 1
let p = p + 1
}
} else {
if depth == 0 {
if is_stmt_start_kind(kk) {
let going = false
} else {
let p = p + 1
}
} else {
let p = p + 1
}
}
}
}
}
}
p
}
// scan_params_c scan a parameter list `(name: Type, ...)` starting at
// position `pos` (which should point at LParen) and return the C parameter
// declaration string along with the new position.
// Returns { "c": String, "pos": Int }.
// This avoids allocating param-map objects during the pre-scan phase.
fn scan_params_c(tokens: [Any], pos: Int) -> Map<String, Any> {
let p: Int = expect(tokens, pos, "LParen")
let parts: [String] = native_list_empty()
let going: Bool = true
while going {
let kk: String = tok_kind(tokens, p)
if str_eq(kk, "RParen") {
let going = false
} else {
if str_eq(kk, "Eof") {
let going = false
} else {
let pname: String = tok_value(tokens, p)
let p = p + 1
let p = expect(tokens, p, "Colon")
let p = skip_type(tokens, p)
let parts = native_list_append(parts, "el_val_t " + pname)
let k2: String = tok_kind(tokens, p)
if str_eq(k2, "Comma") {
let p = p + 1
}
}
}
}
let p = expect(tokens, p, "RParen")
let c_str: String = str_join(parts, ", ")
// parts list fully consumed release to free peak heap.
el_release(parts)
if str_eq(c_str, "") { let c_str = "void" }
{ "c": c_str, "pos": p }
}
// scan_fn_sigs lightweight token-level pre-scan.
//
// Returns a list of descriptor maps (one per top-level item) without building
// full expression ASTs or param-map objects. Descriptors have these shapes:
//
// fn/extern_fn: { "kind": "fn"|"extern_fn", "name": String,
// "params_c": String, <- C param decl string, e.g. "el_val_t a, el_val_t b"
// "is_main": Bool }
// toplevel_let: { "kind": "toplevel_let", "name": String, "ltype": String }
// cgi_block: { "kind": "cgi_block", "name": String }
// service_block: { "kind": "service_block", "name": String }
//
// Import/TypeDef/EnumDef nodes are skipped (codegen treats them as no-ops).
//
// The scan allocates only small string values per entry, keeping peak RSS low.
fn scan_fn_sigs(tokens: [Any]) -> [Map<String, Any>] {
let total: Int = native_list_len(tokens) / 2
let sigs: [Map<String, Any>] = native_list_empty()
let pos: Int = 0
let going: Bool = true
while going {
if pos >= total {
let going = false
} else {
let k: String = tok_kind(tokens, pos)
if str_eq(k, "Eof") {
let going = false
} else {
// --- fn definition ---
if str_eq(k, "Fn") {
let p: Int = pos + 1
let name: String = tok_value(tokens, p)
let p = p + 1
let r = scan_params_c(tokens, p)
let params_c: String = r["c"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
// skip return type
let k2: String = tok_kind(tokens, p)
if str_eq(k2, "Arrow") {
let p = p + 1
let p = skip_type(tokens, p)
}
// skip body
let k3: String = tok_kind(tokens, p)
if str_eq(k3, "LBrace") {
let p = skip_to_rbrace(tokens, p)
}
let is_main: Bool = str_eq(name, "main")
let sigs = native_list_append(sigs, {
"kind": "fn",
"name": name,
"params_c": params_c,
"is_main": is_main
})
let pos = p
} else {
// --- extern fn ---
if str_eq(k, "Extern") {
let p: Int = pos + 1
let k2: String = tok_kind(tokens, p)
if str_eq(k2, "Fn") {
let p = p + 1
let name: String = tok_value(tokens, p)
let p = p + 1
let r = scan_params_c(tokens, p)
let params_c: String = r["c"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let k3: String = tok_kind(tokens, p)
if str_eq(k3, "Arrow") {
let p = p + 1
let p = skip_type(tokens, p)
}
let sigs = native_list_append(sigs, {
"kind": "extern_fn",
"name": name,
"params_c": params_c,
"is_main": false
})
let pos = p
} else {
let pos = pos + 1
}
} else {
// --- top-level let ---
if str_eq(k, "Let") {
let p: Int = pos + 1
let name: String = tok_value(tokens, p)
let p = p + 1
let ltype: String = ""
let k2: String = tok_kind(tokens, p)
if str_eq(k2, "Colon") {
let p = p + 1
let kt: String = tok_kind(tokens, p)
if str_eq(kt, "Ident") { let ltype = tok_value(tokens, p) }
let p = skip_type(tokens, p)
}
let p = expect(tokens, p, "Eq")
let p = skip_expr_to_stmt_boundary(tokens, p)
let sigs = native_list_append(sigs, {
"kind": "toplevel_let",
"name": name,
"ltype": ltype
})
let pos = p
} else {
// --- cgi block ---
if str_eq(k, "Cgi") {
let p: Int = pos + 1
let name: String = tok_value(tokens, p)
let p = p + 1
let k2: String = tok_kind(tokens, p)
if str_eq(k2, "LBrace") {
let p = skip_to_rbrace(tokens, p)
}
let sigs = native_list_append(sigs, {
"kind": "cgi_block",
"name": name
})
let pos = p
} else {
// --- service block ---
if str_eq(k, "Service") {
let p: Int = pos + 1
let name: String = tok_value(tokens, p)
let p = p + 1
let k2: String = tok_kind(tokens, p)
if str_eq(k2, "LBrace") {
let p = skip_to_rbrace(tokens, p)
}
let sigs = native_list_append(sigs, {
"kind": "service_block",
"name": name
})
let pos = p
} else {
// Import, Type, Enum, From, or any other token.
// Skip ahead to the next statement boundary.
let p: Int = pos + 1
let p = skip_expr_to_stmt_boundary(tokens, p)
let pos = p
}}}}}
}
}
}
sigs
}