Merge PR #4: perf: 81% RSS reduction in elc compiler + --test mode
This commit is contained in:
Vendored
+3
-3
@@ -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");
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -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 */
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
""
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user