Compare commits

..

50 Commits

Author SHA1 Message Date
Will Anderson e8b22e16a2 add el-tests vessel: manifest-based test suite under tests/suite/
Restructures the test suite as a proper El vessel with manifest.el and
src/ layout, eliminating the bash run.sh harness. CI runs the suite with
two commands: `cd tests/suite && elb && ./dist/el-tests`. Exit code is
the fail count (0 = all pass).

163 test cases across 7 modules: string (52), math (13), json (26),
state (11), time (25), fs (16), collections (19).
2026-05-04 19:53:35 -05:00
Will Anderson 3086a56b5c enforce source branch in CI: stage←dev, main←stage 2026-05-04 19:34:34 -05:00
Will Anderson 7c644b8d89 Add dev/stage CI pipelines and expand sdk-release to full prod pipeline
- Add ci-dev.yaml: builds elc gen2→gen3, runs 4 test suites, publishes
  el/elc + el_runtime.c + el_runtime.h to foundation-dev Artifact Registry
- Add ci-stage.yaml: same as dev but targets foundation-stage registry
- Update sdk-release.yaml: publish 3 SDK artifacts to foundation-prod
  Artifact Registry after Gitea release; expand dispatch list from 2 to 6
  downstream repos (el-ui, elp, elql, el-ide added alongside engram/forge)
