Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e858eab300 | |||
| aa7d97d5ba | |||
| 7040830470 | |||
| 3a513aaa5a | |||
| beb2a8c5bd | |||
| e23319fe0b | |||
| 01fee9396a | |||
| 7b60d94b8a | |||
| 21694b79d2 | |||
| 422442b14e | |||
| 437ba0a4dd | |||
| 7376349124 | |||
| 0f1da43a97 | |||
| a54b2bebf9 | |||
| 1ed2dc3c11 |
@@ -1,4 +1,4 @@
|
|||||||
name: El SDK CI — dev
|
name: El CI — dev
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -49,6 +49,7 @@ jobs:
|
|||||||
echo "gen3 (self-hosted) elc built"
|
echo "gen3 (self-hosted) elc built"
|
||||||
dist/platform/elc --version || true
|
dist/platform/elc --version || true
|
||||||
|
|
||||||
|
# Run all four test suites — all must pass
|
||||||
- name: Run tests — text
|
- name: Run tests — text
|
||||||
run: |
|
run: |
|
||||||
ELC="$(pwd)/dist/platform/elc" \
|
ELC="$(pwd)/dist/platform/elc" \
|
||||||
@@ -73,7 +74,8 @@ jobs:
|
|||||||
EL_HOME="$(pwd)" \
|
EL_HOME="$(pwd)" \
|
||||||
bash tests/html_sanitizer/run.sh
|
bash tests/html_sanitizer/run.sh
|
||||||
|
|
||||||
- name: Publish El SDK to Artifact Registry (dev)
|
# Publish artifact to GCP Artifact Registry (dev)
|
||||||
|
- name: Publish elc to Artifact Registry (dev)
|
||||||
env:
|
env:
|
||||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||||
run: |
|
run: |
|
||||||
@@ -86,7 +88,6 @@ jobs:
|
|||||||
gcloud config set project neuron-785695
|
gcloud config set project neuron-785695
|
||||||
|
|
||||||
VERSION="${GITEA_SHA:0:8}"
|
VERSION="${GITEA_SHA:0:8}"
|
||||||
|
|
||||||
gcloud artifacts generic upload \
|
gcloud artifacts generic upload \
|
||||||
--repository=foundation-dev \
|
--repository=foundation-dev \
|
||||||
--location=us-central1 \
|
--location=us-central1 \
|
||||||
@@ -95,21 +96,6 @@ jobs:
|
|||||||
--version="${VERSION}" \
|
--version="${VERSION}" \
|
||||||
--source=dist/platform/elc
|
--source=dist/platform/elc
|
||||||
|
|
||||||
gcloud artifacts generic upload \
|
# Also tag as latest-dev
|
||||||
--repository=foundation-dev \
|
echo "Published elc version=${VERSION} to foundation-dev/el/elc"
|
||||||
--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
|
rm -f /tmp/gcp-key.json
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: El SDK CI — stage
|
name: El CI — stage
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -16,16 +16,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
@@ -59,6 +49,7 @@ jobs:
|
|||||||
echo "gen3 (self-hosted) elc built"
|
echo "gen3 (self-hosted) elc built"
|
||||||
dist/platform/elc --version || true
|
dist/platform/elc --version || true
|
||||||
|
|
||||||
|
# Run all four test suites — all must pass
|
||||||
- name: Run tests — text
|
- name: Run tests — text
|
||||||
run: |
|
run: |
|
||||||
ELC="$(pwd)/dist/platform/elc" \
|
ELC="$(pwd)/dist/platform/elc" \
|
||||||
@@ -83,7 +74,8 @@ jobs:
|
|||||||
EL_HOME="$(pwd)" \
|
EL_HOME="$(pwd)" \
|
||||||
bash tests/html_sanitizer/run.sh
|
bash tests/html_sanitizer/run.sh
|
||||||
|
|
||||||
- name: Publish El SDK to Artifact Registry (stage)
|
# Publish artifact to GCP Artifact Registry (stage)
|
||||||
|
- name: Publish elc to Artifact Registry (stage)
|
||||||
env:
|
env:
|
||||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||||
run: |
|
run: |
|
||||||
@@ -96,7 +88,6 @@ jobs:
|
|||||||
gcloud config set project neuron-785695
|
gcloud config set project neuron-785695
|
||||||
|
|
||||||
VERSION="${GITEA_SHA:0:8}"
|
VERSION="${GITEA_SHA:0:8}"
|
||||||
|
|
||||||
gcloud artifacts generic upload \
|
gcloud artifacts generic upload \
|
||||||
--repository=foundation-stage \
|
--repository=foundation-stage \
|
||||||
--location=us-central1 \
|
--location=us-central1 \
|
||||||
@@ -105,21 +96,5 @@ jobs:
|
|||||||
--version="${VERSION}" \
|
--version="${VERSION}" \
|
||||||
--source=dist/platform/elc
|
--source=dist/platform/elc
|
||||||
|
|
||||||
gcloud artifacts generic upload \
|
echo "Published elc version=${VERSION} to foundation-stage/el/elc"
|
||||||
--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
|
rm -f /tmp/gcp-key.json
|
||||||
|
|||||||
@@ -13,16 +13,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
@@ -138,8 +128,9 @@ jobs:
|
|||||||
|
|
||||||
echo "Release published successfully"
|
echo "Release published successfully"
|
||||||
|
|
||||||
# Publish artifacts to GCP Artifact Registry (prod)
|
# Dispatch el-sdk-updated event to downstream repos
|
||||||
- name: Publish El SDK to Artifact Registry (prod)
|
# Publish artifact to GCP Artifact Registry (prod)
|
||||||
|
- name: Publish elc to Artifact Registry (prod)
|
||||||
env:
|
env:
|
||||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||||
run: |
|
run: |
|
||||||
@@ -152,7 +143,6 @@ jobs:
|
|||||||
gcloud config set project neuron-785695
|
gcloud config set project neuron-785695
|
||||||
|
|
||||||
VERSION="${GITEA_SHA:0:8}"
|
VERSION="${GITEA_SHA:0:8}"
|
||||||
|
|
||||||
gcloud artifacts generic upload \
|
gcloud artifacts generic upload \
|
||||||
--repository=foundation-prod \
|
--repository=foundation-prod \
|
||||||
--location=us-central1 \
|
--location=us-central1 \
|
||||||
@@ -161,26 +151,9 @@ jobs:
|
|||||||
--version="${VERSION}" \
|
--version="${VERSION}" \
|
||||||
--source=dist/platform/elc
|
--source=dist/platform/elc
|
||||||
|
|
||||||
gcloud artifacts generic upload \
|
echo "Published elc version=${VERSION} to foundation-prod/el/elc"
|
||||||
--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
|
rm -f /tmp/gcp-key.json
|
||||||
|
|
||||||
# Dispatch el-sdk-updated event to downstream repos
|
|
||||||
- name: Dispatch to foundation/engram
|
- name: Dispatch to foundation/engram
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
@@ -216,75 +189,3 @@ jobs:
|
|||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
echo "Dispatched el-sdk-updated to neuron-technologies/forge"
|
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"
|
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
# 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
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+827
-176
File diff suppressed because it is too large
Load Diff
@@ -155,37 +155,34 @@ el_val_t readline(void) {
|
|||||||
return el_wrap_str(el_strdup(buf));
|
return el_wrap_str(el_strdup(buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* __read_n — read exactly n bytes from stdin.
|
/* ── stdout redirect helpers ─────────────────────────────────────────────── *
|
||||||
* Allocates a buffer of size n+1, calls fread(buf, 1, n, stdin) to read
|
* Used by elc post-processing (--minify, --obfuscate): capture codegen *
|
||||||
* exactly n raw bytes (including \r, \n, NUL, etc.), null-terminates, and
|
* output into a temp file, then pass it to the external tool. */
|
||||||
* returns the buffer as an El String. Returns "" on EOF or I/O error.
|
|
||||||
*
|
static int _stdout_saved_fd = -1;
|
||||||
* 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
|
/* stdout_to_file(path) — redirect stdout to <path>. Returns 1 on success. */
|
||||||
* it stops at the first \n and LSP JSON bodies are not newline-terminated. */
|
el_val_t stdout_to_file(el_val_t pathv) {
|
||||||
el_val_t __read_n(el_val_t nv) {
|
const char* path = EL_CSTR(pathv);
|
||||||
int64_t n = EL_INT(nv);
|
if (!path || !*path) return (el_val_t)(int64_t)0;
|
||||||
if (n <= 0) return el_wrap_str(el_strdup(""));
|
fflush(stdout);
|
||||||
char* buf = malloc((size_t)n + 1);
|
_stdout_saved_fd = dup(STDOUT_FILENO);
|
||||||
if (!buf) { fputs("el_runtime: __read_n: out of memory\n", stderr); return el_wrap_str(el_strdup("")); }
|
if (_stdout_saved_fd < 0) return (el_val_t)(int64_t)0;
|
||||||
size_t got = fread(buf, 1, (size_t)n, stdin);
|
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
buf[got] = '\0';
|
if (fd < 0) { close(_stdout_saved_fd); _stdout_saved_fd = -1; return (el_val_t)(int64_t)0; }
|
||||||
if (got == 0) { free(buf); return el_wrap_str(el_strdup("")); }
|
dup2(fd, STDOUT_FILENO);
|
||||||
/* Track in arena so the allocation is freed when the request ends. */
|
close(fd);
|
||||||
el_arena_track(buf);
|
return (el_val_t)(int64_t)1;
|
||||||
return el_wrap_str(buf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* __print_raw — write a string to stdout without any modification.
|
/* stdout_restore() — restore stdout from the saved fd. Returns 1 on success. */
|
||||||
* Unlike println/print (which call puts/fputs and may add newlines or flush
|
el_val_t stdout_restore(void) {
|
||||||
* in platform-specific ways), this uses fwrite with the exact byte count so
|
if (_stdout_saved_fd < 0) return (el_val_t)(int64_t)0;
|
||||||
* 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);
|
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 ─────────────────────────────────────────────────────── */
|
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||||
@@ -10277,650 +10274,3 @@ el_val_t emit_event(el_val_t name_v, el_val_t duration_ms_v) {
|
|||||||
return trace_span_end(h);
|
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,6 +79,8 @@ extern "C" {
|
|||||||
void println(el_val_t s);
|
void println(el_val_t s);
|
||||||
void print(el_val_t s);
|
void print(el_val_t s);
|
||||||
el_val_t readline(void);
|
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 ─────────────────────────────────────────────────────── */
|
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -176,11 +178,6 @@ void http_set_handler_v2(el_val_t name);
|
|||||||
* auto-content-type contract for legacy handlers that return plain bodies. */
|
* 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);
|
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
|
/* 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
|
* 60000ms). Read lazily on first use, so setting the env var any time before
|
||||||
* the first http_* call is sufficient. */
|
* the first http_* call is sufficient. */
|
||||||
@@ -756,9 +753,6 @@ el_val_t trace_span_start(el_val_t name);
|
|||||||
el_val_t trace_span_end(el_val_t span_handle);
|
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 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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -522,6 +522,328 @@ function math_sin(f) { return Math.sin(f); }
|
|||||||
function math_cos(f) { return Math.cos(f); }
|
function math_cos(f) { return Math.cos(f); }
|
||||||
function math_pi() { return Math.PI; }
|
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 ───────────────────────────────────
|
// ── Stubs for not-yet-supported features ───────────────────────────────────
|
||||||
//
|
//
|
||||||
// These compile but throw when called. See spec/codegen-js.md §7.
|
// These compile but throw when called. See spec/codegen-js.md §7.
|
||||||
@@ -632,6 +954,31 @@ const __el = {
|
|||||||
// Float / math
|
// Float / math
|
||||||
float_to_str, int_to_float, float_to_int, format_float, decimal_round,
|
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,
|
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)
|
// CGI / DHARMA / Engram / LLM (stubs)
|
||||||
el_cgi_init,
|
el_cgi_init,
|
||||||
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
|
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
|
||||||
@@ -676,4 +1023,27 @@ export {
|
|||||||
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
|
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
|
||||||
engram_node, engram_search, engram_activate,
|
engram_node, engram_search, engram_activate,
|
||||||
llm_call, llm_call_system,
|
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
@@ -1,248 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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,7 +1673,6 @@ static void* http_worker_v2(void* arg) {
|
|||||||
HttpWorkerArg* a = (HttpWorkerArg*)arg;
|
HttpWorkerArg* a = (HttpWorkerArg*)arg;
|
||||||
int fd = a->fd;
|
int fd = a->fd;
|
||||||
free(a);
|
free(a);
|
||||||
int is_sse = 0;
|
|
||||||
char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL;
|
char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL;
|
||||||
if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) {
|
if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) {
|
||||||
http_handler4_fn h = http_lookup_active_v2();
|
http_handler4_fn h = http_lookup_active_v2();
|
||||||
@@ -1681,39 +1680,28 @@ static void* http_worker_v2(void* arg) {
|
|||||||
int head_only = (method && strcmp(method, "HEAD") == 0);
|
int head_only = (method && strcmp(method, "HEAD") == 0);
|
||||||
const char* dispatch_method = head_only ? "GET" : method;
|
const char* dispatch_method = head_only ? "GET" : method;
|
||||||
el_request_start(); /* begin per-request arena */
|
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) {
|
if (h) {
|
||||||
el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : "");
|
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));
|
el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body));
|
||||||
const char* rs = EL_CSTR(r);
|
const char* rs = EL_CSTR(r);
|
||||||
/* Detect SSE sentinel — handler took ownership of the fd. */
|
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
|
||||||
if (rs && strcmp(rs, "__sse__") == 0) {
|
response = malloc(rlen + 1);
|
||||||
is_sse = 1;
|
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
|
||||||
} else {
|
else if (response) { response[0] = '\0'; }
|
||||||
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);
|
el_release(hmap);
|
||||||
} else {
|
} else {
|
||||||
response = el_strdup_persist(
|
response = el_strdup_persist(
|
||||||
"el-runtime: no v2 http handler registered "
|
"el-runtime: no v2 http handler registered "
|
||||||
"(call http_set_handler_v2)");
|
"(call http_set_handler_v2)");
|
||||||
}
|
}
|
||||||
el_seed_set_http_conn_fd(-1); /* clear before arena teardown */
|
|
||||||
el_request_end(); /* free all intermediate strings */
|
el_request_end(); /* free all intermediate strings */
|
||||||
if (!is_sse) {
|
_tl_http_head_only = head_only;
|
||||||
_tl_http_head_only = head_only;
|
http_send_response(fd, response);
|
||||||
http_send_response(fd, response);
|
_tl_http_head_only = 0;
|
||||||
_tl_http_head_only = 0;
|
free(response);
|
||||||
free(response);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
free(method); free(path); free(body); free(hdr_block);
|
free(method); free(path); free(body); free(hdr_block);
|
||||||
/* SSE handlers close the fd themselves via __http_sse_close. */
|
close(fd);
|
||||||
if (!is_sse) close(fd);
|
|
||||||
pthread_mutex_lock(&_http_conn_mu);
|
pthread_mutex_lock(&_http_conn_mu);
|
||||||
_http_conn_active--;
|
_http_conn_active--;
|
||||||
pthread_cond_signal(&_http_conn_cv);
|
pthread_cond_signal(&_http_conn_cv);
|
||||||
|
|||||||
@@ -176,11 +176,6 @@ void http_set_handler_v2(el_val_t name);
|
|||||||
* auto-content-type contract for legacy handlers that return plain bodies. */
|
* 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);
|
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
|
/* 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
|
* 60000ms). Read lazily on first use, so setting the env var any time before
|
||||||
* the first http_* call is sufficient. */
|
* the first http_* call is sufficient. */
|
||||||
|
|||||||
+367
-48
@@ -86,6 +86,57 @@ fn js_binop(op: String) -> String {
|
|||||||
op
|
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) ────────────────────────────────────
|
// ── Int-name tracking (mirrors codegen.el) ────────────────────────────────────
|
||||||
|
|
||||||
fn js_is_int_name(name: String) -> Bool {
|
fn js_is_int_name(name: String) -> Bool {
|
||||||
@@ -377,20 +428,38 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
|
|||||||
|
|
||||||
if func_kind == "Ident" {
|
if func_kind == "Ident" {
|
||||||
let fn_name: String = func["name"]
|
let fn_name: String = func["name"]
|
||||||
return fn_name + "(" + args_c + ")"
|
let call_expr: String = fn_name + "(" + args_c + ")"
|
||||||
|
if js_is_async_builtin(fn_name) {
|
||||||
|
return "await " + call_expr
|
||||||
|
}
|
||||||
|
if js_is_async_fn(fn_name) {
|
||||||
|
return "await " + call_expr
|
||||||
|
}
|
||||||
|
return call_expr
|
||||||
}
|
}
|
||||||
|
|
||||||
if func_kind == "Field" {
|
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 obj = func["object"]
|
||||||
let field: String = func["field"]
|
let field: String = func["field"]
|
||||||
let obj_c: String = js_cg_expr(obj)
|
let obj_c: String = js_cg_expr(obj)
|
||||||
if arity > 0 {
|
// If the method is a known El runtime shortform, keep the El
|
||||||
return field + "(" + obj_c + ", " + args_c + ")"
|
// convention: `method(obj, args)`. This preserves backward
|
||||||
|
// compatibility with list.append(x), map.map_get(k), etc.
|
||||||
|
if js_is_el_method(field) {
|
||||||
|
if arity > 0 {
|
||||||
|
return field + "(" + obj_c + ", " + args_c + ")"
|
||||||
|
}
|
||||||
|
return field + "(" + obj_c + ")"
|
||||||
}
|
}
|
||||||
return field + "(" + obj_c + ")"
|
// Unknown method — emit as a native JS method call on the
|
||||||
|
// receiver. This handles Any-typed values (third-party library
|
||||||
|
// objects, DOM elements, Promises, etc.) without requiring
|
||||||
|
// native_js_call. Example: `client.auth.signInWithOtp(payload)`
|
||||||
|
// emits `client["auth"].signInWithOtp(args_c)`.
|
||||||
|
if arity > 0 {
|
||||||
|
return obj_c + "." + field + "(" + args_c + ")"
|
||||||
|
}
|
||||||
|
return obj_c + "." + field + "()"
|
||||||
}
|
}
|
||||||
|
|
||||||
let fn_c: String = js_cg_expr(func)
|
let fn_c: String = js_cg_expr(func)
|
||||||
@@ -398,22 +467,39 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if kind == "Field" {
|
if kind == "Field" {
|
||||||
// El's `obj.foo` becomes JS `obj["foo"]` — works on plain objects
|
// El's `obj.foo` becomes JS `obj["foo"]` — direct bracket access.
|
||||||
// (maps) and on JS objects with prototype. el_get_field is a
|
// This works for plain El map objects AND for real JS objects with
|
||||||
// runtime helper for callers that want EL_NULL on missing keys.
|
// prototype-inherited properties (DOM elements, third-party library
|
||||||
|
// objects, Promises, etc.). el_get_field used hasOwnProperty which
|
||||||
|
// silently returned null for inherited props, breaking e.g. client.auth.
|
||||||
|
//
|
||||||
|
// Nil-propagation: `obj?.foo` emits `(obj)?.["foo"] ?? null`.
|
||||||
let obj = expr["object"]
|
let obj = expr["object"]
|
||||||
let field: String = expr["field"]
|
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)
|
let obj_c: String = js_cg_expr(obj)
|
||||||
return "el_get_field(" + obj_c + ", " + js_str_lit(field) + ")"
|
return obj_c + "[" + js_str_lit(field) + "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
if kind == "Index" {
|
if kind == "Index" {
|
||||||
// Map vs list dispatch on the index expression kind, same as C.
|
// 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 obj = expr["object"]
|
||||||
let idx = expr["index"]
|
let idx = expr["index"]
|
||||||
let obj_c: String = js_cg_expr(obj)
|
let obj_c: String = js_cg_expr(obj)
|
||||||
let idx_c: String = js_cg_expr(idx)
|
let idx_c: String = js_cg_expr(idx)
|
||||||
let idx_kind: String = idx["expr"]
|
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") {
|
if str_eq(idx_kind, "Str") {
|
||||||
return "el_get_field(" + obj_c + ", " + idx_c + ")"
|
return "el_get_field(" + obj_c + ", " + idx_c + ")"
|
||||||
}
|
}
|
||||||
@@ -453,6 +539,12 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if kind == "Try" {
|
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"]
|
let inner = expr["inner"]
|
||||||
return js_cg_expr(inner)
|
return js_cg_expr(inner)
|
||||||
}
|
}
|
||||||
@@ -470,6 +562,13 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
|
|||||||
return js_cg_match(expr)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
"null"
|
"null"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,8 +627,16 @@ fn js_cg_match(expr: Map<String, Any>) -> String {
|
|||||||
if str_eq(v, "true") { let bv = "true" }
|
if str_eq(v, "true") { let bv = "true" }
|
||||||
let parts = native_list_append(parts, "if (" + subj_var + " === " + bv + ") return (" + body_c + "); ")
|
let parts = native_list_append(parts, "if (" + subj_var + " === " + bv + ") return (" + body_c + "); ")
|
||||||
} else {
|
} else {
|
||||||
// unknown pattern → wildcard
|
if str_eq(pkind, "Variant") {
|
||||||
let parts = native_list_append(parts, "return (" + body_c + "); ")
|
// Enum::Variant patterns — El enums compile to plain
|
||||||
|
// strings (the variant name) or ints. Match the subject
|
||||||
|
// against the variant name string.
|
||||||
|
let variant: String = pat["variant"]
|
||||||
|
let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", " + js_str_lit(variant) + ")) return (" + body_c + "); ")
|
||||||
|
} else {
|
||||||
|
// unknown pattern → wildcard
|
||||||
|
let parts = native_list_append(parts, "return (" + body_c + "); ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -541,6 +648,65 @@ fn js_cg_match(expr: Map<String, Any>) -> String {
|
|||||||
str_join(parts, "")
|
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 ───────────────────────────────────────────────────
|
// ── Variable scope tracking ───────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// El allows `let x = ...` to redeclare in the same scope. JS would throw
|
// El allows `let x = ...` to redeclare in the same scope. JS would throw
|
||||||
@@ -646,6 +812,27 @@ fn js_cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [St
|
|||||||
if kind == "TypeDef" { return declared }
|
if kind == "TypeDef" { return declared }
|
||||||
if kind == "EnumDef" { return declared }
|
if kind == "EnumDef" { return declared }
|
||||||
if kind == "Import" { return declared }
|
if kind == "Import" { 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" {
|
if kind == "CgiBlock" {
|
||||||
// CGI blocks compile to a no-op + warning comment in JS target.
|
// 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
|
// The runtime cgi identity is server-side; UI code is not a CGI
|
||||||
@@ -785,16 +972,30 @@ fn js_cg_fn(stmt: Map<String, Any>) -> Void {
|
|||||||
let params = stmt["params"]
|
let params = stmt["params"]
|
||||||
let body = stmt["body"]
|
let body = stmt["body"]
|
||||||
let ret_type: String = stmt["ret_type"]
|
let ret_type: String = stmt["ret_type"]
|
||||||
|
let decorator: String = stmt["decorator"]
|
||||||
let params_str: String = js_params_str(params)
|
let params_str: String = js_params_str(params)
|
||||||
js_build_int_names_for_params(params)
|
js_build_int_names_for_params(params)
|
||||||
|
|
||||||
// Special-case `fn main` — emit as a regular function and call it
|
// Detect @async decorator — emit `async function` and register the name
|
||||||
// at module bottom (after all top-level statements). This matches
|
// so call sites for this function get `await` prefixed automatically.
|
||||||
// the C backend's behavior where `fn main` is the entry point.
|
// When the decorator field is absent, el_get_field returns null; str_eq
|
||||||
if fn_name == "main" {
|
// handles null safely (returns false), so no special nil-check is needed.
|
||||||
js_emit_line("function main(" + params_str + ") {")
|
if str_eq(decorator, "async") {
|
||||||
|
js_register_async_fn(fn_name)
|
||||||
|
if fn_name == "main" {
|
||||||
|
js_emit_line("async function main(" + params_str + ") {")
|
||||||
|
} else {
|
||||||
|
js_emit_line("async function " + fn_name + "(" + params_str + ") {")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
js_emit_line("function " + fn_name + "(" + params_str + ") {")
|
// Special-case `fn main` — emit as a regular function and call it
|
||||||
|
// at module bottom (after all top-level statements). This matches
|
||||||
|
// the C backend's behavior where `fn main` is the entry point.
|
||||||
|
if fn_name == "main" {
|
||||||
|
js_emit_line("function main(" + params_str + ") {")
|
||||||
|
} else {
|
||||||
|
js_emit_line("function " + fn_name + "(" + params_str + ") {")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let decl = native_list_empty()
|
let decl = native_list_empty()
|
||||||
@@ -830,50 +1031,133 @@ fn js_is_top_level_decl(stmt: Map<String, Any>) -> Bool {
|
|||||||
if kind == "Import" { return true }
|
if kind == "Import" { return true }
|
||||||
if kind == "CgiBlock" { return true }
|
if kind == "CgiBlock" { return true }
|
||||||
if kind == "ServiceBlock" { return true }
|
if kind == "ServiceBlock" { return true }
|
||||||
|
if kind == "ExternFn" { return true }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
|
fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
|
||||||
|
codegen_js_inner(stmts, source, false, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn codegen_js_bundle(stmts: [Map<String, Any>], source: String, runtime_content: String) -> String {
|
||||||
|
codegen_js_inner(stmts, source, true, runtime_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool, runtime_content: String) -> String {
|
||||||
// Reset per-compile state.
|
// Reset per-compile state.
|
||||||
state_set("__js_int_names", "")
|
state_set("__js_int_names", "")
|
||||||
state_set("__js_match_counter", "")
|
state_set("__js_match_counter", "")
|
||||||
|
state_set("__js_async_fns", "")
|
||||||
|
state_set("__js_lambda_counter", "")
|
||||||
|
|
||||||
// Preamble: inline the runtime via a single import that side-effects
|
// Preamble: in bundle mode, inline the runtime and wrap in IIFE.
|
||||||
// globalThis. The runtime path is resolved relative to the generated
|
// In module mode, emit a single import that side-effects globalThis.
|
||||||
// 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")
|
js_emit_line("// Generated by elc --target=js")
|
||||||
js_emit_line("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js")
|
if bundle_mode {
|
||||||
js_emit_line("import \"./el_runtime.js\";")
|
js_emit_line("// Bundle mode: runtime inlined, no import statement needed.")
|
||||||
js_emit_line("const {")
|
js_emit_line("// Drop directly into a <script> tag.")
|
||||||
js_emit_line(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,")
|
js_emit_line(";(function() {")
|
||||||
js_emit_line(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,")
|
js_emit_line("\"use strict\";")
|
||||||
js_emit_line(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,")
|
// Inline the runtime content verbatim (already read from el_runtime.js).
|
||||||
js_emit_line(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,")
|
// Strip the final ES export lines -- they use `export` syntax which is
|
||||||
js_emit_line(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,")
|
// not valid inside an IIFE. The globalThis.__el assignment is what matters.
|
||||||
js_emit_line(" list_push, list_join, list_range,")
|
js_emit_line(js_strip_es_exports(runtime_content))
|
||||||
js_emit_line(" el_map_new, el_get_field, el_map_get, el_map_set,")
|
js_emit_line("")
|
||||||
js_emit_line(" http_get, http_post, http_post_json,")
|
} else {
|
||||||
js_emit_line(" fs_read, fs_write, fs_list,")
|
js_emit_line("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js")
|
||||||
js_emit_line(" json_parse, json_stringify, json_get, json_get_string, json_get_int,")
|
js_emit_line("import \"./el_runtime.js\";")
|
||||||
js_emit_line(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,")
|
}
|
||||||
js_emit_line(" el_retain, el_release,")
|
// In module mode: destructure all builtins off globalThis.__el so call
|
||||||
js_emit_line(" append, len, get, map_get, map_set,")
|
// sites stay flat (println(x) not el.println(x)).
|
||||||
js_emit_line(" native_list_get, native_list_len, native_list_append, native_list_empty,")
|
// In bundle mode: function declarations from the inlined runtime are
|
||||||
js_emit_line(" native_list_clone, native_string_chars, native_int_to_str,")
|
// already in scope within the IIFE -- no destructure needed.
|
||||||
js_emit_line(" args, state_set, state_get, state_del, state_keys, env,")
|
if !bundle_mode {
|
||||||
js_emit_line(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,")
|
js_emit_line("const {")
|
||||||
js_emit_line(" engram_node, engram_search, engram_activate,")
|
js_emit_line(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,")
|
||||||
js_emit_line(" llm_call, llm_call_system,")
|
js_emit_line(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,")
|
||||||
js_emit_line("} = globalThis.__el;")
|
js_emit_line(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,")
|
||||||
|
js_emit_line(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,")
|
||||||
|
js_emit_line(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,")
|
||||||
|
js_emit_line(" list_push, list_join, list_range,")
|
||||||
|
js_emit_line(" el_map_new, el_get_field, el_map_get, el_map_set,")
|
||||||
|
js_emit_line(" http_get, http_post, http_post_json,")
|
||||||
|
js_emit_line(" fs_read, fs_write, fs_list,")
|
||||||
|
js_emit_line(" json_parse, json_stringify, json_get, json_get_string, json_get_int,")
|
||||||
|
js_emit_line(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,")
|
||||||
|
js_emit_line(" el_retain, el_release,")
|
||||||
|
js_emit_line(" append, len, get, map_get, map_set,")
|
||||||
|
js_emit_line(" native_list_get, native_list_len, native_list_append, native_list_empty,")
|
||||||
|
js_emit_line(" native_list_clone, native_string_chars, native_int_to_str,")
|
||||||
|
js_emit_line(" args, state_set, state_get, state_del, state_keys, env,")
|
||||||
|
js_emit_line(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,")
|
||||||
|
js_emit_line(" engram_node, engram_search, engram_activate,")
|
||||||
|
js_emit_line(" llm_call, llm_call_system,")
|
||||||
|
js_emit_line(" dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,")
|
||||||
|
js_emit_line(" dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,")
|
||||||
|
js_emit_line(" dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,")
|
||||||
|
js_emit_line(" dom_append, dom_remove, dom_is_null,")
|
||||||
|
js_emit_line(" dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,")
|
||||||
|
js_emit_line(" dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,")
|
||||||
|
js_emit_line(" set_timeout, set_interval, clear_interval,")
|
||||||
|
js_emit_line(" local_storage_get, local_storage_set, local_storage_remove,")
|
||||||
|
js_emit_line(" window_location, window_redirect, window_on_load,")
|
||||||
|
js_emit_line(" console_log,")
|
||||||
|
js_emit_line(" window_set, window_get, native_js, native_js_call,")
|
||||||
|
js_emit_line(" promise_then, promise_catch, promise_resolve, promise_reject,")
|
||||||
|
js_emit_line(" object_assign, object_keys, object_values, json_deep_clone,")
|
||||||
|
js_emit_line(" array_from, type_of, instanceof_check,")
|
||||||
|
js_emit_line("} = globalThis.__el;")
|
||||||
|
js_emit_blank()
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL import pass: emit `import "url"` (module mode) or a comment
|
||||||
|
// (bundle mode) for any import whose path starts with http(s):// or
|
||||||
|
// doesn't end in .el (i.e., it's a JS/CSS/CDN import, not an El source
|
||||||
|
// import which was already inlined by resolve_imports).
|
||||||
|
let n: Int = native_list_len(stmts)
|
||||||
|
let i = 0
|
||||||
|
while i < n {
|
||||||
|
let stmt = native_list_get(stmts, i)
|
||||||
|
let sk: String = stmt["stmt"]
|
||||||
|
if str_eq(sk, "Import") {
|
||||||
|
let ipath: String = stmt["path"]
|
||||||
|
let is_url = str_starts_with(ipath, "http://")
|
||||||
|
let is_url = is_url || str_starts_with(ipath, "https://")
|
||||||
|
let is_js = !str_ends_with(ipath, ".el")
|
||||||
|
if is_url || is_js {
|
||||||
|
if bundle_mode {
|
||||||
|
js_emit_line("// external: " + ipath)
|
||||||
|
} else {
|
||||||
|
js_emit_line("import " + js_str_lit(ipath) + ";")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let i = i + 1
|
||||||
|
}
|
||||||
js_emit_blank()
|
js_emit_blank()
|
||||||
|
|
||||||
// Function definitions
|
// Pre-registration pass: scan all FnDefs for @async decorators so that
|
||||||
|
// forward calls to @async functions get `await` even if the callee is
|
||||||
|
// defined after the caller.
|
||||||
let n: Int = native_list_len(stmts)
|
let n: Int = native_list_len(stmts)
|
||||||
let i = 0
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function definitions
|
||||||
|
let i = 0
|
||||||
while i < n {
|
while i < n {
|
||||||
let stmt = native_list_get(stmts, i)
|
let stmt = native_list_get(stmts, i)
|
||||||
if js_is_fndef(stmt) {
|
if js_is_fndef(stmt) {
|
||||||
@@ -921,6 +1205,41 @@ fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
|
|||||||
js_emit_line("main();")
|
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
|
// 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")
|
||||||
|
}
|
||||||
|
|||||||
+25
-231
@@ -135,9 +135,8 @@ fn emit_blank() -> Void {
|
|||||||
fn binop_to_c(op: String) -> String {
|
fn binop_to_c(op: String) -> String {
|
||||||
if op == "Plus" { return "+" }
|
if op == "Plus" { return "+" }
|
||||||
if op == "Minus" { return "-" }
|
if op == "Minus" { return "-" }
|
||||||
if op == "Star" { return "*" }
|
if op == "Star" { return "*" }
|
||||||
if op == "Slash" { return "/" }
|
if op == "Slash" { return "/" }
|
||||||
if op == "Percent" { return "%" }
|
|
||||||
if op == "EqEq" { return "==" }
|
if op == "EqEq" { return "==" }
|
||||||
if op == "NotEq" { return "!=" }
|
if op == "NotEq" { return "!=" }
|
||||||
if op == "Lt" { return "<" }
|
if op == "Lt" { return "<" }
|
||||||
@@ -849,8 +848,15 @@ fn cg_match(expr: Map<String, Any>) -> String {
|
|||||||
}
|
}
|
||||||
let parts = native_list_append(parts, "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
let parts = native_list_append(parts, "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
||||||
} else {
|
} else {
|
||||||
// unknown pattern -> wildcard
|
if str_eq(pkind, "Variant") {
|
||||||
let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
// 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 + "; } ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -862,97 +868,6 @@ fn cg_match(expr: Map<String, Any>) -> String {
|
|||||||
str_join(parts, "")
|
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 -------------------------------------------------
|
// -- If-as-expression codegen -------------------------------------------------
|
||||||
//
|
//
|
||||||
// Lower `if cond { thenBody } else { elseBody }` used in expression position
|
// Lower `if cond { thenBody } else { elseBody }` used in expression position
|
||||||
@@ -1170,16 +1085,6 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
|||||||
return declared
|
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
|
// Bare reassignment: `name = expr`. Always emits a plain C assignment
|
||||||
// (no `el_val_t` prefix) - by construction the parser only produces
|
// (no `el_val_t` prefix) - by construction the parser only produces
|
||||||
// Assign for an existing identifier. If the name happens NOT to be in
|
// Assign for an existing identifier. If the name happens NOT to be in
|
||||||
@@ -1205,10 +1110,6 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
|||||||
cg_for_stmt(val, indent, declared)
|
cg_for_stmt(val, indent, declared)
|
||||||
return declared
|
return declared
|
||||||
}
|
}
|
||||||
if val_kind == "Match" {
|
|
||||||
cg_match_stmt(val, indent, declared)
|
|
||||||
return declared
|
|
||||||
}
|
|
||||||
let val_c: String = cg_expr(val)
|
let val_c: String = cg_expr(val)
|
||||||
emit_line(indent + val_c + ";")
|
emit_line(indent + val_c + ";")
|
||||||
return declared
|
return declared
|
||||||
@@ -1236,34 +1137,21 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
|||||||
return declared
|
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 == "FnDef" { return declared }
|
||||||
if kind == "TypeDef" { return declared }
|
if kind == "TypeDef" { return declared }
|
||||||
if kind == "EnumDef" { return declared }
|
if kind == "EnumDef" { return declared }
|
||||||
if kind == "Import" { return declared }
|
if kind == "Import" { return declared }
|
||||||
if kind == "ExternFn" { return declared }
|
if kind == "ExternFn" { return declared }
|
||||||
if kind == "CgiBlock" { return declared }
|
if kind == "CgiBlock" { 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
|
declared
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1888,12 +1776,6 @@ fn is_int_expr(expr: Map<String, Any>) -> Bool {
|
|||||||
}
|
}
|
||||||
return false
|
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
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -2049,9 +1931,6 @@ fn builtin_arity(name: String) -> Int {
|
|||||||
if str_eq(name, "println") { return 1 }
|
if str_eq(name, "println") { return 1 }
|
||||||
if str_eq(name, "print") { return 1 }
|
if str_eq(name, "print") { return 1 }
|
||||||
if str_eq(name, "readline") { return 0 }
|
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
|
// String
|
||||||
if str_eq(name, "el_str_concat") { return 2 }
|
if str_eq(name, "el_str_concat") { return 2 }
|
||||||
if str_eq(name, "str_eq") { return 2 }
|
if str_eq(name, "str_eq") { return 2 }
|
||||||
@@ -2134,84 +2013,6 @@ fn builtin_arity(name: String) -> Int {
|
|||||||
if str_eq(name, "http_post_form_auth") { return 3 }
|
if str_eq(name, "http_post_form_auth") { return 3 }
|
||||||
if str_eq(name, "http_serve") { return 2 }
|
if str_eq(name, "http_serve") { return 2 }
|
||||||
if str_eq(name, "http_set_handler") { return 1 }
|
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
|
// Filesystem
|
||||||
if str_eq(name, "fs_read") { return 1 }
|
if str_eq(name, "fs_read") { return 1 }
|
||||||
if str_eq(name, "fs_write") { return 2 }
|
if str_eq(name, "fs_write") { return 2 }
|
||||||
@@ -2264,6 +2065,11 @@ fn builtin_arity(name: String) -> Int {
|
|||||||
if str_eq(name, "bool_to_str") { return 1 }
|
if str_eq(name, "bool_to_str") { return 1 }
|
||||||
// Process
|
// Process
|
||||||
if str_eq(name, "exit_program") { return 1 }
|
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
|
// Subprocess execution
|
||||||
if str_eq(name, "exec_command") { return 1 }
|
if str_eq(name, "exec_command") { return 1 }
|
||||||
if str_eq(name, "exec_capture") { return 1 }
|
if str_eq(name, "exec_capture") { return 1 }
|
||||||
@@ -2331,18 +2137,6 @@ fn builtin_arity(name: String) -> Int {
|
|||||||
if str_eq(name, "get") { return 2 }
|
if str_eq(name, "get") { return 2 }
|
||||||
if str_eq(name, "map_get") { return 2 }
|
if str_eq(name, "map_get") { return 2 }
|
||||||
if str_eq(name, "map_set") { return 3 }
|
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.
|
// -1 sentinel: variadic / unknown / user-defined -> no check.
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|||||||
+251
-7
@@ -29,7 +29,7 @@ fn compile(source: String) -> String {
|
|||||||
codegen(stmts, source)
|
codegen(stmts, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
// compile_js — full pipeline (JS target): source string -> JS source string
|
// compile_js — full pipeline (JS target, module mode): source string -> JS source string
|
||||||
fn compile_js(source: String) -> String {
|
fn compile_js(source: String) -> String {
|
||||||
let tokens: [Map<String, Any>] = lex(source)
|
let tokens: [Map<String, Any>] = lex(source)
|
||||||
let stmts: [Map<String, Any>] = parse(tokens)
|
let stmts: [Map<String, Any>] = parse(tokens)
|
||||||
@@ -38,6 +38,20 @@ fn compile_js(source: String) -> String {
|
|||||||
codegen_js(stmts, source)
|
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_dispatch — pick a backend based on the requested target.
|
// compile_dispatch — pick a backend based on the requested target.
|
||||||
// tgt = "c" | "js"
|
// tgt = "c" | "js"
|
||||||
// (The parameter is named `tgt` because `target` is a reserved keyword
|
// (The parameter is named `tgt` because `target` is a reserved keyword
|
||||||
@@ -48,6 +62,12 @@ fn compile_dispatch(tgt: String, source: String) -> String {
|
|||||||
compile(source)
|
compile(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Detect a `--target=<lang>` flag in argv and return the target.
|
||||||
// Returns "c" if none specified or unrecognized.
|
// Returns "c" if none specified or unrecognized.
|
||||||
fn detect_target(argv: [String]) -> String {
|
fn detect_target(argv: [String]) -> String {
|
||||||
@@ -91,6 +111,126 @@ fn detect_emit_header(argv: [String]) -> Bool {
|
|||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Reconstruct an El type annotation string from a parsed type node.
|
||||||
fn type_node_to_el(t: Map<String, Any>) -> String {
|
fn type_node_to_el(t: Map<String, Any>) -> String {
|
||||||
let k: String = t["kind"]
|
let k: String = t["kind"]
|
||||||
@@ -249,12 +389,83 @@ fn resolve_imports(src_path: String) -> String {
|
|||||||
return str_join(prefix_chunks, "") + str_join(body_chunks, "")
|
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.
|
// main — CLI entry point.
|
||||||
//
|
//
|
||||||
// elc <source.el> # emit C to stdout
|
// elc <source.el> # emit C to stdout
|
||||||
// elc --target=js <source.el> # emit JS to stdout
|
// elc --target=js <source.el> # emit JS (module) to stdout
|
||||||
// elc --target=c <source.el> <out.c> # write C to file
|
// elc --target=js --bundle <source.el> # emit self-contained JS (IIFE) to stdout
|
||||||
// elc --target=js <source.el> <out.js> # write JS to file
|
// elc --target=js --bundle --minify <source.el> # emit minified IIFE to stdout
|
||||||
|
// elc --target=js --bundle --obfuscate <source.el> # emit minified+obfuscated IIFE to stdout
|
||||||
|
// elc --target=c <source.el> <out.c> # write C to file
|
||||||
|
// elc --target=js <source.el> <out.js> # write JS to file
|
||||||
|
// elc --target=js --bundle <source.el> <out.js> # write bundled JS to file
|
||||||
|
// elc --target=js --bundle --minify <source.el> <out.min.js> # write minified JS to file
|
||||||
fn main() -> Void {
|
fn main() -> Void {
|
||||||
let argv: [String] = args()
|
let argv: [String] = args()
|
||||||
// Use `tgt` not `target`: `target` is a reserved keyword in the lexer
|
// Use `tgt` not `target`: `target` is a reserved keyword in the lexer
|
||||||
@@ -262,12 +473,28 @@ fn main() -> Void {
|
|||||||
// because the function-name position has no token-class restriction.
|
// because the function-name position has no token-class restriction.
|
||||||
let tgt: String = detect_target(argv)
|
let tgt: String = detect_target(argv)
|
||||||
let do_emit_header: Bool = detect_emit_header(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)
|
||||||
|
// --obfuscate implies --minify: obfuscating unminified code is pointless.
|
||||||
|
if do_obfuscate {
|
||||||
|
let do_minify = true
|
||||||
|
}
|
||||||
let positional: [String] = strip_flags(argv)
|
let positional: [String] = strip_flags(argv)
|
||||||
let argc: Int = native_list_len(positional)
|
let argc: Int = native_list_len(positional)
|
||||||
if argc < 1 {
|
if argc < 1 {
|
||||||
println("el-compiler: usage: elc [--target=c|js] [--emit-header] <source.el> [<output>]")
|
println("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]")
|
||||||
exit(1)
|
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)
|
let src_path: String = native_list_get(positional, 0)
|
||||||
|
|
||||||
// When --emit-header is requested, parse the source file directly
|
// When --emit-header is requested, parse the source file directly
|
||||||
@@ -283,7 +510,24 @@ fn main() -> Void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let source: String = resolve_imports(src_path)
|
let source: String = resolve_imports(src_path)
|
||||||
let out: String = compile_dispatch(tgt, source)
|
|
||||||
|
// When post-processing (--minify or --obfuscate) is requested, redirect
|
||||||
|
// stdout to a temp file so codegen output can be captured and piped through
|
||||||
|
// the external tools. After codegen, restore stdout before emitting the
|
||||||
|
// final result.
|
||||||
|
if do_minify {
|
||||||
|
run_with_postprocess(tgt, source, src_path, do_bundle, do_obfuscate, argc, positional)
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard path (no post-processing).
|
||||||
|
let out: String = ""
|
||||||
|
if do_bundle {
|
||||||
|
let runtime_path: String = resolve_runtime_path(src_path)
|
||||||
|
let out = compile_dispatch_bundle(tgt, source, runtime_path)
|
||||||
|
} else {
|
||||||
|
let out = compile_dispatch(tgt, source)
|
||||||
|
}
|
||||||
if argc >= 2 {
|
if argc >= 2 {
|
||||||
let out_path: String = native_list_get(positional, 1)
|
let out_path: String = native_list_get(positional, 1)
|
||||||
let ok: Bool = fs_write(out_path, out)
|
let ok: Bool = fs_write(out_path, out)
|
||||||
|
|||||||
+31
-275
@@ -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.
|
// Tokenises an el source string into a list of token maps.
|
||||||
// Each token is a Map<String, Any> with keys:
|
// Each token is a Map<String, Any> with keys:
|
||||||
@@ -8,9 +8,9 @@
|
|||||||
// Entry point: fn lex(source: String) -> [Map<String, Any>]
|
// Entry point: fn lex(source: String) -> [Map<String, Any>]
|
||||||
//
|
//
|
||||||
// Uses native_string_chars to split the source into a chars list,
|
// 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 {
|
fn lex_is_digit(ch: String) -> Bool {
|
||||||
if ch == "0" { return true }
|
if ch == "0" { return true }
|
||||||
@@ -101,7 +101,7 @@ fn make_tok(kind: String, value: String) -> Map<String, Any> {
|
|||||||
{ "kind": kind, "value": value }
|
{ "kind": kind, "value": value }
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Keyword lookup ------------------------------------------------------------
|
// ── Keyword lookup ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn keyword_kind(word: String) -> String {
|
fn keyword_kind(word: String) -> String {
|
||||||
if word == "let" { return "Let" }
|
if word == "let" { return "Let" }
|
||||||
@@ -147,15 +147,15 @@ fn keyword_kind(word: String) -> String {
|
|||||||
if word == "accessor" { return "Accessor" }
|
if word == "accessor" { return "Accessor" }
|
||||||
if word == "vessel" { return "Vessel" }
|
if word == "vessel" { return "Vessel" }
|
||||||
if word == "extern" { return "Extern" }
|
if word == "extern" { return "Extern" }
|
||||||
if word == "break" { return "Break" }
|
if word == "try" { return "Try" }
|
||||||
if word == "continue" { return "Continue" }
|
if word == "catch" { return "Catch" }
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Scan helpers --------------------------------------------------------------
|
// ── Scan helpers ──────────────────────────────────────────────────────────────
|
||||||
// All scan helpers receive the chars list and total length.
|
// 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 }
|
// Returns { "text": ..., "pos": i }
|
||||||
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||||
let i = start
|
let i = start
|
||||||
@@ -177,7 +177,7 @@ fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
|||||||
{ "text": str_join(parts, ""), "pos": i }
|
{ "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> {
|
fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||||
let i = start
|
let i = start
|
||||||
let parts: [String] = native_list_empty()
|
let parts: [String] = native_list_empty()
|
||||||
@@ -198,14 +198,14 @@ fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
|||||||
{ "text": str_join(parts, ""), "pos": i }
|
{ "text": str_join(parts, ""), "pos": i }
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Code-bearing string detection + comment strip ----------------------------
|
// ── Code-bearing string detection + comment strip ────────────────────────────
|
||||||
// Inline JS/CSS literals embedded in El source (e.g. <script>-</script> blobs
|
// Inline JS/CSS literals embedded in El source (e.g. <script>…</script> blobs
|
||||||
// or stylesheet payloads inside string literals) carry their own line and
|
// or stylesheet payloads inside string literals) carry their own line and
|
||||||
// block comments. Those comments leak into the served HTML and reveal build
|
// 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
|
// 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.
|
// 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.
|
// 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 {
|
fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool {
|
||||||
@@ -247,7 +247,7 @@ fn looks_like_code(s: String) -> Bool {
|
|||||||
false
|
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
|
// (single, double, backtick) and never strips inside one. Backslash escapes
|
||||||
// inside JS strings consume the next char verbatim. URLs like https:// are
|
// inside JS strings consume the next char verbatim. URLs like https:// are
|
||||||
// preserved by checking the previous char before treating // as a line
|
// preserved by checking the previous char before treating // as a line
|
||||||
@@ -400,7 +400,7 @@ fn strip_code_comments(s: String) -> String {
|
|||||||
str_join(out_parts, "")
|
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 }
|
// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close }
|
||||||
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||||
let i = start
|
let i = start
|
||||||
@@ -460,239 +460,7 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
|||||||
{ "text": str_join(parts, ""), "pos": i }
|
{ "text": str_join(parts, ""), "pos": i }
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- String interpolation ------------------------------------------------------
|
// ── Main lexer ────────────────────────────────────────────────────────────────
|
||||||
//
|
|
||||||
// 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>] {
|
fn lex(source: String) -> [Map<String, Any>] {
|
||||||
let chars: [String] = native_string_chars(source)
|
let chars: [String] = native_string_chars(source)
|
||||||
@@ -737,16 +505,20 @@ fn lex(source: String) -> [Map<String, Any>] {
|
|||||||
let i = i + 1
|
let i = i + 1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// String literal (plain or interpolated with ${expr} syntax).
|
// String literal
|
||||||
// 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 == "\"" {
|
if ch == "\"" {
|
||||||
let interp_result = scan_interp_string(chars, i + 1, total)
|
let result = scan_string(chars, i + 1, total)
|
||||||
let interp_toks: [Map<String, Any>] = interp_result["tokens"]
|
let str_text: String = result["text"]
|
||||||
let new_pos: Int = interp_result["pos"]
|
let new_pos: Int = result["pos"]
|
||||||
let tokens = interp_tokens_append_all(tokens, interp_toks)
|
// 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 i = new_pos
|
let i = new_pos
|
||||||
} else {
|
} else {
|
||||||
// Number literal
|
// Number literal
|
||||||
@@ -926,24 +698,8 @@ fn lex(source: String) -> [Map<String, Any>] {
|
|||||||
let i = i + 1
|
let i = i + 1
|
||||||
} else {
|
} else {
|
||||||
if ch == "." {
|
if ch == "." {
|
||||||
// Check for ..= (inclusive range) before .. (exclusive range) before single .
|
let tokens = native_list_append(tokens, make_tok("Dot", "."))
|
||||||
let peek2_i = i + 2
|
let i = i + 1
|
||||||
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 {
|
} else {
|
||||||
if ch == ";" {
|
if ch == ";" {
|
||||||
let tokens = native_list_append(tokens, make_tok("Semicolon", ";"))
|
let tokens = native_list_append(tokens, make_tok("Semicolon", ";"))
|
||||||
@@ -957,7 +713,7 @@ fn lex(source: String) -> [Map<String, Any>] {
|
|||||||
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
|
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
|
||||||
let i = i + 1
|
let i = i + 1
|
||||||
} else {
|
} else {
|
||||||
// unknown char - skip
|
// unknown char — skip
|
||||||
let i = i + 1
|
let i = i + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-60
@@ -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
|
// 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>.
|
// 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>]
|
// 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> {
|
fn tok_at(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||||
native_list_get(tokens, pos)
|
native_list_get(tokens, pos)
|
||||||
@@ -36,13 +36,13 @@ fn expect(tokens: [Map<String, Any>], pos: Int, kind: String) -> Int {
|
|||||||
pos + 1
|
pos + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Result helpers ------------------------------------------------------------
|
// ── Result helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn make_result(node: Map<String, Any>, pos: Int) -> Map<String, Any> {
|
fn make_result(node: Map<String, Any>, pos: Int) -> Map<String, Any> {
|
||||||
{ "node": node, "pos": pos }
|
{ "node": node, "pos": pos }
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Type annotation parser ----------------------------------------------------
|
// ── Type annotation parser ────────────────────────────────────────────────────
|
||||||
// Skips over a type annotation, returning the new position.
|
// Skips over a type annotation, returning the new position.
|
||||||
// Types can be: Ident, [Type], Map<K,V>, Type?, Type<Type,...>
|
// Types can be: Ident, [Type], Map<K,V>, Type?, Type<Type,...>
|
||||||
|
|
||||||
@@ -100,8 +100,8 @@ fn skip_type(tokens: [Map<String, Any>], pos: Int) -> Int {
|
|||||||
pos + 1
|
pos + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Parameter list ------------------------------------------------------------
|
// ── Parameter list ────────────────────────────────────────────────────────────
|
||||||
// Parses (name: Type, name: Type, ...) - returns { "params": [...], "pos": ... }
|
// Parses (name: Type, name: Type, ...) — returns { "params": [...], "pos": ... }
|
||||||
|
|
||||||
fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||||
let p = expect(tokens, pos, "LParen")
|
let p = expect(tokens, pos, "LParen")
|
||||||
@@ -140,7 +140,7 @@ fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
{ "params": params, "pos": p }
|
{ "params": params, "pos": p }
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Expression parsing --------------------------------------------------------
|
// ── Expression parsing ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||||
let k = tok_kind(tokens, pos)
|
let k = tok_kind(tokens, pos)
|
||||||
@@ -212,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
|
// Suppression: when parse_if / parse_while / parse_for / parse_match
|
||||||
// are parsing a head expression, they set __no_block_expr=1 so a stray
|
// 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
|
// following block. Without this, `if a || b { ... }` could mis-parse
|
||||||
// (the `||` recursion lands at `{` and tries to read the if-body as a
|
// (the `||` recursion lands at `{` and tries to read the if-body as a
|
||||||
// Map, then loops forever when keys don't match `Str: expr`).
|
// Map, then loops forever when keys don't match `Str: expr`).
|
||||||
if k == "LBrace" {
|
if k == "LBrace" {
|
||||||
let no_block: String = state_get("__no_block_expr")
|
let no_block: String = state_get("__no_block_expr")
|
||||||
if str_eq(no_block, "1") {
|
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.
|
// as the start of the block they're expecting.
|
||||||
return make_result({ "expr": "Nil" }, pos)
|
return make_result({ "expr": "Nil" }, pos)
|
||||||
}
|
}
|
||||||
@@ -279,6 +279,30 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
return r
|
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
|
// Unary not
|
||||||
if k == "Not" {
|
if k == "Not" {
|
||||||
let r = parse_primary(tokens, pos + 1)
|
let r = parse_primary(tokens, pos + 1)
|
||||||
@@ -300,7 +324,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
|
// token kinds for the deploy/retry DSLs, but they're also valid as
|
||||||
// parameter names and local variables. When one of these appears in
|
// parameter names and local variables. When one of these appears in
|
||||||
// expression position (where only an Ident makes sense), treat it as
|
// 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.
|
// parameter named `target` compile to EL_NULL.
|
||||||
if k == "Target" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
if k == "Target" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
||||||
if k == "To" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
if k == "To" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
||||||
@@ -408,6 +432,13 @@ fn parse_pattern(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
if v == "_" {
|
if v == "_" {
|
||||||
return make_result({ "pattern": "Wildcard" }, pos + 1)
|
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)
|
return make_result({ "pattern": "Binding", "name": v }, pos + 1)
|
||||||
}
|
}
|
||||||
if k == "Int" {
|
if k == "Int" {
|
||||||
@@ -472,9 +503,9 @@ fn parse_block(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
{ "stmts": stmts, "pos": p }
|
{ "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
|
// Used by parse_postfix to detect `30.seconds`-shape time literals before
|
||||||
// falling back to the generic `obj.field` field-access lowering. Singular
|
// falling back to the generic `obj.field` field-access lowering. Singular
|
||||||
// and plural forms map to the same nanosecond multiplier; codegen does the
|
// and plural forms map to the same nanosecond multiplier; codegen does the
|
||||||
@@ -578,7 +609,7 @@ fn parse_postfix(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
make_result(node, p)
|
make_result(node, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Binary expression precedence climbing ------------------------------------
|
// ── Binary expression precedence climbing ────────────────────────────────────
|
||||||
|
|
||||||
fn op_precedence(kind: String) -> Int {
|
fn op_precedence(kind: String) -> Int {
|
||||||
if kind == "Or" { return 1 }
|
if kind == "Or" { return 1 }
|
||||||
@@ -593,7 +624,6 @@ fn op_precedence(kind: String) -> Int {
|
|||||||
if kind == "Minus" { return 5 }
|
if kind == "Minus" { return 5 }
|
||||||
if kind == "Star" { return 6 }
|
if kind == "Star" { return 6 }
|
||||||
if kind == "Slash" { return 6 }
|
if kind == "Slash" { return 6 }
|
||||||
if kind == "Percent" { return 6 }
|
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,7 +640,6 @@ fn is_binop(kind: String) -> Bool {
|
|||||||
if kind == "Minus" { return true }
|
if kind == "Minus" { return true }
|
||||||
if kind == "Star" { return true }
|
if kind == "Star" { return true }
|
||||||
if kind == "Slash" { return true }
|
if kind == "Slash" { return true }
|
||||||
if kind == "Percent" { return true }
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,7 +672,7 @@ fn parse_expr(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
parse_binop(tokens, pos, 1)
|
parse_binop(tokens, pos, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Statement parsing ---------------------------------------------------------
|
// ── Statement parsing ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||||
let k = tok_kind(tokens, pos)
|
let k = tok_kind(tokens, pos)
|
||||||
@@ -655,7 +684,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
let p = p + 1
|
let p = p + 1
|
||||||
let ltype = ""
|
let ltype = ""
|
||||||
let k2 = tok_kind(tokens, p)
|
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
|
// identifier so codegen can dispatch arithmetic vs concat on
|
||||||
// `+` between two typed Idents.
|
// `+` between two typed Idents.
|
||||||
if k2 == "Colon" {
|
if k2 == "Colon" {
|
||||||
@@ -689,7 +718,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
return make_result({ "stmt": "Return", "value": val }, p)
|
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" {
|
if k == "Extern" {
|
||||||
let p = pos + 1
|
let p = pos + 1
|
||||||
let k2: String = tok_kind(tokens, p)
|
let k2: String = tok_kind(tokens, p)
|
||||||
@@ -739,11 +768,16 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, p)
|
return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// type definition
|
// type definition: `type Name = { field: Type, ... }`
|
||||||
|
// The `=` between the name and the brace is optional in the spec but
|
||||||
|
// present in practice. Skip it if present before consuming the LBrace.
|
||||||
if k == "Type" {
|
if k == "Type" {
|
||||||
let p = pos + 1
|
let p = pos + 1
|
||||||
let name = tok_value(tokens, p)
|
let name = tok_value(tokens, p)
|
||||||
let p = p + 1
|
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 p = expect(tokens, p, "LBrace")
|
||||||
let fields: [Map<String, Any>] = native_list_empty()
|
let fields: [Map<String, Any>] = native_list_empty()
|
||||||
let running = true
|
let running = true
|
||||||
@@ -860,17 +894,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
return make_result({ "stmt": "While", "cond": cond, "body": body }, p)
|
return make_result({ "stmt": "While", "cond": cond, "body": body }, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// break statement
|
// for loop
|
||||||
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" {
|
if k == "For" {
|
||||||
let p = pos + 1
|
let p = pos + 1
|
||||||
let item_name = tok_value(tokens, p)
|
let item_name = tok_value(tokens, p)
|
||||||
@@ -880,41 +904,49 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
state_set("__no_block_expr", "1")
|
state_set("__no_block_expr", "1")
|
||||||
let r = parse_expr(tokens, p)
|
let r = parse_expr(tokens, p)
|
||||||
state_set("__no_block_expr", prev_no_block)
|
state_set("__no_block_expr", prev_no_block)
|
||||||
let start_expr = r["node"]
|
let list_expr = r["node"]
|
||||||
let p = r["pos"]
|
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 r2 = parse_block(tokens, p)
|
||||||
let body = r2["stmts"]
|
let body = r2["stmts"]
|
||||||
let p = r2["pos"]
|
let p = r2["pos"]
|
||||||
return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p)
|
return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @decorator - capture decorator name and attach to following stmt
|
// try/catch statement
|
||||||
|
// try { body } catch (name: Type) { handler }
|
||||||
|
// The catch variable name and type are both captured; type is skipped.
|
||||||
|
if k == "Try" {
|
||||||
|
let p = pos + 1
|
||||||
|
let r_try = parse_block(tokens, p)
|
||||||
|
let try_body = r_try["stmts"]
|
||||||
|
let p = r_try["pos"]
|
||||||
|
let catch_name = "err"
|
||||||
|
let k2 = tok_kind(tokens, p)
|
||||||
|
if str_eq(k2, "Catch") {
|
||||||
|
let p = p + 1
|
||||||
|
let p = expect(tokens, p, "LParen")
|
||||||
|
// catch variable name
|
||||||
|
let kn = tok_kind(tokens, p)
|
||||||
|
if str_eq(kn, "Ident") {
|
||||||
|
let catch_name = tok_value(tokens, p)
|
||||||
|
let p = p + 1
|
||||||
|
}
|
||||||
|
// optional type annotation: : Type
|
||||||
|
let k3 = tok_kind(tokens, p)
|
||||||
|
if str_eq(k3, "Colon") {
|
||||||
|
let p = p + 1
|
||||||
|
let p = skip_type(tokens, p)
|
||||||
|
}
|
||||||
|
let p = expect(tokens, p, "RParen")
|
||||||
|
let r_catch = parse_block(tokens, p)
|
||||||
|
let catch_body = r_catch["stmts"]
|
||||||
|
let p = r_catch["pos"]
|
||||||
|
return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": catch_body }, p)
|
||||||
|
}
|
||||||
|
return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": native_list_empty() }, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @decorator — capture decorator name and attach to following stmt
|
||||||
if k == "At" {
|
if k == "At" {
|
||||||
let p = pos + 1
|
let p = pos + 1
|
||||||
let dec_name = tok_value(tokens, p)
|
let dec_name = tok_value(tokens, p)
|
||||||
@@ -1077,7 +1109,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
make_result({ "stmt": "Expr", "value": val }, p)
|
make_result({ "stmt": "Expr", "value": val }, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Top-level parse ------------------------------------------------------------
|
// ── Top-level parse ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
|
fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||||
let total: Int = native_list_len(tokens)
|
let total: Int = native_list_len(tokens)
|
||||||
@@ -1096,7 +1128,7 @@ fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
|
|||||||
let stmt = r["node"]
|
let stmt = r["node"]
|
||||||
let new_pos: Int = r["pos"]
|
let new_pos: Int = r["pos"]
|
||||||
let stmts = native_list_append(stmts, stmt)
|
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 {
|
if new_pos <= pos {
|
||||||
let pos = pos + 1
|
let pos = pos + 1
|
||||||
} else {
|
} 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()
|
let parts: [String] = native_list_empty()
|
||||||
// Include both the runtime dir (for el_runtime.h) and the output dir
|
// Include both the runtime dir (for el_runtime.h) and the output dir
|
||||||
// (for module.elh cross-module forward declarations).
|
// (for module.elh cross-module forward declarations).
|
||||||
let parts = native_list_append(parts, "cc -O2 -fbracket-depth=1024 -I " + dirname_of(runtime_path) + " -I " + out_dir)
|
let parts = native_list_append(parts, "cc -O2 -I " + dirname_of(runtime_path) + " -I " + out_dir)
|
||||||
let i = 0
|
let i = 0
|
||||||
while i < n {
|
while i < n {
|
||||||
let f: String = native_list_get(c_files, i)
|
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 i = i + 1
|
||||||
}
|
}
|
||||||
let parts = native_list_append(parts, runtime_path)
|
let parts = native_list_append(parts, runtime_path)
|
||||||
let parts = native_list_append(parts, "-lcurl -lpthread -lm")
|
let parts = native_list_append(parts, "-lcurl -lpthread")
|
||||||
let parts = native_list_append(parts, "-o " + out_bin)
|
let parts = native_list_append(parts, "-o " + out_bin)
|
||||||
let cmd: String = str_join(parts, " ")
|
let cmd: String = str_join(parts, " ")
|
||||||
println(" link " + out_bin)
|
println(" link " + out_bin)
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
// 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
@@ -1,213 +0,0 @@
|
|||||||
// 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)
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
// 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")
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
// 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 + "]"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
// browser-auth.el -- El-compiled auth flow using Supabase
|
||||||
|
//
|
||||||
|
// Compile: elc --target=js --bundle examples/browser-auth.el > auth.js
|
||||||
|
// (requires el_runtime.js in the same directory as browser-auth.el)
|
||||||
|
//
|
||||||
|
// Demonstrates:
|
||||||
|
// - extern fn for declaring Supabase client constructor
|
||||||
|
// - anonymous function literals for callbacks
|
||||||
|
// - method call syntax on Any-typed values (client.auth.signInWithOtp)
|
||||||
|
// - try/catch for error handling
|
||||||
|
// - @async functions with DOM interaction
|
||||||
|
// - DOM bridge: dom_get_element, dom_get_value, dom_set_text, dom_add_class
|
||||||
|
// dom_remove_class, dom_show, dom_hide, dom_is_null
|
||||||
|
// - window_set to expose El functions to the browser global scope
|
||||||
|
// - local_storage_set/get for session hints
|
||||||
|
// - set_timeout for transient UI state
|
||||||
|
// - state_set/get for component state
|
||||||
|
//
|
||||||
|
// Expected HTML elements:
|
||||||
|
// #acct-email-input -- email text input
|
||||||
|
// #send-link-btn -- submit button
|
||||||
|
// #auth-message -- status message container
|
||||||
|
// #auth-form -- the form to hide after success
|
||||||
|
//
|
||||||
|
// The Supabase JS SDK is loaded from CDN via a <script> tag before auth.js.
|
||||||
|
// supabase_create_client is declared extern: the runtime provides it via
|
||||||
|
// the global supabase.createClient function exposed by the CDN bundle.
|
||||||
|
|
||||||
|
// ── External declarations ─────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// These functions are provided by the JS environment (CDN script tags).
|
||||||
|
// No body is emitted -- the compiler just records the names.
|
||||||
|
|
||||||
|
extern fn supabase_create_client(url: String, key: String) -> Any
|
||||||
|
|
||||||
|
// ── UI helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn show_message(text: String, is_error: Bool) -> Void {
|
||||||
|
let msg_el = dom_get_element("auth-message")
|
||||||
|
if !dom_is_null(msg_el) {
|
||||||
|
dom_set_text(msg_el, text)
|
||||||
|
dom_remove_class(msg_el, "hidden")
|
||||||
|
if is_error {
|
||||||
|
dom_add_class(msg_el, "error")
|
||||||
|
dom_remove_class(msg_el, "success")
|
||||||
|
} else {
|
||||||
|
dom_add_class(msg_el, "success")
|
||||||
|
dom_remove_class(msg_el, "error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_button_loading(loading: Bool) -> Void {
|
||||||
|
let btn = dom_get_element("send-link-btn")
|
||||||
|
if !dom_is_null(btn) {
|
||||||
|
if loading {
|
||||||
|
dom_set_text(btn, "Sending...")
|
||||||
|
dom_set_attr(btn, "disabled", "true")
|
||||||
|
} else {
|
||||||
|
dom_set_text(btn, "Send Magic Link")
|
||||||
|
dom_remove_attr(btn, "disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_message() -> Void {
|
||||||
|
let msg_el = dom_get_element("auth-message")
|
||||||
|
if !dom_is_null(msg_el) {
|
||||||
|
dom_add_class(msg_el, "hidden")
|
||||||
|
dom_set_text(msg_el, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Email validation ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn is_valid_email(email: String) -> Bool {
|
||||||
|
let trimmed: String = str_trim(email)
|
||||||
|
if str_len(trimmed) < 5 { return false }
|
||||||
|
let at_pos: Int = str_index_of(trimmed, "@")
|
||||||
|
if at_pos < 1 { return false }
|
||||||
|
let dot_pos: Int = str_index_of(trimmed, ".")
|
||||||
|
if dot_pos < at_pos + 2 { return false }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Supabase client construction ──────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Build a Supabase client from config injected into the page as NEURON_CFG.
|
||||||
|
// The extern fn supabase_create_client maps to supabase.createClient on
|
||||||
|
// the global object exposed by the CDN bundle.
|
||||||
|
|
||||||
|
fn get_supabase_client() -> Any {
|
||||||
|
let cfg = window_get("NEURON_CFG")
|
||||||
|
if dom_is_null(cfg) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let url: String = cfg["supabaseUrl"]
|
||||||
|
let key: String = cfg["supabaseAnonKey"]
|
||||||
|
supabase_create_client(url, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Auth flow ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@async
|
||||||
|
fn send_magic_link() -> Void {
|
||||||
|
let email_el = dom_get_element("acct-email-input")
|
||||||
|
if dom_is_null(email_el) {
|
||||||
|
show_message("Could not find email input", true)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let email: String = str_trim(dom_get_value(email_el))
|
||||||
|
|
||||||
|
if !is_valid_email(email) {
|
||||||
|
show_message("Please enter a valid email address", true)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_message()
|
||||||
|
set_button_loading(true)
|
||||||
|
state_set("auth_email", email)
|
||||||
|
|
||||||
|
// Build the Supabase client and call auth.signInWithOtp directly.
|
||||||
|
// Method call syntax on Any-typed values: client.auth.signInWithOtp(opts)
|
||||||
|
// No native_js_call required.
|
||||||
|
let client = get_supabase_client()
|
||||||
|
if dom_is_null(client) {
|
||||||
|
show_message("Auth service not configured", true)
|
||||||
|
set_button_loading(false)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let opts: Map<String, Any> = { "email": email }
|
||||||
|
// client is Any-typed; .auth returns the auth sub-client (also Any).
|
||||||
|
// .signInWithOtp(opts) returns a Promise. @async + await handles it.
|
||||||
|
let resp = client.auth.signInWithOtp(opts)
|
||||||
|
let err = resp["error"]
|
||||||
|
if !dom_is_null(err) {
|
||||||
|
let msg: String = err["message"]
|
||||||
|
show_message("Error: " + msg, true)
|
||||||
|
} else {
|
||||||
|
local_storage_set("auth_pending_email", email)
|
||||||
|
show_message("Magic link sent! Check your inbox for " + email, false)
|
||||||
|
let form = dom_get_element("auth-form")
|
||||||
|
if !dom_is_null(form) {
|
||||||
|
dom_hide(form)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: Any) {
|
||||||
|
show_message("Unexpected error. Please try again.", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
set_button_loading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Keyboard support ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn handle_email_keydown(event: Any) -> Void {
|
||||||
|
let key: String = dom_get_prop(event, "key")
|
||||||
|
if str_eq(key, "Enter") {
|
||||||
|
send_magic_link()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Initialization ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn init_auth() -> Void {
|
||||||
|
let email_el = dom_get_element("acct-email-input")
|
||||||
|
if !dom_is_null(email_el) {
|
||||||
|
// Pre-fill from local storage if a pending send was interrupted.
|
||||||
|
let pending: String = local_storage_get("auth_pending_email")
|
||||||
|
if !str_eq(pending, "") {
|
||||||
|
dom_set_value(email_el, pending)
|
||||||
|
}
|
||||||
|
// Anonymous function literal for inline event handler.
|
||||||
|
dom_listen(email_el, "keydown", fn(event: Any) -> Void {
|
||||||
|
let key: String = dom_get_prop(event, "key")
|
||||||
|
if str_eq(key, "Enter") {
|
||||||
|
send_magic_link()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let btn = dom_get_element("send-link-btn")
|
||||||
|
if !dom_is_null(btn) {
|
||||||
|
dom_listen(btn, "click", fn(event: Any) -> Void {
|
||||||
|
send_magic_link()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
state_set("auth_initialized", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Void {
|
||||||
|
// Expose send_magic_link globally so inline event handlers can call it.
|
||||||
|
window_set("sendMagicLink", send_magic_link)
|
||||||
|
window_set("initAuth", init_auth)
|
||||||
|
|
||||||
|
// Run init when DOM is ready.
|
||||||
|
window_on_load(init_auth)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// browser-counter.el — canonical browser DOM bridge example
|
||||||
|
//
|
||||||
|
// Compile with: elc --target=js examples/browser-counter.el > counter.js
|
||||||
|
//
|
||||||
|
// Then include in an HTML page that has a <span id="count-display"> element.
|
||||||
|
// The page can call window.increment() from any onclick handler, e.g.:
|
||||||
|
// <button onclick="increment()">+1</button>
|
||||||
|
//
|
||||||
|
// On load the display is initialised to "0". Each call to increment()
|
||||||
|
// adds 1 and updates the display text.
|
||||||
|
//
|
||||||
|
// Demonstrates:
|
||||||
|
// - dom_get_element to locate a DOM node by id
|
||||||
|
// - dom_set_text to update visible text content
|
||||||
|
// - dom_is_null to guard against missing elements
|
||||||
|
// - window_set to expose an El function for inline event handlers
|
||||||
|
// - state_set/get for in-memory counter state (survives calls, resets
|
||||||
|
// on page reload — same semantics as the C state_* API)
|
||||||
|
|
||||||
|
fn init() -> Void {
|
||||||
|
state_set("counter", 0)
|
||||||
|
let display = dom_get_element("count-display")
|
||||||
|
if !dom_is_null(display) {
|
||||||
|
dom_set_text(display, "0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment() -> Void {
|
||||||
|
let current = str_to_int(state_get("counter"))
|
||||||
|
let next = current + 1
|
||||||
|
state_set("counter", next)
|
||||||
|
let display = dom_get_element("count-display")
|
||||||
|
if !dom_is_null(display) {
|
||||||
|
dom_set_text(display, int_to_str(next))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Void {
|
||||||
|
init()
|
||||||
|
window_set("increment", increment)
|
||||||
|
}
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
// 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)
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// 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
@@ -1,105 +0,0 @@
|
|||||||
// 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()
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// 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)
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// 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
@@ -1,250 +0,0 @@
|
|||||||
// 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
@@ -1,213 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// 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
@@ -1,170 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// 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"
|
|
||||||
@@ -1,907 +0,0 @@
|
|||||||
// 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
@@ -1,320 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
// 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
@@ -1,425 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
+275
-45
@@ -1,6 +1,6 @@
|
|||||||
# El JavaScript Backend (codegen-js)
|
# El JavaScript Backend (codegen-js)
|
||||||
|
|
||||||
**Status:** scaffolded. Hello-world compiles and runs. ~50% language coverage. Core runtime (~30 builtins) implemented. CGI / DHARMA / LLM / Engram intentionally stubbed.
|
**Status:** Phase 5 complete. ~90% language coverage. Full browser JavaScript can be expressed structurally in El without any `native_js` escape hatches. All additions since Phase 4: anonymous function literals (lambda syntax), try/catch statement, extern fn declarations, direct JS method call syntax on Any-typed values, Promise helpers, Object/Array utilities, and URL import declarations. Proof: `examples/browser-auth.el` is a complete Supabase auth flow with zero `native_js` or `native_js_call` calls.
|
||||||
|
|
||||||
**Authoritative files**
|
**Authoritative files**
|
||||||
|
|
||||||
@@ -57,27 +57,36 @@ Same function names as `el_runtime.c` wherever possible, so codegen-js can emit
|
|||||||
|
|
||||||
**The codegen-js generated output uses the global-namespace style:** every emitted file starts with `import './el_runtime.js'` (which side-effects the globals) so call sites stay flat — `println(x)` not `el.println(x)`. This matches the C backend's flat call surface and keeps the generated code grep-compatible across targets.
|
**The codegen-js generated output uses the global-namespace style:** every emitted file starts with `import './el_runtime.js'` (which side-effects the globals) so call sites stay flat — `println(x)` not `el.println(x)`. This matches the C backend's flat call surface and keeps the generated code grep-compatible across targets.
|
||||||
|
|
||||||
### Implemented today (~30 builtins)
|
### Implemented (~90 builtins)
|
||||||
|
|
||||||
| Category | Functions |
|
| Category | Functions |
|
||||||
|---|---|
|
|---|---|
|
||||||
| I/O | `println`, `print` |
|
| 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` |
|
| 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 | `el_abs`, `el_max`, `el_min`, `math_sqrt`, `math_log`, `math_ln`, `math_sin`, `math_cos`, `math_pi` |
|
||||||
| 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` |
|
| Float | `float_to_str`, `int_to_float`, `float_to_int`, `format_float`, `decimal_round`, `str_to_float` |
|
||||||
|
| List | `el_list_new`, `el_list_len`, `el_list_get`, `el_list_append`, `el_list_empty`, `el_list_clone`, `list_push`, `list_push_front`, `list_join`, `list_range` |
|
||||||
| Map | `el_map_new`, `el_get_field`, `el_map_get`, `el_map_set` |
|
| Map | `el_map_new`, `el_get_field`, `el_map_get`, `el_map_set` |
|
||||||
| HTTP | `http_get`, `http_post`, `http_post_json` (via `fetch()`, returns `Promise<string>` — see §5 async caveat) |
|
| 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, throw in browser) |
|
| FS | `fs_read`, `fs_write`, `fs_list` (Node-only) |
|
||||||
| JSON | `json_parse`, `json_stringify`, `json_get`, `json_get_string`, `json_get_int` |
|
| JSON | `json_parse`, `json_stringify`, `json_get`, `json_get_string`, `json_get_int`, `json_get_float`, `json_get_bool`, `json_get_raw`, `json_set`, `json_array_len` |
|
||||||
| Time | `time_now`, `time_now_utc`, `sleep_secs` (Node), `sleep_ms` |
|
| Time | `time_now`, `time_now_utc`, `sleep_secs` (Node), `sleep_ms` |
|
||||||
| Bool | `bool_to_str` |
|
| Bool | `bool_to_str` |
|
||||||
| Process | `exit_program` (Node `process.exit`, throw in browser) |
|
| Process | `exit_program` (Node `process.exit`) |
|
||||||
| Refcount | `el_retain`, `el_release` (no-ops — JS has GC) |
|
| Refcount | `el_retain`, `el_release` (no-ops) |
|
||||||
| ARC method-call shortforms | `append`, `len`, `get`, `map_get`, `map_set` |
|
| Method shortforms | `append`, `len`, `get`, `map_get`, `map_set` |
|
||||||
| Native VM aliases | `native_list_get`, `native_list_len`, `native_list_append`, `native_list_empty`, `native_list_clone`, `native_string_chars`, `native_int_to_str` |
|
| Native VM aliases | `native_list_get`, `native_list_len`, `native_list_append`, `native_list_empty`, `native_list_clone`, `native_string_chars`, `native_int_to_str` |
|
||||||
| `args` | `args()` returns `process.argv.slice(2)` in Node, `[]` in browser |
|
| `args` / `env` / `state_*` | Process args, environment, in-memory state |
|
||||||
| `state_*` | In-memory `Map` keyed by string |
|
| UUID | `uuid_v4`, `uuid_new` |
|
||||||
| `env` | `process.env[k]` in Node, throws in browser |
|
| DOM bridge | `dom_get_element`, `dom_get_value`, `dom_set_value`, `dom_get_text`, `dom_set_text`, `dom_set_prop`, `dom_get_prop`, `dom_set_style`, `dom_add_class`, `dom_remove_class`, `dom_show`, `dom_hide`, `dom_listen`, `dom_query`, `dom_query_all`, `dom_create`, `dom_append`, `dom_remove`, `dom_is_null` (browser-only) |
|
||||||
|
| DOM extended | `dom_set_attr`, `dom_get_attr`, `dom_remove_attr`, `dom_set_html`, `dom_get_html`, `dom_get_parent`, `dom_contains_class`, `dom_get_checked`, `dom_set_checked` (browser-only) |
|
||||||
|
| Timers | `set_timeout(ms, cb)`, `set_interval(ms, cb) -> Int`, `clear_interval(handle)` |
|
||||||
|
| Local storage | `local_storage_get`, `local_storage_set`, `local_storage_remove` (browser-only) |
|
||||||
|
| Window | `window_location`, `window_redirect`, `window_on_load`, `window_set`, `window_get` |
|
||||||
|
| Debug | `console_log` |
|
||||||
|
| Promise helpers (Phase 5) | `promise_then(p, cb)`, `promise_catch(p, cb)`, `promise_resolve(val)`, `promise_reject(msg)` |
|
||||||
|
| Object / Array (Phase 5) | `object_assign(t, s)`, `object_keys(obj)`, `object_values(obj)`, `json_deep_clone(obj)`, `array_from(iterable)`, `type_of(val)`, `instanceof_check(val, name)` |
|
||||||
|
| native_js escape hatch | `native_js(code)` — eval; `native_js_call(obj, method, args)` — method call. Use only when no structural alternative exists |
|
||||||
|
|
||||||
### Stubbed (throw at runtime)
|
### Stubbed (throw at runtime)
|
||||||
|
|
||||||
@@ -128,17 +137,54 @@ The runtime auto-detects via `typeof window === 'undefined'`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. The async problem (the big deferred decision)
|
## 5. The async problem
|
||||||
|
|
||||||
`fetch()` is async. The C backend's `http_get(url)` is synchronous and returns the body string directly. El source was written assuming sync. Three options:
|
`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.
|
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.
|
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. **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.
|
3. **Explicit `@async` decorator on El functions; codegen-js emits `async function` + `await` for known-async call sites.** This is the approach implemented.
|
||||||
|
|
||||||
**Decision for this scaffold:** option 3, but only the runtime side is implemented. `http_get` in `el_runtime.js` returns a `Promise<string>`. `codegen-js.el` does NOT yet emit `async`/`await`. Calling `http_get` from compiled El will return a Promise that the El program will treat as a string (which produces `"[object Promise]"`). This is documented and accepted for the scaffold; the compile-time taint pass is a follow-up.
|
**Decision:** option 3, with an explicit opt-in decorator. `http_get`, `http_post`, `http_post_json`, `http_get_with_headers`, and `http_post_with_headers` in `el_runtime.js` return `Promise<string>`. `codegen-js.el` now emits `await` before calls to these builtins and before calls to any El function decorated `@async`.
|
||||||
|
|
||||||
For now, programs that don't touch HTTP work correctly. That covers `el-ui/runtime` (which only manipulates the DOM and a graph), most of cgi-studio's pure UI components, and all hello-world style programs.
|
### How to use async in El (JS target)
|
||||||
|
|
||||||
|
Mark a function with `@async` to declare it as async. Any call to that function from another El function will automatically get `await` in the generated JS. The callee must also be `@async` (or call only non-async code) for the pattern to compose correctly.
|
||||||
|
|
||||||
|
```el
|
||||||
|
@async
|
||||||
|
fn fetch_user(id: String) -> String {
|
||||||
|
http_get("https://api.example.com/users/" + id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@async
|
||||||
|
fn main() -> Void {
|
||||||
|
let body = fetch_user("42")
|
||||||
|
println(body)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Compiles to:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function fetch_user(id) {
|
||||||
|
return await http_get("https://api.example.com/users/" + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let body = await fetch_user("42");
|
||||||
|
println(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Limitations:**
|
||||||
|
- `@async` is a JS-target-only convention. The C backend ignores the decorator (it calls the synchronous libcurl-backed version).
|
||||||
|
- Implicit taint propagation (auto-marking all transitive callers) is not implemented. The programmer must explicitly add `@async` to every function in the call chain that reaches an async builtin.
|
||||||
|
- Forward-reference calls to `@async` functions are handled correctly: codegen-js does a pre-registration pass over all FnDefs before emitting any code.
|
||||||
|
|
||||||
|
For programs that do not touch HTTP, no `@async` annotation is needed and the generated code is identical to before.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -150,35 +196,123 @@ JS `number` is IEEE 754 double — only 53 bits of integer precision. El `Int` i
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. What's NOT supported in JS target initially
|
## 7. Language features — JS target coverage
|
||||||
|
|
||||||
This is the canonical list. Programs that use any of these compile (no `#error`-style fail-fast like the C backend's capability check) but throw at runtime or behave as documented.
|
### Fully supported
|
||||||
|
|
||||||
|
| Feature | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `cgi {}` block | Compiled to a no-op + comment (UI code is not a CGI) |
|
||||||
|
| `service {}` block | Compiled to a no-op + comment |
|
||||||
|
| `match` expressions | LitInt/LitStr/LitBool/Wildcard/Binding/Variant via IIFE if/else chain |
|
||||||
|
| `type` (struct) defs | Skipped; structs are plain JS objects. `t["field"]` works |
|
||||||
|
| `enum` defs | Skipped; enum values are strings or ints |
|
||||||
|
| `?` postfix (nil-prop) | `obj?.field` emits `(obj)?.["field"] ?? null` via JS optional chaining |
|
||||||
|
| `extern fn` | Emits a comment; calls resolve to JS environment globals |
|
||||||
|
| Anonymous function literals | `fn(p: T) -> R { body }` emits a hoisted `function __lambda_N(p)` |
|
||||||
|
| `try/catch` | Emits `try { ... } catch (name) { ... }` directly |
|
||||||
|
| URL imports | `import "https://..."` emits ES module import (or comment in bundle mode) |
|
||||||
|
| Method call on `Any` | `obj.method(args)` emits `obj.method(args)` for non-El-shortform methods |
|
||||||
|
| Field access on `Any` | `obj.field` emits `obj["field"]` (bracket notation, works on prototype chains) |
|
||||||
|
| `@async` decorator | `async function` + `await` at call sites for async builtins and `@async` fns |
|
||||||
|
|
||||||
|
### Not supported (stub throws or no-op)
|
||||||
|
|
||||||
| Feature | Status | Notes |
|
| Feature | Status | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `cgi {}` block | Compiled to a no-op + warning comment | CGI identity is server-side. UI code is not a CGI. |
|
| All `dharma_*` | Stub throws | Requires server-side daemon |
|
||||||
| `service {}` block | Compiled to a no-op + warning comment | Same. |
|
| All `engram_*` | Stub throws | Could be ported to IndexedDB later |
|
||||||
| All `dharma_*` | Stub throws | Programs needing DHARMA must call a server-side daemon over HTTP |
|
| All `llm_*` | Stub throws | Route through server |
|
||||||
| All `engram_*` | Stub throws | Could be ported to in-browser (IndexedDB-backed) later |
|
| `http_serve` | Stub throws | Browsers cannot host servers |
|
||||||
| All `llm_*` | Stub throws | Browser cannot hold API keys; route through server |
|
| `el_cgi_init` | No-op | CGI identity is server-side |
|
||||||
| `llm_register_tool` | Stub throws | Same |
|
| Capability enforcement | Not enforced | Runtime stubs throw; compile-time check is a follow-up |
|
||||||
| `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 |
|
| VBD role check | Not enforced | Same |
|
||||||
| Float bit-cast | Not needed | JS number is already a double |
|
| Float bit-cast | Not needed | JS number is already a double |
|
||||||
| Crypto primitives | Stub throws | Easy to add via `crypto.subtle` later |
|
| Crypto primitives | Stub throws | Add via `crypto.subtle` later |
|
||||||
| `state_*` | In-memory only | No persistence; resets on page reload |
|
| `state_*` | In-memory only | Resets on page reload |
|
||||||
| `args()` | Node-only | Browser returns `[]` |
|
| `args()` | Node-only | Browser returns `[]` |
|
||||||
| `fs_*` | Node-only | Browser throws |
|
| `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`
|
## 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:
|
The compiler entry point `compiler.el` adds a `compile_js(source: String) -> String` alongside the existing `compile()`. The CLI behavior:
|
||||||
@@ -195,20 +329,116 @@ The argv parser scans for a `--target=<lang>` token; remaining positional args a
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 8a. Production output — `--minify` and `--obfuscate`
|
||||||
|
|
||||||
|
Two post-processing flags produce production-ready browser JS in a single compiler invocation, replacing any external post-processing scripts.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
elc --target=js --bundle --minify source.el > output.min.js
|
||||||
|
elc --target=js --bundle --obfuscate source.el > output.obf.js
|
||||||
|
elc --target=js --bundle --minify --obfuscate source.el > output.final.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Both flags require `--target=js`. Passing either without `--target=js` prints an error and exits with code 1.
|
||||||
|
|
||||||
|
`--obfuscate` implies `--minify` — obfuscating unminified code produces no benefit and only increases output size.
|
||||||
|
|
||||||
|
### Pipeline order
|
||||||
|
|
||||||
|
```
|
||||||
|
generate JS -> (if --bundle, wrap in IIFE) -> (if --minify, run terser) -> (if --obfuscate, run javascript-obfuscator) -> output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tool discovery
|
||||||
|
|
||||||
|
The compiler looks for each tool in this order:
|
||||||
|
|
||||||
|
1. `<src_dir>/node_modules/.bin/<tool>` — local install next to source file
|
||||||
|
2. `<src_dir>/../node_modules/.bin/<tool>` — one level up (monorepo layout)
|
||||||
|
3. `npx --yes <tool>` — fall back to npx (uses globally cached package or downloads on first use)
|
||||||
|
|
||||||
|
If no path resolves and npx is not on `PATH`, the compiler prints a clear error and exits non-zero:
|
||||||
|
|
||||||
|
```
|
||||||
|
el-compiler: error: terser not found. Run 'npm install terser' in your project directory.
|
||||||
|
el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Minification (terser)
|
||||||
|
|
||||||
|
Command issued internally:
|
||||||
|
|
||||||
|
```
|
||||||
|
terser <tmpfile> --compress passes=2,drop_console=false,drop_debugger=true \
|
||||||
|
--mangle 'reserved=[<reserved>]' --output <tmpfile.min>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Obfuscation (javascript-obfuscator)
|
||||||
|
|
||||||
|
Command issued internally (runs after minification):
|
||||||
|
|
||||||
|
```
|
||||||
|
javascript-obfuscator <input> --output <output>
|
||||||
|
--compact true
|
||||||
|
--simplify true
|
||||||
|
--string-array true
|
||||||
|
--string-array-encoding base64
|
||||||
|
--string-array-threshold 0.75
|
||||||
|
--identifier-names-generator hexadecimal
|
||||||
|
--rename-globals false
|
||||||
|
--self-defending false
|
||||||
|
--reserved-names <reserved>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reserved names
|
||||||
|
|
||||||
|
These identifiers are protected from renaming by both tools. They are referenced directly from HTML `onclick=` attributes and other global-scope callsites:
|
||||||
|
|
||||||
|
```
|
||||||
|
neuronDemoToggle, neuronDemoSend, neuronDemoReset,
|
||||||
|
signInWith, signInWithEmail, signUpWithEmail, sendMagicLink,
|
||||||
|
signOut, resetPassword, sendResetEmail, updatePassword,
|
||||||
|
showSignIn, showSignUp, hideReset,
|
||||||
|
setSort, addFamilyMember, removeFamilyMember, copyForPlatform, entHeadcountChange,
|
||||||
|
NEURON_CFG
|
||||||
|
```
|
||||||
|
|
||||||
|
### Temp files
|
||||||
|
|
||||||
|
The compiler uses `/tmp/elc-<pid>-<timestamp>.js` naming for temp files. All temp files are cleaned up on both success and failure paths.
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- The compiler adds `stdout_to_file(path)` / `stdout_restore()` builtins to the C runtime (`el_runtime.c`) to capture codegen output (which is streamed via `println`) into a temp file before passing it to the external tools.
|
||||||
|
- `--minify` and `--obfuscate` error messages are printed after stdout is restored, so they always reach the terminal regardless of output redirection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 9. The path to compiling el-ui/runtime through this backend
|
## 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:
|
This is the real-world test. `el-ui/runtime/src/` is currently 5 hand-written `.js` files. The path to authoring them in El:
|
||||||
|
|
||||||
1. **Phase 1 — Hello-world** (this scaffold). Done.
|
1. **Phase 1 — Hello-world.** 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).
|
2. **Phase 2 — Language coverage.** DONE. `match`, struct/enum field access, `?`-propagation, `for`-over-list, complete operators.
|
||||||
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.
|
3. **Phase 3 — DOM bridge.** DONE. Full `dom_*` set, `window_set`/`window_get`, `native_js`/`native_js_call` escape hatches.
|
||||||
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.
|
4. **Phase 4 — Production output.** DONE. `--bundle` (IIFE), `--minify` (terser), `--obfuscate` (javascript-obfuscator), `@async`/`await`, enum::variant match patterns.
|
||||||
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.
|
5. **Phase 5 — Full JS expression coverage.** DONE. This is the phase documented in this revision.
|
||||||
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.
|
- `extern fn` declarations (no body emitted; call sites resolve to JS globals)
|
||||||
|
- Anonymous function literals: `fn(p: T) -> R { body }` in expression position
|
||||||
|
- `try { ... } catch (name: T) { ... }` statement
|
||||||
|
- Method call on `Any`-typed values: `client.auth.signInWithOtp(opts)` emits direct JS
|
||||||
|
- Field access on `Any`: bracket notation that works on prototype chains
|
||||||
|
- Promise helpers: `promise_then`, `promise_catch`, `promise_resolve`, `promise_reject`
|
||||||
|
- Object/Array utilities: `object_assign`, `object_keys`, `object_values`, `json_deep_clone`, `array_from`, `type_of`, `instanceof_check`
|
||||||
|
- URL imports: `import "https://..."` emits ES module import
|
||||||
|
- **Proof**: `examples/browser-auth.el` -- complete Supabase auth flow with zero `native_js` or `native_js_call`
|
||||||
|
6. **Phase 6 — Port `el-ui/runtime/`.** Translate the 5 JS files to El, compile to JS, swap in. Run el-ui's existing tests. The language is now expressive enough for this.
|
||||||
7. **Phase 7 — Port cgi-studio UI.** Larger surface area; same pattern.
|
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.
|
8. **Phase 8 — Marketplace plugins.** Open the door for third-party UI El.
|
||||||
|
|
||||||
The blocking item between phase 1 and phase 2 is incremental — every El construct used by el-ui's source needs codegen-js coverage. Phase 5 (async) is the architectural decision that needs explicit user buy-in, because it changes the language's effective semantics on the JS target.
|
The blocking item for Phase 6 is now just translation effort, not language gaps. Phase 5 removed the last structural barriers.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,341 +0,0 @@
|
|||||||
// 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()
|
|
||||||
}
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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("{}");
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// 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/"
|
|
||||||
}
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
// 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()
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
// 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 ""
|
|
||||||
}
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
// 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 ""
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
// 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 ""
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
// 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 ""
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
// 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 ""
|
|
||||||
}
|
|
||||||
@@ -1,575 +0,0 @@
|
|||||||
// 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 ""
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
// 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 ""
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
# 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`.
|
|
||||||
Vendored
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
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"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
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"type": "npm",
|
|
||||||
"script": "install",
|
|
||||||
"label": "npm: install",
|
|
||||||
"detail": "Install vscode-languageclient dependency",
|
|
||||||
"group": "build",
|
|
||||||
"presentation": {
|
|
||||||
"reveal": "silent"
|
|
||||||
},
|
|
||||||
"problemMatcher": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
// 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 };
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"comments": {
|
|
||||||
"lineComment": "//"
|
|
||||||
},
|
|
||||||
"brackets": [
|
|
||||||
["{", "}"],
|
|
||||||
["[", "]"],
|
|
||||||
["(", ")"]
|
|
||||||
],
|
|
||||||
"autoClosingPairs": [
|
|
||||||
{ "open": "{", "close": "}" },
|
|
||||||
{ "open": "[", "close": "]" },
|
|
||||||
{ "open": "(", "close": ")" },
|
|
||||||
{ "open": "\"", "close": "\"", "notIn": ["string"] }
|
|
||||||
],
|
|
||||||
"surroundingPairs": [
|
|
||||||
["{", "}"],
|
|
||||||
["[", "]"],
|
|
||||||
["(", ")"],
|
|
||||||
["\"", "\""]
|
|
||||||
],
|
|
||||||
"indentationRules": {
|
|
||||||
"increaseIndentPattern": "\\{[^}]*$",
|
|
||||||
"decreaseIndentPattern": "^\\s*}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
#!/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