Compare commits

...

21 Commits

Author SHA1 Message Date
Will Anderson e858eab300 spec: update codegen-js.md to Phase 5, ~90% coverage
Status updated from Phase 4 ~80% to Phase 5 ~90%.

New sections:
- 7. Language features coverage table (supported vs stubbed)
- 7a. Phase 5 constructs: extern fn, anonymous functions, try/catch,
  method call on Any, URL imports -- each with emit shape examples
- 9. Roadmap updated: Phases 1-5 marked DONE, Phase 6 unblocked

Runtime builtin table updated to ~90 builtins including all Phase 5
additions (promise_then/catch/resolve/reject, object_assign/keys/values,
json_deep_clone, array_from, type_of, instanceof_check).
2026-05-04 11:03:47 -05:00
Will Anderson aa7d97d5ba examples: rewrite browser-auth.el using new language features
No native_js or native_js_call anywhere. Full browser auth flow expressed
with proper El constructs:

- extern fn supabase_create_client(url, key) -> Any
  Declares the Supabase CDN global without an El function body.

- client.auth.signInWithOtp(opts)
  Direct method call chain on Any-typed value. The client is built by
  calling the extern fn; .auth field access and .signInWithOtp(opts)
  method call emit clean JS without any escape hatch.

- try { ... } catch (err: Any) { ... }
  Wraps the auth call; unexpected runtime errors are caught and shown
  to the user rather than crashing silently.

- fn(event: Any) -> Void { ... }
  Inline anonymous function literals for DOM event listeners instead
  of named forward-declared callbacks.

The rewrite is the proof: every browser JavaScript pattern used in a
real auth flow can now be expressed structurally in El.
2026-05-04 11:02:13 -05:00
Will Anderson 7040830470 codegen-js: URL import declarations for JS modules
import "https://cdn.example.com/lib.js" now emits:
  - module mode: import "https://..." at the top of the generated file
  - bundle/IIFE mode: // external: https://... comment

El source imports (.el files) are excluded -- they were already inlined
by resolve_imports before codegen. Any import path that doesn't end in
.el or starts with http(s):// is treated as an external JS dependency.
2026-05-04 11:01:36 -05:00
Will Anderson 3a513aaa5a runtime + codegen-js: Promise helpers and object/array utilities
Add to el_runtime.js:
  promise_then(p, cb)    -- p.then(cb), works with any Promise-returning API
  promise_catch(p, cb)   -- p.catch(cb)
  promise_resolve(val)   -- Promise.resolve(val)
  promise_reject(msg)    -- Promise.reject(new Error(msg))
  object_assign(t, s)    -- Object.assign({}, t, s) (non-mutating)
  object_keys(obj)       -- Object.keys(obj)
  object_values(obj)     -- Object.values(obj)
  json_deep_clone(obj)   -- JSON.parse(JSON.stringify(obj))
  array_from(iterable)   -- Array.from(iterable)
  type_of(val)           -- typeof val
  instanceof_check(v, n) -- val instanceof globalThis[name]

All new functions added to __el export object and ES named exports.
codegen-js preamble destructure updated to include all new names.
2026-05-04 11:01:14 -05:00
Will Anderson beb2a8c5bd lexer + parser + codegen: try/catch statement
try { ... } catch (name: Type) { ... } is now a first-class El statement.

Lexer: `try` and `catch` are now keywords (Try, Catch token kinds).
Parser: TryCatch AST node with try_body, catch_name, catch_body.
codegen-js: emits try { ... } catch (name) { ... } directly -- correct
  for all browser error handling patterns.
codegen.el (C backend): emits the try body with a comment; exception
  handling is a no-op since C has no analogous mechanism. Programs using
  try/catch should compile with --target=js.

The catch variable type annotation is parsed and skipped (same treatment
as all other type annotations in El).
2026-05-04 11:00:24 -05:00
Will Anderson e23319fe0b parser + codegen-js: anonymous function literals (lambda syntax)
fn(params) -> RetType { body } is now valid in expression position.
The parser produces a Lambda AST node. codegen-js emits a hoisted
JS function declaration with a generated name (__lambda_N) and returns
the name as the expression value, so inline callbacks compose cleanly:

  dom_listen(btn, "click", fn(event: Any) -> Void { handle(event) })

emits:

  function __lambda_1(event) { handle(event); }
  dom_listen(btn, "click", __lambda_1);

The hoisted-declaration strategy is debuggable, has no closure-capture
issues, and requires no string-buffer mode in the codegen.
2026-05-04 10:59:17 -05:00
Will Anderson 01fee9396a codegen-js: native JS method dispatch and extern fn support
Any-typed receiver method calls now emit obj.method(args) directly
instead of requiring native_js_call. client.auth.signInWithOtp(p)
compiles to client["auth"].signInWithOtp(p) -- no escape hatch needed.

Field access emits obj["field"] (direct bracket notation) instead of
el_get_field, so prototype-inherited JS properties resolve correctly.
el_get_field's hasOwnProperty guard was silently returning null for
real JS objects with inherited fields (Supabase auth, DOM APIs, etc).

El runtime shortform methods (append, len, get, map_get, map_set)
still use the existing method(obj, args) convention for backward compat.

ExternFn statements emit a comment and are excluded from top-level
statement codegen -- the extern declaration tells the compiler the
function exists in the JS environment without emitting a body.
2026-05-04 10:58:07 -05:00
Will Anderson 7b60d94b8a add --minify and --obfuscate flags to elc JS pipeline
Adds two post-processing flags that produce production-ready browser JS in a
single elc invocation, replacing extract-js.py in the web product pipeline:

  elc --target=js --bundle --minify source.el > output.min.js
  elc --target=js --bundle --obfuscate source.el > output.obf.js

--minify shells out to terser (passes=2, no drop_console, drop_debugger).
--obfuscate shells out to javascript-obfuscator with the same options as the
old extract-js.py script. --obfuscate implies --minify.

Tool discovery: checks ./node_modules/.bin/, ../node_modules/.bin/ (monorepo),
then falls back to npx. Both flags require --target=js; passing either without
it exits 1 with a clear error.

Both tools receive a reserved-names list of globals referenced from HTML
onclick= attributes (neuronDemoToggle, signInWith, NEURON_CFG, etc.) so they
are not mangled.

Implementation adds stdout_to_file(path)/stdout_restore() builtins to the C
runtime so codegen's println-streamed output can be captured to a temp file
before being piped through the external tools. Temp files use
/tmp/elc-<pid>-<timestamp>.js naming and are cleaned up on success and failure.

Rebuilds dist/platform/elc and dist/platform/elc.c. Self-hosting verified.
2026-05-04 10:54:34 -05:00
Will Anderson 21694b79d2 implement ? nil-propagation, write browser-auth.el example, update spec
Iteration 5:

? nil-propagation: Field and Index handlers in js_cg_expr now detect when
the object expression is a Try node (the AST node for postfix `?`).
When detected, emit JS optional chaining: `(expr)?.["field"] ?? null`.
The `?? null` normalizes JS undefined to El's null. A bare `expr?` not
followed by field/index still passes through unchanged.

browser-auth.el: a realistic 130-line example demonstrating:
  - @async function with Supabase via native_js_call
  - DOM bridge: get/set value/text/attr, add/remove class, show/hide
  - local_storage_get/set for session hints
  - window_on_load for initialization
  - window_set to expose functions to the browser global scope
  - set_timeout for transient state, is_valid_email for input validation
  Compiles cleanly with elc --target=js --bundle

Spec updated: status promoted to Phase 4 / ~80% coverage, nil-prop
status updated, new example referenced.
2026-05-04 10:42:54 -05:00
Will Anderson 422442b14e add --bundle flag for self-contained IIFE output
elc --target=js --bundle source.el > output.js produces a single file
with no import statement that can drop directly into a <script> tag.

How it works:
  - detect_bundle() reads the --bundle flag from argv
  - resolve_runtime_path() looks for el_runtime.js next to the source file
  - compile_js_with_bundle() reads the runtime, calls codegen_js_bundle()
  - codegen_js_inner(bundle_mode=true):
    - emits ;(function() { "use strict"; at the top
    - inlines the runtime content (stripping ES export statements which
      are invalid inside an IIFE via js_strip_es_exports())
    - skips the const {...} = globalThis.__el destructure -- the inlined
      function declarations are already in scope within the IIFE
    - closes with })(); after main()

Usage: elc --target=js --bundle app.el > app.js
       Place el_runtime.js in the same directory as app.el.
2026-05-04 10:40:46 -05:00
Will Anderson 437ba0a4dd add 20 browser API builtins to JS runtime and codegen preamble
Iteration 3: closes the browser API gap needed for real web pages.

New builtins in el_runtime.js:
  Extended DOM: dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html,
    dom_get_html, dom_get_parent, dom_contains_class, dom_get_checked,
    dom_set_checked
  Timers: set_timeout, set_interval, clear_interval
  Local storage: local_storage_get, local_storage_set, local_storage_remove
  Window: window_location, window_redirect, window_on_load
  Debug: console_log

