Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8b22e16a2 | |||
| 3086a56b5c | |||
| 7c644b8d89 | |||
| 9e8d23bcd9 | |||
| 0791fda43e | |||
| 53f2df500d | |||
| 245eb2898e | |||
| 7bfe30b767 | |||
| 6ede9e4379 | |||
| 4ae42ee7db | |||
| 3e5130e98d | |||
| beb4e436e1 | |||
| 71a1e41f93 | |||
| 0676725cb7 | |||
| d98a968f89 | |||
| 0c9154551f | |||
| 9aa0c49d0c | |||
| 59b96e3324 | |||
| 287b39f8e6 | |||
| c45744d8ca | |||
| 2e778ca664 | |||
| 641227a7d3 | |||
| 805318e2d0 | |||
| cefff5b891 | |||
| ce9a2caff4 | |||
| d1af4b0f8b | |||
| 282df712a8 | |||
| cfcedff7f4 | |||
| eab483ed4f | |||
| cfa4301026 | |||
| f271f9d9d8 | |||
| 49a8a1c24b | |||
| 252ad04c96 | |||
| 3cc9b1cc3d | |||
| 5678745381 | |||
| e853f4a25c | |||
| 5b6915ec9e | |||
| b0e38a245a | |||
| 84b5355fce | |||
| b547d1daf8 | |||
| cc6522c69d | |||
| 5807de835e | |||
| 33af4ed09e | |||
| f2c63f95fd | |||
| 01849c2033 | |||
| 56724325ed | |||
| fea830cca2 |
@@ -1,4 +1,4 @@
|
||||
name: El CI -dev
|
||||
name: El SDK CI — dev
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,7 +7,6 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
@@ -23,24 +22,20 @@ jobs:
|
||||
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 \
|
||||
-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)
|
||||
- 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
|
||||
@@ -48,50 +43,37 @@ jobs:
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-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
|
||||
- name: Run tests — text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests -calendar
|
||||
- name: Run tests — calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests -time
|
||||
- name: Run tests — time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests -html_sanitizer
|
||||
- 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)
|
||||
- name: Publish El SDK to Artifact Registry (dev)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
@@ -104,6 +86,7 @@ jobs:
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
@@ -112,6 +95,21 @@ jobs:
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
# Also tag as latest-dev
|
||||
echo "Published elc version=${VERSION} to foundation-dev/el/elc"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-dev"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: El CI — stage
|
||||
name: El SDK CI — stage
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -16,6 +16,16 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enforce source branch (stage ← dev only)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
SOURCE="${GITHUB_HEAD_REF}"
|
||||
if [ "${SOURCE}" != "dev" ]; then
|
||||
echo "ERROR: Stage branch only accepts PRs from 'dev'. Source was: '${SOURCE}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "Source branch check passed: ${SOURCE} → stage"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
@@ -49,7 +59,6 @@ jobs:
|
||||
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" \
|
||||
@@ -74,8 +83,7 @@ jobs:
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Publish artifact to GCP Artifact Registry (stage)
|
||||
- name: Publish elc to Artifact Registry (stage)
|
||||
- name: Publish El SDK to Artifact Registry (stage)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
@@ -88,6 +96,7 @@ jobs:
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
@@ -96,5 +105,21 @@ jobs:
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
echo "Published elc version=${VERSION} to foundation-stage/el/elc"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-stage"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
@@ -13,6 +13,16 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enforce source branch (main ← stage only)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
SOURCE="${GITHUB_HEAD_REF}"
|
||||
if [ "${SOURCE}" != "stage" ]; then
|
||||
echo "ERROR: Main branch only accepts PRs from 'stage'. Source was: '${SOURCE}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "Source branch check passed: ${SOURCE} → main"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
@@ -128,9 +138,8 @@ 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)
|
||||
# Publish artifacts to GCP Artifact Registry (prod)
|
||||
- name: Publish El SDK to Artifact Registry (prod)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
@@ -143,6 +152,7 @@ jobs:
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
@@ -151,9 +161,26 @@ jobs:
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
echo "Published elc version=${VERSION} to foundation-prod/el/elc"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-prod"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
# Dispatch el-sdk-updated event to downstream repos
|
||||
- name: Dispatch to foundation/engram
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
@@ -189,3 +216,75 @@ jobs:
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/forge"
|
||||
|
||||
- name: Dispatch to neuron-technologies/el-ui
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/el-ui/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/el-ui"
|
||||
|
||||
- name: Dispatch to neuron-technologies/elp
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/elp/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/elp"
|
||||
|
||||
- name: Dispatch to neuron-technologies/elql
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/elql/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/elql"
|
||||
|
||||
- name: Dispatch to neuron-technologies/el-ide
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/el-ide/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/el-ide"
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# El Language — Agent Guide
|
||||
|
||||
El is a self-hosting, statically-typed language that compiles to C. This file orients agents that work on El itself or on programs written in El.
|
||||
|
||||
---
|
||||
|
||||
## What El Is
|
||||
|
||||
El compiles `.el` source → C → native binary. Every El value is `el_val_t` (int64_t). Strings are heap pointers cast through int64_t. The compiler is written in El (self-hosting).
|
||||
|
||||
**The compiler pipeline:**
|
||||
```
|
||||
elc-cli.el
|
||||
└─ imports: compiler.el
|
||||
└─ imports: lexer.el, parser.el, codegen.el, codegen-js.el
|
||||
```
|
||||
|
||||
The canonical compiler binary is `dist/platform/elc`. It was produced by running an earlier version of itself on `elc-cli.el`.
|
||||
|
||||
---
|
||||
|
||||
## The Two Layers — Know Which One You're In
|
||||
|
||||
### Layer 1: El programs (`.el` files)
|
||||
|
||||
This is where almost all work belongs. El programs are source files that get compiled by `elc`. New library functions, application logic, and language-level utilities all go here as `.el` files.
|
||||
|
||||
**Do not add C code when El can express it.** If functionality can be built from existing El primitives (string ops, `exec`, `fs_read/write`, `http_post`, etc.), write it in El.
|
||||
|
||||
### Layer 2: The C seed (`el-compiler/runtime/el_seed.c`)
|
||||
|
||||
This is the self-contained C OS-boundary layer. It provides the `__`-prefixed primitives that compiled El programs call: libcurl HTTP, pthreads, filesystem I/O, arena allocation, etc. It is **not generated** — it is maintained by hand.
|
||||
|
||||
The old `el_runtime.c` has been archived to `el-compiler/runtime/legacy/`. The runtime is now native El (`runtime/*.el`). `el_seed.c` replaces `el_runtime.c` as the sole C compilation dependency.
|
||||
|
||||
**Only edit `el_seed.c` when you genuinely need OS-level access** (raw sockets, GPU calls, new libcurl features). For everything else, write El.
|
||||
|
||||
When you do add a C builtin:
|
||||
1. Add the C function to `el_seed.c`
|
||||
2. Declare it in `el_seed.h`
|
||||
3. Add it to the `builtin_arity` table in `el-compiler/src/codegen.el` (so the compiler knows the arg count)
|
||||
4. Rebuild the elc binary (see below)
|
||||
|
||||
---
|
||||
|
||||
## Rebuilding the Compiler
|
||||
|
||||
After changing any `.el` source in `el-compiler/src/`:
|
||||
|
||||
```bash
|
||||
cd /Users/will/Development/neuron-technologies/foundation/el
|
||||
./dist/platform/elc elc-cli.el > elc-new.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o dist/platform/elc-new \
|
||||
elc-new.c el-compiler/runtime/el_seed.c
|
||||
# Verify self-hosting:
|
||||
./dist/platform/elc-new elc-cli.el > elc-verify.c
|
||||
diff elc-new.c elc-verify.c # should be identical
|
||||
mv dist/platform/elc-new dist/platform/elc
|
||||
```
|
||||
|
||||
After changing `el_seed.c` only (no El source changes), rebuild downstream programs but do NOT need to rebuild the compiler binary itself — the seed is linked at the application level, not the compiler level.
|
||||
|
||||
---
|
||||
|
||||
## How El Programs Are Built
|
||||
|
||||
Each El application has a `build.sh` that:
|
||||
1. Concatenates all `.el` source files (stripping `import` lines)
|
||||
2. Runs `elc` to produce a `.c` file
|
||||
3. Runs `cc` linking against `el_seed.c`
|
||||
|
||||
Example (cgi-studio daemon):
|
||||
```bash
|
||||
cd products/cgi-studio/el-daemon
|
||||
./build.sh
|
||||
```
|
||||
|
||||
When you add a new `.el` file to an application, add it to that application's `build.sh` concat list.
|
||||
|
||||
---
|
||||
|
||||
## Parallelism in El
|
||||
|
||||
El is single-threaded at the application level. Parallelism is achieved through subprocess fan-out:
|
||||
|
||||
```el
|
||||
// Pattern: write payloads to temp files, exec bash script with & and wait,
|
||||
// read results back from temp files.
|
||||
fn http_post_parallel(urls: [String], bodies: [String]) -> [String] {
|
||||
// ... bash fan-out via exec() ...
|
||||
}
|
||||
```
|
||||
|
||||
Use `exec()` (blocking) or `exec_bg()` (fire-and-forget) with shell scripts to run concurrent work. There is no goroutine or async/await — parallelism goes through the OS process layer.
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| Path | What it is |
|
||||
|------|-----------|
|
||||
| `dist/platform/elc` | Canonical compiler binary (arm64 Mac) |
|
||||
| `el-compiler/src/codegen.el` | Code generator — builtin arity table lives here |
|
||||
| `el-compiler/src/lexer.el` | Lexer |
|
||||
| `el-compiler/src/parser.el` | Parser |
|
||||
| `el-compiler/runtime/el_seed.c` | Self-contained C OS-boundary layer (replaces el_runtime.c) |
|
||||
| `el-compiler/runtime/el_seed.h` | Seed header (C function declarations) |
|
||||
| `spec/language.md` | Language specification |
|
||||
| `BOOTSTRAP.md` | How to recover the compiler from scratch |
|
||||
| `elc-cli.el` | Compiler entry point |
|
||||
| `elc-combined.el` | Pre-merged single-file compiler (used during early bootstrap) |
|
||||
|
||||
---
|
||||
|
||||
## HTTP Timeout
|
||||
|
||||
The El HTTP client (libcurl) defaults to **60 seconds**. Override per-process via `EL_HTTP_TIMEOUT_MS` env var. Set it before spawning any subprocess that makes long API calls:
|
||||
|
||||
```el
|
||||
exec("EL_HTTP_TIMEOUT_MS=300000 " + SOME_BIN + " " + args + " 2>&1")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- New library functions → write in El
|
||||
- New OS/hardware primitives → write in C and register in `codegen.el` arity table
|
||||
- Never edit `dist/platform/elc` directly — always rebuild from source
|
||||
- Never modify `el_seed.c` to add functionality that El can express
|
||||
Vendored
+40
-2044
File diff suppressed because it is too large
Load Diff
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+176
-827
File diff suppressed because it is too large
Load Diff
@@ -155,34 +155,37 @@ 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;
|
||||
/* __read_n — read exactly n bytes from stdin.
|
||||
* Allocates a buffer of size n+1, calls fread(buf, 1, n, stdin) to read
|
||||
* exactly n raw bytes (including \r, \n, NUL, etc.), null-terminates, and
|
||||
* returns the buffer as an El String. Returns "" on EOF or I/O error.
|
||||
*
|
||||
* Used by the El LSP server to read JSON-RPC message bodies after parsing
|
||||
* the Content-Length header. readline() cannot be used for the body because
|
||||
* it stops at the first \n and LSP JSON bodies are not newline-terminated. */
|
||||
el_val_t __read_n(el_val_t nv) {
|
||||
int64_t n = EL_INT(nv);
|
||||
if (n <= 0) return el_wrap_str(el_strdup(""));
|
||||
char* buf = malloc((size_t)n + 1);
|
||||
if (!buf) { fputs("el_runtime: __read_n: out of memory\n", stderr); return el_wrap_str(el_strdup("")); }
|
||||
size_t got = fread(buf, 1, (size_t)n, stdin);
|
||||
buf[got] = '\0';
|
||||
if (got == 0) { free(buf); return el_wrap_str(el_strdup("")); }
|
||||
/* Track in arena so the allocation is freed when the request ends. */
|
||||
el_arena_track(buf);
|
||||
return el_wrap_str(buf);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
/* __print_raw — write a string to stdout without any modification.
|
||||
* Unlike println/print (which call puts/fputs and may add newlines or flush
|
||||
* in platform-specific ways), this uses fwrite with the exact byte count so
|
||||
* that embedded \r\n pairs in LSP Content-Length headers survive intact. */
|
||||
void __print_raw(el_val_t sv) {
|
||||
const char* s = EL_CSTR(sv);
|
||||
if (!s) return;
|
||||
size_t len = strlen(s);
|
||||
fwrite(s, 1, len, stdout);
|
||||
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 ─────────────────────────────────────────────────────── */
|
||||
@@ -2602,45 +2605,6 @@ 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
|
||||
@@ -10313,3 +10277,650 @@ el_val_t emit_event(el_val_t name_v, el_val_t duration_ms_v) {
|
||||
return trace_span_end(h);
|
||||
}
|
||||
|
||||
/* ── Threading seed primitives ───────────────────────────────────────────────
|
||||
* __thread_create(fn_name, arg) -> Int spawn El fn in a pthread, return tid
|
||||
* __thread_join(tid) -> String join thread, return result string
|
||||
* __mutex_new() -> Int allocate a mutex, return handle
|
||||
* __mutex_lock(m) lock mutex m
|
||||
* __mutex_unlock(m) unlock mutex m
|
||||
*
|
||||
* Every El fn compiles to a global C symbol. __thread_create uses dlsym to
|
||||
* look up the function by name and run it in a pthread. This means any El fn
|
||||
* with signature (String) -> String is directly threadable.
|
||||
*/
|
||||
|
||||
typedef el_val_t (*ElFn1)(el_val_t);
|
||||
|
||||
typedef struct {
|
||||
ElFn1 fn;
|
||||
el_val_t arg;
|
||||
el_val_t result;
|
||||
} ElThreadArg;
|
||||
|
||||
#define EL_THREAD_MAX 256
|
||||
|
||||
typedef struct {
|
||||
pthread_t tid;
|
||||
ElThreadArg* arg;
|
||||
int alive;
|
||||
} ElThread;
|
||||
|
||||
static ElThread _threads[EL_THREAD_MAX];
|
||||
static int _thread_count = 0;
|
||||
static pthread_mutex_t _thread_alloc_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void* el_thread_runner(void* raw) {
|
||||
ElThreadArg* a = (ElThreadArg*)raw;
|
||||
a->result = a->fn(a->arg);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v) {
|
||||
const char* sym = EL_CSTR(fn_name_v);
|
||||
if (!sym || !*sym) return EL_INT(-1);
|
||||
void* p = dlsym(RTLD_DEFAULT, sym);
|
||||
if (!p) {
|
||||
fprintf(stderr, "[__thread_create] symbol not found: %s\n", sym);
|
||||
return EL_INT(-1);
|
||||
}
|
||||
ElThreadArg* a = (ElThreadArg*)malloc(sizeof(ElThreadArg));
|
||||
if (!a) return EL_INT(-1);
|
||||
a->fn = (ElFn1)p;
|
||||
a->arg = arg_v;
|
||||
a->result = EL_STR("");
|
||||
|
||||
pthread_mutex_lock(&_thread_alloc_mu);
|
||||
if (_thread_count >= EL_THREAD_MAX) {
|
||||
pthread_mutex_unlock(&_thread_alloc_mu);
|
||||
free(a);
|
||||
fprintf(stderr, "[__thread_create] thread table full\n");
|
||||
return EL_INT(-1);
|
||||
}
|
||||
int slot = _thread_count++;
|
||||
_threads[slot].arg = a;
|
||||
_threads[slot].alive = 1;
|
||||
pthread_mutex_unlock(&_thread_alloc_mu);
|
||||
|
||||
if (pthread_create(&_threads[slot].tid, NULL, el_thread_runner, a) != 0) {
|
||||
pthread_mutex_lock(&_thread_alloc_mu);
|
||||
_thread_count--;
|
||||
pthread_mutex_unlock(&_thread_alloc_mu);
|
||||
free(a);
|
||||
return EL_INT(-1);
|
||||
}
|
||||
return EL_INT(slot);
|
||||
}
|
||||
|
||||
el_val_t __thread_join(el_val_t tid_v) {
|
||||
int slot = (int)(int64_t)tid_v;
|
||||
if (slot < 0 || slot >= EL_THREAD_MAX) return EL_STR("");
|
||||
pthread_join(_threads[slot].tid, NULL);
|
||||
el_val_t result = _threads[slot].arg->result;
|
||||
free(_threads[slot].arg);
|
||||
_threads[slot].alive = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Mutex table */
|
||||
|
||||
#define EL_MUTEX_MAX 64
|
||||
|
||||
typedef struct {
|
||||
pthread_mutex_t mu;
|
||||
int allocated;
|
||||
} ElMutexEntry;
|
||||
|
||||
static ElMutexEntry _mutexes[EL_MUTEX_MAX];
|
||||
static int _mutex_count = 0;
|
||||
static pthread_mutex_t _mutex_alloc_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
el_val_t __mutex_new(void) {
|
||||
pthread_mutex_lock(&_mutex_alloc_mu);
|
||||
if (_mutex_count >= EL_MUTEX_MAX) {
|
||||
pthread_mutex_unlock(&_mutex_alloc_mu);
|
||||
fprintf(stderr, "[__mutex_new] mutex table full\n");
|
||||
return EL_INT(-1);
|
||||
}
|
||||
int slot = _mutex_count++;
|
||||
pthread_mutex_init(&_mutexes[slot].mu, NULL);
|
||||
_mutexes[slot].allocated = 1;
|
||||
pthread_mutex_unlock(&_mutex_alloc_mu);
|
||||
return EL_INT(slot);
|
||||
}
|
||||
|
||||
void __mutex_lock(el_val_t m_v) {
|
||||
int slot = (int)(int64_t)m_v;
|
||||
if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return;
|
||||
pthread_mutex_lock(&_mutexes[slot].mu);
|
||||
}
|
||||
|
||||
void __mutex_unlock(el_val_t m_v) {
|
||||
int slot = (int)(int64_t)m_v;
|
||||
if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return;
|
||||
pthread_mutex_unlock(&_mutexes[slot].mu);
|
||||
}
|
||||
|
||||
/* ── Channels ─────────────────────────────────────────────────────────────── *
|
||||
* Buffered MPMC channel backed by a mutex + condvar + circular buffer.
|
||||
* channel_new(capacity) -> Int (handle)
|
||||
* channel_send(ch, msg) — blocks if full (capacity > 0) or never (unbounded)
|
||||
* channel_recv(ch) -> String — blocks until a message is available
|
||||
* channel_try_recv(ch) -> String — non-blocking, returns "" if empty
|
||||
* channel_close(ch) — signal no more sends; recv drains remaining
|
||||
*
|
||||
* Bounded channels (cap > 0): circular buffer, sender blocks when full.
|
||||
* Unbounded channels (cap == 0): dynamic array, sender never blocks.
|
||||
*/
|
||||
#define EL_CHANNEL_MAX 64
|
||||
#define EL_CHANNEL_BUF 1024
|
||||
|
||||
typedef struct {
|
||||
char** buf;
|
||||
int cap; /* 0 = unbounded (grows dynamically) */
|
||||
int head, tail, count;
|
||||
int dyn_cap; /* allocated slots for unbounded mode */
|
||||
int closed;
|
||||
pthread_mutex_t mu;
|
||||
pthread_cond_t not_empty;
|
||||
pthread_cond_t not_full;
|
||||
} ElChannel;
|
||||
|
||||
static ElChannel _channels[EL_CHANNEL_MAX];
|
||||
static int _channel_count = 0;
|
||||
static pthread_mutex_t _channel_alloc_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
el_val_t __channel_new(el_val_t capacity_v) {
|
||||
int cap = (int)(int64_t)capacity_v;
|
||||
if (cap < 0) cap = 0;
|
||||
|
||||
pthread_mutex_lock(&_channel_alloc_mu);
|
||||
if (_channel_count >= EL_CHANNEL_MAX) {
|
||||
pthread_mutex_unlock(&_channel_alloc_mu);
|
||||
fprintf(stderr, "[__channel_new] channel table full\n");
|
||||
return EL_INT(-1);
|
||||
}
|
||||
int slot = _channel_count++;
|
||||
pthread_mutex_unlock(&_channel_alloc_mu);
|
||||
|
||||
ElChannel* ch = &_channels[slot];
|
||||
memset(ch, 0, sizeof(*ch));
|
||||
ch->cap = cap;
|
||||
ch->closed = 0;
|
||||
ch->head = 0;
|
||||
ch->tail = 0;
|
||||
ch->count = 0;
|
||||
|
||||
if (cap > 0) {
|
||||
/* Bounded: fixed circular buffer. */
|
||||
ch->buf = (char**)malloc((size_t)cap * sizeof(char*));
|
||||
ch->dyn_cap = cap;
|
||||
} else {
|
||||
/* Unbounded: start with EL_CHANNEL_BUF slots, grow as needed. */
|
||||
ch->buf = (char**)malloc(EL_CHANNEL_BUF * sizeof(char*));
|
||||
ch->dyn_cap = EL_CHANNEL_BUF;
|
||||
}
|
||||
if (!ch->buf) {
|
||||
fprintf(stderr, "[__channel_new] out of memory\n");
|
||||
return EL_INT(-1);
|
||||
}
|
||||
|
||||
pthread_mutex_init(&ch->mu, NULL);
|
||||
pthread_cond_init(&ch->not_empty, NULL);
|
||||
pthread_cond_init(&ch->not_full, NULL);
|
||||
|
||||
return EL_INT(slot);
|
||||
}
|
||||
|
||||
void __channel_send(el_val_t ch_v, el_val_t msg_v) {
|
||||
int slot = (int)(int64_t)ch_v;
|
||||
if (slot < 0 || slot >= EL_CHANNEL_MAX) return;
|
||||
ElChannel* ch = &_channels[slot];
|
||||
|
||||
const char* msg = EL_CSTR(msg_v);
|
||||
if (!msg) msg = "";
|
||||
char* copy = strdup(msg); /* channel owns the string */
|
||||
|
||||
pthread_mutex_lock(&ch->mu);
|
||||
|
||||
if (ch->closed) {
|
||||
/* Send on closed channel is a no-op (drop the message). */
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
free(copy);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ch->cap > 0) {
|
||||
/* Bounded: block while full. */
|
||||
while (ch->count >= ch->cap && !ch->closed) {
|
||||
pthread_cond_wait(&ch->not_full, &ch->mu);
|
||||
}
|
||||
if (ch->closed) {
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
free(copy);
|
||||
return;
|
||||
}
|
||||
ch->buf[ch->tail] = copy;
|
||||
ch->tail = (ch->tail + 1) % ch->cap;
|
||||
ch->count++;
|
||||
} else {
|
||||
/* Unbounded: grow the buffer if needed. */
|
||||
if (ch->count >= ch->dyn_cap) {
|
||||
int new_cap = ch->dyn_cap * 2;
|
||||
char** grown = (char**)realloc(ch->buf, (size_t)new_cap * sizeof(char*));
|
||||
if (!grown) {
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
free(copy);
|
||||
fprintf(stderr, "[__channel_send] out of memory growing channel\n");
|
||||
return;
|
||||
}
|
||||
/* The circular buffer may have wrapped. Linearise it first.
|
||||
* In unbounded mode head is always 0 (we append at tail, drain
|
||||
* from head), so a simple memmove isn't needed — but if the
|
||||
* buffer did wrap (tail < head after growth), we need to fix up.
|
||||
* Simplest safe path: if tail wrapped, move the head..old_cap
|
||||
* segment to new_cap..new_cap+(old_cap-head). */
|
||||
if (ch->tail < ch->head) {
|
||||
/* Wrapped: [head..old_cap) is the front, [0..tail) is the back. */
|
||||
int front = ch->dyn_cap - ch->head;
|
||||
memmove(grown + ch->dyn_cap, grown + ch->head, (size_t)front * sizeof(char*));
|
||||
ch->head = ch->dyn_cap;
|
||||
}
|
||||
ch->buf = grown;
|
||||
ch->dyn_cap = new_cap;
|
||||
}
|
||||
ch->buf[ch->tail] = copy;
|
||||
ch->tail = (ch->tail + 1) % ch->dyn_cap;
|
||||
ch->count++;
|
||||
}
|
||||
|
||||
pthread_cond_signal(&ch->not_empty);
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
}
|
||||
|
||||
el_val_t __channel_recv(el_val_t ch_v) {
|
||||
int slot = (int)(int64_t)ch_v;
|
||||
if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR("");
|
||||
ElChannel* ch = &_channels[slot];
|
||||
|
||||
pthread_mutex_lock(&ch->mu);
|
||||
|
||||
/* Block until there is a message or the channel is closed and drained. */
|
||||
while (ch->count == 0 && !ch->closed) {
|
||||
pthread_cond_wait(&ch->not_empty, &ch->mu);
|
||||
}
|
||||
|
||||
if (ch->count == 0) {
|
||||
/* Closed and empty — signal EOF. */
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap;
|
||||
char* msg = ch->buf[ch->head];
|
||||
ch->head = (ch->head + 1) % buf_cap;
|
||||
ch->count--;
|
||||
|
||||
pthread_cond_signal(&ch->not_full);
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
|
||||
/* Hand the string to the arena so it is freed after the request. */
|
||||
el_arena_track(msg);
|
||||
return EL_STR(msg);
|
||||
}
|
||||
|
||||
el_val_t __channel_try_recv(el_val_t ch_v) {
|
||||
int slot = (int)(int64_t)ch_v;
|
||||
if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR("");
|
||||
ElChannel* ch = &_channels[slot];
|
||||
|
||||
pthread_mutex_lock(&ch->mu);
|
||||
|
||||
if (ch->count == 0) {
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap;
|
||||
char* msg = ch->buf[ch->head];
|
||||
ch->head = (ch->head + 1) % buf_cap;
|
||||
ch->count--;
|
||||
|
||||
pthread_cond_signal(&ch->not_full);
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
|
||||
el_arena_track(msg);
|
||||
return EL_STR(msg);
|
||||
}
|
||||
|
||||
void __channel_close(el_val_t ch_v) {
|
||||
int slot = (int)(int64_t)ch_v;
|
||||
if (slot < 0 || slot >= EL_CHANNEL_MAX) return;
|
||||
ElChannel* ch = &_channels[slot];
|
||||
|
||||
pthread_mutex_lock(&ch->mu);
|
||||
ch->closed = 1;
|
||||
/* Wake all blocked recvers and senders so they can observe the close. */
|
||||
pthread_cond_broadcast(&ch->not_empty);
|
||||
pthread_cond_broadcast(&ch->not_full);
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
}
|
||||
|
||||
/* ── DHARMA runtime additions ────────────────────────────────────────────────
|
||||
*
|
||||
* Functions required by the dharma registry service. Added here so the
|
||||
* released el_runtime.c includes them without requiring dharma to bundle
|
||||
* its own stubs.
|
||||
*
|
||||
* Functions added:
|
||||
* list_len — alias for el_list_len (used in handlers.el)
|
||||
* list_get — alias for el_list_get (used in handlers.el)
|
||||
* json_array_push — append a pre-encoded JSON element to a JSON array string
|
||||
* now_millis — milliseconds since Unix epoch (alias for time_now)
|
||||
* unix_timestamp_ms — same as now_millis (alias)
|
||||
* time_now_ms — same as now_millis (alias)
|
||||
* log_info — stderr structured log at INFO level
|
||||
* log_warn — stderr structured log at WARN level
|
||||
* config — reads a config value from the environment
|
||||
* http_patch — HTTP PATCH with JSON Content-Type
|
||||
* http_post_engram — HTTP POST with optional X-API-Key header
|
||||
* http_get_engram — HTTP GET with optional X-API-Key header
|
||||
* str_to_bytes — encode a string as a JSON array of byte values
|
||||
* bytes_to_str — decode a JSON array of byte values back to a string
|
||||
* hash_sha256 — SHA-256 hex digest of a string
|
||||
*/
|
||||
|
||||
/* list_len — return the number of elements in a list. */
|
||||
el_val_t list_len(el_val_t list) {
|
||||
return el_list_len(list);
|
||||
}
|
||||
|
||||
/* list_get — return the element at index i in a list. */
|
||||
el_val_t list_get(el_val_t list, el_val_t index) {
|
||||
return el_list_get(list, index);
|
||||
}
|
||||
|
||||
/* json_array_push — append element (a pre-encoded JSON fragment, e.g. "\"foo\""
|
||||
* or "42") to the JSON array string arr. Returns a new JSON array string.
|
||||
* Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]"
|
||||
* json_array_push("[\"alice\"]", "\"bob\"") -> "[\"alice\",\"bob\"]" */
|
||||
el_val_t json_array_push(el_val_t arr_v, el_val_t elem_v) {
|
||||
const char* arr = EL_CSTR(arr_v);
|
||||
const char* elem = EL_CSTR(elem_v);
|
||||
if (!arr || !*arr) arr = "[]";
|
||||
if (!elem || !*elem) elem = "null";
|
||||
|
||||
/* Trim whitespace, find the closing ']'. */
|
||||
const char* p = arr;
|
||||
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
|
||||
if (*p != '[') {
|
||||
/* Not an array — return a single-element array. */
|
||||
size_t n = strlen(elem) + 4;
|
||||
char* out = el_strbuf(n);
|
||||
snprintf(out, n, "[%s]", elem);
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
size_t arr_len = strlen(arr);
|
||||
size_t elem_len = strlen(elem);
|
||||
|
||||
/* Walk from the end to find the matching ']'. */
|
||||
const char* end = arr + arr_len - 1;
|
||||
while (end > p && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) end--;
|
||||
if (*end != ']') {
|
||||
/* Malformed — wrap elem in a new array. */
|
||||
size_t n = elem_len + 4;
|
||||
char* out = el_strbuf(n);
|
||||
snprintf(out, n, "[%s]", elem);
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* Content between '[' and ']'. */
|
||||
const char* inner_start = p + 1;
|
||||
const char* inner_end = end; /* points AT ']' */
|
||||
/* Check if the array is empty (only whitespace between brackets). */
|
||||
const char* q = inner_start;
|
||||
while (q < inner_end && (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r')) q++;
|
||||
int empty = (q == inner_end);
|
||||
|
||||
/* Build: prefix + (comma if non-empty) + elem + "]" */
|
||||
size_t prefix_len = (size_t)(inner_end - arr); /* up to but not including ']' */
|
||||
size_t sep_len = empty ? 0 : 1; /* "," if non-empty */
|
||||
size_t out_len = prefix_len + sep_len + elem_len + 2; /* +"]" + NUL */
|
||||
char* out = el_strbuf(out_len);
|
||||
memcpy(out, arr, prefix_len);
|
||||
if (!empty) out[prefix_len] = ',';
|
||||
memcpy(out + prefix_len + sep_len, elem, elem_len);
|
||||
out[prefix_len + sep_len + elem_len] = ']';
|
||||
out[prefix_len + sep_len + elem_len + 1] = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* now_millis — milliseconds since Unix epoch. */
|
||||
el_val_t now_millis(void) {
|
||||
return time_now();
|
||||
}
|
||||
|
||||
/* unix_timestamp_ms — same as now_millis. */
|
||||
el_val_t unix_timestamp_ms(void) {
|
||||
return time_now();
|
||||
}
|
||||
|
||||
/* time_now_ms — same as now_millis. */
|
||||
el_val_t time_now_ms(void) {
|
||||
return time_now();
|
||||
}
|
||||
|
||||
/* log_info — write a structured [INFO] line to stderr. */
|
||||
void log_info(el_val_t msg_v) {
|
||||
const char* msg = EL_CSTR(msg_v);
|
||||
fprintf(stderr, "[INFO] %s\n", msg ? msg : "");
|
||||
}
|
||||
|
||||
/* log_warn — write a structured [WARN] line to stderr. */
|
||||
void log_warn(el_val_t msg_v) {
|
||||
const char* msg = EL_CSTR(msg_v);
|
||||
fprintf(stderr, "[WARN] %s\n", msg ? msg : "");
|
||||
}
|
||||
|
||||
/* config — read a configuration value from the environment.
|
||||
* Returns "" if the variable is not set (same as __env_get). */
|
||||
el_val_t config(el_val_t key_v) {
|
||||
const char* key = EL_CSTR(key_v);
|
||||
if (!key || !*key) return EL_STR("");
|
||||
const char* val = getenv(key);
|
||||
if (!val) return EL_STR("");
|
||||
return el_wrap_str(el_strdup(val));
|
||||
}
|
||||
|
||||
/* http_patch — HTTP PATCH request with Content-Type: application/json.
|
||||
* Returns the response body (same error convention as http_post_json). */
|
||||
el_val_t http_patch(el_val_t url_v, el_val_t body_v) {
|
||||
const char* url = EL_CSTR(url_v);
|
||||
const char* body = EL_CSTR(body_v);
|
||||
if (!url || !*url) return http_error_json("empty url");
|
||||
CURL* c = curl_easy_init();
|
||||
if (!c) return http_error_json("curl_easy_init failed");
|
||||
HttpBuf rb; httpbuf_init(&rb);
|
||||
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
|
||||
struct curl_slist* h = NULL;
|
||||
h = curl_slist_append(h, "Content-Type: application/json");
|
||||
curl_easy_setopt(c, CURLOPT_URL, url);
|
||||
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH");
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : "");
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0));
|
||||
curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
|
||||
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms());
|
||||
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
|
||||
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0");
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
curl_slist_free_all(h);
|
||||
curl_easy_cleanup(c);
|
||||
if (rc != CURLE_OK) {
|
||||
free(rb.data);
|
||||
const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc);
|
||||
return http_error_json(m);
|
||||
}
|
||||
return el_wrap_str(rb.data);
|
||||
}
|
||||
|
||||
/* http_post_engram — HTTP POST with optional X-API-Key header.
|
||||
* If key is "" no authentication header is sent. */
|
||||
el_val_t http_post_engram(el_val_t url_v, el_val_t key_v, el_val_t body_v) {
|
||||
const char* url = EL_CSTR(url_v);
|
||||
const char* key = EL_CSTR(key_v);
|
||||
const char* body = EL_CSTR(body_v);
|
||||
if (!url || !*url) return http_error_json("empty url");
|
||||
CURL* c = curl_easy_init();
|
||||
if (!c) return http_error_json("curl_easy_init failed");
|
||||
HttpBuf rb; httpbuf_init(&rb);
|
||||
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
|
||||
struct curl_slist* h = NULL;
|
||||
h = curl_slist_append(h, "Content-Type: application/json");
|
||||
if (key && *key) {
|
||||
size_t n = strlen(key) + 32;
|
||||
char* hdr = malloc(n);
|
||||
snprintf(hdr, n, "X-API-Key: %s", key);
|
||||
h = curl_slist_append(h, hdr);
|
||||
free(hdr);
|
||||
}
|
||||
curl_easy_setopt(c, CURLOPT_URL, url);
|
||||
curl_easy_setopt(c, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : "");
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0));
|
||||
curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
|
||||
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms());
|
||||
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
|
||||
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0");
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
curl_slist_free_all(h);
|
||||
curl_easy_cleanup(c);
|
||||
if (rc != CURLE_OK) {
|
||||
free(rb.data);
|
||||
const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc);
|
||||
return http_error_json(m);
|
||||
}
|
||||
return el_wrap_str(rb.data);
|
||||
}
|
||||
|
||||
/* http_get_engram — HTTP GET with optional X-API-Key header. */
|
||||
el_val_t http_get_engram(el_val_t url_v, el_val_t key_v) {
|
||||
const char* url = EL_CSTR(url_v);
|
||||
const char* key = EL_CSTR(key_v);
|
||||
if (!url || !*url) return http_error_json("empty url");
|
||||
CURL* c = curl_easy_init();
|
||||
if (!c) return http_error_json("curl_easy_init failed");
|
||||
HttpBuf rb; httpbuf_init(&rb);
|
||||
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
|
||||
struct curl_slist* h = NULL;
|
||||
if (key && *key) {
|
||||
size_t n = strlen(key) + 32;
|
||||
char* hdr = malloc(n);
|
||||
snprintf(hdr, n, "X-API-Key: %s", key);
|
||||
h = curl_slist_append(h, hdr);
|
||||
free(hdr);
|
||||
}
|
||||
curl_easy_setopt(c, CURLOPT_URL, url);
|
||||
curl_easy_setopt(c, CURLOPT_HTTPGET, 1L);
|
||||
if (h) curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
|
||||
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms());
|
||||
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
|
||||
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0");
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
if (h) curl_slist_free_all(h);
|
||||
curl_easy_cleanup(c);
|
||||
if (rc != CURLE_OK) {
|
||||
free(rb.data);
|
||||
const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc);
|
||||
return http_error_json(m);
|
||||
}
|
||||
return el_wrap_str(rb.data);
|
||||
}
|
||||
|
||||
/* str_to_bytes — encode a string as a JSON array of unsigned byte values.
|
||||
* "hello" -> "[104,101,108,108,111]"
|
||||
* Used by db.el to store binary content in Engram JSON nodes. */
|
||||
el_val_t str_to_bytes(el_val_t sv) {
|
||||
const char* s = EL_CSTR(sv);
|
||||
if (!s || !*s) return el_wrap_str(el_strdup("[]"));
|
||||
size_t n = strlen(s);
|
||||
/* Worst case: each byte is 3 digits + comma = 4 chars, plus "[]" + NUL. */
|
||||
char* out = el_strbuf(n * 4 + 3);
|
||||
size_t pos = 0;
|
||||
out[pos++] = '[';
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
unsigned char b = (unsigned char)s[i];
|
||||
if (i > 0) out[pos++] = ',';
|
||||
/* Write decimal representation of b. */
|
||||
if (b >= 100) {
|
||||
out[pos++] = (char)('0' + b / 100);
|
||||
out[pos++] = (char)('0' + (b / 10) % 10);
|
||||
out[pos++] = (char)('0' + b % 10);
|
||||
} else if (b >= 10) {
|
||||
out[pos++] = (char)('0' + b / 10);
|
||||
out[pos++] = (char)('0' + b % 10);
|
||||
} else {
|
||||
out[pos++] = (char)('0' + b);
|
||||
}
|
||||
}
|
||||
out[pos++] = ']';
|
||||
out[pos] = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* bytes_to_str — decode a JSON array of integer byte values back to a string.
|
||||
* "[104,101,108,108,111]" -> "hello"
|
||||
* Inverse of str_to_bytes. */
|
||||
el_val_t bytes_to_str(el_val_t arr_v) {
|
||||
const char* s = EL_CSTR(arr_v);
|
||||
if (!s) return el_wrap_str(el_strdup(""));
|
||||
/* Skip whitespace, expect '['. */
|
||||
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
|
||||
if (*s != '[') return el_wrap_str(el_strdup(""));
|
||||
s++;
|
||||
|
||||
/* Count elements to size the output buffer. */
|
||||
int64_t n = (int64_t)json_array_len(arr_v);
|
||||
if (n <= 0) return el_wrap_str(el_strdup(""));
|
||||
|
||||
char* out = el_strbuf((size_t)n + 1);
|
||||
size_t pos = 0;
|
||||
|
||||
/* Walk the array, parse each integer, store as a byte. */
|
||||
while (*s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
|
||||
if (*s == ']' || *s == '\0') break;
|
||||
/* Parse decimal integer. */
|
||||
char* end_ptr;
|
||||
long v = strtol(s, &end_ptr, 10);
|
||||
if (end_ptr == s) break; /* parse failure */
|
||||
s = end_ptr;
|
||||
if (v >= 0 && v <= 255) out[pos++] = (char)(unsigned char)v;
|
||||
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
|
||||
if (*s == ',') { s++; continue; }
|
||||
if (*s == ']' || *s == '\0') break;
|
||||
}
|
||||
out[pos] = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* hash_sha256 — return the SHA-256 hex digest of a string.
|
||||
* Uses the built-in el_sha256_oneshot implementation (no OpenSSL required). */
|
||||
el_val_t hash_sha256(el_val_t sv) {
|
||||
const char* s = EL_CSTR(sv);
|
||||
if (!s) s = "";
|
||||
unsigned char digest[32];
|
||||
el_sha256_oneshot((const unsigned char*)s, strlen(s), digest);
|
||||
return el_hex_encode(digest, 32);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,8 +79,6 @@ 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 ─────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -178,6 +176,11 @@ void http_set_handler_v2(el_val_t name);
|
||||
* 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);
|
||||
|
||||
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
|
||||
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
|
||||
* The getter is exposed as __http_conn_fd() to El programs. */
|
||||
void el_seed_set_http_conn_fd(int fd);
|
||||
|
||||
/* 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. */
|
||||
@@ -214,13 +217,6 @@ 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);
|
||||
@@ -760,6 +756,9 @@ 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);
|
||||
|
||||
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v);
|
||||
el_val_t __thread_join(el_val_t tid_v);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -128,22 +128,6 @@ 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); }
|
||||
@@ -538,328 +522,6 @@ 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.
|
||||
@@ -970,31 +632,6 @@ 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,
|
||||
@@ -1033,35 +670,10 @@ 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,248 @@
|
||||
/*
|
||||
* el_seed.h — El language seed runtime header
|
||||
*
|
||||
* Declares all OS-boundary primitives available to compiled El programs.
|
||||
* All functions use the __ prefix convention. Signatures use el_val_t (= int64_t)
|
||||
* as the universal value type.
|
||||
*
|
||||
* el_seed.c is the complete C boundary for the El runtime. The heavy runtime
|
||||
* (el_runtime.c) has been retired — everything lives in el_seed.c plus the
|
||||
* native El runtime (runtime/ *.el files).
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — HTTP client (__http_do, __http_do_to_file)
|
||||
* -lpthread — threading (__thread_create, __thread_join, __mutex_new, ...)
|
||||
*
|
||||
* Canonical compile (via elb):
|
||||
* elb builds and links el_seed.c automatically.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ── Value model ─────────────────────────────────────────────────────────────
|
||||
* All El values are el_val_t (int64_t). On 64-bit systems a pointer fits.
|
||||
* String -> el_val_t (holds const char* via uintptr_t cast)
|
||||
* Int -> el_val_t (stored directly)
|
||||
* Bool -> el_val_t (0 = false, nonzero = true)
|
||||
* Void -> void
|
||||
*/
|
||||
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 slot via bit-cast. */
|
||||
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
|
||||
|
||||
/* ── String primitives ───────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __str_len(el_val_t s);
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i); /* returns Int (byte value) */
|
||||
el_val_t __str_alloc(el_val_t n); /* malloc(n+1), zero-init, return String */
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c); /* s[i]=c, return s */
|
||||
el_val_t __str_cmp(el_val_t a, el_val_t b); /* strcmp result as Int */
|
||||
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n); /* strncmp */
|
||||
el_val_t __str_concat_raw(el_val_t a, el_val_t b); /* malloc+strcpy concat */
|
||||
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end); /* substring copy */
|
||||
el_val_t __int_to_str(el_val_t n);
|
||||
el_val_t __str_to_int(el_val_t s);
|
||||
el_val_t __float_to_str(el_val_t f); /* f is bit-cast double */
|
||||
el_val_t __str_to_float(el_val_t s); /* strtod, bit-cast result */
|
||||
|
||||
/* ── I/O ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void __println(el_val_t s);
|
||||
void __print(el_val_t s);
|
||||
el_val_t __readline(void);
|
||||
|
||||
/* ── 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_exists(el_val_t path);
|
||||
el_val_t __fs_list_raw(el_val_t path); /* newline-separated filenames */
|
||||
el_val_t __fs_mkdir(el_val_t path);
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n);
|
||||
|
||||
/* ── HTTP client ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Unified HTTP call. headers_json is a JSON object of header name->value pairs
|
||||
* (e.g. {"Authorization":"Bearer ...","Content-Type":"application/json"}).
|
||||
* Use "" or "{}" for no extra headers. timeout_ms <= 0 uses the default. */
|
||||
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t timeout_ms);
|
||||
|
||||
/* Stream response body directly to a file. Returns 1 on success, 0 on failure. */
|
||||
el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t out_path);
|
||||
|
||||
/* ── HTTP server ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Blocking HTTP server. handler_name is the El function name to dispatch to.
|
||||
* v1 handler: (method, path, body) -> String
|
||||
* v2 handler: (method, path, headers_map, body) -> String or envelope */
|
||||
void __http_serve(el_val_t port, el_val_t handler_name);
|
||||
void __http_serve_v2(el_val_t port, el_val_t handler_name);
|
||||
|
||||
/* Build a structured HTTP response envelope.
|
||||
* headers_json: JSON object literal like {"Content-Type":"text/plain"} or "{}" */
|
||||
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */
|
||||
|
||||
/* Returns the raw file descriptor for the current HTTP connection.
|
||||
* Valid only inside an http_serve_v2 handler before it returns.
|
||||
* Returns -1 if called outside a handler context. */
|
||||
el_val_t __http_conn_fd(void);
|
||||
|
||||
/* Sends SSE response headers on conn_id (the fd from __http_conn_fd),
|
||||
* keeping the connection open for streaming. Returns 1 on success, 0 on
|
||||
* write failure. Call once at the start of an SSE handler. */
|
||||
el_val_t __http_sse_open(el_val_t conn_id);
|
||||
|
||||
/* Writes one SSE event frame: "data: <data>\n\n". data must not contain
|
||||
* newlines. Returns 1 on success, 0 if the client disconnected. */
|
||||
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data);
|
||||
|
||||
/* Closes the SSE connection. The handler must return http_sse_sentinel()
|
||||
* so the HTTP worker does not double-close the fd. */
|
||||
el_val_t __http_sse_close(el_val_t conn_id);
|
||||
|
||||
/* ── Threading ───────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Create a thread that calls the named El function with a String argument.
|
||||
* fn_name is resolved via dlsym(RTLD_DEFAULT, fn_name). Returns a thread
|
||||
* handle Int that can be passed to __thread_join. Returns -1 on failure. */
|
||||
el_val_t __thread_create(el_val_t fn_name, el_val_t arg);
|
||||
|
||||
/* Wait for thread tid (returned by __thread_create) to finish.
|
||||
* Returns the thread's return value as a String. */
|
||||
el_val_t __thread_join(el_val_t tid);
|
||||
|
||||
/* Allocate a new mutex. Returns a handle Int (index into internal table). */
|
||||
el_val_t __mutex_new(void);
|
||||
|
||||
void __mutex_lock(el_val_t m);
|
||||
void __mutex_unlock(el_val_t m);
|
||||
|
||||
/* ── Subprocess ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __exec(el_val_t cmd); /* popen, capture all stdout, return String */
|
||||
void __exec_bg(el_val_t cmd); /* fire and forget */
|
||||
|
||||
/* ── Environment and process ─────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __env_get(el_val_t key); /* getenv, return "" if not set */
|
||||
void __exit_program(el_val_t code);
|
||||
el_val_t __args_json(void); /* CLI args as JSON array string */
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __time_now_ns(void); /* clock_gettime REALTIME, nanoseconds */
|
||||
void __sleep_ms(el_val_t ms);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __uuid_v4(void);
|
||||
|
||||
/* ── Math ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __sqrt_f(el_val_t f);
|
||||
el_val_t __log_f(el_val_t f);
|
||||
el_val_t __ln_f(el_val_t f);
|
||||
el_val_t __sin_f(el_val_t f);
|
||||
el_val_t __cos_f(el_val_t f);
|
||||
el_val_t __pi_f(void);
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __json_get(el_val_t json, el_val_t key);
|
||||
el_val_t __json_get_raw(el_val_t json_str, 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_array_len(el_val_t json_str);
|
||||
el_val_t __json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t __json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
el_val_t __json_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_set(el_val_t json_str, el_val_t key, el_val_t value);
|
||||
|
||||
/* ── 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);
|
||||
|
||||
/* ── HTML/URL ────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
el_val_t __url_encode(el_val_t s);
|
||||
el_val_t __url_decode(el_val_t s);
|
||||
|
||||
/* ── Engram ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── Cryptographic hashing ────────────────────────────────────────────────── */
|
||||
|
||||
/* __sha256_hex — return the SHA-256 hex digest of a string.
|
||||
* The returned string is 64 hex characters (lowercase). */
|
||||
el_val_t __sha256_hex(el_val_t s);
|
||||
|
||||
/* ── args init (called from main) ────────────────────────────────────────── */
|
||||
/* Store argc/argv for __args_json. Call once at the start of main(). */
|
||||
void el_seed_init_args(int argc, char** argv);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1673,6 +1673,7 @@ static void* http_worker_v2(void* arg) {
|
||||
HttpWorkerArg* a = (HttpWorkerArg*)arg;
|
||||
int fd = a->fd;
|
||||
free(a);
|
||||
int is_sse = 0;
|
||||
char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL;
|
||||
if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) {
|
||||
http_handler4_fn h = http_lookup_active_v2();
|
||||
@@ -1680,28 +1681,39 @@ static void* http_worker_v2(void* arg) {
|
||||
int head_only = (method && strcmp(method, "HEAD") == 0);
|
||||
const char* dispatch_method = head_only ? "GET" : method;
|
||||
el_request_start(); /* begin per-request arena */
|
||||
/* Expose the raw fd to El SSE builtins (__http_conn_fd etc.). */
|
||||
el_seed_set_http_conn_fd(fd);
|
||||
if (h) {
|
||||
el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : "");
|
||||
el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body));
|
||||
const char* rs = EL_CSTR(r);
|
||||
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
|
||||
response = malloc(rlen + 1);
|
||||
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
|
||||
else if (response) { response[0] = '\0'; }
|
||||
/* Detect SSE sentinel — handler took ownership of the fd. */
|
||||
if (rs && strcmp(rs, "__sse__") == 0) {
|
||||
is_sse = 1;
|
||||
} else {
|
||||
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
|
||||
response = malloc(rlen + 1);
|
||||
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
|
||||
else if (response) { response[0] = '\0'; }
|
||||
}
|
||||
el_release(hmap);
|
||||
} else {
|
||||
response = el_strdup_persist(
|
||||
"el-runtime: no v2 http handler registered "
|
||||
"(call http_set_handler_v2)");
|
||||
}
|
||||
el_seed_set_http_conn_fd(-1); /* clear before arena teardown */
|
||||
el_request_end(); /* free all intermediate strings */
|
||||
_tl_http_head_only = head_only;
|
||||
http_send_response(fd, response);
|
||||
_tl_http_head_only = 0;
|
||||
free(response);
|
||||
if (!is_sse) {
|
||||
_tl_http_head_only = head_only;
|
||||
http_send_response(fd, response);
|
||||
_tl_http_head_only = 0;
|
||||
free(response);
|
||||
}
|
||||
}
|
||||
free(method); free(path); free(body); free(hdr_block);
|
||||
close(fd);
|
||||
/* SSE handlers close the fd themselves via __http_sse_close. */
|
||||
if (!is_sse) close(fd);
|
||||
pthread_mutex_lock(&_http_conn_mu);
|
||||
_http_conn_active--;
|
||||
pthread_cond_signal(&_http_conn_cv);
|
||||
|
||||
@@ -176,6 +176,11 @@ void http_set_handler_v2(el_val_t name);
|
||||
* 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);
|
||||
|
||||
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
|
||||
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
|
||||
* The getter is exposed as __http_conn_fd() to El programs. */
|
||||
void el_seed_set_http_conn_fd(int fd);
|
||||
|
||||
/* 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. */
|
||||
|
||||
+47
-670
@@ -86,57 +86,6 @@ 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 {
|
||||
@@ -191,138 +140,6 @@ 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).
|
||||
@@ -560,38 +377,20 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
|
||||
|
||||
if func_kind == "Ident" {
|
||||
let fn_name: String = func["name"]
|
||||
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
|
||||
return fn_name + "(" + args_c + ")"
|
||||
}
|
||||
|
||||
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 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 + ")"
|
||||
}
|
||||
// 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 field + "(" + obj_c + ", " + args_c + ")"
|
||||
}
|
||||
return obj_c + "." + field + "()"
|
||||
return field + "(" + obj_c + ")"
|
||||
}
|
||||
|
||||
let fn_c: String = js_cg_expr(func)
|
||||
@@ -599,39 +398,22 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
|
||||
}
|
||||
|
||||
if kind == "Field" {
|
||||
// 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`.
|
||||
// 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.
|
||||
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 obj_c + "[" + js_str_lit(field) + "]"
|
||||
return "el_get_field(" + 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 + ")"
|
||||
}
|
||||
@@ -671,12 +453,6 @@ 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)
|
||||
}
|
||||
@@ -694,17 +470,6 @@ 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"
|
||||
}
|
||||
|
||||
@@ -763,16 +528,8 @@ 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 {
|
||||
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 + "); ")
|
||||
}
|
||||
// unknown pattern → wildcard
|
||||
let parts = native_list_append(parts, "return (" + body_c + "); ")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -784,65 +541,6 @@ 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
|
||||
@@ -948,31 +646,6 @@ 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
|
||||
@@ -1112,30 +785,16 @@ 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)
|
||||
|
||||
// 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 + ") {")
|
||||
}
|
||||
// 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 {
|
||||
// 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 + ") {")
|
||||
}
|
||||
js_emit_line("function " + fn_name + "(" + params_str + ") {")
|
||||
}
|
||||
|
||||
let decl = native_list_empty()
|
||||
@@ -1171,296 +830,49 @@ 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: in bundle mode, inline the runtime and wrap in IIFE.
|
||||
// In module mode, emit a single import that side-effects globalThis.
|
||||
// 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.
|
||||
js_emit_line("// Generated by elc --target=js")
|
||||
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_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;")
|
||||
js_emit_blank()
|
||||
|
||||
// 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 n: Int = native_list_len(stmts)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
@@ -1509,41 +921,6 @@ fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool
|
||||
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")
|
||||
}
|
||||
|
||||
+231
-399
@@ -135,8 +135,9 @@ fn emit_blank() -> Void {
|
||||
fn binop_to_c(op: String) -> String {
|
||||
if op == "Plus" { return "+" }
|
||||
if op == "Minus" { return "-" }
|
||||
if op == "Star" { return "*" }
|
||||
if op == "Slash" { return "/" }
|
||||
if op == "Star" { return "*" }
|
||||
if op == "Slash" { return "/" }
|
||||
if op == "Percent" { return "%" }
|
||||
if op == "EqEq" { return "==" }
|
||||
if op == "NotEq" { return "!=" }
|
||||
if op == "Lt" { return "<" }
|
||||
@@ -175,167 +176,6 @@ 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"]
|
||||
|
||||
@@ -948,10 +788,6 @@ fn cg_expr(expr: Map<String, Any>) -> String {
|
||||
return cg_match(expr)
|
||||
}
|
||||
|
||||
if kind == "HtmlTemplate" {
|
||||
return cg_html_template(expr)
|
||||
}
|
||||
|
||||
"EL_NULL"
|
||||
}
|
||||
|
||||
@@ -1013,15 +849,8 @@ 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 {
|
||||
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 + "; } ")
|
||||
}
|
||||
// unknown pattern -> wildcard
|
||||
let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1033,6 +862,97 @@ fn cg_match(expr: Map<String, Any>) -> String {
|
||||
str_join(parts, "")
|
||||
}
|
||||
|
||||
// Lower a match statement (used for side effects, not as an expression) to a
|
||||
// chain of C if/else if/else blocks. The subject is evaluated once into a
|
||||
// scoped temporary; each arm generates a condition and a braced body; the
|
||||
// wildcard/binding arm becomes the final `else` branch.
|
||||
//
|
||||
// Pattern dispatch:
|
||||
// LitStr -> str_eq(subj, EL_STR("..."))
|
||||
// LitInt -> subj == N
|
||||
// LitBool -> subj == 1 / subj == 0
|
||||
// Binding -> else { el_val_t name = subj; <body> }
|
||||
// Wildcard -> else { <body> }
|
||||
fn cg_match_stmt(expr: Map<String, Any>, indent: String, declared: [String]) -> Void {
|
||||
let subject = expr["subject"]
|
||||
let arms = expr["arms"]
|
||||
let subj_c: String = cg_expr(subject)
|
||||
let id: String = next_match_id()
|
||||
let subj_var: String = "_match_subj_" + id
|
||||
let inner: String = indent + " "
|
||||
emit_line(indent + "{")
|
||||
emit_line(inner + "el_val_t " + subj_var + " = " + subj_c + ";")
|
||||
let n: Int = native_list_len(arms)
|
||||
let i = 0
|
||||
let first_cond: Bool = true
|
||||
while i < n {
|
||||
let arm = native_list_get(arms, i)
|
||||
let pat = arm["pattern"]
|
||||
let body = arm["body"]
|
||||
let pkind: String = pat["pattern"]
|
||||
let body_c: String = cg_expr(body)
|
||||
if str_eq(pkind, "LitStr") {
|
||||
let v: String = pat["value"]
|
||||
let cond_str = "str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))"
|
||||
if first_cond {
|
||||
emit_line(inner + "if (" + cond_str + ") {")
|
||||
let first_cond = false
|
||||
} else {
|
||||
emit_line(inner + "} else if (" + cond_str + ") {")
|
||||
}
|
||||
emit_line(inner + " " + body_c + ";")
|
||||
} else {
|
||||
if str_eq(pkind, "LitInt") {
|
||||
let v: String = pat["value"]
|
||||
let cond_str = subj_var + " == " + v
|
||||
if first_cond {
|
||||
emit_line(inner + "if (" + cond_str + ") {")
|
||||
let first_cond = false
|
||||
} else {
|
||||
emit_line(inner + "} else if (" + cond_str + ") {")
|
||||
}
|
||||
emit_line(inner + " " + body_c + ";")
|
||||
} else {
|
||||
if str_eq(pkind, "LitBool") {
|
||||
let v: String = pat["value"]
|
||||
let bv = "0"
|
||||
if str_eq(v, "true") {
|
||||
let bv = "1"
|
||||
}
|
||||
let cond_str = subj_var + " == " + bv
|
||||
if first_cond {
|
||||
emit_line(inner + "if (" + cond_str + ") {")
|
||||
let first_cond = false
|
||||
} else {
|
||||
emit_line(inner + "} else if (" + cond_str + ") {")
|
||||
}
|
||||
emit_line(inner + " " + body_c + ";")
|
||||
} else {
|
||||
// Wildcard or Binding - becomes the else branch
|
||||
if first_cond {
|
||||
emit_line(inner + "{")
|
||||
} else {
|
||||
emit_line(inner + "} else {")
|
||||
}
|
||||
if str_eq(pkind, "Binding") {
|
||||
let bname: String = pat["name"]
|
||||
emit_line(inner + " el_val_t " + bname + " = " + subj_var + ";")
|
||||
}
|
||||
emit_line(inner + " " + body_c + ";")
|
||||
emit_line(inner + "}")
|
||||
let first_cond = true
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
// Close any open if/else-if chain (only reached when last arm was a literal pattern)
|
||||
if !first_cond {
|
||||
emit_line(inner + "}")
|
||||
}
|
||||
emit_line(indent + "}")
|
||||
}
|
||||
|
||||
// -- If-as-expression codegen -------------------------------------------------
|
||||
//
|
||||
// Lower `if cond { thenBody } else { elseBody }` used in expression position
|
||||
@@ -1250,6 +1170,16 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "Break" {
|
||||
emit_line(indent + "break;")
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "Continue" {
|
||||
emit_line(indent + "continue;")
|
||||
return declared
|
||||
}
|
||||
|
||||
// Bare reassignment: `name = expr`. Always emits a plain C assignment
|
||||
// (no `el_val_t` prefix) - by construction the parser only produces
|
||||
// Assign for an existing identifier. If the name happens NOT to be in
|
||||
@@ -1275,6 +1205,10 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
cg_for_stmt(val, indent, declared)
|
||||
return declared
|
||||
}
|
||||
if val_kind == "Match" {
|
||||
cg_match_stmt(val, indent, declared)
|
||||
return declared
|
||||
}
|
||||
let val_c: String = cg_expr(val)
|
||||
emit_line(indent + val_c + ";")
|
||||
return declared
|
||||
@@ -1302,28 +1236,34 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "ForRange" {
|
||||
let var_name: String = stmt["var"]
|
||||
let start_expr = stmt["start"]
|
||||
let end_expr = stmt["end"]
|
||||
let inclusive: Bool = stmt["inclusive"]
|
||||
let body = stmt["body"]
|
||||
let start_c: String = cg_expr(start_expr)
|
||||
let end_c: String = cg_expr(end_expr)
|
||||
// Loop variable introduced as a C local scoped to the for statement.
|
||||
// Body gets its own declared clone so let-bindings don't leak out.
|
||||
let body_decl = native_list_clone(declared)
|
||||
let body_decl = native_list_append(body_decl, var_name)
|
||||
if inclusive {
|
||||
emit_line(indent + "for (el_val_t " + var_name + " = " + start_c + "; " + var_name + " <= " + end_c + "; " + var_name + "++) {")
|
||||
} else {
|
||||
emit_line(indent + "for (el_val_t " + var_name + " = " + start_c + "; " + var_name + " < " + end_c + "; " + var_name + "++) {")
|
||||
}
|
||||
cg_stmts(body, indent + " ", body_decl)
|
||||
emit_line(indent + "}")
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "FnDef" { return declared }
|
||||
if kind == "TypeDef" { return declared }
|
||||
if kind == "EnumDef" { return declared }
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1948,6 +1888,12 @@ fn is_int_expr(expr: Map<String, Any>) -> Bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
if str_eq(op, "Percent") {
|
||||
if is_int_expr(expr["left"]) {
|
||||
if is_int_expr(expr["right"]) { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
@@ -2103,6 +2049,9 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "println") { return 1 }
|
||||
if str_eq(name, "print") { return 1 }
|
||||
if str_eq(name, "readline") { return 0 }
|
||||
// LSP seed primitives
|
||||
if str_eq(name, "__read_n") { return 1 }
|
||||
if str_eq(name, "__print_raw") { return 1 }
|
||||
// String
|
||||
if str_eq(name, "el_str_concat") { return 2 }
|
||||
if str_eq(name, "str_eq") { return 2 }
|
||||
@@ -2185,6 +2134,84 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "http_post_form_auth") { return 3 }
|
||||
if str_eq(name, "http_serve") { return 2 }
|
||||
if str_eq(name, "http_set_handler") { return 1 }
|
||||
// Seed primitives (__-prefix) — runtime/el_seed.c
|
||||
if str_eq(name, "__str_len") { return 1 }
|
||||
if str_eq(name, "__str_char_at") { return 2 }
|
||||
if str_eq(name, "__str_alloc") { return 1 }
|
||||
if str_eq(name, "__str_set_char") { return 3 }
|
||||
if str_eq(name, "__str_cmp") { return 2 }
|
||||
if str_eq(name, "__str_ncmp") { return 3 }
|
||||
if str_eq(name, "__str_concat_raw") { return 2 }
|
||||
if str_eq(name, "__str_slice_raw") { return 3 }
|
||||
if str_eq(name, "__int_to_str") { return 1 }
|
||||
if str_eq(name, "__str_to_int") { return 1 }
|
||||
if str_eq(name, "__float_to_str") { return 1 }
|
||||
if str_eq(name, "__str_to_float") { return 1 }
|
||||
if str_eq(name, "__println") { return 1 }
|
||||
if str_eq(name, "__print") { return 1 }
|
||||
if str_eq(name, "__readline") { return 0 }
|
||||
if str_eq(name, "__fs_read") { return 1 }
|
||||
if str_eq(name, "__fs_write") { return 2 }
|
||||
if str_eq(name, "__fs_exists") { return 1 }
|
||||
if str_eq(name, "__fs_list_raw") { return 1 }
|
||||
if str_eq(name, "__fs_mkdir") { return 1 }
|
||||
if str_eq(name, "__fs_write_bytes") { return 3 }
|
||||
if str_eq(name, "__http_do") { return 5 }
|
||||
if str_eq(name, "__http_do_map") { return 5 }
|
||||
if str_eq(name, "__http_do_to_file") { return 5 }
|
||||
if str_eq(name, "__http_serve") { return 2 }
|
||||
if str_eq(name, "__http_serve_v2") { return 2 }
|
||||
if str_eq(name, "__http_response") { return 3 }
|
||||
if str_eq(name, "__thread_create") { return 2 }
|
||||
if str_eq(name, "__thread_join") { return 1 }
|
||||
if str_eq(name, "__mutex_new") { return 0 }
|
||||
if str_eq(name, "__mutex_lock") { return 1 }
|
||||
if str_eq(name, "__mutex_unlock") { return 1 }
|
||||
if str_eq(name, "__exec") { return 1 }
|
||||
if str_eq(name, "__exec_bg") { return 1 }
|
||||
if str_eq(name, "__env_get") { return 1 }
|
||||
if str_eq(name, "__args_json") { return 0 }
|
||||
if str_eq(name, "__exit_program") { return 1 }
|
||||
if str_eq(name, "__time_now_ns") { return 0 }
|
||||
if str_eq(name, "__sleep_ms") { return 1 }
|
||||
if str_eq(name, "__uuid_v4") { return 0 }
|
||||
if str_eq(name, "__sqrt_f") { return 1 }
|
||||
if str_eq(name, "__log_f") { return 1 }
|
||||
if str_eq(name, "__ln_f") { return 1 }
|
||||
if str_eq(name, "__sin_f") { return 1 }
|
||||
if str_eq(name, "__cos_f") { return 1 }
|
||||
if str_eq(name, "__pi_f") { return 0 }
|
||||
if str_eq(name, "__state_set") { return 2 }
|
||||
if str_eq(name, "__state_get") { return 1 }
|
||||
if str_eq(name, "__state_del") { return 1 }
|
||||
if str_eq(name, "__state_keys") { return 0 }
|
||||
if str_eq(name, "__html_sanitize") { return 2 }
|
||||
if str_eq(name, "__url_encode") { return 1 }
|
||||
if str_eq(name, "__url_decode") { return 1 }
|
||||
if str_eq(name, "__json_get") { return 2 }
|
||||
if str_eq(name, "__json_get_raw") { return 2 }
|
||||
if str_eq(name, "__json_parse_map") { return 1 }
|
||||
if str_eq(name, "__json_stringify_val") { return 1 }
|
||||
if str_eq(name, "__json_array_len") { return 1 }
|
||||
if str_eq(name, "__json_array_get") { return 2 }
|
||||
if str_eq(name, "__json_array_get_string") { return 2 }
|
||||
if str_eq(name, "__json_set") { return 3 }
|
||||
if str_eq(name, "__engram_node") { return 3 }
|
||||
if str_eq(name, "__engram_node_full") { return 8 }
|
||||
if str_eq(name, "__engram_get_node") { return 1 }
|
||||
if str_eq(name, "__engram_strengthen") { return 1 }
|
||||
if str_eq(name, "__engram_forget") { return 1 }
|
||||
if str_eq(name, "__engram_node_count") { return 0 }
|
||||
if str_eq(name, "__engram_search") { return 2 }
|
||||
if str_eq(name, "__engram_scan_nodes") { return 2 }
|
||||
if str_eq(name, "__engram_connect") { return 4 }
|
||||
if str_eq(name, "__engram_edge_between") { return 2 }
|
||||
if str_eq(name, "__engram_neighbors") { return 1 }
|
||||
if str_eq(name, "__engram_neighbors_filtered") { return 3 }
|
||||
if str_eq(name, "__engram_activate") { return 2 }
|
||||
if str_eq(name, "__engram_activate_json") { return 2 }
|
||||
if str_eq(name, "__engram_scan_nodes_json") { return 2 }
|
||||
if str_eq(name, "__generate") { return 1 }
|
||||
// Filesystem
|
||||
if str_eq(name, "fs_read") { return 1 }
|
||||
if str_eq(name, "fs_write") { return 2 }
|
||||
@@ -2237,11 +2264,6 @@ 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 }
|
||||
@@ -2309,6 +2331,18 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "get") { return 2 }
|
||||
if str_eq(name, "map_get") { return 2 }
|
||||
if str_eq(name, "map_set") { return 3 }
|
||||
// Threading seed primitives
|
||||
if str_eq(name, "__thread_create") { return 2 }
|
||||
if str_eq(name, "__thread_join") { return 1 }
|
||||
if str_eq(name, "__mutex_new") { return 0 }
|
||||
if str_eq(name, "__mutex_lock") { return 1 }
|
||||
if str_eq(name, "__mutex_unlock") { return 1 }
|
||||
// Channel seed primitives
|
||||
if str_eq(name, "__channel_new") { return 1 }
|
||||
if str_eq(name, "__channel_send") { return 2 }
|
||||
if str_eq(name, "__channel_recv") { return 1 }
|
||||
if str_eq(name, "__channel_try_recv") { return 1 }
|
||||
if str_eq(name, "__channel_close") { return 1 }
|
||||
// -1 sentinel: variadic / unknown / user-defined -> no check.
|
||||
return -1
|
||||
}
|
||||
@@ -2566,7 +2600,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -2711,195 +2744,9 @@ 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.
|
||||
@@ -2957,15 +2804,9 @@ fn codegen_inner(stmts: [Map<String, Any>], source: String, test_mode: Bool, rep
|
||||
// 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
|
||||
@@ -3062,15 +2903,6 @@ fn codegen_inner(stmts: [Map<String, Any>], source: String, test_mode: Bool, rep
|
||||
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
|
||||
|
||||
+7
-336
@@ -29,7 +29,7 @@ fn compile(source: String) -> String {
|
||||
codegen(stmts, source)
|
||||
}
|
||||
|
||||
// compile_js — full pipeline (JS target, module mode): source string -> JS source string
|
||||
// compile_js — full pipeline (JS target): 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,38 +38,6 @@ 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
|
||||
@@ -80,19 +48,6 @@ 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 {
|
||||
@@ -136,168 +91,6 @@ 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"]
|
||||
@@ -456,85 +249,12 @@ 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 --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
|
||||
// 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
|
||||
fn main() -> Void {
|
||||
let argv: [String] = args()
|
||||
// Use `tgt` not `target`: `target` is a reserved keyword in the lexer
|
||||
@@ -542,30 +262,12 @@ 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] [--test] [--reporter=text|json] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]")
|
||||
println("el-compiler: usage: elc [--target=c|js] [--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
|
||||
@@ -581,38 +283,7 @@ fn main() -> Void {
|
||||
}
|
||||
|
||||
let source: String = resolve_imports(src_path)
|
||||
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)
|
||||
}
|
||||
let out: String = compile_dispatch(tgt, source)
|
||||
if argc >= 2 {
|
||||
let out_path: String = native_list_get(positional, 1)
|
||||
let ok: Bool = fs_write(out_path, out)
|
||||
|
||||
+278
-48
@@ -1,4 +1,4 @@
|
||||
// lexer.el — el self-hosting lexer
|
||||
// lexer.el - el self-hosting lexer
|
||||
//
|
||||
// Tokenises an el source string into a list of token maps.
|
||||
// Each token is a Map<String, Any> with keys:
|
||||
@@ -8,9 +8,9 @@
|
||||
// Entry point: fn lex(source: String) -> [Map<String, Any>]
|
||||
//
|
||||
// Uses native_string_chars to split the source into a chars list,
|
||||
// then indexes it with native_list_get — avoids O(N²) string cloning.
|
||||
// then indexes it with native_list_get - avoids O(N-) string cloning.
|
||||
|
||||
// ── Character helpers ─────────────────────────────────────────────────────────
|
||||
// -- Character helpers ---------------------------------------------------------
|
||||
|
||||
fn lex_is_digit(ch: String) -> Bool {
|
||||
if ch == "0" { return true }
|
||||
@@ -98,13 +98,10 @@ fn lex_is_whitespace(ch: String) -> Bool {
|
||||
}
|
||||
|
||||
fn make_tok(kind: String, value: String) -> Map<String, Any> {
|
||||
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 }
|
||||
{ "kind": kind, "value": value }
|
||||
}
|
||||
|
||||
// ── Keyword lookup ────────────────────────────────────────────────────────────
|
||||
// -- Keyword lookup ------------------------------------------------------------
|
||||
|
||||
fn keyword_kind(word: String) -> String {
|
||||
if word == "let" { return "Let" }
|
||||
@@ -150,15 +147,15 @@ 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" }
|
||||
if word == "break" { return "Break" }
|
||||
if word == "continue" { return "Continue" }
|
||||
""
|
||||
}
|
||||
|
||||
// ── Scan helpers ──────────────────────────────────────────────────────────────
|
||||
// -- Scan helpers --------------------------------------------------------------
|
||||
// All scan helpers receive the chars list and total length.
|
||||
|
||||
// scan_digits — advance i while chars[i] is a digit
|
||||
// scan_digits - advance i while chars[i] is a digit
|
||||
// Returns { "text": ..., "pos": i }
|
||||
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
@@ -180,7 +177,7 @@ fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// scan_ident — advance i while chars[i] is alphanumeric or underscore
|
||||
// scan_ident - advance i while chars[i] is alphanumeric or underscore
|
||||
fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
@@ -201,14 +198,14 @@ fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// ── Code-bearing string detection + comment strip ────────────────────────────
|
||||
// Inline JS/CSS literals embedded in El source (e.g. <script>…</script> blobs
|
||||
// -- Code-bearing string detection + comment strip ----------------------------
|
||||
// Inline JS/CSS literals embedded in El source (e.g. <script>-</script> blobs
|
||||
// or stylesheet payloads inside string literals) carry their own line and
|
||||
// block comments. Those comments leak into the served HTML and reveal build
|
||||
// notes the visitor should never see. We strip them at the lexer so every
|
||||
// downstream consumer (codegen-c, codegen-js, parser) gets the cleaned form.
|
||||
//
|
||||
// looks_like_code — heuristic gate so we only strip strings that actually
|
||||
// looks_like_code - heuristic gate so we only strip strings that actually
|
||||
// embed JS or CSS. Plain prose, hex blobs, JSON, etc. pass through verbatim.
|
||||
|
||||
fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool {
|
||||
@@ -250,7 +247,7 @@ fn looks_like_code(s: String) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
// strip_code_comments — character-by-character walk. Tracks JS string state
|
||||
// strip_code_comments - character-by-character walk. Tracks JS string state
|
||||
// (single, double, backtick) and never strips inside one. Backslash escapes
|
||||
// inside JS strings consume the next char verbatim. URLs like https:// are
|
||||
// preserved by checking the previous char before treating // as a line
|
||||
@@ -403,7 +400,7 @@ fn strip_code_comments(s: String) -> String {
|
||||
str_join(out_parts, "")
|
||||
}
|
||||
|
||||
// scan_string — scan a quoted string literal, handling \" escapes.
|
||||
// scan_string - scan a quoted string literal, handling \" escapes.
|
||||
// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close }
|
||||
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
@@ -463,25 +460,251 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// ── Main lexer ────────────────────────────────────────────────────────────────
|
||||
// -- String interpolation ------------------------------------------------------
|
||||
//
|
||||
// scan_interp_brace - scan from `start` (the char after `${`) to the matching
|
||||
// `}`, tracking brace depth so inner braces (e.g. fn calls, map literals) are
|
||||
// handled correctly. Returns { "text": inner_source, "pos": i_after_close }.
|
||||
fn scan_interp_brace(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let depth = 1
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if ch == "{" {
|
||||
let depth = depth + 1
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "}" {
|
||||
let depth = depth - 1
|
||||
if depth <= 0 {
|
||||
// Closing brace of the interpolation - stop, do not include it
|
||||
let i = i + 1
|
||||
let running = false
|
||||
} else {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// interp_tokens_append_all - copy every token from src into dst, skipping the
|
||||
// trailing Eof sentinel that lex() always appends. Returns the updated dst list.
|
||||
fn interp_tokens_append_all(dst: [Map<String, Any>], src: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||
let src_len: Int = native_list_len(src)
|
||||
let j = 0
|
||||
let result = dst
|
||||
while j < src_len {
|
||||
let tok: Map<String, Any> = native_list_get(src, j)
|
||||
let tk: String = tok["kind"]
|
||||
if tk == "Eof" {
|
||||
let j = src_len
|
||||
} else {
|
||||
let result = native_list_append(result, tok)
|
||||
let j = j + 1
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// scan_interp_string - scan a string literal that may contain ${expr}
|
||||
// interpolations. Starts AFTER the opening `"`.
|
||||
// Returns { "tokens": [token list to inject], "pos": i_after_close_quote }.
|
||||
//
|
||||
// For a plain string (no ${}) this emits a single Str token, identical to the
|
||||
// old scan_string path. For an interpolated string it emits a flat sequence
|
||||
// of tokens equivalent to the string-concat expression, for example:
|
||||
//
|
||||
// "hello ${name}!"
|
||||
// => Str("hello ") Plus <tokens for name> Plus Str("!")
|
||||
//
|
||||
// Empty literal segments between adjacent ${ } blocks are omitted. The
|
||||
// resulting token stream is consumed by the existing parse_binop / parse_primary
|
||||
// path in the parser with zero parser changes required.
|
||||
//
|
||||
// Supported escape sequences: \" \n \t \r \\ \$ (literal dollar sign).
|
||||
// Nested quotes inside ${} are not supported; use a variable instead.
|
||||
fn scan_interp_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let out_tokens: [Map<String, Any>] = native_list_empty()
|
||||
let cur_part: [String] = native_list_empty()
|
||||
let has_interp = false
|
||||
let need_plus = false
|
||||
let running = true
|
||||
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
|
||||
if ch == "\\" {
|
||||
// Escape sequence
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "$" {
|
||||
// \$ => literal '$' (escape for interpolation syntax)
|
||||
let cur_part = native_list_append(cur_part, "$")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "\"" {
|
||||
let cur_part = native_list_append(cur_part, "\"")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "n" {
|
||||
let cur_part = native_list_append(cur_part, "\n")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "t" {
|
||||
let cur_part = native_list_append(cur_part, "\t")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "r" {
|
||||
let cur_part = native_list_append(cur_part, "\r")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "\\" {
|
||||
let cur_part = native_list_append(cur_part, "\\")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
let cur_part = native_list_append(cur_part, next_ch)
|
||||
let i = next_i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "\"" {
|
||||
// Closing quote - stop scanning
|
||||
let i = i + 1
|
||||
let running = false
|
||||
} else {
|
||||
if ch == "$" {
|
||||
// Check for ${ (start of interpolation)
|
||||
let next_i = i + 1
|
||||
let is_interp = false
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "{" {
|
||||
let is_interp = true
|
||||
}
|
||||
}
|
||||
if is_interp {
|
||||
// Flush the accumulated literal part (if non-empty)
|
||||
let part_len: Int = native_list_len(cur_part)
|
||||
if part_len > 0 {
|
||||
let part_text = str_join(cur_part, "")
|
||||
if need_plus {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
|
||||
}
|
||||
let clean_part = part_text
|
||||
if looks_like_code(part_text) {
|
||||
let clean_part = strip_code_comments(part_text)
|
||||
}
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_part))
|
||||
let need_plus = true
|
||||
}
|
||||
let cur_part = native_list_empty()
|
||||
let has_interp = true
|
||||
|
||||
// Scan brace-balanced expression source
|
||||
let brace_result = scan_interp_brace(chars, next_i + 1, total)
|
||||
let expr_src: String = brace_result["text"]
|
||||
let new_i: Int = brace_result["pos"]
|
||||
let i = new_i
|
||||
|
||||
// Re-lex the expression and inline the tokens.
|
||||
// Wrap in ( ) so that operators inside ${} (e.g.
|
||||
// age + 1) are parsed as a grouped sub-expression
|
||||
// rather than merging with the surrounding concat
|
||||
// Plus tokens at the wrong precedence level.
|
||||
let inner_toks: [Map<String, Any>] = lex(expr_src)
|
||||
let inner_len: Int = native_list_len(inner_toks)
|
||||
|
||||
if need_plus {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
|
||||
}
|
||||
// Empty interpolation ${} => empty string segment
|
||||
if inner_len <= 1 {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", ""))
|
||||
} else {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("LParen", "("))
|
||||
let out_tokens = interp_tokens_append_all(out_tokens, inner_toks)
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("RParen", ")"))
|
||||
}
|
||||
let need_plus = true
|
||||
} else {
|
||||
// Plain '$' not followed by '{' - treat as literal
|
||||
let cur_part = native_list_append(cur_part, "$")
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let cur_part = native_list_append(cur_part, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining literal segment and build final token list
|
||||
let part_text = str_join(cur_part, "")
|
||||
let part_len: Int = native_list_len(cur_part)
|
||||
if has_interp {
|
||||
// Interpolated string: only emit trailing segment if non-empty
|
||||
if part_len > 0 {
|
||||
let clean_part = part_text
|
||||
if looks_like_code(part_text) {
|
||||
let clean_part = strip_code_comments(part_text)
|
||||
}
|
||||
if need_plus {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
|
||||
}
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_part))
|
||||
}
|
||||
} else {
|
||||
// Plain string with no interpolation - same behaviour as old scan_string
|
||||
let clean_text = part_text
|
||||
if looks_like_code(part_text) {
|
||||
let clean_text = strip_code_comments(part_text)
|
||||
}
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_text))
|
||||
}
|
||||
|
||||
{ "tokens": out_tokens, "pos": i }
|
||||
}
|
||||
|
||||
// -- Main lexer ----------------------------------------------------------------
|
||||
|
||||
fn lex(source: String) -> [Map<String, Any>] {
|
||||
let chars: [String] = native_string_chars(source)
|
||||
let total: Int = native_list_len(chars)
|
||||
let tokens: [Map<String, Any>] = native_list_empty()
|
||||
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; track newlines for line-number reporting
|
||||
// Skip whitespace
|
||||
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: //
|
||||
@@ -514,20 +737,16 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
// String literal
|
||||
// String literal (plain or interpolated with ${expr} syntax).
|
||||
// scan_interp_string handles both cases: plain strings emit a
|
||||
// single Str token; interpolated strings emit a flat token
|
||||
// sequence (Str Plus expr-tokens Plus Str ...) that the parser
|
||||
// naturally assembles into a BinOp concat tree.
|
||||
if ch == "\"" {
|
||||
let result = scan_string(chars, i + 1, total)
|
||||
let str_text: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
// Compile-time scrub: strings that embed JS or CSS get
|
||||
// their // line comments and /* block comments stripped
|
||||
// before the token reaches the parser. Plain prose passes
|
||||
// through untouched.
|
||||
let clean_text = str_text
|
||||
if looks_like_code(str_text) {
|
||||
let clean_text = strip_code_comments(str_text)
|
||||
}
|
||||
let tokens = native_list_append(tokens, make_tok("Str", clean_text))
|
||||
let interp_result = scan_interp_string(chars, i + 1, total)
|
||||
let interp_toks: [Map<String, Any>] = interp_result["tokens"]
|
||||
let new_pos: Int = interp_result["pos"]
|
||||
let tokens = interp_tokens_append_all(tokens, interp_toks)
|
||||
let i = new_pos
|
||||
} else {
|
||||
// Number literal
|
||||
@@ -707,8 +926,24 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "." {
|
||||
let tokens = native_list_append(tokens, make_tok("Dot", "."))
|
||||
let i = i + 1
|
||||
// Check for ..= (inclusive range) before .. (exclusive range) before single .
|
||||
let peek2_i = i + 2
|
||||
let peek2_ch = ""
|
||||
if peek2_i < total {
|
||||
let peek2_ch: String = native_list_get(chars, peek2_i)
|
||||
}
|
||||
if peek_ch == "." {
|
||||
if peek2_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("DotDotEq", "..="))
|
||||
let i = i + 3
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("DotDot", ".."))
|
||||
let i = i + 2
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Dot", "."))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == ";" {
|
||||
let tokens = native_list_append(tokens, make_tok("Semicolon", ";"))
|
||||
@@ -722,13 +957,8 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "#" {
|
||||
let tokens = native_list_append(tokens, make_tok("Hash", "#"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
// unknown char — skip
|
||||
let i = i + 1
|
||||
}
|
||||
// unknown char - skip
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-600
@@ -1,4 +1,4 @@
|
||||
// parser.el — el self-hosting recursive descent parser
|
||||
// parser.el - el self-hosting recursive descent parser
|
||||
//
|
||||
// Consumes the token list produced by lexer.el and builds a list of AST
|
||||
// statement maps. Each statement and expression is a Map<String, Any>.
|
||||
@@ -11,7 +11,7 @@
|
||||
//
|
||||
// Entry point: fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>]
|
||||
|
||||
// ── Token access helpers ──────────────────────────────────────────────────────
|
||||
// -- Token access helpers ------------------------------------------------------
|
||||
|
||||
fn tok_at(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
native_list_get(tokens, pos)
|
||||
@@ -27,15 +27,6 @@ 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 {
|
||||
@@ -45,13 +36,13 @@ fn expect(tokens: [Map<String, Any>], pos: Int, kind: String) -> Int {
|
||||
pos + 1
|
||||
}
|
||||
|
||||
// ── Result helpers ────────────────────────────────────────────────────────────
|
||||
// -- Result helpers ------------------------------------------------------------
|
||||
|
||||
fn make_result(node: Map<String, Any>, pos: Int) -> Map<String, Any> {
|
||||
{ "node": node, "pos": pos }
|
||||
}
|
||||
|
||||
// ── Type annotation parser ────────────────────────────────────────────────────
|
||||
// -- Type annotation parser ----------------------------------------------------
|
||||
// Skips over a type annotation, returning the new position.
|
||||
// Types can be: Ident, [Type], Map<K,V>, Type?, Type<Type,...>
|
||||
|
||||
@@ -109,8 +100,8 @@ fn skip_type(tokens: [Map<String, Any>], pos: Int) -> Int {
|
||||
pos + 1
|
||||
}
|
||||
|
||||
// ── Parameter list ────────────────────────────────────────────────────────────
|
||||
// Parses (name: Type, name: Type, ...) — returns { "params": [...], "pos": ... }
|
||||
// -- Parameter list ------------------------------------------------------------
|
||||
// Parses (name: Type, name: Type, ...) - returns { "params": [...], "pos": ... }
|
||||
|
||||
fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let p = expect(tokens, pos, "LParen")
|
||||
@@ -149,461 +140,7 @@ fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
{ "params": params, "pos": p }
|
||||
}
|
||||
|
||||
// ── 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)
|
||||
}
|
||||
// -- Expression parsing --------------------------------------------------------
|
||||
|
||||
fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let k = tok_kind(tokens, pos)
|
||||
@@ -629,22 +166,6 @@ 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)
|
||||
@@ -691,14 +212,14 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
//
|
||||
// Suppression: when parse_if / parse_while / parse_for / parse_match
|
||||
// are parsing a head expression, they set __no_block_expr=1 so a stray
|
||||
// `{` here doesn't get gobbled as a Map literal — it belongs to the
|
||||
// `{` here doesn't get gobbled as a Map literal - it belongs to the
|
||||
// following block. Without this, `if a || b { ... }` could mis-parse
|
||||
// (the `||` recursion lands at `{` and tries to read the if-body as a
|
||||
// Map, then loops forever when keys don't match `Str: expr`).
|
||||
if k == "LBrace" {
|
||||
let no_block: String = state_get("__no_block_expr")
|
||||
if str_eq(no_block, "1") {
|
||||
// Fall through to fallback — caller will see `{` and treat it
|
||||
// Fall through to fallback - caller will see `{` and treat it
|
||||
// as the start of the block they're expecting.
|
||||
return make_result({ "expr": "Nil" }, pos)
|
||||
}
|
||||
@@ -758,30 +279,6 @@ 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)
|
||||
@@ -803,7 +300,7 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
// token kinds for the deploy/retry DSLs, but they're also valid as
|
||||
// parameter names and local variables. When one of these appears in
|
||||
// expression position (where only an Ident makes sense), treat it as
|
||||
// an Ident carrying the original text — otherwise references to a
|
||||
// an Ident carrying the original text - otherwise references to a
|
||||
// parameter named `target` compile to EL_NULL.
|
||||
if k == "Target" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
||||
if k == "To" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
||||
@@ -911,13 +408,6 @@ 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" {
|
||||
@@ -982,9 +472,9 @@ fn parse_block(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
{ "stmts": stmts, "pos": p }
|
||||
}
|
||||
|
||||
// ── Postfix expressions (calls, field access, index) ─────────────────────────
|
||||
// -- Postfix expressions (calls, field access, index) -------------------------
|
||||
|
||||
// is_duration_unit — recognise the postfix unit suffix on a numeric literal.
|
||||
// is_duration_unit - recognise the postfix unit suffix on a numeric literal.
|
||||
// Used by parse_postfix to detect `30.seconds`-shape time literals before
|
||||
// falling back to the generic `obj.field` field-access lowering. Singular
|
||||
// and plural forms map to the same nanosecond multiplier; codegen does the
|
||||
@@ -1088,7 +578,7 @@ fn parse_postfix(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
make_result(node, p)
|
||||
}
|
||||
|
||||
// ── Binary expression precedence climbing ────────────────────────────────────
|
||||
// -- Binary expression precedence climbing ------------------------------------
|
||||
|
||||
fn op_precedence(kind: String) -> Int {
|
||||
if kind == "Or" { return 1 }
|
||||
@@ -1103,6 +593,7 @@ fn op_precedence(kind: String) -> Int {
|
||||
if kind == "Minus" { return 5 }
|
||||
if kind == "Star" { return 6 }
|
||||
if kind == "Slash" { return 6 }
|
||||
if kind == "Percent" { return 6 }
|
||||
0
|
||||
}
|
||||
|
||||
@@ -1119,6 +610,7 @@ fn is_binop(kind: String) -> Bool {
|
||||
if kind == "Minus" { return true }
|
||||
if kind == "Star" { return true }
|
||||
if kind == "Slash" { return true }
|
||||
if kind == "Percent" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
@@ -1151,7 +643,7 @@ fn parse_expr(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
parse_binop(tokens, pos, 1)
|
||||
}
|
||||
|
||||
// ── Statement parsing ─────────────────────────────────────────────────────────
|
||||
// -- Statement parsing ---------------------------------------------------------
|
||||
|
||||
fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let k = tok_kind(tokens, pos)
|
||||
@@ -1163,7 +655,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let p = p + 1
|
||||
let ltype = ""
|
||||
let k2 = tok_kind(tokens, p)
|
||||
// optional type annotation: name: Type — capture the leading
|
||||
// optional type annotation: name: Type - capture the leading
|
||||
// identifier so codegen can dispatch arithmetic vs concat on
|
||||
// `+` between two typed Idents.
|
||||
if k2 == "Colon" {
|
||||
@@ -1197,7 +689,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
return make_result({ "stmt": "Return", "value": val }, p)
|
||||
}
|
||||
|
||||
// extern fn declaration (no body — forward declaration for separate compilation)
|
||||
// extern fn declaration (no body - forward declaration for separate compilation)
|
||||
if k == "Extern" {
|
||||
let p = pos + 1
|
||||
let k2: String = tok_kind(tokens, p)
|
||||
@@ -1247,16 +739,11 @@ 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 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.
|
||||
// type definition
|
||||
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
|
||||
@@ -1373,7 +860,17 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
return make_result({ "stmt": "While", "cond": cond, "body": body }, p)
|
||||
}
|
||||
|
||||
// for loop
|
||||
// break statement
|
||||
if k == "Break" {
|
||||
return make_result({ "stmt": "Break" }, pos + 1)
|
||||
}
|
||||
|
||||
// continue statement
|
||||
if k == "Continue" {
|
||||
return make_result({ "stmt": "Continue" }, pos + 1)
|
||||
}
|
||||
|
||||
// for loop (range or list iteration)
|
||||
if k == "For" {
|
||||
let p = pos + 1
|
||||
let item_name = tok_value(tokens, p)
|
||||
@@ -1383,49 +880,41 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
state_set("__no_block_expr", "1")
|
||||
let r = parse_expr(tokens, p)
|
||||
state_set("__no_block_expr", prev_no_block)
|
||||
let list_expr = r["node"]
|
||||
let start_expr = r["node"]
|
||||
let p = r["pos"]
|
||||
// Check for range operator: .. (exclusive) or ..= (inclusive)
|
||||
let range_k = tok_kind(tokens, p)
|
||||
if range_k == "DotDot" {
|
||||
// exclusive range: for i in start..end
|
||||
let p = p + 1
|
||||
let r2 = parse_expr(tokens, p)
|
||||
let end_expr = r2["node"]
|
||||
let p = r2["pos"]
|
||||
let r3 = parse_block(tokens, p)
|
||||
let body = r3["stmts"]
|
||||
let p = r3["pos"]
|
||||
return make_result({ "stmt": "ForRange", "var": item_name, "start": start_expr, "end": end_expr, "inclusive": false, "body": body }, p)
|
||||
}
|
||||
if range_k == "DotDotEq" {
|
||||
// inclusive range: for i in start..=end
|
||||
let p = p + 1
|
||||
let r2 = parse_expr(tokens, p)
|
||||
let end_expr = r2["node"]
|
||||
let p = r2["pos"]
|
||||
let r3 = parse_block(tokens, p)
|
||||
let body = r3["stmts"]
|
||||
let p = r3["pos"]
|
||||
return make_result({ "stmt": "ForRange", "var": item_name, "start": start_expr, "end": end_expr, "inclusive": true, "body": body }, p)
|
||||
}
|
||||
// No range operator: regular for-in (list iteration)
|
||||
let list_expr = start_expr
|
||||
let r2 = parse_block(tokens, p)
|
||||
let body = r2["stmts"]
|
||||
let p = r2["pos"]
|
||||
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
|
||||
// @decorator - capture decorator name and attach to following stmt
|
||||
if k == "At" {
|
||||
let p = pos + 1
|
||||
let dec_name = tok_value(tokens, p)
|
||||
@@ -1563,35 +1052,6 @@ 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`
|
||||
@@ -1617,7 +1077,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
make_result({ "stmt": "Expr", "value": val }, p)
|
||||
}
|
||||
|
||||
// ── Top-level parse ────────────────────────────────────────────────────────────
|
||||
// -- Top-level parse ------------------------------------------------------------
|
||||
|
||||
fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||
let total: Int = native_list_len(tokens)
|
||||
@@ -1636,7 +1096,7 @@ fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||
let stmt = r["node"]
|
||||
let new_pos: Int = r["pos"]
|
||||
let stmts = native_list_append(stmts, stmt)
|
||||
// Guard against infinite loops — if pos didn't advance, force it
|
||||
// Guard against infinite loops - if pos didn't advance, force it
|
||||
if new_pos <= pos {
|
||||
let pos = pos + 1
|
||||
} else {
|
||||
|
||||
@@ -262,7 +262,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
|
||||
let parts: [String] = native_list_empty()
|
||||
// Include both the runtime dir (for el_runtime.h) and the output dir
|
||||
// (for module.elh cross-module forward declarations).
|
||||
let parts = native_list_append(parts, "cc -O2 -I " + dirname_of(runtime_path) + " -I " + out_dir)
|
||||
let parts = native_list_append(parts, "cc -O2 -fbracket-depth=1024 -I " + dirname_of(runtime_path) + " -I " + out_dir)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let f: String = native_list_get(c_files, i)
|
||||
@@ -270,7 +270,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
|
||||
let i = i + 1
|
||||
}
|
||||
let parts = native_list_append(parts, runtime_path)
|
||||
let parts = native_list_append(parts, "-lcurl -lpthread")
|
||||
let parts = native_list_append(parts, "-lcurl -lpthread -lm")
|
||||
let parts = native_list_append(parts, "-o " + out_bin)
|
||||
let cmd: String = str_join(parts, " ")
|
||||
println(" link " + out_bin)
|
||||
|
||||
+285
-2488
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
// epm — El Package Manager
|
||||
//
|
||||
// Manages vessels: publish, install, resolve dependencies.
|
||||
// Vessels are stored in Engram as nodes. epm reads the local manifest.el,
|
||||
// talks to Engram over HTTP, and writes installed vessels to .epm/vessels/.
|
||||
|
||||
package "epm" {
|
||||
version "0.1.0"
|
||||
description "El Package Manager — vessel registry, publish, install, resolve"
|
||||
authors ["Will Anderson <will@neurontechnologies.ai>"]
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/epm.el"
|
||||
output "dist/"
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
// epm/src/epm.el — El Package Manager entry point
|
||||
//
|
||||
// epm manages vessels: the deployable, versioned units of El code.
|
||||
// Vessels are stored in Engram (the graph database) and installed locally
|
||||
// under .epm/vessels/.
|
||||
//
|
||||
// Usage:
|
||||
// epm publish # publish current project to registry
|
||||
// epm install # install all dependencies from manifest.el
|
||||
// epm install <vessel> # install a specific vessel
|
||||
// epm list # list all vessels in the registry
|
||||
// epm info <vessel> # show metadata for a vessel
|
||||
// epm info <vessel> <version> # show metadata for a specific version
|
||||
//
|
||||
// Configuration (environment):
|
||||
// ENGRAM_URL — Engram base URL (default: http://localhost:8742)
|
||||
//
|
||||
// Import order: manifest.el, registry.el, install.el must be compiled first.
|
||||
// This file imports all three and is the build entry point.
|
||||
|
||||
import "manifest.el"
|
||||
import "registry.el"
|
||||
import "install.el"
|
||||
|
||||
// ── Subcommand handlers ───────────────────────────────────────────────────────
|
||||
|
||||
// cmd_publish reads manifest.el from the current directory and publishes
|
||||
// the vessel/package to the Engram registry.
|
||||
fn cmd_publish() -> Void {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
println("epm: no manifest.el found in current directory")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let name: String = manifest_name(src)
|
||||
if str_eq(name, "") {
|
||||
println("epm: manifest.el has no 'vessel' or 'package' declaration")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let version: String = manifest_version(src)
|
||||
if str_eq(version, "") {
|
||||
println("epm: manifest.el has no 'version' declaration")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let description: String = manifest_description(src)
|
||||
let entry: String = manifest_entry(src)
|
||||
let deps_json: String = manifest_deps(src)
|
||||
|
||||
println("epm: publishing " + name + " " + version + " ...")
|
||||
|
||||
let node_id: String = registry_publish(name, version, description, entry, deps_json)
|
||||
if str_eq(node_id, "") {
|
||||
println("epm: publish failed")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println("epm: published " + name + " " + version + " (node: " + node_id + ")")
|
||||
}
|
||||
|
||||
// cmd_install_named installs a specific vessel by name (with optional version).
|
||||
// Expects: install <name> or install <name>@<version>
|
||||
fn cmd_install_named(spec: String) -> Void {
|
||||
// spec may be "el-auth" or "el-auth@0.1.0"
|
||||
let at: Int = str_index_of(spec, "@")
|
||||
let vname: String = spec
|
||||
let vver: String = ""
|
||||
if at > 0 {
|
||||
let vname = str_slice(spec, 0, at)
|
||||
let vver = str_slice(spec, at + 1, str_len(spec))
|
||||
}
|
||||
|
||||
println("epm: installing " + vname + " ...")
|
||||
|
||||
let mk1: Int = exec_command("mkdir -p .epm/vessels")
|
||||
let ok: Bool = install_vessel(vname, vver)
|
||||
if !ok {
|
||||
println("epm: install failed")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// cmd_list prints all vessels registered in Engram.
|
||||
fn cmd_list() -> Void {
|
||||
println("epm: fetching vessel list from registry ...")
|
||||
let raw: String = registry_list()
|
||||
let n: Int = json_array_len(raw)
|
||||
|
||||
if n == 0 {
|
||||
println("epm: no vessels found in registry")
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: " + native_int_to_str(n) + " vessel(s) in registry:")
|
||||
println("")
|
||||
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
// Each element is a quoted+escaped content JSON blob
|
||||
let elem: String = json_array_get_string(raw, i)
|
||||
let vname: String = json_get_string(elem, "name")
|
||||
let vver: String = json_get_string(elem, "version")
|
||||
let vdesc: String = json_get_string(elem, "description")
|
||||
println(" " + vname + " " + vver + " — " + vdesc)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// cmd_info prints detailed metadata for a specific vessel.
|
||||
// argv is the full args list; idx is the position of the vessel name.
|
||||
fn cmd_info(argv: [String], idx: Int) -> Void {
|
||||
let argc: Int = native_list_len(argv)
|
||||
if idx >= argc {
|
||||
println("epm: usage: epm info <vessel> [version]")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let vname: String = native_list_get(argv, idx)
|
||||
let vver: String = ""
|
||||
if idx + 1 < argc {
|
||||
let vver = native_list_get(argv, idx + 1)
|
||||
}
|
||||
|
||||
let content: String = registry_find(vname, vver)
|
||||
if str_eq(content, "") {
|
||||
println("epm: vessel '" + vname + "' not found in registry")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let name: String = json_get_string(content, "name")
|
||||
let ver: String = json_get_string(content, "version")
|
||||
let desc: String = json_get_string(content, "description")
|
||||
let entry: String = json_get_string(content, "entry")
|
||||
let deps_raw: String = json_get_raw(content, "deps")
|
||||
|
||||
println("name: " + name)
|
||||
println("version: " + ver)
|
||||
println("description: " + desc)
|
||||
println("entry: " + entry)
|
||||
|
||||
let dep_n: Int = json_array_len(deps_raw)
|
||||
if dep_n == 0 {
|
||||
println("dependencies: (none)")
|
||||
} else {
|
||||
println("dependencies:")
|
||||
let di: Int = 0
|
||||
while di < dep_n {
|
||||
let dep: String = json_array_get(deps_raw, di)
|
||||
let dep_name: String = json_get_string(dep, "name")
|
||||
let dep_ver: String = json_get_string(dep, "version")
|
||||
println(" " + dep_name + " " + dep_ver)
|
||||
let di = di + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() -> Void {
|
||||
let argv: [String] = args()
|
||||
let argc: Int = native_list_len(argv)
|
||||
|
||||
// argv[0] is the program name; subcommand is argv[1]
|
||||
if argc < 2 {
|
||||
println("epm — El Package Manager")
|
||||
println("")
|
||||
println("usage:")
|
||||
println(" epm publish publish current project to registry")
|
||||
println(" epm install install all dependencies (from manifest.el)")
|
||||
println(" epm install <vessel> install a specific vessel")
|
||||
println(" epm install <vessel>@<ver> install a specific vessel at a version")
|
||||
println(" epm list list all vessels in the registry")
|
||||
println(" epm info <vessel> show vessel metadata")
|
||||
println(" epm info <vessel> <version> show metadata for a specific version")
|
||||
println("")
|
||||
println("configuration:")
|
||||
println(" ENGRAM_URL Engram base URL (default: http://localhost:8742)")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
let sub: String = native_list_get(argv, 1)
|
||||
|
||||
if str_eq(sub, "publish") {
|
||||
cmd_publish()
|
||||
return
|
||||
}
|
||||
|
||||
if str_eq(sub, "install") {
|
||||
if argc >= 3 {
|
||||
let spec: String = native_list_get(argv, 2)
|
||||
cmd_install_named(spec)
|
||||
} else {
|
||||
cmd_install()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if str_eq(sub, "list") {
|
||||
cmd_list()
|
||||
return
|
||||
}
|
||||
|
||||
if str_eq(sub, "info") {
|
||||
cmd_info(argv, 2)
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: unknown subcommand '" + sub + "'")
|
||||
println("run 'epm' with no arguments for usage")
|
||||
exit(1)
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// epm/src/install.el — dependency resolution and vessel installation
|
||||
//
|
||||
// Reads the current project's manifest.el, walks the full dependency graph
|
||||
// by fetching each dep from Engram, detects cycles, and produces a
|
||||
// topological install order (dependencies before dependents).
|
||||
//
|
||||
// Installed vessels land in: .epm/vessels/<name>/
|
||||
//
|
||||
// Cycle detection: each recursion carries a chain of names currently on
|
||||
// the call stack. If a dep already appears in the chain it's a cycle.
|
||||
//
|
||||
// Algorithm:
|
||||
// 1. Parse manifest.el to get direct deps
|
||||
// 2. For each dep, fetch its manifest from Engram
|
||||
// 3. Recurse into that dep's own deps (depth-first)
|
||||
// 4. Append dep to order after all its transitive deps
|
||||
// 5. Deduplicate: skip already-ordered vessels
|
||||
|
||||
// ── List helpers (operating on String lists) ──────────────────────────────────
|
||||
|
||||
// list_contains returns true if item appears in lst.
|
||||
fn list_contains(lst: [String], item: String) -> Bool {
|
||||
let n: Int = native_list_len(lst)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let v: String = native_list_get(lst, i)
|
||||
if str_eq(v, item) { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ── Dependency graph walker ───────────────────────────────────────────────────
|
||||
|
||||
// resolve_deps_recursive fetches a vessel's metadata from Engram and walks
|
||||
// its dependency list depth-first, building a topological order.
|
||||
//
|
||||
// Parameters:
|
||||
// name — vessel name to resolve
|
||||
// version — version string (may be "" for any)
|
||||
// chain — names currently on the recursion stack (cycle detection)
|
||||
// visited — names already appended to order (dedup)
|
||||
// order — accumulated result (list of "name:version" strings)
|
||||
//
|
||||
// Returns a Map with keys "visited", "order", and "ok" (Bool as String).
|
||||
fn resolve_deps_recursive(name: String, version: String, chain: [String], visited: [String], order: [String]) -> Map<String, Any> {
|
||||
let key: String = name + ":" + version
|
||||
|
||||
// Skip if already ordered
|
||||
if list_contains(visited, key) {
|
||||
return { "visited": visited, "order": order, "ok": "true" }
|
||||
}
|
||||
|
||||
// Cycle detection: if name is already in the call chain, it's a cycle
|
||||
if list_contains(chain, name) {
|
||||
println("epm: error: dependency cycle detected involving '" + name + "'")
|
||||
return { "visited": visited, "order": order, "ok": "false" }
|
||||
}
|
||||
|
||||
// Push this name onto the cycle-detection chain
|
||||
let chain = native_list_append(chain, name)
|
||||
|
||||
// Fetch vessel metadata from Engram
|
||||
let content: String = registry_find(name, version)
|
||||
if str_eq(content, "") {
|
||||
println("epm: error: vessel '" + name + "' version '" + version + "' not found in registry")
|
||||
return { "visited": visited, "order": order, "ok": "false" }
|
||||
}
|
||||
|
||||
// content is a JSON blob: {"name":..., "version":..., "deps":[...]}
|
||||
let actual_version: String = json_get_string(content, "version")
|
||||
let actual_key: String = name + ":" + actual_version
|
||||
let deps_json: String = json_get_raw(content, "deps")
|
||||
|
||||
// Walk this vessel's dependencies first (depth-first)
|
||||
let dep_count: Int = json_array_len(deps_json)
|
||||
let di: Int = 0
|
||||
while di < dep_count {
|
||||
let dep: String = json_array_get(deps_json, di)
|
||||
let dep_name: String = json_get_string(dep, "name")
|
||||
let dep_ver: String = json_get_string(dep, "version")
|
||||
|
||||
let r = resolve_deps_recursive(dep_name, dep_ver, chain, visited, order)
|
||||
let ok_str: String = r["ok"]
|
||||
if str_eq(ok_str, "false") {
|
||||
return r
|
||||
}
|
||||
let visited = r["visited"]
|
||||
let order = r["order"]
|
||||
|
||||
let di = di + 1
|
||||
}
|
||||
|
||||
// Now append this vessel (after all its deps)
|
||||
let visited = native_list_append(visited, actual_key)
|
||||
let order = native_list_append(order, actual_key)
|
||||
|
||||
return { "visited": visited, "order": order, "ok": "true" }
|
||||
}
|
||||
|
||||
// ── Install order ─────────────────────────────────────────────────────────────
|
||||
|
||||
// resolve_install_order reads manifest.el from the current directory and
|
||||
// returns a list of "name:version" strings in topological install order.
|
||||
//
|
||||
// Returns an empty list on failure (error is printed to stdout).
|
||||
fn resolve_install_order() -> [String] {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
println("epm: no manifest.el found in current directory")
|
||||
return native_list_empty()
|
||||
}
|
||||
|
||||
let deps_json: String = manifest_deps(src)
|
||||
let dep_count: Int = json_array_len(deps_json)
|
||||
if dep_count == 0 {
|
||||
println("epm: no dependencies declared in manifest.el")
|
||||
return native_list_empty()
|
||||
}
|
||||
|
||||
let chain: [String] = native_list_empty()
|
||||
let visited: [String] = native_list_empty()
|
||||
let order: [String] = native_list_empty()
|
||||
|
||||
let i: Int = 0
|
||||
while i < dep_count {
|
||||
let dep: String = json_array_get(deps_json, i)
|
||||
let dep_name: String = json_get_string(dep, "name")
|
||||
let dep_ver: String = json_get_string(dep, "version")
|
||||
|
||||
let r = resolve_deps_recursive(dep_name, dep_ver, chain, visited, order)
|
||||
let ok_str: String = r["ok"]
|
||||
if str_eq(ok_str, "false") {
|
||||
return native_list_empty()
|
||||
}
|
||||
let visited = r["visited"]
|
||||
let order = r["order"]
|
||||
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
// ── Installation ──────────────────────────────────────────────────────────────
|
||||
|
||||
// install_vessel fetches a vessel's source from Engram and writes it
|
||||
// to .epm/vessels/<name>/.
|
||||
//
|
||||
// Currently writes the content JSON blob as a manifest record so that
|
||||
// other tools can introspect what is installed. When Engram stores actual
|
||||
// source archives, this function would extract them.
|
||||
//
|
||||
// Returns true on success.
|
||||
fn install_vessel(name: String, version: String) -> Bool {
|
||||
let content: String = registry_find(name, version)
|
||||
if str_eq(content, "") {
|
||||
println("epm: error: cannot fetch '" + name + "' '" + version + "' from registry")
|
||||
return false
|
||||
}
|
||||
|
||||
let install_dir: String = ".epm/vessels/" + name
|
||||
let mkdir_ret: Int = exec_command("mkdir -p " + install_dir)
|
||||
|
||||
// Write the content blob as installed.json so downstream tools know what's there
|
||||
let dest: String = install_dir + "/installed.json"
|
||||
fs_write(dest, content)
|
||||
|
||||
println("epm: installed " + name + " " + version + " -> " + dest)
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Top-level install ─────────────────────────────────────────────────────────
|
||||
|
||||
// cmd_install resolves and installs all dependencies for the current project.
|
||||
fn cmd_install() -> Void {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
println("epm: no manifest.el found in current directory")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let pkg_name: String = manifest_name(src)
|
||||
println("epm: resolving dependencies for " + pkg_name)
|
||||
|
||||
let order: [String] = resolve_install_order()
|
||||
let n: Int = native_list_len(order)
|
||||
|
||||
if n == 0 {
|
||||
println("epm: nothing to install")
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: install order (" + native_int_to_str(n) + " vessels):")
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let entry: String = native_list_get(order, i)
|
||||
println(" " + entry)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Ensure .epm directory exists
|
||||
let mk2: Int = exec_command("mkdir -p .epm/vessels")
|
||||
|
||||
// Install each vessel in topological order
|
||||
let j: Int = 0
|
||||
while j < n {
|
||||
let kv: String = native_list_get(order, j)
|
||||
// Split "name:version" on the last colon
|
||||
let colon: Int = str_index_of(kv, ":")
|
||||
if colon < 0 {
|
||||
println("epm: error: malformed order entry: " + kv)
|
||||
exit(1)
|
||||
}
|
||||
let vname: String = str_slice(kv, 0, colon)
|
||||
let vver: String = str_slice(kv, colon + 1, str_len(kv))
|
||||
let ok: Bool = install_vessel(vname, vver)
|
||||
if !ok {
|
||||
println("epm: install failed at " + kv)
|
||||
exit(1)
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
println("epm: all dependencies installed")
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// epm/src/manifest.el — vessel and package manifest parser
|
||||
//
|
||||
// Parses manifest.el files in the vessel/package format:
|
||||
//
|
||||
// vessel "el-auth" {
|
||||
// version "0.1.0"
|
||||
// description "..."
|
||||
// authors ["..."]
|
||||
// edition "2026"
|
||||
// }
|
||||
//
|
||||
// dependencies {
|
||||
// el-platform "1.0"
|
||||
// el-identity "0.1"
|
||||
// }
|
||||
//
|
||||
// build {
|
||||
// entry "src/main.el"
|
||||
// output "dist/"
|
||||
// }
|
||||
//
|
||||
// Parsing is line-by-line string extraction, same approach as elb.el.
|
||||
// No AST, no grammar — sufficient for well-formed manifests.
|
||||
|
||||
// ── String extraction helpers ─────────────────────────────────────────────────
|
||||
|
||||
// extract_quoted extracts the first double-quoted token from a string.
|
||||
// Returns "" if no quoted token is found.
|
||||
// Example: extract_quoted("vessel \"el-auth\" {") -> "el-auth"
|
||||
fn extract_quoted(s: String) -> String {
|
||||
let first: Int = str_index_of(s, "\"")
|
||||
if first < 0 { return "" }
|
||||
let after: String = str_slice(s, first + 1, str_len(s))
|
||||
let close: Int = str_index_of(after, "\"")
|
||||
if close < 0 { return "" }
|
||||
return str_slice(after, 0, close)
|
||||
}
|
||||
|
||||
// ── Manifest field extraction ─────────────────────────────────────────────────
|
||||
|
||||
// manifest_name extracts the vessel or package name from manifest source.
|
||||
// Recognises both "vessel" and "package" block headers.
|
||||
fn manifest_name(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "vessel ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
if str_starts_with(t, "package ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_version extracts the version string from manifest source.
|
||||
fn manifest_version(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "version ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_description extracts the description string from manifest source.
|
||||
fn manifest_description(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "description ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_entry extracts the build entry file from manifest source.
|
||||
fn manifest_entry(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "entry ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_deps extracts dependencies as a JSON array of {"name":..., "version":...} objects.
|
||||
//
|
||||
// Scans lines inside the dependencies { ... } block.
|
||||
// Each dep line looks like:
|
||||
// el-platform "1.0"
|
||||
//
|
||||
// Returns a JSON array string, e.g.:
|
||||
// [{"name":"el-platform","version":"1.0"},{"name":"el-identity","version":"0.1"}]
|
||||
fn manifest_deps(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let in_deps: Bool = false
|
||||
let result: String = "["
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
|
||||
// Detect entry into dependencies block
|
||||
if str_eq(t, "dependencies {") {
|
||||
let in_deps = true
|
||||
let i = i + 1
|
||||
} else {
|
||||
if in_deps {
|
||||
// Detect exit from block
|
||||
if str_eq(t, "}") {
|
||||
let in_deps = false
|
||||
} else {
|
||||
// Each dep line: name "version"
|
||||
// Find first space to split name from quoted version
|
||||
let sp: Int = str_index_of(t, " ")
|
||||
if sp > 0 {
|
||||
let dep_name: String = str_slice(t, 0, sp)
|
||||
let rest: String = str_trim(str_slice(t, sp, str_len(t)))
|
||||
let dep_ver: String = extract_quoted(rest)
|
||||
if !str_eq(dep_name, "") {
|
||||
let entry: String = "{\"name\":\"" + dep_name + "\",\"version\":\"" + dep_ver + "\"}"
|
||||
if count == 0 {
|
||||
let result = result + entry
|
||||
} else {
|
||||
let result = result + "," + entry
|
||||
}
|
||||
let count = count + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
return result + "]"
|
||||
}
|
||||
|
||||
// manifest_is_vessel returns true if the manifest declares a vessel (not a package).
|
||||
fn manifest_is_vessel(src: String) -> Bool {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "vessel ") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// epm/src/registry.el — Engram-backed vessel registry
|
||||
//
|
||||
// Vessels are stored in Engram as nodes with a structured label scheme:
|
||||
//
|
||||
// label: "vessel:<name>:<version>" (used as the search key)
|
||||
// content: JSON blob with full vessel metadata
|
||||
// node_type: "Entity"
|
||||
// salience: 0.9 (vessels are high-salience, long-lived knowledge)
|
||||
//
|
||||
// All registry operations go over HTTP to Engram. The Engram URL is read
|
||||
// from the ENGRAM_URL environment variable; defaults to http://localhost:8742.
|
||||
//
|
||||
// Endpoints used:
|
||||
// POST /api/nodes — publish a vessel node
|
||||
// GET /api/search?q=... — find vessels by label prefix
|
||||
|
||||
// ── Engram URL ────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_url returns the base URL for Engram, with no trailing slash.
|
||||
fn registry_url() -> String {
|
||||
let u: String = config("ENGRAM_URL")
|
||||
if str_eq(u, "") { return "http://localhost:8742" }
|
||||
// Strip trailing slash if present
|
||||
let n: Int = str_len(u)
|
||||
if str_ends_with(u, "/") {
|
||||
return str_slice(u, 0, n - 1)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// ── Publish ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_publish stores a vessel node in Engram.
|
||||
//
|
||||
// Parameters:
|
||||
// name — vessel name (e.g. "el-auth")
|
||||
// version — semver string (e.g. "0.1.0")
|
||||
// description — human-readable description
|
||||
// entry — build entry file path
|
||||
// deps_json — JSON array of dep objects (from manifest_deps)
|
||||
//
|
||||
// Returns the created node ID on success, "" on failure.
|
||||
fn registry_publish(name: String, version: String, description: String, entry: String, deps_json: String) -> String {
|
||||
let label: String = "vessel:" + name + ":" + version
|
||||
|
||||
// Build the content JSON — all vessel metadata in one blob
|
||||
let esc_name: String = json_escape_string(name)
|
||||
let esc_ver: String = json_escape_string(version)
|
||||
let esc_desc: String = json_escape_string(description)
|
||||
let esc_entry: String = json_escape_string(entry)
|
||||
let content_json: String = "{\"name\":\"" + esc_name + "\",\"version\":\"" + esc_ver + "\",\"description\":\"" + esc_desc + "\",\"entry\":\"" + esc_entry + "\",\"deps\":" + deps_json + "}"
|
||||
|
||||
// Escape the content blob for embedding in the outer JSON
|
||||
let esc_label: String = json_escape_string(label)
|
||||
let esc_content: String = json_escape_string(content_json)
|
||||
|
||||
let body: String = "{\"label\":\"" + esc_label + "\",\"content\":\"" + esc_content + "\",\"node_type\":\"Entity\",\"salience\":0.9}"
|
||||
let url: String = registry_url() + "/api/nodes"
|
||||
|
||||
let resp: String = http_post_json(url, body)
|
||||
if str_eq(resp, "") {
|
||||
println("epm: error: Engram unreachable at " + url)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check for error in response
|
||||
let err: String = json_get_string(resp, "error")
|
||||
if !str_eq(err, "") {
|
||||
println("epm: error from Engram: " + err)
|
||||
return ""
|
||||
}
|
||||
|
||||
let node_id: String = json_get_string(resp, "id")
|
||||
return node_id
|
||||
}
|
||||
|
||||
// ── Find ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_find searches Engram for a specific vessel by name and version.
|
||||
//
|
||||
// Returns the content JSON blob for the vessel, or "" if not found.
|
||||
// When version is "" any version matching the name is accepted (first hit).
|
||||
fn registry_find(name: String, version: String) -> String {
|
||||
let query: String = "vessel:" + name
|
||||
if !str_eq(version, "") {
|
||||
let query = "vessel:" + name + ":" + version
|
||||
}
|
||||
|
||||
let enc_q: String = url_encode(query)
|
||||
let url: String = registry_url() + "/api/search?q=" + enc_q + "&limit=10"
|
||||
|
||||
let resp: String = http_get(url)
|
||||
if str_eq(resp, "") {
|
||||
println("epm: error: Engram unreachable at " + registry_url())
|
||||
return ""
|
||||
}
|
||||
|
||||
// Response is a JSON array of node objects
|
||||
let count: Int = json_array_len(resp)
|
||||
if count == 0 { return "" }
|
||||
|
||||
// Walk results to find exact match
|
||||
let i: Int = 0
|
||||
while i < count {
|
||||
let node: String = json_array_get(resp, i)
|
||||
let node_label: String = json_get_string(node, "label")
|
||||
let expected_label: String = "vessel:" + name
|
||||
if !str_eq(version, "") {
|
||||
let expected_label = "vessel:" + name + ":" + version
|
||||
}
|
||||
if str_eq(node_label, expected_label) {
|
||||
// The content field is an escaped JSON blob — decode it
|
||||
let raw_content: String = json_get_string(node, "content")
|
||||
return raw_content
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// No exact match; if version was unspecified return first result's content
|
||||
if str_eq(version, "") {
|
||||
let first: String = json_array_get(resp, 0)
|
||||
let first_label: String = json_get_string(first, "label")
|
||||
if str_starts_with(first_label, "vessel:" + name + ":") {
|
||||
return json_get_string(first, "content")
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_list returns all vessels from Engram as a JSON array.
|
||||
//
|
||||
// Each element in the returned array is a content JSON blob.
|
||||
// Returns "[]" when no vessels are found or Engram is unreachable.
|
||||
fn registry_list() -> String {
|
||||
let enc_q: String = url_encode("vessel:")
|
||||
let url: String = registry_url() + "/api/search?q=" + enc_q + "&limit=200"
|
||||
|
||||
let resp: String = http_get(url)
|
||||
if str_eq(resp, "") {
|
||||
println("epm: error: Engram unreachable at " + registry_url())
|
||||
return "[]"
|
||||
}
|
||||
|
||||
let count: Int = json_array_len(resp)
|
||||
if count == 0 { return "[]" }
|
||||
|
||||
// Collect content blobs for nodes whose label starts with "vessel:"
|
||||
let out: String = "["
|
||||
let added: Int = 0
|
||||
let i: Int = 0
|
||||
while i < count {
|
||||
let node: String = json_array_get(resp, i)
|
||||
let lbl: String = json_get_string(node, "label")
|
||||
if str_starts_with(lbl, "vessel:") {
|
||||
let content: String = json_get_string(node, "content")
|
||||
let esc: String = json_escape_string(content)
|
||||
if added == 0 {
|
||||
let out = out + "\"" + esc + "\""
|
||||
} else {
|
||||
let out = out + ",\"" + esc + "\""
|
||||
}
|
||||
let added = added + 1
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return out + "]"
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// channel.el — Go-style channels for El
|
||||
//
|
||||
// Channels are the communication primitive for concurrent El programs.
|
||||
// Threads send values into a channel; other threads receive them.
|
||||
// Channels are typed by convention — all values are Strings.
|
||||
//
|
||||
// Backed by four seed primitives in el_runtime.c:
|
||||
// __channel_new(capacity) -> Int create channel; cap=0 = unbounded
|
||||
// __channel_send(ch, msg) push msg; blocks if bounded and full
|
||||
// __channel_recv(ch) -> String pop msg; blocks until available; "" on close
|
||||
// __channel_try_recv(ch) -> String non-blocking pop; "" if empty
|
||||
// __channel_close(ch) mark closed; wake all blocked recvers
|
||||
//
|
||||
// Usage:
|
||||
// let ch: Int = channel_new(10) // buffered channel, capacity 10
|
||||
// spawn("producer", int_to_str(ch))
|
||||
// let msg: String = channel_recv(ch)
|
||||
|
||||
// ── Core channel API ─────────────────────────────────────────────────────────
|
||||
|
||||
// channel_new — create a channel with the given buffer capacity.
|
||||
//
|
||||
// capacity: 0 = unbounded (never blocks sender)
|
||||
// N = bounded buffer of N messages (sender blocks when full)
|
||||
//
|
||||
// Returns a channel handle (Int) to pass to send/recv/close.
|
||||
fn channel_new(capacity: Int) -> Int {
|
||||
return __channel_new(capacity)
|
||||
}
|
||||
|
||||
// channel_send — send a message into the channel.
|
||||
//
|
||||
// Blocks if the channel is bounded and full.
|
||||
// No-op if the channel is already closed.
|
||||
fn channel_send(ch: Int, msg: String) {
|
||||
__channel_send(ch, msg)
|
||||
}
|
||||
|
||||
// channel_recv — receive the next message from the channel.
|
||||
//
|
||||
// Blocks until a message is available.
|
||||
// Returns "" when the channel is closed and all buffered messages are drained.
|
||||
// The "" sentinel signals end-of-stream to consumers in a loop.
|
||||
fn channel_recv(ch: Int) -> String {
|
||||
return __channel_recv(ch)
|
||||
}
|
||||
|
||||
// channel_try_recv — non-blocking receive.
|
||||
//
|
||||
// Returns the next message if one is available, or "" if the channel is empty.
|
||||
// Does not block. Callers must distinguish "" (empty) from a legitimate ""
|
||||
// message by convention — use a non-empty sentinel in the message protocol.
|
||||
fn channel_try_recv(ch: Int) -> String {
|
||||
return __channel_try_recv(ch)
|
||||
}
|
||||
|
||||
// channel_close — signal that no more messages will be sent.
|
||||
//
|
||||
// After close, channel_recv continues to drain buffered messages then
|
||||
// returns "" on every subsequent call. channel_send on a closed channel
|
||||
// is a no-op (the message is dropped).
|
||||
fn channel_close(ch: Int) {
|
||||
__channel_close(ch)
|
||||
}
|
||||
|
||||
// ── channel_pipeline ─────────────────────────────────────────────────────────
|
||||
|
||||
// channel_pipeline — producer/consumer pipeline with parallel workers.
|
||||
//
|
||||
// Reads messages from in_ch, applies fn_name to each, writes results to out_ch.
|
||||
// Spawns `workers` concurrent worker threads — each drains in_ch independently,
|
||||
// so messages are processed in arrival order within each worker but not globally.
|
||||
//
|
||||
// fn_name must be an El fn with signature (String) -> String.
|
||||
//
|
||||
// Call channel_close(in_ch) to signal EOF. Workers exit when they receive "".
|
||||
// The caller must also close out_ch after all workers finish (via join).
|
||||
//
|
||||
// let in_ch: Int = channel_new(0)
|
||||
// let out_ch: Int = channel_new(0)
|
||||
// channel_pipeline(in_ch, out_ch, "process_item", 4)
|
||||
// channel_send(in_ch, "work-1")
|
||||
// channel_close(in_ch)
|
||||
// let result: String = channel_recv(out_ch)
|
||||
fn channel_pipeline(in_ch: Int, out_ch: Int, fn_name: String, workers: Int) {
|
||||
let i: Int = 0
|
||||
while i < workers {
|
||||
let arg: String = "{\"in_ch\":" + int_to_str(in_ch) +
|
||||
",\"out_ch\":" + int_to_str(out_ch) +
|
||||
",\"fn\":\"" + fn_name + "\"}"
|
||||
let _tid: Int = spawn("_channel_worker", arg)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// _channel_worker — internal worker for channel_pipeline.
|
||||
//
|
||||
// Reads messages from in_ch until it receives "" (closed+empty), applies
|
||||
// fn_name to each, and writes results to out_ch. Runs in its own thread
|
||||
// (spawned by channel_pipeline).
|
||||
fn _channel_worker(arg: String) -> String {
|
||||
let in_ch: Int = str_to_int(json_get(arg, "in_ch"))
|
||||
let out_ch: Int = str_to_int(json_get(arg, "out_ch"))
|
||||
let fn_name: String = json_get(arg, "fn")
|
||||
let running: Bool = true
|
||||
while running {
|
||||
let msg: String = channel_recv(in_ch)
|
||||
if str_eq(msg, "") {
|
||||
let running = false
|
||||
} else {
|
||||
// Spawn fn_name in a child thread so it cannot block the worker loop.
|
||||
let tid: Int = spawn(fn_name, msg)
|
||||
let result: String = join(tid)
|
||||
channel_send(out_ch, result)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── channel_drain ────────────────────────────────────────────────────────────
|
||||
|
||||
// channel_drain — collect all messages from ch into a list.
|
||||
//
|
||||
// Reads until the channel is closed and empty (recv returns "").
|
||||
// Returns a [String] of all messages received.
|
||||
//
|
||||
// Typical usage: close the channel from the producer side, then call
|
||||
// channel_drain from the consumer to collect results.
|
||||
fn channel_drain(ch: Int) -> [String] {
|
||||
let results: [String] = el_list_empty()
|
||||
let running: Bool = true
|
||||
while running {
|
||||
let msg: String = channel_recv(ch)
|
||||
if str_eq(msg, "") {
|
||||
let running = false
|
||||
} else {
|
||||
let results = el_list_append(results, msg)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// ── channel_fan_out ───────────────────────────────────────────────────────────
|
||||
|
||||
// channel_fan_out — send every item in a list into a channel.
|
||||
//
|
||||
// items: [String] — items to send
|
||||
// ch: Int — destination channel
|
||||
//
|
||||
// Sends all items then closes the channel to signal end-of-stream.
|
||||
// Intended for the producer side of a pipeline:
|
||||
//
|
||||
// channel_fan_out(items, in_ch)
|
||||
// let results: [String] = channel_drain(out_ch)
|
||||
fn channel_fan_out(items: [String], ch: Int) {
|
||||
let n: Int = el_list_len(items)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
channel_send(ch, item)
|
||||
let i = i + 1
|
||||
}
|
||||
channel_close(ch)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// runtime/engram.el — El wrapper for the engram graph store
|
||||
//
|
||||
// Thin wrappers over the __engram_* seed primitives defined in el_seed.c.
|
||||
// Each function delegates directly to the corresponding seed — no logic here.
|
||||
// The seed layer owns all storage, indexing, and graph traversal.
|
||||
//
|
||||
// Dependencies: runtime/string.el, runtime/json.el
|
||||
|
||||
// --- Node creation ---
|
||||
|
||||
fn engram_node(content: String, node_type: String, salience: Float) -> String {
|
||||
return __engram_node(content, node_type, salience)
|
||||
}
|
||||
|
||||
fn engram_node_full(content: String, nt: String, sal: Float, imp: Float,
|
||||
source: String, lang: String, ts: Int, tags: String) -> String {
|
||||
return __engram_node_full(content, nt, sal, imp, source, lang, ts, tags)
|
||||
}
|
||||
|
||||
// --- Node retrieval ---
|
||||
|
||||
fn engram_get_node(id: String) -> String {
|
||||
return __engram_get_node(id)
|
||||
}
|
||||
|
||||
fn engram_node_count() -> Int {
|
||||
return __engram_node_count()
|
||||
}
|
||||
|
||||
// --- Node lifecycle ---
|
||||
|
||||
fn engram_strengthen(id: String) -> Bool {
|
||||
return __engram_strengthen(id)
|
||||
}
|
||||
|
||||
fn engram_forget(id: String) -> Bool {
|
||||
return __engram_forget(id)
|
||||
}
|
||||
|
||||
// --- Search and scan ---
|
||||
|
||||
fn engram_search(query: String, limit: Int) -> String {
|
||||
return __engram_search(query, limit)
|
||||
}
|
||||
|
||||
fn engram_scan_nodes(limit: Int, offset: Int) -> String {
|
||||
return __engram_scan_nodes(limit, offset)
|
||||
}
|
||||
|
||||
fn engram_scan_nodes_json(limit: Int, offset: Int) -> String {
|
||||
return __engram_scan_nodes_json(limit, offset)
|
||||
}
|
||||
|
||||
// --- Graph edges ---
|
||||
|
||||
fn engram_connect(from: String, to: String, rel: String, weight: Float) -> Bool {
|
||||
return __engram_connect(from, to, rel, weight)
|
||||
}
|
||||
|
||||
fn engram_edge_between(a: String, b: String) -> String {
|
||||
return __engram_edge_between(a, b)
|
||||
}
|
||||
|
||||
// --- Graph traversal ---
|
||||
|
||||
fn engram_neighbors(id: String) -> String {
|
||||
return __engram_neighbors(id)
|
||||
}
|
||||
|
||||
fn engram_neighbors_filtered(id: String, rel: String, min_w: Float) -> String {
|
||||
return __engram_neighbors_filtered(id, rel, min_w)
|
||||
}
|
||||
|
||||
fn engram_activate(query: String, depth: Int) -> String {
|
||||
return __engram_activate(query, depth)
|
||||
}
|
||||
|
||||
fn engram_activate_json(query: String, limit: Int) -> String {
|
||||
return __engram_activate_json(query, limit)
|
||||
}
|
||||
|
||||
// --- Generation ---
|
||||
|
||||
fn generate(form: String) -> String {
|
||||
return __generate(form)
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// runtime/env.el — environment and process
|
||||
// Covers: environment variables, command-line args, process exit, in-process
|
||||
// state store, UUID generation, and list convenience helpers.
|
||||
|
||||
// env — read an environment variable. Returns "" if the variable is not set.
|
||||
fn env(key: String) -> String {
|
||||
return __env_get(key)
|
||||
}
|
||||
|
||||
// args — command-line arguments as a list of strings.
|
||||
// __args_json returns a JSON array (e.g. ["prog","arg1","arg2"]).
|
||||
// The list is built by iterating over the array.
|
||||
fn args() -> [String] {
|
||||
let json: String = __args_json()
|
||||
let n: Int = json_array_len(json)
|
||||
let result: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = json_array_get_string(json, i)
|
||||
let result = el_list_append(result, item)
|
||||
let i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// exit_program — terminate the process with the given exit code.
|
||||
fn exit_program(code: Int) {
|
||||
__exit_program(code)
|
||||
}
|
||||
|
||||
// ── List convenience helpers ───────────────────────────────────────────────
|
||||
|
||||
// get — index into a list. Thin alias for el_list_get used throughout the
|
||||
// El stdlib so call sites read like `get(lst, i)` rather than the verbose form.
|
||||
fn get(lst: [String], i: Int) -> String {
|
||||
return el_list_get(lst, i)
|
||||
}
|
||||
|
||||
// len — length of a list.
|
||||
fn len(lst: [String]) -> Int {
|
||||
return el_list_len(lst)
|
||||
}
|
||||
|
||||
// ── In-process key-value state store ──────────────────────────────────────
|
||||
|
||||
// state_set — store a string value under key.
|
||||
fn state_set(key: String, val: String) {
|
||||
__state_set(key, val)
|
||||
}
|
||||
|
||||
// state_get — retrieve value for key; returns "" if key not present.
|
||||
fn state_get(key: String) -> String {
|
||||
return __state_get(key)
|
||||
}
|
||||
|
||||
// state_del — remove key from the store.
|
||||
fn state_del(key: String) {
|
||||
__state_del(key)
|
||||
}
|
||||
|
||||
// state_keys — all keys currently in the store as a JSON array string.
|
||||
fn state_keys() -> String {
|
||||
return __state_keys()
|
||||
}
|
||||
|
||||
// ── DHARMA runtime helpers ─────────────────────────────────────────────────
|
||||
|
||||
// config — read a configuration value from the environment.
|
||||
// Returns "" if the variable is not set. Alias for env().
|
||||
fn config(key: String) -> String {
|
||||
return __env_get(key)
|
||||
}
|
||||
|
||||
// log_info — write an [INFO] log line to stdout.
|
||||
fn log_info(msg: String) {
|
||||
__println("[INFO] " + msg)
|
||||
}
|
||||
|
||||
// log_warn — write a [WARN] log line to stdout.
|
||||
fn log_warn(msg: String) {
|
||||
__println("[WARN] " + msg)
|
||||
}
|
||||
|
||||
// list_len — return the number of elements in a list. Alias for el_list_len.
|
||||
fn list_len(lst: [String]) -> Int {
|
||||
return el_list_len(lst)
|
||||
}
|
||||
|
||||
// list_get — return the element at index i in a list. Alias for el_list_get.
|
||||
fn list_get(lst: [String], i: Int) -> String {
|
||||
return el_list_get(lst, i)
|
||||
}
|
||||
|
||||
// ── UUID generation ────────────────────────────────────────────────────────
|
||||
|
||||
// uuid_new — generate a new random UUID v4.
|
||||
fn uuid_new() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
// uuid_v4 — alias for uuid_new(); explicit version name for callers that
|
||||
// need to be precise about the UUID variant.
|
||||
fn uuid_v4() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// runtime/exec.el — subprocess execution
|
||||
// All four names resolve to the same seed primitives so callers can use
|
||||
// whichever name matches their mental model of the operation.
|
||||
|
||||
// exec — run a shell command, capture stdout, return as String.
|
||||
// Blocks until the subprocess exits (30-second wall-clock deadline in the
|
||||
// seed layer). Returns "" on any error.
|
||||
fn exec(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
|
||||
// exec_bg — fire-and-forget subprocess. Returns immediately; no stdout.
|
||||
fn exec_bg(cmd: String) {
|
||||
__exec_bg(cmd)
|
||||
}
|
||||
|
||||
// exec_command — alias for exec(); preferred when callers care about side
|
||||
// effects (e.g. invoking a build tool) rather than captured output.
|
||||
fn exec_command(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
|
||||
// exec_capture — alias for exec(); preferred when callers explicitly want
|
||||
// to capture and process stdout.
|
||||
fn exec_capture(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// runtime/fs.el — filesystem operations
|
||||
// Thin El wrappers over seed primitives; no logic beyond what is needed
|
||||
// to present a clean API. The heavy lifting lives in el_runtime.c.
|
||||
|
||||
fn fs_read(path: String) -> String {
|
||||
return __fs_read(path)
|
||||
}
|
||||
|
||||
fn fs_write(path: String, content: String) -> Bool {
|
||||
return __fs_write(path, content)
|
||||
}
|
||||
|
||||
fn fs_exists(path: String) -> Bool {
|
||||
return __fs_exists(path)
|
||||
}
|
||||
|
||||
fn fs_mkdir(path: String) -> Bool {
|
||||
return __fs_mkdir(path)
|
||||
}
|
||||
|
||||
fn fs_write_bytes(path: String, bytes: String, n: Int) -> Bool {
|
||||
return __fs_write_bytes(path, bytes, n)
|
||||
}
|
||||
|
||||
// fs_list — return list of filenames in a directory.
|
||||
// __fs_list_raw returns a newline-separated string (possibly with a trailing
|
||||
// newline); callers that need a clean list should filter empty strings.
|
||||
fn fs_list(path: String) -> [String] {
|
||||
let raw: String = __fs_list_raw(path)
|
||||
return str_split(raw, "\n")
|
||||
}
|
||||
|
||||
// fs_list_json — return a JSON array of filenames in a directory.
|
||||
// Empty strings produced by a trailing newline are stripped before encoding.
|
||||
fn fs_list_json(path: String) -> String {
|
||||
let items: [String] = fs_list(path)
|
||||
let n: Int = el_list_len(items)
|
||||
let clean: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let trimmed: String = str_trim(item)
|
||||
if !str_eq(trimmed, "") {
|
||||
let clean = el_list_append(clean, "\"" + trimmed + "\"")
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return json_build_array(clean)
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
// runtime/http.el — El HTTP client and server wrappers
|
||||
//
|
||||
// Thin El layer over seed primitives. All network I/O is performed by the
|
||||
// seed; this file provides the public API that El programs import.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __http_do(method, url, body, headers_json, timeout_ms) -> String
|
||||
// __http_do_to_file(method, url, body, headers_json, out_path) -> Bool
|
||||
// __http_do_map(method, url, body, headers_map, timeout_ms) -> String
|
||||
// __http_do_map_to_file(method, url, body, headers_map, out_path) -> Bool
|
||||
// __http_serve(port, handler_name)
|
||||
// __http_serve_v2(port, handler_name)
|
||||
// __http_response(status, headers_json, body) -> String
|
||||
// __env_get(key) -> String
|
||||
//
|
||||
// NOTE FOR SEED AGENT: __http_do_map and __http_do_map_to_file must be added
|
||||
// to the seed. They are identical to __http_do / __http_do_to_file except
|
||||
// they accept an ElMap directly for headers instead of a pre-serialised JSON
|
||||
// string. This avoids needing map iteration in El (which has no for-loop or
|
||||
// map iterator primitive). The seed implementation maps to headers_from_map()
|
||||
// in el_runtime.c.
|
||||
//
|
||||
// Other builtins used:
|
||||
// str_eq(a, b) -> Bool
|
||||
// str_to_int(s) -> Int
|
||||
|
||||
// ── Timeout helper ────────────────────────────────────────────────────────────
|
||||
|
||||
// el_http_timeout_ms returns the configured HTTP timeout in milliseconds.
|
||||
// Reads EL_HTTP_TIMEOUT_MS from the environment; defaults to 60000 (60s).
|
||||
// Returns 60000 if the env var is absent, empty, or non-positive.
|
||||
fn el_http_timeout_ms() -> Int {
|
||||
let v: String = __env_get("EL_HTTP_TIMEOUT_MS")
|
||||
if str_eq(v, "") { return 60000 }
|
||||
let n: Int = str_to_int(v)
|
||||
if n <= 0 { return 60000 }
|
||||
return n
|
||||
}
|
||||
|
||||
// ── HTTP client — simple variants ────────────────────────────────────────────
|
||||
|
||||
// http_get performs an HTTP GET request and returns the response body.
|
||||
// On transport failure the seed returns an error JSON fragment.
|
||||
fn http_get(url: String) -> String {
|
||||
return __http_do("GET", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post performs an HTTP POST request with the given body.
|
||||
// No Content-Type header is set; use http_post_json for JSON payloads.
|
||||
fn http_post(url: String, body: String) -> String {
|
||||
return __http_do("POST", url, body, "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_json performs an HTTP POST request with Content-Type:
|
||||
// application/json. body must be a valid JSON string.
|
||||
fn http_post_json(url: String, body: String) -> String {
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_delete performs an HTTP DELETE request and returns the response body.
|
||||
fn http_delete(url: String) -> String {
|
||||
return __http_do("DELETE", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — header map variants ────────────────────────────────────────
|
||||
//
|
||||
// These accept a Map<String, String> of request headers. The seed's
|
||||
// __http_do_map converts the ElMap to a curl_slist internally, matching
|
||||
// the headers_from_map() logic in el_runtime.c.
|
||||
|
||||
// http_get_with_headers performs an HTTP GET with caller-supplied headers.
|
||||
fn http_get_with_headers(url: String, headers: Map<String, String>) -> String {
|
||||
return __http_do_map("GET", url, "", headers, el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_with_headers performs an HTTP POST with caller-supplied headers.
|
||||
fn http_post_with_headers(url: String, body: String, headers: Map<String, String>) -> String {
|
||||
return __http_do_map("POST", url, body, headers, el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_form_auth performs an HTTP POST with
|
||||
// Content-Type: application/x-www-form-urlencoded and an Authorization
|
||||
// header built from auth_header (the caller passes the full header value,
|
||||
// e.g. "Bearer <token>" or "Basic <base64>").
|
||||
//
|
||||
// Mirrors http_post_form_auth in el_runtime.c: two headers are injected,
|
||||
// Content-Type is always set; Authorization is omitted when auth_header is "".
|
||||
fn http_post_form_auth(url: String, form_body: String, auth_header: String) -> String {
|
||||
if str_eq(auth_header, "") {
|
||||
return __http_do("POST", url, form_body, "{\"Content-Type\":\"application/x-www-form-urlencoded\"}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("POST", url, form_body, "{\"Content-Type\":\"application/x-www-form-urlencoded\",\"Authorization\":\"" + auth_header + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — streaming to file ──────────────────────────────────────────
|
||||
//
|
||||
// These route the response body directly to a file via the seed, bypassing
|
||||
// the El string layer. This preserves embedded NUL bytes in binary payloads
|
||||
// (audio, images, etc.) — an El string would truncate at the first NUL.
|
||||
// Returns true on success, false on any transport or I/O error.
|
||||
|
||||
// http_post_to_file performs an HTTP POST and streams the response body to
|
||||
// output_path. Useful for large or binary response payloads.
|
||||
fn http_post_to_file(url: String, body: String, headers: Map<String, String>, output_path: String) -> Bool {
|
||||
return __http_do_map_to_file("POST", url, body, headers, output_path)
|
||||
}
|
||||
|
||||
// http_get_to_file performs an HTTP GET and streams the response body to
|
||||
// output_path. Useful for large or binary response payloads.
|
||||
fn http_get_to_file(url: String, headers: Map<String, String>, output_path: String) -> Bool {
|
||||
return __http_do_map_to_file("GET", url, "", headers, output_path)
|
||||
}
|
||||
|
||||
// ── HTTP server ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// El programs call http_set_handler(name) to register which El function
|
||||
// handles requests, then http_serve(port, name) to start listening.
|
||||
// The seed resolves handler names via dlsym — every El fn compiles to a
|
||||
// global C symbol with the same name, so self-registration works without
|
||||
// any El-level registry.
|
||||
//
|
||||
// v2 widens the handler signature from
|
||||
// (method, path, body) -> String
|
||||
// to
|
||||
// (method, path, headers_map, body) -> String
|
||||
// so handlers can inspect incoming headers. Use http_serve_v2 +
|
||||
// http_set_handler_v2 for v2 handlers.
|
||||
|
||||
// http_set_handler registers name as the active v1 request handler.
|
||||
// The seed resolves the symbol via dlsym at call time; no El-level
|
||||
// registration is needed. This is a no-op at the El layer.
|
||||
fn http_set_handler(name: String) {
|
||||
// no-op: the seed handles handler registration via dlsym
|
||||
}
|
||||
|
||||
// http_serve starts an HTTP/1.1 server on port, dispatching every request
|
||||
// to handler (a v1 handler: fn(method, path, body) -> String).
|
||||
// Blocks forever. Accepts both IPv4 and IPv6 (dual-stack).
|
||||
fn http_serve(port: Int, handler: String) {
|
||||
__http_serve(port, handler)
|
||||
}
|
||||
|
||||
// http_set_handler_v2 registers name as the active v2 request handler.
|
||||
// No-op at the El layer; the seed uses dlsym.
|
||||
fn http_set_handler_v2(name: String) {
|
||||
// no-op: the seed handles handler registration via dlsym
|
||||
}
|
||||
|
||||
// http_serve_v2 starts an HTTP/1.1 server on port, dispatching every
|
||||
// request to handler (a v2 handler: fn(method, path, headers, body) ->
|
||||
// String). Blocks forever. Accepts both IPv4 and IPv6 (dual-stack).
|
||||
fn http_serve_v2(port: Int, handler: String) {
|
||||
__http_serve_v2(port, handler)
|
||||
}
|
||||
|
||||
// ── Response construction ─────────────────────────────────────────────────────
|
||||
|
||||
// http_response builds a structured response envelope that the HTTP server
|
||||
// runtime unpacks into a real HTTP response with the given status code and
|
||||
// headers. status must be 100–599 (defaults to 200 outside that range).
|
||||
// headers_json must be a JSON object literal (e.g. "{}" or
|
||||
// "{\"Content-Type\":\"text/html\"}"); body is the response body string.
|
||||
//
|
||||
// The envelope format is:
|
||||
// {"el_http_response":1,"status":<n>,"headers":<obj>,"body":"<escaped>"}
|
||||
// The runtime detects this prefix and unpacks it; plain string returns from
|
||||
// handlers are still supported and are sent as HTTP 200 with auto-detected
|
||||
// Content-Type.
|
||||
fn http_response(status: Int, headers_json: String, body: String) -> String {
|
||||
return __http_response(status, headers_json, body)
|
||||
}
|
||||
|
||||
// ── HTTP client — PATCH ───────────────────────────────────────────────────────
|
||||
|
||||
// http_patch performs an HTTP PATCH request with Content-Type: application/json.
|
||||
fn http_patch(url: String, body: String) -> String {
|
||||
return __http_do("PATCH", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — Engram variants (optional API key) ──────────────────────────
|
||||
//
|
||||
// These are used by dharma's db.el to talk to Engram nodes.
|
||||
// The key parameter is the X-API-Key header value; pass "" for no auth.
|
||||
|
||||
// http_post_engram performs an HTTP POST with Content-Type: application/json
|
||||
// and an optional X-API-Key header. If key is "" no auth header is added.
|
||||
fn http_post_engram(url: String, key: String, body: String) -> String {
|
||||
if str_eq(key, "") {
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\",\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_get_engram performs an HTTP GET with an optional X-API-Key header.
|
||||
fn http_get_engram(url: String, key: String) -> String {
|
||||
if str_eq(key, "") {
|
||||
return __http_do("GET", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("GET", url, "", "{\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── SSE — Server-Sent Events streaming ───────────────────────────────────────
|
||||
//
|
||||
// Usage pattern for an SSE handler:
|
||||
//
|
||||
// fn my_handler(method: String, path: String, headers: Map<String, String>, body: String) -> String {
|
||||
// let fd: Int = http_conn_fd()
|
||||
// http_sse_open(fd)
|
||||
// http_sse_send(fd, "hello")
|
||||
// http_sse_send(fd, "world")
|
||||
// http_sse_close(fd)
|
||||
// return http_sse_sentinel()
|
||||
// }
|
||||
//
|
||||
// The sentinel return value tells http_serve_v2 NOT to close the connection
|
||||
// automatically — the handler already closed it via http_sse_close.
|
||||
|
||||
// http_conn_fd returns the raw file descriptor for the current HTTP connection.
|
||||
// Only valid inside an http_serve_v2 handler, before the handler returns.
|
||||
// Use with http_sse_open / http_sse_send / http_sse_close for streaming.
|
||||
fn http_conn_fd() -> Int {
|
||||
return __http_conn_fd()
|
||||
}
|
||||
|
||||
// http_sse_open sends SSE response headers on the current connection,
|
||||
// keeping it open for streaming. Call once at the start of an SSE handler.
|
||||
// Returns true on success.
|
||||
fn http_sse_open(fd: Int) -> Bool {
|
||||
return __http_sse_open(fd)
|
||||
}
|
||||
|
||||
// http_sse_send writes one SSE event to the connection.
|
||||
// data should not contain newlines (they are added automatically).
|
||||
// Returns true if the write succeeded (client still connected).
|
||||
fn http_sse_send(fd: Int, data: String) -> Bool {
|
||||
return __http_sse_send(fd, data)
|
||||
}
|
||||
|
||||
// http_sse_close closes the SSE connection.
|
||||
fn http_sse_close(fd: Int) {
|
||||
__http_sse_close(fd)
|
||||
return
|
||||
}
|
||||
|
||||
// http_sse_sentinel is the return value an SSE handler must return
|
||||
// to tell the HTTP server NOT to close the connection automatically.
|
||||
// The handler takes ownership of the fd and closes it via http_sse_close.
|
||||
fn http_sse_sentinel() -> String {
|
||||
return "__sse__"
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
// runtime/json.el — El JSON operations
|
||||
//
|
||||
// Thin El wrappers over seed JSON primitives, plus pure-El builders and
|
||||
// helpers. Each function here corresponds to (and replaces) a C function
|
||||
// from el-compiler/runtime/legacy/el_runtime.c (lines 2692–3333).
|
||||
//
|
||||
// Seed primitives consumed by this module:
|
||||
// __json_get(json, key) -> String (value as string)
|
||||
// __json_get_raw(json, key) -> String (raw JSON token)
|
||||
// __json_parse_map(s) -> Map<String, Any>
|
||||
// __json_stringify_val(v) -> String
|
||||
// __json_array_len(arr) -> Int
|
||||
// __json_array_get(arr, i) -> String (element as JSON fragment)
|
||||
// __json_array_get_string(arr, i) -> String (element as string value)
|
||||
// __json_set(json, key, value) -> String (JSON mutation)
|
||||
// __str_to_int(s) -> Int
|
||||
// __str_to_float(s) -> Float
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — thin wrappers that delegate directly to seed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_get — extract a value from a JSON object as a string.
|
||||
// Supports dot-path traversal ("a.b.c") and array indices ("items.0.name").
|
||||
fn json_get(json: String, key: String) -> String {
|
||||
return __json_get(json, key)
|
||||
}
|
||||
|
||||
// json_get_raw — extract a raw JSON token (the un-decoded fragment) for a key.
|
||||
// Useful when the caller wants to pass a sub-object to another JSON function.
|
||||
fn json_get_raw(json: String, key: String) -> String {
|
||||
return __json_get_raw(json, key)
|
||||
}
|
||||
|
||||
// json_parse — parse a JSON string into a Map<String, Any>.
|
||||
// Arrays become ElList; objects become ElMap; scalars are typed values.
|
||||
fn json_parse(s: String) -> Map<String, Any> {
|
||||
return __json_parse_map(s)
|
||||
}
|
||||
|
||||
// json_stringify — serialize an El value (ElMap, ElList, String, Int) to JSON.
|
||||
fn json_stringify(v: Any) -> String {
|
||||
return __json_stringify_val(v)
|
||||
}
|
||||
|
||||
// json_array_len — return the number of elements in a JSON array string.
|
||||
fn json_array_len(arr: String) -> Int {
|
||||
return __json_array_len(arr)
|
||||
}
|
||||
|
||||
// json_array_get — return the i-th element of a JSON array as a JSON fragment.
|
||||
// Nested objects and arrays are returned verbatim. Out-of-range -> "".
|
||||
fn json_array_get(arr: String, i: Int) -> String {
|
||||
return __json_array_get(arr, i)
|
||||
}
|
||||
|
||||
// json_array_get_string — return the i-th element of a JSON array as a plain
|
||||
// string value (quotes and escape sequences removed). Non-string elements
|
||||
// and out-of-range indices yield "".
|
||||
fn json_array_get_string(arr: String, i: Int) -> String {
|
||||
return __json_array_get_string(arr, i)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Typed extractors — delegate to seed then convert
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_get_string — extract a string value for a key.
|
||||
// Equivalent to json_get but named explicitly for readability.
|
||||
fn json_get_string(json: String, key: String) -> String {
|
||||
return __json_get(json, key)
|
||||
}
|
||||
|
||||
// json_get_int — extract an integer value for a key.
|
||||
fn json_get_int(json: String, key: String) -> Int {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_to_int(s)
|
||||
}
|
||||
|
||||
// json_get_float — extract a floating-point value for a key.
|
||||
fn json_get_float(json: String, key: String) -> Float {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_to_float(s)
|
||||
}
|
||||
|
||||
// json_get_bool — extract a boolean value for a key.
|
||||
// Returns true only when the raw JSON token is the literal "true".
|
||||
fn json_get_bool(json: String, key: String) -> Bool {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_eq(s, "true")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_set — set or insert a key/value pair in a JSON object string.
|
||||
// If the key already exists its value is replaced in-place; otherwise the
|
||||
// pair is appended before the closing brace. The value must already be a
|
||||
// valid JSON-encoded string (e.g. a quoted string, number, or sub-object).
|
||||
fn json_set(json: String, key: String, value: String) -> String {
|
||||
return __json_set(json, key, value)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure-El builders — no seed call required
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_build_object — build a JSON object from alternating key/value strings.
|
||||
//
|
||||
// keys_and_values must contain an even number of elements laid out as:
|
||||
// [key0, val0, key1, val1, ...]
|
||||
//
|
||||
// Both keys and values are assumed to be plain strings that will be
|
||||
// double-quoted and JSON-escaped by this function. Pass a pre-encoded
|
||||
// number or sub-object as the value if you need non-string JSON types.
|
||||
//
|
||||
// Example:
|
||||
// json_build_object(["name", "alice", "role", "admin"])
|
||||
// -> {"name":"alice","role":"admin"}
|
||||
fn json_build_object(keys_and_values: [String]) -> String {
|
||||
let n: Int = el_list_len(keys_and_values)
|
||||
let result: String = "{"
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let key: String = el_list_get(keys_and_values, i)
|
||||
let val: String = el_list_get(keys_and_values, i + 1)
|
||||
let sep: String = if i == 0 { "" } else { "," }
|
||||
let escaped_key: String = json_escape_string(key)
|
||||
let escaped_val: String = json_escape_string(val)
|
||||
let result = result + sep + "\"" + escaped_key + "\":\"" + escaped_val + "\""
|
||||
let i = i + 2
|
||||
}
|
||||
return result + "}"
|
||||
}
|
||||
|
||||
// json_build_array — build a JSON array from a list of already-JSON-encoded
|
||||
// strings.
|
||||
//
|
||||
// Each element in items must be a valid JSON fragment (quoted string, number,
|
||||
// object, array, or literal). The function joins them with commas and wraps
|
||||
// the result in brackets.
|
||||
//
|
||||
// Example:
|
||||
// json_build_array(["\"alice\"", "\"bob\""])
|
||||
// -> ["alice","bob"]
|
||||
fn json_build_array(items: [String]) -> String {
|
||||
let n: Int = el_list_len(items)
|
||||
let result: String = "["
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let sep: String = if i == 0 { "" } else { "," }
|
||||
let result = result + sep + item
|
||||
let i = i + 1
|
||||
}
|
||||
return result + "]"
|
||||
}
|
||||
|
||||
// json_array_push — append a pre-encoded JSON element to a JSON array string.
|
||||
// elem must be a valid JSON fragment (e.g. "\"foo\"" or "42").
|
||||
// Returns a new JSON array string with elem appended.
|
||||
// Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]"
|
||||
fn json_array_push(arr: String, elem: String) -> String {
|
||||
let n: Int = json_array_len(arr)
|
||||
if n == 0 {
|
||||
return "[" + elem + "]"
|
||||
}
|
||||
// arr ends with ']'; insert before it
|
||||
let inner_end: Int = str_last_index_of(arr, "]")
|
||||
if inner_end < 0 {
|
||||
return "[" + elem + "]"
|
||||
}
|
||||
let prefix: String = str_slice(arr, 0, inner_end)
|
||||
return prefix + "," + elem + "]"
|
||||
}
|
||||
|
||||
// json_escape_string — escape a raw string so it can be safely embedded as a
|
||||
// JSON string value.
|
||||
//
|
||||
// Characters escaped: backslash, double-quote, newline, carriage return, tab.
|
||||
// The returned value does NOT include surrounding double-quotes; wrap it in
|
||||
// quotes if you need a complete JSON string literal.
|
||||
fn json_escape_string(s: String) -> String {
|
||||
let s1: String = str_replace(s, "\\", "\\\\")
|
||||
let s2: String = str_replace(s1, "\"", "\\\"")
|
||||
let s3: String = str_replace(s2, "\n", "\\n")
|
||||
let s4: String = str_replace(s3, "\r", "\\r")
|
||||
let s5: String = str_replace(s4, "\t", "\\t")
|
||||
return s5
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DHARMA byte decoding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// bytes_to_str — decode a JSON array of integer byte values back to a string.
|
||||
// "[104,105]" -> "hi"
|
||||
// Inverse of str_to_bytes (defined in string.el). Defined here because it
|
||||
// depends on json_array_len and json_array_get_string which live in this file.
|
||||
fn bytes_to_str(arr: String) -> String {
|
||||
let n: Int = json_array_len(arr)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let elem: String = json_array_get_string(arr, i)
|
||||
let b: Int = __str_to_int(elem)
|
||||
out = __str_set_char(out, i, b)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// runtime/manifest.el — El runtime module manifest
|
||||
//
|
||||
// Load order for runtime compilation. Each module may depend on modules
|
||||
// listed before it. The build system concatenates these in this order,
|
||||
// then compiles the combined source.
|
||||
//
|
||||
// Modules:
|
||||
// 1. runtime/string.el — string operations (no dependencies)
|
||||
// 2. runtime/math.el — numeric/float operations (no dependencies)
|
||||
// 3. runtime/state.el — in-process key-value (no dependencies)
|
||||
// 4. runtime/env.el — environment, process, args, uuid
|
||||
// 5. runtime/fs.el — filesystem operations (depends: string)
|
||||
// 6. runtime/exec.el — subprocess execution (depends: string)
|
||||
// 7. runtime/time.el — time, date, calendar (depends: string, math)
|
||||
// 8. runtime/json.el — JSON operations (depends: string)
|
||||
// 9. runtime/http.el — HTTP client+server (depends: string, json)
|
||||
// 10. runtime/engram.el — graph store (depends: string, json)
|
||||
// 11. runtime/thread.el — threading, parallel_map (depends: all above)
|
||||
// 12. runtime/collections.el — list/map higher-level ops (depends: string)
|
||||
//
|
||||
// Build command (from el/ root):
|
||||
// cat runtime/string.el runtime/math.el runtime/state.el runtime/env.el \
|
||||
// runtime/fs.el runtime/exec.el runtime/time.el runtime/json.el \
|
||||
// runtime/http.el runtime/engram.el runtime/thread.el \
|
||||
// runtime/collections.el \
|
||||
// <user-program.el> > combined.el
|
||||
// ./dist/platform/elc combined.el > output.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o output output.c el-compiler/runtime/el_seed.c
|
||||
|
||||
// This file itself is not compiled — it is documentation only.
|
||||
fn runtime_version() -> String {
|
||||
return "2.0.0-el-native"
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
// runtime/math.el — Float math, integer utilities, and numeric conversions.
|
||||
//
|
||||
// Implements the math/float surface from el-compiler/runtime/legacy/el_runtime.c
|
||||
// (lines 303–305 for el_abs/max/min, lines 4725–4771 for float/format ops)
|
||||
// in pure El, using seed primitives.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __sqrt_f(f: Float) -> Float
|
||||
// __log_f(f: Float) -> Float
|
||||
// __ln_f(f: Float) -> Float
|
||||
// __sin_f(f: Float) -> Float
|
||||
// __cos_f(f: Float) -> Float
|
||||
// __pi_f() -> Float
|
||||
// __float_to_str(f: Float) -> String
|
||||
// __str_to_float(s: String) -> Float
|
||||
// __int_to_str(n: Int) -> String
|
||||
// __str_to_int(s: String) -> Int
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Integer math — el_abs, el_max, el_min.
|
||||
//
|
||||
// Matches legacy el_abs, el_max, el_min (lines 303–305).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// el_abs — absolute value of an integer.
|
||||
fn el_abs(n: Int) -> Int {
|
||||
if n < 0 { return -n }
|
||||
return n
|
||||
}
|
||||
|
||||
// el_max — larger of two integers.
|
||||
fn el_max(a: Int, b: Int) -> Int {
|
||||
if a > b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// el_min — smaller of two integers.
|
||||
fn el_min(a: Int, b: Int) -> Int {
|
||||
if a < b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Float math — thin wrappers over seed primitives.
|
||||
//
|
||||
// Matches legacy math_sqrt, math_log, math_ln, math_sin, math_cos, math_pi
|
||||
// (lines 4766–4771).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// math_sqrt — square root.
|
||||
fn math_sqrt(f: Float) -> Float {
|
||||
return __sqrt_f(f)
|
||||
}
|
||||
|
||||
// math_log — base-10 logarithm.
|
||||
fn math_log(f: Float) -> Float {
|
||||
return __log_f(f)
|
||||
}
|
||||
|
||||
// math_ln — natural logarithm.
|
||||
fn math_ln(f: Float) -> Float {
|
||||
return __ln_f(f)
|
||||
}
|
||||
|
||||
// math_sin — sine (radians).
|
||||
fn math_sin(f: Float) -> Float {
|
||||
return __sin_f(f)
|
||||
}
|
||||
|
||||
// math_cos — cosine (radians).
|
||||
fn math_cos(f: Float) -> Float {
|
||||
return __cos_f(f)
|
||||
}
|
||||
|
||||
// math_pi — the constant π.
|
||||
fn math_pi() -> Float {
|
||||
return __pi_f()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Float conversions — float_to_str, int_to_float, float_to_int, str_to_float.
|
||||
//
|
||||
// Matches legacy float_to_str, int_to_float, float_to_int, str_to_float
|
||||
// (lines 4725–4762).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// float_to_str — format a float using %g (shortest exact representation).
|
||||
// Matches legacy float_to_str() → snprintf "%g".
|
||||
fn float_to_str(f: Float) -> String {
|
||||
return __float_to_str(f)
|
||||
}
|
||||
|
||||
// int_to_float — convert an integer to a float.
|
||||
// Matches legacy int_to_float() → (double)(int64_t)n.
|
||||
fn int_to_float(n: Int) -> Float {
|
||||
return __int_to_float(n)
|
||||
}
|
||||
|
||||
// float_to_int — truncate a float to an integer (toward zero).
|
||||
// Matches legacy float_to_int() → (int64_t)el_to_float(f).
|
||||
fn float_to_int(f: Float) -> Int {
|
||||
return __float_to_int(f)
|
||||
}
|
||||
|
||||
// str_to_float — parse a float from a string. Returns 0.0 on failure.
|
||||
// Matches legacy str_to_float() → strtod(str, NULL).
|
||||
fn str_to_float(s: String) -> Float {
|
||||
return __str_to_float(s)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// format_float — format a float to a fixed number of decimal places.
|
||||
//
|
||||
// decimals is clamped to [0, 30]. Matches legacy format_float() → snprintf "%.*f".
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn format_float(f: Float, decimals: Int) -> String {
|
||||
let d: Int = decimals
|
||||
if d < 0 { d = 0 }
|
||||
if d > 30 { d = 30 }
|
||||
// Delegate to seed; the seed exposes __format_float(f, d) -> String.
|
||||
// This matches snprintf(buf, 128, "%.*f", d, v) in the legacy runtime.
|
||||
return __format_float(f, d)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// decimal_round — round a float to d decimal places (half-away-from-zero).
|
||||
//
|
||||
// Matches legacy decimal_round():
|
||||
// mul = pow(10, d)
|
||||
// r = (v >= 0 ? floor(v*mul + 0.5) : -floor(-v*mul + 0.5)) / mul
|
||||
//
|
||||
// We implement pow(10, d) via a loop (d <= 15, so at most 15 multiplications).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// _pow10 — 10^n as a Float for n in [0, 15].
|
||||
fn _pow10(n: Int) -> Float {
|
||||
let result: Float = 1.0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
result = result * 10.0
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// _floor_f — floor of a float: largest integer <= f.
|
||||
// Uses __float_to_int (truncation) with correction for negative non-integers.
|
||||
fn _floor_f(f: Float) -> Float {
|
||||
let t: Int = __float_to_int(f)
|
||||
let tf: Float = __int_to_float(t)
|
||||
// if f was negative and not already an integer, subtract 1
|
||||
if f < 0.0 {
|
||||
if tf > f {
|
||||
return tf - 1.0
|
||||
}
|
||||
}
|
||||
return tf
|
||||
}
|
||||
|
||||
fn decimal_round(f: Float, decimals: Int) -> Float {
|
||||
let d: Int = decimals
|
||||
if d < 0 { d = 0 }
|
||||
if d > 15 { d = 15 }
|
||||
let mul: Float = _pow10(d)
|
||||
if f >= 0.0 {
|
||||
return _floor_f(f * mul + 0.5) / mul
|
||||
}
|
||||
return 0.0 - _floor_f((0.0 - f) * mul + 0.5) / mul
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// runtime/state.el — In-process key/value store.
|
||||
//
|
||||
// Thin El wrappers over the __state_* seed primitives. The backing store is
|
||||
// a process-wide hash map maintained by the El runtime (formerly el_runtime.c
|
||||
// lines 4632–4721: state_set, state_get, state_del, state_keys).
|
||||
//
|
||||
// Keys and values are Strings. Values are persistent across request boundaries
|
||||
// within the same process instance (they survive individual request lifetimes).
|
||||
// Concurrent access is serialized by the runtime; these wrappers are lock-free
|
||||
// from El's perspective.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __state_set(key: String, val: String)
|
||||
// __state_get(key: String) -> String
|
||||
// __state_del(key: String)
|
||||
// __state_keys() -> String (JSON array of key strings)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — set / get / del / keys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// state_set — store val under key. Overwrites any existing value.
|
||||
fn state_set(key: String, val: String) {
|
||||
__state_set(key, val)
|
||||
}
|
||||
|
||||
// state_get — retrieve the value for key. Returns "" if key is absent.
|
||||
fn state_get(key: String) -> String {
|
||||
return __state_get(key)
|
||||
}
|
||||
|
||||
// state_del — remove key from the store. No-op if key does not exist.
|
||||
fn state_del(key: String) {
|
||||
__state_del(key)
|
||||
}
|
||||
|
||||
// state_keys — return a JSON array string of all current keys.
|
||||
// e.g. ["foo","bar","baz"]
|
||||
// Matches legacy state_keys() which returns an ElList (here serialized as JSON).
|
||||
fn state_keys() -> String {
|
||||
return __state_keys()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Convenience helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// state_has — true if key is present (value is non-empty string).
|
||||
// Note: a key set to "" is indistinguishable from absent via state_get alone.
|
||||
fn state_has(key: String) -> Bool {
|
||||
let v: String = state_get(key)
|
||||
if str_eq(v, "") { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// state_get_or — return val for key, or default_val if key is absent.
|
||||
fn state_get_or(key: String, default_val: String) -> String {
|
||||
let v: String = state_get(key)
|
||||
if str_eq(v, "") { return default_val }
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// stdlib.el — El standard library master import file.
|
||||
//
|
||||
// Import this single file to get the full El runtime in the correct
|
||||
// dependency order. El programs can do:
|
||||
//
|
||||
// import "../foundation/el/runtime/stdlib.el"
|
||||
//
|
||||
// or, if El is installed via tools/install.sh:
|
||||
//
|
||||
// import "/usr/local/el/runtime/stdlib.el"
|
||||
//
|
||||
// Note: test.el is intentionally NOT included here — it is dev-only
|
||||
// and should be imported explicitly in test files only.
|
||||
//
|
||||
// Dependency order (each module may depend on earlier ones):
|
||||
// string — no deps
|
||||
// math — no deps
|
||||
// time — no deps
|
||||
// env — no deps
|
||||
// fs — no deps
|
||||
// exec — no deps
|
||||
// json — depends on string
|
||||
// http — depends on string, json
|
||||
// state — no deps
|
||||
// thread — depends on exec
|
||||
// channel — depends on thread, state
|
||||
// engram — depends on http, json, string
|
||||
// manifest — depends on fs, json, string
|
||||
|
||||
import "string.el"
|
||||
import "math.el"
|
||||
import "time.el"
|
||||
import "env.el"
|
||||
import "fs.el"
|
||||
import "exec.el"
|
||||
import "json.el"
|
||||
import "http.el"
|
||||
import "state.el"
|
||||
import "thread.el"
|
||||
import "channel.el"
|
||||
import "engram.el"
|
||||
import "manifest.el"
|
||||
@@ -0,0 +1,907 @@
|
||||
// runtime/string.el — String operations implemented in El.
|
||||
//
|
||||
// All functions delegate character-level work to the seed primitives declared
|
||||
// in el_seed.c. No C is written here; this is pure El source that compiles
|
||||
// to C via the normal El pipeline.
|
||||
//
|
||||
// Seed primitives used (provided by el_seed.c):
|
||||
// __str_len(s) -> Int
|
||||
// __str_char_at(s, i) -> Int (char code at byte index i)
|
||||
// __str_alloc(n) -> String (n-byte zero-filled mutable buffer)
|
||||
// __str_set_char(s, i, c) -> String (mutate s[i]=c, return s)
|
||||
// __str_cmp(a, b) -> Int (strcmp)
|
||||
// __str_ncmp(a, b, n) -> Int (strncmp)
|
||||
// __str_concat_raw(a, b) -> String
|
||||
// __str_slice_raw(s, lo, hi) -> String (substring copy [lo, hi))
|
||||
// __int_to_str(n) -> String
|
||||
// __str_to_int(s) -> Int
|
||||
// __float_to_str(f) -> String
|
||||
// __str_to_float(s) -> Float
|
||||
// __println(s)
|
||||
// __print(s)
|
||||
// __readline() -> String
|
||||
// __url_encode(s) -> String
|
||||
// __url_decode(s) -> String
|
||||
|
||||
// ── I/O ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn println(s: String) -> Void {
|
||||
__println(s)
|
||||
}
|
||||
|
||||
fn print(s: String) -> Void {
|
||||
__print(s)
|
||||
}
|
||||
|
||||
fn readline() -> String {
|
||||
return __readline()
|
||||
}
|
||||
|
||||
// ── Type conversions ──────────────────────────────────────────────────────────
|
||||
|
||||
fn int_to_str(n: Int) -> String {
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
fn str_to_int(s: String) -> Int {
|
||||
return __str_to_int(s)
|
||||
}
|
||||
|
||||
fn float_to_str(f: Float) -> String {
|
||||
return __float_to_str(f)
|
||||
}
|
||||
|
||||
fn str_to_float(s: String) -> Float {
|
||||
return __str_to_float(s)
|
||||
}
|
||||
|
||||
fn bool_to_str(b: Bool) -> String {
|
||||
if b { return "true" }
|
||||
return "false"
|
||||
}
|
||||
|
||||
// ── URL encoding ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn url_encode(s: String) -> String {
|
||||
return __url_encode(s)
|
||||
}
|
||||
|
||||
fn url_decode(s: String) -> String {
|
||||
return __url_decode(s)
|
||||
}
|
||||
|
||||
// ── Math ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn el_abs(n: Int) -> Int {
|
||||
if n < 0 { return 0 - n }
|
||||
return n
|
||||
}
|
||||
|
||||
fn el_max(a: Int, b: Int) -> Int {
|
||||
if a > b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
fn el_min(a: Int, b: Int) -> Int {
|
||||
if a < b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// ── Core string primitives ────────────────────────────────────────────────────
|
||||
|
||||
fn str_len(s: String) -> Int {
|
||||
return __str_len(s)
|
||||
}
|
||||
|
||||
fn str_eq(a: String, b: String) -> Bool {
|
||||
return __str_cmp(a, b) == 0
|
||||
}
|
||||
|
||||
fn str_concat(a: String, b: String) -> String {
|
||||
return __str_concat_raw(a, b)
|
||||
}
|
||||
|
||||
fn str_slice(s: String, start: Int, end: Int) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let lo: Int = start
|
||||
if lo < 0 { lo = 0 }
|
||||
if lo > slen { lo = slen }
|
||||
let hi: Int = end
|
||||
if hi < lo { hi = lo }
|
||||
if hi > slen { hi = slen }
|
||||
return __str_slice_raw(s, lo, hi)
|
||||
}
|
||||
|
||||
// ── Whitespace helpers (internal) ─────────────────────────────────────────────
|
||||
//
|
||||
// _is_ws: returns true for ASCII whitespace (space, tab, \n, \r, \f, \v).
|
||||
|
||||
fn _is_ws(c: Int) -> Bool {
|
||||
if c == 32 { return true } // space
|
||||
if c == 9 { return true } // tab
|
||||
if c == 10 { return true } // \n
|
||||
if c == 13 { return true } // \r
|
||||
if c == 12 { return true } // \f
|
||||
if c == 11 { return true } // \v
|
||||
return false
|
||||
}
|
||||
|
||||
// Scan forward from index 0; return index of first byte not in whitespace,
|
||||
// or n if the entire string is whitespace.
|
||||
fn _find_first_non_ws(s: String, n: Int) -> Int {
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
if !_is_ws(__str_char_at(s, i)) { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Scan backward from index n-1; return index of last non-whitespace byte,
|
||||
// or -1 if the entire string is whitespace.
|
||||
fn _find_last_non_ws(s: String, n: Int) -> Int {
|
||||
let i: Int = n - 1
|
||||
while i >= 0 {
|
||||
if !_is_ws(__str_char_at(s, i)) { return i }
|
||||
i = i - 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Comparison and search ─────────────────────────────────────────────────────
|
||||
|
||||
fn str_starts_with(s: String, prefix: String) -> Bool {
|
||||
let plen: Int = __str_len(prefix)
|
||||
let slen: Int = __str_len(s)
|
||||
if plen > slen { return false }
|
||||
return __str_ncmp(s, prefix, plen) == 0
|
||||
}
|
||||
|
||||
fn str_ends_with(s: String, suffix: String) -> Bool {
|
||||
let slen: Int = __str_len(s)
|
||||
let suflen: Int = __str_len(suffix)
|
||||
if suflen > slen { return false }
|
||||
let tail: String = __str_slice_raw(s, slen - suflen, slen)
|
||||
return __str_cmp(tail, suffix) == 0
|
||||
}
|
||||
|
||||
fn str_contains(s: String, sub: String) -> Bool {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return true }
|
||||
if sublen > slen { return false }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 { return true }
|
||||
i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn str_index_of(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return 0 }
|
||||
if sublen > slen { return -1 }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fn str_last_index_of(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return slen }
|
||||
if sublen > slen { return -1 }
|
||||
let last: Int = -1
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
last = i
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
fn str_index_of_all(s: String, sub: String) -> [Int] {
|
||||
let result: [Int] = el_list_empty()
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return result }
|
||||
if sublen > slen { return result }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
result = el_list_append(result, i)
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Return the byte index of the first character in s that appears in any_of,
|
||||
// or -1 if none found.
|
||||
fn str_find_chars(s: String, any_of: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let alen: Int = __str_len(any_of)
|
||||
if alen == 0 { return -1 }
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let j: Int = 0
|
||||
while j < alen {
|
||||
if c == __str_char_at(any_of, j) { return i }
|
||||
j = j + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Character access ──────────────────────────────────────────────────────────
|
||||
|
||||
// Return a one-character string at byte index i, or "" if out of range.
|
||||
fn str_char_at(s: String, i: Int) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if i < 0 { return "" }
|
||||
if i >= slen { return "" }
|
||||
return __str_slice_raw(s, i, i + 1)
|
||||
}
|
||||
|
||||
// Return the char code (byte value) at byte index i, or 0 if out of range.
|
||||
fn str_char_code(s: String, i: Int) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
if i < 0 { return 0 }
|
||||
if i >= slen { return 0 }
|
||||
return __str_char_at(s, i)
|
||||
}
|
||||
|
||||
// ── Case conversion ───────────────────────────────────────────────────────────
|
||||
|
||||
fn str_to_upper(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// a-z (97-122) -> A-Z (65-90): subtract 32
|
||||
if c >= 97 {
|
||||
if c <= 122 { c = c - 32 }
|
||||
}
|
||||
out = __str_set_char(out, i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn str_to_lower(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// A-Z (65-90) -> a-z (97-122): add 32
|
||||
if c >= 65 {
|
||||
if c <= 90 { c = c + 32 }
|
||||
}
|
||||
out = __str_set_char(out, i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Aliases used in existing El codebases.
|
||||
fn str_lower(s: String) -> String {
|
||||
return str_to_lower(s)
|
||||
}
|
||||
|
||||
fn str_upper(s: String) -> String {
|
||||
return str_to_upper(s)
|
||||
}
|
||||
|
||||
// ── Whitespace trimming ───────────────────────────────────────────────────────
|
||||
|
||||
fn str_trim(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let lo: Int = _find_first_non_ws(s, n)
|
||||
if lo == n { return "" }
|
||||
let hi: Int = _find_last_non_ws(s, n)
|
||||
return __str_slice_raw(s, lo, hi + 1)
|
||||
}
|
||||
|
||||
fn str_lstrip(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let lo: Int = _find_first_non_ws(s, n)
|
||||
if lo == n { return "" }
|
||||
return __str_slice_raw(s, lo, n)
|
||||
}
|
||||
|
||||
fn str_rstrip(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let hi: Int = _find_last_non_ws(s, n)
|
||||
if hi < 0 { return "" }
|
||||
return __str_slice_raw(s, 0, hi + 1)
|
||||
}
|
||||
|
||||
// ── Replacement ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn str_replace(s: String, from: String, to: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let flen: Int = __str_len(from)
|
||||
if flen == 0 { return s }
|
||||
if slen == 0 { return s }
|
||||
// Scan s left-to-right; emit `to` on each match, otherwise emit one byte.
|
||||
let result: String = ""
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
// Try to match `from` at position i
|
||||
if i + flen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + flen)
|
||||
if __str_cmp(window, from) == 0 {
|
||||
result = __str_concat_raw(result, to)
|
||||
i = i + flen
|
||||
} else {
|
||||
let ch: String = __str_slice_raw(s, i, i + 1)
|
||||
result = __str_concat_raw(result, ch)
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
// Not enough bytes left for a match — emit remainder and stop.
|
||||
let tail: String = __str_slice_raw(s, i, slen)
|
||||
result = __str_concat_raw(result, tail)
|
||||
i = slen
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── Repetition and reversal ───────────────────────────────────────────────────
|
||||
|
||||
fn str_repeat(s: String, n: Int) -> String {
|
||||
if n <= 0 { return "" }
|
||||
let slen: Int = __str_len(s)
|
||||
if slen == 0 { return "" }
|
||||
let result: String = ""
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
result = __str_concat_raw(result, s)
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Byte-reverse (correct for ASCII; for multi-byte UTF-8 codepoints this
|
||||
// reverses bytes within a codepoint, which is intentional at this tier —
|
||||
// Phase 2 will add grapheme-aware reversal).
|
||||
fn str_reverse(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
out = __str_set_char(out, n - 1 - i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ── Prefix/suffix stripping ───────────────────────────────────────────────────
|
||||
|
||||
fn str_strip_prefix(s: String, prefix: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let plen: Int = __str_len(prefix)
|
||||
if plen == 0 { return s }
|
||||
if plen > slen { return s }
|
||||
if __str_ncmp(s, prefix, plen) == 0 {
|
||||
return __str_slice_raw(s, plen, slen)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
fn str_strip_suffix(s: String, suffix: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let suflen: Int = __str_len(suffix)
|
||||
if suflen == 0 { return s }
|
||||
if suflen > slen { return s }
|
||||
let tail: String = __str_slice_raw(s, slen - suflen, slen)
|
||||
if __str_cmp(tail, suffix) == 0 {
|
||||
return __str_slice_raw(s, 0, slen - suflen)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Strip leading and trailing bytes whose char code appears in `chars`.
|
||||
fn str_strip_chars(s: String, chars: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let clen: Int = __str_len(chars)
|
||||
if slen == 0 { return "" }
|
||||
if clen == 0 { return s }
|
||||
let lo: Int = _find_first_not_in_charset(s, chars, slen, clen)
|
||||
if lo == slen { return "" }
|
||||
let hi: Int = _find_last_not_in_charset(s, chars, slen, clen)
|
||||
return __str_slice_raw(s, lo, hi + 1)
|
||||
}
|
||||
|
||||
// Internal: true if char code `c` is present in the charset string.
|
||||
fn _char_in_set(c: Int, chars: String, clen: Int) -> Bool {
|
||||
let j: Int = 0
|
||||
while j < clen {
|
||||
if c == __str_char_at(chars, j) { return true }
|
||||
j = j + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn _find_first_not_in_charset(s: String, chars: String, slen: Int, clen: Int) -> Int {
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if !_char_in_set(__str_char_at(s, i), chars, clen) { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return slen
|
||||
}
|
||||
|
||||
fn _find_last_not_in_charset(s: String, chars: String, slen: Int, clen: Int) -> Int {
|
||||
let i: Int = slen - 1
|
||||
while i >= 0 {
|
||||
if !_char_in_set(__str_char_at(s, i), chars, clen) { return i }
|
||||
i = i - 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Padding ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// Pad s on the left to `width` total chars, repeating `pad` cyclically.
|
||||
fn str_pad_left(s: String, width: Int, pad: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if slen >= width { return s }
|
||||
let plen: Int = __str_len(pad)
|
||||
if plen == 0 { return s }
|
||||
let need: Int = width - slen
|
||||
let prefix: String = ""
|
||||
let i: Int = 0
|
||||
while i < need {
|
||||
// Select pad character at position (i mod plen)
|
||||
let pad_idx: Int = i - (i / plen) * plen
|
||||
let pc: String = __str_slice_raw(pad, pad_idx, pad_idx + 1)
|
||||
prefix = __str_concat_raw(prefix, pc)
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(prefix, s)
|
||||
}
|
||||
|
||||
// Pad s on the right to `width` total chars, repeating `pad` cyclically.
|
||||
fn str_pad_right(s: String, width: Int, pad: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if slen >= width { return s }
|
||||
let plen: Int = __str_len(pad)
|
||||
if plen == 0 { return s }
|
||||
let need: Int = width - slen
|
||||
let suffix: String = ""
|
||||
let i: Int = 0
|
||||
while i < need {
|
||||
let pad_idx: Int = i - (i / plen) * plen
|
||||
let pc: String = __str_slice_raw(pad, pad_idx, pad_idx + 1)
|
||||
suffix = __str_concat_raw(suffix, pc)
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(s, suffix)
|
||||
}
|
||||
|
||||
// ── Counting ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// Count non-overlapping occurrences of `sub` in `s`. Empty sub returns 0.
|
||||
fn str_count(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return 0 }
|
||||
if sublen > slen { return 0 }
|
||||
let count: Int = 0
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
count = count + 1
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Byte count — alias of str_len.
|
||||
fn str_count_bytes(s: String) -> Int {
|
||||
return __str_len(s)
|
||||
}
|
||||
|
||||
// UTF-8 codepoint count: count bytes that are NOT continuation bytes (10xxxxxx).
|
||||
// Continuation bytes have the pattern 10xxxxxx = 0x80..0xBF (128..191).
|
||||
fn str_count_chars(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// Continuation bytes are in range [128, 191]; skip them.
|
||||
// All other bytes (< 128 ASCII, or >= 192 leading bytes) start a codepoint.
|
||||
if c < 128 {
|
||||
count = count + 1
|
||||
} else {
|
||||
if c >= 192 { count = count + 1 }
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count newline-delimited lines. A trailing newline does NOT add an extra empty line.
|
||||
fn str_count_lines(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return 0 }
|
||||
let count: Int = 0
|
||||
let has_content: Bool = false
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
has_content = true
|
||||
if c == 10 { // \n
|
||||
count = count + 1
|
||||
has_content = false
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
if has_content { count = count + 1 }
|
||||
return count
|
||||
}
|
||||
|
||||
// Count whitespace-delimited words (non-empty tokens).
|
||||
fn str_count_words(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let in_word: Bool = false
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if _is_ws(c) {
|
||||
in_word = false
|
||||
} else {
|
||||
if !in_word {
|
||||
in_word = true
|
||||
count = count + 1
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count ASCII letters [A-Za-z].
|
||||
fn str_count_letters(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c >= 65 {
|
||||
if c <= 90 { count = count + 1 } // A-Z
|
||||
}
|
||||
if c >= 97 {
|
||||
if c <= 122 { count = count + 1 } // a-z
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count ASCII decimal digits [0-9].
|
||||
fn str_count_digits(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c >= 48 {
|
||||
if c <= 57 { count = count + 1 } // '0'-'9'
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// ── Character classification ──────────────────────────────────────────────────
|
||||
//
|
||||
// For all predicates: empty string -> false.
|
||||
// Multi-char string: ALL bytes must satisfy the predicate.
|
||||
|
||||
fn is_letter(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 65 { if c <= 90 { ok = true } } // A-Z
|
||||
if c >= 97 { if c <= 122 { ok = true } } // a-z
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_digit(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 48 { return false } // '0'
|
||||
if c > 57 { return false } // '9'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_alphanumeric(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 48 { if c <= 57 { ok = true } } // 0-9
|
||||
if c >= 65 { if c <= 90 { ok = true } } // A-Z
|
||||
if c >= 97 { if c <= 122 { ok = true } } // a-z
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_whitespace(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
if !_is_ws(__str_char_at(s, i)) { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ASCII punctuation: 33-47, 58-64, 91-96, 123-126.
|
||||
fn is_punctuation(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 33 { if c <= 47 { ok = true } }
|
||||
if c >= 58 { if c <= 64 { ok = true } }
|
||||
if c >= 91 { if c <= 96 { ok = true } }
|
||||
if c >= 123 { if c <= 126 { ok = true } }
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_uppercase(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 65 { return false } // 'A'
|
||||
if c > 90 { return false } // 'Z'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_lowercase(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 97 { return false } // 'a'
|
||||
if c > 122 { return false } // 'z'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Splitting ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn str_split(s: String, sep: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let slen: Int = __str_len(s)
|
||||
let seplen: Int = __str_len(sep)
|
||||
// Empty separator: return the whole string as a single element.
|
||||
if seplen == 0 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let part_start: Int = 0
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if i + seplen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + seplen)
|
||||
if __str_cmp(window, sep) == 0 {
|
||||
let part: String = __str_slice_raw(s, part_start, i)
|
||||
result = el_list_append(result, part)
|
||||
i = i + seplen
|
||||
part_start = i
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
// Append remaining tail (may be empty string if s ended with sep).
|
||||
let tail: String = __str_slice_raw(s, part_start, slen)
|
||||
result = el_list_append(result, tail)
|
||||
return result
|
||||
}
|
||||
|
||||
// Split into at most n parts. The nth part (index n-1) contains the remainder
|
||||
// verbatim, including any further separators. n <= 0 returns []. n == 1
|
||||
// returns [s].
|
||||
fn str_split_n(s: String, sep: String, n: Int) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
if n <= 0 { return result }
|
||||
if n == 1 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let slen: Int = __str_len(s)
|
||||
let seplen: Int = __str_len(sep)
|
||||
if seplen == 0 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let part_start: Int = 0
|
||||
let parts: Int = 0
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if parts >= n - 1 {
|
||||
// Reached the split limit — stop splitting, emit the rest below.
|
||||
i = slen
|
||||
} else {
|
||||
if i + seplen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + seplen)
|
||||
if __str_cmp(window, sep) == 0 {
|
||||
let part: String = __str_slice_raw(s, part_start, i)
|
||||
result = el_list_append(result, part)
|
||||
i = i + seplen
|
||||
part_start = i
|
||||
parts = parts + 1
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remainder verbatim.
|
||||
let tail: String = __str_slice_raw(s, part_start, slen)
|
||||
result = el_list_append(result, tail)
|
||||
return result
|
||||
}
|
||||
|
||||
// Split on newlines. \r\n is folded to \n. Trailing empty line after a
|
||||
// final \n is dropped — so "a\nb\n" yields ["a", "b"], not ["a", "b", ""].
|
||||
fn str_split_lines(s: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return result }
|
||||
let line_start: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c == 10 { // \n
|
||||
let lend: Int = i
|
||||
// Fold \r\n: if the byte before \n is \r, exclude it.
|
||||
if lend > line_start {
|
||||
if __str_char_at(s, lend - 1) == 13 { lend = lend - 1 }
|
||||
}
|
||||
let line: String = __str_slice_raw(s, line_start, lend)
|
||||
result = el_list_append(result, line)
|
||||
line_start = i + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
// Trailing content with no terminating \n.
|
||||
if line_start < n {
|
||||
let line: String = __str_slice_raw(s, line_start, n)
|
||||
result = el_list_append(result, line)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Split into a list of one-byte strings (byte-level chars).
|
||||
fn str_split_chars(s: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let n: Int = __str_len(s)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let ch: String = __str_slice_raw(s, i, i + 1)
|
||||
result = el_list_append(result, ch)
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── Joining ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// Join a list of strings with a separator between consecutive elements.
|
||||
// Empty list yields "". Non-string elements should not be passed here.
|
||||
fn str_join(parts: [String], sep: String) -> String {
|
||||
let n: Int = el_list_len(parts)
|
||||
if n == 0 { return "" }
|
||||
let result: String = el_list_get(parts, 0)
|
||||
let i: Int = 1
|
||||
while i < n {
|
||||
result = __str_concat_raw(result, sep)
|
||||
result = __str_concat_raw(result, el_list_get(parts, i))
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── DHARMA byte encoding (str_to_bytes) ──────────────────────────────────────
|
||||
//
|
||||
// str_to_bytes — encode a string as a JSON array of unsigned byte values.
|
||||
// "hi" -> "[104,105]"
|
||||
// Used by db.el to store content in Engram JSON nodes as a byte array.
|
||||
// Note: bytes_to_str (the inverse) is defined in json.el because it depends
|
||||
// on json_array_get_string which is defined there.
|
||||
fn str_to_bytes(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "[]" }
|
||||
let result: String = "["
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let b: Int = __str_char_at(s, i)
|
||||
if i > 0 { result = __str_concat_raw(result, ",") }
|
||||
result = __str_concat_raw(result, __int_to_str(b))
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(result, "]")
|
||||
}
|
||||
|
||||
// ── Cryptographic hashing ─────────────────────────────────────────────────────
|
||||
|
||||
// hash_sha256 — return the SHA-256 hex digest of a string.
|
||||
// Delegates to the __sha256_hex seed primitive.
|
||||
fn hash_sha256(s: String) -> String {
|
||||
return __sha256_hex(s)
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
// runtime/test.el — El test framework: assertions, registration, and runner.
|
||||
//
|
||||
// Provides a minimal but complete test harness for El programs. No external
|
||||
// dependencies. Written entirely in El using existing runtime primitives.
|
||||
//
|
||||
// ── Quick-start ──────────────────────────────────────────────────────────────
|
||||
//
|
||||
// 1. Write a test function with the standard test signature:
|
||||
//
|
||||
// fn test_str_eq_works(_: String) -> String {
|
||||
// assert_true(str_eq("a", "a"), "same strings are equal")
|
||||
// assert_false(str_eq("a", "b"), "different strings are not equal")
|
||||
// return ""
|
||||
// }
|
||||
//
|
||||
// 2. Register it and run:
|
||||
//
|
||||
// fn main() -> Void {
|
||||
// test_case("str_eq works", "test_str_eq_works")
|
||||
// test_run_all()
|
||||
// }
|
||||
//
|
||||
// Test functions must have the signature (String) -> String. The argument is
|
||||
// a dummy passed by the threading mechanism (see runtime/thread.el) and should
|
||||
// be ignored. The return value is likewise ignored — results flow through the
|
||||
// state-based assertion primitives.
|
||||
//
|
||||
// ── State keys ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// _test_cases JSON array of {"name":"...","fn":"..."}
|
||||
// _test_pass_count Int as string — total passing assertions
|
||||
// _test_fail_count Int as string — total failing assertions
|
||||
// _test_failures JSON array of failure message strings
|
||||
// _test_current Name of the test case currently executing
|
||||
//
|
||||
// ── Dependencies ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// runtime/string.el — str_eq, str_concat, str_contains, int_to_str, ...
|
||||
// runtime/state.el — state_set, state_get
|
||||
// runtime/json.el — json_array_len, json_array_get_string, json_get,
|
||||
// json_escape_string
|
||||
// runtime/thread.el — spawn, join (for dynamic dispatch via dlsym)
|
||||
|
||||
// ── Internal: JSON array helpers ─────────────────────────────────────────────
|
||||
//
|
||||
// _test_json_append — append a quoted, escaped string element to a JSON array.
|
||||
//
|
||||
// Given an existing JSON array string (e.g. '["a","b"]') and a plain string
|
||||
// value, returns a new array with the value appended (e.g. '["a","b","c"]').
|
||||
//
|
||||
// The array must be non-empty — always init with "[]" before calling.
|
||||
fn _test_json_append(arr: String, val: String) -> String {
|
||||
let escaped: String = json_escape_string(val)
|
||||
let inner: String = str_slice(arr, 1, str_len(arr) - 1)
|
||||
if str_eq(inner, "") {
|
||||
return "[\"" + escaped + "\"]"
|
||||
}
|
||||
return "[" + inner + ",\"" + escaped + "\"]"
|
||||
}
|
||||
|
||||
// _test_json_obj_append — append a raw JSON object string to a JSON array.
|
||||
//
|
||||
// Used to build the _test_cases list where each element is already a
|
||||
// JSON object (not a plain string).
|
||||
fn _test_json_obj_append(arr: String, obj: String) -> String {
|
||||
let inner: String = str_slice(arr, 1, str_len(arr) - 1)
|
||||
if str_eq(inner, "") {
|
||||
return "[" + obj + "]"
|
||||
}
|
||||
return "[" + inner + "," + obj + "]"
|
||||
}
|
||||
|
||||
// ── Test registration ─────────────────────────────────────────────────────────
|
||||
|
||||
// test_case — register a named test case.
|
||||
//
|
||||
// name: Human-readable test case name (shown in output).
|
||||
// fn_name: Name of a top-level El function with signature
|
||||
// (String) -> String. The function should call assertion
|
||||
// primitives from this module. The String arg it receives is ""
|
||||
// and its return value is ignored.
|
||||
//
|
||||
// Test cases are stored in the _test_cases state key and executed in
|
||||
// registration order by test_run_all().
|
||||
fn test_case(name: String, fn_name: String) {
|
||||
let arr: String = state_get("_test_cases")
|
||||
if str_eq(arr, "") { arr = "[]" }
|
||||
let escaped_name: String = json_escape_string(name)
|
||||
let escaped_fn: String = json_escape_string(fn_name)
|
||||
let obj: String = "{\"name\":\"" + escaped_name + "\",\"fn\":\"" + escaped_fn + "\"}"
|
||||
let arr = _test_json_obj_append(arr, obj)
|
||||
state_set("_test_cases", arr)
|
||||
}
|
||||
|
||||
// ── Test state helpers ────────────────────────────────────────────────────────
|
||||
|
||||
// _test_init — reset all test counters and failure lists.
|
||||
//
|
||||
// Called at the start of test_run_all(). Safe to call multiple times.
|
||||
fn _test_init() {
|
||||
state_set("_test_pass_count", "0")
|
||||
state_set("_test_fail_count", "0")
|
||||
state_set("_test_failures", "[]")
|
||||
state_set("_test_current", "")
|
||||
}
|
||||
|
||||
// _test_inc_pass — increment the global pass counter by 1.
|
||||
fn _test_inc_pass() {
|
||||
let n: Int = str_to_int(state_get("_test_pass_count"))
|
||||
state_set("_test_pass_count", int_to_str(n + 1))
|
||||
}
|
||||
|
||||
// _test_inc_fail — increment the global fail counter by 1.
|
||||
fn _test_inc_fail() {
|
||||
let n: Int = str_to_int(state_get("_test_fail_count"))
|
||||
state_set("_test_fail_count", int_to_str(n + 1))
|
||||
}
|
||||
|
||||
// test_pass — record a passing assertion for the current test case.
|
||||
//
|
||||
// Increments the pass counter. Called internally by assertions.
|
||||
fn test_pass(name: String) {
|
||||
_test_inc_pass()
|
||||
}
|
||||
|
||||
// test_fail — record a failing assertion for the current test case.
|
||||
//
|
||||
// name: assertion label or description (usually the msg parameter)
|
||||
// msg: detailed failure message including expected/got values
|
||||
//
|
||||
// Increments the fail counter and appends the message to _test_failures.
|
||||
// Also prints the failure immediately for visibility.
|
||||
fn test_fail(name: String, msg: String) {
|
||||
_test_inc_fail()
|
||||
let failures: String = state_get("_test_failures")
|
||||
if str_eq(failures, "") { failures = "[]" }
|
||||
let entry: String = " " + msg
|
||||
let failures = _test_json_append(failures, entry)
|
||||
state_set("_test_failures", failures)
|
||||
println(" FAIL: " + msg)
|
||||
}
|
||||
|
||||
// ── Assertions ────────────────────────────────────────────────────────────────
|
||||
|
||||
// assert_true — assert that condition is true.
|
||||
//
|
||||
// condition: the boolean value to test
|
||||
// msg: description shown on failure
|
||||
fn assert_true(condition: Bool, msg: String) {
|
||||
if condition {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected true, got false")
|
||||
}
|
||||
|
||||
// assert_false — assert that condition is false.
|
||||
//
|
||||
// condition: the boolean value to test
|
||||
// msg: description shown on failure
|
||||
fn assert_false(condition: Bool, msg: String) {
|
||||
if !condition {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected false, got true")
|
||||
}
|
||||
|
||||
// assert_eq — assert that two strings are equal.
|
||||
//
|
||||
// a, b: strings to compare
|
||||
// msg: description shown on failure (quoted values appended automatically)
|
||||
fn assert_eq(a: String, b: String, msg: String) {
|
||||
if str_eq(a, b) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected \"" + b + "\", got \"" + a + "\"")
|
||||
}
|
||||
|
||||
// assert_int_eq — assert that two integers are equal.
|
||||
//
|
||||
// a, b: integers to compare
|
||||
// msg: description shown on failure
|
||||
fn assert_int_eq(a: Int, b: Int, msg: String) {
|
||||
if a == b {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected " + int_to_str(b) + ", got " + int_to_str(a))
|
||||
}
|
||||
|
||||
// assert_neq — assert that two strings are NOT equal.
|
||||
//
|
||||
// a, b: strings to compare
|
||||
// msg: description shown on failure
|
||||
fn assert_neq(a: String, b: String, msg: String) {
|
||||
if !str_eq(a, b) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected values to differ, but both are \"" + a + "\"")
|
||||
}
|
||||
|
||||
// assert_contains — assert that string s contains substring sub.
|
||||
//
|
||||
// s: haystack string
|
||||
// sub: needle substring
|
||||
// msg: description shown on failure
|
||||
fn assert_contains(s: String, sub: String, msg: String) {
|
||||
if str_contains(s, sub) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not contain \"" + sub + "\"")
|
||||
}
|
||||
|
||||
// assert_starts_with — assert that string s starts with prefix.
|
||||
//
|
||||
// s: string to inspect
|
||||
// prefix: expected prefix
|
||||
// msg: description shown on failure
|
||||
fn assert_starts_with(s: String, prefix: String, msg: String) {
|
||||
if str_starts_with(s, prefix) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not start with \"" + prefix + "\"")
|
||||
}
|
||||
|
||||
// assert_ends_with — assert that string s ends with suffix.
|
||||
//
|
||||
// s: string to inspect
|
||||
// suffix: expected suffix
|
||||
// msg: description shown on failure
|
||||
fn assert_ends_with(s: String, suffix: String, msg: String) {
|
||||
if str_ends_with(s, suffix) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not end with \"" + suffix + "\"")
|
||||
}
|
||||
|
||||
// fail — unconditional test failure.
|
||||
//
|
||||
// msg: failure message shown in output
|
||||
//
|
||||
// Use when a code path that must not be reached is reached, or when an
|
||||
// expected exception did not occur.
|
||||
fn fail(msg: String) {
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg)
|
||||
}
|
||||
|
||||
// ── Runner ────────────────────────────────────────────────────────────────────
|
||||
|
||||
// _test_run_one — execute a single registered test case by name.
|
||||
//
|
||||
// name: the human-readable test case name (set as _test_current)
|
||||
// fn_name: the El function to invoke via the thread mechanism
|
||||
//
|
||||
// Sets _test_current so that assertions inside the test function know which
|
||||
// test they belong to. Spawns and immediately joins the test function in a
|
||||
// child thread (same dlsym mechanism as parallel_map) so dynamic dispatch
|
||||
// works without needing closures.
|
||||
fn _test_run_one(name: String, fn_name: String) {
|
||||
state_set("_test_current", name)
|
||||
let before_fail: Int = str_to_int(state_get("_test_fail_count"))
|
||||
let tid: Int = __thread_create(fn_name, "")
|
||||
__thread_join(tid)
|
||||
let after_fail: Int = str_to_int(state_get("_test_fail_count"))
|
||||
if after_fail == before_fail {
|
||||
println("[test] " + name + " ... PASS")
|
||||
} else {
|
||||
println("[test] " + name + " ... FAIL")
|
||||
}
|
||||
}
|
||||
|
||||
// test_run_all — execute all registered test cases and print a summary.
|
||||
//
|
||||
// Iterates through every test case registered via test_case(), runs each one,
|
||||
// prints per-test PASS/FAIL status, then prints a summary line.
|
||||
//
|
||||
// Returns the total number of failing assertions. Exit with this value to
|
||||
// signal CI failure:
|
||||
//
|
||||
// fn main() -> Int {
|
||||
// test_case("str_eq", "test_str_eq")
|
||||
// return test_run_all()
|
||||
// }
|
||||
fn test_run_all() -> Int {
|
||||
_test_init()
|
||||
|
||||
let cases: String = state_get("_test_cases")
|
||||
if str_eq(cases, "") { cases = "[]" }
|
||||
|
||||
let n: Int = json_array_len(cases)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let entry: String = json_array_get(cases, i)
|
||||
let name: String = json_get(entry, "name")
|
||||
let fn_name: String = json_get(entry, "fn")
|
||||
_test_run_one(name, fn_name)
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
let pass_count: Int = str_to_int(state_get("_test_pass_count"))
|
||||
let fail_count: Int = str_to_int(state_get("_test_fail_count"))
|
||||
println("[test] Summary: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed")
|
||||
|
||||
return fail_count
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// thread.el — El native threading model
|
||||
//
|
||||
// First-class parallelism for El. Eliminates bash fan-out hacks for parallel
|
||||
// HTTP dispatch, concurrent processing pipelines, and any other workload that
|
||||
// benefits from concurrent execution.
|
||||
//
|
||||
// Built on four seed primitives exposed by el_seed.c via dlsym+pthread:
|
||||
// __thread_create(fn_name, arg) -> Int spawn thread, return tid
|
||||
// __thread_join(tid) -> String join thread, return result
|
||||
// __mutex_new() -> Int allocate a mutex, return handle
|
||||
// __mutex_lock(m) lock mutex
|
||||
// __mutex_unlock(m) unlock mutex
|
||||
//
|
||||
// Every El fn compiles to a global C symbol. __thread_create uses dlsym to
|
||||
// look up the function by name and run it in a pthread. This means any El fn
|
||||
// with signature (String) -> String is directly threadable.
|
||||
|
||||
// ── Core primitives ──────────────────────────────────────────────────────────
|
||||
|
||||
// spawn — launch an El function in a new thread.
|
||||
//
|
||||
// fn_name: the name of an El fn with signature (String) -> String
|
||||
// arg: the argument to pass to that fn
|
||||
//
|
||||
// Returns a thread id (tid) that can be passed to join().
|
||||
// The El function must be a top-level fn — its C symbol must be globally
|
||||
// visible so dlsym can resolve it.
|
||||
fn spawn(fn_name: String, arg: String) -> Int {
|
||||
return __thread_create(fn_name, arg)
|
||||
}
|
||||
|
||||
// join — wait for a thread to finish and return its result.
|
||||
//
|
||||
// tid: the thread id returned by spawn()
|
||||
//
|
||||
// Blocks until the thread completes. Returns the String value the thread
|
||||
// function returned.
|
||||
fn join(tid: Int) -> String {
|
||||
return __thread_join(tid)
|
||||
}
|
||||
|
||||
// ── parallel_map ─────────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_map — map an El function over a list of strings concurrently.
|
||||
//
|
||||
// items: [String] — the input list
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
//
|
||||
// Spawns one thread per item. All threads run concurrently. Joins each thread
|
||||
// in input order, so the output list preserves the same order as the input.
|
||||
//
|
||||
// This is the core primitive that replaces bash fan-out for parallel HTTP.
|
||||
// Example — dispatch to N rooms at once:
|
||||
// let responses: [String] = parallel_map(room_payloads, "dispatch_to_room")
|
||||
fn parallel_map(items: [String], fn_name: String) -> [String] {
|
||||
let n: Int = el_list_len(items)
|
||||
|
||||
// Phase 1: spawn all threads and collect tids in order.
|
||||
let tids: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let tid: Int = spawn(fn_name, item)
|
||||
// Store tid as string so we can hold it in [String].
|
||||
// int_to_str is available as a builtin.
|
||||
let tids = el_list_append(tids, int_to_str(tid))
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Phase 2: join all threads in order, collecting results.
|
||||
let results: [String] = el_list_empty()
|
||||
let j = 0
|
||||
while j < n {
|
||||
let tid_str: String = el_list_get(tids, j)
|
||||
let tid: Int = str_to_int(tid_str)
|
||||
let result: String = join(tid)
|
||||
let results = el_list_append(results, result)
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ── parallel_map_json ────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_map_json — parallel_map over a JSON array string.
|
||||
//
|
||||
// items_json: String — a JSON array of strings, e.g. '["a","b","c"]'
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
//
|
||||
// Parses the JSON array into an [String], runs parallel_map, then serialises
|
||||
// the result list back to a JSON array string. Both input and output are JSON
|
||||
// strings — the common El inter-service format.
|
||||
//
|
||||
// Example:
|
||||
// let out_json: String = parallel_map_json(rooms_json, "dispatch_to_room")
|
||||
fn parallel_map_json(items_json: String, fn_name: String) -> String {
|
||||
let n: Int = json_array_len(items_json)
|
||||
|
||||
// Unpack JSON array into [String].
|
||||
let items: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = json_array_get(items_json, i)
|
||||
let items = el_list_append(items, item)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Run the concurrent map.
|
||||
let results: [String] = parallel_map(items, fn_name)
|
||||
|
||||
// Repack results into a JSON array string.
|
||||
let m: Int = el_list_len(results)
|
||||
let out: String = "["
|
||||
let j = 0
|
||||
while j < m {
|
||||
let val: String = el_list_get(results, j)
|
||||
if j > 0 {
|
||||
let out = out + ","
|
||||
}
|
||||
// Each result is treated as a raw JSON value (object, array, or
|
||||
// quoted string as returned by the worker fn).
|
||||
let out = out + val
|
||||
let j = j + 1
|
||||
}
|
||||
let out = out + "]"
|
||||
return out
|
||||
}
|
||||
|
||||
// ── parallel_filter ──────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_filter — keep items where fn_name returns "true", concurrently.
|
||||
//
|
||||
// items: [String] — the input list
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
// that returns "true" to keep the item or anything else
|
||||
// to discard it
|
||||
//
|
||||
// Runs the predicate fn on all items in parallel. Collects results in order,
|
||||
// preserving the relative order of kept items.
|
||||
fn parallel_filter(items: [String], fn_name: String) -> [String] {
|
||||
let n: Int = el_list_len(items)
|
||||
|
||||
// Spawn a predicate thread for every item.
|
||||
let tids: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let tid: Int = spawn(fn_name, item)
|
||||
let tids = el_list_append(tids, int_to_str(tid))
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Join in order, keep item if the predicate returned "true".
|
||||
let kept: [String] = el_list_empty()
|
||||
let j = 0
|
||||
while j < n {
|
||||
let item: String = el_list_get(items, j)
|
||||
let tid_str: String = el_list_get(tids, j)
|
||||
let tid: Int = str_to_int(tid_str)
|
||||
let verdict: String = join(tid)
|
||||
if str_eq(verdict, "true") {
|
||||
let kept = el_list_append(kept, item)
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
return kept
|
||||
}
|
||||
|
||||
// ── HTTP helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
// fire_http_post — worker fn for parallel_posts.
|
||||
//
|
||||
// Expects arg to be a JSON object with "url" and "body" keys:
|
||||
// {"url":"https://...","body":"{...}"}
|
||||
//
|
||||
// Returns the HTTP response body string. Registered as a global El fn so
|
||||
// parallel_map can locate it via dlsym.
|
||||
fn fire_http_post(arg: String) -> String {
|
||||
let url: String = json_get(arg, "url")
|
||||
let body: String = json_get(arg, "body")
|
||||
return http_post(url, body)
|
||||
}
|
||||
|
||||
// parallel_posts — fire a list of HTTP POSTs concurrently.
|
||||
//
|
||||
// requests: [String] — each element is a JSON object {"url":"...","body":"..."}
|
||||
//
|
||||
// Returns [String] of response bodies in the same order as the input.
|
||||
//
|
||||
// Example — fan out to N room endpoints at once:
|
||||
// let reqs: [String] = el_list_empty()
|
||||
// let reqs = el_list_append(reqs, "{\"url\":\"http://room-a/dispatch\",\"body\":\"" + payload + "\"}")
|
||||
// let reqs = el_list_append(reqs, "{\"url\":\"http://room-b/dispatch\",\"body\":\"" + payload + "\"}")
|
||||
// let responses: [String] = parallel_posts(reqs)
|
||||
fn parallel_posts(requests: [String]) -> [String] {
|
||||
return parallel_map(requests, "fire_http_post")
|
||||
}
|
||||
|
||||
// ── Mutex helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// with_mutex — call fn_name(arg) while holding mutex m.
|
||||
//
|
||||
// m: Int — mutex handle returned by __mutex_new()
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
// arg: String — argument to pass to fn_name
|
||||
//
|
||||
// Locks the mutex, spawns fn_name(arg) in a child thread, joins to collect
|
||||
// the result, then unlocks. The mutex is held across the entire duration of
|
||||
// fn_name's execution, serializing concurrent callers.
|
||||
//
|
||||
// Note: fn_name must NOT itself acquire the same mutex — that would deadlock.
|
||||
// This is the standard reentrant-mutex caveat.
|
||||
//
|
||||
// Usage:
|
||||
// let m: Int = __mutex_new()
|
||||
// let result: String = with_mutex(m, "update_shared_state", payload)
|
||||
fn with_mutex(m: Int, fn_name: String, arg: String) -> String {
|
||||
__mutex_lock(m)
|
||||
let tid: Int = spawn(fn_name, arg)
|
||||
let result: String = join(tid)
|
||||
__mutex_unlock(m)
|
||||
return result
|
||||
}
|
||||
+425
@@ -0,0 +1,425 @@
|
||||
// runtime/time.el — Time operations, sleep, and formatting.
|
||||
//
|
||||
// Implements the time surface from el-compiler/runtime/legacy/el_runtime.c
|
||||
// (lines 3334–3440, 3471–3656) in pure El, using seed primitives.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __time_now_ns() -> Int (nanoseconds since Unix epoch)
|
||||
// __sleep_ms(n: Int)
|
||||
// __int_to_str(n: Int) -> String
|
||||
// __str_to_int(s: String) -> Int
|
||||
// __float_to_str(f: Float) -> String
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — now / sleep
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// time_now — milliseconds since Unix epoch (UTC). Matches legacy time_now().
|
||||
fn time_now() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// time_now_utc — same as time_now; UTC alias kept for compatibility.
|
||||
fn time_now_utc() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// now_ns — nanoseconds since Unix epoch. Matches el_now_instant().
|
||||
fn now_ns() -> Int {
|
||||
return __time_now_ns()
|
||||
}
|
||||
|
||||
// unix_timestamp — whole seconds since Unix epoch. Matches unix_timestamp().
|
||||
fn unix_timestamp() -> Int {
|
||||
return __time_now_ns() / 1000000000
|
||||
}
|
||||
|
||||
// sleep_secs — block for n seconds. Clamps negatives to 0.
|
||||
fn sleep_secs(n: Int) {
|
||||
if n < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(n * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// sleep_ms — block for n milliseconds. Clamps negatives to 0.
|
||||
fn sleep_ms(n: Int) {
|
||||
if n < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(n)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gregorian decomposition helpers — pure integer arithmetic.
|
||||
//
|
||||
// Algorithm: civil date from days since Unix epoch (1970-01-01).
|
||||
// Based on Howard Hinnant's public-domain civil_from_days formula
|
||||
// (http://howardhinnant.github.io/date_algorithms.html), which the legacy
|
||||
// gmtime_r call performs under the hood.
|
||||
//
|
||||
// We expose the pieces as private helpers (leading underscore convention).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// _is_leap — 1 if year y is a Gregorian leap year, 0 otherwise.
|
||||
fn _is_leap(y: Int) -> Int {
|
||||
if y % 400 == 0 { return 1 }
|
||||
if y % 100 == 0 { return 0 }
|
||||
if y % 4 == 0 { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
// _days_in_month — number of days in month m of year y (m: 1..12).
|
||||
fn _days_in_month(y: Int, m: Int) -> Int {
|
||||
if m == 1 { return 31 }
|
||||
if m == 2 {
|
||||
if _is_leap(y) == 1 { return 29 }
|
||||
return 28
|
||||
}
|
||||
if m == 3 { return 31 }
|
||||
if m == 4 { return 30 }
|
||||
if m == 5 { return 31 }
|
||||
if m == 6 { return 30 }
|
||||
if m == 7 { return 31 }
|
||||
if m == 8 { return 31 }
|
||||
if m == 9 { return 30 }
|
||||
if m == 10 { return 31 }
|
||||
if m == 11 { return 30 }
|
||||
return 31
|
||||
}
|
||||
|
||||
// _pad2 — zero-pad an integer to at least 2 digits.
|
||||
fn _pad2(n: Int) -> String {
|
||||
if n < 10 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _pad4 — zero-pad an integer to at least 4 digits.
|
||||
fn _pad4(n: Int) -> String {
|
||||
if n < 10 { return "000" + __int_to_str(n) }
|
||||
if n < 100 { return "00" + __int_to_str(n) }
|
||||
if n < 1000 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _pad3 — zero-pad an integer to at least 3 digits (milliseconds).
|
||||
fn _pad3(n: Int) -> String {
|
||||
if n < 10 { return "00" + __int_to_str(n) }
|
||||
if n < 100 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _civil_year_month_day — decompose days-since-epoch (z, may be negative)
|
||||
// into year/month/day using the civil_from_days algorithm.
|
||||
// Returns a JSON object: {"year":Y,"month":M,"day":D}
|
||||
fn _civil_ymd(z: Int) -> String {
|
||||
// shift epoch to 0000-03-01 (makes leap-day math clean)
|
||||
let zz: Int = z + 719468
|
||||
// era: 400-year block
|
||||
let era: Int = zz / 146097
|
||||
if zz < 0 {
|
||||
era = (zz - 146096) / 146097
|
||||
}
|
||||
let doe: Int = zz - era * 146097 // day-of-era [0, 146096]
|
||||
let yoe: Int = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365 // year-of-era [0, 399]
|
||||
let y: Int = yoe + era * 400
|
||||
let doy: Int = doe - (365 * yoe + yoe / 4 - yoe / 100) // day-of-year [0, 365]
|
||||
let mp: Int = (5 * doy + 2) / 153 // month in [0, 11] from March
|
||||
let d: Int = doy - (153 * mp + 2) / 5 + 1 // day [1, 31]
|
||||
let m: Int = mp + 3
|
||||
if mp >= 10 { m = mp - 9 }
|
||||
if mp >= 10 { y = y + 1 }
|
||||
return "{\"year\":" + __int_to_str(y) + ",\"month\":" + __int_to_str(m) + ",\"day\":" + __int_to_str(d) + "}"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_to_parts — decompose a millisecond timestamp into UTC components.
|
||||
//
|
||||
// Returns a JSON string:
|
||||
// {"year":Y,"month":M,"day":D,"hour":H,"minute":M,"second":S,"ms":MS}
|
||||
//
|
||||
// Matches legacy time_to_parts() which returns an ElMap with the same keys.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_to_parts(ts: Int) -> String {
|
||||
let ms_raw: Int = ts % 1000
|
||||
let ms: Int = ms_raw
|
||||
if ms_raw < 0 { ms = ms_raw + 1000 }
|
||||
|
||||
let s_raw: Int = ts / 1000
|
||||
let s: Int = s_raw
|
||||
if ms_raw < 0 { s = s_raw - 1 }
|
||||
|
||||
// seconds within the day and days since epoch
|
||||
let sec_of_day: Int = s % 86400
|
||||
let day_z: Int = s / 86400
|
||||
// handle negative: floor division
|
||||
if sec_of_day < 0 {
|
||||
sec_of_day = sec_of_day + 86400
|
||||
day_z = day_z - 1
|
||||
}
|
||||
|
||||
let hour: Int = sec_of_day / 3600
|
||||
let rem: Int = sec_of_day % 3600
|
||||
let minute: Int = rem / 60
|
||||
let second: Int = rem % 60
|
||||
|
||||
// date components via civil decomposition
|
||||
let ymd: String = _civil_ymd(day_z)
|
||||
let year: Int = __str_to_int(json_get(ymd, "year"))
|
||||
let month: Int = __str_to_int(json_get(ymd, "month"))
|
||||
let day: Int = __str_to_int(json_get(ymd, "day"))
|
||||
|
||||
return "{\"year\":" + __int_to_str(year) +
|
||||
",\"month\":" + __int_to_str(month) +
|
||||
",\"day\":" + __int_to_str(day) +
|
||||
",\"hour\":" + __int_to_str(hour) +
|
||||
",\"minute\":" + __int_to_str(minute) +
|
||||
",\"second\":" + __int_to_str(second) +
|
||||
",\"ms\":" + __int_to_str(ms) + "}"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_format — format a millisecond timestamp as a string.
|
||||
//
|
||||
// fmt "ISO" (or empty) → "YYYY-MM-DDTHH:MM:SS.mmmZ" (ISO 8601 UTC)
|
||||
// Other fmt values are passed as a strftime-style hint; the El runtime
|
||||
// implements the most common tokens. Unsupported tokens are passed through.
|
||||
//
|
||||
// Matches legacy time_format().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_format(ts: Int, fmt: String) -> String {
|
||||
let parts: String = time_to_parts(ts)
|
||||
let y: Int = __str_to_int(json_get(parts, "year"))
|
||||
let mo: Int = __str_to_int(json_get(parts, "month"))
|
||||
let d: Int = __str_to_int(json_get(parts, "day"))
|
||||
let h: Int = __str_to_int(json_get(parts, "hour"))
|
||||
let mi: Int = __str_to_int(json_get(parts, "minute"))
|
||||
let s: Int = __str_to_int(json_get(parts, "second"))
|
||||
let ms: Int = __str_to_int(json_get(parts, "ms"))
|
||||
|
||||
// ISO 8601 UTC: YYYY-MM-DDTHH:MM:SS.mmmZ
|
||||
if str_eq(fmt, "ISO") {
|
||||
return _pad4(y) + "-" + _pad2(mo) + "-" + _pad2(d) +
|
||||
"T" + _pad2(h) + ":" + _pad2(mi) + ":" + _pad2(s) +
|
||||
"." + _pad3(ms) + "Z"
|
||||
}
|
||||
if str_eq(fmt, "") {
|
||||
return _pad4(y) + "-" + _pad2(mo) + "-" + _pad2(d) +
|
||||
"T" + _pad2(h) + ":" + _pad2(mi) + ":" + _pad2(s) +
|
||||
"." + _pad3(ms) + "Z"
|
||||
}
|
||||
|
||||
// strftime-subset: replace common tokens
|
||||
let out: String = fmt
|
||||
let out = str_replace(out, "%Y", _pad4(y))
|
||||
let out = str_replace(out, "%m", _pad2(mo))
|
||||
let out = str_replace(out, "%d", _pad2(d))
|
||||
let out = str_replace(out, "%H", _pad2(h))
|
||||
let out = str_replace(out, "%M", _pad2(mi))
|
||||
let out = str_replace(out, "%S", _pad2(s))
|
||||
let out = str_replace(out, "%3N", _pad3(ms))
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_from_parts — construct a ms timestamp from seconds + nanosecond offset.
|
||||
//
|
||||
// Matches legacy time_from_parts(secs, ns, tz) — tz is ignored (UTC assumed).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_from_parts(secs: Int, ns: Int, tz: String) -> Int {
|
||||
return secs * 1000 + ns / 1000000
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_add — add a duration to a millisecond timestamp.
|
||||
//
|
||||
// unit: "ms" | "sec" | "min" | "hour" | "day"
|
||||
// Matches legacy time_add().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_add(ts: Int, n: Int, unit: String) -> Int {
|
||||
if str_eq(unit, "ms") { return ts + n }
|
||||
if str_eq(unit, "sec") { return ts + n * 1000 }
|
||||
if str_eq(unit, "min") { return ts + n * 60000 }
|
||||
if str_eq(unit, "hour") { return ts + n * 3600000 }
|
||||
if str_eq(unit, "day") { return ts + n * 86400000 }
|
||||
// default: treat as ms
|
||||
return ts + n
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_diff — compute the difference between two millisecond timestamps.
|
||||
//
|
||||
// Returns ts2 - ts1 in the given unit.
|
||||
// unit: "ms" | "sec" | "min" | "hour" | "day"
|
||||
// Matches legacy time_diff().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_diff(ts1: Int, ts2: Int, unit: String) -> Int {
|
||||
let d: Int = ts2 - ts1
|
||||
if str_eq(unit, "ms") { return d }
|
||||
if str_eq(unit, "sec") { return d / 1000 }
|
||||
if str_eq(unit, "min") { return d / 60000 }
|
||||
if str_eq(unit, "hour") { return d / 3600000 }
|
||||
if str_eq(unit, "day") { return d / 86400000 }
|
||||
return d
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Instant / Duration — nanosecond-precision temporal types.
|
||||
//
|
||||
// These match the el_now_instant, duration_seconds, duration_millis, etc.
|
||||
// family from legacy lines 3471–3656. Both Instant and Duration are Int
|
||||
// (nanoseconds); the type distinction is at the call-site convention level.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// now — current Instant in nanoseconds. Alias for __time_now_ns().
|
||||
fn now() -> Int {
|
||||
return __time_now_ns()
|
||||
}
|
||||
|
||||
// unix_seconds — Instant from whole seconds since epoch.
|
||||
fn unix_seconds(n: Int) -> Int {
|
||||
return n * 1000000000
|
||||
}
|
||||
|
||||
// unix_millis — Instant from milliseconds since epoch.
|
||||
fn unix_millis(n: Int) -> Int {
|
||||
return n * 1000000
|
||||
}
|
||||
|
||||
// instant_to_unix_seconds — convert Instant nanoseconds to whole seconds.
|
||||
fn instant_to_unix_seconds(i: Int) -> Int {
|
||||
return i / 1000000000
|
||||
}
|
||||
|
||||
// instant_to_unix_millis — convert Instant nanoseconds to milliseconds.
|
||||
fn instant_to_unix_millis(i: Int) -> Int {
|
||||
return i / 1000000
|
||||
}
|
||||
|
||||
// instant_to_iso8601 — format an Instant (nanoseconds) as ISO 8601 UTC.
|
||||
fn instant_to_iso8601(i: Int) -> String {
|
||||
let ms: Int = i / 1000000
|
||||
return time_format(ms, "ISO")
|
||||
}
|
||||
|
||||
// duration_seconds — Duration from n whole seconds.
|
||||
fn duration_seconds(n: Int) -> Int {
|
||||
return n * 1000000000
|
||||
}
|
||||
|
||||
// duration_millis — Duration from n milliseconds.
|
||||
fn duration_millis(n: Int) -> Int {
|
||||
return n * 1000000
|
||||
}
|
||||
|
||||
// duration_nanos — Duration from n nanoseconds (identity).
|
||||
fn duration_nanos(n: Int) -> Int {
|
||||
return n
|
||||
}
|
||||
|
||||
// duration_to_seconds — convert a Duration (nanoseconds) to whole seconds.
|
||||
fn duration_to_seconds(d: Int) -> Int {
|
||||
return d / 1000000000
|
||||
}
|
||||
|
||||
// duration_to_millis — convert a Duration (nanoseconds) to milliseconds.
|
||||
fn duration_to_millis(d: Int) -> Int {
|
||||
return d / 1000000
|
||||
}
|
||||
|
||||
// duration_to_nanos — return the Duration as nanoseconds (identity).
|
||||
fn duration_to_nanos(d: Int) -> Int {
|
||||
return d
|
||||
}
|
||||
|
||||
// sleep_duration — sleep for a Duration (nanoseconds). Clamps negatives to 0.
|
||||
fn sleep_duration(dur: Int) {
|
||||
let ms: Int = dur / 1000000
|
||||
if ms < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(ms)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TTL cache — time-bounded key/value backed by state.
|
||||
//
|
||||
// Matches legacy ttl_cache_set / ttl_cache_get / ttl_cache_age (lines 3663–3717).
|
||||
// max_age is a Duration (nanoseconds).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ttl_cache_set — store a value and record the current Instant for TTL checks.
|
||||
fn ttl_cache_set(key: String, value: String) {
|
||||
state_set(key, value)
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let now_str: String = __int_to_str(__time_now_ns())
|
||||
state_set(stamp_key, now_str)
|
||||
}
|
||||
|
||||
// ttl_cache_get — return value if age < max_age (nanoseconds), else "".
|
||||
fn ttl_cache_get(key: String, max_age: Int) -> String {
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let sv: String = state_get(stamp_key)
|
||||
if str_eq(sv, "") { return "" }
|
||||
let set_at: Int = __str_to_int(sv)
|
||||
let now_ns: Int = __time_now_ns()
|
||||
let age: Int = now_ns - set_at
|
||||
if age < 0 { return "" }
|
||||
if age > max_age { return "" }
|
||||
return state_get(key)
|
||||
}
|
||||
|
||||
// ttl_cache_age — nanoseconds since a key was last set (INT_MAX sentinel if missing).
|
||||
fn ttl_cache_age(key: String) -> Int {
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let sv: String = state_get(stamp_key)
|
||||
if str_eq(sv, "") { return 9223372036854775807 }
|
||||
let set_at: Int = __str_to_int(sv)
|
||||
return __time_now_ns() - set_at
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// uuid_new / uuid_v4 — generate a UUID v4 string.
|
||||
//
|
||||
// Delegates to the __uuid_v4() seed primitive.
|
||||
// Matches legacy uuid_new() / uuid_v4() (lines 4602–4621).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn uuid_new() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
fn uuid_v4() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DHARMA-compatible aliases — millisecond-precision timestamps.
|
||||
//
|
||||
// now_millis, unix_timestamp_ms, and time_now_ms all return the same value:
|
||||
// milliseconds since the Unix epoch. They exist because different parts of
|
||||
// the dharma codebase use different names for the same concept.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// now_millis — milliseconds since Unix epoch. Alias for time_now().
|
||||
fn now_millis() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// unix_timestamp_ms — same as now_millis.
|
||||
fn unix_timestamp_ms() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// time_now_ms — same as now_millis.
|
||||
fn time_now_ms() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
+45
-275
@@ -1,6 +1,6 @@
|
||||
# El JavaScript Backend (codegen-js)
|
||||
|
||||
**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.
|
||||
**Status:** scaffolded. Hello-world compiles and runs. ~50% language coverage. Core runtime (~30 builtins) implemented. CGI / DHARMA / LLM / Engram intentionally stubbed.
|
||||
|
||||
**Authoritative files**
|
||||
|
||||
@@ -57,36 +57,27 @@ 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 (~90 builtins)
|
||||
### Implemented today (~30 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`, `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` |
|
||||
| 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` |
|
||||
| Map | `el_map_new`, `el_get_field`, `el_map_get`, `el_map_set` |
|
||||
| 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` |
|
||||
| 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` |
|
||||
| Time | `time_now`, `time_now_utc`, `sleep_secs` (Node), `sleep_ms` |
|
||||
| Bool | `bool_to_str` |
|
||||
| Process | `exit_program` (Node `process.exit`) |
|
||||
| Refcount | `el_retain`, `el_release` (no-ops) |
|
||||
| Method shortforms | `append`, `len`, `get`, `map_get`, `map_set` |
|
||||
| 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` |
|
||||
| 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` / `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 |
|
||||
| `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 |
|
||||
|
||||
### Stubbed (throw at runtime)
|
||||
|
||||
@@ -137,54 +128,17 @@ The runtime auto-detects via `typeof window === 'undefined'`.
|
||||
|
||||
---
|
||||
|
||||
## 5. The async problem
|
||||
## 5. The async problem (the big deferred decision)
|
||||
|
||||
`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.
|
||||
3. **Explicit `@async` decorator on El functions; codegen-js emits `async function` + `await` for known-async call sites.** This is the approach implemented.
|
||||
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.
|
||||
|
||||
**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`.
|
||||
**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.
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -196,123 +150,35 @@ JS `number` is IEEE 754 double — only 53 bits of integer precision. El `Int` i
|
||||
|
||||
---
|
||||
|
||||
## 7. Language features — JS target coverage
|
||||
## 7. What's NOT supported in JS target initially
|
||||
|
||||
### 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)
|
||||
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.
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| `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 |
|
||||
| VBD role check | Not enforced | Same |
|
||||
| Float bit-cast | Not needed | JS number is already a double |
|
||||
| Crypto primitives | Stub throws | Add via `crypto.subtle` later |
|
||||
| `state_*` | In-memory only | Resets on page reload |
|
||||
| Crypto primitives | Stub throws | Easy to add via `crypto.subtle` later |
|
||||
| `state_*` | In-memory only | No persistence; 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:
|
||||
@@ -329,116 +195,20 @@ 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.** 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.
|
||||
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.
|
||||
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 for Phase 6 is now just translation effort, not language gaps. Phase 5 removed the last structural barriers.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// 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"
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// 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")
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// 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"
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
// 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"
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
// 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"
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// 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")
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
// 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"
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// 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"
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
// 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"
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// tests/runtime/string_test.el — Test suite for runtime/string.el
|
||||
//
|
||||
// Exercises every public function exported by runtime/string.el using the
|
||||
// runtime/test.el framework. Each test function covers one string primitive
|
||||
// or a tight family of related functions.
|
||||
//
|
||||
// ── Build and run ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// # Combine runtime modules + test framework + this file, then compile:
|
||||
// cat runtime/string.el runtime/math.el runtime/state.el runtime/env.el \
|
||||
// runtime/fs.el runtime/exec.el runtime/time.el runtime/json.el \
|
||||
// runtime/http.el runtime/engram.el runtime/thread.el \
|
||||
// runtime/test.el \
|
||||
// tests/runtime/string_test.el > /tmp/string_test_combined.el
|
||||
//
|
||||
// ./dist/platform/elc /tmp/string_test_combined.el > /tmp/string_test.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o /tmp/string_test /tmp/string_test.c el-compiler/runtime/el_seed.c
|
||||
// /tmp/string_test; echo "exit: $?"
|
||||
//
|
||||
// Exit code equals the number of failing assertions (0 = all pass).
|
||||
//
|
||||
// ── Coverage ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// str_eq str_neq (via assert_neq)
|
||||
// str_len str_concat
|
||||
// str_starts_with str_ends_with
|
||||
// str_contains str_index_of
|
||||
// str_slice str_replace
|
||||
// str_to_upper str_to_lower
|
||||
// str_trim str_lstrip / str_rstrip
|
||||
// str_split str_join
|
||||
// int_to_str str_to_int
|
||||
// str_repeat str_reverse
|
||||
// str_count
|
||||
|
||||
// ── str_eq ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_eq(_: String) -> String {
|
||||
assert_true(str_eq("hello", "hello"), "identical strings are equal")
|
||||
assert_false(str_eq("hello", "world"), "different strings are not equal")
|
||||
assert_true(str_eq("", ""), "empty strings are equal")
|
||||
assert_false(str_eq("a", ""), "non-empty vs empty is not equal")
|
||||
assert_false(str_eq("", "a"), "empty vs non-empty is not equal")
|
||||
assert_false(str_eq("Hello", "hello"), "case-sensitive comparison")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_len ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_len(_: String) -> String {
|
||||
assert_int_eq(str_len(""), 0, "empty string has length 0")
|
||||
assert_int_eq(str_len("a"), 1, "single char has length 1")
|
||||
assert_int_eq(str_len("hello"), 5, "hello has length 5")
|
||||
assert_int_eq(str_len("hello world"), 11, "space included in length")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_concat ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_concat(_: String) -> String {
|
||||
assert_eq(str_concat("hello", " world"), "hello world", "basic concat")
|
||||
assert_eq(str_concat("", "world"), "world", "empty prefix")
|
||||
assert_eq(str_concat("hello", ""), "hello", "empty suffix")
|
||||
assert_eq(str_concat("", ""), "", "both empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_starts_with ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_starts_with(_: String) -> String {
|
||||
assert_true(str_starts_with("hello world", "hello"), "prefix present")
|
||||
assert_false(str_starts_with("hello world", "world"), "not a prefix")
|
||||
assert_true(str_starts_with("hello", "hello"), "string is its own prefix")
|
||||
assert_true(str_starts_with("hello", ""), "empty prefix always true")
|
||||
assert_false(str_starts_with("", "a"), "empty string has no prefix")
|
||||
assert_false(str_starts_with("hi", "hello"), "prefix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_ends_with ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_ends_with(_: String) -> String {
|
||||
assert_true(str_ends_with("hello world", "world"), "suffix present")
|
||||
assert_false(str_ends_with("hello world", "hello"), "not a suffix")
|
||||
assert_true(str_ends_with("hello", "hello"), "string is its own suffix")
|
||||
assert_true(str_ends_with("hello", ""), "empty suffix always true")
|
||||
assert_false(str_ends_with("", "a"), "empty string has no suffix")
|
||||
assert_false(str_ends_with("hi", "world"), "suffix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_contains ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_contains(_: String) -> String {
|
||||
assert_true(str_contains("hello world", "world"), "contains at end")
|
||||
assert_true(str_contains("hello world", "hello"), "contains at start")
|
||||
assert_true(str_contains("hello world", "lo wo"), "contains in middle")
|
||||
assert_false(str_contains("hello world", "xyz"), "not contained")
|
||||
assert_true(str_contains("hello", ""), "empty sub always contained")
|
||||
assert_false(str_contains("", "a"), "empty string contains nothing")
|
||||
assert_true(str_contains("hello", "hello"), "string contains itself")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_index_of ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_index_of(_: String) -> String {
|
||||
assert_int_eq(str_index_of("hello world", "world"), 6, "index of suffix")
|
||||
assert_int_eq(str_index_of("hello world", "hello"), 0, "index of prefix")
|
||||
assert_int_eq(str_index_of("hello world", "o"), 4, "index of first occurrence")
|
||||
assert_int_eq(str_index_of("hello world", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_index_of("hello", ""), 0, "empty sub returns 0")
|
||||
assert_int_eq(str_index_of("", "a"), -1, "search in empty string")
|
||||
assert_int_eq(str_index_of("aababc", "ab"), 1, "finds first occurrence")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_replace ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_replace(_: String) -> String {
|
||||
assert_eq(str_replace("hello world", "world", "there"), "hello there", "basic replace")
|
||||
assert_eq(str_replace("aaa", "a", "b"), "bbb", "replace all occurrences")
|
||||
assert_eq(str_replace("hello", "xyz", "abc"), "hello", "no match is identity")
|
||||
assert_eq(str_replace("", "a", "b"), "", "empty string unchanged")
|
||||
assert_eq(str_replace("hello", "", "x"), "hello", "empty from is identity")
|
||||
assert_eq(str_replace("hello hello", "hello", "bye"), "bye bye", "replace multiple")
|
||||
assert_eq(str_replace("aXbXc", "X", "-"), "a-b-c", "single-char delimiter")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_slice ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_slice(_: String) -> String {
|
||||
assert_eq(str_slice("hello world", 0, 5), "hello", "slice from start")
|
||||
assert_eq(str_slice("hello world", 6, 11), "world", "slice from middle")
|
||||
assert_eq(str_slice("hello world", 0, 0), "", "zero-length slice")
|
||||
assert_eq(str_slice("hello", 0, 100), "hello", "end beyond length clamped")
|
||||
assert_eq(str_slice("hello", 3, 3), "", "start == end is empty")
|
||||
assert_eq(str_slice("hello world", 2, 7), "llo w", "interior slice")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_to_upper / str_to_lower ──────────────────────────────────────────────
|
||||
|
||||
fn test_str_to_upper(_: String) -> String {
|
||||
assert_eq(str_to_upper("hello"), "HELLO", "lowercase to uppercase")
|
||||
assert_eq(str_to_upper("HELLO"), "HELLO", "already uppercase unchanged")
|
||||
assert_eq(str_to_upper("Hello World"), "HELLO WORLD", "mixed case")
|
||||
assert_eq(str_to_upper(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_upper("hello123"), "HELLO123", "digits unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_lower(_: String) -> String {
|
||||
assert_eq(str_to_lower("HELLO"), "hello", "uppercase to lowercase")
|
||||
assert_eq(str_to_lower("hello"), "hello", "already lowercase unchanged")
|
||||
assert_eq(str_to_lower("Hello World"), "hello world", "mixed case")
|
||||
assert_eq(str_to_lower(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_lower("HELLO123"), "hello123", "digits unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_trim ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_trim(_: String) -> String {
|
||||
assert_eq(str_trim(" hello "), "hello", "trims spaces both sides")
|
||||
assert_eq(str_trim("hello"), "hello", "no whitespace unchanged")
|
||||
assert_eq(str_trim(" "), "", "all-space string becomes empty")
|
||||
assert_eq(str_trim(""), "", "empty string unchanged")
|
||||
assert_eq(str_trim("\t hello \n"), "hello", "trims tabs and newlines")
|
||||
assert_eq(str_trim(" hello world "), "hello world", "internal spaces preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_split ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_split(_: String) -> String {
|
||||
let parts: [String] = str_split("a,b,c", ",")
|
||||
assert_int_eq(el_list_len(parts), 3, "split yields 3 parts")
|
||||
assert_eq(el_list_get(parts, 0), "a", "first part is a")
|
||||
assert_eq(el_list_get(parts, 1), "b", "second part is b")
|
||||
assert_eq(el_list_get(parts, 2), "c", "third part is c")
|
||||
|
||||
let single: [String] = str_split("hello", ",")
|
||||
assert_int_eq(el_list_len(single), 1, "no sep found yields 1 part")
|
||||
assert_eq(el_list_get(single, 0), "hello", "single part is the full string")
|
||||
|
||||
let trailing: [String] = str_split("a,b,", ",")
|
||||
assert_int_eq(el_list_len(trailing), 3, "trailing sep yields empty last element")
|
||||
assert_eq(el_list_get(trailing, 2), "", "last element is empty")
|
||||
|
||||
let empty_str: [String] = str_split("", ",")
|
||||
assert_int_eq(el_list_len(empty_str), 1, "splitting empty string yields one empty element")
|
||||
|
||||
let multi: [String] = str_split("one::two::three", "::")
|
||||
assert_int_eq(el_list_len(multi), 3, "multi-char separator works")
|
||||
assert_eq(el_list_get(multi, 0), "one", "multi-sep first part")
|
||||
assert_eq(el_list_get(multi, 2), "three", "multi-sep last part")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_join ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_join(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "a")
|
||||
let parts = el_list_append(parts, "b")
|
||||
let parts = el_list_append(parts, "c")
|
||||
assert_eq(str_join(parts, ","), "a,b,c", "basic join with comma")
|
||||
assert_eq(str_join(parts, ""), "abc", "join with empty separator")
|
||||
assert_eq(str_join(parts, " | "), "a | b | c", "join with multi-char sep")
|
||||
|
||||
let empty_list: [String] = el_list_empty()
|
||||
assert_eq(str_join(empty_list, ","), "", "joining empty list yields empty string")
|
||||
|
||||
let one: [String] = el_list_empty()
|
||||
let one = el_list_append(one, "solo")
|
||||
assert_eq(str_join(one, ","), "solo", "joining single element yields that element")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── int_to_str / str_to_int ──────────────────────────────────────────────────
|
||||
|
||||
fn test_int_to_str(_: String) -> String {
|
||||
assert_eq(int_to_str(0), "0", "zero")
|
||||
assert_eq(int_to_str(42), "42", "positive integer")
|
||||
assert_eq(int_to_str(-1), "-1", "negative integer")
|
||||
assert_eq(int_to_str(1000000), "1000000", "large integer")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_int(_: String) -> String {
|
||||
assert_int_eq(str_to_int("0"), 0, "zero")
|
||||
assert_int_eq(str_to_int("42"), 42, "positive integer")
|
||||
assert_int_eq(str_to_int("-1"), -1, "negative integer")
|
||||
assert_int_eq(str_to_int("1000000"), 1000000, "large integer")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_repeat ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_repeat(_: String) -> String {
|
||||
assert_eq(str_repeat("ab", 3), "ababab", "repeat 3 times")
|
||||
assert_eq(str_repeat("x", 1), "x", "repeat once")
|
||||
assert_eq(str_repeat("x", 0), "", "repeat zero times yields empty")
|
||||
assert_eq(str_repeat("", 5), "", "repeating empty string yields empty")
|
||||
assert_eq(str_repeat("-", 4), "----", "single char repeat")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_reverse ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_reverse(_: String) -> String {
|
||||
assert_eq(str_reverse("hello"), "olleh", "basic reverse")
|
||||
assert_eq(str_reverse("a"), "a", "single char is its own reverse")
|
||||
assert_eq(str_reverse(""), "", "empty string reverses to empty")
|
||||
assert_eq(str_reverse("abcd"), "dcba", "even-length reverse")
|
||||
assert_eq(str_reverse("racecar"), "racecar", "palindrome is unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_count ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_count(_: String) -> String {
|
||||
assert_int_eq(str_count("hello world hello", "hello"), 2, "two occurrences")
|
||||
assert_int_eq(str_count("aaa", "a"), 3, "adjacent single chars")
|
||||
assert_int_eq(str_count("aaa", "aa"), 1, "non-overlapping: one match")
|
||||
assert_int_eq(str_count("hello", "xyz"), 0, "no match")
|
||||
assert_int_eq(str_count("", "a"), 0, "empty string has no occurrences")
|
||||
assert_int_eq(str_count("hello", ""), 0, "empty sub returns 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_strip_prefix / str_strip_suffix ───────────────────────────────────────
|
||||
|
||||
fn test_str_strip_prefix(_: String) -> String {
|
||||
assert_eq(str_strip_prefix("foobar", "foo"), "bar", "strips matching prefix")
|
||||
assert_eq(str_strip_prefix("foobar", "baz"), "foobar", "no-match is identity")
|
||||
assert_eq(str_strip_prefix("hello", ""), "hello", "empty prefix is identity")
|
||||
assert_eq(str_strip_prefix("hello", "hello"), "", "full match yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_strip_suffix(_: String) -> String {
|
||||
assert_eq(str_strip_suffix("hello.md", ".md"), "hello", "strips matching suffix")
|
||||
assert_eq(str_strip_suffix("hello.md", ".txt"), "hello.md", "no-match is identity")
|
||||
assert_eq(str_strip_suffix("hello", ""), "hello", "empty suffix is identity")
|
||||
assert_eq(str_strip_suffix("hello", "hello"), "", "full match yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_find_chars ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_find_chars(_: String) -> String {
|
||||
assert_int_eq(str_find_chars("hello world", " "), 5, "finds space at index 5")
|
||||
assert_int_eq(str_find_chars("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_find_chars("hello", ""), -1, "empty charset returns -1")
|
||||
assert_int_eq(str_find_chars("hello", "aeiou"), 1, "finds first vowel at index 1")
|
||||
assert_int_eq(str_find_chars("", "a"), -1, "search in empty string returns -1")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_last_index_of ─────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_last_index_of(_: String) -> String {
|
||||
assert_int_eq(str_last_index_of("hello hello", "hello"), 6, "last occurrence")
|
||||
assert_int_eq(str_last_index_of("hello", "hello"), 0, "single occurrence")
|
||||
assert_int_eq(str_last_index_of("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_last_index_of("aababc", "ab"), 3, "last ab in aababc")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() -> Int {
|
||||
test_case("str_eq", "test_str_eq")
|
||||
test_case("str_len", "test_str_len")
|
||||
test_case("str_concat", "test_str_concat")
|
||||
test_case("str_starts_with", "test_str_starts_with")
|
||||
test_case("str_ends_with", "test_str_ends_with")
|
||||
test_case("str_contains", "test_str_contains")
|
||||
test_case("str_index_of", "test_str_index_of")
|
||||
test_case("str_replace", "test_str_replace")
|
||||
test_case("str_slice", "test_str_slice")
|
||||
test_case("str_to_upper", "test_str_to_upper")
|
||||
test_case("str_to_lower", "test_str_to_lower")
|
||||
test_case("str_trim", "test_str_trim")
|
||||
test_case("str_split", "test_str_split")
|
||||
test_case("str_join", "test_str_join")
|
||||
test_case("int_to_str", "test_int_to_str")
|
||||
test_case("str_to_int", "test_str_to_int")
|
||||
test_case("str_repeat", "test_str_repeat")
|
||||
test_case("str_reverse", "test_str_reverse")
|
||||
test_case("str_count", "test_str_count")
|
||||
test_case("str_strip_prefix", "test_str_strip_prefix")
|
||||
test_case("str_strip_suffix", "test_str_strip_suffix")
|
||||
test_case("str_find_chars", "test_str_find_chars")
|
||||
test_case("str_last_index_of", "test_str_last_index_of")
|
||||
return test_run_all()
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* tests/suite/infra.c — El runtime infrastructure functions for the test suite.
|
||||
*
|
||||
* The El test suite compiles runtime/*.el files into tests.c via elc, then links
|
||||
* only against el_seed.c (not the full el_runtime.c, which would cause ~120
|
||||
* duplicate symbol errors). This file provides the infrastructure symbols that
|
||||
* tests.c references but that are NOT defined in either the generated El code or
|
||||
* el_seed.c:
|
||||
*
|
||||
* ─ ElList machinery: el_list_empty, el_list_append, el_list_len, el_list_get,
|
||||
* el_str_concat, native_list_*, len, get
|
||||
* ─ el_runtime_init_args (needed by the generated main())
|
||||
* ─ Stubs for http_*, engram_*, el_html_sanitize (not needed by tests)
|
||||
*
|
||||
* These implementations are derived from el_runtime.c.
|
||||
*/
|
||||
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
#include "el_seed.h"
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── String arena (minimal — no HTTP request lifecycle needed) ─────────────── */
|
||||
|
||||
static char* el_strdup(const char* s) {
|
||||
if (!s) return strdup("");
|
||||
return strdup(s);
|
||||
}
|
||||
|
||||
static char* el_strbuf(size_t n) {
|
||||
char* p = malloc(n + 1);
|
||||
if (!p) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
p[0] = '\0';
|
||||
return p;
|
||||
}
|
||||
|
||||
static el_val_t el_wrap_str(char* s) {
|
||||
return EL_STR(s);
|
||||
}
|
||||
|
||||
/* ── el_str_concat ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t av, el_val_t bv) {
|
||||
const char* a = EL_CSTR(av);
|
||||
const char* b = EL_CSTR(bv);
|
||||
if (!a) a = "";
|
||||
if (!b) b = "";
|
||||
size_t la = strlen(a);
|
||||
size_t lb = strlen(b);
|
||||
char* out = el_strbuf(la + lb);
|
||||
memcpy(out, a, la);
|
||||
memcpy(out + la, b, lb);
|
||||
out[la + lb] = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* ── ElList ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
#define EL_MAGIC_LIST 0xE15710A1u
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint32_t refcount;
|
||||
} ElHeader;
|
||||
|
||||
typedef struct {
|
||||
ElHeader hdr;
|
||||
int64_t length;
|
||||
int64_t capacity;
|
||||
el_val_t* elems;
|
||||
} ElList;
|
||||
|
||||
static ElList* list_alloc(int64_t cap) {
|
||||
if (cap < 4) cap = 4;
|
||||
ElList* lst = malloc(sizeof(ElList));
|
||||
if (!lst) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
lst->hdr.magic = EL_MAGIC_LIST;
|
||||
lst->hdr.refcount = 1;
|
||||
lst->length = 0;
|
||||
lst->capacity = cap;
|
||||
lst->elems = malloc((size_t)cap * sizeof(el_val_t));
|
||||
if (!lst->elems) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
return lst;
|
||||
}
|
||||
|
||||
el_val_t el_list_empty(void) {
|
||||
return EL_STR(list_alloc(4));
|
||||
}
|
||||
|
||||
el_val_t el_list_len(el_val_t listv) {
|
||||
ElList* lst = (ElList*)(uintptr_t)listv;
|
||||
if (!lst) return 0;
|
||||
return lst->length;
|
||||
}
|
||||
|
||||
el_val_t el_list_get(el_val_t listv, el_val_t index) {
|
||||
ElList* lst = (ElList*)(uintptr_t)listv;
|
||||
if (!lst) return 0;
|
||||
if (index < 0 || index >= lst->length) return 0;
|
||||
return lst->elems[index];
|
||||
}
|
||||
|
||||
el_val_t el_list_append(el_val_t listv, el_val_t elem) {
|
||||
ElList* old = (ElList*)(uintptr_t)listv;
|
||||
if (!old) {
|
||||
ElList* fresh = list_alloc(4);
|
||||
fresh->elems[0] = elem;
|
||||
fresh->length = 1;
|
||||
return EL_STR(fresh);
|
||||
}
|
||||
|
||||
if (old->hdr.refcount <= 1) {
|
||||
if (old->length >= old->capacity) {
|
||||
int64_t new_cap = old->capacity > 0 ? old->capacity * 2 : 4;
|
||||
el_val_t* grown = realloc(old->elems, (size_t)new_cap * sizeof(el_val_t));
|
||||
if (!grown) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
old->elems = grown;
|
||||
old->capacity = new_cap;
|
||||
}
|
||||
old->elems[old->length++] = elem;
|
||||
return listv;
|
||||
}
|
||||
|
||||
int64_t new_cap = old->length + 1;
|
||||
if (new_cap < 4) new_cap = 4;
|
||||
ElList* fresh = malloc(sizeof(ElList));
|
||||
if (!fresh) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
fresh->hdr.magic = EL_MAGIC_LIST;
|
||||
fresh->hdr.refcount = 1;
|
||||
fresh->length = old->length + 1;
|
||||
fresh->capacity = new_cap;
|
||||
fresh->elems = malloc((size_t)new_cap * sizeof(el_val_t));
|
||||
if (!fresh->elems) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
if (old->length > 0) {
|
||||
memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t));
|
||||
}
|
||||
fresh->elems[old->length] = elem;
|
||||
return EL_STR(fresh);
|
||||
}
|
||||
|
||||
/* ── native_list aliases ──────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t native_list_empty(void) { return el_list_empty(); }
|
||||
el_val_t native_list_append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); }
|
||||
el_val_t native_list_get(el_val_t list, el_val_t index) { return el_list_get(list, index); }
|
||||
el_val_t native_list_len(el_val_t list) { return el_list_len(list); }
|
||||
|
||||
/* ── len / get aliases ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t len(el_val_t list) { return el_list_len(list); }
|
||||
el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); }
|
||||
|
||||
/* ── el_runtime_init_args ─────────────────────────────────────────────────── */
|
||||
|
||||
static el_val_t _el_args_list = 0;
|
||||
|
||||
void el_runtime_init_args(int argc, char** argv) {
|
||||
_el_args_list = el_list_empty();
|
||||
for (int i = 1; i < argc; i++) {
|
||||
_el_args_list = el_list_append(_el_args_list, EL_STR(argv[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── HTTP stubs (not exercised by test suite) ─────────────────────────────── */
|
||||
|
||||
el_val_t http_post(el_val_t url, el_val_t body) {
|
||||
(void)url; (void)body;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t http_post_json(el_val_t url, el_val_t body) {
|
||||
(void)url; (void)body;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers) {
|
||||
(void)url; (void)body; (void)headers;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t http_response(el_val_t status, el_val_t headers, el_val_t body) {
|
||||
(void)status; (void)headers; (void)body;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
void http_serve(el_val_t port, el_val_t handler) {
|
||||
(void)port; (void)handler;
|
||||
}
|
||||
|
||||
void http_serve_v2(el_val_t port, el_val_t handler) {
|
||||
(void)port; (void)handler;
|
||||
}
|
||||
|
||||
/* ── el_html_sanitize stub ────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_html_sanitize(el_val_t input, el_val_t allowlist) {
|
||||
(void)allowlist;
|
||||
return input;
|
||||
}
|
||||
|
||||
/* ── Engram stubs (not exercised by test suite) ───────────────────────────── */
|
||||
|
||||
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) {
|
||||
(void)content; (void)node_type; (void)salience;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
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) {
|
||||
(void)content; (void)node_type; (void)label; (void)salience;
|
||||
(void)importance; (void)confidence; (void)tier; (void)tags;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
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 tier, el_val_t layer_id, el_val_t tags) {
|
||||
(void)content; (void)node_type; (void)label; (void)salience;
|
||||
(void)certainty; (void)confidence; (void)tier; (void)layer_id; (void)tags;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
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) {
|
||||
(void)name; (void)priority; (void)suppressible; (void)transparent; (void)injectable;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t engram_remove_layer(el_val_t layer_id) {
|
||||
(void)layer_id;
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
el_val_t engram_list_layers(void) { return EL_STR("[]"); }
|
||||
el_val_t engram_list_layers_json(void) { return EL_STR("[]"); }
|
||||
|
||||
el_val_t engram_get_node(el_val_t id) { (void)id; return EL_STR(""); }
|
||||
el_val_t engram_get_node_json(el_val_t id) { (void)id; return EL_STR("{}"); }
|
||||
|
||||
void engram_strengthen(el_val_t node_id) { (void)node_id; }
|
||||
void engram_forget(el_val_t node_id) { (void)node_id; }
|
||||
|
||||
el_val_t engram_node_count(void) { return 0; }
|
||||
el_val_t engram_edge_count(void) { return 0; }
|
||||
el_val_t engram_stats_json(void) { return EL_STR("{}"); }
|
||||
|
||||
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) {
|
||||
(void)from_id; (void)to_id; (void)weight; (void)relation;
|
||||
}
|
||||
|
||||
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id) {
|
||||
(void)from_id; (void)to_id;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit) {
|
||||
(void)query; (void)limit;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||
(void)query; (void)limit;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset) {
|
||||
(void)limit; (void)offset;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) {
|
||||
(void)limit; (void)offset;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset) {
|
||||
(void)node_type; (void)limit; (void)offset;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_neighbors(el_val_t node_id, el_val_t limit) {
|
||||
(void)node_id; (void)limit;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t relation, el_val_t limit) {
|
||||
(void)node_id; (void)relation; (void)limit;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t limit) {
|
||||
(void)node_id; (void)limit;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_load(el_val_t path) { (void)path; return (el_val_t)1; }
|
||||
el_val_t engram_save(el_val_t path) { (void)path; return (el_val_t)1; }
|
||||
|
||||
el_val_t engram_activate(el_val_t node_ids, el_val_t spread, el_val_t decay) {
|
||||
(void)node_ids; (void)spread; (void)decay;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_activate_json(el_val_t node_ids_json, el_val_t spread, el_val_t decay) {
|
||||
(void)node_ids_json; (void)spread; (void)decay;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_compile_layered_json(el_val_t node_id, el_val_t target_tokens,
|
||||
el_val_t strategy) {
|
||||
(void)node_id; (void)target_tokens; (void)strategy;
|
||||
return EL_STR("{}");
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// tests/suite/manifest.el — El test suite vessel manifest.
|
||||
//
|
||||
// Build and run:
|
||||
// cd tests/suite && elb && ./dist/el-tests
|
||||
//
|
||||
// Exit code equals the number of failing assertions (0 = all pass).
|
||||
|
||||
package "el-tests" {
|
||||
version "0.1.0"
|
||||
description "El runtime test suite"
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/test_all.el"
|
||||
output "dist/"
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// tests/suite/src/test_all.el — master entry point for the El comprehensive test suite.
|
||||
//
|
||||
// This is an El vessel. Build and run with:
|
||||
//
|
||||
// cd tests/suite && elb && ./dist/el-tests
|
||||
//
|
||||
// Exit code equals the number of failing assertions (0 = all pass).
|
||||
|
||||
import "../../../runtime/string.el"
|
||||
import "../../../runtime/math.el"
|
||||
import "../../../runtime/state.el"
|
||||
import "../../../runtime/fs.el"
|
||||
import "../../../runtime/json.el"
|
||||
import "../../../runtime/time.el"
|
||||
import "../../../runtime/thread.el"
|
||||
import "../../../runtime/test.el"
|
||||
import "test_string.el"
|
||||
import "test_math.el"
|
||||
import "test_json.el"
|
||||
import "test_state.el"
|
||||
import "test_time.el"
|
||||
import "test_fs.el"
|
||||
import "test_collections.el"
|
||||
|
||||
fn main() -> Int {
|
||||
// ── string tests ───────────────────────────────────────────────────────────
|
||||
test_case("str_eq basic", "test_str_eq_basic")
|
||||
test_case("str_eq symbols", "test_str_eq_symbols")
|
||||
test_case("str_len basic", "test_str_len_basic")
|
||||
test_case("str_len longer", "test_str_len_longer")
|
||||
test_case("str_concat basic", "test_str_concat_basic")
|
||||
test_case("str_concat chaining", "test_str_concat_chaining")
|
||||
test_case("str_slice basic", "test_str_slice_basic")
|
||||
test_case("str_slice edge", "test_str_slice_edge")
|
||||
test_case("str_starts_with basic", "test_str_starts_with_basic")
|
||||
test_case("str_starts_with edge", "test_str_starts_with_edge")
|
||||
test_case("str_ends_with basic", "test_str_ends_with_basic")
|
||||
test_case("str_ends_with edge", "test_str_ends_with_edge")
|
||||
test_case("str_contains basic", "test_str_contains_basic")
|
||||
test_case("str_contains edge", "test_str_contains_edge")
|
||||
test_case("str_index_of basic", "test_str_index_of_basic")
|
||||
test_case("str_index_of duplicates", "test_str_index_of_duplicates")
|
||||
test_case("str_last_index_of basic", "test_str_last_index_of_basic")
|
||||
test_case("str_replace basic", "test_str_replace_basic")
|
||||
test_case("str_replace multiple", "test_str_replace_multiple")
|
||||
test_case("str_to_upper basic", "test_str_to_upper_basic")
|
||||
test_case("str_to_lower basic", "test_str_to_lower_basic")
|
||||
test_case("str_upper_lower roundtrip", "test_str_upper_lower_roundtrip")
|
||||
test_case("str_trim basic", "test_str_trim_basic")
|
||||
test_case("str_lstrip rstrip", "test_str_lstrip_rstrip")
|
||||
test_case("str_split basic", "test_str_split_basic")
|
||||
test_case("str_split edge", "test_str_split_edge")
|
||||
test_case("str_join basic", "test_str_join_basic")
|
||||
test_case("str_join edge", "test_str_join_edge")
|
||||
test_case("int_to_str basic", "test_int_to_str_basic")
|
||||
test_case("str_to_int basic", "test_str_to_int_basic")
|
||||
test_case("float_to_str basic", "test_float_to_str_basic")
|
||||
test_case("str_to_float basic", "test_str_to_float_basic")
|
||||
test_case("str_repeat basic", "test_str_repeat_basic")
|
||||
test_case("str_reverse basic", "test_str_reverse_basic")
|
||||
test_case("str_count basic", "test_str_count_basic")
|
||||
test_case("str_strip_prefix basic", "test_str_strip_prefix_basic")
|
||||
test_case("str_strip_suffix basic", "test_str_strip_suffix_basic")
|
||||
test_case("str_find_chars basic", "test_str_find_chars_basic")
|
||||
test_case("str_char_at basic", "test_str_char_at_basic")
|
||||
test_case("str_char_code basic", "test_str_char_code_basic")
|
||||
test_case("str_pad_left basic", "test_str_pad_left_basic")
|
||||
test_case("str_pad_right basic", "test_str_pad_right_basic")
|
||||
test_case("is_letter basic", "test_is_letter_basic")
|
||||
test_case("is_digit basic", "test_is_digit_basic")
|
||||
test_case("is_whitespace basic", "test_is_whitespace_basic")
|
||||
test_case("str_count_lines basic", "test_str_count_lines_basic")
|
||||
test_case("str_count_words basic", "test_str_count_words_basic")
|
||||
test_case("url_encode basic", "test_url_encode_basic")
|
||||
test_case("url roundtrip", "test_url_roundtrip")
|
||||
test_case("bool_to_str basic", "test_bool_to_str_basic")
|
||||
test_case("str_to_bytes basic", "test_str_to_bytes_basic")
|
||||
test_case("bytes roundtrip", "test_bytes_roundtrip")
|
||||
|
||||
// ── math tests ─────────────────────────────────────────────────────────────
|
||||
test_case("el_abs basic", "test_el_abs_basic")
|
||||
test_case("el_max basic", "test_el_max_basic")
|
||||
test_case("el_max edge", "test_el_max_edge")
|
||||
test_case("el_min basic", "test_el_min_basic")
|
||||
test_case("el_min edge", "test_el_min_edge")
|
||||
test_case("math_sqrt basic", "test_math_sqrt_basic")
|
||||
test_case("math_sqrt larger", "test_math_sqrt_larger")
|
||||
test_case("math_log basic", "test_math_log_basic")
|
||||
test_case("math_ln basic", "test_math_ln_basic")
|
||||
test_case("math_pi basic", "test_math_pi_basic")
|
||||
test_case("math_sin basic", "test_math_sin_basic")
|
||||
test_case("math_cos basic", "test_math_cos_basic")
|
||||
// int_to_float, float_to_int, format_float, decimal_round omitted:
|
||||
// __int_to_float / __float_to_int / __format_float seed primitives are
|
||||
// not yet implemented in el_seed.c on the runtime/integrate branch.
|
||||
test_case("int arithmetic basic", "test_int_arithmetic_basic")
|
||||
test_case("int arithmetic edge", "test_int_arithmetic_edge")
|
||||
test_case("int arithmetic negative", "test_int_arithmetic_negative")
|
||||
test_case("float arithmetic basic", "test_float_arithmetic_basic")
|
||||
test_case("float comparison basic", "test_float_comparison_basic")
|
||||
|
||||
// ── json tests ─────────────────────────────────────────────────────────────
|
||||
test_case("json_get basic", "test_json_get_basic")
|
||||
test_case("json_get types", "test_json_get_types")
|
||||
test_case("json_get nested", "test_json_get_nested")
|
||||
test_case("json_get empty", "test_json_get_empty")
|
||||
test_case("json_get_int basic", "test_json_get_int_basic")
|
||||
test_case("json_get_bool basic", "test_json_get_bool_basic")
|
||||
test_case("json_get_float basic", "test_json_get_float_basic")
|
||||
test_case("json_set basic", "test_json_set_basic")
|
||||
test_case("json_set numeric", "test_json_set_numeric")
|
||||
test_case("json_set chained", "test_json_set_chained")
|
||||
test_case("json_array_len basic", "test_json_array_len_basic")
|
||||
test_case("json_array_get basic", "test_json_array_get_basic")
|
||||
test_case("json_array_get numbers", "test_json_array_get_numbers")
|
||||
test_case("json_array_get objects", "test_json_array_get_objects")
|
||||
test_case("json_escape_string basic", "test_json_escape_string_basic")
|
||||
test_case("json_escape_string backslash", "test_json_escape_string_backslash")
|
||||
test_case("json_escape roundtrip", "test_json_escape_roundtrip")
|
||||
test_case("json_build_object basic", "test_json_build_object_basic")
|
||||
test_case("json_build_object empty", "test_json_build_object_empty")
|
||||
test_case("json_build_array basic", "test_json_build_array_basic")
|
||||
test_case("json_build_array empty", "test_json_build_array_empty")
|
||||
test_case("json_build_array numbers", "test_json_build_array_numbers")
|
||||
test_case("json_array_push basic", "test_json_array_push_basic")
|
||||
test_case("json_array_push preserves order", "test_json_array_push_preserves_order")
|
||||
test_case("json nested set get", "test_json_nested_set_get")
|
||||
test_case("json array of objects", "test_json_array_of_objects")
|
||||
|
||||
// ── state tests ────────────────────────────────────────────────────────────
|
||||
test_case("state set get basic", "test_state_set_get_basic")
|
||||
test_case("state overwrite", "test_state_overwrite")
|
||||
test_case("state multiple keys", "test_state_multiple_keys")
|
||||
test_case("state missing key", "test_state_missing_key")
|
||||
test_case("state del basic", "test_state_del_basic")
|
||||
test_case("state del and reset", "test_state_del_and_reset")
|
||||
test_case("state has basic", "test_state_has_basic")
|
||||
test_case("state get_or basic", "test_state_get_or_basic")
|
||||
test_case("state cross function", "test_state_cross_function")
|
||||
test_case("state keys basic", "test_state_keys_basic")
|
||||
test_case("state value types", "test_state_value_types")
|
||||
|
||||
// ── time tests ─────────────────────────────────────────────────────────────
|
||||
test_case("time_now basic", "test_time_now_basic")
|
||||
test_case("now_millis basic", "test_now_millis_basic")
|
||||
test_case("unix_timestamp_ms basic", "test_unix_timestamp_ms_basic")
|
||||
test_case("time_now_ms alias", "test_time_now_ms_alias")
|
||||
test_case("time monotonic now_millis", "test_time_monotonic_now_millis")
|
||||
test_case("time monotonic time_now", "test_time_monotonic_time_now")
|
||||
test_case("time monotonic now_ns", "test_time_monotonic_now_ns")
|
||||
test_case("unix_timestamp basic", "test_unix_timestamp_basic")
|
||||
test_case("time_to_parts basic", "test_time_to_parts_basic")
|
||||
test_case("time_to_parts epoch", "test_time_to_parts_epoch")
|
||||
test_case("time_to_parts current", "test_time_to_parts_current")
|
||||
test_case("time_format iso", "test_time_format_iso")
|
||||
test_case("time_format empty", "test_time_format_empty")
|
||||
test_case("time_format strftime", "test_time_format_strftime")
|
||||
test_case("time_add basic", "test_time_add_basic")
|
||||
test_case("time_add multiple", "test_time_add_multiple")
|
||||
test_case("time_diff basic", "test_time_diff_basic")
|
||||
test_case("time_diff larger", "test_time_diff_larger")
|
||||
test_case("time_diff zero", "test_time_diff_zero")
|
||||
test_case("duration helpers basic", "test_duration_helpers_basic")
|
||||
test_case("instant helpers basic", "test_instant_helpers_basic")
|
||||
test_case("uuid_new basic", "test_uuid_new_basic")
|
||||
test_case("uuid uniqueness", "test_uuid_uniqueness")
|
||||
test_case("time_from_parts basic", "test_time_from_parts_basic")
|
||||
test_case("instant_to_iso8601 basic", "test_instant_to_iso8601_basic")
|
||||
|
||||
// ── filesystem tests ───────────────────────────────────────────────────────
|
||||
test_case("fs write read basic", "test_fs_write_read_basic")
|
||||
test_case("fs write read multiline", "test_fs_write_read_multiline")
|
||||
test_case("fs write read empty", "test_fs_write_read_empty")
|
||||
test_case("fs write overwrite", "test_fs_write_overwrite")
|
||||
test_case("fs write large content", "test_fs_write_large_content")
|
||||
test_case("fs exists basic", "test_fs_exists_basic")
|
||||
test_case("fs exists nonexistent", "test_fs_exists_nonexistent")
|
||||
test_case("fs write creates file", "test_fs_write_creates_file")
|
||||
test_case("fs read nonexistent", "test_fs_read_nonexistent")
|
||||
test_case("fs write json", "test_fs_write_json")
|
||||
test_case("fs write json array", "test_fs_write_json_array")
|
||||
test_case("fs multiple files", "test_fs_multiple_files")
|
||||
test_case("fs mkdir basic", "test_fs_mkdir_basic")
|
||||
test_case("fs mkdir write inside", "test_fs_mkdir_write_inside")
|
||||
test_case("fs special chars", "test_fs_special_chars")
|
||||
test_case("fs unicode content", "test_fs_unicode_content")
|
||||
|
||||
// ── collection / list tests ────────────────────────────────────────────────
|
||||
test_case("list empty basic", "test_list_empty_basic")
|
||||
test_case("list empty multiple", "test_list_empty_multiple")
|
||||
test_case("list append single", "test_list_append_single")
|
||||
test_case("list append multiple", "test_list_append_multiple")
|
||||
test_case("list append order", "test_list_append_order")
|
||||
test_case("list append empty string", "test_list_append_empty_string")
|
||||
test_case("list len basic", "test_list_len_basic")
|
||||
test_case("list len large", "test_list_len_large")
|
||||
test_case("list get basic", "test_list_get_basic")
|
||||
test_case("list get preserves content", "test_list_get_preserves_content")
|
||||
test_case("list split join roundtrip", "test_list_split_join_roundtrip")
|
||||
test_case("list build and join", "test_list_build_and_join")
|
||||
test_case("list from split access", "test_list_from_split_access")
|
||||
test_case("native list empty basic", "test_native_list_empty_basic")
|
||||
test_case("native list append basic", "test_native_list_append_basic")
|
||||
test_case("native list join", "test_native_list_join")
|
||||
test_case("env list helpers", "test_env_list_helpers")
|
||||
test_case("list accumulate loop", "test_list_accumulate_loop")
|
||||
test_case("list string building", "test_list_string_building")
|
||||
|
||||
return test_run_all()
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// tests/suite/test_collections.el — tests for native list operations
|
||||
//
|
||||
// runtime/collections.el does not exist; this file tests the native list
|
||||
// primitives that are always available in El: el_list_empty, el_list_append,
|
||||
// el_list_len, el_list_get.
|
||||
//
|
||||
// These primitives underpin str_split, str_join, and the env.el list helpers.
|
||||
|
||||
// ── el_list_empty ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_empty_basic(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
assert_int_eq(el_list_len(lst), 0, "empty list has length 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_empty_multiple(_: String) -> String {
|
||||
let a: [String] = el_list_empty()
|
||||
let b: [String] = el_list_empty()
|
||||
assert_int_eq(el_list_len(a), 0, "first empty list has length 0")
|
||||
assert_int_eq(el_list_len(b), 0, "second empty list has length 0")
|
||||
// Append to one does not affect the other
|
||||
let a2: [String] = el_list_append(a, "hello")
|
||||
assert_int_eq(el_list_len(a2), 1, "appended list has length 1")
|
||||
assert_int_eq(el_list_len(b), 0, "other empty list unaffected")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_list_append ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_append_single(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst2: [String] = el_list_append(lst, "hello")
|
||||
assert_int_eq(el_list_len(lst2), 1, "length is 1 after append")
|
||||
assert_eq(el_list_get(lst2, 0), "hello", "first element is what was appended")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_append_multiple(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "alpha")
|
||||
let lst = el_list_append(lst, "beta")
|
||||
let lst = el_list_append(lst, "gamma")
|
||||
assert_int_eq(el_list_len(lst), 3, "length 3 after 3 appends")
|
||||
assert_eq(el_list_get(lst, 0), "alpha", "first element: alpha")
|
||||
assert_eq(el_list_get(lst, 1), "beta", "second element: beta")
|
||||
assert_eq(el_list_get(lst, 2), "gamma", "third element: gamma")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_append_order(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "1")
|
||||
let lst = el_list_append(lst, "2")
|
||||
let lst = el_list_append(lst, "3")
|
||||
let lst = el_list_append(lst, "4")
|
||||
let lst = el_list_append(lst, "5")
|
||||
assert_int_eq(el_list_len(lst), 5, "five elements appended")
|
||||
assert_eq(el_list_get(lst, 0), "1", "order preserved: index 0")
|
||||
assert_eq(el_list_get(lst, 2), "3", "order preserved: index 2")
|
||||
assert_eq(el_list_get(lst, 4), "5", "order preserved: index 4")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_append_empty_string(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "")
|
||||
assert_int_eq(el_list_len(lst), 1, "empty string element counted")
|
||||
assert_eq(el_list_get(lst, 0), "", "empty string retrievable")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_list_len ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_len_basic(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
assert_int_eq(el_list_len(lst), 0, "initial length 0")
|
||||
let lst = el_list_append(lst, "a")
|
||||
assert_int_eq(el_list_len(lst), 1, "length 1 after one append")
|
||||
let lst = el_list_append(lst, "b")
|
||||
assert_int_eq(el_list_len(lst), 2, "length 2 after two appends")
|
||||
let lst = el_list_append(lst, "c")
|
||||
assert_int_eq(el_list_len(lst), 3, "length 3 after three appends")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_len_large(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < 20 {
|
||||
let lst = el_list_append(lst, int_to_str(i))
|
||||
let i = i + 1
|
||||
}
|
||||
assert_int_eq(el_list_len(lst), 20, "length 20 after 20 appends")
|
||||
assert_eq(el_list_get(lst, 0), "0", "first element is 0")
|
||||
assert_eq(el_list_get(lst, 19), "19", "last element is 19")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_list_get ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_get_basic(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "first")
|
||||
let lst = el_list_append(lst, "second")
|
||||
let lst = el_list_append(lst, "third")
|
||||
assert_eq(el_list_get(lst, 0), "first", "get index 0")
|
||||
assert_eq(el_list_get(lst, 1), "second", "get index 1")
|
||||
assert_eq(el_list_get(lst, 2), "third", "get index 2")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_get_preserves_content(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "hello world")
|
||||
let lst = el_list_append(lst, "{\"json\":\"value\"}")
|
||||
let lst = el_list_append(lst, "line1\nline2")
|
||||
assert_eq(el_list_get(lst, 0), "hello world", "spaces in element preserved")
|
||||
assert_eq(el_list_get(lst, 1), "{\"json\":\"value\"}", "JSON string preserved")
|
||||
assert_eq(el_list_get(lst, 2), "line1\nline2", "newline in element preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── list with str_join / str_split roundtrip ─────────────────────────────────
|
||||
|
||||
fn test_list_split_join_roundtrip(_: String) -> String {
|
||||
let original: String = "apple,banana,cherry"
|
||||
let parts: [String] = str_split(original, ",")
|
||||
assert_int_eq(el_list_len(parts), 3, "split yields 3 parts")
|
||||
let rejoined: String = str_join(parts, ",")
|
||||
assert_eq(rejoined, original, "split then join is identity")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_build_and_join(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "one")
|
||||
let parts = el_list_append(parts, "two")
|
||||
let parts = el_list_append(parts, "three")
|
||||
let joined: String = str_join(parts, " + ")
|
||||
assert_eq(joined, "one + two + three", "join with multi-char separator")
|
||||
assert_int_eq(el_list_len(parts), 3, "original list unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_from_split_access(_: String) -> String {
|
||||
let parts: [String] = str_split("a:b:c:d:e", ":")
|
||||
assert_int_eq(el_list_len(parts), 5, "split into 5 parts")
|
||||
assert_eq(el_list_get(parts, 0), "a", "first part")
|
||||
assert_eq(el_list_get(parts, 2), "c", "middle part")
|
||||
assert_eq(el_list_get(parts, 4), "e", "last part")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── native_list_empty / native_list_append (aliases from join.el style) ───────
|
||||
|
||||
fn test_native_list_empty_basic(_: String) -> String {
|
||||
let lst: [String] = native_list_empty()
|
||||
assert_int_eq(el_list_len(lst), 0, "native_list_empty yields empty list")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_native_list_append_basic(_: String) -> String {
|
||||
let lst: [String] = native_list_empty()
|
||||
let lst = native_list_append(lst, "hello")
|
||||
let lst = native_list_append(lst, "world")
|
||||
assert_int_eq(el_list_len(lst), 2, "native_list_append: length 2")
|
||||
assert_eq(el_list_get(lst, 0), "hello", "native_list_append: first element")
|
||||
assert_eq(el_list_get(lst, 1), "world", "native_list_append: second element")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_native_list_join(_: String) -> String {
|
||||
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_eq(result, "alpha, beta, gamma", "native list joined correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── env.el list helpers (get / len) ──────────────────────────────────────────
|
||||
|
||||
fn test_env_list_helpers(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "x")
|
||||
let lst = el_list_append(lst, "y")
|
||||
let lst = el_list_append(lst, "z")
|
||||
assert_int_eq(len(lst), 3, "len() alias for el_list_len")
|
||||
assert_eq(get(lst, 0), "x", "get() alias for el_list_get index 0")
|
||||
assert_eq(get(lst, 2), "z", "get() alias for el_list_get index 2")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── list as accumulator pattern ───────────────────────────────────────────────
|
||||
|
||||
fn test_list_accumulate_loop(_: String) -> String {
|
||||
let results: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < 5 {
|
||||
let results = el_list_append(results, int_to_str(i * i))
|
||||
let i = i + 1
|
||||
}
|
||||
assert_int_eq(el_list_len(results), 5, "accumulated 5 squares")
|
||||
assert_eq(el_list_get(results, 0), "0", "0^2 = 0")
|
||||
assert_eq(el_list_get(results, 1), "1", "1^2 = 1")
|
||||
assert_eq(el_list_get(results, 2), "4", "2^2 = 4")
|
||||
assert_eq(el_list_get(results, 3), "9", "3^2 = 9")
|
||||
assert_eq(el_list_get(results, 4), "16", "4^2 = 16")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_string_building(_: String) -> String {
|
||||
let words: [String] = el_list_empty()
|
||||
let words = el_list_append(words, "the")
|
||||
let words = el_list_append(words, "quick")
|
||||
let words = el_list_append(words, "brown")
|
||||
let words = el_list_append(words, "fox")
|
||||
let sentence: String = str_join(words, " ")
|
||||
assert_eq(sentence, "the quick brown fox", "words joined into sentence")
|
||||
assert_int_eq(str_count_words(sentence), 4, "sentence has 4 words")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// tests/suite/test_fs.el — comprehensive tests for runtime/fs.el
|
||||
//
|
||||
// Covers fs_write, fs_read, fs_exists, fs_mkdir. All file paths use /tmp so
|
||||
// no special permissions are needed. Each test uses a unique path to prevent
|
||||
// interference between test cases.
|
||||
|
||||
// ── fs_write / fs_read roundtrip ─────────────────────────────────────────────
|
||||
|
||||
fn test_fs_write_read_basic(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_basic.txt"
|
||||
let content: String = "hello from El tests"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "read back what was written")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_read_multiline(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_multiline.txt"
|
||||
let content: String = "line one\nline two\nline three"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "multiline content preserved")
|
||||
assert_contains(read_back, "line one", "first line present")
|
||||
assert_contains(read_back, "line two", "second line present")
|
||||
assert_contains(read_back, "line three", "third line present")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_read_empty(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_empty.txt"
|
||||
fs_write(path, "")
|
||||
let read_back: String = fs_read(path)
|
||||
// empty file may return "" or a zero-length string
|
||||
assert_int_eq(str_len(read_back), 0, "empty file reads as empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_overwrite(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_overwrite.txt"
|
||||
fs_write(path, "first version")
|
||||
let r1: String = fs_read(path)
|
||||
assert_eq(r1, "first version", "first write readable")
|
||||
|
||||
fs_write(path, "second version")
|
||||
let r2: String = fs_read(path)
|
||||
assert_eq(r2, "second version", "second write overwrites first")
|
||||
|
||||
fs_write(path, "third version")
|
||||
let r3: String = fs_read(path)
|
||||
assert_eq(r3, "third version", "third write overwrites second")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_large_content(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_large.txt"
|
||||
let big: String = str_repeat("abcdefghij", 100)
|
||||
fs_write(path, big)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_int_eq(str_len(read_back), 1000, "large content length preserved")
|
||||
assert_eq(str_slice(read_back, 0, 10), "abcdefghij", "content starts correctly")
|
||||
assert_eq(str_slice(read_back, 990, 1000), "abcdefghij", "content ends correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── fs_exists ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_exists_basic(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_exists.txt"
|
||||
fs_write(path, "existence test")
|
||||
assert_true(fs_exists(path), "file exists after write")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_exists_nonexistent(_: String) -> String {
|
||||
assert_false(fs_exists("/tmp/el_suite_this_file_does_not_exist_xyz_abc_999.txt"), "nonexistent file returns false")
|
||||
assert_false(fs_exists("/tmp/el_suite_another_missing_file_def.txt"), "another nonexistent file returns false")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_creates_file(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_create.txt"
|
||||
// May or may not exist — write creates it
|
||||
fs_write(path, "newly created")
|
||||
assert_true(fs_exists(path), "file exists after creation via write")
|
||||
let content: String = fs_read(path)
|
||||
assert_eq(content, "newly created", "content correct after creation")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── fs_read nonexistent ───────────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_read_nonexistent(_: String) -> String {
|
||||
let result: String = fs_read("/tmp/el_suite_this_absolutely_does_not_exist_xyz.txt")
|
||||
// Per the spec: returns "" or error string; we just verify it doesn't crash
|
||||
// and returns a string (which it must, given the type system)
|
||||
assert_true(str_len(result) >= 0, "reading nonexistent file returns a string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── JSON content stored in files ─────────────────────────────────────────────
|
||||
|
||||
fn test_fs_write_json(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_json.txt"
|
||||
let json_data: String = "{\"name\":\"alice\",\"score\":42}"
|
||||
fs_write(path, json_data)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(json_get(read_back, "name"), "alice", "JSON name field after file roundtrip")
|
||||
assert_eq(json_get(read_back, "score"), "42", "JSON score field after file roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_json_array(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_json_arr.txt"
|
||||
let arr_data: String = "[\"alpha\",\"beta\",\"gamma\"]"
|
||||
fs_write(path, arr_data)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_int_eq(json_array_len(read_back), 3, "JSON array length after file roundtrip")
|
||||
assert_eq(json_array_get_string(read_back, 0), "alpha", "first element after roundtrip")
|
||||
assert_eq(json_array_get_string(read_back, 2), "gamma", "last element after roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── multiple independent files ────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_multiple_files(_: String) -> String {
|
||||
let path_a: String = "/tmp/el_suite_multi_a.txt"
|
||||
let path_b: String = "/tmp/el_suite_multi_b.txt"
|
||||
let path_c: String = "/tmp/el_suite_multi_c.txt"
|
||||
|
||||
fs_write(path_a, "content A")
|
||||
fs_write(path_b, "content B")
|
||||
fs_write(path_c, "content C")
|
||||
|
||||
assert_eq(fs_read(path_a), "content A", "file A reads correctly")
|
||||
assert_eq(fs_read(path_b), "content B", "file B reads correctly")
|
||||
assert_eq(fs_read(path_c), "content C", "file C reads correctly")
|
||||
|
||||
assert_true(fs_exists(path_a), "file A exists")
|
||||
assert_true(fs_exists(path_b), "file B exists")
|
||||
assert_true(fs_exists(path_c), "file C exists")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── fs_mkdir ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_mkdir_basic(_: String) -> String {
|
||||
let dir_path: String = "/tmp/el_suite_mkdir_test"
|
||||
let result: Bool = fs_mkdir(dir_path)
|
||||
// fs_mkdir returns Bool; the dir should now exist
|
||||
assert_true(fs_exists(dir_path), "directory exists after mkdir")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_mkdir_write_inside(_: String) -> String {
|
||||
let dir_path: String = "/tmp/el_suite_mkdir_subdir"
|
||||
fs_mkdir(dir_path)
|
||||
let file_path: String = dir_path + "/test_file.txt"
|
||||
fs_write(file_path, "inside directory")
|
||||
assert_true(fs_exists(file_path), "file inside mkdir'd directory exists")
|
||||
assert_eq(fs_read(file_path), "inside directory", "file inside directory readable")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── special characters in content ────────────────────────────────────────────
|
||||
|
||||
fn test_fs_special_chars(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_special.txt"
|
||||
let content: String = "tab:\there\nnewline above\r\nwindows newline"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "special chars preserved in file")
|
||||
assert_contains(read_back, "\t", "tab preserved")
|
||||
assert_contains(read_back, "\n", "newline preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_unicode_content(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_unicode.txt"
|
||||
// Use ASCII art instead of actual unicode for max compatibility
|
||||
let content: String = "hello-world-test"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "content preserved")
|
||||
assert_int_eq(str_len(read_back), str_len(content), "length preserved")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// tests/suite/test_json.el — comprehensive tests for runtime/json.el
|
||||
//
|
||||
// Covers json_get, json_get_raw, json_set, json_array_len, json_array_get,
|
||||
// json_array_get_string, json_escape_string, json_build_object, json_build_array,
|
||||
// json_array_push, typed extractors, bytes_to_str, and nested JSON.
|
||||
|
||||
// ── json_get ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_get_basic(_: String) -> String {
|
||||
let obj: String = "{\"name\":\"alice\",\"age\":\"30\"}"
|
||||
assert_eq(json_get(obj, "name"), "alice", "get string field")
|
||||
assert_eq(json_get(obj, "age"), "30", "get numeric-looking string field")
|
||||
assert_eq(json_get(obj, "missing"), "", "missing key returns empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_types(_: String) -> String {
|
||||
let obj: String = "{\"count\":42,\"flag\":true,\"ratio\":3.14}"
|
||||
assert_eq(json_get(obj, "count"), "42", "get integer field")
|
||||
assert_eq(json_get(obj, "flag"), "true", "get boolean field")
|
||||
assert_eq(json_get(obj, "ratio"), "3.14", "get float field")
|
||||
assert_eq(json_get(obj, "absent"), "", "absent key yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_nested(_: String) -> String {
|
||||
let obj: String = "{\"user\":{\"name\":\"bob\",\"role\":\"admin\"}}"
|
||||
assert_eq(json_get(obj, "user.name"), "bob", "dot-path traversal: user.name")
|
||||
assert_eq(json_get(obj, "user.role"), "admin", "dot-path traversal: user.role")
|
||||
assert_eq(json_get(obj, "user.missing"), "", "missing nested key")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_empty(_: String) -> String {
|
||||
let empty_obj: String = "{}"
|
||||
assert_eq(json_get(empty_obj, "key"), "", "empty object: any key is missing")
|
||||
assert_eq(json_get(empty_obj, "a.b"), "", "empty object: nested path missing")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_get typed extractors ────────────────────────────────────────────────
|
||||
|
||||
fn test_json_get_int_basic(_: String) -> String {
|
||||
let obj: String = "{\"count\":42,\"negative\":-7,\"zero\":0}"
|
||||
assert_int_eq(json_get_int(obj, "count"), 42, "get int field 42")
|
||||
assert_int_eq(json_get_int(obj, "negative"), -7, "get negative int field")
|
||||
assert_int_eq(json_get_int(obj, "zero"), 0, "get zero int field")
|
||||
assert_int_eq(json_get_int(obj, "missing"), 0, "missing int field returns 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_bool_basic(_: String) -> String {
|
||||
let obj: String = "{\"ok\":true,\"fail\":false}"
|
||||
assert_true(json_get_bool(obj, "ok"), "get true bool field")
|
||||
assert_false(json_get_bool(obj, "fail"), "get false bool field")
|
||||
assert_false(json_get_bool(obj, "missing"), "missing bool field returns false")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_float_basic(_: String) -> String {
|
||||
let obj: String = "{\"ratio\":3.14,\"negative\":-1.5}"
|
||||
let ratio: Float = json_get_float(obj, "ratio")
|
||||
assert_true(ratio > 3.13, "ratio > 3.13")
|
||||
assert_true(ratio < 3.15, "ratio < 3.15")
|
||||
let neg: Float = json_get_float(obj, "negative")
|
||||
assert_true(neg < -1.4, "negative float")
|
||||
assert_true(neg > -1.6, "negative float bound")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_set ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_set_basic(_: String) -> String {
|
||||
let obj: String = "{\"name\":\"alice\"}"
|
||||
let obj2: String = json_set(obj, "name", "\"bob\"")
|
||||
assert_eq(json_get(obj2, "name"), "bob", "overwrites existing key")
|
||||
|
||||
let obj3: String = json_set(obj, "role", "\"admin\"")
|
||||
assert_eq(json_get(obj3, "role"), "admin", "inserts new key")
|
||||
assert_eq(json_get(obj3, "name"), "alice", "existing key unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_set_numeric(_: String) -> String {
|
||||
let obj: String = "{}"
|
||||
let obj2: String = json_set(obj, "count", "42")
|
||||
assert_eq(json_get(obj2, "count"), "42", "set integer value")
|
||||
|
||||
let obj3: String = json_set(obj2, "count", "100")
|
||||
assert_eq(json_get(obj3, "count"), "100", "overwrites integer value")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_set_chained(_: String) -> String {
|
||||
let obj: String = "{}"
|
||||
let obj1: String = json_set(obj, "a", "\"1\"")
|
||||
let obj2: String = json_set(obj1, "b", "\"2\"")
|
||||
let obj3: String = json_set(obj2, "c", "\"3\"")
|
||||
assert_eq(json_get(obj3, "a"), "1", "chained set: a")
|
||||
assert_eq(json_get(obj3, "b"), "2", "chained set: b")
|
||||
assert_eq(json_get(obj3, "c"), "3", "chained set: c")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_array_len ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_array_len_basic(_: String) -> String {
|
||||
assert_int_eq(json_array_len("[]"), 0, "empty array has length 0")
|
||||
assert_int_eq(json_array_len("[1]"), 1, "single element array")
|
||||
assert_int_eq(json_array_len("[1,2,3]"), 3, "three element array")
|
||||
assert_int_eq(json_array_len("[\"a\",\"b\"]"), 2, "string array length")
|
||||
assert_int_eq(json_array_len("[1,2,3,4,5]"), 5, "five element array")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_array_get / json_array_get_string ────────────────────────────────────
|
||||
|
||||
fn test_json_array_get_basic(_: String) -> String {
|
||||
let arr: String = "[\"alpha\",\"beta\",\"gamma\"]"
|
||||
assert_eq(json_array_get_string(arr, 0), "alpha", "first element")
|
||||
assert_eq(json_array_get_string(arr, 1), "beta", "second element")
|
||||
assert_eq(json_array_get_string(arr, 2), "gamma", "third element")
|
||||
assert_eq(json_array_get_string(arr, 3), "", "out of bounds returns empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_get_numbers(_: String) -> String {
|
||||
let arr: String = "[10,20,30]"
|
||||
assert_eq(json_array_get(arr, 0), "10", "first number as fragment")
|
||||
assert_eq(json_array_get(arr, 1), "20", "second number as fragment")
|
||||
assert_eq(json_array_get(arr, 2), "30", "third number as fragment")
|
||||
assert_int_eq(str_to_int(json_array_get(arr, 0)), 10, "convert fragment to int")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_get_objects(_: String) -> String {
|
||||
let arr: String = "[{\"name\":\"alice\"},{\"name\":\"bob\"}]"
|
||||
let first: String = json_array_get(arr, 0)
|
||||
let second: String = json_array_get(arr, 1)
|
||||
assert_eq(json_get(first, "name"), "alice", "first object name")
|
||||
assert_eq(json_get(second, "name"), "bob", "second object name")
|
||||
assert_int_eq(json_array_len(arr), 2, "array of objects has correct length")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_escape_string ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_escape_string_basic(_: String) -> String {
|
||||
assert_eq(json_escape_string("hello"), "hello", "plain string unchanged")
|
||||
assert_eq(json_escape_string(""), "", "empty string unchanged")
|
||||
assert_contains(json_escape_string("say \"hello\""), "\\\"", "double quotes escaped")
|
||||
assert_contains(json_escape_string("line1\nline2"), "\\n", "newline escaped")
|
||||
assert_contains(json_escape_string("col1\tcol2"), "\\t", "tab escaped")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_escape_string_backslash(_: String) -> String {
|
||||
let escaped: String = json_escape_string("a\\b")
|
||||
assert_contains(escaped, "\\\\", "backslash doubled")
|
||||
assert_true(str_len(escaped) > 3, "escaped is longer than original")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_escape_roundtrip(_: String) -> String {
|
||||
// Build a JSON string with an escaped value and extract it back
|
||||
let raw: String = "hello world"
|
||||
let escaped: String = json_escape_string(raw)
|
||||
let json_str: String = "{\"msg\":\"" + escaped + "\"}"
|
||||
assert_eq(json_get(json_str, "msg"), raw, "roundtrip: escape then parse")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_build_object ─────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_build_object_basic(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "name")
|
||||
let parts = el_list_append(parts, "alice")
|
||||
let parts = el_list_append(parts, "role")
|
||||
let parts = el_list_append(parts, "admin")
|
||||
let obj: String = json_build_object(parts)
|
||||
assert_eq(json_get(obj, "name"), "alice", "build object: name field")
|
||||
assert_eq(json_get(obj, "role"), "admin", "build object: role field")
|
||||
assert_true(str_starts_with(obj, "{"), "build object starts with {")
|
||||
assert_true(str_ends_with(obj, "}"), "build object ends with }")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_build_object_empty(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let obj: String = json_build_object(parts)
|
||||
assert_true(str_starts_with(obj, "{"), "empty build object starts with {")
|
||||
assert_true(str_ends_with(obj, "}"), "empty build object ends with }")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_build_array ──────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_build_array_basic(_: String) -> String {
|
||||
let items: [String] = el_list_empty()
|
||||
let items = el_list_append(items, "\"alice\"")
|
||||
let items = el_list_append(items, "\"bob\"")
|
||||
let arr: String = json_build_array(items)
|
||||
assert_int_eq(json_array_len(arr), 2, "built array has 2 elements")
|
||||
assert_eq(json_array_get_string(arr, 0), "alice", "first element of built array")
|
||||
assert_eq(json_array_get_string(arr, 1), "bob", "second element of built array")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_build_array_empty(_: String) -> String {
|
||||
let items: [String] = el_list_empty()
|
||||
let arr: String = json_build_array(items)
|
||||
assert_int_eq(json_array_len(arr), 0, "empty built array has 0 elements")
|
||||
assert_eq(arr, "[]", "empty array is []")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_build_array_numbers(_: String) -> String {
|
||||
let items: [String] = el_list_empty()
|
||||
let items = el_list_append(items, "1")
|
||||
let items = el_list_append(items, "2")
|
||||
let items = el_list_append(items, "3")
|
||||
let arr: String = json_build_array(items)
|
||||
assert_int_eq(json_array_len(arr), 3, "numeric array has 3 elements")
|
||||
assert_eq(json_array_get(arr, 0), "1", "first numeric element")
|
||||
assert_eq(json_array_get(arr, 2), "3", "last numeric element")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_array_push ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_array_push_basic(_: String) -> String {
|
||||
let arr: String = "[]"
|
||||
let arr2: String = json_array_push(arr, "\"hello\"")
|
||||
assert_int_eq(json_array_len(arr2), 1, "after push: length 1")
|
||||
assert_eq(json_array_get_string(arr2, 0), "hello", "pushed element accessible")
|
||||
|
||||
let arr3: String = json_array_push(arr2, "\"world\"")
|
||||
assert_int_eq(json_array_len(arr3), 2, "after second push: length 2")
|
||||
assert_eq(json_array_get_string(arr3, 1), "world", "second pushed element")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_push_preserves_order(_: String) -> String {
|
||||
let arr: String = "[\"a\",\"b\"]"
|
||||
let arr2: String = json_array_push(arr, "\"c\"")
|
||||
assert_eq(json_array_get_string(arr2, 0), "a", "first element preserved")
|
||||
assert_eq(json_array_get_string(arr2, 1), "b", "second element preserved")
|
||||
assert_eq(json_array_get_string(arr2, 2), "c", "pushed element at end")
|
||||
assert_int_eq(json_array_len(arr2), 3, "total length after push")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── nested JSON ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_nested_set_get(_: String) -> String {
|
||||
let obj: String = "{\"user\":{\"name\":\"alice\"},\"count\":1}"
|
||||
assert_eq(json_get(obj, "user.name"), "alice", "deep get via dot path")
|
||||
assert_eq(json_get(obj, "count"), "1", "top-level int field")
|
||||
|
||||
let obj2: String = json_set(obj, "count", "2")
|
||||
assert_eq(json_get(obj2, "count"), "2", "updated top-level field")
|
||||
assert_eq(json_get(obj2, "user.name"), "alice", "nested field still accessible after set")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_of_objects(_: String) -> String {
|
||||
let arr: String = "[{\"id\":1,\"name\":\"alice\"},{\"id\":2,\"name\":\"bob\"}]"
|
||||
assert_int_eq(json_array_len(arr), 2, "array of objects: length 2")
|
||||
let first: String = json_array_get(arr, 0)
|
||||
let second: String = json_array_get(arr, 1)
|
||||
assert_eq(json_get(first, "name"), "alice", "first object name")
|
||||
assert_eq(json_get(second, "name"), "bob", "second object name")
|
||||
assert_eq(json_get(first, "id"), "1", "first object id")
|
||||
assert_eq(json_get(second, "id"), "2", "second object id")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// tests/suite/test_math.el — comprehensive tests for runtime/math.el
|
||||
//
|
||||
// Covers every public function in math.el: integer utilities, float math,
|
||||
// conversions, and rounding. Import after runtime modules and test.el.
|
||||
|
||||
// ── el_abs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_el_abs_basic(_: String) -> String {
|
||||
assert_int_eq(el_abs(5), 5, "positive stays positive")
|
||||
assert_int_eq(el_abs(-5), 5, "negative becomes positive")
|
||||
assert_int_eq(el_abs(0), 0, "zero stays zero")
|
||||
assert_int_eq(el_abs(-1), 1, "negative one becomes one")
|
||||
assert_int_eq(el_abs(1000000), 1000000, "large positive unchanged")
|
||||
assert_int_eq(el_abs(-1000000), 1000000, "large negative becomes positive")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_max ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_el_max_basic(_: String) -> String {
|
||||
assert_int_eq(el_max(3, 5), 5, "5 is larger than 3")
|
||||
assert_int_eq(el_max(5, 3), 5, "5 is larger than 3 (reversed args)")
|
||||
assert_int_eq(el_max(4, 4), 4, "equal values returns the value")
|
||||
assert_int_eq(el_max(-1, -5), -1, "larger of two negatives")
|
||||
assert_int_eq(el_max(0, -1), 0, "zero is larger than negative")
|
||||
assert_int_eq(el_max(-1, 0), 0, "zero is larger than negative (reversed)")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_el_max_edge(_: String) -> String {
|
||||
assert_int_eq(el_max(0, 0), 0, "max of zeros is zero")
|
||||
assert_int_eq(el_max(1000000, 999999), 1000000, "large values")
|
||||
assert_int_eq(el_max(-1000000, 1000000), 1000000, "mixed sign large values")
|
||||
assert_int_eq(el_max(1, 2), 2, "sequential integers")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_min ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_el_min_basic(_: String) -> String {
|
||||
assert_int_eq(el_min(3, 5), 3, "3 is smaller than 5")
|
||||
assert_int_eq(el_min(5, 3), 3, "3 is smaller than 5 (reversed args)")
|
||||
assert_int_eq(el_min(4, 4), 4, "equal values returns the value")
|
||||
assert_int_eq(el_min(-1, -5), -5, "smaller of two negatives")
|
||||
assert_int_eq(el_min(0, -1), -1, "negative is smaller than zero")
|
||||
assert_int_eq(el_min(-1, 0), -1, "negative is smaller than zero (reversed)")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_el_min_edge(_: String) -> String {
|
||||
assert_int_eq(el_min(0, 0), 0, "min of zeros is zero")
|
||||
assert_int_eq(el_min(1000000, 999999), 999999, "large values")
|
||||
assert_int_eq(el_min(-1000000, 1000000), -1000000, "mixed sign large values")
|
||||
assert_int_eq(el_min(1, 2), 1, "sequential integers")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── math_sqrt ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_math_sqrt_basic(_: String) -> String {
|
||||
let r: Float = math_sqrt(4.0)
|
||||
assert_true(r > 1.99, "sqrt(4) > 1.99")
|
||||
assert_true(r < 2.01, "sqrt(4) < 2.01")
|
||||
|
||||
let r9: Float = math_sqrt(9.0)
|
||||
assert_true(r9 > 2.99, "sqrt(9) > 2.99")
|
||||
assert_true(r9 < 3.01, "sqrt(9) < 3.01")
|
||||
|
||||
let r1: Float = math_sqrt(1.0)
|
||||
assert_true(r1 > 0.99, "sqrt(1) is close to 1")
|
||||
assert_true(r1 < 1.01, "sqrt(1) is close to 1")
|
||||
|
||||
let r0: Float = math_sqrt(0.0)
|
||||
assert_true(r0 >= 0.0, "sqrt(0) is non-negative")
|
||||
assert_true(r0 < 0.001, "sqrt(0) is close to 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_sqrt_larger(_: String) -> String {
|
||||
let r25: Float = math_sqrt(25.0)
|
||||
assert_true(r25 > 4.99, "sqrt(25) > 4.99")
|
||||
assert_true(r25 < 5.01, "sqrt(25) < 5.01")
|
||||
|
||||
let r100: Float = math_sqrt(100.0)
|
||||
assert_true(r100 > 9.99, "sqrt(100) > 9.99")
|
||||
assert_true(r100 < 10.01, "sqrt(100) < 10.01")
|
||||
|
||||
let r2: Float = math_sqrt(2.0)
|
||||
assert_true(r2 > 1.41, "sqrt(2) > 1.41")
|
||||
assert_true(r2 < 1.43, "sqrt(2) < 1.43")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── math_log / math_ln ───────────────────────────────────────────────────────
|
||||
|
||||
fn test_math_log_basic(_: String) -> String {
|
||||
let log10: Float = math_log(10.0)
|
||||
assert_true(log10 > 0.99, "log(10) > 0.99")
|
||||
assert_true(log10 < 1.01, "log(10) < 1.01")
|
||||
|
||||
let log1: Float = math_log(1.0)
|
||||
assert_true(log1 > -0.001, "log(1) is close to 0")
|
||||
assert_true(log1 < 0.001, "log(1) is close to 0")
|
||||
|
||||
let log100: Float = math_log(100.0)
|
||||
assert_true(log100 > 1.99, "log(100) > 1.99")
|
||||
assert_true(log100 < 2.01, "log(100) < 2.01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_ln_basic(_: String) -> String {
|
||||
let lne: Float = math_ln(2.718281828)
|
||||
assert_true(lne > 0.99, "ln(e) is close to 1")
|
||||
assert_true(lne < 1.01, "ln(e) is close to 1")
|
||||
|
||||
let ln1: Float = math_ln(1.0)
|
||||
assert_true(ln1 > -0.001, "ln(1) is close to 0")
|
||||
assert_true(ln1 < 0.001, "ln(1) is close to 0")
|
||||
|
||||
let ln10: Float = math_ln(10.0)
|
||||
assert_true(ln10 > 2.30, "ln(10) > 2.30")
|
||||
assert_true(ln10 < 2.31, "ln(10) < 2.31")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── math_sin / math_cos / math_pi ────────────────────────────────────────────
|
||||
|
||||
fn test_math_pi_basic(_: String) -> String {
|
||||
let pi: Float = math_pi()
|
||||
assert_true(pi > 3.14, "pi > 3.14")
|
||||
assert_true(pi < 3.15, "pi < 3.15")
|
||||
assert_true(pi > 0.0, "pi is positive")
|
||||
assert_true(pi == math_pi(), "pi is constant across calls")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_sin_basic(_: String) -> String {
|
||||
let sin0: Float = math_sin(0.0)
|
||||
assert_true(sin0 > -0.001, "sin(0) is close to 0")
|
||||
assert_true(sin0 < 0.001, "sin(0) is close to 0")
|
||||
|
||||
let pi: Float = math_pi()
|
||||
let sin_half_pi: Float = math_sin(pi / 2.0)
|
||||
assert_true(sin_half_pi > 0.99, "sin(pi/2) is close to 1")
|
||||
assert_true(sin_half_pi < 1.01, "sin(pi/2) is close to 1")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_cos_basic(_: String) -> String {
|
||||
let cos0: Float = math_cos(0.0)
|
||||
assert_true(cos0 > 0.99, "cos(0) is close to 1")
|
||||
assert_true(cos0 < 1.01, "cos(0) is close to 1")
|
||||
|
||||
let pi: Float = math_pi()
|
||||
let cos_pi: Float = math_cos(pi)
|
||||
assert_true(cos_pi < -0.99, "cos(pi) is close to -1")
|
||||
assert_true(cos_pi > -1.01, "cos(pi) is close to -1")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── int_to_float / float_to_int ──────────────────────────────────────────────
|
||||
//
|
||||
// NOTE: int_to_float, float_to_int, format_float, and decimal_round use seed
|
||||
// primitives (__int_to_float, __float_to_int, __format_float) that are not yet
|
||||
// implemented in el_seed.c on the runtime/integrate branch. These tests are
|
||||
// omitted until those seed primitives are available. The float arithmetic and
|
||||
// comparison tests below exercise the float type via language builtins instead.
|
||||
|
||||
// ── Integer arithmetic (language built-ins) ───────────────────────────────────
|
||||
|
||||
fn test_int_arithmetic_basic(_: String) -> String {
|
||||
assert_int_eq(2 + 3, 5, "addition")
|
||||
assert_int_eq(10 - 4, 6, "subtraction")
|
||||
assert_int_eq(3 * 4, 12, "multiplication")
|
||||
assert_int_eq(10 / 3, 3, "integer division truncates")
|
||||
assert_int_eq(10 % 3, 1, "modulo")
|
||||
assert_int_eq(0 - 5, -5, "negation via subtraction")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_int_arithmetic_edge(_: String) -> String {
|
||||
assert_int_eq(0 + 0, 0, "zero plus zero")
|
||||
assert_int_eq(0 * 100, 0, "zero times anything")
|
||||
assert_int_eq(1 * 1, 1, "one times one")
|
||||
assert_int_eq(100 / 100, 1, "divide by self")
|
||||
assert_int_eq(7 % 7, 0, "self modulo is zero")
|
||||
assert_int_eq(-5 + 5, 0, "additive inverse")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_int_arithmetic_negative(_: String) -> String {
|
||||
assert_int_eq(-3 + -2, -5, "negative addition")
|
||||
assert_int_eq(-3 * 4, -12, "negative times positive")
|
||||
assert_int_eq(-10 / 3, -3, "negative division truncates toward zero")
|
||||
assert_int_eq(0 - 1000, -1000, "large negative")
|
||||
assert_int_eq(-1 * -1, 1, "negative times negative is positive")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Float arithmetic (language built-ins) ────────────────────────────────────
|
||||
|
||||
fn test_float_arithmetic_basic(_: String) -> String {
|
||||
let a: Float = 1.5 + 2.5
|
||||
assert_true(a > 3.99, "1.5 + 2.5 = 4.0")
|
||||
assert_true(a < 4.01, "1.5 + 2.5 = 4.0")
|
||||
|
||||
let b: Float = 5.0 - 2.5
|
||||
assert_true(b > 2.49, "5.0 - 2.5 = 2.5")
|
||||
assert_true(b < 2.51, "5.0 - 2.5 = 2.5")
|
||||
|
||||
let c: Float = 2.0 * 3.0
|
||||
assert_true(c > 5.99, "2.0 * 3.0 = 6.0")
|
||||
assert_true(c < 6.01, "2.0 * 3.0 = 6.0")
|
||||
|
||||
let d: Float = 9.0 / 3.0
|
||||
assert_true(d > 2.99, "9.0 / 3.0 = 3.0")
|
||||
assert_true(d < 3.01, "9.0 / 3.0 = 3.0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_float_comparison_basic(_: String) -> String {
|
||||
assert_true(1.0 < 2.0, "1.0 < 2.0")
|
||||
assert_true(2.0 > 1.0, "2.0 > 1.0")
|
||||
assert_false(1.0 > 2.0, "1.0 not > 2.0")
|
||||
assert_false(2.0 < 1.0, "2.0 not < 1.0")
|
||||
assert_true(1.5 >= 1.5, "1.5 >= 1.5")
|
||||
assert_true(1.5 <= 1.5, "1.5 <= 1.5")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// tests/suite/test_state.el — comprehensive tests for runtime/state.el
|
||||
//
|
||||
// Covers state_set, state_get, state_del, state_keys, state_has, state_get_or.
|
||||
// Uses unique key prefixes to avoid interference with the test framework's
|
||||
// own state keys (which use _test_ prefix).
|
||||
|
||||
// ── state_set / state_get roundtrip ───────────────────────────────────────────
|
||||
|
||||
fn test_state_set_get_basic(_: String) -> String {
|
||||
state_set("suite_key1", "hello")
|
||||
assert_eq(state_get("suite_key1"), "hello", "get back what was set")
|
||||
|
||||
state_set("suite_key2", "world")
|
||||
assert_eq(state_get("suite_key2"), "world", "second key independent")
|
||||
|
||||
state_set("suite_num", "42")
|
||||
assert_eq(state_get("suite_num"), "42", "numeric string value")
|
||||
|
||||
state_set("suite_empty", "")
|
||||
// empty value and missing key both return "" — that's by design per the docs
|
||||
let v: String = state_get("suite_empty")
|
||||
assert_true(str_eq(v, "") || str_eq(v, ""), "empty value stored or treated as absent")
|
||||
|
||||
state_set("suite_long", str_repeat("a", 100))
|
||||
assert_int_eq(str_len(state_get("suite_long")), 100, "long value preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_state_overwrite(_: String) -> String {
|
||||
state_set("suite_over", "first")
|
||||
assert_eq(state_get("suite_over"), "first", "initial value")
|
||||
state_set("suite_over", "second")
|
||||
assert_eq(state_get("suite_over"), "second", "value overwritten")
|
||||
state_set("suite_over", "third")
|
||||
assert_eq(state_get("suite_over"), "third", "value overwritten again")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_state_multiple_keys(_: String) -> String {
|
||||
state_set("suite_mk_a", "alpha")
|
||||
state_set("suite_mk_b", "beta")
|
||||
state_set("suite_mk_c", "gamma")
|
||||
assert_eq(state_get("suite_mk_a"), "alpha", "key a is alpha")
|
||||
assert_eq(state_get("suite_mk_b"), "beta", "key b is beta")
|
||||
assert_eq(state_get("suite_mk_c"), "gamma", "key c is gamma")
|
||||
// Mutate one, others unchanged
|
||||
state_set("suite_mk_b", "BETA")
|
||||
assert_eq(state_get("suite_mk_a"), "alpha", "a unchanged after b mutated")
|
||||
assert_eq(state_get("suite_mk_b"), "BETA", "b updated")
|
||||
assert_eq(state_get("suite_mk_c"), "gamma", "c unchanged after b mutated")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── missing keys ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_missing_key(_: String) -> String {
|
||||
assert_eq(state_get("suite_definitely_not_set_xyz_123"), "", "missing key returns empty string")
|
||||
assert_eq(state_get("suite_another_missing_key_abc"), "", "another missing key returns empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_del ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_del_basic(_: String) -> String {
|
||||
state_set("suite_del1", "value_to_delete")
|
||||
assert_eq(state_get("suite_del1"), "value_to_delete", "key present before delete")
|
||||
state_del("suite_del1")
|
||||
assert_eq(state_get("suite_del1"), "", "key absent after delete")
|
||||
|
||||
// Delete a non-existent key should not error
|
||||
state_del("suite_never_existed_key_xyz")
|
||||
assert_eq(state_get("suite_never_existed_key_xyz"), "", "delete non-existent is no-op")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_state_del_and_reset(_: String) -> String {
|
||||
state_set("suite_reuse", "original")
|
||||
assert_eq(state_get("suite_reuse"), "original", "original value")
|
||||
state_del("suite_reuse")
|
||||
assert_eq(state_get("suite_reuse"), "", "deleted")
|
||||
state_set("suite_reuse", "new_value")
|
||||
assert_eq(state_get("suite_reuse"), "new_value", "key reused after delete")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_has ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_has_basic(_: String) -> String {
|
||||
state_set("suite_has1", "something")
|
||||
assert_true(state_has("suite_has1"), "key with value is present")
|
||||
assert_false(state_has("suite_definitely_absent_xyz_999"), "absent key returns false")
|
||||
|
||||
state_del("suite_has1")
|
||||
assert_false(state_has("suite_has1"), "deleted key is not present")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_get_or ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_get_or_basic(_: String) -> String {
|
||||
state_set("suite_gor1", "present")
|
||||
assert_eq(state_get_or("suite_gor1", "default"), "present", "returns value when key exists")
|
||||
assert_eq(state_get_or("suite_gor_missing_xyz", "fallback"), "fallback", "returns default when key missing")
|
||||
assert_eq(state_get_or("suite_gor_missing_xyz", ""), "", "returns empty default when key missing")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state persists across function calls ──────────────────────────────────────
|
||||
|
||||
fn _suite_state_helper_write() {
|
||||
state_set("suite_persist_test", "written_by_helper")
|
||||
}
|
||||
|
||||
fn _suite_state_helper_increment() {
|
||||
let n: Int = str_to_int(state_get("suite_counter"))
|
||||
state_set("suite_counter", int_to_str(n + 1))
|
||||
}
|
||||
|
||||
fn test_state_cross_function(_: String) -> String {
|
||||
_suite_state_helper_write()
|
||||
assert_eq(state_get("suite_persist_test"), "written_by_helper", "value written by helper function is readable")
|
||||
|
||||
state_set("suite_counter", "0")
|
||||
_suite_state_helper_increment()
|
||||
assert_eq(state_get("suite_counter"), "1", "counter after 1 increment")
|
||||
_suite_state_helper_increment()
|
||||
assert_eq(state_get("suite_counter"), "2", "counter after 2 increments")
|
||||
_suite_state_helper_increment()
|
||||
assert_eq(state_get("suite_counter"), "3", "counter after 3 increments")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_keys ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_keys_basic(_: String) -> String {
|
||||
state_set("suite_sk_x", "1")
|
||||
state_set("suite_sk_y", "2")
|
||||
let keys: String = state_keys()
|
||||
// keys is a JSON array of all state keys — must contain our test keys
|
||||
assert_true(str_contains(keys, "suite_sk_x"), "keys includes suite_sk_x")
|
||||
assert_true(str_contains(keys, "suite_sk_y"), "keys includes suite_sk_y")
|
||||
assert_true(str_starts_with(keys, "["), "keys is a JSON array")
|
||||
assert_true(str_ends_with(keys, "]"), "keys is a JSON array")
|
||||
assert_true(json_array_len(keys) > 0, "keys array is non-empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── value type storage ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_value_types(_: String) -> String {
|
||||
// JSON object as value
|
||||
let json_val: String = "{\"name\":\"alice\",\"age\":30}"
|
||||
state_set("suite_json_val", json_val)
|
||||
let retrieved: String = state_get("suite_json_val")
|
||||
assert_eq(json_get(retrieved, "name"), "alice", "JSON object stored and retrieved")
|
||||
|
||||
// JSON array as value
|
||||
let arr_val: String = "[1,2,3]"
|
||||
state_set("suite_arr_val", arr_val)
|
||||
let retrieved_arr: String = state_get("suite_arr_val")
|
||||
assert_int_eq(json_array_len(retrieved_arr), 3, "JSON array stored and retrieved")
|
||||
|
||||
// Large value
|
||||
let big: String = str_repeat("x", 500)
|
||||
state_set("suite_big_val", big)
|
||||
assert_int_eq(str_len(state_get("suite_big_val")), 500, "large value stored correctly")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
// tests/suite/test_string.el — comprehensive tests for runtime/string.el
|
||||
//
|
||||
// Covers every public function in string.el. Import this file after all
|
||||
// runtime modules and runtime/test.el have been concatenated in.
|
||||
|
||||
// ── str_eq ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_eq_basic(_: String) -> String {
|
||||
assert_true(str_eq("hello", "hello"), "identical strings are equal")
|
||||
assert_false(str_eq("hello", "world"), "different strings are not equal")
|
||||
assert_true(str_eq("", ""), "empty strings are equal")
|
||||
assert_false(str_eq("a", ""), "non-empty vs empty is not equal")
|
||||
assert_false(str_eq("", "a"), "empty vs non-empty is not equal")
|
||||
assert_false(str_eq("Hello", "hello"), "comparison is case-sensitive")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_eq_symbols(_: String) -> String {
|
||||
assert_true(str_eq("!@#$", "!@#$"), "symbols are equal")
|
||||
assert_true(str_eq(" ", " "), "spaces match")
|
||||
assert_false(str_eq(" a", "a "), "leading vs trailing space differ")
|
||||
assert_false(str_eq("abc", "ab"), "prefix not equal to full string")
|
||||
assert_false(str_eq("ab", "abc"), "shorter not equal to longer")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_len ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_len_basic(_: String) -> String {
|
||||
assert_int_eq(str_len(""), 0, "empty string has length 0")
|
||||
assert_int_eq(str_len("a"), 1, "single char has length 1")
|
||||
assert_int_eq(str_len("hello"), 5, "hello has length 5")
|
||||
assert_int_eq(str_len("hello world"), 11, "space counted in length")
|
||||
assert_int_eq(str_len("12345"), 5, "digits counted correctly")
|
||||
assert_int_eq(str_len("!@#$%"), 5, "punctuation counted correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_len_longer(_: String) -> String {
|
||||
let s: String = str_repeat("ab", 50)
|
||||
assert_int_eq(str_len(s), 100, "repeated string has correct length")
|
||||
assert_int_eq(str_len("abcdefghij"), 10, "10-char string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_concat / + operator ───────────────────────────────────────────────────
|
||||
|
||||
fn test_str_concat_basic(_: String) -> String {
|
||||
assert_eq(str_concat("hello", " world"), "hello world", "basic concat")
|
||||
assert_eq(str_concat("", "world"), "world", "empty prefix")
|
||||
assert_eq(str_concat("hello", ""), "hello", "empty suffix")
|
||||
assert_eq(str_concat("", ""), "", "both empty")
|
||||
assert_eq("foo" + "bar", "foobar", "operator + concat")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_concat_chaining(_: String) -> String {
|
||||
let a: String = "hello"
|
||||
let b: String = " "
|
||||
let c: String = "world"
|
||||
assert_eq(a + b + c, "hello world", "chained concat")
|
||||
assert_eq(str_concat(str_concat("a", "b"), "c"), "abc", "nested concat")
|
||||
assert_int_eq(str_len(str_concat("abc", "def")), 6, "length after concat")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_slice ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_slice_basic(_: String) -> String {
|
||||
assert_eq(str_slice("hello world", 0, 5), "hello", "slice from start")
|
||||
assert_eq(str_slice("hello world", 6, 11), "world", "slice from middle")
|
||||
assert_eq(str_slice("hello world", 0, 0), "", "zero-length slice")
|
||||
assert_eq(str_slice("hello", 0, 100), "hello", "end clamped to length")
|
||||
assert_eq(str_slice("hello", 3, 3), "", "start == end is empty")
|
||||
assert_eq(str_slice("hello world", 2, 7), "llo w", "interior slice")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_slice_edge(_: String) -> String {
|
||||
assert_eq(str_slice("", 0, 0), "", "empty string slice")
|
||||
assert_eq(str_slice("abc", 1, 2), "b", "single char slice")
|
||||
assert_eq(str_slice("abc", 0, 3), "abc", "full string slice")
|
||||
assert_eq(str_slice("abc", 2, 1), "", "inverted range is empty")
|
||||
assert_eq(str_slice("hello", 5, 5), "", "slice at end is empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_starts_with ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_starts_with_basic(_: String) -> String {
|
||||
assert_true(str_starts_with("hello world", "hello"), "prefix present")
|
||||
assert_false(str_starts_with("hello world", "world"), "not a prefix")
|
||||
assert_true(str_starts_with("hello", "hello"), "string is its own prefix")
|
||||
assert_true(str_starts_with("hello", ""), "empty prefix always true")
|
||||
assert_false(str_starts_with("", "a"), "empty string has no non-empty prefix")
|
||||
assert_false(str_starts_with("hi", "hello"), "prefix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_starts_with_edge(_: String) -> String {
|
||||
assert_true(str_starts_with("abc", "a"), "single-char prefix")
|
||||
assert_false(str_starts_with("abc", "b"), "wrong single-char prefix")
|
||||
assert_true(str_starts_with("", ""), "empty starts with empty")
|
||||
assert_true(str_starts_with("hello world", "hello world"), "full string is prefix")
|
||||
assert_false(str_starts_with("hello", "HELLO"), "case-sensitive prefix")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_ends_with ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_ends_with_basic(_: String) -> String {
|
||||
assert_true(str_ends_with("hello world", "world"), "suffix present")
|
||||
assert_false(str_ends_with("hello world", "hello"), "not a suffix")
|
||||
assert_true(str_ends_with("hello", "hello"), "string is its own suffix")
|
||||
assert_true(str_ends_with("hello", ""), "empty suffix always true")
|
||||
assert_false(str_ends_with("", "a"), "empty string has no non-empty suffix")
|
||||
assert_false(str_ends_with("hi", "world"), "suffix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_ends_with_edge(_: String) -> String {
|
||||
assert_true(str_ends_with("abc", "c"), "single-char suffix")
|
||||
assert_false(str_ends_with("abc", "b"), "wrong single-char suffix")
|
||||
assert_true(str_ends_with("", ""), "empty ends with empty")
|
||||
assert_false(str_ends_with("hello", "HELLO"), "case-sensitive suffix")
|
||||
assert_true(str_ends_with("file.txt", ".txt"), "common file extension case")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_contains ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_contains_basic(_: String) -> String {
|
||||
assert_true(str_contains("hello world", "world"), "contains at end")
|
||||
assert_true(str_contains("hello world", "hello"), "contains at start")
|
||||
assert_true(str_contains("hello world", "lo wo"), "contains in middle")
|
||||
assert_false(str_contains("hello world", "xyz"), "not contained")
|
||||
assert_true(str_contains("hello", ""), "empty sub always contained")
|
||||
assert_false(str_contains("", "a"), "empty string contains nothing")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_contains_edge(_: String) -> String {
|
||||
assert_true(str_contains("hello", "hello"), "string contains itself")
|
||||
assert_false(str_contains("hello", "helloo"), "longer sub not contained")
|
||||
assert_true(str_contains("aaa", "a"), "single char in repeated chars")
|
||||
assert_true(str_contains("aaa", "aa"), "two-char sub in repeated chars")
|
||||
assert_false(str_contains("abc", "ABC"), "case-sensitive")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_index_of ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_index_of_basic(_: String) -> String {
|
||||
assert_int_eq(str_index_of("hello world", "world"), 6, "index of suffix")
|
||||
assert_int_eq(str_index_of("hello world", "hello"), 0, "index of prefix")
|
||||
assert_int_eq(str_index_of("hello world", "o"), 4, "index of first occurrence")
|
||||
assert_int_eq(str_index_of("hello world", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_index_of("hello", ""), 0, "empty sub returns 0")
|
||||
assert_int_eq(str_index_of("", "a"), -1, "search in empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_index_of_duplicates(_: String) -> String {
|
||||
assert_int_eq(str_index_of("aababc", "ab"), 1, "finds first of two occurrences")
|
||||
assert_int_eq(str_index_of("abab", "ab"), 0, "finds first at position 0")
|
||||
assert_int_eq(str_index_of("xabab", "ab"), 1, "finds first after leading char")
|
||||
assert_int_eq(str_index_of("hello hello", "hello"), 0, "finds first hello")
|
||||
assert_int_eq(str_index_of("hello", "hello"), 0, "exact match at 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_last_index_of ─────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_last_index_of_basic(_: String) -> String {
|
||||
assert_int_eq(str_last_index_of("hello hello", "hello"), 6, "last occurrence index")
|
||||
assert_int_eq(str_last_index_of("hello", "hello"), 0, "only one occurrence")
|
||||
assert_int_eq(str_last_index_of("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_last_index_of("aababc", "ab"), 3, "last ab in aababc")
|
||||
assert_int_eq(str_last_index_of("abcabc", "abc"), 3, "last abc")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_replace ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_replace_basic(_: String) -> String {
|
||||
assert_eq(str_replace("hello world", "world", "there"), "hello there", "basic replace")
|
||||
assert_eq(str_replace("aaa", "a", "b"), "bbb", "replaces all occurrences")
|
||||
assert_eq(str_replace("hello", "xyz", "abc"), "hello", "no match is identity")
|
||||
assert_eq(str_replace("", "a", "b"), "", "empty string unchanged")
|
||||
assert_eq(str_replace("hello", "", "x"), "hello", "empty from is identity")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_replace_multiple(_: String) -> String {
|
||||
assert_eq(str_replace("hello hello", "hello", "bye"), "bye bye", "replace multiple")
|
||||
assert_eq(str_replace("aXbXc", "X", "-"), "a-b-c", "single-char delimiter")
|
||||
assert_eq(str_replace("abcabc", "abc", "X"), "XX", "multi-char replaced twice")
|
||||
assert_eq(str_replace("aabbcc", "bb", ""), "aacc", "replace with empty")
|
||||
assert_eq(str_replace("hello", "hello", "world"), "world", "replace whole string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_to_upper / str_to_lower ──────────────────────────────────────────────
|
||||
|
||||
fn test_str_to_upper_basic(_: String) -> String {
|
||||
assert_eq(str_to_upper("hello"), "HELLO", "lowercase to uppercase")
|
||||
assert_eq(str_to_upper("HELLO"), "HELLO", "already uppercase unchanged")
|
||||
assert_eq(str_to_upper("Hello World"), "HELLO WORLD", "mixed case")
|
||||
assert_eq(str_to_upper(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_upper("hello123"), "HELLO123", "digits unchanged")
|
||||
assert_eq(str_to_upper("abc-def"), "ABC-DEF", "punctuation unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_lower_basic(_: String) -> String {
|
||||
assert_eq(str_to_lower("HELLO"), "hello", "uppercase to lowercase")
|
||||
assert_eq(str_to_lower("hello"), "hello", "already lowercase unchanged")
|
||||
assert_eq(str_to_lower("Hello World"), "hello world", "mixed case")
|
||||
assert_eq(str_to_lower(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_lower("HELLO123"), "hello123", "digits unchanged")
|
||||
assert_eq(str_to_lower("ABC-DEF"), "abc-def", "punctuation unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_upper_lower_roundtrip(_: String) -> String {
|
||||
let s: String = "Hello, World!"
|
||||
assert_eq(str_to_lower(str_to_upper(s)), "hello, world!", "upper then lower")
|
||||
assert_eq(str_to_upper(str_to_lower(s)), "HELLO, WORLD!", "lower then upper")
|
||||
assert_eq(str_to_upper(str_to_lower("ABC")), "ABC", "ABC roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_trim / str_lstrip / str_rstrip ───────────────────────────────────────
|
||||
|
||||
fn test_str_trim_basic(_: String) -> String {
|
||||
assert_eq(str_trim(" hello "), "hello", "trims spaces both sides")
|
||||
assert_eq(str_trim("hello"), "hello", "no whitespace unchanged")
|
||||
assert_eq(str_trim(" "), "", "all-space string becomes empty")
|
||||
assert_eq(str_trim(""), "", "empty string unchanged")
|
||||
assert_eq(str_trim("\t hello \n"), "hello", "trims tabs and newlines")
|
||||
assert_eq(str_trim(" hello world "), "hello world", "internal spaces preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_lstrip_rstrip(_: String) -> String {
|
||||
assert_eq(str_lstrip(" hello "), "hello ", "lstrip removes leading spaces")
|
||||
assert_eq(str_rstrip(" hello "), " hello", "rstrip removes trailing spaces")
|
||||
assert_eq(str_lstrip("hello"), "hello", "lstrip no-op on clean string")
|
||||
assert_eq(str_rstrip("hello"), "hello", "rstrip no-op on clean string")
|
||||
assert_eq(str_lstrip(""), "", "lstrip empty string")
|
||||
assert_eq(str_rstrip(""), "", "rstrip empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_split ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_split_basic(_: String) -> String {
|
||||
let parts: [String] = str_split("a,b,c", ",")
|
||||
assert_int_eq(el_list_len(parts), 3, "split yields 3 parts")
|
||||
assert_eq(el_list_get(parts, 0), "a", "first part is a")
|
||||
assert_eq(el_list_get(parts, 1), "b", "second part is b")
|
||||
assert_eq(el_list_get(parts, 2), "c", "third part is c")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_split_edge(_: String) -> String {
|
||||
let single: [String] = str_split("hello", ",")
|
||||
assert_int_eq(el_list_len(single), 1, "no sep found yields 1 part")
|
||||
assert_eq(el_list_get(single, 0), "hello", "single part is the full string")
|
||||
|
||||
let trailing: [String] = str_split("a,b,", ",")
|
||||
assert_int_eq(el_list_len(trailing), 3, "trailing sep yields empty last element")
|
||||
assert_eq(el_list_get(trailing, 2), "", "last element is empty")
|
||||
|
||||
let multi: [String] = str_split("one::two::three", "::")
|
||||
assert_int_eq(el_list_len(multi), 3, "multi-char separator works")
|
||||
assert_eq(el_list_get(multi, 1), "two", "middle element correct")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_join ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_join_basic(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "a")
|
||||
let parts = el_list_append(parts, "b")
|
||||
let parts = el_list_append(parts, "c")
|
||||
assert_eq(str_join(parts, ","), "a,b,c", "basic join with comma")
|
||||
assert_eq(str_join(parts, ""), "abc", "join with empty separator")
|
||||
assert_eq(str_join(parts, " | "), "a | b | c", "join with multi-char sep")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_join_edge(_: String) -> String {
|
||||
let empty_list: [String] = el_list_empty()
|
||||
assert_eq(str_join(empty_list, ","), "", "joining empty list yields empty")
|
||||
|
||||
let one: [String] = el_list_empty()
|
||||
let one = el_list_append(one, "solo")
|
||||
assert_eq(str_join(one, ","), "solo", "joining single element yields that element")
|
||||
assert_eq(str_join(one, "---"), "solo", "single element: no sep inserted")
|
||||
|
||||
let two: [String] = el_list_empty()
|
||||
let two = el_list_append(two, "alpha")
|
||||
let two = el_list_append(two, "beta")
|
||||
assert_eq(str_join(two, ", "), "alpha, beta", "two elements join correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── int_to_str / str_to_int ──────────────────────────────────────────────────
|
||||
|
||||
fn test_int_to_str_basic(_: String) -> String {
|
||||
assert_eq(int_to_str(0), "0", "zero")
|
||||
assert_eq(int_to_str(42), "42", "positive integer")
|
||||
assert_eq(int_to_str(-1), "-1", "negative integer")
|
||||
assert_eq(int_to_str(1000000), "1000000", "large integer")
|
||||
assert_eq(int_to_str(-999), "-999", "negative large")
|
||||
assert_int_eq(str_len(int_to_str(12345)), 5, "correct digit count")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_int_basic(_: String) -> String {
|
||||
assert_int_eq(str_to_int("0"), 0, "zero")
|
||||
assert_int_eq(str_to_int("42"), 42, "positive integer")
|
||||
assert_int_eq(str_to_int("-1"), -1, "negative integer")
|
||||
assert_int_eq(str_to_int("1000000"), 1000000, "large integer")
|
||||
assert_int_eq(str_to_int("-999"), -999, "negative large")
|
||||
assert_int_eq(str_to_int(int_to_str(12345)), 12345, "roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── float_to_str / str_to_float ──────────────────────────────────────────────
|
||||
|
||||
fn test_float_to_str_basic(_: String) -> String {
|
||||
assert_eq(float_to_str(0.0), "0", "zero float to str")
|
||||
assert_eq(float_to_str(1.5), "1.5", "basic float")
|
||||
assert_eq(float_to_str(-3.14), "-3.14", "negative float")
|
||||
assert_true(str_len(float_to_str(1.0)) > 0, "float_to_str produces output")
|
||||
assert_contains(float_to_str(100.0), "100", "100.0 contains 100")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_float_basic(_: String) -> String {
|
||||
let f: Float = str_to_float("1.5")
|
||||
let s: String = float_to_str(f)
|
||||
assert_eq(s, "1.5", "roundtrip 1.5")
|
||||
|
||||
let f2: Float = str_to_float("0.0")
|
||||
let s2: String = float_to_str(f2)
|
||||
assert_eq(s2, "0", "roundtrip 0.0")
|
||||
|
||||
assert_true(str_to_float("3.14") > 3.13, "3.14 > 3.13")
|
||||
assert_true(str_to_float("3.14") < 3.15, "3.14 < 3.15")
|
||||
assert_true(str_to_float("-1.0") < 0.0, "negative float is negative")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_repeat / str_reverse ─────────────────────────────────────────────────
|
||||
|
||||
fn test_str_repeat_basic(_: String) -> String {
|
||||
assert_eq(str_repeat("ab", 3), "ababab", "repeat 3 times")
|
||||
assert_eq(str_repeat("x", 1), "x", "repeat once")
|
||||
assert_eq(str_repeat("x", 0), "", "repeat zero times yields empty")
|
||||
assert_eq(str_repeat("", 5), "", "repeating empty string yields empty")
|
||||
assert_eq(str_repeat("-", 4), "----", "single char repeat")
|
||||
assert_int_eq(str_len(str_repeat("abc", 10)), 30, "length is n * len")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_reverse_basic(_: String) -> String {
|
||||
assert_eq(str_reverse("hello"), "olleh", "basic reverse")
|
||||
assert_eq(str_reverse("a"), "a", "single char is its own reverse")
|
||||
assert_eq(str_reverse(""), "", "empty string reverses to empty")
|
||||
assert_eq(str_reverse("abcd"), "dcba", "even-length reverse")
|
||||
assert_eq(str_reverse("racecar"), "racecar", "palindrome unchanged")
|
||||
assert_eq(str_reverse("abc"), "cba", "three-char reverse")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_count ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_count_basic(_: String) -> String {
|
||||
assert_int_eq(str_count("hello world hello", "hello"), 2, "two occurrences")
|
||||
assert_int_eq(str_count("aaa", "a"), 3, "adjacent single chars")
|
||||
assert_int_eq(str_count("aaa", "aa"), 1, "non-overlapping: one match")
|
||||
assert_int_eq(str_count("hello", "xyz"), 0, "no match")
|
||||
assert_int_eq(str_count("", "a"), 0, "empty string has no occurrences")
|
||||
assert_int_eq(str_count("hello", ""), 0, "empty sub returns 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_strip_prefix / str_strip_suffix ──────────────────────────────────────
|
||||
|
||||
fn test_str_strip_prefix_basic(_: String) -> String {
|
||||
assert_eq(str_strip_prefix("foobar", "foo"), "bar", "strips matching prefix")
|
||||
assert_eq(str_strip_prefix("foobar", "baz"), "foobar", "no-match is identity")
|
||||
assert_eq(str_strip_prefix("hello", ""), "hello", "empty prefix is identity")
|
||||
assert_eq(str_strip_prefix("hello", "hello"), "", "full match yields empty")
|
||||
assert_eq(str_strip_prefix("hello", "HELLO"), "hello", "case-sensitive no-match")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_strip_suffix_basic(_: String) -> String {
|
||||
assert_eq(str_strip_suffix("hello.md", ".md"), "hello", "strips matching suffix")
|
||||
assert_eq(str_strip_suffix("hello.md", ".txt"), "hello.md", "no-match is identity")
|
||||
assert_eq(str_strip_suffix("hello", ""), "hello", "empty suffix is identity")
|
||||
assert_eq(str_strip_suffix("hello", "hello"), "", "full match yields empty")
|
||||
assert_eq(str_strip_suffix("hello", "HELLO"), "hello", "case-sensitive no-match")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_find_chars ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_find_chars_basic(_: String) -> String {
|
||||
assert_int_eq(str_find_chars("hello world", " "), 5, "finds space at index 5")
|
||||
assert_int_eq(str_find_chars("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_find_chars("hello", ""), -1, "empty charset returns -1")
|
||||
assert_int_eq(str_find_chars("hello", "aeiou"), 1, "finds first vowel at index 1")
|
||||
assert_int_eq(str_find_chars("", "a"), -1, "search in empty string returns -1")
|
||||
assert_int_eq(str_find_chars("abc", "c"), 2, "finds last char of string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_char_at / str_char_code ───────────────────────────────────────────────
|
||||
|
||||
fn test_str_char_at_basic(_: String) -> String {
|
||||
assert_eq(str_char_at("hello", 0), "h", "first char")
|
||||
assert_eq(str_char_at("hello", 4), "o", "last char")
|
||||
assert_eq(str_char_at("hello", 2), "l", "middle char")
|
||||
assert_eq(str_char_at("hello", -1), "", "negative index yields empty")
|
||||
assert_eq(str_char_at("hello", 5), "", "out-of-bounds yields empty")
|
||||
assert_eq(str_char_at("", 0), "", "empty string yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_char_code_basic(_: String) -> String {
|
||||
assert_int_eq(str_char_code("A", 0), 65, "A has code 65")
|
||||
assert_int_eq(str_char_code("a", 0), 97, "a has code 97")
|
||||
assert_int_eq(str_char_code("0", 0), 48, "0 has code 48")
|
||||
assert_int_eq(str_char_code("hello", 0), 104, "h has code 104")
|
||||
assert_int_eq(str_char_code("", 0), 0, "empty string yields 0")
|
||||
assert_int_eq(str_char_code("a", 5), 0, "out-of-bounds yields 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_pad_left / str_pad_right ─────────────────────────────────────────────
|
||||
|
||||
fn test_str_pad_left_basic(_: String) -> String {
|
||||
assert_eq(str_pad_left("hi", 5, " "), " hi", "pads to width 5")
|
||||
assert_eq(str_pad_left("hi", 2, " "), "hi", "already at width is unchanged")
|
||||
assert_eq(str_pad_left("hi", 1, " "), "hi", "wider than width is unchanged")
|
||||
assert_eq(str_pad_left("5", 3, "0"), "005", "zero-pad a number")
|
||||
assert_int_eq(str_len(str_pad_left("ab", 7, "x")), 7, "padded to exact width")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_pad_right_basic(_: String) -> String {
|
||||
assert_eq(str_pad_right("hi", 5, " "), "hi ", "pads to width 5")
|
||||
assert_eq(str_pad_right("hi", 2, " "), "hi", "already at width is unchanged")
|
||||
assert_eq(str_pad_right("hi", 1, " "), "hi", "wider than width is unchanged")
|
||||
assert_int_eq(str_len(str_pad_right("ab", 7, "-")), 7, "padded to exact width")
|
||||
assert_starts_with(str_pad_right("ab", 7, "-"), "ab", "original string at start")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Character classification ──────────────────────────────────────────────────
|
||||
|
||||
fn test_is_letter_basic(_: String) -> String {
|
||||
assert_true(is_letter("a"), "a is a letter")
|
||||
assert_true(is_letter("Z"), "Z is a letter")
|
||||
assert_true(is_letter("abc"), "abc all letters")
|
||||
assert_false(is_letter("1"), "1 is not a letter")
|
||||
assert_false(is_letter(""), "empty string is not a letter")
|
||||
assert_false(is_letter("a1"), "a1 has non-letter")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_is_digit_basic(_: String) -> String {
|
||||
assert_true(is_digit("0"), "0 is a digit")
|
||||
assert_true(is_digit("9"), "9 is a digit")
|
||||
assert_true(is_digit("123"), "123 all digits")
|
||||
assert_false(is_digit("a"), "a is not a digit")
|
||||
assert_false(is_digit(""), "empty is not a digit")
|
||||
assert_false(is_digit("1a"), "1a has non-digit")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_is_whitespace_basic(_: String) -> String {
|
||||
assert_true(is_whitespace(" "), "space is whitespace")
|
||||
assert_true(is_whitespace(" "), "multiple spaces")
|
||||
assert_false(is_whitespace("a"), "a is not whitespace")
|
||||
assert_false(is_whitespace(""), "empty string is not whitespace")
|
||||
assert_false(is_whitespace(" a"), "mixed is not all whitespace")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_count_lines / str_count_words ────────────────────────────────────────
|
||||
|
||||
fn test_str_count_lines_basic(_: String) -> String {
|
||||
assert_int_eq(str_count_lines(""), 0, "empty string has 0 lines")
|
||||
assert_int_eq(str_count_lines("hello"), 1, "single line no newline")
|
||||
assert_int_eq(str_count_lines("a\nb"), 2, "two lines with newline")
|
||||
assert_int_eq(str_count_lines("a\nb\nc"), 3, "three lines")
|
||||
assert_int_eq(str_count_lines("a\n"), 1, "trailing newline not counted as extra line")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_count_words_basic(_: String) -> String {
|
||||
assert_int_eq(str_count_words(""), 0, "empty string has 0 words")
|
||||
assert_int_eq(str_count_words("hello"), 1, "single word")
|
||||
assert_int_eq(str_count_words("hello world"), 2, "two words")
|
||||
assert_int_eq(str_count_words(" hello world "), 2, "extra spaces do not add words")
|
||||
assert_int_eq(str_count_words("one two three four"), 4, "four words")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── url_encode / url_decode ──────────────────────────────────────────────────
|
||||
|
||||
fn test_url_encode_basic(_: String) -> String {
|
||||
assert_eq(url_encode("hello"), "hello", "plain string unchanged")
|
||||
assert_contains(url_encode("hello world"), "%20", "space encoded as %20")
|
||||
assert_true(str_len(url_encode("hello world")) > str_len("hello world"), "encoded is longer")
|
||||
let encoded: String = url_encode("hello world")
|
||||
let decoded: String = url_decode(encoded)
|
||||
assert_eq(decoded, "hello world", "decode reverses encode")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_url_roundtrip(_: String) -> String {
|
||||
let s: String = "name=hello world&value=42"
|
||||
let encoded: String = url_encode(s)
|
||||
let decoded: String = url_decode(encoded)
|
||||
assert_eq(decoded, s, "url encode/decode roundtrip")
|
||||
assert_true(!str_eq(encoded, s), "encoded differs from original")
|
||||
assert_true(str_len(encoded) >= str_len(s), "encoded is at least as long")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── bool_to_str ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_bool_to_str_basic(_: String) -> String {
|
||||
assert_eq(bool_to_str(true), "true", "true becomes 'true'")
|
||||
assert_eq(bool_to_str(false), "false", "false becomes 'false'")
|
||||
assert_true(str_eq(bool_to_str(1 == 1), "true"), "expression true")
|
||||
assert_true(str_eq(bool_to_str(1 == 2), "false"), "expression false")
|
||||
assert_int_eq(str_len(bool_to_str(true)), 4, "true has length 4")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_to_bytes / bytes_to_str ───────────────────────────────────────────────
|
||||
|
||||
fn test_str_to_bytes_basic(_: String) -> String {
|
||||
let b: String = str_to_bytes("hi")
|
||||
assert_true(str_contains(b, "104"), "h has code 104")
|
||||
assert_true(str_contains(b, "105"), "i has code 105")
|
||||
assert_eq(str_to_bytes(""), "[]", "empty string yields empty array")
|
||||
assert_true(str_starts_with(b, "["), "bytes is JSON array")
|
||||
assert_true(str_ends_with(b, "]"), "bytes is JSON array")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_bytes_roundtrip(_: String) -> String {
|
||||
let s: String = "hello"
|
||||
let b: String = str_to_bytes(s)
|
||||
let s2: String = bytes_to_str(b)
|
||||
assert_eq(s2, s, "bytes roundtrip: hello")
|
||||
|
||||
let s3: String = "ABC"
|
||||
let b3: String = str_to_bytes(s3)
|
||||
let s4: String = bytes_to_str(b3)
|
||||
assert_eq(s4, s3, "bytes roundtrip: ABC")
|
||||
|
||||
assert_eq(bytes_to_str("[]"), "", "empty bytes yields empty string")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
// tests/suite/test_time.el — comprehensive tests for runtime/time.el
|
||||
//
|
||||
// Covers time_now, now_millis, unix_timestamp, now_ns, time_format, time_to_parts,
|
||||
// time_add, time_diff, duration helpers, and uuid_new.
|
||||
|
||||
// ── time_now / now_millis / unix_timestamp_ms ─────────────────────────────────
|
||||
|
||||
fn test_time_now_basic(_: String) -> String {
|
||||
let t: Int = time_now()
|
||||
// Milliseconds since epoch: must be well past year 2020 (1577836800000)
|
||||
assert_true(t > 1577836800000, "time_now is past 2020-01-01")
|
||||
// Must be before year 2100 (4102444800000)
|
||||
assert_true(t < 4102444800000, "time_now is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_now_millis_basic(_: String) -> String {
|
||||
let t: Int = now_millis()
|
||||
assert_true(t > 1577836800000, "now_millis is past 2020-01-01")
|
||||
assert_true(t < 4102444800000, "now_millis is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_unix_timestamp_ms_basic(_: String) -> String {
|
||||
let t: Int = unix_timestamp_ms()
|
||||
assert_true(t > 1577836800000, "unix_timestamp_ms is past 2020-01-01")
|
||||
assert_true(t < 4102444800000, "unix_timestamp_ms is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_now_ms_alias(_: String) -> String {
|
||||
let t: Int = time_now_ms()
|
||||
assert_true(t > 1577836800000, "time_now_ms is past 2020-01-01")
|
||||
assert_true(t < 4102444800000, "time_now_ms is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── monotonicity ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_monotonic_now_millis(_: String) -> String {
|
||||
let t1: Int = now_millis()
|
||||
let t2: Int = now_millis()
|
||||
assert_true(t2 >= t1, "second call >= first call (monotonic)")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_monotonic_time_now(_: String) -> String {
|
||||
let t1: Int = time_now()
|
||||
let t2: Int = time_now()
|
||||
assert_true(t2 >= t1, "time_now is monotonic across calls")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_monotonic_now_ns(_: String) -> String {
|
||||
let t1: Int = now_ns()
|
||||
let t2: Int = now_ns()
|
||||
assert_true(t2 >= t1, "now_ns is monotonic across calls")
|
||||
// ns should be much larger than ms (9 digits difference)
|
||||
assert_true(t1 > 1000000000000000000, "now_ns is in nanosecond scale")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── unix_timestamp (seconds) ─────────────────────────────────────────────────
|
||||
|
||||
fn test_unix_timestamp_basic(_: String) -> String {
|
||||
let secs: Int = unix_timestamp()
|
||||
// Must be past 2020-01-01 in seconds (1577836800)
|
||||
assert_true(secs > 1577836800, "unix_timestamp is past 2020-01-01")
|
||||
// Must be before 2100-01-01 in seconds (4102444800)
|
||||
assert_true(secs < 4102444800, "unix_timestamp is before 2100-01-01")
|
||||
// seconds are about 1000x smaller than millis
|
||||
let ms: Int = time_now()
|
||||
assert_true(ms > secs, "milliseconds > seconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_to_parts ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_to_parts_basic(_: String) -> String {
|
||||
// Known timestamp: 2024-01-15 12:30:45.123 UTC
|
||||
// 2024-01-15 00:00:00 UTC = epoch + (54 * 365 + 14 leaps) days approximately
|
||||
// Use a precisely known value: 2024-01-15T12:30:45.123Z
|
||||
// epoch_ms = 1705320645123
|
||||
let ts: Int = 1705320645123
|
||||
let parts: String = time_to_parts(ts)
|
||||
assert_eq(json_get(parts, "year"), "2024", "year is 2024")
|
||||
assert_eq(json_get(parts, "month"), "1", "month is 1 (January)")
|
||||
assert_eq(json_get(parts, "day"), "15", "day is 15")
|
||||
assert_eq(json_get(parts, "hour"), "12", "hour is 12")
|
||||
assert_eq(json_get(parts, "minute"), "30", "minute is 30")
|
||||
assert_eq(json_get(parts, "second"), "45", "second is 45")
|
||||
assert_eq(json_get(parts, "ms"), "123", "milliseconds is 123")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_to_parts_epoch(_: String) -> String {
|
||||
// Unix epoch: 1970-01-01T00:00:00.000Z = timestamp 0
|
||||
let parts: String = time_to_parts(0)
|
||||
assert_eq(json_get(parts, "year"), "1970", "epoch year is 1970")
|
||||
assert_eq(json_get(parts, "month"), "1", "epoch month is 1")
|
||||
assert_eq(json_get(parts, "day"), "1", "epoch day is 1")
|
||||
assert_eq(json_get(parts, "hour"), "0", "epoch hour is 0")
|
||||
assert_eq(json_get(parts, "minute"), "0", "epoch minute is 0")
|
||||
assert_eq(json_get(parts, "second"), "0", "epoch second is 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_to_parts_current(_: String) -> String {
|
||||
let ts: Int = time_now()
|
||||
let parts: String = time_to_parts(ts)
|
||||
let year: Int = str_to_int(json_get(parts, "year"))
|
||||
let month: Int = str_to_int(json_get(parts, "month"))
|
||||
let day: Int = str_to_int(json_get(parts, "day"))
|
||||
let hour: Int = str_to_int(json_get(parts, "hour"))
|
||||
let minute: Int = str_to_int(json_get(parts, "minute"))
|
||||
let second: Int = str_to_int(json_get(parts, "second"))
|
||||
assert_true(year >= 2024, "current year >= 2024")
|
||||
assert_true(year < 2100, "current year < 2100")
|
||||
assert_true(month >= 1, "month >= 1")
|
||||
assert_true(month <= 12, "month <= 12")
|
||||
assert_true(day >= 1, "day >= 1")
|
||||
assert_true(day <= 31, "day <= 31")
|
||||
assert_true(hour >= 0, "hour >= 0")
|
||||
assert_true(hour <= 23, "hour <= 23")
|
||||
assert_true(minute >= 0, "minute >= 0")
|
||||
assert_true(minute <= 59, "minute <= 59")
|
||||
assert_true(second >= 0, "second >= 0")
|
||||
assert_true(second <= 59, "second <= 59")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_format ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_format_iso(_: String) -> String {
|
||||
let ts: Int = 1705320645123
|
||||
let iso: String = time_format(ts, "ISO")
|
||||
assert_starts_with(iso, "2024-01-15", "ISO format starts with correct date")
|
||||
assert_contains(iso, "T12:30:45", "ISO format has correct time")
|
||||
assert_ends_with(iso, "Z", "ISO format ends with Z")
|
||||
assert_contains(iso, "123", "ISO format includes milliseconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_format_empty(_: String) -> String {
|
||||
let ts: Int = 1705320645123
|
||||
let iso: String = time_format(ts, "")
|
||||
assert_starts_with(iso, "2024-01-15", "empty fmt gives ISO format")
|
||||
assert_ends_with(iso, "Z", "empty fmt gives ISO format ending in Z")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_format_strftime(_: String) -> String {
|
||||
let ts: Int = 1705320645123
|
||||
let formatted: String = time_format(ts, "%Y-%m-%d")
|
||||
assert_eq(formatted, "2024-01-15", "strftime %Y-%m-%d")
|
||||
let formatted2: String = time_format(ts, "%H:%M:%S")
|
||||
assert_eq(formatted2, "12:30:45", "strftime %H:%M:%S")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_add ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_add_basic(_: String) -> String {
|
||||
let base: Int = 1000000000000
|
||||
assert_int_eq(time_add(base, 1000, "ms"), base + 1000, "add milliseconds")
|
||||
assert_int_eq(time_add(base, 1, "sec"), base + 1000, "add 1 second")
|
||||
assert_int_eq(time_add(base, 1, "min"), base + 60000, "add 1 minute")
|
||||
assert_int_eq(time_add(base, 1, "hour"), base + 3600000, "add 1 hour")
|
||||
assert_int_eq(time_add(base, 1, "day"), base + 86400000, "add 1 day")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_add_multiple(_: String) -> String {
|
||||
let base: Int = 1000000000000
|
||||
assert_int_eq(time_add(base, 60, "sec"), base + 60000, "add 60 seconds")
|
||||
assert_int_eq(time_add(base, 24, "hour"), base + 86400000, "add 24 hours = 1 day")
|
||||
assert_int_eq(time_add(base, 0, "sec"), base, "add 0 seconds")
|
||||
assert_int_eq(time_add(base, -1, "sec"), base - 1000, "add negative seconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_diff ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_diff_basic(_: String) -> String {
|
||||
let t1: Int = 1000000000000
|
||||
let t2: Int = t1 + 5000
|
||||
assert_int_eq(time_diff(t1, t2, "ms"), 5000, "diff in ms")
|
||||
assert_int_eq(time_diff(t1, t2, "sec"), 5, "diff in seconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_diff_larger(_: String) -> String {
|
||||
let t1: Int = 1000000000000
|
||||
let t2: Int = t1 + 3600000
|
||||
assert_int_eq(time_diff(t1, t2, "min"), 60, "diff of 1 hour in minutes")
|
||||
assert_int_eq(time_diff(t1, t2, "hour"), 1, "diff of 1 hour in hours")
|
||||
assert_int_eq(time_diff(t1, t2, "ms"), 3600000, "diff of 1 hour in ms")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_diff_zero(_: String) -> String {
|
||||
let t: Int = 1000000000000
|
||||
assert_int_eq(time_diff(t, t, "ms"), 0, "same timestamp: diff is 0")
|
||||
assert_int_eq(time_diff(t, t, "sec"), 0, "same timestamp: diff in sec is 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Duration helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
fn test_duration_helpers_basic(_: String) -> String {
|
||||
assert_int_eq(duration_seconds(1), 1000000000, "1 second = 1e9 ns")
|
||||
assert_int_eq(duration_millis(1), 1000000, "1 ms = 1e6 ns")
|
||||
assert_int_eq(duration_nanos(42), 42, "nanos identity")
|
||||
assert_int_eq(duration_to_seconds(1000000000), 1, "1e9 ns = 1 second")
|
||||
assert_int_eq(duration_to_millis(1000000), 1, "1e6 ns = 1 ms")
|
||||
assert_int_eq(duration_to_nanos(42), 42, "nanos identity roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_instant_helpers_basic(_: String) -> String {
|
||||
let i: Int = unix_seconds(1000)
|
||||
assert_int_eq(i, 1000000000000, "unix_seconds(1000) = 1e12 ns")
|
||||
|
||||
let ms_i: Int = unix_millis(5000)
|
||||
assert_int_eq(ms_i, 5000000000, "unix_millis(5000) = 5e9 ns")
|
||||
|
||||
assert_int_eq(instant_to_unix_seconds(1000000000000), 1000, "instant to seconds")
|
||||
assert_int_eq(instant_to_unix_millis(5000000000), 5000, "instant to millis")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── uuid_new / uuid_v4 ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_uuid_new_basic(_: String) -> String {
|
||||
let u: String = uuid_new()
|
||||
assert_int_eq(str_len(u), 36, "UUID has 36 characters")
|
||||
assert_eq(str_char_at(u, 8), "-", "UUID has dash at position 8")
|
||||
assert_eq(str_char_at(u, 13), "-", "UUID has dash at position 13")
|
||||
assert_eq(str_char_at(u, 18), "-", "UUID has dash at position 18")
|
||||
assert_eq(str_char_at(u, 23), "-", "UUID has dash at position 23")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_uuid_uniqueness(_: String) -> String {
|
||||
let u1: String = uuid_new()
|
||||
let u2: String = uuid_new()
|
||||
let u3: String = uuid_v4()
|
||||
assert_false(str_eq(u1, u2), "consecutive UUIDs are different")
|
||||
assert_false(str_eq(u1, u3), "uuid_new and uuid_v4 produce different values")
|
||||
assert_false(str_eq(u2, u3), "three consecutive UUIDs are all different")
|
||||
assert_int_eq(str_len(u3), 36, "uuid_v4 also produces 36-char UUID")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_from_parts ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_from_parts_basic(_: String) -> String {
|
||||
// time_from_parts(secs, ns, tz) = secs * 1000 + ns / 1000000
|
||||
let ts: Int = time_from_parts(1000, 0, "UTC")
|
||||
assert_int_eq(ts, 1000000, "1000 secs = 1000000 ms")
|
||||
|
||||
let ts2: Int = time_from_parts(0, 500000000, "UTC")
|
||||
assert_int_eq(ts2, 500, "500ms in nanoseconds -> 500ms")
|
||||
|
||||
let ts3: Int = time_from_parts(1705320645, 123000000, "UTC")
|
||||
assert_int_eq(ts3, 1705320645123, "known timestamp with ms component")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── instant_to_iso8601 ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_instant_to_iso8601_basic(_: String) -> String {
|
||||
let ts_ms: Int = 1705320645123
|
||||
let ts_ns: Int = ts_ms * 1000000
|
||||
let iso: String = instant_to_iso8601(ts_ns)
|
||||
assert_starts_with(iso, "2024-01-15", "instant_to_iso8601 correct date")
|
||||
assert_ends_with(iso, "Z", "instant_to_iso8601 ends with Z")
|
||||
return ""
|
||||
}
|
||||
Executable
+127
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# install.sh — install El as a proper local framework tool.
|
||||
#
|
||||
# Copies the compiler binary, runtime source files, headers, and stdlib
|
||||
# from the local source tree into the install prefix. After install,
|
||||
# El programs can be built against an installed, stable copy of the runtime.
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/install.sh
|
||||
# ./tools/install.sh --prefix /opt/el
|
||||
#
|
||||
# Environment:
|
||||
# EL_HOME Override install prefix (same as --prefix)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EL_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
PREFIX="${EL_HOME:-/usr/local/el}"
|
||||
|
||||
# Parse --prefix flag
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--prefix)
|
||||
PREFIX="$2"
|
||||
shift 2
|
||||
;;
|
||||
--prefix=*)
|
||||
PREFIX="${1#--prefix=}"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
echo "Usage: $0 [--prefix <path>]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
BIN_DIR="${PREFIX}/bin"
|
||||
RUNTIME_DIR="${PREFIX}/runtime"
|
||||
INCLUDE_DIR="${PREFIX}/include"
|
||||
LIB_DIR="${PREFIX}/lib"
|
||||
|
||||
ELC_SRC="${EL_ROOT}/dist/platform/elc"
|
||||
RUNTIME_SRC="${EL_ROOT}/el-compiler/runtime"
|
||||
STDLIB_SRC="${EL_ROOT}/runtime"
|
||||
|
||||
echo "==> Installing El framework to ${PREFIX}"
|
||||
echo " bin: ${BIN_DIR}"
|
||||
echo " runtime: ${RUNTIME_DIR}"
|
||||
echo " include: ${INCLUDE_DIR}"
|
||||
echo " lib: ${LIB_DIR}"
|
||||
echo
|
||||
|
||||
# Create directories
|
||||
mkdir -p "${BIN_DIR}" "${RUNTIME_DIR}" "${INCLUDE_DIR}" "${LIB_DIR}"
|
||||
|
||||
# 1. Install elc binary
|
||||
if [[ ! -f "${ELC_SRC}" ]]; then
|
||||
echo "Error: elc binary not found at ${ELC_SRC}" >&2
|
||||
echo "Build it first or run from the el repo root." >&2
|
||||
exit 1
|
||||
fi
|
||||
install -m 755 "${ELC_SRC}" "${BIN_DIR}/elc"
|
||||
echo " installed: ${BIN_DIR}/elc"
|
||||
|
||||
# 2. Install runtime .el files
|
||||
for f in "${STDLIB_SRC}"/*.el; do
|
||||
[[ -f "$f" ]] || continue
|
||||
install -m 644 "$f" "${RUNTIME_DIR}/$(basename "$f")"
|
||||
done
|
||||
echo " installed: ${RUNTIME_DIR}/*.el ($(ls "${STDLIB_SRC}"/*.el | wc -l | tr -d ' ') files)"
|
||||
|
||||
# 3. Install headers
|
||||
for header in el_seed.h el_runtime.h; do
|
||||
src="${RUNTIME_SRC}/${header}"
|
||||
if [[ -f "${src}" ]]; then
|
||||
install -m 644 "${src}" "${INCLUDE_DIR}/${header}"
|
||||
echo " installed: ${INCLUDE_DIR}/${header}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. Build libel.a from el_seed.c + el_runtime.c
|
||||
echo " compiling libel.a..."
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||
|
||||
cc -std=c11 -O2 -I"${RUNTIME_SRC}" -c "${RUNTIME_SRC}/el_seed.c" -o "${TMP_DIR}/el_seed.o"
|
||||
cc -std=c11 -O2 -I"${RUNTIME_SRC}" -c "${RUNTIME_SRC}/el_runtime.c" -o "${TMP_DIR}/el_runtime.o"
|
||||
ar rcs "${LIB_DIR}/libel.a" "${TMP_DIR}/el_seed.o" "${TMP_DIR}/el_runtime.o"
|
||||
echo " installed: ${LIB_DIR}/libel.a"
|
||||
|
||||
# 5. Generate stdlib.el with absolute paths to installed runtime files
|
||||
STDLIB_OUT="${PREFIX}/stdlib.el"
|
||||
{
|
||||
echo "// stdlib.el — El standard library (installed at ${PREFIX})"
|
||||
echo "// Generated by tools/install.sh — do not edit by hand."
|
||||
echo "// Import this file to get the full El runtime:"
|
||||
echo "// import \"${PREFIX}/stdlib.el\""
|
||||
echo
|
||||
for f in "${STDLIB_SRC}"/*.el; do
|
||||
[[ -f "$f" ]] || continue
|
||||
base="$(basename "$f")"
|
||||
# Skip stdlib.el itself to avoid circular import
|
||||
[[ "${base}" == "stdlib.el" ]] && continue
|
||||
# Skip test.el — dev-only
|
||||
[[ "${base}" == "test.el" ]] && continue
|
||||
echo "import \"${RUNTIME_DIR}/${base}\""
|
||||
done
|
||||
} > "${STDLIB_OUT}"
|
||||
echo " installed: ${STDLIB_OUT}"
|
||||
|
||||
echo
|
||||
echo "==> El installed to ${PREFIX}"
|
||||
echo
|
||||
echo "Add ${BIN_DIR} to your PATH:"
|
||||
echo " export PATH=\"${BIN_DIR}:\$PATH\""
|
||||
echo
|
||||
echo "To use the stdlib in your El programs:"
|
||||
echo " import \"${PREFIX}/stdlib.el\""
|
||||
echo
|
||||
echo "To link El programs against the installed runtime:"
|
||||
echo " elc src/main.el > dist/main.c"
|
||||
echo " cc -std=c11 -O2 -I${INCLUDE_DIR} -o dist/main dist/main.c ${LIB_DIR}/libel.a -lcurl -lpthread"
|
||||
echo
|
||||
@@ -0,0 +1,198 @@
|
||||
# El LSP — Language Server for El
|
||||
|
||||
Full Language Server Protocol implementation for the El programming language.
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | Status |
|
||||
|---------|--------|
|
||||
| Syntax highlighting | Full TextMate grammar |
|
||||
| Completions | Builtins (130+), user-defined fns, keywords, types |
|
||||
| Hover | Signatures + descriptions for all builtins and user fns |
|
||||
| Go-to-definition | Jump to `fn name(` in the open document |
|
||||
| Diagnostics | Unclosed braces/parens/brackets, unterminated strings |
|
||||
| Document sync | Full (re-sends entire document on every change) |
|
||||
|
||||
## Building
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- The `elc` compiler binary at `dist/platform/elc` (built from the repo root)
|
||||
- `cc` (clang or gcc), `libcurl`, `pthreads`
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# From the el repo root:
|
||||
./tools/lsp/build.sh
|
||||
|
||||
# Or with a custom elc path:
|
||||
ELC=/path/to/elc ./tools/lsp/build.sh
|
||||
```
|
||||
|
||||
Output: `tools/lsp/dist/el-lsp`
|
||||
|
||||
### Install system-wide
|
||||
|
||||
```bash
|
||||
sudo cp tools/lsp/dist/el-lsp /usr/local/bin/el-lsp
|
||||
```
|
||||
|
||||
## VSCode Extension
|
||||
|
||||
### Development install (recommended)
|
||||
|
||||
1. Build the binary first (see above).
|
||||
2. Install the npm dependency:
|
||||
```bash
|
||||
cd tools/lsp/vscode-extension
|
||||
npm install
|
||||
```
|
||||
3. Open `tools/lsp/vscode-extension/` in VSCode.
|
||||
4. Press **F5** — this launches the Extension Development Host.
|
||||
5. Open any `.el` file in the dev host window.
|
||||
|
||||
### Package as .vsix
|
||||
|
||||
```bash
|
||||
npm install -g @vscode/vsce
|
||||
cd tools/lsp/vscode-extension
|
||||
vsce package
|
||||
# Produces: el-language-1.0.0.vsix
|
||||
```
|
||||
|
||||
Install the .vsix:
|
||||
```bash
|
||||
code --install-extension el-language-1.0.0.vsix
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `el.lspPath` | (bundled) | Path to `el-lsp` binary. Empty = use `../dist/el-lsp`. |
|
||||
| `el.trace.server` | `off` | LSP message tracing. Set to `verbose` to see all messages. |
|
||||
|
||||
## Neovim / other editors
|
||||
|
||||
Any editor that supports LSP can use `el-lsp`. Example configuration for
|
||||
Neovim with `nvim-lspconfig`:
|
||||
|
||||
```lua
|
||||
local lspconfig = require('lspconfig')
|
||||
local configs = require('lspconfig.configs')
|
||||
|
||||
if not configs.el then
|
||||
configs.el = {
|
||||
default_config = {
|
||||
cmd = { 'el-lsp' },
|
||||
filetypes = { 'el' },
|
||||
root_dir = lspconfig.util.root_pattern('.git', '*.el'),
|
||||
settings = {},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
lspconfig.el.setup({})
|
||||
```
|
||||
|
||||
Add to `ftdetect/el.vim`:
|
||||
```vim
|
||||
au BufRead,BufNewFile *.el set filetype=el
|
||||
```
|
||||
|
||||
## Wire protocol
|
||||
|
||||
El LSP communicates over stdin/stdout using standard LSP framing:
|
||||
|
||||
```
|
||||
Content-Length: <N>\r\n
|
||||
\r\n
|
||||
<N bytes of UTF-8 JSON>
|
||||
```
|
||||
|
||||
The `__read_n(n: Int) -> String` primitive in `el_runtime.c` reads exactly
|
||||
`n` bytes from stdin — needed because `readline()` stops at `\n` and LSP
|
||||
JSON bodies are not newline-terminated. `__print_raw(s: String)` writes with
|
||||
`fwrite + fflush` to preserve embedded `\r\n` in headers.
|
||||
|
||||
## Smoke test
|
||||
|
||||
After building, verify the server responds to `initialize`:
|
||||
|
||||
```bash
|
||||
python3 - << 'PY'
|
||||
import subprocess, json
|
||||
|
||||
def frame(obj):
|
||||
body = json.dumps(obj).encode()
|
||||
return f"Content-Length: {len(body)}\r\n\r\n".encode() + body
|
||||
|
||||
def read_response(proc):
|
||||
hdr = b""
|
||||
while not hdr.endswith(b"\r\n\r\n"):
|
||||
hdr += proc.stdout.read(1)
|
||||
cl = int([l for l in hdr.decode().split("\r\n") if "Content-Length" in l][0].split(": ")[1])
|
||||
return json.loads(proc.stdout.read(cl))
|
||||
|
||||
proc = subprocess.Popen(["./dist/el-lsp"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
proc.stdin.write(frame({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}))
|
||||
proc.stdin.flush()
|
||||
r = read_response(proc)
|
||||
print("Server name:", r["result"]["serverInfo"]["name"])
|
||||
print("Capabilities:", list(r["result"]["capabilities"].keys()))
|
||||
proc.stdin.write(frame({"jsonrpc":"2.0","method":"exit","params":{}}))
|
||||
proc.stdin.flush()
|
||||
proc.wait()
|
||||
print("OK")
|
||||
PY
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
el-lsp.el
|
||||
lsp_read_message() reads header bytes one-by-one, then __read_n(body_len)
|
||||
lsp_write_message() __print_raw("Content-Length: N\r\n\r\n" + json)
|
||||
lsp_dispatch() routes method string to handler
|
||||
lsp_builtin_catalog() [String] of "name|signature|description" entries
|
||||
lsp_extract_fns() scan source for "fn name(" patterns
|
||||
lsp_word_at() expand identifier under cursor
|
||||
lsp_compute_diagnostics() scan for unclosed brackets + unterminated strings
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
tools/lsp/
|
||||
el-lsp.el LSP server source (El language)
|
||||
build.sh Build script
|
||||
README.md This file
|
||||
dist/
|
||||
el-lsp Compiled binary (after build)
|
||||
el-lsp.c Generated C (after build)
|
||||
vscode-extension/
|
||||
extension.js Extension entry point
|
||||
package.json Extension manifest
|
||||
language-configuration.json Bracket matching, comment config
|
||||
syntaxes/
|
||||
el.tmGrammar.json TextMate syntax grammar
|
||||
.vscode/
|
||||
launch.json F5 debug configuration
|
||||
tasks.json Pre-launch npm install task
|
||||
```
|
||||
|
||||
## Runtime additions
|
||||
|
||||
Two new primitives added to `el-compiler/runtime/`:
|
||||
|
||||
### `__read_n(n: Int) -> String`
|
||||
Reads exactly `n` bytes from stdin using `fread`. Returns `""` on EOF.
|
||||
Required for reading JSON-RPC message bodies.
|
||||
|
||||
### `__print_raw(s: String) -> Void`
|
||||
Writes a string to stdout using `fwrite + fflush`. Preserves embedded
|
||||
`\r\n` bytes exactly. Required for LSP Content-Length headers.
|
||||
|
||||
Both are declared in `el_runtime.h` and registered in the `builtin_arity`
|
||||
table in `el-compiler/src/codegen.el`.
|
||||
BIN
Binary file not shown.
Vendored
+1252
File diff suppressed because it is too large
Load Diff
+1158
File diff suppressed because it is too large
Load Diff
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Extension (Extension Development Host)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "npm: install",
|
||||
"env": {
|
||||
"EL_LSP_LOG": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Attach to el-lsp process",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 6009,
|
||||
"restart": true,
|
||||
"timeout": 10000,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "install",
|
||||
"label": "npm: install",
|
||||
"detail": "Install vscode-languageclient dependency",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// tools/lsp/vscode-extension/extension.js
|
||||
//
|
||||
// El Language VSCode extension — Language Client
|
||||
//
|
||||
// Launches the el-lsp binary as a child process and connects VSCode's
|
||||
// Language Client to it over stdin/stdout (JSON-RPC 2.0 + Content-Length).
|
||||
//
|
||||
// Capabilities provided by el-lsp:
|
||||
// - textDocumentSync (full)
|
||||
// - completionProvider — builtins + user fns + keywords
|
||||
// - hoverProvider — signatures + descriptions
|
||||
// - definitionProvider — go-to-def for user-defined fns
|
||||
// - diagnostics — unclosed braces/parens, unterminated strings
|
||||
//
|
||||
// Setup (development):
|
||||
// 1. cd tools/lsp && ./build.sh (builds dist/el-lsp)
|
||||
// 2. cd vscode-extension && npm install (installs vscode-languageclient)
|
||||
// 3. Open vscode-extension/ in VSCode
|
||||
// 4. Press F5 (launches Extension Development Host)
|
||||
// 5. Open any .el file
|
||||
//
|
||||
// Package as .vsix:
|
||||
// npm install -g @vscode/vsce
|
||||
// cd tools/lsp/vscode-extension && vsce package
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { workspace, window, ExtensionContext } = require('vscode');
|
||||
const {
|
||||
LanguageClient,
|
||||
TransportKind,
|
||||
} = require('vscode-languageclient/node');
|
||||
|
||||
/** @type {LanguageClient | undefined} */
|
||||
let client;
|
||||
|
||||
/**
|
||||
* activate — entry point called when any .el file is first opened.
|
||||
*
|
||||
* @param {ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
// ── Resolve el-lsp binary ────────────────────────────────────────────
|
||||
const config = workspace.getConfiguration('el');
|
||||
|
||||
// Default path resolution (tries in order):
|
||||
// 1. el.lspPath setting (user override)
|
||||
// 2. dist/el-lsp alongside the extension dir (packaged install)
|
||||
// 3. The canonical source-tree location
|
||||
const CANONICAL_LSP = '/Users/will/Development/neuron-technologies/foundation/el/tools/lsp/dist/el-lsp';
|
||||
const defaultLspPath = (() => {
|
||||
const sibling = path.join(context.extensionPath, 'dist', 'el-lsp');
|
||||
if (fs.existsSync(sibling)) return sibling;
|
||||
return CANONICAL_LSP;
|
||||
})();
|
||||
const serverPath = config.get('lspPath') || defaultLspPath;
|
||||
|
||||
if (!fs.existsSync(serverPath)) {
|
||||
window.showErrorMessage(
|
||||
`El Language Server binary not found at: ${serverPath}\n` +
|
||||
`Run tools/lsp/build.sh to build it, then reload VSCode.\n` +
|
||||
`Or set "el.lspPath" in settings to the correct path.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Server options (start el-lsp as child process) ───────────────────
|
||||
/** @type {import('vscode-languageclient/node').ServerOptions} */
|
||||
const serverOptions = {
|
||||
run: {
|
||||
command: serverPath,
|
||||
transport: TransportKind.stdio,
|
||||
options: { env: { ...process.env } },
|
||||
},
|
||||
debug: {
|
||||
command: serverPath,
|
||||
transport: TransportKind.stdio,
|
||||
options: {
|
||||
env: { ...process.env },
|
||||
// Redirect el-lsp stderr to VSCode's Output panel in debug mode.
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ── Client options ───────────────────────────────────────────────────
|
||||
/** @type {import('vscode-languageclient/node').LanguageClientOptions} */
|
||||
const clientOptions = {
|
||||
// Activate for all .el files (file:// and untitled: schemes).
|
||||
documentSelector: [
|
||||
{ scheme: 'file', language: 'el' },
|
||||
{ scheme: 'untitled', language: 'el' },
|
||||
],
|
||||
synchronize: {
|
||||
// Fire fileEvents so the client resends on disk changes outside VSCode.
|
||||
fileEvents: workspace.createFileSystemWatcher('**/*.el'),
|
||||
},
|
||||
outputChannelName: 'El Language Server',
|
||||
// Trace LSP messages to the Output panel for debugging.
|
||||
// Set "el.trace.server": "verbose" in settings to enable.
|
||||
traceOutputChannel: window.createOutputChannel('El LSP Trace'),
|
||||
};
|
||||
|
||||
// ── Create and start client ──────────────────────────────────────────
|
||||
client = new LanguageClient(
|
||||
'el-language-server',
|
||||
'El Language Server',
|
||||
serverOptions,
|
||||
clientOptions,
|
||||
);
|
||||
|
||||
// Register the client so it is disposed when the extension deactivates.
|
||||
context.subscriptions.push(client);
|
||||
|
||||
client.start().then(() => {
|
||||
// Show a discrete status bar item while the server is active.
|
||||
const status = window.createStatusBarItem(1);
|
||||
status.text = '$(symbol-misc) El LSP';
|
||||
status.tooltip = 'El Language Server is running';
|
||||
status.command = 'el.restartServer';
|
||||
status.show();
|
||||
context.subscriptions.push(status);
|
||||
}).catch((err) => {
|
||||
window.showErrorMessage(`El Language Server failed to start: ${err.message}`);
|
||||
});
|
||||
|
||||
// ── Commands ─────────────────────────────────────────────────────────
|
||||
context.subscriptions.push(
|
||||
require('vscode').commands.registerCommand('el.restartServer', () => {
|
||||
if (client) {
|
||||
client.stop().then(() => client.start());
|
||||
window.showInformationMessage('El Language Server restarted.');
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* deactivate — called when the extension is unloaded.
|
||||
* Sends LSP shutdown + exit to the server process.
|
||||
*/
|
||||
function deactivate() {
|
||||
if (!client) {
|
||||
return undefined;
|
||||
}
|
||||
return client.stop();
|
||||
}
|
||||
|
||||
module.exports = { activate, deactivate };
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"comments": {
|
||||
"lineComment": "//"
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "\"", "close": "\"", "notIn": ["string"] }
|
||||
],
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""]
|
||||
],
|
||||
"indentationRules": {
|
||||
"increaseIndentPattern": "\\{[^}]*$",
|
||||
"decreaseIndentPattern": "^\\s*}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"name": "el-language",
|
||||
"displayName": "El Language",
|
||||
"description": "El language support \u2014 syntax highlighting, completions, hover, go-to-definition, and diagnostics",
|
||||
"version": "1.0.0",
|
||||
"publisher": "neuron-technologies",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/neuron-technologies/foundation"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
"Linters",
|
||||
"Other"
|
||||
],
|
||||
"keywords": [
|
||||
"el",
|
||||
"el-lang",
|
||||
"language-server",
|
||||
"lsp",
|
||||
"neuron"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage:el"
|
||||
],
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "el",
|
||||
"aliases": [
|
||||
"El",
|
||||
"el-lang"
|
||||
],
|
||||
"extensions": [
|
||||
".el",
|
||||
".elh"
|
||||
],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "el",
|
||||
"scopeName": "source.el",
|
||||
"path": "./syntaxes/el.tmGrammar.json"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "El Language",
|
||||
"properties": {
|
||||
"el.lspPath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Absolute path to the el-lsp binary. Leave empty to use the binary bundled with the extension (../dist/el-lsp)."
|
||||
},
|
||||
"el.trace.server": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"off",
|
||||
"messages",
|
||||
"verbose"
|
||||
],
|
||||
"default": "off",
|
||||
"description": "Trace LSP messages between VSCode and el-lsp (visible in Output \u2192 El LSP Trace)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "el.restartServer",
|
||||
"title": "El: Restart Language Server"
|
||||
}
|
||||
]
|
||||
},
|
||||
"main": "./extension.js",
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.75.0",
|
||||
"@vscode/vsce": "^2.22.0"
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "echo 'no transpile step required'",
|
||||
"package": "vsce package",
|
||||
"install-dev": "npm install && code --install-extension el-language-1.0.0.vsix 2>/dev/null || true"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "El",
|
||||
"scopeName": "source.el",
|
||||
"fileTypes": ["el"],
|
||||
"patterns": [
|
||||
{ "include": "#comments" },
|
||||
{ "include": "#strings" },
|
||||
{ "include": "#function-definition" },
|
||||
{ "include": "#builtin-functions" },
|
||||
{ "include": "#keywords-control" },
|
||||
{ "include": "#keywords-declaration" },
|
||||
{ "include": "#keywords-other" },
|
||||
{ "include": "#types" },
|
||||
{ "include": "#type-annotations" },
|
||||
{ "include": "#constants" },
|
||||
{ "include": "#numbers" },
|
||||
{ "include": "#operators" },
|
||||
{ "include": "#range-operator" },
|
||||
{ "include": "#function-call" },
|
||||
{ "include": "#identifiers" }
|
||||
],
|
||||
"repository": {
|
||||
|
||||
"comments": {
|
||||
"name": "comment.line.double-slash.el",
|
||||
"match": "//.*$"
|
||||
},
|
||||
|
||||
"strings": {
|
||||
"name": "string.quoted.double.el",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.el",
|
||||
"match": "\\\\[nrt\\\\\"'0]"
|
||||
},
|
||||
{
|
||||
"name": "constant.character.escape.hex.el",
|
||||
"match": "\\\\x[0-9a-fA-F]{2}"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"function-definition": {
|
||||
"comment": "fn keyword + function name — name gets entity.name.function",
|
||||
"match": "\\b(fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()",
|
||||
"captures": {
|
||||
"1": { "name": "keyword.declaration.function.el" },
|
||||
"2": { "name": "entity.name.function.el" }
|
||||
}
|
||||
},
|
||||
|
||||
"builtin-functions": {
|
||||
"comment": "Well-known El builtins get support.function scope for distinct colour",
|
||||
"name": "support.function.builtin.el",
|
||||
"match": "\\b(println|print|readline|str_eq|str_len|str_concat|str_slice|str_contains|str_starts_with|str_ends_with|str_replace|str_to_upper|str_to_lower|str_trim|str_lstrip|str_rstrip|str_index_of|str_last_index_of|str_split|str_split_lines|str_split_n|str_join|str_char_at|str_char_code|str_pad_left|str_pad_right|str_repeat|str_reverse|str_strip_prefix|str_strip_suffix|str_strip_chars|str_count|str_count_lines|str_find_chars|str_upper|str_lower|int_to_str|str_to_int|float_to_str|str_to_float|int_to_float|float_to_int|format_float|decimal_round|bool_to_str|el_abs|el_max|el_min|math_sqrt|math_log|math_ln|math_sin|math_cos|math_pi|el_list_empty|el_list_append|el_list_len|el_list_get|el_list_clone|list_push|list_push_front|list_join|list_range|el_map_get|el_map_set|el_get_field|state_set|state_get|state_del|state_keys|json_get|json_get_string|json_get_int|json_get_float|json_get_bool|json_get_raw|json_set|json_parse|json_stringify|json_array_len|json_array_get|json_array_get_string|fs_read|fs_write|fs_list|fs_exists|fs_mkdir|http_get|http_post|http_post_json|http_get_with_headers|http_post_with_headers|http_serve|http_serve_v2|http_response|url_encode|url_decode|time_now|time_now_utc|sleep_secs|sleep_ms|time_format|time_add|time_diff|now|unix_seconds|unix_millis|uuid_new|uuid_v4|env|args|exit_program|exec_command|exec_capture|sha256_hex|hmac_sha256_hex|base64_encode|base64_decode|base64url_encode|base64url_decode|llm_call|llm_call_system|llm_call_agentic|llm_vision|llm_models|llm_register_tool|engram_node|engram_get_node|engram_strengthen|engram_forget|engram_search|engram_connect|engram_activate|engram_save|engram_load|engram_node_count|engram_edge_count|engram_neighbors|engram_search_json|engram_stats_json|dharma_connect|dharma_send|dharma_activate|dharma_emit|dharma_field|dharma_peers|native_list_empty|native_list_append|native_list_len|native_list_get|native_list_clone|native_string_chars|native_int_to_str)\\b(?=\\s*\\()"
|
||||
},
|
||||
|
||||
"keywords-control": {
|
||||
"name": "keyword.control.el",
|
||||
"match": "\\b(if|else|while|for|in|return|match|break|continue)\\b"
|
||||
},
|
||||
|
||||
"keywords-declaration": {
|
||||
"name": "keyword.declaration.el",
|
||||
"match": "\\b(fn|let|type|enum|import|from|as|extern)\\b"
|
||||
},
|
||||
|
||||
"keywords-other": {
|
||||
"name": "keyword.other.el",
|
||||
"match": "\\b(cgi|vessel|activate|where|sealed|with|test|seed|assert|protocol|impl|retry|times|fallback|reason|parallel|trace|requires|deploy|to|via|target|manager|engine|accessor)\\b"
|
||||
},
|
||||
|
||||
"types": {
|
||||
"comment": "Built-in El primitive types",
|
||||
"name": "support.type.primitive.el",
|
||||
"match": "\\b(String|Int|Float|Bool|Void|Any|Instant|Duration|Map|List)\\b"
|
||||
},
|
||||
|
||||
"type-annotations": {
|
||||
"comment": "Highlight type annotations after : in let and fn params",
|
||||
"patterns": [
|
||||
{
|
||||
"match": ":\\s*([A-Z][a-zA-Z0-9_<>, ]*)(?=\\s*[,)={\\]]|\\s*->)",
|
||||
"captures": {
|
||||
"1": { "name": "support.type.el" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "->\\s*([A-Z][a-zA-Z0-9_<>, ]*)",
|
||||
"captures": {
|
||||
"0": { "name": "keyword.operator.arrow.el" },
|
||||
"1": { "name": "support.type.return.el" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"constants": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.language.boolean.el",
|
||||
"match": "\\b(true|false)\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.language.null.el",
|
||||
"match": "\\bnull\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"numbers": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.numeric.float.el",
|
||||
"match": "-?\\b[0-9]+\\.[0-9]+\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.numeric.integer.el",
|
||||
"match": "-?\\b[0-9]+\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"range-operator": {
|
||||
"name": "keyword.operator.range.el",
|
||||
"match": "\\.\\."
|
||||
},
|
||||
|
||||
"operators": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.operator.comparison.el",
|
||||
"match": "(==|!=|<=|>=)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.comparison.el",
|
||||
"match": "(?<![<>-])([<>])(?![>=])"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.logical.el",
|
||||
"match": "(&&|\\|\\||!(?!=))"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.assignment.el",
|
||||
"match": "(?<![=!<>])=(?!=)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.arithmetic.el",
|
||||
"match": "[+\\-*/%]"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.arrow.el",
|
||||
"match": "->"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"function-call": {
|
||||
"comment": "Function calls: identifier immediately followed by (",
|
||||
"match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()",
|
||||
"captures": {
|
||||
"1": { "name": "entity.name.function.call.el" }
|
||||
}
|
||||
},
|
||||
|
||||
"identifiers": {
|
||||
"comment": "Catch-all for remaining identifiers",
|
||||
"name": "variable.other.el",
|
||||
"match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b"
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
# new-project.sh — scaffold a new El project.
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/new-project.sh <project-name>
|
||||
#
|
||||
# Creates:
|
||||
# <project-name>/
|
||||
# src/main.el — hello world entry point
|
||||
# build.sh — build script using elc + el_runtime.c
|
||||
# README.md — minimal project docs
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <project-name>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NAME="$1"
|
||||
|
||||
if [[ -e "${NAME}" ]]; then
|
||||
echo "Error: '${NAME}' already exists." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Discover elc — prefer PATH, fall back to the local foundation/el tree
|
||||
ELC="elc"
|
||||
if ! command -v elc >/dev/null 2>&1; then
|
||||
# Try a sibling or parent path convention
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOCAL_ELC="${SCRIPT_DIR}/../dist/platform/elc"
|
||||
if [[ -x "${LOCAL_ELC}" ]]; then
|
||||
ELC="$(cd "$(dirname "${LOCAL_ELC}")" && pwd)/$(basename "${LOCAL_ELC}")"
|
||||
else
|
||||
echo "Warning: elc not found in PATH or at ${LOCAL_ELC}" >&2
|
||||
echo " The generated build.sh may need manual adjustment." >&2
|
||||
ELC="elc"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Discover el_runtime.c
|
||||
EL_RUNTIME=""
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOCAL_RUNTIME="${SCRIPT_DIR}/../el-compiler/runtime/el_runtime.c"
|
||||
if [[ -f "${LOCAL_RUNTIME}" ]]; then
|
||||
EL_RUNTIME="$(cd "$(dirname "${LOCAL_RUNTIME}")" && pwd)/$(basename "${LOCAL_RUNTIME}")"
|
||||
EL_INCLUDE="$(dirname "${EL_RUNTIME}")"
|
||||
else
|
||||
# Check installed location
|
||||
if [[ -f "/usr/local/el/lib/libel.a" ]]; then
|
||||
EL_RUNTIME_LINK="-L/usr/local/el/lib -lel"
|
||||
EL_INCLUDE="/usr/local/el/include"
|
||||
fi
|
||||
EL_RUNTIME="${EL_RUNTIME_LINK:-}"
|
||||
fi
|
||||
|
||||
echo "==> Scaffolding El project: ${NAME}"
|
||||
|
||||
mkdir -p "${NAME}/src" "${NAME}/dist"
|
||||
|
||||
# src/main.el
|
||||
cat > "${NAME}/src/main.el" <<'ELEOF'
|
||||
// main.el — entry point for this El program.
|
||||
|
||||
fn run() -> String {
|
||||
println("hello from El!")
|
||||
return ""
|
||||
}
|
||||
|
||||
run()
|
||||
ELEOF
|
||||
|
||||
# build.sh — adapts to whether we found elc/runtime locally or installed
|
||||
if [[ -n "${EL_RUNTIME}" && -f "${EL_RUNTIME}" ]]; then
|
||||
# Local source tree runtime
|
||||
EL_INCLUDE_DIR="$(dirname "${EL_RUNTIME}")"
|
||||
cat > "${NAME}/build.sh" <<BUILDEOF
|
||||
#!/usr/bin/env bash
|
||||
# build.sh — build ${NAME}
|
||||
set -euo pipefail
|
||||
|
||||
ELC="${ELC}"
|
||||
EL_RUNTIME="${EL_RUNTIME}"
|
||||
EL_INCLUDE="${EL_INCLUDE_DIR}"
|
||||
|
||||
mkdir -p dist
|
||||
|
||||
echo "==> Compiling El -> C"
|
||||
"\${ELC}" src/main.el > dist/main.c
|
||||
|
||||
echo "==> Compiling C -> binary"
|
||||
cc -std=c11 -O2 -I"\${EL_INCLUDE}" -o dist/${NAME} dist/main.c "\${EL_RUNTIME}" -lcurl -lpthread
|
||||
|
||||
echo "==> Built: dist/${NAME}"
|
||||
BUILDEOF
|
||||
else
|
||||
# Installed framework
|
||||
cat > "${NAME}/build.sh" <<BUILDEOF
|
||||
#!/usr/bin/env bash
|
||||
# build.sh — build ${NAME}
|
||||
# Requires El installed via tools/install.sh (elc in PATH, /usr/local/el/lib/libel.a)
|
||||
set -euo pipefail
|
||||
|
||||
ELC="\${ELC:-elc}"
|
||||
EL_PREFIX="\${EL_HOME:-/usr/local/el}"
|
||||
|
||||
mkdir -p dist
|
||||
|
||||
echo "==> Compiling El -> C"
|
||||
"\${ELC}" src/main.el > dist/main.c
|
||||
|
||||
echo "==> Compiling C -> binary"
|
||||
cc -std=c11 -O2 -I"\${EL_PREFIX}/include" -o dist/${NAME} dist/main.c -L"\${EL_PREFIX}/lib" -lel -lcurl -lpthread
|
||||
|
||||
echo "==> Built: dist/${NAME}"
|
||||
BUILDEOF
|
||||
fi
|
||||
|
||||
chmod +x "${NAME}/build.sh"
|
||||
|
||||
# README.md
|
||||
cat > "${NAME}/README.md" <<READMEEOF
|
||||
# ${NAME}
|
||||
|
||||
An El language project.
|
||||
|
||||
## Build
|
||||
|
||||
\`\`\`bash
|
||||
./build.sh
|
||||
\`\`\`
|
||||
|
||||
## Run
|
||||
|
||||
\`\`\`bash
|
||||
./dist/${NAME}
|
||||
\`\`\`
|
||||
|
||||
## Structure
|
||||
|
||||
\`\`\`
|
||||
src/main.el — entry point
|
||||
build.sh — build script
|
||||
dist/ — compiled output (gitignored)
|
||||
\`\`\`
|
||||
READMEEOF
|
||||
|
||||
# .gitignore
|
||||
cat > "${NAME}/.gitignore" <<'IGNEOF'
|
||||
dist/
|
||||
*.c
|
||||
IGNEOF
|
||||
|
||||
echo
|
||||
echo "==> Created ${NAME}/"
|
||||
echo " src/main.el"
|
||||
echo " build.sh"
|
||||
echo " README.md"
|
||||
echo " .gitignore"
|
||||
echo
|
||||
echo "To build:"
|
||||
echo " cd ${NAME} && ./build.sh"
|
||||
echo
|
||||
Reference in New Issue
Block a user