Compare commits

...

42 Commits

Author SHA1 Message Date
Will Anderson 3f83adf458 ci: retrigger after ci-base image rebuild
El CI -dev / build-and-test (pull_request) Failing after 23s
2026-05-04 20:15:52 -05:00
Will Anderson 9f734b037c add comprehensive native test suite
El CI -dev / build-and-test (pull_request) Failing after 21s
2026-05-04 19:58:23 -05:00
Will Anderson 049a7712f4 ci: add -lm and allow-multiple-definition for bootstrap build
El CI -dev / build-and-test (pull_request) Failing after 54s
el_runtime.c uses pow/sqrt/log/sin/cos/exp - needs -lm.
elc-bootstrap.c predates the text-processing primitives commit so it
has its own C definitions of is_digit/is_whitespace; -Wl,--allow-multiple-definition
lets the linker accept both (equivalent implementations).
2026-05-04 16:08:47 -05:00
Will Anderson c64cbd21e2 retrigger - capture build error
El CI -dev / build-and-test (pull_request) Failing after 2m4s
2026-05-04 16:00:49 -05:00
Will Anderson 37488e9485 retrigger CI debug
El CI -dev / build-and-test (pull_request) Failing after 35s
2026-05-04 15:56:17 -05:00
Will Anderson 8641b4045e retrigger CI - free capacity
El CI -dev / build-and-test (pull_request) Failing after 1m1s
2026-05-04 15:53:20 -05:00
Will Anderson 49d68fbb20 retrigger CI - fix gitea DNS on host
El CI -dev / build-and-test (pull_request) Failing after 59s
2026-05-04 15:50:28 -05:00
Will Anderson 77a0658d56 retrigger CI attempt 5
El CI -dev / build-and-test (pull_request) Failing after 13m51s
2026-05-04 15:32:11 -05:00
Will Anderson bff0ad4f22 retrigger CI attempt 4
El CI -dev / build-and-test (pull_request) Failing after 32s
2026-05-04 15:30:08 -05:00
Will Anderson 49f96126b2 retrigger CI attempt 3
El CI -dev / build-and-test (pull_request) Failing after 39s
2026-05-04 15:27:52 -05:00
Will Anderson c954142063 retrigger CI after runner restart
El CI -dev / build-and-test (pull_request) Failing after 36s
2026-05-04 15:24:58 -05:00
Will Anderson 3fd5fec965 retrigger CI
El CI -dev / build-and-test (pull_request) Failing after 7s
2026-05-04 15:23:36 -05:00
Will Anderson 5476cbb2b1 ci: fix YAML - remove colon in step name, replace em dashes
El CI -dev / build-and-test (pull_request) Failing after 1s
2026-05-04 14:33:30 -05:00
Will Anderson 65792f7e4c ci: add workflow_dispatch trigger 2026-05-04 14:32:37 -05:00
Will Anderson c09023003d ci: retrigger after workflow bootstrap 2026-05-04 14:23:34 -05:00
Will Anderson 15b9ccd9e2 ci: retrigger 2026-05-04 14:13:56 -05:00
Will Anderson 9163af81aa ci: trigger CI run 2026-05-04 14:11:00 -05:00
Will Anderson 3dababa4ad dist: update elc-new binary to match elc 2026-05-04 13:27:57 -05:00
Will Anderson 5888258c9f rebuild elc: reporter=json, line numbers, em-dash FAIL format
Rebuild elc binary from the feat/native-testing source to match the
full reporter implementation:
- Lexer tracks line numbers in every token via state (__lex_line)
- Parser propagates line numbers into TestDef and Assert AST nodes
- Text reporter: "  FAIL  <name> — <msg>" (em dash, stderr)
- JSON reporter (--reporter=json): newline-delimited JSON to stdout
  with suite_start, test_start, test_pass, test_fail, suite_end events
- All fields: file (basename), line (test block), assert_line, message
- Fixed point verified: gen2 == gen3

elc-combined.el regenerated from source.
2026-05-04 13:27:01 -05:00
Will Anderson a9dc38ed82 ci: add native El test step to dev pipeline 2026-05-04 13:26:09 -05:00
Will Anderson 4af2b687e1 feat: native test/assert system -- elc --test runs test blocks in El
Add test { } and assert to the El language: the parser recognises TestDef
and Assert nodes; the C and JS codegens emit inert no-ops in normal mode
and a full test runner (with RUN/PASS/FAIL output and non-zero exit on
failure) when invoked with --test. compiler.el wires up compile_test /
compile_js_test and exposes --test / --reporter flags in the CLI.

