From 7aa993d193cb61e3d5683c6c2f980a04da576fa5 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Thu, 7 May 2026 01:18:47 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20PR=20CI=20works=20without=20secrets=20?= =?UTF-8?q?=E2=80=94=20use=20committed=20El=20runtime=20for=20pull=5Freque?= =?UTF-8?q?st=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea does not inject secrets for pull_request events. All push/workflow_dispatch CI was already working. PR builds were failing at Clone el and Authenticate to GCP. Fix: for pull_request events, skip El clone and GCP auth entirely. Instead build EL_HOME from committed files: bin/elc-linux-amd64 + runtime/{el_runtime.c,.h,.js}. build-stage.sh already knows about bin/elc-linux-amd64 for JS compilation; this extends that pattern to the native compiler and JS runtime. Docker --cache-from is skipped implicitly (no docker auth configured for PR builds) — BuildKit handles unauthenticated cache-from gracefully, continuing without cache. --- .gitea/workflows/dev.yaml | 23 + runtime/el_runtime.js | 1049 +++++++++++++++++++++++++++++++++++++ 2 files changed, 1072 insertions(+) create mode 100644 runtime/el_runtime.js diff --git a/.gitea/workflows/dev.yaml b/.gitea/workflows/dev.yaml index da0fe1d..0e59dfd 100644 --- a/.gitea/workflows/dev.yaml +++ b/.gitea/workflows/dev.yaml @@ -47,6 +47,8 @@ jobs: fetch-depth: 2 - name: Clone el (provides elc compiler) + # push/workflow_dispatch only — pull_request events don't get secrets injected + if: github.event_name != 'pull_request' env: CHECKOUT_TOKEN: ${{ secrets.CHECKOUT_TOKEN }} run: | @@ -58,20 +60,41 @@ jobs: "$DEST" echo "EL_HOME=$DEST" >> "$GITHUB_ENV" + - name: Set up El SDK from committed runtime (PR builds) + # pull_request events have no secrets — build from committed bin/ and runtime/ + if: github.event_name == 'pull_request' + run: | + set -euo pipefail + DEST="${{ github.workspace }}/../foundation-el" + mkdir -p "$DEST/dist/platform" "$DEST/el-compiler/runtime" + cp bin/elc-linux-amd64 "$DEST/dist/platform/elc" + cp bin/elc-linux-amd64 "$DEST/dist/platform/elc-linux-amd64" + chmod +x "$DEST/dist/platform/elc" "$DEST/dist/platform/elc-linux-amd64" + cp runtime/el_runtime.c "$DEST/el-compiler/runtime/" + cp runtime/el_runtime.h "$DEST/el-compiler/runtime/" + cp runtime/el_runtime.js "$DEST/el-compiler/runtime/" + echo "EL_HOME=$DEST" >> "$GITHUB_ENV" + echo "El SDK set up from committed runtime files (no CHECKOUT_TOKEN needed)" + - name: Authenticate to GCP + if: github.event_name != 'pull_request' uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.GCP_SA_KEY }} - name: Set up gcloud SDK + if: github.event_name != 'pull_request' uses: google-github-actions/setup-gcloud@v2 with: project_id: neuron-785695 - name: Configure docker auth for Artifact Registry + if: github.event_name != 'pull_request' run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet - name: Get elc (pre-built linux/amd64 from El repo) + # Only needed for push/workflow_dispatch — PR builds set up elc from committed bin/ + if: github.event_name != 'pull_request' run: | set -euo pipefail ELC_SRC="$EL_HOME/dist/platform/elc-linux-amd64" diff --git a/runtime/el_runtime.js b/runtime/el_runtime.js new file mode 100644 index 0000000..a223fdb --- /dev/null +++ b/runtime/el_runtime.js @@ -0,0 +1,1049 @@ +/* + * el_runtime.js — El language JS runtime. + * + * The browser/Node analog of el_runtime.c. Compiled-from-El JS source + * imports this file once; it side-effects globalThis.__el with every + * builtin, so generated programs can destructure the names they need + * (see codegen-js.el's preamble). + * + * Value model: + * El's tagged el_val_t collapses into JS native types here: + * String -> string + * Int -> number (caveat: only 53 bits of integer precision) + * Float -> number (already a double) + * Bool -> boolean + * [T] -> Array + * Map<,> -> plain object + * Void -> undefined + * null -> null + * + * Runtime mode auto-detection: + * typeof window === 'undefined' -> Node mode + * otherwise -> Browser mode + * + * See spec/codegen-js.md for the full design rationale. + */ + +const IS_NODE = typeof window === 'undefined' && typeof process !== 'undefined' && process.versions != null && process.versions.node != null; + +// ── I/O ───────────────────────────────────────────────────────────────────── + +function println(s) { + if (IS_NODE) { + process.stdout.write(String(s) + '\n'); + } else { + console.log(String(s)); + } +} + +function print(s) { + if (IS_NODE) { + process.stdout.write(String(s)); + } else { + // Browser has no stdout — fall back to console with no newline group + console.log(String(s)); + } +} + +// ── String builtins ───────────────────────────────────────────────────────── + +// Coerce both args to string and concat. Mirrors el_str_concat in C; +// the C version handles both string-and-string and string-and-int. +function el_str_concat(a, b) { + return String(a) + String(b); +} + +function str_concat(a, b) { return el_str_concat(a, b); } + +// Strict equality with string coercion. Matches str_eq() in C — which +// strcmp's the underlying char*. Here we just === after coercion. +function str_eq(a, b) { + if (a === null || b === null) return a === b; + return String(a) === String(b); +} + +function str_starts_with(s, prefix) { + return String(s).startsWith(String(prefix)); +} + +function str_ends_with(s, suffix) { + return String(s).endsWith(String(suffix)); +} + +function str_len(s) { + return String(s).length; +} + +function int_to_str(n) { + return String(n); +} + +function str_to_int(s) { + const n = parseInt(String(s), 10); + return Number.isNaN(n) ? 0 : n; +} + +function str_slice(s, start, end) { + return String(s).slice(start, end); +} + +function str_contains(s, sub) { + return String(s).indexOf(String(sub)) >= 0; +} + +function str_replace(s, from, to) { + // Replace ALL occurrences (matches C runtime semantics). + return String(s).split(String(from)).join(String(to)); +} + +function str_to_upper(s) { return String(s).toUpperCase(); } +function str_to_lower(s) { return String(s).toLowerCase(); } +function str_upper(s) { return String(s).toUpperCase(); } +function str_lower(s) { return String(s).toLowerCase(); } + +function str_trim(s) { return String(s).trim(); } + +function str_index_of(s, sub) { + return String(s).indexOf(String(sub)); +} + +function str_split(s, sep) { + return String(s).split(String(sep)); +} + +function str_char_at(s, i) { + return String(s).charAt(i); +} + +function str_char_code(s, i) { + const c = String(s).charCodeAt(i); + return Number.isNaN(c) ? 0 : c; +} + +function str_pad_left(s, width, pad) { + return String(s).padStart(width, String(pad)); +} + +function str_pad_right(s, width, pad) { + return String(s).padEnd(width, String(pad)); +} + +// ── Math ──────────────────────────────────────────────────────────────────── + +function el_abs(n) { return Math.abs(n); } +function el_max(a, b) { return a > b ? a : b; } +function el_min(a, b) { return a < b ? a : b; } + +// ── Refcount (no-op — JS has GC) ──────────────────────────────────────────── + +function el_retain(_v) { /* no-op */ } +function el_release(_v) { /* no-op */ } + +// ── List ──────────────────────────────────────────────────────────────────── + +// Variadic constructor matching el_list_new(count, items...). Exposed so +// codegen-js can emit the same call shape if we ever want it (currently +// codegen-js emits JS array literals directly). +function el_list_new(_count, ...items) { + return items.slice(0); +} + +function el_list_empty() { return []; } +function el_list_clone(list) { return Array.isArray(list) ? list.slice() : []; } +function el_list_len(list) { return Array.isArray(list) ? list.length : 0; } + +function el_list_get(list, index) { + if (!Array.isArray(list)) return null; + if (index < 0 || index >= list.length) return null; + return list[index]; +} + +function el_list_append(list, elem) { + if (!Array.isArray(list)) return [elem]; + const out = list.slice(); + out.push(elem); + return out; +} + +function list_push(list, elem) { return el_list_append(list, elem); } + +function list_push_front(list, elem) { + if (!Array.isArray(list)) return [elem]; + return [elem, ...list]; +} + +function list_join(list, sep) { + if (!Array.isArray(list)) return ''; + return list.map(String).join(String(sep)); +} + +function list_range(start, end) { + const out = []; + for (let i = start; i < end; i++) out.push(i); + return out; +} + +// ── Map ───────────────────────────────────────────────────────────────────── + +// Variadic constructor (key, val, key, val, ...). +function el_map_new(_pairCount, ...kvs) { + const out = {}; + for (let i = 0; i < kvs.length; i += 2) { + out[String(kvs[i])] = kvs[i + 1]; + } + return out; +} + +function el_get_field(map, key) { + if (map === null || map === undefined) return null; + if (typeof map !== 'object') return null; + const k = String(key); + if (Object.prototype.hasOwnProperty.call(map, k)) return map[k]; + return null; +} + +function el_map_get(map, key) { return el_get_field(map, key); } + +function el_map_set(map, key, value) { + // Match the C runtime: shallow-copy + set, persistent semantics. + const out = (map && typeof map === 'object') ? { ...map } : {}; + out[String(key)] = value; + return out; +} + +// ── Method-call shorthand aliases ────────────────────────────────────────── +// `obj.method(args)` compiles to `method(obj, args)` per El convention. + +function append(list, elem) { return el_list_append(list, elem); } +function len(v) { + if (Array.isArray(v)) return v.length; + if (typeof v === 'string') return v.length; + if (v && typeof v === 'object') return Object.keys(v).length; + return 0; +} +function get(list, index) { return el_list_get(list, index); } +function map_get(m, k) { return el_get_field(m, k); } +function map_set(m, k, v) { return el_map_set(m, k, v); } + +// ── Native VM aliases ────────────────────────────────────────────────────── + +function native_list_get(list, index) { return el_list_get(list, index); } +function native_list_len(list) { return el_list_len(list); } +function native_list_append(list, elem) { return el_list_append(list, elem); } +function native_list_empty() { return []; } +function native_list_clone(list) { return el_list_clone(list); } +function native_string_chars(s) { return String(s).split(''); } +function native_int_to_str(n) { return String(n); } + +// ── HTTP ─────────────────────────────────────────────────────────────────── +// +// fetch() is async. These return Promise. Generated El code does +// not yet emit await — that's the async-taint pass (see spec §5). For +// programs that don't touch HTTP this is fine; for programs that do, +// the value will appear as "[object Promise]" until the taint pass lands. + +function http_get(url) { + if (typeof fetch === 'undefined') { + throw new Error('http_get: fetch() not available in this runtime'); + } + return fetch(String(url)).then(r => r.text()); +} + +function http_post(url, body) { + if (typeof fetch === 'undefined') { + throw new Error('http_post: fetch() not available in this runtime'); + } + return fetch(String(url), { method: 'POST', body: String(body) }).then(r => r.text()); +} + +function http_post_json(url, jsonBody) { + if (typeof fetch === 'undefined') { + throw new Error('http_post_json: fetch() not available in this runtime'); + } + return fetch(String(url), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: String(jsonBody), + }).then(r => r.text()); +} + +function http_get_with_headers(url, headersMap) { + if (typeof fetch === 'undefined') { + throw new Error('http_get_with_headers: fetch() not available'); + } + return fetch(String(url), { headers: headersMap || {} }).then(r => r.text()); +} + +function http_post_with_headers(url, body, headersMap) { + if (typeof fetch === 'undefined') { + throw new Error('http_post_with_headers: fetch() not available'); + } + return fetch(String(url), { + method: 'POST', + headers: headersMap || {}, + body: String(body), + }).then(r => r.text()); +} + +function http_serve(_port, _handler) { + throw new Error('http_serve: not supported in JS target — needs server-side runtime mode'); +} + +function http_set_handler(_name) { + throw new Error('http_set_handler: not supported in JS target'); +} + +// ── Filesystem (Node-only) ───────────────────────────────────────────────── + +function _ensureNode(name) { + if (!IS_NODE) { + throw new Error(`${name}: not supported in browser runtime`); + } +} + +function fs_read(path) { + _ensureNode('fs_read'); + const fs = require('node:fs'); + try { + return fs.readFileSync(String(path), 'utf8'); + } catch (_e) { + return ''; + } +} + +function fs_write(path, content) { + _ensureNode('fs_write'); + const fs = require('node:fs'); + try { + fs.writeFileSync(String(path), String(content)); + return true; + } catch (_e) { + return false; + } +} + +function fs_list(path) { + _ensureNode('fs_list'); + const fs = require('node:fs'); + try { + return fs.readdirSync(String(path)); + } catch (_e) { + return []; + } +} + +// ── JSON ─────────────────────────────────────────────────────────────────── + +function json_parse(s) { + try { return JSON.parse(String(s)); } + catch (_e) { return null; } +} + +function json_stringify(v) { + try { return JSON.stringify(v); } + catch (_e) { return ''; } +} + +function json_get(jsonStr, key) { + const o = json_parse(jsonStr); + if (o === null) return null; + return el_get_field(o, key); +} + +function json_get_string(jsonStr, key) { + const v = json_get(jsonStr, key); + return v === null ? '' : String(v); +} + +function json_get_int(jsonStr, key) { + const v = json_get(jsonStr, key); + if (typeof v === 'number') return Math.trunc(v); + if (typeof v === 'string') return str_to_int(v); + return 0; +} + +function json_get_float(jsonStr, key) { + const v = json_get(jsonStr, key); + return typeof v === 'number' ? v : 0; +} + +function json_get_bool(jsonStr, key) { + const v = json_get(jsonStr, key); + return v === true; +} + +function json_get_raw(jsonStr, key) { + const v = json_get(jsonStr, key); + return v === null ? '' : json_stringify(v); +} + +function json_set(jsonStr, key, value) { + const o = json_parse(jsonStr) ?? {}; + o[String(key)] = value; + return json_stringify(o); +} + +function json_array_len(jsonStr) { + const o = json_parse(jsonStr); + return Array.isArray(o) ? o.length : 0; +} + +// ── Time ─────────────────────────────────────────────────────────────────── + +function time_now() { + return Math.floor(Date.now() / 1000); +} + +function time_now_utc() { + // In the C runtime this returns nanoseconds since epoch. JS number + // can't represent that range past ~2^53. We return milliseconds — a + // safe range — and document the divergence. + return Date.now(); +} + +function sleep_secs(secs) { + if (!IS_NODE) { + throw new Error('sleep_secs: blocking sleep not supported in browser'); + } + // Simple sync sleep via Atomics.wait on a SharedArrayBuffer-backed Int32. + const sab = new SharedArrayBuffer(4); + const i32 = new Int32Array(sab); + Atomics.wait(i32, 0, 0, Math.floor(secs * 1000)); + return secs; +} + +function sleep_ms(ms) { + if (!IS_NODE) { + throw new Error('sleep_ms: blocking sleep not supported in browser'); + } + const sab = new SharedArrayBuffer(4); + const i32 = new Int32Array(sab); + Atomics.wait(i32, 0, 0, Math.floor(ms)); + return ms; +} + +// ── Bool ─────────────────────────────────────────────────────────────────── + +function bool_to_str(b) { return b ? 'true' : 'false'; } + +// ── Process ──────────────────────────────────────────────────────────────── + +function exit_program(code) { + if (IS_NODE) { + process.exit(code | 0); + } else { + throw new Error(`exit_program(${code}) called in browser`); + } +} + +// ── args() ───────────────────────────────────────────────────────────────── + +function args() { + if (IS_NODE) { + // process.argv is [node, script, ...args] — slice off node + script. + return process.argv.slice(2); + } + return []; +} + +// ── env ──────────────────────────────────────────────────────────────────── + +function env(key) { + if (IS_NODE) { + const v = process.env[String(key)]; + return v === undefined ? null : v; + } + return null; +} + +// ── In-process state K/V ─────────────────────────────────────────────────── + +const _stateMap = new Map(); + +function state_set(key, value) { + _stateMap.set(String(key), value); + return value; +} + +function state_get(key) { + const v = _stateMap.get(String(key)); + return v === undefined ? '' : v; +} + +function state_del(key) { + return _stateMap.delete(String(key)); +} + +function state_keys() { + return Array.from(_stateMap.keys()); +} + +// ── UUID ─────────────────────────────────────────────────────────────────── + +function uuid_v4() { + // RFC 4122-ish — uses crypto when available, falls back to Math.random. + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID(); + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} +function uuid_new() { return uuid_v4(); } + +// ── Float formatting ─────────────────────────────────────────────────────── + +function float_to_str(f) { return String(f); } +function int_to_float(n) { return n; } +function float_to_int(f) { return Math.trunc(f); } + +function format_float(f, decimals) { + return Number(f).toFixed(decimals); +} + +function decimal_round(f, decimals) { + const m = Math.pow(10, decimals); + return Math.round(f * m) / m; +} + +function str_to_float(s) { + const n = parseFloat(String(s)); + return Number.isNaN(n) ? 0 : n; +} + +// ── Math (Float-aware) ───────────────────────────────────────────────────── + +function math_sqrt(f) { return Math.sqrt(f); } +function math_log(f) { return Math.log10(f); } +function math_ln(f) { return Math.log(f); } +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. + +function _notSupported(name) { + return () => { throw new Error(`${name}: not supported in JS target — needs server-side delegation`); }; +} + +// CGI identity +function el_cgi_init(_name, _did, _principal, _network, _engram) { + // No-op — UI code is not a CGI principal. See spec §7. +} + +// DHARMA — all stubbed. +const dharma_connect = _notSupported('dharma_connect'); +const dharma_send = _notSupported('dharma_send'); +const dharma_activate = _notSupported('dharma_activate'); +const dharma_emit = _notSupported('dharma_emit'); +const dharma_field = _notSupported('dharma_field'); +const dharma_strengthen = _notSupported('dharma_strengthen'); +const dharma_relationship = _notSupported('dharma_relationship'); +const dharma_peers = _notSupported('dharma_peers'); + +// Engram — stubbed (could be ported to in-browser later). +const engram_node = _notSupported('engram_node'); +const engram_node_full = _notSupported('engram_node_full'); +const engram_get_node = _notSupported('engram_get_node'); +const engram_strengthen = _notSupported('engram_strengthen'); +const engram_forget = _notSupported('engram_forget'); +const engram_node_count = _notSupported('engram_node_count'); +const engram_search = _notSupported('engram_search'); +const engram_scan_nodes = _notSupported('engram_scan_nodes'); +const engram_connect = _notSupported('engram_connect'); +const engram_edge_between = _notSupported('engram_edge_between'); +const engram_neighbors = _notSupported('engram_neighbors'); +const engram_neighbors_filtered = _notSupported('engram_neighbors_filtered'); +const engram_edge_count = _notSupported('engram_edge_count'); +const engram_activate = _notSupported('engram_activate'); +const engram_save = _notSupported('engram_save'); +const engram_load = _notSupported('engram_load'); + +// LLM — stubbed (browser cannot hold API keys safely). +const llm_call = _notSupported('llm_call'); +const llm_call_system = _notSupported('llm_call_system'); +const llm_call_agentic = _notSupported('llm_call_agentic'); +const llm_vision = _notSupported('llm_vision'); +const llm_models = _notSupported('llm_models'); +const llm_register_tool = _notSupported('llm_register_tool'); + +// Crypto — stubbed; could be backed by SubtleCrypto later. +const sha256_hex = _notSupported('sha256_hex'); +const sha256_bytes = _notSupported('sha256_bytes'); +const hmac_sha256_hex = _notSupported('hmac_sha256_hex'); +const hmac_sha256_bytes = _notSupported('hmac_sha256_bytes'); +const base64_encode = _notSupported('base64_encode'); +const base64_decode = _notSupported('base64_decode'); +const base64url_encode = _notSupported('base64url_encode'); +const base64url_decode = _notSupported('base64url_decode'); + +// ── Export to globalThis.__el ────────────────────────────────────────────── +// +// Generated programs destructure off this object. Keeping it on globalThis +// means a single `import "./el_runtime.js"` is enough; no per-call namespace +// prefix is required at codegen time. + +const __el = { + // 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, str_pad_left, str_pad_right, + // Math + el_abs, el_max, el_min, + // Refcount + el_retain, el_release, + // List + el_list_new, el_list_empty, el_list_clone, el_list_len, el_list_get, + el_list_append, list_push, list_push_front, list_join, list_range, + // Map + el_map_new, el_get_field, el_map_get, el_map_set, + // Method-call 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, + // HTTP + http_get, http_post, http_post_json, http_get_with_headers, + http_post_with_headers, http_serve, http_set_handler, + // FS + fs_read, fs_write, fs_list, + // 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, sleep_ms, + // Bool + bool_to_str, + // Process + exit_program, + // Args / env + args, env, + // State + state_set, state_get, state_del, state_keys, + // UUID + uuid_v4, uuid_new, + // 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, + dharma_strengthen, dharma_relationship, dharma_peers, + engram_node, engram_node_full, engram_get_node, engram_strengthen, + engram_forget, engram_node_count, engram_search, engram_scan_nodes, + engram_connect, engram_edge_between, engram_neighbors, + engram_neighbors_filtered, engram_edge_count, engram_activate, + engram_save, engram_load, + llm_call, llm_call_system, llm_call_agentic, llm_vision, + llm_models, llm_register_tool, + // Crypto (stubs) + sha256_hex, sha256_bytes, hmac_sha256_hex, hmac_sha256_bytes, + base64_encode, base64_decode, base64url_encode, base64url_decode, +}; + +globalThis.__el = __el; + +// Also re-export as ES module exports for consumers that prefer that style. +export { __el as default }; +export { + println, print, + 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, + el_abs, el_max, el_min, + el_retain, el_release, + el_list_new, el_list_empty, el_list_clone, el_list_len, el_list_get, + el_list_append, list_push, list_push_front, list_join, list_range, + el_map_new, el_get_field, el_map_get, el_map_set, + append, len, get, map_get, map_set, + native_list_get, native_list_len, native_list_append, native_list_empty, + native_list_clone, native_string_chars, native_int_to_str, + http_get, http_post, http_post_json, + 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, + 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, +};