All browser-only functions use _ensureBrowser guard. Timer functions
work in both Node and browser. All new names added to __el export
object, ES named exports, and codegen-js.el destructure preamble.
Spec table updated to document new categories.
2026-05-04 10:38:20 -05:00
Will Anderson 7376349124 fix TypeDef parser to consume optional = before field block
type User = { name: String } was silently broken: the parser consumed
the type name then called expect(LBrace) while sitting on the = token.
expect() advances unconditionally on mismatch, so it consumed = and
treated { as the first field name, producing a corrupt TypeDef node.

The FnDef following the broken TypeDef was then parsed incorrectly or
lost entirely -- causing greet() and similar functions to vanish from
JS/C output with no error.

Fix: detect and skip the optional Eq token before expecting LBrace.
Both targets benefit; rebuild elc to pick up the fix.
2026-05-04 10:36:53 -05:00
Will Anderson 0f1da43a97 implement Enum::Variant match patterns in parser and both codegens
Parser now handles `SomeEnum::Variant` in match arm patterns, emitting
a Variant pattern node with enum_name and variant fields. Previously
these fell through to Binding, producing broken codegen.

JS codegen: emit str_eq check against the variant name string (El enums
are plain strings at runtime). C codegen: same, via EL_STR + str_eq.

Rebuild elc to pick up the parser change.
2026-05-04 10:35:35 -05:00
Will Anderson a54b2bebf9 add DOM bridge, async/await, window export, and native_js to JS target
- el_runtime.js: add 19 dom_* builtins (browser-only, throw in Node),
  window_set/window_get for exposing El functions to the browser global
  scope, and native_js/native_js_call escape hatches for third-party libs
- codegen-js.el: destructure all new builtins in generated preamble; add
  @async decorator support that emits async function + await at call sites
  for known-async HTTP builtins and user-declared @async functions; pre-
  registration pass ensures forward calls to @async functions get await
- spec/codegen-js.md: mark Phase 3 (DOM bridge) implemented, document
  @async approach and its limitations, update builtin table and status
- examples/browser-counter.el: canonical example showing dom_get_element,
  dom_set_text, dom_is_null, window_set, and state_set/get
2026-05-04 10:29:43 -05:00
Will Anderson 1ed2dc3c11 add gitflow CI for dev/stage/prod environments 2026-05-04 08:55:21 -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
Will Anderson e180baf776 fix looks_like_string for empty strings and UTF-8, add cross-module includes in codegen 2026-05-03 00:27:20 -05:00
Will Anderson 3d71db4958 Fix O(n²) string construction in codegen-js, lexer, parser, elb
Replace accumulate-by-concatenation loops with native_list_append + str_join.
Eliminates quadratic memory growth when processing large source files.
This is the v2 compiler state — what produced /tmp/elc-v2.
2026-05-02 22:35:49 -05:00
Will Anderson 2a211992d4 Document bootstrapping path and language architecture 2026-05-02 21:23:22 -05:00
25 changed files with 20992 additions and 688 deletions
+101
View File
@@ -0,0 +1,101 @@
name: El 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
# Run all four test suites — all must pass
- name: Run tests — text
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/text/run.sh
- name: Run tests — calendar
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/calendar/run.sh
- name: Run tests — time
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/time/run.sh
- name: Run tests — html_sanitizer
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/html_sanitizer/run.sh
# Publish artifact to GCP Artifact Registry (dev)
- name: Publish elc to Artifact Registry (dev)
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
VERSION="${GITEA_SHA:0:8}"
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--version="${VERSION}" \
--source=dist/platform/elc
# Also tag as latest-dev
echo "Published elc version=${VERSION} to foundation-dev/el/elc"
rm -f /tmp/gcp-key.json
+100
View File
@@ -0,0 +1,100 @@
name: El CI — stage
on:
push:
branches:
- stage
pull_request:
branches:
- stage
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev
# Gen2: compile the bootstrap C source into a working elc binary
- name: Build elc from bootstrap (gen2)
run: |
gcc -O2 \
-I el-compiler/runtime \
dist/elc-bootstrap.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \
-o dist/elc-gen2
chmod +x dist/elc-gen2
echo "gen2 elc built"
dist/elc-gen2 --version || true
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
- name: Self-host: compile El compiler with gen2 (gen3)
run: |
mkdir -p dist/platform
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
gcc -O2 \
-I el-compiler/runtime \
dist/elc-gen3.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \
-o dist/platform/elc
chmod +x dist/platform/elc
echo "gen3 (self-hosted) elc built"
dist/platform/elc --version || true
# Run all four test suites — all must pass
- name: Run tests — text
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/text/run.sh
- name: Run tests — calendar
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/calendar/run.sh
- name: Run tests — time
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/time/run.sh
- name: Run tests — html_sanitizer
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/html_sanitizer/run.sh
# Publish artifact to GCP Artifact Registry (stage)
- name: Publish elc to Artifact Registry (stage)
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
VERSION="${GITEA_SHA:0:8}"
gcloud artifacts generic upload \
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--version="${VERSION}" \
--source=dist/platform/elc
echo "Published elc version=${VERSION} to foundation-stage/el/elc"
rm -f /tmp/gcp-key.json
+25
View File
@@ -129,6 +129,31 @@ jobs:
echo "Release published successfully"
# Dispatch el-sdk-updated event to downstream repos
# Publish artifact to GCP Artifact Registry (prod)
- name: Publish elc to Artifact Registry (prod)
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
VERSION="${GITEA_SHA:0:8}"
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--version="${VERSION}" \
--source=dist/platform/elc
echo "Published elc version=${VERSION} to foundation-prod/el/elc"
rm -f /tmp/gcp-key.json
- name: Dispatch to foundation/engram
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
+979
View File
@@ -0,0 +1,979 @@
# El Language Bootstrap Guide
This document is the authoritative guide for reconstructing the El compiler toolchain from scratch. If the bootstrap binary at `dist/platform/elc` is ever lost, this document is the path back.
---
## 1. The Bootstrap Chain (Current State)
### The Trust Chain
El is a self-hosting language. The compiler is written in El. This creates a circular dependency: you need an El compiler to compile the El compiler. The chain is resolved by a seed binary:
```
dist/platform/elc (Mach-O arm64 native binary)
compiles elc-cli.el
new self-hosted elc binary
compiles itself again (identity check)
stable self-hosted compiler
```
The binary at `dist/platform/elc` is a **Mach-O 64-bit arm64 executable**. The `elc.preselfhost` and `elc.legacy` files in the same directory are older snapshots kept as fallback checkpoints.
The key property: every binary in `dist/platform/` was produced by compiling the El source in `el-compiler/src/` using a previous version of that same binary. The chain is auditable: the source is the ground truth, not the binary.
### The Self-Hosting Pipeline
```
elc-cli.el
imports → el-compiler/src/compiler.el
imports → el-compiler/src/lexer.el
imports → el-compiler/src/parser.el
imports → el-compiler/src/codegen.el
imports → el-compiler/src/codegen-js.el
```
Import resolution is textual. `compiler.el` recursively inlines all imported `.el` files before lex/parse. The result is one large unified source string that the compiler then processes in a single pass.
`elc-combined.el` in the repo root is a pre-merged single-file edition used during early bootstrap iterations.
### What the Bootstrap Binary Actually Is
The `dist/platform/elc` binary is a compiled El program that was produced by running an earlier version of itself on `elc-cli.el`. It is not a Rust binary. The `elc.legacy` and `elc.preselfhost` checkpoints suggest the chain has been continuously self-hosting and re-stamped. The original genesis compiler (referenced in the language spec as a "Rust genesis compiler") was used to produce the first self-hosted binary; that Rust binary is not present in this repo.
To rebuild the current binary from source using the current binary:
```bash
cd /path/to/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_runtime.c
```
Verify self-hosting by using `elc-new` to recompile itself and diffing the outputs.
---
## 2. The Language
### 2.1 Lexical Structure
El source is UTF-8. File extension `.el`. Comments are single-line only: `//` to end of line.
**Token representation:** every token is a map `{ "kind": String, "value": String }`.
**Keywords** — from `keyword_kind()` in `lexer.el`:
| Keyword | Token Kind | Notes |
|---------|-----------|-------|
| `let` | `Let` | variable binding |
| `fn` | `Fn` | function definition |
| `type` | `Type` | struct definition |
| `enum` | `Enum` | enum definition |
| `match` | `Match` | pattern match |
| `return` | `Return` | function return |
| `if` | `If` | conditional |
| `else` | `Else` | |
| `for` | `For` | iteration |
| `in` | `In` | used in `for x in list` |
| `while` | `While` | loop |
| `import` | `Import` | module import |
| `from` | `From` | `from mod import { Name }` |
| `as` | `As` | (reserved, no parse form) |
| `with` | `With` | (reserved) |
| `sealed` | `Sealed` | (reserved) |
| `activate` | `Activate` | (reserved) |
| `where` | `Where` | (reserved) |
| `test` | `Test` | (reserved) |
| `seed` | `Seed` | (reserved) |
| `assert` | `Assert` | (reserved) |
| `protocol` | `Protocol` | (reserved) |
| `impl` | `Impl` | (reserved) |
| `retry` | `Retry` | reserved / soft keyword in expr position |
| `times` | `Times` | reserved / soft keyword |
| `fallback` | `Fallback` | reserved / soft keyword |
| `reason` | `Reason` | reserved / soft keyword |
| `parallel` | `Parallel` | reserved / soft keyword |
| `trace` | `Trace` | reserved / soft keyword |
| `requires` | `Requires` | reserved / soft keyword |
| `deploy` | `Deploy` | reserved / soft keyword |
| `to` | `To` | reserved / soft keyword |
| `via` | `Via` | reserved / soft keyword |
| `target` | `Target` | **RESERVED — cannot use as identifier** |
| `true` | `Bool` | literal value `true` |
| `false` | `Bool` | literal value `false` |
| `cgi` | `Cgi` | CGI identity block |
| `service` | `Service` | service declaration block |
| `manager` | `Manager` | VBD role decorator / soft keyword |
| `engine` | `Engine` | VBD role decorator / soft keyword |
| `accessor` | `Accessor` | VBD role decorator / soft keyword |
| `vessel` | `Vessel` | soft keyword |
| `extern` | `Extern` | `extern fn` forward declaration |
**Soft keywords** (`target`, `to`, `via`, `deploy`, `reason`, `times`, `fallback`, `retry`, `parallel`, `trace`, `requires`, `where`, `as`, `with`, `manager`, `engine`, `accessor`, `vessel`): these have dedicated token kinds but the parser re-interprets them as `Ident` nodes when they appear in expression position (e.g., as parameter names or local variable names).
**All token kinds:**
| Kind | Pattern |
|------|---------|
| `Int` | `[0-9]+` |
| `Float` | `[0-9]+ '.' [0-9]+` |
| `Str` | `"…"` with `\"`, `\n`, `\t`, `\r`, `\\` escapes |
| `Bool` | `true` or `false` |
| `Ident` | `[a-zA-Z_][a-zA-Z0-9_]*` (not a keyword) |
| keyword tokens | one per keyword above |
| `Eq` | `=` |
| `EqEq` | `==` |
| `NotEq` | `!=` |
| `Not` | `!` |
| `Lt` / `LtEq` / `Gt` / `GtEq` | `<` `<=` `>` `>=` |
| `And` | `&&` (single `&` is consumed and discarded) |
| `Or` | `\|\|` |
| `Pipe` | `\|` |
| `PipeOp` | `\|>` |
| `Plus` / `Minus` / `Star` / `Slash` | `+` `-` `*` `/` |
| `Percent` | `%` |
| `Arrow` | `->` |
| `FatArrow` | `=>` |
| `Colon` / `ColonColon` | `:` `::` |
| `LParen` / `RParen` | `(` `)` |
| `LBrace` / `RBrace` | `{` `}` |
| `LBracket` / `RBracket` | `[` `]` |
| `Comma` / `Dot` / `Semicolon` | `,` `.` `;` |
| `At` | `@` |
| `QuestionMark` | `?` |
| `Eof` | end-of-input sentinel |
**String comment stripping:** the lexer contains a special heuristic for string literals that embed JavaScript or CSS (`looks_like_code`). If a string contains `<script`, `<style`, or `function` + `;`, the lexer strips `//` and `/* */` comments from the string value before producing the `Str` token. This is a compile-time content sanitization pass.
### 2.2 AST Node Types
Every AST node is a `Map<String, Any>`. The `"expr"` or `"stmt"` key names the node type.
**Expression nodes:**
| `expr` value | Fields | Meaning |
|-------------|--------|---------|
| `Int` | `value: String` | integer literal |
| `Float` | `value: String` | float literal |
| `Str` | `value: String` | string literal |
| `Bool` | `value: String` | `"true"` or `"false"` |
| `Nil` | — | null / missing |
| `Ident` | `name: String` | identifier reference |
| `BinOp` | `op: String`, `left`, `right` | binary operation |
| `Not` | `inner` | unary `!` |
| `Neg` | `inner` | unary `-` |
| `Call` | `func`, `args: [expr]` | function call |
| `Field` | `object`, `field: String` | `obj.field` |
| `Index` | `object`, `index` | `obj[idx]` |
| `Array` | `elems: [expr]` | `[e1, e2, …]` |
| `Map` | `pairs: [{ key: String, value: expr }]` | `{ "k": v, … }` |
| `If` | `cond`, `then: [stmt]`, `else: [stmt]`, `has_else: Bool` | conditional expression |
| `For` | `item: String`, `list`, `body: [stmt]` | for-in expression |
| `Match` | `subject`, `arms: [{ pattern, body }]` | pattern match |
| `DurationLit` | `count: String`, `unit: String` | `30.seconds`, `1.hour` |
| `Try` | `inner` | postfix `?` (no-op passthrough today) |
**Binary operators** (`op` field values): `Plus`, `Minus`, `Star`, `Slash`, `EqEq`, `NotEq`, `Lt`, `Gt`, `LtEq`, `GtEq`, `And`, `Or`.
**Operator precedence** (higher = tighter binding):
| Level | Operators |
|-------|-----------|
| 6 | `Star`, `Slash` |
| 5 | `Plus`, `Minus` |
| 4 | `Lt`, `Gt`, `LtEq`, `GtEq` |
| 3 | `EqEq`, `NotEq` |
| 2 | `And` |
| 1 | `Or` |
**Pattern nodes** (used inside `Match` arms):
| `pattern` value | Fields | Meaning |
|----------------|--------|---------|
| `Wildcard` | — | `_` — always matches |
| `Binding` | `name: String` | binds subject to name |
| `LitInt` | `value: String` | integer literal pattern |
| `LitStr` | `value: String` | string literal pattern |
| `LitBool` | `value: String` | boolean literal pattern |
**Statement nodes:**
| `stmt` value | Fields | Meaning |
|-------------|--------|---------|
| `Let` | `name: String`, `value: expr`, `type: String` | variable binding |
| `Assign` | `name: String`, `value: expr` | bare reassignment `name = expr` |
| `Return` | `value: expr` | return statement |
| `While` | `cond: expr`, `body: [stmt]` | while loop |
| `For` | `item: String`, `list: expr`, `body: [stmt]` | for-in loop |
| `FnDef` | `name: String`, `params: [param]`, `body: [stmt]`, `ret_type: String`, `decorator?: String` | function definition |
| `ExternFn` | `name: String`, `params: [param]`, `ret_type: String` | forward declaration |
| `TypeDef` | `name: String`, `fields: [{ name: String }]` | struct type definition |
| `EnumDef` | `name: String`, `variants: [{ name: String }]` | enum definition |
| `Import` | `path: String` | `import "file.el"` or `from mod import { … }` |
| `CgiBlock` | `name`, `dharma_id`, `principal`, `network`, `engram`, `has_*: Bool` | CGI identity declaration |
| `ServiceBlock` | `name`, `sponsor`, `domain` | service declaration |
| `Expr` | `value: expr` | bare expression statement |
**Param nodes:** `{ "name": String, "type": String }` where `type` is the leading identifier of the type annotation (e.g., `"Int"`, `"String"`, `"Map"`) or `""` if unannotated.
### 2.3 The Type System
Type annotations are parsed and stored but not type-checked at compile time. They serve as documentation and as hints to the codegen for arithmetic dispatch.
**Built-in types:**
| Type | C representation | Notes |
|------|-----------------|-------|
| `String` | `const char*` cast to `el_val_t` | via `EL_STR()` macro |
| `Int` | `int64_t` | direct |
| `Bool` | `int64_t` | `0` = false, nonzero = true |
| `Float` | `int64_t` | bit-cast double via `el_from_float()` |
| `Void` | `void` | functions returning nothing |
| `Any` | `void*` cast to `el_val_t` | generic containers |
| `[T]` | `el_val_t` | pointer to ElList struct |
| `Map<K,V>` | `el_val_t` | pointer to ElMap struct |
**Temporal types** (first-class in codegen):
| Type | Representation | Notes |
|------|---------------|-------|
| `Instant` | nanoseconds since Unix epoch as `int64_t` | `now()` returns this |
| `Duration` | signed nanoseconds as `int64_t` | `30.seconds` = `30 * 1000000000` |
| `Calendar` | pointer to heap-allocated struct | `earth_calendar(zone)` |
| `CalendarTime` | pointer to heap-allocated struct | `now_in(cal)` |
| `LocalDate` | pointer to heap-allocated struct | `local_date(y, m, d)` |
| `LocalTime` | nanoseconds since midnight, direct `int64_t` | `local_time(h, m, s, ns)` |
| `Zone` | pointer to heap-allocated struct | `zone("America/New_York")` |
| `Rhythm` | pointer to heap-allocated struct | recurrence pattern |
The codegen tracks type-annotated variable names in per-function process state (`__int_names`, `__instant_names`, `__duration_names`, etc.) to dispatch arithmetic and comparisons through the correct runtime wrappers. Type-mismatched operations (e.g., `Instant + Instant`) are emitted as `#error` directives.
**Duration postfix literals:** `30.seconds`, `1.hour`, `500.millis`, `30.nanos` are parsed as `DurationLit` AST nodes and compiled to `el_duration_from_nanos(count * multiplier)`. The multipliers:
| Unit | Nanoseconds |
|------|------------|
| `nano` / `nanos` | 1 |
| `milli` / `millis` / `millisecond` / `milliseconds` | 1,000,000 |
| `second` / `seconds` | 1,000,000,000 |
| `minute` / `minutes` | 60,000,000,000 |
| `hour` / `hours` | 3,600,000,000,000 |
| `day` / `days` | 86,400,000,000,000 |
### 2.4 Key Language Semantics
**Implicit return.** The final expression in a function body becomes the return value if it is not a control-flow construct (`If`, `For`). The codegen's `transform_implicit_return` rewrites the last `Expr` statement into a `Return` statement before emitting.
**Let-rebinding, not mutation.** El uses `let` for both initial binding and rebinding:
```el
let count = 0
let count = count + 1 // NOT mutation creates a new binding in the same scope
```
The codegen tracks declared names per C scope. When `count` is already in `declared`, it emits `count = count + 1;` (plain assignment). When it is new, it emits `el_val_t count = 0;`. This means **El does not have mutable variables in the traditional sense** — every `let` is a potential redeclaration. The practical effect is that shadowing and in-place update use identical syntax.
**Bare reassignment.** The parser also handles `name = expr` (without `let`) when an `Ident` is immediately followed by `Eq`. This emits a plain C assignment.
**`target` is reserved.** The word `target` is lexed as the `Target` token kind — it cannot be used as a variable or parameter name. Use `tgt` or another name instead. This is a live gotcha in `compiler.el` itself, which uses `tgt` for exactly this reason.
**`__no_block_expr` guard.** The parser uses process state key `__no_block_expr` to suppress Map-literal parsing when parsing the condition of `if`, `while`, `for`, and `match`. This prevents a stray `{` (the start of the then-block) from being parsed as a Map literal.
**Arena memory model.** The runtime includes an arena allocator that is activated in server/long-running contexts. In CLI mode (`elc`, `elb`) the arena is inactive. Memory is managed via ARC (reference counting): `el_retain()` and `el_release()` on Lists and Maps. Strings and ints are not refcounted — the retain/release functions are safe no-ops on non-tagged values.
---
## 3. The Runtime API
All runtime functions are declared in `el-compiler/runtime/el_runtime.h`. Every compiled El program links against `el-compiler/runtime/el_runtime.c`.
All values are `el_val_t` (`int64_t`). Strings are pointers cast through `int64_t` using `EL_STR(s)` / `EL_CSTR(v)` macros.
Canonical compile command:
```bash
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
-o <out> <prog>.c el-compiler/runtime/el_runtime.c
```
### I/O
| Function | Signature | Description |
|----------|-----------|-------------|
| `println` | `(s) -> Void` | print string + newline to stdout |
| `print` | `(s) -> Void` | print string without newline |
| `readline` | `() -> String` | read one line from stdin |
### String Operations
| Function | Signature | Description |
|----------|-----------|-------------|
| `el_str_concat` | `(a, b) -> String` | concatenate two strings |
| `str_concat` | `(a, b) -> String` | alias for `el_str_concat` |
| `str_eq` | `(a, b) -> Bool` | string equality comparison |
| `str_starts_with` | `(s, prefix) -> Bool` | prefix test |
| `str_ends_with` | `(s, suffix) -> Bool` | suffix test |
| `str_contains` | `(s, sub) -> Bool` | substring test |
| `str_len` | `(s) -> Int` | byte length |
| `str_slice` | `(s, start, end) -> String` | substring (byte offsets) |
| `str_replace` | `(s, from, to) -> String` | replace all occurrences |
| `str_to_upper` / `str_upper` | `(s) -> String` | uppercase |
| `str_to_lower` / `str_lower` | `(s) -> String` | lowercase |
| `str_trim` | `(s) -> String` | strip leading/trailing whitespace |
| `str_lstrip` / `str_rstrip` | `(s) -> String` | one-sided strip |
| `str_index_of` | `(s, sub) -> Int` | position of substring; `-1` if absent |
| `str_last_index_of` | `(s, sub) -> Int` | last position |
| `str_index_of_all` | `(s, sub) -> [Int]` | all byte offsets (non-overlapping) |
| `str_find_chars` | `(s, any_of) -> Int` | first index of any char in set |
| `str_split` | `(s, sep) -> [String]` | split on separator |
| `str_split_lines` | `(s) -> [String]` | split on newlines |
| `str_split_chars` | `(s) -> [String]` | split into individual characters |
| `str_split_n` | `(s, sep, n) -> [String]` | split at most `n` times |
| `str_join` | `(list, sep) -> String` | join list with separator |
| `str_char_at` | `(s, i) -> String` | character at byte index |
| `str_char_code` | `(s, i) -> Int` | Unicode code point at index |
| `str_pad_left` | `(s, width, pad) -> String` | left-pad to width |
| `str_pad_right` | `(s, width, pad) -> String` | right-pad to width |
| `str_format` | `(fmt, data) -> String` | `{key}` interpolation |
| `str_repeat` | `(s, n) -> String` | repeat string n times |
| `str_reverse` | `(s) -> String` | reverse by codepoint |
| `str_strip_prefix` | `(s, prefix) -> String` | remove prefix if present |
| `str_strip_suffix` | `(s, suffix) -> String` | remove suffix if present |
| `str_strip_chars` | `(s, chars) -> String` | strip characters from both ends |
| `str_count` | `(s, sub) -> Int` | count non-overlapping occurrences |
| `str_count_chars` | `(s) -> Int` | codepoint count |
| `str_count_bytes` | `(s) -> Int` | alias for `str_len` |
| `str_count_lines` | `(s) -> Int` | line count |
| `str_count_words` | `(s) -> Int` | word count |
| `str_count_letters` | `(s) -> Int` | ASCII letter count |
| `str_count_digits` | `(s) -> Int` | ASCII digit count |
| `is_letter` / `is_digit` / `is_alphanumeric` | `(s) -> Bool` | ASCII char classification |
| `is_whitespace` / `is_punctuation` | `(s) -> Bool` | |
| `is_uppercase` / `is_lowercase` | `(s) -> Bool` | |
| `int_to_str` | `(n) -> String` | format integer |
| `str_to_int` | `(s) -> Int` | parse integer |
| `str_to_float` | `(s) -> Float` | parse float |
| `parse_int` | `(s, default) -> Int` | parse with fallback |
| `bool_to_str` | `(b) -> String` | format bool |
### Integer/Float Math
| Function | Description |
|----------|-------------|
| `el_abs(n)` | absolute value |
| `el_max(a, b)` | maximum |
| `el_min(a, b)` | minimum |
| `float_to_str(f)` | format float as string |
| `int_to_float(n)` | widen Int to Float |
| `float_to_int(f)` | truncate Float to Int |
| `format_float(f, decimals)` | format with N decimal places |
| `decimal_round(f, decimals)` | round to N decimals |
| `math_sqrt(f)` | square root |
| `math_log(f)` / `math_ln(f)` | logarithms |
| `math_sin(f)` / `math_cos(f)` / `math_pi()` | trigonometry |
### List Operations
| Function | Description |
|----------|-------------|
| `el_list_empty()` | create empty list |
| `el_list_new(count, …)` | create list from N values (varargs) |
| `el_list_len(list)` | length |
| `el_list_get(list, i)` | element at index; `0` on out-of-bounds |
| `el_list_append(list, e)` | append; returns updated list |
| `el_list_clone(list)` | shallow copy |
| `list_push(list, e)` | alias for `el_list_append` |
| `list_push_front(list, e)` | prepend |
| `list_join(list, sep)` | join to string |
| `list_range(start, end)` | integer range `[start, end)` |
| `native_list_empty()` | alias for `el_list_empty` (used in compiler source) |
| `native_list_append(l, v)` | alias for `el_list_append` |
| `native_list_get(l, idx)` | alias for `el_list_get` |
| `native_list_len(l)` | alias for `el_list_len` |
| `native_list_clone(l)` | alias for `el_list_clone` |
| `append(l, e)` | method-call alias: `list.append(e)` |
| `len(l)` | method-call alias: `list.len()` |
| `get(l, i)` | method-call alias: `list.get(i)` |
### Map Operations
| Function | Description |
|----------|-------------|
| `el_map_new(count, …)` | create map from key/value pairs (varargs) |
| `el_map_get(map, key)` | get value by key |
| `el_map_set(map, key, value)` | set key; returns map |
| `el_get_field(map, key)` | alias; emitted for `.field` access |
| `map_get(map, key)` | method-call alias |
| `map_set(map, key, value)` | method-call alias |
### ARC (Reference Counting)
| Function | Description |
|----------|-------------|
| `el_retain(v)` | increment refcount; no-op for non-heap values |
| `el_release(v)` | decrement refcount; free when zero |
### In-Process State
| Function | Description |
|----------|-------------|
| `state_set(key, value)` | store in process-global key/value table |
| `state_get(key)` | retrieve; `""` if absent |
| `state_del(key)` | delete key |
| `state_keys()` | all keys as `[String]` |
### Filesystem
| Function | Description |
|----------|-------------|
| `fs_read(path)` | read file to string; `""` on error |
| `fs_write(path, content)` | write string; returns `1` on success |
| `fs_write_bytes(path, bytes, length)` | write raw bytes of known length |
| `fs_list(path)` | list directory entries |
| `fs_exists(path)` | check if path exists |
| `fs_mkdir(path)` | mkdir -p |
### HTTP Client
| Function | Description |
|----------|-------------|
| `http_get(url)` | GET; returns body string |
| `http_post(url, body)` | POST; returns body string |
| `http_post_json(url, json_body)` | POST with Content-Type: application/json |
| `http_get_with_headers(url, headers_map)` | GET with custom headers |
| `http_post_with_headers(url, body, headers_map)` | POST with custom headers |
| `http_post_form_auth(url, form_body, auth_header)` | POST with auth |
| `http_delete(url)` | DELETE |
| `http_get_to_file(url, headers_map, output_path)` | stream response to file |
| `http_post_to_file(url, body, headers_map, output_path)` | stream POST response to file |
| `http_response(status, headers_json, body)` | build response envelope |
| `url_encode(s)` | RFC 3986 percent-encoding |
| `url_decode(s)` | URL decode |
| `el_html_sanitize(html, allowlist_json)` | allowlist HTML sanitizer |
### HTTP Server
| Function | Description |
|----------|-------------|
| `http_serve(port, handler)` | start server; handler: `(method, path, body) -> String` |
| `http_serve_v2(port, handler)` | start server; handler: `(method, path, headers_map, body) -> String` |
| `http_set_handler(name)` | set handler by symbol name |
| `http_set_handler_v2(name)` | v2 variant |
### JSON
| Function | Description |
|----------|-------------|
| `json_get(json, key)` | substring lookup of `"key": value` |
| `json_parse(s)` | parse JSON string to List/Map |
| `json_stringify(v)` | serialize Any to JSON string |
| `json_get_string(j, key)` | typed extract: String |
| `json_get_int(j, key)` | typed extract: Int |
| `json_get_float(j, key)` | typed extract: Float |
| `json_get_bool(j, key)` | typed extract: Bool |
| `json_get_raw(j, key)` | extract nested object/array as JSON string |
| `json_set(j, key, value)` | update field, return new JSON string |
| `json_array_len(j)` | length of JSON array string |
| `json_array_get(j, index)` | element at index |
| `json_array_get_string(j, index)` | string element at index |
### Time (Epoch-Based)
| Function | Description |
|----------|-------------|
| `time_now()` | Unix epoch milliseconds |
| `time_now_utc()` | same, explicit UTC |
| `time_format(ts, fmt)` | format timestamp |
| `time_to_parts(ts)` | decompose to Map of fields |
| `time_from_parts(secs, ns, tz)` | construct timestamp |
| `time_add(ts, n, unit)` | add duration |
| `time_diff(ts1, ts2, unit)` | difference |
| `unix_timestamp()` | Unix seconds as Int |
| `sleep_secs(secs)` | sleep N seconds |
| `sleep_ms(ms)` | sleep N milliseconds |
### Time (First-Class Instant/Duration)
| Function | Description |
|----------|-------------|
| `now()` / `el_now_instant()` | current time as Instant (nanoseconds) |
| `unix_seconds(n)` | construct Instant from Unix seconds |
| `unix_millis(n)` | construct Instant from Unix milliseconds |
| `instant_from_iso8601(s)` | parse ISO 8601 string |
| `instant_to_unix_seconds(i)` | extract Unix seconds |
| `instant_to_unix_millis(i)` | extract Unix milliseconds |
| `instant_to_iso8601(i)` | format as ISO 8601 |
| `el_duration_from_nanos(ns)` | construct Duration from nanoseconds |
| `duration_seconds(n)` | Duration from seconds |
| `duration_millis(n)` | Duration from milliseconds |
| `duration_nanos(n)` | Duration from nanoseconds |
| `duration_to_seconds(d)` | extract seconds |
| `duration_to_millis(d)` | extract milliseconds |
| `duration_to_nanos(d)` | extract nanoseconds |
| `el_instant_add_dur(inst, dur)` | Instant + Duration |
| `el_instant_sub_dur(inst, dur)` | Instant - Duration |
| `el_instant_diff(a, b)` | Instant - Instant = Duration |
| `el_duration_add/sub/scale/div` | Duration arithmetic |
| `el_instant_lt/le/gt/ge/eq/ne` | Instant comparison |
| `el_duration_lt/le/gt/ge/eq/ne` | Duration comparison |
| `el_sleep_duration(dur)` | sleep for a Duration |
| `ttl_cache_set(key, value)` | store with TTL |
| `ttl_cache_get(key, max_age)` | retrieve if within max_age |
| `ttl_cache_age(key)` | age of cached value as Duration |
### Calendar System
| Function | Description |
|----------|-------------|
| `zone(id)` | IANA zone or fixed offset |
| `zone_utc()` / `zone_local()` | UTC and local zone |
| `zone_offset(hours, minutes)` | fixed offset zone |
| `earth_calendar(z)` | Gregorian calendar in zone |
| `earth_calendar_default()` | system default |
| `mars_calendar()` / `cycle_calendar(period)` | non-Earth calendars |
| `no_cycle_calendar()` / `relative_calendar(epoch)` | abstract calendars |
| `now_in(cal)` | current time as CalendarTime |
| `in_calendar(inst, cal)` | project Instant into Calendar |
| `cal_format(ct, pattern)` | format CalendarTime |
| `cal_to_instant(ct)` | extract underlying Instant |
| `cal_cycle_phase(ct)` / `cal_in(ct, cal)` | calendar ops |
| `local_date(y, m, d)` | construct LocalDate |
| `local_time(h, m, s, ns)` | construct LocalTime |
| `local_datetime(date, time)` | construct LocalDateTime |
| `zoned(date, time, cal)` | zoned datetime |
| `local_date_year/month/day` | LocalDate accessors |
| `local_time_hour/minute/second/nanos` | LocalTime accessors |
| `el_local_date_add_dur` / `el_local_time_add_dur` | date/time arithmetic |
| `el_local_date_lt` / `el_local_date_eq` | date comparison |
| `rhythm_*` | recurrence patterns (cycle_start, weekday, weekly_at, next_after, matches, …) |
### Process / Execution
| Function | Description |
|----------|-------------|
| `args()` | command-line arguments as `[String]` (excludes argv[0]) |
| `env(key)` | read environment variable; `""` if unset |
| `exit(code)` | exit process with code |
| `exit_program(code)` | alias for `exit` |
| `getpid_now()` | current process ID |
| `exec_command(cmd)` | run shell command; return exit code |
| `exec_capture(cmd)` | run shell command; capture and return stdout |
| `uuid_new()` / `uuid_v4()` | generate UUID v4 |
| `native_int_to_str(n)` | format integer (alias, used in compiler source) |
| `native_string_chars(s)` | split string into `[String]` of single characters |
### Crypto
| Function | Description |
|----------|-------------|
| `sha256_hex(input)` | SHA-256, hex output |
| `sha256_bytes(input)` | SHA-256, raw bytes |
| `hmac_sha256_hex(key, msg)` | HMAC-SHA-256, hex |
| `hmac_sha256_bytes(key, msg)` | HMAC-SHA-256, raw bytes |
| `base64_encode(input)` / `base64_decode(input)` | standard base64 |
| `base64url_encode(input)` / `base64url_decode(input)` | URL-safe base64 |
| `sha3_256_hex(input)` | SHA3-256 (Keccak) |
| `pq_keygen_signature()` | Dilithium-3 key pair |
| `pq_sign(sk_hex, msg)` / `pq_verify(pk_hex, msg, sig_hex)` | PQ signatures |
| `pq_kem_keygen()` / `pq_kem_encaps(pk)` / `pq_kem_decaps(sk, ct)` | Kyber-768 KEM |
| `pq_hybrid_keygen()` / `pq_hybrid_handshake(remote_pub)` | X25519 + Kyber hybrid |
| `aead_encrypt(key_hex, plaintext)` | AES-256-GCM encrypt |
| `aead_decrypt(key_hex, nonce_hex, ct_hex)` | AES-256-GCM decrypt |
### DHARMA Network (CGI programs only)
| Function | Description |
|----------|-------------|
| `el_cgi_init(name, dharma_id, principal, network, engram)` | initialize CGI identity (called by generated `main()`) |
| `dharma_connect(cgi_id)` | open channel to peer |
| `dharma_send(channel, content)` | send message; blocks for response |
| `dharma_activate(query)` | spreading activation across DHARMA network |
| `dharma_emit(event_type, payload)` | emit network event (@manager only) |
| `dharma_field(event_type)` | wait for event (@manager only) |
| `dharma_strengthen(cgi_id, weight)` | Hebbian potentiation |
| `dharma_relationship(cgi_id)` | current relationship weight |
| `dharma_peers()` | all connected peers sorted by weight |
### Engram Knowledge Graph
| Function | Description |
|----------|-------------|
| `engram_node(content, type, salience)` | create node; returns ID |
| `engram_node_full(content, type, label, salience, importance, confidence, tier, tags)` | full node creation |
| `engram_node_layered(…, layer_id)` | create node in specific layer |
| `engram_get_node(id)` | retrieve node by ID |
| `engram_strengthen(node_id)` | Hebbian potentiation |
| `engram_forget(node_id)` | delete node and edges |
| `engram_node_count()` | total node count |
| `engram_edge_count()` | total edge count |
| `engram_search(query, limit)` | full-text search |
| `engram_scan_nodes(limit, offset)` | paginated node scan |
| `engram_connect(from, to, weight, relation)` | create directed edge |
| `engram_edge_between(from, to)` | get edge |
| `engram_neighbors(node_id)` | BFS neighbors |
| `engram_neighbors_filtered(node_id, max_depth, direction)` | filtered BFS |
| `engram_activate(query, depth)` | spreading activation |
| `engram_save(path)` / `engram_load(path)` | snapshot to/from disk |
| `engram_add_layer(name, priority, suppressible, transparent, injectable)` | add consciousness layer |
| `engram_remove_layer(layer_id)` / `engram_list_layers()` | layer management |
| `engram_*_json` variants | JSON-string versions of search/scan/activate |
| `engram_compile_layered_json(intent, depth)` | prompt-ready context block |
### LLM (Anthropic API)
| Function | Description |
|----------|-------------|
| `llm_call(model, prompt)` | single-turn call |
| `llm_call_system(model, system, user)` | call with system prompt |
| `llm_call_agentic(model, system, user, tools)` | agentic call with tools (CGI only) |
| `llm_vision(model, system, prompt, image)` | vision call |
| `llm_models()` | list available models |
| `llm_register_tool(name, handler_fn_name)` | register tool handler (CGI only) |
### Observability
| Function | Description |
|----------|-------------|
| `emit_log(level, msg, fields_json)` | emit OTLP log |
| `emit_metric(name, value, tags_json)` | emit OTLP metric |
| `trace_span_start(name)` | start trace span |
| `trace_span_end(span_handle)` | end trace span |
| `emit_event(name, duration_ms)` | emit event |
---
## 4. How to Re-Bootstrap from Zero
This section assumes the bootstrap binary is gone. Everything else (source files, runtime) is intact.
### What You Need to Implement
A minimal El compiler has three parts: lexer, parser, codegen. Each can be written in any language. The goal is to compile `elc-cli.el` into a working `elc` binary, after which El is self-hosting again.
### Step 1: Write a Minimal Lexer
The lexer must produce a list of `{ "kind": String, "value": String }` maps (or equivalent structures). Required token kinds: `Int`, `Float`, `Str`, `Bool`, `Ident`, `Eof`, and all keywords and operators listed in section 2.1.
The minimal subset needed to compile the compiler itself:
- Keywords: `let`, `fn`, `return`, `if`, `else`, `while`, `for`, `in`, `import`, `from`, `true`, `false`, `extern`
- Literals: `Int`, `Str`, `Bool`, `Ident`
- Operators: `=`, `==`, `!=`, `!`, `<`, `>`, `<=`, `>=`, `&&`, `||`, `+`, `-`, `*`, `/`, `->`, `=>`, `:`, `,`, `.`, `(`, `)`, `{`, `}`, `[`, `]`, `@`, `?`
- Special: `Eof`
The lexer in `lexer.el` walks a char array using `native_list_get` to avoid O(n²) string slicing. A Python implementation can use a simple index into a string. Escapes to handle: `\"`, `\n`, `\t`, `\r`, `\\`.
### Step 2: Write a Minimal Parser
The parser is a standard recursive descent parser. It produces AST maps as described in section 2.2.
The minimal statement forms needed to compile the compiler:
- `let name [: Type] = expr`
- `fn name(params) [-> Type] { body }`
- `extern fn name(params) [-> Type]`
- `return expr`
- `while cond { body }`
- `for item in list { body }`
- `if cond { body } [else [if] { body }]`
- `import "path"`
- `from module import { … }`
- `@decorator stmt`
- `name = expr` (bare assignment)
- bare expression statement
The minimal expression forms:
- Integer, float, string, bool literals
- Identifier
- Binary operations with the precedence table from section 2.2
- Unary `!` and `-`
- Function call: `f(a, b, …)`
- Method call: `obj.method(args)` (parsed as Call with Field func)
- Field access: `obj.field`
- Index access: `obj[i]`
- Array literal: `[e1, e2, …]`
- Map literal: `{ "key": value, … }`
- `if` as expression
- `match` expression
- Postfix `?` (can be a no-op)
- Duration literal: `N.unit`
The `__no_block_expr` guard (section 2.4) is important: without it, `if a || b { ... }` will incorrectly parse `{` as a Map literal.
### Step 3: Write a Minimal Codegen
The codegen emits C11 source. Required output structure:
```c
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
// Forward declarations for all non-main functions
el_val_t fn_name(el_val_t p1, el_val_t p2);
...
// File-scope let bindings (if any)
el_val_t GLOBAL_NAME;
// Function bodies
el_val_t fn_name(el_val_t p1, el_val_t p2) {
...
return 0;
}
// Entry point
int main(int _argc, char** _argv) {
el_runtime_init_args(_argc, _argv);
...
return 0;
}
```
Critical codegen rules:
1. **All values are `el_val_t`**. Every parameter, local variable, and return type is `el_val_t` unless the function has `ret_type == "Void"` (use `void`).
2. **Let-rebinding**: track declared names per C scope. Emit `el_val_t name = val;` on first occurrence; emit `name = val;` on subsequent occurrences of the same name in the same scope.
3. **`+` dispatch**: if either operand is a string literal → `el_str_concat(a, b)`. If both are provably integers → `(a + b)`. Default fallback → `el_str_concat`.
4. **`==` dispatch**: if either operand is a string or identifier → `str_eq(a, b)`. If both are integer literals or provably Int → `(a == b)`.
5. **String literals**: wrap in `EL_STR("…")` and escape: `\"``\\\"`, `\n``\\n`, `\t``\\t`, `\\``\\\\`.
6. **Map literals**: `el_map_new(N, "k1", v1, "k2", v2, …)`. Empty map: `el_map_new(0)`.
7. **Array literals**: `el_list_new(N, e1, e2, …)`. Empty: `el_list_empty()`.
8. **Index access**: string-literal index → `el_get_field(obj, EL_STR("key"))`. Integer index → `el_list_get(obj, idx)`.
9. **Field access** `obj.field``el_get_field(obj, EL_STR("field"))`.
10. **Method call** `obj.method(args)``method(obj, args)`.
11. **`for item in list`** → emit:
```c
{ el_val_t _el_lst = <list>; el_val_t _el_len = el_list_len(_el_lst);
for (el_val_t _el_i = 0; _el_i < _el_len; _el_i++) {
el_val_t item = el_list_get(_el_lst, _el_i);
<body>
}
}
```
12. **`match`** → GCC/Clang statement expression with `goto`:
```c
({ el_val_t _s = <subject>; el_val_t _r = 0;
if (_s == 42) { _r = <arm_body>; goto _done; }
if (str_eq(_s, EL_STR("str"))) { _r = <arm_body>; goto _done; }
{ _r = <wildcard_body>; goto _done; }
_done:; _r; })
```
13. **`if` as expression** → similarly wrapped in a GCC/Clang statement expression.
14. **Implicit return**: if the last statement in a function body is a bare `Expr` (not `If` or `For`), emit it as `return <expr>;` instead of `<expr>;`.
15. **Float literals**: emit as `el_from_float(<value>)`.
16. **Bool literals**: `true` → `1`, `false` → `0`.
17. **`fn main()`**: do not emit as a regular `el_val_t` function. Instead, fold its body into C's `int main()` after any top-level statements.
18. **`extern fn`**: emit only a forward declaration (no body).
19. **Forward declarations**: scan for all `FnDef` nodes before emitting bodies. This enables mutual recursion.
### Step 4: Compile the El Compiler
Using your minimal implementation, compile `elc-cli.el` (which imports the entire compiler chain):
```bash
# Your minimal compiler
python3 minimal_elc.py elc-cli.el > elc-new.c
# Build with the runtime
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
-o elc-new elc-new.c el-compiler/runtime/el_runtime.c
```
### Step 5: Verify Self-Hosting
```bash
# Compile elc-cli.el with the new compiler
./elc-new elc-cli.el elc-v2.c
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
-o elc-v2 elc-v2.c el-compiler/runtime/el_runtime.c
# Compile again with the second-generation compiler
./elc-v2 elc-cli.el elc-v3.c
# The outputs should be identical
diff elc-v2.c elc-v3.c
```
A clean diff confirms you have a stable fixed point: the compiler reproduces itself exactly.
### Step 6: Replace the Bootstrap Binary
```bash
cp elc-v2 dist/platform/elc
```
You are bootstrapped.
### Minimal El Subset for the Compiler Itself
The El compiler source (`lexer.el`, `parser.el`, `codegen.el`, `compiler.el`) uses:
- `fn`, `let`, `while`, `if`/`else`, `return`, `for`/`in`, `import`
- `extern fn` (for `.elh` headers)
- `String`, `Int`, `Bool`, `Void`, `Any`, `Map<String, Any>`, `[String]`, `[Map<String, Any>]`
- Map literals `{ "key": val }`
- Array literals `[...]` (and `native_list_empty()`)
- List operations: `native_list_empty()`, `native_list_append()`, `native_list_get()`, `native_list_len()`, `native_list_clone()`
- String operations: `str_join()`, `str_eq()`, `str_contains()`, `str_starts_with()`, `str_slice()`, `str_trim()`, `str_split()`, `str_index_of()`, `str_len()`, `str_to_int()`, `native_string_chars()`, `native_int_to_str()`
- `state_get()`, `state_set()`
- `println()`, `fs_read()`, `fs_write()`, `exit()`
- `el_release()` (ARC cleanup)
The compiler does not use: HTTP, engram, dharma, LLM, crypto, UUID, float arithmetic.
---
## 5. The Long-Term Solution: elvm
### Why a VM Makes Bootstrapping More Auditable
The current bootstrap chain relies on trusting a binary whose source we cannot fully audit by inspection alone. This is the classic "trusting trust" problem (Ken Thompson, 1984). A virtual machine breaks the chain:
- `elc` targets `elvm` bytecode (instead of C)
- `elvm` is a minimal interpreter hand-written in ~500 lines of C
- The hand-written C is small enough to audit completely
- Anyone can compile `elvm.c` with any C compiler
- From there: `elvm` interprets `elc.elvm` → `elc` compiles El → `cc` builds native binaries
The benefit: the trusted base shrinks from "a Mach-O binary" to "500 lines of straightforward C code that anyone can read in an afternoon."
### The elvm Design
A minimal elvm needs:
- A stack or register machine (stack is simpler)
- Instructions: push, pop, add, sub, mul, div, cmp, jump, call, return, load, store
- A string table (El strings are mostly literals)
- A heap for ElList and ElMap
- An FFI table mapping El runtime builtins to C functions
The El compiler would gain a `--target=elvm` flag in `compile_dispatch()`. Codegen would emit bytecode instead of C text. The runtime interface stays the same — builtins map to FFI slots by name.
This is the planned path. It does not exist yet.
---
## 6. Compiler Source Map
| File | Role | Lines |
|------|------|-------|
| `elc-cli.el` | Entry point; imports compiler.el | 7 |
| `el-compiler/src/compiler.el` | Pipeline wiring: lex → parse → codegen. Import resolution, `--emit-header`, `fn main()`. Defines `compile()`, `compile_js()`, `compile_dispatch()`, `resolve_imports()` | 298 |
| `el-compiler/src/lexer.el` | Tokenizer. `lex(source)` → token list. Char helpers, keyword lookup, scan_digits, scan_ident, scan_string, strip_code_comments | 747 |
| `el-compiler/src/parser.el` | Recursive descent parser. `parse(tokens)` → AST. All statement and expression forms | 1071 |
| `el-compiler/src/codegen.el` | C code emitter. `codegen(stmts, source)` → (streams to stdout). Expression codegen, statement codegen, function codegen, type tracking, capability enforcement, temporal type dispatch | 2721 |
| `el-compiler/src/codegen-js.el` | JavaScript backend. `codegen_js(stmts, source)` → JS source | ~500 |
| `el-compiler/runtime/el_runtime.h` | Full runtime API declaration | 755 |
| `el-compiler/runtime/el_runtime.c` | Full runtime implementation | large |
| `el-compiler/runtime/el_runtime.js` | JS runtime | — |
| `elb.el` | Build coordinator. Reads `manifest.el`, walks import graph, compiles modules, links binary. The `.NET`-style incremental build model | 367 |
| `elc-combined.el` | Pre-merged single-file bootstrap edition (for early bootstrap iterations) | large |
| `spec/language.md` | Language specification v1.2.0 | — |
| `dist/platform/elc` | Current bootstrap binary (Mach-O arm64) | — |
---
## 7. Key Decisions and Gotchas
### `target` is a Reserved Keyword
`target` is lexed as the `Target` token kind. It cannot be used as a variable or parameter name anywhere in El source. If you write `fn compile(target: String)`, the parameter name will be tokenized as `Target`, which the parser does not recognize as an `Ident` in parameter position.
**Workaround:** use `tgt`, `dest`, `backend`, or any other name. The compiler source uses `tgt` specifically for this reason. This comes up whenever writing code that handles compilation targets.
### `let x = x + 1` is Let-Rebinding, Not Mutation
El has no mutable variables. `let count = count + 1` re-introduces `count` into the current scope, shadowing the previous binding. At the C level, the codegen tracks declared names and emits plain assignment for subsequent bindings of the same name:
- First `let count = 0` → `el_val_t count = 0;`
- Second `let count = count + 1` → `count = count + 1;`
This means you cannot have two different values named `count` in the same C scope — the second binding overwrites the first. This is by design. Scoped shadowing works correctly because each block (if body, while body, for body) gets its own copy of the `declared` list.
### Arena is Inactive in CLI Mode
The runtime includes an arena allocator designed for long-running server processes. In CLI mode (`elc`, `elb`) the arena is not activated. Memory is managed by ARC (reference counting via `el_retain`/`el_release`). The compiler source explicitly calls `el_release(tokens)` after parsing and `el_release(stmt)` after codegen to prevent memory exhaustion on large source files.
If you are implementing a new runtime or embedding El, be aware that the ARC model expects callers to release values they are done with.
### The `extern fn` / `.elh` Separate Compilation Model
`elb` (the build coordinator) supports separate compilation. When a module changes:
1. `elc --emit-header module.el module.c` compiles the module and writes `module.elh`
2. `module.elh` contains `extern fn` declarations for all public functions
3. Other modules that import `module.el` use the `.elh` header instead of re-parsing the source
The `resolve_imports` function in `compiler.el` checks for a `.elh` file before recursively inlining the `.el` source. If the header exists, it is used (and the `.el` is marked as seen to prevent double-inclusion).
This is important for bootstrap: if you have pre-compiled headers lying around from a broken build, they may shadow updated source. Delete `.elh` files (or use `elb --clean`) when debugging unexpected compilation behavior.
### Import Resolution: Depth-First with Deduplication
`resolve_imports` in `compiler.el`:
1. Walks imports depth-first (dependencies before dependents)
2. Uses `state_set("__elc_imp__:" + path, "1")` to deduplicate: each file is included exactly once
3. Builds the combined source string by concatenating import bodies ahead of the entry file's body
4. If a `.elh` header exists for an import, uses that instead of recursing into the `.el`
The result is one large string that gets passed through `lex` → `parse` → `codegen` as a single unit. The codegen emits forward declarations for all functions before any body, so declaration order within the combined source does not matter.
### `+` Operator Dispatch is Heuristic
El's `+` operator serves double duty: integer addition and string concatenation. The codegen dispatches based on static analysis of the AST:
- If either operand is a `Str` literal → `el_str_concat`
- If both operands are provably `Int` (via `is_int_expr`) → `(a + b)`
- If either operand is a `Call` or `Ident` → `el_str_concat` (conservative fallback)
The `is_int_expr` predicate recurses through the AST: literal `Int`, names in `__int_names` (from `: Int` annotations), known Int-returning builtins, and arithmetic BinOps over Int operands all count as "provably Int."
If you write `let result = some_int_var + 1` and `some_int_var` is not annotated `: Int`, the codegen may emit `el_str_concat` instead of integer addition. Fix by adding `: Int` to the variable declaration.
### `==` Operator Dispatch is Also Heuristic
Similarly, `==` dispatches between `str_eq(a, b)` (string comparison) and `(a == b)` (integer comparison) based on operand types. The codegen tracks Int-typed names in `__int_names`. Two `Ident` operands where both are known Int-typed use `==`; all other Ident-Ident comparisons use `str_eq`.
This means comparing two integer variables that were not annotated `: Int` can silently produce `str_eq` on what are actually integer values — and `str_eq` treats them as `const char*` pointers, producing incorrect results or segfaults.
**Rule:** always annotate variables `: Int` when they will participate in `==` comparisons or `+` arithmetic.
### Capability Kind Enforcement
The codegen classifies programs into three capability tiers based on top-level declarations:
- `cgi` block present → full capability (all primitives allowed)
- `service` block present → restricted (no `llm_call_agentic`, `llm_register_tool`, `dharma_emit`, `dharma_field`)
- Neither → `utility` (no DHARMA, no LLM)
Violations are collected during codegen and emitted as `#error` directives at the bottom of the generated C. The downstream `cc` step then fails with a clear message naming the forbidden call.
### The `__no_block_expr` Parse Guard
When parsing the condition of `if`, `while`, `for`, and `match`, the parser sets `state_set("__no_block_expr", "1")`. This prevents `parse_primary` from treating a `{` as the start of a Map literal — instead it returns `{ "expr": "Nil" }` and the caller sees the `{` and treats it as the block delimiter.
Without this guard, `if a || b { ... }` would recurse into `parse_expr` for `b`, hit `{`, try to parse it as a Map literal, fail to find string keys, loop in error-recovery mode, and hang.
### Codegen Streams Output via `println`
The codegen does not build the output as a string — it calls `println()` for each line as it is emitted. The `compile()` / `compile_js()` / `codegen()` functions return `""`. Output goes to stdout.
This design avoids O(n²) string concatenation for large programs. It also means you cannot capture the compiler's output in a variable within El itself — you must redirect stdout at the OS level (`elc source.el > output.c`).
When writing to a file, `elc` detects the output path argument, redirects C's `stdout` to the file (via `freopen` in the runtime), and the `println` calls go there instead.
+2080 -154
View File
File diff suppressed because it is too large Load Diff
Vendored Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+827 -176
View File
File diff suppressed because it is too large Load Diff
+93 -2
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,36 @@ el_val_t readline(void) {
return el_wrap_str(el_strdup(buf));
}
/* ── stdout redirect helpers ─────────────────────────────────────────────── *
* Used by elc post-processing (--minify, --obfuscate): capture codegen *
* output into a temp file, then pass it to the external tool. */
static int _stdout_saved_fd = -1;
/* stdout_to_file(path) — redirect stdout to <path>. Returns 1 on success. */
el_val_t stdout_to_file(el_val_t pathv) {
const char* path = EL_CSTR(pathv);
if (!path || !*path) return (el_val_t)(int64_t)0;
fflush(stdout);
_stdout_saved_fd = dup(STDOUT_FILENO);
if (_stdout_saved_fd < 0) return (el_val_t)(int64_t)0;
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { close(_stdout_saved_fd); _stdout_saved_fd = -1; return (el_val_t)(int64_t)0; }
dup2(fd, STDOUT_FILENO);
close(fd);
return (el_val_t)(int64_t)1;
}
/* stdout_restore() — restore stdout from the saved fd. Returns 1 on success. */
el_val_t stdout_restore(void) {
if (_stdout_saved_fd < 0) return (el_val_t)(int64_t)0;
fflush(stdout);
dup2(_stdout_saved_fd, STDOUT_FILENO);
close(_stdout_saved_fd);
_stdout_saved_fd = -1;
return (el_val_t)(int64_t)1;
}
/* ── String builtins ─────────────────────────────────────────────────────── */
el_val_t el_str_concat(el_val_t av, el_val_t bv) {
@@ -1876,6 +1907,61 @@ el_val_t exec_capture(el_val_t cmdv) {
return el_wrap_str(b.buf);
}
// exec — run a shell command via /bin/sh, capture stdout, return as String.
// Times out after 30 seconds. Returns "" on any error.
// El name: exec(cmd) -> String
el_val_t exec(el_val_t cmdv) {
const char* cmd = EL_CSTR(cmdv);
if (!cmd || !*cmd) return el_wrap_str(el_strdup(""));
/* Build a time-limited command: wrap with timeout(1) if available,
* otherwise rely on the 30s read loop guard below. We use the simple
* popen approach with a deadline measured by wall clock so the caller
* is never blocked indefinitely. */
FILE* f = popen(cmd, "r");
if (!f) return el_wrap_str(el_strdup(""));
JsonBuf b; jb_init(&b);
char buf[4096];
/* 30-second wall-clock deadline */
time_t deadline = time(NULL) + 30;
while (time(NULL) < deadline) {
if (fgets(buf, sizeof(buf), f) == NULL) break;
jb_puts(&b, buf);
}
pclose(f);
return el_wrap_str(b.buf);
}
// exec_bg — run a shell command in background, return PID as String.
// The child process runs independently; the caller is not blocked.
// Returns "" on fork failure.
// El name: exec_bg(cmd) -> String
el_val_t exec_bg(el_val_t cmdv) {
const char* cmd = EL_CSTR(cmdv);
if (!cmd || !*cmd) return el_wrap_str(el_strdup(""));
pid_t pid = fork();
if (pid < 0) {
/* fork failed */
return el_wrap_str(el_strdup(""));
}
if (pid == 0) {
/* child: detach from parent's stdio, exec via shell */
setsid();
int devnull = open("/dev/null", O_RDWR);
if (devnull >= 0) {
dup2(devnull, STDIN_FILENO);
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
execl("/bin/sh", "sh", "-c", cmd, (char*)NULL);
_exit(127);
}
/* parent: convert pid to string and return immediately */
char pidbuf[32];
snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid);
return el_wrap_str(el_strdup(pidbuf));
}
el_val_t fs_list(el_val_t pathv) {
const char* path = EL_CSTR(pathv);
el_val_t lst = el_list_empty();
@@ -2939,8 +3025,13 @@ static int looks_like_string(el_val_t v) {
const unsigned char* s = (const unsigned char*)p;
for (int i = 0; i < 16; i++) {
unsigned char c = s[i];
if (c == '\0') return i > 0; /* terminated string */
if (c < 0x09 || (c > 0x0d && c < 0x20) || c >= 0x7f) return 0;
if (c == '\0') return 1; /* terminated string (empty string is still a valid string) */
/* Reject C0 control chars (non-whitespace), allow UTF-8 high bytes.
* 0x09-0x0d = tab/newline/cr/vt/ff (whitespace, OK)
* 0x20-0x7e = printable ASCII (OK)
* 0x7f = DEL (reject)
* 0x80-0xff = UTF-8 continuation/lead bytes (OK for multi-byte chars) */
if (c < 0x09 || (c > 0x0d && c < 0x20) || c == 0x7f) return 0;
}
return 1; /* 16+ printable bytes — call it a string */
}
+4
View File
@@ -79,6 +79,8 @@ extern "C" {
void println(el_val_t s);
void print(el_val_t s);
el_val_t readline(void);
el_val_t stdout_to_file(el_val_t path); /* redirect println to a file */
el_val_t stdout_restore(void); /* restore stdout after capture */
/* ── String builtins ─────────────────────────────────────────────────────── */
@@ -742,6 +744,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);
+370
View File
@@ -522,6 +522,328 @@ function math_sin(f) { return Math.sin(f); }
function math_cos(f) { return Math.cos(f); }
function math_pi() { return Math.PI; }
// ── DOM bridge (browser-only) ──────────────────────────────────────────────
//
// These functions wrap the browser DOM API. Each throws a descriptive error
// when called from a Node environment, mirroring the pattern used by fs_*
// in browser mode.
function _ensureBrowser(name) {
if (IS_NODE) {
throw new Error(`${name}: not supported in Node runtime — DOM is browser-only`);
}
}
function dom_get_element(id) {
_ensureBrowser('dom_get_element');
return document.getElementById(String(id));
}
function dom_get_value(el) {
_ensureBrowser('dom_get_value');
return el == null ? '' : String(el.value ?? '');
}
function dom_set_value(el, v) {
_ensureBrowser('dom_set_value');
if (el != null) el.value = String(v);
}
function dom_get_text(el) {
_ensureBrowser('dom_get_text');
return el == null ? '' : String(el.textContent ?? '');
}
function dom_set_text(el, text) {
_ensureBrowser('dom_set_text');
if (el != null) el.textContent = String(text);
}
function dom_set_prop(el, prop, val) {
_ensureBrowser('dom_set_prop');
if (el != null) el[String(prop)] = val;
}
function dom_get_prop(el, prop) {
_ensureBrowser('dom_get_prop');
if (el == null) return null;
const v = el[String(prop)];
return v === undefined ? null : v;
}
function dom_set_style(el, prop, val) {
_ensureBrowser('dom_set_style');
if (el != null) el.style[String(prop)] = String(val);
}
function dom_add_class(el, cls) {
_ensureBrowser('dom_add_class');
if (el != null) el.classList.add(String(cls));
}
function dom_remove_class(el, cls) {
_ensureBrowser('dom_remove_class');
if (el != null) el.classList.remove(String(cls));
}
function dom_show(el) {
_ensureBrowser('dom_show');
if (el != null) el.style.display = '';
}
function dom_hide(el) {
_ensureBrowser('dom_hide');
if (el != null) el.style.display = 'none';
}
function dom_listen(el, event, handler) {
_ensureBrowser('dom_listen');
if (el != null) el.addEventListener(String(event), handler);
}
function dom_query(selector) {
_ensureBrowser('dom_query');
return document.querySelector(String(selector));
}
function dom_query_all(selector) {
_ensureBrowser('dom_query_all');
return Array.from(document.querySelectorAll(String(selector)));
}
function dom_create(tag) {
_ensureBrowser('dom_create');
return document.createElement(String(tag));
}
function dom_append(parent, child) {
_ensureBrowser('dom_append');
if (parent != null && child != null) parent.appendChild(child);
}
function dom_remove(el) {
_ensureBrowser('dom_remove');
if (el != null) el.remove();
}
function dom_is_null(el) {
return el === null || el === undefined;
}
// ── Extended DOM API (browser-only) ───────────────────────────────────────
function dom_set_attr(el, attr, val) {
_ensureBrowser('dom_set_attr');
if (el != null) el.setAttribute(String(attr), String(val));
}
function dom_get_attr(el, attr) {
_ensureBrowser('dom_get_attr');
if (el == null) return '';
return el.getAttribute(String(attr)) ?? '';
}
function dom_remove_attr(el, attr) {
_ensureBrowser('dom_remove_attr');
if (el != null) el.removeAttribute(String(attr));
}
function dom_set_html(el, html) {
_ensureBrowser('dom_set_html');
if (el != null) el.innerHTML = String(html);
}
function dom_get_html(el) {
_ensureBrowser('dom_get_html');
return el == null ? '' : String(el.innerHTML ?? '');
}
function dom_get_parent(el) {
_ensureBrowser('dom_get_parent');
return el == null ? null : (el.parentElement ?? null);
}
function dom_contains_class(el, cls) {
_ensureBrowser('dom_contains_class');
if (el == null) return false;
return el.classList.contains(String(cls));
}
function dom_get_checked(el) {
_ensureBrowser('dom_get_checked');
return el == null ? false : Boolean(el.checked);
}
function dom_set_checked(el, val) {
_ensureBrowser('dom_set_checked');
if (el != null) el.checked = Boolean(val);
}
// ── Timer API (browser + Node) ─────────────────────────────────────────────
function set_timeout(ms, cb) {
if (typeof setTimeout === 'undefined') {
throw new Error('set_timeout: setTimeout not available in this environment');
}
setTimeout(cb, ms | 0);
}
function set_interval(ms, cb) {
if (typeof setInterval === 'undefined') {
throw new Error('set_interval: setInterval not available in this environment');
}
return setInterval(cb, ms | 0);
}
function clear_interval(handle) {
if (typeof clearInterval !== 'undefined') clearInterval(handle);
}
// ── Local storage (browser-only) ───────────────────────────────────────────
function local_storage_get(key) {
_ensureBrowser('local_storage_get');
return localStorage.getItem(String(key)) ?? '';
}
function local_storage_set(key, val) {
_ensureBrowser('local_storage_set');
localStorage.setItem(String(key), String(val));
}
function local_storage_remove(key) {
_ensureBrowser('local_storage_remove');
localStorage.removeItem(String(key));
}
// ── Window location / navigation (browser-only) ────────────────────────────
function window_location() {
_ensureBrowser('window_location');
return window.location.href;
}
function window_redirect(url) {
_ensureBrowser('window_redirect');
window.location.href = String(url);
}
function window_on_load(cb) {
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', cb);
} else if (typeof window !== 'undefined') {
window.addEventListener('load', cb);
}
// In Node: no-op
}
// ── console_log (explicit debug log, distinct from println) ────────────────
function console_log(msg) {
// eslint-disable-next-line no-console
console.log(String(msg));
}
// ── Window export helpers ──────────────────────────────────────────────────
//
// Expose El functions to the browser's global scope so they can be called
// from inline event handlers (onclick="increment()") or by external JS.
// In Node mode, writes to globalThis so the same pattern works in tests.
function window_set(name, val) {
if (typeof window !== 'undefined') {
window[String(name)] = val;
} else if (typeof globalThis !== 'undefined') {
globalThis[String(name)] = val;
}
}
function window_get(name) {
if (typeof window !== 'undefined') {
const v = window[String(name)];
return v === undefined ? null : v;
}
return null;
}
// ── Promise helpers ────────────────────────────────────────────────────────
//
// Third-party APIs often return Promises but are not El @async functions.
// These helpers let El programs chain .then / .catch without needing
// native_js, and without requiring the callee to be @async.
function promise_then(p, cb) {
return Promise.resolve(p).then(cb);
}
function promise_catch(p, cb) {
return Promise.resolve(p).catch(cb);
}
function promise_resolve(val) {
return Promise.resolve(val);
}
function promise_reject(msg) {
return Promise.reject(new Error(String(msg)));
}
// ── Object / Array utilities ───────────────────────────────────────────────
//
// Structural operations on Any-typed JS values. These complement the
// El map/list primitives for interop with third-party library objects.
function object_assign(target, source) {
return Object.assign(Object.assign({}, target), source);
}
function object_keys(obj) {
if (obj === null || obj === undefined) return [];
return Object.keys(obj);
}
function object_values(obj) {
if (obj === null || obj === undefined) return [];
return Object.values(obj);
}
function json_deep_clone(obj) {
if (obj === null || obj === undefined) return null;
return JSON.parse(JSON.stringify(obj));
}
function array_from(iterable) {
if (iterable === null || iterable === undefined) return [];
return Array.from(iterable);
}
function type_of(val) {
return typeof val;
}
function instanceof_check(val, constructor_name) {
if (typeof globalThis[constructor_name] === 'function') {
return val instanceof globalThis[constructor_name];
}
return false;
}
// ── native_js escape hatch ─────────────────────────────────────────────────
//
// Evaluate arbitrary JS from El source. Intended for calling third-party
// browser libraries (Supabase, Stripe, etc.) until proper El bindings exist.
// Use sparingly — this bypasses El's type system entirely.
function native_js(code) {
// eslint-disable-next-line no-eval
return eval(String(code));
}
function native_js_call(obj, method, args) {
if (obj == null) throw new Error('native_js_call: object is null');
return obj[String(method)](...(Array.isArray(args) ? args : []));
}
// ── Stubs for not-yet-supported features ───────────────────────────────────
//
// These compile but throw when called. See spec/codegen-js.md §7.
@@ -632,6 +954,31 @@ const __el = {
// Float / math
float_to_str, int_to_float, float_to_int, format_float, decimal_round,
str_to_float, math_sqrt, math_log, math_ln, math_sin, math_cos, math_pi,
// DOM bridge (browser-only)
dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,
dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,
dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,
dom_append, dom_remove, dom_is_null,
// Extended DOM
dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,
dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,
// Timers
set_timeout, set_interval, clear_interval,
// Local storage
local_storage_get, local_storage_set, local_storage_remove,
// Window location
window_location, window_redirect, window_on_load,
// Debug
console_log,
// Window export helpers
window_set, window_get,
// Promise helpers
promise_then, promise_catch, promise_resolve, promise_reject,
// Object / Array utilities
object_assign, object_keys, object_values, json_deep_clone,
array_from, type_of, instanceof_check,
// native_js escape hatch
native_js, native_js_call,
// CGI / DHARMA / Engram / LLM (stubs)
el_cgi_init,
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
@@ -676,4 +1023,27 @@ export {
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
engram_node, engram_search, engram_activate,
llm_call, llm_call_system,
// DOM bridge
dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,
dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,
dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,
dom_append, dom_remove, dom_is_null,
// Extended DOM
dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,
dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,
// Timers
set_timeout, set_interval, clear_interval,
// Local storage
local_storage_get, local_storage_set, local_storage_remove,
// Window location
window_location, window_redirect, window_on_load,
// Debug
console_log,
// Window / native_js
window_set, window_get, native_js, native_js_call,
// Promise helpers
promise_then, promise_catch, promise_resolve, promise_reject,
// Object / Array utilities
object_assign, object_keys, object_values, json_deep_clone,
array_from, type_of, instanceof_check,
};
File diff suppressed because it is too large Load Diff
+756
View File
@@ -0,0 +1,756 @@
/*
* el_runtime.h El language C runtime header
*
* Declares all built-in functions available to compiled El programs.
* Include this in every generated .c file.
*
* Value model:
* All El values are represented as el_val_t (= int64_t).
* On 64-bit systems a pointer fits in int64_t.
* String values are cast: (el_val_t)(uintptr_t)"hello"
* Integer values are stored directly.
* This lets arithmetic work naturally while still passing strings around.
*
* Type conventions (El -> C):
* String -> el_val_t (holds const char* via uintptr_t cast)
* Int -> el_val_t
* Bool -> el_val_t (0 = false, nonzero = true)
* Any -> el_val_t
* Void -> void
*
* Macros for convenience:
* EL_STR(s) cast string literal to el_val_t
* EL_CSTR(v) cast el_val_t back to const char*
* EL_INT(v) identity el_val_t is already int64_t
*
* Link requirements:
* -lcurl required for the HTTP client (http_get, http_post, llm_*).
* -lpthread required for the HTTP server (one detached thread per
* connection, capped at 64 concurrent).
* -loqs optional; required only when liboqs is installed and the
* pq_* / sha3_256_hex entry points are needed. Detected at
* compile time via __has_include(<oqs/oqs.h>).
* -lcrypto optional; pulled in alongside -loqs. Used for X25519 in
* pq_hybrid_* and HKDF-SHA256 derivation.
*
* Canonical compile command:
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
*
* With liboqs (post-quantum stack):
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
*/
#pragma once
#include <stdint.h>
#include <stdlib.h>
typedef int64_t el_val_t;
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
#define EL_INT(v) (v)
#define EL_NULL ((el_val_t)0)
/* Float values share the el_val_t (int64) slot via a bit-cast.
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
* underlying bits represent the IEEE 754 double. Float-aware builtins
* (math, format, json) round-trip via these helpers. */
static inline double el_to_float(el_val_t v) {
union { int64_t i; double f; } u;
u.i = (int64_t)v;
return u.f;
}
static inline el_val_t el_from_float(double f) {
union { double f; int64_t i; } u;
u.f = f;
return (el_val_t)u.i;
}
#ifdef __cplusplus
extern "C" {
#endif
/* ── I/O ──────────────────────────────────────────────────────────────────── */
void println(el_val_t s);
void print(el_val_t s);
el_val_t readline(void);
/* ── String builtins ─────────────────────────────────────────────────────── */
el_val_t el_str_concat(el_val_t a, el_val_t b);
el_val_t str_eq(el_val_t a, el_val_t b);
el_val_t str_starts_with(el_val_t s, el_val_t prefix);
el_val_t str_ends_with(el_val_t s, el_val_t suffix);
el_val_t str_len(el_val_t s);
el_val_t str_concat(el_val_t a, el_val_t b);
el_val_t int_to_str(el_val_t n);
el_val_t str_to_int(el_val_t s);
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
el_val_t str_contains(el_val_t s, el_val_t sub);
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
el_val_t str_to_upper(el_val_t s);
el_val_t str_to_lower(el_val_t s);
el_val_t str_trim(el_val_t s);
/* ── Math ────────────────────────────────────────────────────────────────── */
el_val_t el_abs(el_val_t n);
el_val_t el_max(el_val_t a, el_val_t b);
el_val_t el_min(el_val_t a, el_val_t b);
/* ── Refcount (ARC) ──────────────────────────────────────────────────────────
* Lists and Maps carry a refcount. Strings and ints do not el_retain and
* el_release are safe no-ops on non-refcounted values (they sniff a magic
* header at offset 0 and only act if the magic matches).
*
* Codegen emits these at let-binding shadowing, function entry (params), and
* function exit (locals other than the returned value). The refcount lets
* el_list_append and el_map_set mutate in place when uniquely owned (cheap)
* and copy-on-write when shared (preserves persistent semantics across
* accumulator patterns in the compiler itself). */
void el_retain(el_val_t v);
void el_release(el_val_t v);
/* ── List ────────────────────────────────────────────────────────────────── */
el_val_t el_list_new(el_val_t count, ...);
el_val_t el_list_len(el_val_t list);
el_val_t el_list_get(el_val_t list, el_val_t index);
el_val_t el_list_append(el_val_t list, el_val_t elem);
el_val_t el_list_empty(void);
el_val_t el_list_clone(el_val_t list);
/* ── Map ─────────────────────────────────────────────────────────────────── */
el_val_t el_map_new(el_val_t pair_count, ...);
el_val_t el_get_field(el_val_t map, el_val_t key);
el_val_t el_map_get(el_val_t map, el_val_t key);
el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value);
/* ── HTTP ─────────────────────────────────────────────────────────────────── */
el_val_t http_get(el_val_t url);
el_val_t http_post(el_val_t url, el_val_t body);
el_val_t http_post_json(el_val_t url, el_val_t json_body);
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
el_val_t http_delete(el_val_t url);
void http_serve(el_val_t port, el_val_t handler);
void http_set_handler(el_val_t name);
/* HTTP server v2 ─────────────────────────────────────────────────────────────
* Same dispatch model as http_serve, but the handler signature is widened:
*
* el_val_t handler(method, path, headers_map, body)
*
* `headers_map` is an ElMap from lowercased header name header value (both
* Strings). Repeated headers are joined with ", " per RFC 7230.
*
* Response value: the handler may return either
* (a) a plain body string same auto-content-type / 200-OK behaviour as
* http_serve (3-arg) or
* (b) a response envelope built with `http_response(status, headers_json,
* body)`. The runtime detects the envelope discriminator
* `"el_http_response":1` at the start of the returned string and
* unpacks status / headers / body before sending.
*
* The 3-arg http_serve(port, handler) remains supported unchanged for
* existing handlers (e.g. products/web/server.el): it dispatches with
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
void http_serve_v2(el_val_t port, el_val_t handler);
void http_set_handler_v2(el_val_t name);
/* Build an HTTP response envelope. `headers_json` should be a JSON object
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
* returned string carries the discriminator `{"el_http_response":1,...}`
* which the runtime's send-path detects and unpacks. Detection happens
* uniformly inside http_send_response, so a 3-arg handler may also return
* an envelope. The 3-arg variant remains documented as a fixed 200-OK
* auto-content-type contract for legacy handlers that return plain bodies. */
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
* 60000ms). Read lazily on first use, so setting the env var any time before
* the first http_* call is sufficient. */
/* Streaming variants — write the response body straight to a file via
* libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string
* wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive
* embedded NUL bytes that would truncate a strlen()-based code path.
*
* Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same
* `headers_map` shape as http_post_with_headers (ElMap of 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
+397 -95
View File
@@ -23,26 +23,26 @@
fn js_escape(s: String) -> String {
let chars: [String] = native_string_chars(s)
let total: Int = native_list_len(chars)
let out = ""
let parts: [String] = native_list_empty()
let i = 0
while i < total {
let ch: String = native_list_get(chars, i)
if ch == "\"" {
let out = out + "\\\""
let parts = native_list_append(parts, "\\\"")
} else {
if ch == "\\" {
let out = out + "\\\\"
let parts = native_list_append(parts, "\\\\")
} else {
if ch == "\n" {
let out = out + "\\n"
let parts = native_list_append(parts, "\\n")
} else {
if ch == "\r" {
let out = out + "\\r"
let parts = native_list_append(parts, "\\r")
} else {
if ch == "\t" {
let out = out + "\\t"
let parts = native_list_append(parts, "\\t")
} else {
let out = out + ch
let parts = native_list_append(parts, ch)
}
}
}
@@ -50,7 +50,7 @@ fn js_escape(s: String) -> String {
}
let i = i + 1
}
out
str_join(parts, "")
}
fn js_str_lit(s: String) -> String {
@@ -86,6 +86,57 @@ fn js_binop(op: String) -> String {
op
}
// Known El runtime method names
//
// These are the method shortforms exported by el_runtime.js and used by the
// El C-backend convention of `obj.method(args)` -> `method(obj, args)`.
// Any method name NOT in this set is treated as a native JS method call on the
// receiver object, emitting `obj.method(args)` directly.
//
// This is the mechanism that makes `client.auth.signInWithOtp(payload)` work
// without `native_js_call`: the receiver is Any-typed, the method is unknown
// to El, so codegen emits the JS call directly.
fn js_is_el_method(name: String) -> Bool {
if str_eq(name, "append") { return true }
if str_eq(name, "len") { return true }
if str_eq(name, "get") { return true }
if str_eq(name, "map_get") { return true }
if str_eq(name, "map_set") { return true }
false
}
// Async function tracking
//
// Functions decorated with @async are recorded here. Any call to a known-async
// builtin (http_get, http_post, http_post_json) or to a user-declared @async
// function gets an `await` prefix in generated JS.
//
// Known-async builtins these return Promise<T> in el_runtime.js.
fn js_is_async_builtin(name: String) -> Bool {
if str_eq(name, "http_get") { return true }
if str_eq(name, "http_post") { return true }
if str_eq(name, "http_post_json") { return true }
if str_eq(name, "http_get_with_headers") { return true }
if str_eq(name, "http_post_with_headers") { return true }
false
}
fn js_register_async_fn(name: String) -> Bool {
let csv: String = state_get("__js_async_fns")
if str_eq(csv, "") { csv = "," }
let key: String = "," + name + ","
if str_contains(csv, key) { return true }
state_set("__js_async_fns", csv + name + ",")
return true
}
fn js_is_async_fn(name: String) -> Bool {
let csv: String = state_get("__js_async_fns")
if str_eq(csv, "") { return false }
return str_contains(csv, "," + name + ",")
}
// Int-name tracking (mirrors codegen.el)
fn js_is_int_name(name: String) -> Bool {
@@ -365,34 +416,50 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
let arity: Int = native_list_len(args)
let func_kind: String = func["expr"]
let args_c = ""
let args_parts: [String] = native_list_empty()
let i = 0
while i < arity {
let arg = native_list_get(args, i)
let arg_c: String = js_cg_expr(arg)
if i > 0 {
let args_c = args_c + ", "
}
let args_c = args_c + arg_c
let args_parts = native_list_append(args_parts, arg_c)
let i = i + 1
}
let args_c: String = str_join(args_parts, ", ")
if func_kind == "Ident" {
let fn_name: String = func["name"]
return fn_name + "(" + args_c + ")"
let call_expr: String = fn_name + "(" + args_c + ")"
if js_is_async_builtin(fn_name) {
return "await " + call_expr
}
if js_is_async_fn(fn_name) {
return "await " + call_expr
}
return call_expr
}
if func_kind == "Field" {
// El's `obj.method(args)` becomes `method(obj, args)` same
// convention as the C backend. The runtime exports method
// shortforms (append, len, get, map_get, map_set) that match.
let obj = func["object"]
let field: String = func["field"]
let obj_c: String = js_cg_expr(obj)
if arity > 0 {
return field + "(" + obj_c + ", " + args_c + ")"
// If the method is a known El runtime shortform, keep the El
// convention: `method(obj, args)`. This preserves backward
// compatibility with list.append(x), map.map_get(k), etc.
if js_is_el_method(field) {
if arity > 0 {
return field + "(" + obj_c + ", " + args_c + ")"
}
return field + "(" + obj_c + ")"
}
return field + "(" + obj_c + ")"
// Unknown method emit as a native JS method call on the
// receiver. This handles Any-typed values (third-party library
// objects, DOM elements, Promises, etc.) without requiring
// native_js_call. Example: `client.auth.signInWithOtp(payload)`
// emits `client["auth"].signInWithOtp(args_c)`.
if arity > 0 {
return obj_c + "." + field + "(" + args_c + ")"
}
return obj_c + "." + field + "()"
}
let fn_c: String = js_cg_expr(func)
@@ -400,22 +467,39 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
}
if kind == "Field" {
// El's `obj.foo` becomes JS `obj["foo"]` works on plain objects
// (maps) and on JS objects with prototype. el_get_field is a
// runtime helper for callers that want EL_NULL on missing keys.
// El's `obj.foo` becomes JS `obj["foo"]` direct bracket access.
// This works for plain El map objects AND for real JS objects with
// prototype-inherited properties (DOM elements, third-party library
// objects, Promises, etc.). el_get_field used hasOwnProperty which
// silently returned null for inherited props, breaking e.g. client.auth.
//
// Nil-propagation: `obj?.foo` emits `(obj)?.["foo"] ?? null`.
let obj = expr["object"]
let field: String = expr["field"]
let obj_kind: String = obj["expr"]
if str_eq(obj_kind, "Try") {
let inner = obj["inner"]
let inner_c: String = js_cg_expr(inner)
return "(" + inner_c + ")?.[" + js_str_lit(field) + "] ?? null"
}
let obj_c: String = js_cg_expr(obj)
return "el_get_field(" + obj_c + ", " + js_str_lit(field) + ")"
return obj_c + "[" + js_str_lit(field) + "]"
}
if kind == "Index" {
// Map vs list dispatch on the index expression kind, same as C.
// If the object is a Try (nil-propagation), use JS optional indexing.
let obj = expr["object"]
let idx = expr["index"]
let obj_c: String = js_cg_expr(obj)
let idx_c: String = js_cg_expr(idx)
let idx_kind: String = idx["expr"]
let obj_kind: String = obj["expr"]
if str_eq(obj_kind, "Try") {
let inner = obj["inner"]
let inner_c: String = js_cg_expr(inner)
return "(" + inner_c + ")?.[" + idx_c + "] ?? null"
}
if str_eq(idx_kind, "Str") {
return "el_get_field(" + obj_c + ", " + idx_c + ")"
}
@@ -426,41 +510,41 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
let elems = expr["elems"]
let n: Int = native_list_len(elems)
if n == 0 { return "[]" }
let items = ""
let items_parts: [String] = native_list_empty()
let i = 0
while i < n {
let elem = native_list_get(elems, i)
let elem_c: String = js_cg_expr(elem)
if i > 0 {
let items = items + ", "
}
let items = items + elem_c
let items_parts = native_list_append(items_parts, elem_c)
let i = i + 1
}
return "[" + items + "]"
return "[" + str_join(items_parts, ", ") + "]"
}
if kind == "Map" {
let pairs = expr["pairs"]
let n: Int = native_list_len(pairs)
if n == 0 { return "{}" }
let items = ""
let items_parts: [String] = native_list_empty()
let i = 0
while i < n {
let pair = native_list_get(pairs, i)
let key: String = pair["key"]
let val = pair["value"]
let val_c: String = js_cg_expr(val)
if i > 0 {
let items = items + ", "
}
let items = items + js_str_lit(key) + ": " + val_c
let items_parts = native_list_append(items_parts, js_str_lit(key) + ": " + val_c)
let i = i + 1
}
return "{" + items + "}"
return "{" + str_join(items_parts, ", ") + "}"
}
if kind == "Try" {
// Postfix `?` nil-propagation guard.
// When used as `expr?.field` the Field handler above intercepts and
// emits `(expr)?.["field"]`. Here, a bare `expr?` (not followed by
// field/index access) passes through to the inner expression unchanged
// (it acts as an identity but marks the value as "nil-propagating" for
// its caller). This matches the C backend's current behavior.
let inner = expr["inner"]
return js_cg_expr(inner)
}
@@ -478,6 +562,13 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
return js_cg_match(expr)
}
// Lambda (anonymous function literal): fn(params) -> RetType { body }
// Emitted as a JS arrow function expression: (params) => { body }.
// Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... })
if kind == "Lambda" {
return js_cg_lambda(expr)
}
"null"
}
@@ -505,7 +596,8 @@ fn js_cg_match(expr: Map<String, Any>) -> String {
let subj_c: String = js_cg_expr(subject)
let id: String = js_next_match_id()
let subj_var: String = "_match_subj_" + id
let out: String = "((" + subj_var + ") => { "
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, "((" + subj_var + ") => { ")
let n: Int = native_list_len(arms)
let i = 0
while i < n {
@@ -515,28 +607,36 @@ fn js_cg_match(expr: Map<String, Any>) -> String {
let pkind: String = pat["pattern"]
let body_c: String = js_cg_expr(body)
if str_eq(pkind, "Wildcard") {
let out = out + "return (" + body_c + "); "
let parts = native_list_append(parts, "return (" + body_c + "); ")
} else {
if str_eq(pkind, "Binding") {
let bname: String = pat["name"]
let out = out + "{ const " + bname + " = " + subj_var + "; return (" + body_c + "); } "
let parts = native_list_append(parts, "{ const " + bname + " = " + subj_var + "; return (" + body_c + "); } ")
} else {
if str_eq(pkind, "LitInt") {
let v: String = pat["value"]
let out = out + "if (" + subj_var + " === " + v + ") return (" + body_c + "); "
let parts = native_list_append(parts, "if (" + subj_var + " === " + v + ") return (" + body_c + "); ")
} else {
if str_eq(pkind, "LitStr") {
let v: String = pat["value"]
let out = out + "if (str_eq(" + subj_var + ", " + js_str_lit(v) + ")) return (" + body_c + "); "
let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", " + js_str_lit(v) + ")) return (" + body_c + "); ")
} else {
if str_eq(pkind, "LitBool") {
let v: String = pat["value"]
let bv = "false"
if str_eq(v, "true") { let bv = "true" }
let out = out + "if (" + subj_var + " === " + bv + ") return (" + body_c + "); "
let parts = native_list_append(parts, "if (" + subj_var + " === " + bv + ") return (" + body_c + "); ")
} else {
// unknown pattern wildcard
let out = out + "return (" + body_c + "); "
if str_eq(pkind, "Variant") {
// Enum::Variant patterns El enums compile to plain
// strings (the variant name) or ints. Match the subject
// against the variant name string.
let variant: String = pat["variant"]
let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", " + js_str_lit(variant) + ")) return (" + body_c + "); ")
} else {
// unknown pattern wildcard
let parts = native_list_append(parts, "return (" + body_c + "); ")
}
}
}
}
@@ -544,8 +644,67 @@ fn js_cg_match(expr: Map<String, Any>) -> String {
}
let i = i + 1
}
let out = out + "return null; })(" + subj_c + ")"
out
let parts = native_list_append(parts, "return null; })(" + subj_c + ")")
str_join(parts, "")
}
// Lambda codegen
//
// Anonymous function literals: fn(params) -> RetType { body }
//
// Strategy: emit the lambda as a hoisted JS function declaration with a
// generated name (__lambda_N), then return the name as the expression value.
// This works because JS function declarations are hoisted within their scope,
// so the generated name is valid at any use site within the same function or
// module. The emitted code looks like:
//
// function __lambda_1(event) { dom_hide(spinner); }
// ...
// dom_listen(btn, "click", __lambda_1);
//
// This approach is clean, debuggable, and avoids any need for a string-buffer
// mode in the codegen.
fn js_next_lambda_id() -> String {
let csv: String = state_get("__js_lambda_counter")
let n = 0
if !str_eq(csv, "") {
let n = str_to_int(csv)
}
let n = n + 1
state_set("__js_lambda_counter", native_int_to_str(n))
native_int_to_str(n)
}
fn js_cg_lambda(expr: Map<String, Any>) -> String {
let params = expr["params"]
let body = expr["body"]
let ret_type: String = expr["ret_type"]
let id: String = js_next_lambda_id()
let lambda_name: String = "__lambda_" + id
let params_str: String = js_params_str(params)
// Emit the function definition immediately into the output stream.
// It will appear before the statement containing this expression.
js_emit_line("function " + lambda_name + "(" + params_str + ") {")
let decl = native_list_empty()
let np: Int = native_list_len(params)
let pi = 0
while pi < np {
let param = native_list_get(params, pi)
let pname: String = param["name"]
let decl = native_list_append(decl, pname)
let pi = pi + 1
}
let body_xformed = body
if !str_eq(ret_type, "Void") {
let body_xformed = js_transform_implicit_return(body)
}
js_build_int_names_for_params(params)
js_cg_stmts(body_xformed, " ", decl)
js_emit_line("}")
js_emit_blank()
// Return the function name as the expression value.
lambda_name
}
// Variable scope tracking
@@ -653,6 +812,27 @@ fn js_cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [St
if kind == "TypeDef" { return declared }
if kind == "EnumDef" { return declared }
if kind == "Import" { return declared }
if kind == "TryCatch" {
let try_body = stmt["try_body"]
let catch_name: String = stmt["catch_name"]
let catch_body = stmt["catch_body"]
js_emit_line(indent + "try {")
js_cg_stmts(try_body, indent + " ", native_list_clone(declared))
js_emit_line(indent + "} catch (" + catch_name + ") {")
js_cg_stmts(catch_body, indent + " ", native_list_clone(declared))
js_emit_line(indent + "}")
return declared
}
// ExternFn: the function exists in the JS environment (loaded via <script>
// tag or the module context). Emit a comment so the generated file is
// self-documenting, but no JS function body the implementation is external.
if kind == "ExternFn" {
let ename: String = stmt["name"]
js_emit_line(indent + "// extern fn " + ename + " — provided by the JS environment")
return declared
}
if kind == "CgiBlock" {
// CGI blocks compile to a no-op + warning comment in JS target.
// The runtime cgi identity is server-side; UI code is not a CGI
@@ -696,14 +876,7 @@ fn js_strip_outer_parens(s: String) -> String {
let i = i + 1
}
if balanced {
let inner = ""
let j = 1
while j < n - 1 {
let ch: String = native_list_get(chars, j)
let inner = inner + ch
let j = j + 1
}
return inner
return str_slice(s, 1, n - 1)
}
}
}
@@ -759,18 +932,15 @@ fn js_cg_stmts(stmts: [Map<String, Any>], indent: String, declared: [String]) ->
fn js_params_str(params: [Map<String, Any>]) -> String {
let n: Int = native_list_len(params)
if n == 0 { return "" }
let out = ""
let parts: [String] = native_list_empty()
let i = 0
while i < n {
let param = native_list_get(params, i)
let name: String = param["name"]
if i > 0 {
let out = out + ", "
}
let out = out + name
let parts = native_list_append(parts, name)
let i = i + 1
}
out
str_join(parts, ", ")
}
// Same implicit-return transform as the C backend.
@@ -802,16 +972,30 @@ fn js_cg_fn(stmt: Map<String, Any>) -> Void {
let params = stmt["params"]
let body = stmt["body"]
let ret_type: String = stmt["ret_type"]
let decorator: String = stmt["decorator"]
let params_str: String = js_params_str(params)
js_build_int_names_for_params(params)
// Special-case `fn main` emit as a regular function and call it
// at module bottom (after all top-level statements). This matches
// the C backend's behavior where `fn main` is the entry point.
if fn_name == "main" {
js_emit_line("function main(" + params_str + ") {")
// Detect @async decorator emit `async function` and register the name
// so call sites for this function get `await` prefixed automatically.
// When the decorator field is absent, el_get_field returns null; str_eq
// handles null safely (returns false), so no special nil-check is needed.
if str_eq(decorator, "async") {
js_register_async_fn(fn_name)
if fn_name == "main" {
js_emit_line("async function main(" + params_str + ") {")
} else {
js_emit_line("async function " + fn_name + "(" + params_str + ") {")
}
} else {
js_emit_line("function " + fn_name + "(" + params_str + ") {")
// Special-case `fn main` emit as a regular function and call it
// at module bottom (after all top-level statements). This matches
// the C backend's behavior where `fn main` is the entry point.
if fn_name == "main" {
js_emit_line("function main(" + params_str + ") {")
} else {
js_emit_line("function " + fn_name + "(" + params_str + ") {")
}
}
let decl = native_list_empty()
@@ -847,50 +1031,133 @@ fn js_is_top_level_decl(stmt: Map<String, Any>) -> Bool {
if kind == "Import" { return true }
if kind == "CgiBlock" { return true }
if kind == "ServiceBlock" { return true }
if kind == "ExternFn" { return true }
false
}
// Entry point
fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
codegen_js_inner(stmts, source, false, "")
}
fn codegen_js_bundle(stmts: [Map<String, Any>], source: String, runtime_content: String) -> String {
codegen_js_inner(stmts, source, true, runtime_content)
}
fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool, runtime_content: String) -> String {
// Reset per-compile state.
state_set("__js_int_names", "")
state_set("__js_match_counter", "")
state_set("__js_async_fns", "")
state_set("__js_lambda_counter", "")
// Preamble: inline the runtime via a single import that side-effects
// globalThis. The runtime path is resolved relative to the generated
// output; users running `elc --target=js` are responsible for ensuring
// el_runtime.js is reachable. For self-contained output, the runtime
// could be inlined; that is a follow-up.
// Preamble: in bundle mode, inline the runtime and wrap in IIFE.
// In module mode, emit a single import that side-effects globalThis.
js_emit_line("// Generated by elc --target=js")
js_emit_line("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js")
js_emit_line("import \"./el_runtime.js\";")
js_emit_line("const {")
js_emit_line(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,")
js_emit_line(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,")
js_emit_line(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,")
js_emit_line(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,")
js_emit_line(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,")
js_emit_line(" list_push, list_join, list_range,")
js_emit_line(" el_map_new, el_get_field, el_map_get, el_map_set,")
js_emit_line(" http_get, http_post, http_post_json,")
js_emit_line(" fs_read, fs_write, fs_list,")
js_emit_line(" json_parse, json_stringify, json_get, json_get_string, json_get_int,")
js_emit_line(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,")
js_emit_line(" el_retain, el_release,")
js_emit_line(" append, len, get, map_get, map_set,")
js_emit_line(" native_list_get, native_list_len, native_list_append, native_list_empty,")
js_emit_line(" native_list_clone, native_string_chars, native_int_to_str,")
js_emit_line(" args, state_set, state_get, state_del, state_keys, env,")
js_emit_line(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,")
js_emit_line(" engram_node, engram_search, engram_activate,")
js_emit_line(" llm_call, llm_call_system,")
js_emit_line("} = globalThis.__el;")
if bundle_mode {
js_emit_line("// Bundle mode: runtime inlined, no import statement needed.")
js_emit_line("// Drop directly into a <script> tag.")
js_emit_line(";(function() {")
js_emit_line("\"use strict\";")
// Inline the runtime content verbatim (already read from el_runtime.js).
// Strip the final ES export lines -- they use `export` syntax which is
// not valid inside an IIFE. The globalThis.__el assignment is what matters.
js_emit_line(js_strip_es_exports(runtime_content))
js_emit_line("")
} else {
js_emit_line("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js")
js_emit_line("import \"./el_runtime.js\";")
}
// In module mode: destructure all builtins off globalThis.__el so call
// sites stay flat (println(x) not el.println(x)).
// In bundle mode: function declarations from the inlined runtime are
// already in scope within the IIFE -- no destructure needed.
if !bundle_mode {
js_emit_line("const {")
js_emit_line(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,")
js_emit_line(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,")
js_emit_line(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,")
js_emit_line(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,")
js_emit_line(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,")
js_emit_line(" list_push, list_join, list_range,")
js_emit_line(" el_map_new, el_get_field, el_map_get, el_map_set,")
js_emit_line(" http_get, http_post, http_post_json,")
js_emit_line(" fs_read, fs_write, fs_list,")
js_emit_line(" json_parse, json_stringify, json_get, json_get_string, json_get_int,")
js_emit_line(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,")
js_emit_line(" el_retain, el_release,")
js_emit_line(" append, len, get, map_get, map_set,")
js_emit_line(" native_list_get, native_list_len, native_list_append, native_list_empty,")
js_emit_line(" native_list_clone, native_string_chars, native_int_to_str,")
js_emit_line(" args, state_set, state_get, state_del, state_keys, env,")
js_emit_line(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,")
js_emit_line(" engram_node, engram_search, engram_activate,")
js_emit_line(" llm_call, llm_call_system,")
js_emit_line(" dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,")
js_emit_line(" dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,")
js_emit_line(" dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,")
js_emit_line(" dom_append, dom_remove, dom_is_null,")
js_emit_line(" dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,")
js_emit_line(" dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,")
js_emit_line(" set_timeout, set_interval, clear_interval,")
js_emit_line(" local_storage_get, local_storage_set, local_storage_remove,")
js_emit_line(" window_location, window_redirect, window_on_load,")
js_emit_line(" console_log,")
js_emit_line(" window_set, window_get, native_js, native_js_call,")
js_emit_line(" promise_then, promise_catch, promise_resolve, promise_reject,")
js_emit_line(" object_assign, object_keys, object_values, json_deep_clone,")
js_emit_line(" array_from, type_of, instanceof_check,")
js_emit_line("} = globalThis.__el;")
js_emit_blank()
}
// URL import pass: emit `import "url"` (module mode) or a comment
// (bundle mode) for any import whose path starts with http(s):// or
// doesn't end in .el (i.e., it's a JS/CSS/CDN import, not an El source
// import which was already inlined by resolve_imports).
let n: Int = native_list_len(stmts)
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
let sk: String = stmt["stmt"]
if str_eq(sk, "Import") {
let ipath: String = stmt["path"]
let is_url = str_starts_with(ipath, "http://")
let is_url = is_url || str_starts_with(ipath, "https://")
let is_js = !str_ends_with(ipath, ".el")
if is_url || is_js {
if bundle_mode {
js_emit_line("// external: " + ipath)
} else {
js_emit_line("import " + js_str_lit(ipath) + ";")
}
}
}
let i = i + 1
}
js_emit_blank()
// Function definitions
// Pre-registration pass: scan all FnDefs for @async decorators so that
// forward calls to @async functions get `await` even if the callee is
// defined after the caller.
let n: Int = native_list_len(stmts)
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
let sk: String = stmt["stmt"]
if str_eq(sk, "FnDef") {
let dec: String = stmt["decorator"]
if str_eq(dec, "async") {
let aname: String = stmt["name"]
js_register_async_fn(aname)
}
}
let i = i + 1
}
// Function definitions
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
if js_is_fndef(stmt) {
@@ -938,6 +1205,41 @@ fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
js_emit_line("main();")
}
// Close IIFE in bundle mode.
if bundle_mode {
js_emit_line("")
js_emit_line("})();")
}
// Return empty string output was streamed via println
""
}
// Strip ES module export statements from runtime content for IIFE embedding.
// The runtime ends with `export { ... }` and `export { __el as default }` lines
// that are invalid inside an IIFE. We strip everything from the first top-level
// `export {` line onward.
//
// Also strips `import` statements at the top if any (though el_runtime.js has none).
fn js_strip_es_exports(content: String) -> String {
let lines: [String] = str_split(content, "\n")
let n: Int = native_list_len(lines)
let out: [String] = native_list_empty()
let i = 0
while i < n {
let line: String = native_list_get(lines, i)
let trimmed: String = str_trim(line)
// Stop at top-level `export {` or `export default`
if str_starts_with(trimmed, "export {") {
let i = n
} else {
if str_starts_with(trimmed, "export default") {
let i = n
} else {
let out = native_list_append(out, line)
}
}
let i = i + 1
}
str_join(out, "\n")
}
+227 -99
View File
@@ -1,4 +1,4 @@
// codegen.el El compiler C source code generator
// codegen.el - El compiler C source code generator
//
// Input: list of AST statement maps (from parser.el)
// Output: C source printed to stdout (streamed, one line at a time)
@@ -7,37 +7,90 @@
// Functions map directly to C functions; top-level statements become main().
//
// Entry point: fn codegen(stmts: [Map<String, Any>], source: String) -> String
// Returns "" output goes to stdout via println().
// Returns "" - output goes to stdout via println().
//
// Streaming output avoids O(n²) string concatenation: each emitted line is
// Streaming output avoids O(n-) string concatenation: each emitted line is
// printed immediately rather than appended to a growing string.
// String helpers
// -- String helpers ------------------------------------------------------------
// Escape a C string literal (double-quotes and backslashes).
// Hex-encode a single nibble (0-15) as a lowercase hex character.
fn nibble_to_hex(n: Int) -> String {
str_char_at("0123456789abcdef", n)
}
// Encode a byte value (0-255) as a two-character hex string.
fn byte_to_hex2(b: Int) -> String {
let hi: Int = (b / 16)
let lo: Int = (b - hi * 16)
nibble_to_hex(hi) + nibble_to_hex(lo)
}
// Return true if the byte value is a C hex digit (0-9, a-f, A-F).
// Used to determine whether a \xNN escape needs a string-literal split
// to prevent the C preprocessor from greedily consuming following hex chars.
fn is_hex_digit_byte(b: Int) -> Bool {
if b >= 48 { if b <= 57 { return true } } // 0-9
if b >= 65 { if b <= 70 { return true } } // A-F
if b >= 97 { if b <= 102 { return true } } // a-f
false
}
fn c_escape(s: String) -> String {
let chars: [String] = native_string_chars(s)
let total: Int = native_list_len(chars)
// Use index-based byte scanning via str_char_code(s, i) and str_char_at(s, i).
// This avoids native_string_chars + str_join, which corrupts high-byte (>= 0x80)
// characters because list_join's looks_like_string heuristic rejects strings
// whose first byte is >= 0x7F and emits them as decimal pointer values instead.
//
// IMPORTANT: after a \xNN hex escape, if the next byte is a hex digit
// (0-9, a-f, A-F), we emit `""` to split the C string literal so the C
// compiler does not greedily read extra hex digits as part of the escape.
// E.g. "\xad" followed by "bamos" must become "\xad" "bamos" because 'b'
// is a hex digit and C would otherwise read "\xadb" (= 0xADB, out of range).
let total: Int = str_len(s)
let parts: [String] = native_list_empty()
let i = 0
let i: Int = 0
let prev_was_hex_escape: Bool = false
while i < total {
let ch: String = native_list_get(chars, i)
if ch == "\"" {
let bval: Int = str_char_code(s, i)
// If the previous token was a \xNN escape and the current byte is a
// hex digit, insert an empty string literal ("") to break the escape.
if prev_was_hex_escape {
if is_hex_digit_byte(bval) {
let parts = native_list_append(parts, "\"\"")
}
}
let prev_was_hex_escape = false
if bval == 34 {
// 34 = '"'
let parts = native_list_append(parts, "\\\"")
} else {
if ch == "\\" {
if bval == 92 {
// 92 = '\\'
let parts = native_list_append(parts, "\\\\")
} else {
if ch == "\n" {
if bval == 10 {
// 10 = '\n'
let parts = native_list_append(parts, "\\n")
} else {
if ch == "\r" {
if bval == 13 {
// 13 = '\r'
let parts = native_list_append(parts, "\\r")
} else {
if ch == "\t" {
if bval == 9 {
// 9 = '\t'
let parts = native_list_append(parts, "\\t")
} else {
let parts = native_list_append(parts, ch)
if bval >= 128 {
// Escape non-ASCII bytes (>= 0x80) as \xNN so
// Clang does not misinterpret multi-byte UTF-8
// sequences in C string literals.
let parts = native_list_append(parts, "\\x" + byte_to_hex2(bval))
let prev_was_hex_escape = true
} else {
let parts = native_list_append(parts, str_char_at(s, i))
}
}
}
}
@@ -52,7 +105,7 @@ fn c_str_lit(s: String) -> String {
"\"" + c_escape(s) + "\""
}
// Type mapping
// -- Type mapping --------------------------------------------------------------
fn el_type_to_c(type_str: String) -> String {
if type_str == "String" { return "const char*" }
@@ -64,7 +117,7 @@ fn el_type_to_c(type_str: String) -> String {
"void*"
}
// Code emission
// -- Code emission -------------------------------------------------------------
//
// emit_line/emit_blank stream output directly via println.
// This avoids building a large string in memory.
@@ -77,7 +130,7 @@ fn emit_blank() -> Void {
println("")
}
// Operator helpers
// -- Operator helpers ----------------------------------------------------------
fn binop_to_c(op: String) -> String {
if op == "Plus" { return "+" }
@@ -95,11 +148,11 @@ fn binop_to_c(op: String) -> String {
op
}
// Expression codegen
// -- Expression codegen --------------------------------------------------------
//
// cg_expr returns a C expression string (not a statement).
// duration_unit_nanos multiplier from a postfix-literal unit name to
// duration_unit_nanos - multiplier from a postfix-literal unit name to
// nanoseconds. Singular and plural forms collapse to the same multiplier;
// the parser already restricted `unit` to the set is_duration_unit accepts.
// Returns the multiplier as a decimal string suitable for splicing into
@@ -130,7 +183,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
return v
}
// DurationLit postfix-literal time value (e.g. 30.seconds, 1.hour).
// DurationLit - postfix-literal time value (e.g. 30.seconds, 1.hour).
// Lowered to a literal int64 nanosecond count, wrapped in the runtime
// entry point so the intent is explicit at the C level. The arithmetic
// is fully constant-folded by any optimising C compiler.
@@ -144,7 +197,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
if kind == "Float" {
// Wrap Float literals in el_from_float() so the bit pattern is
// preserved through the el_val_t (int64) slot. Without this,
// implicit doubleint64 conversion in C truncates `0.8` to `0`
// implicit double->int64 conversion in C truncates `0.8` to `0`
// when passed to a builtin that expects el_val_t.
let v: String = expr["value"]
return "el_from_float(" + v + ")"
@@ -191,12 +244,12 @@ fn cg_expr(expr: Map<String, Any>) -> String {
let left_kind: String = left["expr"]
let right_kind: String = right["expr"]
// String/equality fast-path: skip O(N²) temporal traversals
// -- String/equality fast-path: skip O(N-) temporal traversals --------
// The 10 temporal predicates below each recurse into the left subtree:
// O(depth) state_get calls per predicate, O(N²) total for a chain of N
// O(depth) state_get calls per predicate, O(N-) total for a chain of N
// string-concat BinOps (e.g. the 70-100-part HTML chains in soul.el).
// When either operand is a bare Str literal the result is always concat
// or str_eq no temporal dispatch is possible. Exit immediately.
// or str_eq - no temporal dispatch is possible. Exit immediately.
if str_eq(op, "Plus") {
if str_eq(left_kind, "Str") { return "el_str_concat(" + left_c + ", " + right_c + ")" }
if str_eq(right_kind, "Str") { return "el_str_concat(" + left_c + ", " + right_c + ")" }
@@ -210,7 +263,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
if str_eq(right_kind, "Str") { return "!str_eq(" + left_c + ", " + right_c + ")" }
}
// Temporal-type dispatch (Instant + Duration first-class)
// -- Temporal-type dispatch (Instant + Duration first-class) --------
// Run BEFORE the int / string / generic paths so typed temporal
// operands route through the runtime wrappers and invalid combos
// become #error directives rather than silently falling through to
@@ -396,7 +449,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
if right_is_dur { return "el_duration_ne(" + left_c + ", " + right_c + ")" }
}
}
// Fall through let the existing path handle anything we
// Fall through - let the existing path handle anything we
// didn't explicitly cover (typically string-concat with a
// typed temporal value, e.g. for debug prints, which works
// because both share the int64 slot).
@@ -415,7 +468,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
// builtin, or BinOp arithmetic over Ints) participates in
// arithmetic, not string concat. Recursion into BinOp lets
// `a + b + c` (chained Int adds) and `acc * 16 + d` route to
// arithmetic instead of falling to el_str_concat both sides
// arithmetic instead of falling to el_str_concat - both sides
// are Int so the outer `+` is too.
if is_int_expr(left) {
if is_int_expr(right) {
@@ -436,7 +489,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
return "(" + left_c + " " + op_c + " " + right_c + ")"
}
// Otherwise: BinOp(+) with a Call/Ident side without int-typed
// evidence fall back to string concat (the historical default).
// evidence - fall back to string concat (the historical default).
if left_kind == "Call" {
return "el_str_concat(" + left_c + ", " + right_c + ")"
}
@@ -468,7 +521,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
// identifiers tracked in __int_names (typed Int via `let x: Int = ...`).
// Without the int-name check, `seen == idx` between two Int locals
// miscompiles to str_eq(seen, idx), strcmp'ing what are integer values
// dressed as char* segfault on the first non-printable byte.
// dressed as char* - segfault on the first non-printable byte.
if op == "EqEq" {
if left_kind == "Int" {
return "(" + left_c + " == " + right_c + ")"
@@ -602,17 +655,17 @@ fn cg_expr(expr: Map<String, Any>) -> String {
// violations to be emitted as #error directives at the
// top of the generated C, so cc fails with a clear msg.
cap_check_call(fn_name)
// Arity check against the builtin table refuse, with a clear
// Arity check against the builtin table - refuse, with a clear
// El-source message, when a known builtin gets the wrong arg
// count (e.g. `http_serve(port)` instead of `http_serve(port,
// handler)`). User-defined fns and variadic builtins pass
// through (builtin_arity returns -1).
arity_check_call(fn_name, arity)
// sleep(Duration) Phase 1 of the typed-time work. When the
// sleep(Duration) - Phase 1 of the typed-time work. When the
// single arg is provably a Duration we lower to el_sleep_duration
// so the runtime sees nanos directly. Existing sleep() callers
// that pass an Int still emit `sleep(<int>)`, which falls through
// to the no-such-symbol path those call sites must migrate to
// to the no-such-symbol path - those call sites must migrate to
// a typed Duration. Acceptable: the spec marks them out for an
// audit pass during Phase 1.
if str_eq(fn_name, "sleep") {
@@ -623,6 +676,20 @@ fn cg_expr(expr: Map<String, Any>) -> String {
}
}
}
// el_from_float takes a raw C double - do not wrap the float
// argument in el_from_float() again. Without this, the float
// literal codegen (which wraps every Float in el_from_float())
// produces el_from_float(el_from_float(0.7)) - double-encoded.
if str_eq(fn_name, "el_from_float") {
if arity == 1 {
let only_arg = native_list_get(args, 0)
let arg_kind: String = only_arg["expr"]
if str_eq(arg_kind, "Float") {
let v: String = only_arg["value"]
return "el_from_float(" + v + ")"
}
}
}
return fn_name + "(" + args_c + ")"
}
@@ -656,8 +723,8 @@ fn cg_expr(expr: Map<String, Any>) -> String {
// El programs use `t["field"]` for map access and `arr[i]` for
// list access. The parser emits the same Index node for both.
// Dispatch at codegen time on the index expression kind: string-
// literal index map field access (`el_get_field`); anything
// else list element access (`el_list_get`).
// literal index -> map field access (`el_get_field`); anything
// else -> list element access (`el_list_get`).
let obj = expr["object"]
let idx = expr["index"]
let obj_c: String = cg_expr(obj)
@@ -691,7 +758,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
let n: Int = native_list_len(pairs)
// Empty literal: `el_map_new(0, )` is malformed C (trailing comma in
// a varargs call). Emit `el_map_new(0)` directly so empty-map
// shadowing inside for/while/if bodies `let acc: Map = {}`
// shadowing inside for/while/if bodies - `let acc: Map = {}` -
// doesn't fail downstream cc with parse errors.
if n == 0 { return "el_map_new(0)" }
let items_parts: [String] = native_list_empty()
@@ -723,7 +790,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
"EL_NULL"
}
// Match codegen
// -- Match codegen -------------------------------------------------------------
//
// Lower a match expression to a GCC/Clang statement-expression.
// A unique label suffix is allocated per match via state_set("__match_counter").
@@ -747,7 +814,7 @@ fn cg_match(expr: Map<String, Any>) -> String {
let subj_var: String = "_match_subj_" + id
let result_var: String = "_match_result_" + id
let done_label: String = "_match_done_" + id
// Accumulate arm fragments into a list to avoid O(n²) string growth.
// Accumulate arm fragments into a list to avoid O(n-) string growth.
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, "({ el_val_t " + subj_var + " = " + subj_c + "; el_val_t " + result_var + " = 0; ")
let n: Int = native_list_len(arms)
@@ -781,8 +848,15 @@ fn cg_match(expr: Map<String, Any>) -> String {
}
let parts = native_list_append(parts, "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
} else {
// unknown pattern wildcard
let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
if str_eq(pkind, "Variant") {
// Enum::Variant pattern match against the variant name
// string (El enums compile to plain strings).
let variant: String = pat["variant"]
let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(variant) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
} else {
// unknown pattern -> wildcard
let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
}
}
}
}
@@ -794,7 +868,7 @@ fn cg_match(expr: Map<String, Any>) -> String {
str_join(parts, "")
}
// If-as-expression codegen
// -- If-as-expression codegen -------------------------------------------------
//
// Lower `if cond { thenBody } else { elseBody }` used in expression position
// (e.g. `let x = if a { b } else { c }`) to a GCC/Clang statement-expression
@@ -822,7 +896,7 @@ fn next_if_id() -> String {
// result var stays at its initial 0.
fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
let n: Int = native_list_len(stmts)
// Collect statement fragments into a list to avoid O(n²) string growth.
// Collect statement fragments into a list to avoid O(n-) string growth.
let parts: [String] = native_list_empty()
let i = 0
while i < n {
@@ -851,7 +925,7 @@ fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
}
} else {
if str_eq(sk, "Assign") {
// Real reassignment in an expression-position arm
// Real reassignment in an expression-position arm -
// emit the store; the arm's "value" stays whatever
// result_var was last set to, which is the El
// semantics (assignment is a statement, not a value).
@@ -889,7 +963,7 @@ fn cg_if_expr(expr: Map<String, Any>) -> String {
out
}
// Variable scope tracking
// -- Variable scope tracking ---------------------------------------------------
//
// El allows `let x = expr` to both declare and reassign x in the same scope.
// C doesn't allow redeclaring the same name in the same block.
@@ -908,7 +982,7 @@ fn list_contains(lst: [String], s: String) -> Bool {
false
}
// Statement codegen
// -- Statement codegen ---------------------------------------------------------
//
// cg_stmt emits C lines via println. declared is a list of already-declared
// variable names in the current C scope; returns updated declared list.
@@ -957,7 +1031,7 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
if str_eq(ltype, "Zone") {
add_zone_name(name)
}
// Inference from RHS duration literals and known-typed calls
// Inference from RHS - duration literals and known-typed calls
// propagate even when the let is unannotated.
if is_instant_expr(val) {
add_instant_name(name)
@@ -1012,7 +1086,7 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
}
// Bare reassignment: `name = expr`. Always emits a plain C assignment
// (no `el_val_t` prefix) by construction the parser only produces
// (no `el_val_t` prefix) - by construction the parser only produces
// Assign for an existing identifier. If the name happens NOT to be in
// `declared` for the current C scope (it was let-bound by an enclosing
// block) the emit still resolves at C level because the variable lives
@@ -1047,7 +1121,7 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
let cond_c: String = cg_expr(cond)
let cond_c = strip_outer_parens(cond_c)
emit_line(indent + "while (" + cond_c + ") {")
// Body lives in its own C block clone so let-bindings inside the
// Body lives in its own C block - clone so let-bindings inside the
// loop don't leak into the parent's `declared` list (which would make
// a sibling scope's `let x` emit assignment on an undeclared name).
cg_stmts(body, indent + " ", native_list_clone(declared))
@@ -1069,6 +1143,15 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
if kind == "Import" { return declared }
if kind == "ExternFn" { return declared }
if kind == "CgiBlock" { return declared }
// TryCatch: browser-only control flow. In the C target, emit a comment
// noting that the try body runs unconditionally; error handling is a no-op.
// Programs that rely on catching JS exceptions should compile with --target=js.
if kind == "TryCatch" {
let try_body = stmt["try_body"]
emit_line(indent + "/* try (C target: exception handling not supported) */")
cg_stmts(try_body, indent, native_list_clone(declared))
return declared
}
declared
}
@@ -1114,7 +1197,7 @@ fn cg_if_stmt(expr: Map<String, Any>, indent: String, declared: [String]) -> Voi
let cond_c: String = cg_expr(cond)
let cond_c = strip_outer_parens(cond_c)
emit_line(indent + "if (" + cond_c + ") {")
// Each branch gets its own clone of `declared` variables let-bound
// Each branch gets its own clone of `declared` - variables let-bound
// inside the then/else block live only in that C scope, and must not
// leak back to the parent (or to the sibling branch) through shared
// list mutation. Cheap shallow copy; the entries (variable name strings)
@@ -1166,7 +1249,7 @@ fn cg_stmts(stmts: [Map<String, Any>], indent: String, declared: [String]) -> [S
decl
}
// Function declaration codegen
// -- Function declaration codegen -----------------------------------------------
fn param_decl(param: Map<String, Any>, idx: Int) -> String {
let name: String = param["name"]
@@ -1235,7 +1318,7 @@ fn is_int_name(name: String) -> Bool {
// Same shape as is_int_name, for Instant- and Duration-typed bindings.
// Used by the BinOp/comparison codegen to dispatch arithmetic through the
// typed runtime wrappers (el_instant_add_dur, el_duration_lt, ) and to
// typed runtime wrappers (el_instant_add_dur, el_duration_lt, -) and to
// surface mismatches (Instant + Instant, Duration + Int) as #error
// directives at the top of the generated C.
fn is_instant_name(name: String) -> Bool {
@@ -1297,7 +1380,7 @@ fn is_int_call(call_expr: Map<String, Any>) -> Bool {
}
// Builtins that return an Instant. Used by is_instant_expr and the BinOp
// dispatch `now() + 5.seconds` types as Instant only because we can see
// dispatch - `now() + 5.seconds` types as Instant only because we can see
// that now() is an Instant-returning Call.
fn is_instant_call(call_expr: Map<String, Any>) -> Bool {
let func = call_expr["func"]
@@ -1333,7 +1416,7 @@ fn is_duration_call(call_expr: Map<String, Any>) -> Bool {
return false
}
// Phase 1.5 Calendar / CalendarTime / Rhythm / LocalDate / LocalTime /
// Phase 1.5 - Calendar / CalendarTime / Rhythm / LocalDate / LocalTime /
// LocalDateTime / Zone are first-class boxed types. Each has its own name
// set in process state, populated from typed `let` bindings and parameter
// annotations. The BinOp dispatcher consults these to forbid mismatched
@@ -1521,7 +1604,7 @@ fn is_zone_expr(expr: Map<String, Any>) -> Bool {
// Recursive type predicates for Instant / Duration. Mirror is_int_expr.
// is_instant_expr / is_duration_expr return true only when the expression
// is provably of that type at codegen time. Anything ambiguous returns
// false the BinOp dispatcher then leaves the expression on the
// false - the BinOp dispatcher then leaves the expression on the
// untyped-int path, which is the safest fallback because at the runtime
// level all three types share the int64 slot.
fn is_instant_expr(expr: Map<String, Any>) -> Bool {
@@ -1536,8 +1619,8 @@ fn is_instant_expr(expr: Map<String, Any>) -> Bool {
if str_eq(k, "BinOp") {
let op: String = expr["op"]
if str_eq(op, "Plus") {
// Instant + Duration Instant
// Duration + Instant Instant
// Instant + Duration -> Instant
// Duration + Instant -> Instant
if is_instant_expr(expr["left"]) {
if is_duration_expr(expr["right"]) { return true }
}
@@ -1547,7 +1630,7 @@ fn is_instant_expr(expr: Map<String, Any>) -> Bool {
return false
}
if str_eq(op, "Minus") {
// Instant - Duration Instant
// Instant - Duration -> Instant
if is_instant_expr(expr["left"]) {
if is_duration_expr(expr["right"]) { return true }
}
@@ -1574,15 +1657,15 @@ fn is_duration_expr(expr: Map<String, Any>) -> Bool {
if str_eq(k, "BinOp") {
let op: String = expr["op"]
if str_eq(op, "Plus") {
// Duration + Duration Duration
// Duration + Duration -> Duration
if is_duration_expr(expr["left"]) {
if is_duration_expr(expr["right"]) { return true }
}
return false
}
if str_eq(op, "Minus") {
// Duration - Duration Duration
// Instant - Instant Duration (caught here, not in is_instant_expr)
// Duration - Duration -> Duration
// Instant - Instant -> Duration (caught here, not in is_instant_expr)
if is_duration_expr(expr["left"]) {
if is_duration_expr(expr["right"]) { return true }
}
@@ -1592,8 +1675,8 @@ fn is_duration_expr(expr: Map<String, Any>) -> Bool {
return false
}
if str_eq(op, "Star") {
// Duration * Int Duration
// Int * Duration Duration
// Duration * Int -> Duration
// Int * Duration -> Duration
if is_duration_expr(expr["left"]) {
if is_int_expr(expr["right"]) { return true }
}
@@ -1603,7 +1686,7 @@ fn is_duration_expr(expr: Map<String, Any>) -> Bool {
return false
}
if str_eq(op, "Slash") {
// Duration / Int Duration
// Duration / Int -> Duration
if is_duration_expr(expr["left"]) {
if is_int_expr(expr["right"]) { return true }
}
@@ -1634,13 +1717,13 @@ fn time_record_violation(kind: String, detail: String) -> Bool {
// the outer dispatch only checks the immediate kind, not the inner.
//
// Rules:
// Int literal Int
// Ident in __int_names Int
// Call to known-Int builtin Int
// Neg of Int Int
// BinOp arithmetic of two Ints Int (Plus, Minus, Star, Slash, Percent)
// BinOp comparison/logical Int (yields 0/1; safe to treat as Int)
// anything else not provably Int
// Int literal -> Int
// Ident in __int_names -> Int
// Call to known-Int builtin -> Int
// Neg of Int -> Int
// BinOp arithmetic of two Ints -> Int (Plus, Minus, Star, Slash, Percent)
// BinOp comparison/logical -> Int (yields 0/1; safe to treat as Int)
// anything else -> not provably Int
fn is_int_expr(expr: Map<String, Any>) -> Bool {
let k: String = expr["expr"]
if str_eq(k, "Int") { return true }
@@ -1659,7 +1742,7 @@ fn is_int_expr(expr: Map<String, Any>) -> Bool {
}
if str_eq(k, "BinOp") {
let op: String = expr["op"]
// Comparisons and logicals always yield 0/1 safe Int.
// Comparisons and logicals always yield 0/1 - safe Int.
if str_eq(op, "EqEq") { return true }
if str_eq(op, "NotEq") { return true }
if str_eq(op, "Lt") { return true }
@@ -1668,7 +1751,7 @@ fn is_int_expr(expr: Map<String, Any>) -> Bool {
if str_eq(op, "GtEq") { return true }
if str_eq(op, "And") { return true }
if str_eq(op, "Or") { return true }
// Arithmetic propagates: Int op Int Int.
// Arithmetic propagates: Int op Int -> Int.
if str_eq(op, "Plus") {
if is_int_expr(expr["left"]) {
if is_int_expr(expr["right"]) { return true }
@@ -1698,7 +1781,7 @@ fn is_int_expr(expr: Map<String, Any>) -> Bool {
return false
}
// Capability-kind enforcement
// -- Capability-kind enforcement ----------------------------------------------
//
// A program's top-level block (cgi / service / none) determines which
// runtime primitives it may call. The compiler records violations in
@@ -1707,11 +1790,11 @@ fn is_int_expr(expr: Map<String, Any>) -> Bool {
// downstream cc step fails with a clear message.
//
// Capability tiers:
// "cgi" full self-formation. All primitives.
// "service" bounded. Cannot call self-formation primitives:
// "cgi" - full self-formation. All primitives.
// "service" - bounded. Cannot call self-formation primitives:
// llm_call_agentic, llm_register_tool, dharma_emit,
// dharma_field. Single-turn LLM calls are allowed.
// "utility" default. No DHARMA, no LLM. Pure compute + I/O.
// "utility" - default. No DHARMA, no LLM. Pure compute + I/O.
//
// The compiler-level rule is structural: the binary either CAN or CANNOT
// emit the call. There is no runtime check, no opt-in, no override.
@@ -1726,7 +1809,7 @@ fn cap_record_violation(kind: String, fn_name: String) -> Bool {
return true
}
// Self-formation primitives the cut between CGI and service. A program
// Self-formation primitives - the cut between CGI and service. A program
// that emits these calls IS structurally a CGI; we forbid them everywhere
// else.
fn is_self_formation_call(fn_name: String) -> Bool {
@@ -1737,7 +1820,7 @@ fn is_self_formation_call(fn_name: String) -> Bool {
return false
}
// Any DHARMA primitive utilities have zero network presence.
// Any DHARMA primitive - utilities have zero network presence.
fn is_dharma_call(fn_name: String) -> Bool {
if str_eq(fn_name, "dharma_connect") { return true }
if str_eq(fn_name, "dharma_send") { return true }
@@ -1750,7 +1833,7 @@ fn is_dharma_call(fn_name: String) -> Bool {
return false
}
// Any LLM primitive utilities have no LLM access at all.
// Any LLM primitive - utilities have no LLM access at all.
fn is_llm_call(fn_name: String) -> Bool {
if str_eq(fn_name, "llm_call") { return true }
if str_eq(fn_name, "llm_call_system") { return true }
@@ -1800,14 +1883,14 @@ fn emit_cap_violations() -> Void {
if colon > 0 {
let kind: String = str_slice(entry, 0, colon)
let fn_name: String = str_slice(entry, colon + 1, str_len(entry))
emit_line("#error \"capability violation: '" + kind + "' programs may not call '" + fn_name + "' (self-formation primitive only 'cgi' programs may use it)\"")
emit_line("#error \"capability violation: '" + kind + "' programs may not call '" + fn_name + "' (self-formation primitive - only 'cgi' programs may use it)\"")
}
let i = i + next_comma + 1
}
}
// Surface temporal-type violations as #error directives. The cg_expr BinOp
// dispatcher records each violation (Instant + Instant, Duration + Int, )
// dispatcher records each violation (Instant + Instant, Duration + Int, -)
// as a CSV entry "kind:detail" via time_record_violation. Each entry maps
// to a single #error so downstream cc fails the build with a clear El-
// source-level message before the bogus C even links.
@@ -1830,7 +1913,7 @@ fn emit_time_violations() -> Void {
}
}
// Builtin arity table
// -- Builtin arity table -------------------------------------------------------
//
// El programs sometimes call runtime builtins with the wrong number of
// arguments (e.g. `http_serve(port)` instead of `http_serve(port, handler)`).
@@ -1840,7 +1923,7 @@ fn emit_time_violations() -> Void {
//
// Strategy: a small static table mirrors el_runtime.h. Variadic builtins
// (el_list_new, el_map_new, args) and unknown identifiers (user fns,
// dynamic dispatch) return -1 no check. A mismatch records a violation
// dynamic dispatch) return -1 -> no check. A mismatch records a violation
// in process state, which emit_arity_violations() turns into #error
// directives at the top of the generated C.
fn builtin_arity(name: String) -> Int {
@@ -1982,6 +2065,16 @@ fn builtin_arity(name: String) -> Int {
if str_eq(name, "bool_to_str") { return 1 }
// Process
if str_eq(name, "exit_program") { return 1 }
// Process info
if str_eq(name, "getpid_now") { return 0 }
// stdout redirect (used by elc post-processing)
if str_eq(name, "stdout_to_file") { return 1 }
if str_eq(name, "stdout_restore") { return 0 }
// Subprocess execution
if str_eq(name, "exec_command") { return 1 }
if str_eq(name, "exec_capture") { return 1 }
if str_eq(name, "exec") { return 1 }
if str_eq(name, "exec_bg") { return 1 }
// CGI / DHARMA
if str_eq(name, "dharma_connect") { return 1 }
if str_eq(name, "dharma_send") { return 2 }
@@ -2044,7 +2137,7 @@ 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 }
// -1 sentinel: variadic / unknown / user-defined no check.
// -1 sentinel: variadic / unknown / user-defined -> no check.
return -1
}
@@ -2242,7 +2335,7 @@ fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
fn cg_fn(stmt: Map<String, Any>) -> Void {
let fn_name: String = stmt["name"]
// Skip El's `fn main()` C provides its own main() for top-level stmts
// Skip El's `fn main()` - C provides its own main() for top-level stmts
// and a duplicate `el_val_t main(void)` would collide with it.
if fn_name == "main" { return }
let params = stmt["params"]
@@ -2274,8 +2367,8 @@ fn cg_fn(stmt: Map<String, Any>) -> Void {
}
// Lift the final bare expression into an explicit return so implicit
// returns ("fn lex(s) { ... tokens }") actually return their value.
// Void-returning functions skip this wrapping `println(x)` in
// `return ` is a C type error.
// Void-returning functions skip this - wrapping `println(x)` in
// `return -` is a C type error.
let body_xformed = body
if !str_eq(ret_type, "Void") {
let body_xformed = transform_implicit_return(body)
@@ -2286,7 +2379,7 @@ fn cg_fn(stmt: Map<String, Any>) -> Void {
emit_blank()
}
// Top-level codegen
// -- Top-level codegen ---------------------------------------------------------
fn is_fndef(stmt: Map<String, Any>) -> Bool {
let kind: String = stmt["stmt"]
@@ -2312,7 +2405,7 @@ fn cgi_arg(value: String, has_value: Bool) -> String {
return "EL_NULL"
}
// VBD role enforcement
// -- VBD role enforcement ------------------------------------------------------
//
// Scan a function body for direct calls to DHARMA-restricted builtins
// (dharma_emit, dharma_field). These may only appear inside @manager fns.
@@ -2445,16 +2538,16 @@ fn vbd_has_restricted_call(stmts: [Map<String, Any>]) -> Bool {
false
}
// Entry point
// -- Entry point ----------------------------------------------------------------
fn codegen(stmts: [Map<String, Any>], source: String) -> String {
// Detect cgi/service blocks: at most one declarative top-level block.
// The block determines the program's CAPABILITY KIND:
// "cgi" full self-formation. Calls all primitives.
// "service" bounded. Cannot call self-formation primitives
// "cgi" - full self-formation. Calls all primitives.
// "service" - bounded. Cannot call self-formation primitives
// (llm_call_agentic, llm_register_tool, dharma_emit,
// dharma_field, mindlink-creation).
// "utility" default; no DHARMA membership, no LLM, no agentic.
// "utility" - default; no DHARMA membership, no LLM, no agentic.
// Codegen enforces this with #error directives at every restricted
// call site. The capability boundary is structural: a binary either
// CAN or CANNOT do a thing, and the compiler decides at emission time.
@@ -2489,7 +2582,7 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
}
if cgi_count >= 1 {
if svc_count >= 1 {
emit_line("#error \"El: program declares both cgi and service blocks (mutually exclusive pick one)\"")
emit_line("#error \"El: program declares both cgi and service blocks (mutually exclusive - pick one)\"")
}
}
// Stash the program kind so cg_expr's Call branch can enforce
@@ -2509,9 +2602,44 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
emit_line("#include <stdint.h>")
emit_line("#include <stdlib.h>")
emit_line("#include \"el_runtime.h\"")
// Cross-module forward declarations: for each imported module, emit
// #include "module.elh" so Clang sees the function signatures from
// that module without needing the full source inlined. The .elh files
// are generated by `elc --emit-header` and live in the same dist/
// directory as the generated .c files. We use basename only (strip
// the directory prefix and .el extension) so the include resolves
// correctly regardless of the source tree layout.
let imp_n: Int = native_list_len(stmts)
let imp_i = 0
while imp_i < imp_n {
let imp_stmt = native_list_get(stmts, imp_i)
let imp_kind: String = imp_stmt["stmt"]
if str_eq(imp_kind, "Import") {
let imp_path: String = imp_stmt["path"]
// Extract basename: find last '/' and strip from there.
let imp_path_len: Int = str_len(imp_path)
let imp_last_slash: Int = -1
let imp_j: Int = 0
while imp_j < imp_path_len {
let imp_c: String = str_slice(imp_path, imp_j, imp_j + 1)
if str_eq(imp_c, "/") { let imp_last_slash = imp_j }
let imp_j = imp_j + 1
}
let imp_base: String = str_slice(imp_path, imp_last_slash + 1, imp_path_len)
// Strip .el extension if present.
let imp_base_len: Int = str_len(imp_base)
let imp_bname: String = imp_base
if str_ends_with(imp_base, ".el") {
let imp_bname = str_slice(imp_base, 0, imp_base_len - 3)
}
emit_line("#include \"" + imp_bname + ".elh\"")
}
let imp_i = imp_i + 1
}
emit_blank()
// Forward declarations (skip `main` C provides its own)
// Forward declarations (skip `main` - C provides its own)
let n: Int = native_list_len(stmts)
let i = 0
while i < n {
@@ -2535,7 +2663,7 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
}
emit_blank()
// Top-level `let` bindings file-scope storage. El programs use
// Top-level `let` bindings -> file-scope storage. El programs use
// top-level `let GREETING = "..."` as module constants that any
// function below should be able to read. Without this pass, a top-
// level Let only declares the name inside main()'s scope and any
@@ -2683,7 +2811,7 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
let main_decl = cg_stmt(stmt, " ", main_decl)
}
}
// Release AST node after final use each stmt is fully processed
// Release AST node after final use - each stmt is fully processed
// by this point (forward decls, fn defs, top-level lets, and now
// the main-body pass are all done). Releasing here prevents the
// accumulated AST from exhausting memory on large source files.
@@ -2706,16 +2834,16 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
// Emit any accumulated capability-violation #error directives. cc
// will fail on the first one and surface the message; placement at
// the bottom is fine preprocessor errors halt the build wherever
// the bottom is fine - preprocessor errors halt the build wherever
// they appear.
emit_cap_violations()
// Same for builtin-arity violations: cc halts on the first #error,
// so a misuse of a known builtin (wrong arg count) fails the build
// with a clear message naming the builtin and its expected arity.
emit_arity_violations()
// Temporal-type violations (Instant + Instant, Duration + Int, ).
// Temporal-type violations (Instant + Instant, Duration + Int, -).
emit_time_violations()
// Return empty string output was streamed via println
// Return empty string - output was streamed via println
""
}
+251 -7
View File
@@ -29,7 +29,7 @@ fn compile(source: String) -> String {
codegen(stmts, source)
}
// compile_js full pipeline (JS target): source string -> JS source string
// compile_js full pipeline (JS target, module mode): source string -> JS source string
fn compile_js(source: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
@@ -38,6 +38,20 @@ fn compile_js(source: String) -> String {
codegen_js(stmts, source)
}
// compile_js_with_bundle JS target in bundle mode.
// Reads el_runtime.js from runtime_path and inlines it inside an IIFE.
fn compile_js_with_bundle(source: String, runtime_path: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
el_release(tokens)
let runtime_content: String = fs_read(runtime_path)
if str_eq(runtime_content, "") {
println("el-compiler: warning: --bundle: could not read runtime at " + runtime_path)
println("el-compiler: warning: bundle output will be incomplete")
}
codegen_js_bundle(stmts, source, runtime_content)
}
// compile_dispatch pick a backend based on the requested target.
// tgt = "c" | "js"
// (The parameter is named `tgt` because `target` is a reserved keyword
@@ -48,6 +62,12 @@ fn compile_dispatch(tgt: String, source: String) -> String {
compile(source)
}
// compile_dispatch_bundle like compile_dispatch but bundle mode for JS.
fn compile_dispatch_bundle(tgt: String, source: String, runtime_path: String) -> String {
if str_eq(tgt, "js") { return compile_js_with_bundle(source, runtime_path) }
compile(source)
}
// Detect a `--target=<lang>` flag in argv and return the target.
// Returns "c" if none specified or unrecognized.
fn detect_target(argv: [String]) -> String {
@@ -91,6 +111,126 @@ fn detect_emit_header(argv: [String]) -> Bool {
return false
}
// Detect --bundle flag in argv.
fn detect_bundle(argv: [String]) -> Bool {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_eq(a, "--bundle") { return true }
let i = i + 1
}
return false
}
// Detect --minify flag in argv.
fn detect_minify(argv: [String]) -> Bool {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_eq(a, "--minify") { return true }
let i = i + 1
}
return false
}
// Detect --obfuscate flag in argv.
fn detect_obfuscate(argv: [String]) -> Bool {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_eq(a, "--obfuscate") { return true }
let i = i + 1
}
return false
}
// Build a unique temp file path: /tmp/elc-<pid>-<timestamp>.<suffix>
fn make_temp_path(suffix: String) -> String {
let pid: Int = getpid_now()
let ts: Int = time_now()
"/tmp/elc-" + native_int_to_str(pid) + "-" + native_int_to_str(ts) + "." + suffix
}
// Reserved globals that terser and javascript-obfuscator must not mangle.
// These are referenced from HTML onclick= attributes and other direct window usage.
fn js_reserved_names() -> String {
"neuronDemoToggle,neuronDemoSend,neuronDemoReset,signInWith,signInWithEmail,signUpWithEmail,sendMagicLink,signOut,resetPassword,sendResetEmail,updatePassword,showSignIn,showSignUp,hideReset,setSort,addFamilyMember,removeFamilyMember,copyForPlatform,entHeadcountChange,NEURON_CFG"
}
// Find a CLI tool by checking node_modules paths first, then falling back to npx.
// src_dir is the directory of the source file being compiled.
// Returns the command string to invoke the tool, or "" if not found.
fn find_node_tool(tool_name: String, src_dir: String) -> String {
// 1. Check ./node_modules/.bin/<tool> relative to source file
let cand1: String = src_dir + "/node_modules/.bin/" + tool_name
let check1: String = str_trim(exec_capture("test -x " + cand1 + " && echo yes 2>/dev/null"))
if str_eq(check1, "yes") { return cand1 }
// 2. Check ../node_modules/.bin/<tool> (monorepo layout)
let parent_dir: String = dirname_of(src_dir)
let cand2: String = parent_dir + "/node_modules/.bin/" + tool_name
let check2: String = str_trim(exec_capture("test -x " + cand2 + " && echo yes 2>/dev/null"))
if str_eq(check2, "yes") { return cand2 }
// 3. Fall back to npx if it is on PATH. npx will use the globally cached
// package or download on first use. Use --no to avoid auto-install if
// the package is not already cached; if that fails, try with --yes.
let npx_path: String = str_trim(exec_capture("which npx 2>/dev/null"))
if !str_eq(npx_path, "") { return "npx --yes " + tool_name }
return ""
}
// apply_minify run terser on js_path, write result to out_path.
// Returns true on success, false on failure.
fn apply_minify(js_path: String, out_path: String, src_dir: String) -> Bool {
let terser: String = find_node_tool("terser", src_dir)
if str_eq(terser, "") {
println("el-compiler: error: terser not found. Run 'npm install terser' in your project directory.")
return false
}
let names: String = js_reserved_names()
// Single-quote the mangle reserved list so the shell does not glob-expand
// the bracket expression. The compress options are safe without quoting.
let compress_opts: String = "passes=2,drop_console=false,drop_debugger=true"
let mangle_reserved: String = "'reserved=[" + names + "]'"
let cmd: String = terser + " " + js_path + " --compress " + compress_opts + " --mangle " + mangle_reserved + " --output " + out_path
let ret: Int = exec_command(cmd)
if ret == 0 { return true }
println("el-compiler: error: terser failed (exit " + native_int_to_str(ret) + ")")
return false
}
// apply_obfuscate run javascript-obfuscator on js_path, write result to out_path.
// Returns true on success, false on failure.
fn apply_obfuscate(js_path: String, out_path: String, src_dir: String) -> Bool {
let obfuscator: String = find_node_tool("javascript-obfuscator", src_dir)
if str_eq(obfuscator, "") {
println("el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory.")
return false
}
let names: String = js_reserved_names()
let cmd: String = obfuscator + " " + js_path + " --output " + out_path + " --compact true --simplify true --string-array true --string-array-encoding base64 --string-array-threshold 0.75 --identifier-names-generator hexadecimal --rename-globals false --self-defending false --reserved-names " + names
let ret: Int = exec_command(cmd)
if ret == 0 { return true }
println("el-compiler: error: javascript-obfuscator failed (exit " + native_int_to_str(ret) + ")")
return false
}
// Resolve the runtime path for --bundle mode.
// Looks for el_runtime.js next to the source file first;
// if not found there, looks next to the elc binary itself.
// Returns "" if not found anywhere (caller emits a warning).
fn resolve_runtime_path(src_path: String) -> String {
let src_dir: String = dirname_of(src_path)
let candidate: String = src_dir + "/el_runtime.js"
let existing: String = fs_read(candidate)
if !str_eq(existing, "") {
return candidate
}
return ""
}
// Reconstruct an El type annotation string from a parsed type node.
fn type_node_to_el(t: Map<String, Any>) -> String {
let k: String = t["kind"]
@@ -249,12 +389,83 @@ fn resolve_imports(src_path: String) -> String {
return str_join(prefix_chunks, "") + str_join(body_chunks, "")
}
// run_with_postprocess codegen + minify + optional obfuscate pipeline.
//
// Called from main() when --minify or --obfuscate is active. Redirects stdout
// to a temp file during codegen so the output can be passed through the
// external tools (terser, javascript-obfuscator) before final emission.
//
// Pipeline: codegen -> terser -> (javascript-obfuscator) -> stdout or file
fn run_with_postprocess(tgt: String, source: String, src_path: String, do_bundle: Bool, do_obfuscate: Bool, argc: Int, positional: [String]) -> Void {
let src_dir: String = dirname_of(src_path)
let tmp_gen: String = make_temp_path("js")
let tmp_min: String = make_temp_path("min.js")
// Redirect stdout to tmp_gen so codegen println output is captured.
stdout_to_file(tmp_gen)
if do_bundle {
let runtime_path: String = resolve_runtime_path(src_path)
compile_dispatch_bundle(tgt, source, runtime_path)
} else {
compile_dispatch(tgt, source)
}
stdout_restore()
// Run terser: tmp_gen -> tmp_min
let ok_min: Bool = apply_minify(tmp_gen, tmp_min, src_dir)
if !ok_min {
exec_command("rm -f " + tmp_gen + " " + tmp_min)
exit(1)
}
// Determine final result path (either tmp_min or post-obfuscation file).
// Use state to pass the final path out of the optional obfuscation branch.
state_set("__elc_final_js", tmp_min)
if do_obfuscate {
let tmp_obf: String = make_temp_path("obf.js")
let ok_obf: Bool = apply_obfuscate(tmp_min, tmp_obf, src_dir)
if !ok_obf {
exec_command("rm -f " + tmp_gen + " " + tmp_min + " " + tmp_obf)
exit(1)
}
state_set("__elc_final_js", tmp_obf)
}
let final_path: String = state_get("__elc_final_js")
let final_js: String = fs_read(final_path)
// Clean up all temp files.
exec_command("rm -f " + tmp_gen + " " + tmp_min)
if do_obfuscate {
exec_command("rm -f " + final_path)
}
if argc >= 2 {
let out_path: String = native_list_get(positional, 1)
let ok: Bool = fs_write(out_path, final_js)
if ok {
return
} else {
println("el-compiler: failed to write output")
exit(1)
}
}
// No output file: print final JS to stdout.
print(final_js)
}
// main CLI entry point.
//
// elc <source.el> # emit C to stdout
// elc --target=js <source.el> # emit JS to stdout
// elc --target=c <source.el> <out.c> # write C to file
// elc --target=js <source.el> <out.js> # write JS to file
// elc <source.el> # emit C to stdout
// elc --target=js <source.el> # emit JS (module) to stdout
// elc --target=js --bundle <source.el> # emit self-contained JS (IIFE) to stdout
// elc --target=js --bundle --minify <source.el> # emit minified IIFE to stdout
// elc --target=js --bundle --obfuscate <source.el> # emit minified+obfuscated IIFE to stdout
// elc --target=c <source.el> <out.c> # write C to file
// elc --target=js <source.el> <out.js> # write JS to file
// elc --target=js --bundle <source.el> <out.js> # write bundled JS to file
// elc --target=js --bundle --minify <source.el> <out.min.js> # write minified JS to file
fn main() -> Void {
let argv: [String] = args()
// Use `tgt` not `target`: `target` is a reserved keyword in the lexer
@@ -262,12 +473,28 @@ fn main() -> Void {
// because the function-name position has no token-class restriction.
let tgt: String = detect_target(argv)
let do_emit_header: Bool = detect_emit_header(argv)
let do_bundle: Bool = detect_bundle(argv)
let do_minify: Bool = detect_minify(argv)
let do_obfuscate: Bool = detect_obfuscate(argv)
// --obfuscate implies --minify: obfuscating unminified code is pointless.
if do_obfuscate {
let do_minify = true
}
let positional: [String] = strip_flags(argv)
let argc: Int = native_list_len(positional)
if argc < 1 {
println("el-compiler: usage: elc [--target=c|js] [--emit-header] <source.el> [<output>]")
println("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]")
exit(1)
}
// --minify and --obfuscate require --target=js
if do_minify {
if !str_eq(tgt, "js") {
println("el-compiler: error: --minify and --obfuscate require --target=js")
exit(1)
}
}
let src_path: String = native_list_get(positional, 0)
// When --emit-header is requested, parse the source file directly
@@ -283,7 +510,24 @@ fn main() -> Void {
}
let source: String = resolve_imports(src_path)
let out: String = compile_dispatch(tgt, source)
// When post-processing (--minify or --obfuscate) is requested, redirect
// stdout to a temp file so codegen output can be captured and piped through
// the external tools. After codegen, restore stdout before emitting the
// final result.
if do_minify {
run_with_postprocess(tgt, source, src_path, do_bundle, do_obfuscate, argc, positional)
exit(0)
}
// Standard path (no post-processing).
let out: String = ""
if do_bundle {
let runtime_path: String = resolve_runtime_path(src_path)
let out = compile_dispatch_bundle(tgt, source, runtime_path)
} else {
let out = compile_dispatch(tgt, source)
}
if argc >= 2 {
let out_path: String = native_list_get(positional, 1)
let ok: Bool = fs_write(out_path, out)
+29 -26
View File
@@ -146,6 +146,9 @@ fn keyword_kind(word: String) -> String {
if word == "engine" { return "Engine" }
if word == "accessor" { return "Accessor" }
if word == "vessel" { return "Vessel" }
if word == "extern" { return "Extern" }
if word == "try" { return "Try" }
if word == "catch" { return "Catch" }
""
}
@@ -156,7 +159,7 @@ fn keyword_kind(word: String) -> String {
// Returns { "text": ..., "pos": i }
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
let i = start
let text = ""
let parts: [String] = native_list_empty()
let running = true
while running {
if i >= total {
@@ -164,20 +167,20 @@ fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
} else {
let ch: String = native_list_get(chars, i)
if lex_is_digit(ch) {
let text = text + ch
let parts = native_list_append(parts, ch)
let i = i + 1
} else {
let running = false
}
}
}
{ "text": text, "pos": i }
{ "text": str_join(parts, ""), "pos": i }
}
// 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 text = ""
let parts: [String] = native_list_empty()
let running = true
while running {
if i >= total {
@@ -185,14 +188,14 @@ fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
} else {
let ch: String = native_list_get(chars, i)
if is_alnum_or_underscore(ch) {
let text = text + ch
let parts = native_list_append(parts, ch)
let i = i + 1
} else {
let running = false
}
}
}
{ "text": text, "pos": i }
{ "text": str_join(parts, ""), "pos": i }
}
// Code-bearing string detection + comment strip
@@ -253,7 +256,7 @@ fn looks_like_code(s: String) -> Bool {
fn strip_code_comments(s: String) -> String {
let chars: [String] = native_string_chars(s)
let total: Int = native_list_len(chars)
let out = ""
let out_parts: [String] = native_list_empty()
let i = 0
let in_squote = false
let in_dquote = false
@@ -269,11 +272,11 @@ fn strip_code_comments(s: String) -> String {
if in_js_string {
// Backslash escape: consume next char verbatim regardless of which.
if ch == "\\" {
let out = out + ch
let out_parts = native_list_append(out_parts, ch)
let next_i = i + 1
if next_i < total {
let nc: String = native_list_get(chars, next_i)
let out = out + nc
let out_parts = native_list_append(out_parts, nc)
let prev = nc
let i = next_i + 1
} else {
@@ -292,7 +295,7 @@ fn strip_code_comments(s: String) -> String {
}
}
}
let out = out + ch
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
}
@@ -308,7 +311,7 @@ fn strip_code_comments(s: String) -> String {
if next_ch == "/" {
// URL guard: prev char ':' means this is "://", not a comment.
if prev == ":" {
let out = out + ch
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
@@ -360,7 +363,7 @@ fn strip_code_comments(s: String) -> String {
}
let prev = ""
} else {
let out = out + ch
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
}
@@ -369,23 +372,23 @@ fn strip_code_comments(s: String) -> String {
// Open a JS string?
if ch == "'" {
let in_squote = true
let out = out + ch
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
if ch == "\"" {
let in_dquote = true
let out = out + ch
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
if ch == "`" {
let in_btick = true
let out = out + ch
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
let out = out + ch
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
}
@@ -394,14 +397,14 @@ fn strip_code_comments(s: String) -> String {
}
}
}
out
str_join(out_parts, "")
}
// 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
let text = ""
let parts: [String] = native_list_empty()
let running = true
while running {
if i >= total {
@@ -414,26 +417,26 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
if next_ch == "\"" {
let text = text + "\""
let parts = native_list_append(parts, "\"")
let i = next_i + 1
} else {
if next_ch == "n" {
let text = text + "\n"
let parts = native_list_append(parts, "\n")
let i = next_i + 1
} else {
if next_ch == "t" {
let text = text + "\t"
let parts = native_list_append(parts, "\t")
let i = next_i + 1
} else {
if next_ch == "r" {
let text = text + "\r"
let parts = native_list_append(parts, "\r")
let i = next_i + 1
} else {
if next_ch == "\\" {
let text = text + "\\"
let parts = native_list_append(parts, "\\")
let i = next_i + 1
} else {
let text = text + next_ch
let parts = native_list_append(parts, next_ch)
let i = next_i + 1
}
}
@@ -448,13 +451,13 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
let i = i + 1
let running = false
} else {
let text = text + ch
let parts = native_list_append(parts, ch)
let i = i + 1
}
}
}
}
{ "text": text, "pos": i }
{ "text": str_join(parts, ""), "pos": i }
}
// Main lexer
+94 -1
View File
@@ -279,6 +279,30 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
return r
}
// Anonymous function literal (lambda): fn(params) -> RetType { body }
// Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... })
// Produces a Lambda expression node (distinct from a named FnDef statement).
if k == "Fn" {
let p = pos + 1
let r = parse_params(tokens, p)
let params = r["params"]
let p = r["pos"]
let ret_type = ""
let k2 = tok_kind(tokens, p)
if k2 == "Arrow" {
let p = p + 1
let kt = tok_kind(tokens, p)
if kt == "Ident" {
let ret_type = tok_value(tokens, p)
}
let p = skip_type(tokens, p)
}
let r2 = parse_block(tokens, p)
let body = r2["stmts"]
let p = r2["pos"]
return make_result({ "expr": "Lambda", "params": params, "body": body, "ret_type": ret_type }, p)
}
// Unary not
if k == "Not" {
let r = parse_primary(tokens, pos + 1)
@@ -408,6 +432,13 @@ fn parse_pattern(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
if v == "_" {
return make_result({ "pattern": "Wildcard" }, pos + 1)
}
// Check for Enum::Variant pattern (Color::Red, Status::Ok, etc.)
// Lexed as: Ident ColonColon Ident
let next_k = tok_kind(tokens, pos + 1)
if next_k == "ColonColon" {
let variant_name = tok_value(tokens, pos + 2)
return make_result({ "pattern": "Variant", "enum_name": v, "variant": variant_name }, pos + 3)
}
return make_result({ "pattern": "Binding", "name": v }, pos + 1)
}
if k == "Int" {
@@ -687,6 +718,29 @@ 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)
if k == "Extern" {
let p = pos + 1
let k2: String = tok_kind(tokens, p)
if str_eq(k2, "Fn") {
let p = p + 1
let name: String = tok_value(tokens, p)
let p = p + 1
let r = parse_params(tokens, p)
let params = r["params"]
let p = r["pos"]
let ret_type = ""
let k3: String = tok_kind(tokens, p)
if str_eq(k3, "Arrow") {
let p = p + 1
let kt: String = tok_kind(tokens, p)
if str_eq(kt, "Ident") { let ret_type = tok_value(tokens, p) }
let p = skip_type(tokens, p)
}
return make_result({ "stmt": "ExternFn", "name": name, "params": params, "ret_type": ret_type }, p)
}
}
// fn definition
if k == "Fn" {
let p = pos + 1
@@ -714,11 +768,16 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, p)
}
// type definition
// type definition: `type Name = { field: Type, ... }`
// The `=` between the name and the brace is optional in the spec but
// present in practice. Skip it if present before consuming the LBrace.
if k == "Type" {
let p = pos + 1
let name = tok_value(tokens, p)
let p = p + 1
// Consume optional `=` before the opening brace
let pk = tok_kind(tokens, p)
if pk == "Eq" { let p = p + 1 }
let p = expect(tokens, p, "LBrace")
let fields: [Map<String, Any>] = native_list_empty()
let running = true
@@ -853,6 +912,40 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p)
}
// try/catch statement
// try { body } catch (name: Type) { handler }
// The catch variable name and type are both captured; type is skipped.
if k == "Try" {
let p = pos + 1
let r_try = parse_block(tokens, p)
let try_body = r_try["stmts"]
let p = r_try["pos"]
let catch_name = "err"
let k2 = tok_kind(tokens, p)
if str_eq(k2, "Catch") {
let p = p + 1
let p = expect(tokens, p, "LParen")
// catch variable name
let kn = tok_kind(tokens, p)
if str_eq(kn, "Ident") {
let catch_name = tok_value(tokens, p)
let p = p + 1
}
// optional type annotation: : Type
let k3 = tok_kind(tokens, p)
if str_eq(k3, "Colon") {
let p = p + 1
let p = skip_type(tokens, p)
}
let p = expect(tokens, p, "RParen")
let r_catch = parse_block(tokens, p)
let catch_body = r_catch["stmts"]
let p = r_catch["pos"]
return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": catch_body }, p)
}
return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": native_list_empty() }, p)
}
// @decorator capture decorator name and attach to following stmt
if k == "At" {
let p = pos + 1
+27 -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)
@@ -234,7 +234,9 @@ fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: B
return true
}
let cmd: String = elc_bin + " --emit-header " + src_path + " " + c_out
// elc streams C to stdout (collect mode not yet implemented); use
// shell redirection so the output lands in the file, not the terminal.
let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>&1"
println(" compile " + src_path)
if dry_run { return true }
@@ -244,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 -I " + dirname_of(runtime_path) + " -I " + out_dir)
let i = 0
while i < n {
let f: String = native_list_get(c_files, i)
@@ -271,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()
@@ -309,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)
}
@@ -357,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)
}
+200
View File
@@ -0,0 +1,200 @@
// browser-auth.el -- El-compiled auth flow using Supabase
//
// Compile: elc --target=js --bundle examples/browser-auth.el > auth.js
// (requires el_runtime.js in the same directory as browser-auth.el)
//
// Demonstrates:
// - extern fn for declaring Supabase client constructor
// - anonymous function literals for callbacks
// - method call syntax on Any-typed values (client.auth.signInWithOtp)
// - try/catch for error handling
// - @async functions with DOM interaction
// - DOM bridge: dom_get_element, dom_get_value, dom_set_text, dom_add_class
// dom_remove_class, dom_show, dom_hide, dom_is_null
// - window_set to expose El functions to the browser global scope
// - local_storage_set/get for session hints
// - set_timeout for transient UI state
// - state_set/get for component state
//
// Expected HTML elements:
// #acct-email-input -- email text input
// #send-link-btn -- submit button
// #auth-message -- status message container
// #auth-form -- the form to hide after success
//
// The Supabase JS SDK is loaded from CDN via a <script> tag before auth.js.
// supabase_create_client is declared extern: the runtime provides it via
// the global supabase.createClient function exposed by the CDN bundle.
// External declarations
//
// These functions are provided by the JS environment (CDN script tags).
// No body is emitted -- the compiler just records the names.
extern fn supabase_create_client(url: String, key: String) -> Any
// UI helpers
fn show_message(text: String, is_error: Bool) -> Void {
let msg_el = dom_get_element("auth-message")
if !dom_is_null(msg_el) {
dom_set_text(msg_el, text)
dom_remove_class(msg_el, "hidden")
if is_error {
dom_add_class(msg_el, "error")
dom_remove_class(msg_el, "success")
} else {
dom_add_class(msg_el, "success")
dom_remove_class(msg_el, "error")
}
}
}
fn set_button_loading(loading: Bool) -> Void {
let btn = dom_get_element("send-link-btn")
if !dom_is_null(btn) {
if loading {
dom_set_text(btn, "Sending...")
dom_set_attr(btn, "disabled", "true")
} else {
dom_set_text(btn, "Send Magic Link")
dom_remove_attr(btn, "disabled")
}
}
}
fn clear_message() -> Void {
let msg_el = dom_get_element("auth-message")
if !dom_is_null(msg_el) {
dom_add_class(msg_el, "hidden")
dom_set_text(msg_el, "")
}
}
// Email validation
fn is_valid_email(email: String) -> Bool {
let trimmed: String = str_trim(email)
if str_len(trimmed) < 5 { return false }
let at_pos: Int = str_index_of(trimmed, "@")
if at_pos < 1 { return false }
let dot_pos: Int = str_index_of(trimmed, ".")
if dot_pos < at_pos + 2 { return false }
return true
}
// Supabase client construction
//
// Build a Supabase client from config injected into the page as NEURON_CFG.
// The extern fn supabase_create_client maps to supabase.createClient on
// the global object exposed by the CDN bundle.
fn get_supabase_client() -> Any {
let cfg = window_get("NEURON_CFG")
if dom_is_null(cfg) {
return null
}
let url: String = cfg["supabaseUrl"]
let key: String = cfg["supabaseAnonKey"]
supabase_create_client(url, key)
}
// Auth flow
@async
fn send_magic_link() -> Void {
let email_el = dom_get_element("acct-email-input")
if dom_is_null(email_el) {
show_message("Could not find email input", true)
return null
}
let email: String = str_trim(dom_get_value(email_el))
if !is_valid_email(email) {
show_message("Please enter a valid email address", true)
return null
}
clear_message()
set_button_loading(true)
state_set("auth_email", email)
// Build the Supabase client and call auth.signInWithOtp directly.
// Method call syntax on Any-typed values: client.auth.signInWithOtp(opts)
// No native_js_call required.
let client = get_supabase_client()
if dom_is_null(client) {
show_message("Auth service not configured", true)
set_button_loading(false)
return null
}
try {
let opts: Map<String, Any> = { "email": email }
// client is Any-typed; .auth returns the auth sub-client (also Any).
// .signInWithOtp(opts) returns a Promise. @async + await handles it.
let resp = client.auth.signInWithOtp(opts)
let err = resp["error"]
if !dom_is_null(err) {
let msg: String = err["message"]
show_message("Error: " + msg, true)
} else {
local_storage_set("auth_pending_email", email)
show_message("Magic link sent! Check your inbox for " + email, false)
let form = dom_get_element("auth-form")
if !dom_is_null(form) {
dom_hide(form)
}
}
} catch (err: Any) {
show_message("Unexpected error. Please try again.", true)
}
set_button_loading(false)
}
// Keyboard support
fn handle_email_keydown(event: Any) -> Void {
let key: String = dom_get_prop(event, "key")
if str_eq(key, "Enter") {
send_magic_link()
}
}
// Initialization
fn init_auth() -> Void {
let email_el = dom_get_element("acct-email-input")
if !dom_is_null(email_el) {
// Pre-fill from local storage if a pending send was interrupted.
let pending: String = local_storage_get("auth_pending_email")
if !str_eq(pending, "") {
dom_set_value(email_el, pending)
}
// Anonymous function literal for inline event handler.
dom_listen(email_el, "keydown", fn(event: Any) -> Void {
let key: String = dom_get_prop(event, "key")
if str_eq(key, "Enter") {
send_magic_link()
}
})
}
let btn = dom_get_element("send-link-btn")
if !dom_is_null(btn) {
dom_listen(btn, "click", fn(event: Any) -> Void {
send_magic_link()
})
}
state_set("auth_initialized", "true")
}
fn main() -> Void {
// Expose send_magic_link globally so inline event handlers can call it.
window_set("sendMagicLink", send_magic_link)
window_set("initAuth", init_auth)
// Run init when DOM is ready.
window_on_load(init_auth)
}
+41
View File
@@ -0,0 +1,41 @@
// browser-counter.el canonical browser DOM bridge example
//
// Compile with: elc --target=js examples/browser-counter.el > counter.js
//
// Then include in an HTML page that has a <span id="count-display"> element.
// The page can call window.increment() from any onclick handler, e.g.:
// <button onclick="increment()">+1</button>
//
// On load the display is initialised to "0". Each call to increment()
// adds 1 and updates the display text.
//
// Demonstrates:
// - dom_get_element to locate a DOM node by id
// - dom_set_text to update visible text content
// - dom_is_null to guard against missing elements
// - window_set to expose an El function for inline event handlers
// - state_set/get for in-memory counter state (survives calls, resets
// on page reload same semantics as the C state_* API)
fn init() -> Void {
state_set("counter", 0)
let display = dom_get_element("count-display")
if !dom_is_null(display) {
dom_set_text(display, "0")
}
}
fn increment() -> Void {
let current = str_to_int(state_get("counter"))
let next = current + 1
state_set("counter", next)
let display = dom_get_element("count-display")
if !dom_is_null(display) {
dom_set_text(display, int_to_str(next))
}
}
fn main() -> Void {
init()
window_set("increment", increment)
}
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
+275 -45
View File
@@ -1,6 +1,6 @@
# El JavaScript Backend (codegen-js)
**Status:** scaffolded. Hello-world compiles and runs. ~50% language coverage. Core runtime (~30 builtins) implemented. CGI / DHARMA / LLM / Engram intentionally stubbed.
**Status:** Phase 5 complete. ~90% language coverage. Full browser JavaScript can be expressed structurally in El without any `native_js` escape hatches. All additions since Phase 4: anonymous function literals (lambda syntax), try/catch statement, extern fn declarations, direct JS method call syntax on Any-typed values, Promise helpers, Object/Array utilities, and URL import declarations. Proof: `examples/browser-auth.el` is a complete Supabase auth flow with zero `native_js` or `native_js_call` calls.
**Authoritative files**
@@ -57,27 +57,36 @@ Same function names as `el_runtime.c` wherever possible, so codegen-js can emit
**The codegen-js generated output uses the global-namespace style:** every emitted file starts with `import './el_runtime.js'` (which side-effects the globals) so call sites stay flat — `println(x)` not `el.println(x)`. This matches the C backend's flat call surface and keeps the generated code grep-compatible across targets.
### Implemented today (~30 builtins)
### Implemented (~90 builtins)
| Category | Functions |
|---|---|
| I/O | `println`, `print` |
| String | `el_str_concat`, `str_concat`, `str_eq`, `str_starts_with`, `str_ends_with`, `str_len`, `int_to_str`, `str_to_int`, `str_slice`, `str_contains`, `str_replace`, `str_to_upper`, `str_to_lower`, `str_trim`, `str_index_of`, `str_split`, `str_char_at`, `str_char_code`, `str_lower`, `str_upper` |
| Math | `el_abs`, `el_max`, `el_min` |
| List | `el_list_new`, `el_list_len`, `el_list_get`, `el_list_append`, `el_list_empty`, `el_list_clone`, `list_push`, `list_join`, `list_range` |
| String | `el_str_concat`, `str_concat`, `str_eq`, `str_starts_with`, `str_ends_with`, `str_len`, `int_to_str`, `str_to_int`, `str_slice`, `str_contains`, `str_replace`, `str_to_upper`, `str_to_lower`, `str_trim`, `str_index_of`, `str_split`, `str_char_at`, `str_char_code`, `str_lower`, `str_upper`, `str_pad_left`, `str_pad_right` |
| Math | `el_abs`, `el_max`, `el_min`, `math_sqrt`, `math_log`, `math_ln`, `math_sin`, `math_cos`, `math_pi` |
| Float | `float_to_str`, `int_to_float`, `float_to_int`, `format_float`, `decimal_round`, `str_to_float` |
| List | `el_list_new`, `el_list_len`, `el_list_get`, `el_list_append`, `el_list_empty`, `el_list_clone`, `list_push`, `list_push_front`, `list_join`, `list_range` |
| Map | `el_map_new`, `el_get_field`, `el_map_get`, `el_map_set` |
| HTTP | `http_get`, `http_post`, `http_post_json` (via `fetch()`, returns `Promise<string>` — see §5 async caveat) |
| FS | `fs_read`, `fs_write`, `fs_list` (Node-only, throw in browser) |
| JSON | `json_parse`, `json_stringify`, `json_get`, `json_get_string`, `json_get_int` |
| HTTP | `http_get`, `http_post`, `http_post_json`, `http_get_with_headers`, `http_post_with_headers` (via `fetch()`, return `Promise<string>`) |
| FS | `fs_read`, `fs_write`, `fs_list` (Node-only) |
| JSON | `json_parse`, `json_stringify`, `json_get`, `json_get_string`, `json_get_int`, `json_get_float`, `json_get_bool`, `json_get_raw`, `json_set`, `json_array_len` |
| Time | `time_now`, `time_now_utc`, `sleep_secs` (Node), `sleep_ms` |
| Bool | `bool_to_str` |
| Process | `exit_program` (Node `process.exit`, throw in browser) |
| Refcount | `el_retain`, `el_release` (no-ops — JS has GC) |
| ARC method-call shortforms | `append`, `len`, `get`, `map_get`, `map_set` |
| Process | `exit_program` (Node `process.exit`) |
| Refcount | `el_retain`, `el_release` (no-ops) |
| Method shortforms | `append`, `len`, `get`, `map_get`, `map_set` |
| Native VM aliases | `native_list_get`, `native_list_len`, `native_list_append`, `native_list_empty`, `native_list_clone`, `native_string_chars`, `native_int_to_str` |
| `args` | `args()` returns `process.argv.slice(2)` in Node, `[]` in browser |
| `state_*` | In-memory `Map` keyed by string |
| `env` | `process.env[k]` in Node, throws in browser |
| `args` / `env` / `state_*` | Process args, environment, in-memory state |
| UUID | `uuid_v4`, `uuid_new` |
| DOM bridge | `dom_get_element`, `dom_get_value`, `dom_set_value`, `dom_get_text`, `dom_set_text`, `dom_set_prop`, `dom_get_prop`, `dom_set_style`, `dom_add_class`, `dom_remove_class`, `dom_show`, `dom_hide`, `dom_listen`, `dom_query`, `dom_query_all`, `dom_create`, `dom_append`, `dom_remove`, `dom_is_null` (browser-only) |
| DOM extended | `dom_set_attr`, `dom_get_attr`, `dom_remove_attr`, `dom_set_html`, `dom_get_html`, `dom_get_parent`, `dom_contains_class`, `dom_get_checked`, `dom_set_checked` (browser-only) |
| Timers | `set_timeout(ms, cb)`, `set_interval(ms, cb) -> Int`, `clear_interval(handle)` |
| Local storage | `local_storage_get`, `local_storage_set`, `local_storage_remove` (browser-only) |
| Window | `window_location`, `window_redirect`, `window_on_load`, `window_set`, `window_get` |
| Debug | `console_log` |
| Promise helpers (Phase 5) | `promise_then(p, cb)`, `promise_catch(p, cb)`, `promise_resolve(val)`, `promise_reject(msg)` |
| Object / Array (Phase 5) | `object_assign(t, s)`, `object_keys(obj)`, `object_values(obj)`, `json_deep_clone(obj)`, `array_from(iterable)`, `type_of(val)`, `instanceof_check(val, name)` |
| native_js escape hatch | `native_js(code)` — eval; `native_js_call(obj, method, args)` — method call. Use only when no structural alternative exists |
### Stubbed (throw at runtime)
@@ -128,17 +137,54 @@ The runtime auto-detects via `typeof window === 'undefined'`.
---
## 5. The async problem (the big deferred decision)
## 5. The async problem
`fetch()` is async. The C backend's `http_get(url)` is synchronous and returns the body string directly. El source was written assuming sync. Three options:
1. **Pretend it's sync from El's POV; use synchronous XHR (browser) or `child_process.execSync('curl ')` (Node).** Bad: synchronous XHR is deprecated and frozen on the main thread; `execSync` is a hack.
2. **Make every `http_*` builtin in the JS runtime return a `Promise`, and rewrite codegen-js to insert `await` everywhere.** This requires turning every El function that transitively calls a network builtin into an `async fn` in JS. Doable, but invasive — the El AST does not currently mark async-ness.
3. **Compile El's call sites with implicit await; compile-time taint tracking marks every fn that transitively calls a network builtin as `async`. Generated JS uses `async function` and `await`.** This is the right answer long-term.
1. **Pretend it's sync from El's POV; use synchronous XHR (browser) or `child_process.execSync('curl ...')` (Node).** Bad: synchronous XHR is deprecated and frozen on the main thread; `execSync` is a hack.
2. **Make every `http_*` builtin in the JS runtime return a `Promise`, and rewrite codegen-js to insert `await` everywhere.** This requires turning every El function that transitively calls a network builtin into an `async fn` in JS. Doable, but invasive.
3. **Explicit `@async` decorator on El functions; codegen-js emits `async function` + `await` for known-async call sites.** This is the approach implemented.
**Decision for this scaffold:** option 3, but only the runtime side is implemented. `http_get` in `el_runtime.js` returns a `Promise<string>`. `codegen-js.el` does NOT yet emit `async`/`await`. Calling `http_get` from compiled El will return a Promise that the El program will treat as a string (which produces `"[object Promise]"`). This is documented and accepted for the scaffold; the compile-time taint pass is a follow-up.
**Decision:** option 3, with an explicit opt-in decorator. `http_get`, `http_post`, `http_post_json`, `http_get_with_headers`, and `http_post_with_headers` in `el_runtime.js` return `Promise<string>`. `codegen-js.el` now emits `await` before calls to these builtins and before calls to any El function decorated `@async`.
For now, programs that don't touch HTTP work correctly. That covers `el-ui/runtime` (which only manipulates the DOM and a graph), most of cgi-studio's pure UI components, and all hello-world style programs.
### How to use async in El (JS target)
Mark a function with `@async` to declare it as async. Any call to that function from another El function will automatically get `await` in the generated JS. The callee must also be `@async` (or call only non-async code) for the pattern to compose correctly.
```el
@async
fn fetch_user(id: String) -> String {
http_get("https://api.example.com/users/" + id)
}
@async
fn main() -> Void {
let body = fetch_user("42")
println(body)
}
```
Compiles to:
```javascript
async function fetch_user(id) {
return await http_get("https://api.example.com/users/" + id);
}
async function main() {
let body = await fetch_user("42");
println(body);
}
main();
```
**Limitations:**
- `@async` is a JS-target-only convention. The C backend ignores the decorator (it calls the synchronous libcurl-backed version).
- Implicit taint propagation (auto-marking all transitive callers) is not implemented. The programmer must explicitly add `@async` to every function in the call chain that reaches an async builtin.
- Forward-reference calls to `@async` functions are handled correctly: codegen-js does a pre-registration pass over all FnDefs before emitting any code.
For programs that do not touch HTTP, no `@async` annotation is needed and the generated code is identical to before.
---
@@ -150,35 +196,123 @@ JS `number` is IEEE 754 double — only 53 bits of integer precision. El `Int` i
---
## 7. What's NOT supported in JS target initially
## 7. Language features — JS target coverage
This is the canonical list. Programs that use any of these compile (no `#error`-style fail-fast like the C backend's capability check) but throw at runtime or behave as documented.
### Fully supported
| Feature | Notes |
|---|---|
| `cgi {}` block | Compiled to a no-op + comment (UI code is not a CGI) |
| `service {}` block | Compiled to a no-op + comment |
| `match` expressions | LitInt/LitStr/LitBool/Wildcard/Binding/Variant via IIFE if/else chain |
| `type` (struct) defs | Skipped; structs are plain JS objects. `t["field"]` works |
| `enum` defs | Skipped; enum values are strings or ints |
| `?` postfix (nil-prop) | `obj?.field` emits `(obj)?.["field"] ?? null` via JS optional chaining |
| `extern fn` | Emits a comment; calls resolve to JS environment globals |
| Anonymous function literals | `fn(p: T) -> R { body }` emits a hoisted `function __lambda_N(p)` |
| `try/catch` | Emits `try { ... } catch (name) { ... }` directly |
| URL imports | `import "https://..."` emits ES module import (or comment in bundle mode) |
| Method call on `Any` | `obj.method(args)` emits `obj.method(args)` for non-El-shortform methods |
| Field access on `Any` | `obj.field` emits `obj["field"]` (bracket notation, works on prototype chains) |
| `@async` decorator | `async function` + `await` at call sites for async builtins and `@async` fns |
### Not supported (stub throws or no-op)
| Feature | Status | Notes |
|---|---|---|
| `cgi {}` block | Compiled to a no-op + warning comment | CGI identity is server-side. UI code is not a CGI. |
| `service {}` block | Compiled to a no-op + warning comment | Same. |
| All `dharma_*` | Stub throws | Programs needing DHARMA must call a server-side daemon over HTTP |
| All `engram_*` | Stub throws | Could be ported to in-browser (IndexedDB-backed) later |
| All `llm_*` | Stub throws | Browser cannot hold API keys; route through server |
| `llm_register_tool` | Stub throws | Same |
| `http_serve` | Stub throws | Browsers cannot serve. Node-mode could, deferred |
| `http_set_handler` | Stub throws | Same |
| `match` expressions | Compiled (basic) | LitInt/LitStr/LitBool/Wildcard/Binding all work via `if/else` chain. Tagged-union match deferred |
| `type` (struct) defs | Skipped at codegen | Treated as documentation; structs are plain JS objects. `t["field"]` works |
| `enum` defs | Skipped at codegen | Same — enum values are bare strings or ints |
| `?` postfix (nil-prop) | No-op | Same as C backend's current state |
| `try` postfix | Stripped to inner | Same as C backend |
| Capability enforcement | Not enforced | The C backend uses `#error` directives; the JS backend lets the runtime stubs throw. Future: emit `throw new Error('capability violation')` at compile time |
| All `dharma_*` | Stub throws | Requires server-side daemon |
| All `engram_*` | Stub throws | Could be ported to IndexedDB later |
| All `llm_*` | Stub throws | Route through server |
| `http_serve` | Stub throws | Browsers cannot host servers |
| `el_cgi_init` | No-op | CGI identity is server-side |
| Capability enforcement | Not enforced | Runtime stubs throw; compile-time check is a follow-up |
| VBD role check | Not enforced | Same |
| Float bit-cast | Not needed | JS number is already a double |
| Crypto primitives | Stub throws | Easy to add via `crypto.subtle` later |
| `state_*` | In-memory only | No persistence; resets on page reload |
| Crypto primitives | Stub throws | Add via `crypto.subtle` later |
| `state_*` | In-memory only | Resets on page reload |
| `args()` | Node-only | Browser returns `[]` |
| `fs_*` | Node-only | Browser throws |
---
## 7a. Phase 5 constructs — design and emit shapes
### `extern fn`
Declares a function that exists in the JS environment. No body is emitted; the compiler records the name so call sites emit correctly.
```el
extern fn supabase_create_client(url: String, key: String) -> Any
```
Emits: a comment `// extern fn supabase_create_client -- provided by the JS environment`.
Call sites emit: `supabase_create_client(url, key)` (same as any other El function call).
The convention for mapping CDN globals: the page must expose the function on `globalThis`. For Supabase, the CDN bundle exposes `supabase.createClient`; a thin adapter assigns `globalThis.supabase_create_client = supabase.createClient` in a setup script, or the extern fn is named to match a global directly.
### Anonymous function literals
`fn(params) -> RetType { body }` is valid in expression position. Emitted as a hoisted function declaration with a generated name.
```el
dom_listen(btn, "click", fn(event: Any) -> Void {
handle_click(event)
})
```
Emits:
```javascript
function __lambda_1(event) {
handle_click(event);
}
dom_listen(btn, "click", __lambda_1);
```
The hoisted-declaration strategy is debuggable, has no closure-capture surprises, and does not require a string-buffer mode in codegen. The generated name appears in stack traces.
### `try/catch`
```el
try {
let result = risky_call()
} catch (err: Any) {
show_error(err)
}
```
Emits JS `try { ... } catch (err) { ... }` directly. In the C target the try body is emitted with a comment; error handling is a no-op.
### Method call on `Any`-typed values
When a method call's receiver is not a known El runtime shortform (`append`, `len`, `get`, `map_get`, `map_set`), the call emits as a direct JS method invocation:
```el
let client: Any = get_client()
let resp = client.auth.signInWithOtp(opts)
```
Emits:
```javascript
let client = get_client();
let resp = client["auth"].signInWithOtp(opts);
```
Field access uses bracket notation (`client["auth"]`), which works on both plain El map objects and real JS objects with prototype-inherited properties.
### URL imports
```el
import "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js"
```
In module mode: `import "https://...";` at the top of the generated file.
In bundle/IIFE mode: `// external: https://...` comment.
El source imports (`.el` files) are excluded -- they were already inlined by `resolve_imports`.
---
## 8. CLI dispatch — `--target=js`
The compiler entry point `compiler.el` adds a `compile_js(source: String) -> String` alongside the existing `compile()`. The CLI behavior:
@@ -195,20 +329,116 @@ The argv parser scans for a `--target=<lang>` token; remaining positional args a
---
## 8a. Production output — `--minify` and `--obfuscate`
Two post-processing flags produce production-ready browser JS in a single compiler invocation, replacing any external post-processing scripts.
### Usage
```
elc --target=js --bundle --minify source.el > output.min.js
elc --target=js --bundle --obfuscate source.el > output.obf.js
elc --target=js --bundle --minify --obfuscate source.el > output.final.js
```
Both flags require `--target=js`. Passing either without `--target=js` prints an error and exits with code 1.
`--obfuscate` implies `--minify` — obfuscating unminified code produces no benefit and only increases output size.
### Pipeline order
```
generate JS -> (if --bundle, wrap in IIFE) -> (if --minify, run terser) -> (if --obfuscate, run javascript-obfuscator) -> output
```
### Tool discovery
The compiler looks for each tool in this order:
1. `<src_dir>/node_modules/.bin/<tool>` — local install next to source file
2. `<src_dir>/../node_modules/.bin/<tool>` — one level up (monorepo layout)
3. `npx --yes <tool>` — fall back to npx (uses globally cached package or downloads on first use)
If no path resolves and npx is not on `PATH`, the compiler prints a clear error and exits non-zero:
```
el-compiler: error: terser not found. Run 'npm install terser' in your project directory.
el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory.
```
### Minification (terser)
Command issued internally:
```
terser <tmpfile> --compress passes=2,drop_console=false,drop_debugger=true \
--mangle 'reserved=[<reserved>]' --output <tmpfile.min>
```
### Obfuscation (javascript-obfuscator)
Command issued internally (runs after minification):
```
javascript-obfuscator <input> --output <output>
--compact true
--simplify true
--string-array true
--string-array-encoding base64
--string-array-threshold 0.75
--identifier-names-generator hexadecimal
--rename-globals false
--self-defending false
--reserved-names <reserved>
```
### Reserved names
These identifiers are protected from renaming by both tools. They are referenced directly from HTML `onclick=` attributes and other global-scope callsites:
```
neuronDemoToggle, neuronDemoSend, neuronDemoReset,
signInWith, signInWithEmail, signUpWithEmail, sendMagicLink,
signOut, resetPassword, sendResetEmail, updatePassword,
showSignIn, showSignUp, hideReset,
setSort, addFamilyMember, removeFamilyMember, copyForPlatform, entHeadcountChange,
NEURON_CFG
```
### Temp files
The compiler uses `/tmp/elc-<pid>-<timestamp>.js` naming for temp files. All temp files are cleaned up on both success and failure paths.
### Implementation notes
- The compiler adds `stdout_to_file(path)` / `stdout_restore()` builtins to the C runtime (`el_runtime.c`) to capture codegen output (which is streamed via `println`) into a temp file before passing it to the external tools.
- `--minify` and `--obfuscate` error messages are printed after stdout is restored, so they always reach the terminal regardless of output redirection.
---
## 9. The path to compiling el-ui/runtime through this backend
This is the real-world test. `el-ui/runtime/src/` is currently 5 hand-written `.js` files. The path to authoring them in El:
1. **Phase 1 — Hello-world** (this scaffold). Done.
2. **Phase 2 — language coverage.** Get codegen-js to ~95% parity with codegen.el for non-network features. Specifically: `match`, struct/enum field access, `?`-propagation, full `for`-over-list, complete unary/binary operators, lexical closures (the C backend doesn't have these but we'll need them for el-ui's component model).
3. **Phase 3 — DOM bridge.** Add `dom_*` builtins to el_runtime.js: `dom_create_element`, `dom_set_text`, `dom_append_child`, `dom_query`, `dom_listen`, etc. These are Node-as-El builtins for the browser; the C backend will add a stub set that errors. Source-shareable El UI code becomes possible.
4. **Phase 4 — Component class lowering.** El doesn't have classes; el-ui's `Component` is a JS class. Decide: extend El with a `component` keyword that compiles to JS class + C struct? Or have el-ui authors define components as `fn render_<name>(state) -> String` and provide a small bootstrap. The latter is the lower-impact path.
5. **Phase 5 — Async taint pass.** Implement compile-time async tracking so `http_get` and friends produce `await fetch()` correctly. Required before authoring code that fetches data.
6. **Phase 6 — Port `el-ui/runtime/`.** Translate the 5 JS files to El, compile to JS, swap in. Run el-ui's existing tests. Iterate.
1. **Phase 1 — Hello-world.** DONE.
2. **Phase 2 — Language coverage.** DONE. `match`, struct/enum field access, `?`-propagation, `for`-over-list, complete operators.
3. **Phase 3 — DOM bridge.** DONE. Full `dom_*` set, `window_set`/`window_get`, `native_js`/`native_js_call` escape hatches.
4. **Phase 4 — Production output.** DONE. `--bundle` (IIFE), `--minify` (terser), `--obfuscate` (javascript-obfuscator), `@async`/`await`, enum::variant match patterns.
5. **Phase 5 — Full JS expression coverage.** DONE. This is the phase documented in this revision.
- `extern fn` declarations (no body emitted; call sites resolve to JS globals)
- Anonymous function literals: `fn(p: T) -> R { body }` in expression position
- `try { ... } catch (name: T) { ... }` statement
- Method call on `Any`-typed values: `client.auth.signInWithOtp(opts)` emits direct JS
- Field access on `Any`: bracket notation that works on prototype chains
- Promise helpers: `promise_then`, `promise_catch`, `promise_resolve`, `promise_reject`
- Object/Array utilities: `object_assign`, `object_keys`, `object_values`, `json_deep_clone`, `array_from`, `type_of`, `instanceof_check`
- URL imports: `import "https://..."` emits ES module import
- **Proof**: `examples/browser-auth.el` -- complete Supabase auth flow with zero `native_js` or `native_js_call`
6. **Phase 6 — Port `el-ui/runtime/`.** Translate the 5 JS files to El, compile to JS, swap in. Run el-ui's existing tests. The language is now expressive enough for this.
7. **Phase 7 — Port cgi-studio UI.** Larger surface area; same pattern.
8. **Phase 8 — Marketplace plugins.** Open the door for third-party UI El.
The blocking item between phase 1 and phase 2 is incremental — every El construct used by el-ui's source needs codegen-js coverage. Phase 5 (async) is the architectural decision that needs explicit user buy-in, because it changes the language's effective semantics on the JS target.
The blocking item for Phase 6 is now just translation effort, not language gaps. Phase 5 removed the last structural barriers.
---