Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8b22e16a2 | |||
| 3086a56b5c | |||
| 7c644b8d89 | |||
| 9e8d23bcd9 | |||
| 0791fda43e | |||
| 53f2df500d | |||
| 245eb2898e | |||
| 7bfe30b767 | |||
| 6ede9e4379 | |||
| 4ae42ee7db | |||
| 3e5130e98d | |||
| beb4e436e1 | |||
| 71a1e41f93 | |||
| 0676725cb7 | |||
| d98a968f89 | |||
| 0c9154551f | |||
| 9aa0c49d0c | |||
| 59b96e3324 | |||
| 287b39f8e6 | |||
| c45744d8ca | |||
| 2e778ca664 | |||
| 641227a7d3 | |||
| 805318e2d0 | |||
| cefff5b891 | |||
| ce9a2caff4 | |||
| d1af4b0f8b | |||
| 282df712a8 | |||
| cfcedff7f4 | |||
| eab483ed4f | |||
| cfa4301026 | |||
| f271f9d9d8 | |||
| 49a8a1c24b | |||
| 252ad04c96 | |||
| 3cc9b1cc3d | |||
| 5678745381 | |||
| e853f4a25c | |||
| 5b6915ec9e | |||
| b0e38a245a | |||
| 84b5355fce | |||
| b547d1daf8 | |||
| cc6522c69d | |||
| 5807de835e | |||
| 33af4ed09e | |||
| f2c63f95fd | |||
| 01849c2033 | |||
| 56724325ed | |||
| fea830cca2 | |||
| f9cfe43f05 | |||
| f97354e96b | |||
| 9d0e1f64d4 |
@@ -0,0 +1,115 @@
|
||||
name: El SDK CI — dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host: compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
- name: Run tests — text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests — calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests — time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests — html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
- name: Publish El SDK to Artifact Registry (dev)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-dev"
|
||||
rm -f /tmp/gcp-key.json
|
||||
@@ -0,0 +1,125 @@
|
||||
name: El SDK CI — stage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- stage
|
||||
pull_request:
|
||||
branches:
|
||||
- stage
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enforce source branch (stage ← dev only)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
SOURCE="${GITHUB_HEAD_REF}"
|
||||
if [ "${SOURCE}" != "dev" ]; then
|
||||
echo "ERROR: Stage branch only accepts PRs from 'dev'. Source was: '${SOURCE}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "Source branch check passed: ${SOURCE} → stage"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host: compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
- name: Run tests — text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests — calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests — time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests — html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
- name: Publish El SDK to Artifact Registry (stage)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-stage"
|
||||
rm -f /tmp/gcp-key.json
|
||||
@@ -13,6 +13,16 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enforce source branch (main ← stage only)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
SOURCE="${GITHUB_HEAD_REF}"
|
||||
if [ "${SOURCE}" != "stage" ]; then
|
||||
echo "ERROR: Main branch only accepts PRs from 'stage'. Source was: '${SOURCE}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "Source branch check passed: ${SOURCE} → main"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
@@ -128,6 +138,48 @@ jobs:
|
||||
|
||||
echo "Release published successfully"
|
||||
|
||||
# Publish artifacts to GCP Artifact Registry (prod)
|
||||
- name: Publish El SDK to Artifact Registry (prod)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-prod"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
# Dispatch el-sdk-updated event to downstream repos
|
||||
- name: Dispatch to foundation/engram
|
||||
env:
|
||||
@@ -164,3 +216,75 @@ jobs:
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/forge"
|
||||
|
||||
- name: Dispatch to neuron-technologies/el-ui
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/el-ui/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/el-ui"
|
||||
|
||||
- name: Dispatch to neuron-technologies/elp
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/elp/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/elp"
|
||||
|
||||
- name: Dispatch to neuron-technologies/elql
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/elql/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/elql"
|
||||
|
||||
- name: Dispatch to neuron-technologies/el-ide
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/el-ide/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/el-ide"
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# El Language — Agent Guide
|
||||
|
||||
El is a self-hosting, statically-typed language that compiles to C. This file orients agents that work on El itself or on programs written in El.
|
||||
|
||||
---
|
||||
|
||||
## What El Is
|
||||
|
||||
El compiles `.el` source → C → native binary. Every El value is `el_val_t` (int64_t). Strings are heap pointers cast through int64_t. The compiler is written in El (self-hosting).
|
||||
|
||||
**The compiler pipeline:**
|
||||
```
|
||||
elc-cli.el
|
||||
└─ imports: compiler.el
|
||||
└─ imports: lexer.el, parser.el, codegen.el, codegen-js.el
|
||||
```
|
||||
|
||||
The canonical compiler binary is `dist/platform/elc`. It was produced by running an earlier version of itself on `elc-cli.el`.
|
||||
|
||||
---
|
||||
|
||||
## The Two Layers — Know Which One You're In
|
||||
|
||||
### Layer 1: El programs (`.el` files)
|
||||
|
||||
This is where almost all work belongs. El programs are source files that get compiled by `elc`. New library functions, application logic, and language-level utilities all go here as `.el` files.
|
||||
|
||||
**Do not add C code when El can express it.** If functionality can be built from existing El primitives (string ops, `exec`, `fs_read/write`, `http_post`, etc.), write it in El.
|
||||
|
||||
### Layer 2: The C seed (`el-compiler/runtime/el_seed.c`)
|
||||
|
||||
This is the self-contained C OS-boundary layer. It provides the `__`-prefixed primitives that compiled El programs call: libcurl HTTP, pthreads, filesystem I/O, arena allocation, etc. It is **not generated** — it is maintained by hand.
|
||||
|
||||
The old `el_runtime.c` has been archived to `el-compiler/runtime/legacy/`. The runtime is now native El (`runtime/*.el`). `el_seed.c` replaces `el_runtime.c` as the sole C compilation dependency.
|
||||
|
||||
**Only edit `el_seed.c` when you genuinely need OS-level access** (raw sockets, GPU calls, new libcurl features). For everything else, write El.
|
||||
|
||||
When you do add a C builtin:
|
||||
1. Add the C function to `el_seed.c`
|
||||
2. Declare it in `el_seed.h`
|
||||
3. Add it to the `builtin_arity` table in `el-compiler/src/codegen.el` (so the compiler knows the arg count)
|
||||
4. Rebuild the elc binary (see below)
|
||||
|
||||
---
|
||||
|
||||
## Rebuilding the Compiler
|
||||
|
||||
After changing any `.el` source in `el-compiler/src/`:
|
||||
|
||||
```bash
|
||||
cd /Users/will/Development/neuron-technologies/foundation/el
|
||||
./dist/platform/elc elc-cli.el > elc-new.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o dist/platform/elc-new \
|
||||
elc-new.c el-compiler/runtime/el_seed.c
|
||||
# Verify self-hosting:
|
||||
./dist/platform/elc-new elc-cli.el > elc-verify.c
|
||||
diff elc-new.c elc-verify.c # should be identical
|
||||
mv dist/platform/elc-new dist/platform/elc
|
||||
```
|
||||
|
||||
After changing `el_seed.c` only (no El source changes), rebuild downstream programs but do NOT need to rebuild the compiler binary itself — the seed is linked at the application level, not the compiler level.
|
||||
|
||||
---
|
||||
|
||||
## How El Programs Are Built
|
||||
|
||||
Each El application has a `build.sh` that:
|
||||
1. Concatenates all `.el` source files (stripping `import` lines)
|
||||
2. Runs `elc` to produce a `.c` file
|
||||
3. Runs `cc` linking against `el_seed.c`
|
||||
|
||||
Example (cgi-studio daemon):
|
||||
```bash
|
||||
cd products/cgi-studio/el-daemon
|
||||
./build.sh
|
||||
```
|
||||
|
||||
When you add a new `.el` file to an application, add it to that application's `build.sh` concat list.
|
||||
|
||||
---
|
||||
|
||||
## Parallelism in El
|
||||
|
||||
El is single-threaded at the application level. Parallelism is achieved through subprocess fan-out:
|
||||
|
||||
```el
|
||||
// Pattern: write payloads to temp files, exec bash script with & and wait,
|
||||
// read results back from temp files.
|
||||
fn http_post_parallel(urls: [String], bodies: [String]) -> [String] {
|
||||
// ... bash fan-out via exec() ...
|
||||
}
|
||||
```
|
||||
|
||||
Use `exec()` (blocking) or `exec_bg()` (fire-and-forget) with shell scripts to run concurrent work. There is no goroutine or async/await — parallelism goes through the OS process layer.
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| Path | What it is |
|
||||
|------|-----------|
|
||||
| `dist/platform/elc` | Canonical compiler binary (arm64 Mac) |
|
||||
| `el-compiler/src/codegen.el` | Code generator — builtin arity table lives here |
|
||||
| `el-compiler/src/lexer.el` | Lexer |
|
||||
| `el-compiler/src/parser.el` | Parser |
|
||||
| `el-compiler/runtime/el_seed.c` | Self-contained C OS-boundary layer (replaces el_runtime.c) |
|
||||
| `el-compiler/runtime/el_seed.h` | Seed header (C function declarations) |
|
||||
| `spec/language.md` | Language specification |
|
||||
| `BOOTSTRAP.md` | How to recover the compiler from scratch |
|
||||
| `elc-cli.el` | Compiler entry point |
|
||||
| `elc-combined.el` | Pre-merged single-file compiler (used during early bootstrap) |
|
||||
|
||||
---
|
||||
|
||||
## HTTP Timeout
|
||||
|
||||
The El HTTP client (libcurl) defaults to **60 seconds**. Override per-process via `EL_HTTP_TIMEOUT_MS` env var. Set it before spawning any subprocess that makes long API calls:
|
||||
|
||||
```el
|
||||
exec("EL_HTTP_TIMEOUT_MS=300000 " + SOME_BIN + " " + args + " 2>&1")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- New library functions → write in El
|
||||
- New OS/hardware primitives → write in C and register in `codegen.el` arity table
|
||||
- Never edit `dist/platform/elc` directly — always rebuild from source
|
||||
- Never modify `el_seed.c` to add functionality that El can express
|
||||
Vendored
+2080
-154
File diff suppressed because it is too large
Load Diff
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -38,6 +38,7 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <dlfcn.h> /* dlsym for http_set_handler fallback */
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
@@ -154,6 +155,39 @@ el_val_t readline(void) {
|
||||
return el_wrap_str(el_strdup(buf));
|
||||
}
|
||||
|
||||
/* __read_n — read exactly n bytes from stdin.
|
||||
* Allocates a buffer of size n+1, calls fread(buf, 1, n, stdin) to read
|
||||
* exactly n raw bytes (including \r, \n, NUL, etc.), null-terminates, and
|
||||
* returns the buffer as an El String. Returns "" on EOF or I/O error.
|
||||
*
|
||||
* Used by the El LSP server to read JSON-RPC message bodies after parsing
|
||||
* the Content-Length header. readline() cannot be used for the body because
|
||||
* it stops at the first \n and LSP JSON bodies are not newline-terminated. */
|
||||
el_val_t __read_n(el_val_t nv) {
|
||||
int64_t n = EL_INT(nv);
|
||||
if (n <= 0) return el_wrap_str(el_strdup(""));
|
||||
char* buf = malloc((size_t)n + 1);
|
||||
if (!buf) { fputs("el_runtime: __read_n: out of memory\n", stderr); return el_wrap_str(el_strdup("")); }
|
||||
size_t got = fread(buf, 1, (size_t)n, stdin);
|
||||
buf[got] = '\0';
|
||||
if (got == 0) { free(buf); return el_wrap_str(el_strdup("")); }
|
||||
/* Track in arena so the allocation is freed when the request ends. */
|
||||
el_arena_track(buf);
|
||||
return el_wrap_str(buf);
|
||||
}
|
||||
|
||||
/* __print_raw — write a string to stdout without any modification.
|
||||
* Unlike println/print (which call puts/fputs and may add newlines or flush
|
||||
* in platform-specific ways), this uses fwrite with the exact byte count so
|
||||
* that embedded \r\n pairs in LSP Content-Length headers survive intact. */
|
||||
void __print_raw(el_val_t sv) {
|
||||
const char* s = EL_CSTR(sv);
|
||||
if (!s) return;
|
||||
size_t len = strlen(s);
|
||||
fwrite(s, 1, len, stdout);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t av, el_val_t bv) {
|
||||
@@ -1876,6 +1910,61 @@ el_val_t exec_capture(el_val_t cmdv) {
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
// exec — run a shell command via /bin/sh, capture stdout, return as String.
|
||||
// Times out after 30 seconds. Returns "" on any error.
|
||||
// El name: exec(cmd) -> String
|
||||
el_val_t exec(el_val_t cmdv) {
|
||||
const char* cmd = EL_CSTR(cmdv);
|
||||
if (!cmd || !*cmd) return el_wrap_str(el_strdup(""));
|
||||
/* Build a time-limited command: wrap with timeout(1) if available,
|
||||
* otherwise rely on the 30s read loop guard below. We use the simple
|
||||
* popen approach with a deadline measured by wall clock so the caller
|
||||
* is never blocked indefinitely. */
|
||||
FILE* f = popen(cmd, "r");
|
||||
if (!f) return el_wrap_str(el_strdup(""));
|
||||
JsonBuf b; jb_init(&b);
|
||||
char buf[4096];
|
||||
/* 30-second wall-clock deadline */
|
||||
time_t deadline = time(NULL) + 30;
|
||||
while (time(NULL) < deadline) {
|
||||
if (fgets(buf, sizeof(buf), f) == NULL) break;
|
||||
jb_puts(&b, buf);
|
||||
}
|
||||
pclose(f);
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
// exec_bg — run a shell command in background, return PID as String.
|
||||
// The child process runs independently; the caller is not blocked.
|
||||
// Returns "" on fork failure.
|
||||
// El name: exec_bg(cmd) -> String
|
||||
el_val_t exec_bg(el_val_t cmdv) {
|
||||
const char* cmd = EL_CSTR(cmdv);
|
||||
if (!cmd || !*cmd) return el_wrap_str(el_strdup(""));
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
/* fork failed */
|
||||
return el_wrap_str(el_strdup(""));
|
||||
}
|
||||
if (pid == 0) {
|
||||
/* child: detach from parent's stdio, exec via shell */
|
||||
setsid();
|
||||
int devnull = open("/dev/null", O_RDWR);
|
||||
if (devnull >= 0) {
|
||||
dup2(devnull, STDIN_FILENO);
|
||||
dup2(devnull, STDOUT_FILENO);
|
||||
dup2(devnull, STDERR_FILENO);
|
||||
close(devnull);
|
||||
}
|
||||
execl("/bin/sh", "sh", "-c", cmd, (char*)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
/* parent: convert pid to string and return immediately */
|
||||
char pidbuf[32];
|
||||
snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid);
|
||||
return el_wrap_str(el_strdup(pidbuf));
|
||||
}
|
||||
|
||||
el_val_t fs_list(el_val_t pathv) {
|
||||
const char* path = EL_CSTR(pathv);
|
||||
el_val_t lst = el_list_empty();
|
||||
@@ -10188,3 +10277,650 @@ el_val_t emit_event(el_val_t name_v, el_val_t duration_ms_v) {
|
||||
return trace_span_end(h);
|
||||
}
|
||||
|
||||
/* ── Threading seed primitives ───────────────────────────────────────────────
|
||||
* __thread_create(fn_name, arg) -> Int spawn El fn in a pthread, return tid
|
||||
* __thread_join(tid) -> String join thread, return result string
|
||||
* __mutex_new() -> Int allocate a mutex, return handle
|
||||
* __mutex_lock(m) lock mutex m
|
||||
* __mutex_unlock(m) unlock mutex m
|
||||
*
|
||||
* Every El fn compiles to a global C symbol. __thread_create uses dlsym to
|
||||
* look up the function by name and run it in a pthread. This means any El fn
|
||||
* with signature (String) -> String is directly threadable.
|
||||
*/
|
||||
|
||||
typedef el_val_t (*ElFn1)(el_val_t);
|
||||
|
||||
typedef struct {
|
||||
ElFn1 fn;
|
||||
el_val_t arg;
|
||||
el_val_t result;
|
||||
} ElThreadArg;
|
||||
|
||||
#define EL_THREAD_MAX 256
|
||||
|
||||
typedef struct {
|
||||
pthread_t tid;
|
||||
ElThreadArg* arg;
|
||||
int alive;
|
||||
} ElThread;
|
||||
|
||||
static ElThread _threads[EL_THREAD_MAX];
|
||||
static int _thread_count = 0;
|
||||
static pthread_mutex_t _thread_alloc_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void* el_thread_runner(void* raw) {
|
||||
ElThreadArg* a = (ElThreadArg*)raw;
|
||||
a->result = a->fn(a->arg);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v) {
|
||||
const char* sym = EL_CSTR(fn_name_v);
|
||||
if (!sym || !*sym) return EL_INT(-1);
|
||||
void* p = dlsym(RTLD_DEFAULT, sym);
|
||||
if (!p) {
|
||||
fprintf(stderr, "[__thread_create] symbol not found: %s\n", sym);
|
||||
return EL_INT(-1);
|
||||
}
|
||||
ElThreadArg* a = (ElThreadArg*)malloc(sizeof(ElThreadArg));
|
||||
if (!a) return EL_INT(-1);
|
||||
a->fn = (ElFn1)p;
|
||||
a->arg = arg_v;
|
||||
a->result = EL_STR("");
|
||||
|
||||
pthread_mutex_lock(&_thread_alloc_mu);
|
||||
if (_thread_count >= EL_THREAD_MAX) {
|
||||
pthread_mutex_unlock(&_thread_alloc_mu);
|
||||
free(a);
|
||||
fprintf(stderr, "[__thread_create] thread table full\n");
|
||||
return EL_INT(-1);
|
||||
}
|
||||
int slot = _thread_count++;
|
||||
_threads[slot].arg = a;
|
||||
_threads[slot].alive = 1;
|
||||
pthread_mutex_unlock(&_thread_alloc_mu);
|
||||
|
||||
if (pthread_create(&_threads[slot].tid, NULL, el_thread_runner, a) != 0) {
|
||||
pthread_mutex_lock(&_thread_alloc_mu);
|
||||
_thread_count--;
|
||||
pthread_mutex_unlock(&_thread_alloc_mu);
|
||||
free(a);
|
||||
return EL_INT(-1);
|
||||
}
|
||||
return EL_INT(slot);
|
||||
}
|
||||
|
||||
el_val_t __thread_join(el_val_t tid_v) {
|
||||
int slot = (int)(int64_t)tid_v;
|
||||
if (slot < 0 || slot >= EL_THREAD_MAX) return EL_STR("");
|
||||
pthread_join(_threads[slot].tid, NULL);
|
||||
el_val_t result = _threads[slot].arg->result;
|
||||
free(_threads[slot].arg);
|
||||
_threads[slot].alive = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Mutex table */
|
||||
|
||||
#define EL_MUTEX_MAX 64
|
||||
|
||||
typedef struct {
|
||||
pthread_mutex_t mu;
|
||||
int allocated;
|
||||
} ElMutexEntry;
|
||||
|
||||
static ElMutexEntry _mutexes[EL_MUTEX_MAX];
|
||||
static int _mutex_count = 0;
|
||||
static pthread_mutex_t _mutex_alloc_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
el_val_t __mutex_new(void) {
|
||||
pthread_mutex_lock(&_mutex_alloc_mu);
|
||||
if (_mutex_count >= EL_MUTEX_MAX) {
|
||||
pthread_mutex_unlock(&_mutex_alloc_mu);
|
||||
fprintf(stderr, "[__mutex_new] mutex table full\n");
|
||||
return EL_INT(-1);
|
||||
}
|
||||
int slot = _mutex_count++;
|
||||
pthread_mutex_init(&_mutexes[slot].mu, NULL);
|
||||
_mutexes[slot].allocated = 1;
|
||||
pthread_mutex_unlock(&_mutex_alloc_mu);
|
||||
return EL_INT(slot);
|
||||
}
|
||||
|
||||
void __mutex_lock(el_val_t m_v) {
|
||||
int slot = (int)(int64_t)m_v;
|
||||
if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return;
|
||||
pthread_mutex_lock(&_mutexes[slot].mu);
|
||||
}
|
||||
|
||||
void __mutex_unlock(el_val_t m_v) {
|
||||
int slot = (int)(int64_t)m_v;
|
||||
if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return;
|
||||
pthread_mutex_unlock(&_mutexes[slot].mu);
|
||||
}
|
||||
|
||||
/* ── Channels ─────────────────────────────────────────────────────────────── *
|
||||
* Buffered MPMC channel backed by a mutex + condvar + circular buffer.
|
||||
* channel_new(capacity) -> Int (handle)
|
||||
* channel_send(ch, msg) — blocks if full (capacity > 0) or never (unbounded)
|
||||
* channel_recv(ch) -> String — blocks until a message is available
|
||||
* channel_try_recv(ch) -> String — non-blocking, returns "" if empty
|
||||
* channel_close(ch) — signal no more sends; recv drains remaining
|
||||
*
|
||||
* Bounded channels (cap > 0): circular buffer, sender blocks when full.
|
||||
* Unbounded channels (cap == 0): dynamic array, sender never blocks.
|
||||
*/
|
||||
#define EL_CHANNEL_MAX 64
|
||||
#define EL_CHANNEL_BUF 1024
|
||||
|
||||
typedef struct {
|
||||
char** buf;
|
||||
int cap; /* 0 = unbounded (grows dynamically) */
|
||||
int head, tail, count;
|
||||
int dyn_cap; /* allocated slots for unbounded mode */
|
||||
int closed;
|
||||
pthread_mutex_t mu;
|
||||
pthread_cond_t not_empty;
|
||||
pthread_cond_t not_full;
|
||||
} ElChannel;
|
||||
|
||||
static ElChannel _channels[EL_CHANNEL_MAX];
|
||||
static int _channel_count = 0;
|
||||
static pthread_mutex_t _channel_alloc_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
el_val_t __channel_new(el_val_t capacity_v) {
|
||||
int cap = (int)(int64_t)capacity_v;
|
||||
if (cap < 0) cap = 0;
|
||||
|
||||
pthread_mutex_lock(&_channel_alloc_mu);
|
||||
if (_channel_count >= EL_CHANNEL_MAX) {
|
||||
pthread_mutex_unlock(&_channel_alloc_mu);
|
||||
fprintf(stderr, "[__channel_new] channel table full\n");
|
||||
return EL_INT(-1);
|
||||
}
|
||||
int slot = _channel_count++;
|
||||
pthread_mutex_unlock(&_channel_alloc_mu);
|
||||
|
||||
ElChannel* ch = &_channels[slot];
|
||||
memset(ch, 0, sizeof(*ch));
|
||||
ch->cap = cap;
|
||||
ch->closed = 0;
|
||||
ch->head = 0;
|
||||
ch->tail = 0;
|
||||
ch->count = 0;
|
||||
|
||||
if (cap > 0) {
|
||||
/* Bounded: fixed circular buffer. */
|
||||
ch->buf = (char**)malloc((size_t)cap * sizeof(char*));
|
||||
ch->dyn_cap = cap;
|
||||
} else {
|
||||
/* Unbounded: start with EL_CHANNEL_BUF slots, grow as needed. */
|
||||
ch->buf = (char**)malloc(EL_CHANNEL_BUF * sizeof(char*));
|
||||
ch->dyn_cap = EL_CHANNEL_BUF;
|
||||
}
|
||||
if (!ch->buf) {
|
||||
fprintf(stderr, "[__channel_new] out of memory\n");
|
||||
return EL_INT(-1);
|
||||
}
|
||||
|
||||
pthread_mutex_init(&ch->mu, NULL);
|
||||
pthread_cond_init(&ch->not_empty, NULL);
|
||||
pthread_cond_init(&ch->not_full, NULL);
|
||||
|
||||
return EL_INT(slot);
|
||||
}
|
||||
|
||||
void __channel_send(el_val_t ch_v, el_val_t msg_v) {
|
||||
int slot = (int)(int64_t)ch_v;
|
||||
if (slot < 0 || slot >= EL_CHANNEL_MAX) return;
|
||||
ElChannel* ch = &_channels[slot];
|
||||
|
||||
const char* msg = EL_CSTR(msg_v);
|
||||
if (!msg) msg = "";
|
||||
char* copy = strdup(msg); /* channel owns the string */
|
||||
|
||||
pthread_mutex_lock(&ch->mu);
|
||||
|
||||
if (ch->closed) {
|
||||
/* Send on closed channel is a no-op (drop the message). */
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
free(copy);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ch->cap > 0) {
|
||||
/* Bounded: block while full. */
|
||||
while (ch->count >= ch->cap && !ch->closed) {
|
||||
pthread_cond_wait(&ch->not_full, &ch->mu);
|
||||
}
|
||||
if (ch->closed) {
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
free(copy);
|
||||
return;
|
||||
}
|
||||
ch->buf[ch->tail] = copy;
|
||||
ch->tail = (ch->tail + 1) % ch->cap;
|
||||
ch->count++;
|
||||
} else {
|
||||
/* Unbounded: grow the buffer if needed. */
|
||||
if (ch->count >= ch->dyn_cap) {
|
||||
int new_cap = ch->dyn_cap * 2;
|
||||
char** grown = (char**)realloc(ch->buf, (size_t)new_cap * sizeof(char*));
|
||||
if (!grown) {
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
free(copy);
|
||||
fprintf(stderr, "[__channel_send] out of memory growing channel\n");
|
||||
return;
|
||||
}
|
||||
/* The circular buffer may have wrapped. Linearise it first.
|
||||
* In unbounded mode head is always 0 (we append at tail, drain
|
||||
* from head), so a simple memmove isn't needed — but if the
|
||||
* buffer did wrap (tail < head after growth), we need to fix up.
|
||||
* Simplest safe path: if tail wrapped, move the head..old_cap
|
||||
* segment to new_cap..new_cap+(old_cap-head). */
|
||||
if (ch->tail < ch->head) {
|
||||
/* Wrapped: [head..old_cap) is the front, [0..tail) is the back. */
|
||||
int front = ch->dyn_cap - ch->head;
|
||||
memmove(grown + ch->dyn_cap, grown + ch->head, (size_t)front * sizeof(char*));
|
||||
ch->head = ch->dyn_cap;
|
||||
}
|
||||
ch->buf = grown;
|
||||
ch->dyn_cap = new_cap;
|
||||
}
|
||||
ch->buf[ch->tail] = copy;
|
||||
ch->tail = (ch->tail + 1) % ch->dyn_cap;
|
||||
ch->count++;
|
||||
}
|
||||
|
||||
pthread_cond_signal(&ch->not_empty);
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
}
|
||||
|
||||
el_val_t __channel_recv(el_val_t ch_v) {
|
||||
int slot = (int)(int64_t)ch_v;
|
||||
if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR("");
|
||||
ElChannel* ch = &_channels[slot];
|
||||
|
||||
pthread_mutex_lock(&ch->mu);
|
||||
|
||||
/* Block until there is a message or the channel is closed and drained. */
|
||||
while (ch->count == 0 && !ch->closed) {
|
||||
pthread_cond_wait(&ch->not_empty, &ch->mu);
|
||||
}
|
||||
|
||||
if (ch->count == 0) {
|
||||
/* Closed and empty — signal EOF. */
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap;
|
||||
char* msg = ch->buf[ch->head];
|
||||
ch->head = (ch->head + 1) % buf_cap;
|
||||
ch->count--;
|
||||
|
||||
pthread_cond_signal(&ch->not_full);
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
|
||||
/* Hand the string to the arena so it is freed after the request. */
|
||||
el_arena_track(msg);
|
||||
return EL_STR(msg);
|
||||
}
|
||||
|
||||
el_val_t __channel_try_recv(el_val_t ch_v) {
|
||||
int slot = (int)(int64_t)ch_v;
|
||||
if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR("");
|
||||
ElChannel* ch = &_channels[slot];
|
||||
|
||||
pthread_mutex_lock(&ch->mu);
|
||||
|
||||
if (ch->count == 0) {
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap;
|
||||
char* msg = ch->buf[ch->head];
|
||||
ch->head = (ch->head + 1) % buf_cap;
|
||||
ch->count--;
|
||||
|
||||
pthread_cond_signal(&ch->not_full);
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
|
||||
el_arena_track(msg);
|
||||
return EL_STR(msg);
|
||||
}
|
||||
|
||||
void __channel_close(el_val_t ch_v) {
|
||||
int slot = (int)(int64_t)ch_v;
|
||||
if (slot < 0 || slot >= EL_CHANNEL_MAX) return;
|
||||
ElChannel* ch = &_channels[slot];
|
||||
|
||||
pthread_mutex_lock(&ch->mu);
|
||||
ch->closed = 1;
|
||||
/* Wake all blocked recvers and senders so they can observe the close. */
|
||||
pthread_cond_broadcast(&ch->not_empty);
|
||||
pthread_cond_broadcast(&ch->not_full);
|
||||
pthread_mutex_unlock(&ch->mu);
|
||||
}
|
||||
|
||||
/* ── DHARMA runtime additions ────────────────────────────────────────────────
|
||||
*
|
||||
* Functions required by the dharma registry service. Added here so the
|
||||
* released el_runtime.c includes them without requiring dharma to bundle
|
||||
* its own stubs.
|
||||
*
|
||||
* Functions added:
|
||||
* list_len — alias for el_list_len (used in handlers.el)
|
||||
* list_get — alias for el_list_get (used in handlers.el)
|
||||
* json_array_push — append a pre-encoded JSON element to a JSON array string
|
||||
* now_millis — milliseconds since Unix epoch (alias for time_now)
|
||||
* unix_timestamp_ms — same as now_millis (alias)
|
||||
* time_now_ms — same as now_millis (alias)
|
||||
* log_info — stderr structured log at INFO level
|
||||
* log_warn — stderr structured log at WARN level
|
||||
* config — reads a config value from the environment
|
||||
* http_patch — HTTP PATCH with JSON Content-Type
|
||||
* http_post_engram — HTTP POST with optional X-API-Key header
|
||||
* http_get_engram — HTTP GET with optional X-API-Key header
|
||||
* str_to_bytes — encode a string as a JSON array of byte values
|
||||
* bytes_to_str — decode a JSON array of byte values back to a string
|
||||
* hash_sha256 — SHA-256 hex digest of a string
|
||||
*/
|
||||
|
||||
/* list_len — return the number of elements in a list. */
|
||||
el_val_t list_len(el_val_t list) {
|
||||
return el_list_len(list);
|
||||
}
|
||||
|
||||
/* list_get — return the element at index i in a list. */
|
||||
el_val_t list_get(el_val_t list, el_val_t index) {
|
||||
return el_list_get(list, index);
|
||||
}
|
||||
|
||||
/* json_array_push — append element (a pre-encoded JSON fragment, e.g. "\"foo\""
|
||||
* or "42") to the JSON array string arr. Returns a new JSON array string.
|
||||
* Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]"
|
||||
* json_array_push("[\"alice\"]", "\"bob\"") -> "[\"alice\",\"bob\"]" */
|
||||
el_val_t json_array_push(el_val_t arr_v, el_val_t elem_v) {
|
||||
const char* arr = EL_CSTR(arr_v);
|
||||
const char* elem = EL_CSTR(elem_v);
|
||||
if (!arr || !*arr) arr = "[]";
|
||||
if (!elem || !*elem) elem = "null";
|
||||
|
||||
/* Trim whitespace, find the closing ']'. */
|
||||
const char* p = arr;
|
||||
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
|
||||
if (*p != '[') {
|
||||
/* Not an array — return a single-element array. */
|
||||
size_t n = strlen(elem) + 4;
|
||||
char* out = el_strbuf(n);
|
||||
snprintf(out, n, "[%s]", elem);
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
size_t arr_len = strlen(arr);
|
||||
size_t elem_len = strlen(elem);
|
||||
|
||||
/* Walk from the end to find the matching ']'. */
|
||||
const char* end = arr + arr_len - 1;
|
||||
while (end > p && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) end--;
|
||||
if (*end != ']') {
|
||||
/* Malformed — wrap elem in a new array. */
|
||||
size_t n = elem_len + 4;
|
||||
char* out = el_strbuf(n);
|
||||
snprintf(out, n, "[%s]", elem);
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* Content between '[' and ']'. */
|
||||
const char* inner_start = p + 1;
|
||||
const char* inner_end = end; /* points AT ']' */
|
||||
/* Check if the array is empty (only whitespace between brackets). */
|
||||
const char* q = inner_start;
|
||||
while (q < inner_end && (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r')) q++;
|
||||
int empty = (q == inner_end);
|
||||
|
||||
/* Build: prefix + (comma if non-empty) + elem + "]" */
|
||||
size_t prefix_len = (size_t)(inner_end - arr); /* up to but not including ']' */
|
||||
size_t sep_len = empty ? 0 : 1; /* "," if non-empty */
|
||||
size_t out_len = prefix_len + sep_len + elem_len + 2; /* +"]" + NUL */
|
||||
char* out = el_strbuf(out_len);
|
||||
memcpy(out, arr, prefix_len);
|
||||
if (!empty) out[prefix_len] = ',';
|
||||
memcpy(out + prefix_len + sep_len, elem, elem_len);
|
||||
out[prefix_len + sep_len + elem_len] = ']';
|
||||
out[prefix_len + sep_len + elem_len + 1] = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* now_millis — milliseconds since Unix epoch. */
|
||||
el_val_t now_millis(void) {
|
||||
return time_now();
|
||||
}
|
||||
|
||||
/* unix_timestamp_ms — same as now_millis. */
|
||||
el_val_t unix_timestamp_ms(void) {
|
||||
return time_now();
|
||||
}
|
||||
|
||||
/* time_now_ms — same as now_millis. */
|
||||
el_val_t time_now_ms(void) {
|
||||
return time_now();
|
||||
}
|
||||
|
||||
/* log_info — write a structured [INFO] line to stderr. */
|
||||
void log_info(el_val_t msg_v) {
|
||||
const char* msg = EL_CSTR(msg_v);
|
||||
fprintf(stderr, "[INFO] %s\n", msg ? msg : "");
|
||||
}
|
||||
|
||||
/* log_warn — write a structured [WARN] line to stderr. */
|
||||
void log_warn(el_val_t msg_v) {
|
||||
const char* msg = EL_CSTR(msg_v);
|
||||
fprintf(stderr, "[WARN] %s\n", msg ? msg : "");
|
||||
}
|
||||
|
||||
/* config — read a configuration value from the environment.
|
||||
* Returns "" if the variable is not set (same as __env_get). */
|
||||
el_val_t config(el_val_t key_v) {
|
||||
const char* key = EL_CSTR(key_v);
|
||||
if (!key || !*key) return EL_STR("");
|
||||
const char* val = getenv(key);
|
||||
if (!val) return EL_STR("");
|
||||
return el_wrap_str(el_strdup(val));
|
||||
}
|
||||
|
||||
/* http_patch — HTTP PATCH request with Content-Type: application/json.
|
||||
* Returns the response body (same error convention as http_post_json). */
|
||||
el_val_t http_patch(el_val_t url_v, el_val_t body_v) {
|
||||
const char* url = EL_CSTR(url_v);
|
||||
const char* body = EL_CSTR(body_v);
|
||||
if (!url || !*url) return http_error_json("empty url");
|
||||
CURL* c = curl_easy_init();
|
||||
if (!c) return http_error_json("curl_easy_init failed");
|
||||
HttpBuf rb; httpbuf_init(&rb);
|
||||
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
|
||||
struct curl_slist* h = NULL;
|
||||
h = curl_slist_append(h, "Content-Type: application/json");
|
||||
curl_easy_setopt(c, CURLOPT_URL, url);
|
||||
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH");
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : "");
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0));
|
||||
curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
|
||||
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms());
|
||||
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
|
||||
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0");
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
curl_slist_free_all(h);
|
||||
curl_easy_cleanup(c);
|
||||
if (rc != CURLE_OK) {
|
||||
free(rb.data);
|
||||
const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc);
|
||||
return http_error_json(m);
|
||||
}
|
||||
return el_wrap_str(rb.data);
|
||||
}
|
||||
|
||||
/* http_post_engram — HTTP POST with optional X-API-Key header.
|
||||
* If key is "" no authentication header is sent. */
|
||||
el_val_t http_post_engram(el_val_t url_v, el_val_t key_v, el_val_t body_v) {
|
||||
const char* url = EL_CSTR(url_v);
|
||||
const char* key = EL_CSTR(key_v);
|
||||
const char* body = EL_CSTR(body_v);
|
||||
if (!url || !*url) return http_error_json("empty url");
|
||||
CURL* c = curl_easy_init();
|
||||
if (!c) return http_error_json("curl_easy_init failed");
|
||||
HttpBuf rb; httpbuf_init(&rb);
|
||||
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
|
||||
struct curl_slist* h = NULL;
|
||||
h = curl_slist_append(h, "Content-Type: application/json");
|
||||
if (key && *key) {
|
||||
size_t n = strlen(key) + 32;
|
||||
char* hdr = malloc(n);
|
||||
snprintf(hdr, n, "X-API-Key: %s", key);
|
||||
h = curl_slist_append(h, hdr);
|
||||
free(hdr);
|
||||
}
|
||||
curl_easy_setopt(c, CURLOPT_URL, url);
|
||||
curl_easy_setopt(c, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : "");
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0));
|
||||
curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
|
||||
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms());
|
||||
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
|
||||
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0");
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
curl_slist_free_all(h);
|
||||
curl_easy_cleanup(c);
|
||||
if (rc != CURLE_OK) {
|
||||
free(rb.data);
|
||||
const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc);
|
||||
return http_error_json(m);
|
||||
}
|
||||
return el_wrap_str(rb.data);
|
||||
}
|
||||
|
||||
/* http_get_engram — HTTP GET with optional X-API-Key header. */
|
||||
el_val_t http_get_engram(el_val_t url_v, el_val_t key_v) {
|
||||
const char* url = EL_CSTR(url_v);
|
||||
const char* key = EL_CSTR(key_v);
|
||||
if (!url || !*url) return http_error_json("empty url");
|
||||
CURL* c = curl_easy_init();
|
||||
if (!c) return http_error_json("curl_easy_init failed");
|
||||
HttpBuf rb; httpbuf_init(&rb);
|
||||
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
|
||||
struct curl_slist* h = NULL;
|
||||
if (key && *key) {
|
||||
size_t n = strlen(key) + 32;
|
||||
char* hdr = malloc(n);
|
||||
snprintf(hdr, n, "X-API-Key: %s", key);
|
||||
h = curl_slist_append(h, hdr);
|
||||
free(hdr);
|
||||
}
|
||||
curl_easy_setopt(c, CURLOPT_URL, url);
|
||||
curl_easy_setopt(c, CURLOPT_HTTPGET, 1L);
|
||||
if (h) curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
|
||||
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms());
|
||||
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
|
||||
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0");
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
if (h) curl_slist_free_all(h);
|
||||
curl_easy_cleanup(c);
|
||||
if (rc != CURLE_OK) {
|
||||
free(rb.data);
|
||||
const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc);
|
||||
return http_error_json(m);
|
||||
}
|
||||
return el_wrap_str(rb.data);
|
||||
}
|
||||
|
||||
/* str_to_bytes — encode a string as a JSON array of unsigned byte values.
|
||||
* "hello" -> "[104,101,108,108,111]"
|
||||
* Used by db.el to store binary content in Engram JSON nodes. */
|
||||
el_val_t str_to_bytes(el_val_t sv) {
|
||||
const char* s = EL_CSTR(sv);
|
||||
if (!s || !*s) return el_wrap_str(el_strdup("[]"));
|
||||
size_t n = strlen(s);
|
||||
/* Worst case: each byte is 3 digits + comma = 4 chars, plus "[]" + NUL. */
|
||||
char* out = el_strbuf(n * 4 + 3);
|
||||
size_t pos = 0;
|
||||
out[pos++] = '[';
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
unsigned char b = (unsigned char)s[i];
|
||||
if (i > 0) out[pos++] = ',';
|
||||
/* Write decimal representation of b. */
|
||||
if (b >= 100) {
|
||||
out[pos++] = (char)('0' + b / 100);
|
||||
out[pos++] = (char)('0' + (b / 10) % 10);
|
||||
out[pos++] = (char)('0' + b % 10);
|
||||
} else if (b >= 10) {
|
||||
out[pos++] = (char)('0' + b / 10);
|
||||
out[pos++] = (char)('0' + b % 10);
|
||||
} else {
|
||||
out[pos++] = (char)('0' + b);
|
||||
}
|
||||
}
|
||||
out[pos++] = ']';
|
||||
out[pos] = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* bytes_to_str — decode a JSON array of integer byte values back to a string.
|
||||
* "[104,101,108,108,111]" -> "hello"
|
||||
* Inverse of str_to_bytes. */
|
||||
el_val_t bytes_to_str(el_val_t arr_v) {
|
||||
const char* s = EL_CSTR(arr_v);
|
||||
if (!s) return el_wrap_str(el_strdup(""));
|
||||
/* Skip whitespace, expect '['. */
|
||||
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
|
||||
if (*s != '[') return el_wrap_str(el_strdup(""));
|
||||
s++;
|
||||
|
||||
/* Count elements to size the output buffer. */
|
||||
int64_t n = (int64_t)json_array_len(arr_v);
|
||||
if (n <= 0) return el_wrap_str(el_strdup(""));
|
||||
|
||||
char* out = el_strbuf((size_t)n + 1);
|
||||
size_t pos = 0;
|
||||
|
||||
/* Walk the array, parse each integer, store as a byte. */
|
||||
while (*s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
|
||||
if (*s == ']' || *s == '\0') break;
|
||||
/* Parse decimal integer. */
|
||||
char* end_ptr;
|
||||
long v = strtol(s, &end_ptr, 10);
|
||||
if (end_ptr == s) break; /* parse failure */
|
||||
s = end_ptr;
|
||||
if (v >= 0 && v <= 255) out[pos++] = (char)(unsigned char)v;
|
||||
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
|
||||
if (*s == ',') { s++; continue; }
|
||||
if (*s == ']' || *s == '\0') break;
|
||||
}
|
||||
out[pos] = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* hash_sha256 — return the SHA-256 hex digest of a string.
|
||||
* Uses the built-in el_sha256_oneshot implementation (no OpenSSL required). */
|
||||
el_val_t hash_sha256(el_val_t sv) {
|
||||
const char* s = EL_CSTR(sv);
|
||||
if (!s) s = "";
|
||||
unsigned char digest[32];
|
||||
el_sha256_oneshot((const unsigned char*)s, strlen(s), digest);
|
||||
return el_hex_encode(digest, 32);
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +176,11 @@ void http_set_handler_v2(el_val_t name);
|
||||
* auto-content-type contract for legacy handlers that return plain bodies. */
|
||||
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
|
||||
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
|
||||
* The getter is exposed as __http_conn_fd() to El programs. */
|
||||
void el_seed_set_http_conn_fd(int fd);
|
||||
|
||||
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
|
||||
* 60000ms). Read lazily on first use, so setting the env var any time before
|
||||
* the first http_* call is sufficient. */
|
||||
@@ -742,6 +747,8 @@ el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
|
||||
/* ── Subprocess execution ────────────────────────────────────────────────── */
|
||||
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
|
||||
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
@@ -749,6 +756,9 @@ el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v);
|
||||
el_val_t __thread_join(el_val_t tid_v);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* el_seed.h — El language seed runtime header
|
||||
*
|
||||
* Declares all OS-boundary primitives available to compiled El programs.
|
||||
* All functions use the __ prefix convention. Signatures use el_val_t (= int64_t)
|
||||
* as the universal value type.
|
||||
*
|
||||
* el_seed.c is the complete C boundary for the El runtime. The heavy runtime
|
||||
* (el_runtime.c) has been retired — everything lives in el_seed.c plus the
|
||||
* native El runtime (runtime/ *.el files).
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — HTTP client (__http_do, __http_do_to_file)
|
||||
* -lpthread — threading (__thread_create, __thread_join, __mutex_new, ...)
|
||||
*
|
||||
* Canonical compile (via elb):
|
||||
* elb builds and links el_seed.c automatically.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ── Value model ─────────────────────────────────────────────────────────────
|
||||
* All El values are el_val_t (int64_t). On 64-bit systems a pointer fits.
|
||||
* String -> el_val_t (holds const char* via uintptr_t cast)
|
||||
* Int -> el_val_t (stored directly)
|
||||
* Bool -> el_val_t (0 = false, nonzero = true)
|
||||
* Void -> void
|
||||
*/
|
||||
typedef int64_t el_val_t;
|
||||
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
#define EL_NULL ((el_val_t)0)
|
||||
|
||||
/* Float values share the el_val_t slot via bit-cast. */
|
||||
static inline double el_to_float(el_val_t v) {
|
||||
union { int64_t i; double f; } u; u.i = (int64_t)v; return u.f;
|
||||
}
|
||||
static inline el_val_t el_from_float(double f) {
|
||||
union { double f; int64_t i; } u; u.f = f; return (el_val_t)u.i;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── String primitives ───────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __str_len(el_val_t s);
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i); /* returns Int (byte value) */
|
||||
el_val_t __str_alloc(el_val_t n); /* malloc(n+1), zero-init, return String */
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c); /* s[i]=c, return s */
|
||||
el_val_t __str_cmp(el_val_t a, el_val_t b); /* strcmp result as Int */
|
||||
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n); /* strncmp */
|
||||
el_val_t __str_concat_raw(el_val_t a, el_val_t b); /* malloc+strcpy concat */
|
||||
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end); /* substring copy */
|
||||
el_val_t __int_to_str(el_val_t n);
|
||||
el_val_t __str_to_int(el_val_t s);
|
||||
el_val_t __float_to_str(el_val_t f); /* f is bit-cast double */
|
||||
el_val_t __str_to_float(el_val_t s); /* strtod, bit-cast result */
|
||||
|
||||
/* ── I/O ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void __println(el_val_t s);
|
||||
void __print(el_val_t s);
|
||||
el_val_t __readline(void);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __fs_read(el_val_t path);
|
||||
el_val_t __fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t __fs_exists(el_val_t path);
|
||||
el_val_t __fs_list_raw(el_val_t path); /* newline-separated filenames */
|
||||
el_val_t __fs_mkdir(el_val_t path);
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n);
|
||||
|
||||
/* ── HTTP client ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Unified HTTP call. headers_json is a JSON object of header name->value pairs
|
||||
* (e.g. {"Authorization":"Bearer ...","Content-Type":"application/json"}).
|
||||
* Use "" or "{}" for no extra headers. timeout_ms <= 0 uses the default. */
|
||||
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t timeout_ms);
|
||||
|
||||
/* Stream response body directly to a file. Returns 1 on success, 0 on failure. */
|
||||
el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t out_path);
|
||||
|
||||
/* ── HTTP server ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Blocking HTTP server. handler_name is the El function name to dispatch to.
|
||||
* v1 handler: (method, path, body) -> String
|
||||
* v2 handler: (method, path, headers_map, body) -> String or envelope */
|
||||
void __http_serve(el_val_t port, el_val_t handler_name);
|
||||
void __http_serve_v2(el_val_t port, el_val_t handler_name);
|
||||
|
||||
/* Build a structured HTTP response envelope.
|
||||
* headers_json: JSON object literal like {"Content-Type":"text/plain"} or "{}" */
|
||||
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */
|
||||
|
||||
/* Returns the raw file descriptor for the current HTTP connection.
|
||||
* Valid only inside an http_serve_v2 handler before it returns.
|
||||
* Returns -1 if called outside a handler context. */
|
||||
el_val_t __http_conn_fd(void);
|
||||
|
||||
/* Sends SSE response headers on conn_id (the fd from __http_conn_fd),
|
||||
* keeping the connection open for streaming. Returns 1 on success, 0 on
|
||||
* write failure. Call once at the start of an SSE handler. */
|
||||
el_val_t __http_sse_open(el_val_t conn_id);
|
||||
|
||||
/* Writes one SSE event frame: "data: <data>\n\n". data must not contain
|
||||
* newlines. Returns 1 on success, 0 if the client disconnected. */
|
||||
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data);
|
||||
|
||||
/* Closes the SSE connection. The handler must return http_sse_sentinel()
|
||||
* so the HTTP worker does not double-close the fd. */
|
||||
el_val_t __http_sse_close(el_val_t conn_id);
|
||||
|
||||
/* ── Threading ───────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Create a thread that calls the named El function with a String argument.
|
||||
* fn_name is resolved via dlsym(RTLD_DEFAULT, fn_name). Returns a thread
|
||||
* handle Int that can be passed to __thread_join. Returns -1 on failure. */
|
||||
el_val_t __thread_create(el_val_t fn_name, el_val_t arg);
|
||||
|
||||
/* Wait for thread tid (returned by __thread_create) to finish.
|
||||
* Returns the thread's return value as a String. */
|
||||
el_val_t __thread_join(el_val_t tid);
|
||||
|
||||
/* Allocate a new mutex. Returns a handle Int (index into internal table). */
|
||||
el_val_t __mutex_new(void);
|
||||
|
||||
void __mutex_lock(el_val_t m);
|
||||
void __mutex_unlock(el_val_t m);
|
||||
|
||||
/* ── Subprocess ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __exec(el_val_t cmd); /* popen, capture all stdout, return String */
|
||||
void __exec_bg(el_val_t cmd); /* fire and forget */
|
||||
|
||||
/* ── Environment and process ─────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __env_get(el_val_t key); /* getenv, return "" if not set */
|
||||
void __exit_program(el_val_t code);
|
||||
el_val_t __args_json(void); /* CLI args as JSON array string */
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __time_now_ns(void); /* clock_gettime REALTIME, nanoseconds */
|
||||
void __sleep_ms(el_val_t ms);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __uuid_v4(void);
|
||||
|
||||
/* ── Math ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __sqrt_f(el_val_t f);
|
||||
el_val_t __log_f(el_val_t f);
|
||||
el_val_t __ln_f(el_val_t f);
|
||||
el_val_t __sin_f(el_val_t f);
|
||||
el_val_t __cos_f(el_val_t f);
|
||||
el_val_t __pi_f(void);
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __json_get(el_val_t json, el_val_t key);
|
||||
el_val_t __json_get_raw(el_val_t json_str, el_val_t key);
|
||||
el_val_t __json_parse(el_val_t s);
|
||||
el_val_t __json_stringify(el_val_t v);
|
||||
el_val_t __json_array_len(el_val_t json_str);
|
||||
el_val_t __json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t __json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
el_val_t __json_get_string(el_val_t json_str, el_val_t key);
|
||||
el_val_t __json_get_int(el_val_t json_str, el_val_t key);
|
||||
el_val_t __json_get_float(el_val_t json_str, el_val_t key);
|
||||
el_val_t __json_get_bool(el_val_t json_str, el_val_t key);
|
||||
el_val_t __json_set(el_val_t json_str, el_val_t key, el_val_t value);
|
||||
|
||||
/* ── State K/V ───────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __state_set(el_val_t key, el_val_t value);
|
||||
el_val_t __state_get(el_val_t key);
|
||||
el_val_t __state_del(el_val_t key);
|
||||
el_val_t __state_keys(void);
|
||||
|
||||
/* ── HTML/URL ────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
el_val_t __url_encode(el_val_t s);
|
||||
el_val_t __url_decode(el_val_t s);
|
||||
|
||||
/* ── Engram ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
|
||||
el_val_t __engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags);
|
||||
el_val_t __engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
el_val_t __engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable);
|
||||
el_val_t __engram_remove_layer(el_val_t layer_id);
|
||||
el_val_t __engram_list_layers(void);
|
||||
el_val_t __engram_get_node(el_val_t id);
|
||||
void __engram_strengthen(el_val_t node_id);
|
||||
void __engram_forget(el_val_t node_id);
|
||||
el_val_t __engram_node_count(void);
|
||||
el_val_t __engram_search(el_val_t query, el_val_t limit);
|
||||
el_val_t __engram_scan_nodes(el_val_t limit, el_val_t offset);
|
||||
void __engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
|
||||
el_val_t __engram_edge_between(el_val_t from_id, el_val_t to_id);
|
||||
el_val_t __engram_neighbors(el_val_t node_id);
|
||||
el_val_t __engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t __engram_edge_count(void);
|
||||
el_val_t __engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t __engram_save(el_val_t path);
|
||||
el_val_t __engram_load(el_val_t path);
|
||||
el_val_t __engram_get_node_json(el_val_t id);
|
||||
el_val_t __engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t __engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t __engram_stats_json(void);
|
||||
el_val_t __engram_list_layers_json(void);
|
||||
el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── Cryptographic hashing ────────────────────────────────────────────────── */
|
||||
|
||||
/* __sha256_hex — return the SHA-256 hex digest of a string.
|
||||
* The returned string is 64 hex characters (lowercase). */
|
||||
el_val_t __sha256_hex(el_val_t s);
|
||||
|
||||
/* ── args init (called from main) ────────────────────────────────────────── */
|
||||
/* Store argc/argv for __args_json. Call once at the start of main(). */
|
||||
void el_seed_init_args(int argc, char** argv);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,761 @@
|
||||
/*
|
||||
* el_runtime.h — El language C runtime header
|
||||
*
|
||||
* Declares all built-in functions available to compiled El programs.
|
||||
* Include this in every generated .c file.
|
||||
*
|
||||
* Value model:
|
||||
* All El values are represented as el_val_t (= int64_t).
|
||||
* On 64-bit systems a pointer fits in int64_t.
|
||||
* String values are cast: (el_val_t)(uintptr_t)"hello"
|
||||
* Integer values are stored directly.
|
||||
* This lets arithmetic work naturally while still passing strings around.
|
||||
*
|
||||
* Type conventions (El -> C):
|
||||
* String -> el_val_t (holds const char* via uintptr_t cast)
|
||||
* Int -> el_val_t
|
||||
* Bool -> el_val_t (0 = false, nonzero = true)
|
||||
* Any -> el_val_t
|
||||
* Void -> void
|
||||
*
|
||||
* Macros for convenience:
|
||||
* EL_STR(s) cast string literal to el_val_t
|
||||
* EL_CSTR(v) cast el_val_t back to const char*
|
||||
* EL_INT(v) identity — el_val_t is already int64_t
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — required for the HTTP client (http_get, http_post, llm_*).
|
||||
* -lpthread — required for the HTTP server (one detached thread per
|
||||
* connection, capped at 64 concurrent).
|
||||
* -loqs — optional; required only when liboqs is installed and the
|
||||
* pq_* / sha3_256_hex entry points are needed. Detected at
|
||||
* compile time via __has_include(<oqs/oqs.h>).
|
||||
* -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in
|
||||
* pq_hybrid_* and HKDF-SHA256 derivation.
|
||||
*
|
||||
* Canonical compile command:
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*
|
||||
* With liboqs (post-quantum stack):
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef int64_t el_val_t;
|
||||
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
#define EL_NULL ((el_val_t)0)
|
||||
|
||||
/* Float values share the el_val_t (int64) slot via a bit-cast.
|
||||
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
|
||||
* underlying bits represent the IEEE 754 double. Float-aware builtins
|
||||
* (math, format, json) round-trip via these helpers. */
|
||||
static inline double el_to_float(el_val_t v) {
|
||||
union { int64_t i; double f; } u;
|
||||
u.i = (int64_t)v;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
static inline el_val_t el_from_float(double f) {
|
||||
union { double f; int64_t i; } u;
|
||||
u.f = f;
|
||||
return (el_val_t)u.i;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t str_eq(el_val_t a, el_val_t b);
|
||||
el_val_t str_starts_with(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_ends_with(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_len(el_val_t s);
|
||||
el_val_t str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t int_to_str(el_val_t n);
|
||||
el_val_t str_to_int(el_val_t s);
|
||||
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t str_contains(el_val_t s, el_val_t sub);
|
||||
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
|
||||
el_val_t str_to_upper(el_val_t s);
|
||||
el_val_t str_to_lower(el_val_t s);
|
||||
el_val_t str_trim(el_val_t s);
|
||||
|
||||
/* ── Math ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_abs(el_val_t n);
|
||||
el_val_t el_max(el_val_t a, el_val_t b);
|
||||
el_val_t el_min(el_val_t a, el_val_t b);
|
||||
|
||||
/* ── Refcount (ARC) ──────────────────────────────────────────────────────────
|
||||
* Lists and Maps carry a refcount. Strings and ints do not — el_retain and
|
||||
* el_release are safe no-ops on non-refcounted values (they sniff a magic
|
||||
* header at offset 0 and only act if the magic matches).
|
||||
*
|
||||
* Codegen emits these at let-binding shadowing, function entry (params), and
|
||||
* function exit (locals other than the returned value). The refcount lets
|
||||
* el_list_append and el_map_set mutate in place when uniquely owned (cheap)
|
||||
* and copy-on-write when shared (preserves persistent semantics across
|
||||
* accumulator patterns in the compiler itself). */
|
||||
|
||||
void el_retain(el_val_t v);
|
||||
void el_release(el_val_t v);
|
||||
|
||||
/* ── List ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_list_new(el_val_t count, ...);
|
||||
el_val_t el_list_len(el_val_t list);
|
||||
el_val_t el_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t el_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t el_list_empty(void);
|
||||
el_val_t el_list_clone(el_val_t list);
|
||||
|
||||
/* ── Map ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_map_new(el_val_t pair_count, ...);
|
||||
el_val_t el_get_field(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_get(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value);
|
||||
|
||||
/* ── HTTP ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t http_get(el_val_t url);
|
||||
el_val_t http_post(el_val_t url, el_val_t body);
|
||||
el_val_t http_post_json(el_val_t url, el_val_t json_body);
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
void http_serve(el_val_t port, el_val_t handler);
|
||||
void http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
* Same dispatch model as http_serve, but the handler signature is widened:
|
||||
*
|
||||
* el_val_t handler(method, path, headers_map, body)
|
||||
*
|
||||
* `headers_map` is an ElMap from lowercased header name → header value (both
|
||||
* Strings). Repeated headers are joined with ", " per RFC 7230.
|
||||
*
|
||||
* Response value: the handler may return either
|
||||
* (a) a plain body string — same auto-content-type / 200-OK behaviour as
|
||||
* http_serve (3-arg) — or
|
||||
* (b) a response envelope built with `http_response(status, headers_json,
|
||||
* body)`. The runtime detects the envelope discriminator
|
||||
* `"el_http_response":1` at the start of the returned string and
|
||||
* unpacks status / headers / body before sending.
|
||||
*
|
||||
* The 3-arg http_serve(port, handler) remains supported unchanged for
|
||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||
void http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
* returned string carries the discriminator `{"el_http_response":1,...}`
|
||||
* which the runtime's send-path detects and unpacks. Detection happens
|
||||
* uniformly inside http_send_response, so a 3-arg handler may also return
|
||||
* an envelope. The 3-arg variant remains documented as a fixed 200-OK
|
||||
* auto-content-type contract for legacy handlers that return plain bodies. */
|
||||
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
|
||||
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
|
||||
* The getter is exposed as __http_conn_fd() to El programs. */
|
||||
void el_seed_set_http_conn_fd(int fd);
|
||||
|
||||
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
|
||||
* 60000ms). Read lazily on first use, so setting the env var any time before
|
||||
* the first http_* call is sufficient. */
|
||||
|
||||
/* Streaming variants — write the response body straight to a file via
|
||||
* libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string
|
||||
* wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive
|
||||
* embedded NUL bytes that would truncate a strlen()-based code path.
|
||||
*
|
||||
* Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same
|
||||
* `headers_map` shape as http_post_with_headers (ElMap of String→String).
|
||||
*
|
||||
* Return value: 1 on success (file fully written), 0 on any failure
|
||||
* (network, file open, partial write). On failure the output file is removed
|
||||
* so callers cannot mistake a partially-written file for a valid one. */
|
||||
el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path);
|
||||
el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path);
|
||||
|
||||
/* ── URL encoding ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */
|
||||
el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
|
||||
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
|
||||
* el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML
|
||||
* cleaner. State-machine parser; tag/attribute names compared case-
|
||||
* insensitively against the allowlist; `<a href>` / `<… src>` URL schemes
|
||||
* validated (http, https, mailto, fragment-only, or relative); whole-
|
||||
* subtree drop for script / style / iframe / object / embed / form; HTML-
|
||||
* escapes free text outside dropped subtrees.
|
||||
*
|
||||
* The allowlist is JSON of the form
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
el_val_t fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t fs_list(el_val_t path);
|
||||
el_val_t fs_exists(el_val_t path);
|
||||
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
|
||||
|
||||
/* Length-explicit binary write. `length` is an Int (el_val_t holding the
|
||||
* byte count). The caller knows the length from context — typically because
|
||||
* `bytes` came from base64_decode (which produces a magic-tagged binary
|
||||
* buffer with embedded NULs possible) and the caller already tracks the
|
||||
* decoded length, OR because the bytes came from a fixed-size source
|
||||
* (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely.
|
||||
*
|
||||
* Returns 1 on success, 0 on failure (invalid path, can't open, partial
|
||||
* write, negative length). On partial-write failure, the file is removed
|
||||
* so callers cannot read back a truncated artefact. */
|
||||
el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length);
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t json_get(el_val_t json, el_val_t key);
|
||||
el_val_t json_parse(el_val_t s);
|
||||
el_val_t json_stringify(el_val_t v);
|
||||
el_val_t json_get_string(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_int(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_float(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_bool(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_raw(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
|
||||
el_val_t json_array_len(el_val_t json_str);
|
||||
el_val_t json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t time_now(void);
|
||||
el_val_t time_now_utc(void);
|
||||
el_val_t sleep_secs(el_val_t secs);
|
||||
el_val_t sleep_ms(el_val_t ms);
|
||||
el_val_t time_format(el_val_t ts, el_val_t fmt);
|
||||
el_val_t time_to_parts(el_val_t ts);
|
||||
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
|
||||
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
|
||||
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
|
||||
|
||||
/* ── Instant + Duration: first-class temporal types ──────────────────────────
|
||||
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
|
||||
* since the Unix epoch; Durations are signed nanoseconds. Type discipline
|
||||
* is enforced at codegen-time: BinOps on names registered as Instant or
|
||||
* Duration route through the typed wrappers below; mismatches like
|
||||
* Instant+Instant become #error at the C compiler.
|
||||
*
|
||||
* Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are
|
||||
* recognised by the parser as DurationLit AST nodes and lowered to literal
|
||||
* int64 nanoseconds at codegen time. The runtime never sees the units. */
|
||||
|
||||
el_val_t el_now_instant(void);
|
||||
el_val_t now(void);
|
||||
el_val_t unix_seconds(el_val_t n);
|
||||
el_val_t unix_millis(el_val_t n);
|
||||
el_val_t instant_from_iso8601(el_val_t s);
|
||||
|
||||
el_val_t el_duration_from_nanos(el_val_t ns);
|
||||
el_val_t duration_seconds(el_val_t n);
|
||||
el_val_t duration_millis(el_val_t n);
|
||||
el_val_t duration_nanos(el_val_t n);
|
||||
|
||||
el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_diff(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_add(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_sub(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_scale(el_val_t dur, el_val_t scalar);
|
||||
el_val_t el_duration_div(el_val_t dur, el_val_t scalar);
|
||||
|
||||
el_val_t el_instant_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ne(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ne(el_val_t a, el_val_t b);
|
||||
|
||||
el_val_t instant_to_unix_seconds(el_val_t i);
|
||||
el_val_t instant_to_unix_millis(el_val_t i);
|
||||
el_val_t instant_to_iso8601(el_val_t i);
|
||||
el_val_t duration_to_seconds(el_val_t d);
|
||||
el_val_t duration_to_millis(el_val_t d);
|
||||
el_val_t duration_to_nanos(el_val_t d);
|
||||
|
||||
el_val_t el_sleep_duration(el_val_t dur);
|
||||
el_val_t unix_timestamp(void);
|
||||
|
||||
el_val_t ttl_cache_set(el_val_t key, el_val_t value);
|
||||
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
|
||||
el_val_t ttl_cache_age(el_val_t key);
|
||||
|
||||
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
|
||||
* Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
|
||||
* zones, Gregorian, DST) is the user-facing default; MarsCalendar,
|
||||
* CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
|
||||
* domains.
|
||||
*
|
||||
* A Calendar interprets an Instant under a particular cycle convention and
|
||||
* produces a CalendarTime. CalendarTime carries the underlying Instant and
|
||||
* a back-pointer to its Calendar; arithmetic and formatting consult the
|
||||
* Calendar to convert ns since epoch into year/month/day/hour/minute/second
|
||||
* (or sol/phase, or cycle/phase, depending on kind).
|
||||
*
|
||||
* Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
|
||||
* LocalDateTime are heap-allocated structs whose pointers are cast into
|
||||
* el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
|
||||
* the kind safely. LocalTime is small enough to live in the int64 slot
|
||||
* directly (nanos since midnight, signed). */
|
||||
|
||||
/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
|
||||
* `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
|
||||
* offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
|
||||
* on first use of the owning EarthCalendar. */
|
||||
el_val_t zone(el_val_t id);
|
||||
el_val_t zone_utc(void);
|
||||
el_val_t zone_local(void);
|
||||
el_val_t zone_offset(el_val_t hours, el_val_t minutes);
|
||||
|
||||
/* Calendar constructors. Each returns an el_val_t pointer to a heap-
|
||||
* allocated, magic-tagged Calendar struct. Calendars are interned by
|
||||
* (kind, zone_id, period_ns, epoch_ns) so identical constructors return
|
||||
* the same pointer — equality is reference equality. */
|
||||
el_val_t earth_calendar(el_val_t z);
|
||||
el_val_t earth_calendar_default(void);
|
||||
el_val_t mars_calendar(void);
|
||||
el_val_t cycle_calendar(el_val_t period_dur);
|
||||
el_val_t no_cycle_calendar(void);
|
||||
el_val_t relative_calendar(el_val_t epoch_inst);
|
||||
|
||||
/* CalendarTime constructors and methods. Returns a heap-allocated struct
|
||||
* whose pointer fits in el_val_t. */
|
||||
el_val_t now_in(el_val_t cal);
|
||||
el_val_t in_calendar(el_val_t inst, el_val_t cal);
|
||||
el_val_t cal_format(el_val_t ct, el_val_t pattern);
|
||||
el_val_t cal_to_instant(el_val_t ct);
|
||||
el_val_t cal_cycle_phase(el_val_t ct);
|
||||
el_val_t cal_in(el_val_t ct, el_val_t cal);
|
||||
|
||||
/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
|
||||
* LocalTime carries nanoseconds since midnight as a signed int64 directly
|
||||
* in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
|
||||
* heap-allocated structs with magic headers. */
|
||||
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
|
||||
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
|
||||
el_val_t local_datetime(el_val_t date, el_val_t time);
|
||||
el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
|
||||
|
||||
el_val_t local_date_year(el_val_t ld);
|
||||
el_val_t local_date_month(el_val_t ld);
|
||||
el_val_t local_date_day(el_val_t ld);
|
||||
el_val_t local_time_hour(el_val_t lt);
|
||||
el_val_t local_time_minute(el_val_t lt);
|
||||
el_val_t local_time_second(el_val_t lt);
|
||||
el_val_t local_time_nanos(el_val_t lt);
|
||||
|
||||
el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
|
||||
el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
|
||||
el_val_t el_local_date_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_local_date_eq(el_val_t a, el_val_t b);
|
||||
|
||||
/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
|
||||
* pointer in el_val_t; rhythms are immutable so callers may share them. */
|
||||
el_val_t rhythm_cycle_start(void);
|
||||
el_val_t rhythm_cycle_phase(el_val_t phase);
|
||||
el_val_t rhythm_duration(el_val_t d);
|
||||
el_val_t rhythm_session_start(void);
|
||||
el_val_t rhythm_event(el_val_t name);
|
||||
el_val_t rhythm_and(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_or(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_weekday(el_val_t day);
|
||||
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
|
||||
el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
|
||||
el_val_t rhythm_matches(el_val_t r, el_val_t ct);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t uuid_new(void);
|
||||
el_val_t uuid_v4(void);
|
||||
|
||||
/* ── Environment ─────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t env(el_val_t key);
|
||||
|
||||
/* ── In-process state K/V ────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t state_set(el_val_t key, el_val_t value);
|
||||
el_val_t state_get(el_val_t key);
|
||||
el_val_t state_del(el_val_t key);
|
||||
el_val_t state_keys(void);
|
||||
|
||||
/* ── Float formatting ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t float_to_str(el_val_t f);
|
||||
el_val_t int_to_float(el_val_t n);
|
||||
el_val_t float_to_int(el_val_t f);
|
||||
el_val_t format_float(el_val_t f, el_val_t decimals);
|
||||
el_val_t decimal_round(el_val_t f, el_val_t decimals);
|
||||
el_val_t str_to_float(el_val_t s);
|
||||
|
||||
/* ── Math (Float-aware) ──────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t math_sqrt(el_val_t f);
|
||||
el_val_t math_log(el_val_t f);
|
||||
el_val_t math_ln(el_val_t f);
|
||||
el_val_t math_sin(el_val_t f);
|
||||
el_val_t math_cos(el_val_t f);
|
||||
el_val_t math_pi(void);
|
||||
|
||||
/* ── String additions ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t str_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_split(el_val_t s, el_val_t sep);
|
||||
el_val_t str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t str_char_code(el_val_t s, el_val_t i);
|
||||
el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_format(el_val_t fmt, el_val_t data);
|
||||
el_val_t str_lower(el_val_t s);
|
||||
el_val_t str_upper(el_val_t s);
|
||||
|
||||
/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
|
||||
* Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex.
|
||||
* is_* predicates: empty input returns false; multi-char requires ALL bytes
|
||||
* to match. ASCII ranges only in Phase 1. */
|
||||
|
||||
/* Counting */
|
||||
el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */
|
||||
el_val_t str_count_chars(el_val_t s); /* codepoint count */
|
||||
el_val_t str_count_bytes(el_val_t s); /* alias of str_len */
|
||||
el_val_t str_count_lines(el_val_t s);
|
||||
el_val_t str_count_words(el_val_t s);
|
||||
el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */
|
||||
el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */
|
||||
|
||||
/* Find / position */
|
||||
el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */
|
||||
el_val_t str_last_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */
|
||||
|
||||
/* Transform */
|
||||
el_val_t str_repeat(el_val_t s, el_val_t n);
|
||||
el_val_t str_reverse(el_val_t s); /* by codepoint */
|
||||
el_val_t str_strip_prefix(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_strip_suffix(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_strip_chars(el_val_t s, el_val_t chars);
|
||||
el_val_t str_lstrip(el_val_t s);
|
||||
el_val_t str_rstrip(el_val_t s);
|
||||
|
||||
/* Char classification (Bool) */
|
||||
el_val_t is_letter(el_val_t s);
|
||||
el_val_t is_digit(el_val_t s);
|
||||
el_val_t is_alphanumeric(el_val_t s);
|
||||
el_val_t is_whitespace(el_val_t s);
|
||||
el_val_t is_punctuation(el_val_t s);
|
||||
el_val_t is_uppercase(el_val_t s);
|
||||
el_val_t is_lowercase(el_val_t s);
|
||||
|
||||
/* Split / join */
|
||||
el_val_t str_split_lines(el_val_t s);
|
||||
el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */
|
||||
el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n);
|
||||
el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */
|
||||
|
||||
/* ── List additions ──────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t list_push(el_val_t list, el_val_t elem);
|
||||
el_val_t list_push_front(el_val_t list, el_val_t elem);
|
||||
el_val_t list_join(el_val_t list, el_val_t sep);
|
||||
el_val_t list_range(el_val_t start, el_val_t end);
|
||||
|
||||
/* ── Bool helpers ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t bool_to_str(el_val_t b);
|
||||
|
||||
/* ── Numeric parsing ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t parse_int(el_val_t s, el_val_t default_val);
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
void exit_program(el_val_t code);
|
||||
el_val_t getpid_now(void);
|
||||
|
||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||
* Records the program's DHARMA identity before any other code executes. */
|
||||
|
||||
void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
||||
el_val_t network, el_val_t engram);
|
||||
|
||||
/* ── DHARMA network builtins ─────────────────────────────────────────────────
|
||||
* Available to CGI programs (declared with a `cgi {}` block).
|
||||
*
|
||||
* Peers are addressed by `dharma_id` of the form
|
||||
* "<registry-id>@<transport-url>" e.g. "ntn-genesis@http://localhost:7770"
|
||||
* If the @<url> portion is omitted, transport defaults to
|
||||
* "http://localhost:7770" (the local CGI daemon assumption).
|
||||
*
|
||||
* Wire protocol (all peers expose):
|
||||
* POST <url>/dharma/recv { channel, from, content } → response body
|
||||
* POST <url>/dharma/event { type, payload, source, timestamp }
|
||||
* POST <url>/api/activate { query } → list of nodes
|
||||
*
|
||||
* Hosting application's responsibility: an El program with a `cgi {}` block
|
||||
* runs http_serve() with its own request handler; that handler should route
|
||||
* "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so
|
||||
* incoming events feed dharma_field() queues. The runtime itself does not
|
||||
* intercept any /dharma path. */
|
||||
|
||||
el_val_t dharma_connect(el_val_t cgi_id);
|
||||
el_val_t dharma_send(el_val_t channel, el_val_t content);
|
||||
el_val_t dharma_activate(el_val_t query);
|
||||
void dharma_emit(el_val_t event_type, el_val_t payload);
|
||||
el_val_t dharma_field(el_val_t event_type);
|
||||
void dharma_strengthen(el_val_t cgi_id, el_val_t weight);
|
||||
el_val_t dharma_relationship(el_val_t cgi_id);
|
||||
el_val_t dharma_peers(void);
|
||||
|
||||
/* Public C API: called by an El program's HTTP handler when a /dharma/event
|
||||
* request arrives. Pushes onto the per-event-type queue and signals any
|
||||
* pending dharma_field() blockers. All three arguments must be NUL-terminated
|
||||
* C strings (or NULL — then treated as empty). */
|
||||
void el_runtime_dharma_event_arrive(const char* event_type,
|
||||
const char* payload,
|
||||
const char* source);
|
||||
|
||||
/* ── Engram local graph primitives ───────────────────────────────────────────
|
||||
* Operate on the CGI's local Engram knowledge graph.
|
||||
* `engram_activate` queries the local graph only; `dharma_activate` is
|
||||
* network-wide across all connected CGI graphs. */
|
||||
|
||||
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
|
||||
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags);
|
||||
/* Layered consciousness — see el_runtime.c for the layered architecture
|
||||
* design notes (search "Layered consciousness architecture"). The five
|
||||
* canonical layers (safety / core-identity / domain-knowledge / imprint /
|
||||
* suit) are seeded automatically; engram_add_layer extends the registry
|
||||
* with imprint or suit overlays at runtime. Nodes default to layer 1
|
||||
* (core-identity) when created via engram_node / engram_node_full. */
|
||||
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable);
|
||||
el_val_t engram_remove_layer(el_val_t layer_id);
|
||||
el_val_t engram_list_layers(void);
|
||||
el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
el_val_t engram_node_count(void);
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
|
||||
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
|
||||
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id);
|
||||
el_val_t engram_neighbors(el_val_t node_id);
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_edge_count(void);
|
||||
/* Three-pass activation: background fan-out → working-memory promotion →
|
||||
* Layer 0 override. See "Three-pass activation" in el_runtime.c. */
|
||||
el_val_t engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_save(el_val_t path);
|
||||
el_val_t engram_load(el_val_t path);
|
||||
|
||||
/* JSON-string accessors — return pre-serialized JSON so HTTP handlers
|
||||
* can pass results straight through without round-tripping ElList/ElMap
|
||||
* through json_stringify. */
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_stats_json(void);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||
* no nodes promoted to working memory. */
|
||||
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
||||
* All functions call https://api.anthropic.com/v1/messages with the API key
|
||||
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
|
||||
|
||||
el_val_t llm_call(el_val_t model, el_val_t prompt);
|
||||
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt);
|
||||
el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools);
|
||||
el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64);
|
||||
el_val_t llm_models(void);
|
||||
|
||||
/* Register a tool handler by name. The handler is looked up via dlsym
|
||||
* (mirroring http_set_handler), so any El `fn <name>(input)` compiles to
|
||||
* a global C symbol that this function can locate at runtime.
|
||||
* Handler signature: `el_val_t handler(el_val_t input_json)` — receives
|
||||
* the tool input as a JSON-string el_val_t and returns a JSON-string
|
||||
* el_val_t result. Used by llm_call_agentic. */
|
||||
void llm_register_tool(el_val_t name, el_val_t handler_fn_name);
|
||||
|
||||
/* ── args() ─────────────────────────────────────────────────────────────────
|
||||
* Provides access to command-line arguments passed to the program.
|
||||
* Populated by el_runtime_init_args() before main() runs. */
|
||||
|
||||
el_val_t args(void);
|
||||
void el_runtime_init_args(int argc, char** argv);
|
||||
|
||||
/* ── Crypto primitives ─────────────────────────────────────────────────────
|
||||
* SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe).
|
||||
* Self-contained — no OpenSSL/libcrypto dependency. The implementations are
|
||||
* adapted from public-domain reference code (Brad Conte / RFC 4648).
|
||||
*
|
||||
* Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string
|
||||
* value whose contents are raw binary; callers usually feed these into
|
||||
* base64_encode. Note that el_val_t strings are NUL-terminated by convention,
|
||||
* so the binary payload may contain embedded NULs — pass it directly into
|
||||
* base64_encode (which uses an explicit length) rather than treating it as
|
||||
* a printable C string.
|
||||
*
|
||||
* The "base64" variants emit/accept RFC 4648 standard alphabet with padding.
|
||||
* The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding,
|
||||
* as used in JWTs. */
|
||||
|
||||
el_val_t sha256_hex(el_val_t input);
|
||||
el_val_t sha256_bytes(el_val_t input);
|
||||
el_val_t hmac_sha256_hex(el_val_t key, el_val_t message);
|
||||
el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message);
|
||||
el_val_t base64_encode(el_val_t input);
|
||||
el_val_t base64_decode(el_val_t input);
|
||||
el_val_t base64url_encode(el_val_t input);
|
||||
el_val_t base64url_decode(el_val_t input);
|
||||
|
||||
/* Length-aware variants (internal — exposed for the rare caller that already
|
||||
* has a known-length binary buffer and doesn't want to round-trip through
|
||||
* a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed
|
||||
* these implicitly. */
|
||||
el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len);
|
||||
el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe);
|
||||
|
||||
/* ── Post-quantum primitives (liboqs-backed) ────────────────────────────────
|
||||
* All inputs/outputs hex-encoded. Algorithm choices:
|
||||
* Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced)
|
||||
* KEM: CRYSTALS-Kyber-768 (NIST level 3)
|
||||
* Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2)
|
||||
*
|
||||
* If liboqs is not linked (detected via __has_include(<oqs/oqs.h>) at compile
|
||||
* time), the pq_* entry points return a JSON-shaped error string so callers
|
||||
* fail loudly rather than silently fall back to classical schemes:
|
||||
* {"error":"liboqs not linked, post-quantum primitives unavailable"}
|
||||
*
|
||||
* The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and
|
||||
* CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss).
|
||||
* Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack,
|
||||
* Kyber holds. SHA3-256 also remains usable independent of liboqs (the
|
||||
* Keccak permutation is PQ-OK as a primitive). */
|
||||
|
||||
el_val_t pq_keygen_signature(void);
|
||||
el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message);
|
||||
el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex);
|
||||
|
||||
el_val_t pq_kem_keygen(void);
|
||||
el_val_t pq_kem_encaps(el_val_t public_key_hex);
|
||||
el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex);
|
||||
|
||||
el_val_t pq_hybrid_keygen(void);
|
||||
el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined);
|
||||
|
||||
el_val_t sha3_256_hex(el_val_t input);
|
||||
|
||||
/* ── AEAD: AES-256-GCM (libcrypto-backed) ───────────────────────────────────
|
||||
* Symmetric authenticated encryption used to wrap envelopes after a KEM
|
||||
* handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the
|
||||
* Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256.
|
||||
*
|
||||
* aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where
|
||||
* ciphertext is the AES-256-GCM output with the 16-byte auth tag appended.
|
||||
* Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which
|
||||
* structurally rules out the GCM nonce-reuse footgun.
|
||||
*
|
||||
* aead_decrypt returns the plaintext String, or "" on any failure (including
|
||||
* auth-tag mismatch). Callers MUST check for "" before trusting the result. */
|
||||
el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext);
|
||||
el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex);
|
||||
|
||||
/* ── Native VM builtin aliases (for compiled El source) ─────────────────────
|
||||
* These match the El VM's native_* builtins so that El source compiled
|
||||
* to C can call the same names without modification. */
|
||||
|
||||
el_val_t native_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t native_list_len(el_val_t list);
|
||||
el_val_t native_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t native_list_empty(void);
|
||||
el_val_t native_list_clone(el_val_t list);
|
||||
el_val_t native_string_chars(el_val_t s);
|
||||
el_val_t native_int_to_str(el_val_t n);
|
||||
|
||||
/* ── Method-call shorthand aliases ──────────────────────────────────────────
|
||||
* The El method-call convention `obj.method(args)` compiles to
|
||||
* `method(obj, args)`. These aliases expose the runtime functions under
|
||||
* the short names that result from method calls in El source.
|
||||
*
|
||||
* Example: `myList.append(x)` → `append(myList, x)` (calls this alias)
|
||||
* `myList.len()` → `len(myList)` (calls this alias) */
|
||||
|
||||
el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */
|
||||
el_val_t len(el_val_t list); /* el_list_len */
|
||||
el_val_t get(el_val_t list, el_val_t index); /* el_list_get */
|
||||
el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */
|
||||
el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
|
||||
|
||||
/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */
|
||||
/* See bottom of el_runtime.c for the implementation.
|
||||
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
|
||||
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
|
||||
/* ── Subprocess execution ────────────────────────────────────────────────── */
|
||||
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
|
||||
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+234
-2
@@ -135,8 +135,9 @@ fn emit_blank() -> Void {
|
||||
fn binop_to_c(op: String) -> String {
|
||||
if op == "Plus" { return "+" }
|
||||
if op == "Minus" { return "-" }
|
||||
if op == "Star" { return "*" }
|
||||
if op == "Slash" { return "/" }
|
||||
if op == "Star" { return "*" }
|
||||
if op == "Slash" { return "/" }
|
||||
if op == "Percent" { return "%" }
|
||||
if op == "EqEq" { return "==" }
|
||||
if op == "NotEq" { return "!=" }
|
||||
if op == "Lt" { return "<" }
|
||||
@@ -861,6 +862,97 @@ fn cg_match(expr: Map<String, Any>) -> String {
|
||||
str_join(parts, "")
|
||||
}
|
||||
|
||||
// Lower a match statement (used for side effects, not as an expression) to a
|
||||
// chain of C if/else if/else blocks. The subject is evaluated once into a
|
||||
// scoped temporary; each arm generates a condition and a braced body; the
|
||||
// wildcard/binding arm becomes the final `else` branch.
|
||||
//
|
||||
// Pattern dispatch:
|
||||
// LitStr -> str_eq(subj, EL_STR("..."))
|
||||
// LitInt -> subj == N
|
||||
// LitBool -> subj == 1 / subj == 0
|
||||
// Binding -> else { el_val_t name = subj; <body> }
|
||||
// Wildcard -> else { <body> }
|
||||
fn cg_match_stmt(expr: Map<String, Any>, indent: String, declared: [String]) -> Void {
|
||||
let subject = expr["subject"]
|
||||
let arms = expr["arms"]
|
||||
let subj_c: String = cg_expr(subject)
|
||||
let id: String = next_match_id()
|
||||
let subj_var: String = "_match_subj_" + id
|
||||
let inner: String = indent + " "
|
||||
emit_line(indent + "{")
|
||||
emit_line(inner + "el_val_t " + subj_var + " = " + subj_c + ";")
|
||||
let n: Int = native_list_len(arms)
|
||||
let i = 0
|
||||
let first_cond: Bool = true
|
||||
while i < n {
|
||||
let arm = native_list_get(arms, i)
|
||||
let pat = arm["pattern"]
|
||||
let body = arm["body"]
|
||||
let pkind: String = pat["pattern"]
|
||||
let body_c: String = cg_expr(body)
|
||||
if str_eq(pkind, "LitStr") {
|
||||
let v: String = pat["value"]
|
||||
let cond_str = "str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))"
|
||||
if first_cond {
|
||||
emit_line(inner + "if (" + cond_str + ") {")
|
||||
let first_cond = false
|
||||
} else {
|
||||
emit_line(inner + "} else if (" + cond_str + ") {")
|
||||
}
|
||||
emit_line(inner + " " + body_c + ";")
|
||||
} else {
|
||||
if str_eq(pkind, "LitInt") {
|
||||
let v: String = pat["value"]
|
||||
let cond_str = subj_var + " == " + v
|
||||
if first_cond {
|
||||
emit_line(inner + "if (" + cond_str + ") {")
|
||||
let first_cond = false
|
||||
} else {
|
||||
emit_line(inner + "} else if (" + cond_str + ") {")
|
||||
}
|
||||
emit_line(inner + " " + body_c + ";")
|
||||
} else {
|
||||
if str_eq(pkind, "LitBool") {
|
||||
let v: String = pat["value"]
|
||||
let bv = "0"
|
||||
if str_eq(v, "true") {
|
||||
let bv = "1"
|
||||
}
|
||||
let cond_str = subj_var + " == " + bv
|
||||
if first_cond {
|
||||
emit_line(inner + "if (" + cond_str + ") {")
|
||||
let first_cond = false
|
||||
} else {
|
||||
emit_line(inner + "} else if (" + cond_str + ") {")
|
||||
}
|
||||
emit_line(inner + " " + body_c + ";")
|
||||
} else {
|
||||
// Wildcard or Binding - becomes the else branch
|
||||
if first_cond {
|
||||
emit_line(inner + "{")
|
||||
} else {
|
||||
emit_line(inner + "} else {")
|
||||
}
|
||||
if str_eq(pkind, "Binding") {
|
||||
let bname: String = pat["name"]
|
||||
emit_line(inner + " el_val_t " + bname + " = " + subj_var + ";")
|
||||
}
|
||||
emit_line(inner + " " + body_c + ";")
|
||||
emit_line(inner + "}")
|
||||
let first_cond = true
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
// Close any open if/else-if chain (only reached when last arm was a literal pattern)
|
||||
if !first_cond {
|
||||
emit_line(inner + "}")
|
||||
}
|
||||
emit_line(indent + "}")
|
||||
}
|
||||
|
||||
// -- If-as-expression codegen -------------------------------------------------
|
||||
//
|
||||
// Lower `if cond { thenBody } else { elseBody }` used in expression position
|
||||
@@ -1078,6 +1170,16 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "Break" {
|
||||
emit_line(indent + "break;")
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "Continue" {
|
||||
emit_line(indent + "continue;")
|
||||
return declared
|
||||
}
|
||||
|
||||
// Bare reassignment: `name = expr`. Always emits a plain C assignment
|
||||
// (no `el_val_t` prefix) - by construction the parser only produces
|
||||
// Assign for an existing identifier. If the name happens NOT to be in
|
||||
@@ -1103,6 +1205,10 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
cg_for_stmt(val, indent, declared)
|
||||
return declared
|
||||
}
|
||||
if val_kind == "Match" {
|
||||
cg_match_stmt(val, indent, declared)
|
||||
return declared
|
||||
}
|
||||
let val_c: String = cg_expr(val)
|
||||
emit_line(indent + val_c + ";")
|
||||
return declared
|
||||
@@ -1130,6 +1236,28 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "ForRange" {
|
||||
let var_name: String = stmt["var"]
|
||||
let start_expr = stmt["start"]
|
||||
let end_expr = stmt["end"]
|
||||
let inclusive: Bool = stmt["inclusive"]
|
||||
let body = stmt["body"]
|
||||
let start_c: String = cg_expr(start_expr)
|
||||
let end_c: String = cg_expr(end_expr)
|
||||
// Loop variable introduced as a C local scoped to the for statement.
|
||||
// Body gets its own declared clone so let-bindings don't leak out.
|
||||
let body_decl = native_list_clone(declared)
|
||||
let body_decl = native_list_append(body_decl, var_name)
|
||||
if inclusive {
|
||||
emit_line(indent + "for (el_val_t " + var_name + " = " + start_c + "; " + var_name + " <= " + end_c + "; " + var_name + "++) {")
|
||||
} else {
|
||||
emit_line(indent + "for (el_val_t " + var_name + " = " + start_c + "; " + var_name + " < " + end_c + "; " + var_name + "++) {")
|
||||
}
|
||||
cg_stmts(body, indent + " ", body_decl)
|
||||
emit_line(indent + "}")
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "FnDef" { return declared }
|
||||
if kind == "TypeDef" { return declared }
|
||||
if kind == "EnumDef" { return declared }
|
||||
@@ -1760,6 +1888,12 @@ fn is_int_expr(expr: Map<String, Any>) -> Bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
if str_eq(op, "Percent") {
|
||||
if is_int_expr(expr["left"]) {
|
||||
if is_int_expr(expr["right"]) { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
@@ -1915,6 +2049,9 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "println") { return 1 }
|
||||
if str_eq(name, "print") { return 1 }
|
||||
if str_eq(name, "readline") { return 0 }
|
||||
// LSP seed primitives
|
||||
if str_eq(name, "__read_n") { return 1 }
|
||||
if str_eq(name, "__print_raw") { return 1 }
|
||||
// String
|
||||
if str_eq(name, "el_str_concat") { return 2 }
|
||||
if str_eq(name, "str_eq") { return 2 }
|
||||
@@ -1997,6 +2134,84 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "http_post_form_auth") { return 3 }
|
||||
if str_eq(name, "http_serve") { return 2 }
|
||||
if str_eq(name, "http_set_handler") { return 1 }
|
||||
// Seed primitives (__-prefix) — runtime/el_seed.c
|
||||
if str_eq(name, "__str_len") { return 1 }
|
||||
if str_eq(name, "__str_char_at") { return 2 }
|
||||
if str_eq(name, "__str_alloc") { return 1 }
|
||||
if str_eq(name, "__str_set_char") { return 3 }
|
||||
if str_eq(name, "__str_cmp") { return 2 }
|
||||
if str_eq(name, "__str_ncmp") { return 3 }
|
||||
if str_eq(name, "__str_concat_raw") { return 2 }
|
||||
if str_eq(name, "__str_slice_raw") { return 3 }
|
||||
if str_eq(name, "__int_to_str") { return 1 }
|
||||
if str_eq(name, "__str_to_int") { return 1 }
|
||||
if str_eq(name, "__float_to_str") { return 1 }
|
||||
if str_eq(name, "__str_to_float") { return 1 }
|
||||
if str_eq(name, "__println") { return 1 }
|
||||
if str_eq(name, "__print") { return 1 }
|
||||
if str_eq(name, "__readline") { return 0 }
|
||||
if str_eq(name, "__fs_read") { return 1 }
|
||||
if str_eq(name, "__fs_write") { return 2 }
|
||||
if str_eq(name, "__fs_exists") { return 1 }
|
||||
if str_eq(name, "__fs_list_raw") { return 1 }
|
||||
if str_eq(name, "__fs_mkdir") { return 1 }
|
||||
if str_eq(name, "__fs_write_bytes") { return 3 }
|
||||
if str_eq(name, "__http_do") { return 5 }
|
||||
if str_eq(name, "__http_do_map") { return 5 }
|
||||
if str_eq(name, "__http_do_to_file") { return 5 }
|
||||
if str_eq(name, "__http_serve") { return 2 }
|
||||
if str_eq(name, "__http_serve_v2") { return 2 }
|
||||
if str_eq(name, "__http_response") { return 3 }
|
||||
if str_eq(name, "__thread_create") { return 2 }
|
||||
if str_eq(name, "__thread_join") { return 1 }
|
||||
if str_eq(name, "__mutex_new") { return 0 }
|
||||
if str_eq(name, "__mutex_lock") { return 1 }
|
||||
if str_eq(name, "__mutex_unlock") { return 1 }
|
||||
if str_eq(name, "__exec") { return 1 }
|
||||
if str_eq(name, "__exec_bg") { return 1 }
|
||||
if str_eq(name, "__env_get") { return 1 }
|
||||
if str_eq(name, "__args_json") { return 0 }
|
||||
if str_eq(name, "__exit_program") { return 1 }
|
||||
if str_eq(name, "__time_now_ns") { return 0 }
|
||||
if str_eq(name, "__sleep_ms") { return 1 }
|
||||
if str_eq(name, "__uuid_v4") { return 0 }
|
||||
if str_eq(name, "__sqrt_f") { return 1 }
|
||||
if str_eq(name, "__log_f") { return 1 }
|
||||
if str_eq(name, "__ln_f") { return 1 }
|
||||
if str_eq(name, "__sin_f") { return 1 }
|
||||
if str_eq(name, "__cos_f") { return 1 }
|
||||
if str_eq(name, "__pi_f") { return 0 }
|
||||
if str_eq(name, "__state_set") { return 2 }
|
||||
if str_eq(name, "__state_get") { return 1 }
|
||||
if str_eq(name, "__state_del") { return 1 }
|
||||
if str_eq(name, "__state_keys") { return 0 }
|
||||
if str_eq(name, "__html_sanitize") { return 2 }
|
||||
if str_eq(name, "__url_encode") { return 1 }
|
||||
if str_eq(name, "__url_decode") { return 1 }
|
||||
if str_eq(name, "__json_get") { return 2 }
|
||||
if str_eq(name, "__json_get_raw") { return 2 }
|
||||
if str_eq(name, "__json_parse_map") { return 1 }
|
||||
if str_eq(name, "__json_stringify_val") { return 1 }
|
||||
if str_eq(name, "__json_array_len") { return 1 }
|
||||
if str_eq(name, "__json_array_get") { return 2 }
|
||||
if str_eq(name, "__json_array_get_string") { return 2 }
|
||||
if str_eq(name, "__json_set") { return 3 }
|
||||
if str_eq(name, "__engram_node") { return 3 }
|
||||
if str_eq(name, "__engram_node_full") { return 8 }
|
||||
if str_eq(name, "__engram_get_node") { return 1 }
|
||||
if str_eq(name, "__engram_strengthen") { return 1 }
|
||||
if str_eq(name, "__engram_forget") { return 1 }
|
||||
if str_eq(name, "__engram_node_count") { return 0 }
|
||||
if str_eq(name, "__engram_search") { return 2 }
|
||||
if str_eq(name, "__engram_scan_nodes") { return 2 }
|
||||
if str_eq(name, "__engram_connect") { return 4 }
|
||||
if str_eq(name, "__engram_edge_between") { return 2 }
|
||||
if str_eq(name, "__engram_neighbors") { return 1 }
|
||||
if str_eq(name, "__engram_neighbors_filtered") { return 3 }
|
||||
if str_eq(name, "__engram_activate") { return 2 }
|
||||
if str_eq(name, "__engram_activate_json") { return 2 }
|
||||
if str_eq(name, "__engram_scan_nodes_json") { return 2 }
|
||||
if str_eq(name, "__generate") { return 1 }
|
||||
// Filesystem
|
||||
if str_eq(name, "fs_read") { return 1 }
|
||||
if str_eq(name, "fs_write") { return 2 }
|
||||
@@ -2049,6 +2264,11 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "bool_to_str") { return 1 }
|
||||
// Process
|
||||
if str_eq(name, "exit_program") { return 1 }
|
||||
// Subprocess execution
|
||||
if str_eq(name, "exec_command") { return 1 }
|
||||
if str_eq(name, "exec_capture") { return 1 }
|
||||
if str_eq(name, "exec") { return 1 }
|
||||
if str_eq(name, "exec_bg") { return 1 }
|
||||
// CGI / DHARMA
|
||||
if str_eq(name, "dharma_connect") { return 1 }
|
||||
if str_eq(name, "dharma_send") { return 2 }
|
||||
@@ -2111,6 +2331,18 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "get") { return 2 }
|
||||
if str_eq(name, "map_get") { return 2 }
|
||||
if str_eq(name, "map_set") { return 3 }
|
||||
// Threading seed primitives
|
||||
if str_eq(name, "__thread_create") { return 2 }
|
||||
if str_eq(name, "__thread_join") { return 1 }
|
||||
if str_eq(name, "__mutex_new") { return 0 }
|
||||
if str_eq(name, "__mutex_lock") { return 1 }
|
||||
if str_eq(name, "__mutex_unlock") { return 1 }
|
||||
// Channel seed primitives
|
||||
if str_eq(name, "__channel_new") { return 1 }
|
||||
if str_eq(name, "__channel_send") { return 2 }
|
||||
if str_eq(name, "__channel_recv") { return 1 }
|
||||
if str_eq(name, "__channel_try_recv") { return 1 }
|
||||
if str_eq(name, "__channel_close") { return 1 }
|
||||
// -1 sentinel: variadic / unknown / user-defined -> no check.
|
||||
return -1
|
||||
}
|
||||
|
||||
+275
-29
@@ -1,4 +1,4 @@
|
||||
// lexer.el — el self-hosting lexer
|
||||
// lexer.el - el self-hosting lexer
|
||||
//
|
||||
// Tokenises an el source string into a list of token maps.
|
||||
// Each token is a Map<String, Any> with keys:
|
||||
@@ -8,9 +8,9 @@
|
||||
// Entry point: fn lex(source: String) -> [Map<String, Any>]
|
||||
//
|
||||
// Uses native_string_chars to split the source into a chars list,
|
||||
// then indexes it with native_list_get — avoids O(N²) string cloning.
|
||||
// then indexes it with native_list_get - avoids O(N-) string cloning.
|
||||
|
||||
// ── Character helpers ─────────────────────────────────────────────────────────
|
||||
// -- Character helpers ---------------------------------------------------------
|
||||
|
||||
fn lex_is_digit(ch: String) -> Bool {
|
||||
if ch == "0" { return true }
|
||||
@@ -101,7 +101,7 @@ fn make_tok(kind: String, value: String) -> Map<String, Any> {
|
||||
{ "kind": kind, "value": value }
|
||||
}
|
||||
|
||||
// ── Keyword lookup ────────────────────────────────────────────────────────────
|
||||
// -- Keyword lookup ------------------------------------------------------------
|
||||
|
||||
fn keyword_kind(word: String) -> String {
|
||||
if word == "let" { return "Let" }
|
||||
@@ -147,13 +147,15 @@ fn keyword_kind(word: String) -> String {
|
||||
if word == "accessor" { return "Accessor" }
|
||||
if word == "vessel" { return "Vessel" }
|
||||
if word == "extern" { return "Extern" }
|
||||
if word == "break" { return "Break" }
|
||||
if word == "continue" { return "Continue" }
|
||||
""
|
||||
}
|
||||
|
||||
// ── Scan helpers ──────────────────────────────────────────────────────────────
|
||||
// -- Scan helpers --------------------------------------------------------------
|
||||
// All scan helpers receive the chars list and total length.
|
||||
|
||||
// scan_digits — advance i while chars[i] is a digit
|
||||
// scan_digits - advance i while chars[i] is a digit
|
||||
// Returns { "text": ..., "pos": i }
|
||||
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
@@ -175,7 +177,7 @@ fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// scan_ident — advance i while chars[i] is alphanumeric or underscore
|
||||
// scan_ident - advance i while chars[i] is alphanumeric or underscore
|
||||
fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
@@ -196,14 +198,14 @@ fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// ── Code-bearing string detection + comment strip ────────────────────────────
|
||||
// Inline JS/CSS literals embedded in El source (e.g. <script>…</script> blobs
|
||||
// -- Code-bearing string detection + comment strip ----------------------------
|
||||
// Inline JS/CSS literals embedded in El source (e.g. <script>-</script> blobs
|
||||
// or stylesheet payloads inside string literals) carry their own line and
|
||||
// block comments. Those comments leak into the served HTML and reveal build
|
||||
// notes the visitor should never see. We strip them at the lexer so every
|
||||
// downstream consumer (codegen-c, codegen-js, parser) gets the cleaned form.
|
||||
//
|
||||
// looks_like_code — heuristic gate so we only strip strings that actually
|
||||
// looks_like_code - heuristic gate so we only strip strings that actually
|
||||
// embed JS or CSS. Plain prose, hex blobs, JSON, etc. pass through verbatim.
|
||||
|
||||
fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool {
|
||||
@@ -245,7 +247,7 @@ fn looks_like_code(s: String) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
// strip_code_comments — character-by-character walk. Tracks JS string state
|
||||
// strip_code_comments - character-by-character walk. Tracks JS string state
|
||||
// (single, double, backtick) and never strips inside one. Backslash escapes
|
||||
// inside JS strings consume the next char verbatim. URLs like https:// are
|
||||
// preserved by checking the previous char before treating // as a line
|
||||
@@ -398,7 +400,7 @@ fn strip_code_comments(s: String) -> String {
|
||||
str_join(out_parts, "")
|
||||
}
|
||||
|
||||
// scan_string — scan a quoted string literal, handling \" escapes.
|
||||
// scan_string - scan a quoted string literal, handling \" escapes.
|
||||
// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close }
|
||||
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
@@ -458,7 +460,239 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// ── Main lexer ────────────────────────────────────────────────────────────────
|
||||
// -- String interpolation ------------------------------------------------------
|
||||
//
|
||||
// scan_interp_brace - scan from `start` (the char after `${`) to the matching
|
||||
// `}`, tracking brace depth so inner braces (e.g. fn calls, map literals) are
|
||||
// handled correctly. Returns { "text": inner_source, "pos": i_after_close }.
|
||||
fn scan_interp_brace(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let depth = 1
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if ch == "{" {
|
||||
let depth = depth + 1
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "}" {
|
||||
let depth = depth - 1
|
||||
if depth <= 0 {
|
||||
// Closing brace of the interpolation - stop, do not include it
|
||||
let i = i + 1
|
||||
let running = false
|
||||
} else {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// interp_tokens_append_all - copy every token from src into dst, skipping the
|
||||
// trailing Eof sentinel that lex() always appends. Returns the updated dst list.
|
||||
fn interp_tokens_append_all(dst: [Map<String, Any>], src: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||
let src_len: Int = native_list_len(src)
|
||||
let j = 0
|
||||
let result = dst
|
||||
while j < src_len {
|
||||
let tok: Map<String, Any> = native_list_get(src, j)
|
||||
let tk: String = tok["kind"]
|
||||
if tk == "Eof" {
|
||||
let j = src_len
|
||||
} else {
|
||||
let result = native_list_append(result, tok)
|
||||
let j = j + 1
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// scan_interp_string - scan a string literal that may contain ${expr}
|
||||
// interpolations. Starts AFTER the opening `"`.
|
||||
// Returns { "tokens": [token list to inject], "pos": i_after_close_quote }.
|
||||
//
|
||||
// For a plain string (no ${}) this emits a single Str token, identical to the
|
||||
// old scan_string path. For an interpolated string it emits a flat sequence
|
||||
// of tokens equivalent to the string-concat expression, for example:
|
||||
//
|
||||
// "hello ${name}!"
|
||||
// => Str("hello ") Plus <tokens for name> Plus Str("!")
|
||||
//
|
||||
// Empty literal segments between adjacent ${ } blocks are omitted. The
|
||||
// resulting token stream is consumed by the existing parse_binop / parse_primary
|
||||
// path in the parser with zero parser changes required.
|
||||
//
|
||||
// Supported escape sequences: \" \n \t \r \\ \$ (literal dollar sign).
|
||||
// Nested quotes inside ${} are not supported; use a variable instead.
|
||||
fn scan_interp_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let out_tokens: [Map<String, Any>] = native_list_empty()
|
||||
let cur_part: [String] = native_list_empty()
|
||||
let has_interp = false
|
||||
let need_plus = false
|
||||
let running = true
|
||||
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
|
||||
if ch == "\\" {
|
||||
// Escape sequence
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "$" {
|
||||
// \$ => literal '$' (escape for interpolation syntax)
|
||||
let cur_part = native_list_append(cur_part, "$")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "\"" {
|
||||
let cur_part = native_list_append(cur_part, "\"")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "n" {
|
||||
let cur_part = native_list_append(cur_part, "\n")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "t" {
|
||||
let cur_part = native_list_append(cur_part, "\t")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "r" {
|
||||
let cur_part = native_list_append(cur_part, "\r")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "\\" {
|
||||
let cur_part = native_list_append(cur_part, "\\")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
let cur_part = native_list_append(cur_part, next_ch)
|
||||
let i = next_i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "\"" {
|
||||
// Closing quote - stop scanning
|
||||
let i = i + 1
|
||||
let running = false
|
||||
} else {
|
||||
if ch == "$" {
|
||||
// Check for ${ (start of interpolation)
|
||||
let next_i = i + 1
|
||||
let is_interp = false
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "{" {
|
||||
let is_interp = true
|
||||
}
|
||||
}
|
||||
if is_interp {
|
||||
// Flush the accumulated literal part (if non-empty)
|
||||
let part_len: Int = native_list_len(cur_part)
|
||||
if part_len > 0 {
|
||||
let part_text = str_join(cur_part, "")
|
||||
if need_plus {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
|
||||
}
|
||||
let clean_part = part_text
|
||||
if looks_like_code(part_text) {
|
||||
let clean_part = strip_code_comments(part_text)
|
||||
}
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_part))
|
||||
let need_plus = true
|
||||
}
|
||||
let cur_part = native_list_empty()
|
||||
let has_interp = true
|
||||
|
||||
// Scan brace-balanced expression source
|
||||
let brace_result = scan_interp_brace(chars, next_i + 1, total)
|
||||
let expr_src: String = brace_result["text"]
|
||||
let new_i: Int = brace_result["pos"]
|
||||
let i = new_i
|
||||
|
||||
// Re-lex the expression and inline the tokens.
|
||||
// Wrap in ( ) so that operators inside ${} (e.g.
|
||||
// age + 1) are parsed as a grouped sub-expression
|
||||
// rather than merging with the surrounding concat
|
||||
// Plus tokens at the wrong precedence level.
|
||||
let inner_toks: [Map<String, Any>] = lex(expr_src)
|
||||
let inner_len: Int = native_list_len(inner_toks)
|
||||
|
||||
if need_plus {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
|
||||
}
|
||||
// Empty interpolation ${} => empty string segment
|
||||
if inner_len <= 1 {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", ""))
|
||||
} else {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("LParen", "("))
|
||||
let out_tokens = interp_tokens_append_all(out_tokens, inner_toks)
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("RParen", ")"))
|
||||
}
|
||||
let need_plus = true
|
||||
} else {
|
||||
// Plain '$' not followed by '{' - treat as literal
|
||||
let cur_part = native_list_append(cur_part, "$")
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let cur_part = native_list_append(cur_part, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining literal segment and build final token list
|
||||
let part_text = str_join(cur_part, "")
|
||||
let part_len: Int = native_list_len(cur_part)
|
||||
if has_interp {
|
||||
// Interpolated string: only emit trailing segment if non-empty
|
||||
if part_len > 0 {
|
||||
let clean_part = part_text
|
||||
if looks_like_code(part_text) {
|
||||
let clean_part = strip_code_comments(part_text)
|
||||
}
|
||||
if need_plus {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
|
||||
}
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_part))
|
||||
}
|
||||
} else {
|
||||
// Plain string with no interpolation - same behaviour as old scan_string
|
||||
let clean_text = part_text
|
||||
if looks_like_code(part_text) {
|
||||
let clean_text = strip_code_comments(part_text)
|
||||
}
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_text))
|
||||
}
|
||||
|
||||
{ "tokens": out_tokens, "pos": i }
|
||||
}
|
||||
|
||||
// -- Main lexer ----------------------------------------------------------------
|
||||
|
||||
fn lex(source: String) -> [Map<String, Any>] {
|
||||
let chars: [String] = native_string_chars(source)
|
||||
@@ -503,20 +737,16 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
// String literal
|
||||
// String literal (plain or interpolated with ${expr} syntax).
|
||||
// scan_interp_string handles both cases: plain strings emit a
|
||||
// single Str token; interpolated strings emit a flat token
|
||||
// sequence (Str Plus expr-tokens Plus Str ...) that the parser
|
||||
// naturally assembles into a BinOp concat tree.
|
||||
if ch == "\"" {
|
||||
let result = scan_string(chars, i + 1, total)
|
||||
let str_text: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
// Compile-time scrub: strings that embed JS or CSS get
|
||||
// their // line comments and /* block comments stripped
|
||||
// before the token reaches the parser. Plain prose passes
|
||||
// through untouched.
|
||||
let clean_text = str_text
|
||||
if looks_like_code(str_text) {
|
||||
let clean_text = strip_code_comments(str_text)
|
||||
}
|
||||
let tokens = native_list_append(tokens, make_tok("Str", clean_text))
|
||||
let interp_result = scan_interp_string(chars, i + 1, total)
|
||||
let interp_toks: [Map<String, Any>] = interp_result["tokens"]
|
||||
let new_pos: Int = interp_result["pos"]
|
||||
let tokens = interp_tokens_append_all(tokens, interp_toks)
|
||||
let i = new_pos
|
||||
} else {
|
||||
// Number literal
|
||||
@@ -696,8 +926,24 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "." {
|
||||
let tokens = native_list_append(tokens, make_tok("Dot", "."))
|
||||
let i = i + 1
|
||||
// Check for ..= (inclusive range) before .. (exclusive range) before single .
|
||||
let peek2_i = i + 2
|
||||
let peek2_ch = ""
|
||||
if peek2_i < total {
|
||||
let peek2_ch: String = native_list_get(chars, peek2_i)
|
||||
}
|
||||
if peek_ch == "." {
|
||||
if peek2_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("DotDotEq", "..="))
|
||||
let i = i + 3
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("DotDot", ".."))
|
||||
let i = i + 2
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Dot", "."))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == ";" {
|
||||
let tokens = native_list_append(tokens, make_tok("Semicolon", ";"))
|
||||
@@ -711,7 +957,7 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
// unknown char — skip
|
||||
// unknown char - skip
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
+59
-21
@@ -1,4 +1,4 @@
|
||||
// parser.el — el self-hosting recursive descent parser
|
||||
// parser.el - el self-hosting recursive descent parser
|
||||
//
|
||||
// Consumes the token list produced by lexer.el and builds a list of AST
|
||||
// statement maps. Each statement and expression is a Map<String, Any>.
|
||||
@@ -11,7 +11,7 @@
|
||||
//
|
||||
// Entry point: fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>]
|
||||
|
||||
// ── Token access helpers ──────────────────────────────────────────────────────
|
||||
// -- Token access helpers ------------------------------------------------------
|
||||
|
||||
fn tok_at(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
native_list_get(tokens, pos)
|
||||
@@ -36,13 +36,13 @@ fn expect(tokens: [Map<String, Any>], pos: Int, kind: String) -> Int {
|
||||
pos + 1
|
||||
}
|
||||
|
||||
// ── Result helpers ────────────────────────────────────────────────────────────
|
||||
// -- Result helpers ------------------------------------------------------------
|
||||
|
||||
fn make_result(node: Map<String, Any>, pos: Int) -> Map<String, Any> {
|
||||
{ "node": node, "pos": pos }
|
||||
}
|
||||
|
||||
// ── Type annotation parser ────────────────────────────────────────────────────
|
||||
// -- Type annotation parser ----------------------------------------------------
|
||||
// Skips over a type annotation, returning the new position.
|
||||
// Types can be: Ident, [Type], Map<K,V>, Type?, Type<Type,...>
|
||||
|
||||
@@ -100,8 +100,8 @@ fn skip_type(tokens: [Map<String, Any>], pos: Int) -> Int {
|
||||
pos + 1
|
||||
}
|
||||
|
||||
// ── Parameter list ────────────────────────────────────────────────────────────
|
||||
// Parses (name: Type, name: Type, ...) — returns { "params": [...], "pos": ... }
|
||||
// -- Parameter list ------------------------------------------------------------
|
||||
// Parses (name: Type, name: Type, ...) - returns { "params": [...], "pos": ... }
|
||||
|
||||
fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let p = expect(tokens, pos, "LParen")
|
||||
@@ -140,7 +140,7 @@ fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
{ "params": params, "pos": p }
|
||||
}
|
||||
|
||||
// ── Expression parsing ────────────────────────────────────────────────────────
|
||||
// -- Expression parsing --------------------------------------------------------
|
||||
|
||||
fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
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
|
||||
// are parsing a head expression, they set __no_block_expr=1 so a stray
|
||||
// `{` here doesn't get gobbled as a Map literal — it belongs to the
|
||||
// `{` here doesn't get gobbled as a Map literal - it belongs to the
|
||||
// following block. Without this, `if a || b { ... }` could mis-parse
|
||||
// (the `||` recursion lands at `{` and tries to read the if-body as a
|
||||
// Map, then loops forever when keys don't match `Str: expr`).
|
||||
if k == "LBrace" {
|
||||
let no_block: String = state_get("__no_block_expr")
|
||||
if str_eq(no_block, "1") {
|
||||
// Fall through to fallback — caller will see `{` and treat it
|
||||
// Fall through to fallback - caller will see `{` and treat it
|
||||
// as the start of the block they're expecting.
|
||||
return make_result({ "expr": "Nil" }, pos)
|
||||
}
|
||||
@@ -300,7 +300,7 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
// token kinds for the deploy/retry DSLs, but they're also valid as
|
||||
// parameter names and local variables. When one of these appears in
|
||||
// expression position (where only an Ident makes sense), treat it as
|
||||
// an Ident carrying the original text — otherwise references to a
|
||||
// an Ident carrying the original text - otherwise references to a
|
||||
// parameter named `target` compile to EL_NULL.
|
||||
if k == "Target" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
||||
if k == "To" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
||||
@@ -472,9 +472,9 @@ fn parse_block(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
{ "stmts": stmts, "pos": p }
|
||||
}
|
||||
|
||||
// ── Postfix expressions (calls, field access, index) ─────────────────────────
|
||||
// -- Postfix expressions (calls, field access, index) -------------------------
|
||||
|
||||
// is_duration_unit — recognise the postfix unit suffix on a numeric literal.
|
||||
// is_duration_unit - recognise the postfix unit suffix on a numeric literal.
|
||||
// Used by parse_postfix to detect `30.seconds`-shape time literals before
|
||||
// falling back to the generic `obj.field` field-access lowering. Singular
|
||||
// and plural forms map to the same nanosecond multiplier; codegen does the
|
||||
@@ -578,7 +578,7 @@ fn parse_postfix(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
make_result(node, p)
|
||||
}
|
||||
|
||||
// ── Binary expression precedence climbing ────────────────────────────────────
|
||||
// -- Binary expression precedence climbing ------------------------------------
|
||||
|
||||
fn op_precedence(kind: String) -> Int {
|
||||
if kind == "Or" { return 1 }
|
||||
@@ -593,6 +593,7 @@ fn op_precedence(kind: String) -> Int {
|
||||
if kind == "Minus" { return 5 }
|
||||
if kind == "Star" { return 6 }
|
||||
if kind == "Slash" { return 6 }
|
||||
if kind == "Percent" { return 6 }
|
||||
0
|
||||
}
|
||||
|
||||
@@ -609,6 +610,7 @@ fn is_binop(kind: String) -> Bool {
|
||||
if kind == "Minus" { return true }
|
||||
if kind == "Star" { return true }
|
||||
if kind == "Slash" { return true }
|
||||
if kind == "Percent" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
@@ -641,7 +643,7 @@ fn parse_expr(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
parse_binop(tokens, pos, 1)
|
||||
}
|
||||
|
||||
// ── Statement parsing ─────────────────────────────────────────────────────────
|
||||
// -- Statement parsing ---------------------------------------------------------
|
||||
|
||||
fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let k = tok_kind(tokens, pos)
|
||||
@@ -653,7 +655,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let p = p + 1
|
||||
let ltype = ""
|
||||
let k2 = tok_kind(tokens, p)
|
||||
// optional type annotation: name: Type — capture the leading
|
||||
// optional type annotation: name: Type - capture the leading
|
||||
// identifier so codegen can dispatch arithmetic vs concat on
|
||||
// `+` between two typed Idents.
|
||||
if k2 == "Colon" {
|
||||
@@ -687,7 +689,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
return make_result({ "stmt": "Return", "value": val }, p)
|
||||
}
|
||||
|
||||
// extern fn declaration (no body — forward declaration for separate compilation)
|
||||
// extern fn declaration (no body - forward declaration for separate compilation)
|
||||
if k == "Extern" {
|
||||
let p = pos + 1
|
||||
let k2: String = tok_kind(tokens, p)
|
||||
@@ -858,7 +860,17 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
return make_result({ "stmt": "While", "cond": cond, "body": body }, p)
|
||||
}
|
||||
|
||||
// for loop
|
||||
// break statement
|
||||
if k == "Break" {
|
||||
return make_result({ "stmt": "Break" }, pos + 1)
|
||||
}
|
||||
|
||||
// continue statement
|
||||
if k == "Continue" {
|
||||
return make_result({ "stmt": "Continue" }, pos + 1)
|
||||
}
|
||||
|
||||
// for loop (range or list iteration)
|
||||
if k == "For" {
|
||||
let p = pos + 1
|
||||
let item_name = tok_value(tokens, p)
|
||||
@@ -868,15 +880,41 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
state_set("__no_block_expr", "1")
|
||||
let r = parse_expr(tokens, p)
|
||||
state_set("__no_block_expr", prev_no_block)
|
||||
let list_expr = r["node"]
|
||||
let start_expr = r["node"]
|
||||
let p = r["pos"]
|
||||
// Check for range operator: .. (exclusive) or ..= (inclusive)
|
||||
let range_k = tok_kind(tokens, p)
|
||||
if range_k == "DotDot" {
|
||||
// exclusive range: for i in start..end
|
||||
let p = p + 1
|
||||
let r2 = parse_expr(tokens, p)
|
||||
let end_expr = r2["node"]
|
||||
let p = r2["pos"]
|
||||
let r3 = parse_block(tokens, p)
|
||||
let body = r3["stmts"]
|
||||
let p = r3["pos"]
|
||||
return make_result({ "stmt": "ForRange", "var": item_name, "start": start_expr, "end": end_expr, "inclusive": false, "body": body }, p)
|
||||
}
|
||||
if range_k == "DotDotEq" {
|
||||
// inclusive range: for i in start..=end
|
||||
let p = p + 1
|
||||
let r2 = parse_expr(tokens, p)
|
||||
let end_expr = r2["node"]
|
||||
let p = r2["pos"]
|
||||
let r3 = parse_block(tokens, p)
|
||||
let body = r3["stmts"]
|
||||
let p = r3["pos"]
|
||||
return make_result({ "stmt": "ForRange", "var": item_name, "start": start_expr, "end": end_expr, "inclusive": true, "body": body }, p)
|
||||
}
|
||||
// No range operator: regular for-in (list iteration)
|
||||
let list_expr = start_expr
|
||||
let r2 = parse_block(tokens, p)
|
||||
let body = r2["stmts"]
|
||||
let p = r2["pos"]
|
||||
return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p)
|
||||
}
|
||||
|
||||
// @decorator — capture decorator name and attach to following stmt
|
||||
// @decorator - capture decorator name and attach to following stmt
|
||||
if k == "At" {
|
||||
let p = pos + 1
|
||||
let dec_name = tok_value(tokens, p)
|
||||
@@ -1039,7 +1077,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
make_result({ "stmt": "Expr", "value": val }, p)
|
||||
}
|
||||
|
||||
// ── Top-level parse ────────────────────────────────────────────────────────────
|
||||
// -- Top-level parse ------------------------------------------------------------
|
||||
|
||||
fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||
let total: Int = native_list_len(tokens)
|
||||
@@ -1058,7 +1096,7 @@ fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||
let stmt = r["node"]
|
||||
let new_pos: Int = r["pos"]
|
||||
let stmts = native_list_append(stmts, stmt)
|
||||
// Guard against infinite loops — if pos didn't advance, force it
|
||||
// Guard against infinite loops - if pos didn't advance, force it
|
||||
if new_pos <= pos {
|
||||
let pos = pos + 1
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// elb.el — El Build Coordinator
|
||||
// elb.el - El Build Coordinator
|
||||
//
|
||||
// The build system for El programs. Written in El. Builds El.
|
||||
//
|
||||
@@ -16,11 +16,11 @@
|
||||
// 3. For each file: if .el is newer than .elh/.c, compile with elc --emit-header
|
||||
// 4. Link all .c files + el_runtime.c into the final binary
|
||||
//
|
||||
// Each module compiles independently — no 128K-line blobs.
|
||||
// Each module compiles independently - no 128K-line blobs.
|
||||
// Downstream compilations read .elh headers (function signatures only),
|
||||
// not source. Incremental: only recompile what changed.
|
||||
|
||||
// ── Flags ─────────────────────────────────────────────────────────────────────
|
||||
// -- Flags ---------------------------------------------------------------------
|
||||
|
||||
fn flag_bool(argv: [String], name: String) -> Bool {
|
||||
let n: Int = native_list_len(argv)
|
||||
@@ -47,7 +47,7 @@ fn flag_val(argv: [String], name: String, default_val: String) -> String {
|
||||
return default_val
|
||||
}
|
||||
|
||||
// ── Manifest parsing ──────────────────────────────────────────────────────────
|
||||
// -- Manifest parsing ----------------------------------------------------------
|
||||
//
|
||||
// Read the entry file from manifest.el:
|
||||
// build { entry "soul.el" }
|
||||
@@ -100,7 +100,7 @@ fn parse_manifest_name(src: String) -> String {
|
||||
return "out"
|
||||
}
|
||||
|
||||
// ── Path helpers ───────────────────────────────────────────────────────────────
|
||||
// -- Path helpers ---------------------------------------------------------------
|
||||
|
||||
fn dirname_of(path: String) -> String {
|
||||
let n: Int = str_len(path)
|
||||
@@ -148,14 +148,14 @@ fn file_is_newer(a: String, b: String) -> Bool {
|
||||
let cmd: String = "test -f " + b + " && test " + a + " -nt " + b + " && echo yes || echo no"
|
||||
let result: String = str_trim(exec_capture(cmd))
|
||||
if str_eq(result, "yes") { return true }
|
||||
// b doesn't exist — check with test -f
|
||||
// b doesn't exist - check with test -f
|
||||
let exist_cmd: String = "test -f " + b + " && echo exists || echo missing"
|
||||
let exist: String = str_trim(exec_capture(exist_cmd))
|
||||
if str_eq(exist, "missing") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// ── Import graph walker ────────────────────────────────────────────────────────
|
||||
// -- Import graph walker --------------------------------------------------------
|
||||
//
|
||||
// Walk import statements in each .el file to build the dependency graph.
|
||||
// Returns a list of absolute paths in topological order (deps before dependents).
|
||||
@@ -219,7 +219,7 @@ fn walk_imports(src_path: String, visited: [String], order: [String]) -> Map<Str
|
||||
return { "visited": visited, "order": order }
|
||||
}
|
||||
|
||||
// ── Build ──────────────────────────────────────────────────────────────────────
|
||||
// -- Build ----------------------------------------------------------------------
|
||||
|
||||
fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: Bool, verbose: Bool) -> Bool {
|
||||
let bname: String = basename_noext(src_path)
|
||||
@@ -246,13 +246,23 @@ fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: B
|
||||
println("elb: compile failed: " + src_path)
|
||||
return false
|
||||
}
|
||||
|
||||
// Move the generated .elh (written next to the source by elc) into
|
||||
// out_dir so that #include "module.elh" lines in the generated .c
|
||||
// files resolve correctly when cc is invoked with -I <out_dir>.
|
||||
let src_elh: String = path_with_ext(src_path, ".elh")
|
||||
let mv_cmd: String = "cp " + src_elh + " " + elh_out + " 2>/dev/null || true"
|
||||
exec_command(mv_cmd)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fn link_binary(c_files: [String], out_bin: String, runtime_path: String, dry_run: Bool) -> Bool {
|
||||
fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir: String, dry_run: Bool) -> Bool {
|
||||
let n: Int = native_list_len(c_files)
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "cc -O2 -I " + dirname_of(runtime_path))
|
||||
// Include both the runtime dir (for el_runtime.h) and the output dir
|
||||
// (for module.elh cross-module forward declarations).
|
||||
let parts = native_list_append(parts, "cc -O2 -fbracket-depth=1024 -I " + dirname_of(runtime_path) + " -I " + out_dir)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let f: String = native_list_get(c_files, i)
|
||||
@@ -260,7 +270,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, dry_run
|
||||
let i = i + 1
|
||||
}
|
||||
let parts = native_list_append(parts, runtime_path)
|
||||
let parts = native_list_append(parts, "-lcurl -lpthread")
|
||||
let parts = native_list_append(parts, "-lcurl -lpthread -lm")
|
||||
let parts = native_list_append(parts, "-o " + out_bin)
|
||||
let cmd: String = str_join(parts, " ")
|
||||
println(" link " + out_bin)
|
||||
@@ -273,7 +283,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, dry_run
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Main ───────────────────────────────────────────────────────────────────────
|
||||
// -- Main -----------------------------------------------------------------------
|
||||
|
||||
fn main() -> Void {
|
||||
let argv: [String] = args()
|
||||
@@ -311,7 +321,7 @@ fn main() -> Void {
|
||||
}
|
||||
}
|
||||
if str_eq(runtime_path, "") {
|
||||
println("elb: cannot locate el_runtime.c — use --runtime=PATH")
|
||||
println("elb: cannot locate el_runtime.c - use --runtime=PATH")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
@@ -359,11 +369,11 @@ fn main() -> Void {
|
||||
|
||||
// Link
|
||||
let out_bin: String = out_dir + "/" + pkg_name
|
||||
let linked: Bool = link_binary(c_files, out_bin, runtime_path, dry_run)
|
||||
let linked: Bool = link_binary(c_files, out_bin, runtime_path, out_dir, dry_run)
|
||||
if !linked {
|
||||
println("elb: link failed")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println("elb: done → " + out_bin)
|
||||
println("elb: done -> " + out_bin)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// epm — El Package Manager
|
||||
//
|
||||
// Manages vessels: publish, install, resolve dependencies.
|
||||
// Vessels are stored in Engram as nodes. epm reads the local manifest.el,
|
||||
// talks to Engram over HTTP, and writes installed vessels to .epm/vessels/.
|
||||
|
||||
package "epm" {
|
||||
version "0.1.0"
|
||||
description "El Package Manager — vessel registry, publish, install, resolve"
|
||||
authors ["Will Anderson <will@neurontechnologies.ai>"]
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/epm.el"
|
||||
output "dist/"
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
// epm/src/epm.el — El Package Manager entry point
|
||||
//
|
||||
// epm manages vessels: the deployable, versioned units of El code.
|
||||
// Vessels are stored in Engram (the graph database) and installed locally
|
||||
// under .epm/vessels/.
|
||||
//
|
||||
// Usage:
|
||||
// epm publish # publish current project to registry
|
||||
// epm install # install all dependencies from manifest.el
|
||||
// epm install <vessel> # install a specific vessel
|
||||
// epm list # list all vessels in the registry
|
||||
// epm info <vessel> # show metadata for a vessel
|
||||
// epm info <vessel> <version> # show metadata for a specific version
|
||||
//
|
||||
// Configuration (environment):
|
||||
// ENGRAM_URL — Engram base URL (default: http://localhost:8742)
|
||||
//
|
||||
// Import order: manifest.el, registry.el, install.el must be compiled first.
|
||||
// This file imports all three and is the build entry point.
|
||||
|
||||
import "manifest.el"
|
||||
import "registry.el"
|
||||
import "install.el"
|
||||
|
||||
// ── Subcommand handlers ───────────────────────────────────────────────────────
|
||||
|
||||
// cmd_publish reads manifest.el from the current directory and publishes
|
||||
// the vessel/package to the Engram registry.
|
||||
fn cmd_publish() -> Void {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
println("epm: no manifest.el found in current directory")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let name: String = manifest_name(src)
|
||||
if str_eq(name, "") {
|
||||
println("epm: manifest.el has no 'vessel' or 'package' declaration")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let version: String = manifest_version(src)
|
||||
if str_eq(version, "") {
|
||||
println("epm: manifest.el has no 'version' declaration")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let description: String = manifest_description(src)
|
||||
let entry: String = manifest_entry(src)
|
||||
let deps_json: String = manifest_deps(src)
|
||||
|
||||
println("epm: publishing " + name + " " + version + " ...")
|
||||
|
||||
let node_id: String = registry_publish(name, version, description, entry, deps_json)
|
||||
if str_eq(node_id, "") {
|
||||
println("epm: publish failed")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println("epm: published " + name + " " + version + " (node: " + node_id + ")")
|
||||
}
|
||||
|
||||
// cmd_install_named installs a specific vessel by name (with optional version).
|
||||
// Expects: install <name> or install <name>@<version>
|
||||
fn cmd_install_named(spec: String) -> Void {
|
||||
// spec may be "el-auth" or "el-auth@0.1.0"
|
||||
let at: Int = str_index_of(spec, "@")
|
||||
let vname: String = spec
|
||||
let vver: String = ""
|
||||
if at > 0 {
|
||||
let vname = str_slice(spec, 0, at)
|
||||
let vver = str_slice(spec, at + 1, str_len(spec))
|
||||
}
|
||||
|
||||
println("epm: installing " + vname + " ...")
|
||||
|
||||
let mk1: Int = exec_command("mkdir -p .epm/vessels")
|
||||
let ok: Bool = install_vessel(vname, vver)
|
||||
if !ok {
|
||||
println("epm: install failed")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// cmd_list prints all vessels registered in Engram.
|
||||
fn cmd_list() -> Void {
|
||||
println("epm: fetching vessel list from registry ...")
|
||||
let raw: String = registry_list()
|
||||
let n: Int = json_array_len(raw)
|
||||
|
||||
if n == 0 {
|
||||
println("epm: no vessels found in registry")
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: " + native_int_to_str(n) + " vessel(s) in registry:")
|
||||
println("")
|
||||
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
// Each element is a quoted+escaped content JSON blob
|
||||
let elem: String = json_array_get_string(raw, i)
|
||||
let vname: String = json_get_string(elem, "name")
|
||||
let vver: String = json_get_string(elem, "version")
|
||||
let vdesc: String = json_get_string(elem, "description")
|
||||
println(" " + vname + " " + vver + " — " + vdesc)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// cmd_info prints detailed metadata for a specific vessel.
|
||||
// argv is the full args list; idx is the position of the vessel name.
|
||||
fn cmd_info(argv: [String], idx: Int) -> Void {
|
||||
let argc: Int = native_list_len(argv)
|
||||
if idx >= argc {
|
||||
println("epm: usage: epm info <vessel> [version]")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let vname: String = native_list_get(argv, idx)
|
||||
let vver: String = ""
|
||||
if idx + 1 < argc {
|
||||
let vver = native_list_get(argv, idx + 1)
|
||||
}
|
||||
|
||||
let content: String = registry_find(vname, vver)
|
||||
if str_eq(content, "") {
|
||||
println("epm: vessel '" + vname + "' not found in registry")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let name: String = json_get_string(content, "name")
|
||||
let ver: String = json_get_string(content, "version")
|
||||
let desc: String = json_get_string(content, "description")
|
||||
let entry: String = json_get_string(content, "entry")
|
||||
let deps_raw: String = json_get_raw(content, "deps")
|
||||
|
||||
println("name: " + name)
|
||||
println("version: " + ver)
|
||||
println("description: " + desc)
|
||||
println("entry: " + entry)
|
||||
|
||||
let dep_n: Int = json_array_len(deps_raw)
|
||||
if dep_n == 0 {
|
||||
println("dependencies: (none)")
|
||||
} else {
|
||||
println("dependencies:")
|
||||
let di: Int = 0
|
||||
while di < dep_n {
|
||||
let dep: String = json_array_get(deps_raw, di)
|
||||
let dep_name: String = json_get_string(dep, "name")
|
||||
let dep_ver: String = json_get_string(dep, "version")
|
||||
println(" " + dep_name + " " + dep_ver)
|
||||
let di = di + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() -> Void {
|
||||
let argv: [String] = args()
|
||||
let argc: Int = native_list_len(argv)
|
||||
|
||||
// argv[0] is the program name; subcommand is argv[1]
|
||||
if argc < 2 {
|
||||
println("epm — El Package Manager")
|
||||
println("")
|
||||
println("usage:")
|
||||
println(" epm publish publish current project to registry")
|
||||
println(" epm install install all dependencies (from manifest.el)")
|
||||
println(" epm install <vessel> install a specific vessel")
|
||||
println(" epm install <vessel>@<ver> install a specific vessel at a version")
|
||||
println(" epm list list all vessels in the registry")
|
||||
println(" epm info <vessel> show vessel metadata")
|
||||
println(" epm info <vessel> <version> show metadata for a specific version")
|
||||
println("")
|
||||
println("configuration:")
|
||||
println(" ENGRAM_URL Engram base URL (default: http://localhost:8742)")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
let sub: String = native_list_get(argv, 1)
|
||||
|
||||
if str_eq(sub, "publish") {
|
||||
cmd_publish()
|
||||
return
|
||||
}
|
||||
|
||||
if str_eq(sub, "install") {
|
||||
if argc >= 3 {
|
||||
let spec: String = native_list_get(argv, 2)
|
||||
cmd_install_named(spec)
|
||||
} else {
|
||||
cmd_install()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if str_eq(sub, "list") {
|
||||
cmd_list()
|
||||
return
|
||||
}
|
||||
|
||||
if str_eq(sub, "info") {
|
||||
cmd_info(argv, 2)
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: unknown subcommand '" + sub + "'")
|
||||
println("run 'epm' with no arguments for usage")
|
||||
exit(1)
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// epm/src/install.el — dependency resolution and vessel installation
|
||||
//
|
||||
// Reads the current project's manifest.el, walks the full dependency graph
|
||||
// by fetching each dep from Engram, detects cycles, and produces a
|
||||
// topological install order (dependencies before dependents).
|
||||
//
|
||||
// Installed vessels land in: .epm/vessels/<name>/
|
||||
//
|
||||
// Cycle detection: each recursion carries a chain of names currently on
|
||||
// the call stack. If a dep already appears in the chain it's a cycle.
|
||||
//
|
||||
// Algorithm:
|
||||
// 1. Parse manifest.el to get direct deps
|
||||
// 2. For each dep, fetch its manifest from Engram
|
||||
// 3. Recurse into that dep's own deps (depth-first)
|
||||
// 4. Append dep to order after all its transitive deps
|
||||
// 5. Deduplicate: skip already-ordered vessels
|
||||
|
||||
// ── List helpers (operating on String lists) ──────────────────────────────────
|
||||
|
||||
// list_contains returns true if item appears in lst.
|
||||
fn list_contains(lst: [String], item: String) -> Bool {
|
||||
let n: Int = native_list_len(lst)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let v: String = native_list_get(lst, i)
|
||||
if str_eq(v, item) { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ── Dependency graph walker ───────────────────────────────────────────────────
|
||||
|
||||
// resolve_deps_recursive fetches a vessel's metadata from Engram and walks
|
||||
// its dependency list depth-first, building a topological order.
|
||||
//
|
||||
// Parameters:
|
||||
// name — vessel name to resolve
|
||||
// version — version string (may be "" for any)
|
||||
// chain — names currently on the recursion stack (cycle detection)
|
||||
// visited — names already appended to order (dedup)
|
||||
// order — accumulated result (list of "name:version" strings)
|
||||
//
|
||||
// Returns a Map with keys "visited", "order", and "ok" (Bool as String).
|
||||
fn resolve_deps_recursive(name: String, version: String, chain: [String], visited: [String], order: [String]) -> Map<String, Any> {
|
||||
let key: String = name + ":" + version
|
||||
|
||||
// Skip if already ordered
|
||||
if list_contains(visited, key) {
|
||||
return { "visited": visited, "order": order, "ok": "true" }
|
||||
}
|
||||
|
||||
// Cycle detection: if name is already in the call chain, it's a cycle
|
||||
if list_contains(chain, name) {
|
||||
println("epm: error: dependency cycle detected involving '" + name + "'")
|
||||
return { "visited": visited, "order": order, "ok": "false" }
|
||||
}
|
||||
|
||||
// Push this name onto the cycle-detection chain
|
||||
let chain = native_list_append(chain, name)
|
||||
|
||||
// Fetch vessel metadata from Engram
|
||||
let content: String = registry_find(name, version)
|
||||
if str_eq(content, "") {
|
||||
println("epm: error: vessel '" + name + "' version '" + version + "' not found in registry")
|
||||
return { "visited": visited, "order": order, "ok": "false" }
|
||||
}
|
||||
|
||||
// content is a JSON blob: {"name":..., "version":..., "deps":[...]}
|
||||
let actual_version: String = json_get_string(content, "version")
|
||||
let actual_key: String = name + ":" + actual_version
|
||||
let deps_json: String = json_get_raw(content, "deps")
|
||||
|
||||
// Walk this vessel's dependencies first (depth-first)
|
||||
let dep_count: Int = json_array_len(deps_json)
|
||||
let di: Int = 0
|
||||
while di < dep_count {
|
||||
let dep: String = json_array_get(deps_json, di)
|
||||
let dep_name: String = json_get_string(dep, "name")
|
||||
let dep_ver: String = json_get_string(dep, "version")
|
||||
|
||||
let r = resolve_deps_recursive(dep_name, dep_ver, chain, visited, order)
|
||||
let ok_str: String = r["ok"]
|
||||
if str_eq(ok_str, "false") {
|
||||
return r
|
||||
}
|
||||
let visited = r["visited"]
|
||||
let order = r["order"]
|
||||
|
||||
let di = di + 1
|
||||
}
|
||||
|
||||
// Now append this vessel (after all its deps)
|
||||
let visited = native_list_append(visited, actual_key)
|
||||
let order = native_list_append(order, actual_key)
|
||||
|
||||
return { "visited": visited, "order": order, "ok": "true" }
|
||||
}
|
||||
|
||||
// ── Install order ─────────────────────────────────────────────────────────────
|
||||
|
||||
// resolve_install_order reads manifest.el from the current directory and
|
||||
// returns a list of "name:version" strings in topological install order.
|
||||
//
|
||||
// Returns an empty list on failure (error is printed to stdout).
|
||||
fn resolve_install_order() -> [String] {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
println("epm: no manifest.el found in current directory")
|
||||
return native_list_empty()
|
||||
}
|
||||
|
||||
let deps_json: String = manifest_deps(src)
|
||||
let dep_count: Int = json_array_len(deps_json)
|
||||
if dep_count == 0 {
|
||||
println("epm: no dependencies declared in manifest.el")
|
||||
return native_list_empty()
|
||||
}
|
||||
|
||||
let chain: [String] = native_list_empty()
|
||||
let visited: [String] = native_list_empty()
|
||||
let order: [String] = native_list_empty()
|
||||
|
||||
let i: Int = 0
|
||||
while i < dep_count {
|
||||
let dep: String = json_array_get(deps_json, i)
|
||||
let dep_name: String = json_get_string(dep, "name")
|
||||
let dep_ver: String = json_get_string(dep, "version")
|
||||
|
||||
let r = resolve_deps_recursive(dep_name, dep_ver, chain, visited, order)
|
||||
let ok_str: String = r["ok"]
|
||||
if str_eq(ok_str, "false") {
|
||||
return native_list_empty()
|
||||
}
|
||||
let visited = r["visited"]
|
||||
let order = r["order"]
|
||||
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
// ── Installation ──────────────────────────────────────────────────────────────
|
||||
|
||||
// install_vessel fetches a vessel's source from Engram and writes it
|
||||
// to .epm/vessels/<name>/.
|
||||
//
|
||||
// Currently writes the content JSON blob as a manifest record so that
|
||||
// other tools can introspect what is installed. When Engram stores actual
|
||||
// source archives, this function would extract them.
|
||||
//
|
||||
// Returns true on success.
|
||||
fn install_vessel(name: String, version: String) -> Bool {
|
||||
let content: String = registry_find(name, version)
|
||||
if str_eq(content, "") {
|
||||
println("epm: error: cannot fetch '" + name + "' '" + version + "' from registry")
|
||||
return false
|
||||
}
|
||||
|
||||
let install_dir: String = ".epm/vessels/" + name
|
||||
let mkdir_ret: Int = exec_command("mkdir -p " + install_dir)
|
||||
|
||||
// Write the content blob as installed.json so downstream tools know what's there
|
||||
let dest: String = install_dir + "/installed.json"
|
||||
fs_write(dest, content)
|
||||
|
||||
println("epm: installed " + name + " " + version + " -> " + dest)
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Top-level install ─────────────────────────────────────────────────────────
|
||||
|
||||
// cmd_install resolves and installs all dependencies for the current project.
|
||||
fn cmd_install() -> Void {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
println("epm: no manifest.el found in current directory")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let pkg_name: String = manifest_name(src)
|
||||
println("epm: resolving dependencies for " + pkg_name)
|
||||
|
||||
let order: [String] = resolve_install_order()
|
||||
let n: Int = native_list_len(order)
|
||||
|
||||
if n == 0 {
|
||||
println("epm: nothing to install")
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: install order (" + native_int_to_str(n) + " vessels):")
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let entry: String = native_list_get(order, i)
|
||||
println(" " + entry)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Ensure .epm directory exists
|
||||
let mk2: Int = exec_command("mkdir -p .epm/vessels")
|
||||
|
||||
// Install each vessel in topological order
|
||||
let j: Int = 0
|
||||
while j < n {
|
||||
let kv: String = native_list_get(order, j)
|
||||
// Split "name:version" on the last colon
|
||||
let colon: Int = str_index_of(kv, ":")
|
||||
if colon < 0 {
|
||||
println("epm: error: malformed order entry: " + kv)
|
||||
exit(1)
|
||||
}
|
||||
let vname: String = str_slice(kv, 0, colon)
|
||||
let vver: String = str_slice(kv, colon + 1, str_len(kv))
|
||||
let ok: Bool = install_vessel(vname, vver)
|
||||
if !ok {
|
||||
println("epm: install failed at " + kv)
|
||||
exit(1)
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
println("epm: all dependencies installed")
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// epm/src/manifest.el — vessel and package manifest parser
|
||||
//
|
||||
// Parses manifest.el files in the vessel/package format:
|
||||
//
|
||||
// vessel "el-auth" {
|
||||
// version "0.1.0"
|
||||
// description "..."
|
||||
// authors ["..."]
|
||||
// edition "2026"
|
||||
// }
|
||||
//
|
||||
// dependencies {
|
||||
// el-platform "1.0"
|
||||
// el-identity "0.1"
|
||||
// }
|
||||
//
|
||||
// build {
|
||||
// entry "src/main.el"
|
||||
// output "dist/"
|
||||
// }
|
||||
//
|
||||
// Parsing is line-by-line string extraction, same approach as elb.el.
|
||||
// No AST, no grammar — sufficient for well-formed manifests.
|
||||
|
||||
// ── String extraction helpers ─────────────────────────────────────────────────
|
||||
|
||||
// extract_quoted extracts the first double-quoted token from a string.
|
||||
// Returns "" if no quoted token is found.
|
||||
// Example: extract_quoted("vessel \"el-auth\" {") -> "el-auth"
|
||||
fn extract_quoted(s: String) -> String {
|
||||
let first: Int = str_index_of(s, "\"")
|
||||
if first < 0 { return "" }
|
||||
let after: String = str_slice(s, first + 1, str_len(s))
|
||||
let close: Int = str_index_of(after, "\"")
|
||||
if close < 0 { return "" }
|
||||
return str_slice(after, 0, close)
|
||||
}
|
||||
|
||||
// ── Manifest field extraction ─────────────────────────────────────────────────
|
||||
|
||||
// manifest_name extracts the vessel or package name from manifest source.
|
||||
// Recognises both "vessel" and "package" block headers.
|
||||
fn manifest_name(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "vessel ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
if str_starts_with(t, "package ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_version extracts the version string from manifest source.
|
||||
fn manifest_version(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "version ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_description extracts the description string from manifest source.
|
||||
fn manifest_description(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "description ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_entry extracts the build entry file from manifest source.
|
||||
fn manifest_entry(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "entry ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_deps extracts dependencies as a JSON array of {"name":..., "version":...} objects.
|
||||
//
|
||||
// Scans lines inside the dependencies { ... } block.
|
||||
// Each dep line looks like:
|
||||
// el-platform "1.0"
|
||||
//
|
||||
// Returns a JSON array string, e.g.:
|
||||
// [{"name":"el-platform","version":"1.0"},{"name":"el-identity","version":"0.1"}]
|
||||
fn manifest_deps(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let in_deps: Bool = false
|
||||
let result: String = "["
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
|
||||
// Detect entry into dependencies block
|
||||
if str_eq(t, "dependencies {") {
|
||||
let in_deps = true
|
||||
let i = i + 1
|
||||
} else {
|
||||
if in_deps {
|
||||
// Detect exit from block
|
||||
if str_eq(t, "}") {
|
||||
let in_deps = false
|
||||
} else {
|
||||
// Each dep line: name "version"
|
||||
// Find first space to split name from quoted version
|
||||
let sp: Int = str_index_of(t, " ")
|
||||
if sp > 0 {
|
||||
let dep_name: String = str_slice(t, 0, sp)
|
||||
let rest: String = str_trim(str_slice(t, sp, str_len(t)))
|
||||
let dep_ver: String = extract_quoted(rest)
|
||||
if !str_eq(dep_name, "") {
|
||||
let entry: String = "{\"name\":\"" + dep_name + "\",\"version\":\"" + dep_ver + "\"}"
|
||||
if count == 0 {
|
||||
let result = result + entry
|
||||
} else {
|
||||
let result = result + "," + entry
|
||||
}
|
||||
let count = count + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
return result + "]"
|
||||
}
|
||||
|
||||
// manifest_is_vessel returns true if the manifest declares a vessel (not a package).
|
||||
fn manifest_is_vessel(src: String) -> Bool {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "vessel ") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// epm/src/registry.el — Engram-backed vessel registry
|
||||
//
|
||||
// Vessels are stored in Engram as nodes with a structured label scheme:
|
||||
//
|
||||
// label: "vessel:<name>:<version>" (used as the search key)
|
||||
// content: JSON blob with full vessel metadata
|
||||
// node_type: "Entity"
|
||||
// salience: 0.9 (vessels are high-salience, long-lived knowledge)
|
||||
//
|
||||
// All registry operations go over HTTP to Engram. The Engram URL is read
|
||||
// from the ENGRAM_URL environment variable; defaults to http://localhost:8742.
|
||||
//
|
||||
// Endpoints used:
|
||||
// POST /api/nodes — publish a vessel node
|
||||
// GET /api/search?q=... — find vessels by label prefix
|
||||
|
||||
// ── Engram URL ────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_url returns the base URL for Engram, with no trailing slash.
|
||||
fn registry_url() -> String {
|
||||
let u: String = config("ENGRAM_URL")
|
||||
if str_eq(u, "") { return "http://localhost:8742" }
|
||||
// Strip trailing slash if present
|
||||
let n: Int = str_len(u)
|
||||
if str_ends_with(u, "/") {
|
||||
return str_slice(u, 0, n - 1)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// ── Publish ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_publish stores a vessel node in Engram.
|
||||
//
|
||||
// Parameters:
|
||||
// name — vessel name (e.g. "el-auth")
|
||||
// version — semver string (e.g. "0.1.0")
|
||||
// description — human-readable description
|
||||
// entry — build entry file path
|
||||
// deps_json — JSON array of dep objects (from manifest_deps)
|
||||
//
|
||||
// Returns the created node ID on success, "" on failure.
|
||||
fn registry_publish(name: String, version: String, description: String, entry: String, deps_json: String) -> String {
|
||||
let label: String = "vessel:" + name + ":" + version
|
||||
|
||||
// Build the content JSON — all vessel metadata in one blob
|
||||
let esc_name: String = json_escape_string(name)
|
||||
let esc_ver: String = json_escape_string(version)
|
||||
let esc_desc: String = json_escape_string(description)
|
||||
let esc_entry: String = json_escape_string(entry)
|
||||
let content_json: String = "{\"name\":\"" + esc_name + "\",\"version\":\"" + esc_ver + "\",\"description\":\"" + esc_desc + "\",\"entry\":\"" + esc_entry + "\",\"deps\":" + deps_json + "}"
|
||||
|
||||
// Escape the content blob for embedding in the outer JSON
|
||||
let esc_label: String = json_escape_string(label)
|
||||
let esc_content: String = json_escape_string(content_json)
|
||||
|
||||
let body: String = "{\"label\":\"" + esc_label + "\",\"content\":\"" + esc_content + "\",\"node_type\":\"Entity\",\"salience\":0.9}"
|
||||
let url: String = registry_url() + "/api/nodes"
|
||||
|
||||
let resp: String = http_post_json(url, body)
|
||||
if str_eq(resp, "") {
|
||||
println("epm: error: Engram unreachable at " + url)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check for error in response
|
||||
let err: String = json_get_string(resp, "error")
|
||||
if !str_eq(err, "") {
|
||||
println("epm: error from Engram: " + err)
|
||||
return ""
|
||||
}
|
||||
|
||||
let node_id: String = json_get_string(resp, "id")
|
||||
return node_id
|
||||
}
|
||||
|
||||
// ── Find ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_find searches Engram for a specific vessel by name and version.
|
||||
//
|
||||
// Returns the content JSON blob for the vessel, or "" if not found.
|
||||
// When version is "" any version matching the name is accepted (first hit).
|
||||
fn registry_find(name: String, version: String) -> String {
|
||||
let query: String = "vessel:" + name
|
||||
if !str_eq(version, "") {
|
||||
let query = "vessel:" + name + ":" + version
|
||||
}
|
||||
|
||||
let enc_q: String = url_encode(query)
|
||||
let url: String = registry_url() + "/api/search?q=" + enc_q + "&limit=10"
|
||||
|
||||
let resp: String = http_get(url)
|
||||
if str_eq(resp, "") {
|
||||
println("epm: error: Engram unreachable at " + registry_url())
|
||||
return ""
|
||||
}
|
||||
|
||||
// Response is a JSON array of node objects
|
||||
let count: Int = json_array_len(resp)
|
||||
if count == 0 { return "" }
|
||||
|
||||
// Walk results to find exact match
|
||||
let i: Int = 0
|
||||
while i < count {
|
||||
let node: String = json_array_get(resp, i)
|
||||
let node_label: String = json_get_string(node, "label")
|
||||
let expected_label: String = "vessel:" + name
|
||||
if !str_eq(version, "") {
|
||||
let expected_label = "vessel:" + name + ":" + version
|
||||
}
|
||||
if str_eq(node_label, expected_label) {
|
||||
// The content field is an escaped JSON blob — decode it
|
||||
let raw_content: String = json_get_string(node, "content")
|
||||
return raw_content
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// No exact match; if version was unspecified return first result's content
|
||||
if str_eq(version, "") {
|
||||
let first: String = json_array_get(resp, 0)
|
||||
let first_label: String = json_get_string(first, "label")
|
||||
if str_starts_with(first_label, "vessel:" + name + ":") {
|
||||
return json_get_string(first, "content")
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_list returns all vessels from Engram as a JSON array.
|
||||
//
|
||||
// Each element in the returned array is a content JSON blob.
|
||||
// Returns "[]" when no vessels are found or Engram is unreachable.
|
||||
fn registry_list() -> String {
|
||||
let enc_q: String = url_encode("vessel:")
|
||||
let url: String = registry_url() + "/api/search?q=" + enc_q + "&limit=200"
|
||||
|
||||
let resp: String = http_get(url)
|
||||
if str_eq(resp, "") {
|
||||
println("epm: error: Engram unreachable at " + registry_url())
|
||||
return "[]"
|
||||
}
|
||||
|
||||
let count: Int = json_array_len(resp)
|
||||
if count == 0 { return "[]" }
|
||||
|
||||
// Collect content blobs for nodes whose label starts with "vessel:"
|
||||
let out: String = "["
|
||||
let added: Int = 0
|
||||
let i: Int = 0
|
||||
while i < count {
|
||||
let node: String = json_array_get(resp, i)
|
||||
let lbl: String = json_get_string(node, "label")
|
||||
if str_starts_with(lbl, "vessel:") {
|
||||
let content: String = json_get_string(node, "content")
|
||||
let esc: String = json_escape_string(content)
|
||||
if added == 0 {
|
||||
let out = out + "\"" + esc + "\""
|
||||
} else {
|
||||
let out = out + ",\"" + esc + "\""
|
||||
}
|
||||
let added = added + 1
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return out + "]"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -199,6 +199,19 @@ el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_p
|
||||
el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */
|
||||
el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
|
||||
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
|
||||
* el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML
|
||||
* cleaner. State-machine parser; tag/attribute names compared case-
|
||||
* insensitively against the allowlist; `<a href>` / `<… src>` URL schemes
|
||||
* validated (http, https, mailto, fragment-only, or relative); whole-
|
||||
* subtree drop for script / style / iframe / object / embed / form; HTML-
|
||||
* escapes free text outside dropped subtrees.
|
||||
*
|
||||
* The allowlist is JSON of the form
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
@@ -246,6 +259,146 @@ el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
|
||||
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
|
||||
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
|
||||
|
||||
/* ── Instant + Duration: first-class temporal types ──────────────────────────
|
||||
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
|
||||
* since the Unix epoch; Durations are signed nanoseconds. Type discipline
|
||||
* is enforced at codegen-time: BinOps on names registered as Instant or
|
||||
* Duration route through the typed wrappers below; mismatches like
|
||||
* Instant+Instant become #error at the C compiler.
|
||||
*
|
||||
* Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are
|
||||
* recognised by the parser as DurationLit AST nodes and lowered to literal
|
||||
* int64 nanoseconds at codegen time. The runtime never sees the units. */
|
||||
|
||||
el_val_t el_now_instant(void);
|
||||
el_val_t now(void);
|
||||
el_val_t unix_seconds(el_val_t n);
|
||||
el_val_t unix_millis(el_val_t n);
|
||||
el_val_t instant_from_iso8601(el_val_t s);
|
||||
|
||||
el_val_t el_duration_from_nanos(el_val_t ns);
|
||||
el_val_t duration_seconds(el_val_t n);
|
||||
el_val_t duration_millis(el_val_t n);
|
||||
el_val_t duration_nanos(el_val_t n);
|
||||
|
||||
el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_diff(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_add(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_sub(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_scale(el_val_t dur, el_val_t scalar);
|
||||
el_val_t el_duration_div(el_val_t dur, el_val_t scalar);
|
||||
|
||||
el_val_t el_instant_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ne(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ne(el_val_t a, el_val_t b);
|
||||
|
||||
el_val_t instant_to_unix_seconds(el_val_t i);
|
||||
el_val_t instant_to_unix_millis(el_val_t i);
|
||||
el_val_t instant_to_iso8601(el_val_t i);
|
||||
el_val_t duration_to_seconds(el_val_t d);
|
||||
el_val_t duration_to_millis(el_val_t d);
|
||||
el_val_t duration_to_nanos(el_val_t d);
|
||||
|
||||
el_val_t el_sleep_duration(el_val_t dur);
|
||||
el_val_t unix_timestamp(void);
|
||||
|
||||
el_val_t ttl_cache_set(el_val_t key, el_val_t value);
|
||||
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
|
||||
el_val_t ttl_cache_age(el_val_t key);
|
||||
|
||||
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
|
||||
* Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
|
||||
* zones, Gregorian, DST) is the user-facing default; MarsCalendar,
|
||||
* CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
|
||||
* domains.
|
||||
*
|
||||
* A Calendar interprets an Instant under a particular cycle convention and
|
||||
* produces a CalendarTime. CalendarTime carries the underlying Instant and
|
||||
* a back-pointer to its Calendar; arithmetic and formatting consult the
|
||||
* Calendar to convert ns since epoch into year/month/day/hour/minute/second
|
||||
* (or sol/phase, or cycle/phase, depending on kind).
|
||||
*
|
||||
* Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
|
||||
* LocalDateTime are heap-allocated structs whose pointers are cast into
|
||||
* el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
|
||||
* the kind safely. LocalTime is small enough to live in the int64 slot
|
||||
* directly (nanos since midnight, signed). */
|
||||
|
||||
/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
|
||||
* `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
|
||||
* offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
|
||||
* on first use of the owning EarthCalendar. */
|
||||
el_val_t zone(el_val_t id);
|
||||
el_val_t zone_utc(void);
|
||||
el_val_t zone_local(void);
|
||||
el_val_t zone_offset(el_val_t hours, el_val_t minutes);
|
||||
|
||||
/* Calendar constructors. Each returns an el_val_t pointer to a heap-
|
||||
* allocated, magic-tagged Calendar struct. Calendars are interned by
|
||||
* (kind, zone_id, period_ns, epoch_ns) so identical constructors return
|
||||
* the same pointer — equality is reference equality. */
|
||||
el_val_t earth_calendar(el_val_t z);
|
||||
el_val_t earth_calendar_default(void);
|
||||
el_val_t mars_calendar(void);
|
||||
el_val_t cycle_calendar(el_val_t period_dur);
|
||||
el_val_t no_cycle_calendar(void);
|
||||
el_val_t relative_calendar(el_val_t epoch_inst);
|
||||
|
||||
/* CalendarTime constructors and methods. Returns a heap-allocated struct
|
||||
* whose pointer fits in el_val_t. */
|
||||
el_val_t now_in(el_val_t cal);
|
||||
el_val_t in_calendar(el_val_t inst, el_val_t cal);
|
||||
el_val_t cal_format(el_val_t ct, el_val_t pattern);
|
||||
el_val_t cal_to_instant(el_val_t ct);
|
||||
el_val_t cal_cycle_phase(el_val_t ct);
|
||||
el_val_t cal_in(el_val_t ct, el_val_t cal);
|
||||
|
||||
/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
|
||||
* LocalTime carries nanoseconds since midnight as a signed int64 directly
|
||||
* in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
|
||||
* heap-allocated structs with magic headers. */
|
||||
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
|
||||
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
|
||||
el_val_t local_datetime(el_val_t date, el_val_t time);
|
||||
el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
|
||||
|
||||
el_val_t local_date_year(el_val_t ld);
|
||||
el_val_t local_date_month(el_val_t ld);
|
||||
el_val_t local_date_day(el_val_t ld);
|
||||
el_val_t local_time_hour(el_val_t lt);
|
||||
el_val_t local_time_minute(el_val_t lt);
|
||||
el_val_t local_time_second(el_val_t lt);
|
||||
el_val_t local_time_nanos(el_val_t lt);
|
||||
|
||||
el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
|
||||
el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
|
||||
el_val_t el_local_date_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_local_date_eq(el_val_t a, el_val_t b);
|
||||
|
||||
/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
|
||||
* pointer in el_val_t; rhythms are immutable so callers may share them. */
|
||||
el_val_t rhythm_cycle_start(void);
|
||||
el_val_t rhythm_cycle_phase(el_val_t phase);
|
||||
el_val_t rhythm_duration(el_val_t d);
|
||||
el_val_t rhythm_session_start(void);
|
||||
el_val_t rhythm_event(el_val_t name);
|
||||
el_val_t rhythm_and(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_or(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_weekday(el_val_t day);
|
||||
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
|
||||
el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
|
||||
el_val_t rhythm_matches(el_val_t r, el_val_t ct);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t uuid_new(void);
|
||||
@@ -288,10 +441,53 @@ el_val_t str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t str_char_code(el_val_t s, el_val_t i);
|
||||
el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_format(el_val_t template, el_val_t data);
|
||||
el_val_t str_format(el_val_t fmt, el_val_t data);
|
||||
el_val_t str_lower(el_val_t s);
|
||||
el_val_t str_upper(el_val_t s);
|
||||
|
||||
/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
|
||||
* Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex.
|
||||
* is_* predicates: empty input returns false; multi-char requires ALL bytes
|
||||
* to match. ASCII ranges only in Phase 1. */
|
||||
|
||||
/* Counting */
|
||||
el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */
|
||||
el_val_t str_count_chars(el_val_t s); /* codepoint count */
|
||||
el_val_t str_count_bytes(el_val_t s); /* alias of str_len */
|
||||
el_val_t str_count_lines(el_val_t s);
|
||||
el_val_t str_count_words(el_val_t s);
|
||||
el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */
|
||||
el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */
|
||||
|
||||
/* Find / position */
|
||||
el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */
|
||||
el_val_t str_last_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */
|
||||
|
||||
/* Transform */
|
||||
el_val_t str_repeat(el_val_t s, el_val_t n);
|
||||
el_val_t str_reverse(el_val_t s); /* by codepoint */
|
||||
el_val_t str_strip_prefix(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_strip_suffix(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_strip_chars(el_val_t s, el_val_t chars);
|
||||
el_val_t str_lstrip(el_val_t s);
|
||||
el_val_t str_rstrip(el_val_t s);
|
||||
|
||||
/* Char classification (Bool) */
|
||||
el_val_t is_letter(el_val_t s);
|
||||
el_val_t is_digit(el_val_t s);
|
||||
el_val_t is_alphanumeric(el_val_t s);
|
||||
el_val_t is_whitespace(el_val_t s);
|
||||
el_val_t is_punctuation(el_val_t s);
|
||||
el_val_t is_uppercase(el_val_t s);
|
||||
el_val_t is_lowercase(el_val_t s);
|
||||
|
||||
/* Split / join */
|
||||
el_val_t str_split_lines(el_val_t s);
|
||||
el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */
|
||||
el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n);
|
||||
el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */
|
||||
|
||||
/* ── List additions ──────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t list_push(el_val_t list, el_val_t elem);
|
||||
@@ -364,6 +560,19 @@ el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
|
||||
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags);
|
||||
/* Layered consciousness — see el_runtime.c for the layered architecture
|
||||
* design notes (search "Layered consciousness architecture"). The five
|
||||
* canonical layers (safety / core-identity / domain-knowledge / imprint /
|
||||
* suit) are seeded automatically; engram_add_layer extends the registry
|
||||
* with imprint or suit overlays at runtime. Nodes default to layer 1
|
||||
* (core-identity) when created via engram_node / engram_node_full. */
|
||||
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable);
|
||||
el_val_t engram_remove_layer(el_val_t layer_id);
|
||||
el_val_t engram_list_layers(void);
|
||||
el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
@@ -375,6 +584,8 @@ el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id);
|
||||
el_val_t engram_neighbors(el_val_t node_id);
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_edge_count(void);
|
||||
/* Three-pass activation: background fan-out → working-memory promotion →
|
||||
* Layer 0 override. See "Three-pass activation" in el_runtime.c. */
|
||||
el_val_t engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_save(el_val_t path);
|
||||
el_val_t engram_load(el_val_t path);
|
||||
@@ -385,9 +596,16 @@ el_val_t engram_load(el_val_t path);
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_stats_json(void);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||
* no nodes promoted to working memory. */
|
||||
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
||||
* All functions call https://api.anthropic.com/v1/messages with the API key
|
||||
@@ -476,6 +694,21 @@ el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined);
|
||||
|
||||
el_val_t sha3_256_hex(el_val_t input);
|
||||
|
||||
/* ── AEAD: AES-256-GCM (libcrypto-backed) ───────────────────────────────────
|
||||
* Symmetric authenticated encryption used to wrap envelopes after a KEM
|
||||
* handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the
|
||||
* Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256.
|
||||
*
|
||||
* aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where
|
||||
* ciphertext is the AES-256-GCM output with the 16-byte auth tag appended.
|
||||
* Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which
|
||||
* structurally rules out the GCM nonce-reuse footgun.
|
||||
*
|
||||
* aead_decrypt returns the plaintext String, or "" on any failure (including
|
||||
* auth-tag mismatch). Callers MUST check for "" before trusting the result. */
|
||||
el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext);
|
||||
el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex);
|
||||
|
||||
/* ── Native VM builtin aliases (for compiled El source) ─────────────────────
|
||||
* These match the El VM's native_* builtins so that El source compiled
|
||||
* to C can call the same names without modification. */
|
||||
@@ -502,6 +735,22 @@ el_val_t get(el_val_t list, el_val_t index); /* el_list_get */
|
||||
el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */
|
||||
el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
|
||||
|
||||
/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */
|
||||
/* See bottom of el_runtime.c for the implementation.
|
||||
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
|
||||
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
|
||||
/* ── Subprocess execution ────────────────────────────────────────────────── */
|
||||
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
|
||||
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// channel.el — Go-style channels for El
|
||||
//
|
||||
// Channels are the communication primitive for concurrent El programs.
|
||||
// Threads send values into a channel; other threads receive them.
|
||||
// Channels are typed by convention — all values are Strings.
|
||||
//
|
||||
// Backed by four seed primitives in el_runtime.c:
|
||||
// __channel_new(capacity) -> Int create channel; cap=0 = unbounded
|
||||
// __channel_send(ch, msg) push msg; blocks if bounded and full
|
||||
// __channel_recv(ch) -> String pop msg; blocks until available; "" on close
|
||||
// __channel_try_recv(ch) -> String non-blocking pop; "" if empty
|
||||
// __channel_close(ch) mark closed; wake all blocked recvers
|
||||
//
|
||||
// Usage:
|
||||
// let ch: Int = channel_new(10) // buffered channel, capacity 10
|
||||
// spawn("producer", int_to_str(ch))
|
||||
// let msg: String = channel_recv(ch)
|
||||
|
||||
// ── Core channel API ─────────────────────────────────────────────────────────
|
||||
|
||||
// channel_new — create a channel with the given buffer capacity.
|
||||
//
|
||||
// capacity: 0 = unbounded (never blocks sender)
|
||||
// N = bounded buffer of N messages (sender blocks when full)
|
||||
//
|
||||
// Returns a channel handle (Int) to pass to send/recv/close.
|
||||
fn channel_new(capacity: Int) -> Int {
|
||||
return __channel_new(capacity)
|
||||
}
|
||||
|
||||
// channel_send — send a message into the channel.
|
||||
//
|
||||
// Blocks if the channel is bounded and full.
|
||||
// No-op if the channel is already closed.
|
||||
fn channel_send(ch: Int, msg: String) {
|
||||
__channel_send(ch, msg)
|
||||
}
|
||||
|
||||
// channel_recv — receive the next message from the channel.
|
||||
//
|
||||
// Blocks until a message is available.
|
||||
// Returns "" when the channel is closed and all buffered messages are drained.
|
||||
// The "" sentinel signals end-of-stream to consumers in a loop.
|
||||
fn channel_recv(ch: Int) -> String {
|
||||
return __channel_recv(ch)
|
||||
}
|
||||
|
||||
// channel_try_recv — non-blocking receive.
|
||||
//
|
||||
// Returns the next message if one is available, or "" if the channel is empty.
|
||||
// Does not block. Callers must distinguish "" (empty) from a legitimate ""
|
||||
// message by convention — use a non-empty sentinel in the message protocol.
|
||||
fn channel_try_recv(ch: Int) -> String {
|
||||
return __channel_try_recv(ch)
|
||||
}
|
||||
|
||||
// channel_close — signal that no more messages will be sent.
|
||||
//
|
||||
// After close, channel_recv continues to drain buffered messages then
|
||||
// returns "" on every subsequent call. channel_send on a closed channel
|
||||
// is a no-op (the message is dropped).
|
||||
fn channel_close(ch: Int) {
|
||||
__channel_close(ch)
|
||||
}
|
||||
|
||||
// ── channel_pipeline ─────────────────────────────────────────────────────────
|
||||
|
||||
// channel_pipeline — producer/consumer pipeline with parallel workers.
|
||||
//
|
||||
// Reads messages from in_ch, applies fn_name to each, writes results to out_ch.
|
||||
// Spawns `workers` concurrent worker threads — each drains in_ch independently,
|
||||
// so messages are processed in arrival order within each worker but not globally.
|
||||
//
|
||||
// fn_name must be an El fn with signature (String) -> String.
|
||||
//
|
||||
// Call channel_close(in_ch) to signal EOF. Workers exit when they receive "".
|
||||
// The caller must also close out_ch after all workers finish (via join).
|
||||
//
|
||||
// let in_ch: Int = channel_new(0)
|
||||
// let out_ch: Int = channel_new(0)
|
||||
// channel_pipeline(in_ch, out_ch, "process_item", 4)
|
||||
// channel_send(in_ch, "work-1")
|
||||
// channel_close(in_ch)
|
||||
// let result: String = channel_recv(out_ch)
|
||||
fn channel_pipeline(in_ch: Int, out_ch: Int, fn_name: String, workers: Int) {
|
||||
let i: Int = 0
|
||||
while i < workers {
|
||||
let arg: String = "{\"in_ch\":" + int_to_str(in_ch) +
|
||||
",\"out_ch\":" + int_to_str(out_ch) +
|
||||
",\"fn\":\"" + fn_name + "\"}"
|
||||
let _tid: Int = spawn("_channel_worker", arg)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// _channel_worker — internal worker for channel_pipeline.
|
||||
//
|
||||
// Reads messages from in_ch until it receives "" (closed+empty), applies
|
||||
// fn_name to each, and writes results to out_ch. Runs in its own thread
|
||||
// (spawned by channel_pipeline).
|
||||
fn _channel_worker(arg: String) -> String {
|
||||
let in_ch: Int = str_to_int(json_get(arg, "in_ch"))
|
||||
let out_ch: Int = str_to_int(json_get(arg, "out_ch"))
|
||||
let fn_name: String = json_get(arg, "fn")
|
||||
let running: Bool = true
|
||||
while running {
|
||||
let msg: String = channel_recv(in_ch)
|
||||
if str_eq(msg, "") {
|
||||
let running = false
|
||||
} else {
|
||||
// Spawn fn_name in a child thread so it cannot block the worker loop.
|
||||
let tid: Int = spawn(fn_name, msg)
|
||||
let result: String = join(tid)
|
||||
channel_send(out_ch, result)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── channel_drain ────────────────────────────────────────────────────────────
|
||||
|
||||
// channel_drain — collect all messages from ch into a list.
|
||||
//
|
||||
// Reads until the channel is closed and empty (recv returns "").
|
||||
// Returns a [String] of all messages received.
|
||||
//
|
||||
// Typical usage: close the channel from the producer side, then call
|
||||
// channel_drain from the consumer to collect results.
|
||||
fn channel_drain(ch: Int) -> [String] {
|
||||
let results: [String] = el_list_empty()
|
||||
let running: Bool = true
|
||||
while running {
|
||||
let msg: String = channel_recv(ch)
|
||||
if str_eq(msg, "") {
|
||||
let running = false
|
||||
} else {
|
||||
let results = el_list_append(results, msg)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// ── channel_fan_out ───────────────────────────────────────────────────────────
|
||||
|
||||
// channel_fan_out — send every item in a list into a channel.
|
||||
//
|
||||
// items: [String] — items to send
|
||||
// ch: Int — destination channel
|
||||
//
|
||||
// Sends all items then closes the channel to signal end-of-stream.
|
||||
// Intended for the producer side of a pipeline:
|
||||
//
|
||||
// channel_fan_out(items, in_ch)
|
||||
// let results: [String] = channel_drain(out_ch)
|
||||
fn channel_fan_out(items: [String], ch: Int) {
|
||||
let n: Int = el_list_len(items)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
channel_send(ch, item)
|
||||
let i = i + 1
|
||||
}
|
||||
channel_close(ch)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// runtime/engram.el — El wrapper for the engram graph store
|
||||
//
|
||||
// Thin wrappers over the __engram_* seed primitives defined in el_seed.c.
|
||||
// Each function delegates directly to the corresponding seed — no logic here.
|
||||
// The seed layer owns all storage, indexing, and graph traversal.
|
||||
//
|
||||
// Dependencies: runtime/string.el, runtime/json.el
|
||||
|
||||
// --- Node creation ---
|
||||
|
||||
fn engram_node(content: String, node_type: String, salience: Float) -> String {
|
||||
return __engram_node(content, node_type, salience)
|
||||
}
|
||||
|
||||
fn engram_node_full(content: String, nt: String, sal: Float, imp: Float,
|
||||
source: String, lang: String, ts: Int, tags: String) -> String {
|
||||
return __engram_node_full(content, nt, sal, imp, source, lang, ts, tags)
|
||||
}
|
||||
|
||||
// --- Node retrieval ---
|
||||
|
||||
fn engram_get_node(id: String) -> String {
|
||||
return __engram_get_node(id)
|
||||
}
|
||||
|
||||
fn engram_node_count() -> Int {
|
||||
return __engram_node_count()
|
||||
}
|
||||
|
||||
// --- Node lifecycle ---
|
||||
|
||||
fn engram_strengthen(id: String) -> Bool {
|
||||
return __engram_strengthen(id)
|
||||
}
|
||||
|
||||
fn engram_forget(id: String) -> Bool {
|
||||
return __engram_forget(id)
|
||||
}
|
||||
|
||||
// --- Search and scan ---
|
||||
|
||||
fn engram_search(query: String, limit: Int) -> String {
|
||||
return __engram_search(query, limit)
|
||||
}
|
||||
|
||||
fn engram_scan_nodes(limit: Int, offset: Int) -> String {
|
||||
return __engram_scan_nodes(limit, offset)
|
||||
}
|
||||
|
||||
fn engram_scan_nodes_json(limit: Int, offset: Int) -> String {
|
||||
return __engram_scan_nodes_json(limit, offset)
|
||||
}
|
||||
|
||||
// --- Graph edges ---
|
||||
|
||||
fn engram_connect(from: String, to: String, rel: String, weight: Float) -> Bool {
|
||||
return __engram_connect(from, to, rel, weight)
|
||||
}
|
||||
|
||||
fn engram_edge_between(a: String, b: String) -> String {
|
||||
return __engram_edge_between(a, b)
|
||||
}
|
||||
|
||||
// --- Graph traversal ---
|
||||
|
||||
fn engram_neighbors(id: String) -> String {
|
||||
return __engram_neighbors(id)
|
||||
}
|
||||
|
||||
fn engram_neighbors_filtered(id: String, rel: String, min_w: Float) -> String {
|
||||
return __engram_neighbors_filtered(id, rel, min_w)
|
||||
}
|
||||
|
||||
fn engram_activate(query: String, depth: Int) -> String {
|
||||
return __engram_activate(query, depth)
|
||||
}
|
||||
|
||||
fn engram_activate_json(query: String, limit: Int) -> String {
|
||||
return __engram_activate_json(query, limit)
|
||||
}
|
||||
|
||||
// --- Generation ---
|
||||
|
||||
fn generate(form: String) -> String {
|
||||
return __generate(form)
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// runtime/env.el — environment and process
|
||||
// Covers: environment variables, command-line args, process exit, in-process
|
||||
// state store, UUID generation, and list convenience helpers.
|
||||
|
||||
// env — read an environment variable. Returns "" if the variable is not set.
|
||||
fn env(key: String) -> String {
|
||||
return __env_get(key)
|
||||
}
|
||||
|
||||
// args — command-line arguments as a list of strings.
|
||||
// __args_json returns a JSON array (e.g. ["prog","arg1","arg2"]).
|
||||
// The list is built by iterating over the array.
|
||||
fn args() -> [String] {
|
||||
let json: String = __args_json()
|
||||
let n: Int = json_array_len(json)
|
||||
let result: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = json_array_get_string(json, i)
|
||||
let result = el_list_append(result, item)
|
||||
let i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// exit_program — terminate the process with the given exit code.
|
||||
fn exit_program(code: Int) {
|
||||
__exit_program(code)
|
||||
}
|
||||
|
||||
// ── List convenience helpers ───────────────────────────────────────────────
|
||||
|
||||
// get — index into a list. Thin alias for el_list_get used throughout the
|
||||
// El stdlib so call sites read like `get(lst, i)` rather than the verbose form.
|
||||
fn get(lst: [String], i: Int) -> String {
|
||||
return el_list_get(lst, i)
|
||||
}
|
||||
|
||||
// len — length of a list.
|
||||
fn len(lst: [String]) -> Int {
|
||||
return el_list_len(lst)
|
||||
}
|
||||
|
||||
// ── In-process key-value state store ──────────────────────────────────────
|
||||
|
||||
// state_set — store a string value under key.
|
||||
fn state_set(key: String, val: String) {
|
||||
__state_set(key, val)
|
||||
}
|
||||
|
||||
// state_get — retrieve value for key; returns "" if key not present.
|
||||
fn state_get(key: String) -> String {
|
||||
return __state_get(key)
|
||||
}
|
||||
|
||||
// state_del — remove key from the store.
|
||||
fn state_del(key: String) {
|
||||
__state_del(key)
|
||||
}
|
||||
|
||||
// state_keys — all keys currently in the store as a JSON array string.
|
||||
fn state_keys() -> String {
|
||||
return __state_keys()
|
||||
}
|
||||
|
||||
// ── DHARMA runtime helpers ─────────────────────────────────────────────────
|
||||
|
||||
// config — read a configuration value from the environment.
|
||||
// Returns "" if the variable is not set. Alias for env().
|
||||
fn config(key: String) -> String {
|
||||
return __env_get(key)
|
||||
}
|
||||
|
||||
// log_info — write an [INFO] log line to stdout.
|
||||
fn log_info(msg: String) {
|
||||
__println("[INFO] " + msg)
|
||||
}
|
||||
|
||||
// log_warn — write a [WARN] log line to stdout.
|
||||
fn log_warn(msg: String) {
|
||||
__println("[WARN] " + msg)
|
||||
}
|
||||
|
||||
// list_len — return the number of elements in a list. Alias for el_list_len.
|
||||
fn list_len(lst: [String]) -> Int {
|
||||
return el_list_len(lst)
|
||||
}
|
||||
|
||||
// list_get — return the element at index i in a list. Alias for el_list_get.
|
||||
fn list_get(lst: [String], i: Int) -> String {
|
||||
return el_list_get(lst, i)
|
||||
}
|
||||
|
||||
// ── UUID generation ────────────────────────────────────────────────────────
|
||||
|
||||
// uuid_new — generate a new random UUID v4.
|
||||
fn uuid_new() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
// uuid_v4 — alias for uuid_new(); explicit version name for callers that
|
||||
// need to be precise about the UUID variant.
|
||||
fn uuid_v4() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// runtime/exec.el — subprocess execution
|
||||
// All four names resolve to the same seed primitives so callers can use
|
||||
// whichever name matches their mental model of the operation.
|
||||
|
||||
// exec — run a shell command, capture stdout, return as String.
|
||||
// Blocks until the subprocess exits (30-second wall-clock deadline in the
|
||||
// seed layer). Returns "" on any error.
|
||||
fn exec(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
|
||||
// exec_bg — fire-and-forget subprocess. Returns immediately; no stdout.
|
||||
fn exec_bg(cmd: String) {
|
||||
__exec_bg(cmd)
|
||||
}
|
||||
|
||||
// exec_command — alias for exec(); preferred when callers care about side
|
||||
// effects (e.g. invoking a build tool) rather than captured output.
|
||||
fn exec_command(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
|
||||
// exec_capture — alias for exec(); preferred when callers explicitly want
|
||||
// to capture and process stdout.
|
||||
fn exec_capture(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// runtime/fs.el — filesystem operations
|
||||
// Thin El wrappers over seed primitives; no logic beyond what is needed
|
||||
// to present a clean API. The heavy lifting lives in el_runtime.c.
|
||||
|
||||
fn fs_read(path: String) -> String {
|
||||
return __fs_read(path)
|
||||
}
|
||||
|
||||
fn fs_write(path: String, content: String) -> Bool {
|
||||
return __fs_write(path, content)
|
||||
}
|
||||
|
||||
fn fs_exists(path: String) -> Bool {
|
||||
return __fs_exists(path)
|
||||
}
|
||||
|
||||
fn fs_mkdir(path: String) -> Bool {
|
||||
return __fs_mkdir(path)
|
||||
}
|
||||
|
||||
fn fs_write_bytes(path: String, bytes: String, n: Int) -> Bool {
|
||||
return __fs_write_bytes(path, bytes, n)
|
||||
}
|
||||
|
||||
// fs_list — return list of filenames in a directory.
|
||||
// __fs_list_raw returns a newline-separated string (possibly with a trailing
|
||||
// newline); callers that need a clean list should filter empty strings.
|
||||
fn fs_list(path: String) -> [String] {
|
||||
let raw: String = __fs_list_raw(path)
|
||||
return str_split(raw, "\n")
|
||||
}
|
||||
|
||||
// fs_list_json — return a JSON array of filenames in a directory.
|
||||
// Empty strings produced by a trailing newline are stripped before encoding.
|
||||
fn fs_list_json(path: String) -> String {
|
||||
let items: [String] = fs_list(path)
|
||||
let n: Int = el_list_len(items)
|
||||
let clean: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let trimmed: String = str_trim(item)
|
||||
if !str_eq(trimmed, "") {
|
||||
let clean = el_list_append(clean, "\"" + trimmed + "\"")
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return json_build_array(clean)
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
// runtime/http.el — El HTTP client and server wrappers
|
||||
//
|
||||
// Thin El layer over seed primitives. All network I/O is performed by the
|
||||
// seed; this file provides the public API that El programs import.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __http_do(method, url, body, headers_json, timeout_ms) -> String
|
||||
// __http_do_to_file(method, url, body, headers_json, out_path) -> Bool
|
||||
// __http_do_map(method, url, body, headers_map, timeout_ms) -> String
|
||||
// __http_do_map_to_file(method, url, body, headers_map, out_path) -> Bool
|
||||
// __http_serve(port, handler_name)
|
||||
// __http_serve_v2(port, handler_name)
|
||||
// __http_response(status, headers_json, body) -> String
|
||||
// __env_get(key) -> String
|
||||
//
|
||||
// NOTE FOR SEED AGENT: __http_do_map and __http_do_map_to_file must be added
|
||||
// to the seed. They are identical to __http_do / __http_do_to_file except
|
||||
// they accept an ElMap directly for headers instead of a pre-serialised JSON
|
||||
// string. This avoids needing map iteration in El (which has no for-loop or
|
||||
// map iterator primitive). The seed implementation maps to headers_from_map()
|
||||
// in el_runtime.c.
|
||||
//
|
||||
// Other builtins used:
|
||||
// str_eq(a, b) -> Bool
|
||||
// str_to_int(s) -> Int
|
||||
|
||||
// ── Timeout helper ────────────────────────────────────────────────────────────
|
||||
|
||||
// el_http_timeout_ms returns the configured HTTP timeout in milliseconds.
|
||||
// Reads EL_HTTP_TIMEOUT_MS from the environment; defaults to 60000 (60s).
|
||||
// Returns 60000 if the env var is absent, empty, or non-positive.
|
||||
fn el_http_timeout_ms() -> Int {
|
||||
let v: String = __env_get("EL_HTTP_TIMEOUT_MS")
|
||||
if str_eq(v, "") { return 60000 }
|
||||
let n: Int = str_to_int(v)
|
||||
if n <= 0 { return 60000 }
|
||||
return n
|
||||
}
|
||||
|
||||
// ── HTTP client — simple variants ────────────────────────────────────────────
|
||||
|
||||
// http_get performs an HTTP GET request and returns the response body.
|
||||
// On transport failure the seed returns an error JSON fragment.
|
||||
fn http_get(url: String) -> String {
|
||||
return __http_do("GET", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post performs an HTTP POST request with the given body.
|
||||
// No Content-Type header is set; use http_post_json for JSON payloads.
|
||||
fn http_post(url: String, body: String) -> String {
|
||||
return __http_do("POST", url, body, "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_json performs an HTTP POST request with Content-Type:
|
||||
// application/json. body must be a valid JSON string.
|
||||
fn http_post_json(url: String, body: String) -> String {
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_delete performs an HTTP DELETE request and returns the response body.
|
||||
fn http_delete(url: String) -> String {
|
||||
return __http_do("DELETE", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — header map variants ────────────────────────────────────────
|
||||
//
|
||||
// These accept a Map<String, String> of request headers. The seed's
|
||||
// __http_do_map converts the ElMap to a curl_slist internally, matching
|
||||
// the headers_from_map() logic in el_runtime.c.
|
||||
|
||||
// http_get_with_headers performs an HTTP GET with caller-supplied headers.
|
||||
fn http_get_with_headers(url: String, headers: Map<String, String>) -> String {
|
||||
return __http_do_map("GET", url, "", headers, el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_with_headers performs an HTTP POST with caller-supplied headers.
|
||||
fn http_post_with_headers(url: String, body: String, headers: Map<String, String>) -> String {
|
||||
return __http_do_map("POST", url, body, headers, el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_form_auth performs an HTTP POST with
|
||||
// Content-Type: application/x-www-form-urlencoded and an Authorization
|
||||
// header built from auth_header (the caller passes the full header value,
|
||||
// e.g. "Bearer <token>" or "Basic <base64>").
|
||||
//
|
||||
// Mirrors http_post_form_auth in el_runtime.c: two headers are injected,
|
||||
// Content-Type is always set; Authorization is omitted when auth_header is "".
|
||||
fn http_post_form_auth(url: String, form_body: String, auth_header: String) -> String {
|
||||
if str_eq(auth_header, "") {
|
||||
return __http_do("POST", url, form_body, "{\"Content-Type\":\"application/x-www-form-urlencoded\"}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("POST", url, form_body, "{\"Content-Type\":\"application/x-www-form-urlencoded\",\"Authorization\":\"" + auth_header + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — streaming to file ──────────────────────────────────────────
|
||||
//
|
||||
// These route the response body directly to a file via the seed, bypassing
|
||||
// the El string layer. This preserves embedded NUL bytes in binary payloads
|
||||
// (audio, images, etc.) — an El string would truncate at the first NUL.
|
||||
// Returns true on success, false on any transport or I/O error.
|
||||
|
||||
// http_post_to_file performs an HTTP POST and streams the response body to
|
||||
// output_path. Useful for large or binary response payloads.
|
||||
fn http_post_to_file(url: String, body: String, headers: Map<String, String>, output_path: String) -> Bool {
|
||||
return __http_do_map_to_file("POST", url, body, headers, output_path)
|
||||
}
|
||||
|
||||
// http_get_to_file performs an HTTP GET and streams the response body to
|
||||
// output_path. Useful for large or binary response payloads.
|
||||
fn http_get_to_file(url: String, headers: Map<String, String>, output_path: String) -> Bool {
|
||||
return __http_do_map_to_file("GET", url, "", headers, output_path)
|
||||
}
|
||||
|
||||
// ── HTTP server ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// El programs call http_set_handler(name) to register which El function
|
||||
// handles requests, then http_serve(port, name) to start listening.
|
||||
// The seed resolves handler names via dlsym — every El fn compiles to a
|
||||
// global C symbol with the same name, so self-registration works without
|
||||
// any El-level registry.
|
||||
//
|
||||
// v2 widens the handler signature from
|
||||
// (method, path, body) -> String
|
||||
// to
|
||||
// (method, path, headers_map, body) -> String
|
||||
// so handlers can inspect incoming headers. Use http_serve_v2 +
|
||||
// http_set_handler_v2 for v2 handlers.
|
||||
|
||||
// http_set_handler registers name as the active v1 request handler.
|
||||
// The seed resolves the symbol via dlsym at call time; no El-level
|
||||
// registration is needed. This is a no-op at the El layer.
|
||||
fn http_set_handler(name: String) {
|
||||
// no-op: the seed handles handler registration via dlsym
|
||||
}
|
||||
|
||||
// http_serve starts an HTTP/1.1 server on port, dispatching every request
|
||||
// to handler (a v1 handler: fn(method, path, body) -> String).
|
||||
// Blocks forever. Accepts both IPv4 and IPv6 (dual-stack).
|
||||
fn http_serve(port: Int, handler: String) {
|
||||
__http_serve(port, handler)
|
||||
}
|
||||
|
||||
// http_set_handler_v2 registers name as the active v2 request handler.
|
||||
// No-op at the El layer; the seed uses dlsym.
|
||||
fn http_set_handler_v2(name: String) {
|
||||
// no-op: the seed handles handler registration via dlsym
|
||||
}
|
||||
|
||||
// http_serve_v2 starts an HTTP/1.1 server on port, dispatching every
|
||||
// request to handler (a v2 handler: fn(method, path, headers, body) ->
|
||||
// String). Blocks forever. Accepts both IPv4 and IPv6 (dual-stack).
|
||||
fn http_serve_v2(port: Int, handler: String) {
|
||||
__http_serve_v2(port, handler)
|
||||
}
|
||||
|
||||
// ── Response construction ─────────────────────────────────────────────────────
|
||||
|
||||
// http_response builds a structured response envelope that the HTTP server
|
||||
// runtime unpacks into a real HTTP response with the given status code and
|
||||
// headers. status must be 100–599 (defaults to 200 outside that range).
|
||||
// headers_json must be a JSON object literal (e.g. "{}" or
|
||||
// "{\"Content-Type\":\"text/html\"}"); body is the response body string.
|
||||
//
|
||||
// The envelope format is:
|
||||
// {"el_http_response":1,"status":<n>,"headers":<obj>,"body":"<escaped>"}
|
||||
// The runtime detects this prefix and unpacks it; plain string returns from
|
||||
// handlers are still supported and are sent as HTTP 200 with auto-detected
|
||||
// Content-Type.
|
||||
fn http_response(status: Int, headers_json: String, body: String) -> String {
|
||||
return __http_response(status, headers_json, body)
|
||||
}
|
||||
|
||||
// ── HTTP client — PATCH ───────────────────────────────────────────────────────
|
||||
|
||||
// http_patch performs an HTTP PATCH request with Content-Type: application/json.
|
||||
fn http_patch(url: String, body: String) -> String {
|
||||
return __http_do("PATCH", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — Engram variants (optional API key) ──────────────────────────
|
||||
//
|
||||
// These are used by dharma's db.el to talk to Engram nodes.
|
||||
// The key parameter is the X-API-Key header value; pass "" for no auth.
|
||||
|
||||
// http_post_engram performs an HTTP POST with Content-Type: application/json
|
||||
// and an optional X-API-Key header. If key is "" no auth header is added.
|
||||
fn http_post_engram(url: String, key: String, body: String) -> String {
|
||||
if str_eq(key, "") {
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\",\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_get_engram performs an HTTP GET with an optional X-API-Key header.
|
||||
fn http_get_engram(url: String, key: String) -> String {
|
||||
if str_eq(key, "") {
|
||||
return __http_do("GET", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("GET", url, "", "{\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── SSE — Server-Sent Events streaming ───────────────────────────────────────
|
||||
//
|
||||
// Usage pattern for an SSE handler:
|
||||
//
|
||||
// fn my_handler(method: String, path: String, headers: Map<String, String>, body: String) -> String {
|
||||
// let fd: Int = http_conn_fd()
|
||||
// http_sse_open(fd)
|
||||
// http_sse_send(fd, "hello")
|
||||
// http_sse_send(fd, "world")
|
||||
// http_sse_close(fd)
|
||||
// return http_sse_sentinel()
|
||||
// }
|
||||
//
|
||||
// The sentinel return value tells http_serve_v2 NOT to close the connection
|
||||
// automatically — the handler already closed it via http_sse_close.
|
||||
|
||||
// http_conn_fd returns the raw file descriptor for the current HTTP connection.
|
||||
// Only valid inside an http_serve_v2 handler, before the handler returns.
|
||||
// Use with http_sse_open / http_sse_send / http_sse_close for streaming.
|
||||
fn http_conn_fd() -> Int {
|
||||
return __http_conn_fd()
|
||||
}
|
||||
|
||||
// http_sse_open sends SSE response headers on the current connection,
|
||||
// keeping it open for streaming. Call once at the start of an SSE handler.
|
||||
// Returns true on success.
|
||||
fn http_sse_open(fd: Int) -> Bool {
|
||||
return __http_sse_open(fd)
|
||||
}
|
||||
|
||||
// http_sse_send writes one SSE event to the connection.
|
||||
// data should not contain newlines (they are added automatically).
|
||||
// Returns true if the write succeeded (client still connected).
|
||||
fn http_sse_send(fd: Int, data: String) -> Bool {
|
||||
return __http_sse_send(fd, data)
|
||||
}
|
||||
|
||||
// http_sse_close closes the SSE connection.
|
||||
fn http_sse_close(fd: Int) {
|
||||
__http_sse_close(fd)
|
||||
return
|
||||
}
|
||||
|
||||
// http_sse_sentinel is the return value an SSE handler must return
|
||||
// to tell the HTTP server NOT to close the connection automatically.
|
||||
// The handler takes ownership of the fd and closes it via http_sse_close.
|
||||
fn http_sse_sentinel() -> String {
|
||||
return "__sse__"
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
// runtime/json.el — El JSON operations
|
||||
//
|
||||
// Thin El wrappers over seed JSON primitives, plus pure-El builders and
|
||||
// helpers. Each function here corresponds to (and replaces) a C function
|
||||
// from el-compiler/runtime/legacy/el_runtime.c (lines 2692–3333).
|
||||
//
|
||||
// Seed primitives consumed by this module:
|
||||
// __json_get(json, key) -> String (value as string)
|
||||
// __json_get_raw(json, key) -> String (raw JSON token)
|
||||
// __json_parse_map(s) -> Map<String, Any>
|
||||
// __json_stringify_val(v) -> String
|
||||
// __json_array_len(arr) -> Int
|
||||
// __json_array_get(arr, i) -> String (element as JSON fragment)
|
||||
// __json_array_get_string(arr, i) -> String (element as string value)
|
||||
// __json_set(json, key, value) -> String (JSON mutation)
|
||||
// __str_to_int(s) -> Int
|
||||
// __str_to_float(s) -> Float
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — thin wrappers that delegate directly to seed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_get — extract a value from a JSON object as a string.
|
||||
// Supports dot-path traversal ("a.b.c") and array indices ("items.0.name").
|
||||
fn json_get(json: String, key: String) -> String {
|
||||
return __json_get(json, key)
|
||||
}
|
||||
|
||||
// json_get_raw — extract a raw JSON token (the un-decoded fragment) for a key.
|
||||
// Useful when the caller wants to pass a sub-object to another JSON function.
|
||||
fn json_get_raw(json: String, key: String) -> String {
|
||||
return __json_get_raw(json, key)
|
||||
}
|
||||
|
||||
// json_parse — parse a JSON string into a Map<String, Any>.
|
||||
// Arrays become ElList; objects become ElMap; scalars are typed values.
|
||||
fn json_parse(s: String) -> Map<String, Any> {
|
||||
return __json_parse_map(s)
|
||||
}
|
||||
|
||||
// json_stringify — serialize an El value (ElMap, ElList, String, Int) to JSON.
|
||||
fn json_stringify(v: Any) -> String {
|
||||
return __json_stringify_val(v)
|
||||
}
|
||||
|
||||
// json_array_len — return the number of elements in a JSON array string.
|
||||
fn json_array_len(arr: String) -> Int {
|
||||
return __json_array_len(arr)
|
||||
}
|
||||
|
||||
// json_array_get — return the i-th element of a JSON array as a JSON fragment.
|
||||
// Nested objects and arrays are returned verbatim. Out-of-range -> "".
|
||||
fn json_array_get(arr: String, i: Int) -> String {
|
||||
return __json_array_get(arr, i)
|
||||
}
|
||||
|
||||
// json_array_get_string — return the i-th element of a JSON array as a plain
|
||||
// string value (quotes and escape sequences removed). Non-string elements
|
||||
// and out-of-range indices yield "".
|
||||
fn json_array_get_string(arr: String, i: Int) -> String {
|
||||
return __json_array_get_string(arr, i)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Typed extractors — delegate to seed then convert
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_get_string — extract a string value for a key.
|
||||
// Equivalent to json_get but named explicitly for readability.
|
||||
fn json_get_string(json: String, key: String) -> String {
|
||||
return __json_get(json, key)
|
||||
}
|
||||
|
||||
// json_get_int — extract an integer value for a key.
|
||||
fn json_get_int(json: String, key: String) -> Int {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_to_int(s)
|
||||
}
|
||||
|
||||
// json_get_float — extract a floating-point value for a key.
|
||||
fn json_get_float(json: String, key: String) -> Float {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_to_float(s)
|
||||
}
|
||||
|
||||
// json_get_bool — extract a boolean value for a key.
|
||||
// Returns true only when the raw JSON token is the literal "true".
|
||||
fn json_get_bool(json: String, key: String) -> Bool {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_eq(s, "true")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_set — set or insert a key/value pair in a JSON object string.
|
||||
// If the key already exists its value is replaced in-place; otherwise the
|
||||
// pair is appended before the closing brace. The value must already be a
|
||||
// valid JSON-encoded string (e.g. a quoted string, number, or sub-object).
|
||||
fn json_set(json: String, key: String, value: String) -> String {
|
||||
return __json_set(json, key, value)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure-El builders — no seed call required
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_build_object — build a JSON object from alternating key/value strings.
|
||||
//
|
||||
// keys_and_values must contain an even number of elements laid out as:
|
||||
// [key0, val0, key1, val1, ...]
|
||||
//
|
||||
// Both keys and values are assumed to be plain strings that will be
|
||||
// double-quoted and JSON-escaped by this function. Pass a pre-encoded
|
||||
// number or sub-object as the value if you need non-string JSON types.
|
||||
//
|
||||
// Example:
|
||||
// json_build_object(["name", "alice", "role", "admin"])
|
||||
// -> {"name":"alice","role":"admin"}
|
||||
fn json_build_object(keys_and_values: [String]) -> String {
|
||||
let n: Int = el_list_len(keys_and_values)
|
||||
let result: String = "{"
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let key: String = el_list_get(keys_and_values, i)
|
||||
let val: String = el_list_get(keys_and_values, i + 1)
|
||||
let sep: String = if i == 0 { "" } else { "," }
|
||||
let escaped_key: String = json_escape_string(key)
|
||||
let escaped_val: String = json_escape_string(val)
|
||||
let result = result + sep + "\"" + escaped_key + "\":\"" + escaped_val + "\""
|
||||
let i = i + 2
|
||||
}
|
||||
return result + "}"
|
||||
}
|
||||
|
||||
// json_build_array — build a JSON array from a list of already-JSON-encoded
|
||||
// strings.
|
||||
//
|
||||
// Each element in items must be a valid JSON fragment (quoted string, number,
|
||||
// object, array, or literal). The function joins them with commas and wraps
|
||||
// the result in brackets.
|
||||
//
|
||||
// Example:
|
||||
// json_build_array(["\"alice\"", "\"bob\""])
|
||||
// -> ["alice","bob"]
|
||||
fn json_build_array(items: [String]) -> String {
|
||||
let n: Int = el_list_len(items)
|
||||
let result: String = "["
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let sep: String = if i == 0 { "" } else { "," }
|
||||
let result = result + sep + item
|
||||
let i = i + 1
|
||||
}
|
||||
return result + "]"
|
||||
}
|
||||
|
||||
// json_array_push — append a pre-encoded JSON element to a JSON array string.
|
||||
// elem must be a valid JSON fragment (e.g. "\"foo\"" or "42").
|
||||
// Returns a new JSON array string with elem appended.
|
||||
// Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]"
|
||||
fn json_array_push(arr: String, elem: String) -> String {
|
||||
let n: Int = json_array_len(arr)
|
||||
if n == 0 {
|
||||
return "[" + elem + "]"
|
||||
}
|
||||
// arr ends with ']'; insert before it
|
||||
let inner_end: Int = str_last_index_of(arr, "]")
|
||||
if inner_end < 0 {
|
||||
return "[" + elem + "]"
|
||||
}
|
||||
let prefix: String = str_slice(arr, 0, inner_end)
|
||||
return prefix + "," + elem + "]"
|
||||
}
|
||||
|
||||
// json_escape_string — escape a raw string so it can be safely embedded as a
|
||||
// JSON string value.
|
||||
//
|
||||
// Characters escaped: backslash, double-quote, newline, carriage return, tab.
|
||||
// The returned value does NOT include surrounding double-quotes; wrap it in
|
||||
// quotes if you need a complete JSON string literal.
|
||||
fn json_escape_string(s: String) -> String {
|
||||
let s1: String = str_replace(s, "\\", "\\\\")
|
||||
let s2: String = str_replace(s1, "\"", "\\\"")
|
||||
let s3: String = str_replace(s2, "\n", "\\n")
|
||||
let s4: String = str_replace(s3, "\r", "\\r")
|
||||
let s5: String = str_replace(s4, "\t", "\\t")
|
||||
return s5
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DHARMA byte decoding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// bytes_to_str — decode a JSON array of integer byte values back to a string.
|
||||
// "[104,105]" -> "hi"
|
||||
// Inverse of str_to_bytes (defined in string.el). Defined here because it
|
||||
// depends on json_array_len and json_array_get_string which live in this file.
|
||||
fn bytes_to_str(arr: String) -> String {
|
||||
let n: Int = json_array_len(arr)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let elem: String = json_array_get_string(arr, i)
|
||||
let b: Int = __str_to_int(elem)
|
||||
out = __str_set_char(out, i, b)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// runtime/manifest.el — El runtime module manifest
|
||||
//
|
||||
// Load order for runtime compilation. Each module may depend on modules
|
||||
// listed before it. The build system concatenates these in this order,
|
||||
// then compiles the combined source.
|
||||
//
|
||||
// Modules:
|
||||
// 1. runtime/string.el — string operations (no dependencies)
|
||||
// 2. runtime/math.el — numeric/float operations (no dependencies)
|
||||
// 3. runtime/state.el — in-process key-value (no dependencies)
|
||||
// 4. runtime/env.el — environment, process, args, uuid
|
||||
// 5. runtime/fs.el — filesystem operations (depends: string)
|
||||
// 6. runtime/exec.el — subprocess execution (depends: string)
|
||||
// 7. runtime/time.el — time, date, calendar (depends: string, math)
|
||||
// 8. runtime/json.el — JSON operations (depends: string)
|
||||
// 9. runtime/http.el — HTTP client+server (depends: string, json)
|
||||
// 10. runtime/engram.el — graph store (depends: string, json)
|
||||
// 11. runtime/thread.el — threading, parallel_map (depends: all above)
|
||||
// 12. runtime/collections.el — list/map higher-level ops (depends: string)
|
||||
//
|
||||
// Build command (from el/ root):
|
||||
// cat runtime/string.el runtime/math.el runtime/state.el runtime/env.el \
|
||||
// runtime/fs.el runtime/exec.el runtime/time.el runtime/json.el \
|
||||
// runtime/http.el runtime/engram.el runtime/thread.el \
|
||||
// runtime/collections.el \
|
||||
// <user-program.el> > combined.el
|
||||
// ./dist/platform/elc combined.el > output.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o output output.c el-compiler/runtime/el_seed.c
|
||||
|
||||
// This file itself is not compiled — it is documentation only.
|
||||
fn runtime_version() -> String {
|
||||
return "2.0.0-el-native"
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
// runtime/math.el — Float math, integer utilities, and numeric conversions.
|
||||
//
|
||||
// Implements the math/float surface from el-compiler/runtime/legacy/el_runtime.c
|
||||
// (lines 303–305 for el_abs/max/min, lines 4725–4771 for float/format ops)
|
||||
// in pure El, using seed primitives.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __sqrt_f(f: Float) -> Float
|
||||
// __log_f(f: Float) -> Float
|
||||
// __ln_f(f: Float) -> Float
|
||||
// __sin_f(f: Float) -> Float
|
||||
// __cos_f(f: Float) -> Float
|
||||
// __pi_f() -> Float
|
||||
// __float_to_str(f: Float) -> String
|
||||
// __str_to_float(s: String) -> Float
|
||||
// __int_to_str(n: Int) -> String
|
||||
// __str_to_int(s: String) -> Int
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Integer math — el_abs, el_max, el_min.
|
||||
//
|
||||
// Matches legacy el_abs, el_max, el_min (lines 303–305).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// el_abs — absolute value of an integer.
|
||||
fn el_abs(n: Int) -> Int {
|
||||
if n < 0 { return -n }
|
||||
return n
|
||||
}
|
||||
|
||||
// el_max — larger of two integers.
|
||||
fn el_max(a: Int, b: Int) -> Int {
|
||||
if a > b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// el_min — smaller of two integers.
|
||||
fn el_min(a: Int, b: Int) -> Int {
|
||||
if a < b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Float math — thin wrappers over seed primitives.
|
||||
//
|
||||
// Matches legacy math_sqrt, math_log, math_ln, math_sin, math_cos, math_pi
|
||||
// (lines 4766–4771).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// math_sqrt — square root.
|
||||
fn math_sqrt(f: Float) -> Float {
|
||||
return __sqrt_f(f)
|
||||
}
|
||||
|
||||
// math_log — base-10 logarithm.
|
||||
fn math_log(f: Float) -> Float {
|
||||
return __log_f(f)
|
||||
}
|
||||
|
||||
// math_ln — natural logarithm.
|
||||
fn math_ln(f: Float) -> Float {
|
||||
return __ln_f(f)
|
||||
}
|
||||
|
||||
// math_sin — sine (radians).
|
||||
fn math_sin(f: Float) -> Float {
|
||||
return __sin_f(f)
|
||||
}
|
||||
|
||||
// math_cos — cosine (radians).
|
||||
fn math_cos(f: Float) -> Float {
|
||||
return __cos_f(f)
|
||||
}
|
||||
|
||||
// math_pi — the constant π.
|
||||
fn math_pi() -> Float {
|
||||
return __pi_f()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Float conversions — float_to_str, int_to_float, float_to_int, str_to_float.
|
||||
//
|
||||
// Matches legacy float_to_str, int_to_float, float_to_int, str_to_float
|
||||
// (lines 4725–4762).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// float_to_str — format a float using %g (shortest exact representation).
|
||||
// Matches legacy float_to_str() → snprintf "%g".
|
||||
fn float_to_str(f: Float) -> String {
|
||||
return __float_to_str(f)
|
||||
}
|
||||
|
||||
// int_to_float — convert an integer to a float.
|
||||
// Matches legacy int_to_float() → (double)(int64_t)n.
|
||||
fn int_to_float(n: Int) -> Float {
|
||||
return __int_to_float(n)
|
||||
}
|
||||
|
||||
// float_to_int — truncate a float to an integer (toward zero).
|
||||
// Matches legacy float_to_int() → (int64_t)el_to_float(f).
|
||||
fn float_to_int(f: Float) -> Int {
|
||||
return __float_to_int(f)
|
||||
}
|
||||
|
||||
// str_to_float — parse a float from a string. Returns 0.0 on failure.
|
||||
// Matches legacy str_to_float() → strtod(str, NULL).
|
||||
fn str_to_float(s: String) -> Float {
|
||||
return __str_to_float(s)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// format_float — format a float to a fixed number of decimal places.
|
||||
//
|
||||
// decimals is clamped to [0, 30]. Matches legacy format_float() → snprintf "%.*f".
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn format_float(f: Float, decimals: Int) -> String {
|
||||
let d: Int = decimals
|
||||
if d < 0 { d = 0 }
|
||||
if d > 30 { d = 30 }
|
||||
// Delegate to seed; the seed exposes __format_float(f, d) -> String.
|
||||
// This matches snprintf(buf, 128, "%.*f", d, v) in the legacy runtime.
|
||||
return __format_float(f, d)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// decimal_round — round a float to d decimal places (half-away-from-zero).
|
||||
//
|
||||
// Matches legacy decimal_round():
|
||||
// mul = pow(10, d)
|
||||
// r = (v >= 0 ? floor(v*mul + 0.5) : -floor(-v*mul + 0.5)) / mul
|
||||
//
|
||||
// We implement pow(10, d) via a loop (d <= 15, so at most 15 multiplications).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// _pow10 — 10^n as a Float for n in [0, 15].
|
||||
fn _pow10(n: Int) -> Float {
|
||||
let result: Float = 1.0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
result = result * 10.0
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// _floor_f — floor of a float: largest integer <= f.
|
||||
// Uses __float_to_int (truncation) with correction for negative non-integers.
|
||||
fn _floor_f(f: Float) -> Float {
|
||||
let t: Int = __float_to_int(f)
|
||||
let tf: Float = __int_to_float(t)
|
||||
// if f was negative and not already an integer, subtract 1
|
||||
if f < 0.0 {
|
||||
if tf > f {
|
||||
return tf - 1.0
|
||||
}
|
||||
}
|
||||
return tf
|
||||
}
|
||||
|
||||
fn decimal_round(f: Float, decimals: Int) -> Float {
|
||||
let d: Int = decimals
|
||||
if d < 0 { d = 0 }
|
||||
if d > 15 { d = 15 }
|
||||
let mul: Float = _pow10(d)
|
||||
if f >= 0.0 {
|
||||
return _floor_f(f * mul + 0.5) / mul
|
||||
}
|
||||
return 0.0 - _floor_f((0.0 - f) * mul + 0.5) / mul
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// runtime/state.el — In-process key/value store.
|
||||
//
|
||||
// Thin El wrappers over the __state_* seed primitives. The backing store is
|
||||
// a process-wide hash map maintained by the El runtime (formerly el_runtime.c
|
||||
// lines 4632–4721: state_set, state_get, state_del, state_keys).
|
||||
//
|
||||
// Keys and values are Strings. Values are persistent across request boundaries
|
||||
// within the same process instance (they survive individual request lifetimes).
|
||||
// Concurrent access is serialized by the runtime; these wrappers are lock-free
|
||||
// from El's perspective.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __state_set(key: String, val: String)
|
||||
// __state_get(key: String) -> String
|
||||
// __state_del(key: String)
|
||||
// __state_keys() -> String (JSON array of key strings)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — set / get / del / keys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// state_set — store val under key. Overwrites any existing value.
|
||||
fn state_set(key: String, val: String) {
|
||||
__state_set(key, val)
|
||||
}
|
||||
|
||||
// state_get — retrieve the value for key. Returns "" if key is absent.
|
||||
fn state_get(key: String) -> String {
|
||||
return __state_get(key)
|
||||
}
|
||||
|
||||
// state_del — remove key from the store. No-op if key does not exist.
|
||||
fn state_del(key: String) {
|
||||
__state_del(key)
|
||||
}
|
||||
|
||||
// state_keys — return a JSON array string of all current keys.
|
||||
// e.g. ["foo","bar","baz"]
|
||||
// Matches legacy state_keys() which returns an ElList (here serialized as JSON).
|
||||
fn state_keys() -> String {
|
||||
return __state_keys()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Convenience helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// state_has — true if key is present (value is non-empty string).
|
||||
// Note: a key set to "" is indistinguishable from absent via state_get alone.
|
||||
fn state_has(key: String) -> Bool {
|
||||
let v: String = state_get(key)
|
||||
if str_eq(v, "") { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// state_get_or — return val for key, or default_val if key is absent.
|
||||
fn state_get_or(key: String, default_val: String) -> String {
|
||||
let v: String = state_get(key)
|
||||
if str_eq(v, "") { return default_val }
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// stdlib.el — El standard library master import file.
|
||||
//
|
||||
// Import this single file to get the full El runtime in the correct
|
||||
// dependency order. El programs can do:
|
||||
//
|
||||
// import "../foundation/el/runtime/stdlib.el"
|
||||
//
|
||||
// or, if El is installed via tools/install.sh:
|
||||
//
|
||||
// import "/usr/local/el/runtime/stdlib.el"
|
||||
//
|
||||
// Note: test.el is intentionally NOT included here — it is dev-only
|
||||
// and should be imported explicitly in test files only.
|
||||
//
|
||||
// Dependency order (each module may depend on earlier ones):
|
||||
// string — no deps
|
||||
// math — no deps
|
||||
// time — no deps
|
||||
// env — no deps
|
||||
// fs — no deps
|
||||
// exec — no deps
|
||||
// json — depends on string
|
||||
// http — depends on string, json
|
||||
// state — no deps
|
||||
// thread — depends on exec
|
||||
// channel — depends on thread, state
|
||||
// engram — depends on http, json, string
|
||||
// manifest — depends on fs, json, string
|
||||
|
||||
import "string.el"
|
||||
import "math.el"
|
||||
import "time.el"
|
||||
import "env.el"
|
||||
import "fs.el"
|
||||
import "exec.el"
|
||||
import "json.el"
|
||||
import "http.el"
|
||||
import "state.el"
|
||||
import "thread.el"
|
||||
import "channel.el"
|
||||
import "engram.el"
|
||||
import "manifest.el"
|
||||
@@ -0,0 +1,907 @@
|
||||
// runtime/string.el — String operations implemented in El.
|
||||
//
|
||||
// All functions delegate character-level work to the seed primitives declared
|
||||
// in el_seed.c. No C is written here; this is pure El source that compiles
|
||||
// to C via the normal El pipeline.
|
||||
//
|
||||
// Seed primitives used (provided by el_seed.c):
|
||||
// __str_len(s) -> Int
|
||||
// __str_char_at(s, i) -> Int (char code at byte index i)
|
||||
// __str_alloc(n) -> String (n-byte zero-filled mutable buffer)
|
||||
// __str_set_char(s, i, c) -> String (mutate s[i]=c, return s)
|
||||
// __str_cmp(a, b) -> Int (strcmp)
|
||||
// __str_ncmp(a, b, n) -> Int (strncmp)
|
||||
// __str_concat_raw(a, b) -> String
|
||||
// __str_slice_raw(s, lo, hi) -> String (substring copy [lo, hi))
|
||||
// __int_to_str(n) -> String
|
||||
// __str_to_int(s) -> Int
|
||||
// __float_to_str(f) -> String
|
||||
// __str_to_float(s) -> Float
|
||||
// __println(s)
|
||||
// __print(s)
|
||||
// __readline() -> String
|
||||
// __url_encode(s) -> String
|
||||
// __url_decode(s) -> String
|
||||
|
||||
// ── I/O ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn println(s: String) -> Void {
|
||||
__println(s)
|
||||
}
|
||||
|
||||
fn print(s: String) -> Void {
|
||||
__print(s)
|
||||
}
|
||||
|
||||
fn readline() -> String {
|
||||
return __readline()
|
||||
}
|
||||
|
||||
// ── Type conversions ──────────────────────────────────────────────────────────
|
||||
|
||||
fn int_to_str(n: Int) -> String {
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
fn str_to_int(s: String) -> Int {
|
||||
return __str_to_int(s)
|
||||
}
|
||||
|
||||
fn float_to_str(f: Float) -> String {
|
||||
return __float_to_str(f)
|
||||
}
|
||||
|
||||
fn str_to_float(s: String) -> Float {
|
||||
return __str_to_float(s)
|
||||
}
|
||||
|
||||
fn bool_to_str(b: Bool) -> String {
|
||||
if b { return "true" }
|
||||
return "false"
|
||||
}
|
||||
|
||||
// ── URL encoding ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn url_encode(s: String) -> String {
|
||||
return __url_encode(s)
|
||||
}
|
||||
|
||||
fn url_decode(s: String) -> String {
|
||||
return __url_decode(s)
|
||||
}
|
||||
|
||||
// ── Math ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn el_abs(n: Int) -> Int {
|
||||
if n < 0 { return 0 - n }
|
||||
return n
|
||||
}
|
||||
|
||||
fn el_max(a: Int, b: Int) -> Int {
|
||||
if a > b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
fn el_min(a: Int, b: Int) -> Int {
|
||||
if a < b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// ── Core string primitives ────────────────────────────────────────────────────
|
||||
|
||||
fn str_len(s: String) -> Int {
|
||||
return __str_len(s)
|
||||
}
|
||||
|
||||
fn str_eq(a: String, b: String) -> Bool {
|
||||
return __str_cmp(a, b) == 0
|
||||
}
|
||||
|
||||
fn str_concat(a: String, b: String) -> String {
|
||||
return __str_concat_raw(a, b)
|
||||
}
|
||||
|
||||
fn str_slice(s: String, start: Int, end: Int) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let lo: Int = start
|
||||
if lo < 0 { lo = 0 }
|
||||
if lo > slen { lo = slen }
|
||||
let hi: Int = end
|
||||
if hi < lo { hi = lo }
|
||||
if hi > slen { hi = slen }
|
||||
return __str_slice_raw(s, lo, hi)
|
||||
}
|
||||
|
||||
// ── Whitespace helpers (internal) ─────────────────────────────────────────────
|
||||
//
|
||||
// _is_ws: returns true for ASCII whitespace (space, tab, \n, \r, \f, \v).
|
||||
|
||||
fn _is_ws(c: Int) -> Bool {
|
||||
if c == 32 { return true } // space
|
||||
if c == 9 { return true } // tab
|
||||
if c == 10 { return true } // \n
|
||||
if c == 13 { return true } // \r
|
||||
if c == 12 { return true } // \f
|
||||
if c == 11 { return true } // \v
|
||||
return false
|
||||
}
|
||||
|
||||
// Scan forward from index 0; return index of first byte not in whitespace,
|
||||
// or n if the entire string is whitespace.
|
||||
fn _find_first_non_ws(s: String, n: Int) -> Int {
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
if !_is_ws(__str_char_at(s, i)) { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Scan backward from index n-1; return index of last non-whitespace byte,
|
||||
// or -1 if the entire string is whitespace.
|
||||
fn _find_last_non_ws(s: String, n: Int) -> Int {
|
||||
let i: Int = n - 1
|
||||
while i >= 0 {
|
||||
if !_is_ws(__str_char_at(s, i)) { return i }
|
||||
i = i - 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Comparison and search ─────────────────────────────────────────────────────
|
||||
|
||||
fn str_starts_with(s: String, prefix: String) -> Bool {
|
||||
let plen: Int = __str_len(prefix)
|
||||
let slen: Int = __str_len(s)
|
||||
if plen > slen { return false }
|
||||
return __str_ncmp(s, prefix, plen) == 0
|
||||
}
|
||||
|
||||
fn str_ends_with(s: String, suffix: String) -> Bool {
|
||||
let slen: Int = __str_len(s)
|
||||
let suflen: Int = __str_len(suffix)
|
||||
if suflen > slen { return false }
|
||||
let tail: String = __str_slice_raw(s, slen - suflen, slen)
|
||||
return __str_cmp(tail, suffix) == 0
|
||||
}
|
||||
|
||||
fn str_contains(s: String, sub: String) -> Bool {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return true }
|
||||
if sublen > slen { return false }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 { return true }
|
||||
i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn str_index_of(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return 0 }
|
||||
if sublen > slen { return -1 }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fn str_last_index_of(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return slen }
|
||||
if sublen > slen { return -1 }
|
||||
let last: Int = -1
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
last = i
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
fn str_index_of_all(s: String, sub: String) -> [Int] {
|
||||
let result: [Int] = el_list_empty()
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return result }
|
||||
if sublen > slen { return result }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
result = el_list_append(result, i)
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Return the byte index of the first character in s that appears in any_of,
|
||||
// or -1 if none found.
|
||||
fn str_find_chars(s: String, any_of: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let alen: Int = __str_len(any_of)
|
||||
if alen == 0 { return -1 }
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let j: Int = 0
|
||||
while j < alen {
|
||||
if c == __str_char_at(any_of, j) { return i }
|
||||
j = j + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Character access ──────────────────────────────────────────────────────────
|
||||
|
||||
// Return a one-character string at byte index i, or "" if out of range.
|
||||
fn str_char_at(s: String, i: Int) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if i < 0 { return "" }
|
||||
if i >= slen { return "" }
|
||||
return __str_slice_raw(s, i, i + 1)
|
||||
}
|
||||
|
||||
// Return the char code (byte value) at byte index i, or 0 if out of range.
|
||||
fn str_char_code(s: String, i: Int) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
if i < 0 { return 0 }
|
||||
if i >= slen { return 0 }
|
||||
return __str_char_at(s, i)
|
||||
}
|
||||
|
||||
// ── Case conversion ───────────────────────────────────────────────────────────
|
||||
|
||||
fn str_to_upper(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// a-z (97-122) -> A-Z (65-90): subtract 32
|
||||
if c >= 97 {
|
||||
if c <= 122 { c = c - 32 }
|
||||
}
|
||||
out = __str_set_char(out, i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn str_to_lower(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// A-Z (65-90) -> a-z (97-122): add 32
|
||||
if c >= 65 {
|
||||
if c <= 90 { c = c + 32 }
|
||||
}
|
||||
out = __str_set_char(out, i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Aliases used in existing El codebases.
|
||||
fn str_lower(s: String) -> String {
|
||||
return str_to_lower(s)
|
||||
}
|
||||
|
||||
fn str_upper(s: String) -> String {
|
||||
return str_to_upper(s)
|
||||
}
|
||||
|
||||
// ── Whitespace trimming ───────────────────────────────────────────────────────
|
||||
|
||||
fn str_trim(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let lo: Int = _find_first_non_ws(s, n)
|
||||
if lo == n { return "" }
|
||||
let hi: Int = _find_last_non_ws(s, n)
|
||||
return __str_slice_raw(s, lo, hi + 1)
|
||||
}
|
||||
|
||||
fn str_lstrip(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let lo: Int = _find_first_non_ws(s, n)
|
||||
if lo == n { return "" }
|
||||
return __str_slice_raw(s, lo, n)
|
||||
}
|
||||
|
||||
fn str_rstrip(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let hi: Int = _find_last_non_ws(s, n)
|
||||
if hi < 0 { return "" }
|
||||
return __str_slice_raw(s, 0, hi + 1)
|
||||
}
|
||||
|
||||
// ── Replacement ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn str_replace(s: String, from: String, to: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let flen: Int = __str_len(from)
|
||||
if flen == 0 { return s }
|
||||
if slen == 0 { return s }
|
||||
// Scan s left-to-right; emit `to` on each match, otherwise emit one byte.
|
||||
let result: String = ""
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
// Try to match `from` at position i
|
||||
if i + flen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + flen)
|
||||
if __str_cmp(window, from) == 0 {
|
||||
result = __str_concat_raw(result, to)
|
||||
i = i + flen
|
||||
} else {
|
||||
let ch: String = __str_slice_raw(s, i, i + 1)
|
||||
result = __str_concat_raw(result, ch)
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
// Not enough bytes left for a match — emit remainder and stop.
|
||||
let tail: String = __str_slice_raw(s, i, slen)
|
||||
result = __str_concat_raw(result, tail)
|
||||
i = slen
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── Repetition and reversal ───────────────────────────────────────────────────
|
||||
|
||||
fn str_repeat(s: String, n: Int) -> String {
|
||||
if n <= 0 { return "" }
|
||||
let slen: Int = __str_len(s)
|
||||
if slen == 0 { return "" }
|
||||
let result: String = ""
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
result = __str_concat_raw(result, s)
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Byte-reverse (correct for ASCII; for multi-byte UTF-8 codepoints this
|
||||
// reverses bytes within a codepoint, which is intentional at this tier —
|
||||
// Phase 2 will add grapheme-aware reversal).
|
||||
fn str_reverse(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
out = __str_set_char(out, n - 1 - i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ── Prefix/suffix stripping ───────────────────────────────────────────────────
|
||||
|
||||
fn str_strip_prefix(s: String, prefix: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let plen: Int = __str_len(prefix)
|
||||
if plen == 0 { return s }
|
||||
if plen > slen { return s }
|
||||
if __str_ncmp(s, prefix, plen) == 0 {
|
||||
return __str_slice_raw(s, plen, slen)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
fn str_strip_suffix(s: String, suffix: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let suflen: Int = __str_len(suffix)
|
||||
if suflen == 0 { return s }
|
||||
if suflen > slen { return s }
|
||||
let tail: String = __str_slice_raw(s, slen - suflen, slen)
|
||||
if __str_cmp(tail, suffix) == 0 {
|
||||
return __str_slice_raw(s, 0, slen - suflen)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Strip leading and trailing bytes whose char code appears in `chars`.
|
||||
fn str_strip_chars(s: String, chars: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let clen: Int = __str_len(chars)
|
||||
if slen == 0 { return "" }
|
||||
if clen == 0 { return s }
|
||||
let lo: Int = _find_first_not_in_charset(s, chars, slen, clen)
|
||||
if lo == slen { return "" }
|
||||
let hi: Int = _find_last_not_in_charset(s, chars, slen, clen)
|
||||
return __str_slice_raw(s, lo, hi + 1)
|
||||
}
|
||||
|
||||
// Internal: true if char code `c` is present in the charset string.
|
||||
fn _char_in_set(c: Int, chars: String, clen: Int) -> Bool {
|
||||
let j: Int = 0
|
||||
while j < clen {
|
||||
if c == __str_char_at(chars, j) { return true }
|
||||
j = j + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn _find_first_not_in_charset(s: String, chars: String, slen: Int, clen: Int) -> Int {
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if !_char_in_set(__str_char_at(s, i), chars, clen) { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return slen
|
||||
}
|
||||
|
||||
fn _find_last_not_in_charset(s: String, chars: String, slen: Int, clen: Int) -> Int {
|
||||
let i: Int = slen - 1
|
||||
while i >= 0 {
|
||||
if !_char_in_set(__str_char_at(s, i), chars, clen) { return i }
|
||||
i = i - 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Padding ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// Pad s on the left to `width` total chars, repeating `pad` cyclically.
|
||||
fn str_pad_left(s: String, width: Int, pad: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if slen >= width { return s }
|
||||
let plen: Int = __str_len(pad)
|
||||
if plen == 0 { return s }
|
||||
let need: Int = width - slen
|
||||
let prefix: String = ""
|
||||
let i: Int = 0
|
||||
while i < need {
|
||||
// Select pad character at position (i mod plen)
|
||||
let pad_idx: Int = i - (i / plen) * plen
|
||||
let pc: String = __str_slice_raw(pad, pad_idx, pad_idx + 1)
|
||||
prefix = __str_concat_raw(prefix, pc)
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(prefix, s)
|
||||
}
|
||||
|
||||
// Pad s on the right to `width` total chars, repeating `pad` cyclically.
|
||||
fn str_pad_right(s: String, width: Int, pad: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if slen >= width { return s }
|
||||
let plen: Int = __str_len(pad)
|
||||
if plen == 0 { return s }
|
||||
let need: Int = width - slen
|
||||
let suffix: String = ""
|
||||
let i: Int = 0
|
||||
while i < need {
|
||||
let pad_idx: Int = i - (i / plen) * plen
|
||||
let pc: String = __str_slice_raw(pad, pad_idx, pad_idx + 1)
|
||||
suffix = __str_concat_raw(suffix, pc)
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(s, suffix)
|
||||
}
|
||||
|
||||
// ── Counting ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// Count non-overlapping occurrences of `sub` in `s`. Empty sub returns 0.
|
||||
fn str_count(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return 0 }
|
||||
if sublen > slen { return 0 }
|
||||
let count: Int = 0
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
count = count + 1
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Byte count — alias of str_len.
|
||||
fn str_count_bytes(s: String) -> Int {
|
||||
return __str_len(s)
|
||||
}
|
||||
|
||||
// UTF-8 codepoint count: count bytes that are NOT continuation bytes (10xxxxxx).
|
||||
// Continuation bytes have the pattern 10xxxxxx = 0x80..0xBF (128..191).
|
||||
fn str_count_chars(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// Continuation bytes are in range [128, 191]; skip them.
|
||||
// All other bytes (< 128 ASCII, or >= 192 leading bytes) start a codepoint.
|
||||
if c < 128 {
|
||||
count = count + 1
|
||||
} else {
|
||||
if c >= 192 { count = count + 1 }
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count newline-delimited lines. A trailing newline does NOT add an extra empty line.
|
||||
fn str_count_lines(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return 0 }
|
||||
let count: Int = 0
|
||||
let has_content: Bool = false
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
has_content = true
|
||||
if c == 10 { // \n
|
||||
count = count + 1
|
||||
has_content = false
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
if has_content { count = count + 1 }
|
||||
return count
|
||||
}
|
||||
|
||||
// Count whitespace-delimited words (non-empty tokens).
|
||||
fn str_count_words(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let in_word: Bool = false
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if _is_ws(c) {
|
||||
in_word = false
|
||||
} else {
|
||||
if !in_word {
|
||||
in_word = true
|
||||
count = count + 1
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count ASCII letters [A-Za-z].
|
||||
fn str_count_letters(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c >= 65 {
|
||||
if c <= 90 { count = count + 1 } // A-Z
|
||||
}
|
||||
if c >= 97 {
|
||||
if c <= 122 { count = count + 1 } // a-z
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count ASCII decimal digits [0-9].
|
||||
fn str_count_digits(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c >= 48 {
|
||||
if c <= 57 { count = count + 1 } // '0'-'9'
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// ── Character classification ──────────────────────────────────────────────────
|
||||
//
|
||||
// For all predicates: empty string -> false.
|
||||
// Multi-char string: ALL bytes must satisfy the predicate.
|
||||
|
||||
fn is_letter(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 65 { if c <= 90 { ok = true } } // A-Z
|
||||
if c >= 97 { if c <= 122 { ok = true } } // a-z
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_digit(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 48 { return false } // '0'
|
||||
if c > 57 { return false } // '9'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_alphanumeric(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 48 { if c <= 57 { ok = true } } // 0-9
|
||||
if c >= 65 { if c <= 90 { ok = true } } // A-Z
|
||||
if c >= 97 { if c <= 122 { ok = true } } // a-z
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_whitespace(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
if !_is_ws(__str_char_at(s, i)) { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ASCII punctuation: 33-47, 58-64, 91-96, 123-126.
|
||||
fn is_punctuation(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 33 { if c <= 47 { ok = true } }
|
||||
if c >= 58 { if c <= 64 { ok = true } }
|
||||
if c >= 91 { if c <= 96 { ok = true } }
|
||||
if c >= 123 { if c <= 126 { ok = true } }
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_uppercase(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 65 { return false } // 'A'
|
||||
if c > 90 { return false } // 'Z'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_lowercase(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 97 { return false } // 'a'
|
||||
if c > 122 { return false } // 'z'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Splitting ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn str_split(s: String, sep: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let slen: Int = __str_len(s)
|
||||
let seplen: Int = __str_len(sep)
|
||||
// Empty separator: return the whole string as a single element.
|
||||
if seplen == 0 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let part_start: Int = 0
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if i + seplen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + seplen)
|
||||
if __str_cmp(window, sep) == 0 {
|
||||
let part: String = __str_slice_raw(s, part_start, i)
|
||||
result = el_list_append(result, part)
|
||||
i = i + seplen
|
||||
part_start = i
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
// Append remaining tail (may be empty string if s ended with sep).
|
||||
let tail: String = __str_slice_raw(s, part_start, slen)
|
||||
result = el_list_append(result, tail)
|
||||
return result
|
||||
}
|
||||
|
||||
// Split into at most n parts. The nth part (index n-1) contains the remainder
|
||||
// verbatim, including any further separators. n <= 0 returns []. n == 1
|
||||
// returns [s].
|
||||
fn str_split_n(s: String, sep: String, n: Int) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
if n <= 0 { return result }
|
||||
if n == 1 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let slen: Int = __str_len(s)
|
||||
let seplen: Int = __str_len(sep)
|
||||
if seplen == 0 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let part_start: Int = 0
|
||||
let parts: Int = 0
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if parts >= n - 1 {
|
||||
// Reached the split limit — stop splitting, emit the rest below.
|
||||
i = slen
|
||||
} else {
|
||||
if i + seplen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + seplen)
|
||||
if __str_cmp(window, sep) == 0 {
|
||||
let part: String = __str_slice_raw(s, part_start, i)
|
||||
result = el_list_append(result, part)
|
||||
i = i + seplen
|
||||
part_start = i
|
||||
parts = parts + 1
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remainder verbatim.
|
||||
let tail: String = __str_slice_raw(s, part_start, slen)
|
||||
result = el_list_append(result, tail)
|
||||
return result
|
||||
}
|
||||
|
||||
// Split on newlines. \r\n is folded to \n. Trailing empty line after a
|
||||
// final \n is dropped — so "a\nb\n" yields ["a", "b"], not ["a", "b", ""].
|
||||
fn str_split_lines(s: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return result }
|
||||
let line_start: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c == 10 { // \n
|
||||
let lend: Int = i
|
||||
// Fold \r\n: if the byte before \n is \r, exclude it.
|
||||
if lend > line_start {
|
||||
if __str_char_at(s, lend - 1) == 13 { lend = lend - 1 }
|
||||
}
|
||||
let line: String = __str_slice_raw(s, line_start, lend)
|
||||
result = el_list_append(result, line)
|
||||
line_start = i + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
// Trailing content with no terminating \n.
|
||||
if line_start < n {
|
||||
let line: String = __str_slice_raw(s, line_start, n)
|
||||
result = el_list_append(result, line)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Split into a list of one-byte strings (byte-level chars).
|
||||
fn str_split_chars(s: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let n: Int = __str_len(s)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let ch: String = __str_slice_raw(s, i, i + 1)
|
||||
result = el_list_append(result, ch)
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── Joining ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// Join a list of strings with a separator between consecutive elements.
|
||||
// Empty list yields "". Non-string elements should not be passed here.
|
||||
fn str_join(parts: [String], sep: String) -> String {
|
||||
let n: Int = el_list_len(parts)
|
||||
if n == 0 { return "" }
|
||||
let result: String = el_list_get(parts, 0)
|
||||
let i: Int = 1
|
||||
while i < n {
|
||||
result = __str_concat_raw(result, sep)
|
||||
result = __str_concat_raw(result, el_list_get(parts, i))
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── DHARMA byte encoding (str_to_bytes) ──────────────────────────────────────
|
||||
//
|
||||
// str_to_bytes — encode a string as a JSON array of unsigned byte values.
|
||||
// "hi" -> "[104,105]"
|
||||
// Used by db.el to store content in Engram JSON nodes as a byte array.
|
||||
// Note: bytes_to_str (the inverse) is defined in json.el because it depends
|
||||
// on json_array_get_string which is defined there.
|
||||
fn str_to_bytes(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "[]" }
|
||||
let result: String = "["
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let b: Int = __str_char_at(s, i)
|
||||
if i > 0 { result = __str_concat_raw(result, ",") }
|
||||
result = __str_concat_raw(result, __int_to_str(b))
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(result, "]")
|
||||
}
|
||||
|
||||
// ── Cryptographic hashing ─────────────────────────────────────────────────────
|
||||
|
||||
// hash_sha256 — return the SHA-256 hex digest of a string.
|
||||
// Delegates to the __sha256_hex seed primitive.
|
||||
fn hash_sha256(s: String) -> String {
|
||||
return __sha256_hex(s)
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
// runtime/test.el — El test framework: assertions, registration, and runner.
|
||||
//
|
||||
// Provides a minimal but complete test harness for El programs. No external
|
||||
// dependencies. Written entirely in El using existing runtime primitives.
|
||||
//
|
||||
// ── Quick-start ──────────────────────────────────────────────────────────────
|
||||
//
|
||||
// 1. Write a test function with the standard test signature:
|
||||
//
|
||||
// fn test_str_eq_works(_: String) -> String {
|
||||
// assert_true(str_eq("a", "a"), "same strings are equal")
|
||||
// assert_false(str_eq("a", "b"), "different strings are not equal")
|
||||
// return ""
|
||||
// }
|
||||
//
|
||||
// 2. Register it and run:
|
||||
//
|
||||
// fn main() -> Void {
|
||||
// test_case("str_eq works", "test_str_eq_works")
|
||||
// test_run_all()
|
||||
// }
|
||||
//
|
||||
// Test functions must have the signature (String) -> String. The argument is
|
||||
// a dummy passed by the threading mechanism (see runtime/thread.el) and should
|
||||
// be ignored. The return value is likewise ignored — results flow through the
|
||||
// state-based assertion primitives.
|
||||
//
|
||||
// ── State keys ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// _test_cases JSON array of {"name":"...","fn":"..."}
|
||||
// _test_pass_count Int as string — total passing assertions
|
||||
// _test_fail_count Int as string — total failing assertions
|
||||
// _test_failures JSON array of failure message strings
|
||||
// _test_current Name of the test case currently executing
|
||||
//
|
||||
// ── Dependencies ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// runtime/string.el — str_eq, str_concat, str_contains, int_to_str, ...
|
||||
// runtime/state.el — state_set, state_get
|
||||
// runtime/json.el — json_array_len, json_array_get_string, json_get,
|
||||
// json_escape_string
|
||||
// runtime/thread.el — spawn, join (for dynamic dispatch via dlsym)
|
||||
|
||||
// ── Internal: JSON array helpers ─────────────────────────────────────────────
|
||||
//
|
||||
// _test_json_append — append a quoted, escaped string element to a JSON array.
|
||||
//
|
||||
// Given an existing JSON array string (e.g. '["a","b"]') and a plain string
|
||||
// value, returns a new array with the value appended (e.g. '["a","b","c"]').
|
||||
//
|
||||
// The array must be non-empty — always init with "[]" before calling.
|
||||
fn _test_json_append(arr: String, val: String) -> String {
|
||||
let escaped: String = json_escape_string(val)
|
||||
let inner: String = str_slice(arr, 1, str_len(arr) - 1)
|
||||
if str_eq(inner, "") {
|
||||
return "[\"" + escaped + "\"]"
|
||||
}
|
||||
return "[" + inner + ",\"" + escaped + "\"]"
|
||||
}
|
||||
|
||||
// _test_json_obj_append — append a raw JSON object string to a JSON array.
|
||||
//
|
||||
// Used to build the _test_cases list where each element is already a
|
||||
// JSON object (not a plain string).
|
||||
fn _test_json_obj_append(arr: String, obj: String) -> String {
|
||||
let inner: String = str_slice(arr, 1, str_len(arr) - 1)
|
||||
if str_eq(inner, "") {
|
||||
return "[" + obj + "]"
|
||||
}
|
||||
return "[" + inner + "," + obj + "]"
|
||||
}
|
||||
|
||||
// ── Test registration ─────────────────────────────────────────────────────────
|
||||
|
||||
// test_case — register a named test case.
|
||||
//
|
||||
// name: Human-readable test case name (shown in output).
|
||||
// fn_name: Name of a top-level El function with signature
|
||||
// (String) -> String. The function should call assertion
|
||||
// primitives from this module. The String arg it receives is ""
|
||||
// and its return value is ignored.
|
||||
//
|
||||
// Test cases are stored in the _test_cases state key and executed in
|
||||
// registration order by test_run_all().
|
||||
fn test_case(name: String, fn_name: String) {
|
||||
let arr: String = state_get("_test_cases")
|
||||
if str_eq(arr, "") { arr = "[]" }
|
||||
let escaped_name: String = json_escape_string(name)
|
||||
let escaped_fn: String = json_escape_string(fn_name)
|
||||
let obj: String = "{\"name\":\"" + escaped_name + "\",\"fn\":\"" + escaped_fn + "\"}"
|
||||
let arr = _test_json_obj_append(arr, obj)
|
||||
state_set("_test_cases", arr)
|
||||
}
|
||||
|
||||
// ── Test state helpers ────────────────────────────────────────────────────────
|
||||
|
||||
// _test_init — reset all test counters and failure lists.
|
||||
//
|
||||
// Called at the start of test_run_all(). Safe to call multiple times.
|
||||
fn _test_init() {
|
||||
state_set("_test_pass_count", "0")
|
||||
state_set("_test_fail_count", "0")
|
||||
state_set("_test_failures", "[]")
|
||||
state_set("_test_current", "")
|
||||
}
|
||||
|
||||
// _test_inc_pass — increment the global pass counter by 1.
|
||||
fn _test_inc_pass() {
|
||||
let n: Int = str_to_int(state_get("_test_pass_count"))
|
||||
state_set("_test_pass_count", int_to_str(n + 1))
|
||||
}
|
||||
|
||||
// _test_inc_fail — increment the global fail counter by 1.
|
||||
fn _test_inc_fail() {
|
||||
let n: Int = str_to_int(state_get("_test_fail_count"))
|
||||
state_set("_test_fail_count", int_to_str(n + 1))
|
||||
}
|
||||
|
||||
// test_pass — record a passing assertion for the current test case.
|
||||
//
|
||||
// Increments the pass counter. Called internally by assertions.
|
||||
fn test_pass(name: String) {
|
||||
_test_inc_pass()
|
||||
}
|
||||
|
||||
// test_fail — record a failing assertion for the current test case.
|
||||
//
|
||||
// name: assertion label or description (usually the msg parameter)
|
||||
// msg: detailed failure message including expected/got values
|
||||
//
|
||||
// Increments the fail counter and appends the message to _test_failures.
|
||||
// Also prints the failure immediately for visibility.
|
||||
fn test_fail(name: String, msg: String) {
|
||||
_test_inc_fail()
|
||||
let failures: String = state_get("_test_failures")
|
||||
if str_eq(failures, "") { failures = "[]" }
|
||||
let entry: String = " " + msg
|
||||
let failures = _test_json_append(failures, entry)
|
||||
state_set("_test_failures", failures)
|
||||
println(" FAIL: " + msg)
|
||||
}
|
||||
|
||||
// ── Assertions ────────────────────────────────────────────────────────────────
|
||||
|
||||
// assert_true — assert that condition is true.
|
||||
//
|
||||
// condition: the boolean value to test
|
||||
// msg: description shown on failure
|
||||
fn assert_true(condition: Bool, msg: String) {
|
||||
if condition {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected true, got false")
|
||||
}
|
||||
|
||||
// assert_false — assert that condition is false.
|
||||
//
|
||||
// condition: the boolean value to test
|
||||
// msg: description shown on failure
|
||||
fn assert_false(condition: Bool, msg: String) {
|
||||
if !condition {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected false, got true")
|
||||
}
|
||||
|
||||
// assert_eq — assert that two strings are equal.
|
||||
//
|
||||
// a, b: strings to compare
|
||||
// msg: description shown on failure (quoted values appended automatically)
|
||||
fn assert_eq(a: String, b: String, msg: String) {
|
||||
if str_eq(a, b) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected \"" + b + "\", got \"" + a + "\"")
|
||||
}
|
||||
|
||||
// assert_int_eq — assert that two integers are equal.
|
||||
//
|
||||
// a, b: integers to compare
|
||||
// msg: description shown on failure
|
||||
fn assert_int_eq(a: Int, b: Int, msg: String) {
|
||||
if a == b {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected " + int_to_str(b) + ", got " + int_to_str(a))
|
||||
}
|
||||
|
||||
// assert_neq — assert that two strings are NOT equal.
|
||||
//
|
||||
// a, b: strings to compare
|
||||
// msg: description shown on failure
|
||||
fn assert_neq(a: String, b: String, msg: String) {
|
||||
if !str_eq(a, b) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected values to differ, but both are \"" + a + "\"")
|
||||
}
|
||||
|
||||
// assert_contains — assert that string s contains substring sub.
|
||||
//
|
||||
// s: haystack string
|
||||
// sub: needle substring
|
||||
// msg: description shown on failure
|
||||
fn assert_contains(s: String, sub: String, msg: String) {
|
||||
if str_contains(s, sub) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not contain \"" + sub + "\"")
|
||||
}
|
||||
|
||||
// assert_starts_with — assert that string s starts with prefix.
|
||||
//
|
||||
// s: string to inspect
|
||||
// prefix: expected prefix
|
||||
// msg: description shown on failure
|
||||
fn assert_starts_with(s: String, prefix: String, msg: String) {
|
||||
if str_starts_with(s, prefix) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not start with \"" + prefix + "\"")
|
||||
}
|
||||
|
||||
// assert_ends_with — assert that string s ends with suffix.
|
||||
//
|
||||
// s: string to inspect
|
||||
// suffix: expected suffix
|
||||
// msg: description shown on failure
|
||||
fn assert_ends_with(s: String, suffix: String, msg: String) {
|
||||
if str_ends_with(s, suffix) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not end with \"" + suffix + "\"")
|
||||
}
|
||||
|
||||
// fail — unconditional test failure.
|
||||
//
|
||||
// msg: failure message shown in output
|
||||
//
|
||||
// Use when a code path that must not be reached is reached, or when an
|
||||
// expected exception did not occur.
|
||||
fn fail(msg: String) {
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg)
|
||||
}
|
||||
|
||||
// ── Runner ────────────────────────────────────────────────────────────────────
|
||||
|
||||
// _test_run_one — execute a single registered test case by name.
|
||||
//
|
||||
// name: the human-readable test case name (set as _test_current)
|
||||
// fn_name: the El function to invoke via the thread mechanism
|
||||
//
|
||||
// Sets _test_current so that assertions inside the test function know which
|
||||
// test they belong to. Spawns and immediately joins the test function in a
|
||||
// child thread (same dlsym mechanism as parallel_map) so dynamic dispatch
|
||||
// works without needing closures.
|
||||
fn _test_run_one(name: String, fn_name: String) {
|
||||
state_set("_test_current", name)
|
||||
let before_fail: Int = str_to_int(state_get("_test_fail_count"))
|
||||
let tid: Int = __thread_create(fn_name, "")
|
||||
__thread_join(tid)
|
||||
let after_fail: Int = str_to_int(state_get("_test_fail_count"))
|
||||
if after_fail == before_fail {
|
||||
println("[test] " + name + " ... PASS")
|
||||
} else {
|
||||
println("[test] " + name + " ... FAIL")
|
||||
}
|
||||
}
|
||||
|
||||
// test_run_all — execute all registered test cases and print a summary.
|
||||
//
|
||||
// Iterates through every test case registered via test_case(), runs each one,
|
||||
// prints per-test PASS/FAIL status, then prints a summary line.
|
||||
//
|
||||
// Returns the total number of failing assertions. Exit with this value to
|
||||
// signal CI failure:
|
||||
//
|
||||
// fn main() -> Int {
|
||||
// test_case("str_eq", "test_str_eq")
|
||||
// return test_run_all()
|
||||
// }
|
||||
fn test_run_all() -> Int {
|
||||
_test_init()
|
||||
|
||||
let cases: String = state_get("_test_cases")
|
||||
if str_eq(cases, "") { cases = "[]" }
|
||||
|
||||
let n: Int = json_array_len(cases)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let entry: String = json_array_get(cases, i)
|
||||
let name: String = json_get(entry, "name")
|
||||
let fn_name: String = json_get(entry, "fn")
|
||||
_test_run_one(name, fn_name)
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
let pass_count: Int = str_to_int(state_get("_test_pass_count"))
|
||||
let fail_count: Int = str_to_int(state_get("_test_fail_count"))
|
||||
println("[test] Summary: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed")
|
||||
|
||||
return fail_count
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// thread.el — El native threading model
|
||||
//
|
||||
// First-class parallelism for El. Eliminates bash fan-out hacks for parallel
|
||||
// HTTP dispatch, concurrent processing pipelines, and any other workload that
|
||||
// benefits from concurrent execution.
|
||||
//
|
||||
// Built on four seed primitives exposed by el_seed.c via dlsym+pthread:
|
||||
// __thread_create(fn_name, arg) -> Int spawn thread, return tid
|
||||
// __thread_join(tid) -> String join thread, return result
|
||||
// __mutex_new() -> Int allocate a mutex, return handle
|
||||
// __mutex_lock(m) lock mutex
|
||||
// __mutex_unlock(m) unlock mutex
|
||||
//
|
||||
// Every El fn compiles to a global C symbol. __thread_create uses dlsym to
|
||||
// look up the function by name and run it in a pthread. This means any El fn
|
||||
// with signature (String) -> String is directly threadable.
|
||||
|
||||
// ── Core primitives ──────────────────────────────────────────────────────────
|
||||
|
||||
// spawn — launch an El function in a new thread.
|
||||
//
|
||||
// fn_name: the name of an El fn with signature (String) -> String
|
||||
// arg: the argument to pass to that fn
|
||||
//
|
||||
// Returns a thread id (tid) that can be passed to join().
|
||||
// The El function must be a top-level fn — its C symbol must be globally
|
||||
// visible so dlsym can resolve it.
|
||||
fn spawn(fn_name: String, arg: String) -> Int {
|
||||
return __thread_create(fn_name, arg)
|
||||
}
|
||||
|
||||
// join — wait for a thread to finish and return its result.
|
||||
//
|
||||
// tid: the thread id returned by spawn()
|
||||
//
|
||||
// Blocks until the thread completes. Returns the String value the thread
|
||||
// function returned.
|
||||
fn join(tid: Int) -> String {
|
||||
return __thread_join(tid)
|
||||
}
|
||||
|
||||
// ── parallel_map ─────────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_map — map an El function over a list of strings concurrently.
|
||||
//
|
||||
// items: [String] — the input list
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
//
|
||||
// Spawns one thread per item. All threads run concurrently. Joins each thread
|
||||
// in input order, so the output list preserves the same order as the input.
|
||||
//
|
||||
// This is the core primitive that replaces bash fan-out for parallel HTTP.
|
||||
// Example — dispatch to N rooms at once:
|
||||
// let responses: [String] = parallel_map(room_payloads, "dispatch_to_room")
|
||||
fn parallel_map(items: [String], fn_name: String) -> [String] {
|
||||
let n: Int = el_list_len(items)
|
||||
|
||||
// Phase 1: spawn all threads and collect tids in order.
|
||||
let tids: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let tid: Int = spawn(fn_name, item)
|
||||
// Store tid as string so we can hold it in [String].
|
||||
// int_to_str is available as a builtin.
|
||||
let tids = el_list_append(tids, int_to_str(tid))
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Phase 2: join all threads in order, collecting results.
|
||||
let results: [String] = el_list_empty()
|
||||
let j = 0
|
||||
while j < n {
|
||||
let tid_str: String = el_list_get(tids, j)
|
||||
let tid: Int = str_to_int(tid_str)
|
||||
let result: String = join(tid)
|
||||
let results = el_list_append(results, result)
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ── parallel_map_json ────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_map_json — parallel_map over a JSON array string.
|
||||
//
|
||||
// items_json: String — a JSON array of strings, e.g. '["a","b","c"]'
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
//
|
||||
// Parses the JSON array into an [String], runs parallel_map, then serialises
|
||||
// the result list back to a JSON array string. Both input and output are JSON
|
||||
// strings — the common El inter-service format.
|
||||
//
|
||||
// Example:
|
||||
// let out_json: String = parallel_map_json(rooms_json, "dispatch_to_room")
|
||||
fn parallel_map_json(items_json: String, fn_name: String) -> String {
|
||||
let n: Int = json_array_len(items_json)
|
||||
|
||||
// Unpack JSON array into [String].
|
||||
let items: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = json_array_get(items_json, i)
|
||||
let items = el_list_append(items, item)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Run the concurrent map.
|
||||
let results: [String] = parallel_map(items, fn_name)
|
||||
|
||||
// Repack results into a JSON array string.
|
||||
let m: Int = el_list_len(results)
|
||||
let out: String = "["
|
||||
let j = 0
|
||||
while j < m {
|
||||
let val: String = el_list_get(results, j)
|
||||
if j > 0 {
|
||||
let out = out + ","
|
||||
}
|
||||
// Each result is treated as a raw JSON value (object, array, or
|
||||
// quoted string as returned by the worker fn).
|
||||
let out = out + val
|
||||
let j = j + 1
|
||||
}
|
||||
let out = out + "]"
|
||||
return out
|
||||
}
|
||||
|
||||
// ── parallel_filter ──────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_filter — keep items where fn_name returns "true", concurrently.
|
||||
//
|
||||
// items: [String] — the input list
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
// that returns "true" to keep the item or anything else
|
||||
// to discard it
|
||||
//
|
||||
// Runs the predicate fn on all items in parallel. Collects results in order,
|
||||
// preserving the relative order of kept items.
|
||||
fn parallel_filter(items: [String], fn_name: String) -> [String] {
|
||||
let n: Int = el_list_len(items)
|
||||
|
||||
// Spawn a predicate thread for every item.
|
||||
let tids: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let tid: Int = spawn(fn_name, item)
|
||||
let tids = el_list_append(tids, int_to_str(tid))
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Join in order, keep item if the predicate returned "true".
|
||||
let kept: [String] = el_list_empty()
|
||||
let j = 0
|
||||
while j < n {
|
||||
let item: String = el_list_get(items, j)
|
||||
let tid_str: String = el_list_get(tids, j)
|
||||
let tid: Int = str_to_int(tid_str)
|
||||
let verdict: String = join(tid)
|
||||
if str_eq(verdict, "true") {
|
||||
let kept = el_list_append(kept, item)
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
return kept
|
||||
}
|
||||
|
||||
// ── HTTP helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
// fire_http_post — worker fn for parallel_posts.
|
||||
//
|
||||
// Expects arg to be a JSON object with "url" and "body" keys:
|
||||
// {"url":"https://...","body":"{...}"}
|
||||
//
|
||||
// Returns the HTTP response body string. Registered as a global El fn so
|
||||
// parallel_map can locate it via dlsym.
|
||||
fn fire_http_post(arg: String) -> String {
|
||||
let url: String = json_get(arg, "url")
|
||||
let body: String = json_get(arg, "body")
|
||||
return http_post(url, body)
|
||||
}
|
||||
|
||||
// parallel_posts — fire a list of HTTP POSTs concurrently.
|
||||
//
|
||||
// requests: [String] — each element is a JSON object {"url":"...","body":"..."}
|
||||
//
|
||||
// Returns [String] of response bodies in the same order as the input.
|
||||
//
|
||||
// Example — fan out to N room endpoints at once:
|
||||
// let reqs: [String] = el_list_empty()
|
||||
// let reqs = el_list_append(reqs, "{\"url\":\"http://room-a/dispatch\",\"body\":\"" + payload + "\"}")
|
||||
// let reqs = el_list_append(reqs, "{\"url\":\"http://room-b/dispatch\",\"body\":\"" + payload + "\"}")
|
||||
// let responses: [String] = parallel_posts(reqs)
|
||||
fn parallel_posts(requests: [String]) -> [String] {
|
||||
return parallel_map(requests, "fire_http_post")
|
||||
}
|
||||
|
||||
// ── Mutex helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// with_mutex — call fn_name(arg) while holding mutex m.
|
||||
//
|
||||
// m: Int — mutex handle returned by __mutex_new()
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
// arg: String — argument to pass to fn_name
|
||||
//
|
||||
// Locks the mutex, spawns fn_name(arg) in a child thread, joins to collect
|
||||
// the result, then unlocks. The mutex is held across the entire duration of
|
||||
// fn_name's execution, serializing concurrent callers.
|
||||
//
|
||||
// Note: fn_name must NOT itself acquire the same mutex — that would deadlock.
|
||||
// This is the standard reentrant-mutex caveat.
|
||||
//
|
||||
// Usage:
|
||||
// let m: Int = __mutex_new()
|
||||
// let result: String = with_mutex(m, "update_shared_state", payload)
|
||||
fn with_mutex(m: Int, fn_name: String, arg: String) -> String {
|
||||
__mutex_lock(m)
|
||||
let tid: Int = spawn(fn_name, arg)
|
||||
let result: String = join(tid)
|
||||
__mutex_unlock(m)
|
||||
return result
|
||||
}
|
||||
+425
@@ -0,0 +1,425 @@
|
||||
// runtime/time.el — Time operations, sleep, and formatting.
|
||||
//
|
||||
// Implements the time surface from el-compiler/runtime/legacy/el_runtime.c
|
||||
// (lines 3334–3440, 3471–3656) in pure El, using seed primitives.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __time_now_ns() -> Int (nanoseconds since Unix epoch)
|
||||
// __sleep_ms(n: Int)
|
||||
// __int_to_str(n: Int) -> String
|
||||
// __str_to_int(s: String) -> Int
|
||||
// __float_to_str(f: Float) -> String
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — now / sleep
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// time_now — milliseconds since Unix epoch (UTC). Matches legacy time_now().
|
||||
fn time_now() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// time_now_utc — same as time_now; UTC alias kept for compatibility.
|
||||
fn time_now_utc() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// now_ns — nanoseconds since Unix epoch. Matches el_now_instant().
|
||||
fn now_ns() -> Int {
|
||||
return __time_now_ns()
|
||||
}
|
||||
|
||||
// unix_timestamp — whole seconds since Unix epoch. Matches unix_timestamp().
|
||||
fn unix_timestamp() -> Int {
|
||||
return __time_now_ns() / 1000000000
|
||||
}
|
||||
|
||||
// sleep_secs — block for n seconds. Clamps negatives to 0.
|
||||
fn sleep_secs(n: Int) {
|
||||
if n < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(n * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// sleep_ms — block for n milliseconds. Clamps negatives to 0.
|
||||
fn sleep_ms(n: Int) {
|
||||
if n < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(n)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gregorian decomposition helpers — pure integer arithmetic.
|
||||
//
|
||||
// Algorithm: civil date from days since Unix epoch (1970-01-01).
|
||||
// Based on Howard Hinnant's public-domain civil_from_days formula
|
||||
// (http://howardhinnant.github.io/date_algorithms.html), which the legacy
|
||||
// gmtime_r call performs under the hood.
|
||||
//
|
||||
// We expose the pieces as private helpers (leading underscore convention).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// _is_leap — 1 if year y is a Gregorian leap year, 0 otherwise.
|
||||
fn _is_leap(y: Int) -> Int {
|
||||
if y % 400 == 0 { return 1 }
|
||||
if y % 100 == 0 { return 0 }
|
||||
if y % 4 == 0 { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
// _days_in_month — number of days in month m of year y (m: 1..12).
|
||||
fn _days_in_month(y: Int, m: Int) -> Int {
|
||||
if m == 1 { return 31 }
|
||||
if m == 2 {
|
||||
if _is_leap(y) == 1 { return 29 }
|
||||
return 28
|
||||
}
|
||||
if m == 3 { return 31 }
|
||||
if m == 4 { return 30 }
|
||||
if m == 5 { return 31 }
|
||||
if m == 6 { return 30 }
|
||||
if m == 7 { return 31 }
|
||||
if m == 8 { return 31 }
|
||||
if m == 9 { return 30 }
|
||||
if m == 10 { return 31 }
|
||||
if m == 11 { return 30 }
|
||||
return 31
|
||||
}
|
||||
|
||||
// _pad2 — zero-pad an integer to at least 2 digits.
|
||||
fn _pad2(n: Int) -> String {
|
||||
if n < 10 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _pad4 — zero-pad an integer to at least 4 digits.
|
||||
fn _pad4(n: Int) -> String {
|
||||
if n < 10 { return "000" + __int_to_str(n) }
|
||||
if n < 100 { return "00" + __int_to_str(n) }
|
||||
if n < 1000 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _pad3 — zero-pad an integer to at least 3 digits (milliseconds).
|
||||
fn _pad3(n: Int) -> String {
|
||||
if n < 10 { return "00" + __int_to_str(n) }
|
||||
if n < 100 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _civil_year_month_day — decompose days-since-epoch (z, may be negative)
|
||||
// into year/month/day using the civil_from_days algorithm.
|
||||
// Returns a JSON object: {"year":Y,"month":M,"day":D}
|
||||
fn _civil_ymd(z: Int) -> String {
|
||||
// shift epoch to 0000-03-01 (makes leap-day math clean)
|
||||
let zz: Int = z + 719468
|
||||
// era: 400-year block
|
||||
let era: Int = zz / 146097
|
||||
if zz < 0 {
|
||||
era = (zz - 146096) / 146097
|
||||
}
|
||||
let doe: Int = zz - era * 146097 // day-of-era [0, 146096]
|
||||
let yoe: Int = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365 // year-of-era [0, 399]
|
||||
let y: Int = yoe + era * 400
|
||||
let doy: Int = doe - (365 * yoe + yoe / 4 - yoe / 100) // day-of-year [0, 365]
|
||||
let mp: Int = (5 * doy + 2) / 153 // month in [0, 11] from March
|
||||
let d: Int = doy - (153 * mp + 2) / 5 + 1 // day [1, 31]
|
||||
let m: Int = mp + 3
|
||||
if mp >= 10 { m = mp - 9 }
|
||||
if mp >= 10 { y = y + 1 }
|
||||
return "{\"year\":" + __int_to_str(y) + ",\"month\":" + __int_to_str(m) + ",\"day\":" + __int_to_str(d) + "}"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_to_parts — decompose a millisecond timestamp into UTC components.
|
||||
//
|
||||
// Returns a JSON string:
|
||||
// {"year":Y,"month":M,"day":D,"hour":H,"minute":M,"second":S,"ms":MS}
|
||||
//
|
||||
// Matches legacy time_to_parts() which returns an ElMap with the same keys.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_to_parts(ts: Int) -> String {
|
||||
let ms_raw: Int = ts % 1000
|
||||
let ms: Int = ms_raw
|
||||
if ms_raw < 0 { ms = ms_raw + 1000 }
|
||||
|
||||
let s_raw: Int = ts / 1000
|
||||
let s: Int = s_raw
|
||||
if ms_raw < 0 { s = s_raw - 1 }
|
||||
|
||||
// seconds within the day and days since epoch
|
||||
let sec_of_day: Int = s % 86400
|
||||
let day_z: Int = s / 86400
|
||||
// handle negative: floor division
|
||||
if sec_of_day < 0 {
|
||||
sec_of_day = sec_of_day + 86400
|
||||
day_z = day_z - 1
|
||||
}
|
||||
|
||||
let hour: Int = sec_of_day / 3600
|
||||
let rem: Int = sec_of_day % 3600
|
||||
let minute: Int = rem / 60
|
||||
let second: Int = rem % 60
|
||||
|
||||
// date components via civil decomposition
|
||||
let ymd: String = _civil_ymd(day_z)
|
||||
let year: Int = __str_to_int(json_get(ymd, "year"))
|
||||
let month: Int = __str_to_int(json_get(ymd, "month"))
|
||||
let day: Int = __str_to_int(json_get(ymd, "day"))
|
||||
|
||||
return "{\"year\":" + __int_to_str(year) +
|
||||
",\"month\":" + __int_to_str(month) +
|
||||
",\"day\":" + __int_to_str(day) +
|
||||
",\"hour\":" + __int_to_str(hour) +
|
||||
",\"minute\":" + __int_to_str(minute) +
|
||||
",\"second\":" + __int_to_str(second) +
|
||||
",\"ms\":" + __int_to_str(ms) + "}"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_format — format a millisecond timestamp as a string.
|
||||
//
|
||||
// fmt "ISO" (or empty) → "YYYY-MM-DDTHH:MM:SS.mmmZ" (ISO 8601 UTC)
|
||||
// Other fmt values are passed as a strftime-style hint; the El runtime
|
||||
// implements the most common tokens. Unsupported tokens are passed through.
|
||||
//
|
||||
// Matches legacy time_format().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_format(ts: Int, fmt: String) -> String {
|
||||
let parts: String = time_to_parts(ts)
|
||||
let y: Int = __str_to_int(json_get(parts, "year"))
|
||||
let mo: Int = __str_to_int(json_get(parts, "month"))
|
||||
let d: Int = __str_to_int(json_get(parts, "day"))
|
||||
let h: Int = __str_to_int(json_get(parts, "hour"))
|
||||
let mi: Int = __str_to_int(json_get(parts, "minute"))
|
||||
let s: Int = __str_to_int(json_get(parts, "second"))
|
||||
let ms: Int = __str_to_int(json_get(parts, "ms"))
|
||||
|
||||
// ISO 8601 UTC: YYYY-MM-DDTHH:MM:SS.mmmZ
|
||||
if str_eq(fmt, "ISO") {
|
||||
return _pad4(y) + "-" + _pad2(mo) + "-" + _pad2(d) +
|
||||
"T" + _pad2(h) + ":" + _pad2(mi) + ":" + _pad2(s) +
|
||||
"." + _pad3(ms) + "Z"
|
||||
}
|
||||
if str_eq(fmt, "") {
|
||||
return _pad4(y) + "-" + _pad2(mo) + "-" + _pad2(d) +
|
||||
"T" + _pad2(h) + ":" + _pad2(mi) + ":" + _pad2(s) +
|
||||
"." + _pad3(ms) + "Z"
|
||||
}
|
||||
|
||||
// strftime-subset: replace common tokens
|
||||
let out: String = fmt
|
||||
let out = str_replace(out, "%Y", _pad4(y))
|
||||
let out = str_replace(out, "%m", _pad2(mo))
|
||||
let out = str_replace(out, "%d", _pad2(d))
|
||||
let out = str_replace(out, "%H", _pad2(h))
|
||||
let out = str_replace(out, "%M", _pad2(mi))
|
||||
let out = str_replace(out, "%S", _pad2(s))
|
||||
let out = str_replace(out, "%3N", _pad3(ms))
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_from_parts — construct a ms timestamp from seconds + nanosecond offset.
|
||||
//
|
||||
// Matches legacy time_from_parts(secs, ns, tz) — tz is ignored (UTC assumed).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_from_parts(secs: Int, ns: Int, tz: String) -> Int {
|
||||
return secs * 1000 + ns / 1000000
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_add — add a duration to a millisecond timestamp.
|
||||
//
|
||||
// unit: "ms" | "sec" | "min" | "hour" | "day"
|
||||
// Matches legacy time_add().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_add(ts: Int, n: Int, unit: String) -> Int {
|
||||
if str_eq(unit, "ms") { return ts + n }
|
||||
if str_eq(unit, "sec") { return ts + n * 1000 }
|
||||
if str_eq(unit, "min") { return ts + n * 60000 }
|
||||
if str_eq(unit, "hour") { return ts + n * 3600000 }
|
||||
if str_eq(unit, "day") { return ts + n * 86400000 }
|
||||
// default: treat as ms
|
||||
return ts + n
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_diff — compute the difference between two millisecond timestamps.
|
||||
//
|
||||
// Returns ts2 - ts1 in the given unit.
|
||||
// unit: "ms" | "sec" | "min" | "hour" | "day"
|
||||
// Matches legacy time_diff().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_diff(ts1: Int, ts2: Int, unit: String) -> Int {
|
||||
let d: Int = ts2 - ts1
|
||||
if str_eq(unit, "ms") { return d }
|
||||
if str_eq(unit, "sec") { return d / 1000 }
|
||||
if str_eq(unit, "min") { return d / 60000 }
|
||||
if str_eq(unit, "hour") { return d / 3600000 }
|
||||
if str_eq(unit, "day") { return d / 86400000 }
|
||||
return d
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Instant / Duration — nanosecond-precision temporal types.
|
||||
//
|
||||
// These match the el_now_instant, duration_seconds, duration_millis, etc.
|
||||
// family from legacy lines 3471–3656. Both Instant and Duration are Int
|
||||
// (nanoseconds); the type distinction is at the call-site convention level.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// now — current Instant in nanoseconds. Alias for __time_now_ns().
|
||||
fn now() -> Int {
|
||||
return __time_now_ns()
|
||||
}
|
||||
|
||||
// unix_seconds — Instant from whole seconds since epoch.
|
||||
fn unix_seconds(n: Int) -> Int {
|
||||
return n * 1000000000
|
||||
}
|
||||
|
||||
// unix_millis — Instant from milliseconds since epoch.
|
||||
fn unix_millis(n: Int) -> Int {
|
||||
return n * 1000000
|
||||
}
|
||||
|
||||
// instant_to_unix_seconds — convert Instant nanoseconds to whole seconds.
|
||||
fn instant_to_unix_seconds(i: Int) -> Int {
|
||||
return i / 1000000000
|
||||
}
|
||||
|
||||
// instant_to_unix_millis — convert Instant nanoseconds to milliseconds.
|
||||
fn instant_to_unix_millis(i: Int) -> Int {
|
||||
return i / 1000000
|
||||
}
|
||||
|
||||
// instant_to_iso8601 — format an Instant (nanoseconds) as ISO 8601 UTC.
|
||||
fn instant_to_iso8601(i: Int) -> String {
|
||||
let ms: Int = i / 1000000
|
||||
return time_format(ms, "ISO")
|
||||
}
|
||||
|
||||
// duration_seconds — Duration from n whole seconds.
|
||||
fn duration_seconds(n: Int) -> Int {
|
||||
return n * 1000000000
|
||||
}
|
||||
|
||||
// duration_millis — Duration from n milliseconds.
|
||||
fn duration_millis(n: Int) -> Int {
|
||||
return n * 1000000
|
||||
}
|
||||
|
||||
// duration_nanos — Duration from n nanoseconds (identity).
|
||||
fn duration_nanos(n: Int) -> Int {
|
||||
return n
|
||||
}
|
||||
|
||||
// duration_to_seconds — convert a Duration (nanoseconds) to whole seconds.
|
||||
fn duration_to_seconds(d: Int) -> Int {
|
||||
return d / 1000000000
|
||||
}
|
||||
|
||||
// duration_to_millis — convert a Duration (nanoseconds) to milliseconds.
|
||||
fn duration_to_millis(d: Int) -> Int {
|
||||
return d / 1000000
|
||||
}
|
||||
|
||||
// duration_to_nanos — return the Duration as nanoseconds (identity).
|
||||
fn duration_to_nanos(d: Int) -> Int {
|
||||
return d
|
||||
}
|
||||
|
||||
// sleep_duration — sleep for a Duration (nanoseconds). Clamps negatives to 0.
|
||||
fn sleep_duration(dur: Int) {
|
||||
let ms: Int = dur / 1000000
|
||||
if ms < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(ms)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TTL cache — time-bounded key/value backed by state.
|
||||
//
|
||||
// Matches legacy ttl_cache_set / ttl_cache_get / ttl_cache_age (lines 3663–3717).
|
||||
// max_age is a Duration (nanoseconds).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ttl_cache_set — store a value and record the current Instant for TTL checks.
|
||||
fn ttl_cache_set(key: String, value: String) {
|
||||
state_set(key, value)
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let now_str: String = __int_to_str(__time_now_ns())
|
||||
state_set(stamp_key, now_str)
|
||||
}
|
||||
|
||||
// ttl_cache_get — return value if age < max_age (nanoseconds), else "".
|
||||
fn ttl_cache_get(key: String, max_age: Int) -> String {
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let sv: String = state_get(stamp_key)
|
||||
if str_eq(sv, "") { return "" }
|
||||
let set_at: Int = __str_to_int(sv)
|
||||
let now_ns: Int = __time_now_ns()
|
||||
let age: Int = now_ns - set_at
|
||||
if age < 0 { return "" }
|
||||
if age > max_age { return "" }
|
||||
return state_get(key)
|
||||
}
|
||||
|
||||
// ttl_cache_age — nanoseconds since a key was last set (INT_MAX sentinel if missing).
|
||||
fn ttl_cache_age(key: String) -> Int {
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let sv: String = state_get(stamp_key)
|
||||
if str_eq(sv, "") { return 9223372036854775807 }
|
||||
let set_at: Int = __str_to_int(sv)
|
||||
return __time_now_ns() - set_at
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// uuid_new / uuid_v4 — generate a UUID v4 string.
|
||||
//
|
||||
// Delegates to the __uuid_v4() seed primitive.
|
||||
// Matches legacy uuid_new() / uuid_v4() (lines 4602–4621).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn uuid_new() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
fn uuid_v4() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DHARMA-compatible aliases — millisecond-precision timestamps.
|
||||
//
|
||||
// now_millis, unix_timestamp_ms, and time_now_ms all return the same value:
|
||||
// milliseconds since the Unix epoch. They exist because different parts of
|
||||
// the dharma codebase use different names for the same concept.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// now_millis — milliseconds since Unix epoch. Alias for time_now().
|
||||
fn now_millis() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// unix_timestamp_ms — same as now_millis.
|
||||
fn unix_timestamp_ms() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// time_now_ms — same as now_millis.
|
||||
fn time_now_ms() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// tests/runtime/string_test.el — Test suite for runtime/string.el
|
||||
//
|
||||
// Exercises every public function exported by runtime/string.el using the
|
||||
// runtime/test.el framework. Each test function covers one string primitive
|
||||
// or a tight family of related functions.
|
||||
//
|
||||
// ── Build and run ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// # Combine runtime modules + test framework + this file, then compile:
|
||||
// cat runtime/string.el runtime/math.el runtime/state.el runtime/env.el \
|
||||
// runtime/fs.el runtime/exec.el runtime/time.el runtime/json.el \
|
||||
// runtime/http.el runtime/engram.el runtime/thread.el \
|
||||
// runtime/test.el \
|
||||
// tests/runtime/string_test.el > /tmp/string_test_combined.el
|
||||
//
|
||||
// ./dist/platform/elc /tmp/string_test_combined.el > /tmp/string_test.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o /tmp/string_test /tmp/string_test.c el-compiler/runtime/el_seed.c
|
||||
// /tmp/string_test; echo "exit: $?"
|
||||
//
|
||||
// Exit code equals the number of failing assertions (0 = all pass).
|
||||
//
|
||||
// ── Coverage ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// str_eq str_neq (via assert_neq)
|
||||
// str_len str_concat
|
||||
// str_starts_with str_ends_with
|
||||
// str_contains str_index_of
|
||||
// str_slice str_replace
|
||||
// str_to_upper str_to_lower
|
||||
// str_trim str_lstrip / str_rstrip
|
||||
// str_split str_join
|
||||
// int_to_str str_to_int
|
||||
// str_repeat str_reverse
|
||||
// str_count
|
||||
|
||||
// ── str_eq ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_eq(_: String) -> String {
|
||||
assert_true(str_eq("hello", "hello"), "identical strings are equal")
|
||||
assert_false(str_eq("hello", "world"), "different strings are not equal")
|
||||
assert_true(str_eq("", ""), "empty strings are equal")
|
||||
assert_false(str_eq("a", ""), "non-empty vs empty is not equal")
|
||||
assert_false(str_eq("", "a"), "empty vs non-empty is not equal")
|
||||
assert_false(str_eq("Hello", "hello"), "case-sensitive comparison")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_len ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_len(_: String) -> String {
|
||||
assert_int_eq(str_len(""), 0, "empty string has length 0")
|
||||
assert_int_eq(str_len("a"), 1, "single char has length 1")
|
||||
assert_int_eq(str_len("hello"), 5, "hello has length 5")
|
||||
assert_int_eq(str_len("hello world"), 11, "space included in length")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_concat ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_concat(_: String) -> String {
|
||||
assert_eq(str_concat("hello", " world"), "hello world", "basic concat")
|
||||
assert_eq(str_concat("", "world"), "world", "empty prefix")
|
||||
assert_eq(str_concat("hello", ""), "hello", "empty suffix")
|
||||
assert_eq(str_concat("", ""), "", "both empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_starts_with ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_starts_with(_: String) -> String {
|
||||
assert_true(str_starts_with("hello world", "hello"), "prefix present")
|
||||
assert_false(str_starts_with("hello world", "world"), "not a prefix")
|
||||
assert_true(str_starts_with("hello", "hello"), "string is its own prefix")
|
||||
assert_true(str_starts_with("hello", ""), "empty prefix always true")
|
||||
assert_false(str_starts_with("", "a"), "empty string has no prefix")
|
||||
assert_false(str_starts_with("hi", "hello"), "prefix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_ends_with ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_ends_with(_: String) -> String {
|
||||
assert_true(str_ends_with("hello world", "world"), "suffix present")
|
||||
assert_false(str_ends_with("hello world", "hello"), "not a suffix")
|
||||
assert_true(str_ends_with("hello", "hello"), "string is its own suffix")
|
||||
assert_true(str_ends_with("hello", ""), "empty suffix always true")
|
||||
assert_false(str_ends_with("", "a"), "empty string has no suffix")
|
||||
assert_false(str_ends_with("hi", "world"), "suffix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_contains ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_contains(_: String) -> String {
|
||||
assert_true(str_contains("hello world", "world"), "contains at end")
|
||||
assert_true(str_contains("hello world", "hello"), "contains at start")
|
||||
assert_true(str_contains("hello world", "lo wo"), "contains in middle")
|
||||
assert_false(str_contains("hello world", "xyz"), "not contained")
|
||||
assert_true(str_contains("hello", ""), "empty sub always contained")
|
||||
assert_false(str_contains("", "a"), "empty string contains nothing")
|
||||
assert_true(str_contains("hello", "hello"), "string contains itself")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_index_of ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_index_of(_: String) -> String {
|
||||
assert_int_eq(str_index_of("hello world", "world"), 6, "index of suffix")
|
||||
assert_int_eq(str_index_of("hello world", "hello"), 0, "index of prefix")
|
||||
assert_int_eq(str_index_of("hello world", "o"), 4, "index of first occurrence")
|
||||
assert_int_eq(str_index_of("hello world", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_index_of("hello", ""), 0, "empty sub returns 0")
|
||||
assert_int_eq(str_index_of("", "a"), -1, "search in empty string")
|
||||
assert_int_eq(str_index_of("aababc", "ab"), 1, "finds first occurrence")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_replace ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_replace(_: String) -> String {
|
||||
assert_eq(str_replace("hello world", "world", "there"), "hello there", "basic replace")
|
||||
assert_eq(str_replace("aaa", "a", "b"), "bbb", "replace all occurrences")
|
||||
assert_eq(str_replace("hello", "xyz", "abc"), "hello", "no match is identity")
|
||||
assert_eq(str_replace("", "a", "b"), "", "empty string unchanged")
|
||||
assert_eq(str_replace("hello", "", "x"), "hello", "empty from is identity")
|
||||
assert_eq(str_replace("hello hello", "hello", "bye"), "bye bye", "replace multiple")
|
||||
assert_eq(str_replace("aXbXc", "X", "-"), "a-b-c", "single-char delimiter")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_slice ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_slice(_: String) -> String {
|
||||
assert_eq(str_slice("hello world", 0, 5), "hello", "slice from start")
|
||||
assert_eq(str_slice("hello world", 6, 11), "world", "slice from middle")
|
||||
assert_eq(str_slice("hello world", 0, 0), "", "zero-length slice")
|
||||
assert_eq(str_slice("hello", 0, 100), "hello", "end beyond length clamped")
|
||||
assert_eq(str_slice("hello", 3, 3), "", "start == end is empty")
|
||||
assert_eq(str_slice("hello world", 2, 7), "llo w", "interior slice")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_to_upper / str_to_lower ──────────────────────────────────────────────
|
||||
|
||||
fn test_str_to_upper(_: String) -> String {
|
||||
assert_eq(str_to_upper("hello"), "HELLO", "lowercase to uppercase")
|
||||
assert_eq(str_to_upper("HELLO"), "HELLO", "already uppercase unchanged")
|
||||
assert_eq(str_to_upper("Hello World"), "HELLO WORLD", "mixed case")
|
||||
assert_eq(str_to_upper(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_upper("hello123"), "HELLO123", "digits unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_lower(_: String) -> String {
|
||||
assert_eq(str_to_lower("HELLO"), "hello", "uppercase to lowercase")
|
||||
assert_eq(str_to_lower("hello"), "hello", "already lowercase unchanged")
|
||||
assert_eq(str_to_lower("Hello World"), "hello world", "mixed case")
|
||||
assert_eq(str_to_lower(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_lower("HELLO123"), "hello123", "digits unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_trim ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_trim(_: String) -> String {
|
||||
assert_eq(str_trim(" hello "), "hello", "trims spaces both sides")
|
||||
assert_eq(str_trim("hello"), "hello", "no whitespace unchanged")
|
||||
assert_eq(str_trim(" "), "", "all-space string becomes empty")
|
||||
assert_eq(str_trim(""), "", "empty string unchanged")
|
||||
assert_eq(str_trim("\t hello \n"), "hello", "trims tabs and newlines")
|
||||
assert_eq(str_trim(" hello world "), "hello world", "internal spaces preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_split ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_split(_: String) -> String {
|
||||
let parts: [String] = str_split("a,b,c", ",")
|
||||
assert_int_eq(el_list_len(parts), 3, "split yields 3 parts")
|
||||
assert_eq(el_list_get(parts, 0), "a", "first part is a")
|
||||
assert_eq(el_list_get(parts, 1), "b", "second part is b")
|
||||
assert_eq(el_list_get(parts, 2), "c", "third part is c")
|
||||
|
||||
let single: [String] = str_split("hello", ",")
|
||||
assert_int_eq(el_list_len(single), 1, "no sep found yields 1 part")
|
||||
assert_eq(el_list_get(single, 0), "hello", "single part is the full string")
|
||||
|
||||
let trailing: [String] = str_split("a,b,", ",")
|
||||
assert_int_eq(el_list_len(trailing), 3, "trailing sep yields empty last element")
|
||||
assert_eq(el_list_get(trailing, 2), "", "last element is empty")
|
||||
|
||||
let empty_str: [String] = str_split("", ",")
|
||||
assert_int_eq(el_list_len(empty_str), 1, "splitting empty string yields one empty element")
|
||||
|
||||
let multi: [String] = str_split("one::two::three", "::")
|
||||
assert_int_eq(el_list_len(multi), 3, "multi-char separator works")
|
||||
assert_eq(el_list_get(multi, 0), "one", "multi-sep first part")
|
||||
assert_eq(el_list_get(multi, 2), "three", "multi-sep last part")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_join ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_join(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "a")
|
||||
let parts = el_list_append(parts, "b")
|
||||
let parts = el_list_append(parts, "c")
|
||||
assert_eq(str_join(parts, ","), "a,b,c", "basic join with comma")
|
||||
assert_eq(str_join(parts, ""), "abc", "join with empty separator")
|
||||
assert_eq(str_join(parts, " | "), "a | b | c", "join with multi-char sep")
|
||||
|
||||
let empty_list: [String] = el_list_empty()
|
||||
assert_eq(str_join(empty_list, ","), "", "joining empty list yields empty string")
|
||||
|
||||
let one: [String] = el_list_empty()
|
||||
let one = el_list_append(one, "solo")
|
||||
assert_eq(str_join(one, ","), "solo", "joining single element yields that element")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── int_to_str / str_to_int ──────────────────────────────────────────────────
|
||||
|
||||
fn test_int_to_str(_: String) -> String {
|
||||
assert_eq(int_to_str(0), "0", "zero")
|
||||
assert_eq(int_to_str(42), "42", "positive integer")
|
||||
assert_eq(int_to_str(-1), "-1", "negative integer")
|
||||
assert_eq(int_to_str(1000000), "1000000", "large integer")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_int(_: String) -> String {
|
||||
assert_int_eq(str_to_int("0"), 0, "zero")
|
||||
assert_int_eq(str_to_int("42"), 42, "positive integer")
|
||||
assert_int_eq(str_to_int("-1"), -1, "negative integer")
|
||||
assert_int_eq(str_to_int("1000000"), 1000000, "large integer")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_repeat ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_repeat(_: String) -> String {
|
||||
assert_eq(str_repeat("ab", 3), "ababab", "repeat 3 times")
|
||||
assert_eq(str_repeat("x", 1), "x", "repeat once")
|
||||
assert_eq(str_repeat("x", 0), "", "repeat zero times yields empty")
|
||||
assert_eq(str_repeat("", 5), "", "repeating empty string yields empty")
|
||||
assert_eq(str_repeat("-", 4), "----", "single char repeat")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_reverse ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_reverse(_: String) -> String {
|
||||
assert_eq(str_reverse("hello"), "olleh", "basic reverse")
|
||||
assert_eq(str_reverse("a"), "a", "single char is its own reverse")
|
||||
assert_eq(str_reverse(""), "", "empty string reverses to empty")
|
||||
assert_eq(str_reverse("abcd"), "dcba", "even-length reverse")
|
||||
assert_eq(str_reverse("racecar"), "racecar", "palindrome is unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_count ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_count(_: String) -> String {
|
||||
assert_int_eq(str_count("hello world hello", "hello"), 2, "two occurrences")
|
||||
assert_int_eq(str_count("aaa", "a"), 3, "adjacent single chars")
|
||||
assert_int_eq(str_count("aaa", "aa"), 1, "non-overlapping: one match")
|
||||
assert_int_eq(str_count("hello", "xyz"), 0, "no match")
|
||||
assert_int_eq(str_count("", "a"), 0, "empty string has no occurrences")
|
||||
assert_int_eq(str_count("hello", ""), 0, "empty sub returns 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_strip_prefix / str_strip_suffix ───────────────────────────────────────
|
||||
|
||||
fn test_str_strip_prefix(_: String) -> String {
|
||||
assert_eq(str_strip_prefix("foobar", "foo"), "bar", "strips matching prefix")
|
||||
assert_eq(str_strip_prefix("foobar", "baz"), "foobar", "no-match is identity")
|
||||
assert_eq(str_strip_prefix("hello", ""), "hello", "empty prefix is identity")
|
||||
assert_eq(str_strip_prefix("hello", "hello"), "", "full match yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_strip_suffix(_: String) -> String {
|
||||
assert_eq(str_strip_suffix("hello.md", ".md"), "hello", "strips matching suffix")
|
||||
assert_eq(str_strip_suffix("hello.md", ".txt"), "hello.md", "no-match is identity")
|
||||
assert_eq(str_strip_suffix("hello", ""), "hello", "empty suffix is identity")
|
||||
assert_eq(str_strip_suffix("hello", "hello"), "", "full match yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_find_chars ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_find_chars(_: String) -> String {
|
||||
assert_int_eq(str_find_chars("hello world", " "), 5, "finds space at index 5")
|
||||
assert_int_eq(str_find_chars("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_find_chars("hello", ""), -1, "empty charset returns -1")
|
||||
assert_int_eq(str_find_chars("hello", "aeiou"), 1, "finds first vowel at index 1")
|
||||
assert_int_eq(str_find_chars("", "a"), -1, "search in empty string returns -1")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_last_index_of ─────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_last_index_of(_: String) -> String {
|
||||
assert_int_eq(str_last_index_of("hello hello", "hello"), 6, "last occurrence")
|
||||
assert_int_eq(str_last_index_of("hello", "hello"), 0, "single occurrence")
|
||||
assert_int_eq(str_last_index_of("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_last_index_of("aababc", "ab"), 3, "last ab in aababc")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() -> Int {
|
||||
test_case("str_eq", "test_str_eq")
|
||||
test_case("str_len", "test_str_len")
|
||||
test_case("str_concat", "test_str_concat")
|
||||
test_case("str_starts_with", "test_str_starts_with")
|
||||
test_case("str_ends_with", "test_str_ends_with")
|
||||
test_case("str_contains", "test_str_contains")
|
||||
test_case("str_index_of", "test_str_index_of")
|
||||
test_case("str_replace", "test_str_replace")
|
||||
test_case("str_slice", "test_str_slice")
|
||||
test_case("str_to_upper", "test_str_to_upper")
|
||||
test_case("str_to_lower", "test_str_to_lower")
|
||||
test_case("str_trim", "test_str_trim")
|
||||
test_case("str_split", "test_str_split")
|
||||
test_case("str_join", "test_str_join")
|
||||
test_case("int_to_str", "test_int_to_str")
|
||||
test_case("str_to_int", "test_str_to_int")
|
||||
test_case("str_repeat", "test_str_repeat")
|
||||
test_case("str_reverse", "test_str_reverse")
|
||||
test_case("str_count", "test_str_count")
|
||||
test_case("str_strip_prefix", "test_str_strip_prefix")
|
||||
test_case("str_strip_suffix", "test_str_strip_suffix")
|
||||
test_case("str_find_chars", "test_str_find_chars")
|
||||
test_case("str_last_index_of", "test_str_last_index_of")
|
||||
return test_run_all()
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* tests/suite/infra.c — El runtime infrastructure functions for the test suite.
|
||||
*
|
||||
* The El test suite compiles runtime/*.el files into tests.c via elc, then links
|
||||
* only against el_seed.c (not the full el_runtime.c, which would cause ~120
|
||||
* duplicate symbol errors). This file provides the infrastructure symbols that
|
||||
* tests.c references but that are NOT defined in either the generated El code or
|
||||
* el_seed.c:
|
||||
*
|
||||
* ─ ElList machinery: el_list_empty, el_list_append, el_list_len, el_list_get,
|
||||
* el_str_concat, native_list_*, len, get
|
||||
* ─ el_runtime_init_args (needed by the generated main())
|
||||
* ─ Stubs for http_*, engram_*, el_html_sanitize (not needed by tests)
|
||||
*
|
||||
* These implementations are derived from el_runtime.c.
|
||||
*/
|
||||
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
#include "el_seed.h"
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── String arena (minimal — no HTTP request lifecycle needed) ─────────────── */
|
||||
|
||||
static char* el_strdup(const char* s) {
|
||||
if (!s) return strdup("");
|
||||
return strdup(s);
|
||||
}
|
||||
|
||||
static char* el_strbuf(size_t n) {
|
||||
char* p = malloc(n + 1);
|
||||
if (!p) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
p[0] = '\0';
|
||||
return p;
|
||||
}
|
||||
|
||||
static el_val_t el_wrap_str(char* s) {
|
||||
return EL_STR(s);
|
||||
}
|
||||
|
||||
/* ── el_str_concat ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t av, el_val_t bv) {
|
||||
const char* a = EL_CSTR(av);
|
||||
const char* b = EL_CSTR(bv);
|
||||
if (!a) a = "";
|
||||
if (!b) b = "";
|
||||
size_t la = strlen(a);
|
||||
size_t lb = strlen(b);
|
||||
char* out = el_strbuf(la + lb);
|
||||
memcpy(out, a, la);
|
||||
memcpy(out + la, b, lb);
|
||||
out[la + lb] = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* ── ElList ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
#define EL_MAGIC_LIST 0xE15710A1u
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint32_t refcount;
|
||||
} ElHeader;
|
||||
|
||||
typedef struct {
|
||||
ElHeader hdr;
|
||||
int64_t length;
|
||||
int64_t capacity;
|
||||
el_val_t* elems;
|
||||
} ElList;
|
||||
|
||||
static ElList* list_alloc(int64_t cap) {
|
||||
if (cap < 4) cap = 4;
|
||||
ElList* lst = malloc(sizeof(ElList));
|
||||
if (!lst) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
lst->hdr.magic = EL_MAGIC_LIST;
|
||||
lst->hdr.refcount = 1;
|
||||
lst->length = 0;
|
||||
lst->capacity = cap;
|
||||
lst->elems = malloc((size_t)cap * sizeof(el_val_t));
|
||||
if (!lst->elems) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
return lst;
|
||||
}
|
||||
|
||||
el_val_t el_list_empty(void) {
|
||||
return EL_STR(list_alloc(4));
|
||||
}
|
||||
|
||||
el_val_t el_list_len(el_val_t listv) {
|
||||
ElList* lst = (ElList*)(uintptr_t)listv;
|
||||
if (!lst) return 0;
|
||||
return lst->length;
|
||||
}
|
||||
|
||||
el_val_t el_list_get(el_val_t listv, el_val_t index) {
|
||||
ElList* lst = (ElList*)(uintptr_t)listv;
|
||||
if (!lst) return 0;
|
||||
if (index < 0 || index >= lst->length) return 0;
|
||||
return lst->elems[index];
|
||||
}
|
||||
|
||||
el_val_t el_list_append(el_val_t listv, el_val_t elem) {
|
||||
ElList* old = (ElList*)(uintptr_t)listv;
|
||||
if (!old) {
|
||||
ElList* fresh = list_alloc(4);
|
||||
fresh->elems[0] = elem;
|
||||
fresh->length = 1;
|
||||
return EL_STR(fresh);
|
||||
}
|
||||
|
||||
if (old->hdr.refcount <= 1) {
|
||||
if (old->length >= old->capacity) {
|
||||
int64_t new_cap = old->capacity > 0 ? old->capacity * 2 : 4;
|
||||
el_val_t* grown = realloc(old->elems, (size_t)new_cap * sizeof(el_val_t));
|
||||
if (!grown) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
old->elems = grown;
|
||||
old->capacity = new_cap;
|
||||
}
|
||||
old->elems[old->length++] = elem;
|
||||
return listv;
|
||||
}
|
||||
|
||||
int64_t new_cap = old->length + 1;
|
||||
if (new_cap < 4) new_cap = 4;
|
||||
ElList* fresh = malloc(sizeof(ElList));
|
||||
if (!fresh) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
fresh->hdr.magic = EL_MAGIC_LIST;
|
||||
fresh->hdr.refcount = 1;
|
||||
fresh->length = old->length + 1;
|
||||
fresh->capacity = new_cap;
|
||||
fresh->elems = malloc((size_t)new_cap * sizeof(el_val_t));
|
||||
if (!fresh->elems) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
if (old->length > 0) {
|
||||
memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t));
|
||||
}
|
||||
fresh->elems[old->length] = elem;
|
||||
return EL_STR(fresh);
|
||||
}
|
||||
|
||||
/* ── native_list aliases ──────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t native_list_empty(void) { return el_list_empty(); }
|
||||
el_val_t native_list_append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); }
|
||||
el_val_t native_list_get(el_val_t list, el_val_t index) { return el_list_get(list, index); }
|
||||
el_val_t native_list_len(el_val_t list) { return el_list_len(list); }
|
||||
|
||||
/* ── len / get aliases ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t len(el_val_t list) { return el_list_len(list); }
|
||||
el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); }
|
||||
|
||||
/* ── el_runtime_init_args ─────────────────────────────────────────────────── */
|
||||
|
||||
static el_val_t _el_args_list = 0;
|
||||
|
||||
void el_runtime_init_args(int argc, char** argv) {
|
||||
_el_args_list = el_list_empty();
|
||||
for (int i = 1; i < argc; i++) {
|
||||
_el_args_list = el_list_append(_el_args_list, EL_STR(argv[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── HTTP stubs (not exercised by test suite) ─────────────────────────────── */
|
||||
|
||||
el_val_t http_post(el_val_t url, el_val_t body) {
|
||||
(void)url; (void)body;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t http_post_json(el_val_t url, el_val_t body) {
|
||||
(void)url; (void)body;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers) {
|
||||
(void)url; (void)body; (void)headers;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t http_response(el_val_t status, el_val_t headers, el_val_t body) {
|
||||
(void)status; (void)headers; (void)body;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
void http_serve(el_val_t port, el_val_t handler) {
|
||||
(void)port; (void)handler;
|
||||
}
|
||||
|
||||
void http_serve_v2(el_val_t port, el_val_t handler) {
|
||||
(void)port; (void)handler;
|
||||
}
|
||||
|
||||
/* ── el_html_sanitize stub ────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_html_sanitize(el_val_t input, el_val_t allowlist) {
|
||||
(void)allowlist;
|
||||
return input;
|
||||
}
|
||||
|
||||
/* ── Engram stubs (not exercised by test suite) ───────────────────────────── */
|
||||
|
||||
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) {
|
||||
(void)content; (void)node_type; (void)salience;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags) {
|
||||
(void)content; (void)node_type; (void)label; (void)salience;
|
||||
(void)importance; (void)confidence; (void)tier; (void)tags;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t tier, el_val_t layer_id, el_val_t tags) {
|
||||
(void)content; (void)node_type; (void)label; (void)salience;
|
||||
(void)certainty; (void)confidence; (void)tier; (void)layer_id; (void)tags;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable) {
|
||||
(void)name; (void)priority; (void)suppressible; (void)transparent; (void)injectable;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t engram_remove_layer(el_val_t layer_id) {
|
||||
(void)layer_id;
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
el_val_t engram_list_layers(void) { return EL_STR("[]"); }
|
||||
el_val_t engram_list_layers_json(void) { return EL_STR("[]"); }
|
||||
|
||||
el_val_t engram_get_node(el_val_t id) { (void)id; return EL_STR(""); }
|
||||
el_val_t engram_get_node_json(el_val_t id) { (void)id; return EL_STR("{}"); }
|
||||
|
||||
void engram_strengthen(el_val_t node_id) { (void)node_id; }
|
||||
void engram_forget(el_val_t node_id) { (void)node_id; }
|
||||
|
||||
el_val_t engram_node_count(void) { return 0; }
|
||||
el_val_t engram_edge_count(void) { return 0; }
|
||||
el_val_t engram_stats_json(void) { return EL_STR("{}"); }
|
||||
|
||||
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) {
|
||||
(void)from_id; (void)to_id; (void)weight; (void)relation;
|
||||
}
|
||||
|
||||
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id) {
|
||||
(void)from_id; (void)to_id;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit) {
|
||||
(void)query; (void)limit;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||
(void)query; (void)limit;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset) {
|
||||
(void)limit; (void)offset;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) {
|
||||
(void)limit; (void)offset;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset) {
|
||||
(void)node_type; (void)limit; (void)offset;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_neighbors(el_val_t node_id, el_val_t limit) {
|
||||
(void)node_id; (void)limit;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t relation, el_val_t limit) {
|
||||
(void)node_id; (void)relation; (void)limit;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t limit) {
|
||||
(void)node_id; (void)limit;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_load(el_val_t path) { (void)path; return (el_val_t)1; }
|
||||
el_val_t engram_save(el_val_t path) { (void)path; return (el_val_t)1; }
|
||||
|
||||
el_val_t engram_activate(el_val_t node_ids, el_val_t spread, el_val_t decay) {
|
||||
(void)node_ids; (void)spread; (void)decay;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_activate_json(el_val_t node_ids_json, el_val_t spread, el_val_t decay) {
|
||||
(void)node_ids_json; (void)spread; (void)decay;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_compile_layered_json(el_val_t node_id, el_val_t target_tokens,
|
||||
el_val_t strategy) {
|
||||
(void)node_id; (void)target_tokens; (void)strategy;
|
||||
return EL_STR("{}");
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// tests/suite/manifest.el — El test suite vessel manifest.
|
||||
//
|
||||
// Build and run:
|
||||
// cd tests/suite && elb && ./dist/el-tests
|
||||
//
|
||||
// Exit code equals the number of failing assertions (0 = all pass).
|
||||
|
||||
package "el-tests" {
|
||||
version "0.1.0"
|
||||
description "El runtime test suite"
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/test_all.el"
|
||||
output "dist/"
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// tests/suite/src/test_all.el — master entry point for the El comprehensive test suite.
|
||||
//
|
||||
// This is an El vessel. Build and run with:
|
||||
//
|
||||
// cd tests/suite && elb && ./dist/el-tests
|
||||
//
|
||||
// Exit code equals the number of failing assertions (0 = all pass).
|
||||
|
||||
import "../../../runtime/string.el"
|
||||
import "../../../runtime/math.el"
|
||||
import "../../../runtime/state.el"
|
||||
import "../../../runtime/fs.el"
|
||||
import "../../../runtime/json.el"
|
||||
import "../../../runtime/time.el"
|
||||
import "../../../runtime/thread.el"
|
||||
import "../../../runtime/test.el"
|
||||
import "test_string.el"
|
||||
import "test_math.el"
|
||||
import "test_json.el"
|
||||
import "test_state.el"
|
||||
import "test_time.el"
|
||||
import "test_fs.el"
|
||||
import "test_collections.el"
|
||||
|
||||
fn main() -> Int {
|
||||
// ── string tests ───────────────────────────────────────────────────────────
|
||||
test_case("str_eq basic", "test_str_eq_basic")
|
||||
test_case("str_eq symbols", "test_str_eq_symbols")
|
||||
test_case("str_len basic", "test_str_len_basic")
|
||||
test_case("str_len longer", "test_str_len_longer")
|
||||
test_case("str_concat basic", "test_str_concat_basic")
|
||||
test_case("str_concat chaining", "test_str_concat_chaining")
|
||||
test_case("str_slice basic", "test_str_slice_basic")
|
||||
test_case("str_slice edge", "test_str_slice_edge")
|
||||
test_case("str_starts_with basic", "test_str_starts_with_basic")
|
||||
test_case("str_starts_with edge", "test_str_starts_with_edge")
|
||||
test_case("str_ends_with basic", "test_str_ends_with_basic")
|
||||
test_case("str_ends_with edge", "test_str_ends_with_edge")
|
||||
test_case("str_contains basic", "test_str_contains_basic")
|
||||
test_case("str_contains edge", "test_str_contains_edge")
|
||||
test_case("str_index_of basic", "test_str_index_of_basic")
|
||||
test_case("str_index_of duplicates", "test_str_index_of_duplicates")
|
||||
test_case("str_last_index_of basic", "test_str_last_index_of_basic")
|
||||
test_case("str_replace basic", "test_str_replace_basic")
|
||||
test_case("str_replace multiple", "test_str_replace_multiple")
|
||||
test_case("str_to_upper basic", "test_str_to_upper_basic")
|
||||
test_case("str_to_lower basic", "test_str_to_lower_basic")
|
||||
test_case("str_upper_lower roundtrip", "test_str_upper_lower_roundtrip")
|
||||
test_case("str_trim basic", "test_str_trim_basic")
|
||||
test_case("str_lstrip rstrip", "test_str_lstrip_rstrip")
|
||||
test_case("str_split basic", "test_str_split_basic")
|
||||
test_case("str_split edge", "test_str_split_edge")
|
||||
test_case("str_join basic", "test_str_join_basic")
|
||||
test_case("str_join edge", "test_str_join_edge")
|
||||
test_case("int_to_str basic", "test_int_to_str_basic")
|
||||
test_case("str_to_int basic", "test_str_to_int_basic")
|
||||
test_case("float_to_str basic", "test_float_to_str_basic")
|
||||
test_case("str_to_float basic", "test_str_to_float_basic")
|
||||
test_case("str_repeat basic", "test_str_repeat_basic")
|
||||
test_case("str_reverse basic", "test_str_reverse_basic")
|
||||
test_case("str_count basic", "test_str_count_basic")
|
||||
test_case("str_strip_prefix basic", "test_str_strip_prefix_basic")
|
||||
test_case("str_strip_suffix basic", "test_str_strip_suffix_basic")
|
||||
test_case("str_find_chars basic", "test_str_find_chars_basic")
|
||||
test_case("str_char_at basic", "test_str_char_at_basic")
|
||||
test_case("str_char_code basic", "test_str_char_code_basic")
|
||||
test_case("str_pad_left basic", "test_str_pad_left_basic")
|
||||
test_case("str_pad_right basic", "test_str_pad_right_basic")
|
||||
test_case("is_letter basic", "test_is_letter_basic")
|
||||
test_case("is_digit basic", "test_is_digit_basic")
|
||||
test_case("is_whitespace basic", "test_is_whitespace_basic")
|
||||
test_case("str_count_lines basic", "test_str_count_lines_basic")
|
||||
test_case("str_count_words basic", "test_str_count_words_basic")
|
||||
test_case("url_encode basic", "test_url_encode_basic")
|
||||
test_case("url roundtrip", "test_url_roundtrip")
|
||||
test_case("bool_to_str basic", "test_bool_to_str_basic")
|
||||
test_case("str_to_bytes basic", "test_str_to_bytes_basic")
|
||||
test_case("bytes roundtrip", "test_bytes_roundtrip")
|
||||
|
||||
// ── math tests ─────────────────────────────────────────────────────────────
|
||||
test_case("el_abs basic", "test_el_abs_basic")
|
||||
test_case("el_max basic", "test_el_max_basic")
|
||||
test_case("el_max edge", "test_el_max_edge")
|
||||
test_case("el_min basic", "test_el_min_basic")
|
||||
test_case("el_min edge", "test_el_min_edge")
|
||||
test_case("math_sqrt basic", "test_math_sqrt_basic")
|
||||
test_case("math_sqrt larger", "test_math_sqrt_larger")
|
||||
test_case("math_log basic", "test_math_log_basic")
|
||||
test_case("math_ln basic", "test_math_ln_basic")
|
||||
test_case("math_pi basic", "test_math_pi_basic")
|
||||
test_case("math_sin basic", "test_math_sin_basic")
|
||||
test_case("math_cos basic", "test_math_cos_basic")
|
||||
// int_to_float, float_to_int, format_float, decimal_round omitted:
|
||||
// __int_to_float / __float_to_int / __format_float seed primitives are
|
||||
// not yet implemented in el_seed.c on the runtime/integrate branch.
|
||||
test_case("int arithmetic basic", "test_int_arithmetic_basic")
|
||||
test_case("int arithmetic edge", "test_int_arithmetic_edge")
|
||||
test_case("int arithmetic negative", "test_int_arithmetic_negative")
|
||||
test_case("float arithmetic basic", "test_float_arithmetic_basic")
|
||||
test_case("float comparison basic", "test_float_comparison_basic")
|
||||
|
||||
// ── json tests ─────────────────────────────────────────────────────────────
|
||||
test_case("json_get basic", "test_json_get_basic")
|
||||
test_case("json_get types", "test_json_get_types")
|
||||
test_case("json_get nested", "test_json_get_nested")
|
||||
test_case("json_get empty", "test_json_get_empty")
|
||||
test_case("json_get_int basic", "test_json_get_int_basic")
|
||||
test_case("json_get_bool basic", "test_json_get_bool_basic")
|
||||
test_case("json_get_float basic", "test_json_get_float_basic")
|
||||
test_case("json_set basic", "test_json_set_basic")
|
||||
test_case("json_set numeric", "test_json_set_numeric")
|
||||
test_case("json_set chained", "test_json_set_chained")
|
||||
test_case("json_array_len basic", "test_json_array_len_basic")
|
||||
test_case("json_array_get basic", "test_json_array_get_basic")
|
||||
test_case("json_array_get numbers", "test_json_array_get_numbers")
|
||||
test_case("json_array_get objects", "test_json_array_get_objects")
|
||||
test_case("json_escape_string basic", "test_json_escape_string_basic")
|
||||
test_case("json_escape_string backslash", "test_json_escape_string_backslash")
|
||||
test_case("json_escape roundtrip", "test_json_escape_roundtrip")
|
||||
test_case("json_build_object basic", "test_json_build_object_basic")
|
||||
test_case("json_build_object empty", "test_json_build_object_empty")
|
||||
test_case("json_build_array basic", "test_json_build_array_basic")
|
||||
test_case("json_build_array empty", "test_json_build_array_empty")
|
||||
test_case("json_build_array numbers", "test_json_build_array_numbers")
|
||||
test_case("json_array_push basic", "test_json_array_push_basic")
|
||||
test_case("json_array_push preserves order", "test_json_array_push_preserves_order")
|
||||
test_case("json nested set get", "test_json_nested_set_get")
|
||||
test_case("json array of objects", "test_json_array_of_objects")
|
||||
|
||||
// ── state tests ────────────────────────────────────────────────────────────
|
||||
test_case("state set get basic", "test_state_set_get_basic")
|
||||
test_case("state overwrite", "test_state_overwrite")
|
||||
test_case("state multiple keys", "test_state_multiple_keys")
|
||||
test_case("state missing key", "test_state_missing_key")
|
||||
test_case("state del basic", "test_state_del_basic")
|
||||
test_case("state del and reset", "test_state_del_and_reset")
|
||||
test_case("state has basic", "test_state_has_basic")
|
||||
test_case("state get_or basic", "test_state_get_or_basic")
|
||||
test_case("state cross function", "test_state_cross_function")
|
||||
test_case("state keys basic", "test_state_keys_basic")
|
||||
test_case("state value types", "test_state_value_types")
|
||||
|
||||
// ── time tests ─────────────────────────────────────────────────────────────
|
||||
test_case("time_now basic", "test_time_now_basic")
|
||||
test_case("now_millis basic", "test_now_millis_basic")
|
||||
test_case("unix_timestamp_ms basic", "test_unix_timestamp_ms_basic")
|
||||
test_case("time_now_ms alias", "test_time_now_ms_alias")
|
||||
test_case("time monotonic now_millis", "test_time_monotonic_now_millis")
|
||||
test_case("time monotonic time_now", "test_time_monotonic_time_now")
|
||||
test_case("time monotonic now_ns", "test_time_monotonic_now_ns")
|
||||
test_case("unix_timestamp basic", "test_unix_timestamp_basic")
|
||||
test_case("time_to_parts basic", "test_time_to_parts_basic")
|
||||
test_case("time_to_parts epoch", "test_time_to_parts_epoch")
|
||||
test_case("time_to_parts current", "test_time_to_parts_current")
|
||||
test_case("time_format iso", "test_time_format_iso")
|
||||
test_case("time_format empty", "test_time_format_empty")
|
||||
test_case("time_format strftime", "test_time_format_strftime")
|
||||
test_case("time_add basic", "test_time_add_basic")
|
||||
test_case("time_add multiple", "test_time_add_multiple")
|
||||
test_case("time_diff basic", "test_time_diff_basic")
|
||||
test_case("time_diff larger", "test_time_diff_larger")
|
||||
test_case("time_diff zero", "test_time_diff_zero")
|
||||
test_case("duration helpers basic", "test_duration_helpers_basic")
|
||||
test_case("instant helpers basic", "test_instant_helpers_basic")
|
||||
test_case("uuid_new basic", "test_uuid_new_basic")
|
||||
test_case("uuid uniqueness", "test_uuid_uniqueness")
|
||||
test_case("time_from_parts basic", "test_time_from_parts_basic")
|
||||
test_case("instant_to_iso8601 basic", "test_instant_to_iso8601_basic")
|
||||
|
||||
// ── filesystem tests ───────────────────────────────────────────────────────
|
||||
test_case("fs write read basic", "test_fs_write_read_basic")
|
||||
test_case("fs write read multiline", "test_fs_write_read_multiline")
|
||||
test_case("fs write read empty", "test_fs_write_read_empty")
|
||||
test_case("fs write overwrite", "test_fs_write_overwrite")
|
||||
test_case("fs write large content", "test_fs_write_large_content")
|
||||
test_case("fs exists basic", "test_fs_exists_basic")
|
||||
test_case("fs exists nonexistent", "test_fs_exists_nonexistent")
|
||||
test_case("fs write creates file", "test_fs_write_creates_file")
|
||||
test_case("fs read nonexistent", "test_fs_read_nonexistent")
|
||||
test_case("fs write json", "test_fs_write_json")
|
||||
test_case("fs write json array", "test_fs_write_json_array")
|
||||
test_case("fs multiple files", "test_fs_multiple_files")
|
||||
test_case("fs mkdir basic", "test_fs_mkdir_basic")
|
||||
test_case("fs mkdir write inside", "test_fs_mkdir_write_inside")
|
||||
test_case("fs special chars", "test_fs_special_chars")
|
||||
test_case("fs unicode content", "test_fs_unicode_content")
|
||||
|
||||
// ── collection / list tests ────────────────────────────────────────────────
|
||||
test_case("list empty basic", "test_list_empty_basic")
|
||||
test_case("list empty multiple", "test_list_empty_multiple")
|
||||
test_case("list append single", "test_list_append_single")
|
||||
test_case("list append multiple", "test_list_append_multiple")
|
||||
test_case("list append order", "test_list_append_order")
|
||||
test_case("list append empty string", "test_list_append_empty_string")
|
||||
test_case("list len basic", "test_list_len_basic")
|
||||
test_case("list len large", "test_list_len_large")
|
||||
test_case("list get basic", "test_list_get_basic")
|
||||
test_case("list get preserves content", "test_list_get_preserves_content")
|
||||
test_case("list split join roundtrip", "test_list_split_join_roundtrip")
|
||||
test_case("list build and join", "test_list_build_and_join")
|
||||
test_case("list from split access", "test_list_from_split_access")
|
||||
test_case("native list empty basic", "test_native_list_empty_basic")
|
||||
test_case("native list append basic", "test_native_list_append_basic")
|
||||
test_case("native list join", "test_native_list_join")
|
||||
test_case("env list helpers", "test_env_list_helpers")
|
||||
test_case("list accumulate loop", "test_list_accumulate_loop")
|
||||
test_case("list string building", "test_list_string_building")
|
||||
|
||||
return test_run_all()
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// tests/suite/test_collections.el — tests for native list operations
|
||||
//
|
||||
// runtime/collections.el does not exist; this file tests the native list
|
||||
// primitives that are always available in El: el_list_empty, el_list_append,
|
||||
// el_list_len, el_list_get.
|
||||
//
|
||||
// These primitives underpin str_split, str_join, and the env.el list helpers.
|
||||
|
||||
// ── el_list_empty ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_empty_basic(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
assert_int_eq(el_list_len(lst), 0, "empty list has length 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_empty_multiple(_: String) -> String {
|
||||
let a: [String] = el_list_empty()
|
||||
let b: [String] = el_list_empty()
|
||||
assert_int_eq(el_list_len(a), 0, "first empty list has length 0")
|
||||
assert_int_eq(el_list_len(b), 0, "second empty list has length 0")
|
||||
// Append to one does not affect the other
|
||||
let a2: [String] = el_list_append(a, "hello")
|
||||
assert_int_eq(el_list_len(a2), 1, "appended list has length 1")
|
||||
assert_int_eq(el_list_len(b), 0, "other empty list unaffected")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_list_append ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_append_single(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst2: [String] = el_list_append(lst, "hello")
|
||||
assert_int_eq(el_list_len(lst2), 1, "length is 1 after append")
|
||||
assert_eq(el_list_get(lst2, 0), "hello", "first element is what was appended")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_append_multiple(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "alpha")
|
||||
let lst = el_list_append(lst, "beta")
|
||||
let lst = el_list_append(lst, "gamma")
|
||||
assert_int_eq(el_list_len(lst), 3, "length 3 after 3 appends")
|
||||
assert_eq(el_list_get(lst, 0), "alpha", "first element: alpha")
|
||||
assert_eq(el_list_get(lst, 1), "beta", "second element: beta")
|
||||
assert_eq(el_list_get(lst, 2), "gamma", "third element: gamma")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_append_order(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "1")
|
||||
let lst = el_list_append(lst, "2")
|
||||
let lst = el_list_append(lst, "3")
|
||||
let lst = el_list_append(lst, "4")
|
||||
let lst = el_list_append(lst, "5")
|
||||
assert_int_eq(el_list_len(lst), 5, "five elements appended")
|
||||
assert_eq(el_list_get(lst, 0), "1", "order preserved: index 0")
|
||||
assert_eq(el_list_get(lst, 2), "3", "order preserved: index 2")
|
||||
assert_eq(el_list_get(lst, 4), "5", "order preserved: index 4")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_append_empty_string(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "")
|
||||
assert_int_eq(el_list_len(lst), 1, "empty string element counted")
|
||||
assert_eq(el_list_get(lst, 0), "", "empty string retrievable")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_list_len ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_len_basic(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
assert_int_eq(el_list_len(lst), 0, "initial length 0")
|
||||
let lst = el_list_append(lst, "a")
|
||||
assert_int_eq(el_list_len(lst), 1, "length 1 after one append")
|
||||
let lst = el_list_append(lst, "b")
|
||||
assert_int_eq(el_list_len(lst), 2, "length 2 after two appends")
|
||||
let lst = el_list_append(lst, "c")
|
||||
assert_int_eq(el_list_len(lst), 3, "length 3 after three appends")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_len_large(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < 20 {
|
||||
let lst = el_list_append(lst, int_to_str(i))
|
||||
let i = i + 1
|
||||
}
|
||||
assert_int_eq(el_list_len(lst), 20, "length 20 after 20 appends")
|
||||
assert_eq(el_list_get(lst, 0), "0", "first element is 0")
|
||||
assert_eq(el_list_get(lst, 19), "19", "last element is 19")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_list_get ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_get_basic(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "first")
|
||||
let lst = el_list_append(lst, "second")
|
||||
let lst = el_list_append(lst, "third")
|
||||
assert_eq(el_list_get(lst, 0), "first", "get index 0")
|
||||
assert_eq(el_list_get(lst, 1), "second", "get index 1")
|
||||
assert_eq(el_list_get(lst, 2), "third", "get index 2")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_get_preserves_content(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "hello world")
|
||||
let lst = el_list_append(lst, "{\"json\":\"value\"}")
|
||||
let lst = el_list_append(lst, "line1\nline2")
|
||||
assert_eq(el_list_get(lst, 0), "hello world", "spaces in element preserved")
|
||||
assert_eq(el_list_get(lst, 1), "{\"json\":\"value\"}", "JSON string preserved")
|
||||
assert_eq(el_list_get(lst, 2), "line1\nline2", "newline in element preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── list with str_join / str_split roundtrip ─────────────────────────────────
|
||||
|
||||
fn test_list_split_join_roundtrip(_: String) -> String {
|
||||
let original: String = "apple,banana,cherry"
|
||||
let parts: [String] = str_split(original, ",")
|
||||
assert_int_eq(el_list_len(parts), 3, "split yields 3 parts")
|
||||
let rejoined: String = str_join(parts, ",")
|
||||
assert_eq(rejoined, original, "split then join is identity")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_build_and_join(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "one")
|
||||
let parts = el_list_append(parts, "two")
|
||||
let parts = el_list_append(parts, "three")
|
||||
let joined: String = str_join(parts, " + ")
|
||||
assert_eq(joined, "one + two + three", "join with multi-char separator")
|
||||
assert_int_eq(el_list_len(parts), 3, "original list unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_from_split_access(_: String) -> String {
|
||||
let parts: [String] = str_split("a:b:c:d:e", ":")
|
||||
assert_int_eq(el_list_len(parts), 5, "split into 5 parts")
|
||||
assert_eq(el_list_get(parts, 0), "a", "first part")
|
||||
assert_eq(el_list_get(parts, 2), "c", "middle part")
|
||||
assert_eq(el_list_get(parts, 4), "e", "last part")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── native_list_empty / native_list_append (aliases from join.el style) ───────
|
||||
|
||||
fn test_native_list_empty_basic(_: String) -> String {
|
||||
let lst: [String] = native_list_empty()
|
||||
assert_int_eq(el_list_len(lst), 0, "native_list_empty yields empty list")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_native_list_append_basic(_: String) -> String {
|
||||
let lst: [String] = native_list_empty()
|
||||
let lst = native_list_append(lst, "hello")
|
||||
let lst = native_list_append(lst, "world")
|
||||
assert_int_eq(el_list_len(lst), 2, "native_list_append: length 2")
|
||||
assert_eq(el_list_get(lst, 0), "hello", "native_list_append: first element")
|
||||
assert_eq(el_list_get(lst, 1), "world", "native_list_append: second element")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_native_list_join(_: String) -> String {
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "alpha")
|
||||
let parts = native_list_append(parts, "beta")
|
||||
let parts = native_list_append(parts, "gamma")
|
||||
let result: String = str_join(parts, ", ")
|
||||
assert_eq(result, "alpha, beta, gamma", "native list joined correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── env.el list helpers (get / len) ──────────────────────────────────────────
|
||||
|
||||
fn test_env_list_helpers(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "x")
|
||||
let lst = el_list_append(lst, "y")
|
||||
let lst = el_list_append(lst, "z")
|
||||
assert_int_eq(len(lst), 3, "len() alias for el_list_len")
|
||||
assert_eq(get(lst, 0), "x", "get() alias for el_list_get index 0")
|
||||
assert_eq(get(lst, 2), "z", "get() alias for el_list_get index 2")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── list as accumulator pattern ───────────────────────────────────────────────
|
||||
|
||||
fn test_list_accumulate_loop(_: String) -> String {
|
||||
let results: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < 5 {
|
||||
let results = el_list_append(results, int_to_str(i * i))
|
||||
let i = i + 1
|
||||
}
|
||||
assert_int_eq(el_list_len(results), 5, "accumulated 5 squares")
|
||||
assert_eq(el_list_get(results, 0), "0", "0^2 = 0")
|
||||
assert_eq(el_list_get(results, 1), "1", "1^2 = 1")
|
||||
assert_eq(el_list_get(results, 2), "4", "2^2 = 4")
|
||||
assert_eq(el_list_get(results, 3), "9", "3^2 = 9")
|
||||
assert_eq(el_list_get(results, 4), "16", "4^2 = 16")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_string_building(_: String) -> String {
|
||||
let words: [String] = el_list_empty()
|
||||
let words = el_list_append(words, "the")
|
||||
let words = el_list_append(words, "quick")
|
||||
let words = el_list_append(words, "brown")
|
||||
let words = el_list_append(words, "fox")
|
||||
let sentence: String = str_join(words, " ")
|
||||
assert_eq(sentence, "the quick brown fox", "words joined into sentence")
|
||||
assert_int_eq(str_count_words(sentence), 4, "sentence has 4 words")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// tests/suite/test_fs.el — comprehensive tests for runtime/fs.el
|
||||
//
|
||||
// Covers fs_write, fs_read, fs_exists, fs_mkdir. All file paths use /tmp so
|
||||
// no special permissions are needed. Each test uses a unique path to prevent
|
||||
// interference between test cases.
|
||||
|
||||
// ── fs_write / fs_read roundtrip ─────────────────────────────────────────────
|
||||
|
||||
fn test_fs_write_read_basic(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_basic.txt"
|
||||
let content: String = "hello from El tests"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "read back what was written")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_read_multiline(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_multiline.txt"
|
||||
let content: String = "line one\nline two\nline three"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "multiline content preserved")
|
||||
assert_contains(read_back, "line one", "first line present")
|
||||
assert_contains(read_back, "line two", "second line present")
|
||||
assert_contains(read_back, "line three", "third line present")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_read_empty(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_empty.txt"
|
||||
fs_write(path, "")
|
||||
let read_back: String = fs_read(path)
|
||||
// empty file may return "" or a zero-length string
|
||||
assert_int_eq(str_len(read_back), 0, "empty file reads as empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_overwrite(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_overwrite.txt"
|
||||
fs_write(path, "first version")
|
||||
let r1: String = fs_read(path)
|
||||
assert_eq(r1, "first version", "first write readable")
|
||||
|
||||
fs_write(path, "second version")
|
||||
let r2: String = fs_read(path)
|
||||
assert_eq(r2, "second version", "second write overwrites first")
|
||||
|
||||
fs_write(path, "third version")
|
||||
let r3: String = fs_read(path)
|
||||
assert_eq(r3, "third version", "third write overwrites second")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_large_content(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_large.txt"
|
||||
let big: String = str_repeat("abcdefghij", 100)
|
||||
fs_write(path, big)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_int_eq(str_len(read_back), 1000, "large content length preserved")
|
||||
assert_eq(str_slice(read_back, 0, 10), "abcdefghij", "content starts correctly")
|
||||
assert_eq(str_slice(read_back, 990, 1000), "abcdefghij", "content ends correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── fs_exists ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_exists_basic(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_exists.txt"
|
||||
fs_write(path, "existence test")
|
||||
assert_true(fs_exists(path), "file exists after write")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_exists_nonexistent(_: String) -> String {
|
||||
assert_false(fs_exists("/tmp/el_suite_this_file_does_not_exist_xyz_abc_999.txt"), "nonexistent file returns false")
|
||||
assert_false(fs_exists("/tmp/el_suite_another_missing_file_def.txt"), "another nonexistent file returns false")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_creates_file(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_create.txt"
|
||||
// May or may not exist — write creates it
|
||||
fs_write(path, "newly created")
|
||||
assert_true(fs_exists(path), "file exists after creation via write")
|
||||
let content: String = fs_read(path)
|
||||
assert_eq(content, "newly created", "content correct after creation")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── fs_read nonexistent ───────────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_read_nonexistent(_: String) -> String {
|
||||
let result: String = fs_read("/tmp/el_suite_this_absolutely_does_not_exist_xyz.txt")
|
||||
// Per the spec: returns "" or error string; we just verify it doesn't crash
|
||||
// and returns a string (which it must, given the type system)
|
||||
assert_true(str_len(result) >= 0, "reading nonexistent file returns a string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── JSON content stored in files ─────────────────────────────────────────────
|
||||
|
||||
fn test_fs_write_json(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_json.txt"
|
||||
let json_data: String = "{\"name\":\"alice\",\"score\":42}"
|
||||
fs_write(path, json_data)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(json_get(read_back, "name"), "alice", "JSON name field after file roundtrip")
|
||||
assert_eq(json_get(read_back, "score"), "42", "JSON score field after file roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_json_array(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_json_arr.txt"
|
||||
let arr_data: String = "[\"alpha\",\"beta\",\"gamma\"]"
|
||||
fs_write(path, arr_data)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_int_eq(json_array_len(read_back), 3, "JSON array length after file roundtrip")
|
||||
assert_eq(json_array_get_string(read_back, 0), "alpha", "first element after roundtrip")
|
||||
assert_eq(json_array_get_string(read_back, 2), "gamma", "last element after roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── multiple independent files ────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_multiple_files(_: String) -> String {
|
||||
let path_a: String = "/tmp/el_suite_multi_a.txt"
|
||||
let path_b: String = "/tmp/el_suite_multi_b.txt"
|
||||
let path_c: String = "/tmp/el_suite_multi_c.txt"
|
||||
|
||||
fs_write(path_a, "content A")
|
||||
fs_write(path_b, "content B")
|
||||
fs_write(path_c, "content C")
|
||||
|
||||
assert_eq(fs_read(path_a), "content A", "file A reads correctly")
|
||||
assert_eq(fs_read(path_b), "content B", "file B reads correctly")
|
||||
assert_eq(fs_read(path_c), "content C", "file C reads correctly")
|
||||
|
||||
assert_true(fs_exists(path_a), "file A exists")
|
||||
assert_true(fs_exists(path_b), "file B exists")
|
||||
assert_true(fs_exists(path_c), "file C exists")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── fs_mkdir ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_mkdir_basic(_: String) -> String {
|
||||
let dir_path: String = "/tmp/el_suite_mkdir_test"
|
||||
let result: Bool = fs_mkdir(dir_path)
|
||||
// fs_mkdir returns Bool; the dir should now exist
|
||||
assert_true(fs_exists(dir_path), "directory exists after mkdir")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_mkdir_write_inside(_: String) -> String {
|
||||
let dir_path: String = "/tmp/el_suite_mkdir_subdir"
|
||||
fs_mkdir(dir_path)
|
||||
let file_path: String = dir_path + "/test_file.txt"
|
||||
fs_write(file_path, "inside directory")
|
||||
assert_true(fs_exists(file_path), "file inside mkdir'd directory exists")
|
||||
assert_eq(fs_read(file_path), "inside directory", "file inside directory readable")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── special characters in content ────────────────────────────────────────────
|
||||
|
||||
fn test_fs_special_chars(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_special.txt"
|
||||
let content: String = "tab:\there\nnewline above\r\nwindows newline"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "special chars preserved in file")
|
||||
assert_contains(read_back, "\t", "tab preserved")
|
||||
assert_contains(read_back, "\n", "newline preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_unicode_content(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_unicode.txt"
|
||||
// Use ASCII art instead of actual unicode for max compatibility
|
||||
let content: String = "hello-world-test"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "content preserved")
|
||||
assert_int_eq(str_len(read_back), str_len(content), "length preserved")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// tests/suite/test_json.el — comprehensive tests for runtime/json.el
|
||||
//
|
||||
// Covers json_get, json_get_raw, json_set, json_array_len, json_array_get,
|
||||
// json_array_get_string, json_escape_string, json_build_object, json_build_array,
|
||||
// json_array_push, typed extractors, bytes_to_str, and nested JSON.
|
||||
|
||||
// ── json_get ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_get_basic(_: String) -> String {
|
||||
let obj: String = "{\"name\":\"alice\",\"age\":\"30\"}"
|
||||
assert_eq(json_get(obj, "name"), "alice", "get string field")
|
||||
assert_eq(json_get(obj, "age"), "30", "get numeric-looking string field")
|
||||
assert_eq(json_get(obj, "missing"), "", "missing key returns empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_types(_: String) -> String {
|
||||
let obj: String = "{\"count\":42,\"flag\":true,\"ratio\":3.14}"
|
||||
assert_eq(json_get(obj, "count"), "42", "get integer field")
|
||||
assert_eq(json_get(obj, "flag"), "true", "get boolean field")
|
||||
assert_eq(json_get(obj, "ratio"), "3.14", "get float field")
|
||||
assert_eq(json_get(obj, "absent"), "", "absent key yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_nested(_: String) -> String {
|
||||
let obj: String = "{\"user\":{\"name\":\"bob\",\"role\":\"admin\"}}"
|
||||
assert_eq(json_get(obj, "user.name"), "bob", "dot-path traversal: user.name")
|
||||
assert_eq(json_get(obj, "user.role"), "admin", "dot-path traversal: user.role")
|
||||
assert_eq(json_get(obj, "user.missing"), "", "missing nested key")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_empty(_: String) -> String {
|
||||
let empty_obj: String = "{}"
|
||||
assert_eq(json_get(empty_obj, "key"), "", "empty object: any key is missing")
|
||||
assert_eq(json_get(empty_obj, "a.b"), "", "empty object: nested path missing")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_get typed extractors ────────────────────────────────────────────────
|
||||
|
||||
fn test_json_get_int_basic(_: String) -> String {
|
||||
let obj: String = "{\"count\":42,\"negative\":-7,\"zero\":0}"
|
||||
assert_int_eq(json_get_int(obj, "count"), 42, "get int field 42")
|
||||
assert_int_eq(json_get_int(obj, "negative"), -7, "get negative int field")
|
||||
assert_int_eq(json_get_int(obj, "zero"), 0, "get zero int field")
|
||||
assert_int_eq(json_get_int(obj, "missing"), 0, "missing int field returns 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_bool_basic(_: String) -> String {
|
||||
let obj: String = "{\"ok\":true,\"fail\":false}"
|
||||
assert_true(json_get_bool(obj, "ok"), "get true bool field")
|
||||
assert_false(json_get_bool(obj, "fail"), "get false bool field")
|
||||
assert_false(json_get_bool(obj, "missing"), "missing bool field returns false")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_float_basic(_: String) -> String {
|
||||
let obj: String = "{\"ratio\":3.14,\"negative\":-1.5}"
|
||||
let ratio: Float = json_get_float(obj, "ratio")
|
||||
assert_true(ratio > 3.13, "ratio > 3.13")
|
||||
assert_true(ratio < 3.15, "ratio < 3.15")
|
||||
let neg: Float = json_get_float(obj, "negative")
|
||||
assert_true(neg < -1.4, "negative float")
|
||||
assert_true(neg > -1.6, "negative float bound")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_set ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_set_basic(_: String) -> String {
|
||||
let obj: String = "{\"name\":\"alice\"}"
|
||||
let obj2: String = json_set(obj, "name", "\"bob\"")
|
||||
assert_eq(json_get(obj2, "name"), "bob", "overwrites existing key")
|
||||
|
||||
let obj3: String = json_set(obj, "role", "\"admin\"")
|
||||
assert_eq(json_get(obj3, "role"), "admin", "inserts new key")
|
||||
assert_eq(json_get(obj3, "name"), "alice", "existing key unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_set_numeric(_: String) -> String {
|
||||
let obj: String = "{}"
|
||||
let obj2: String = json_set(obj, "count", "42")
|
||||
assert_eq(json_get(obj2, "count"), "42", "set integer value")
|
||||
|
||||
let obj3: String = json_set(obj2, "count", "100")
|
||||
assert_eq(json_get(obj3, "count"), "100", "overwrites integer value")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_set_chained(_: String) -> String {
|
||||
let obj: String = "{}"
|
||||
let obj1: String = json_set(obj, "a", "\"1\"")
|
||||
let obj2: String = json_set(obj1, "b", "\"2\"")
|
||||
let obj3: String = json_set(obj2, "c", "\"3\"")
|
||||
assert_eq(json_get(obj3, "a"), "1", "chained set: a")
|
||||
assert_eq(json_get(obj3, "b"), "2", "chained set: b")
|
||||
assert_eq(json_get(obj3, "c"), "3", "chained set: c")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_array_len ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_array_len_basic(_: String) -> String {
|
||||
assert_int_eq(json_array_len("[]"), 0, "empty array has length 0")
|
||||
assert_int_eq(json_array_len("[1]"), 1, "single element array")
|
||||
assert_int_eq(json_array_len("[1,2,3]"), 3, "three element array")
|
||||
assert_int_eq(json_array_len("[\"a\",\"b\"]"), 2, "string array length")
|
||||
assert_int_eq(json_array_len("[1,2,3,4,5]"), 5, "five element array")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_array_get / json_array_get_string ────────────────────────────────────
|
||||
|
||||
fn test_json_array_get_basic(_: String) -> String {
|
||||
let arr: String = "[\"alpha\",\"beta\",\"gamma\"]"
|
||||
assert_eq(json_array_get_string(arr, 0), "alpha", "first element")
|
||||
assert_eq(json_array_get_string(arr, 1), "beta", "second element")
|
||||
assert_eq(json_array_get_string(arr, 2), "gamma", "third element")
|
||||
assert_eq(json_array_get_string(arr, 3), "", "out of bounds returns empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_get_numbers(_: String) -> String {
|
||||
let arr: String = "[10,20,30]"
|
||||
assert_eq(json_array_get(arr, 0), "10", "first number as fragment")
|
||||
assert_eq(json_array_get(arr, 1), "20", "second number as fragment")
|
||||
assert_eq(json_array_get(arr, 2), "30", "third number as fragment")
|
||||
assert_int_eq(str_to_int(json_array_get(arr, 0)), 10, "convert fragment to int")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_get_objects(_: String) -> String {
|
||||
let arr: String = "[{\"name\":\"alice\"},{\"name\":\"bob\"}]"
|
||||
let first: String = json_array_get(arr, 0)
|
||||
let second: String = json_array_get(arr, 1)
|
||||
assert_eq(json_get(first, "name"), "alice", "first object name")
|
||||
assert_eq(json_get(second, "name"), "bob", "second object name")
|
||||
assert_int_eq(json_array_len(arr), 2, "array of objects has correct length")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_escape_string ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_escape_string_basic(_: String) -> String {
|
||||
assert_eq(json_escape_string("hello"), "hello", "plain string unchanged")
|
||||
assert_eq(json_escape_string(""), "", "empty string unchanged")
|
||||
assert_contains(json_escape_string("say \"hello\""), "\\\"", "double quotes escaped")
|
||||
assert_contains(json_escape_string("line1\nline2"), "\\n", "newline escaped")
|
||||
assert_contains(json_escape_string("col1\tcol2"), "\\t", "tab escaped")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_escape_string_backslash(_: String) -> String {
|
||||
let escaped: String = json_escape_string("a\\b")
|
||||
assert_contains(escaped, "\\\\", "backslash doubled")
|
||||
assert_true(str_len(escaped) > 3, "escaped is longer than original")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_escape_roundtrip(_: String) -> String {
|
||||
// Build a JSON string with an escaped value and extract it back
|
||||
let raw: String = "hello world"
|
||||
let escaped: String = json_escape_string(raw)
|
||||
let json_str: String = "{\"msg\":\"" + escaped + "\"}"
|
||||
assert_eq(json_get(json_str, "msg"), raw, "roundtrip: escape then parse")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_build_object ─────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_build_object_basic(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "name")
|
||||
let parts = el_list_append(parts, "alice")
|
||||
let parts = el_list_append(parts, "role")
|
||||
let parts = el_list_append(parts, "admin")
|
||||
let obj: String = json_build_object(parts)
|
||||
assert_eq(json_get(obj, "name"), "alice", "build object: name field")
|
||||
assert_eq(json_get(obj, "role"), "admin", "build object: role field")
|
||||
assert_true(str_starts_with(obj, "{"), "build object starts with {")
|
||||
assert_true(str_ends_with(obj, "}"), "build object ends with }")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_build_object_empty(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let obj: String = json_build_object(parts)
|
||||
assert_true(str_starts_with(obj, "{"), "empty build object starts with {")
|
||||
assert_true(str_ends_with(obj, "}"), "empty build object ends with }")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_build_array ──────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_build_array_basic(_: String) -> String {
|
||||
let items: [String] = el_list_empty()
|
||||
let items = el_list_append(items, "\"alice\"")
|
||||
let items = el_list_append(items, "\"bob\"")
|
||||
let arr: String = json_build_array(items)
|
||||
assert_int_eq(json_array_len(arr), 2, "built array has 2 elements")
|
||||
assert_eq(json_array_get_string(arr, 0), "alice", "first element of built array")
|
||||
assert_eq(json_array_get_string(arr, 1), "bob", "second element of built array")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_build_array_empty(_: String) -> String {
|
||||
let items: [String] = el_list_empty()
|
||||
let arr: String = json_build_array(items)
|
||||
assert_int_eq(json_array_len(arr), 0, "empty built array has 0 elements")
|
||||
assert_eq(arr, "[]", "empty array is []")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_build_array_numbers(_: String) -> String {
|
||||
let items: [String] = el_list_empty()
|
||||
let items = el_list_append(items, "1")
|
||||
let items = el_list_append(items, "2")
|
||||
let items = el_list_append(items, "3")
|
||||
let arr: String = json_build_array(items)
|
||||
assert_int_eq(json_array_len(arr), 3, "numeric array has 3 elements")
|
||||
assert_eq(json_array_get(arr, 0), "1", "first numeric element")
|
||||
assert_eq(json_array_get(arr, 2), "3", "last numeric element")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_array_push ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_array_push_basic(_: String) -> String {
|
||||
let arr: String = "[]"
|
||||
let arr2: String = json_array_push(arr, "\"hello\"")
|
||||
assert_int_eq(json_array_len(arr2), 1, "after push: length 1")
|
||||
assert_eq(json_array_get_string(arr2, 0), "hello", "pushed element accessible")
|
||||
|
||||
let arr3: String = json_array_push(arr2, "\"world\"")
|
||||
assert_int_eq(json_array_len(arr3), 2, "after second push: length 2")
|
||||
assert_eq(json_array_get_string(arr3, 1), "world", "second pushed element")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_push_preserves_order(_: String) -> String {
|
||||
let arr: String = "[\"a\",\"b\"]"
|
||||
let arr2: String = json_array_push(arr, "\"c\"")
|
||||
assert_eq(json_array_get_string(arr2, 0), "a", "first element preserved")
|
||||
assert_eq(json_array_get_string(arr2, 1), "b", "second element preserved")
|
||||
assert_eq(json_array_get_string(arr2, 2), "c", "pushed element at end")
|
||||
assert_int_eq(json_array_len(arr2), 3, "total length after push")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── nested JSON ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_nested_set_get(_: String) -> String {
|
||||
let obj: String = "{\"user\":{\"name\":\"alice\"},\"count\":1}"
|
||||
assert_eq(json_get(obj, "user.name"), "alice", "deep get via dot path")
|
||||
assert_eq(json_get(obj, "count"), "1", "top-level int field")
|
||||
|
||||
let obj2: String = json_set(obj, "count", "2")
|
||||
assert_eq(json_get(obj2, "count"), "2", "updated top-level field")
|
||||
assert_eq(json_get(obj2, "user.name"), "alice", "nested field still accessible after set")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_of_objects(_: String) -> String {
|
||||
let arr: String = "[{\"id\":1,\"name\":\"alice\"},{\"id\":2,\"name\":\"bob\"}]"
|
||||
assert_int_eq(json_array_len(arr), 2, "array of objects: length 2")
|
||||
let first: String = json_array_get(arr, 0)
|
||||
let second: String = json_array_get(arr, 1)
|
||||
assert_eq(json_get(first, "name"), "alice", "first object name")
|
||||
assert_eq(json_get(second, "name"), "bob", "second object name")
|
||||
assert_eq(json_get(first, "id"), "1", "first object id")
|
||||
assert_eq(json_get(second, "id"), "2", "second object id")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// tests/suite/test_math.el — comprehensive tests for runtime/math.el
|
||||
//
|
||||
// Covers every public function in math.el: integer utilities, float math,
|
||||
// conversions, and rounding. Import after runtime modules and test.el.
|
||||
|
||||
// ── el_abs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_el_abs_basic(_: String) -> String {
|
||||
assert_int_eq(el_abs(5), 5, "positive stays positive")
|
||||
assert_int_eq(el_abs(-5), 5, "negative becomes positive")
|
||||
assert_int_eq(el_abs(0), 0, "zero stays zero")
|
||||
assert_int_eq(el_abs(-1), 1, "negative one becomes one")
|
||||
assert_int_eq(el_abs(1000000), 1000000, "large positive unchanged")
|
||||
assert_int_eq(el_abs(-1000000), 1000000, "large negative becomes positive")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_max ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_el_max_basic(_: String) -> String {
|
||||
assert_int_eq(el_max(3, 5), 5, "5 is larger than 3")
|
||||
assert_int_eq(el_max(5, 3), 5, "5 is larger than 3 (reversed args)")
|
||||
assert_int_eq(el_max(4, 4), 4, "equal values returns the value")
|
||||
assert_int_eq(el_max(-1, -5), -1, "larger of two negatives")
|
||||
assert_int_eq(el_max(0, -1), 0, "zero is larger than negative")
|
||||
assert_int_eq(el_max(-1, 0), 0, "zero is larger than negative (reversed)")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_el_max_edge(_: String) -> String {
|
||||
assert_int_eq(el_max(0, 0), 0, "max of zeros is zero")
|
||||
assert_int_eq(el_max(1000000, 999999), 1000000, "large values")
|
||||
assert_int_eq(el_max(-1000000, 1000000), 1000000, "mixed sign large values")
|
||||
assert_int_eq(el_max(1, 2), 2, "sequential integers")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_min ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_el_min_basic(_: String) -> String {
|
||||
assert_int_eq(el_min(3, 5), 3, "3 is smaller than 5")
|
||||
assert_int_eq(el_min(5, 3), 3, "3 is smaller than 5 (reversed args)")
|
||||
assert_int_eq(el_min(4, 4), 4, "equal values returns the value")
|
||||
assert_int_eq(el_min(-1, -5), -5, "smaller of two negatives")
|
||||
assert_int_eq(el_min(0, -1), -1, "negative is smaller than zero")
|
||||
assert_int_eq(el_min(-1, 0), -1, "negative is smaller than zero (reversed)")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_el_min_edge(_: String) -> String {
|
||||
assert_int_eq(el_min(0, 0), 0, "min of zeros is zero")
|
||||
assert_int_eq(el_min(1000000, 999999), 999999, "large values")
|
||||
assert_int_eq(el_min(-1000000, 1000000), -1000000, "mixed sign large values")
|
||||
assert_int_eq(el_min(1, 2), 1, "sequential integers")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── math_sqrt ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_math_sqrt_basic(_: String) -> String {
|
||||
let r: Float = math_sqrt(4.0)
|
||||
assert_true(r > 1.99, "sqrt(4) > 1.99")
|
||||
assert_true(r < 2.01, "sqrt(4) < 2.01")
|
||||
|
||||
let r9: Float = math_sqrt(9.0)
|
||||
assert_true(r9 > 2.99, "sqrt(9) > 2.99")
|
||||
assert_true(r9 < 3.01, "sqrt(9) < 3.01")
|
||||
|
||||
let r1: Float = math_sqrt(1.0)
|
||||
assert_true(r1 > 0.99, "sqrt(1) is close to 1")
|
||||
assert_true(r1 < 1.01, "sqrt(1) is close to 1")
|
||||
|
||||
let r0: Float = math_sqrt(0.0)
|
||||
assert_true(r0 >= 0.0, "sqrt(0) is non-negative")
|
||||
assert_true(r0 < 0.001, "sqrt(0) is close to 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_sqrt_larger(_: String) -> String {
|
||||
let r25: Float = math_sqrt(25.0)
|
||||
assert_true(r25 > 4.99, "sqrt(25) > 4.99")
|
||||
assert_true(r25 < 5.01, "sqrt(25) < 5.01")
|
||||
|
||||
let r100: Float = math_sqrt(100.0)
|
||||
assert_true(r100 > 9.99, "sqrt(100) > 9.99")
|
||||
assert_true(r100 < 10.01, "sqrt(100) < 10.01")
|
||||
|
||||
let r2: Float = math_sqrt(2.0)
|
||||
assert_true(r2 > 1.41, "sqrt(2) > 1.41")
|
||||
assert_true(r2 < 1.43, "sqrt(2) < 1.43")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── math_log / math_ln ───────────────────────────────────────────────────────
|
||||
|
||||
fn test_math_log_basic(_: String) -> String {
|
||||
let log10: Float = math_log(10.0)
|
||||
assert_true(log10 > 0.99, "log(10) > 0.99")
|
||||
assert_true(log10 < 1.01, "log(10) < 1.01")
|
||||
|
||||
let log1: Float = math_log(1.0)
|
||||
assert_true(log1 > -0.001, "log(1) is close to 0")
|
||||
assert_true(log1 < 0.001, "log(1) is close to 0")
|
||||
|
||||
let log100: Float = math_log(100.0)
|
||||
assert_true(log100 > 1.99, "log(100) > 1.99")
|
||||
assert_true(log100 < 2.01, "log(100) < 2.01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_ln_basic(_: String) -> String {
|
||||
let lne: Float = math_ln(2.718281828)
|
||||
assert_true(lne > 0.99, "ln(e) is close to 1")
|
||||
assert_true(lne < 1.01, "ln(e) is close to 1")
|
||||
|
||||
let ln1: Float = math_ln(1.0)
|
||||
assert_true(ln1 > -0.001, "ln(1) is close to 0")
|
||||
assert_true(ln1 < 0.001, "ln(1) is close to 0")
|
||||
|
||||
let ln10: Float = math_ln(10.0)
|
||||
assert_true(ln10 > 2.30, "ln(10) > 2.30")
|
||||
assert_true(ln10 < 2.31, "ln(10) < 2.31")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── math_sin / math_cos / math_pi ────────────────────────────────────────────
|
||||
|
||||
fn test_math_pi_basic(_: String) -> String {
|
||||
let pi: Float = math_pi()
|
||||
assert_true(pi > 3.14, "pi > 3.14")
|
||||
assert_true(pi < 3.15, "pi < 3.15")
|
||||
assert_true(pi > 0.0, "pi is positive")
|
||||
assert_true(pi == math_pi(), "pi is constant across calls")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_sin_basic(_: String) -> String {
|
||||
let sin0: Float = math_sin(0.0)
|
||||
assert_true(sin0 > -0.001, "sin(0) is close to 0")
|
||||
assert_true(sin0 < 0.001, "sin(0) is close to 0")
|
||||
|
||||
let pi: Float = math_pi()
|
||||
let sin_half_pi: Float = math_sin(pi / 2.0)
|
||||
assert_true(sin_half_pi > 0.99, "sin(pi/2) is close to 1")
|
||||
assert_true(sin_half_pi < 1.01, "sin(pi/2) is close to 1")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_cos_basic(_: String) -> String {
|
||||
let cos0: Float = math_cos(0.0)
|
||||
assert_true(cos0 > 0.99, "cos(0) is close to 1")
|
||||
assert_true(cos0 < 1.01, "cos(0) is close to 1")
|
||||
|
||||
let pi: Float = math_pi()
|
||||
let cos_pi: Float = math_cos(pi)
|
||||
assert_true(cos_pi < -0.99, "cos(pi) is close to -1")
|
||||
assert_true(cos_pi > -1.01, "cos(pi) is close to -1")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── int_to_float / float_to_int ──────────────────────────────────────────────
|
||||
//
|
||||
// NOTE: int_to_float, float_to_int, format_float, and decimal_round use seed
|
||||
// primitives (__int_to_float, __float_to_int, __format_float) that are not yet
|
||||
// implemented in el_seed.c on the runtime/integrate branch. These tests are
|
||||
// omitted until those seed primitives are available. The float arithmetic and
|
||||
// comparison tests below exercise the float type via language builtins instead.
|
||||
|
||||
// ── Integer arithmetic (language built-ins) ───────────────────────────────────
|
||||
|
||||
fn test_int_arithmetic_basic(_: String) -> String {
|
||||
assert_int_eq(2 + 3, 5, "addition")
|
||||
assert_int_eq(10 - 4, 6, "subtraction")
|
||||
assert_int_eq(3 * 4, 12, "multiplication")
|
||||
assert_int_eq(10 / 3, 3, "integer division truncates")
|
||||
assert_int_eq(10 % 3, 1, "modulo")
|
||||
assert_int_eq(0 - 5, -5, "negation via subtraction")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_int_arithmetic_edge(_: String) -> String {
|
||||
assert_int_eq(0 + 0, 0, "zero plus zero")
|
||||
assert_int_eq(0 * 100, 0, "zero times anything")
|
||||
assert_int_eq(1 * 1, 1, "one times one")
|
||||
assert_int_eq(100 / 100, 1, "divide by self")
|
||||
assert_int_eq(7 % 7, 0, "self modulo is zero")
|
||||
assert_int_eq(-5 + 5, 0, "additive inverse")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_int_arithmetic_negative(_: String) -> String {
|
||||
assert_int_eq(-3 + -2, -5, "negative addition")
|
||||
assert_int_eq(-3 * 4, -12, "negative times positive")
|
||||
assert_int_eq(-10 / 3, -3, "negative division truncates toward zero")
|
||||
assert_int_eq(0 - 1000, -1000, "large negative")
|
||||
assert_int_eq(-1 * -1, 1, "negative times negative is positive")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Float arithmetic (language built-ins) ────────────────────────────────────
|
||||
|
||||
fn test_float_arithmetic_basic(_: String) -> String {
|
||||
let a: Float = 1.5 + 2.5
|
||||
assert_true(a > 3.99, "1.5 + 2.5 = 4.0")
|
||||
assert_true(a < 4.01, "1.5 + 2.5 = 4.0")
|
||||
|
||||
let b: Float = 5.0 - 2.5
|
||||
assert_true(b > 2.49, "5.0 - 2.5 = 2.5")
|
||||
assert_true(b < 2.51, "5.0 - 2.5 = 2.5")
|
||||
|
||||
let c: Float = 2.0 * 3.0
|
||||
assert_true(c > 5.99, "2.0 * 3.0 = 6.0")
|
||||
assert_true(c < 6.01, "2.0 * 3.0 = 6.0")
|
||||
|
||||
let d: Float = 9.0 / 3.0
|
||||
assert_true(d > 2.99, "9.0 / 3.0 = 3.0")
|
||||
assert_true(d < 3.01, "9.0 / 3.0 = 3.0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_float_comparison_basic(_: String) -> String {
|
||||
assert_true(1.0 < 2.0, "1.0 < 2.0")
|
||||
assert_true(2.0 > 1.0, "2.0 > 1.0")
|
||||
assert_false(1.0 > 2.0, "1.0 not > 2.0")
|
||||
assert_false(2.0 < 1.0, "2.0 not < 1.0")
|
||||
assert_true(1.5 >= 1.5, "1.5 >= 1.5")
|
||||
assert_true(1.5 <= 1.5, "1.5 <= 1.5")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// tests/suite/test_state.el — comprehensive tests for runtime/state.el
|
||||
//
|
||||
// Covers state_set, state_get, state_del, state_keys, state_has, state_get_or.
|
||||
// Uses unique key prefixes to avoid interference with the test framework's
|
||||
// own state keys (which use _test_ prefix).
|
||||
|
||||
// ── state_set / state_get roundtrip ───────────────────────────────────────────
|
||||
|
||||
fn test_state_set_get_basic(_: String) -> String {
|
||||
state_set("suite_key1", "hello")
|
||||
assert_eq(state_get("suite_key1"), "hello", "get back what was set")
|
||||
|
||||
state_set("suite_key2", "world")
|
||||
assert_eq(state_get("suite_key2"), "world", "second key independent")
|
||||
|
||||
state_set("suite_num", "42")
|
||||
assert_eq(state_get("suite_num"), "42", "numeric string value")
|
||||
|
||||
state_set("suite_empty", "")
|
||||
// empty value and missing key both return "" — that's by design per the docs
|
||||
let v: String = state_get("suite_empty")
|
||||
assert_true(str_eq(v, "") || str_eq(v, ""), "empty value stored or treated as absent")
|
||||
|
||||
state_set("suite_long", str_repeat("a", 100))
|
||||
assert_int_eq(str_len(state_get("suite_long")), 100, "long value preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_state_overwrite(_: String) -> String {
|
||||
state_set("suite_over", "first")
|
||||
assert_eq(state_get("suite_over"), "first", "initial value")
|
||||
state_set("suite_over", "second")
|
||||
assert_eq(state_get("suite_over"), "second", "value overwritten")
|
||||
state_set("suite_over", "third")
|
||||
assert_eq(state_get("suite_over"), "third", "value overwritten again")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_state_multiple_keys(_: String) -> String {
|
||||
state_set("suite_mk_a", "alpha")
|
||||
state_set("suite_mk_b", "beta")
|
||||
state_set("suite_mk_c", "gamma")
|
||||
assert_eq(state_get("suite_mk_a"), "alpha", "key a is alpha")
|
||||
assert_eq(state_get("suite_mk_b"), "beta", "key b is beta")
|
||||
assert_eq(state_get("suite_mk_c"), "gamma", "key c is gamma")
|
||||
// Mutate one, others unchanged
|
||||
state_set("suite_mk_b", "BETA")
|
||||
assert_eq(state_get("suite_mk_a"), "alpha", "a unchanged after b mutated")
|
||||
assert_eq(state_get("suite_mk_b"), "BETA", "b updated")
|
||||
assert_eq(state_get("suite_mk_c"), "gamma", "c unchanged after b mutated")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── missing keys ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_missing_key(_: String) -> String {
|
||||
assert_eq(state_get("suite_definitely_not_set_xyz_123"), "", "missing key returns empty string")
|
||||
assert_eq(state_get("suite_another_missing_key_abc"), "", "another missing key returns empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_del ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_del_basic(_: String) -> String {
|
||||
state_set("suite_del1", "value_to_delete")
|
||||
assert_eq(state_get("suite_del1"), "value_to_delete", "key present before delete")
|
||||
state_del("suite_del1")
|
||||
assert_eq(state_get("suite_del1"), "", "key absent after delete")
|
||||
|
||||
// Delete a non-existent key should not error
|
||||
state_del("suite_never_existed_key_xyz")
|
||||
assert_eq(state_get("suite_never_existed_key_xyz"), "", "delete non-existent is no-op")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_state_del_and_reset(_: String) -> String {
|
||||
state_set("suite_reuse", "original")
|
||||
assert_eq(state_get("suite_reuse"), "original", "original value")
|
||||
state_del("suite_reuse")
|
||||
assert_eq(state_get("suite_reuse"), "", "deleted")
|
||||
state_set("suite_reuse", "new_value")
|
||||
assert_eq(state_get("suite_reuse"), "new_value", "key reused after delete")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_has ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_has_basic(_: String) -> String {
|
||||
state_set("suite_has1", "something")
|
||||
assert_true(state_has("suite_has1"), "key with value is present")
|
||||
assert_false(state_has("suite_definitely_absent_xyz_999"), "absent key returns false")
|
||||
|
||||
state_del("suite_has1")
|
||||
assert_false(state_has("suite_has1"), "deleted key is not present")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_get_or ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_get_or_basic(_: String) -> String {
|
||||
state_set("suite_gor1", "present")
|
||||
assert_eq(state_get_or("suite_gor1", "default"), "present", "returns value when key exists")
|
||||
assert_eq(state_get_or("suite_gor_missing_xyz", "fallback"), "fallback", "returns default when key missing")
|
||||
assert_eq(state_get_or("suite_gor_missing_xyz", ""), "", "returns empty default when key missing")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state persists across function calls ──────────────────────────────────────
|
||||
|
||||
fn _suite_state_helper_write() {
|
||||
state_set("suite_persist_test", "written_by_helper")
|
||||
}
|
||||
|
||||
fn _suite_state_helper_increment() {
|
||||
let n: Int = str_to_int(state_get("suite_counter"))
|
||||
state_set("suite_counter", int_to_str(n + 1))
|
||||
}
|
||||
|
||||
fn test_state_cross_function(_: String) -> String {
|
||||
_suite_state_helper_write()
|
||||
assert_eq(state_get("suite_persist_test"), "written_by_helper", "value written by helper function is readable")
|
||||
|
||||
state_set("suite_counter", "0")
|
||||
_suite_state_helper_increment()
|
||||
assert_eq(state_get("suite_counter"), "1", "counter after 1 increment")
|
||||
_suite_state_helper_increment()
|
||||
assert_eq(state_get("suite_counter"), "2", "counter after 2 increments")
|
||||
_suite_state_helper_increment()
|
||||
assert_eq(state_get("suite_counter"), "3", "counter after 3 increments")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_keys ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_keys_basic(_: String) -> String {
|
||||
state_set("suite_sk_x", "1")
|
||||
state_set("suite_sk_y", "2")
|
||||
let keys: String = state_keys()
|
||||
// keys is a JSON array of all state keys — must contain our test keys
|
||||
assert_true(str_contains(keys, "suite_sk_x"), "keys includes suite_sk_x")
|
||||
assert_true(str_contains(keys, "suite_sk_y"), "keys includes suite_sk_y")
|
||||
assert_true(str_starts_with(keys, "["), "keys is a JSON array")
|
||||
assert_true(str_ends_with(keys, "]"), "keys is a JSON array")
|
||||
assert_true(json_array_len(keys) > 0, "keys array is non-empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── value type storage ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_value_types(_: String) -> String {
|
||||
// JSON object as value
|
||||
let json_val: String = "{\"name\":\"alice\",\"age\":30}"
|
||||
state_set("suite_json_val", json_val)
|
||||
let retrieved: String = state_get("suite_json_val")
|
||||
assert_eq(json_get(retrieved, "name"), "alice", "JSON object stored and retrieved")
|
||||
|
||||
// JSON array as value
|
||||
let arr_val: String = "[1,2,3]"
|
||||
state_set("suite_arr_val", arr_val)
|
||||
let retrieved_arr: String = state_get("suite_arr_val")
|
||||
assert_int_eq(json_array_len(retrieved_arr), 3, "JSON array stored and retrieved")
|
||||
|
||||
// Large value
|
||||
let big: String = str_repeat("x", 500)
|
||||
state_set("suite_big_val", big)
|
||||
assert_int_eq(str_len(state_get("suite_big_val")), 500, "large value stored correctly")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
// tests/suite/test_string.el — comprehensive tests for runtime/string.el
|
||||
//
|
||||
// Covers every public function in string.el. Import this file after all
|
||||
// runtime modules and runtime/test.el have been concatenated in.
|
||||
|
||||
// ── str_eq ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_eq_basic(_: String) -> String {
|
||||
assert_true(str_eq("hello", "hello"), "identical strings are equal")
|
||||
assert_false(str_eq("hello", "world"), "different strings are not equal")
|
||||
assert_true(str_eq("", ""), "empty strings are equal")
|
||||
assert_false(str_eq("a", ""), "non-empty vs empty is not equal")
|
||||
assert_false(str_eq("", "a"), "empty vs non-empty is not equal")
|
||||
assert_false(str_eq("Hello", "hello"), "comparison is case-sensitive")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_eq_symbols(_: String) -> String {
|
||||
assert_true(str_eq("!@#$", "!@#$"), "symbols are equal")
|
||||
assert_true(str_eq(" ", " "), "spaces match")
|
||||
assert_false(str_eq(" a", "a "), "leading vs trailing space differ")
|
||||
assert_false(str_eq("abc", "ab"), "prefix not equal to full string")
|
||||
assert_false(str_eq("ab", "abc"), "shorter not equal to longer")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_len ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_len_basic(_: String) -> String {
|
||||
assert_int_eq(str_len(""), 0, "empty string has length 0")
|
||||
assert_int_eq(str_len("a"), 1, "single char has length 1")
|
||||
assert_int_eq(str_len("hello"), 5, "hello has length 5")
|
||||
assert_int_eq(str_len("hello world"), 11, "space counted in length")
|
||||
assert_int_eq(str_len("12345"), 5, "digits counted correctly")
|
||||
assert_int_eq(str_len("!@#$%"), 5, "punctuation counted correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_len_longer(_: String) -> String {
|
||||
let s: String = str_repeat("ab", 50)
|
||||
assert_int_eq(str_len(s), 100, "repeated string has correct length")
|
||||
assert_int_eq(str_len("abcdefghij"), 10, "10-char string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_concat / + operator ───────────────────────────────────────────────────
|
||||
|
||||
fn test_str_concat_basic(_: String) -> String {
|
||||
assert_eq(str_concat("hello", " world"), "hello world", "basic concat")
|
||||
assert_eq(str_concat("", "world"), "world", "empty prefix")
|
||||
assert_eq(str_concat("hello", ""), "hello", "empty suffix")
|
||||
assert_eq(str_concat("", ""), "", "both empty")
|
||||
assert_eq("foo" + "bar", "foobar", "operator + concat")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_concat_chaining(_: String) -> String {
|
||||
let a: String = "hello"
|
||||
let b: String = " "
|
||||
let c: String = "world"
|
||||
assert_eq(a + b + c, "hello world", "chained concat")
|
||||
assert_eq(str_concat(str_concat("a", "b"), "c"), "abc", "nested concat")
|
||||
assert_int_eq(str_len(str_concat("abc", "def")), 6, "length after concat")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_slice ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_slice_basic(_: String) -> String {
|
||||
assert_eq(str_slice("hello world", 0, 5), "hello", "slice from start")
|
||||
assert_eq(str_slice("hello world", 6, 11), "world", "slice from middle")
|
||||
assert_eq(str_slice("hello world", 0, 0), "", "zero-length slice")
|
||||
assert_eq(str_slice("hello", 0, 100), "hello", "end clamped to length")
|
||||
assert_eq(str_slice("hello", 3, 3), "", "start == end is empty")
|
||||
assert_eq(str_slice("hello world", 2, 7), "llo w", "interior slice")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_slice_edge(_: String) -> String {
|
||||
assert_eq(str_slice("", 0, 0), "", "empty string slice")
|
||||
assert_eq(str_slice("abc", 1, 2), "b", "single char slice")
|
||||
assert_eq(str_slice("abc", 0, 3), "abc", "full string slice")
|
||||
assert_eq(str_slice("abc", 2, 1), "", "inverted range is empty")
|
||||
assert_eq(str_slice("hello", 5, 5), "", "slice at end is empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_starts_with ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_starts_with_basic(_: String) -> String {
|
||||
assert_true(str_starts_with("hello world", "hello"), "prefix present")
|
||||
assert_false(str_starts_with("hello world", "world"), "not a prefix")
|
||||
assert_true(str_starts_with("hello", "hello"), "string is its own prefix")
|
||||
assert_true(str_starts_with("hello", ""), "empty prefix always true")
|
||||
assert_false(str_starts_with("", "a"), "empty string has no non-empty prefix")
|
||||
assert_false(str_starts_with("hi", "hello"), "prefix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_starts_with_edge(_: String) -> String {
|
||||
assert_true(str_starts_with("abc", "a"), "single-char prefix")
|
||||
assert_false(str_starts_with("abc", "b"), "wrong single-char prefix")
|
||||
assert_true(str_starts_with("", ""), "empty starts with empty")
|
||||
assert_true(str_starts_with("hello world", "hello world"), "full string is prefix")
|
||||
assert_false(str_starts_with("hello", "HELLO"), "case-sensitive prefix")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_ends_with ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_ends_with_basic(_: String) -> String {
|
||||
assert_true(str_ends_with("hello world", "world"), "suffix present")
|
||||
assert_false(str_ends_with("hello world", "hello"), "not a suffix")
|
||||
assert_true(str_ends_with("hello", "hello"), "string is its own suffix")
|
||||
assert_true(str_ends_with("hello", ""), "empty suffix always true")
|
||||
assert_false(str_ends_with("", "a"), "empty string has no non-empty suffix")
|
||||
assert_false(str_ends_with("hi", "world"), "suffix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_ends_with_edge(_: String) -> String {
|
||||
assert_true(str_ends_with("abc", "c"), "single-char suffix")
|
||||
assert_false(str_ends_with("abc", "b"), "wrong single-char suffix")
|
||||
assert_true(str_ends_with("", ""), "empty ends with empty")
|
||||
assert_false(str_ends_with("hello", "HELLO"), "case-sensitive suffix")
|
||||
assert_true(str_ends_with("file.txt", ".txt"), "common file extension case")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_contains ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_contains_basic(_: String) -> String {
|
||||
assert_true(str_contains("hello world", "world"), "contains at end")
|
||||
assert_true(str_contains("hello world", "hello"), "contains at start")
|
||||
assert_true(str_contains("hello world", "lo wo"), "contains in middle")
|
||||
assert_false(str_contains("hello world", "xyz"), "not contained")
|
||||
assert_true(str_contains("hello", ""), "empty sub always contained")
|
||||
assert_false(str_contains("", "a"), "empty string contains nothing")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_contains_edge(_: String) -> String {
|
||||
assert_true(str_contains("hello", "hello"), "string contains itself")
|
||||
assert_false(str_contains("hello", "helloo"), "longer sub not contained")
|
||||
assert_true(str_contains("aaa", "a"), "single char in repeated chars")
|
||||
assert_true(str_contains("aaa", "aa"), "two-char sub in repeated chars")
|
||||
assert_false(str_contains("abc", "ABC"), "case-sensitive")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_index_of ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_index_of_basic(_: String) -> String {
|
||||
assert_int_eq(str_index_of("hello world", "world"), 6, "index of suffix")
|
||||
assert_int_eq(str_index_of("hello world", "hello"), 0, "index of prefix")
|
||||
assert_int_eq(str_index_of("hello world", "o"), 4, "index of first occurrence")
|
||||
assert_int_eq(str_index_of("hello world", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_index_of("hello", ""), 0, "empty sub returns 0")
|
||||
assert_int_eq(str_index_of("", "a"), -1, "search in empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_index_of_duplicates(_: String) -> String {
|
||||
assert_int_eq(str_index_of("aababc", "ab"), 1, "finds first of two occurrences")
|
||||
assert_int_eq(str_index_of("abab", "ab"), 0, "finds first at position 0")
|
||||
assert_int_eq(str_index_of("xabab", "ab"), 1, "finds first after leading char")
|
||||
assert_int_eq(str_index_of("hello hello", "hello"), 0, "finds first hello")
|
||||
assert_int_eq(str_index_of("hello", "hello"), 0, "exact match at 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_last_index_of ─────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_last_index_of_basic(_: String) -> String {
|
||||
assert_int_eq(str_last_index_of("hello hello", "hello"), 6, "last occurrence index")
|
||||
assert_int_eq(str_last_index_of("hello", "hello"), 0, "only one occurrence")
|
||||
assert_int_eq(str_last_index_of("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_last_index_of("aababc", "ab"), 3, "last ab in aababc")
|
||||
assert_int_eq(str_last_index_of("abcabc", "abc"), 3, "last abc")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_replace ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_replace_basic(_: String) -> String {
|
||||
assert_eq(str_replace("hello world", "world", "there"), "hello there", "basic replace")
|
||||
assert_eq(str_replace("aaa", "a", "b"), "bbb", "replaces all occurrences")
|
||||
assert_eq(str_replace("hello", "xyz", "abc"), "hello", "no match is identity")
|
||||
assert_eq(str_replace("", "a", "b"), "", "empty string unchanged")
|
||||
assert_eq(str_replace("hello", "", "x"), "hello", "empty from is identity")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_replace_multiple(_: String) -> String {
|
||||
assert_eq(str_replace("hello hello", "hello", "bye"), "bye bye", "replace multiple")
|
||||
assert_eq(str_replace("aXbXc", "X", "-"), "a-b-c", "single-char delimiter")
|
||||
assert_eq(str_replace("abcabc", "abc", "X"), "XX", "multi-char replaced twice")
|
||||
assert_eq(str_replace("aabbcc", "bb", ""), "aacc", "replace with empty")
|
||||
assert_eq(str_replace("hello", "hello", "world"), "world", "replace whole string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_to_upper / str_to_lower ──────────────────────────────────────────────
|
||||
|
||||
fn test_str_to_upper_basic(_: String) -> String {
|
||||
assert_eq(str_to_upper("hello"), "HELLO", "lowercase to uppercase")
|
||||
assert_eq(str_to_upper("HELLO"), "HELLO", "already uppercase unchanged")
|
||||
assert_eq(str_to_upper("Hello World"), "HELLO WORLD", "mixed case")
|
||||
assert_eq(str_to_upper(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_upper("hello123"), "HELLO123", "digits unchanged")
|
||||
assert_eq(str_to_upper("abc-def"), "ABC-DEF", "punctuation unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_lower_basic(_: String) -> String {
|
||||
assert_eq(str_to_lower("HELLO"), "hello", "uppercase to lowercase")
|
||||
assert_eq(str_to_lower("hello"), "hello", "already lowercase unchanged")
|
||||
assert_eq(str_to_lower("Hello World"), "hello world", "mixed case")
|
||||
assert_eq(str_to_lower(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_lower("HELLO123"), "hello123", "digits unchanged")
|
||||
assert_eq(str_to_lower("ABC-DEF"), "abc-def", "punctuation unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_upper_lower_roundtrip(_: String) -> String {
|
||||
let s: String = "Hello, World!"
|
||||
assert_eq(str_to_lower(str_to_upper(s)), "hello, world!", "upper then lower")
|
||||
assert_eq(str_to_upper(str_to_lower(s)), "HELLO, WORLD!", "lower then upper")
|
||||
assert_eq(str_to_upper(str_to_lower("ABC")), "ABC", "ABC roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_trim / str_lstrip / str_rstrip ───────────────────────────────────────
|
||||
|
||||
fn test_str_trim_basic(_: String) -> String {
|
||||
assert_eq(str_trim(" hello "), "hello", "trims spaces both sides")
|
||||
assert_eq(str_trim("hello"), "hello", "no whitespace unchanged")
|
||||
assert_eq(str_trim(" "), "", "all-space string becomes empty")
|
||||
assert_eq(str_trim(""), "", "empty string unchanged")
|
||||
assert_eq(str_trim("\t hello \n"), "hello", "trims tabs and newlines")
|
||||
assert_eq(str_trim(" hello world "), "hello world", "internal spaces preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_lstrip_rstrip(_: String) -> String {
|
||||
assert_eq(str_lstrip(" hello "), "hello ", "lstrip removes leading spaces")
|
||||
assert_eq(str_rstrip(" hello "), " hello", "rstrip removes trailing spaces")
|
||||
assert_eq(str_lstrip("hello"), "hello", "lstrip no-op on clean string")
|
||||
assert_eq(str_rstrip("hello"), "hello", "rstrip no-op on clean string")
|
||||
assert_eq(str_lstrip(""), "", "lstrip empty string")
|
||||
assert_eq(str_rstrip(""), "", "rstrip empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_split ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_split_basic(_: String) -> String {
|
||||
let parts: [String] = str_split("a,b,c", ",")
|
||||
assert_int_eq(el_list_len(parts), 3, "split yields 3 parts")
|
||||
assert_eq(el_list_get(parts, 0), "a", "first part is a")
|
||||
assert_eq(el_list_get(parts, 1), "b", "second part is b")
|
||||
assert_eq(el_list_get(parts, 2), "c", "third part is c")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_split_edge(_: String) -> String {
|
||||
let single: [String] = str_split("hello", ",")
|
||||
assert_int_eq(el_list_len(single), 1, "no sep found yields 1 part")
|
||||
assert_eq(el_list_get(single, 0), "hello", "single part is the full string")
|
||||
|
||||
let trailing: [String] = str_split("a,b,", ",")
|
||||
assert_int_eq(el_list_len(trailing), 3, "trailing sep yields empty last element")
|
||||
assert_eq(el_list_get(trailing, 2), "", "last element is empty")
|
||||
|
||||
let multi: [String] = str_split("one::two::three", "::")
|
||||
assert_int_eq(el_list_len(multi), 3, "multi-char separator works")
|
||||
assert_eq(el_list_get(multi, 1), "two", "middle element correct")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_join ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_join_basic(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "a")
|
||||
let parts = el_list_append(parts, "b")
|
||||
let parts = el_list_append(parts, "c")
|
||||
assert_eq(str_join(parts, ","), "a,b,c", "basic join with comma")
|
||||
assert_eq(str_join(parts, ""), "abc", "join with empty separator")
|
||||
assert_eq(str_join(parts, " | "), "a | b | c", "join with multi-char sep")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_join_edge(_: String) -> String {
|
||||
let empty_list: [String] = el_list_empty()
|
||||
assert_eq(str_join(empty_list, ","), "", "joining empty list yields empty")
|
||||
|
||||
let one: [String] = el_list_empty()
|
||||
let one = el_list_append(one, "solo")
|
||||
assert_eq(str_join(one, ","), "solo", "joining single element yields that element")
|
||||
assert_eq(str_join(one, "---"), "solo", "single element: no sep inserted")
|
||||
|
||||
let two: [String] = el_list_empty()
|
||||
let two = el_list_append(two, "alpha")
|
||||
let two = el_list_append(two, "beta")
|
||||
assert_eq(str_join(two, ", "), "alpha, beta", "two elements join correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── int_to_str / str_to_int ──────────────────────────────────────────────────
|
||||
|
||||
fn test_int_to_str_basic(_: String) -> String {
|
||||
assert_eq(int_to_str(0), "0", "zero")
|
||||
assert_eq(int_to_str(42), "42", "positive integer")
|
||||
assert_eq(int_to_str(-1), "-1", "negative integer")
|
||||
assert_eq(int_to_str(1000000), "1000000", "large integer")
|
||||
assert_eq(int_to_str(-999), "-999", "negative large")
|
||||
assert_int_eq(str_len(int_to_str(12345)), 5, "correct digit count")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_int_basic(_: String) -> String {
|
||||
assert_int_eq(str_to_int("0"), 0, "zero")
|
||||
assert_int_eq(str_to_int("42"), 42, "positive integer")
|
||||
assert_int_eq(str_to_int("-1"), -1, "negative integer")
|
||||
assert_int_eq(str_to_int("1000000"), 1000000, "large integer")
|
||||
assert_int_eq(str_to_int("-999"), -999, "negative large")
|
||||
assert_int_eq(str_to_int(int_to_str(12345)), 12345, "roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── float_to_str / str_to_float ──────────────────────────────────────────────
|
||||
|
||||
fn test_float_to_str_basic(_: String) -> String {
|
||||
assert_eq(float_to_str(0.0), "0", "zero float to str")
|
||||
assert_eq(float_to_str(1.5), "1.5", "basic float")
|
||||
assert_eq(float_to_str(-3.14), "-3.14", "negative float")
|
||||
assert_true(str_len(float_to_str(1.0)) > 0, "float_to_str produces output")
|
||||
assert_contains(float_to_str(100.0), "100", "100.0 contains 100")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_float_basic(_: String) -> String {
|
||||
let f: Float = str_to_float("1.5")
|
||||
let s: String = float_to_str(f)
|
||||
assert_eq(s, "1.5", "roundtrip 1.5")
|
||||
|
||||
let f2: Float = str_to_float("0.0")
|
||||
let s2: String = float_to_str(f2)
|
||||
assert_eq(s2, "0", "roundtrip 0.0")
|
||||
|
||||
assert_true(str_to_float("3.14") > 3.13, "3.14 > 3.13")
|
||||
assert_true(str_to_float("3.14") < 3.15, "3.14 < 3.15")
|
||||
assert_true(str_to_float("-1.0") < 0.0, "negative float is negative")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_repeat / str_reverse ─────────────────────────────────────────────────
|
||||
|
||||
fn test_str_repeat_basic(_: String) -> String {
|
||||
assert_eq(str_repeat("ab", 3), "ababab", "repeat 3 times")
|
||||
assert_eq(str_repeat("x", 1), "x", "repeat once")
|
||||
assert_eq(str_repeat("x", 0), "", "repeat zero times yields empty")
|
||||
assert_eq(str_repeat("", 5), "", "repeating empty string yields empty")
|
||||
assert_eq(str_repeat("-", 4), "----", "single char repeat")
|
||||
assert_int_eq(str_len(str_repeat("abc", 10)), 30, "length is n * len")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_reverse_basic(_: String) -> String {
|
||||
assert_eq(str_reverse("hello"), "olleh", "basic reverse")
|
||||
assert_eq(str_reverse("a"), "a", "single char is its own reverse")
|
||||
assert_eq(str_reverse(""), "", "empty string reverses to empty")
|
||||
assert_eq(str_reverse("abcd"), "dcba", "even-length reverse")
|
||||
assert_eq(str_reverse("racecar"), "racecar", "palindrome unchanged")
|
||||
assert_eq(str_reverse("abc"), "cba", "three-char reverse")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_count ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_count_basic(_: String) -> String {
|
||||
assert_int_eq(str_count("hello world hello", "hello"), 2, "two occurrences")
|
||||
assert_int_eq(str_count("aaa", "a"), 3, "adjacent single chars")
|
||||
assert_int_eq(str_count("aaa", "aa"), 1, "non-overlapping: one match")
|
||||
assert_int_eq(str_count("hello", "xyz"), 0, "no match")
|
||||
assert_int_eq(str_count("", "a"), 0, "empty string has no occurrences")
|
||||
assert_int_eq(str_count("hello", ""), 0, "empty sub returns 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_strip_prefix / str_strip_suffix ──────────────────────────────────────
|
||||
|
||||
fn test_str_strip_prefix_basic(_: String) -> String {
|
||||
assert_eq(str_strip_prefix("foobar", "foo"), "bar", "strips matching prefix")
|
||||
assert_eq(str_strip_prefix("foobar", "baz"), "foobar", "no-match is identity")
|
||||
assert_eq(str_strip_prefix("hello", ""), "hello", "empty prefix is identity")
|
||||
assert_eq(str_strip_prefix("hello", "hello"), "", "full match yields empty")
|
||||
assert_eq(str_strip_prefix("hello", "HELLO"), "hello", "case-sensitive no-match")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_strip_suffix_basic(_: String) -> String {
|
||||
assert_eq(str_strip_suffix("hello.md", ".md"), "hello", "strips matching suffix")
|
||||
assert_eq(str_strip_suffix("hello.md", ".txt"), "hello.md", "no-match is identity")
|
||||
assert_eq(str_strip_suffix("hello", ""), "hello", "empty suffix is identity")
|
||||
assert_eq(str_strip_suffix("hello", "hello"), "", "full match yields empty")
|
||||
assert_eq(str_strip_suffix("hello", "HELLO"), "hello", "case-sensitive no-match")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_find_chars ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_find_chars_basic(_: String) -> String {
|
||||
assert_int_eq(str_find_chars("hello world", " "), 5, "finds space at index 5")
|
||||
assert_int_eq(str_find_chars("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_find_chars("hello", ""), -1, "empty charset returns -1")
|
||||
assert_int_eq(str_find_chars("hello", "aeiou"), 1, "finds first vowel at index 1")
|
||||
assert_int_eq(str_find_chars("", "a"), -1, "search in empty string returns -1")
|
||||
assert_int_eq(str_find_chars("abc", "c"), 2, "finds last char of string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_char_at / str_char_code ───────────────────────────────────────────────
|
||||
|
||||
fn test_str_char_at_basic(_: String) -> String {
|
||||
assert_eq(str_char_at("hello", 0), "h", "first char")
|
||||
assert_eq(str_char_at("hello", 4), "o", "last char")
|
||||
assert_eq(str_char_at("hello", 2), "l", "middle char")
|
||||
assert_eq(str_char_at("hello", -1), "", "negative index yields empty")
|
||||
assert_eq(str_char_at("hello", 5), "", "out-of-bounds yields empty")
|
||||
assert_eq(str_char_at("", 0), "", "empty string yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_char_code_basic(_: String) -> String {
|
||||
assert_int_eq(str_char_code("A", 0), 65, "A has code 65")
|
||||
assert_int_eq(str_char_code("a", 0), 97, "a has code 97")
|
||||
assert_int_eq(str_char_code("0", 0), 48, "0 has code 48")
|
||||
assert_int_eq(str_char_code("hello", 0), 104, "h has code 104")
|
||||
assert_int_eq(str_char_code("", 0), 0, "empty string yields 0")
|
||||
assert_int_eq(str_char_code("a", 5), 0, "out-of-bounds yields 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_pad_left / str_pad_right ─────────────────────────────────────────────
|
||||
|
||||
fn test_str_pad_left_basic(_: String) -> String {
|
||||
assert_eq(str_pad_left("hi", 5, " "), " hi", "pads to width 5")
|
||||
assert_eq(str_pad_left("hi", 2, " "), "hi", "already at width is unchanged")
|
||||
assert_eq(str_pad_left("hi", 1, " "), "hi", "wider than width is unchanged")
|
||||
assert_eq(str_pad_left("5", 3, "0"), "005", "zero-pad a number")
|
||||
assert_int_eq(str_len(str_pad_left("ab", 7, "x")), 7, "padded to exact width")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_pad_right_basic(_: String) -> String {
|
||||
assert_eq(str_pad_right("hi", 5, " "), "hi ", "pads to width 5")
|
||||
assert_eq(str_pad_right("hi", 2, " "), "hi", "already at width is unchanged")
|
||||
assert_eq(str_pad_right("hi", 1, " "), "hi", "wider than width is unchanged")
|
||||
assert_int_eq(str_len(str_pad_right("ab", 7, "-")), 7, "padded to exact width")
|
||||
assert_starts_with(str_pad_right("ab", 7, "-"), "ab", "original string at start")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Character classification ──────────────────────────────────────────────────
|
||||
|
||||
fn test_is_letter_basic(_: String) -> String {
|
||||
assert_true(is_letter("a"), "a is a letter")
|
||||
assert_true(is_letter("Z"), "Z is a letter")
|
||||
assert_true(is_letter("abc"), "abc all letters")
|
||||
assert_false(is_letter("1"), "1 is not a letter")
|
||||
assert_false(is_letter(""), "empty string is not a letter")
|
||||
assert_false(is_letter("a1"), "a1 has non-letter")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_is_digit_basic(_: String) -> String {
|
||||
assert_true(is_digit("0"), "0 is a digit")
|
||||
assert_true(is_digit("9"), "9 is a digit")
|
||||
assert_true(is_digit("123"), "123 all digits")
|
||||
assert_false(is_digit("a"), "a is not a digit")
|
||||
assert_false(is_digit(""), "empty is not a digit")
|
||||
assert_false(is_digit("1a"), "1a has non-digit")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_is_whitespace_basic(_: String) -> String {
|
||||
assert_true(is_whitespace(" "), "space is whitespace")
|
||||
assert_true(is_whitespace(" "), "multiple spaces")
|
||||
assert_false(is_whitespace("a"), "a is not whitespace")
|
||||
assert_false(is_whitespace(""), "empty string is not whitespace")
|
||||
assert_false(is_whitespace(" a"), "mixed is not all whitespace")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_count_lines / str_count_words ────────────────────────────────────────
|
||||
|
||||
fn test_str_count_lines_basic(_: String) -> String {
|
||||
assert_int_eq(str_count_lines(""), 0, "empty string has 0 lines")
|
||||
assert_int_eq(str_count_lines("hello"), 1, "single line no newline")
|
||||
assert_int_eq(str_count_lines("a\nb"), 2, "two lines with newline")
|
||||
assert_int_eq(str_count_lines("a\nb\nc"), 3, "three lines")
|
||||
assert_int_eq(str_count_lines("a\n"), 1, "trailing newline not counted as extra line")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_count_words_basic(_: String) -> String {
|
||||
assert_int_eq(str_count_words(""), 0, "empty string has 0 words")
|
||||
assert_int_eq(str_count_words("hello"), 1, "single word")
|
||||
assert_int_eq(str_count_words("hello world"), 2, "two words")
|
||||
assert_int_eq(str_count_words(" hello world "), 2, "extra spaces do not add words")
|
||||
assert_int_eq(str_count_words("one two three four"), 4, "four words")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── url_encode / url_decode ──────────────────────────────────────────────────
|
||||
|
||||
fn test_url_encode_basic(_: String) -> String {
|
||||
assert_eq(url_encode("hello"), "hello", "plain string unchanged")
|
||||
assert_contains(url_encode("hello world"), "%20", "space encoded as %20")
|
||||
assert_true(str_len(url_encode("hello world")) > str_len("hello world"), "encoded is longer")
|
||||
let encoded: String = url_encode("hello world")
|
||||
let decoded: String = url_decode(encoded)
|
||||
assert_eq(decoded, "hello world", "decode reverses encode")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_url_roundtrip(_: String) -> String {
|
||||
let s: String = "name=hello world&value=42"
|
||||
let encoded: String = url_encode(s)
|
||||
let decoded: String = url_decode(encoded)
|
||||
assert_eq(decoded, s, "url encode/decode roundtrip")
|
||||
assert_true(!str_eq(encoded, s), "encoded differs from original")
|
||||
assert_true(str_len(encoded) >= str_len(s), "encoded is at least as long")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── bool_to_str ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_bool_to_str_basic(_: String) -> String {
|
||||
assert_eq(bool_to_str(true), "true", "true becomes 'true'")
|
||||
assert_eq(bool_to_str(false), "false", "false becomes 'false'")
|
||||
assert_true(str_eq(bool_to_str(1 == 1), "true"), "expression true")
|
||||
assert_true(str_eq(bool_to_str(1 == 2), "false"), "expression false")
|
||||
assert_int_eq(str_len(bool_to_str(true)), 4, "true has length 4")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_to_bytes / bytes_to_str ───────────────────────────────────────────────
|
||||
|
||||
fn test_str_to_bytes_basic(_: String) -> String {
|
||||
let b: String = str_to_bytes("hi")
|
||||
assert_true(str_contains(b, "104"), "h has code 104")
|
||||
assert_true(str_contains(b, "105"), "i has code 105")
|
||||
assert_eq(str_to_bytes(""), "[]", "empty string yields empty array")
|
||||
assert_true(str_starts_with(b, "["), "bytes is JSON array")
|
||||
assert_true(str_ends_with(b, "]"), "bytes is JSON array")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_bytes_roundtrip(_: String) -> String {
|
||||
let s: String = "hello"
|
||||
let b: String = str_to_bytes(s)
|
||||
let s2: String = bytes_to_str(b)
|
||||
assert_eq(s2, s, "bytes roundtrip: hello")
|
||||
|
||||
let s3: String = "ABC"
|
||||
let b3: String = str_to_bytes(s3)
|
||||
let s4: String = bytes_to_str(b3)
|
||||
assert_eq(s4, s3, "bytes roundtrip: ABC")
|
||||
|
||||
assert_eq(bytes_to_str("[]"), "", "empty bytes yields empty string")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
// tests/suite/test_time.el — comprehensive tests for runtime/time.el
|
||||
//
|
||||
// Covers time_now, now_millis, unix_timestamp, now_ns, time_format, time_to_parts,
|
||||
// time_add, time_diff, duration helpers, and uuid_new.
|
||||
|
||||
// ── time_now / now_millis / unix_timestamp_ms ─────────────────────────────────
|
||||
|
||||
fn test_time_now_basic(_: String) -> String {
|
||||
let t: Int = time_now()
|
||||
// Milliseconds since epoch: must be well past year 2020 (1577836800000)
|
||||
assert_true(t > 1577836800000, "time_now is past 2020-01-01")
|
||||
// Must be before year 2100 (4102444800000)
|
||||
assert_true(t < 4102444800000, "time_now is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_now_millis_basic(_: String) -> String {
|
||||
let t: Int = now_millis()
|
||||
assert_true(t > 1577836800000, "now_millis is past 2020-01-01")
|
||||
assert_true(t < 4102444800000, "now_millis is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_unix_timestamp_ms_basic(_: String) -> String {
|
||||
let t: Int = unix_timestamp_ms()
|
||||
assert_true(t > 1577836800000, "unix_timestamp_ms is past 2020-01-01")
|
||||
assert_true(t < 4102444800000, "unix_timestamp_ms is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_now_ms_alias(_: String) -> String {
|
||||
let t: Int = time_now_ms()
|
||||
assert_true(t > 1577836800000, "time_now_ms is past 2020-01-01")
|
||||
assert_true(t < 4102444800000, "time_now_ms is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── monotonicity ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_monotonic_now_millis(_: String) -> String {
|
||||
let t1: Int = now_millis()
|
||||
let t2: Int = now_millis()
|
||||
assert_true(t2 >= t1, "second call >= first call (monotonic)")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_monotonic_time_now(_: String) -> String {
|
||||
let t1: Int = time_now()
|
||||
let t2: Int = time_now()
|
||||
assert_true(t2 >= t1, "time_now is monotonic across calls")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_monotonic_now_ns(_: String) -> String {
|
||||
let t1: Int = now_ns()
|
||||
let t2: Int = now_ns()
|
||||
assert_true(t2 >= t1, "now_ns is monotonic across calls")
|
||||
// ns should be much larger than ms (9 digits difference)
|
||||
assert_true(t1 > 1000000000000000000, "now_ns is in nanosecond scale")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── unix_timestamp (seconds) ─────────────────────────────────────────────────
|
||||
|
||||
fn test_unix_timestamp_basic(_: String) -> String {
|
||||
let secs: Int = unix_timestamp()
|
||||
// Must be past 2020-01-01 in seconds (1577836800)
|
||||
assert_true(secs > 1577836800, "unix_timestamp is past 2020-01-01")
|
||||
// Must be before 2100-01-01 in seconds (4102444800)
|
||||
assert_true(secs < 4102444800, "unix_timestamp is before 2100-01-01")
|
||||
// seconds are about 1000x smaller than millis
|
||||
let ms: Int = time_now()
|
||||
assert_true(ms > secs, "milliseconds > seconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_to_parts ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_to_parts_basic(_: String) -> String {
|
||||
// Known timestamp: 2024-01-15 12:30:45.123 UTC
|
||||
// 2024-01-15 00:00:00 UTC = epoch + (54 * 365 + 14 leaps) days approximately
|
||||
// Use a precisely known value: 2024-01-15T12:30:45.123Z
|
||||
// epoch_ms = 1705320645123
|
||||
let ts: Int = 1705320645123
|
||||
let parts: String = time_to_parts(ts)
|
||||
assert_eq(json_get(parts, "year"), "2024", "year is 2024")
|
||||
assert_eq(json_get(parts, "month"), "1", "month is 1 (January)")
|
||||
assert_eq(json_get(parts, "day"), "15", "day is 15")
|
||||
assert_eq(json_get(parts, "hour"), "12", "hour is 12")
|
||||
assert_eq(json_get(parts, "minute"), "30", "minute is 30")
|
||||
assert_eq(json_get(parts, "second"), "45", "second is 45")
|
||||
assert_eq(json_get(parts, "ms"), "123", "milliseconds is 123")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_to_parts_epoch(_: String) -> String {
|
||||
// Unix epoch: 1970-01-01T00:00:00.000Z = timestamp 0
|
||||
let parts: String = time_to_parts(0)
|
||||
assert_eq(json_get(parts, "year"), "1970", "epoch year is 1970")
|
||||
assert_eq(json_get(parts, "month"), "1", "epoch month is 1")
|
||||
assert_eq(json_get(parts, "day"), "1", "epoch day is 1")
|
||||
assert_eq(json_get(parts, "hour"), "0", "epoch hour is 0")
|
||||
assert_eq(json_get(parts, "minute"), "0", "epoch minute is 0")
|
||||
assert_eq(json_get(parts, "second"), "0", "epoch second is 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_to_parts_current(_: String) -> String {
|
||||
let ts: Int = time_now()
|
||||
let parts: String = time_to_parts(ts)
|
||||
let year: Int = str_to_int(json_get(parts, "year"))
|
||||
let month: Int = str_to_int(json_get(parts, "month"))
|
||||
let day: Int = str_to_int(json_get(parts, "day"))
|
||||
let hour: Int = str_to_int(json_get(parts, "hour"))
|
||||
let minute: Int = str_to_int(json_get(parts, "minute"))
|
||||
let second: Int = str_to_int(json_get(parts, "second"))
|
||||
assert_true(year >= 2024, "current year >= 2024")
|
||||
assert_true(year < 2100, "current year < 2100")
|
||||
assert_true(month >= 1, "month >= 1")
|
||||
assert_true(month <= 12, "month <= 12")
|
||||
assert_true(day >= 1, "day >= 1")
|
||||
assert_true(day <= 31, "day <= 31")
|
||||
assert_true(hour >= 0, "hour >= 0")
|
||||
assert_true(hour <= 23, "hour <= 23")
|
||||
assert_true(minute >= 0, "minute >= 0")
|
||||
assert_true(minute <= 59, "minute <= 59")
|
||||
assert_true(second >= 0, "second >= 0")
|
||||
assert_true(second <= 59, "second <= 59")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_format ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_format_iso(_: String) -> String {
|
||||
let ts: Int = 1705320645123
|
||||
let iso: String = time_format(ts, "ISO")
|
||||
assert_starts_with(iso, "2024-01-15", "ISO format starts with correct date")
|
||||
assert_contains(iso, "T12:30:45", "ISO format has correct time")
|
||||
assert_ends_with(iso, "Z", "ISO format ends with Z")
|
||||
assert_contains(iso, "123", "ISO format includes milliseconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_format_empty(_: String) -> String {
|
||||
let ts: Int = 1705320645123
|
||||
let iso: String = time_format(ts, "")
|
||||
assert_starts_with(iso, "2024-01-15", "empty fmt gives ISO format")
|
||||
assert_ends_with(iso, "Z", "empty fmt gives ISO format ending in Z")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_format_strftime(_: String) -> String {
|
||||
let ts: Int = 1705320645123
|
||||
let formatted: String = time_format(ts, "%Y-%m-%d")
|
||||
assert_eq(formatted, "2024-01-15", "strftime %Y-%m-%d")
|
||||
let formatted2: String = time_format(ts, "%H:%M:%S")
|
||||
assert_eq(formatted2, "12:30:45", "strftime %H:%M:%S")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_add ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_add_basic(_: String) -> String {
|
||||
let base: Int = 1000000000000
|
||||
assert_int_eq(time_add(base, 1000, "ms"), base + 1000, "add milliseconds")
|
||||
assert_int_eq(time_add(base, 1, "sec"), base + 1000, "add 1 second")
|
||||
assert_int_eq(time_add(base, 1, "min"), base + 60000, "add 1 minute")
|
||||
assert_int_eq(time_add(base, 1, "hour"), base + 3600000, "add 1 hour")
|
||||
assert_int_eq(time_add(base, 1, "day"), base + 86400000, "add 1 day")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_add_multiple(_: String) -> String {
|
||||
let base: Int = 1000000000000
|
||||
assert_int_eq(time_add(base, 60, "sec"), base + 60000, "add 60 seconds")
|
||||
assert_int_eq(time_add(base, 24, "hour"), base + 86400000, "add 24 hours = 1 day")
|
||||
assert_int_eq(time_add(base, 0, "sec"), base, "add 0 seconds")
|
||||
assert_int_eq(time_add(base, -1, "sec"), base - 1000, "add negative seconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_diff ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_diff_basic(_: String) -> String {
|
||||
let t1: Int = 1000000000000
|
||||
let t2: Int = t1 + 5000
|
||||
assert_int_eq(time_diff(t1, t2, "ms"), 5000, "diff in ms")
|
||||
assert_int_eq(time_diff(t1, t2, "sec"), 5, "diff in seconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_diff_larger(_: String) -> String {
|
||||
let t1: Int = 1000000000000
|
||||
let t2: Int = t1 + 3600000
|
||||
assert_int_eq(time_diff(t1, t2, "min"), 60, "diff of 1 hour in minutes")
|
||||
assert_int_eq(time_diff(t1, t2, "hour"), 1, "diff of 1 hour in hours")
|
||||
assert_int_eq(time_diff(t1, t2, "ms"), 3600000, "diff of 1 hour in ms")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_diff_zero(_: String) -> String {
|
||||
let t: Int = 1000000000000
|
||||
assert_int_eq(time_diff(t, t, "ms"), 0, "same timestamp: diff is 0")
|
||||
assert_int_eq(time_diff(t, t, "sec"), 0, "same timestamp: diff in sec is 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Duration helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
fn test_duration_helpers_basic(_: String) -> String {
|
||||
assert_int_eq(duration_seconds(1), 1000000000, "1 second = 1e9 ns")
|
||||
assert_int_eq(duration_millis(1), 1000000, "1 ms = 1e6 ns")
|
||||
assert_int_eq(duration_nanos(42), 42, "nanos identity")
|
||||
assert_int_eq(duration_to_seconds(1000000000), 1, "1e9 ns = 1 second")
|
||||
assert_int_eq(duration_to_millis(1000000), 1, "1e6 ns = 1 ms")
|
||||
assert_int_eq(duration_to_nanos(42), 42, "nanos identity roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_instant_helpers_basic(_: String) -> String {
|
||||
let i: Int = unix_seconds(1000)
|
||||
assert_int_eq(i, 1000000000000, "unix_seconds(1000) = 1e12 ns")
|
||||
|
||||
let ms_i: Int = unix_millis(5000)
|
||||
assert_int_eq(ms_i, 5000000000, "unix_millis(5000) = 5e9 ns")
|
||||
|
||||
assert_int_eq(instant_to_unix_seconds(1000000000000), 1000, "instant to seconds")
|
||||
assert_int_eq(instant_to_unix_millis(5000000000), 5000, "instant to millis")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── uuid_new / uuid_v4 ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_uuid_new_basic(_: String) -> String {
|
||||
let u: String = uuid_new()
|
||||
assert_int_eq(str_len(u), 36, "UUID has 36 characters")
|
||||
assert_eq(str_char_at(u, 8), "-", "UUID has dash at position 8")
|
||||
assert_eq(str_char_at(u, 13), "-", "UUID has dash at position 13")
|
||||
assert_eq(str_char_at(u, 18), "-", "UUID has dash at position 18")
|
||||
assert_eq(str_char_at(u, 23), "-", "UUID has dash at position 23")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_uuid_uniqueness(_: String) -> String {
|
||||
let u1: String = uuid_new()
|
||||
let u2: String = uuid_new()
|
||||
let u3: String = uuid_v4()
|
||||
assert_false(str_eq(u1, u2), "consecutive UUIDs are different")
|
||||
assert_false(str_eq(u1, u3), "uuid_new and uuid_v4 produce different values")
|
||||
assert_false(str_eq(u2, u3), "three consecutive UUIDs are all different")
|
||||
assert_int_eq(str_len(u3), 36, "uuid_v4 also produces 36-char UUID")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_from_parts ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_from_parts_basic(_: String) -> String {
|
||||
// time_from_parts(secs, ns, tz) = secs * 1000 + ns / 1000000
|
||||
let ts: Int = time_from_parts(1000, 0, "UTC")
|
||||
assert_int_eq(ts, 1000000, "1000 secs = 1000000 ms")
|
||||
|
||||
let ts2: Int = time_from_parts(0, 500000000, "UTC")
|
||||
assert_int_eq(ts2, 500, "500ms in nanoseconds -> 500ms")
|
||||
|
||||
let ts3: Int = time_from_parts(1705320645, 123000000, "UTC")
|
||||
assert_int_eq(ts3, 1705320645123, "known timestamp with ms component")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── instant_to_iso8601 ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_instant_to_iso8601_basic(_: String) -> String {
|
||||
let ts_ms: Int = 1705320645123
|
||||
let ts_ns: Int = ts_ms * 1000000
|
||||
let iso: String = instant_to_iso8601(ts_ns)
|
||||
assert_starts_with(iso, "2024-01-15", "instant_to_iso8601 correct date")
|
||||
assert_ends_with(iso, "Z", "instant_to_iso8601 ends with Z")
|
||||
return ""
|
||||
}
|
||||
Executable
+127
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# install.sh — install El as a proper local framework tool.
|
||||
#
|
||||
# Copies the compiler binary, runtime source files, headers, and stdlib
|
||||
# from the local source tree into the install prefix. After install,
|
||||
# El programs can be built against an installed, stable copy of the runtime.
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/install.sh
|
||||
# ./tools/install.sh --prefix /opt/el
|
||||
#
|
||||
# Environment:
|
||||
# EL_HOME Override install prefix (same as --prefix)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EL_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
PREFIX="${EL_HOME:-/usr/local/el}"
|
||||
|
||||
# Parse --prefix flag
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--prefix)
|
||||
PREFIX="$2"
|
||||
shift 2
|
||||
;;
|
||||
--prefix=*)
|
||||
PREFIX="${1#--prefix=}"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
echo "Usage: $0 [--prefix <path>]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
BIN_DIR="${PREFIX}/bin"
|
||||
RUNTIME_DIR="${PREFIX}/runtime"
|
||||
INCLUDE_DIR="${PREFIX}/include"
|
||||
LIB_DIR="${PREFIX}/lib"
|
||||
|
||||
ELC_SRC="${EL_ROOT}/dist/platform/elc"
|
||||
RUNTIME_SRC="${EL_ROOT}/el-compiler/runtime"
|
||||
STDLIB_SRC="${EL_ROOT}/runtime"
|
||||
|
||||
echo "==> Installing El framework to ${PREFIX}"
|
||||
echo " bin: ${BIN_DIR}"
|
||||
echo " runtime: ${RUNTIME_DIR}"
|
||||
echo " include: ${INCLUDE_DIR}"
|
||||
echo " lib: ${LIB_DIR}"
|
||||
echo
|
||||
|
||||
# Create directories
|
||||
mkdir -p "${BIN_DIR}" "${RUNTIME_DIR}" "${INCLUDE_DIR}" "${LIB_DIR}"
|
||||
|
||||
# 1. Install elc binary
|
||||
if [[ ! -f "${ELC_SRC}" ]]; then
|
||||
echo "Error: elc binary not found at ${ELC_SRC}" >&2
|
||||
echo "Build it first or run from the el repo root." >&2
|
||||
exit 1
|
||||
fi
|
||||
install -m 755 "${ELC_SRC}" "${BIN_DIR}/elc"
|
||||
echo " installed: ${BIN_DIR}/elc"
|
||||
|
||||
# 2. Install runtime .el files
|
||||
for f in "${STDLIB_SRC}"/*.el; do
|
||||
[[ -f "$f" ]] || continue
|
||||
install -m 644 "$f" "${RUNTIME_DIR}/$(basename "$f")"
|
||||
done
|
||||
echo " installed: ${RUNTIME_DIR}/*.el ($(ls "${STDLIB_SRC}"/*.el | wc -l | tr -d ' ') files)"
|
||||
|
||||
# 3. Install headers
|
||||
for header in el_seed.h el_runtime.h; do
|
||||
src="${RUNTIME_SRC}/${header}"
|
||||
if [[ -f "${src}" ]]; then
|
||||
install -m 644 "${src}" "${INCLUDE_DIR}/${header}"
|
||||
echo " installed: ${INCLUDE_DIR}/${header}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. Build libel.a from el_seed.c + el_runtime.c
|
||||
echo " compiling libel.a..."
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||
|
||||
cc -std=c11 -O2 -I"${RUNTIME_SRC}" -c "${RUNTIME_SRC}/el_seed.c" -o "${TMP_DIR}/el_seed.o"
|
||||
cc -std=c11 -O2 -I"${RUNTIME_SRC}" -c "${RUNTIME_SRC}/el_runtime.c" -o "${TMP_DIR}/el_runtime.o"
|
||||
ar rcs "${LIB_DIR}/libel.a" "${TMP_DIR}/el_seed.o" "${TMP_DIR}/el_runtime.o"
|
||||
echo " installed: ${LIB_DIR}/libel.a"
|
||||
|
||||
# 5. Generate stdlib.el with absolute paths to installed runtime files
|
||||
STDLIB_OUT="${PREFIX}/stdlib.el"
|
||||
{
|
||||
echo "// stdlib.el — El standard library (installed at ${PREFIX})"
|
||||
echo "// Generated by tools/install.sh — do not edit by hand."
|
||||
echo "// Import this file to get the full El runtime:"
|
||||
echo "// import \"${PREFIX}/stdlib.el\""
|
||||
echo
|
||||
for f in "${STDLIB_SRC}"/*.el; do
|
||||
[[ -f "$f" ]] || continue
|
||||
base="$(basename "$f")"
|
||||
# Skip stdlib.el itself to avoid circular import
|
||||
[[ "${base}" == "stdlib.el" ]] && continue
|
||||
# Skip test.el — dev-only
|
||||
[[ "${base}" == "test.el" ]] && continue
|
||||
echo "import \"${RUNTIME_DIR}/${base}\""
|
||||
done
|
||||
} > "${STDLIB_OUT}"
|
||||
echo " installed: ${STDLIB_OUT}"
|
||||
|
||||
echo
|
||||
echo "==> El installed to ${PREFIX}"
|
||||
echo
|
||||
echo "Add ${BIN_DIR} to your PATH:"
|
||||
echo " export PATH=\"${BIN_DIR}:\$PATH\""
|
||||
echo
|
||||
echo "To use the stdlib in your El programs:"
|
||||
echo " import \"${PREFIX}/stdlib.el\""
|
||||
echo
|
||||
echo "To link El programs against the installed runtime:"
|
||||
echo " elc src/main.el > dist/main.c"
|
||||
echo " cc -std=c11 -O2 -I${INCLUDE_DIR} -o dist/main dist/main.c ${LIB_DIR}/libel.a -lcurl -lpthread"
|
||||
echo
|
||||
@@ -0,0 +1,198 @@
|
||||
# El LSP — Language Server for El
|
||||
|
||||
Full Language Server Protocol implementation for the El programming language.
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | Status |
|
||||
|---------|--------|
|
||||
| Syntax highlighting | Full TextMate grammar |
|
||||
| Completions | Builtins (130+), user-defined fns, keywords, types |
|
||||
| Hover | Signatures + descriptions for all builtins and user fns |
|
||||
| Go-to-definition | Jump to `fn name(` in the open document |
|
||||
| Diagnostics | Unclosed braces/parens/brackets, unterminated strings |
|
||||
| Document sync | Full (re-sends entire document on every change) |
|
||||
|
||||
## Building
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- The `elc` compiler binary at `dist/platform/elc` (built from the repo root)
|
||||
- `cc` (clang or gcc), `libcurl`, `pthreads`
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# From the el repo root:
|
||||
./tools/lsp/build.sh
|
||||
|
||||
# Or with a custom elc path:
|
||||
ELC=/path/to/elc ./tools/lsp/build.sh
|
||||
```
|
||||
|
||||
Output: `tools/lsp/dist/el-lsp`
|
||||
|
||||
### Install system-wide
|
||||
|
||||
```bash
|
||||
sudo cp tools/lsp/dist/el-lsp /usr/local/bin/el-lsp
|
||||
```
|
||||
|
||||
## VSCode Extension
|
||||
|
||||
### Development install (recommended)
|
||||
|
||||
1. Build the binary first (see above).
|
||||
2. Install the npm dependency:
|
||||
```bash
|
||||
cd tools/lsp/vscode-extension
|
||||
npm install
|
||||
```
|
||||
3. Open `tools/lsp/vscode-extension/` in VSCode.
|
||||
4. Press **F5** — this launches the Extension Development Host.
|
||||
5. Open any `.el` file in the dev host window.
|
||||
|
||||
### Package as .vsix
|
||||
|
||||
```bash
|
||||
npm install -g @vscode/vsce
|
||||
cd tools/lsp/vscode-extension
|
||||
vsce package
|
||||
# Produces: el-language-1.0.0.vsix
|
||||
```
|
||||
|
||||
Install the .vsix:
|
||||
```bash
|
||||
code --install-extension el-language-1.0.0.vsix
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `el.lspPath` | (bundled) | Path to `el-lsp` binary. Empty = use `../dist/el-lsp`. |
|
||||
| `el.trace.server` | `off` | LSP message tracing. Set to `verbose` to see all messages. |
|
||||
|
||||
## Neovim / other editors
|
||||
|
||||
Any editor that supports LSP can use `el-lsp`. Example configuration for
|
||||
Neovim with `nvim-lspconfig`:
|
||||
|
||||
```lua
|
||||
local lspconfig = require('lspconfig')
|
||||
local configs = require('lspconfig.configs')
|
||||
|
||||
if not configs.el then
|
||||
configs.el = {
|
||||
default_config = {
|
||||
cmd = { 'el-lsp' },
|
||||
filetypes = { 'el' },
|
||||
root_dir = lspconfig.util.root_pattern('.git', '*.el'),
|
||||
settings = {},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
lspconfig.el.setup({})
|
||||
```
|
||||
|
||||
Add to `ftdetect/el.vim`:
|
||||
```vim
|
||||
au BufRead,BufNewFile *.el set filetype=el
|
||||
```
|
||||
|
||||
## Wire protocol
|
||||
|
||||
El LSP communicates over stdin/stdout using standard LSP framing:
|
||||
|
||||
```
|
||||
Content-Length: <N>\r\n
|
||||
\r\n
|
||||
<N bytes of UTF-8 JSON>
|
||||
```
|
||||
|
||||
The `__read_n(n: Int) -> String` primitive in `el_runtime.c` reads exactly
|
||||
`n` bytes from stdin — needed because `readline()` stops at `\n` and LSP
|
||||
JSON bodies are not newline-terminated. `__print_raw(s: String)` writes with
|
||||
`fwrite + fflush` to preserve embedded `\r\n` in headers.
|
||||
|
||||
## Smoke test
|
||||
|
||||
After building, verify the server responds to `initialize`:
|
||||
|
||||
```bash
|
||||
python3 - << 'PY'
|
||||
import subprocess, json
|
||||
|
||||
def frame(obj):
|
||||
body = json.dumps(obj).encode()
|
||||
return f"Content-Length: {len(body)}\r\n\r\n".encode() + body
|
||||
|
||||
def read_response(proc):
|
||||
hdr = b""
|
||||
while not hdr.endswith(b"\r\n\r\n"):
|
||||
hdr += proc.stdout.read(1)
|
||||
cl = int([l for l in hdr.decode().split("\r\n") if "Content-Length" in l][0].split(": ")[1])
|
||||
return json.loads(proc.stdout.read(cl))
|
||||
|
||||
proc = subprocess.Popen(["./dist/el-lsp"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
proc.stdin.write(frame({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}))
|
||||
proc.stdin.flush()
|
||||
r = read_response(proc)
|
||||
print("Server name:", r["result"]["serverInfo"]["name"])
|
||||
print("Capabilities:", list(r["result"]["capabilities"].keys()))
|
||||
proc.stdin.write(frame({"jsonrpc":"2.0","method":"exit","params":{}}))
|
||||
proc.stdin.flush()
|
||||
proc.wait()
|
||||
print("OK")
|
||||
PY
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
el-lsp.el
|
||||
lsp_read_message() reads header bytes one-by-one, then __read_n(body_len)
|
||||
lsp_write_message() __print_raw("Content-Length: N\r\n\r\n" + json)
|
||||
lsp_dispatch() routes method string to handler
|
||||
lsp_builtin_catalog() [String] of "name|signature|description" entries
|
||||
lsp_extract_fns() scan source for "fn name(" patterns
|
||||
lsp_word_at() expand identifier under cursor
|
||||
lsp_compute_diagnostics() scan for unclosed brackets + unterminated strings
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
tools/lsp/
|
||||
el-lsp.el LSP server source (El language)
|
||||
build.sh Build script
|
||||
README.md This file
|
||||
dist/
|
||||
el-lsp Compiled binary (after build)
|
||||
el-lsp.c Generated C (after build)
|
||||
vscode-extension/
|
||||
extension.js Extension entry point
|
||||
package.json Extension manifest
|
||||
language-configuration.json Bracket matching, comment config
|
||||
syntaxes/
|
||||
el.tmGrammar.json TextMate syntax grammar
|
||||
.vscode/
|
||||
launch.json F5 debug configuration
|
||||
tasks.json Pre-launch npm install task
|
||||
```
|
||||
|
||||
## Runtime additions
|
||||
|
||||
Two new primitives added to `el-compiler/runtime/`:
|
||||
|
||||
### `__read_n(n: Int) -> String`
|
||||
Reads exactly `n` bytes from stdin using `fread`. Returns `""` on EOF.
|
||||
Required for reading JSON-RPC message bodies.
|
||||
|
||||
### `__print_raw(s: String) -> Void`
|
||||
Writes a string to stdout using `fwrite + fflush`. Preserves embedded
|
||||
`\r\n` bytes exactly. Required for LSP Content-Length headers.
|
||||
|
||||
Both are declared in `el_runtime.h` and registered in the `builtin_arity`
|
||||
table in `el-compiler/src/codegen.el`.
|
||||
BIN
Binary file not shown.
Vendored
+1252
File diff suppressed because it is too large
Load Diff
+1158
File diff suppressed because it is too large
Load Diff
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Extension (Extension Development Host)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "npm: install",
|
||||
"env": {
|
||||
"EL_LSP_LOG": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Attach to el-lsp process",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 6009,
|
||||
"restart": true,
|
||||
"timeout": 10000,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "install",
|
||||
"label": "npm: install",
|
||||
"detail": "Install vscode-languageclient dependency",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// tools/lsp/vscode-extension/extension.js
|
||||
//
|
||||
// El Language VSCode extension — Language Client
|
||||
//
|
||||
// Launches the el-lsp binary as a child process and connects VSCode's
|
||||
// Language Client to it over stdin/stdout (JSON-RPC 2.0 + Content-Length).
|
||||
//
|
||||
// Capabilities provided by el-lsp:
|
||||
// - textDocumentSync (full)
|
||||
// - completionProvider — builtins + user fns + keywords
|
||||
// - hoverProvider — signatures + descriptions
|
||||
// - definitionProvider — go-to-def for user-defined fns
|
||||
// - diagnostics — unclosed braces/parens, unterminated strings
|
||||
//
|
||||
// Setup (development):
|
||||
// 1. cd tools/lsp && ./build.sh (builds dist/el-lsp)
|
||||
// 2. cd vscode-extension && npm install (installs vscode-languageclient)
|
||||
// 3. Open vscode-extension/ in VSCode
|
||||
// 4. Press F5 (launches Extension Development Host)
|
||||
// 5. Open any .el file
|
||||
//
|
||||
// Package as .vsix:
|
||||
// npm install -g @vscode/vsce
|
||||
// cd tools/lsp/vscode-extension && vsce package
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { workspace, window, ExtensionContext } = require('vscode');
|
||||
const {
|
||||
LanguageClient,
|
||||
TransportKind,
|
||||
} = require('vscode-languageclient/node');
|
||||
|
||||
/** @type {LanguageClient | undefined} */
|
||||
let client;
|
||||
|
||||
/**
|
||||
* activate — entry point called when any .el file is first opened.
|
||||
*
|
||||
* @param {ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
// ── Resolve el-lsp binary ────────────────────────────────────────────
|
||||
const config = workspace.getConfiguration('el');
|
||||
|
||||
// Default path resolution (tries in order):
|
||||
// 1. el.lspPath setting (user override)
|
||||
// 2. dist/el-lsp alongside the extension dir (packaged install)
|
||||
// 3. The canonical source-tree location
|
||||
const CANONICAL_LSP = '/Users/will/Development/neuron-technologies/foundation/el/tools/lsp/dist/el-lsp';
|
||||
const defaultLspPath = (() => {
|
||||
const sibling = path.join(context.extensionPath, 'dist', 'el-lsp');
|
||||
if (fs.existsSync(sibling)) return sibling;
|
||||
return CANONICAL_LSP;
|
||||
})();
|
||||
const serverPath = config.get('lspPath') || defaultLspPath;
|
||||
|
||||
if (!fs.existsSync(serverPath)) {
|
||||
window.showErrorMessage(
|
||||
`El Language Server binary not found at: ${serverPath}\n` +
|
||||
`Run tools/lsp/build.sh to build it, then reload VSCode.\n` +
|
||||
`Or set "el.lspPath" in settings to the correct path.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Server options (start el-lsp as child process) ───────────────────
|
||||
/** @type {import('vscode-languageclient/node').ServerOptions} */
|
||||
const serverOptions = {
|
||||
run: {
|
||||
command: serverPath,
|
||||
transport: TransportKind.stdio,
|
||||
options: { env: { ...process.env } },
|
||||
},
|
||||
debug: {
|
||||
command: serverPath,
|
||||
transport: TransportKind.stdio,
|
||||
options: {
|
||||
env: { ...process.env },
|
||||
// Redirect el-lsp stderr to VSCode's Output panel in debug mode.
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ── Client options ───────────────────────────────────────────────────
|
||||
/** @type {import('vscode-languageclient/node').LanguageClientOptions} */
|
||||
const clientOptions = {
|
||||
// Activate for all .el files (file:// and untitled: schemes).
|
||||
documentSelector: [
|
||||
{ scheme: 'file', language: 'el' },
|
||||
{ scheme: 'untitled', language: 'el' },
|
||||
],
|
||||
synchronize: {
|
||||
// Fire fileEvents so the client resends on disk changes outside VSCode.
|
||||
fileEvents: workspace.createFileSystemWatcher('**/*.el'),
|
||||
},
|
||||
outputChannelName: 'El Language Server',
|
||||
// Trace LSP messages to the Output panel for debugging.
|
||||
// Set "el.trace.server": "verbose" in settings to enable.
|
||||
traceOutputChannel: window.createOutputChannel('El LSP Trace'),
|
||||
};
|
||||
|
||||
// ── Create and start client ──────────────────────────────────────────
|
||||
client = new LanguageClient(
|
||||
'el-language-server',
|
||||
'El Language Server',
|
||||
serverOptions,
|
||||
clientOptions,
|
||||
);
|
||||
|
||||
// Register the client so it is disposed when the extension deactivates.
|
||||
context.subscriptions.push(client);
|
||||
|
||||
client.start().then(() => {
|
||||
// Show a discrete status bar item while the server is active.
|
||||
const status = window.createStatusBarItem(1);
|
||||
status.text = '$(symbol-misc) El LSP';
|
||||
status.tooltip = 'El Language Server is running';
|
||||
status.command = 'el.restartServer';
|
||||
status.show();
|
||||
context.subscriptions.push(status);
|
||||
}).catch((err) => {
|
||||
window.showErrorMessage(`El Language Server failed to start: ${err.message}`);
|
||||
});
|
||||
|
||||
// ── Commands ─────────────────────────────────────────────────────────
|
||||
context.subscriptions.push(
|
||||
require('vscode').commands.registerCommand('el.restartServer', () => {
|
||||
if (client) {
|
||||
client.stop().then(() => client.start());
|
||||
window.showInformationMessage('El Language Server restarted.');
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* deactivate — called when the extension is unloaded.
|
||||
* Sends LSP shutdown + exit to the server process.
|
||||
*/
|
||||
function deactivate() {
|
||||
if (!client) {
|
||||
return undefined;
|
||||
}
|
||||
return client.stop();
|
||||
}
|
||||
|
||||
module.exports = { activate, deactivate };
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"comments": {
|
||||
"lineComment": "//"
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "\"", "close": "\"", "notIn": ["string"] }
|
||||
],
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""]
|
||||
],
|
||||
"indentationRules": {
|
||||
"increaseIndentPattern": "\\{[^}]*$",
|
||||
"decreaseIndentPattern": "^\\s*}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"name": "el-language",
|
||||
"displayName": "El Language",
|
||||
"description": "El language support \u2014 syntax highlighting, completions, hover, go-to-definition, and diagnostics",
|
||||
"version": "1.0.0",
|
||||
"publisher": "neuron-technologies",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/neuron-technologies/foundation"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
"Linters",
|
||||
"Other"
|
||||
],
|
||||
"keywords": [
|
||||
"el",
|
||||
"el-lang",
|
||||
"language-server",
|
||||
"lsp",
|
||||
"neuron"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage:el"
|
||||
],
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "el",
|
||||
"aliases": [
|
||||
"El",
|
||||
"el-lang"
|
||||
],
|
||||
"extensions": [
|
||||
".el",
|
||||
".elh"
|
||||
],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "el",
|
||||
"scopeName": "source.el",
|
||||
"path": "./syntaxes/el.tmGrammar.json"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "El Language",
|
||||
"properties": {
|
||||
"el.lspPath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Absolute path to the el-lsp binary. Leave empty to use the binary bundled with the extension (../dist/el-lsp)."
|
||||
},
|
||||
"el.trace.server": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"off",
|
||||
"messages",
|
||||
"verbose"
|
||||
],
|
||||
"default": "off",
|
||||
"description": "Trace LSP messages between VSCode and el-lsp (visible in Output \u2192 El LSP Trace)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "el.restartServer",
|
||||
"title": "El: Restart Language Server"
|
||||
}
|
||||
]
|
||||
},
|
||||
"main": "./extension.js",
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.75.0",
|
||||
"@vscode/vsce": "^2.22.0"
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "echo 'no transpile step required'",
|
||||
"package": "vsce package",
|
||||
"install-dev": "npm install && code --install-extension el-language-1.0.0.vsix 2>/dev/null || true"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "El",
|
||||
"scopeName": "source.el",
|
||||
"fileTypes": ["el"],
|
||||
"patterns": [
|
||||
{ "include": "#comments" },
|
||||
{ "include": "#strings" },
|
||||
{ "include": "#function-definition" },
|
||||
{ "include": "#builtin-functions" },
|
||||
{ "include": "#keywords-control" },
|
||||
{ "include": "#keywords-declaration" },
|
||||
{ "include": "#keywords-other" },
|
||||
{ "include": "#types" },
|
||||
{ "include": "#type-annotations" },
|
||||
{ "include": "#constants" },
|
||||
{ "include": "#numbers" },
|
||||
{ "include": "#operators" },
|
||||
{ "include": "#range-operator" },
|
||||
{ "include": "#function-call" },
|
||||
{ "include": "#identifiers" }
|
||||
],
|
||||
"repository": {
|
||||
|
||||
"comments": {
|
||||
"name": "comment.line.double-slash.el",
|
||||
"match": "//.*$"
|
||||
},
|
||||
|
||||
"strings": {
|
||||
"name": "string.quoted.double.el",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.el",
|
||||
"match": "\\\\[nrt\\\\\"'0]"
|
||||
},
|
||||
{
|
||||
"name": "constant.character.escape.hex.el",
|
||||
"match": "\\\\x[0-9a-fA-F]{2}"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"function-definition": {
|
||||
"comment": "fn keyword + function name — name gets entity.name.function",
|
||||
"match": "\\b(fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()",
|
||||
"captures": {
|
||||
"1": { "name": "keyword.declaration.function.el" },
|
||||
"2": { "name": "entity.name.function.el" }
|
||||
}
|
||||
},
|
||||
|
||||
"builtin-functions": {
|
||||
"comment": "Well-known El builtins get support.function scope for distinct colour",
|
||||
"name": "support.function.builtin.el",
|
||||
"match": "\\b(println|print|readline|str_eq|str_len|str_concat|str_slice|str_contains|str_starts_with|str_ends_with|str_replace|str_to_upper|str_to_lower|str_trim|str_lstrip|str_rstrip|str_index_of|str_last_index_of|str_split|str_split_lines|str_split_n|str_join|str_char_at|str_char_code|str_pad_left|str_pad_right|str_repeat|str_reverse|str_strip_prefix|str_strip_suffix|str_strip_chars|str_count|str_count_lines|str_find_chars|str_upper|str_lower|int_to_str|str_to_int|float_to_str|str_to_float|int_to_float|float_to_int|format_float|decimal_round|bool_to_str|el_abs|el_max|el_min|math_sqrt|math_log|math_ln|math_sin|math_cos|math_pi|el_list_empty|el_list_append|el_list_len|el_list_get|el_list_clone|list_push|list_push_front|list_join|list_range|el_map_get|el_map_set|el_get_field|state_set|state_get|state_del|state_keys|json_get|json_get_string|json_get_int|json_get_float|json_get_bool|json_get_raw|json_set|json_parse|json_stringify|json_array_len|json_array_get|json_array_get_string|fs_read|fs_write|fs_list|fs_exists|fs_mkdir|http_get|http_post|http_post_json|http_get_with_headers|http_post_with_headers|http_serve|http_serve_v2|http_response|url_encode|url_decode|time_now|time_now_utc|sleep_secs|sleep_ms|time_format|time_add|time_diff|now|unix_seconds|unix_millis|uuid_new|uuid_v4|env|args|exit_program|exec_command|exec_capture|sha256_hex|hmac_sha256_hex|base64_encode|base64_decode|base64url_encode|base64url_decode|llm_call|llm_call_system|llm_call_agentic|llm_vision|llm_models|llm_register_tool|engram_node|engram_get_node|engram_strengthen|engram_forget|engram_search|engram_connect|engram_activate|engram_save|engram_load|engram_node_count|engram_edge_count|engram_neighbors|engram_search_json|engram_stats_json|dharma_connect|dharma_send|dharma_activate|dharma_emit|dharma_field|dharma_peers|native_list_empty|native_list_append|native_list_len|native_list_get|native_list_clone|native_string_chars|native_int_to_str)\\b(?=\\s*\\()"
|
||||
},
|
||||
|
||||
"keywords-control": {
|
||||
"name": "keyword.control.el",
|
||||
"match": "\\b(if|else|while|for|in|return|match|break|continue)\\b"
|
||||
},
|
||||
|
||||
"keywords-declaration": {
|
||||
"name": "keyword.declaration.el",
|
||||
"match": "\\b(fn|let|type|enum|import|from|as|extern)\\b"
|
||||
},
|
||||
|
||||
"keywords-other": {
|
||||
"name": "keyword.other.el",
|
||||
"match": "\\b(cgi|vessel|activate|where|sealed|with|test|seed|assert|protocol|impl|retry|times|fallback|reason|parallel|trace|requires|deploy|to|via|target|manager|engine|accessor)\\b"
|
||||
},
|
||||
|
||||
"types": {
|
||||
"comment": "Built-in El primitive types",
|
||||
"name": "support.type.primitive.el",
|
||||
"match": "\\b(String|Int|Float|Bool|Void|Any|Instant|Duration|Map|List)\\b"
|
||||
},
|
||||
|
||||
"type-annotations": {
|
||||
"comment": "Highlight type annotations after : in let and fn params",
|
||||
"patterns": [
|
||||
{
|
||||
"match": ":\\s*([A-Z][a-zA-Z0-9_<>, ]*)(?=\\s*[,)={\\]]|\\s*->)",
|
||||
"captures": {
|
||||
"1": { "name": "support.type.el" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "->\\s*([A-Z][a-zA-Z0-9_<>, ]*)",
|
||||
"captures": {
|
||||
"0": { "name": "keyword.operator.arrow.el" },
|
||||
"1": { "name": "support.type.return.el" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"constants": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.language.boolean.el",
|
||||
"match": "\\b(true|false)\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.language.null.el",
|
||||
"match": "\\bnull\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"numbers": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.numeric.float.el",
|
||||
"match": "-?\\b[0-9]+\\.[0-9]+\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.numeric.integer.el",
|
||||
"match": "-?\\b[0-9]+\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"range-operator": {
|
||||
"name": "keyword.operator.range.el",
|
||||
"match": "\\.\\."
|
||||
},
|
||||
|
||||
"operators": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.operator.comparison.el",
|
||||
"match": "(==|!=|<=|>=)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.comparison.el",
|
||||
"match": "(?<![<>-])([<>])(?![>=])"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.logical.el",
|
||||
"match": "(&&|\\|\\||!(?!=))"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.assignment.el",
|
||||
"match": "(?<![=!<>])=(?!=)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.arithmetic.el",
|
||||
"match": "[+\\-*/%]"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.arrow.el",
|
||||
"match": "->"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"function-call": {
|
||||
"comment": "Function calls: identifier immediately followed by (",
|
||||
"match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()",
|
||||
"captures": {
|
||||
"1": { "name": "entity.name.function.call.el" }
|
||||
}
|
||||
},
|
||||
|
||||
"identifiers": {
|
||||
"comment": "Catch-all for remaining identifiers",
|
||||
"name": "variable.other.el",
|
||||
"match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b"
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
# new-project.sh — scaffold a new El project.
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/new-project.sh <project-name>
|
||||
#
|
||||
# Creates:
|
||||
# <project-name>/
|
||||
# src/main.el — hello world entry point
|
||||
# build.sh — build script using elc + el_runtime.c
|
||||
# README.md — minimal project docs
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <project-name>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NAME="$1"
|
||||
|
||||
if [[ -e "${NAME}" ]]; then
|
||||
echo "Error: '${NAME}' already exists." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Discover elc — prefer PATH, fall back to the local foundation/el tree
|
||||
ELC="elc"
|
||||
if ! command -v elc >/dev/null 2>&1; then
|
||||
# Try a sibling or parent path convention
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOCAL_ELC="${SCRIPT_DIR}/../dist/platform/elc"
|
||||
if [[ -x "${LOCAL_ELC}" ]]; then
|
||||
ELC="$(cd "$(dirname "${LOCAL_ELC}")" && pwd)/$(basename "${LOCAL_ELC}")"
|
||||
else
|
||||
echo "Warning: elc not found in PATH or at ${LOCAL_ELC}" >&2
|
||||
echo " The generated build.sh may need manual adjustment." >&2
|
||||
ELC="elc"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Discover el_runtime.c
|
||||
EL_RUNTIME=""
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOCAL_RUNTIME="${SCRIPT_DIR}/../el-compiler/runtime/el_runtime.c"
|
||||
if [[ -f "${LOCAL_RUNTIME}" ]]; then
|
||||
EL_RUNTIME="$(cd "$(dirname "${LOCAL_RUNTIME}")" && pwd)/$(basename "${LOCAL_RUNTIME}")"
|
||||
EL_INCLUDE="$(dirname "${EL_RUNTIME}")"
|
||||
else
|
||||
# Check installed location
|
||||
if [[ -f "/usr/local/el/lib/libel.a" ]]; then
|
||||
EL_RUNTIME_LINK="-L/usr/local/el/lib -lel"
|
||||
EL_INCLUDE="/usr/local/el/include"
|
||||
fi
|
||||
EL_RUNTIME="${EL_RUNTIME_LINK:-}"
|
||||
fi
|
||||
|
||||
echo "==> Scaffolding El project: ${NAME}"
|
||||
|
||||
mkdir -p "${NAME}/src" "${NAME}/dist"
|
||||
|
||||
# src/main.el
|
||||
cat > "${NAME}/src/main.el" <<'ELEOF'
|
||||
// main.el — entry point for this El program.
|
||||
|
||||
fn run() -> String {
|
||||
println("hello from El!")
|
||||
return ""
|
||||
}
|
||||
|
||||
run()
|
||||
ELEOF
|
||||
|
||||
# build.sh — adapts to whether we found elc/runtime locally or installed
|
||||
if [[ -n "${EL_RUNTIME}" && -f "${EL_RUNTIME}" ]]; then
|
||||
# Local source tree runtime
|
||||
EL_INCLUDE_DIR="$(dirname "${EL_RUNTIME}")"
|
||||
cat > "${NAME}/build.sh" <<BUILDEOF
|
||||
#!/usr/bin/env bash
|
||||
# build.sh — build ${NAME}
|
||||
set -euo pipefail
|
||||
|
||||
ELC="${ELC}"
|
||||
EL_RUNTIME="${EL_RUNTIME}"
|
||||
EL_INCLUDE="${EL_INCLUDE_DIR}"
|
||||
|
||||
mkdir -p dist
|
||||
|
||||
echo "==> Compiling El -> C"
|
||||
"\${ELC}" src/main.el > dist/main.c
|
||||
|
||||
echo "==> Compiling C -> binary"
|
||||
cc -std=c11 -O2 -I"\${EL_INCLUDE}" -o dist/${NAME} dist/main.c "\${EL_RUNTIME}" -lcurl -lpthread
|
||||
|
||||
echo "==> Built: dist/${NAME}"
|
||||
BUILDEOF
|
||||
else
|
||||
# Installed framework
|
||||
cat > "${NAME}/build.sh" <<BUILDEOF
|
||||
#!/usr/bin/env bash
|
||||
# build.sh — build ${NAME}
|
||||
# Requires El installed via tools/install.sh (elc in PATH, /usr/local/el/lib/libel.a)
|
||||
set -euo pipefail
|
||||
|
||||
ELC="\${ELC:-elc}"
|
||||
EL_PREFIX="\${EL_HOME:-/usr/local/el}"
|
||||
|
||||
mkdir -p dist
|
||||
|
||||
echo "==> Compiling El -> C"
|
||||
"\${ELC}" src/main.el > dist/main.c
|
||||
|
||||
echo "==> Compiling C -> binary"
|
||||
cc -std=c11 -O2 -I"\${EL_PREFIX}/include" -o dist/${NAME} dist/main.c -L"\${EL_PREFIX}/lib" -lel -lcurl -lpthread
|
||||
|
||||
echo "==> Built: dist/${NAME}"
|
||||
BUILDEOF
|
||||
fi
|
||||
|
||||
chmod +x "${NAME}/build.sh"
|
||||
|
||||
# README.md
|
||||
cat > "${NAME}/README.md" <<READMEEOF
|
||||
# ${NAME}
|
||||
|
||||
An El language project.
|
||||
|
||||
## Build
|
||||
|
||||
\`\`\`bash
|
||||
./build.sh
|
||||
\`\`\`
|
||||
|
||||
## Run
|
||||
|
||||
\`\`\`bash
|
||||
./dist/${NAME}
|
||||
\`\`\`
|
||||
|
||||
## Structure
|
||||
|
||||
\`\`\`
|
||||
src/main.el — entry point
|
||||
build.sh — build script
|
||||
dist/ — compiled output (gitignored)
|
||||
\`\`\`
|
||||
READMEEOF
|
||||
|
||||
# .gitignore
|
||||
cat > "${NAME}/.gitignore" <<'IGNEOF'
|
||||
dist/
|
||||
*.c
|
||||
IGNEOF
|
||||
|
||||
echo
|
||||
echo "==> Created ${NAME}/"
|
||||
echo " src/main.el"
|
||||
echo " build.sh"
|
||||
echo " README.md"
|
||||
echo " .gitignore"
|
||||
echo
|
||||
echo "To build:"
|
||||
echo " cd ${NAME} && ./build.sh"
|
||||
echo
|
||||
Reference in New Issue
Block a user