2026-05-04 19:32:05 -05:00
Will Anderson 9e8d23bcd9 add epm — El Package Manager
Introduces epm/, a new component written entirely in native El.
epm manages vessels (El's deployable package format): publish to Engram,
install with full dependency resolution, list registry contents, and
inspect vessel metadata.

- epm/manifest.el         — package manifest
- epm/src/manifest.el     — vessel/package manifest parser (line-by-line,
                            same approach as elb.el)
- epm/src/registry.el     — Engram-backed vessel registry (POST /api/nodes,
                            GET /api/search); vessels stored as Entity nodes
                            with label "vessel:<name>:<version>"
- epm/src/install.el      — topological dependency resolver with cycle
                            detection; installs to .epm/vessels/<name>/
- epm/src/epm.el          — main entry point: publish / install / list / info
2026-05-04 19:31:24 -05:00
Will Anderson 0791fda43e elb: add -lm to link flags — el_runtime.c uses math.h functions
el_runtime.c includes <math.h> and calls pow(), sqrt(), log() in several
places (math operations, engram dampening, float formatting). Without -lm
the linker fails on Linux when linking programs built with elb.
2026-05-04 19:14:09 -05:00
Will Anderson 53f2df500d runtime: add dharma-required functions to el_runtime.c and runtime/*.el
Add the following functions that dharma registry calls but were missing
from the El runtime:

el_runtime.c (consumed by the old build system via released SDK):
  - list_len, list_get — aliases for el_list_len/el_list_get (handlers.el)
  - json_array_push — append pre-encoded element to JSON array string
  - now_millis, unix_timestamp_ms, time_now_ms — ms-since-epoch aliases
  - log_info, log_warn — structured stderr log helpers
  - config — reads config from environment (alias for getenv)
  - http_patch — HTTP PATCH with Content-Type: application/json
  - 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 string as JSON byte array [72,101,...]
  - bytes_to_str — decode JSON byte array back to string
  - hash_sha256 — SHA-256 hex digest using built-in sha256 impl

runtime/*.el (consumed by the new build system):
  - http.el: http_patch, http_post_engram, http_get_engram
  - time.el: now_millis, unix_timestamp_ms, time_now_ms
  - env.el: config, log_info, log_warn, list_len, list_get
  - json.el: json_array_push, bytes_to_str
  - string.el: str_to_bytes, hash_sha256 (via __sha256_hex seed)

el_seed.h / el_seed.c:
  - __sha256_hex primitive with self-contained SHA-256 implementation
2026-05-04 19:07:08 -05:00
Will Anderson 245eb2898e runtime: declare __thread_create and __thread_join in header for C99 compliance 2026-05-03 18:00:47 -05:00
Will Anderson 7bfe30b767 elb: raise clang bracket depth to 1024 — fixes compile failure for large JS string renders 2026-05-03 17:37:56 -05:00
Will Anderson 6ede9e4379 runtime: restore el_runtime.c as build shim; fix el_seed.h self-contained types
el_runtime.c was deleted prematurely — elb still resolves the runtime at build time
via a hardcoded relative path, and the elc code generator still emits
#include "el_runtime.h" in generated C.

Restoring el_runtime.c + el_runtime.h as the working build runtime until the
compiler is updated to emit #include "el_seed.h" and link against el_seed.c
directly.

el_seed.h: remove #include "el_runtime.h" that broke after el_runtime.h deletion;
add inline el_val_t typedef + macros + float cast helpers so el_seed.h is fully
self-contained.
2026-05-03 17:35:14 -05:00
Will Anderson 4ae42ee7db runtime: native SSE streaming — http_sse_open/send/close
Add Server-Sent Events support to the El runtime. El v2 handlers can now
hold HTTP connections open and push events in real time.

New builtins in el_seed.c:
  __http_conn_fd()          — retrieve raw fd from thread-local set by worker
  __http_sse_open(fd)       — send SSE headers (text/event-stream), keep-alive
  __http_sse_send(fd, data) — write "data: <data>\n\n" frame
  __http_sse_close(fd)      — close the connection fd

http_worker_v2 in legacy/el_runtime.c now:
  - stashes the fd via el_seed_set_http_conn_fd() before calling the handler
  - detects the "__sse__" sentinel return value to skip http_send_response
    and skip close(fd) — SSE handler took ownership of the fd
  - clears the thread-local after the handler returns

El wrappers added to runtime/http.el:
  http_conn_fd() http_sse_open(fd) http_sse_send(fd, data)
  http_sse_close(fd) http_sse_sentinel()
2026-05-03 17:15:37 -05:00
Will Anderson 3e5130e98d remove el_runtime.c — runtime is 100% native El
el_runtime.c and el_runtime.h removed from the active runtime directory
(archived copies remain in el-compiler/runtime/legacy/).
tools/lsp/build.sh removed as it depended on el_runtime.c directly.
AGENTS.md updated to reflect el_seed.c as the sole C dependency.
2026-05-03 17:10:04 -05:00
Will Anderson beb4e436e1 archive el_runtime.c — native El runtime complete, seed is self-contained
el_seed.c now defines el_request_start/el_request_end directly (delegating
to its own seed arena) rather than declaring them as externs from el_runtime.c.
Header comment updated to reflect self-contained build.
2026-05-03 17:08:49 -05:00
Will Anderson 71a1e41f93 lsp: type-aware field completions — scan type/let/param decls, complete on dot-access
When a document is opened or changed, scan for `type Name { field: Type }` blocks
and `let var: Type` / `fn foo(param: Type)` annotations. On completion requests,
if the text before the cursor ends with `identifier.`, look up the variable's type
and return its fields as Field (kind=5) completion items instead of the full list.
2026-05-03 16:16:11 -05:00
Will Anderson 0676725cb7 Bundle El runtime as installable framework
- runtime/stdlib.el: master import file for the full El standard library
  in correct dependency order (string→math→time→env→fs→exec→json→http→
  state→thread→channel→engram→manifest); test.el excluded (dev-only)

- tools/install.sh: installs El to a prefix (default /usr/local/el);
  copies elc binary, runtime .el files, headers, compiles libel.a from
  el_seed.c + el_runtime.c, generates an installed stdlib.el with absolute
  paths

- tools/new-project.sh: scaffolds a new El project with src/main.el,
  build.sh (auto-discovers local elc/runtime), README.md, .gitignore;
  verified working end-to-end

- AGENTS.md: fix elc rebuild docs — elc writes to stdout, not to its second
  argument; correct usage is ./dist/platform/elc elc-cli.el > elc-new.c
2026-05-03 16:04:26 -05:00
Will Anderson d98a968f89 rebuild elc — add __read_n and __print_raw for LSP stdin framing (self-hosting verified) 2026-05-03 16:03:34 -05:00
Will Anderson 0c9154551f merge tools/lsp (full) — 184 completions, hover, go-to-def, live diagnostics, enhanced VSCode extension 2026-05-03 16:01:11 -05:00
Will Anderson 9aa0c49d0c add full El LSP — completions, hover, go-to-def, diagnostics, VSCode extension 2026-05-03 15:59:42 -05:00
Will Anderson 59b96e3324 rebuild elc from sprint-integrated source — all language features verified
Self-hosting sprint merges compiled in:
- compiler: break/continue, % modulo, for-range loops, match-stmt codegen, string interpolation
- runtime: threading, http, json, fs/exec/env, time/math/state, engram, string (57 fns), channels
- seed: el_seed.c minimal C OS boundary (968 lines)
- tools: LSP skeleton with JSON-RPC, completions, hover, go-to-def, VSCode extension
- tests: El test framework with state-backed runner + string test suite

Self-hosting verified: elc-new → C → binary → same C output (idempotent).
Provenance snapshot: dist/platform/elc.20260503-1555-post-sprint
2026-05-03 15:55:50 -05:00
Will Anderson 287b39f8e6 merge tools/lsp — El LSP skeleton: JSON-RPC, completions, hover, go-to-def, VSCode extension 2026-05-03 15:53:39 -05:00
Will Anderson c45744d8ca merge runtime/seed — el_seed.c minimal C OS boundary (968 lines) 2026-05-03 15:52:21 -05:00
Will Anderson 2e778ca664 merge compiler/string-interp — string interpolation via lexer desugaring 2026-05-03 15:52:21 -05:00
Will Anderson 641227a7d3 merge runtime/channels — MPMC buffered channels, channel_pipeline, channel_fan_out 2026-05-03 15:52:21 -05:00
Will Anderson 805318e2d0 merge runtime/test — El test framework with state-backed runner and string test suite 2026-05-03 15:52:21 -05:00
Will Anderson cefff5b891 add El LSP skeleton — language server, VSCode extension, syntax highlighting
Implements a Language Server Protocol server for El files over stdin/stdout
JSON-RPC. Provides completions (builtins + keywords + document functions),
hover documentation, go-to-definition, and full document sync.

Also adds a VSCode extension that launches the binary as a child process and
a TextMate grammar for .el syntax highlighting.

NOTE: el-lsp.el calls __read_n(n: Int) -> String, a seed primitive not yet
in el_runtime.c. Build.sh documents the required C implementation; the seed
agent must add it before the binary will link.
2026-05-03 15:52:10 -05:00
Will Anderson ce9a2caff4 add string interpolation to El ("hello ${name}")
Lexer gains scan_interp_string which replaces scan_string in the main
lex loop. When no ${ is found it behaves identically to before (single
Str token). When interpolations are present it emits a flat token
sequence — Str, Plus, (expr tokens), Plus, Str, … — that the existing
parse_binop / cg_expr BinOp-Plus-string path assembles into nested
el_str_concat calls with zero parser or codegen changes.

Key design choices:
- scan_interp_brace tracks { depth so fn(a, b) inside ${} is safe
- inner expr tokens are wrapped in ( ) so operators like + in ${n+1}
  do not associate with the surrounding concat Plus tokens
- \$ escapes to a literal dollar sign; bare $ not before { passes through
- empty ${} emits an empty string segment
2026-05-03 15:50:23 -05:00
Will Anderson d1af4b0f8b add channels to El — buffered MPMC channel with send/recv/close
Introduces Go-style channels as El's mid-flight communication primitive,
completing the threading model: threads can now not only spawn/join but
also communicate while running.

Part 1 — seed layer (el_runtime.c / el_runtime.h):
- Add __thread_create/__thread_join/__mutex_new/__mutex_lock/__mutex_unlock
  as C seed primitives (dlsym-based thread dispatch, pthread mutex table)
- Add __channel_new/__channel_send/__channel_recv/__channel_try_recv/__channel_close
  as MPMC channel seed primitives backed by mutex + condvar + circular buffer
- Bounded channels (cap > 0): circular buffer, sender blocks when full
- Unbounded channels (cap == 0): dynamic array, grows on demand, never blocks
- channel_close wakes all blocked recvers/senders; recv drains then returns ""

Part 2 — El API (runtime/channel.el):
- channel_new/send/recv/try_recv/close — thin wrappers over seed layer
- channel_pipeline — spawn N worker threads reading from in_ch, applying
  fn_name, writing to out_ch; workers exit on "" sentinel from close
- channel_drain — collect all messages from a closed channel into [String]
- channel_fan_out — send a [String] list into a channel then close it

Part 3 — codegen.el:
- Register all 10 seed builtins (__thread_* + __channel_*) in builtin_arity
  so the arity checker validates call sites at compile time
2026-05-03 15:50:13 -05:00
Will Anderson 282df712a8 add runtime/test.el — El test framework with assertions and runner
Provides assert_true/false, assert_eq, assert_int_eq, assert_neq,
assert_contains, assert_starts_with, assert_ends_with, and fail.
Test cases are registered by name+fn_name, executed sequentially via
the thread/dlsym dispatch mechanism, with results tracked in state_.
Includes tests/runtime/string_test.el covering all 23 string.el exports.
2026-05-03 15:49:01 -05:00
Will Anderson cfcedff7f4 implement match statement codegen in El
Add cg_match_stmt() to lower match-as-statement to proper C if/else if/else
chains. Previously, match in statement position fell through to cg_expr() which
emitted a GCC statement-expression — fine for expression arms but wrong for the
statement form. Now matched using the same dispatch pattern as If and For in the
Expr handler of cg_stmt().

Pattern dispatch mirrors cg_match (expression form):
  LitStr  -> str_eq(subj, EL_STR("..."))
  LitInt  -> subj == N
  LitBool -> subj == 1 / 0
  Binding -> else { el_val_t name = subj; body; }
  Wildcard -> else { body; }

Subject is evaluated once into a scoped temporary to avoid double evaluation.
2026-05-03 15:47:50 -05:00
Will Anderson eab483ed4f add el_seed.c — minimal C OS boundary for El runtime migration
Introduces el_seed.c / el_seed.h as the clean OS-boundary layer for new-generation
El programs. All public symbols use the __ prefix convention; el_val_t (int64_t) is
the universal value type throughout.

Key additions over el_runtime.c:
- __thread_create / __thread_join: pthreads + dlsym(RTLD_DEFAULT) parallelism
  foundation. Static ElThread table (64 slots); worker resolves El fn symbols at
  runtime, stores result string for join to return.
- __mutex_new / __mutex_lock / __mutex_unlock: pooled pthread_mutex_t handles
- __http_do: unified curl call with JSON headers string (vs ElMap) and explicit
  timeout_ms parameter
- __fs_list_raw: returns newline-separated filename string (not ElList)
- __str_char_at: returns Int byte value (not single-char String)
- __args_json: CLI args as JSON array string; seeded by el_seed_init_args()

JSON, state, engram, HTML/URL, serve — thin __ wrappers over el_runtime.c.
Private seed arena (parallel to el_runtime.c arena) for standalone use.
2026-05-03 15:45:52 -05:00
Will Anderson cfa4301026 merge compiler features: break/continue, % modulo, for-range loops 2026-05-03 15:45:45 -05:00
Will Anderson f271f9d9d8 add for-range loops to El (for i in 0..n)
Adds `for i in start..end` (exclusive) and `for i in start..=end`
(inclusive) range loop syntax. Existing `for item in list` iteration
is preserved; the parser branches on DotDot/DotDotEq presence after
the start expression. Lexer adds DotDot and DotDotEq tokens with
longer-match-first priority. Codegen emits a C `for` loop with the
loop variable scoped to the statement; inclusive uses `<=`, exclusive `<`.
2026-05-03 15:44:58 -05:00
Will Anderson 49a8a1c24b add % modulo operator to El lexer, parser, codegen
Lexer and parser already had Percent token and precedence on the
compiler/string-interp branch. This commit adds the missing is_int_expr
case for Percent so that modulo expressions over Int operands are
correctly typed as Int (enabling arithmetic dispatch rather than
falling through to string concat or untyped paths).

binop_to_c already mapped Percent -> % at HEAD; only is_int_expr
needed the Percent arm.
2026-05-03 15:44:19 -05:00
Will Anderson 252ad04c96 add break and continue statements to El 2026-05-03 15:43:20 -05:00
Will Anderson 3cc9b1cc3d merge runtime/util — time, math, state in El 2026-05-03 15:40:55 -05:00
Will Anderson 5678745381 add runtime/time.el, math.el, state.el — time, math, and state in El
Migrates the time, math/float, and in-process state surfaces from
el-compiler/runtime/legacy/el_runtime.c to self-hosted El source:

- runtime/time.el: time_now, sleep_secs/ms, time_to_parts (via pure-El
  Gregorian civil_from_days decomposition), time_format (ISO + strftime
  subset), time_add, time_diff, time_from_parts; full Instant/Duration
  nanosecond API (now, unix_seconds/millis, duration_seconds/millis,
  instant_to_iso8601, sleep_duration); TTL cache (ttl_cache_set/get/age
  backed by state); uuid_new / uuid_v4 via __uuid_v4 seed.

- runtime/math.el: el_abs, el_max, el_min (Int); math_sqrt/log/ln/sin/cos/pi
  (Float seed wrappers); float_to_str, int_to_float, float_to_int, str_to_float,
  format_float (__format_float seed), decimal_round (half-away-from-zero via
  pure-El _pow10/_floor_f helpers).

- runtime/state.el: state_set/get/del/keys thin wrappers over __state_* seeds;
  convenience helpers state_has and state_get_or.
2026-05-03 15:39:48 -05:00
Will Anderson e853f4a25c merge runtime/string — all string operations in El (57 functions) 2026-05-03 15:37:51 -05:00
Will Anderson 5b6915ec9e merge runtime/engram-build — engram wrappers, manifest, seed arity table 2026-05-03 15:37:42 -05:00
Will Anderson b0e38a245a merge runtime/fs-exec-env — filesystem, subprocess, environment in El 2026-05-03 15:37:42 -05:00
Will Anderson 84b5355fce merge runtime/json — JSON operations in El 2026-05-03 15:37:41 -05:00
Will Anderson b547d1daf8 merge runtime/http — HTTP client and server in El 2026-05-03 15:37:41 -05:00
Will Anderson cc6522c69d merge runtime/thread — native El threading model, parallel_map 2026-05-03 15:37:41 -05:00
Will Anderson 5807de835e add runtime/string.el — string operations implemented in El
All string, I/O, math, classification, splitting, joining, counting, padding,
and URL encoding functions from el_runtime.c implemented in El using seed
primitives. No C required; compiles via the normal El pipeline.
2026-05-03 15:37:33 -05:00
Will Anderson 33af4ed09e add runtime/engram.el, manifest.el; register seed builtins in codegen arity table
- runtime/engram.el: thin El wrappers over all __engram_* and __generate
  seed primitives (16 functions), matching the el_seed.c API exactly
- runtime/manifest.el: build manifest documenting module load order and
  the cat+compile+cc command for runtime builds
- el-compiler/src/codegen.el: add 77 __-prefix seed primitive entries to
  builtin_arity, covering str, fs, http, thread, exec, env, time, uuid,
  math, state, html, json, and engram seeds
2026-05-03 15:37:05 -05:00
Will Anderson f2c63f95fd add runtime/fs.el, exec.el, env.el — filesystem, subprocess, environment in El
Migrates fs_read/write/exists/mkdir/write_bytes/list, exec/exec_bg/exec_command/exec_capture,
env/args/exit_program, state_set/get/del/keys, uuid_new/v4, and list helpers get/len from
el_runtime.c into El source as thin wrappers over seed primitives.
2026-05-03 15:35:45 -05:00
Will Anderson 01849c2033 add runtime/thread.el — native El threading model with parallel_map
Introduces El's first-class threading primitives built on the seed layer's
__thread_create/__thread_join/mutex ops. parallel_map is the key deliverable:
spawns one thread per item, joins in order — replaces bash fan-out for room
dispatch and any other concurrent HTTP workload.
2026-05-03 15:35:31 -05:00
Will Anderson 56724325ed add runtime/json.el — JSON operations in El
Thin El wrappers over seed JSON primitives (json_get, json_get_raw,
json_parse, json_stringify, json_set, json_array_len, json_array_get,
json_array_get_string) plus typed extractors (json_get_string/int/float/bool)
and pure-El builders (json_build_object, json_build_array,
json_escape_string) that require no seed call.
2026-05-03 15:35:20 -05:00
Will Anderson fea830cca2 add runtime/http.el — HTTP client and server in El
Thin El wrappers over seed primitives that form the public HTTP API for
El programs. Covers GET/POST/DELETE, header-map variants, binary streaming
to file, form-auth, v1/v2 server dispatch, and http_response envelope
construction. Documents two new seed primitives needed: __http_do_map and
__http_do_map_to_file (ElMap-accepting variants to avoid needing map
iteration in El).
2026-05-03 15:35:04 -05:00
Will Anderson f9cfe43f05 preserve original el_runtime.c/h in legacy/ for reference 2026-05-03 15:31:35 -05:00
Will Anderson f97354e96b add exec() and exec_bg() builtins to El runtime
- exec(cmd) -> String: runs shell command, captures stdout, 30s timeout
- exec_bg(cmd) -> String: forks command in background, returns PID string
- add both to codegen arity table (builtin_arity)
- rebuild elc with updated arity table (self-hosting, identity-verified)
- update release snapshot at releases/v1.0.0-20260501/
2026-05-03 02:57:53 -05:00
Will Anderson 9d0e1f64d4 fix elb: cp instead of mv for .elh files, preserves headers for downstream modules 2026-05-03 01:19:58 -05:00
65 changed files with 30352 additions and 289 deletions
+115
View File
@@ -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
+125
View File
@@ -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
+124
View File
@@ -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"
+131
View File
@@ -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
+2080 -154
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+736
View File
@@ -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);
}
+10
View File
@@ -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
+248
View File
@@ -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
+761
View File
@@ -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 StringString).
*
* 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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+25 -15
View File
@@ -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)
}
+17
View File
@@ -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
View File
@@ -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)
}
+226
View File
@@ -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")
}
+175
View File
@@ -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
}
+170
View File
@@ -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
+250 -1
View File
@@ -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
+164
View File
@@ -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)
}
+86
View File
@@ -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
View File
@@ -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()
}
+27
View File
@@ -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)
}
+49
View File
@@ -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
View File
@@ -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 100599 (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
View File
@@ -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 26923333).
//
// 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
}
+34
View File
@@ -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
View File
@@ -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 303305 for el_abs/max/min, lines 47254771 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 303305).
// ---------------------------------------------------------------------------
// 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 47664771).
// ---------------------------------------------------------------------------
// 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 47254762).
// ---------------------------------------------------------------------------
// 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
}
+61
View File
@@ -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 46324721: 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
}
+42
View File
@@ -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"
+907
View File
@@ -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
View File
@@ -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
}
+225
View File
@@ -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
View File
@@ -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 33343440, 34713656) 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 34713656. 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 36633717).
// 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 46024621).
// ---------------------------------------------------------------------------
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
}
+341
View File
@@ -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()
}
+320
View File
@@ -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("{}");
}
+17
View File
@@ -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/"
}
+210
View File
@@ -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()
}
+224
View File
@@ -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 ""
}
+187
View File
@@ -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 ""
}
+277
View File
@@ -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 ""
}
+229
View File
@@ -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 ""
}
+168
View File
@@ -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 ""
}
+575
View File
@@ -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 ""
}
+279
View File
@@ -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 ""
}
+127
View File
@@ -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
+198
View File
@@ -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`.
Vendored Executable
BIN
View File
Binary file not shown.
+1252
View File
File diff suppressed because it is too large Load Diff
+1158
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -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
View File
@@ -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": []
}
]
}
+150
View File
@@ -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*}"
}
}
+93
View File
@@ -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"
}
}
}
+164
View File
@@ -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