Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f83adf458 | |||
| 9f734b037c | |||
| 049a7712f4 | |||
| c64cbd21e2 | |||
| 37488e9485 | |||
| 8641b4045e | |||
| 49d68fbb20 | |||
| 77a0658d56 | |||
| bff0ad4f22 | |||
| 49f96126b2 | |||
| c954142063 | |||
| 3fd5fec965 | |||
| 5476cbb2b1 | |||
| 65792f7e4c | |||
| c09023003d | |||
| 15b9ccd9e2 | |||
| 9163af81aa | |||
| 3dababa4ad | |||
| 5888258c9f | |||
| a9dc38ed82 | |||
| 4af2b687e1 | |||
| 32f0cf7b5d | |||
| 65e26cd7a5 | |||
| 1fd7cd5545 | |||
| 71689520b6 | |||
| e858eab300 | |||
| aa7d97d5ba | |||
| 7040830470 | |||
| 3a513aaa5a | |||
| beb2a8c5bd | |||
| e23319fe0b | |||
| 01fee9396a | |||
| 7b60d94b8a | |||
| 21694b79d2 | |||
| 422442b14e | |||
| 437ba0a4dd | |||
| 7376349124 | |||
| 0f1da43a97 | |||
| a54b2bebf9 | |||
| 1ed2dc3c11 | |||
| f9cfe43f05 | |||
| f97354e96b |
@@ -0,0 +1,117 @@
|
||||
name: El CI -dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
# -Wl,--allow-multiple-definition: is_digit/is_whitespace exist in both
|
||||
# elc-bootstrap.c (pre-dates runtime text primitives) and el_runtime.c.
|
||||
# Both definitions are equivalent; allow the linker to pick one.
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-Wl,--allow-multiple-definition \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Run all four test suites -all must pass
|
||||
- name: Run tests -text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests -calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests -time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests -html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
- name: Run tests -native (text)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
# Publish artifact to GCP Artifact Registry (dev)
|
||||
- name: Publish elc to Artifact Registry (dev)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
# Also tag as latest-dev
|
||||
echo "Published elc version=${VERSION} to foundation-dev/el/elc"
|
||||
rm -f /tmp/gcp-key.json
|
||||
@@ -0,0 +1,100 @@
|
||||
name: El CI — stage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- stage
|
||||
pull_request:
|
||||
branches:
|
||||
- stage
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host: compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Run all four test suites — all must pass
|
||||
- name: Run tests — text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests — calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests — time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests — html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Publish artifact to GCP Artifact Registry (stage)
|
||||
- name: Publish elc to Artifact Registry (stage)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
echo "Published elc version=${VERSION} to foundation-stage/el/elc"
|
||||
rm -f /tmp/gcp-key.json
|
||||
@@ -129,6 +129,31 @@ jobs:
|
||||
echo "Release published successfully"
|
||||
|
||||
# Dispatch el-sdk-updated event to downstream repos
|
||||
# Publish artifact to GCP Artifact Registry (prod)
|
||||
- name: Publish elc to Artifact Registry (prod)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
echo "Published elc version=${VERSION} to foundation-prod/el/elc"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
- name: Dispatch to foundation/engram
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
Vendored
+4122
-192
File diff suppressed because it is too large
Load Diff
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+827
-176
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <dlfcn.h> /* dlsym for http_set_handler fallback */
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
@@ -154,6 +155,36 @@ el_val_t readline(void) {
|
||||
return el_wrap_str(el_strdup(buf));
|
||||
}
|
||||
|
||||
/* ── stdout redirect helpers ─────────────────────────────────────────────── *
|
||||
* Used by elc post-processing (--minify, --obfuscate): capture codegen *
|
||||
* output into a temp file, then pass it to the external tool. */
|
||||
|
||||
static int _stdout_saved_fd = -1;
|
||||
|
||||
/* stdout_to_file(path) — redirect stdout to <path>. Returns 1 on success. */
|
||||
el_val_t stdout_to_file(el_val_t pathv) {
|
||||
const char* path = EL_CSTR(pathv);
|
||||
if (!path || !*path) return (el_val_t)(int64_t)0;
|
||||
fflush(stdout);
|
||||
_stdout_saved_fd = dup(STDOUT_FILENO);
|
||||
if (_stdout_saved_fd < 0) return (el_val_t)(int64_t)0;
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) { close(_stdout_saved_fd); _stdout_saved_fd = -1; return (el_val_t)(int64_t)0; }
|
||||
dup2(fd, STDOUT_FILENO);
|
||||
close(fd);
|
||||
return (el_val_t)(int64_t)1;
|
||||
}
|
||||
|
||||
/* stdout_restore() — restore stdout from the saved fd. Returns 1 on success. */
|
||||
el_val_t stdout_restore(void) {
|
||||
if (_stdout_saved_fd < 0) return (el_val_t)(int64_t)0;
|
||||
fflush(stdout);
|
||||
dup2(_stdout_saved_fd, STDOUT_FILENO);
|
||||
close(_stdout_saved_fd);
|
||||
_stdout_saved_fd = -1;
|
||||
return (el_val_t)(int64_t)1;
|
||||
}
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t av, el_val_t bv) {
|
||||
@@ -1876,6 +1907,61 @@ el_val_t exec_capture(el_val_t cmdv) {
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
// exec — run a shell command via /bin/sh, capture stdout, return as String.
|
||||
// Times out after 30 seconds. Returns "" on any error.
|
||||
// El name: exec(cmd) -> String
|
||||
el_val_t exec(el_val_t cmdv) {
|
||||
const char* cmd = EL_CSTR(cmdv);
|
||||
if (!cmd || !*cmd) return el_wrap_str(el_strdup(""));
|
||||
/* Build a time-limited command: wrap with timeout(1) if available,
|
||||
* otherwise rely on the 30s read loop guard below. We use the simple
|
||||
* popen approach with a deadline measured by wall clock so the caller
|
||||
* is never blocked indefinitely. */
|
||||
FILE* f = popen(cmd, "r");
|
||||
if (!f) return el_wrap_str(el_strdup(""));
|
||||
JsonBuf b; jb_init(&b);
|
||||
char buf[4096];
|
||||
/* 30-second wall-clock deadline */
|
||||
time_t deadline = time(NULL) + 30;
|
||||
while (time(NULL) < deadline) {
|
||||
if (fgets(buf, sizeof(buf), f) == NULL) break;
|
||||
jb_puts(&b, buf);
|
||||
}
|
||||
pclose(f);
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
// exec_bg — run a shell command in background, return PID as String.
|
||||
// The child process runs independently; the caller is not blocked.
|
||||
// Returns "" on fork failure.
|
||||
// El name: exec_bg(cmd) -> String
|
||||
el_val_t exec_bg(el_val_t cmdv) {
|
||||
const char* cmd = EL_CSTR(cmdv);
|
||||
if (!cmd || !*cmd) return el_wrap_str(el_strdup(""));
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
/* fork failed */
|
||||
return el_wrap_str(el_strdup(""));
|
||||
}
|
||||
if (pid == 0) {
|
||||
/* child: detach from parent's stdio, exec via shell */
|
||||
setsid();
|
||||
int devnull = open("/dev/null", O_RDWR);
|
||||
if (devnull >= 0) {
|
||||
dup2(devnull, STDIN_FILENO);
|
||||
dup2(devnull, STDOUT_FILENO);
|
||||
dup2(devnull, STDERR_FILENO);
|
||||
close(devnull);
|
||||
}
|
||||
execl("/bin/sh", "sh", "-c", cmd, (char*)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
/* parent: convert pid to string and return immediately */
|
||||
char pidbuf[32];
|
||||
snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid);
|
||||
return el_wrap_str(el_strdup(pidbuf));
|
||||
}
|
||||
|
||||
el_val_t fs_list(el_val_t pathv) {
|
||||
const char* path = EL_CSTR(pathv);
|
||||
el_val_t lst = el_list_empty();
|
||||
@@ -2516,6 +2602,45 @@ el_val_t el_html_sanitize(el_val_t input_v, el_val_t allowlist_v) {
|
||||
return el_wrap_str(result);
|
||||
}
|
||||
|
||||
/* ── html_escape / html_raw ──────────────────────────────────────────────── */
|
||||
/*
|
||||
* html_escape(s) — escape a user-supplied string for safe inline interpolation
|
||||
* in HTML text content or attribute values. Escapes: & < > " '
|
||||
*
|
||||
* html_raw(s) — identity function; used by the `raw()` escape hatch in El HTML
|
||||
* templates to explicitly opt out of escaping.
|
||||
*/
|
||||
|
||||
el_val_t html_escape(el_val_t sv) {
|
||||
const char* s = EL_CSTR(sv);
|
||||
if (!s) return EL_STR("");
|
||||
html_buf_t out;
|
||||
html_buf_init(&out);
|
||||
for (const char* p = s; *p; p++) {
|
||||
unsigned char c = (unsigned char)*p;
|
||||
switch (c) {
|
||||
case '&': html_buf_puts(&out, "&"); break;
|
||||
case '<': html_buf_puts(&out, "<"); break;
|
||||
case '>': html_buf_puts(&out, ">"); break;
|
||||
case '"': html_buf_puts(&out, """); break;
|
||||
case '\'': html_buf_puts(&out, "'"); break;
|
||||
default: html_buf_putc(&out, (char)c); break;
|
||||
}
|
||||
}
|
||||
char* result = el_strbuf(out.len);
|
||||
memcpy(result, out.data, out.len);
|
||||
result[out.len] = '\0';
|
||||
html_buf_free(&out);
|
||||
return el_wrap_str(result);
|
||||
}
|
||||
|
||||
el_val_t html_raw(el_val_t sv) {
|
||||
/* Identity — returns the value unchanged. The name exists so generated
|
||||
* code can call html_raw(expr) instead of expr directly, making it clear
|
||||
* at the call site that escaping is intentionally bypassed. */
|
||||
return sv;
|
||||
}
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* True iff the segment is non-empty and every byte is an ASCII digit. We treat
|
||||
|
||||
@@ -79,6 +79,8 @@ extern "C" {
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
el_val_t stdout_to_file(el_val_t path); /* redirect println to a file */
|
||||
el_val_t stdout_restore(void); /* restore stdout after capture */
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -212,6 +214,13 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
|
||||
/* ── HTML template helpers ───────────────────────────────────────────────────
|
||||
* Used by compiled El HTML template expressions.
|
||||
* html_escape(s) — escape & < > " ' for safe inline interpolation.
|
||||
* html_raw(s) — identity; explicit opt-out from escaping (`raw()` form). */
|
||||
el_val_t html_escape(el_val_t s);
|
||||
el_val_t html_raw(el_val_t s);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
@@ -742,6 +751,8 @@ el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
|
||||
/* ── Subprocess execution ────────────────────────────────────────────────── */
|
||||
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
|
||||
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
|
||||
@@ -128,6 +128,22 @@ function str_pad_right(s, width, pad) {
|
||||
return String(s).padEnd(width, String(pad));
|
||||
}
|
||||
|
||||
// ── HTML template helpers ────────────────────────────────────────────────────
|
||||
// Used by compiled El HTML template expressions.
|
||||
// html_escape(s) — escape & < > " ' for safe inline interpolation.
|
||||
// html_raw(s) — identity; explicit opt-out from escaping (raw() form).
|
||||
|
||||
function html_escape(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function html_raw(s) { return s; }
|
||||
|
||||
// ── Math ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function el_abs(n) { return Math.abs(n); }
|
||||
@@ -522,6 +538,328 @@ function math_sin(f) { return Math.sin(f); }
|
||||
function math_cos(f) { return Math.cos(f); }
|
||||
function math_pi() { return Math.PI; }
|
||||
|
||||
// ── DOM bridge (browser-only) ──────────────────────────────────────────────
|
||||
//
|
||||
// These functions wrap the browser DOM API. Each throws a descriptive error
|
||||
// when called from a Node environment, mirroring the pattern used by fs_*
|
||||
// in browser mode.
|
||||
|
||||
function _ensureBrowser(name) {
|
||||
if (IS_NODE) {
|
||||
throw new Error(`${name}: not supported in Node runtime — DOM is browser-only`);
|
||||
}
|
||||
}
|
||||
|
||||
function dom_get_element(id) {
|
||||
_ensureBrowser('dom_get_element');
|
||||
return document.getElementById(String(id));
|
||||
}
|
||||
|
||||
function dom_get_value(el) {
|
||||
_ensureBrowser('dom_get_value');
|
||||
return el == null ? '' : String(el.value ?? '');
|
||||
}
|
||||
|
||||
function dom_set_value(el, v) {
|
||||
_ensureBrowser('dom_set_value');
|
||||
if (el != null) el.value = String(v);
|
||||
}
|
||||
|
||||
function dom_get_text(el) {
|
||||
_ensureBrowser('dom_get_text');
|
||||
return el == null ? '' : String(el.textContent ?? '');
|
||||
}
|
||||
|
||||
function dom_set_text(el, text) {
|
||||
_ensureBrowser('dom_set_text');
|
||||
if (el != null) el.textContent = String(text);
|
||||
}
|
||||
|
||||
function dom_set_prop(el, prop, val) {
|
||||
_ensureBrowser('dom_set_prop');
|
||||
if (el != null) el[String(prop)] = val;
|
||||
}
|
||||
|
||||
function dom_get_prop(el, prop) {
|
||||
_ensureBrowser('dom_get_prop');
|
||||
if (el == null) return null;
|
||||
const v = el[String(prop)];
|
||||
return v === undefined ? null : v;
|
||||
}
|
||||
|
||||
function dom_set_style(el, prop, val) {
|
||||
_ensureBrowser('dom_set_style');
|
||||
if (el != null) el.style[String(prop)] = String(val);
|
||||
}
|
||||
|
||||
function dom_add_class(el, cls) {
|
||||
_ensureBrowser('dom_add_class');
|
||||
if (el != null) el.classList.add(String(cls));
|
||||
}
|
||||
|
||||
function dom_remove_class(el, cls) {
|
||||
_ensureBrowser('dom_remove_class');
|
||||
if (el != null) el.classList.remove(String(cls));
|
||||
}
|
||||
|
||||
function dom_show(el) {
|
||||
_ensureBrowser('dom_show');
|
||||
if (el != null) el.style.display = '';
|
||||
}
|
||||
|
||||
function dom_hide(el) {
|
||||
_ensureBrowser('dom_hide');
|
||||
if (el != null) el.style.display = 'none';
|
||||
}
|
||||
|
||||
function dom_listen(el, event, handler) {
|
||||
_ensureBrowser('dom_listen');
|
||||
if (el != null) el.addEventListener(String(event), handler);
|
||||
}
|
||||
|
||||
function dom_query(selector) {
|
||||
_ensureBrowser('dom_query');
|
||||
return document.querySelector(String(selector));
|
||||
}
|
||||
|
||||
function dom_query_all(selector) {
|
||||
_ensureBrowser('dom_query_all');
|
||||
return Array.from(document.querySelectorAll(String(selector)));
|
||||
}
|
||||
|
||||
function dom_create(tag) {
|
||||
_ensureBrowser('dom_create');
|
||||
return document.createElement(String(tag));
|
||||
}
|
||||
|
||||
function dom_append(parent, child) {
|
||||
_ensureBrowser('dom_append');
|
||||
if (parent != null && child != null) parent.appendChild(child);
|
||||
}
|
||||
|
||||
function dom_remove(el) {
|
||||
_ensureBrowser('dom_remove');
|
||||
if (el != null) el.remove();
|
||||
}
|
||||
|
||||
function dom_is_null(el) {
|
||||
return el === null || el === undefined;
|
||||
}
|
||||
|
||||
// ── Extended DOM API (browser-only) ───────────────────────────────────────
|
||||
|
||||
function dom_set_attr(el, attr, val) {
|
||||
_ensureBrowser('dom_set_attr');
|
||||
if (el != null) el.setAttribute(String(attr), String(val));
|
||||
}
|
||||
|
||||
function dom_get_attr(el, attr) {
|
||||
_ensureBrowser('dom_get_attr');
|
||||
if (el == null) return '';
|
||||
return el.getAttribute(String(attr)) ?? '';
|
||||
}
|
||||
|
||||
function dom_remove_attr(el, attr) {
|
||||
_ensureBrowser('dom_remove_attr');
|
||||
if (el != null) el.removeAttribute(String(attr));
|
||||
}
|
||||
|
||||
function dom_set_html(el, html) {
|
||||
_ensureBrowser('dom_set_html');
|
||||
if (el != null) el.innerHTML = String(html);
|
||||
}
|
||||
|
||||
function dom_get_html(el) {
|
||||
_ensureBrowser('dom_get_html');
|
||||
return el == null ? '' : String(el.innerHTML ?? '');
|
||||
}
|
||||
|
||||
function dom_get_parent(el) {
|
||||
_ensureBrowser('dom_get_parent');
|
||||
return el == null ? null : (el.parentElement ?? null);
|
||||
}
|
||||
|
||||
function dom_contains_class(el, cls) {
|
||||
_ensureBrowser('dom_contains_class');
|
||||
if (el == null) return false;
|
||||
return el.classList.contains(String(cls));
|
||||
}
|
||||
|
||||
function dom_get_checked(el) {
|
||||
_ensureBrowser('dom_get_checked');
|
||||
return el == null ? false : Boolean(el.checked);
|
||||
}
|
||||
|
||||
function dom_set_checked(el, val) {
|
||||
_ensureBrowser('dom_set_checked');
|
||||
if (el != null) el.checked = Boolean(val);
|
||||
}
|
||||
|
||||
// ── Timer API (browser + Node) ─────────────────────────────────────────────
|
||||
|
||||
function set_timeout(ms, cb) {
|
||||
if (typeof setTimeout === 'undefined') {
|
||||
throw new Error('set_timeout: setTimeout not available in this environment');
|
||||
}
|
||||
setTimeout(cb, ms | 0);
|
||||
}
|
||||
|
||||
function set_interval(ms, cb) {
|
||||
if (typeof setInterval === 'undefined') {
|
||||
throw new Error('set_interval: setInterval not available in this environment');
|
||||
}
|
||||
return setInterval(cb, ms | 0);
|
||||
}
|
||||
|
||||
function clear_interval(handle) {
|
||||
if (typeof clearInterval !== 'undefined') clearInterval(handle);
|
||||
}
|
||||
|
||||
// ── Local storage (browser-only) ───────────────────────────────────────────
|
||||
|
||||
function local_storage_get(key) {
|
||||
_ensureBrowser('local_storage_get');
|
||||
return localStorage.getItem(String(key)) ?? '';
|
||||
}
|
||||
|
||||
function local_storage_set(key, val) {
|
||||
_ensureBrowser('local_storage_set');
|
||||
localStorage.setItem(String(key), String(val));
|
||||
}
|
||||
|
||||
function local_storage_remove(key) {
|
||||
_ensureBrowser('local_storage_remove');
|
||||
localStorage.removeItem(String(key));
|
||||
}
|
||||
|
||||
// ── Window location / navigation (browser-only) ────────────────────────────
|
||||
|
||||
function window_location() {
|
||||
_ensureBrowser('window_location');
|
||||
return window.location.href;
|
||||
}
|
||||
|
||||
function window_redirect(url) {
|
||||
_ensureBrowser('window_redirect');
|
||||
window.location.href = String(url);
|
||||
}
|
||||
|
||||
function window_on_load(cb) {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('DOMContentLoaded', cb);
|
||||
} else if (typeof window !== 'undefined') {
|
||||
window.addEventListener('load', cb);
|
||||
}
|
||||
// In Node: no-op
|
||||
}
|
||||
|
||||
// ── console_log (explicit debug log, distinct from println) ────────────────
|
||||
|
||||
function console_log(msg) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(String(msg));
|
||||
}
|
||||
|
||||
// ── Window export helpers ──────────────────────────────────────────────────
|
||||
//
|
||||
// Expose El functions to the browser's global scope so they can be called
|
||||
// from inline event handlers (onclick="increment()") or by external JS.
|
||||
// In Node mode, writes to globalThis so the same pattern works in tests.
|
||||
|
||||
function window_set(name, val) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window[String(name)] = val;
|
||||
} else if (typeof globalThis !== 'undefined') {
|
||||
globalThis[String(name)] = val;
|
||||
}
|
||||
}
|
||||
|
||||
function window_get(name) {
|
||||
if (typeof window !== 'undefined') {
|
||||
const v = window[String(name)];
|
||||
return v === undefined ? null : v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Promise helpers ────────────────────────────────────────────────────────
|
||||
//
|
||||
// Third-party APIs often return Promises but are not El @async functions.
|
||||
// These helpers let El programs chain .then / .catch without needing
|
||||
// native_js, and without requiring the callee to be @async.
|
||||
|
||||
function promise_then(p, cb) {
|
||||
return Promise.resolve(p).then(cb);
|
||||
}
|
||||
|
||||
function promise_catch(p, cb) {
|
||||
return Promise.resolve(p).catch(cb);
|
||||
}
|
||||
|
||||
function promise_resolve(val) {
|
||||
return Promise.resolve(val);
|
||||
}
|
||||
|
||||
function promise_reject(msg) {
|
||||
return Promise.reject(new Error(String(msg)));
|
||||
}
|
||||
|
||||
// ── Object / Array utilities ───────────────────────────────────────────────
|
||||
//
|
||||
// Structural operations on Any-typed JS values. These complement the
|
||||
// El map/list primitives for interop with third-party library objects.
|
||||
|
||||
function object_assign(target, source) {
|
||||
return Object.assign(Object.assign({}, target), source);
|
||||
}
|
||||
|
||||
function object_keys(obj) {
|
||||
if (obj === null || obj === undefined) return [];
|
||||
return Object.keys(obj);
|
||||
}
|
||||
|
||||
function object_values(obj) {
|
||||
if (obj === null || obj === undefined) return [];
|
||||
return Object.values(obj);
|
||||
}
|
||||
|
||||
function json_deep_clone(obj) {
|
||||
if (obj === null || obj === undefined) return null;
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
function array_from(iterable) {
|
||||
if (iterable === null || iterable === undefined) return [];
|
||||
return Array.from(iterable);
|
||||
}
|
||||
|
||||
function type_of(val) {
|
||||
return typeof val;
|
||||
}
|
||||
|
||||
function instanceof_check(val, constructor_name) {
|
||||
if (typeof globalThis[constructor_name] === 'function') {
|
||||
return val instanceof globalThis[constructor_name];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── native_js escape hatch ─────────────────────────────────────────────────
|
||||
//
|
||||
// Evaluate arbitrary JS from El source. Intended for calling third-party
|
||||
// browser libraries (Supabase, Stripe, etc.) until proper El bindings exist.
|
||||
// Use sparingly — this bypasses El's type system entirely.
|
||||
|
||||
function native_js(code) {
|
||||
// eslint-disable-next-line no-eval
|
||||
return eval(String(code));
|
||||
}
|
||||
|
||||
function native_js_call(obj, method, args) {
|
||||
if (obj == null) throw new Error('native_js_call: object is null');
|
||||
return obj[String(method)](...(Array.isArray(args) ? args : []));
|
||||
}
|
||||
|
||||
// ── Stubs for not-yet-supported features ───────────────────────────────────
|
||||
//
|
||||
// These compile but throw when called. See spec/codegen-js.md §7.
|
||||
@@ -632,6 +970,31 @@ const __el = {
|
||||
// Float / math
|
||||
float_to_str, int_to_float, float_to_int, format_float, decimal_round,
|
||||
str_to_float, math_sqrt, math_log, math_ln, math_sin, math_cos, math_pi,
|
||||
// DOM bridge (browser-only)
|
||||
dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,
|
||||
dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,
|
||||
dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,
|
||||
dom_append, dom_remove, dom_is_null,
|
||||
// Extended DOM
|
||||
dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,
|
||||
dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,
|
||||
// Timers
|
||||
set_timeout, set_interval, clear_interval,
|
||||
// Local storage
|
||||
local_storage_get, local_storage_set, local_storage_remove,
|
||||
// Window location
|
||||
window_location, window_redirect, window_on_load,
|
||||
// Debug
|
||||
console_log,
|
||||
// Window export helpers
|
||||
window_set, window_get,
|
||||
// Promise helpers
|
||||
promise_then, promise_catch, promise_resolve, promise_reject,
|
||||
// Object / Array utilities
|
||||
object_assign, object_keys, object_values, json_deep_clone,
|
||||
array_from, type_of, instanceof_check,
|
||||
// native_js escape hatch
|
||||
native_js, native_js_call,
|
||||
// CGI / DHARMA / Engram / LLM (stubs)
|
||||
el_cgi_init,
|
||||
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
|
||||
@@ -670,10 +1033,35 @@ export {
|
||||
fs_read, fs_write, fs_list,
|
||||
json_parse, json_stringify, json_get, json_get_string, json_get_int,
|
||||
time_now, time_now_utc, sleep_ms,
|
||||
// HTML template helpers
|
||||
html_escape, html_raw,
|
||||
bool_to_str, exit_program, args, env,
|
||||
state_set, state_get, state_del, state_keys,
|
||||
el_cgi_init,
|
||||
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
|
||||
engram_node, engram_search, engram_activate,
|
||||
llm_call, llm_call_system,
|
||||
// DOM bridge
|
||||
dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,
|
||||
dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,
|
||||
dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,
|
||||
dom_append, dom_remove, dom_is_null,
|
||||
// Extended DOM
|
||||
dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,
|
||||
dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,
|
||||
// Timers
|
||||
set_timeout, set_interval, clear_interval,
|
||||
// Local storage
|
||||
local_storage_get, local_storage_set, local_storage_remove,
|
||||
// Window location
|
||||
window_location, window_redirect, window_on_load,
|
||||
// Debug
|
||||
console_log,
|
||||
// Window / native_js
|
||||
window_set, window_get, native_js, native_js_call,
|
||||
// Promise helpers
|
||||
promise_then, promise_catch, promise_resolve, promise_reject,
|
||||
// Object / Array utilities
|
||||
object_assign, object_keys, object_values, json_deep_clone,
|
||||
array_from, type_of, instanceof_check,
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,756 @@
|
||||
/*
|
||||
* el_runtime.h — El language C runtime header
|
||||
*
|
||||
* Declares all built-in functions available to compiled El programs.
|
||||
* Include this in every generated .c file.
|
||||
*
|
||||
* Value model:
|
||||
* All El values are represented as el_val_t (= int64_t).
|
||||
* On 64-bit systems a pointer fits in int64_t.
|
||||
* String values are cast: (el_val_t)(uintptr_t)"hello"
|
||||
* Integer values are stored directly.
|
||||
* This lets arithmetic work naturally while still passing strings around.
|
||||
*
|
||||
* Type conventions (El -> C):
|
||||
* String -> el_val_t (holds const char* via uintptr_t cast)
|
||||
* Int -> el_val_t
|
||||
* Bool -> el_val_t (0 = false, nonzero = true)
|
||||
* Any -> el_val_t
|
||||
* Void -> void
|
||||
*
|
||||
* Macros for convenience:
|
||||
* EL_STR(s) cast string literal to el_val_t
|
||||
* EL_CSTR(v) cast el_val_t back to const char*
|
||||
* EL_INT(v) identity — el_val_t is already int64_t
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — required for the HTTP client (http_get, http_post, llm_*).
|
||||
* -lpthread — required for the HTTP server (one detached thread per
|
||||
* connection, capped at 64 concurrent).
|
||||
* -loqs — optional; required only when liboqs is installed and the
|
||||
* pq_* / sha3_256_hex entry points are needed. Detected at
|
||||
* compile time via __has_include(<oqs/oqs.h>).
|
||||
* -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in
|
||||
* pq_hybrid_* and HKDF-SHA256 derivation.
|
||||
*
|
||||
* Canonical compile command:
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*
|
||||
* With liboqs (post-quantum stack):
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef int64_t el_val_t;
|
||||
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
#define EL_NULL ((el_val_t)0)
|
||||
|
||||
/* Float values share the el_val_t (int64) slot via a bit-cast.
|
||||
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
|
||||
* underlying bits represent the IEEE 754 double. Float-aware builtins
|
||||
* (math, format, json) round-trip via these helpers. */
|
||||
static inline double el_to_float(el_val_t v) {
|
||||
union { int64_t i; double f; } u;
|
||||
u.i = (int64_t)v;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
static inline el_val_t el_from_float(double f) {
|
||||
union { double f; int64_t i; } u;
|
||||
u.f = f;
|
||||
return (el_val_t)u.i;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t str_eq(el_val_t a, el_val_t b);
|
||||
el_val_t str_starts_with(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_ends_with(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_len(el_val_t s);
|
||||
el_val_t str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t int_to_str(el_val_t n);
|
||||
el_val_t str_to_int(el_val_t s);
|
||||
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t str_contains(el_val_t s, el_val_t sub);
|
||||
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
|
||||
el_val_t str_to_upper(el_val_t s);
|
||||
el_val_t str_to_lower(el_val_t s);
|
||||
el_val_t str_trim(el_val_t s);
|
||||
|
||||
/* ── Math ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_abs(el_val_t n);
|
||||
el_val_t el_max(el_val_t a, el_val_t b);
|
||||
el_val_t el_min(el_val_t a, el_val_t b);
|
||||
|
||||
/* ── Refcount (ARC) ──────────────────────────────────────────────────────────
|
||||
* Lists and Maps carry a refcount. Strings and ints do not — el_retain and
|
||||
* el_release are safe no-ops on non-refcounted values (they sniff a magic
|
||||
* header at offset 0 and only act if the magic matches).
|
||||
*
|
||||
* Codegen emits these at let-binding shadowing, function entry (params), and
|
||||
* function exit (locals other than the returned value). The refcount lets
|
||||
* el_list_append and el_map_set mutate in place when uniquely owned (cheap)
|
||||
* and copy-on-write when shared (preserves persistent semantics across
|
||||
* accumulator patterns in the compiler itself). */
|
||||
|
||||
void el_retain(el_val_t v);
|
||||
void el_release(el_val_t v);
|
||||
|
||||
/* ── List ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_list_new(el_val_t count, ...);
|
||||
el_val_t el_list_len(el_val_t list);
|
||||
el_val_t el_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t el_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t el_list_empty(void);
|
||||
el_val_t el_list_clone(el_val_t list);
|
||||
|
||||
/* ── Map ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_map_new(el_val_t pair_count, ...);
|
||||
el_val_t el_get_field(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_get(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value);
|
||||
|
||||
/* ── HTTP ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t http_get(el_val_t url);
|
||||
el_val_t http_post(el_val_t url, el_val_t body);
|
||||
el_val_t http_post_json(el_val_t url, el_val_t json_body);
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
void http_serve(el_val_t port, el_val_t handler);
|
||||
void http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
* Same dispatch model as http_serve, but the handler signature is widened:
|
||||
*
|
||||
* el_val_t handler(method, path, headers_map, body)
|
||||
*
|
||||
* `headers_map` is an ElMap from lowercased header name → header value (both
|
||||
* Strings). Repeated headers are joined with ", " per RFC 7230.
|
||||
*
|
||||
* Response value: the handler may return either
|
||||
* (a) a plain body string — same auto-content-type / 200-OK behaviour as
|
||||
* http_serve (3-arg) — or
|
||||
* (b) a response envelope built with `http_response(status, headers_json,
|
||||
* body)`. The runtime detects the envelope discriminator
|
||||
* `"el_http_response":1` at the start of the returned string and
|
||||
* unpacks status / headers / body before sending.
|
||||
*
|
||||
* The 3-arg http_serve(port, handler) remains supported unchanged for
|
||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||
void http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
* returned string carries the discriminator `{"el_http_response":1,...}`
|
||||
* which the runtime's send-path detects and unpacks. Detection happens
|
||||
* uniformly inside http_send_response, so a 3-arg handler may also return
|
||||
* an envelope. The 3-arg variant remains documented as a fixed 200-OK
|
||||
* auto-content-type contract for legacy handlers that return plain bodies. */
|
||||
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
|
||||
* 60000ms). Read lazily on first use, so setting the env var any time before
|
||||
* the first http_* call is sufficient. */
|
||||
|
||||
/* Streaming variants — write the response body straight to a file via
|
||||
* libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string
|
||||
* wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive
|
||||
* embedded NUL bytes that would truncate a strlen()-based code path.
|
||||
*
|
||||
* Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same
|
||||
* `headers_map` shape as http_post_with_headers (ElMap of String→String).
|
||||
*
|
||||
* Return value: 1 on success (file fully written), 0 on any failure
|
||||
* (network, file open, partial write). On failure the output file is removed
|
||||
* so callers cannot mistake a partially-written file for a valid one. */
|
||||
el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path);
|
||||
el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path);
|
||||
|
||||
/* ── URL encoding ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */
|
||||
el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
|
||||
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
|
||||
* el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML
|
||||
* cleaner. State-machine parser; tag/attribute names compared case-
|
||||
* insensitively against the allowlist; `<a href>` / `<… src>` URL schemes
|
||||
* validated (http, https, mailto, fragment-only, or relative); whole-
|
||||
* subtree drop for script / style / iframe / object / embed / form; HTML-
|
||||
* escapes free text outside dropped subtrees.
|
||||
*
|
||||
* The allowlist is JSON of the form
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
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_exists(el_val_t path);
|
||||
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
|
||||
|
||||
/* Length-explicit binary write. `length` is an Int (el_val_t holding the
|
||||
* byte count). The caller knows the length from context — typically because
|
||||
* `bytes` came from base64_decode (which produces a magic-tagged binary
|
||||
* buffer with embedded NULs possible) and the caller already tracks the
|
||||
* decoded length, OR because the bytes came from a fixed-size source
|
||||
* (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely.
|
||||
*
|
||||
* Returns 1 on success, 0 on failure (invalid path, can't open, partial
|
||||
* write, negative length). On partial-write failure, the file is removed
|
||||
* so callers cannot read back a truncated artefact. */
|
||||
el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length);
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t json_get(el_val_t json, el_val_t key);
|
||||
el_val_t json_parse(el_val_t s);
|
||||
el_val_t json_stringify(el_val_t v);
|
||||
el_val_t json_get_string(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_int(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_float(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_bool(el_val_t json_str, el_val_t key);
|
||||
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);
|
||||
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);
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t time_now(void);
|
||||
el_val_t time_now_utc(void);
|
||||
el_val_t sleep_secs(el_val_t secs);
|
||||
el_val_t sleep_ms(el_val_t ms);
|
||||
el_val_t time_format(el_val_t ts, el_val_t fmt);
|
||||
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);
|
||||
|
||||
/* ── Instant + Duration: first-class temporal types ──────────────────────────
|
||||
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
|
||||
* since the Unix epoch; Durations are signed nanoseconds. Type discipline
|
||||
* is enforced at codegen-time: BinOps on names registered as Instant or
|
||||
* Duration route through the typed wrappers below; mismatches like
|
||||
* Instant+Instant become #error at the C compiler.
|
||||
*
|
||||
* Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are
|
||||
* recognised by the parser as DurationLit AST nodes and lowered to literal
|
||||
* int64 nanoseconds at codegen time. The runtime never sees the units. */
|
||||
|
||||
el_val_t el_now_instant(void);
|
||||
el_val_t now(void);
|
||||
el_val_t unix_seconds(el_val_t n);
|
||||
el_val_t unix_millis(el_val_t n);
|
||||
el_val_t instant_from_iso8601(el_val_t s);
|
||||
|
||||
el_val_t el_duration_from_nanos(el_val_t ns);
|
||||
el_val_t duration_seconds(el_val_t n);
|
||||
el_val_t duration_millis(el_val_t n);
|
||||
el_val_t duration_nanos(el_val_t n);
|
||||
|
||||
el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_diff(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_add(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_sub(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_scale(el_val_t dur, el_val_t scalar);
|
||||
el_val_t el_duration_div(el_val_t dur, el_val_t scalar);
|
||||
|
||||
el_val_t el_instant_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ne(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ne(el_val_t a, el_val_t b);
|
||||
|
||||
el_val_t instant_to_unix_seconds(el_val_t i);
|
||||
el_val_t instant_to_unix_millis(el_val_t i);
|
||||
el_val_t instant_to_iso8601(el_val_t i);
|
||||
el_val_t duration_to_seconds(el_val_t d);
|
||||
el_val_t duration_to_millis(el_val_t d);
|
||||
el_val_t duration_to_nanos(el_val_t d);
|
||||
|
||||
el_val_t el_sleep_duration(el_val_t dur);
|
||||
el_val_t unix_timestamp(void);
|
||||
|
||||
el_val_t ttl_cache_set(el_val_t key, el_val_t value);
|
||||
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
|
||||
el_val_t ttl_cache_age(el_val_t key);
|
||||
|
||||
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
|
||||
* Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
|
||||
* zones, Gregorian, DST) is the user-facing default; MarsCalendar,
|
||||
* CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
|
||||
* domains.
|
||||
*
|
||||
* A Calendar interprets an Instant under a particular cycle convention and
|
||||
* produces a CalendarTime. CalendarTime carries the underlying Instant and
|
||||
* a back-pointer to its Calendar; arithmetic and formatting consult the
|
||||
* Calendar to convert ns since epoch into year/month/day/hour/minute/second
|
||||
* (or sol/phase, or cycle/phase, depending on kind).
|
||||
*
|
||||
* Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
|
||||
* LocalDateTime are heap-allocated structs whose pointers are cast into
|
||||
* el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
|
||||
* the kind safely. LocalTime is small enough to live in the int64 slot
|
||||
* directly (nanos since midnight, signed). */
|
||||
|
||||
/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
|
||||
* `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
|
||||
* offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
|
||||
* on first use of the owning EarthCalendar. */
|
||||
el_val_t zone(el_val_t id);
|
||||
el_val_t zone_utc(void);
|
||||
el_val_t zone_local(void);
|
||||
el_val_t zone_offset(el_val_t hours, el_val_t minutes);
|
||||
|
||||
/* Calendar constructors. Each returns an el_val_t pointer to a heap-
|
||||
* allocated, magic-tagged Calendar struct. Calendars are interned by
|
||||
* (kind, zone_id, period_ns, epoch_ns) so identical constructors return
|
||||
* the same pointer — equality is reference equality. */
|
||||
el_val_t earth_calendar(el_val_t z);
|
||||
el_val_t earth_calendar_default(void);
|
||||
el_val_t mars_calendar(void);
|
||||
el_val_t cycle_calendar(el_val_t period_dur);
|
||||
el_val_t no_cycle_calendar(void);
|
||||
el_val_t relative_calendar(el_val_t epoch_inst);
|
||||
|
||||
/* CalendarTime constructors and methods. Returns a heap-allocated struct
|
||||
* whose pointer fits in el_val_t. */
|
||||
el_val_t now_in(el_val_t cal);
|
||||
el_val_t in_calendar(el_val_t inst, el_val_t cal);
|
||||
el_val_t cal_format(el_val_t ct, el_val_t pattern);
|
||||
el_val_t cal_to_instant(el_val_t ct);
|
||||
el_val_t cal_cycle_phase(el_val_t ct);
|
||||
el_val_t cal_in(el_val_t ct, el_val_t cal);
|
||||
|
||||
/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
|
||||
* LocalTime carries nanoseconds since midnight as a signed int64 directly
|
||||
* in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
|
||||
* heap-allocated structs with magic headers. */
|
||||
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
|
||||
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
|
||||
el_val_t local_datetime(el_val_t date, el_val_t time);
|
||||
el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
|
||||
|
||||
el_val_t local_date_year(el_val_t ld);
|
||||
el_val_t local_date_month(el_val_t ld);
|
||||
el_val_t local_date_day(el_val_t ld);
|
||||
el_val_t local_time_hour(el_val_t lt);
|
||||
el_val_t local_time_minute(el_val_t lt);
|
||||
el_val_t local_time_second(el_val_t lt);
|
||||
el_val_t local_time_nanos(el_val_t lt);
|
||||
|
||||
el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
|
||||
el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
|
||||
el_val_t el_local_date_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_local_date_eq(el_val_t a, el_val_t b);
|
||||
|
||||
/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
|
||||
* pointer in el_val_t; rhythms are immutable so callers may share them. */
|
||||
el_val_t rhythm_cycle_start(void);
|
||||
el_val_t rhythm_cycle_phase(el_val_t phase);
|
||||
el_val_t rhythm_duration(el_val_t d);
|
||||
el_val_t rhythm_session_start(void);
|
||||
el_val_t rhythm_event(el_val_t name);
|
||||
el_val_t rhythm_and(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_or(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_weekday(el_val_t day);
|
||||
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
|
||||
el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
|
||||
el_val_t rhythm_matches(el_val_t r, el_val_t ct);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t uuid_new(void);
|
||||
el_val_t uuid_v4(void);
|
||||
|
||||
/* ── Environment ─────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t env(el_val_t key);
|
||||
|
||||
/* ── In-process state K/V ────────────────────────────────────────────────── */
|
||||
|
||||
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);
|
||||
|
||||
/* ── Float formatting ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t float_to_str(el_val_t f);
|
||||
el_val_t int_to_float(el_val_t n);
|
||||
el_val_t float_to_int(el_val_t f);
|
||||
el_val_t format_float(el_val_t f, el_val_t decimals);
|
||||
el_val_t decimal_round(el_val_t f, el_val_t decimals);
|
||||
el_val_t str_to_float(el_val_t s);
|
||||
|
||||
/* ── Math (Float-aware) ──────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t math_sqrt(el_val_t f);
|
||||
el_val_t math_log(el_val_t f);
|
||||
el_val_t math_ln(el_val_t f);
|
||||
el_val_t math_sin(el_val_t f);
|
||||
el_val_t math_cos(el_val_t f);
|
||||
el_val_t math_pi(void);
|
||||
|
||||
/* ── String additions ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t str_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_split(el_val_t s, el_val_t sep);
|
||||
el_val_t str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t str_char_code(el_val_t s, el_val_t i);
|
||||
el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_format(el_val_t fmt, el_val_t data);
|
||||
el_val_t str_lower(el_val_t s);
|
||||
el_val_t str_upper(el_val_t s);
|
||||
|
||||
/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
|
||||
* Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex.
|
||||
* is_* predicates: empty input returns false; multi-char requires ALL bytes
|
||||
* to match. ASCII ranges only in Phase 1. */
|
||||
|
||||
/* Counting */
|
||||
el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */
|
||||
el_val_t str_count_chars(el_val_t s); /* codepoint count */
|
||||
el_val_t str_count_bytes(el_val_t s); /* alias of str_len */
|
||||
el_val_t str_count_lines(el_val_t s);
|
||||
el_val_t str_count_words(el_val_t s);
|
||||
el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */
|
||||
el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */
|
||||
|
||||
/* Find / position */
|
||||
el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */
|
||||
el_val_t str_last_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */
|
||||
|
||||
/* Transform */
|
||||
el_val_t str_repeat(el_val_t s, el_val_t n);
|
||||
el_val_t str_reverse(el_val_t s); /* by codepoint */
|
||||
el_val_t str_strip_prefix(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_strip_suffix(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_strip_chars(el_val_t s, el_val_t chars);
|
||||
el_val_t str_lstrip(el_val_t s);
|
||||
el_val_t str_rstrip(el_val_t s);
|
||||
|
||||
/* Char classification (Bool) */
|
||||
el_val_t is_letter(el_val_t s);
|
||||
el_val_t is_digit(el_val_t s);
|
||||
el_val_t is_alphanumeric(el_val_t s);
|
||||
el_val_t is_whitespace(el_val_t s);
|
||||
el_val_t is_punctuation(el_val_t s);
|
||||
el_val_t is_uppercase(el_val_t s);
|
||||
el_val_t is_lowercase(el_val_t s);
|
||||
|
||||
/* Split / join */
|
||||
el_val_t str_split_lines(el_val_t s);
|
||||
el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */
|
||||
el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n);
|
||||
el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */
|
||||
|
||||
/* ── List additions ──────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t list_push(el_val_t list, el_val_t elem);
|
||||
el_val_t list_push_front(el_val_t list, el_val_t elem);
|
||||
el_val_t list_join(el_val_t list, el_val_t sep);
|
||||
el_val_t list_range(el_val_t start, el_val_t end);
|
||||
|
||||
/* ── Bool helpers ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t bool_to_str(el_val_t b);
|
||||
|
||||
/* ── Numeric parsing ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t parse_int(el_val_t s, el_val_t default_val);
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
void exit_program(el_val_t code);
|
||||
el_val_t getpid_now(void);
|
||||
|
||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||
* Records the program's DHARMA identity before any other code executes. */
|
||||
|
||||
void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
||||
el_val_t network, el_val_t engram);
|
||||
|
||||
/* ── DHARMA network builtins ─────────────────────────────────────────────────
|
||||
* Available to CGI programs (declared with a `cgi {}` block).
|
||||
*
|
||||
* Peers are addressed by `dharma_id` of the form
|
||||
* "<registry-id>@<transport-url>" e.g. "ntn-genesis@http://localhost:7770"
|
||||
* If the @<url> portion is omitted, transport defaults to
|
||||
* "http://localhost:7770" (the local CGI daemon assumption).
|
||||
*
|
||||
* Wire protocol (all peers expose):
|
||||
* POST <url>/dharma/recv { channel, from, content } → response body
|
||||
* POST <url>/dharma/event { type, payload, source, timestamp }
|
||||
* POST <url>/api/activate { query } → list of nodes
|
||||
*
|
||||
* Hosting application's responsibility: an El program with a `cgi {}` block
|
||||
* runs http_serve() with its own request handler; that handler should route
|
||||
* "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so
|
||||
* incoming events feed dharma_field() queues. The runtime itself does not
|
||||
* intercept any /dharma path. */
|
||||
|
||||
el_val_t dharma_connect(el_val_t cgi_id);
|
||||
el_val_t dharma_send(el_val_t channel, el_val_t content);
|
||||
el_val_t dharma_activate(el_val_t query);
|
||||
void dharma_emit(el_val_t event_type, el_val_t payload);
|
||||
el_val_t dharma_field(el_val_t event_type);
|
||||
void dharma_strengthen(el_val_t cgi_id, el_val_t weight);
|
||||
el_val_t dharma_relationship(el_val_t cgi_id);
|
||||
el_val_t dharma_peers(void);
|
||||
|
||||
/* Public C API: called by an El program's HTTP handler when a /dharma/event
|
||||
* request arrives. Pushes onto the per-event-type queue and signals any
|
||||
* pending dharma_field() blockers. All three arguments must be NUL-terminated
|
||||
* C strings (or NULL — then treated as empty). */
|
||||
void el_runtime_dharma_event_arrive(const char* event_type,
|
||||
const char* payload,
|
||||
const char* source);
|
||||
|
||||
/* ── Engram local graph primitives ───────────────────────────────────────────
|
||||
* Operate on the CGI's local Engram knowledge graph.
|
||||
* `engram_activate` queries the local graph only; `dharma_activate` is
|
||||
* network-wide across all connected CGI graphs. */
|
||||
|
||||
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
|
||||
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags);
|
||||
/* Layered consciousness — see el_runtime.c for the layered architecture
|
||||
* design notes (search "Layered consciousness architecture"). The five
|
||||
* canonical layers (safety / core-identity / domain-knowledge / imprint /
|
||||
* suit) are seeded automatically; engram_add_layer extends the registry
|
||||
* with imprint or suit overlays at runtime. Nodes default to layer 1
|
||||
* (core-identity) when created via engram_node / engram_node_full. */
|
||||
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable);
|
||||
el_val_t engram_remove_layer(el_val_t layer_id);
|
||||
el_val_t engram_list_layers(void);
|
||||
el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
el_val_t engram_node_count(void);
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
|
||||
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
|
||||
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id);
|
||||
el_val_t engram_neighbors(el_val_t node_id);
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_edge_count(void);
|
||||
/* Three-pass activation: background fan-out → working-memory promotion →
|
||||
* Layer 0 override. See "Three-pass activation" in el_runtime.c. */
|
||||
el_val_t engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_save(el_val_t path);
|
||||
el_val_t engram_load(el_val_t path);
|
||||
|
||||
/* JSON-string accessors — return pre-serialized JSON so HTTP handlers
|
||||
* can pass results straight through without round-tripping ElList/ElMap
|
||||
* through json_stringify. */
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_stats_json(void);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||
* no nodes promoted to working memory. */
|
||||
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
||||
* All functions call https://api.anthropic.com/v1/messages with the API key
|
||||
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
|
||||
|
||||
el_val_t llm_call(el_val_t model, el_val_t prompt);
|
||||
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt);
|
||||
el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools);
|
||||
el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64);
|
||||
el_val_t llm_models(void);
|
||||
|
||||
/* Register a tool handler by name. The handler is looked up via dlsym
|
||||
* (mirroring http_set_handler), so any El `fn <name>(input)` compiles to
|
||||
* a global C symbol that this function can locate at runtime.
|
||||
* Handler signature: `el_val_t handler(el_val_t input_json)` — receives
|
||||
* the tool input as a JSON-string el_val_t and returns a JSON-string
|
||||
* el_val_t result. Used by llm_call_agentic. */
|
||||
void llm_register_tool(el_val_t name, el_val_t handler_fn_name);
|
||||
|
||||
/* ── args() ─────────────────────────────────────────────────────────────────
|
||||
* Provides access to command-line arguments passed to the program.
|
||||
* Populated by el_runtime_init_args() before main() runs. */
|
||||
|
||||
el_val_t args(void);
|
||||
void el_runtime_init_args(int argc, char** argv);
|
||||
|
||||
/* ── Crypto primitives ─────────────────────────────────────────────────────
|
||||
* SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe).
|
||||
* Self-contained — no OpenSSL/libcrypto dependency. The implementations are
|
||||
* adapted from public-domain reference code (Brad Conte / RFC 4648).
|
||||
*
|
||||
* Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string
|
||||
* value whose contents are raw binary; callers usually feed these into
|
||||
* base64_encode. Note that el_val_t strings are NUL-terminated by convention,
|
||||
* so the binary payload may contain embedded NULs — pass it directly into
|
||||
* base64_encode (which uses an explicit length) rather than treating it as
|
||||
* a printable C string.
|
||||
*
|
||||
* The "base64" variants emit/accept RFC 4648 standard alphabet with padding.
|
||||
* The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding,
|
||||
* as used in JWTs. */
|
||||
|
||||
el_val_t sha256_hex(el_val_t input);
|
||||
el_val_t sha256_bytes(el_val_t input);
|
||||
el_val_t hmac_sha256_hex(el_val_t key, el_val_t message);
|
||||
el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message);
|
||||
el_val_t base64_encode(el_val_t input);
|
||||
el_val_t base64_decode(el_val_t input);
|
||||
el_val_t base64url_encode(el_val_t input);
|
||||
el_val_t base64url_decode(el_val_t input);
|
||||
|
||||
/* Length-aware variants (internal — exposed for the rare caller that already
|
||||
* has a known-length binary buffer and doesn't want to round-trip through
|
||||
* a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed
|
||||
* these implicitly. */
|
||||
el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len);
|
||||
el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe);
|
||||
|
||||
/* ── Post-quantum primitives (liboqs-backed) ────────────────────────────────
|
||||
* All inputs/outputs hex-encoded. Algorithm choices:
|
||||
* Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced)
|
||||
* KEM: CRYSTALS-Kyber-768 (NIST level 3)
|
||||
* Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2)
|
||||
*
|
||||
* If liboqs is not linked (detected via __has_include(<oqs/oqs.h>) at compile
|
||||
* time), the pq_* entry points return a JSON-shaped error string so callers
|
||||
* fail loudly rather than silently fall back to classical schemes:
|
||||
* {"error":"liboqs not linked, post-quantum primitives unavailable"}
|
||||
*
|
||||
* The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and
|
||||
* CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss).
|
||||
* Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack,
|
||||
* Kyber holds. SHA3-256 also remains usable independent of liboqs (the
|
||||
* Keccak permutation is PQ-OK as a primitive). */
|
||||
|
||||
el_val_t pq_keygen_signature(void);
|
||||
el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message);
|
||||
el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex);
|
||||
|
||||
el_val_t pq_kem_keygen(void);
|
||||
el_val_t pq_kem_encaps(el_val_t public_key_hex);
|
||||
el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex);
|
||||
|
||||
el_val_t pq_hybrid_keygen(void);
|
||||
el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined);
|
||||
|
||||
el_val_t sha3_256_hex(el_val_t input);
|
||||
|
||||
/* ── AEAD: AES-256-GCM (libcrypto-backed) ───────────────────────────────────
|
||||
* Symmetric authenticated encryption used to wrap envelopes after a KEM
|
||||
* handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the
|
||||
* Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256.
|
||||
*
|
||||
* aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where
|
||||
* ciphertext is the AES-256-GCM output with the 16-byte auth tag appended.
|
||||
* Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which
|
||||
* structurally rules out the GCM nonce-reuse footgun.
|
||||
*
|
||||
* aead_decrypt returns the plaintext String, or "" on any failure (including
|
||||
* auth-tag mismatch). Callers MUST check for "" before trusting the result. */
|
||||
el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext);
|
||||
el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex);
|
||||
|
||||
/* ── Native VM builtin aliases (for compiled El source) ─────────────────────
|
||||
* These match the El VM's native_* builtins so that El source compiled
|
||||
* to C can call the same names without modification. */
|
||||
|
||||
el_val_t native_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t native_list_len(el_val_t list);
|
||||
el_val_t native_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t native_list_empty(void);
|
||||
el_val_t native_list_clone(el_val_t list);
|
||||
el_val_t native_string_chars(el_val_t s);
|
||||
el_val_t native_int_to_str(el_val_t n);
|
||||
|
||||
/* ── Method-call shorthand aliases ──────────────────────────────────────────
|
||||
* The El method-call convention `obj.method(args)` compiles to
|
||||
* `method(obj, args)`. These aliases expose the runtime functions under
|
||||
* the short names that result from method calls in El source.
|
||||
*
|
||||
* Example: `myList.append(x)` → `append(myList, x)` (calls this alias)
|
||||
* `myList.len()` → `len(myList)` (calls this alias) */
|
||||
|
||||
el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */
|
||||
el_val_t len(el_val_t list); /* el_list_len */
|
||||
el_val_t get(el_val_t list, el_val_t index); /* el_list_get */
|
||||
el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */
|
||||
el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
|
||||
|
||||
/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */
|
||||
/* See bottom of el_runtime.c for the implementation.
|
||||
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
|
||||
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
|
||||
/* ── Subprocess execution ────────────────────────────────────────────────── */
|
||||
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
|
||||
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+671
-48
@@ -86,6 +86,57 @@ fn js_binop(op: String) -> String {
|
||||
op
|
||||
}
|
||||
|
||||
// ── Known El runtime method names ─────────────────────────────────────────────
|
||||
//
|
||||
// These are the method shortforms exported by el_runtime.js and used by the
|
||||
// El C-backend convention of `obj.method(args)` -> `method(obj, args)`.
|
||||
// Any method name NOT in this set is treated as a native JS method call on the
|
||||
// receiver object, emitting `obj.method(args)` directly.
|
||||
//
|
||||
// This is the mechanism that makes `client.auth.signInWithOtp(payload)` work
|
||||
// without `native_js_call`: the receiver is Any-typed, the method is unknown
|
||||
// to El, so codegen emits the JS call directly.
|
||||
|
||||
fn js_is_el_method(name: String) -> Bool {
|
||||
if str_eq(name, "append") { return true }
|
||||
if str_eq(name, "len") { return true }
|
||||
if str_eq(name, "get") { return true }
|
||||
if str_eq(name, "map_get") { return true }
|
||||
if str_eq(name, "map_set") { return true }
|
||||
false
|
||||
}
|
||||
|
||||
// ── Async function tracking ───────────────────────────────────────────────────
|
||||
//
|
||||
// Functions decorated with @async are recorded here. Any call to a known-async
|
||||
// builtin (http_get, http_post, http_post_json) or to a user-declared @async
|
||||
// function gets an `await` prefix in generated JS.
|
||||
//
|
||||
// Known-async builtins — these return Promise<T> in el_runtime.js.
|
||||
fn js_is_async_builtin(name: String) -> Bool {
|
||||
if str_eq(name, "http_get") { return true }
|
||||
if str_eq(name, "http_post") { return true }
|
||||
if str_eq(name, "http_post_json") { return true }
|
||||
if str_eq(name, "http_get_with_headers") { return true }
|
||||
if str_eq(name, "http_post_with_headers") { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn js_register_async_fn(name: String) -> Bool {
|
||||
let csv: String = state_get("__js_async_fns")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__js_async_fns", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn js_is_async_fn(name: String) -> Bool {
|
||||
let csv: String = state_get("__js_async_fns")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
// ── Int-name tracking (mirrors codegen.el) ────────────────────────────────────
|
||||
|
||||
fn js_is_int_name(name: String) -> Bool {
|
||||
@@ -140,6 +191,138 @@ fn js_is_int_call(call_expr: Map<String, Any>) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ── HTML template codegen (JS) ────────────────────────────────────────────────
|
||||
//
|
||||
// HTML template expressions compile to a JS IIFE that builds the HTML string
|
||||
// using string concatenation. Interpolated values go through html_escape();
|
||||
// raw() bypasses escaping. {#each} blocks compile to Array.forEach or a
|
||||
// for-loop that pushes fragments into a parts array.
|
||||
//
|
||||
// Entry point: js_cg_html_template(expr) → JS expression string.
|
||||
|
||||
fn js_next_html_id() -> String {
|
||||
let csv: String = state_get("__js_html_counter")
|
||||
let n = 0
|
||||
if !str_eq(csv, "") {
|
||||
let n = str_to_int(csv)
|
||||
}
|
||||
let n = n + 1
|
||||
state_set("__js_html_counter", native_int_to_str(n))
|
||||
native_int_to_str(n)
|
||||
}
|
||||
|
||||
fn js_cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
|
||||
let n: Int = native_list_len(children)
|
||||
let i = 0
|
||||
let out = ""
|
||||
while i < n {
|
||||
let child: Map<String, Any> = native_list_get(children, i)
|
||||
let html_kind: String = child["html"]
|
||||
if str_eq(html_kind, "Text") {
|
||||
let text: String = child["text"]
|
||||
let out = out + acc_var + " += " + js_str_lit(text) + "; "
|
||||
}
|
||||
if str_eq(html_kind, "Doctype") {
|
||||
let out = out + acc_var + " += \"<!doctype html>\"; "
|
||||
}
|
||||
if str_eq(html_kind, "Interp") {
|
||||
let val_node = child["value"]
|
||||
let val_c: String = js_cg_expr(val_node)
|
||||
let out = out + acc_var + " += html_escape(" + val_c + "); "
|
||||
}
|
||||
if str_eq(html_kind, "Raw") {
|
||||
let val_node = child["value"]
|
||||
let val_c: String = js_cg_expr(val_node)
|
||||
let out = out + acc_var + " += html_raw(" + val_c + "); "
|
||||
}
|
||||
if str_eq(html_kind, "Element") {
|
||||
let elem_c: String = js_cg_html_element_str(child, acc_var)
|
||||
let out = out + elem_c
|
||||
}
|
||||
if str_eq(html_kind, "Each") {
|
||||
let each_c: String = js_cg_html_each(child, acc_var)
|
||||
let out = out + each_c
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn js_cg_html_attrs_str(attrs: [Map<String, Any>], acc_var: String) -> String {
|
||||
let n: Int = native_list_len(attrs)
|
||||
let i = 0
|
||||
let out = ""
|
||||
while i < n {
|
||||
let attr: Map<String, Any> = native_list_get(attrs, i)
|
||||
let attr_name: String = attr["name"]
|
||||
let kind: String = attr["kind"]
|
||||
// open-attr snippet: " name=\""
|
||||
let open_val: String = " " + attr_name + "=\""
|
||||
if str_eq(kind, "static") {
|
||||
let sv: String = attr["value"]
|
||||
let out = out + acc_var + " += " + js_str_lit(open_val) + "; "
|
||||
let out = out + acc_var + " += " + js_str_lit(sv) + "; "
|
||||
let out = out + acc_var + " += " + js_str_lit("\"") + "; "
|
||||
} else {
|
||||
if str_eq(kind, "dynamic") {
|
||||
let val_node = attr["value"]
|
||||
let val_c: String = js_cg_expr(val_node)
|
||||
let out = out + acc_var + " += " + js_str_lit(open_val) + "; "
|
||||
let out = out + acc_var + " += html_escape(" + val_c + "); "
|
||||
let out = out + acc_var + " += " + js_str_lit("\"") + "; "
|
||||
} else {
|
||||
// Boolean attribute
|
||||
let out = out + acc_var + " += " + js_str_lit(" " + attr_name) + "; "
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn js_cg_html_element_str(elem: Map<String, Any>, acc_var: String) -> String {
|
||||
let tag: String = elem["tag"]
|
||||
let attrs: [Map<String, Any>] = elem["attrs"]
|
||||
let children: [Map<String, Any>] = elem["children"]
|
||||
let self_closing: Bool = elem["self_closing"]
|
||||
let out = acc_var + " += " + js_str_lit("<" + tag) + "; "
|
||||
let out = out + js_cg_html_attrs_str(attrs, acc_var)
|
||||
if self_closing {
|
||||
let out = out + acc_var + " += \"/>\"" + "; "
|
||||
} else {
|
||||
let out = out + acc_var + " += \">\"; "
|
||||
let out = out + js_cg_html_parts(children, acc_var)
|
||||
let out = out + acc_var + " += " + js_str_lit("</" + tag + ">") + "; "
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn js_cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
|
||||
let list_expr = node["list"]
|
||||
let item_name: String = node["item"]
|
||||
let body_children: [Map<String, Any>] = node["body"]
|
||||
let id: String = js_next_html_id()
|
||||
let list_var: String = "_html_list_" + id
|
||||
let len_var: String = "_html_len_" + id
|
||||
let idx_var: String = "_html_i_" + id
|
||||
let list_c: String = js_cg_expr(list_expr)
|
||||
let inner_c: String = js_cg_html_parts(body_children, acc_var)
|
||||
"{ const " + list_var + " = " + list_c + "; const " + len_var + " = el_list_len(" + list_var + "); for (let " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { const " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
|
||||
}
|
||||
|
||||
fn js_cg_html_template(expr: Map<String, Any>) -> String {
|
||||
let root = expr["root"]
|
||||
let id: String = js_next_html_id()
|
||||
let acc: String = "_html_" + id
|
||||
let doctype_flag: Bool = root["doctype"]
|
||||
let doctype_prefix: String = ""
|
||||
if doctype_flag {
|
||||
let doctype_prefix = acc + " += \"<!doctype html>\"; "
|
||||
}
|
||||
let body: String = js_cg_html_element_str(root, acc)
|
||||
"(() => { let " + acc + " = \"\"; " + doctype_prefix + body + "return " + acc + "; })()"
|
||||
}
|
||||
|
||||
// ── Expression codegen ────────────────────────────────────────────────────────
|
||||
//
|
||||
// js_cg_expr returns a JS expression string (not a statement).
|
||||
@@ -377,20 +560,38 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
|
||||
|
||||
if func_kind == "Ident" {
|
||||
let fn_name: String = func["name"]
|
||||
return fn_name + "(" + args_c + ")"
|
||||
let call_expr: String = fn_name + "(" + args_c + ")"
|
||||
if js_is_async_builtin(fn_name) {
|
||||
return "await " + call_expr
|
||||
}
|
||||
if js_is_async_fn(fn_name) {
|
||||
return "await " + call_expr
|
||||
}
|
||||
return call_expr
|
||||
}
|
||||
|
||||
if func_kind == "Field" {
|
||||
// El's `obj.method(args)` becomes `method(obj, args)` — same
|
||||
// convention as the C backend. The runtime exports method
|
||||
// shortforms (append, len, get, map_get, map_set) that match.
|
||||
let obj = func["object"]
|
||||
let field: String = func["field"]
|
||||
let obj_c: String = js_cg_expr(obj)
|
||||
if arity > 0 {
|
||||
return field + "(" + obj_c + ", " + args_c + ")"
|
||||
// If the method is a known El runtime shortform, keep the El
|
||||
// convention: `method(obj, args)`. This preserves backward
|
||||
// compatibility with list.append(x), map.map_get(k), etc.
|
||||
if js_is_el_method(field) {
|
||||
if arity > 0 {
|
||||
return field + "(" + obj_c + ", " + args_c + ")"
|
||||
}
|
||||
return field + "(" + obj_c + ")"
|
||||
}
|
||||
return field + "(" + obj_c + ")"
|
||||
// Unknown method — emit as a native JS method call on the
|
||||
// receiver. This handles Any-typed values (third-party library
|
||||
// objects, DOM elements, Promises, etc.) without requiring
|
||||
// native_js_call. Example: `client.auth.signInWithOtp(payload)`
|
||||
// emits `client["auth"].signInWithOtp(args_c)`.
|
||||
if arity > 0 {
|
||||
return obj_c + "." + field + "(" + args_c + ")"
|
||||
}
|
||||
return obj_c + "." + field + "()"
|
||||
}
|
||||
|
||||
let fn_c: String = js_cg_expr(func)
|
||||
@@ -398,22 +599,39 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
|
||||
}
|
||||
|
||||
if kind == "Field" {
|
||||
// El's `obj.foo` becomes JS `obj["foo"]` — works on plain objects
|
||||
// (maps) and on JS objects with prototype. el_get_field is a
|
||||
// runtime helper for callers that want EL_NULL on missing keys.
|
||||
// El's `obj.foo` becomes JS `obj["foo"]` — direct bracket access.
|
||||
// This works for plain El map objects AND for real JS objects with
|
||||
// prototype-inherited properties (DOM elements, third-party library
|
||||
// objects, Promises, etc.). el_get_field used hasOwnProperty which
|
||||
// silently returned null for inherited props, breaking e.g. client.auth.
|
||||
//
|
||||
// Nil-propagation: `obj?.foo` emits `(obj)?.["foo"] ?? null`.
|
||||
let obj = expr["object"]
|
||||
let field: String = expr["field"]
|
||||
let obj_kind: String = obj["expr"]
|
||||
if str_eq(obj_kind, "Try") {
|
||||
let inner = obj["inner"]
|
||||
let inner_c: String = js_cg_expr(inner)
|
||||
return "(" + inner_c + ")?.[" + js_str_lit(field) + "] ?? null"
|
||||
}
|
||||
let obj_c: String = js_cg_expr(obj)
|
||||
return "el_get_field(" + obj_c + ", " + js_str_lit(field) + ")"
|
||||
return obj_c + "[" + js_str_lit(field) + "]"
|
||||
}
|
||||
|
||||
if kind == "Index" {
|
||||
// Map vs list dispatch on the index expression kind, same as C.
|
||||
// If the object is a Try (nil-propagation), use JS optional indexing.
|
||||
let obj = expr["object"]
|
||||
let idx = expr["index"]
|
||||
let obj_c: String = js_cg_expr(obj)
|
||||
let idx_c: String = js_cg_expr(idx)
|
||||
let idx_kind: String = idx["expr"]
|
||||
let obj_kind: String = obj["expr"]
|
||||
if str_eq(obj_kind, "Try") {
|
||||
let inner = obj["inner"]
|
||||
let inner_c: String = js_cg_expr(inner)
|
||||
return "(" + inner_c + ")?.[" + idx_c + "] ?? null"
|
||||
}
|
||||
if str_eq(idx_kind, "Str") {
|
||||
return "el_get_field(" + obj_c + ", " + idx_c + ")"
|
||||
}
|
||||
@@ -453,6 +671,12 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
|
||||
}
|
||||
|
||||
if kind == "Try" {
|
||||
// Postfix `?` — nil-propagation guard.
|
||||
// When used as `expr?.field` the Field handler above intercepts and
|
||||
// emits `(expr)?.["field"]`. Here, a bare `expr?` (not followed by
|
||||
// field/index access) passes through to the inner expression unchanged
|
||||
// (it acts as an identity but marks the value as "nil-propagating" for
|
||||
// its caller). This matches the C backend's current behavior.
|
||||
let inner = expr["inner"]
|
||||
return js_cg_expr(inner)
|
||||
}
|
||||
@@ -470,6 +694,17 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
|
||||
return js_cg_match(expr)
|
||||
}
|
||||
|
||||
// Lambda (anonymous function literal): fn(params) -> RetType { body }
|
||||
// Emitted as a JS arrow function expression: (params) => { body }.
|
||||
// Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... })
|
||||
if kind == "Lambda" {
|
||||
return js_cg_lambda(expr)
|
||||
}
|
||||
|
||||
if kind == "HtmlTemplate" {
|
||||
return js_cg_html_template(expr)
|
||||
}
|
||||
|
||||
"null"
|
||||
}
|
||||
|
||||
@@ -528,8 +763,16 @@ fn js_cg_match(expr: Map<String, Any>) -> String {
|
||||
if str_eq(v, "true") { let bv = "true" }
|
||||
let parts = native_list_append(parts, "if (" + subj_var + " === " + bv + ") return (" + body_c + "); ")
|
||||
} else {
|
||||
// unknown pattern → wildcard
|
||||
let parts = native_list_append(parts, "return (" + body_c + "); ")
|
||||
if str_eq(pkind, "Variant") {
|
||||
// Enum::Variant patterns — El enums compile to plain
|
||||
// strings (the variant name) or ints. Match the subject
|
||||
// against the variant name string.
|
||||
let variant: String = pat["variant"]
|
||||
let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", " + js_str_lit(variant) + ")) return (" + body_c + "); ")
|
||||
} else {
|
||||
// unknown pattern → wildcard
|
||||
let parts = native_list_append(parts, "return (" + body_c + "); ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,6 +784,65 @@ fn js_cg_match(expr: Map<String, Any>) -> String {
|
||||
str_join(parts, "")
|
||||
}
|
||||
|
||||
// ── Lambda codegen ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Anonymous function literals: fn(params) -> RetType { body }
|
||||
//
|
||||
// Strategy: emit the lambda as a hoisted JS function declaration with a
|
||||
// generated name (__lambda_N), then return the name as the expression value.
|
||||
// This works because JS function declarations are hoisted within their scope,
|
||||
// so the generated name is valid at any use site within the same function or
|
||||
// module. The emitted code looks like:
|
||||
//
|
||||
// function __lambda_1(event) { dom_hide(spinner); }
|
||||
// ...
|
||||
// dom_listen(btn, "click", __lambda_1);
|
||||
//
|
||||
// This approach is clean, debuggable, and avoids any need for a string-buffer
|
||||
// mode in the codegen.
|
||||
|
||||
fn js_next_lambda_id() -> String {
|
||||
let csv: String = state_get("__js_lambda_counter")
|
||||
let n = 0
|
||||
if !str_eq(csv, "") {
|
||||
let n = str_to_int(csv)
|
||||
}
|
||||
let n = n + 1
|
||||
state_set("__js_lambda_counter", native_int_to_str(n))
|
||||
native_int_to_str(n)
|
||||
}
|
||||
|
||||
fn js_cg_lambda(expr: Map<String, Any>) -> String {
|
||||
let params = expr["params"]
|
||||
let body = expr["body"]
|
||||
let ret_type: String = expr["ret_type"]
|
||||
let id: String = js_next_lambda_id()
|
||||
let lambda_name: String = "__lambda_" + id
|
||||
let params_str: String = js_params_str(params)
|
||||
// Emit the function definition immediately into the output stream.
|
||||
// It will appear before the statement containing this expression.
|
||||
js_emit_line("function " + lambda_name + "(" + params_str + ") {")
|
||||
let decl = native_list_empty()
|
||||
let np: Int = native_list_len(params)
|
||||
let pi = 0
|
||||
while pi < np {
|
||||
let param = native_list_get(params, pi)
|
||||
let pname: String = param["name"]
|
||||
let decl = native_list_append(decl, pname)
|
||||
let pi = pi + 1
|
||||
}
|
||||
let body_xformed = body
|
||||
if !str_eq(ret_type, "Void") {
|
||||
let body_xformed = js_transform_implicit_return(body)
|
||||
}
|
||||
js_build_int_names_for_params(params)
|
||||
js_cg_stmts(body_xformed, " ", decl)
|
||||
js_emit_line("}")
|
||||
js_emit_blank()
|
||||
// Return the function name as the expression value.
|
||||
lambda_name
|
||||
}
|
||||
|
||||
// ── Variable scope tracking ───────────────────────────────────────────────────
|
||||
//
|
||||
// El allows `let x = ...` to redeclare in the same scope. JS would throw
|
||||
@@ -646,6 +948,31 @@ fn js_cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [St
|
||||
if kind == "TypeDef" { return declared }
|
||||
if kind == "EnumDef" { return declared }
|
||||
if kind == "Import" { return declared }
|
||||
// TestDef: skip in normal mode; handled by js_codegen_test in test mode.
|
||||
if kind == "TestDef" { return declared }
|
||||
// Assert: no-op in normal mode; handled by js_cg_stmt_assert in test mode.
|
||||
if kind == "Assert" { return declared }
|
||||
|
||||
if kind == "TryCatch" {
|
||||
let try_body = stmt["try_body"]
|
||||
let catch_name: String = stmt["catch_name"]
|
||||
let catch_body = stmt["catch_body"]
|
||||
js_emit_line(indent + "try {")
|
||||
js_cg_stmts(try_body, indent + " ", native_list_clone(declared))
|
||||
js_emit_line(indent + "} catch (" + catch_name + ") {")
|
||||
js_cg_stmts(catch_body, indent + " ", native_list_clone(declared))
|
||||
js_emit_line(indent + "}")
|
||||
return declared
|
||||
}
|
||||
|
||||
// ExternFn: the function exists in the JS environment (loaded via <script>
|
||||
// tag or the module context). Emit a comment so the generated file is
|
||||
// self-documenting, but no JS function body — the implementation is external.
|
||||
if kind == "ExternFn" {
|
||||
let ename: String = stmt["name"]
|
||||
js_emit_line(indent + "// extern fn " + ename + " — provided by the JS environment")
|
||||
return declared
|
||||
}
|
||||
if kind == "CgiBlock" {
|
||||
// CGI blocks compile to a no-op + warning comment in JS target.
|
||||
// The runtime cgi identity is server-side; UI code is not a CGI
|
||||
@@ -785,16 +1112,30 @@ fn js_cg_fn(stmt: Map<String, Any>) -> Void {
|
||||
let params = stmt["params"]
|
||||
let body = stmt["body"]
|
||||
let ret_type: String = stmt["ret_type"]
|
||||
let decorator: String = stmt["decorator"]
|
||||
let params_str: String = js_params_str(params)
|
||||
js_build_int_names_for_params(params)
|
||||
|
||||
// Special-case `fn main` — emit as a regular function and call it
|
||||
// at module bottom (after all top-level statements). This matches
|
||||
// the C backend's behavior where `fn main` is the entry point.
|
||||
if fn_name == "main" {
|
||||
js_emit_line("function main(" + params_str + ") {")
|
||||
// Detect @async decorator — emit `async function` and register the name
|
||||
// so call sites for this function get `await` prefixed automatically.
|
||||
// When the decorator field is absent, el_get_field returns null; str_eq
|
||||
// handles null safely (returns false), so no special nil-check is needed.
|
||||
if str_eq(decorator, "async") {
|
||||
js_register_async_fn(fn_name)
|
||||
if fn_name == "main" {
|
||||
js_emit_line("async function main(" + params_str + ") {")
|
||||
} else {
|
||||
js_emit_line("async function " + fn_name + "(" + params_str + ") {")
|
||||
}
|
||||
} else {
|
||||
js_emit_line("function " + fn_name + "(" + params_str + ") {")
|
||||
// Special-case `fn main` — emit as a regular function and call it
|
||||
// at module bottom (after all top-level statements). This matches
|
||||
// the C backend's behavior where `fn main` is the entry point.
|
||||
if fn_name == "main" {
|
||||
js_emit_line("function main(" + params_str + ") {")
|
||||
} else {
|
||||
js_emit_line("function " + fn_name + "(" + params_str + ") {")
|
||||
}
|
||||
}
|
||||
|
||||
let decl = native_list_empty()
|
||||
@@ -830,50 +1171,297 @@ fn js_is_top_level_decl(stmt: Map<String, Any>) -> Bool {
|
||||
if kind == "Import" { return true }
|
||||
if kind == "CgiBlock" { return true }
|
||||
if kind == "ServiceBlock" { return true }
|
||||
if kind == "ExternFn" { return true }
|
||||
if kind == "TestDef" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
// ── Test mode codegen (JS) ────────────────────────────────────────────────────
|
||||
//
|
||||
// reporter = "text" → human-readable output to stderr (console.error)
|
||||
// reporter = "json" → newline-delimited JSON to stdout (process.stdout.write)
|
||||
//
|
||||
// The test function returns bool: true = pass, false = fail.
|
||||
|
||||
fn js_cg_stmt_assert_text(stmt: Map<String, Any>, test_name: String) -> Void {
|
||||
let expr_node = stmt["expr"]
|
||||
let msg: String = stmt["msg"]
|
||||
let expr_c: String = js_cg_expr(expr_node)
|
||||
let disp_msg = "assert failed"
|
||||
if !str_eq(msg, "") { let disp_msg = msg }
|
||||
js_emit_line(" if (!(" + expr_c + ")) {")
|
||||
js_emit_line(" process.stderr.write(\" FAIL " + js_escape(test_name) + " — " + js_escape(disp_msg) + "\\n\");")
|
||||
js_emit_line(" return false;")
|
||||
js_emit_line(" }")
|
||||
}
|
||||
|
||||
fn js_cg_stmt_assert_json(stmt: Map<String, Any>, test_name: String, file_name: String, test_line: Int) -> Void {
|
||||
let expr_node = stmt["expr"]
|
||||
let msg: String = stmt["msg"]
|
||||
let assert_line: Int = stmt["line"]
|
||||
let expr_c: String = js_cg_expr(expr_node)
|
||||
let disp_msg = "assert failed"
|
||||
if !str_eq(msg, "") { let disp_msg = msg }
|
||||
js_emit_line(" if (!(" + expr_c + ")) {")
|
||||
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_fail\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + ",assert_line:" + native_int_to_str(assert_line) + ",message:" + js_str_lit(disp_msg) + "}) + \"\\n\");")
|
||||
js_emit_line(" return false;")
|
||||
js_emit_line(" }")
|
||||
}
|
||||
|
||||
// js_cg_stmts_in_test: emit test body, routing Assert to the right handler.
|
||||
fn js_cg_stmts_in_test(stmts: [Map<String, Any>], indent: String, declared: [String], test_name: String, reporter: String, file_name: String, test_line: Int) -> [String] {
|
||||
let n: Int = native_list_len(stmts)
|
||||
let i = 0
|
||||
let decl = declared
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk: String = stmt["stmt"]
|
||||
if str_eq(sk, "Assert") {
|
||||
if str_eq(reporter, "json") {
|
||||
js_cg_stmt_assert_json(stmt, test_name, file_name, test_line)
|
||||
} else {
|
||||
js_cg_stmt_assert_text(stmt, test_name)
|
||||
}
|
||||
} else {
|
||||
let decl = js_cg_stmt(stmt, indent, decl)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
decl
|
||||
}
|
||||
|
||||
// js_cg_test_fn: emit a single async test function.
|
||||
fn js_cg_test_fn(test_def: Map<String, Any>, idx: Int, reporter: String, file_name: String) -> String {
|
||||
let fn_name: String = "el_test_" + native_int_to_str(idx)
|
||||
let test_name: String = test_def["name"]
|
||||
let test_line: Int = test_def["line"]
|
||||
let body = test_def["body"]
|
||||
js_emit_line("async function " + fn_name + "() {")
|
||||
js_cg_stmts_in_test(body, " ", native_list_empty(), test_name, reporter, file_name, test_line)
|
||||
js_emit_line(" return true;")
|
||||
js_emit_line("}")
|
||||
js_emit_blank()
|
||||
fn_name
|
||||
}
|
||||
|
||||
// js_codegen_test: emit the test runner (replaces main() when --test active).
|
||||
// reporter: "text" or "json"
|
||||
// file_name: basename of the source file (used in JSON output)
|
||||
fn js_codegen_test(stmts: [Map<String, Any>], reporter: String, file_name: String) -> Void {
|
||||
// Collect TestDef nodes in order.
|
||||
let n: Int = native_list_len(stmts)
|
||||
let test_defs: [Map<String, Any>] = native_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk: String = stmt["stmt"]
|
||||
if str_eq(sk, "TestDef") {
|
||||
let test_defs = native_list_append(test_defs, stmt)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
let n_tests: Int = native_list_len(test_defs)
|
||||
|
||||
// Emit non-test function definitions (skip fn main and TestDef nodes).
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
if js_is_fndef(stmt) {
|
||||
let fn_name: String = stmt["name"]
|
||||
if !str_eq(fn_name, "main") {
|
||||
js_cg_fn(stmt)
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Emit each test function.
|
||||
let ti = 0
|
||||
while ti < n_tests {
|
||||
let test_def = native_list_get(test_defs, ti)
|
||||
js_cg_test_fn(test_def, ti, reporter, file_name)
|
||||
let ti = ti + 1
|
||||
}
|
||||
|
||||
// Emit the test runner IIFE.
|
||||
let test_word = "tests"
|
||||
if n_tests == 1 { let test_word = "test" }
|
||||
js_emit_line("(async () => {")
|
||||
js_emit_line(" let pass = 0; let fail = 0;")
|
||||
|
||||
if str_eq(reporter, "json") {
|
||||
// JSON reporter: suite_start to stdout
|
||||
js_emit_line(" process.stdout.write(JSON.stringify({type:\"suite_start\",file:" + js_str_lit(file_name) + ",total:" + native_int_to_str(n_tests) + "}) + \"\\n\");")
|
||||
let ti = 0
|
||||
while ti < n_tests {
|
||||
let test_def = native_list_get(test_defs, ti)
|
||||
let test_name: String = test_def["name"]
|
||||
let test_line: Int = test_def["line"]
|
||||
let fn_name: String = "el_test_" + native_int_to_str(ti)
|
||||
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_start\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + "}) + \"\\n\");")
|
||||
js_emit_line(" if (await " + fn_name + "()) {")
|
||||
js_emit_line(" pass++;")
|
||||
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_pass\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + ",duration_ms:0}) + \"\\n\");")
|
||||
js_emit_line(" } else { fail++; }")
|
||||
let ti = ti + 1
|
||||
}
|
||||
js_emit_line(" process.stdout.write(JSON.stringify({type:\"suite_end\",passed:pass,failed:fail}) + \"\\n\");")
|
||||
} else {
|
||||
// Text reporter: human-readable to stderr
|
||||
js_emit_line(" process.stderr.write(\"==> running " + native_int_to_str(n_tests) + " " + test_word + "\\n\\n\");")
|
||||
let ti = 0
|
||||
while ti < n_tests {
|
||||
let test_def = native_list_get(test_defs, ti)
|
||||
let test_name: String = test_def["name"]
|
||||
let fn_name: String = "el_test_" + native_int_to_str(ti)
|
||||
js_emit_line(" process.stderr.write(\" RUN " + js_escape(test_name) + "\\n\");")
|
||||
js_emit_line(" if (await " + fn_name + "()) { pass++; process.stderr.write(\" PASS " + js_escape(test_name) + "\\n\"); }")
|
||||
js_emit_line(" else { fail++; }")
|
||||
let ti = ti + 1
|
||||
}
|
||||
js_emit_line(" process.stderr.write(\"\\n\" + pass + \" passed, \" + fail + \" failed\\n\");")
|
||||
}
|
||||
|
||||
js_emit_line(" process.exit(fail > 0 ? 1 : 0);")
|
||||
js_emit_line("})();")
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
|
||||
codegen_js_inner(stmts, source, false, "", false, "text", "")
|
||||
}
|
||||
|
||||
// codegen_js_test: emit a JS test binary.
|
||||
// reporter: "text" or "json"
|
||||
// file_name: basename of the source file (used in JSON output)
|
||||
fn codegen_js_test(stmts: [Map<String, Any>], source: String, reporter: String, file_name: String) -> String {
|
||||
codegen_js_inner(stmts, source, false, "", true, reporter, file_name)
|
||||
}
|
||||
|
||||
fn codegen_js_bundle(stmts: [Map<String, Any>], source: String, runtime_content: String) -> String {
|
||||
codegen_js_inner(stmts, source, true, runtime_content, false, "text", "")
|
||||
}
|
||||
|
||||
fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool, runtime_content: String, test_mode: Bool, reporter: String, file_name: String) -> String {
|
||||
// Reset per-compile state.
|
||||
state_set("__js_int_names", "")
|
||||
state_set("__js_match_counter", "")
|
||||
state_set("__js_async_fns", "")
|
||||
state_set("__js_lambda_counter", "")
|
||||
|
||||
// Preamble: inline the runtime via a single import that side-effects
|
||||
// globalThis. The runtime path is resolved relative to the generated
|
||||
// output; users running `elc --target=js` are responsible for ensuring
|
||||
// el_runtime.js is reachable. For self-contained output, the runtime
|
||||
// could be inlined; that is a follow-up.
|
||||
// Preamble: in bundle mode, inline the runtime and wrap in IIFE.
|
||||
// In module mode, emit a single import that side-effects globalThis.
|
||||
js_emit_line("// Generated by elc --target=js")
|
||||
js_emit_line("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js")
|
||||
js_emit_line("import \"./el_runtime.js\";")
|
||||
js_emit_line("const {")
|
||||
js_emit_line(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,")
|
||||
js_emit_line(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,")
|
||||
js_emit_line(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,")
|
||||
js_emit_line(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,")
|
||||
js_emit_line(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,")
|
||||
js_emit_line(" list_push, list_join, list_range,")
|
||||
js_emit_line(" el_map_new, el_get_field, el_map_get, el_map_set,")
|
||||
js_emit_line(" http_get, http_post, http_post_json,")
|
||||
js_emit_line(" fs_read, fs_write, fs_list,")
|
||||
js_emit_line(" json_parse, json_stringify, json_get, json_get_string, json_get_int,")
|
||||
js_emit_line(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,")
|
||||
js_emit_line(" el_retain, el_release,")
|
||||
js_emit_line(" append, len, get, map_get, map_set,")
|
||||
js_emit_line(" native_list_get, native_list_len, native_list_append, native_list_empty,")
|
||||
js_emit_line(" native_list_clone, native_string_chars, native_int_to_str,")
|
||||
js_emit_line(" args, state_set, state_get, state_del, state_keys, env,")
|
||||
js_emit_line(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,")
|
||||
js_emit_line(" engram_node, engram_search, engram_activate,")
|
||||
js_emit_line(" llm_call, llm_call_system,")
|
||||
js_emit_line("} = globalThis.__el;")
|
||||
if bundle_mode {
|
||||
js_emit_line("// Bundle mode: runtime inlined, no import statement needed.")
|
||||
js_emit_line("// Drop directly into a <script> tag.")
|
||||
js_emit_line(";(function() {")
|
||||
js_emit_line("\"use strict\";")
|
||||
// Inline the runtime content verbatim (already read from el_runtime.js).
|
||||
// Strip the final ES export lines -- they use `export` syntax which is
|
||||
// not valid inside an IIFE. The globalThis.__el assignment is what matters.
|
||||
js_emit_line(js_strip_es_exports(runtime_content))
|
||||
js_emit_line("")
|
||||
} else {
|
||||
js_emit_line("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js")
|
||||
js_emit_line("import \"./el_runtime.js\";")
|
||||
}
|
||||
// In module mode: destructure all builtins off globalThis.__el so call
|
||||
// sites stay flat (println(x) not el.println(x)).
|
||||
// In bundle mode: function declarations from the inlined runtime are
|
||||
// already in scope within the IIFE -- no destructure needed.
|
||||
if !bundle_mode {
|
||||
js_emit_line("const {")
|
||||
js_emit_line(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,")
|
||||
js_emit_line(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,")
|
||||
js_emit_line(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,")
|
||||
js_emit_line(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,")
|
||||
js_emit_line(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,")
|
||||
js_emit_line(" list_push, list_join, list_range,")
|
||||
js_emit_line(" el_map_new, el_get_field, el_map_get, el_map_set,")
|
||||
js_emit_line(" http_get, http_post, http_post_json,")
|
||||
js_emit_line(" fs_read, fs_write, fs_list,")
|
||||
js_emit_line(" json_parse, json_stringify, json_get, json_get_string, json_get_int,")
|
||||
js_emit_line(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,")
|
||||
js_emit_line(" el_retain, el_release,")
|
||||
js_emit_line(" append, len, get, map_get, map_set,")
|
||||
js_emit_line(" native_list_get, native_list_len, native_list_append, native_list_empty,")
|
||||
js_emit_line(" native_list_clone, native_string_chars, native_int_to_str,")
|
||||
js_emit_line(" args, state_set, state_get, state_del, state_keys, env,")
|
||||
js_emit_line(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,")
|
||||
js_emit_line(" engram_node, engram_search, engram_activate,")
|
||||
js_emit_line(" llm_call, llm_call_system,")
|
||||
js_emit_line(" dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,")
|
||||
js_emit_line(" dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,")
|
||||
js_emit_line(" dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,")
|
||||
js_emit_line(" dom_append, dom_remove, dom_is_null,")
|
||||
js_emit_line(" dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,")
|
||||
js_emit_line(" dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,")
|
||||
js_emit_line(" set_timeout, set_interval, clear_interval,")
|
||||
js_emit_line(" local_storage_get, local_storage_set, local_storage_remove,")
|
||||
js_emit_line(" window_location, window_redirect, window_on_load,")
|
||||
js_emit_line(" console_log,")
|
||||
js_emit_line(" window_set, window_get, native_js, native_js_call,")
|
||||
js_emit_line(" promise_then, promise_catch, promise_resolve, promise_reject,")
|
||||
js_emit_line(" object_assign, object_keys, object_values, json_deep_clone,")
|
||||
js_emit_line(" array_from, type_of, instanceof_check,")
|
||||
js_emit_line("} = globalThis.__el;")
|
||||
js_emit_blank()
|
||||
}
|
||||
|
||||
// URL import pass: emit `import "url"` (module mode) or a comment
|
||||
// (bundle mode) for any import whose path starts with http(s):// or
|
||||
// doesn't end in .el (i.e., it's a JS/CSS/CDN import, not an El source
|
||||
// import which was already inlined by resolve_imports).
|
||||
let n: Int = native_list_len(stmts)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk: String = stmt["stmt"]
|
||||
if str_eq(sk, "Import") {
|
||||
let ipath: String = stmt["path"]
|
||||
let is_url = str_starts_with(ipath, "http://")
|
||||
let is_url = is_url || str_starts_with(ipath, "https://")
|
||||
let is_js = !str_ends_with(ipath, ".el")
|
||||
if is_url || is_js {
|
||||
if bundle_mode {
|
||||
js_emit_line("// external: " + ipath)
|
||||
} else {
|
||||
js_emit_line("import " + js_str_lit(ipath) + ";")
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
js_emit_blank()
|
||||
|
||||
// Function definitions
|
||||
// Pre-registration pass: scan all FnDefs for @async decorators so that
|
||||
// forward calls to @async functions get `await` even if the callee is
|
||||
// defined after the caller.
|
||||
let n: Int = native_list_len(stmts)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk: String = stmt["stmt"]
|
||||
if str_eq(sk, "FnDef") {
|
||||
let dec: String = stmt["decorator"]
|
||||
if str_eq(dec, "async") {
|
||||
let aname: String = stmt["name"]
|
||||
js_register_async_fn(aname)
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Test mode: emit test functions and runner, skip normal program logic.
|
||||
if test_mode {
|
||||
js_codegen_test(stmts, reporter, file_name)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Function definitions
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
if js_is_fndef(stmt) {
|
||||
@@ -921,6 +1509,41 @@ fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
|
||||
js_emit_line("main();")
|
||||
}
|
||||
|
||||
// Close IIFE in bundle mode.
|
||||
if bundle_mode {
|
||||
js_emit_line("")
|
||||
js_emit_line("})();")
|
||||
}
|
||||
|
||||
// Return empty string — output was streamed via println
|
||||
""
|
||||
}
|
||||
|
||||
// Strip ES module export statements from runtime content for IIFE embedding.
|
||||
// The runtime ends with `export { ... }` and `export { __el as default }` lines
|
||||
// that are invalid inside an IIFE. We strip everything from the first top-level
|
||||
// `export {` line onward.
|
||||
//
|
||||
// Also strips `import` statements at the top if any (though el_runtime.js has none).
|
||||
fn js_strip_es_exports(content: String) -> String {
|
||||
let lines: [String] = str_split(content, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let out: [String] = native_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let trimmed: String = str_trim(line)
|
||||
// Stop at top-level `export {` or `export default`
|
||||
if str_starts_with(trimmed, "export {") {
|
||||
let i = n
|
||||
} else {
|
||||
if str_starts_with(trimmed, "export default") {
|
||||
let i = n
|
||||
} else {
|
||||
let out = native_list_append(out, line)
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
str_join(out, "\n")
|
||||
}
|
||||
|
||||
+402
-2
@@ -175,6 +175,167 @@ fn duration_unit_nanos(unit: String) -> String {
|
||||
"1LL"
|
||||
}
|
||||
|
||||
// ── HTML template codegen ─────────────────────────────────────────────────
|
||||
//
|
||||
// cg_html_template(expr) emits a C statement-expression `({ ... })` that
|
||||
// builds the HTML string by chaining el_str_concat calls.
|
||||
//
|
||||
// Interpolated values are passed through html_escape(); the raw() form
|
||||
// bypasses escaping. {#each} blocks compile to C for-loops that index into
|
||||
// the list with el_list_get / el_list_len.
|
||||
//
|
||||
// A per-template accumulator variable `_html_N` holds the growing string.
|
||||
// A global counter stored in state keeps names unique.
|
||||
|
||||
fn next_html_id() -> String {
|
||||
let csv: String = state_get("__html_counter")
|
||||
let n = 0
|
||||
if !str_eq(csv, "") {
|
||||
let n = str_to_int(csv)
|
||||
}
|
||||
let n = n + 1
|
||||
state_set("__html_counter", native_int_to_str(n))
|
||||
native_int_to_str(n)
|
||||
}
|
||||
|
||||
// Emit children nodes into a flat list of C fragment strings (parts).
|
||||
// Each part is either a static string fragment (already C-literal form) or
|
||||
// a dynamic expression that produces an el_val_t string.
|
||||
// We build them all into parts, then the caller wraps with concat chain.
|
||||
|
||||
fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
|
||||
let n: Int = native_list_len(children)
|
||||
let i = 0
|
||||
let out = ""
|
||||
while i < n {
|
||||
let child: Map<String, Any> = native_list_get(children, i)
|
||||
let html_kind: String = child["html"]
|
||||
if str_eq(html_kind, "Text") {
|
||||
let text: String = child["text"]
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(text) + ")); "
|
||||
}
|
||||
if str_eq(html_kind, "Doctype") {
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<!doctype html>\")); "
|
||||
}
|
||||
if str_eq(html_kind, "Interp") {
|
||||
let val_node = child["value"]
|
||||
let val_c: String = cg_expr(val_node)
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); "
|
||||
}
|
||||
if str_eq(html_kind, "Raw") {
|
||||
let val_node = child["value"]
|
||||
let val_c: String = cg_expr(val_node)
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_raw(" + val_c + ")); "
|
||||
}
|
||||
if str_eq(html_kind, "Element") {
|
||||
let elem_c: String = cg_html_element_str(child, acc_var)
|
||||
let out = out + elem_c
|
||||
}
|
||||
if str_eq(html_kind, "Each") {
|
||||
let each_c: String = cg_html_each(child, acc_var)
|
||||
let out = out + each_c
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// Generate open-tag attribute fragments inline.
|
||||
// Parser stores attrs with "kind": "static" | "dynamic" | "bool".
|
||||
// Static: "value" is the raw string value (not an expr node).
|
||||
// Dynamic: "value" is an expr node.
|
||||
// Bool: no "value" field.
|
||||
fn cg_html_attrs_str(attrs: [Map<String, Any>], acc_var: String) -> String {
|
||||
let n: Int = native_list_len(attrs)
|
||||
let i = 0
|
||||
let out = ""
|
||||
// Closing-quote snippet: EL_STR("\"") in C text.
|
||||
let close_q: String = "EL_STR(" + c_str_lit("\"") + ")"
|
||||
while i < n {
|
||||
let attr: Map<String, Any> = native_list_get(attrs, i)
|
||||
let attr_name: String = attr["name"]
|
||||
let kind: String = attr["kind"]
|
||||
// Build: EL_STR(" name=\"")
|
||||
let open_val: String = " " + attr_name + "=\""
|
||||
let open_attr: String = "EL_STR(" + c_str_lit(open_val) + ")"
|
||||
if str_eq(kind, "static") {
|
||||
// Static attribute: value is a raw string.
|
||||
let sv: String = attr["value"]
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); "
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(sv) + ")); "
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); "
|
||||
} else {
|
||||
if str_eq(kind, "dynamic") {
|
||||
// Dynamic attribute: value is an expr node — html_escape it.
|
||||
let val_node = attr["value"]
|
||||
let val_c: String = cg_expr(val_node)
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); "
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); "
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); "
|
||||
} else {
|
||||
// Boolean attribute (no value): emit " name"
|
||||
let bool_attr: String = "EL_STR(" + c_str_lit(" " + attr_name) + ")"
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + bool_attr + "); "
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// Generate code for a single element, appending into acc_var.
|
||||
fn cg_html_element_str(elem: Map<String, Any>, acc_var: String) -> String {
|
||||
let tag: String = elem["tag"]
|
||||
let attrs: [Map<String, Any>] = elem["attrs"]
|
||||
let children: [Map<String, Any>] = elem["children"]
|
||||
let self_closing: Bool = elem["self_closing"]
|
||||
// Open tag: <tagname
|
||||
let out = acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<" + tag + "\")); "
|
||||
let out = out + cg_html_attrs_str(attrs, acc_var)
|
||||
if self_closing {
|
||||
// Self-closing void element: />
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"/>\")); "
|
||||
} else {
|
||||
// Close open tag: >
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\">\")); "
|
||||
let out = out + cg_html_parts(children, acc_var)
|
||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"</" + tag + ">\")); "
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// Generate code for {#each list as item} ... {/each}.
|
||||
fn cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
|
||||
let list_expr = node["list"]
|
||||
let item_name: String = node["item"]
|
||||
let body_children: [Map<String, Any>] = node["body"]
|
||||
let id: String = next_html_id()
|
||||
let list_var: String = "_html_list_" + id
|
||||
let len_var: String = "_html_len_" + id
|
||||
let idx_var: String = "_html_i_" + id
|
||||
let list_c: String = cg_expr(list_expr)
|
||||
let inner_c: String = cg_html_parts(body_children, acc_var)
|
||||
// Emit: { el_val_t _list = expr; int _len = el_list_len(_list);
|
||||
// for (int _i = 0; _i < _len; _i++) {
|
||||
// el_val_t item = el_list_get(_list, _i); inner_c } }
|
||||
"{ el_val_t " + list_var + " = (" + list_c + "); el_val_t " + len_var + " = el_list_len(" + list_var + "); for (el_val_t " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { el_val_t " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
|
||||
}
|
||||
|
||||
// Top-level HTML template codegen — returns a C statement-expression string.
|
||||
fn cg_html_template(expr: Map<String, Any>) -> String {
|
||||
let root = expr["root"]
|
||||
let id: String = next_html_id()
|
||||
let acc: String = "_html_" + id
|
||||
// If the root element has doctype:true the parser tagged it from <!doctype html>
|
||||
let doctype_flag: Bool = root["doctype"]
|
||||
let doctype_prefix: String = ""
|
||||
if doctype_flag {
|
||||
let doctype_prefix = acc + " = el_str_concat(" + acc + ", EL_STR(\"<!doctype html>\")); "
|
||||
}
|
||||
let body: String = cg_html_element_str(root, acc)
|
||||
"({ el_val_t " + acc + " = EL_STR(\"\"); " + doctype_prefix + body + acc + "; })"
|
||||
}
|
||||
|
||||
fn cg_expr(expr: Map<String, Any>) -> String {
|
||||
let kind: String = expr["expr"]
|
||||
|
||||
@@ -787,6 +948,10 @@ fn cg_expr(expr: Map<String, Any>) -> String {
|
||||
return cg_match(expr)
|
||||
}
|
||||
|
||||
if kind == "HtmlTemplate" {
|
||||
return cg_html_template(expr)
|
||||
}
|
||||
|
||||
"EL_NULL"
|
||||
}
|
||||
|
||||
@@ -848,8 +1013,15 @@ fn cg_match(expr: Map<String, Any>) -> String {
|
||||
}
|
||||
let parts = native_list_append(parts, "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
||||
} else {
|
||||
// unknown pattern -> wildcard
|
||||
let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
||||
if str_eq(pkind, "Variant") {
|
||||
// Enum::Variant pattern — match against the variant name
|
||||
// string (El enums compile to plain strings).
|
||||
let variant: String = pat["variant"]
|
||||
let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(variant) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
||||
} else {
|
||||
// unknown pattern -> wildcard
|
||||
let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1136,6 +1308,22 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
if kind == "Import" { return declared }
|
||||
if kind == "ExternFn" { return declared }
|
||||
if kind == "CgiBlock" { return declared }
|
||||
if kind == "ServiceBlock" { return declared }
|
||||
// TestDef: skip in normal (non-test) mode.
|
||||
// In test mode the body is emitted by cg_test_fn, not here.
|
||||
if kind == "TestDef" { return declared }
|
||||
// Assert: no-op in normal mode. In test mode, cg_stmt_assert is used
|
||||
// directly when emitting the per-test function body.
|
||||
if kind == "Assert" { return declared }
|
||||
// TryCatch: browser-only control flow. In the C target, emit a comment
|
||||
// noting that the try body runs unconditionally; error handling is a no-op.
|
||||
// Programs that rely on catching JS exceptions should compile with --target=js.
|
||||
if kind == "TryCatch" {
|
||||
let try_body = stmt["try_body"]
|
||||
emit_line(indent + "/* try (C target: exception handling not supported) */")
|
||||
cg_stmts(try_body, indent, native_list_clone(declared))
|
||||
return declared
|
||||
}
|
||||
declared
|
||||
}
|
||||
|
||||
@@ -2049,6 +2237,16 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "bool_to_str") { return 1 }
|
||||
// Process
|
||||
if str_eq(name, "exit_program") { return 1 }
|
||||
// Process info
|
||||
if str_eq(name, "getpid_now") { return 0 }
|
||||
// stdout redirect (used by elc post-processing)
|
||||
if str_eq(name, "stdout_to_file") { return 1 }
|
||||
if str_eq(name, "stdout_restore") { return 0 }
|
||||
// Subprocess execution
|
||||
if str_eq(name, "exec_command") { return 1 }
|
||||
if str_eq(name, "exec_capture") { return 1 }
|
||||
if str_eq(name, "exec") { return 1 }
|
||||
if str_eq(name, "exec_bg") { return 1 }
|
||||
// CGI / DHARMA
|
||||
if str_eq(name, "dharma_connect") { return 1 }
|
||||
if str_eq(name, "dharma_send") { return 2 }
|
||||
@@ -2368,6 +2566,7 @@ fn is_top_level_decl(stmt: Map<String, Any>) -> Bool {
|
||||
if kind == "Import" { return true }
|
||||
if kind == "CgiBlock" { return true }
|
||||
if kind == "ExternFn" { return true }
|
||||
if kind == "TestDef" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
@@ -2512,9 +2711,195 @@ fn vbd_has_restricted_call(stmts: [Map<String, Any>]) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
// -- Test mode codegen ----------------------------------------------------------
|
||||
//
|
||||
// reporter = "text" → human-readable output to stderr
|
||||
// reporter = "json" → newline-delimited JSON to stdout
|
||||
//
|
||||
// Each test function signature: static int el_test_N(void)
|
||||
// Returns 0 = pass, non-zero = fail.
|
||||
//
|
||||
// Text assert: if (!expr) { fprintf(stderr, " FAIL <name> — <msg>\n"); return 1; }
|
||||
// JSON assert: stores the assert_line for the runner's suite_end emission.
|
||||
// Emits {"type":"test_fail",...} to stdout then returns 1.
|
||||
|
||||
fn cg_stmt_assert_text(stmt: Map<String, Any>, test_name: String) -> Void {
|
||||
let expr_node = stmt["expr"]
|
||||
let msg: String = stmt["msg"]
|
||||
let expr_c: String = cg_expr(expr_node)
|
||||
let expr_c = strip_outer_parens(expr_c)
|
||||
let disp_msg = "assert failed"
|
||||
if !str_eq(msg, "") { let disp_msg = msg }
|
||||
emit_line(" if (!(" + expr_c + ")) {")
|
||||
emit_line(" fprintf(stderr, \" FAIL " + c_escape(test_name) + " \\xe2\\x80\\x94 " + c_escape(disp_msg) + "\\n\");")
|
||||
emit_line(" return 1;")
|
||||
emit_line(" }")
|
||||
}
|
||||
|
||||
fn cg_stmt_assert_json(stmt: Map<String, Any>, test_name: String, file_name: String, test_line: Int) -> Void {
|
||||
let expr_node = stmt["expr"]
|
||||
let msg: String = stmt["msg"]
|
||||
let assert_line: Int = stmt["line"]
|
||||
let expr_c: String = cg_expr(expr_node)
|
||||
let expr_c = strip_outer_parens(expr_c)
|
||||
let disp_msg = "assert failed"
|
||||
if !str_eq(msg, "") { let disp_msg = msg }
|
||||
// Embed all compile-time-known strings as C string literals (no %s runtime formatting).
|
||||
let json_line: String = "{\"type\":\"test_fail\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + ",\"assert_line\":" + native_int_to_str(assert_line) + ",\"message\":\"" + c_escape(disp_msg) + "\"}"
|
||||
emit_line(" if (!(" + expr_c + ")) {")
|
||||
emit_line(" puts(" + c_str_lit(json_line) + ");")
|
||||
emit_line(" return 1;")
|
||||
emit_line(" }")
|
||||
}
|
||||
|
||||
// cg_stmts_in_test: emit test body statements, routing Assert to the
|
||||
// appropriate handler based on reporter mode.
|
||||
fn cg_stmts_in_test(stmts: [Map<String, Any>], indent: String, declared: [String], test_name: String, reporter: String, file_name: String, test_line: Int) -> [String] {
|
||||
let n: Int = native_list_len(stmts)
|
||||
let i = 0
|
||||
let decl = declared
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk: String = stmt["stmt"]
|
||||
if str_eq(sk, "Assert") {
|
||||
if str_eq(reporter, "json") {
|
||||
cg_stmt_assert_json(stmt, test_name, file_name, test_line)
|
||||
} else {
|
||||
cg_stmt_assert_text(stmt, test_name)
|
||||
}
|
||||
} else {
|
||||
let decl = cg_stmt(stmt, indent, decl)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
decl
|
||||
}
|
||||
|
||||
// cg_test_fn: emit a single test function.
|
||||
fn cg_test_fn(test_def: Map<String, Any>, idx: Int, reporter: String, file_name: String) -> String {
|
||||
let fn_name: String = "el_test_" + native_int_to_str(idx)
|
||||
let test_name: String = test_def["name"]
|
||||
let test_line: Int = test_def["line"]
|
||||
let body = test_def["body"]
|
||||
emit_line("static int " + fn_name + "(void) {")
|
||||
cg_stmts_in_test(body, " ", native_list_empty(), test_name, reporter, file_name, test_line)
|
||||
emit_line(" return 0;")
|
||||
emit_line("}")
|
||||
emit_blank()
|
||||
fn_name
|
||||
}
|
||||
|
||||
// codegen_test: emit a complete test binary.
|
||||
// reporter: "text" (stderr human-readable) or "json" (stdout ndjson)
|
||||
// file_name: basename of the source file (used in JSON output)
|
||||
fn codegen_test(stmts: [Map<String, Any>], reporter: String, file_name: String) -> Void {
|
||||
// Collect all TestDef nodes in order.
|
||||
let n: Int = native_list_len(stmts)
|
||||
let test_defs: [Map<String, Any>] = native_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk: String = stmt["stmt"]
|
||||
if str_eq(sk, "TestDef") {
|
||||
let test_defs = native_list_append(test_defs, stmt)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
let n_tests: Int = native_list_len(test_defs)
|
||||
|
||||
// Emit forward declarations for test functions.
|
||||
let ti = 0
|
||||
while ti < n_tests {
|
||||
let fn_name: String = "el_test_" + native_int_to_str(ti)
|
||||
emit_line("static int " + fn_name + "(void);")
|
||||
let ti = ti + 1
|
||||
}
|
||||
emit_blank()
|
||||
|
||||
// Emit all non-test, non-main function definitions.
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
if is_fndef(stmt) {
|
||||
let fn_name: String = stmt["name"]
|
||||
if !str_eq(fn_name, "main") {
|
||||
cg_fn(stmt)
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Emit each test function.
|
||||
let ti = 0
|
||||
while ti < n_tests {
|
||||
let test_def = native_list_get(test_defs, ti)
|
||||
cg_test_fn(test_def, ti, reporter, file_name)
|
||||
let ti = ti + 1
|
||||
}
|
||||
|
||||
// Emit the test runner main().
|
||||
let test_word = "tests"
|
||||
if n_tests == 1 { let test_word = "test" }
|
||||
emit_line("int main(void) {")
|
||||
emit_line(" int pass = 0; int fail = 0;")
|
||||
|
||||
if str_eq(reporter, "json") {
|
||||
// JSON reporter: all strings are compile-time constants; use puts.
|
||||
// Only suite_end needs runtime pass/fail counts (printf).
|
||||
let suite_start_json: String = "{\"type\":\"suite_start\",\"file\":\"" + c_escape(file_name) + "\",\"total\":" + native_int_to_str(n_tests) + "}"
|
||||
emit_line(" puts(" + c_str_lit(suite_start_json) + ");")
|
||||
let ti = 0
|
||||
while ti < n_tests {
|
||||
let test_def = native_list_get(test_defs, ti)
|
||||
let test_name: String = test_def["name"]
|
||||
let test_line: Int = test_def["line"]
|
||||
let fn_name: String = "el_test_" + native_int_to_str(ti)
|
||||
let start_json: String = "{\"type\":\"test_start\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + "}"
|
||||
let pass_json: String = "{\"type\":\"test_pass\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + ",\"duration_ms\":0}"
|
||||
emit_line(" puts(" + c_str_lit(start_json) + ");")
|
||||
emit_line(" if (" + fn_name + "() == 0) {")
|
||||
emit_line(" pass++;")
|
||||
emit_line(" puts(" + c_str_lit(pass_json) + ");")
|
||||
emit_line(" } else { fail++; }")
|
||||
let ti = ti + 1
|
||||
}
|
||||
// suite_end needs runtime pass/fail counts
|
||||
emit_line(" printf(\"{\\\"type\\\":\\\"suite_end\\\",\\\"passed\\\":%d,\\\"failed\\\":%d}\\n\", pass, fail);")
|
||||
} else {
|
||||
// Text reporter: human-readable to stderr
|
||||
emit_line(" fprintf(stderr, \"==> running " + native_int_to_str(n_tests) + " " + test_word + "\\n\\n\");")
|
||||
let ti = 0
|
||||
while ti < n_tests {
|
||||
let test_def = native_list_get(test_defs, ti)
|
||||
let test_name: String = test_def["name"]
|
||||
let fn_name: String = "el_test_" + native_int_to_str(ti)
|
||||
emit_line(" fprintf(stderr, \" RUN " + c_escape(test_name) + "\\n\");")
|
||||
emit_line(" if (" + fn_name + "() == 0) { pass++; fprintf(stderr, \" PASS " + c_escape(test_name) + "\\n\"); }")
|
||||
emit_line(" else { fail++; }")
|
||||
let ti = ti + 1
|
||||
}
|
||||
emit_line(" fprintf(stderr, \"\\n%d passed, %d failed\\n\", pass, fail);")
|
||||
}
|
||||
|
||||
emit_line(" return fail > 0 ? 1 : 0;")
|
||||
emit_line("}")
|
||||
emit_blank()
|
||||
}
|
||||
|
||||
// -- Entry point ----------------------------------------------------------------
|
||||
|
||||
fn codegen(stmts: [Map<String, Any>], source: String) -> String {
|
||||
codegen_inner(stmts, source, false, "text", "")
|
||||
}
|
||||
|
||||
// codegen_with_tests: emit a test binary.
|
||||
// reporter: "text" or "json"
|
||||
// file_name: basename of the source file (used in JSON output)
|
||||
fn codegen_with_tests(stmts: [Map<String, Any>], source: String, reporter: String, file_name: String) -> String {
|
||||
codegen_inner(stmts, source, true, reporter, file_name)
|
||||
}
|
||||
|
||||
fn codegen_inner(stmts: [Map<String, Any>], source: String, test_mode: Bool, reporter: String, file_name: String) -> String {
|
||||
// Detect cgi/service blocks: at most one declarative top-level block.
|
||||
// The block determines the program's CAPABILITY KIND:
|
||||
// "cgi" - full self-formation. Calls all primitives.
|
||||
@@ -2572,9 +2957,15 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
|
||||
// Clear temporal-type-violation accumulator from any prior compile.
|
||||
state_set("__time_violations", "")
|
||||
|
||||
// In test mode, delegate to the test-specific path which emits test
|
||||
// functions and a test runner main() instead of the normal program.
|
||||
// Test mode still needs the standard preamble (#includes, forward
|
||||
// decls) so we emit that before branching.
|
||||
//
|
||||
// Preamble
|
||||
emit_line("#include <stdint.h>")
|
||||
emit_line("#include <stdlib.h>")
|
||||
emit_line("#include <stdio.h>")
|
||||
emit_line("#include \"el_runtime.h\"")
|
||||
|
||||
// Cross-module forward declarations: for each imported module, emit
|
||||
@@ -2671,6 +3062,15 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
|
||||
emit_blank()
|
||||
}
|
||||
|
||||
// Test mode: emit test functions and test runner instead of normal program.
|
||||
if test_mode {
|
||||
codegen_test(stmts, reporter, file_name)
|
||||
emit_cap_violations()
|
||||
emit_arity_violations()
|
||||
emit_time_violations()
|
||||
return ""
|
||||
}
|
||||
|
||||
// Detect whether this compilation unit has an entry point.
|
||||
// A unit is a library (no C main emitted) when there is no fn main()
|
||||
// and no top-level executable statements. This supports separate
|
||||
|
||||
+336
-7
@@ -29,7 +29,7 @@ fn compile(source: String) -> String {
|
||||
codegen(stmts, source)
|
||||
}
|
||||
|
||||
// compile_js — full pipeline (JS target): source string -> JS source string
|
||||
// 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 stmts: [Map<String, Any>] = parse(tokens)
|
||||
@@ -38,6 +38,38 @@ fn compile_js(source: String) -> String {
|
||||
codegen_js(stmts, source)
|
||||
}
|
||||
|
||||
// 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 stmts: [Map<String, Any>] = parse(tokens)
|
||||
el_release(tokens)
|
||||
let runtime_content: String = fs_read(runtime_path)
|
||||
if str_eq(runtime_content, "") {
|
||||
println("el-compiler: warning: --bundle: could not read runtime at " + runtime_path)
|
||||
println("el-compiler: warning: bundle output will be incomplete")
|
||||
}
|
||||
codegen_js_bundle(stmts, source, runtime_content)
|
||||
}
|
||||
|
||||
// compile_test — full pipeline (C target, test mode): source -> C test runner.
|
||||
// reporter: "text" or "json"; file_name: basename of the source file.
|
||||
fn compile_test(source: String, reporter: String, file_name: String) -> String {
|
||||
let tokens: [Map<String, Any>] = lex(source)
|
||||
let stmts: [Map<String, Any>] = parse(tokens)
|
||||
el_release(tokens)
|
||||
codegen_with_tests(stmts, source, reporter, file_name)
|
||||
}
|
||||
|
||||
// compile_js_test — full pipeline (JS target, test mode): source -> JS test runner.
|
||||
// reporter: "text" or "json"; file_name: basename of the source file.
|
||||
fn compile_js_test(source: String, reporter: String, file_name: String) -> String {
|
||||
let tokens: [Map<String, Any>] = lex(source)
|
||||
let stmts: [Map<String, Any>] = parse(tokens)
|
||||
el_release(tokens)
|
||||
codegen_js_test(stmts, source, reporter, file_name)
|
||||
}
|
||||
|
||||
// compile_dispatch — pick a backend based on the requested target.
|
||||
// tgt = "c" | "js"
|
||||
// (The parameter is named `tgt` because `target` is a reserved keyword
|
||||
@@ -48,6 +80,19 @@ fn compile_dispatch(tgt: String, source: String) -> String {
|
||||
compile(source)
|
||||
}
|
||||
|
||||
// compile_dispatch_test — pick test-mode backend.
|
||||
// reporter: "text" or "json"; file_name: basename of the source file.
|
||||
fn compile_dispatch_test(tgt: String, source: String, reporter: String, file_name: String) -> String {
|
||||
if str_eq(tgt, "js") { return compile_js_test(source, reporter, file_name) }
|
||||
compile_test(source, reporter, file_name)
|
||||
}
|
||||
|
||||
// compile_dispatch_bundle — like compile_dispatch but bundle mode for JS.
|
||||
fn compile_dispatch_bundle(tgt: String, source: String, runtime_path: String) -> String {
|
||||
if str_eq(tgt, "js") { return compile_js_with_bundle(source, runtime_path) }
|
||||
compile(source)
|
||||
}
|
||||
|
||||
// Detect a `--target=<lang>` flag in argv and return the target.
|
||||
// Returns "c" if none specified or unrecognized.
|
||||
fn detect_target(argv: [String]) -> String {
|
||||
@@ -91,6 +136,168 @@ fn detect_emit_header(argv: [String]) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Detect --bundle flag in argv.
|
||||
fn detect_bundle(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, "--bundle") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Detect --minify flag in argv.
|
||||
fn detect_minify(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, "--minify") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Detect --obfuscate flag in argv.
|
||||
fn detect_obfuscate(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, "--obfuscate") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// Detect --reporter=<value> flag in argv.
|
||||
// Returns "json" if --reporter=json, otherwise "text" (default).
|
||||
fn detect_reporter(argv: [String]) -> String {
|
||||
let n: Int = native_list_len(argv)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let a: String = native_list_get(argv, i)
|
||||
if str_starts_with(a, "--reporter=") {
|
||||
let v: String = str_slice(a, 11, str_len(a))
|
||||
return v
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return "text"
|
||||
}
|
||||
|
||||
// basename_of — extract the filename portion of a path (after last '/').
|
||||
fn basename_of(path: String) -> String {
|
||||
let n: Int = str_len(path)
|
||||
let i: Int = n - 1
|
||||
while i >= 0 {
|
||||
let c: String = str_slice(path, i, i + 1)
|
||||
if str_eq(c, "/") {
|
||||
return str_slice(path, i + 1, n)
|
||||
}
|
||||
let i = i - 1
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// Build a unique temp file path: /tmp/elc-<pid>-<timestamp>.<suffix>
|
||||
fn make_temp_path(suffix: String) -> String {
|
||||
let pid: Int = getpid_now()
|
||||
let ts: Int = time_now()
|
||||
"/tmp/elc-" + native_int_to_str(pid) + "-" + native_int_to_str(ts) + "." + suffix
|
||||
}
|
||||
|
||||
// Reserved globals that terser and javascript-obfuscator must not mangle.
|
||||
// These are referenced from HTML onclick= attributes and other direct window usage.
|
||||
fn js_reserved_names() -> String {
|
||||
"neuronDemoToggle,neuronDemoSend,neuronDemoReset,signInWith,signInWithEmail,signUpWithEmail,sendMagicLink,signOut,resetPassword,sendResetEmail,updatePassword,showSignIn,showSignUp,hideReset,setSort,addFamilyMember,removeFamilyMember,copyForPlatform,entHeadcountChange,NEURON_CFG"
|
||||
}
|
||||
|
||||
// Find a CLI tool by checking node_modules paths first, then falling back to npx.
|
||||
// src_dir is the directory of the source file being compiled.
|
||||
// Returns the command string to invoke the tool, or "" if not found.
|
||||
fn find_node_tool(tool_name: String, src_dir: String) -> String {
|
||||
// 1. Check ./node_modules/.bin/<tool> relative to source file
|
||||
let cand1: String = src_dir + "/node_modules/.bin/" + tool_name
|
||||
let check1: String = str_trim(exec_capture("test -x " + cand1 + " && echo yes 2>/dev/null"))
|
||||
if str_eq(check1, "yes") { return cand1 }
|
||||
// 2. Check ../node_modules/.bin/<tool> (monorepo layout)
|
||||
let parent_dir: String = dirname_of(src_dir)
|
||||
let cand2: String = parent_dir + "/node_modules/.bin/" + tool_name
|
||||
let check2: String = str_trim(exec_capture("test -x " + cand2 + " && echo yes 2>/dev/null"))
|
||||
if str_eq(check2, "yes") { return cand2 }
|
||||
// 3. Fall back to npx if it is on PATH. npx will use the globally cached
|
||||
// package or download on first use. Use --no to avoid auto-install if
|
||||
// the package is not already cached; if that fails, try with --yes.
|
||||
let npx_path: String = str_trim(exec_capture("which npx 2>/dev/null"))
|
||||
if !str_eq(npx_path, "") { return "npx --yes " + tool_name }
|
||||
return ""
|
||||
}
|
||||
|
||||
// apply_minify — run terser on js_path, write result to out_path.
|
||||
// Returns true on success, false on failure.
|
||||
fn apply_minify(js_path: String, out_path: String, src_dir: String) -> Bool {
|
||||
let terser: String = find_node_tool("terser", src_dir)
|
||||
if str_eq(terser, "") {
|
||||
println("el-compiler: error: terser not found. Run 'npm install terser' in your project directory.")
|
||||
return false
|
||||
}
|
||||
let names: String = js_reserved_names()
|
||||
// Single-quote the mangle reserved list so the shell does not glob-expand
|
||||
// the bracket expression. The compress options are safe without quoting.
|
||||
let compress_opts: String = "passes=2,drop_console=false,drop_debugger=true"
|
||||
let mangle_reserved: String = "'reserved=[" + names + "]'"
|
||||
let cmd: String = terser + " " + js_path + " --compress " + compress_opts + " --mangle " + mangle_reserved + " --output " + out_path
|
||||
let ret: Int = exec_command(cmd)
|
||||
if ret == 0 { return true }
|
||||
println("el-compiler: error: terser failed (exit " + native_int_to_str(ret) + ")")
|
||||
return false
|
||||
}
|
||||
|
||||
// apply_obfuscate — run javascript-obfuscator on js_path, write result to out_path.
|
||||
// Returns true on success, false on failure.
|
||||
fn apply_obfuscate(js_path: String, out_path: String, src_dir: String) -> Bool {
|
||||
let obfuscator: String = find_node_tool("javascript-obfuscator", src_dir)
|
||||
if str_eq(obfuscator, "") {
|
||||
println("el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory.")
|
||||
return false
|
||||
}
|
||||
let names: String = js_reserved_names()
|
||||
let cmd: String = obfuscator + " " + js_path + " --output " + out_path + " --compact true --simplify true --string-array true --string-array-encoding base64 --string-array-threshold 0.75 --identifier-names-generator hexadecimal --rename-globals false --self-defending false --reserved-names " + names
|
||||
let ret: Int = exec_command(cmd)
|
||||
if ret == 0 { return true }
|
||||
println("el-compiler: error: javascript-obfuscator failed (exit " + native_int_to_str(ret) + ")")
|
||||
return false
|
||||
}
|
||||
|
||||
// Resolve the runtime path for --bundle mode.
|
||||
// Looks for el_runtime.js next to the source file first;
|
||||
// if not found there, looks next to the elc binary itself.
|
||||
// Returns "" if not found anywhere (caller emits a warning).
|
||||
fn resolve_runtime_path(src_path: String) -> String {
|
||||
let src_dir: String = dirname_of(src_path)
|
||||
let candidate: String = src_dir + "/el_runtime.js"
|
||||
let existing: String = fs_read(candidate)
|
||||
if !str_eq(existing, "") {
|
||||
return candidate
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Reconstruct an El type annotation string from a parsed type node.
|
||||
fn type_node_to_el(t: Map<String, Any>) -> String {
|
||||
let k: String = t["kind"]
|
||||
@@ -249,12 +456,85 @@ fn resolve_imports(src_path: String) -> String {
|
||||
return str_join(prefix_chunks, "") + str_join(body_chunks, "")
|
||||
}
|
||||
|
||||
// run_with_postprocess — codegen + minify + optional obfuscate pipeline.
|
||||
//
|
||||
// Called from main() when --minify or --obfuscate is active. Redirects stdout
|
||||
// to a temp file during codegen so the output can be passed through the
|
||||
// external tools (terser, javascript-obfuscator) before final emission.
|
||||
//
|
||||
// Pipeline: codegen -> terser -> (javascript-obfuscator) -> stdout or file
|
||||
fn run_with_postprocess(tgt: String, source: String, src_path: String, do_bundle: Bool, do_obfuscate: Bool, argc: Int, positional: [String]) -> Void {
|
||||
let src_dir: String = dirname_of(src_path)
|
||||
let tmp_gen: String = make_temp_path("js")
|
||||
let tmp_min: String = make_temp_path("min.js")
|
||||
|
||||
// Redirect stdout to tmp_gen so codegen println output is captured.
|
||||
stdout_to_file(tmp_gen)
|
||||
if do_bundle {
|
||||
let runtime_path: String = resolve_runtime_path(src_path)
|
||||
compile_dispatch_bundle(tgt, source, runtime_path)
|
||||
} else {
|
||||
compile_dispatch(tgt, source)
|
||||
}
|
||||
stdout_restore()
|
||||
|
||||
// Run terser: tmp_gen -> tmp_min
|
||||
let ok_min: Bool = apply_minify(tmp_gen, tmp_min, src_dir)
|
||||
if !ok_min {
|
||||
exec_command("rm -f " + tmp_gen + " " + tmp_min)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Determine final result path (either tmp_min or post-obfuscation file).
|
||||
// Use state to pass the final path out of the optional obfuscation branch.
|
||||
state_set("__elc_final_js", tmp_min)
|
||||
|
||||
if do_obfuscate {
|
||||
let tmp_obf: String = make_temp_path("obf.js")
|
||||
let ok_obf: Bool = apply_obfuscate(tmp_min, tmp_obf, src_dir)
|
||||
if !ok_obf {
|
||||
exec_command("rm -f " + tmp_gen + " " + tmp_min + " " + tmp_obf)
|
||||
exit(1)
|
||||
}
|
||||
state_set("__elc_final_js", tmp_obf)
|
||||
}
|
||||
|
||||
let final_path: String = state_get("__elc_final_js")
|
||||
let final_js: String = fs_read(final_path)
|
||||
|
||||
// Clean up all temp files.
|
||||
exec_command("rm -f " + tmp_gen + " " + tmp_min)
|
||||
if do_obfuscate {
|
||||
exec_command("rm -f " + final_path)
|
||||
}
|
||||
|
||||
if argc >= 2 {
|
||||
let out_path: String = native_list_get(positional, 1)
|
||||
let ok: Bool = fs_write(out_path, final_js)
|
||||
if ok {
|
||||
return
|
||||
} else {
|
||||
println("el-compiler: failed to write output")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
// No output file: print final JS to stdout.
|
||||
print(final_js)
|
||||
}
|
||||
|
||||
// main — CLI entry point.
|
||||
//
|
||||
// elc <source.el> # emit C to stdout
|
||||
// elc --target=js <source.el> # emit JS to stdout
|
||||
// elc --target=c <source.el> <out.c> # write C to file
|
||||
// elc --target=js <source.el> <out.js> # write JS to file
|
||||
// elc <source.el> # emit C to stdout
|
||||
// elc --test <source.el> # emit C test runner to stdout
|
||||
// elc --target=js <source.el> # emit JS (module) to stdout
|
||||
// elc --target=js --test <source.el> # emit JS test runner to stdout
|
||||
// elc --target=js --bundle <source.el> # emit self-contained JS (IIFE) to stdout
|
||||
// elc --target=js --bundle --minify <source.el> # emit minified IIFE to stdout
|
||||
// elc --target=js --bundle --obfuscate <source.el> # emit minified+obfuscated IIFE to stdout
|
||||
// elc --target=c <source.el> <out.c> # write C to file
|
||||
// elc --target=js <source.el> <out.js> # write JS to file
|
||||
// elc --target=js --bundle <source.el> <out.js> # write bundled JS to file
|
||||
// elc --target=js --bundle --minify <source.el> <out.min.js> # write minified JS to file
|
||||
fn main() -> Void {
|
||||
let argv: [String] = args()
|
||||
// Use `tgt` not `target`: `target` is a reserved keyword in the lexer
|
||||
@@ -262,12 +542,30 @@ fn main() -> Void {
|
||||
// because the function-name position has no token-class restriction.
|
||||
let tgt: String = detect_target(argv)
|
||||
let do_emit_header: Bool = detect_emit_header(argv)
|
||||
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)
|
||||
let reporter: String = detect_reporter(argv)
|
||||
// --obfuscate implies --minify: obfuscating unminified code is pointless.
|
||||
if do_obfuscate {
|
||||
let do_minify = true
|
||||
}
|
||||
let positional: [String] = strip_flags(argv)
|
||||
let argc: Int = native_list_len(positional)
|
||||
if argc < 1 {
|
||||
println("el-compiler: usage: elc [--target=c|js] [--emit-header] <source.el> [<output>]")
|
||||
println("el-compiler: usage: elc [--target=c|js] [--test] [--reporter=text|json] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// --minify and --obfuscate require --target=js
|
||||
if do_minify {
|
||||
if !str_eq(tgt, "js") {
|
||||
println("el-compiler: error: --minify and --obfuscate require --target=js")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
let src_path: String = native_list_get(positional, 0)
|
||||
|
||||
// When --emit-header is requested, parse the source file directly
|
||||
@@ -283,7 +581,38 @@ fn main() -> Void {
|
||||
}
|
||||
|
||||
let source: String = resolve_imports(src_path)
|
||||
let out: String = compile_dispatch(tgt, source)
|
||||
let file_name: String = basename_of(src_path)
|
||||
|
||||
// --test mode: emit a test runner binary instead of the normal program.
|
||||
if do_test {
|
||||
let out: String = compile_dispatch_test(tgt, source, reporter, file_name)
|
||||
if argc >= 2 {
|
||||
let out_path: String = native_list_get(positional, 1)
|
||||
let ok: Bool = fs_write(out_path, out)
|
||||
if ok { exit(0) }
|
||||
println("el-compiler: failed to write output")
|
||||
exit(1)
|
||||
}
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// When post-processing (--minify or --obfuscate) is requested, redirect
|
||||
// stdout to a temp file so codegen output can be captured and piped through
|
||||
// the external tools. After codegen, restore stdout before emitting the
|
||||
// final result.
|
||||
if do_minify {
|
||||
run_with_postprocess(tgt, source, src_path, do_bundle, do_obfuscate, argc, positional)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Standard path (no post-processing).
|
||||
let out: String = ""
|
||||
if do_bundle {
|
||||
let runtime_path: String = resolve_runtime_path(src_path)
|
||||
let out = compile_dispatch_bundle(tgt, source, runtime_path)
|
||||
} else {
|
||||
let out = compile_dispatch(tgt, source)
|
||||
}
|
||||
if argc >= 2 {
|
||||
let out_path: String = native_list_get(positional, 1)
|
||||
let ok: Bool = fs_write(out_path, out)
|
||||
|
||||
@@ -98,7 +98,10 @@ fn lex_is_whitespace(ch: String) -> Bool {
|
||||
}
|
||||
|
||||
fn make_tok(kind: String, value: String) -> Map<String, Any> {
|
||||
{ "kind": kind, "value": value }
|
||||
let ln_s: String = state_get("__lex_line")
|
||||
let ln: Int = 1
|
||||
if !str_eq(ln_s, "") { let ln = str_to_int(ln_s) }
|
||||
{ "kind": kind, "value": value, "line": ln }
|
||||
}
|
||||
|
||||
// ── Keyword lookup ────────────────────────────────────────────────────────────
|
||||
@@ -147,6 +150,8 @@ fn keyword_kind(word: String) -> String {
|
||||
if word == "accessor" { return "Accessor" }
|
||||
if word == "vessel" { return "Vessel" }
|
||||
if word == "extern" { return "Extern" }
|
||||
if word == "try" { return "Try" }
|
||||
if word == "catch" { return "Catch" }
|
||||
""
|
||||
}
|
||||
|
||||
@@ -465,12 +470,18 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let total: Int = native_list_len(chars)
|
||||
let tokens: [Map<String, Any>] = native_list_empty()
|
||||
let i: Int = 0
|
||||
let line_num: Int = 1
|
||||
state_set("__lex_line", "1")
|
||||
|
||||
while i < total {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
|
||||
// Skip whitespace
|
||||
// Skip whitespace; track newlines for line-number reporting
|
||||
if lex_is_whitespace(ch) {
|
||||
if ch == "\n" {
|
||||
let line_num = line_num + 1
|
||||
state_set("__lex_line", native_int_to_str(line_num))
|
||||
}
|
||||
let i = i + 1
|
||||
} else {
|
||||
// Line comments: //
|
||||
@@ -711,8 +722,13 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
// unknown char — skip
|
||||
let i = i + 1
|
||||
if ch == "#" {
|
||||
let tokens = native_list_append(tokens, make_tok("Hash", "#"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
// unknown char — skip
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+579
-1
@@ -27,6 +27,15 @@ fn tok_value(tokens: [Map<String, Any>], pos: Int) -> String {
|
||||
t["value"]
|
||||
}
|
||||
|
||||
// tok_line — return the source line number of the token at pos (1-indexed).
|
||||
// Returns 1 if the token has no "line" field or line is 0.
|
||||
fn tok_line(tokens: [Map<String, Any>], pos: Int) -> Int {
|
||||
let t = native_list_get(tokens, pos)
|
||||
let ln: Int = t["line"]
|
||||
if ln <= 0 { return 1 }
|
||||
ln
|
||||
}
|
||||
|
||||
fn expect(tokens: [Map<String, Any>], pos: Int, kind: String) -> Int {
|
||||
let k = tok_kind(tokens, pos)
|
||||
if k == kind {
|
||||
@@ -142,6 +151,460 @@ fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
|
||||
// ── Expression parsing ────────────────────────────────────────────────────────
|
||||
|
||||
// ── HTML template parser ──────────────────────────────────────────────────────
|
||||
//
|
||||
// HTML templates are written as unquoted HTML in expression position:
|
||||
// return <div class="x"><h1>{title}</h1></div>
|
||||
//
|
||||
// The parser detects an HTML template when parse_primary sees Lt followed
|
||||
// by a lowercase ident (a known or assumed HTML element name) or `!` (for
|
||||
// <!doctype html>). It then recursively parses the HTML into an AST.
|
||||
//
|
||||
// AST nodes produced:
|
||||
// { "expr": "HtmlTemplate", "root": child_node }
|
||||
// { "html": "Element", "tag": "div", "attrs": [...], "children": [...], "self_closing": bool }
|
||||
// { "html": "Text", "text": "..." }
|
||||
// { "html": "Interp", "value": expr_node }
|
||||
// { "html": "Each", "list": expr_node, "item": "name", "body": [...] }
|
||||
// { "html": "Doctype" }
|
||||
// { "html": "Raw", "value": expr_node }
|
||||
|
||||
fn is_html_tag_name(name: String) -> Bool {
|
||||
if str_eq(name, "a") { return true }
|
||||
if str_eq(name, "abbr") { return true }
|
||||
if str_eq(name, "address") { return true }
|
||||
if str_eq(name, "area") { return true }
|
||||
if str_eq(name, "article") { return true }
|
||||
if str_eq(name, "aside") { return true }
|
||||
if str_eq(name, "audio") { return true }
|
||||
if str_eq(name, "b") { return true }
|
||||
if str_eq(name, "base") { return true }
|
||||
if str_eq(name, "blockquote") { return true }
|
||||
if str_eq(name, "body") { return true }
|
||||
if str_eq(name, "br") { return true }
|
||||
if str_eq(name, "button") { return true }
|
||||
if str_eq(name, "canvas") { return true }
|
||||
if str_eq(name, "caption") { return true }
|
||||
if str_eq(name, "cite") { return true }
|
||||
if str_eq(name, "code") { return true }
|
||||
if str_eq(name, "col") { return true }
|
||||
if str_eq(name, "colgroup") { return true }
|
||||
if str_eq(name, "data") { return true }
|
||||
if str_eq(name, "datalist") { return true }
|
||||
if str_eq(name, "dd") { return true }
|
||||
if str_eq(name, "del") { return true }
|
||||
if str_eq(name, "details") { return true }
|
||||
if str_eq(name, "dfn") { return true }
|
||||
if str_eq(name, "dialog") { return true }
|
||||
if str_eq(name, "div") { return true }
|
||||
if str_eq(name, "dl") { return true }
|
||||
if str_eq(name, "dt") { return true }
|
||||
if str_eq(name, "em") { return true }
|
||||
if str_eq(name, "embed") { return true }
|
||||
if str_eq(name, "fieldset") { return true }
|
||||
if str_eq(name, "figcaption") { return true }
|
||||
if str_eq(name, "figure") { return true }
|
||||
if str_eq(name, "footer") { return true }
|
||||
if str_eq(name, "form") { return true }
|
||||
if str_eq(name, "h1") { return true }
|
||||
if str_eq(name, "h2") { return true }
|
||||
if str_eq(name, "h3") { return true }
|
||||
if str_eq(name, "h4") { return true }
|
||||
if str_eq(name, "h5") { return true }
|
||||
if str_eq(name, "h6") { return true }
|
||||
if str_eq(name, "head") { return true }
|
||||
if str_eq(name, "header") { return true }
|
||||
if str_eq(name, "hr") { return true }
|
||||
if str_eq(name, "html") { return true }
|
||||
if str_eq(name, "i") { return true }
|
||||
if str_eq(name, "iframe") { return true }
|
||||
if str_eq(name, "img") { return true }
|
||||
if str_eq(name, "input") { return true }
|
||||
if str_eq(name, "ins") { return true }
|
||||
if str_eq(name, "kbd") { return true }
|
||||
if str_eq(name, "label") { return true }
|
||||
if str_eq(name, "legend") { return true }
|
||||
if str_eq(name, "li") { return true }
|
||||
if str_eq(name, "link") { return true }
|
||||
if str_eq(name, "main") { return true }
|
||||
if str_eq(name, "map") { return true }
|
||||
if str_eq(name, "mark") { return true }
|
||||
if str_eq(name, "menu") { return true }
|
||||
if str_eq(name, "meta") { return true }
|
||||
if str_eq(name, "meter") { return true }
|
||||
if str_eq(name, "nav") { return true }
|
||||
if str_eq(name, "noscript") { return true }
|
||||
if str_eq(name, "object") { return true }
|
||||
if str_eq(name, "ol") { return true }
|
||||
if str_eq(name, "optgroup") { return true }
|
||||
if str_eq(name, "option") { return true }
|
||||
if str_eq(name, "output") { return true }
|
||||
if str_eq(name, "p") { return true }
|
||||
if str_eq(name, "param") { return true }
|
||||
if str_eq(name, "picture") { return true }
|
||||
if str_eq(name, "pre") { return true }
|
||||
if str_eq(name, "progress") { return true }
|
||||
if str_eq(name, "q") { return true }
|
||||
if str_eq(name, "rp") { return true }
|
||||
if str_eq(name, "rt") { return true }
|
||||
if str_eq(name, "ruby") { return true }
|
||||
if str_eq(name, "s") { return true }
|
||||
if str_eq(name, "samp") { return true }
|
||||
if str_eq(name, "script") { return true }
|
||||
if str_eq(name, "section") { return true }
|
||||
if str_eq(name, "select") { return true }
|
||||
if str_eq(name, "small") { return true }
|
||||
if str_eq(name, "source") { return true }
|
||||
if str_eq(name, "span") { return true }
|
||||
if str_eq(name, "strong") { return true }
|
||||
if str_eq(name, "style") { return true }
|
||||
if str_eq(name, "sub") { return true }
|
||||
if str_eq(name, "summary") { return true }
|
||||
if str_eq(name, "sup") { return true }
|
||||
if str_eq(name, "table") { return true }
|
||||
if str_eq(name, "tbody") { return true }
|
||||
if str_eq(name, "td") { return true }
|
||||
if str_eq(name, "template") { return true }
|
||||
if str_eq(name, "textarea") { return true }
|
||||
if str_eq(name, "tfoot") { return true }
|
||||
if str_eq(name, "th") { return true }
|
||||
if str_eq(name, "thead") { return true }
|
||||
if str_eq(name, "time") { return true }
|
||||
if str_eq(name, "title") { return true }
|
||||
if str_eq(name, "tr") { return true }
|
||||
if str_eq(name, "track") { return true }
|
||||
if str_eq(name, "u") { return true }
|
||||
if str_eq(name, "ul") { return true }
|
||||
if str_eq(name, "var") { return true }
|
||||
if str_eq(name, "video") { return true }
|
||||
if str_eq(name, "wbr") { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn is_void_element(name: String) -> Bool {
|
||||
if str_eq(name, "area") { return true }
|
||||
if str_eq(name, "base") { return true }
|
||||
if str_eq(name, "br") { return true }
|
||||
if str_eq(name, "col") { return true }
|
||||
if str_eq(name, "embed") { return true }
|
||||
if str_eq(name, "hr") { return true }
|
||||
if str_eq(name, "img") { return true }
|
||||
if str_eq(name, "input") { return true }
|
||||
if str_eq(name, "link") { return true }
|
||||
if str_eq(name, "meta") { return true }
|
||||
if str_eq(name, "param") { return true }
|
||||
if str_eq(name, "source") { return true }
|
||||
if str_eq(name, "track") { return true }
|
||||
if str_eq(name, "wbr") { return true }
|
||||
false
|
||||
}
|
||||
|
||||
// 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> {
|
||||
let parts: [String] = native_list_empty()
|
||||
let p = pos
|
||||
let running = true
|
||||
while running {
|
||||
let k = tok_kind(tokens, p)
|
||||
if str_eq(k, "Eof") {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(k, "Lt") {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(k, "LBrace") {
|
||||
let running = false
|
||||
} else {
|
||||
// Check for </: Lt already stops us, but Slash alone
|
||||
// (after consuming whitespace) also stops text.
|
||||
// Anything else is text content.
|
||||
let v = tok_value(tokens, p)
|
||||
let parts = native_list_append(parts, v)
|
||||
let p = p + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, " "), "pos": p }
|
||||
}
|
||||
|
||||
// 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> {
|
||||
let attrs: [Map<String, Any>] = native_list_empty()
|
||||
let p = pos
|
||||
let running = true
|
||||
while running {
|
||||
let k = tok_kind(tokens, p)
|
||||
if str_eq(k, "Gt") {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(k, "Slash") {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(k, "Eof") {
|
||||
let running = false
|
||||
} else {
|
||||
// Attribute name: could be Ident or keyword used as attr name
|
||||
let attr_name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let k2 = tok_kind(tokens, p)
|
||||
if str_eq(k2, "Eq") {
|
||||
let p = p + 1
|
||||
let k3 = tok_kind(tokens, p)
|
||||
if str_eq(k3, "Str") {
|
||||
// static: attr="value"
|
||||
let attr_val = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let attrs = native_list_append(attrs, { "name": attr_name, "kind": "static", "value": attr_val })
|
||||
} else {
|
||||
if str_eq(k3, "LBrace") {
|
||||
// dynamic: attr={expr}
|
||||
let r = parse_expr(tokens, p + 1)
|
||||
let val_node = r["node"]
|
||||
let p = r["pos"]
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
let attrs = native_list_append(attrs, { "name": attr_name, "kind": "dynamic", "value": val_node })
|
||||
} else {
|
||||
// malformed, skip
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// boolean attribute
|
||||
let attrs = native_list_append(attrs, { "name": attr_name, "kind": "bool" })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "attrs": attrs, "pos": p }
|
||||
}
|
||||
|
||||
// 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> {
|
||||
let children: [Map<String, Any>] = native_list_empty()
|
||||
let p = pos
|
||||
let running = true
|
||||
while running {
|
||||
let k = tok_kind(tokens, p)
|
||||
if str_eq(k, "Eof") {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(k, "Lt") {
|
||||
// Check for closing tag: </
|
||||
let k2 = tok_kind(tokens, p + 1)
|
||||
if str_eq(k2, "Slash") {
|
||||
// </tagname> — consume and stop
|
||||
let p = p + 2
|
||||
// skip tag name
|
||||
let close_name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
// consume >
|
||||
let p = expect(tokens, p, "Gt")
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(k2, "Not") {
|
||||
// Possible <!doctype html>
|
||||
let k3_v = tok_value(tokens, p + 2)
|
||||
if str_eq(k3_v, "doctype") {
|
||||
// consume <!doctype html>
|
||||
let p = p + 2
|
||||
// skip until >
|
||||
let scanning = true
|
||||
while scanning {
|
||||
let ck = tok_kind(tokens, p)
|
||||
if str_eq(ck, "Eof") { let scanning = false }
|
||||
if str_eq(ck, "Gt") {
|
||||
let p = p + 1
|
||||
let scanning = false
|
||||
} else {
|
||||
let p = p + 1
|
||||
}
|
||||
}
|
||||
let children = native_list_append(children, { "html": "Doctype" })
|
||||
} else {
|
||||
let p = p + 1
|
||||
}
|
||||
} else {
|
||||
// nested element
|
||||
let r = parse_html_element(tokens, p)
|
||||
let child = r["node"]
|
||||
let p = r["pos"]
|
||||
let children = native_list_append(children, child)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if str_eq(k, "LBrace") {
|
||||
// Interpolation: {expr} or {#each ...} or {/each}
|
||||
let k2 = tok_kind(tokens, p + 1)
|
||||
if str_eq(k2, "Hash") {
|
||||
// {#each list as item}
|
||||
let k3_v = tok_value(tokens, p + 2)
|
||||
if str_eq(k3_v, "each") {
|
||||
let p = p + 3
|
||||
// parse list expr up to "as" keyword
|
||||
let prev_no_block: String = state_get("__no_block_expr")
|
||||
state_set("__no_block_expr", "1")
|
||||
let r_list = parse_expr(tokens, p)
|
||||
state_set("__no_block_expr", prev_no_block)
|
||||
let list_expr = r_list["node"]
|
||||
let p = r_list["pos"]
|
||||
// expect "as"
|
||||
let p = expect(tokens, p, "As")
|
||||
// item variable name
|
||||
let item_name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
// consume closing }
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
// parse body until {/each}
|
||||
let r_body = parse_html_each_body(tokens, p)
|
||||
let body_children = r_body["children"]
|
||||
let p = r_body["pos"]
|
||||
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 {
|
||||
let p = p + 1
|
||||
}
|
||||
} else {
|
||||
if str_eq(k2, "Slash") {
|
||||
// {/each} — end of each block, stop
|
||||
// skip {/each}
|
||||
let p = p + 2
|
||||
// skip "each"
|
||||
let p = p + 1
|
||||
// skip }
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
let running = false
|
||||
} else {
|
||||
// regular {expr}
|
||||
let r = parse_expr(tokens, p + 1)
|
||||
let interp_val = r["node"]
|
||||
let p = r["pos"]
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
// Check if the expr is a call to raw()
|
||||
let is_raw_call = false
|
||||
let interp_kind: String = interp_val["expr"]
|
||||
if str_eq(interp_kind, "Call") {
|
||||
let fn_node = interp_val["func"]
|
||||
let fn_kind: String = fn_node["expr"]
|
||||
if str_eq(fn_kind, "Ident") {
|
||||
let fn_name_v: String = fn_node["name"]
|
||||
if str_eq(fn_name_v, "raw") {
|
||||
let is_raw_call = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if is_raw_call {
|
||||
let raw_args = interp_val["args"]
|
||||
let raw_inner = native_list_get(raw_args, 0)
|
||||
let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
|
||||
} else {
|
||||
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Text tokens — collect run of non-special tokens
|
||||
let r_text = parse_html_text_tokens(tokens, p)
|
||||
let text_str: String = r_text["text"]
|
||||
let p = r_text["pos"]
|
||||
let text_trimmed: String = str_trim(text_str)
|
||||
if !str_eq(text_trimmed, "") {
|
||||
let children = native_list_append(children, { "html": "Text", "text": text_trimmed })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "children": children, "pos": p }
|
||||
}
|
||||
|
||||
// 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> {
|
||||
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> {
|
||||
let p = pos
|
||||
// consume <
|
||||
let p = expect(tokens, p, "Lt")
|
||||
// tag name
|
||||
let tag_name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
// parse attributes
|
||||
let r_attrs = parse_html_attrs(tokens, p)
|
||||
let attrs = r_attrs["attrs"]
|
||||
let p = r_attrs["pos"]
|
||||
// check for self-closing /> or void element
|
||||
let k = tok_kind(tokens, p)
|
||||
let self_closing = false
|
||||
if str_eq(k, "Slash") {
|
||||
// />
|
||||
let p = p + 1
|
||||
let p = expect(tokens, p, "Gt")
|
||||
let self_closing = true
|
||||
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
|
||||
}
|
||||
// consume >
|
||||
let p = expect(tokens, p, "Gt")
|
||||
// void elements have no children, no closing tag
|
||||
if is_void_element(tag_name) {
|
||||
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
|
||||
}
|
||||
// parse children
|
||||
let r_children = parse_html_children(tokens, p, tag_name)
|
||||
let children = r_children["children"]
|
||||
let p = r_children["pos"]
|
||||
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> {
|
||||
let p = pos
|
||||
// Check for <!doctype html>
|
||||
let doctype = false
|
||||
let k = tok_kind(tokens, p)
|
||||
let k2 = tok_kind(tokens, p + 1)
|
||||
if str_eq(k, "Lt") {
|
||||
if str_eq(k2, "Not") {
|
||||
let k3_v = tok_value(tokens, p + 2)
|
||||
if str_eq(k3_v, "doctype") {
|
||||
let doctype = true
|
||||
// consume <!doctype html>
|
||||
let p = p + 2
|
||||
let scanning = true
|
||||
while scanning {
|
||||
let ck = tok_kind(tokens, p)
|
||||
if str_eq(ck, "Eof") { let scanning = false }
|
||||
if str_eq(ck, "Gt") {
|
||||
let p = p + 1
|
||||
let scanning = false
|
||||
} else {
|
||||
let p = p + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Parse root element
|
||||
let r = parse_html_element(tokens, p)
|
||||
let root = r["node"]
|
||||
let p = r["pos"]
|
||||
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 }
|
||||
}
|
||||
make_result({ "expr": "HtmlTemplate", "root": root_with_doctype }, p)
|
||||
}
|
||||
|
||||
fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let k = tok_kind(tokens, pos)
|
||||
let v = tok_value(tokens, pos)
|
||||
@@ -166,6 +629,22 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
return make_result({ "expr": "Bool", "value": v }, pos + 1)
|
||||
}
|
||||
|
||||
// HTML template literal: <tagname ...>...</tagname> or <!doctype html>...
|
||||
// Detected in value position only; `<` in comparison position is already
|
||||
// consumed by parse_binop before parse_primary is reached.
|
||||
if k == "Lt" {
|
||||
let k2 = tok_kind(tokens, pos + 1)
|
||||
if str_eq(k2, "Not") {
|
||||
return parse_html_template(tokens, pos)
|
||||
}
|
||||
if str_eq(k2, "Ident") {
|
||||
let tag_candidate = tok_value(tokens, pos + 1)
|
||||
if is_html_tag_name(tag_candidate) {
|
||||
return parse_html_template(tokens, pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Identifier
|
||||
if k == "Ident" {
|
||||
return make_result({ "expr": "Ident", "name": v }, pos + 1)
|
||||
@@ -279,6 +758,30 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
return r
|
||||
}
|
||||
|
||||
// Anonymous function literal (lambda): fn(params) -> RetType { body }
|
||||
// Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... })
|
||||
// Produces a Lambda expression node (distinct from a named FnDef statement).
|
||||
if k == "Fn" {
|
||||
let p = pos + 1
|
||||
let r = parse_params(tokens, p)
|
||||
let params = r["params"]
|
||||
let p = r["pos"]
|
||||
let ret_type = ""
|
||||
let k2 = tok_kind(tokens, p)
|
||||
if k2 == "Arrow" {
|
||||
let p = p + 1
|
||||
let kt = tok_kind(tokens, p)
|
||||
if kt == "Ident" {
|
||||
let ret_type = tok_value(tokens, p)
|
||||
}
|
||||
let p = skip_type(tokens, p)
|
||||
}
|
||||
let r2 = parse_block(tokens, p)
|
||||
let body = r2["stmts"]
|
||||
let p = r2["pos"]
|
||||
return make_result({ "expr": "Lambda", "params": params, "body": body, "ret_type": ret_type }, p)
|
||||
}
|
||||
|
||||
// Unary not
|
||||
if k == "Not" {
|
||||
let r = parse_primary(tokens, pos + 1)
|
||||
@@ -408,6 +911,13 @@ fn parse_pattern(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
if v == "_" {
|
||||
return make_result({ "pattern": "Wildcard" }, pos + 1)
|
||||
}
|
||||
// Check for Enum::Variant pattern (Color::Red, Status::Ok, etc.)
|
||||
// Lexed as: Ident ColonColon Ident
|
||||
let next_k = tok_kind(tokens, pos + 1)
|
||||
if next_k == "ColonColon" {
|
||||
let variant_name = tok_value(tokens, pos + 2)
|
||||
return make_result({ "pattern": "Variant", "enum_name": v, "variant": variant_name }, pos + 3)
|
||||
}
|
||||
return make_result({ "pattern": "Binding", "name": v }, pos + 1)
|
||||
}
|
||||
if k == "Int" {
|
||||
@@ -737,11 +1247,16 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, p)
|
||||
}
|
||||
|
||||
// type definition
|
||||
// type definition: `type Name = { field: Type, ... }`
|
||||
// The `=` between the name and the brace is optional in the spec but
|
||||
// present in practice. Skip it if present before consuming the LBrace.
|
||||
if k == "Type" {
|
||||
let p = pos + 1
|
||||
let name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
// Consume optional `=` before the opening brace
|
||||
let pk = tok_kind(tokens, p)
|
||||
if pk == "Eq" { let p = p + 1 }
|
||||
let p = expect(tokens, p, "LBrace")
|
||||
let fields: [Map<String, Any>] = native_list_empty()
|
||||
let running = true
|
||||
@@ -876,6 +1391,40 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p)
|
||||
}
|
||||
|
||||
// try/catch statement
|
||||
// try { body } catch (name: Type) { handler }
|
||||
// The catch variable name and type are both captured; type is skipped.
|
||||
if k == "Try" {
|
||||
let p = pos + 1
|
||||
let r_try = parse_block(tokens, p)
|
||||
let try_body = r_try["stmts"]
|
||||
let p = r_try["pos"]
|
||||
let catch_name = "err"
|
||||
let k2 = tok_kind(tokens, p)
|
||||
if str_eq(k2, "Catch") {
|
||||
let p = p + 1
|
||||
let p = expect(tokens, p, "LParen")
|
||||
// catch variable name
|
||||
let kn = tok_kind(tokens, p)
|
||||
if str_eq(kn, "Ident") {
|
||||
let catch_name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
}
|
||||
// optional type annotation: : Type
|
||||
let k3 = tok_kind(tokens, p)
|
||||
if str_eq(k3, "Colon") {
|
||||
let p = p + 1
|
||||
let p = skip_type(tokens, p)
|
||||
}
|
||||
let p = expect(tokens, p, "RParen")
|
||||
let r_catch = parse_block(tokens, p)
|
||||
let catch_body = r_catch["stmts"]
|
||||
let p = r_catch["pos"]
|
||||
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)
|
||||
}
|
||||
|
||||
// @decorator — capture decorator name and attach to following stmt
|
||||
if k == "At" {
|
||||
let p = pos + 1
|
||||
@@ -1014,6 +1563,35 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
}, p)
|
||||
}
|
||||
|
||||
// assert statement: assert <expr> or assert <expr>, "msg"
|
||||
if k == "Assert" {
|
||||
let assert_line: Int = tok_line(tokens, pos)
|
||||
let p = pos + 1
|
||||
let r = parse_expr(tokens, p)
|
||||
let expr_node = r["node"]
|
||||
let p = r["pos"]
|
||||
let msg = ""
|
||||
let k2 = tok_kind(tokens, p)
|
||||
if k2 == "Comma" {
|
||||
let p = p + 1
|
||||
let msg = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
}
|
||||
return make_result({ "stmt": "Assert", "expr": expr_node, "msg": msg, "line": assert_line }, p)
|
||||
}
|
||||
|
||||
// test block: test "name" { stmts }
|
||||
if k == "Test" {
|
||||
let test_line: Int = tok_line(tokens, pos)
|
||||
let p = pos + 1
|
||||
let name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let r2 = parse_block(tokens, p)
|
||||
let body = r2["stmts"]
|
||||
let p = r2["pos"]
|
||||
return make_result({ "stmt": "TestDef", "name": name, "body": body, "line": test_line }, 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`
|
||||
|
||||
+2489
-286
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,200 @@
|
||||
// browser-auth.el -- El-compiled auth flow using Supabase
|
||||
//
|
||||
// Compile: elc --target=js --bundle examples/browser-auth.el > auth.js
|
||||
// (requires el_runtime.js in the same directory as browser-auth.el)
|
||||
//
|
||||
// Demonstrates:
|
||||
// - extern fn for declaring Supabase client constructor
|
||||
// - anonymous function literals for callbacks
|
||||
// - method call syntax on Any-typed values (client.auth.signInWithOtp)
|
||||
// - try/catch for error handling
|
||||
// - @async functions with DOM interaction
|
||||
// - DOM bridge: dom_get_element, dom_get_value, dom_set_text, dom_add_class
|
||||
// dom_remove_class, dom_show, dom_hide, dom_is_null
|
||||
// - window_set to expose El functions to the browser global scope
|
||||
// - local_storage_set/get for session hints
|
||||
// - set_timeout for transient UI state
|
||||
// - state_set/get for component state
|
||||
//
|
||||
// Expected HTML elements:
|
||||
// #acct-email-input -- email text input
|
||||
// #send-link-btn -- submit button
|
||||
// #auth-message -- status message container
|
||||
// #auth-form -- the form to hide after success
|
||||
//
|
||||
// The Supabase JS SDK is loaded from CDN via a <script> tag before auth.js.
|
||||
// supabase_create_client is declared extern: the runtime provides it via
|
||||
// the global supabase.createClient function exposed by the CDN bundle.
|
||||
|
||||
// ── External declarations ─────────────────────────────────────────────────
|
||||
//
|
||||
// These functions are provided by the JS environment (CDN script tags).
|
||||
// No body is emitted -- the compiler just records the names.
|
||||
|
||||
extern fn supabase_create_client(url: String, key: String) -> Any
|
||||
|
||||
// ── UI helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn show_message(text: String, is_error: Bool) -> Void {
|
||||
let msg_el = dom_get_element("auth-message")
|
||||
if !dom_is_null(msg_el) {
|
||||
dom_set_text(msg_el, text)
|
||||
dom_remove_class(msg_el, "hidden")
|
||||
if is_error {
|
||||
dom_add_class(msg_el, "error")
|
||||
dom_remove_class(msg_el, "success")
|
||||
} else {
|
||||
dom_add_class(msg_el, "success")
|
||||
dom_remove_class(msg_el, "error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_button_loading(loading: Bool) -> Void {
|
||||
let btn = dom_get_element("send-link-btn")
|
||||
if !dom_is_null(btn) {
|
||||
if loading {
|
||||
dom_set_text(btn, "Sending...")
|
||||
dom_set_attr(btn, "disabled", "true")
|
||||
} else {
|
||||
dom_set_text(btn, "Send Magic Link")
|
||||
dom_remove_attr(btn, "disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_message() -> Void {
|
||||
let msg_el = dom_get_element("auth-message")
|
||||
if !dom_is_null(msg_el) {
|
||||
dom_add_class(msg_el, "hidden")
|
||||
dom_set_text(msg_el, "")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Email validation ───────────────────────────────────────────────────────
|
||||
|
||||
fn is_valid_email(email: String) -> Bool {
|
||||
let trimmed: String = str_trim(email)
|
||||
if str_len(trimmed) < 5 { return false }
|
||||
let at_pos: Int = str_index_of(trimmed, "@")
|
||||
if at_pos < 1 { return false }
|
||||
let dot_pos: Int = str_index_of(trimmed, ".")
|
||||
if dot_pos < at_pos + 2 { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Supabase client construction ──────────────────────────────────────────
|
||||
//
|
||||
// Build a Supabase client from config injected into the page as NEURON_CFG.
|
||||
// The extern fn supabase_create_client maps to supabase.createClient on
|
||||
// the global object exposed by the CDN bundle.
|
||||
|
||||
fn get_supabase_client() -> Any {
|
||||
let cfg = window_get("NEURON_CFG")
|
||||
if dom_is_null(cfg) {
|
||||
return null
|
||||
}
|
||||
let url: String = cfg["supabaseUrl"]
|
||||
let key: String = cfg["supabaseAnonKey"]
|
||||
supabase_create_client(url, key)
|
||||
}
|
||||
|
||||
// ── Auth flow ──────────────────────────────────────────────────────────────
|
||||
|
||||
@async
|
||||
fn send_magic_link() -> Void {
|
||||
let email_el = dom_get_element("acct-email-input")
|
||||
if dom_is_null(email_el) {
|
||||
show_message("Could not find email input", true)
|
||||
return null
|
||||
}
|
||||
|
||||
let email: String = str_trim(dom_get_value(email_el))
|
||||
|
||||
if !is_valid_email(email) {
|
||||
show_message("Please enter a valid email address", true)
|
||||
return null
|
||||
}
|
||||
|
||||
clear_message()
|
||||
set_button_loading(true)
|
||||
state_set("auth_email", email)
|
||||
|
||||
// Build the Supabase client and call auth.signInWithOtp directly.
|
||||
// Method call syntax on Any-typed values: client.auth.signInWithOtp(opts)
|
||||
// No native_js_call required.
|
||||
let client = get_supabase_client()
|
||||
if dom_is_null(client) {
|
||||
show_message("Auth service not configured", true)
|
||||
set_button_loading(false)
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
let opts: Map<String, Any> = { "email": email }
|
||||
// client is Any-typed; .auth returns the auth sub-client (also Any).
|
||||
// .signInWithOtp(opts) returns a Promise. @async + await handles it.
|
||||
let resp = client.auth.signInWithOtp(opts)
|
||||
let err = resp["error"]
|
||||
if !dom_is_null(err) {
|
||||
let msg: String = err["message"]
|
||||
show_message("Error: " + msg, true)
|
||||
} else {
|
||||
local_storage_set("auth_pending_email", email)
|
||||
show_message("Magic link sent! Check your inbox for " + email, false)
|
||||
let form = dom_get_element("auth-form")
|
||||
if !dom_is_null(form) {
|
||||
dom_hide(form)
|
||||
}
|
||||
}
|
||||
} catch (err: Any) {
|
||||
show_message("Unexpected error. Please try again.", true)
|
||||
}
|
||||
|
||||
set_button_loading(false)
|
||||
}
|
||||
|
||||
// ── Keyboard support ───────────────────────────────────────────────────────
|
||||
|
||||
fn handle_email_keydown(event: Any) -> Void {
|
||||
let key: String = dom_get_prop(event, "key")
|
||||
if str_eq(key, "Enter") {
|
||||
send_magic_link()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Initialization ─────────────────────────────────────────────────────────
|
||||
|
||||
fn init_auth() -> Void {
|
||||
let email_el = dom_get_element("acct-email-input")
|
||||
if !dom_is_null(email_el) {
|
||||
// Pre-fill from local storage if a pending send was interrupted.
|
||||
let pending: String = local_storage_get("auth_pending_email")
|
||||
if !str_eq(pending, "") {
|
||||
dom_set_value(email_el, pending)
|
||||
}
|
||||
// Anonymous function literal for inline event handler.
|
||||
dom_listen(email_el, "keydown", fn(event: Any) -> Void {
|
||||
let key: String = dom_get_prop(event, "key")
|
||||
if str_eq(key, "Enter") {
|
||||
send_magic_link()
|
||||
}
|
||||
})
|
||||
}
|
||||
let btn = dom_get_element("send-link-btn")
|
||||
if !dom_is_null(btn) {
|
||||
dom_listen(btn, "click", fn(event: Any) -> Void {
|
||||
send_magic_link()
|
||||
})
|
||||
}
|
||||
state_set("auth_initialized", "true")
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
// Expose send_magic_link globally so inline event handlers can call it.
|
||||
window_set("sendMagicLink", send_magic_link)
|
||||
window_set("initAuth", init_auth)
|
||||
|
||||
// Run init when DOM is ready.
|
||||
window_on_load(init_auth)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// browser-counter.el — canonical browser DOM bridge example
|
||||
//
|
||||
// Compile with: elc --target=js examples/browser-counter.el > counter.js
|
||||
//
|
||||
// Then include in an HTML page that has a <span id="count-display"> element.
|
||||
// The page can call window.increment() from any onclick handler, e.g.:
|
||||
// <button onclick="increment()">+1</button>
|
||||
//
|
||||
// On load the display is initialised to "0". Each call to increment()
|
||||
// adds 1 and updates the display text.
|
||||
//
|
||||
// Demonstrates:
|
||||
// - dom_get_element to locate a DOM node by id
|
||||
// - dom_set_text to update visible text content
|
||||
// - dom_is_null to guard against missing elements
|
||||
// - window_set to expose an El function for inline event handlers
|
||||
// - state_set/get for in-memory counter state (survives calls, resets
|
||||
// on page reload — same semantics as the C state_* API)
|
||||
|
||||
fn init() -> Void {
|
||||
state_set("counter", 0)
|
||||
let display = dom_get_element("count-display")
|
||||
if !dom_is_null(display) {
|
||||
dom_set_text(display, "0")
|
||||
}
|
||||
}
|
||||
|
||||
fn increment() -> Void {
|
||||
let current = str_to_int(state_get("counter"))
|
||||
let next = current + 1
|
||||
state_set("counter", next)
|
||||
let display = dom_get_element("count-display")
|
||||
if !dom_is_null(display) {
|
||||
dom_set_text(display, int_to_str(next))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
init()
|
||||
window_set("increment", increment)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// html-page.el — Example of native HTML template syntax in El.
|
||||
//
|
||||
// El HTML templates let you write HTML directly in expression position.
|
||||
// Interpolated values are automatically HTML-escaped.
|
||||
// Use raw(expr) to bypass escaping when you know the content is safe.
|
||||
//
|
||||
// Compile and run:
|
||||
// ./dist/platform/elc examples/html-page.el > /tmp/html-page.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o /tmp/html-page /tmp/html-page.c el-compiler/runtime/el_runtime.c
|
||||
// /tmp/html-page
|
||||
|
||||
fn render_item(item: String) -> String {
|
||||
return <li class="item">{item}</li>
|
||||
}
|
||||
|
||||
fn render_page(title: String, items: [String]) -> String {
|
||||
return <!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{title}</h1>
|
||||
<ul>
|
||||
{#each items as item}
|
||||
<li class="item">{item}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p>Built with El HTML templates</p>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
let items: [String] = ["Lexer", "Parser", "Codegen", "Runtime"]
|
||||
let page: String = render_page("El Compiler Stages", items)
|
||||
println(page)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -199,6 +199,19 @@ el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_p
|
||||
el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */
|
||||
el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
|
||||
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
|
||||
* el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML
|
||||
* cleaner. State-machine parser; tag/attribute names compared case-
|
||||
* insensitively against the allowlist; `<a href>` / `<… src>` URL schemes
|
||||
* validated (http, https, mailto, fragment-only, or relative); whole-
|
||||
* subtree drop for script / style / iframe / object / embed / form; HTML-
|
||||
* escapes free text outside dropped subtrees.
|
||||
*
|
||||
* The allowlist is JSON of the form
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
@@ -246,6 +259,146 @@ 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);
|
||||
|
||||
/* ── Instant + Duration: first-class temporal types ──────────────────────────
|
||||
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
|
||||
* since the Unix epoch; Durations are signed nanoseconds. Type discipline
|
||||
* is enforced at codegen-time: BinOps on names registered as Instant or
|
||||
* Duration route through the typed wrappers below; mismatches like
|
||||
* Instant+Instant become #error at the C compiler.
|
||||
*
|
||||
* Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are
|
||||
* recognised by the parser as DurationLit AST nodes and lowered to literal
|
||||
* int64 nanoseconds at codegen time. The runtime never sees the units. */
|
||||
|
||||
el_val_t el_now_instant(void);
|
||||
el_val_t now(void);
|
||||
el_val_t unix_seconds(el_val_t n);
|
||||
el_val_t unix_millis(el_val_t n);
|
||||
el_val_t instant_from_iso8601(el_val_t s);
|
||||
|
||||
el_val_t el_duration_from_nanos(el_val_t ns);
|
||||
el_val_t duration_seconds(el_val_t n);
|
||||
el_val_t duration_millis(el_val_t n);
|
||||
el_val_t duration_nanos(el_val_t n);
|
||||
|
||||
el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_diff(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_add(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_sub(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_scale(el_val_t dur, el_val_t scalar);
|
||||
el_val_t el_duration_div(el_val_t dur, el_val_t scalar);
|
||||
|
||||
el_val_t el_instant_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ne(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ne(el_val_t a, el_val_t b);
|
||||
|
||||
el_val_t instant_to_unix_seconds(el_val_t i);
|
||||
el_val_t instant_to_unix_millis(el_val_t i);
|
||||
el_val_t instant_to_iso8601(el_val_t i);
|
||||
el_val_t duration_to_seconds(el_val_t d);
|
||||
el_val_t duration_to_millis(el_val_t d);
|
||||
el_val_t duration_to_nanos(el_val_t d);
|
||||
|
||||
el_val_t el_sleep_duration(el_val_t dur);
|
||||
el_val_t unix_timestamp(void);
|
||||
|
||||
el_val_t ttl_cache_set(el_val_t key, el_val_t value);
|
||||
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
|
||||
el_val_t ttl_cache_age(el_val_t key);
|
||||
|
||||
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
|
||||
* Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
|
||||
* zones, Gregorian, DST) is the user-facing default; MarsCalendar,
|
||||
* CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
|
||||
* domains.
|
||||
*
|
||||
* A Calendar interprets an Instant under a particular cycle convention and
|
||||
* produces a CalendarTime. CalendarTime carries the underlying Instant and
|
||||
* a back-pointer to its Calendar; arithmetic and formatting consult the
|
||||
* Calendar to convert ns since epoch into year/month/day/hour/minute/second
|
||||
* (or sol/phase, or cycle/phase, depending on kind).
|
||||
*
|
||||
* Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
|
||||
* LocalDateTime are heap-allocated structs whose pointers are cast into
|
||||
* el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
|
||||
* the kind safely. LocalTime is small enough to live in the int64 slot
|
||||
* directly (nanos since midnight, signed). */
|
||||
|
||||
/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
|
||||
* `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
|
||||
* offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
|
||||
* on first use of the owning EarthCalendar. */
|
||||
el_val_t zone(el_val_t id);
|
||||
el_val_t zone_utc(void);
|
||||
el_val_t zone_local(void);
|
||||
el_val_t zone_offset(el_val_t hours, el_val_t minutes);
|
||||
|
||||
/* Calendar constructors. Each returns an el_val_t pointer to a heap-
|
||||
* allocated, magic-tagged Calendar struct. Calendars are interned by
|
||||
* (kind, zone_id, period_ns, epoch_ns) so identical constructors return
|
||||
* the same pointer — equality is reference equality. */
|
||||
el_val_t earth_calendar(el_val_t z);
|
||||
el_val_t earth_calendar_default(void);
|
||||
el_val_t mars_calendar(void);
|
||||
el_val_t cycle_calendar(el_val_t period_dur);
|
||||
el_val_t no_cycle_calendar(void);
|
||||
el_val_t relative_calendar(el_val_t epoch_inst);
|
||||
|
||||
/* CalendarTime constructors and methods. Returns a heap-allocated struct
|
||||
* whose pointer fits in el_val_t. */
|
||||
el_val_t now_in(el_val_t cal);
|
||||
el_val_t in_calendar(el_val_t inst, el_val_t cal);
|
||||
el_val_t cal_format(el_val_t ct, el_val_t pattern);
|
||||
el_val_t cal_to_instant(el_val_t ct);
|
||||
el_val_t cal_cycle_phase(el_val_t ct);
|
||||
el_val_t cal_in(el_val_t ct, el_val_t cal);
|
||||
|
||||
/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
|
||||
* LocalTime carries nanoseconds since midnight as a signed int64 directly
|
||||
* in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
|
||||
* heap-allocated structs with magic headers. */
|
||||
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
|
||||
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
|
||||
el_val_t local_datetime(el_val_t date, el_val_t time);
|
||||
el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
|
||||
|
||||
el_val_t local_date_year(el_val_t ld);
|
||||
el_val_t local_date_month(el_val_t ld);
|
||||
el_val_t local_date_day(el_val_t ld);
|
||||
el_val_t local_time_hour(el_val_t lt);
|
||||
el_val_t local_time_minute(el_val_t lt);
|
||||
el_val_t local_time_second(el_val_t lt);
|
||||
el_val_t local_time_nanos(el_val_t lt);
|
||||
|
||||
el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
|
||||
el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
|
||||
el_val_t el_local_date_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_local_date_eq(el_val_t a, el_val_t b);
|
||||
|
||||
/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
|
||||
* pointer in el_val_t; rhythms are immutable so callers may share them. */
|
||||
el_val_t rhythm_cycle_start(void);
|
||||
el_val_t rhythm_cycle_phase(el_val_t phase);
|
||||
el_val_t rhythm_duration(el_val_t d);
|
||||
el_val_t rhythm_session_start(void);
|
||||
el_val_t rhythm_event(el_val_t name);
|
||||
el_val_t rhythm_and(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_or(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_weekday(el_val_t day);
|
||||
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
|
||||
el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
|
||||
el_val_t rhythm_matches(el_val_t r, el_val_t ct);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t uuid_new(void);
|
||||
@@ -288,10 +441,53 @@ el_val_t str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t str_char_code(el_val_t s, el_val_t i);
|
||||
el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_format(el_val_t template, el_val_t data);
|
||||
el_val_t str_format(el_val_t fmt, el_val_t data);
|
||||
el_val_t str_lower(el_val_t s);
|
||||
el_val_t str_upper(el_val_t s);
|
||||
|
||||
/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
|
||||
* Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex.
|
||||
* is_* predicates: empty input returns false; multi-char requires ALL bytes
|
||||
* to match. ASCII ranges only in Phase 1. */
|
||||
|
||||
/* Counting */
|
||||
el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */
|
||||
el_val_t str_count_chars(el_val_t s); /* codepoint count */
|
||||
el_val_t str_count_bytes(el_val_t s); /* alias of str_len */
|
||||
el_val_t str_count_lines(el_val_t s);
|
||||
el_val_t str_count_words(el_val_t s);
|
||||
el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */
|
||||
el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */
|
||||
|
||||
/* Find / position */
|
||||
el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */
|
||||
el_val_t str_last_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */
|
||||
|
||||
/* Transform */
|
||||
el_val_t str_repeat(el_val_t s, el_val_t n);
|
||||
el_val_t str_reverse(el_val_t s); /* by codepoint */
|
||||
el_val_t str_strip_prefix(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_strip_suffix(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_strip_chars(el_val_t s, el_val_t chars);
|
||||
el_val_t str_lstrip(el_val_t s);
|
||||
el_val_t str_rstrip(el_val_t s);
|
||||
|
||||
/* Char classification (Bool) */
|
||||
el_val_t is_letter(el_val_t s);
|
||||
el_val_t is_digit(el_val_t s);
|
||||
el_val_t is_alphanumeric(el_val_t s);
|
||||
el_val_t is_whitespace(el_val_t s);
|
||||
el_val_t is_punctuation(el_val_t s);
|
||||
el_val_t is_uppercase(el_val_t s);
|
||||
el_val_t is_lowercase(el_val_t s);
|
||||
|
||||
/* Split / join */
|
||||
el_val_t str_split_lines(el_val_t s);
|
||||
el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */
|
||||
el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n);
|
||||
el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */
|
||||
|
||||
/* ── List additions ──────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t list_push(el_val_t list, el_val_t elem);
|
||||
@@ -364,6 +560,19 @@ el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
|
||||
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags);
|
||||
/* Layered consciousness — see el_runtime.c for the layered architecture
|
||||
* design notes (search "Layered consciousness architecture"). The five
|
||||
* canonical layers (safety / core-identity / domain-knowledge / imprint /
|
||||
* suit) are seeded automatically; engram_add_layer extends the registry
|
||||
* with imprint or suit overlays at runtime. Nodes default to layer 1
|
||||
* (core-identity) when created via engram_node / engram_node_full. */
|
||||
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable);
|
||||
el_val_t engram_remove_layer(el_val_t layer_id);
|
||||
el_val_t engram_list_layers(void);
|
||||
el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
@@ -375,6 +584,8 @@ el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id);
|
||||
el_val_t engram_neighbors(el_val_t node_id);
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_edge_count(void);
|
||||
/* Three-pass activation: background fan-out → working-memory promotion →
|
||||
* Layer 0 override. See "Three-pass activation" in el_runtime.c. */
|
||||
el_val_t engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_save(el_val_t path);
|
||||
el_val_t engram_load(el_val_t path);
|
||||
@@ -385,9 +596,16 @@ el_val_t engram_load(el_val_t path);
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_stats_json(void);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||
* no nodes promoted to working memory. */
|
||||
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
||||
* All functions call https://api.anthropic.com/v1/messages with the API key
|
||||
@@ -476,6 +694,21 @@ el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined);
|
||||
|
||||
el_val_t sha3_256_hex(el_val_t input);
|
||||
|
||||
/* ── AEAD: AES-256-GCM (libcrypto-backed) ───────────────────────────────────
|
||||
* Symmetric authenticated encryption used to wrap envelopes after a KEM
|
||||
* handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the
|
||||
* Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256.
|
||||
*
|
||||
* aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where
|
||||
* ciphertext is the AES-256-GCM output with the 16-byte auth tag appended.
|
||||
* Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which
|
||||
* structurally rules out the GCM nonce-reuse footgun.
|
||||
*
|
||||
* aead_decrypt returns the plaintext String, or "" on any failure (including
|
||||
* auth-tag mismatch). Callers MUST check for "" before trusting the result. */
|
||||
el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext);
|
||||
el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex);
|
||||
|
||||
/* ── Native VM builtin aliases (for compiled El source) ─────────────────────
|
||||
* These match the El VM's native_* builtins so that El source compiled
|
||||
* to C can call the same names without modification. */
|
||||
@@ -502,6 +735,22 @@ el_val_t get(el_val_t list, el_val_t index); /* el_list_get */
|
||||
el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */
|
||||
el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
|
||||
|
||||
/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */
|
||||
/* See bottom of el_runtime.c for the implementation.
|
||||
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
|
||||
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
|
||||
/* ── Subprocess execution ────────────────────────────────────────────────── */
|
||||
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
|
||||
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+275
-45
@@ -1,6 +1,6 @@
|
||||
# El JavaScript Backend (codegen-js)
|
||||
|
||||
**Status:** scaffolded. Hello-world compiles and runs. ~50% language coverage. Core runtime (~30 builtins) implemented. CGI / DHARMA / LLM / Engram intentionally stubbed.
|
||||
**Status:** Phase 5 complete. ~90% language coverage. Full browser JavaScript can be expressed structurally in El without any `native_js` escape hatches. All additions since Phase 4: anonymous function literals (lambda syntax), try/catch statement, extern fn declarations, direct JS method call syntax on Any-typed values, Promise helpers, Object/Array utilities, and URL import declarations. Proof: `examples/browser-auth.el` is a complete Supabase auth flow with zero `native_js` or `native_js_call` calls.
|
||||
|
||||
**Authoritative files**
|
||||
|
||||
@@ -57,27 +57,36 @@ Same function names as `el_runtime.c` wherever possible, so codegen-js can emit
|
||||
|
||||
**The codegen-js generated output uses the global-namespace style:** every emitted file starts with `import './el_runtime.js'` (which side-effects the globals) so call sites stay flat — `println(x)` not `el.println(x)`. This matches the C backend's flat call surface and keeps the generated code grep-compatible across targets.
|
||||
|
||||
### Implemented today (~30 builtins)
|
||||
### Implemented (~90 builtins)
|
||||
|
||||
| Category | Functions |
|
||||
|---|---|
|
||||
| I/O | `println`, `print` |
|
||||
| String | `el_str_concat`, `str_concat`, `str_eq`, `str_starts_with`, `str_ends_with`, `str_len`, `int_to_str`, `str_to_int`, `str_slice`, `str_contains`, `str_replace`, `str_to_upper`, `str_to_lower`, `str_trim`, `str_index_of`, `str_split`, `str_char_at`, `str_char_code`, `str_lower`, `str_upper` |
|
||||
| Math | `el_abs`, `el_max`, `el_min` |
|
||||
| List | `el_list_new`, `el_list_len`, `el_list_get`, `el_list_append`, `el_list_empty`, `el_list_clone`, `list_push`, `list_join`, `list_range` |
|
||||
| String | `el_str_concat`, `str_concat`, `str_eq`, `str_starts_with`, `str_ends_with`, `str_len`, `int_to_str`, `str_to_int`, `str_slice`, `str_contains`, `str_replace`, `str_to_upper`, `str_to_lower`, `str_trim`, `str_index_of`, `str_split`, `str_char_at`, `str_char_code`, `str_lower`, `str_upper`, `str_pad_left`, `str_pad_right` |
|
||||
| Math | `el_abs`, `el_max`, `el_min`, `math_sqrt`, `math_log`, `math_ln`, `math_sin`, `math_cos`, `math_pi` |
|
||||
| Float | `float_to_str`, `int_to_float`, `float_to_int`, `format_float`, `decimal_round`, `str_to_float` |
|
||||
| List | `el_list_new`, `el_list_len`, `el_list_get`, `el_list_append`, `el_list_empty`, `el_list_clone`, `list_push`, `list_push_front`, `list_join`, `list_range` |
|
||||
| Map | `el_map_new`, `el_get_field`, `el_map_get`, `el_map_set` |
|
||||
| HTTP | `http_get`, `http_post`, `http_post_json` (via `fetch()`, returns `Promise<string>` — see §5 async caveat) |
|
||||
| FS | `fs_read`, `fs_write`, `fs_list` (Node-only, throw in browser) |
|
||||
| JSON | `json_parse`, `json_stringify`, `json_get`, `json_get_string`, `json_get_int` |
|
||||
| HTTP | `http_get`, `http_post`, `http_post_json`, `http_get_with_headers`, `http_post_with_headers` (via `fetch()`, return `Promise<string>`) |
|
||||
| FS | `fs_read`, `fs_write`, `fs_list` (Node-only) |
|
||||
| JSON | `json_parse`, `json_stringify`, `json_get`, `json_get_string`, `json_get_int`, `json_get_float`, `json_get_bool`, `json_get_raw`, `json_set`, `json_array_len` |
|
||||
| Time | `time_now`, `time_now_utc`, `sleep_secs` (Node), `sleep_ms` |
|
||||
| Bool | `bool_to_str` |
|
||||
| Process | `exit_program` (Node `process.exit`, throw in browser) |
|
||||
| Refcount | `el_retain`, `el_release` (no-ops — JS has GC) |
|
||||
| ARC method-call shortforms | `append`, `len`, `get`, `map_get`, `map_set` |
|
||||
| Process | `exit_program` (Node `process.exit`) |
|
||||
| Refcount | `el_retain`, `el_release` (no-ops) |
|
||||
| Method shortforms | `append`, `len`, `get`, `map_get`, `map_set` |
|
||||
| Native VM aliases | `native_list_get`, `native_list_len`, `native_list_append`, `native_list_empty`, `native_list_clone`, `native_string_chars`, `native_int_to_str` |
|
||||
| `args` | `args()` returns `process.argv.slice(2)` in Node, `[]` in browser |
|
||||
| `state_*` | In-memory `Map` keyed by string |
|
||||
| `env` | `process.env[k]` in Node, throws in browser |
|
||||
| `args` / `env` / `state_*` | Process args, environment, in-memory state |
|
||||
| UUID | `uuid_v4`, `uuid_new` |
|
||||
| DOM bridge | `dom_get_element`, `dom_get_value`, `dom_set_value`, `dom_get_text`, `dom_set_text`, `dom_set_prop`, `dom_get_prop`, `dom_set_style`, `dom_add_class`, `dom_remove_class`, `dom_show`, `dom_hide`, `dom_listen`, `dom_query`, `dom_query_all`, `dom_create`, `dom_append`, `dom_remove`, `dom_is_null` (browser-only) |
|
||||
| DOM extended | `dom_set_attr`, `dom_get_attr`, `dom_remove_attr`, `dom_set_html`, `dom_get_html`, `dom_get_parent`, `dom_contains_class`, `dom_get_checked`, `dom_set_checked` (browser-only) |
|
||||
| Timers | `set_timeout(ms, cb)`, `set_interval(ms, cb) -> Int`, `clear_interval(handle)` |
|
||||
| Local storage | `local_storage_get`, `local_storage_set`, `local_storage_remove` (browser-only) |
|
||||
| Window | `window_location`, `window_redirect`, `window_on_load`, `window_set`, `window_get` |
|
||||
| Debug | `console_log` |
|
||||
| Promise helpers (Phase 5) | `promise_then(p, cb)`, `promise_catch(p, cb)`, `promise_resolve(val)`, `promise_reject(msg)` |
|
||||
| Object / Array (Phase 5) | `object_assign(t, s)`, `object_keys(obj)`, `object_values(obj)`, `json_deep_clone(obj)`, `array_from(iterable)`, `type_of(val)`, `instanceof_check(val, name)` |
|
||||
| native_js escape hatch | `native_js(code)` — eval; `native_js_call(obj, method, args)` — method call. Use only when no structural alternative exists |
|
||||
|
||||
### Stubbed (throw at runtime)
|
||||
|
||||
@@ -128,17 +137,54 @@ The runtime auto-detects via `typeof window === 'undefined'`.
|
||||
|
||||
---
|
||||
|
||||
## 5. The async problem (the big deferred decision)
|
||||
## 5. The async problem
|
||||
|
||||
`fetch()` is async. The C backend's `http_get(url)` is synchronous and returns the body string directly. El source was written assuming sync. Three options:
|
||||
|
||||
1. **Pretend it's sync from El's POV; use synchronous XHR (browser) or `child_process.execSync('curl …')` (Node).** Bad: synchronous XHR is deprecated and frozen on the main thread; `execSync` is a hack.
|
||||
2. **Make every `http_*` builtin in the JS runtime return a `Promise`, and rewrite codegen-js to insert `await` everywhere.** This requires turning every El function that transitively calls a network builtin into an `async fn` in JS. Doable, but invasive — the El AST does not currently mark async-ness.
|
||||
3. **Compile El's call sites with implicit await; compile-time taint tracking marks every fn that transitively calls a network builtin as `async`. Generated JS uses `async function` and `await`.** This is the right answer long-term.
|
||||
1. **Pretend it's sync from El's POV; use synchronous XHR (browser) or `child_process.execSync('curl ...')` (Node).** Bad: synchronous XHR is deprecated and frozen on the main thread; `execSync` is a hack.
|
||||
2. **Make every `http_*` builtin in the JS runtime return a `Promise`, and rewrite codegen-js to insert `await` everywhere.** This requires turning every El function that transitively calls a network builtin into an `async fn` in JS. Doable, but invasive.
|
||||
3. **Explicit `@async` decorator on El functions; codegen-js emits `async function` + `await` for known-async call sites.** This is the approach implemented.
|
||||
|
||||
**Decision for this scaffold:** option 3, but only the runtime side is implemented. `http_get` in `el_runtime.js` returns a `Promise<string>`. `codegen-js.el` does NOT yet emit `async`/`await`. Calling `http_get` from compiled El will return a Promise that the El program will treat as a string (which produces `"[object Promise]"`). This is documented and accepted for the scaffold; the compile-time taint pass is a follow-up.
|
||||
**Decision:** option 3, with an explicit opt-in decorator. `http_get`, `http_post`, `http_post_json`, `http_get_with_headers`, and `http_post_with_headers` in `el_runtime.js` return `Promise<string>`. `codegen-js.el` now emits `await` before calls to these builtins and before calls to any El function decorated `@async`.
|
||||
|
||||
For now, programs that don't touch HTTP work correctly. That covers `el-ui/runtime` (which only manipulates the DOM and a graph), most of cgi-studio's pure UI components, and all hello-world style programs.
|
||||
### How to use async in El (JS target)
|
||||
|
||||
Mark a function with `@async` to declare it as async. Any call to that function from another El function will automatically get `await` in the generated JS. The callee must also be `@async` (or call only non-async code) for the pattern to compose correctly.
|
||||
|
||||
```el
|
||||
@async
|
||||
fn fetch_user(id: String) -> String {
|
||||
http_get("https://api.example.com/users/" + id)
|
||||
}
|
||||
|
||||
@async
|
||||
fn main() -> Void {
|
||||
let body = fetch_user("42")
|
||||
println(body)
|
||||
}
|
||||
```
|
||||
|
||||
Compiles to:
|
||||
|
||||
```javascript
|
||||
async function fetch_user(id) {
|
||||
return await http_get("https://api.example.com/users/" + id);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let body = await fetch_user("42");
|
||||
println(body);
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
- `@async` is a JS-target-only convention. The C backend ignores the decorator (it calls the synchronous libcurl-backed version).
|
||||
- Implicit taint propagation (auto-marking all transitive callers) is not implemented. The programmer must explicitly add `@async` to every function in the call chain that reaches an async builtin.
|
||||
- Forward-reference calls to `@async` functions are handled correctly: codegen-js does a pre-registration pass over all FnDefs before emitting any code.
|
||||
|
||||
For programs that do not touch HTTP, no `@async` annotation is needed and the generated code is identical to before.
|
||||
|
||||
---
|
||||
|
||||
@@ -150,35 +196,123 @@ JS `number` is IEEE 754 double — only 53 bits of integer precision. El `Int` i
|
||||
|
||||
---
|
||||
|
||||
## 7. What's NOT supported in JS target initially
|
||||
## 7. Language features — JS target coverage
|
||||
|
||||
This is the canonical list. Programs that use any of these compile (no `#error`-style fail-fast like the C backend's capability check) but throw at runtime or behave as documented.
|
||||
### Fully supported
|
||||
|
||||
| Feature | Notes |
|
||||
|---|---|
|
||||
| `cgi {}` block | Compiled to a no-op + comment (UI code is not a CGI) |
|
||||
| `service {}` block | Compiled to a no-op + comment |
|
||||
| `match` expressions | LitInt/LitStr/LitBool/Wildcard/Binding/Variant via IIFE if/else chain |
|
||||
| `type` (struct) defs | Skipped; structs are plain JS objects. `t["field"]` works |
|
||||
| `enum` defs | Skipped; enum values are strings or ints |
|
||||
| `?` postfix (nil-prop) | `obj?.field` emits `(obj)?.["field"] ?? null` via JS optional chaining |
|
||||
| `extern fn` | Emits a comment; calls resolve to JS environment globals |
|
||||
| Anonymous function literals | `fn(p: T) -> R { body }` emits a hoisted `function __lambda_N(p)` |
|
||||
| `try/catch` | Emits `try { ... } catch (name) { ... }` directly |
|
||||
| URL imports | `import "https://..."` emits ES module import (or comment in bundle mode) |
|
||||
| Method call on `Any` | `obj.method(args)` emits `obj.method(args)` for non-El-shortform methods |
|
||||
| Field access on `Any` | `obj.field` emits `obj["field"]` (bracket notation, works on prototype chains) |
|
||||
| `@async` decorator | `async function` + `await` at call sites for async builtins and `@async` fns |
|
||||
|
||||
### Not supported (stub throws or no-op)
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---|---|---|
|
||||
| `cgi {}` block | Compiled to a no-op + warning comment | CGI identity is server-side. UI code is not a CGI. |
|
||||
| `service {}` block | Compiled to a no-op + warning comment | Same. |
|
||||
| All `dharma_*` | Stub throws | Programs needing DHARMA must call a server-side daemon over HTTP |
|
||||
| All `engram_*` | Stub throws | Could be ported to in-browser (IndexedDB-backed) later |
|
||||
| All `llm_*` | Stub throws | Browser cannot hold API keys; route through server |
|
||||
| `llm_register_tool` | Stub throws | Same |
|
||||
| `http_serve` | Stub throws | Browsers cannot serve. Node-mode could, deferred |
|
||||
| `http_set_handler` | Stub throws | Same |
|
||||
| `match` expressions | Compiled (basic) | LitInt/LitStr/LitBool/Wildcard/Binding all work via `if/else` chain. Tagged-union match deferred |
|
||||
| `type` (struct) defs | Skipped at codegen | Treated as documentation; structs are plain JS objects. `t["field"]` works |
|
||||
| `enum` defs | Skipped at codegen | Same — enum values are bare strings or ints |
|
||||
| `?` postfix (nil-prop) | No-op | Same as C backend's current state |
|
||||
| `try` postfix | Stripped to inner | Same as C backend |
|
||||
| Capability enforcement | Not enforced | The C backend uses `#error` directives; the JS backend lets the runtime stubs throw. Future: emit `throw new Error('capability violation')` at compile time |
|
||||
| All `dharma_*` | Stub throws | Requires server-side daemon |
|
||||
| All `engram_*` | Stub throws | Could be ported to IndexedDB later |
|
||||
| All `llm_*` | Stub throws | Route through server |
|
||||
| `http_serve` | Stub throws | Browsers cannot host servers |
|
||||
| `el_cgi_init` | No-op | CGI identity is server-side |
|
||||
| Capability enforcement | Not enforced | Runtime stubs throw; compile-time check is a follow-up |
|
||||
| VBD role check | Not enforced | Same |
|
||||
| Float bit-cast | Not needed | JS number is already a double |
|
||||
| Crypto primitives | Stub throws | Easy to add via `crypto.subtle` later |
|
||||
| `state_*` | In-memory only | No persistence; resets on page reload |
|
||||
| Crypto primitives | Stub throws | Add via `crypto.subtle` later |
|
||||
| `state_*` | In-memory only | Resets on page reload |
|
||||
| `args()` | Node-only | Browser returns `[]` |
|
||||
| `fs_*` | Node-only | Browser throws |
|
||||
|
||||
---
|
||||
|
||||
## 7a. Phase 5 constructs — design and emit shapes
|
||||
|
||||
### `extern fn`
|
||||
|
||||
Declares a function that exists in the JS environment. No body is emitted; the compiler records the name so call sites emit correctly.
|
||||
|
||||
```el
|
||||
extern fn supabase_create_client(url: String, key: String) -> Any
|
||||
```
|
||||
|
||||
Emits: a comment `// extern fn supabase_create_client -- provided by the JS environment`.
|
||||
Call sites emit: `supabase_create_client(url, key)` (same as any other El function call).
|
||||
|
||||
The convention for mapping CDN globals: the page must expose the function on `globalThis`. For Supabase, the CDN bundle exposes `supabase.createClient`; a thin adapter assigns `globalThis.supabase_create_client = supabase.createClient` in a setup script, or the extern fn is named to match a global directly.
|
||||
|
||||
### Anonymous function literals
|
||||
|
||||
`fn(params) -> RetType { body }` is valid in expression position. Emitted as a hoisted function declaration with a generated name.
|
||||
|
||||
```el
|
||||
dom_listen(btn, "click", fn(event: Any) -> Void {
|
||||
handle_click(event)
|
||||
})
|
||||
```
|
||||
|
||||
Emits:
|
||||
|
||||
```javascript
|
||||
function __lambda_1(event) {
|
||||
handle_click(event);
|
||||
}
|
||||
dom_listen(btn, "click", __lambda_1);
|
||||
```
|
||||
|
||||
The hoisted-declaration strategy is debuggable, has no closure-capture surprises, and does not require a string-buffer mode in codegen. The generated name appears in stack traces.
|
||||
|
||||
### `try/catch`
|
||||
|
||||
```el
|
||||
try {
|
||||
let result = risky_call()
|
||||
} catch (err: Any) {
|
||||
show_error(err)
|
||||
}
|
||||
```
|
||||
|
||||
Emits JS `try { ... } catch (err) { ... }` directly. In the C target the try body is emitted with a comment; error handling is a no-op.
|
||||
|
||||
### Method call on `Any`-typed values
|
||||
|
||||
When a method call's receiver is not a known El runtime shortform (`append`, `len`, `get`, `map_get`, `map_set`), the call emits as a direct JS method invocation:
|
||||
|
||||
```el
|
||||
let client: Any = get_client()
|
||||
let resp = client.auth.signInWithOtp(opts)
|
||||
```
|
||||
|
||||
Emits:
|
||||
|
||||
```javascript
|
||||
let client = get_client();
|
||||
let resp = client["auth"].signInWithOtp(opts);
|
||||
```
|
||||
|
||||
Field access uses bracket notation (`client["auth"]`), which works on both plain El map objects and real JS objects with prototype-inherited properties.
|
||||
|
||||
### URL imports
|
||||
|
||||
```el
|
||||
import "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js"
|
||||
```
|
||||
|
||||
In module mode: `import "https://...";` at the top of the generated file.
|
||||
In bundle/IIFE mode: `// external: https://...` comment.
|
||||
El source imports (`.el` files) are excluded -- they were already inlined by `resolve_imports`.
|
||||
|
||||
---
|
||||
|
||||
## 8. CLI dispatch — `--target=js`
|
||||
|
||||
The compiler entry point `compiler.el` adds a `compile_js(source: String) -> String` alongside the existing `compile()`. The CLI behavior:
|
||||
@@ -195,20 +329,116 @@ The argv parser scans for a `--target=<lang>` token; remaining positional args a
|
||||
|
||||
---
|
||||
|
||||
## 8a. Production output — `--minify` and `--obfuscate`
|
||||
|
||||
Two post-processing flags produce production-ready browser JS in a single compiler invocation, replacing any external post-processing scripts.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
elc --target=js --bundle --minify source.el > output.min.js
|
||||
elc --target=js --bundle --obfuscate source.el > output.obf.js
|
||||
elc --target=js --bundle --minify --obfuscate source.el > output.final.js
|
||||
```
|
||||
|
||||
Both flags require `--target=js`. Passing either without `--target=js` prints an error and exits with code 1.
|
||||
|
||||
`--obfuscate` implies `--minify` — obfuscating unminified code produces no benefit and only increases output size.
|
||||
|
||||
### Pipeline order
|
||||
|
||||
```
|
||||
generate JS -> (if --bundle, wrap in IIFE) -> (if --minify, run terser) -> (if --obfuscate, run javascript-obfuscator) -> output
|
||||
```
|
||||
|
||||
### Tool discovery
|
||||
|
||||
The compiler looks for each tool in this order:
|
||||
|
||||
1. `<src_dir>/node_modules/.bin/<tool>` — local install next to source file
|
||||
2. `<src_dir>/../node_modules/.bin/<tool>` — one level up (monorepo layout)
|
||||
3. `npx --yes <tool>` — fall back to npx (uses globally cached package or downloads on first use)
|
||||
|
||||
If no path resolves and npx is not on `PATH`, the compiler prints a clear error and exits non-zero:
|
||||
|
||||
```
|
||||
el-compiler: error: terser not found. Run 'npm install terser' in your project directory.
|
||||
el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory.
|
||||
```
|
||||
|
||||
### Minification (terser)
|
||||
|
||||
Command issued internally:
|
||||
|
||||
```
|
||||
terser <tmpfile> --compress passes=2,drop_console=false,drop_debugger=true \
|
||||
--mangle 'reserved=[<reserved>]' --output <tmpfile.min>
|
||||
```
|
||||
|
||||
### Obfuscation (javascript-obfuscator)
|
||||
|
||||
Command issued internally (runs after minification):
|
||||
|
||||
```
|
||||
javascript-obfuscator <input> --output <output>
|
||||
--compact true
|
||||
--simplify true
|
||||
--string-array true
|
||||
--string-array-encoding base64
|
||||
--string-array-threshold 0.75
|
||||
--identifier-names-generator hexadecimal
|
||||
--rename-globals false
|
||||
--self-defending false
|
||||
--reserved-names <reserved>
|
||||
```
|
||||
|
||||
### Reserved names
|
||||
|
||||
These identifiers are protected from renaming by both tools. They are referenced directly from HTML `onclick=` attributes and other global-scope callsites:
|
||||
|
||||
```
|
||||
neuronDemoToggle, neuronDemoSend, neuronDemoReset,
|
||||
signInWith, signInWithEmail, signUpWithEmail, sendMagicLink,
|
||||
signOut, resetPassword, sendResetEmail, updatePassword,
|
||||
showSignIn, showSignUp, hideReset,
|
||||
setSort, addFamilyMember, removeFamilyMember, copyForPlatform, entHeadcountChange,
|
||||
NEURON_CFG
|
||||
```
|
||||
|
||||
### Temp files
|
||||
|
||||
The compiler uses `/tmp/elc-<pid>-<timestamp>.js` naming for temp files. All temp files are cleaned up on both success and failure paths.
|
||||
|
||||
### Implementation notes
|
||||
|
||||
- The compiler adds `stdout_to_file(path)` / `stdout_restore()` builtins to the C runtime (`el_runtime.c`) to capture codegen output (which is streamed via `println`) into a temp file before passing it to the external tools.
|
||||
- `--minify` and `--obfuscate` error messages are printed after stdout is restored, so they always reach the terminal regardless of output redirection.
|
||||
|
||||
---
|
||||
|
||||
## 9. The path to compiling el-ui/runtime through this backend
|
||||
|
||||
This is the real-world test. `el-ui/runtime/src/` is currently 5 hand-written `.js` files. The path to authoring them in El:
|
||||
|
||||
1. **Phase 1 — Hello-world** (this scaffold). Done.
|
||||
2. **Phase 2 — language coverage.** Get codegen-js to ~95% parity with codegen.el for non-network features. Specifically: `match`, struct/enum field access, `?`-propagation, full `for`-over-list, complete unary/binary operators, lexical closures (the C backend doesn't have these but we'll need them for el-ui's component model).
|
||||
3. **Phase 3 — DOM bridge.** Add `dom_*` builtins to el_runtime.js: `dom_create_element`, `dom_set_text`, `dom_append_child`, `dom_query`, `dom_listen`, etc. These are Node-as-El builtins for the browser; the C backend will add a stub set that errors. Source-shareable El UI code becomes possible.
|
||||
4. **Phase 4 — Component class lowering.** El doesn't have classes; el-ui's `Component` is a JS class. Decide: extend El with a `component` keyword that compiles to JS class + C struct? Or have el-ui authors define components as `fn render_<name>(state) -> String` and provide a small bootstrap. The latter is the lower-impact path.
|
||||
5. **Phase 5 — Async taint pass.** Implement compile-time async tracking so `http_get` and friends produce `await fetch()` correctly. Required before authoring code that fetches data.
|
||||
6. **Phase 6 — Port `el-ui/runtime/`.** Translate the 5 JS files to El, compile to JS, swap in. Run el-ui's existing tests. Iterate.
|
||||
1. **Phase 1 — Hello-world.** DONE.
|
||||
2. **Phase 2 — Language coverage.** DONE. `match`, struct/enum field access, `?`-propagation, `for`-over-list, complete operators.
|
||||
3. **Phase 3 — DOM bridge.** DONE. Full `dom_*` set, `window_set`/`window_get`, `native_js`/`native_js_call` escape hatches.
|
||||
4. **Phase 4 — Production output.** DONE. `--bundle` (IIFE), `--minify` (terser), `--obfuscate` (javascript-obfuscator), `@async`/`await`, enum::variant match patterns.
|
||||
5. **Phase 5 — Full JS expression coverage.** DONE. This is the phase documented in this revision.
|
||||
- `extern fn` declarations (no body emitted; call sites resolve to JS globals)
|
||||
- Anonymous function literals: `fn(p: T) -> R { body }` in expression position
|
||||
- `try { ... } catch (name: T) { ... }` statement
|
||||
- Method call on `Any`-typed values: `client.auth.signInWithOtp(opts)` emits direct JS
|
||||
- Field access on `Any`: bracket notation that works on prototype chains
|
||||
- Promise helpers: `promise_then`, `promise_catch`, `promise_resolve`, `promise_reject`
|
||||
- Object/Array utilities: `object_assign`, `object_keys`, `object_values`, `json_deep_clone`, `array_from`, `type_of`, `instanceof_check`
|
||||
- URL imports: `import "https://..."` emits ES module import
|
||||
- **Proof**: `examples/browser-auth.el` -- complete Supabase auth flow with zero `native_js` or `native_js_call`
|
||||
6. **Phase 6 — Port `el-ui/runtime/`.** Translate the 5 JS files to El, compile to JS, swap in. Run el-ui's existing tests. The language is now expressive enough for this.
|
||||
7. **Phase 7 — Port cgi-studio UI.** Larger surface area; same pattern.
|
||||
8. **Phase 8 — Marketplace plugins.** Open the door for third-party UI El.
|
||||
|
||||
The blocking item between phase 1 and phase 2 is incremental — every El construct used by el-ui's source needs codegen-js coverage. Phase 5 (async) is the architectural decision that needs explicit user buy-in, because it changes the language's effective semantics on the JS target.
|
||||
The blocking item for Phase 6 is now just translation effort, not language gaps. Phase 5 removed the last structural barriers.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// test_codegen_js.el - basic tests for JS codegen features.
|
||||
//
|
||||
// These tests verify that core El language features produce correct values
|
||||
// when compiled and executed via the C backend. They serve as a
|
||||
// regression baseline for the codegen pipeline.
|
||||
|
||||
test "arithmetic" {
|
||||
let x: Int = 2 + 3
|
||||
assert x == 5, "addition"
|
||||
let y: Int = 10 - 4
|
||||
assert y == 6, "subtraction"
|
||||
let z: Int = 3 * 4
|
||||
assert z == 12, "multiplication"
|
||||
let w: Int = 15 / 3
|
||||
assert w == 5, "division"
|
||||
}
|
||||
|
||||
test "string-concat" {
|
||||
let a: String = "hello"
|
||||
let b: String = " world"
|
||||
let c: String = a + b
|
||||
assert c == "hello world", "string concatenation"
|
||||
}
|
||||
|
||||
test "str-len" {
|
||||
let n: Int = str_len("hello")
|
||||
assert n == 5, "str_len hello"
|
||||
let m: Int = str_len("")
|
||||
assert m == 0, "str_len empty"
|
||||
}
|
||||
|
||||
test "bool-logic" {
|
||||
let t = true
|
||||
let f = false
|
||||
assert t, "true is truthy"
|
||||
assert !f, "false negated is truthy"
|
||||
assert t && !f, "true and not false"
|
||||
assert t || f, "true or false"
|
||||
}
|
||||
|
||||
test "list-operations" {
|
||||
let lst: [Int] = native_list_empty()
|
||||
let lst = native_list_append(lst, 10)
|
||||
let lst = native_list_append(lst, 20)
|
||||
let lst = native_list_append(lst, 30)
|
||||
let n: Int = native_list_len(lst)
|
||||
assert n == 3, "list length 3"
|
||||
let v0: Int = native_list_get(lst, 0)
|
||||
let v1: Int = native_list_get(lst, 1)
|
||||
let v2: Int = native_list_get(lst, 2)
|
||||
assert v0 == 10, "first element"
|
||||
assert v1 == 20, "second element"
|
||||
assert v2 == 30, "third element"
|
||||
}
|
||||
|
||||
test "str-slice" {
|
||||
let s: String = str_slice("hello world", 6, 11)
|
||||
assert s == "world", "slice from 6 to 11"
|
||||
}
|
||||
|
||||
test "str-contains" {
|
||||
assert str_contains("hello world", "world"), "contains world"
|
||||
assert !str_contains("hello world", "xyz"), "does not contain xyz"
|
||||
}
|
||||
|
||||
test "int-to-str" {
|
||||
let s: String = int_to_str(42)
|
||||
assert s == "42", "int to string"
|
||||
}
|
||||
|
||||
test "str-to-int" {
|
||||
let n: Int = str_to_int("123")
|
||||
assert n == 123, "string to int"
|
||||
}
|
||||
|
||||
test "str-starts-ends" {
|
||||
assert str_starts_with("hello world", "hello"), "starts with hello"
|
||||
assert str_ends_with("hello world", "world"), "ends with world"
|
||||
assert !str_starts_with("hello world", "world"), "does not start with world"
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// test_env.el - native test suite for runtime/env.el
|
||||
//
|
||||
// Covers: env() for reading environment variables, args() returning a list,
|
||||
// state_set/get/del/keys via the env module re-exports, uuid_new/uuid_v4
|
||||
// format validation.
|
||||
|
||||
test "env-missing-returns-empty" {
|
||||
let v: String = env("__EL_NO_SUCH_VAR_XYZ__")
|
||||
assert v == "", "missing env var returns empty string"
|
||||
}
|
||||
|
||||
test "env-path-is-set" {
|
||||
// PATH is expected to be set in virtually any UNIX environment.
|
||||
let v: String = env("PATH")
|
||||
assert str_len(v) > 0, "PATH env var is non-empty"
|
||||
}
|
||||
|
||||
test "env-home-is-set" {
|
||||
// HOME is present on macOS/Linux test environments.
|
||||
let v: String = env("HOME")
|
||||
assert str_len(v) > 0, "HOME env var is non-empty"
|
||||
}
|
||||
|
||||
test "args-returns-list" {
|
||||
let a: [String] = args()
|
||||
// Even with no arguments, the list should be non-null (at minimum the
|
||||
// program name is argv[0]).
|
||||
let n: Int = native_list_len(a)
|
||||
assert n >= 0, "args returns a list (may be empty if runtime strips argv)"
|
||||
}
|
||||
|
||||
test "uuid-new-format" {
|
||||
let id: String = uuid_new()
|
||||
// UUID v4: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 chars)
|
||||
let n: Int = str_len(id)
|
||||
assert n == 36, "uuid_new returns 36-character string"
|
||||
// Check the dashes at correct positions
|
||||
let d1: String = str_char_at(id, 8)
|
||||
let d2: String = str_char_at(id, 13)
|
||||
let d3: String = str_char_at(id, 18)
|
||||
let d4: String = str_char_at(id, 23)
|
||||
assert d1 == "-", "dash at position 8"
|
||||
assert d2 == "-", "dash at position 13"
|
||||
assert d3 == "-", "dash at position 18"
|
||||
assert d4 == "-", "dash at position 23"
|
||||
}
|
||||
|
||||
test "uuid-v4-format" {
|
||||
let id: String = uuid_v4()
|
||||
let n: Int = str_len(id)
|
||||
assert n == 36, "uuid_v4 returns 36-character string"
|
||||
// The version nibble must be '4'
|
||||
let version_char: String = str_char_at(id, 14)
|
||||
assert version_char == "4", "uuid_v4 version nibble is '4'"
|
||||
}
|
||||
|
||||
test "uuid-uniqueness" {
|
||||
let id1: String = uuid_new()
|
||||
let id2: String = uuid_new()
|
||||
assert !str_eq(id1, id2), "two uuid_new calls produce different UUIDs"
|
||||
}
|
||||
|
||||
test "env-state-set-get-via-env-module" {
|
||||
// runtime/env.el re-exports state_set / state_get.
|
||||
state_set("env_test_key", "env_test_val")
|
||||
let v: String = state_get("env_test_key")
|
||||
assert v == "env_test_val", "state_set/get work via env module"
|
||||
state_del("env_test_key")
|
||||
let after: String = state_get("env_test_key")
|
||||
assert after == "", "state_del removes key"
|
||||
}
|
||||
|
||||
test "env-state-keys-json-via-env-module" {
|
||||
state_set("esk_a", "1")
|
||||
state_set("esk_b", "2")
|
||||
let ks: String = state_keys()
|
||||
assert str_starts_with(ks, "["), "state_keys returns JSON array"
|
||||
assert str_contains(ks, "esk_a"), "esk_a in keys"
|
||||
assert str_contains(ks, "esk_b"), "esk_b in keys"
|
||||
state_del("esk_a")
|
||||
state_del("esk_b")
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// test_fs.el - native test suite for runtime/fs.el
|
||||
//
|
||||
// Covers: fs_write/read round-trip, fs_exists, fs_mkdir, fs_list,
|
||||
// fs_list_json, and edge cases (empty file, overwrite, non-existent path).
|
||||
|
||||
test "fs-write-and-read" {
|
||||
let path: String = "/tmp/el_test_fs_basic.txt"
|
||||
let content: String = "hello from El"
|
||||
let ok: Bool = fs_write(path, content)
|
||||
assert ok, "fs_write returns true on success"
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "fs_read returns what was written"
|
||||
}
|
||||
|
||||
test "fs-exists" {
|
||||
let path: String = "/tmp/el_test_fs_exists.txt"
|
||||
fs_write(path, "exists check")
|
||||
let exists: Bool = fs_exists(path)
|
||||
assert exists, "fs_exists returns true for created file"
|
||||
let missing: Bool = fs_exists("/tmp/__el_no_such_file_xyz__.txt")
|
||||
assert !missing, "fs_exists returns false for non-existent file"
|
||||
}
|
||||
|
||||
test "fs-read-nonexistent" {
|
||||
let got: String = fs_read("/tmp/__el_no_such_file_abc__.txt")
|
||||
assert got == "", "reading nonexistent file returns empty string"
|
||||
}
|
||||
|
||||
test "fs-write-overwrite" {
|
||||
let path: String = "/tmp/el_test_fs_overwrite.txt"
|
||||
fs_write(path, "original content")
|
||||
fs_write(path, "new content")
|
||||
let got: String = fs_read(path)
|
||||
assert got == "new content", "second write overwrites first"
|
||||
}
|
||||
|
||||
test "fs-write-empty-file" {
|
||||
let path: String = "/tmp/el_test_fs_empty.txt"
|
||||
let ok: Bool = fs_write(path, "")
|
||||
assert ok, "writing empty file succeeds"
|
||||
let got: String = fs_read(path)
|
||||
assert got == "", "reading empty file returns empty string"
|
||||
let exists: Bool = fs_exists(path)
|
||||
assert exists, "empty file still exists"
|
||||
}
|
||||
|
||||
test "fs-write-multiline" {
|
||||
let path: String = "/tmp/el_test_fs_multiline.txt"
|
||||
let content: String = "line one\nline two\nline three"
|
||||
fs_write(path, content)
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "multiline content round-trips"
|
||||
let lines: [String] = str_split_lines(got)
|
||||
let n: Int = native_list_len(lines)
|
||||
assert n == 3, "three lines after read"
|
||||
}
|
||||
|
||||
test "fs-mkdir" {
|
||||
let dir: String = "/tmp/el_test_fs_mkdir_dir"
|
||||
let ok: Bool = fs_mkdir(dir)
|
||||
assert ok, "fs_mkdir returns true"
|
||||
let exists: Bool = fs_exists(dir)
|
||||
assert exists, "created directory exists"
|
||||
}
|
||||
|
||||
test "fs-list" {
|
||||
let dir: String = "/tmp/el_test_fs_list_dir"
|
||||
fs_mkdir(dir)
|
||||
fs_write(dir + "/a.txt", "a")
|
||||
fs_write(dir + "/b.txt", "b")
|
||||
let files: [String] = fs_list(dir)
|
||||
// Filter out empty strings from trailing newline
|
||||
let n: Int = native_list_len(files)
|
||||
// We expect at least 2 entries (a.txt and b.txt)
|
||||
assert n >= 2, "fs_list returns at least 2 entries"
|
||||
}
|
||||
|
||||
test "fs-list-json" {
|
||||
let dir: String = "/tmp/el_test_fs_listjson_dir"
|
||||
fs_mkdir(dir)
|
||||
fs_write(dir + "/x.txt", "x")
|
||||
fs_write(dir + "/y.txt", "y")
|
||||
let result: String = fs_list_json(dir)
|
||||
assert str_starts_with(result, "["), "fs_list_json returns JSON array"
|
||||
assert str_ends_with(result, "]"), "fs_list_json JSON array is closed"
|
||||
// Should contain at least one filename
|
||||
assert str_len(result) > 2, "fs_list_json result is non-empty array"
|
||||
}
|
||||
|
||||
test "fs-write-and-read-special-chars" {
|
||||
let path: String = "/tmp/el_test_fs_special.txt"
|
||||
let content: String = "tabs:\there\nnewlines: ok\n\"quoted\""
|
||||
fs_write(path, content)
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "special chars in content round-trip"
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// test_json.el - native test suite for runtime/json.el
|
||||
//
|
||||
// Covers: json_get (dot-path), typed extractors (int, bool, float),
|
||||
// json_array_len/get, json_set, json_build_object, json_build_array,
|
||||
// and json_escape_string.
|
||||
|
||||
test "json-get-string" {
|
||||
let obj: String = "{\"name\":\"alice\",\"role\":\"admin\"}"
|
||||
let name: String = json_get(obj, "name")
|
||||
assert name == "alice", "get string field name"
|
||||
let role: String = json_get(obj, "role")
|
||||
assert role == "admin", "get string field role"
|
||||
let missing: String = json_get(obj, "xyz")
|
||||
assert missing == "", "missing key returns empty string"
|
||||
}
|
||||
|
||||
test "json-get-int" {
|
||||
let obj: String = "{\"count\":42,\"offset\":0}"
|
||||
let count: Int = json_get_int(obj, "count")
|
||||
assert count == 42, "get int field count"
|
||||
let offset: Int = json_get_int(obj, "offset")
|
||||
assert offset == 0, "get int field offset = 0"
|
||||
}
|
||||
|
||||
test "json-get-bool" {
|
||||
let obj: String = "{\"active\":true,\"deleted\":false}"
|
||||
let active: Bool = json_get_bool(obj, "active")
|
||||
assert active, "get bool true"
|
||||
let deleted: Bool = json_get_bool(obj, "deleted")
|
||||
assert !deleted, "get bool false"
|
||||
}
|
||||
|
||||
test "json-dot-path" {
|
||||
let obj: String = "{\"user\":{\"name\":\"bob\",\"age\":30}}"
|
||||
let name: String = json_get(obj, "user.name")
|
||||
assert name == "bob", "dot-path traversal user.name"
|
||||
let age: String = json_get(obj, "user.age")
|
||||
assert age == "30", "dot-path traversal user.age"
|
||||
}
|
||||
|
||||
test "json-array-len" {
|
||||
let arr: String = "[\"a\",\"b\",\"c\"]"
|
||||
let n: Int = json_array_len(arr)
|
||||
assert n == 3, "array length 3"
|
||||
let empty_arr: String = "[]"
|
||||
let m: Int = json_array_len(empty_arr)
|
||||
assert m == 0, "empty array length 0"
|
||||
}
|
||||
|
||||
test "json-array-get" {
|
||||
let arr: String = "[\"foo\",\"bar\",\"baz\"]"
|
||||
let first: String = json_array_get_string(arr, 0)
|
||||
assert first == "foo", "first element"
|
||||
let second: String = json_array_get_string(arr, 1)
|
||||
assert second == "bar", "second element"
|
||||
let third: String = json_array_get_string(arr, 2)
|
||||
assert third == "baz", "third element"
|
||||
}
|
||||
|
||||
test "json-get-raw" {
|
||||
let obj: String = "{\"items\":[1,2,3],\"meta\":{\"page\":1}}"
|
||||
let raw_arr: String = json_get_raw(obj, "items")
|
||||
let arr_len: Int = json_array_len(raw_arr)
|
||||
assert arr_len == 3, "raw array has 3 elements"
|
||||
let raw_meta: String = json_get_raw(obj, "meta")
|
||||
let page: String = json_get(raw_meta, "page")
|
||||
assert page == "1", "page from nested raw object"
|
||||
}
|
||||
|
||||
test "json-set" {
|
||||
let obj: String = "{\"name\":\"alice\"}"
|
||||
let updated: String = json_set(obj, "name", "\"bob\"")
|
||||
let name: String = json_get(updated, "name")
|
||||
assert name == "bob", "set replaces existing key"
|
||||
}
|
||||
|
||||
test "json-build-object" {
|
||||
let kvs: [String] = native_list_empty()
|
||||
let kvs = native_list_append(kvs, "name")
|
||||
let kvs = native_list_append(kvs, "alice")
|
||||
let kvs = native_list_append(kvs, "role")
|
||||
let kvs = native_list_append(kvs, "admin")
|
||||
let obj: String = json_build_object(kvs)
|
||||
let name: String = json_get(obj, "name")
|
||||
let role: String = json_get(obj, "role")
|
||||
assert name == "alice", "built object name field"
|
||||
assert role == "admin", "built object role field"
|
||||
}
|
||||
|
||||
test "json-build-array" {
|
||||
let items: [String] = native_list_empty()
|
||||
let items = native_list_append(items, "\"alpha\"")
|
||||
let items = native_list_append(items, "\"beta\"")
|
||||
let items = native_list_append(items, "\"gamma\"")
|
||||
let arr: String = json_build_array(items)
|
||||
let n: Int = json_array_len(arr)
|
||||
assert n == 3, "built array has 3 elements"
|
||||
let first: String = json_array_get_string(arr, 0)
|
||||
assert first == "alpha", "first built element"
|
||||
let third: String = json_array_get_string(arr, 2)
|
||||
assert third == "gamma", "third built element"
|
||||
}
|
||||
|
||||
test "json-escape-string" {
|
||||
let escaped: String = json_escape_string("say \"hello\"")
|
||||
assert str_contains(escaped, "\\\""), "double quotes are escaped"
|
||||
let tab_esc: String = json_escape_string("a\tb")
|
||||
assert str_contains(tab_esc, "\\t"), "tabs are escaped"
|
||||
let plain: String = json_escape_string("no special chars")
|
||||
assert plain == "no special chars", "plain string unchanged"
|
||||
}
|
||||
|
||||
test "json-get-float" {
|
||||
let obj: String = "{\"price\":9.99,\"tax\":0.0}"
|
||||
let price: Float = json_get_float(obj, "price")
|
||||
let diff: Float = price - 9.99
|
||||
assert diff > -0.001, "price close to 9.99 low"
|
||||
assert diff < 0.001, "price close to 9.99 high"
|
||||
let tax: Float = json_get_float(obj, "tax")
|
||||
assert tax == 0.0, "tax is zero"
|
||||
}
|
||||
|
||||
test "json-nested-array-index" {
|
||||
let obj: String = "{\"tags\":[\"go\",\"el\",\"rust\"]}"
|
||||
let tags_raw: String = json_get_raw(obj, "tags")
|
||||
let count: Int = json_array_len(tags_raw)
|
||||
assert count == 3, "tags array has 3 elements"
|
||||
let first_tag: String = json_array_get_string(tags_raw, 0)
|
||||
assert first_tag == "go", "first tag is go"
|
||||
let last_tag: String = json_array_get_string(tags_raw, 2)
|
||||
assert last_tag == "rust", "last tag is rust"
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// test_math.el - native test suite for runtime/math.el
|
||||
//
|
||||
// Covers: integer math (abs, max, min), float math (sqrt, log, sin, cos, pi),
|
||||
// float conversions, format_float, and decimal_round.
|
||||
|
||||
test "el-abs" {
|
||||
let pos: Int = el_abs(5)
|
||||
assert pos == 5, "abs of positive"
|
||||
let neg: Int = el_abs(-5)
|
||||
assert neg == 5, "abs of negative"
|
||||
let zero: Int = el_abs(0)
|
||||
assert zero == 0, "abs of zero"
|
||||
let large: Int = el_abs(-1000000)
|
||||
assert large == 1000000, "abs of large negative"
|
||||
}
|
||||
|
||||
test "el-max" {
|
||||
let m: Int = el_max(3, 7)
|
||||
assert m == 7, "max of 3 and 7"
|
||||
let m2: Int = el_max(7, 3)
|
||||
assert m2 == 7, "max is commutative"
|
||||
let same: Int = el_max(5, 5)
|
||||
assert same == 5, "max of equal values"
|
||||
let neg: Int = el_max(-3, -7)
|
||||
assert neg == -3, "max of two negatives"
|
||||
}
|
||||
|
||||
test "el-min" {
|
||||
let m: Int = el_min(3, 7)
|
||||
assert m == 3, "min of 3 and 7"
|
||||
let m2: Int = el_min(7, 3)
|
||||
assert m2 == 3, "min is commutative"
|
||||
let same: Int = el_min(5, 5)
|
||||
assert same == 5, "min of equal values"
|
||||
let neg: Int = el_min(-3, -7)
|
||||
assert neg == -7, "min of two negatives"
|
||||
}
|
||||
|
||||
test "math-pi" {
|
||||
let pi: Float = math_pi()
|
||||
// pi ~ 3.14159265358979
|
||||
// Check it's between 3.141 and 3.142
|
||||
let too_low: Float = pi - 3.141
|
||||
let too_high: Float = 3.142 - pi
|
||||
assert too_low > 0.0, "pi > 3.141"
|
||||
assert too_high > 0.0, "pi < 3.142"
|
||||
}
|
||||
|
||||
test "math-sqrt" {
|
||||
let r4: Float = math_sqrt(4.0)
|
||||
let diff4: Float = r4 - 2.0
|
||||
assert diff4 == 0.0, "sqrt(4) == 2.0"
|
||||
let r9: Float = math_sqrt(9.0)
|
||||
let diff9: Float = r9 - 3.0
|
||||
assert diff9 == 0.0, "sqrt(9) == 3.0"
|
||||
let r1: Float = math_sqrt(1.0)
|
||||
let diff1: Float = r1 - 1.0
|
||||
assert diff1 == 0.0, "sqrt(1) == 1.0"
|
||||
let r0: Float = math_sqrt(0.0)
|
||||
assert r0 == 0.0, "sqrt(0) == 0.0"
|
||||
}
|
||||
|
||||
test "math-sin-cos" {
|
||||
// sin(0) == 0, cos(0) == 1
|
||||
let s0: Float = math_sin(0.0)
|
||||
assert s0 == 0.0, "sin(0) == 0.0"
|
||||
let c0: Float = math_cos(0.0)
|
||||
assert c0 == 1.0, "cos(0) == 1.0"
|
||||
// sin(pi/2) ~ 1.0, cos(pi/2) ~ 0.0
|
||||
let half_pi: Float = math_pi() / 2.0
|
||||
let s_half: Float = math_sin(half_pi)
|
||||
// Check within 0.000001 of 1.0
|
||||
let diff_s: Float = s_half - 1.0
|
||||
assert diff_s > -0.000001, "sin(pi/2) close to 1.0 low"
|
||||
assert diff_s < 0.000001, "sin(pi/2) close to 1.0 high"
|
||||
}
|
||||
|
||||
test "int-to-float-and-back" {
|
||||
let f: Float = int_to_float(42)
|
||||
let back: Int = float_to_int(f)
|
||||
assert back == 42, "42 round-trips int->float->int"
|
||||
let neg: Float = int_to_float(-7)
|
||||
let neg_back: Int = float_to_int(neg)
|
||||
assert neg_back == -7, "-7 round-trips"
|
||||
let zero: Float = int_to_float(0)
|
||||
let zero_back: Int = float_to_int(zero)
|
||||
assert zero_back == 0, "0 round-trips"
|
||||
}
|
||||
|
||||
test "float-to-int-truncates" {
|
||||
let t1: Int = float_to_int(3.9)
|
||||
assert t1 == 3, "3.9 truncates to 3"
|
||||
let t2: Int = float_to_int(3.1)
|
||||
assert t2 == 3, "3.1 truncates to 3"
|
||||
let t3: Int = float_to_int(-3.7)
|
||||
assert t3 == -3, "-3.7 truncates toward zero to -3"
|
||||
}
|
||||
|
||||
test "str-to-float-and-back" {
|
||||
let f: Float = str_to_float("3.14")
|
||||
// Check it's between 3.13 and 3.15
|
||||
let lo: Float = f - 3.13
|
||||
let hi: Float = 3.15 - f
|
||||
assert lo > 0.0, "3.14 parsed > 3.13"
|
||||
assert hi > 0.0, "3.14 parsed < 3.15"
|
||||
let zero: Float = str_to_float("0.0")
|
||||
assert zero == 0.0, "parse 0.0"
|
||||
}
|
||||
|
||||
test "format-float" {
|
||||
let s0: String = format_float(3.14159, 2)
|
||||
assert s0 == "3.14", "format to 2 decimals"
|
||||
let s1: String = format_float(1.0, 0)
|
||||
assert s1 == "1", "format to 0 decimals"
|
||||
let s2: String = format_float(0.0, 3)
|
||||
assert s2 == "0.000", "format zero to 3 decimals"
|
||||
let s3: String = format_float(-2.5, 1)
|
||||
assert s3 == "-2.5", "format negative to 1 decimal"
|
||||
}
|
||||
|
||||
test "decimal-round" {
|
||||
let r0: Float = decimal_round(2.5, 0)
|
||||
assert r0 == 3.0, "round 2.5 to 0 places"
|
||||
let r1: Float = decimal_round(2.45, 1)
|
||||
assert r1 == 2.5, "round 2.45 to 1 place"
|
||||
let neg: Float = decimal_round(-2.5, 0)
|
||||
assert neg == -3.0, "round -2.5 to 0 places (half-away-from-zero)"
|
||||
let exact: Float = decimal_round(1.0, 2)
|
||||
assert exact == 1.0, "rounding exact value unchanged"
|
||||
}
|
||||
|
||||
test "math-log" {
|
||||
// log10(100) == 2
|
||||
let l100: Float = math_log(100.0)
|
||||
let diff: Float = l100 - 2.0
|
||||
assert diff > -0.000001, "log10(100) close to 2 low"
|
||||
assert diff < 0.000001, "log10(100) close to 2 high"
|
||||
// log10(1) == 0
|
||||
let l1: Float = math_log(1.0)
|
||||
assert l1 == 0.0, "log10(1) == 0"
|
||||
}
|
||||
|
||||
test "math-ln" {
|
||||
// ln(1) == 0
|
||||
let l1: Float = math_ln(1.0)
|
||||
assert l1 == 0.0, "ln(1) == 0"
|
||||
// ln(e) ~ 1.0 — e ~ 2.71828
|
||||
let e: Float = 2.718281828
|
||||
let le: Float = math_ln(e)
|
||||
let diff: Float = le - 1.0
|
||||
assert diff > -0.000001, "ln(e) close to 1 low"
|
||||
assert diff < 0.000001, "ln(e) close to 1 high"
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// test_state.el - native test suite for runtime/state.el
|
||||
//
|
||||
// Covers: state_set/get/del, state_has, state_get_or, state_keys,
|
||||
// and edge cases such as empty values, overwrite, and multiple keys.
|
||||
|
||||
test "state-set-get-basic" {
|
||||
state_set("test_key", "hello")
|
||||
let v: String = state_get("test_key")
|
||||
assert v == "hello", "get returns set value"
|
||||
}
|
||||
|
||||
test "state-get-missing" {
|
||||
let v: String = state_get("__nonexistent_key_xyz__")
|
||||
assert v == "", "missing key returns empty string"
|
||||
}
|
||||
|
||||
test "state-overwrite" {
|
||||
state_set("ow_key", "first")
|
||||
state_set("ow_key", "second")
|
||||
let v: String = state_get("ow_key")
|
||||
assert v == "second", "second write overwrites first"
|
||||
}
|
||||
|
||||
test "state-del" {
|
||||
state_set("del_key", "to be deleted")
|
||||
state_del("del_key")
|
||||
let v: String = state_get("del_key")
|
||||
assert v == "", "deleted key returns empty string"
|
||||
}
|
||||
|
||||
test "state-del-nonexistent" {
|
||||
// Should not panic or error on deleting a non-existent key.
|
||||
state_del("__never_set_key__")
|
||||
let v: String = state_get("__never_set_key__")
|
||||
assert v == "", "del of nonexistent key is a no-op"
|
||||
}
|
||||
|
||||
test "state-has" {
|
||||
state_set("has_key", "value")
|
||||
assert state_has("has_key"), "has returns true for set key"
|
||||
assert !state_has("__no_has_key__"), "has returns false for absent key"
|
||||
state_del("has_key")
|
||||
assert !state_has("has_key"), "has returns false after del"
|
||||
}
|
||||
|
||||
test "state-get-or" {
|
||||
state_set("gor_key", "actual")
|
||||
let v1: String = state_get_or("gor_key", "default")
|
||||
assert v1 == "actual", "get_or returns value when key set"
|
||||
let v2: String = state_get_or("__absent_gor_key__", "fallback")
|
||||
assert v2 == "fallback", "get_or returns default when key absent"
|
||||
}
|
||||
|
||||
test "state-multiple-keys" {
|
||||
state_set("mk_a", "alpha")
|
||||
state_set("mk_b", "beta")
|
||||
state_set("mk_c", "gamma")
|
||||
let a: String = state_get("mk_a")
|
||||
let b: String = state_get("mk_b")
|
||||
let c: String = state_get("mk_c")
|
||||
assert a == "alpha", "key a correct"
|
||||
assert b == "beta", "key b correct"
|
||||
assert c == "gamma", "key c correct"
|
||||
state_del("mk_a")
|
||||
state_del("mk_b")
|
||||
state_del("mk_c")
|
||||
}
|
||||
|
||||
test "state-keys-returns-json-array" {
|
||||
state_set("keys_test_1", "v1")
|
||||
state_set("keys_test_2", "v2")
|
||||
let ks: String = state_keys()
|
||||
// The result is a JSON array string like ["keys_test_1","keys_test_2",...]
|
||||
assert str_starts_with(ks, "["), "state_keys returns JSON array"
|
||||
assert str_ends_with(ks, "]"), "state_keys JSON array is closed"
|
||||
assert str_contains(ks, "keys_test_1"), "keys array contains keys_test_1"
|
||||
assert str_contains(ks, "keys_test_2"), "keys array contains keys_test_2"
|
||||
state_del("keys_test_1")
|
||||
state_del("keys_test_2")
|
||||
}
|
||||
|
||||
test "state-numeric-value-as-string" {
|
||||
state_set("num_key", "42")
|
||||
let v: String = state_get("num_key")
|
||||
let n: Int = str_to_int(v)
|
||||
assert n == 42, "stored numeric string round-trips to int"
|
||||
state_del("num_key")
|
||||
}
|
||||
|
||||
test "state-long-value" {
|
||||
let long_val: String = str_repeat("abcdefghij", 100)
|
||||
state_set("long_val_key", long_val)
|
||||
let got: String = state_get("long_val_key")
|
||||
assert got == long_val, "long value round-trips correctly"
|
||||
let got_len: Int = str_len(got)
|
||||
assert got_len == 1000, "long value is 1000 bytes"
|
||||
state_del("long_val_key")
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// test_string.el - native test suite for runtime/string.el
|
||||
//
|
||||
// Covers: type conversions, core primitives, comparison and search,
|
||||
// case conversion, whitespace trimming, replacement, repetition,
|
||||
// reversal, prefix/suffix stripping, padding, counting, character
|
||||
// classification, splitting, and joining.
|
||||
|
||||
test "int-to-str" {
|
||||
let s: String = int_to_str(42)
|
||||
assert s == "42", "int 42 to string"
|
||||
let neg: String = int_to_str(-7)
|
||||
assert neg == "-7", "negative int to string"
|
||||
let zero: String = int_to_str(0)
|
||||
assert zero == "0", "zero to string"
|
||||
}
|
||||
|
||||
test "str-to-int" {
|
||||
let n: Int = str_to_int("123")
|
||||
assert n == 123, "parse 123"
|
||||
let neg: Int = str_to_int("-5")
|
||||
assert neg == -5, "parse negative"
|
||||
let zero: Int = str_to_int("0")
|
||||
assert zero == 0, "parse zero"
|
||||
}
|
||||
|
||||
test "bool-to-str" {
|
||||
let t: String = bool_to_str(true)
|
||||
assert t == "true", "true to string"
|
||||
let f: String = bool_to_str(false)
|
||||
assert f == "false", "false to string"
|
||||
}
|
||||
|
||||
test "str-len" {
|
||||
let n: Int = str_len("hello")
|
||||
assert n == 5, "length of hello"
|
||||
let e: Int = str_len("")
|
||||
assert e == 0, "length of empty string"
|
||||
let space: Int = str_len("a b")
|
||||
assert space == 3, "length with spaces"
|
||||
}
|
||||
|
||||
test "str-eq" {
|
||||
assert str_eq("abc", "abc"), "identical strings are equal"
|
||||
assert !str_eq("abc", "ABC"), "case-sensitive comparison"
|
||||
assert str_eq("", ""), "empty strings are equal"
|
||||
assert !str_eq("a", "b"), "different single chars"
|
||||
}
|
||||
|
||||
test "str-slice" {
|
||||
let s: String = str_slice("hello world", 6, 11)
|
||||
assert s == "world", "slice end of string"
|
||||
let start: String = str_slice("hello world", 0, 5)
|
||||
assert start == "hello", "slice start of string"
|
||||
let empty: String = str_slice("hello", 2, 2)
|
||||
assert empty == "", "zero-length slice"
|
||||
}
|
||||
|
||||
test "str-starts-ends-with" {
|
||||
assert str_starts_with("hello world", "hello"), "starts with hello"
|
||||
assert str_ends_with("hello world", "world"), "ends with world"
|
||||
assert !str_starts_with("hello world", "world"), "does not start with world"
|
||||
assert !str_ends_with("hello world", "hello"), "does not end with hello"
|
||||
assert str_starts_with("abc", ""), "empty prefix always matches"
|
||||
assert str_ends_with("abc", ""), "empty suffix always matches"
|
||||
}
|
||||
|
||||
test "str-contains" {
|
||||
assert str_contains("hello world", "world"), "contains world"
|
||||
assert str_contains("hello world", "lo wo"), "contains interior substring"
|
||||
assert !str_contains("hello world", "xyz"), "does not contain xyz"
|
||||
assert str_contains("abc", ""), "empty sub is always contained"
|
||||
assert !str_contains("", "x"), "empty string does not contain nonempty sub"
|
||||
}
|
||||
|
||||
test "str-index-of" {
|
||||
let i: Int = str_index_of("hello world", "world")
|
||||
assert i == 6, "index of world"
|
||||
let j: Int = str_index_of("hello world", "xyz")
|
||||
assert j == -1, "not found returns -1"
|
||||
let k: Int = str_index_of("aabbcc", "bb")
|
||||
assert k == 2, "index of bb in aabbcc"
|
||||
}
|
||||
|
||||
test "str-last-index-of" {
|
||||
let i: Int = str_last_index_of("abcabc", "bc")
|
||||
assert i == 4, "last occurrence of bc"
|
||||
let j: Int = str_last_index_of("hello", "xyz")
|
||||
assert j == -1, "not found returns -1"
|
||||
let k: Int = str_last_index_of("aaa", "a")
|
||||
assert k == 2, "last single-char match"
|
||||
}
|
||||
|
||||
test "str-to-upper-lower" {
|
||||
let up: String = str_to_upper("hello")
|
||||
assert up == "HELLO", "to upper"
|
||||
let lo: String = str_to_lower("WORLD")
|
||||
assert lo == "world", "to lower"
|
||||
let mixed: String = str_to_upper("Hello World")
|
||||
assert mixed == "HELLO WORLD", "mixed to upper"
|
||||
let empty: String = str_to_lower("")
|
||||
assert empty == "", "empty stays empty"
|
||||
}
|
||||
|
||||
test "str-trim" {
|
||||
let s: String = str_trim(" hello ")
|
||||
assert s == "hello", "trim both ends"
|
||||
let lonly: String = str_trim(" hello")
|
||||
assert lonly == "hello", "trim left only"
|
||||
let ronly: String = str_trim("hello ")
|
||||
assert ronly == "hello", "trim right only"
|
||||
let tabs: String = str_trim("\thello\n")
|
||||
assert tabs == "hello", "trim tabs and newlines"
|
||||
let empty: String = str_trim(" ")
|
||||
assert empty == "", "all whitespace trims to empty"
|
||||
}
|
||||
|
||||
test "str-replace" {
|
||||
let s: String = str_replace("hello world", "world", "there")
|
||||
assert s == "hello there", "replace word"
|
||||
let none: String = str_replace("hello", "xyz", "abc")
|
||||
assert none == "hello", "no match leaves string unchanged"
|
||||
let multi: String = str_replace("aaa", "a", "b")
|
||||
assert multi == "bbb", "replace all occurrences"
|
||||
let empty_from: String = str_replace("hello", "", "x")
|
||||
assert empty_from == "hello", "empty from returns original"
|
||||
}
|
||||
|
||||
test "str-repeat" {
|
||||
let s: String = str_repeat("ab", 3)
|
||||
assert s == "ababab", "repeat 3 times"
|
||||
let once: String = str_repeat("x", 1)
|
||||
assert once == "x", "repeat once"
|
||||
let zero: String = str_repeat("abc", 0)
|
||||
assert zero == "", "repeat zero times"
|
||||
let neg: String = str_repeat("abc", -1)
|
||||
assert neg == "", "negative repeat is empty"
|
||||
}
|
||||
|
||||
test "str-reverse" {
|
||||
let s: String = str_reverse("hello")
|
||||
assert s == "olleh", "reverse hello"
|
||||
let single: String = str_reverse("a")
|
||||
assert single == "a", "reverse single char"
|
||||
let empty: String = str_reverse("")
|
||||
assert empty == "", "reverse empty string"
|
||||
let palindrome: String = str_reverse("racecar")
|
||||
assert palindrome == "racecar", "reverse palindrome"
|
||||
}
|
||||
|
||||
test "str-strip-prefix-suffix" {
|
||||
let p: String = str_strip_prefix("foobar", "foo")
|
||||
assert p == "bar", "strip prefix foo"
|
||||
let no_prefix: String = str_strip_prefix("foobar", "baz")
|
||||
assert no_prefix == "foobar", "no match leaves string unchanged"
|
||||
let s: String = str_strip_suffix("hello.md", ".md")
|
||||
assert s == "hello", "strip suffix .md"
|
||||
let no_suffix: String = str_strip_suffix("hello.md", ".txt")
|
||||
assert no_suffix == "hello.md", "non-matching suffix unchanged"
|
||||
}
|
||||
|
||||
test "str-strip-chars" {
|
||||
let s: String = str_strip_chars(" \thello \n", " \t\n")
|
||||
assert s == "hello", "strip whitespace chars"
|
||||
let dots: String = str_strip_chars("...hello...", ".")
|
||||
assert dots == "hello", "strip dot chars"
|
||||
let all: String = str_strip_chars("aaa", "a")
|
||||
assert all == "", "strip all chars leaves empty"
|
||||
}
|
||||
|
||||
test "str-pad-left-right" {
|
||||
let l: String = str_pad_left("42", 5, "0")
|
||||
assert l == "00042", "zero-pad left to width 5"
|
||||
let r: String = str_pad_right("hi", 5, "-")
|
||||
assert r == "hi---", "dash-pad right to width 5"
|
||||
let no_pad: String = str_pad_left("hello", 3, "x")
|
||||
assert no_pad == "hello", "no pad when string already wide enough"
|
||||
}
|
||||
|
||||
test "str-count" {
|
||||
let n: Int = str_count("abc abc abc", "abc")
|
||||
assert n == 3, "count three occurrences"
|
||||
let overlap: Int = str_count("aaa", "aa")
|
||||
assert overlap == 1, "non-overlapping count"
|
||||
let zero: Int = str_count("hello", "xyz")
|
||||
assert zero == 0, "not found gives 0"
|
||||
let empty_sub: Int = str_count("hello", "")
|
||||
assert empty_sub == 0, "empty sub gives 0"
|
||||
}
|
||||
|
||||
test "str-count-lines-words-letters" {
|
||||
let s: String = "hello world\nfoo bar"
|
||||
let lines: Int = str_count_lines(s)
|
||||
let words: Int = str_count_words(s)
|
||||
let letters: Int = str_count_letters(s)
|
||||
assert lines == 2, "line count"
|
||||
assert words == 4, "word count"
|
||||
assert letters == 16, "letter count"
|
||||
}
|
||||
|
||||
test "str-count-chars-and-digits" {
|
||||
let digits: Int = str_count_digits("abc123def456")
|
||||
assert digits == 6, "six digits"
|
||||
let none: Int = str_count_digits("hello")
|
||||
assert none == 0, "no digits"
|
||||
let chars: Int = str_count_chars("hello")
|
||||
assert chars == 5, "five ASCII chars"
|
||||
}
|
||||
|
||||
test "char-classes" {
|
||||
assert is_letter("A"), "A is a letter"
|
||||
assert is_digit("7"), "7 is a digit"
|
||||
assert is_whitespace(" "), "space is whitespace"
|
||||
assert !is_letter("3"), "3 is not a letter"
|
||||
assert !is_digit("X"), "X is not a digit"
|
||||
assert is_alphanumeric("abc123"), "abc123 is alphanumeric"
|
||||
assert !is_alphanumeric("abc!"), "abc! is not alphanumeric"
|
||||
assert is_uppercase("ABC"), "ABC is uppercase"
|
||||
assert is_lowercase("abc"), "abc is lowercase"
|
||||
assert !is_uppercase("Abc"), "mixed is not uppercase"
|
||||
}
|
||||
|
||||
test "str-split" {
|
||||
let parts: [String] = str_split("a,b,c", ",")
|
||||
let n: Int = native_list_len(parts)
|
||||
assert n == 3, "split into 3 parts"
|
||||
let p0: String = native_list_get(parts, 0)
|
||||
let p1: String = native_list_get(parts, 1)
|
||||
let p2: String = native_list_get(parts, 2)
|
||||
assert p0 == "a", "first part"
|
||||
assert p1 == "b", "second part"
|
||||
assert p2 == "c", "third part"
|
||||
}
|
||||
|
||||
test "str-split-lines" {
|
||||
let lines: [String] = str_split_lines("alpha\nbeta\r\ngamma\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
assert n == 3, "split into 3 lines"
|
||||
let l0: String = native_list_get(lines, 0)
|
||||
let l1: String = native_list_get(lines, 1)
|
||||
let l2: String = native_list_get(lines, 2)
|
||||
assert l0 == "alpha", "first line"
|
||||
assert l1 == "beta", "second line strips CR"
|
||||
assert l2 == "gamma", "third line"
|
||||
}
|
||||
|
||||
test "str-join" {
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "alpha")
|
||||
let parts = native_list_append(parts, "beta")
|
||||
let parts = native_list_append(parts, "gamma")
|
||||
let result: String = str_join(parts, ", ")
|
||||
assert result == "alpha, beta, gamma", "join with separator"
|
||||
let empty_parts: [String] = native_list_empty()
|
||||
let empty_result: String = str_join(empty_parts, ",")
|
||||
assert empty_result == "", "join empty list gives empty string"
|
||||
}
|
||||
|
||||
test "str-char-at" {
|
||||
let c: String = str_char_at("hello", 1)
|
||||
assert c == "e", "char at index 1"
|
||||
let first: String = str_char_at("abc", 0)
|
||||
assert first == "a", "first char"
|
||||
let oob: String = str_char_at("abc", 10)
|
||||
assert oob == "", "out of bounds returns empty"
|
||||
}
|
||||
|
||||
test "url-encode-decode" {
|
||||
let encoded: String = url_encode("hello world")
|
||||
assert !str_contains(encoded, " "), "space is encoded"
|
||||
let decoded: String = url_decode(encoded)
|
||||
assert decoded == "hello world", "round-trip decode"
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// test_text.el - native test suite for text primitives.
|
||||
//
|
||||
// Mirrors the acceptance corpus in tests/text/examples/ using the
|
||||
// native test/assert system instead of run.sh + expected output files.
|
||||
|
||||
test "count-substring" {
|
||||
let x: Int = str_count("abc abc abc", "abc")
|
||||
assert x == 3
|
||||
}
|
||||
|
||||
test "count-overlap-skip" {
|
||||
let x: Int = str_count("aaa", "aa")
|
||||
assert x == 1, "non-overlapping count of aa in aaa"
|
||||
}
|
||||
|
||||
test "count-lines-words-letters" {
|
||||
let s: String = "hello world\nfoo bar"
|
||||
let lines: Int = str_count_lines(s)
|
||||
let words: Int = str_count_words(s)
|
||||
let letters: Int = str_count_letters(s)
|
||||
assert lines == 2, "line count"
|
||||
assert words == 4, "word count"
|
||||
assert letters == 16, "letter count"
|
||||
}
|
||||
|
||||
test "index-of-all" {
|
||||
let positions: [Int] = str_index_of_all("abXcdXefX", "X")
|
||||
let n: Int = native_list_len(positions)
|
||||
assert n == 3, "should find 3 occurrences"
|
||||
let p0: Int = native_list_get(positions, 0)
|
||||
let p1: Int = native_list_get(positions, 1)
|
||||
let p2: Int = native_list_get(positions, 2)
|
||||
assert p0 == 2, "first X at index 2"
|
||||
assert p1 == 5, "second X at index 5"
|
||||
assert p2 == 8, "third X at index 8"
|
||||
}
|
||||
|
||||
test "str-repeat" {
|
||||
let s: String = str_repeat("ab", 3)
|
||||
assert s == "ababab", "repeat 3 times"
|
||||
}
|
||||
|
||||
test "str-reverse" {
|
||||
let s: String = str_reverse("hello")
|
||||
assert s == "olleh", "reverse hello"
|
||||
}
|
||||
|
||||
test "str-strip-prefix" {
|
||||
let s: String = str_strip_prefix("foobar", "foo")
|
||||
assert s == "bar", "strip prefix foo"
|
||||
}
|
||||
|
||||
test "str-strip-suffix" {
|
||||
let s: String = str_strip_suffix("hello.md", ".md")
|
||||
assert s == "hello", "strip suffix .md"
|
||||
}
|
||||
|
||||
test "str-strip-chars" {
|
||||
let s: String = str_strip_chars(" \thello \n", " \t\n")
|
||||
assert s == "hello", "strip whitespace chars"
|
||||
}
|
||||
|
||||
test "split-lines" {
|
||||
let lines: [String] = str_split_lines("alpha\nbeta\r\ngamma\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
assert n == 3, "split into 3 lines"
|
||||
}
|
||||
|
||||
test "str-join" {
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "alpha")
|
||||
let parts = native_list_append(parts, "beta")
|
||||
let parts = native_list_append(parts, "gamma")
|
||||
let result: String = str_join(parts, ", ")
|
||||
assert result == "alpha, beta, gamma", "join with separator"
|
||||
}
|
||||
|
||||
test "char-classes" {
|
||||
assert is_letter("A"), "A is a letter"
|
||||
assert is_digit("7"), "7 is a digit"
|
||||
assert is_whitespace(" "), "space is whitespace"
|
||||
assert !is_letter("3"), "3 is not a letter"
|
||||
assert !is_digit("X"), "X is not a digit"
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// test_time.el - native test suite for runtime/time.el
|
||||
//
|
||||
// Covers: time_now (positive timestamp), time_to_parts (UTC decomposition),
|
||||
// time_format (ISO 8601 and strftime tokens), time_add/diff, unix_timestamp,
|
||||
// duration helpers, and instant conversions.
|
||||
|
||||
test "time-now-positive" {
|
||||
let ts: Int = time_now()
|
||||
// time_now() returns milliseconds since epoch.
|
||||
// Any value greater than 1_700_000_000_000 (Nov 2023) is valid in 2025+.
|
||||
assert ts > 1700000000000, "time_now returns a reasonable recent timestamp"
|
||||
}
|
||||
|
||||
test "unix-timestamp-positive" {
|
||||
let s: Int = unix_timestamp()
|
||||
// Should be greater than 1_700_000_000 (seconds, Nov 2023)
|
||||
assert s > 1700000000, "unix_timestamp returns seconds > 1700000000"
|
||||
}
|
||||
|
||||
test "now-ns-positive" {
|
||||
let ns: Int = now_ns()
|
||||
// Should be a large nanosecond value
|
||||
assert ns > 0, "now_ns returns positive value"
|
||||
}
|
||||
|
||||
test "time-to-parts-epoch" {
|
||||
// Unix epoch: 0 ms = 1970-01-01T00:00:00.000Z
|
||||
let parts: String = time_to_parts(0)
|
||||
let year: String = json_get(parts, "year")
|
||||
let month: String = json_get(parts, "month")
|
||||
let day: String = json_get(parts, "day")
|
||||
let hour: String = json_get(parts, "hour")
|
||||
assert year == "1970", "epoch year is 1970"
|
||||
assert month == "1", "epoch month is 1"
|
||||
assert day == "1", "epoch day is 1"
|
||||
assert hour == "0", "epoch hour is 0"
|
||||
}
|
||||
|
||||
test "time-to-parts-known-date" {
|
||||
// 2024-03-15T12:30:45.000Z
|
||||
// seconds = 2024-03-15 12:30:45 UTC
|
||||
// epoch ms: use a known value
|
||||
// 2024-03-15 00:00:00 UTC = 1710460800 seconds
|
||||
// + 12*3600 + 30*60 + 45 = 43200 + 1800 + 45 = 45045 seconds
|
||||
// total: 1710505845 seconds = 1710505845000 ms
|
||||
let ts: Int = 1710505845000
|
||||
let parts: String = time_to_parts(ts)
|
||||
let year: String = json_get(parts, "year")
|
||||
let month: String = json_get(parts, "month")
|
||||
let day: String = json_get(parts, "day")
|
||||
let hour: String = json_get(parts, "hour")
|
||||
let minute: String = json_get(parts, "minute")
|
||||
let second: String = json_get(parts, "second")
|
||||
assert year == "2024", "year is 2024"
|
||||
assert month == "3", "month is 3 (March)"
|
||||
assert day == "15", "day is 15"
|
||||
assert hour == "12", "hour is 12"
|
||||
assert minute == "30", "minute is 30"
|
||||
assert second == "45", "second is 45"
|
||||
}
|
||||
|
||||
test "time-format-iso" {
|
||||
// Epoch = 1970-01-01T00:00:00.000Z
|
||||
let formatted: String = time_format(0, "ISO")
|
||||
assert formatted == "1970-01-01T00:00:00.000Z", "epoch formats to ISO correctly"
|
||||
}
|
||||
|
||||
test "time-format-iso-empty-fmt" {
|
||||
// Empty format string should also produce ISO 8601
|
||||
let formatted: String = time_format(0, "")
|
||||
assert formatted == "1970-01-01T00:00:00.000Z", "empty fmt produces ISO"
|
||||
}
|
||||
|
||||
test "time-format-strftime" {
|
||||
// 2024-03-15T12:30:45.000Z
|
||||
let ts: Int = 1710505845000
|
||||
let fmt: String = time_format(ts, "%Y-%m-%d")
|
||||
assert fmt == "2024-03-15", "strftime %Y-%m-%d"
|
||||
let hms: String = time_format(ts, "%H:%M:%S")
|
||||
assert hms == "12:30:45", "strftime %H:%M:%S"
|
||||
}
|
||||
|
||||
test "time-add-milliseconds" {
|
||||
let base: Int = 1000000
|
||||
let result: Int = time_add(base, 500, "ms")
|
||||
assert result == 1000500, "add 500 ms"
|
||||
}
|
||||
|
||||
test "time-add-seconds" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 60, "sec")
|
||||
assert result == 60000, "add 60 seconds = 60000 ms"
|
||||
}
|
||||
|
||||
test "time-add-minutes" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 2, "min")
|
||||
assert result == 120000, "add 2 minutes = 120000 ms"
|
||||
}
|
||||
|
||||
test "time-add-hours" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 1, "hour")
|
||||
assert result == 3600000, "add 1 hour = 3600000 ms"
|
||||
}
|
||||
|
||||
test "time-add-days" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 1, "day")
|
||||
assert result == 86400000, "add 1 day = 86400000 ms"
|
||||
}
|
||||
|
||||
test "time-diff-seconds" {
|
||||
let t1: Int = 0
|
||||
let t2: Int = 90000
|
||||
let d: Int = time_diff(t1, t2, "sec")
|
||||
assert d == 90, "diff 90000 ms = 90 seconds"
|
||||
}
|
||||
|
||||
test "time-diff-negative" {
|
||||
let t1: Int = 5000
|
||||
let t2: Int = 2000
|
||||
let d: Int = time_diff(t1, t2, "sec")
|
||||
assert d == -3, "negative diff when t2 < t1"
|
||||
}
|
||||
|
||||
test "duration-helpers" {
|
||||
let d_secs: Int = duration_seconds(5)
|
||||
assert d_secs == 5000000000, "5 seconds in nanoseconds"
|
||||
let d_ms: Int = duration_millis(100)
|
||||
assert d_ms == 100000000, "100 ms in nanoseconds"
|
||||
let d_ns: Int = duration_nanos(42)
|
||||
assert d_ns == 42, "42 nanos is identity"
|
||||
let back_secs: Int = duration_to_seconds(d_secs)
|
||||
assert back_secs == 5, "convert back to seconds"
|
||||
let back_ms: Int = duration_to_millis(d_ms)
|
||||
assert back_ms == 100, "convert back to milliseconds"
|
||||
}
|
||||
|
||||
test "instant-conversions" {
|
||||
let inst: Int = unix_millis(1000)
|
||||
assert inst == 1000000000, "unix_millis(1000) in nanoseconds"
|
||||
let inst_secs: Int = unix_seconds(2)
|
||||
assert inst_secs == 2000000000, "unix_seconds(2) in nanoseconds"
|
||||
let back_ms: Int = instant_to_unix_millis(inst)
|
||||
assert back_ms == 1000, "instant to unix millis"
|
||||
let back_secs: Int = instant_to_unix_seconds(inst_secs)
|
||||
assert back_secs == 2, "instant to unix seconds"
|
||||
}
|
||||
|
||||
test "time-from-parts" {
|
||||
// time_from_parts(secs, ns, tz) -> secs*1000 + ns/1_000_000
|
||||
let ts: Int = time_from_parts(1000, 500000000, "UTC")
|
||||
assert ts == 1000500, "time_from_parts: 1000 secs + 500ms"
|
||||
}
|
||||
Reference in New Issue
Block a user