Add two native test suites under tests/native/ (test_text.el,
test_codegen_js.el) covering string primitives, arithmetic, and list
operations. All 22 new native tests pass; the four existing run.sh
acceptance corpora are unaffected.
2026-05-04 13:24:55 -05:00
Will Anderson 32f0cf7b5d Add html-page.el example and rebuild elc binary
examples/html-page.el demonstrates HTML template syntax:
- <!doctype html> prefix handling
- Attribute values (static and interpolated)
- {#each list as item} iteration
- Auto-escaped interpolation via {expr}
- Self-closing void elements (meta, br, etc.)

Rebuilt dist/platform/elc from modified compiler source. The new
binary is self-hosted from the HTML-capable compiler source and
passes the standard identity check.
2026-05-04 13:02:54 -05:00
Will Anderson 65e26cd7a5 Add HTML template codegen and runtime for JS backend
JS codegen (codegen-js.el):
- js_cg_html_template: emits an IIFE that builds HTML via += concat
- js_cg_html_element_str / js_cg_html_parts / js_cg_html_attrs_str:
  mirror the C codegen structure using JS string accumulator
- js_cg_html_each: {#each} compiles to a JS for-loop
- Reuses existing js_str_lit / js_escape from the file header

Runtime (el_runtime.js):
- html_escape(s): replaces & < > " ' using regex chains
- html_raw(s): identity function
- Both exported from the runtime module exports object
2026-05-04 13:02:50 -05:00
Will Anderson 1fd7cd5545 Add HTML template codegen and runtime for C backend
C codegen (codegen.el):
- cg_html_template: emits a GCC/Clang statement-expression that
  builds the HTML string via el_str_concat chains
- cg_html_element_str / cg_html_parts / cg_html_attrs_str: recursive
  element and attribute emitters
- cg_html_each: {#each} compiles to a C for-loop with el_list_get
- __html_counter state tracks unique accumulator variable names
- Handles both 'static' (raw string) and 'dynamic' (expr node) attrs
  matching the parser's attribute kind convention

Runtime (el_runtime.c / el_runtime.h):
- html_escape(s): escapes & < > " ' for safe interpolation
- html_raw(s): identity function for raw() bypass
- Both use the existing html_buf_t infrastructure from el_html_sanitize
2026-05-04 13:02:44 -05:00
Will Anderson 71689520b6 Add HTML template syntax to El parser
Adds native HTML template literals to the El parser. Templates are
detected in value position when Lt is followed by a known HTML tag
name (is_html_tag_name) or by '!' for <!doctype html>.

New parser helpers:
- is_html_tag_name / is_void_element: classify tag names
- parse_html_text_tokens: collect intertoken text content
- parse_html_attrs: parse name, name="val", name={expr} attributes
- parse_html_children: recursive children with {expr}, {#each} support
- parse_html_element / parse_html_template: entry points

Adds Hash token kind ('#') to lexer for {#each} block syntax.

AST nodes: HtmlTemplate, html:Element, html:Text, html:Interp,
html:Raw, html:Each, html:Doctype. Doctype flag is stored on the
root element node rather than as a separate AST layer.

HTML templates parse correctly after 'return' and as the sole
expression in a function body. The 'return' keyword is required when
other let bindings precede the template, as El has no newline-as-
statement-terminator and '<' would otherwise be parsed as comparison.
2026-05-04 13:02:36 -05:00
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
34 changed files with 26792 additions and 829 deletions
+117
View File
@@ -0,0 +1,117 @@
name: El CI -dev
on:
push:
branches:
- dev
pull_request:
branches:
- dev
workflow_dispatch:
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
# -Wl,--allow-multiple-definition: is_digit/is_whitespace exist in both
# elc-bootstrap.c (pre-dates runtime text primitives) and el_runtime.c.
# Both definitions are equivalent; allow the linker to pick one.
- name: Build elc from bootstrap (gen2)
run: |
gcc -O2 \
-I el-compiler/runtime \
dist/elc-bootstrap.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread -lm \
-Wl,--allow-multiple-definition \
-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 -lm \
-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
# Native El test suites (elc --test, compile-link-run)
- name: Run tests -native (text)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
-lcurl -lpthread -lm -o /tmp/el_native_text
/tmp/el_native_text
# Publish artifact to GCP Artifact Registry (dev)
- name: Publish elc to Artifact Registry (dev)
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 }}
+4122 -192
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+827 -176
View File
File diff suppressed because it is too large Load Diff
+125
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();
@@ -2516,6 +2602,45 @@ el_val_t el_html_sanitize(el_val_t input_v, el_val_t allowlist_v) {
return el_wrap_str(result);
}
/* ── html_escape / html_raw ──────────────────────────────────────────────── */
/*
* html_escape(s) escape a user-supplied string for safe inline interpolation
* in HTML text content or attribute values. Escapes: & < > " '
*
* html_raw(s) identity function; used by the `raw()` escape hatch in El HTML
* templates to explicitly opt out of escaping.
*/
el_val_t html_escape(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return EL_STR("");
html_buf_t out;
html_buf_init(&out);
for (const char* p = s; *p; p++) {
unsigned char c = (unsigned char)*p;
switch (c) {
case '&': html_buf_puts(&out, "&amp;"); break;
case '<': html_buf_puts(&out, "&lt;"); break;
case '>': html_buf_puts(&out, "&gt;"); break;
case '"': html_buf_puts(&out, "&quot;"); break;
case '\'': html_buf_puts(&out, "&#39;"); break;
default: html_buf_putc(&out, (char)c); break;
}
}
char* result = el_strbuf(out.len);
memcpy(result, out.data, out.len);
result[out.len] = '\0';
html_buf_free(&out);
return el_wrap_str(result);
}
el_val_t html_raw(el_val_t sv) {
/* Identity — returns the value unchanged. The name exists so generated
* code can call html_raw(expr) instead of expr directly, making it clear
* at the call site that escaping is intentionally bypassed. */
return sv;
}
/* ── JSON ────────────────────────────────────────────────────────────────── */
/* True iff the segment is non-empty and every byte is an ASCII digit. We treat
+11
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 ─────────────────────────────────────────────────────── */
@@ -212,6 +214,13 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
* where each value is the array of attribute names allowed for that tag. */
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
/* ── HTML template helpers ───────────────────────────────────────────────────
* Used by compiled El HTML template expressions.
* html_escape(s) escape & < > " ' for safe inline interpolation.
* html_raw(s) identity; explicit opt-out from escaping (`raw()` form). */
el_val_t html_escape(el_val_t s);
el_val_t html_raw(el_val_t s);
/* ── Filesystem ──────────────────────────────────────────────────────────── */
el_val_t fs_read(el_val_t path);
@@ -742,6 +751,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);
+388
View File
@@ -128,6 +128,22 @@ function str_pad_right(s, width, pad) {
return String(s).padEnd(width, String(pad));
}
// ── HTML template helpers ────────────────────────────────────────────────────
// Used by compiled El HTML template expressions.
// html_escape(s) — escape & < > " ' for safe inline interpolation.
// html_raw(s) — identity; explicit opt-out from escaping (raw() form).
function html_escape(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function html_raw(s) { return s; }
// ── Math ────────────────────────────────────────────────────────────────────
function el_abs(n) { return Math.abs(n); }
@@ -522,6 +538,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 +970,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,
@@ -670,10 +1033,35 @@ export {
fs_read, fs_write, fs_list,
json_parse, json_stringify, json_get, json_get_string, json_get_int,
time_now, time_now_utc, sleep_ms,
// HTML template helpers
html_escape, html_raw,
bool_to_str, exit_program, args, env,
state_set, state_get, state_del, state_keys,
el_cgi_init,
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
engram_node, engram_search, engram_activate,
llm_call, llm_call_system,
// DOM bridge
dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,
dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,
dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,
dom_append, dom_remove, dom_is_null,
// Extended DOM
dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,
dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,
// Timers
set_timeout, set_interval, clear_interval,
// Local storage
local_storage_get, local_storage_set, local_storage_remove,
// Window location
window_location, window_redirect, window_on_load,
// Debug
console_log,
// Window / native_js
window_set, window_get, native_js, native_js_call,
// Promise helpers
promise_then, promise_catch, promise_resolve, promise_reject,
// Object / Array utilities
object_assign, object_keys, object_values, json_deep_clone,
array_from, type_of, instanceof_check,
};
File diff suppressed because it is too large Load Diff
+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
+671 -48
View File
@@ -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 {
@@ -140,6 +191,138 @@ fn js_is_int_call(call_expr: Map<String, Any>) -> Bool {
return false
}
// HTML template codegen (JS)
//
// HTML template expressions compile to a JS IIFE that builds the HTML string
// using string concatenation. Interpolated values go through html_escape();
// raw() bypasses escaping. {#each} blocks compile to Array.forEach or a
// for-loop that pushes fragments into a parts array.
//
// Entry point: js_cg_html_template(expr) JS expression string.
fn js_next_html_id() -> String {
let csv: String = state_get("__js_html_counter")
let n = 0
if !str_eq(csv, "") {
let n = str_to_int(csv)
}
let n = n + 1
state_set("__js_html_counter", native_int_to_str(n))
native_int_to_str(n)
}
fn js_cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
let n: Int = native_list_len(children)
let i = 0
let out = ""
while i < n {
let child: Map<String, Any> = native_list_get(children, i)
let html_kind: String = child["html"]
if str_eq(html_kind, "Text") {
let text: String = child["text"]
let out = out + acc_var + " += " + js_str_lit(text) + "; "
}
if str_eq(html_kind, "Doctype") {
let out = out + acc_var + " += \"<!doctype html>\"; "
}
if str_eq(html_kind, "Interp") {
let val_node = child["value"]
let val_c: String = js_cg_expr(val_node)
let out = out + acc_var + " += html_escape(" + val_c + "); "
}
if str_eq(html_kind, "Raw") {
let val_node = child["value"]
let val_c: String = js_cg_expr(val_node)
let out = out + acc_var + " += html_raw(" + val_c + "); "
}
if str_eq(html_kind, "Element") {
let elem_c: String = js_cg_html_element_str(child, acc_var)
let out = out + elem_c
}
if str_eq(html_kind, "Each") {
let each_c: String = js_cg_html_each(child, acc_var)
let out = out + each_c
}
let i = i + 1
}
out
}
fn js_cg_html_attrs_str(attrs: [Map<String, Any>], acc_var: String) -> String {
let n: Int = native_list_len(attrs)
let i = 0
let out = ""
while i < n {
let attr: Map<String, Any> = native_list_get(attrs, i)
let attr_name: String = attr["name"]
let kind: String = attr["kind"]
// open-attr snippet: " name=\""
let open_val: String = " " + attr_name + "=\""
if str_eq(kind, "static") {
let sv: String = attr["value"]
let out = out + acc_var + " += " + js_str_lit(open_val) + "; "
let out = out + acc_var + " += " + js_str_lit(sv) + "; "
let out = out + acc_var + " += " + js_str_lit("\"") + "; "
} else {
if str_eq(kind, "dynamic") {
let val_node = attr["value"]
let val_c: String = js_cg_expr(val_node)
let out = out + acc_var + " += " + js_str_lit(open_val) + "; "
let out = out + acc_var + " += html_escape(" + val_c + "); "
let out = out + acc_var + " += " + js_str_lit("\"") + "; "
} else {
// Boolean attribute
let out = out + acc_var + " += " + js_str_lit(" " + attr_name) + "; "
}
}
let i = i + 1
}
out
}
fn js_cg_html_element_str(elem: Map<String, Any>, acc_var: String) -> String {
let tag: String = elem["tag"]
let attrs: [Map<String, Any>] = elem["attrs"]
let children: [Map<String, Any>] = elem["children"]
let self_closing: Bool = elem["self_closing"]
let out = acc_var + " += " + js_str_lit("<" + tag) + "; "
let out = out + js_cg_html_attrs_str(attrs, acc_var)
if self_closing {
let out = out + acc_var + " += \"/>\"" + "; "
} else {
let out = out + acc_var + " += \">\"; "
let out = out + js_cg_html_parts(children, acc_var)
let out = out + acc_var + " += " + js_str_lit("</" + tag + ">") + "; "
}
out
}
fn js_cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
let list_expr = node["list"]
let item_name: String = node["item"]
let body_children: [Map<String, Any>] = node["body"]
let id: String = js_next_html_id()
let list_var: String = "_html_list_" + id
let len_var: String = "_html_len_" + id
let idx_var: String = "_html_i_" + id
let list_c: String = js_cg_expr(list_expr)
let inner_c: String = js_cg_html_parts(body_children, acc_var)
"{ const " + list_var + " = " + list_c + "; const " + len_var + " = el_list_len(" + list_var + "); for (let " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { const " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
}
fn js_cg_html_template(expr: Map<String, Any>) -> String {
let root = expr["root"]
let id: String = js_next_html_id()
let acc: String = "_html_" + id
let doctype_flag: Bool = root["doctype"]
let doctype_prefix: String = ""
if doctype_flag {
let doctype_prefix = acc + " += \"<!doctype html>\"; "
}
let body: String = js_cg_html_element_str(root, acc)
"(() => { let " + acc + " = \"\"; " + doctype_prefix + body + "return " + acc + "; })()"
}
// Expression codegen
//
// js_cg_expr returns a JS expression string (not a statement).
@@ -377,20 +560,38 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
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)
@@ -398,22 +599,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 + ")"
}
@@ -453,6 +671,12 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
}
if kind == "Try" {
// Postfix `?` nil-propagation guard.
// When used as `expr?.field` the Field handler above intercepts and
// emits `(expr)?.["field"]`. Here, a bare `expr?` (not followed by
// field/index access) passes through to the inner expression unchanged
// (it acts as an identity but marks the value as "nil-propagating" for
// its caller). This matches the C backend's current behavior.
let inner = expr["inner"]
return js_cg_expr(inner)
}
@@ -470,6 +694,17 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
return js_cg_match(expr)
}
// Lambda (anonymous function literal): fn(params) -> RetType { body }
// Emitted as a JS arrow function expression: (params) => { body }.
// Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... })
if kind == "Lambda" {
return js_cg_lambda(expr)
}
if kind == "HtmlTemplate" {
return js_cg_html_template(expr)
}
"null"
}
@@ -528,8 +763,16 @@ fn js_cg_match(expr: Map<String, Any>) -> String {
if str_eq(v, "true") { let bv = "true" }
let parts = native_list_append(parts, "if (" + subj_var + " === " + bv + ") return (" + body_c + "); ")
} else {
// unknown pattern wildcard
let parts = native_list_append(parts, "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 + "); ")
}
}
}
}
@@ -541,6 +784,65 @@ fn js_cg_match(expr: Map<String, Any>) -> String {
str_join(parts, "")
}
// Lambda codegen
//
// Anonymous function literals: fn(params) -> RetType { body }
//
// Strategy: emit the lambda as a hoisted JS function declaration with a
// generated name (__lambda_N), then return the name as the expression value.
// This works because JS function declarations are hoisted within their scope,
// so the generated name is valid at any use site within the same function or
// module. The emitted code looks like:
//
// function __lambda_1(event) { dom_hide(spinner); }
// ...
// dom_listen(btn, "click", __lambda_1);
//
// This approach is clean, debuggable, and avoids any need for a string-buffer
// mode in the codegen.
fn js_next_lambda_id() -> String {
let csv: String = state_get("__js_lambda_counter")
let n = 0
if !str_eq(csv, "") {
let n = str_to_int(csv)
}
let n = n + 1
state_set("__js_lambda_counter", native_int_to_str(n))
native_int_to_str(n)
}
fn js_cg_lambda(expr: Map<String, Any>) -> String {
let params = expr["params"]
let body = expr["body"]
let ret_type: String = expr["ret_type"]
let id: String = js_next_lambda_id()
let lambda_name: String = "__lambda_" + id
let params_str: String = js_params_str(params)
// Emit the function definition immediately into the output stream.
// It will appear before the statement containing this expression.
js_emit_line("function " + lambda_name + "(" + params_str + ") {")
let decl = native_list_empty()
let np: Int = native_list_len(params)
let pi = 0
while pi < np {
let param = native_list_get(params, pi)
let pname: String = param["name"]
let decl = native_list_append(decl, pname)
let pi = pi + 1
}
let body_xformed = body
if !str_eq(ret_type, "Void") {
let body_xformed = js_transform_implicit_return(body)
}
js_build_int_names_for_params(params)
js_cg_stmts(body_xformed, " ", decl)
js_emit_line("}")
js_emit_blank()
// Return the function name as the expression value.
lambda_name
}
// Variable scope tracking
//
// El allows `let x = ...` to redeclare in the same scope. JS would throw
@@ -646,6 +948,31 @@ fn js_cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [St
if kind == "TypeDef" { return declared }
if kind == "EnumDef" { return declared }
if kind == "Import" { return declared }
// TestDef: skip in normal mode; handled by js_codegen_test in test mode.
if kind == "TestDef" { return declared }
// Assert: no-op in normal mode; handled by js_cg_stmt_assert in test mode.
if kind == "Assert" { return declared }
if kind == "TryCatch" {
let try_body = stmt["try_body"]
let catch_name: String = stmt["catch_name"]
let catch_body = stmt["catch_body"]
js_emit_line(indent + "try {")
js_cg_stmts(try_body, indent + " ", native_list_clone(declared))
js_emit_line(indent + "} catch (" + catch_name + ") {")
js_cg_stmts(catch_body, indent + " ", native_list_clone(declared))
js_emit_line(indent + "}")
return declared
}
// ExternFn: the function exists in the JS environment (loaded via <script>
// tag or the module context). Emit a comment so the generated file is
// self-documenting, but no JS function body the implementation is external.
if kind == "ExternFn" {
let ename: String = stmt["name"]
js_emit_line(indent + "// extern fn " + ename + " — provided by the JS environment")
return declared
}
if kind == "CgiBlock" {
// CGI blocks compile to a no-op + warning comment in JS target.
// The runtime cgi identity is server-side; UI code is not a CGI
@@ -785,16 +1112,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()
@@ -830,50 +1171,297 @@ fn js_is_top_level_decl(stmt: Map<String, Any>) -> Bool {
if kind == "Import" { return true }
if kind == "CgiBlock" { return true }
if kind == "ServiceBlock" { return true }
if kind == "ExternFn" { return true }
if kind == "TestDef" { return true }
false
}
// Test mode codegen (JS)
//
// reporter = "text" human-readable output to stderr (console.error)
// reporter = "json" newline-delimited JSON to stdout (process.stdout.write)
//
// The test function returns bool: true = pass, false = fail.
fn js_cg_stmt_assert_text(stmt: Map<String, Any>, test_name: String) -> Void {
let expr_node = stmt["expr"]
let msg: String = stmt["msg"]
let expr_c: String = js_cg_expr(expr_node)
let disp_msg = "assert failed"
if !str_eq(msg, "") { let disp_msg = msg }
js_emit_line(" if (!(" + expr_c + ")) {")
js_emit_line(" process.stderr.write(\" FAIL " + js_escape(test_name) + "" + js_escape(disp_msg) + "\\n\");")
js_emit_line(" return false;")
js_emit_line(" }")
}
fn js_cg_stmt_assert_json(stmt: Map<String, Any>, test_name: String, file_name: String, test_line: Int) -> Void {
let expr_node = stmt["expr"]
let msg: String = stmt["msg"]
let assert_line: Int = stmt["line"]
let expr_c: String = js_cg_expr(expr_node)
let disp_msg = "assert failed"
if !str_eq(msg, "") { let disp_msg = msg }
js_emit_line(" if (!(" + expr_c + ")) {")
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_fail\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + ",assert_line:" + native_int_to_str(assert_line) + ",message:" + js_str_lit(disp_msg) + "}) + \"\\n\");")
js_emit_line(" return false;")
js_emit_line(" }")
}
// js_cg_stmts_in_test: emit test body, routing Assert to the right handler.
fn js_cg_stmts_in_test(stmts: [Map<String, Any>], indent: String, declared: [String], test_name: String, reporter: String, file_name: String, test_line: Int) -> [String] {
let n: Int = native_list_len(stmts)
let i = 0
let decl = declared
while i < n {
let stmt = native_list_get(stmts, i)
let sk: String = stmt["stmt"]
if str_eq(sk, "Assert") {
if str_eq(reporter, "json") {
js_cg_stmt_assert_json(stmt, test_name, file_name, test_line)
} else {
js_cg_stmt_assert_text(stmt, test_name)
}
} else {
let decl = js_cg_stmt(stmt, indent, decl)
}
let i = i + 1
}
decl
}
// js_cg_test_fn: emit a single async test function.
fn js_cg_test_fn(test_def: Map<String, Any>, idx: Int, reporter: String, file_name: String) -> String {
let fn_name: String = "el_test_" + native_int_to_str(idx)
let test_name: String = test_def["name"]
let test_line: Int = test_def["line"]
let body = test_def["body"]
js_emit_line("async function " + fn_name + "() {")
js_cg_stmts_in_test(body, " ", native_list_empty(), test_name, reporter, file_name, test_line)
js_emit_line(" return true;")
js_emit_line("}")
js_emit_blank()
fn_name
}
// js_codegen_test: emit the test runner (replaces main() when --test active).
// reporter: "text" or "json"
// file_name: basename of the source file (used in JSON output)
fn js_codegen_test(stmts: [Map<String, Any>], reporter: String, file_name: String) -> Void {
// Collect TestDef nodes in order.
let n: Int = native_list_len(stmts)
let test_defs: [Map<String, Any>] = native_list_empty()
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
let sk: String = stmt["stmt"]
if str_eq(sk, "TestDef") {
let test_defs = native_list_append(test_defs, stmt)
}
let i = i + 1
}
let n_tests: Int = native_list_len(test_defs)
// Emit non-test function definitions (skip fn main and TestDef nodes).
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
if js_is_fndef(stmt) {
let fn_name: String = stmt["name"]
if !str_eq(fn_name, "main") {
js_cg_fn(stmt)
}
}
let i = i + 1
}
// Emit each test function.
let ti = 0
while ti < n_tests {
let test_def = native_list_get(test_defs, ti)
js_cg_test_fn(test_def, ti, reporter, file_name)
let ti = ti + 1
}
// Emit the test runner IIFE.
let test_word = "tests"
if n_tests == 1 { let test_word = "test" }
js_emit_line("(async () => {")
js_emit_line(" let pass = 0; let fail = 0;")
if str_eq(reporter, "json") {
// JSON reporter: suite_start to stdout
js_emit_line(" process.stdout.write(JSON.stringify({type:\"suite_start\",file:" + js_str_lit(file_name) + ",total:" + native_int_to_str(n_tests) + "}) + \"\\n\");")
let ti = 0
while ti < n_tests {
let test_def = native_list_get(test_defs, ti)
let test_name: String = test_def["name"]
let test_line: Int = test_def["line"]
let fn_name: String = "el_test_" + native_int_to_str(ti)
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_start\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + "}) + \"\\n\");")
js_emit_line(" if (await " + fn_name + "()) {")
js_emit_line(" pass++;")
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_pass\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + ",duration_ms:0}) + \"\\n\");")
js_emit_line(" } else { fail++; }")
let ti = ti + 1
}
js_emit_line(" process.stdout.write(JSON.stringify({type:\"suite_end\",passed:pass,failed:fail}) + \"\\n\");")
} else {
// Text reporter: human-readable to stderr
js_emit_line(" process.stderr.write(\"==> running " + native_int_to_str(n_tests) + " " + test_word + "\\n\\n\");")
let ti = 0
while ti < n_tests {
let test_def = native_list_get(test_defs, ti)
let test_name: String = test_def["name"]
let fn_name: String = "el_test_" + native_int_to_str(ti)
js_emit_line(" process.stderr.write(\" RUN " + js_escape(test_name) + "\\n\");")
js_emit_line(" if (await " + fn_name + "()) { pass++; process.stderr.write(\" PASS " + js_escape(test_name) + "\\n\"); }")
js_emit_line(" else { fail++; }")
let ti = ti + 1
}
js_emit_line(" process.stderr.write(\"\\n\" + pass + \" passed, \" + fail + \" failed\\n\");")
}
js_emit_line(" process.exit(fail > 0 ? 1 : 0);")
js_emit_line("})();")
}
// Entry point
fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
codegen_js_inner(stmts, source, false, "", false, "text", "")
}
// codegen_js_test: emit a JS test binary.
// reporter: "text" or "json"
// file_name: basename of the source file (used in JSON output)
fn codegen_js_test(stmts: [Map<String, Any>], source: String, reporter: String, file_name: String) -> String {
codegen_js_inner(stmts, source, false, "", true, reporter, file_name)
}
fn codegen_js_bundle(stmts: [Map<String, Any>], source: String, runtime_content: String) -> String {
codegen_js_inner(stmts, source, true, runtime_content, false, "text", "")
}
fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool, runtime_content: String, test_mode: Bool, reporter: String, file_name: String) -> String {
// Reset per-compile state.
state_set("__js_int_names", "")
state_set("__js_match_counter", "")
state_set("__js_async_fns", "")
state_set("__js_lambda_counter", "")
// Preamble: 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
}
// Test mode: emit test functions and runner, skip normal program logic.
if test_mode {
js_codegen_test(stmts, reporter, file_name)
return ""
}
// Function definitions
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
if js_is_fndef(stmt) {
@@ -921,6 +1509,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")
}
+402 -2
View File
@@ -175,6 +175,167 @@ fn duration_unit_nanos(unit: String) -> String {
"1LL"
}
// HTML template codegen
//
// cg_html_template(expr) emits a C statement-expression `({ ... })` that
// builds the HTML string by chaining el_str_concat calls.
//
// Interpolated values are passed through html_escape(); the raw() form
// bypasses escaping. {#each} blocks compile to C for-loops that index into
// the list with el_list_get / el_list_len.
//
// A per-template accumulator variable `_html_N` holds the growing string.
// A global counter stored in state keeps names unique.
fn next_html_id() -> String {
let csv: String = state_get("__html_counter")
let n = 0
if !str_eq(csv, "") {
let n = str_to_int(csv)
}
let n = n + 1
state_set("__html_counter", native_int_to_str(n))
native_int_to_str(n)
}
// Emit children nodes into a flat list of C fragment strings (parts).
// Each part is either a static string fragment (already C-literal form) or
// a dynamic expression that produces an el_val_t string.
// We build them all into parts, then the caller wraps with concat chain.
fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
let n: Int = native_list_len(children)
let i = 0
let out = ""
while i < n {
let child: Map<String, Any> = native_list_get(children, i)
let html_kind: String = child["html"]
if str_eq(html_kind, "Text") {
let text: String = child["text"]
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(text) + ")); "
}
if str_eq(html_kind, "Doctype") {
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<!doctype html>\")); "
}
if str_eq(html_kind, "Interp") {
let val_node = child["value"]
let val_c: String = cg_expr(val_node)
let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); "
}
if str_eq(html_kind, "Raw") {
let val_node = child["value"]
let val_c: String = cg_expr(val_node)
let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_raw(" + val_c + ")); "
}
if str_eq(html_kind, "Element") {
let elem_c: String = cg_html_element_str(child, acc_var)
let out = out + elem_c
}
if str_eq(html_kind, "Each") {
let each_c: String = cg_html_each(child, acc_var)
let out = out + each_c
}
let i = i + 1
}
out
}
// Generate open-tag attribute fragments inline.
// Parser stores attrs with "kind": "static" | "dynamic" | "bool".
// Static: "value" is the raw string value (not an expr node).
// Dynamic: "value" is an expr node.
// Bool: no "value" field.
fn cg_html_attrs_str(attrs: [Map<String, Any>], acc_var: String) -> String {
let n: Int = native_list_len(attrs)
let i = 0
let out = ""
// Closing-quote snippet: EL_STR("\"") in C text.
let close_q: String = "EL_STR(" + c_str_lit("\"") + ")"
while i < n {
let attr: Map<String, Any> = native_list_get(attrs, i)
let attr_name: String = attr["name"]
let kind: String = attr["kind"]
// Build: EL_STR(" name=\"")
let open_val: String = " " + attr_name + "=\""
let open_attr: String = "EL_STR(" + c_str_lit(open_val) + ")"
if str_eq(kind, "static") {
// Static attribute: value is a raw string.
let sv: String = attr["value"]
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); "
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(sv) + ")); "
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); "
} else {
if str_eq(kind, "dynamic") {
// Dynamic attribute: value is an expr node html_escape it.
let val_node = attr["value"]
let val_c: String = cg_expr(val_node)
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); "
let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); "
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); "
} else {
// Boolean attribute (no value): emit " name"
let bool_attr: String = "EL_STR(" + c_str_lit(" " + attr_name) + ")"
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + bool_attr + "); "
}
}
let i = i + 1
}
out
}
// Generate code for a single element, appending into acc_var.
fn cg_html_element_str(elem: Map<String, Any>, acc_var: String) -> String {
let tag: String = elem["tag"]
let attrs: [Map<String, Any>] = elem["attrs"]
let children: [Map<String, Any>] = elem["children"]
let self_closing: Bool = elem["self_closing"]
// Open tag: <tagname
let out = acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<" + tag + "\")); "
let out = out + cg_html_attrs_str(attrs, acc_var)
if self_closing {
// Self-closing void element: />
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"/>\")); "
} else {
// Close open tag: >
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\">\")); "
let out = out + cg_html_parts(children, acc_var)
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"</" + tag + ">\")); "
}
out
}
// Generate code for {#each list as item} ... {/each}.
fn cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
let list_expr = node["list"]
let item_name: String = node["item"]
let body_children: [Map<String, Any>] = node["body"]
let id: String = next_html_id()
let list_var: String = "_html_list_" + id
let len_var: String = "_html_len_" + id
let idx_var: String = "_html_i_" + id
let list_c: String = cg_expr(list_expr)
let inner_c: String = cg_html_parts(body_children, acc_var)
// Emit: { el_val_t _list = expr; int _len = el_list_len(_list);
// for (int _i = 0; _i < _len; _i++) {
// el_val_t item = el_list_get(_list, _i); inner_c } }
"{ el_val_t " + list_var + " = (" + list_c + "); el_val_t " + len_var + " = el_list_len(" + list_var + "); for (el_val_t " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { el_val_t " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
}
// Top-level HTML template codegen returns a C statement-expression string.
fn cg_html_template(expr: Map<String, Any>) -> String {
let root = expr["root"]
let id: String = next_html_id()
let acc: String = "_html_" + id
// If the root element has doctype:true the parser tagged it from <!doctype html>
let doctype_flag: Bool = root["doctype"]
let doctype_prefix: String = ""
if doctype_flag {
let doctype_prefix = acc + " = el_str_concat(" + acc + ", EL_STR(\"<!doctype html>\")); "
}
let body: String = cg_html_element_str(root, acc)
"({ el_val_t " + acc + " = EL_STR(\"\"); " + doctype_prefix + body + acc + "; })"
}
fn cg_expr(expr: Map<String, Any>) -> String {
let kind: String = expr["expr"]
@@ -787,6 +948,10 @@ fn cg_expr(expr: Map<String, Any>) -> String {
return cg_match(expr)
}
if kind == "HtmlTemplate" {
return cg_html_template(expr)
}
"EL_NULL"
}
@@ -848,8 +1013,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 + "; } ")
}
}
}
}
@@ -1136,6 +1308,22 @@ 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 }
if kind == "ServiceBlock" { return declared }
// TestDef: skip in normal (non-test) mode.
// In test mode the body is emitted by cg_test_fn, not here.
if kind == "TestDef" { return declared }
// Assert: no-op in normal mode. In test mode, cg_stmt_assert is used
// directly when emitting the per-test function body.
if kind == "Assert" { return declared }
// TryCatch: browser-only control flow. In the C target, emit a comment
// noting that the try body runs unconditionally; error handling is a no-op.
// Programs that rely on catching JS exceptions should compile with --target=js.
if kind == "TryCatch" {
let try_body = stmt["try_body"]
emit_line(indent + "/* try (C target: exception handling not supported) */")
cg_stmts(try_body, indent, native_list_clone(declared))
return declared
}
declared
}
@@ -2049,6 +2237,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 }
@@ -2368,6 +2566,7 @@ fn is_top_level_decl(stmt: Map<String, Any>) -> Bool {
if kind == "Import" { return true }
if kind == "CgiBlock" { return true }
if kind == "ExternFn" { return true }
if kind == "TestDef" { return true }
false
}
@@ -2512,9 +2711,195 @@ fn vbd_has_restricted_call(stmts: [Map<String, Any>]) -> Bool {
false
}
// -- Test mode codegen ----------------------------------------------------------
//
// reporter = "text" human-readable output to stderr
// reporter = "json" newline-delimited JSON to stdout
//
// Each test function signature: static int el_test_N(void)
// Returns 0 = pass, non-zero = fail.
//
// Text assert: if (!expr) { fprintf(stderr, " FAIL <name> — <msg>\n"); return 1; }
// JSON assert: stores the assert_line for the runner's suite_end emission.
// Emits {"type":"test_fail",...} to stdout then returns 1.
fn cg_stmt_assert_text(stmt: Map<String, Any>, test_name: String) -> Void {
let expr_node = stmt["expr"]
let msg: String = stmt["msg"]
let expr_c: String = cg_expr(expr_node)
let expr_c = strip_outer_parens(expr_c)
let disp_msg = "assert failed"
if !str_eq(msg, "") { let disp_msg = msg }
emit_line(" if (!(" + expr_c + ")) {")
emit_line(" fprintf(stderr, \" FAIL " + c_escape(test_name) + " \\xe2\\x80\\x94 " + c_escape(disp_msg) + "\\n\");")
emit_line(" return 1;")
emit_line(" }")
}
fn cg_stmt_assert_json(stmt: Map<String, Any>, test_name: String, file_name: String, test_line: Int) -> Void {
let expr_node = stmt["expr"]
let msg: String = stmt["msg"]
let assert_line: Int = stmt["line"]
let expr_c: String = cg_expr(expr_node)
let expr_c = strip_outer_parens(expr_c)
let disp_msg = "assert failed"
if !str_eq(msg, "") { let disp_msg = msg }
// Embed all compile-time-known strings as C string literals (no %s runtime formatting).
let json_line: String = "{\"type\":\"test_fail\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + ",\"assert_line\":" + native_int_to_str(assert_line) + ",\"message\":\"" + c_escape(disp_msg) + "\"}"
emit_line(" if (!(" + expr_c + ")) {")
emit_line(" puts(" + c_str_lit(json_line) + ");")
emit_line(" return 1;")
emit_line(" }")
}
// cg_stmts_in_test: emit test body statements, routing Assert to the
// appropriate handler based on reporter mode.
fn cg_stmts_in_test(stmts: [Map<String, Any>], indent: String, declared: [String], test_name: String, reporter: String, file_name: String, test_line: Int) -> [String] {
let n: Int = native_list_len(stmts)
let i = 0
let decl = declared
while i < n {
let stmt = native_list_get(stmts, i)
let sk: String = stmt["stmt"]
if str_eq(sk, "Assert") {
if str_eq(reporter, "json") {
cg_stmt_assert_json(stmt, test_name, file_name, test_line)
} else {
cg_stmt_assert_text(stmt, test_name)
}
} else {
let decl = cg_stmt(stmt, indent, decl)
}
let i = i + 1
}
decl
}
// cg_test_fn: emit a single test function.
fn cg_test_fn(test_def: Map<String, Any>, idx: Int, reporter: String, file_name: String) -> String {
let fn_name: String = "el_test_" + native_int_to_str(idx)
let test_name: String = test_def["name"]
let test_line: Int = test_def["line"]
let body = test_def["body"]
emit_line("static int " + fn_name + "(void) {")
cg_stmts_in_test(body, " ", native_list_empty(), test_name, reporter, file_name, test_line)
emit_line(" return 0;")
emit_line("}")
emit_blank()
fn_name
}
// codegen_test: emit a complete test binary.
// reporter: "text" (stderr human-readable) or "json" (stdout ndjson)
// file_name: basename of the source file (used in JSON output)
fn codegen_test(stmts: [Map<String, Any>], reporter: String, file_name: String) -> Void {
// Collect all TestDef nodes in order.
let n: Int = native_list_len(stmts)
let test_defs: [Map<String, Any>] = native_list_empty()
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
let sk: String = stmt["stmt"]
if str_eq(sk, "TestDef") {
let test_defs = native_list_append(test_defs, stmt)
}
let i = i + 1
}
let n_tests: Int = native_list_len(test_defs)
// Emit forward declarations for test functions.
let ti = 0
while ti < n_tests {
let fn_name: String = "el_test_" + native_int_to_str(ti)
emit_line("static int " + fn_name + "(void);")
let ti = ti + 1
}
emit_blank()
// Emit all non-test, non-main function definitions.
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
if is_fndef(stmt) {
let fn_name: String = stmt["name"]
if !str_eq(fn_name, "main") {
cg_fn(stmt)
}
}
let i = i + 1
}
// Emit each test function.
let ti = 0
while ti < n_tests {
let test_def = native_list_get(test_defs, ti)
cg_test_fn(test_def, ti, reporter, file_name)
let ti = ti + 1
}
// Emit the test runner main().
let test_word = "tests"
if n_tests == 1 { let test_word = "test" }
emit_line("int main(void) {")
emit_line(" int pass = 0; int fail = 0;")
if str_eq(reporter, "json") {
// JSON reporter: all strings are compile-time constants; use puts.
// Only suite_end needs runtime pass/fail counts (printf).
let suite_start_json: String = "{\"type\":\"suite_start\",\"file\":\"" + c_escape(file_name) + "\",\"total\":" + native_int_to_str(n_tests) + "}"
emit_line(" puts(" + c_str_lit(suite_start_json) + ");")
let ti = 0
while ti < n_tests {
let test_def = native_list_get(test_defs, ti)
let test_name: String = test_def["name"]
let test_line: Int = test_def["line"]
let fn_name: String = "el_test_" + native_int_to_str(ti)
let start_json: String = "{\"type\":\"test_start\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + "}"
let pass_json: String = "{\"type\":\"test_pass\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + ",\"duration_ms\":0}"
emit_line(" puts(" + c_str_lit(start_json) + ");")
emit_line(" if (" + fn_name + "() == 0) {")
emit_line(" pass++;")
emit_line(" puts(" + c_str_lit(pass_json) + ");")
emit_line(" } else { fail++; }")
let ti = ti + 1
}
// suite_end needs runtime pass/fail counts
emit_line(" printf(\"{\\\"type\\\":\\\"suite_end\\\",\\\"passed\\\":%d,\\\"failed\\\":%d}\\n\", pass, fail);")
} else {
// Text reporter: human-readable to stderr
emit_line(" fprintf(stderr, \"==> running " + native_int_to_str(n_tests) + " " + test_word + "\\n\\n\");")
let ti = 0
while ti < n_tests {
let test_def = native_list_get(test_defs, ti)
let test_name: String = test_def["name"]
let fn_name: String = "el_test_" + native_int_to_str(ti)
emit_line(" fprintf(stderr, \" RUN " + c_escape(test_name) + "\\n\");")
emit_line(" if (" + fn_name + "() == 0) { pass++; fprintf(stderr, \" PASS " + c_escape(test_name) + "\\n\"); }")
emit_line(" else { fail++; }")
let ti = ti + 1
}
emit_line(" fprintf(stderr, \"\\n%d passed, %d failed\\n\", pass, fail);")
}
emit_line(" return fail > 0 ? 1 : 0;")
emit_line("}")
emit_blank()
}
// -- Entry point ----------------------------------------------------------------
fn codegen(stmts: [Map<String, Any>], source: String) -> String {
codegen_inner(stmts, source, false, "text", "")
}
// codegen_with_tests: emit a test binary.
// reporter: "text" or "json"
// file_name: basename of the source file (used in JSON output)
fn codegen_with_tests(stmts: [Map<String, Any>], source: String, reporter: String, file_name: String) -> String {
codegen_inner(stmts, source, true, reporter, file_name)
}
fn codegen_inner(stmts: [Map<String, Any>], source: String, test_mode: Bool, reporter: String, file_name: String) -> String {
// Detect cgi/service blocks: at most one declarative top-level block.
// The block determines the program's CAPABILITY KIND:
// "cgi" - full self-formation. Calls all primitives.
@@ -2572,9 +2957,15 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
// Clear temporal-type-violation accumulator from any prior compile.
state_set("__time_violations", "")
// In test mode, delegate to the test-specific path which emits test
// functions and a test runner main() instead of the normal program.
// Test mode still needs the standard preamble (#includes, forward
// decls) so we emit that before branching.
//
// Preamble
emit_line("#include <stdint.h>")
emit_line("#include <stdlib.h>")
emit_line("#include <stdio.h>")
emit_line("#include \"el_runtime.h\"")
// Cross-module forward declarations: for each imported module, emit
@@ -2671,6 +3062,15 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
emit_blank()
}
// Test mode: emit test functions and test runner instead of normal program.
if test_mode {
codegen_test(stmts, reporter, file_name)
emit_cap_violations()
emit_arity_violations()
emit_time_violations()
return ""
}
// Detect whether this compilation unit has an entry point.
// A unit is a library (no C main emitted) when there is no fn main()
// and no top-level executable statements. This supports separate
+336 -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,38 @@ fn compile_js(source: String) -> String {
codegen_js(stmts, source)
}
// compile_js_with_bundle JS target in bundle mode.
// Reads el_runtime.js from runtime_path and inlines it inside an IIFE.
fn compile_js_with_bundle(source: String, runtime_path: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
el_release(tokens)
let runtime_content: String = fs_read(runtime_path)
if str_eq(runtime_content, "") {
println("el-compiler: warning: --bundle: could not read runtime at " + runtime_path)
println("el-compiler: warning: bundle output will be incomplete")
}
codegen_js_bundle(stmts, source, runtime_content)
}
// compile_test full pipeline (C target, test mode): source -> C test runner.
// reporter: "text" or "json"; file_name: basename of the source file.
fn compile_test(source: String, reporter: String, file_name: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
el_release(tokens)
codegen_with_tests(stmts, source, reporter, file_name)
}
// compile_js_test full pipeline (JS target, test mode): source -> JS test runner.
// reporter: "text" or "json"; file_name: basename of the source file.
fn compile_js_test(source: String, reporter: String, file_name: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
el_release(tokens)
codegen_js_test(stmts, source, reporter, file_name)
}
// compile_dispatch pick a backend based on the requested target.
// tgt = "c" | "js"
// (The parameter is named `tgt` because `target` is a reserved keyword
@@ -48,6 +80,19 @@ fn compile_dispatch(tgt: String, source: String) -> String {
compile(source)
}
// compile_dispatch_test pick test-mode backend.
// reporter: "text" or "json"; file_name: basename of the source file.
fn compile_dispatch_test(tgt: String, source: String, reporter: String, file_name: String) -> String {
if str_eq(tgt, "js") { return compile_js_test(source, reporter, file_name) }
compile_test(source, reporter, file_name)
}
// compile_dispatch_bundle like compile_dispatch but bundle mode for JS.
fn compile_dispatch_bundle(tgt: String, source: String, runtime_path: String) -> String {
if str_eq(tgt, "js") { return compile_js_with_bundle(source, runtime_path) }
compile(source)
}
// Detect a `--target=<lang>` flag in argv and return the target.
// Returns "c" if none specified or unrecognized.
fn detect_target(argv: [String]) -> String {
@@ -91,6 +136,168 @@ fn detect_emit_header(argv: [String]) -> Bool {
return false
}
// Detect --bundle flag in argv.
fn detect_bundle(argv: [String]) -> Bool {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_eq(a, "--bundle") { return true }
let i = i + 1
}
return false
}
// Detect --minify flag in argv.
fn detect_minify(argv: [String]) -> Bool {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_eq(a, "--minify") { return true }
let i = i + 1
}
return false
}
// Detect --obfuscate flag in argv.
fn detect_obfuscate(argv: [String]) -> Bool {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_eq(a, "--obfuscate") { return true }
let i = i + 1
}
return false
}
// Detect --test flag in argv.
fn detect_test(argv: [String]) -> Bool {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_eq(a, "--test") { return true }
let i = i + 1
}
return false
}
// Detect --reporter=<value> flag in argv.
// Returns "json" if --reporter=json, otherwise "text" (default).
fn detect_reporter(argv: [String]) -> String {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_starts_with(a, "--reporter=") {
let v: String = str_slice(a, 11, str_len(a))
return v
}
let i = i + 1
}
return "text"
}
// basename_of extract the filename portion of a path (after last '/').
fn basename_of(path: String) -> String {
let n: Int = str_len(path)
let i: Int = n - 1
while i >= 0 {
let c: String = str_slice(path, i, i + 1)
if str_eq(c, "/") {
return str_slice(path, i + 1, n)
}
let i = i - 1
}
return path
}
// Build a unique temp file path: /tmp/elc-<pid>-<timestamp>.<suffix>
fn make_temp_path(suffix: String) -> String {
let pid: Int = getpid_now()
let ts: Int = time_now()
"/tmp/elc-" + native_int_to_str(pid) + "-" + native_int_to_str(ts) + "." + suffix
}
// Reserved globals that terser and javascript-obfuscator must not mangle.
// These are referenced from HTML onclick= attributes and other direct window usage.
fn js_reserved_names() -> String {
"neuronDemoToggle,neuronDemoSend,neuronDemoReset,signInWith,signInWithEmail,signUpWithEmail,sendMagicLink,signOut,resetPassword,sendResetEmail,updatePassword,showSignIn,showSignUp,hideReset,setSort,addFamilyMember,removeFamilyMember,copyForPlatform,entHeadcountChange,NEURON_CFG"
}
// Find a CLI tool by checking node_modules paths first, then falling back to npx.
// src_dir is the directory of the source file being compiled.
// Returns the command string to invoke the tool, or "" if not found.
fn find_node_tool(tool_name: String, src_dir: String) -> String {
// 1. Check ./node_modules/.bin/<tool> relative to source file
let cand1: String = src_dir + "/node_modules/.bin/" + tool_name
let check1: String = str_trim(exec_capture("test -x " + cand1 + " && echo yes 2>/dev/null"))
if str_eq(check1, "yes") { return cand1 }
// 2. Check ../node_modules/.bin/<tool> (monorepo layout)
let parent_dir: String = dirname_of(src_dir)
let cand2: String = parent_dir + "/node_modules/.bin/" + tool_name
let check2: String = str_trim(exec_capture("test -x " + cand2 + " && echo yes 2>/dev/null"))
if str_eq(check2, "yes") { return cand2 }
// 3. Fall back to npx if it is on PATH. npx will use the globally cached
// package or download on first use. Use --no to avoid auto-install if
// the package is not already cached; if that fails, try with --yes.
let npx_path: String = str_trim(exec_capture("which npx 2>/dev/null"))
if !str_eq(npx_path, "") { return "npx --yes " + tool_name }
return ""
}
// apply_minify run terser on js_path, write result to out_path.
// Returns true on success, false on failure.
fn apply_minify(js_path: String, out_path: String, src_dir: String) -> Bool {
let terser: String = find_node_tool("terser", src_dir)
if str_eq(terser, "") {
println("el-compiler: error: terser not found. Run 'npm install terser' in your project directory.")
return false
}
let names: String = js_reserved_names()
// Single-quote the mangle reserved list so the shell does not glob-expand
// the bracket expression. The compress options are safe without quoting.
let compress_opts: String = "passes=2,drop_console=false,drop_debugger=true"
let mangle_reserved: String = "'reserved=[" + names + "]'"
let cmd: String = terser + " " + js_path + " --compress " + compress_opts + " --mangle " + mangle_reserved + " --output " + out_path
let ret: Int = exec_command(cmd)
if ret == 0 { return true }
println("el-compiler: error: terser failed (exit " + native_int_to_str(ret) + ")")
return false
}
// apply_obfuscate run javascript-obfuscator on js_path, write result to out_path.
// Returns true on success, false on failure.
fn apply_obfuscate(js_path: String, out_path: String, src_dir: String) -> Bool {
let obfuscator: String = find_node_tool("javascript-obfuscator", src_dir)
if str_eq(obfuscator, "") {
println("el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory.")
return false
}
let names: String = js_reserved_names()
let cmd: String = obfuscator + " " + js_path + " --output " + out_path + " --compact true --simplify true --string-array true --string-array-encoding base64 --string-array-threshold 0.75 --identifier-names-generator hexadecimal --rename-globals false --self-defending false --reserved-names " + names
let ret: Int = exec_command(cmd)
if ret == 0 { return true }
println("el-compiler: error: javascript-obfuscator failed (exit " + native_int_to_str(ret) + ")")
return false
}
// Resolve the runtime path for --bundle mode.
// Looks for el_runtime.js next to the source file first;
// if not found there, looks next to the elc binary itself.
// Returns "" if not found anywhere (caller emits a warning).
fn resolve_runtime_path(src_path: String) -> String {
let src_dir: String = dirname_of(src_path)
let candidate: String = src_dir + "/el_runtime.js"
let existing: String = fs_read(candidate)
if !str_eq(existing, "") {
return candidate
}
return ""
}
// Reconstruct an El type annotation string from a parsed type node.
fn type_node_to_el(t: Map<String, Any>) -> String {
let k: String = t["kind"]
@@ -249,12 +456,85 @@ 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 --test <source.el> # emit C test runner to stdout
// elc --target=js <source.el> # emit JS (module) to stdout
// elc --target=js --test <source.el> # emit JS test runner to stdout
// elc --target=js --bundle <source.el> # emit self-contained JS (IIFE) to stdout
// elc --target=js --bundle --minify <source.el> # emit minified IIFE to stdout
// elc --target=js --bundle --obfuscate <source.el> # emit minified+obfuscated IIFE to stdout
// elc --target=c <source.el> <out.c> # write C to file
// elc --target=js <source.el> <out.js> # write JS to file
// elc --target=js --bundle <source.el> <out.js> # write bundled JS to file
// elc --target=js --bundle --minify <source.el> <out.min.js> # write minified JS to file
fn main() -> Void {
let argv: [String] = args()
// Use `tgt` not `target`: `target` is a reserved keyword in the lexer
@@ -262,12 +542,30 @@ fn main() -> Void {
// because the function-name position has no token-class restriction.
let tgt: String = detect_target(argv)
let do_emit_header: Bool = detect_emit_header(argv)
let do_bundle: Bool = detect_bundle(argv)
let do_minify: Bool = detect_minify(argv)
let do_obfuscate: Bool = detect_obfuscate(argv)
let do_test: Bool = detect_test(argv)
let reporter: String = detect_reporter(argv)
// --obfuscate implies --minify: obfuscating unminified code is pointless.
if do_obfuscate {
let do_minify = true
}
let positional: [String] = strip_flags(argv)
let argc: Int = native_list_len(positional)
if argc < 1 {
println("el-compiler: usage: elc [--target=c|js] [--emit-header] <source.el> [<output>]")
println("el-compiler: usage: elc [--target=c|js] [--test] [--reporter=text|json] [--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 +581,38 @@ fn main() -> Void {
}
let source: String = resolve_imports(src_path)
let out: String = compile_dispatch(tgt, source)
let file_name: String = basename_of(src_path)
// --test mode: emit a test runner binary instead of the normal program.
if do_test {
let out: String = compile_dispatch_test(tgt, source, reporter, file_name)
if argc >= 2 {
let out_path: String = native_list_get(positional, 1)
let ok: Bool = fs_write(out_path, out)
if ok { exit(0) }
println("el-compiler: failed to write output")
exit(1)
}
exit(0)
}
// When post-processing (--minify or --obfuscate) is requested, redirect
// stdout to a temp file so codegen output can be captured and piped through
// the external tools. After codegen, restore stdout before emitting the
// final result.
if do_minify {
run_with_postprocess(tgt, source, src_path, do_bundle, do_obfuscate, argc, positional)
exit(0)
}
// Standard path (no post-processing).
let out: String = ""
if do_bundle {
let runtime_path: String = resolve_runtime_path(src_path)
let out = compile_dispatch_bundle(tgt, source, runtime_path)
} else {
let out = compile_dispatch(tgt, source)
}
if argc >= 2 {
let out_path: String = native_list_get(positional, 1)
let ok: Bool = fs_write(out_path, out)
+20 -4
View File
@@ -98,7 +98,10 @@ fn lex_is_whitespace(ch: String) -> Bool {
}
fn make_tok(kind: String, value: String) -> Map<String, Any> {
{ "kind": kind, "value": value }
let ln_s: String = state_get("__lex_line")
let ln: Int = 1
if !str_eq(ln_s, "") { let ln = str_to_int(ln_s) }
{ "kind": kind, "value": value, "line": ln }
}
// Keyword lookup
@@ -147,6 +150,8 @@ fn keyword_kind(word: String) -> String {
if word == "accessor" { return "Accessor" }
if word == "vessel" { return "Vessel" }
if word == "extern" { return "Extern" }
if word == "try" { return "Try" }
if word == "catch" { return "Catch" }
""
}
@@ -465,12 +470,18 @@ fn lex(source: String) -> [Map<String, Any>] {
let total: Int = native_list_len(chars)
let tokens: [Map<String, Any>] = native_list_empty()
let i: Int = 0
let line_num: Int = 1
state_set("__lex_line", "1")
while i < total {
let ch: String = native_list_get(chars, i)
// Skip whitespace
// Skip whitespace; track newlines for line-number reporting
if lex_is_whitespace(ch) {
if ch == "\n" {
let line_num = line_num + 1
state_set("__lex_line", native_int_to_str(line_num))
}
let i = i + 1
} else {
// Line comments: //
@@ -711,8 +722,13 @@ fn lex(source: String) -> [Map<String, Any>] {
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
let i = i + 1
} else {
// unknown char skip
let i = i + 1
if ch == "#" {
let tokens = native_list_append(tokens, make_tok("Hash", "#"))
let i = i + 1
} else {
// unknown char skip
let i = i + 1
}
}
}
}
+579 -1
View File
@@ -27,6 +27,15 @@ fn tok_value(tokens: [Map<String, Any>], pos: Int) -> String {
t["value"]
}
// tok_line return the source line number of the token at pos (1-indexed).
// Returns 1 if the token has no "line" field or line is 0.
fn tok_line(tokens: [Map<String, Any>], pos: Int) -> Int {
let t = native_list_get(tokens, pos)
let ln: Int = t["line"]
if ln <= 0 { return 1 }
ln
}
fn expect(tokens: [Map<String, Any>], pos: Int, kind: String) -> Int {
let k = tok_kind(tokens, pos)
if k == kind {
@@ -142,6 +151,460 @@ fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
// Expression parsing
// HTML template parser
//
// HTML templates are written as unquoted HTML in expression position:
// return <div class="x"><h1>{title}</h1></div>
//
// The parser detects an HTML template when parse_primary sees Lt followed
// by a lowercase ident (a known or assumed HTML element name) or `!` (for
// <!doctype html>). It then recursively parses the HTML into an AST.
//
// AST nodes produced:
// { "expr": "HtmlTemplate", "root": child_node }
// { "html": "Element", "tag": "div", "attrs": [...], "children": [...], "self_closing": bool }
// { "html": "Text", "text": "..." }
// { "html": "Interp", "value": expr_node }
// { "html": "Each", "list": expr_node, "item": "name", "body": [...] }
// { "html": "Doctype" }
// { "html": "Raw", "value": expr_node }
fn is_html_tag_name(name: String) -> Bool {
if str_eq(name, "a") { return true }
if str_eq(name, "abbr") { return true }
if str_eq(name, "address") { return true }
if str_eq(name, "area") { return true }
if str_eq(name, "article") { return true }
if str_eq(name, "aside") { return true }
if str_eq(name, "audio") { return true }
if str_eq(name, "b") { return true }
if str_eq(name, "base") { return true }
if str_eq(name, "blockquote") { return true }
if str_eq(name, "body") { return true }
if str_eq(name, "br") { return true }
if str_eq(name, "button") { return true }
if str_eq(name, "canvas") { return true }
if str_eq(name, "caption") { return true }
if str_eq(name, "cite") { return true }
if str_eq(name, "code") { return true }
if str_eq(name, "col") { return true }
if str_eq(name, "colgroup") { return true }
if str_eq(name, "data") { return true }
if str_eq(name, "datalist") { return true }
if str_eq(name, "dd") { return true }
if str_eq(name, "del") { return true }
if str_eq(name, "details") { return true }
if str_eq(name, "dfn") { return true }
if str_eq(name, "dialog") { return true }
if str_eq(name, "div") { return true }
if str_eq(name, "dl") { return true }
if str_eq(name, "dt") { return true }
if str_eq(name, "em") { return true }
if str_eq(name, "embed") { return true }
if str_eq(name, "fieldset") { return true }
if str_eq(name, "figcaption") { return true }
if str_eq(name, "figure") { return true }
if str_eq(name, "footer") { return true }
if str_eq(name, "form") { return true }
if str_eq(name, "h1") { return true }
if str_eq(name, "h2") { return true }
if str_eq(name, "h3") { return true }
if str_eq(name, "h4") { return true }
if str_eq(name, "h5") { return true }
if str_eq(name, "h6") { return true }
if str_eq(name, "head") { return true }
if str_eq(name, "header") { return true }
if str_eq(name, "hr") { return true }
if str_eq(name, "html") { return true }
if str_eq(name, "i") { return true }
if str_eq(name, "iframe") { return true }
if str_eq(name, "img") { return true }
if str_eq(name, "input") { return true }
if str_eq(name, "ins") { return true }
if str_eq(name, "kbd") { return true }
if str_eq(name, "label") { return true }
if str_eq(name, "legend") { return true }
if str_eq(name, "li") { return true }
if str_eq(name, "link") { return true }
if str_eq(name, "main") { return true }
if str_eq(name, "map") { return true }
if str_eq(name, "mark") { return true }
if str_eq(name, "menu") { return true }
if str_eq(name, "meta") { return true }
if str_eq(name, "meter") { return true }
if str_eq(name, "nav") { return true }
if str_eq(name, "noscript") { return true }
if str_eq(name, "object") { return true }
if str_eq(name, "ol") { return true }
if str_eq(name, "optgroup") { return true }
if str_eq(name, "option") { return true }
if str_eq(name, "output") { return true }
if str_eq(name, "p") { return true }
if str_eq(name, "param") { return true }
if str_eq(name, "picture") { return true }
if str_eq(name, "pre") { return true }
if str_eq(name, "progress") { return true }
if str_eq(name, "q") { return true }
if str_eq(name, "rp") { return true }
if str_eq(name, "rt") { return true }
if str_eq(name, "ruby") { return true }
if str_eq(name, "s") { return true }
if str_eq(name, "samp") { return true }
if str_eq(name, "script") { return true }
if str_eq(name, "section") { return true }
if str_eq(name, "select") { return true }
if str_eq(name, "small") { return true }
if str_eq(name, "source") { return true }
if str_eq(name, "span") { return true }
if str_eq(name, "strong") { return true }
if str_eq(name, "style") { return true }
if str_eq(name, "sub") { return true }
if str_eq(name, "summary") { return true }
if str_eq(name, "sup") { return true }
if str_eq(name, "table") { return true }
if str_eq(name, "tbody") { return true }
if str_eq(name, "td") { return true }
if str_eq(name, "template") { return true }
if str_eq(name, "textarea") { return true }
if str_eq(name, "tfoot") { return true }
if str_eq(name, "th") { return true }
if str_eq(name, "thead") { return true }
if str_eq(name, "time") { return true }
if str_eq(name, "title") { return true }
if str_eq(name, "tr") { return true }
if str_eq(name, "track") { return true }
if str_eq(name, "u") { return true }
if str_eq(name, "ul") { return true }
if str_eq(name, "var") { return true }
if str_eq(name, "video") { return true }
if str_eq(name, "wbr") { return true }
false
}
fn is_void_element(name: String) -> Bool {
if str_eq(name, "area") { return true }
if str_eq(name, "base") { return true }
if str_eq(name, "br") { return true }
if str_eq(name, "col") { return true }
if str_eq(name, "embed") { return true }
if str_eq(name, "hr") { return true }
if str_eq(name, "img") { return true }
if str_eq(name, "input") { return true }
if str_eq(name, "link") { return true }
if str_eq(name, "meta") { return true }
if str_eq(name, "param") { return true }
if str_eq(name, "source") { return true }
if str_eq(name, "track") { return true }
if str_eq(name, "wbr") { return true }
false
}
// Collect tokens as text content until we hit Lt, LBrace, Eof, or a
// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p }
fn parse_html_text_tokens(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let parts: [String] = native_list_empty()
let p = pos
let running = true
while running {
let k = tok_kind(tokens, p)
if str_eq(k, "Eof") {
let running = false
} else {
if str_eq(k, "Lt") {
let running = false
} else {
if str_eq(k, "LBrace") {
let running = false
} else {
// Check for </: Lt already stops us, but Slash alone
// (after consuming whitespace) also stops text.
// Anything else is text content.
let v = tok_value(tokens, p)
let parts = native_list_append(parts, v)
let p = p + 1
}
}
}
}
{ "text": str_join(parts, " "), "pos": p }
}
// Parse an attribute list: (attrname | attrname="val" | attrname={expr})*
// Stops at Gt or Slash (for self-closing />).
fn parse_html_attrs(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let attrs: [Map<String, Any>] = native_list_empty()
let p = pos
let running = true
while running {
let k = tok_kind(tokens, p)
if str_eq(k, "Gt") {
let running = false
} else {
if str_eq(k, "Slash") {
let running = false
} else {
if str_eq(k, "Eof") {
let running = false
} else {
// Attribute name: could be Ident or keyword used as attr name
let attr_name = tok_value(tokens, p)
let p = p + 1
let k2 = tok_kind(tokens, p)
if str_eq(k2, "Eq") {
let p = p + 1
let k3 = tok_kind(tokens, p)
if str_eq(k3, "Str") {
// static: attr="value"
let attr_val = tok_value(tokens, p)
let p = p + 1
let attrs = native_list_append(attrs, { "name": attr_name, "kind": "static", "value": attr_val })
} else {
if str_eq(k3, "LBrace") {
// dynamic: attr={expr}
let r = parse_expr(tokens, p + 1)
let val_node = r["node"]
let p = r["pos"]
let p = expect(tokens, p, "RBrace")
let attrs = native_list_append(attrs, { "name": attr_name, "kind": "dynamic", "value": val_node })
} else {
// malformed, skip
}
}
} else {
// boolean attribute
let attrs = native_list_append(attrs, { "name": attr_name, "kind": "bool" })
}
}
}
}
}
{ "attrs": attrs, "pos": p }
}
// Parse the children of an HTML element until we see the closing tag </tag>
// or EOF. Returns { "children": [...], "pos": p_after_closing_tag }
fn parse_html_children(tokens: [Map<String, Any>], pos: Int, parent_tag: String) -> Map<String, Any> {
let children: [Map<String, Any>] = native_list_empty()
let p = pos
let running = true
while running {
let k = tok_kind(tokens, p)
if str_eq(k, "Eof") {
let running = false
} else {
if str_eq(k, "Lt") {
// Check for closing tag: </
let k2 = tok_kind(tokens, p + 1)
if str_eq(k2, "Slash") {
// </tagname> consume and stop
let p = p + 2
// skip tag name
let close_name = tok_value(tokens, p)
let p = p + 1
// consume >
let p = expect(tokens, p, "Gt")
let running = false
} else {
if str_eq(k2, "Not") {
// Possible <!doctype html>
let k3_v = tok_value(tokens, p + 2)
if str_eq(k3_v, "doctype") {
// consume <!doctype html>
let p = p + 2
// skip until >
let scanning = true
while scanning {
let ck = tok_kind(tokens, p)
if str_eq(ck, "Eof") { let scanning = false }
if str_eq(ck, "Gt") {
let p = p + 1
let scanning = false
} else {
let p = p + 1
}
}
let children = native_list_append(children, { "html": "Doctype" })
} else {
let p = p + 1
}
} else {
// nested element
let r = parse_html_element(tokens, p)
let child = r["node"]
let p = r["pos"]
let children = native_list_append(children, child)
}
}
} else {
if str_eq(k, "LBrace") {
// Interpolation: {expr} or {#each ...} or {/each}
let k2 = tok_kind(tokens, p + 1)
if str_eq(k2, "Hash") {
// {#each list as item}
let k3_v = tok_value(tokens, p + 2)
if str_eq(k3_v, "each") {
let p = p + 3
// parse list expr up to "as" keyword
let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1")
let r_list = parse_expr(tokens, p)
state_set("__no_block_expr", prev_no_block)
let list_expr = r_list["node"]
let p = r_list["pos"]
// expect "as"
let p = expect(tokens, p, "As")
// item variable name
let item_name = tok_value(tokens, p)
let p = p + 1
// consume closing }
let p = expect(tokens, p, "RBrace")
// parse body until {/each}
let r_body = parse_html_each_body(tokens, p)
let body_children = r_body["children"]
let p = r_body["pos"]
let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
let children = native_list_append(children, each_node)
} else {
let p = p + 1
}
} else {
if str_eq(k2, "Slash") {
// {/each} end of each block, stop
// skip {/each}
let p = p + 2
// skip "each"
let p = p + 1
// skip }
let p = expect(tokens, p, "RBrace")
let running = false
} else {
// regular {expr}
let r = parse_expr(tokens, p + 1)
let interp_val = r["node"]
let p = r["pos"]
let p = expect(tokens, p, "RBrace")
// Check if the expr is a call to raw()
let is_raw_call = false
let interp_kind: String = interp_val["expr"]
if str_eq(interp_kind, "Call") {
let fn_node = interp_val["func"]
let fn_kind: String = fn_node["expr"]
if str_eq(fn_kind, "Ident") {
let fn_name_v: String = fn_node["name"]
if str_eq(fn_name_v, "raw") {
let is_raw_call = true
}
}
}
if is_raw_call {
let raw_args = interp_val["args"]
let raw_inner = native_list_get(raw_args, 0)
let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
} else {
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
}
}
}
} else {
// Text tokens collect run of non-special tokens
let r_text = parse_html_text_tokens(tokens, p)
let text_str: String = r_text["text"]
let p = r_text["pos"]
let text_trimmed: String = str_trim(text_str)
if !str_eq(text_trimmed, "") {
let children = native_list_append(children, { "html": "Text", "text": text_trimmed })
}
}
}
}
}
{ "children": children, "pos": p }
}
// Parse body of {#each} until {/each}. Mirrors parse_html_children but
// stops at the {/each} sentinel rather than a closing element tag.
fn parse_html_each_body(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
parse_html_children(tokens, pos, "__each__")
}
// Parse a single HTML element: <tag attrs> children </tag>
// or self-closing: <tag attrs/>
// Pos points to the Lt token.
fn parse_html_element(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let p = pos
// consume <
let p = expect(tokens, p, "Lt")
// tag name
let tag_name = tok_value(tokens, p)
let p = p + 1
// parse attributes
let r_attrs = parse_html_attrs(tokens, p)
let attrs = r_attrs["attrs"]
let p = r_attrs["pos"]
// check for self-closing /> or void element
let k = tok_kind(tokens, p)
let self_closing = false
if str_eq(k, "Slash") {
// />
let p = p + 1
let p = expect(tokens, p, "Gt")
let self_closing = true
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
}
// consume >
let p = expect(tokens, p, "Gt")
// void elements have no children, no closing tag
if is_void_element(tag_name) {
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
}
// parse children
let r_children = parse_html_children(tokens, p, tag_name)
let children = r_children["children"]
let p = r_children["pos"]
make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": children, "self_closing": false }, p)
}
// Entry point for HTML template parsing.
// Pos points to Lt (or Lt Not for <!doctype>).
// May parse an optional <!doctype html> prefix followed by the root element.
fn parse_html_template(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let p = pos
// Check for <!doctype html>
let doctype = false
let k = tok_kind(tokens, p)
let k2 = tok_kind(tokens, p + 1)
if str_eq(k, "Lt") {
if str_eq(k2, "Not") {
let k3_v = tok_value(tokens, p + 2)
if str_eq(k3_v, "doctype") {
let doctype = true
// consume <!doctype html>
let p = p + 2
let scanning = true
while scanning {
let ck = tok_kind(tokens, p)
if str_eq(ck, "Eof") { let scanning = false }
if str_eq(ck, "Gt") {
let p = p + 1
let scanning = false
} else {
let p = p + 1
}
}
}
}
}
// Parse root element
let r = parse_html_element(tokens, p)
let root = r["node"]
let p = r["pos"]
let root_with_doctype = root
if doctype {
let root_with_doctype = { "html": root["html"], "tag": root["tag"], "attrs": root["attrs"], "children": root["children"], "self_closing": root["self_closing"], "doctype": true }
}
make_result({ "expr": "HtmlTemplate", "root": root_with_doctype }, p)
}
fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let k = tok_kind(tokens, pos)
let v = tok_value(tokens, pos)
@@ -166,6 +629,22 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
return make_result({ "expr": "Bool", "value": v }, pos + 1)
}
// HTML template literal: <tagname ...>...</tagname> or <!doctype html>...
// Detected in value position only; `<` in comparison position is already
// consumed by parse_binop before parse_primary is reached.
if k == "Lt" {
let k2 = tok_kind(tokens, pos + 1)
if str_eq(k2, "Not") {
return parse_html_template(tokens, pos)
}
if str_eq(k2, "Ident") {
let tag_candidate = tok_value(tokens, pos + 1)
if is_html_tag_name(tag_candidate) {
return parse_html_template(tokens, pos)
}
}
}
// Identifier
if k == "Ident" {
return make_result({ "expr": "Ident", "name": v }, pos + 1)
@@ -279,6 +758,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 +911,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" {
@@ -737,11 +1247,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
@@ -876,6 +1391,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
@@ -1014,6 +1563,35 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
}, p)
}
// assert statement: assert <expr> or assert <expr>, "msg"
if k == "Assert" {
let assert_line: Int = tok_line(tokens, pos)
let p = pos + 1
let r = parse_expr(tokens, p)
let expr_node = r["node"]
let p = r["pos"]
let msg = ""
let k2 = tok_kind(tokens, p)
if k2 == "Comma" {
let p = p + 1
let msg = tok_value(tokens, p)
let p = p + 1
}
return make_result({ "stmt": "Assert", "expr": expr_node, "msg": msg, "line": assert_line }, p)
}
// test block: test "name" { stmts }
if k == "Test" {
let test_line: Int = tok_line(tokens, pos)
let p = pos + 1
let name = tok_value(tokens, p)
let p = p + 1
let r2 = parse_block(tokens, p)
let body = r2["stmts"]
let p = r2["pos"]
return make_result({ "stmt": "TestDef", "name": name, "body": body, "line": test_line }, p)
}
// Bare reassignment: `name = expr`. Handled BEFORE the expression
// fallback so we don't drop the assign on the floor and emit three
// orphan expressions (the original silent-miscompile bug). El's `let`
+2489 -286
View File
File diff suppressed because it is too large Load Diff
+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)
}
+40
View File
@@ -0,0 +1,40 @@
// html-page.el Example of native HTML template syntax in El.
//
// El HTML templates let you write HTML directly in expression position.
// Interpolated values are automatically HTML-escaped.
// Use raw(expr) to bypass escaping when you know the content is safe.
//
// Compile and run:
// ./dist/platform/elc examples/html-page.el > /tmp/html-page.c
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
// -o /tmp/html-page /tmp/html-page.c el-compiler/runtime/el_runtime.c
// /tmp/html-page
fn render_item(item: String) -> String {
return <li class="item">{item}</li>
}
fn render_page(title: String, items: [String]) -> String {
return <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
<ul>
{#each items as item}
<li class="item">{item}</li>
{/each}
</ul>
<p>Built with El HTML templates</p>
</body>
</html>
}
fn main() -> Void {
let items: [String] = ["Lexer", "Parser", "Codegen", "Runtime"]
let page: String = render_page("El Compiler Stages", items)
println(page)
}
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.
---
+80
View File
@@ -0,0 +1,80 @@
// test_codegen_js.el - basic tests for JS codegen features.
//
// These tests verify that core El language features produce correct values
// when compiled and executed via the C backend. They serve as a
// regression baseline for the codegen pipeline.
test "arithmetic" {
let x: Int = 2 + 3
assert x == 5, "addition"
let y: Int = 10 - 4
assert y == 6, "subtraction"
let z: Int = 3 * 4
assert z == 12, "multiplication"
let w: Int = 15 / 3
assert w == 5, "division"
}
test "string-concat" {
let a: String = "hello"
let b: String = " world"
let c: String = a + b
assert c == "hello world", "string concatenation"
}
test "str-len" {
let n: Int = str_len("hello")
assert n == 5, "str_len hello"
let m: Int = str_len("")
assert m == 0, "str_len empty"
}
test "bool-logic" {
let t = true
let f = false
assert t, "true is truthy"
assert !f, "false negated is truthy"
assert t && !f, "true and not false"
assert t || f, "true or false"
}
test "list-operations" {
let lst: [Int] = native_list_empty()
let lst = native_list_append(lst, 10)
let lst = native_list_append(lst, 20)
let lst = native_list_append(lst, 30)
let n: Int = native_list_len(lst)
assert n == 3, "list length 3"
let v0: Int = native_list_get(lst, 0)
let v1: Int = native_list_get(lst, 1)
let v2: Int = native_list_get(lst, 2)
assert v0 == 10, "first element"
assert v1 == 20, "second element"
assert v2 == 30, "third element"
}
test "str-slice" {
let s: String = str_slice("hello world", 6, 11)
assert s == "world", "slice from 6 to 11"
}
test "str-contains" {
assert str_contains("hello world", "world"), "contains world"
assert !str_contains("hello world", "xyz"), "does not contain xyz"
}
test "int-to-str" {
let s: String = int_to_str(42)
assert s == "42", "int to string"
}
test "str-to-int" {
let n: Int = str_to_int("123")
assert n == 123, "string to int"
}
test "str-starts-ends" {
assert str_starts_with("hello world", "hello"), "starts with hello"
assert str_ends_with("hello world", "world"), "ends with world"
assert !str_starts_with("hello world", "world"), "does not start with world"
}
+82
View File
@@ -0,0 +1,82 @@
// test_env.el - native test suite for runtime/env.el
//
// Covers: env() for reading environment variables, args() returning a list,
// state_set/get/del/keys via the env module re-exports, uuid_new/uuid_v4
// format validation.
test "env-missing-returns-empty" {
let v: String = env("__EL_NO_SUCH_VAR_XYZ__")
assert v == "", "missing env var returns empty string"
}
test "env-path-is-set" {
// PATH is expected to be set in virtually any UNIX environment.
let v: String = env("PATH")
assert str_len(v) > 0, "PATH env var is non-empty"
}
test "env-home-is-set" {
// HOME is present on macOS/Linux test environments.
let v: String = env("HOME")
assert str_len(v) > 0, "HOME env var is non-empty"
}
test "args-returns-list" {
let a: [String] = args()
// Even with no arguments, the list should be non-null (at minimum the
// program name is argv[0]).
let n: Int = native_list_len(a)
assert n >= 0, "args returns a list (may be empty if runtime strips argv)"
}
test "uuid-new-format" {
let id: String = uuid_new()
// UUID v4: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 chars)
let n: Int = str_len(id)
assert n == 36, "uuid_new returns 36-character string"
// Check the dashes at correct positions
let d1: String = str_char_at(id, 8)
let d2: String = str_char_at(id, 13)
let d3: String = str_char_at(id, 18)
let d4: String = str_char_at(id, 23)
assert d1 == "-", "dash at position 8"
assert d2 == "-", "dash at position 13"
assert d3 == "-", "dash at position 18"
assert d4 == "-", "dash at position 23"
}
test "uuid-v4-format" {
let id: String = uuid_v4()
let n: Int = str_len(id)
assert n == 36, "uuid_v4 returns 36-character string"
// The version nibble must be '4'
let version_char: String = str_char_at(id, 14)
assert version_char == "4", "uuid_v4 version nibble is '4'"
}
test "uuid-uniqueness" {
let id1: String = uuid_new()
let id2: String = uuid_new()
assert !str_eq(id1, id2), "two uuid_new calls produce different UUIDs"
}
test "env-state-set-get-via-env-module" {
// runtime/env.el re-exports state_set / state_get.
state_set("env_test_key", "env_test_val")
let v: String = state_get("env_test_key")
assert v == "env_test_val", "state_set/get work via env module"
state_del("env_test_key")
let after: String = state_get("env_test_key")
assert after == "", "state_del removes key"
}
test "env-state-keys-json-via-env-module" {
state_set("esk_a", "1")
state_set("esk_b", "2")
let ks: String = state_keys()
assert str_starts_with(ks, "["), "state_keys returns JSON array"
assert str_contains(ks, "esk_a"), "esk_a in keys"
assert str_contains(ks, "esk_b"), "esk_b in keys"
state_del("esk_a")
state_del("esk_b")
}
+96
View File
@@ -0,0 +1,96 @@
// test_fs.el - native test suite for runtime/fs.el
//
// Covers: fs_write/read round-trip, fs_exists, fs_mkdir, fs_list,
// fs_list_json, and edge cases (empty file, overwrite, non-existent path).
test "fs-write-and-read" {
let path: String = "/tmp/el_test_fs_basic.txt"
let content: String = "hello from El"
let ok: Bool = fs_write(path, content)
assert ok, "fs_write returns true on success"
let got: String = fs_read(path)
assert got == content, "fs_read returns what was written"
}
test "fs-exists" {
let path: String = "/tmp/el_test_fs_exists.txt"
fs_write(path, "exists check")
let exists: Bool = fs_exists(path)
assert exists, "fs_exists returns true for created file"
let missing: Bool = fs_exists("/tmp/__el_no_such_file_xyz__.txt")
assert !missing, "fs_exists returns false for non-existent file"
}
test "fs-read-nonexistent" {
let got: String = fs_read("/tmp/__el_no_such_file_abc__.txt")
assert got == "", "reading nonexistent file returns empty string"
}
test "fs-write-overwrite" {
let path: String = "/tmp/el_test_fs_overwrite.txt"
fs_write(path, "original content")
fs_write(path, "new content")
let got: String = fs_read(path)
assert got == "new content", "second write overwrites first"
}
test "fs-write-empty-file" {
let path: String = "/tmp/el_test_fs_empty.txt"
let ok: Bool = fs_write(path, "")
assert ok, "writing empty file succeeds"
let got: String = fs_read(path)
assert got == "", "reading empty file returns empty string"
let exists: Bool = fs_exists(path)
assert exists, "empty file still exists"
}
test "fs-write-multiline" {
let path: String = "/tmp/el_test_fs_multiline.txt"
let content: String = "line one\nline two\nline three"
fs_write(path, content)
let got: String = fs_read(path)
assert got == content, "multiline content round-trips"
let lines: [String] = str_split_lines(got)
let n: Int = native_list_len(lines)
assert n == 3, "three lines after read"
}
test "fs-mkdir" {
let dir: String = "/tmp/el_test_fs_mkdir_dir"
let ok: Bool = fs_mkdir(dir)
assert ok, "fs_mkdir returns true"
let exists: Bool = fs_exists(dir)
assert exists, "created directory exists"
}
test "fs-list" {
let dir: String = "/tmp/el_test_fs_list_dir"
fs_mkdir(dir)
fs_write(dir + "/a.txt", "a")
fs_write(dir + "/b.txt", "b")
let files: [String] = fs_list(dir)
// Filter out empty strings from trailing newline
let n: Int = native_list_len(files)
// We expect at least 2 entries (a.txt and b.txt)
assert n >= 2, "fs_list returns at least 2 entries"
}
test "fs-list-json" {
let dir: String = "/tmp/el_test_fs_listjson_dir"
fs_mkdir(dir)
fs_write(dir + "/x.txt", "x")
fs_write(dir + "/y.txt", "y")
let result: String = fs_list_json(dir)
assert str_starts_with(result, "["), "fs_list_json returns JSON array"
assert str_ends_with(result, "]"), "fs_list_json JSON array is closed"
// Should contain at least one filename
assert str_len(result) > 2, "fs_list_json result is non-empty array"
}
test "fs-write-and-read-special-chars" {
let path: String = "/tmp/el_test_fs_special.txt"
let content: String = "tabs:\there\nnewlines: ok\n\"quoted\""
fs_write(path, content)
let got: String = fs_read(path)
assert got == content, "special chars in content round-trip"
}
+132
View File
@@ -0,0 +1,132 @@
// test_json.el - native test suite for runtime/json.el
//
// Covers: json_get (dot-path), typed extractors (int, bool, float),
// json_array_len/get, json_set, json_build_object, json_build_array,
// and json_escape_string.
test "json-get-string" {
let obj: String = "{\"name\":\"alice\",\"role\":\"admin\"}"
let name: String = json_get(obj, "name")
assert name == "alice", "get string field name"
let role: String = json_get(obj, "role")
assert role == "admin", "get string field role"
let missing: String = json_get(obj, "xyz")
assert missing == "", "missing key returns empty string"
}
test "json-get-int" {
let obj: String = "{\"count\":42,\"offset\":0}"
let count: Int = json_get_int(obj, "count")
assert count == 42, "get int field count"
let offset: Int = json_get_int(obj, "offset")
assert offset == 0, "get int field offset = 0"
}
test "json-get-bool" {
let obj: String = "{\"active\":true,\"deleted\":false}"
let active: Bool = json_get_bool(obj, "active")
assert active, "get bool true"
let deleted: Bool = json_get_bool(obj, "deleted")
assert !deleted, "get bool false"
}
test "json-dot-path" {
let obj: String = "{\"user\":{\"name\":\"bob\",\"age\":30}}"
let name: String = json_get(obj, "user.name")
assert name == "bob", "dot-path traversal user.name"
let age: String = json_get(obj, "user.age")
assert age == "30", "dot-path traversal user.age"
}
test "json-array-len" {
let arr: String = "[\"a\",\"b\",\"c\"]"
let n: Int = json_array_len(arr)
assert n == 3, "array length 3"
let empty_arr: String = "[]"
let m: Int = json_array_len(empty_arr)
assert m == 0, "empty array length 0"
}
test "json-array-get" {
let arr: String = "[\"foo\",\"bar\",\"baz\"]"
let first: String = json_array_get_string(arr, 0)
assert first == "foo", "first element"
let second: String = json_array_get_string(arr, 1)
assert second == "bar", "second element"
let third: String = json_array_get_string(arr, 2)
assert third == "baz", "third element"
}
test "json-get-raw" {
let obj: String = "{\"items\":[1,2,3],\"meta\":{\"page\":1}}"
let raw_arr: String = json_get_raw(obj, "items")
let arr_len: Int = json_array_len(raw_arr)
assert arr_len == 3, "raw array has 3 elements"
let raw_meta: String = json_get_raw(obj, "meta")
let page: String = json_get(raw_meta, "page")
assert page == "1", "page from nested raw object"
}
test "json-set" {
let obj: String = "{\"name\":\"alice\"}"
let updated: String = json_set(obj, "name", "\"bob\"")
let name: String = json_get(updated, "name")
assert name == "bob", "set replaces existing key"
}
test "json-build-object" {
let kvs: [String] = native_list_empty()
let kvs = native_list_append(kvs, "name")
let kvs = native_list_append(kvs, "alice")
let kvs = native_list_append(kvs, "role")
let kvs = native_list_append(kvs, "admin")
let obj: String = json_build_object(kvs)
let name: String = json_get(obj, "name")
let role: String = json_get(obj, "role")
assert name == "alice", "built object name field"
assert role == "admin", "built object role field"
}
test "json-build-array" {
let items: [String] = native_list_empty()
let items = native_list_append(items, "\"alpha\"")
let items = native_list_append(items, "\"beta\"")
let items = native_list_append(items, "\"gamma\"")
let arr: String = json_build_array(items)
let n: Int = json_array_len(arr)
assert n == 3, "built array has 3 elements"
let first: String = json_array_get_string(arr, 0)
assert first == "alpha", "first built element"
let third: String = json_array_get_string(arr, 2)
assert third == "gamma", "third built element"
}
test "json-escape-string" {
let escaped: String = json_escape_string("say \"hello\"")
assert str_contains(escaped, "\\\""), "double quotes are escaped"
let tab_esc: String = json_escape_string("a\tb")
assert str_contains(tab_esc, "\\t"), "tabs are escaped"
let plain: String = json_escape_string("no special chars")
assert plain == "no special chars", "plain string unchanged"
}
test "json-get-float" {
let obj: String = "{\"price\":9.99,\"tax\":0.0}"
let price: Float = json_get_float(obj, "price")
let diff: Float = price - 9.99
assert diff > -0.001, "price close to 9.99 low"
assert diff < 0.001, "price close to 9.99 high"
let tax: Float = json_get_float(obj, "tax")
assert tax == 0.0, "tax is zero"
}
test "json-nested-array-index" {
let obj: String = "{\"tags\":[\"go\",\"el\",\"rust\"]}"
let tags_raw: String = json_get_raw(obj, "tags")
let count: Int = json_array_len(tags_raw)
assert count == 3, "tags array has 3 elements"
let first_tag: String = json_array_get_string(tags_raw, 0)
assert first_tag == "go", "first tag is go"
let last_tag: String = json_array_get_string(tags_raw, 2)
assert last_tag == "rust", "last tag is rust"
}
+153
View File
@@ -0,0 +1,153 @@
// test_math.el - native test suite for runtime/math.el
//
// Covers: integer math (abs, max, min), float math (sqrt, log, sin, cos, pi),
// float conversions, format_float, and decimal_round.
test "el-abs" {
let pos: Int = el_abs(5)
assert pos == 5, "abs of positive"
let neg: Int = el_abs(-5)
assert neg == 5, "abs of negative"
let zero: Int = el_abs(0)
assert zero == 0, "abs of zero"
let large: Int = el_abs(-1000000)
assert large == 1000000, "abs of large negative"
}
test "el-max" {
let m: Int = el_max(3, 7)
assert m == 7, "max of 3 and 7"
let m2: Int = el_max(7, 3)
assert m2 == 7, "max is commutative"
let same: Int = el_max(5, 5)
assert same == 5, "max of equal values"
let neg: Int = el_max(-3, -7)
assert neg == -3, "max of two negatives"
}
test "el-min" {
let m: Int = el_min(3, 7)
assert m == 3, "min of 3 and 7"
let m2: Int = el_min(7, 3)
assert m2 == 3, "min is commutative"
let same: Int = el_min(5, 5)
assert same == 5, "min of equal values"
let neg: Int = el_min(-3, -7)
assert neg == -7, "min of two negatives"
}
test "math-pi" {
let pi: Float = math_pi()
// pi ~ 3.14159265358979
// Check it's between 3.141 and 3.142
let too_low: Float = pi - 3.141
let too_high: Float = 3.142 - pi
assert too_low > 0.0, "pi > 3.141"
assert too_high > 0.0, "pi < 3.142"
}
test "math-sqrt" {
let r4: Float = math_sqrt(4.0)
let diff4: Float = r4 - 2.0
assert diff4 == 0.0, "sqrt(4) == 2.0"
let r9: Float = math_sqrt(9.0)
let diff9: Float = r9 - 3.0
assert diff9 == 0.0, "sqrt(9) == 3.0"
let r1: Float = math_sqrt(1.0)
let diff1: Float = r1 - 1.0
assert diff1 == 0.0, "sqrt(1) == 1.0"
let r0: Float = math_sqrt(0.0)
assert r0 == 0.0, "sqrt(0) == 0.0"
}
test "math-sin-cos" {
// sin(0) == 0, cos(0) == 1
let s0: Float = math_sin(0.0)
assert s0 == 0.0, "sin(0) == 0.0"
let c0: Float = math_cos(0.0)
assert c0 == 1.0, "cos(0) == 1.0"
// sin(pi/2) ~ 1.0, cos(pi/2) ~ 0.0
let half_pi: Float = math_pi() / 2.0
let s_half: Float = math_sin(half_pi)
// Check within 0.000001 of 1.0
let diff_s: Float = s_half - 1.0
assert diff_s > -0.000001, "sin(pi/2) close to 1.0 low"
assert diff_s < 0.000001, "sin(pi/2) close to 1.0 high"
}
test "int-to-float-and-back" {
let f: Float = int_to_float(42)
let back: Int = float_to_int(f)
assert back == 42, "42 round-trips int->float->int"
let neg: Float = int_to_float(-7)
let neg_back: Int = float_to_int(neg)
assert neg_back == -7, "-7 round-trips"
let zero: Float = int_to_float(0)
let zero_back: Int = float_to_int(zero)
assert zero_back == 0, "0 round-trips"
}
test "float-to-int-truncates" {
let t1: Int = float_to_int(3.9)
assert t1 == 3, "3.9 truncates to 3"
let t2: Int = float_to_int(3.1)
assert t2 == 3, "3.1 truncates to 3"
let t3: Int = float_to_int(-3.7)
assert t3 == -3, "-3.7 truncates toward zero to -3"
}
test "str-to-float-and-back" {
let f: Float = str_to_float("3.14")
// Check it's between 3.13 and 3.15
let lo: Float = f - 3.13
let hi: Float = 3.15 - f
assert lo > 0.0, "3.14 parsed > 3.13"
assert hi > 0.0, "3.14 parsed < 3.15"
let zero: Float = str_to_float("0.0")
assert zero == 0.0, "parse 0.0"
}
test "format-float" {
let s0: String = format_float(3.14159, 2)
assert s0 == "3.14", "format to 2 decimals"
let s1: String = format_float(1.0, 0)
assert s1 == "1", "format to 0 decimals"
let s2: String = format_float(0.0, 3)
assert s2 == "0.000", "format zero to 3 decimals"
let s3: String = format_float(-2.5, 1)
assert s3 == "-2.5", "format negative to 1 decimal"
}
test "decimal-round" {
let r0: Float = decimal_round(2.5, 0)
assert r0 == 3.0, "round 2.5 to 0 places"
let r1: Float = decimal_round(2.45, 1)
assert r1 == 2.5, "round 2.45 to 1 place"
let neg: Float = decimal_round(-2.5, 0)
assert neg == -3.0, "round -2.5 to 0 places (half-away-from-zero)"
let exact: Float = decimal_round(1.0, 2)
assert exact == 1.0, "rounding exact value unchanged"
}
test "math-log" {
// log10(100) == 2
let l100: Float = math_log(100.0)
let diff: Float = l100 - 2.0
assert diff > -0.000001, "log10(100) close to 2 low"
assert diff < 0.000001, "log10(100) close to 2 high"
// log10(1) == 0
let l1: Float = math_log(1.0)
assert l1 == 0.0, "log10(1) == 0"
}
test "math-ln" {
// ln(1) == 0
let l1: Float = math_ln(1.0)
assert l1 == 0.0, "ln(1) == 0"
// ln(e) ~ 1.0 e ~ 2.71828
let e: Float = 2.718281828
let le: Float = math_ln(e)
let diff: Float = le - 1.0
assert diff > -0.000001, "ln(e) close to 1 low"
assert diff < 0.000001, "ln(e) close to 1 high"
}
+98
View File
@@ -0,0 +1,98 @@
// test_state.el - native test suite for runtime/state.el
//
// Covers: state_set/get/del, state_has, state_get_or, state_keys,
// and edge cases such as empty values, overwrite, and multiple keys.
test "state-set-get-basic" {
state_set("test_key", "hello")
let v: String = state_get("test_key")
assert v == "hello", "get returns set value"
}
test "state-get-missing" {
let v: String = state_get("__nonexistent_key_xyz__")
assert v == "", "missing key returns empty string"
}
test "state-overwrite" {
state_set("ow_key", "first")
state_set("ow_key", "second")
let v: String = state_get("ow_key")
assert v == "second", "second write overwrites first"
}
test "state-del" {
state_set("del_key", "to be deleted")
state_del("del_key")
let v: String = state_get("del_key")
assert v == "", "deleted key returns empty string"
}
test "state-del-nonexistent" {
// Should not panic or error on deleting a non-existent key.
state_del("__never_set_key__")
let v: String = state_get("__never_set_key__")
assert v == "", "del of nonexistent key is a no-op"
}
test "state-has" {
state_set("has_key", "value")
assert state_has("has_key"), "has returns true for set key"
assert !state_has("__no_has_key__"), "has returns false for absent key"
state_del("has_key")
assert !state_has("has_key"), "has returns false after del"
}
test "state-get-or" {
state_set("gor_key", "actual")
let v1: String = state_get_or("gor_key", "default")
assert v1 == "actual", "get_or returns value when key set"
let v2: String = state_get_or("__absent_gor_key__", "fallback")
assert v2 == "fallback", "get_or returns default when key absent"
}
test "state-multiple-keys" {
state_set("mk_a", "alpha")
state_set("mk_b", "beta")
state_set("mk_c", "gamma")
let a: String = state_get("mk_a")
let b: String = state_get("mk_b")
let c: String = state_get("mk_c")
assert a == "alpha", "key a correct"
assert b == "beta", "key b correct"
assert c == "gamma", "key c correct"
state_del("mk_a")
state_del("mk_b")
state_del("mk_c")
}
test "state-keys-returns-json-array" {
state_set("keys_test_1", "v1")
state_set("keys_test_2", "v2")
let ks: String = state_keys()
// The result is a JSON array string like ["keys_test_1","keys_test_2",...]
assert str_starts_with(ks, "["), "state_keys returns JSON array"
assert str_ends_with(ks, "]"), "state_keys JSON array is closed"
assert str_contains(ks, "keys_test_1"), "keys array contains keys_test_1"
assert str_contains(ks, "keys_test_2"), "keys array contains keys_test_2"
state_del("keys_test_1")
state_del("keys_test_2")
}
test "state-numeric-value-as-string" {
state_set("num_key", "42")
let v: String = state_get("num_key")
let n: Int = str_to_int(v)
assert n == 42, "stored numeric string round-trips to int"
state_del("num_key")
}
test "state-long-value" {
let long_val: String = str_repeat("abcdefghij", 100)
state_set("long_val_key", long_val)
let got: String = state_get("long_val_key")
assert got == long_val, "long value round-trips correctly"
let got_len: Int = str_len(got)
assert got_len == 1000, "long value is 1000 bytes"
state_del("long_val_key")
}
+272
View File
@@ -0,0 +1,272 @@
// test_string.el - native test suite for runtime/string.el
//
// Covers: type conversions, core primitives, comparison and search,
// case conversion, whitespace trimming, replacement, repetition,
// reversal, prefix/suffix stripping, padding, counting, character
// classification, splitting, and joining.
test "int-to-str" {
let s: String = int_to_str(42)
assert s == "42", "int 42 to string"
let neg: String = int_to_str(-7)
assert neg == "-7", "negative int to string"
let zero: String = int_to_str(0)
assert zero == "0", "zero to string"
}
test "str-to-int" {
let n: Int = str_to_int("123")
assert n == 123, "parse 123"
let neg: Int = str_to_int("-5")
assert neg == -5, "parse negative"
let zero: Int = str_to_int("0")
assert zero == 0, "parse zero"
}
test "bool-to-str" {
let t: String = bool_to_str(true)
assert t == "true", "true to string"
let f: String = bool_to_str(false)
assert f == "false", "false to string"
}
test "str-len" {
let n: Int = str_len("hello")
assert n == 5, "length of hello"
let e: Int = str_len("")
assert e == 0, "length of empty string"
let space: Int = str_len("a b")
assert space == 3, "length with spaces"
}
test "str-eq" {
assert str_eq("abc", "abc"), "identical strings are equal"
assert !str_eq("abc", "ABC"), "case-sensitive comparison"
assert str_eq("", ""), "empty strings are equal"
assert !str_eq("a", "b"), "different single chars"
}
test "str-slice" {
let s: String = str_slice("hello world", 6, 11)
assert s == "world", "slice end of string"
let start: String = str_slice("hello world", 0, 5)
assert start == "hello", "slice start of string"
let empty: String = str_slice("hello", 2, 2)
assert empty == "", "zero-length slice"
}
test "str-starts-ends-with" {
assert str_starts_with("hello world", "hello"), "starts with hello"
assert str_ends_with("hello world", "world"), "ends with world"
assert !str_starts_with("hello world", "world"), "does not start with world"
assert !str_ends_with("hello world", "hello"), "does not end with hello"
assert str_starts_with("abc", ""), "empty prefix always matches"
assert str_ends_with("abc", ""), "empty suffix always matches"
}
test "str-contains" {
assert str_contains("hello world", "world"), "contains world"
assert str_contains("hello world", "lo wo"), "contains interior substring"
assert !str_contains("hello world", "xyz"), "does not contain xyz"
assert str_contains("abc", ""), "empty sub is always contained"
assert !str_contains("", "x"), "empty string does not contain nonempty sub"
}
test "str-index-of" {
let i: Int = str_index_of("hello world", "world")
assert i == 6, "index of world"
let j: Int = str_index_of("hello world", "xyz")
assert j == -1, "not found returns -1"
let k: Int = str_index_of("aabbcc", "bb")
assert k == 2, "index of bb in aabbcc"
}
test "str-last-index-of" {
let i: Int = str_last_index_of("abcabc", "bc")
assert i == 4, "last occurrence of bc"
let j: Int = str_last_index_of("hello", "xyz")
assert j == -1, "not found returns -1"
let k: Int = str_last_index_of("aaa", "a")
assert k == 2, "last single-char match"
}
test "str-to-upper-lower" {
let up: String = str_to_upper("hello")
assert up == "HELLO", "to upper"
let lo: String = str_to_lower("WORLD")
assert lo == "world", "to lower"
let mixed: String = str_to_upper("Hello World")
assert mixed == "HELLO WORLD", "mixed to upper"
let empty: String = str_to_lower("")
assert empty == "", "empty stays empty"
}
test "str-trim" {
let s: String = str_trim(" hello ")
assert s == "hello", "trim both ends"
let lonly: String = str_trim(" hello")
assert lonly == "hello", "trim left only"
let ronly: String = str_trim("hello ")
assert ronly == "hello", "trim right only"
let tabs: String = str_trim("\thello\n")
assert tabs == "hello", "trim tabs and newlines"
let empty: String = str_trim(" ")
assert empty == "", "all whitespace trims to empty"
}
test "str-replace" {
let s: String = str_replace("hello world", "world", "there")
assert s == "hello there", "replace word"
let none: String = str_replace("hello", "xyz", "abc")
assert none == "hello", "no match leaves string unchanged"
let multi: String = str_replace("aaa", "a", "b")
assert multi == "bbb", "replace all occurrences"
let empty_from: String = str_replace("hello", "", "x")
assert empty_from == "hello", "empty from returns original"
}
test "str-repeat" {
let s: String = str_repeat("ab", 3)
assert s == "ababab", "repeat 3 times"
let once: String = str_repeat("x", 1)
assert once == "x", "repeat once"
let zero: String = str_repeat("abc", 0)
assert zero == "", "repeat zero times"
let neg: String = str_repeat("abc", -1)
assert neg == "", "negative repeat is empty"
}
test "str-reverse" {
let s: String = str_reverse("hello")
assert s == "olleh", "reverse hello"
let single: String = str_reverse("a")
assert single == "a", "reverse single char"
let empty: String = str_reverse("")
assert empty == "", "reverse empty string"
let palindrome: String = str_reverse("racecar")
assert palindrome == "racecar", "reverse palindrome"
}
test "str-strip-prefix-suffix" {
let p: String = str_strip_prefix("foobar", "foo")
assert p == "bar", "strip prefix foo"
let no_prefix: String = str_strip_prefix("foobar", "baz")
assert no_prefix == "foobar", "no match leaves string unchanged"
let s: String = str_strip_suffix("hello.md", ".md")
assert s == "hello", "strip suffix .md"
let no_suffix: String = str_strip_suffix("hello.md", ".txt")
assert no_suffix == "hello.md", "non-matching suffix unchanged"
}
test "str-strip-chars" {
let s: String = str_strip_chars(" \thello \n", " \t\n")
assert s == "hello", "strip whitespace chars"
let dots: String = str_strip_chars("...hello...", ".")
assert dots == "hello", "strip dot chars"
let all: String = str_strip_chars("aaa", "a")
assert all == "", "strip all chars leaves empty"
}
test "str-pad-left-right" {
let l: String = str_pad_left("42", 5, "0")
assert l == "00042", "zero-pad left to width 5"
let r: String = str_pad_right("hi", 5, "-")
assert r == "hi---", "dash-pad right to width 5"
let no_pad: String = str_pad_left("hello", 3, "x")
assert no_pad == "hello", "no pad when string already wide enough"
}
test "str-count" {
let n: Int = str_count("abc abc abc", "abc")
assert n == 3, "count three occurrences"
let overlap: Int = str_count("aaa", "aa")
assert overlap == 1, "non-overlapping count"
let zero: Int = str_count("hello", "xyz")
assert zero == 0, "not found gives 0"
let empty_sub: Int = str_count("hello", "")
assert empty_sub == 0, "empty sub gives 0"
}
test "str-count-lines-words-letters" {
let s: String = "hello world\nfoo bar"
let lines: Int = str_count_lines(s)
let words: Int = str_count_words(s)
let letters: Int = str_count_letters(s)
assert lines == 2, "line count"
assert words == 4, "word count"
assert letters == 16, "letter count"
}
test "str-count-chars-and-digits" {
let digits: Int = str_count_digits("abc123def456")
assert digits == 6, "six digits"
let none: Int = str_count_digits("hello")
assert none == 0, "no digits"
let chars: Int = str_count_chars("hello")
assert chars == 5, "five ASCII chars"
}
test "char-classes" {
assert is_letter("A"), "A is a letter"
assert is_digit("7"), "7 is a digit"
assert is_whitespace(" "), "space is whitespace"
assert !is_letter("3"), "3 is not a letter"
assert !is_digit("X"), "X is not a digit"
assert is_alphanumeric("abc123"), "abc123 is alphanumeric"
assert !is_alphanumeric("abc!"), "abc! is not alphanumeric"
assert is_uppercase("ABC"), "ABC is uppercase"
assert is_lowercase("abc"), "abc is lowercase"
assert !is_uppercase("Abc"), "mixed is not uppercase"
}
test "str-split" {
let parts: [String] = str_split("a,b,c", ",")
let n: Int = native_list_len(parts)
assert n == 3, "split into 3 parts"
let p0: String = native_list_get(parts, 0)
let p1: String = native_list_get(parts, 1)
let p2: String = native_list_get(parts, 2)
assert p0 == "a", "first part"
assert p1 == "b", "second part"
assert p2 == "c", "third part"
}
test "str-split-lines" {
let lines: [String] = str_split_lines("alpha\nbeta\r\ngamma\n")
let n: Int = native_list_len(lines)
assert n == 3, "split into 3 lines"
let l0: String = native_list_get(lines, 0)
let l1: String = native_list_get(lines, 1)
let l2: String = native_list_get(lines, 2)
assert l0 == "alpha", "first line"
assert l1 == "beta", "second line strips CR"
assert l2 == "gamma", "third line"
}
test "str-join" {
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, "alpha")
let parts = native_list_append(parts, "beta")
let parts = native_list_append(parts, "gamma")
let result: String = str_join(parts, ", ")
assert result == "alpha, beta, gamma", "join with separator"
let empty_parts: [String] = native_list_empty()
let empty_result: String = str_join(empty_parts, ",")
assert empty_result == "", "join empty list gives empty string"
}
test "str-char-at" {
let c: String = str_char_at("hello", 1)
assert c == "e", "char at index 1"
let first: String = str_char_at("abc", 0)
assert first == "a", "first char"
let oob: String = str_char_at("abc", 10)
assert oob == "", "out of bounds returns empty"
}
test "url-encode-decode" {
let encoded: String = url_encode("hello world")
assert !str_contains(encoded, " "), "space is encoded"
let decoded: String = url_decode(encoded)
assert decoded == "hello world", "round-trip decode"
}
+84
View File
@@ -0,0 +1,84 @@
// test_text.el - native test suite for text primitives.
//
// Mirrors the acceptance corpus in tests/text/examples/ using the
// native test/assert system instead of run.sh + expected output files.
test "count-substring" {
let x: Int = str_count("abc abc abc", "abc")
assert x == 3
}
test "count-overlap-skip" {
let x: Int = str_count("aaa", "aa")
assert x == 1, "non-overlapping count of aa in aaa"
}
test "count-lines-words-letters" {
let s: String = "hello world\nfoo bar"
let lines: Int = str_count_lines(s)
let words: Int = str_count_words(s)
let letters: Int = str_count_letters(s)
assert lines == 2, "line count"
assert words == 4, "word count"
assert letters == 16, "letter count"
}
test "index-of-all" {
let positions: [Int] = str_index_of_all("abXcdXefX", "X")
let n: Int = native_list_len(positions)
assert n == 3, "should find 3 occurrences"
let p0: Int = native_list_get(positions, 0)
let p1: Int = native_list_get(positions, 1)
let p2: Int = native_list_get(positions, 2)
assert p0 == 2, "first X at index 2"
assert p1 == 5, "second X at index 5"
assert p2 == 8, "third X at index 8"
}
test "str-repeat" {
let s: String = str_repeat("ab", 3)
assert s == "ababab", "repeat 3 times"
}
test "str-reverse" {
let s: String = str_reverse("hello")
assert s == "olleh", "reverse hello"
}
test "str-strip-prefix" {
let s: String = str_strip_prefix("foobar", "foo")
assert s == "bar", "strip prefix foo"
}
test "str-strip-suffix" {
let s: String = str_strip_suffix("hello.md", ".md")
assert s == "hello", "strip suffix .md"
}
test "str-strip-chars" {
let s: String = str_strip_chars(" \thello \n", " \t\n")
assert s == "hello", "strip whitespace chars"
}
test "split-lines" {
let lines: [String] = str_split_lines("alpha\nbeta\r\ngamma\n")
let n: Int = native_list_len(lines)
assert n == 3, "split into 3 lines"
}
test "str-join" {
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, "alpha")
let parts = native_list_append(parts, "beta")
let parts = native_list_append(parts, "gamma")
let result: String = str_join(parts, ", ")
assert result == "alpha, beta, gamma", "join with separator"
}
test "char-classes" {
assert is_letter("A"), "A is a letter"
assert is_digit("7"), "7 is a digit"
assert is_whitespace(" "), "space is whitespace"
assert !is_letter("3"), "3 is not a letter"
assert !is_digit("X"), "X is not a digit"
}
+155
View File
@@ -0,0 +1,155 @@
// test_time.el - native test suite for runtime/time.el
//
// Covers: time_now (positive timestamp), time_to_parts (UTC decomposition),
// time_format (ISO 8601 and strftime tokens), time_add/diff, unix_timestamp,
// duration helpers, and instant conversions.
test "time-now-positive" {
let ts: Int = time_now()
// time_now() returns milliseconds since epoch.
// Any value greater than 1_700_000_000_000 (Nov 2023) is valid in 2025+.
assert ts > 1700000000000, "time_now returns a reasonable recent timestamp"
}
test "unix-timestamp-positive" {
let s: Int = unix_timestamp()
// Should be greater than 1_700_000_000 (seconds, Nov 2023)
assert s > 1700000000, "unix_timestamp returns seconds > 1700000000"
}
test "now-ns-positive" {
let ns: Int = now_ns()
// Should be a large nanosecond value
assert ns > 0, "now_ns returns positive value"
}
test "time-to-parts-epoch" {
// Unix epoch: 0 ms = 1970-01-01T00:00:00.000Z
let parts: String = time_to_parts(0)
let year: String = json_get(parts, "year")
let month: String = json_get(parts, "month")
let day: String = json_get(parts, "day")
let hour: String = json_get(parts, "hour")
assert year == "1970", "epoch year is 1970"
assert month == "1", "epoch month is 1"
assert day == "1", "epoch day is 1"
assert hour == "0", "epoch hour is 0"
}
test "time-to-parts-known-date" {
// 2024-03-15T12:30:45.000Z
// seconds = 2024-03-15 12:30:45 UTC
// epoch ms: use a known value
// 2024-03-15 00:00:00 UTC = 1710460800 seconds
// + 12*3600 + 30*60 + 45 = 43200 + 1800 + 45 = 45045 seconds
// total: 1710505845 seconds = 1710505845000 ms
let ts: Int = 1710505845000
let parts: String = time_to_parts(ts)
let year: String = json_get(parts, "year")
let month: String = json_get(parts, "month")
let day: String = json_get(parts, "day")
let hour: String = json_get(parts, "hour")
let minute: String = json_get(parts, "minute")
let second: String = json_get(parts, "second")
assert year == "2024", "year is 2024"
assert month == "3", "month is 3 (March)"
assert day == "15", "day is 15"
assert hour == "12", "hour is 12"
assert minute == "30", "minute is 30"
assert second == "45", "second is 45"
}
test "time-format-iso" {
// Epoch = 1970-01-01T00:00:00.000Z
let formatted: String = time_format(0, "ISO")
assert formatted == "1970-01-01T00:00:00.000Z", "epoch formats to ISO correctly"
}
test "time-format-iso-empty-fmt" {
// Empty format string should also produce ISO 8601
let formatted: String = time_format(0, "")
assert formatted == "1970-01-01T00:00:00.000Z", "empty fmt produces ISO"
}
test "time-format-strftime" {
// 2024-03-15T12:30:45.000Z
let ts: Int = 1710505845000
let fmt: String = time_format(ts, "%Y-%m-%d")
assert fmt == "2024-03-15", "strftime %Y-%m-%d"
let hms: String = time_format(ts, "%H:%M:%S")
assert hms == "12:30:45", "strftime %H:%M:%S"
}
test "time-add-milliseconds" {
let base: Int = 1000000
let result: Int = time_add(base, 500, "ms")
assert result == 1000500, "add 500 ms"
}
test "time-add-seconds" {
let base: Int = 0
let result: Int = time_add(base, 60, "sec")
assert result == 60000, "add 60 seconds = 60000 ms"
}
test "time-add-minutes" {
let base: Int = 0
let result: Int = time_add(base, 2, "min")
assert result == 120000, "add 2 minutes = 120000 ms"
}
test "time-add-hours" {
let base: Int = 0
let result: Int = time_add(base, 1, "hour")
assert result == 3600000, "add 1 hour = 3600000 ms"
}
test "time-add-days" {
let base: Int = 0
let result: Int = time_add(base, 1, "day")
assert result == 86400000, "add 1 day = 86400000 ms"
}
test "time-diff-seconds" {
let t1: Int = 0
let t2: Int = 90000
let d: Int = time_diff(t1, t2, "sec")
assert d == 90, "diff 90000 ms = 90 seconds"
}
test "time-diff-negative" {
let t1: Int = 5000
let t2: Int = 2000
let d: Int = time_diff(t1, t2, "sec")
assert d == -3, "negative diff when t2 < t1"
}
test "duration-helpers" {
let d_secs: Int = duration_seconds(5)
assert d_secs == 5000000000, "5 seconds in nanoseconds"
let d_ms: Int = duration_millis(100)
assert d_ms == 100000000, "100 ms in nanoseconds"
let d_ns: Int = duration_nanos(42)
assert d_ns == 42, "42 nanos is identity"
let back_secs: Int = duration_to_seconds(d_secs)
assert back_secs == 5, "convert back to seconds"
let back_ms: Int = duration_to_millis(d_ms)
assert back_ms == 100, "convert back to milliseconds"
}
test "instant-conversions" {
let inst: Int = unix_millis(1000)
assert inst == 1000000000, "unix_millis(1000) in nanoseconds"
let inst_secs: Int = unix_seconds(2)
assert inst_secs == 2000000000, "unix_seconds(2) in nanoseconds"
let back_ms: Int = instant_to_unix_millis(inst)
assert back_ms == 1000, "instant to unix millis"
let back_secs: Int = instant_to_unix_seconds(inst_secs)
assert back_secs == 2, "instant to unix seconds"
}
test "time-from-parts" {
// time_from_parts(secs, ns, tz) -> secs*1000 + ns/1_000_000
let ts: Int = time_from_parts(1000, 500000000, "UTC")
assert ts == 1000500, "time_from_parts: 1000 secs + 500ms"
}