0ace906823
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 8s
PR builds can't pull ci-base (no GCP secrets on pull_request events). dev.yaml falls back to committed bin/ + runtime/ instead. Extracted from ci-base:latest (sdk-release.yaml run 1411).
1050 lines
36 KiB
JavaScript
1050 lines
36 KiB
JavaScript
/*
|
|
* 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<string>. 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,
|
|
};
|