add El SDK CI/CD pipeline and install script
- .gitea/workflows/sdk-release.yaml: build elc from bootstrap, run tests, publish latest release, dispatch el-sdk-updated to downstream repos - install.sh: one-command El SDK install from Gitea release
This commit is contained in:
@@ -3099,6 +3099,9 @@ el_val_t json_get_raw(el_val_t json_str, el_val_t key) {
|
||||
const char* json = EL_CSTR(json_str);
|
||||
const char* k = EL_CSTR(key);
|
||||
const char* p = json_find_key(json, k);
|
||||
/* Clear fs_read binary-length hint — result is a fresh null-terminated
|
||||
* string, not the raw file bytes, so Content-Length must use strlen. */
|
||||
_tl_fs_read_len = 0;
|
||||
if (!p) return el_wrap_str(el_strdup(""));
|
||||
const char* end = json_skip_value(p);
|
||||
size_t n = (size_t)(end - p);
|
||||
|
||||
@@ -0,0 +1,679 @@
|
||||
/*
|
||||
* 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; }
|
||||
|
||||
// ── 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,
|
||||
// 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,
|
||||
};
|
||||
@@ -0,0 +1,943 @@
|
||||
// codegen-js.el — El compiler JavaScript source code generator
|
||||
//
|
||||
// Input: list of AST statement maps (from parser.el)
|
||||
// Output: JavaScript source printed to stdout (streamed, one line at a time)
|
||||
//
|
||||
// Each El program compiles to a single .js file that imports el_runtime.js
|
||||
// (which side-effects globals so call sites stay flat — println(x), not
|
||||
// el.println(x)). Functions map to JS function declarations; top-level
|
||||
// statements run at module load.
|
||||
//
|
||||
// Entry point: fn codegen_js(stmts: [Map<String, Any>], source: String) -> String
|
||||
// Returns "" — output goes to stdout via println().
|
||||
//
|
||||
// This file mirrors codegen.el (the C backend). Where the C backend has to
|
||||
// fight the int64_t-everywhere convention to dispatch arithmetic vs concat
|
||||
// or `==` vs `str_eq`, the JS backend can usually let JS's own operator
|
||||
// semantics do the right thing. We retain the dispatch logic for clarity
|
||||
// and so that explicit calls to `el_str_concat` or `str_eq` still work.
|
||||
|
||||
// ── String helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// Escape a JS string literal (double-quotes, backslashes, newlines, etc.).
|
||||
fn js_escape(s: String) -> String {
|
||||
let chars: [String] = native_string_chars(s)
|
||||
let total: Int = native_list_len(chars)
|
||||
let out = ""
|
||||
let i = 0
|
||||
while i < total {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if ch == "\"" {
|
||||
let out = out + "\\\""
|
||||
} else {
|
||||
if ch == "\\" {
|
||||
let out = out + "\\\\"
|
||||
} else {
|
||||
if ch == "\n" {
|
||||
let out = out + "\\n"
|
||||
} else {
|
||||
if ch == "\r" {
|
||||
let out = out + "\\r"
|
||||
} else {
|
||||
if ch == "\t" {
|
||||
let out = out + "\\t"
|
||||
} else {
|
||||
let out = out + ch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn js_str_lit(s: String) -> String {
|
||||
"\"" + js_escape(s) + "\""
|
||||
}
|
||||
|
||||
// ── Code emission ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn js_emit_line(line: String) -> Void {
|
||||
println(line)
|
||||
}
|
||||
|
||||
fn js_emit_blank() -> Void {
|
||||
println("")
|
||||
}
|
||||
|
||||
// ── Operator helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
fn js_binop(op: String) -> String {
|
||||
if op == "Plus" { return "+" }
|
||||
if op == "Minus" { return "-" }
|
||||
if op == "Star" { return "*" }
|
||||
if op == "Slash" { return "/" }
|
||||
if op == "Percent" { return "%" }
|
||||
if op == "EqEq" { return "===" }
|
||||
if op == "NotEq" { return "!==" }
|
||||
if op == "Lt" { return "<" }
|
||||
if op == "Gt" { return ">" }
|
||||
if op == "LtEq" { return "<=" }
|
||||
if op == "GtEq" { return ">=" }
|
||||
if op == "And" { return "&&" }
|
||||
if op == "Or" { return "||" }
|
||||
op
|
||||
}
|
||||
|
||||
// ── Int-name tracking (mirrors codegen.el) ────────────────────────────────────
|
||||
|
||||
fn js_is_int_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__js_int_names")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
fn js_add_int_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__js_int_names")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__js_int_names", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn js_build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
|
||||
state_set("__js_int_names", ",")
|
||||
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 ptype: String = param["type"]
|
||||
if str_eq(ptype, "Int") {
|
||||
js_add_int_name(pname)
|
||||
}
|
||||
let pi = pi + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn js_is_int_call(call_expr: Map<String, Any>) -> Bool {
|
||||
let func = call_expr["func"]
|
||||
let fk: String = func["expr"]
|
||||
if !str_eq(fk, "Ident") { return false }
|
||||
let name: String = func["name"]
|
||||
if str_eq(name, "str_len") { return true }
|
||||
if str_eq(name, "str_index_of") { return true }
|
||||
if str_eq(name, "str_to_int") { return true }
|
||||
if str_eq(name, "str_char_code") { return true }
|
||||
if str_eq(name, "native_list_len") { return true }
|
||||
if str_eq(name, "el_list_len") { return true }
|
||||
if str_eq(name, "len") { return true }
|
||||
if str_eq(name, "json_get_int") { return true }
|
||||
if str_eq(name, "time_now") { return true }
|
||||
if str_eq(name, "time_now_utc") { return true }
|
||||
if str_eq(name, "el_abs") { return true }
|
||||
if str_eq(name, "el_max") { return true }
|
||||
if str_eq(name, "el_min") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// ── Expression codegen ────────────────────────────────────────────────────────
|
||||
//
|
||||
// js_cg_expr returns a JS expression string (not a statement).
|
||||
//
|
||||
// Note: the C backend's `+` dispatch is preserved here for two reasons:
|
||||
// 1) Generated output stays grep-equivalent across targets
|
||||
// 2) Explicit `el_str_concat()` lives in the runtime; codegen routes
|
||||
// through it for ambiguous (Ident+Ident, Call+Call) cases. JS's
|
||||
// own `+` would also work, but el_str_concat coerces both sides
|
||||
// to strings — closer to the C semantics.
|
||||
|
||||
fn js_cg_expr(expr: Map<String, Any>) -> String {
|
||||
let kind: String = expr["expr"]
|
||||
|
||||
if kind == "Int" {
|
||||
let v: String = expr["value"]
|
||||
return v
|
||||
}
|
||||
|
||||
// DurationLit — postfix-literal time value (e.g. 30.seconds, 1.hour).
|
||||
// The JS backend lowers to a literal integer nanosecond count. The C
|
||||
// backend uses the typed wrapper el_duration_from_nanos to make intent
|
||||
// explicit at the runtime boundary; JS has no equivalent shim yet, so
|
||||
// we lower directly. A future Phase 2 JS time runtime can route through
|
||||
// a wrapper once added.
|
||||
if kind == "DurationLit" {
|
||||
let count: String = expr["count"]
|
||||
let unit: String = expr["unit"]
|
||||
let mult_ns = "1"
|
||||
if str_eq(unit, "nano") { let mult_ns = "1" }
|
||||
if str_eq(unit, "nanos") { let mult_ns = "1" }
|
||||
if str_eq(unit, "milli") { let mult_ns = "1000000" }
|
||||
if str_eq(unit, "millis") { let mult_ns = "1000000" }
|
||||
if str_eq(unit, "millisecond") { let mult_ns = "1000000" }
|
||||
if str_eq(unit, "milliseconds") { let mult_ns = "1000000" }
|
||||
if str_eq(unit, "second") { let mult_ns = "1000000000" }
|
||||
if str_eq(unit, "seconds") { let mult_ns = "1000000000" }
|
||||
if str_eq(unit, "minute") { let mult_ns = "60000000000" }
|
||||
if str_eq(unit, "minutes") { let mult_ns = "60000000000" }
|
||||
if str_eq(unit, "hour") { let mult_ns = "3600000000000" }
|
||||
if str_eq(unit, "hours") { let mult_ns = "3600000000000" }
|
||||
if str_eq(unit, "day") { let mult_ns = "86400000000000" }
|
||||
if str_eq(unit, "days") { let mult_ns = "86400000000000" }
|
||||
return "(" + count + " * " + mult_ns + ")"
|
||||
}
|
||||
|
||||
if kind == "Float" {
|
||||
// JS numbers are already doubles — no bit-cast trick needed.
|
||||
let v: String = expr["value"]
|
||||
return v
|
||||
}
|
||||
|
||||
if kind == "Str" {
|
||||
let v: String = expr["value"]
|
||||
return js_str_lit(v)
|
||||
}
|
||||
|
||||
if kind == "Bool" {
|
||||
let v: String = expr["value"]
|
||||
if v == "true" { return "true" }
|
||||
return "false"
|
||||
}
|
||||
|
||||
if kind == "Nil" {
|
||||
return "null"
|
||||
}
|
||||
|
||||
if kind == "Ident" {
|
||||
let name: String = expr["name"]
|
||||
return name
|
||||
}
|
||||
|
||||
if kind == "Not" {
|
||||
let inner = expr["inner"]
|
||||
let inner_c: String = js_cg_expr(inner)
|
||||
return "!" + inner_c
|
||||
}
|
||||
|
||||
if kind == "Neg" {
|
||||
let inner = expr["inner"]
|
||||
let inner_c: String = js_cg_expr(inner)
|
||||
return "(-" + inner_c + ")"
|
||||
}
|
||||
|
||||
if kind == "BinOp" {
|
||||
let op: String = expr["op"]
|
||||
let left = expr["left"]
|
||||
let right = expr["right"]
|
||||
let left_c: String = js_cg_expr(left)
|
||||
let right_c: String = js_cg_expr(right)
|
||||
let left_kind: String = left["expr"]
|
||||
let right_kind: String = right["expr"]
|
||||
|
||||
// Plus dispatch — same shape as C backend, but we route through
|
||||
// el_str_concat for the string-concat path (its JS impl coerces
|
||||
// and matches C's behavior). Arithmetic uses bare JS `+`.
|
||||
if op == "Plus" {
|
||||
if left_kind == "Str" {
|
||||
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
if right_kind == "Str" {
|
||||
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
if left_kind == "Int" {
|
||||
return "(" + left_c + " + " + right_c + ")"
|
||||
}
|
||||
if right_kind == "Int" {
|
||||
return "(" + left_c + " + " + right_c + ")"
|
||||
}
|
||||
if left_kind == "Ident" {
|
||||
if right_kind == "Ident" {
|
||||
let lname: String = left["name"]
|
||||
let rname: String = right["name"]
|
||||
if js_is_int_name(lname) {
|
||||
if js_is_int_name(rname) {
|
||||
return "(" + left_c + " + " + right_c + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if left_kind == "Ident" {
|
||||
if right_kind == "Call" {
|
||||
let lname: String = left["name"]
|
||||
if js_is_int_name(lname) {
|
||||
if js_is_int_call(right) {
|
||||
return "(" + left_c + " + " + right_c + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if right_kind == "Ident" {
|
||||
if left_kind == "Call" {
|
||||
let rname: String = right["name"]
|
||||
if js_is_int_name(rname) {
|
||||
if js_is_int_call(left) {
|
||||
return "(" + left_c + " + " + right_c + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if left_kind == "Call" {
|
||||
if right_kind == "Call" {
|
||||
if js_is_int_call(left) {
|
||||
if js_is_int_call(right) {
|
||||
return "(" + left_c + " + " + right_c + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
if right_kind == "Call" {
|
||||
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
// Fallback: when in doubt, route through el_str_concat. JS's
|
||||
// own + handles strings and numbers natively, but el_str_concat
|
||||
// gives us a single point of control if behavior needs to diverge.
|
||||
if left_kind == "Ident" {
|
||||
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
if right_kind == "Ident" {
|
||||
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
}
|
||||
|
||||
// Equality dispatch — C backend disambiguates via str_eq for
|
||||
// strings and == for ints. JS does both with === if we know
|
||||
// the types are uniform; for ambiguous identifier pairs we
|
||||
// route through str_eq for safety (it falls back to === in JS).
|
||||
if op == "EqEq" {
|
||||
if left_kind == "Int" { return "(" + left_c + " === " + right_c + ")" }
|
||||
if right_kind == "Int" { return "(" + left_c + " === " + right_c + ")" }
|
||||
if left_kind == "Bool" { return "(" + left_c + " === " + right_c + ")" }
|
||||
if right_kind == "Bool" { return "(" + left_c + " === " + right_c + ")" }
|
||||
if left_kind == "Nil" { return "(" + left_c + " === " + right_c + ")" }
|
||||
if right_kind == "Nil" { return "(" + left_c + " === " + right_c + ")" }
|
||||
if left_kind == "Ident" {
|
||||
if right_kind == "Ident" {
|
||||
let lname: String = left["name"]
|
||||
let rname: String = right["name"]
|
||||
if js_is_int_name(lname) {
|
||||
if js_is_int_name(rname) {
|
||||
return "(" + left_c + " === " + right_c + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if left_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" }
|
||||
if right_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" }
|
||||
// Default: === (works for strings, numbers, bools in JS)
|
||||
return "(" + left_c + " === " + right_c + ")"
|
||||
}
|
||||
|
||||
if op == "NotEq" {
|
||||
if left_kind == "Int" { return "(" + left_c + " !== " + right_c + ")" }
|
||||
if right_kind == "Int" { return "(" + left_c + " !== " + right_c + ")" }
|
||||
if left_kind == "Bool" { return "(" + left_c + " !== " + right_c + ")" }
|
||||
if right_kind == "Bool" { return "(" + left_c + " !== " + right_c + ")" }
|
||||
if left_kind == "Nil" { return "(" + left_c + " !== " + right_c + ")" }
|
||||
if right_kind == "Nil" { return "(" + left_c + " !== " + right_c + ")" }
|
||||
if left_kind == "Ident" {
|
||||
if right_kind == "Ident" {
|
||||
let lname: String = left["name"]
|
||||
let rname: String = right["name"]
|
||||
if js_is_int_name(lname) {
|
||||
if js_is_int_name(rname) {
|
||||
return "(" + left_c + " !== " + right_c + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if left_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" }
|
||||
if right_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" }
|
||||
return "(" + left_c + " !== " + right_c + ")"
|
||||
}
|
||||
|
||||
let op_c: String = js_binop(op)
|
||||
return "(" + left_c + " " + op_c + " " + right_c + ")"
|
||||
}
|
||||
|
||||
if kind == "Call" {
|
||||
let func = expr["func"]
|
||||
let args = expr["args"]
|
||||
let arity: Int = native_list_len(args)
|
||||
let func_kind: String = func["expr"]
|
||||
|
||||
let args_c = ""
|
||||
let i = 0
|
||||
while i < arity {
|
||||
let arg = native_list_get(args, i)
|
||||
let arg_c: String = js_cg_expr(arg)
|
||||
if i > 0 {
|
||||
let args_c = args_c + ", "
|
||||
}
|
||||
let args_c = args_c + arg_c
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
if func_kind == "Ident" {
|
||||
let fn_name: String = func["name"]
|
||||
return fn_name + "(" + args_c + ")"
|
||||
}
|
||||
|
||||
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 + ")"
|
||||
}
|
||||
return field + "(" + obj_c + ")"
|
||||
}
|
||||
|
||||
let fn_c: String = js_cg_expr(func)
|
||||
return fn_c + "(" + args_c + ")"
|
||||
}
|
||||
|
||||
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.
|
||||
let obj = expr["object"]
|
||||
let field: String = expr["field"]
|
||||
let obj_c: String = js_cg_expr(obj)
|
||||
return "el_get_field(" + obj_c + ", " + js_str_lit(field) + ")"
|
||||
}
|
||||
|
||||
if kind == "Index" {
|
||||
// Map vs list dispatch on the index expression kind, same as C.
|
||||
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"]
|
||||
if str_eq(idx_kind, "Str") {
|
||||
return "el_get_field(" + obj_c + ", " + idx_c + ")"
|
||||
}
|
||||
return "el_list_get(" + obj_c + ", " + idx_c + ")"
|
||||
}
|
||||
|
||||
if kind == "Array" {
|
||||
let elems = expr["elems"]
|
||||
let n: Int = native_list_len(elems)
|
||||
if n == 0 { return "[]" }
|
||||
let items = ""
|
||||
let i = 0
|
||||
while i < n {
|
||||
let elem = native_list_get(elems, i)
|
||||
let elem_c: String = js_cg_expr(elem)
|
||||
if i > 0 {
|
||||
let items = items + ", "
|
||||
}
|
||||
let items = items + elem_c
|
||||
let i = i + 1
|
||||
}
|
||||
return "[" + items + "]"
|
||||
}
|
||||
|
||||
if kind == "Map" {
|
||||
let pairs = expr["pairs"]
|
||||
let n: Int = native_list_len(pairs)
|
||||
if n == 0 { return "{}" }
|
||||
let items = ""
|
||||
let i = 0
|
||||
while i < n {
|
||||
let pair = native_list_get(pairs, i)
|
||||
let key: String = pair["key"]
|
||||
let val = pair["value"]
|
||||
let val_c: String = js_cg_expr(val)
|
||||
if i > 0 {
|
||||
let items = items + ", "
|
||||
}
|
||||
let items = items + js_str_lit(key) + ": " + val_c
|
||||
let i = i + 1
|
||||
}
|
||||
return "{" + items + "}"
|
||||
}
|
||||
|
||||
if kind == "Try" {
|
||||
let inner = expr["inner"]
|
||||
return js_cg_expr(inner)
|
||||
}
|
||||
|
||||
if kind == "If" {
|
||||
let cond = expr["cond"]
|
||||
let cond_c: String = js_cg_expr(cond)
|
||||
// If as expression: ternary. Body of the if-expression is not
|
||||
// currently emitted as expression-form for compound bodies; this
|
||||
// matches the C backend's if-expr stub.
|
||||
return "(" + cond_c + " ? 1 : 0)"
|
||||
}
|
||||
|
||||
if kind == "Match" {
|
||||
return js_cg_match(expr)
|
||||
}
|
||||
|
||||
"null"
|
||||
}
|
||||
|
||||
// ── Match codegen (basic) ─────────────────────────────────────────────────────
|
||||
//
|
||||
// Lower a match expression to an IIFE with if/else chain. Works for
|
||||
// LitInt / LitStr / LitBool / Wildcard / Binding patterns. Tagged-union
|
||||
// destructuring is not implemented — it's stubbed and falls through to
|
||||
// the wildcard path.
|
||||
|
||||
fn js_next_match_id() -> String {
|
||||
let csv: String = state_get("__js_match_counter")
|
||||
let n = 0
|
||||
if !str_eq(csv, "") {
|
||||
let n = str_to_int(csv)
|
||||
}
|
||||
let n = n + 1
|
||||
state_set("__js_match_counter", native_int_to_str(n))
|
||||
native_int_to_str(n)
|
||||
}
|
||||
|
||||
fn js_cg_match(expr: Map<String, Any>) -> String {
|
||||
let subject = expr["subject"]
|
||||
let arms = expr["arms"]
|
||||
let subj_c: String = js_cg_expr(subject)
|
||||
let id: String = js_next_match_id()
|
||||
let subj_var: String = "_match_subj_" + id
|
||||
let out: String = "((" + subj_var + ") => { "
|
||||
let n: Int = native_list_len(arms)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let arm = native_list_get(arms, i)
|
||||
let pat = arm["pattern"]
|
||||
let body = arm["body"]
|
||||
let pkind: String = pat["pattern"]
|
||||
let body_c: String = js_cg_expr(body)
|
||||
if str_eq(pkind, "Wildcard") {
|
||||
let out = out + "return (" + body_c + "); "
|
||||
} else {
|
||||
if str_eq(pkind, "Binding") {
|
||||
let bname: String = pat["name"]
|
||||
let out = out + "{ const " + bname + " = " + subj_var + "; return (" + body_c + "); } "
|
||||
} else {
|
||||
if str_eq(pkind, "LitInt") {
|
||||
let v: String = pat["value"]
|
||||
let out = out + "if (" + subj_var + " === " + v + ") return (" + body_c + "); "
|
||||
} else {
|
||||
if str_eq(pkind, "LitStr") {
|
||||
let v: String = pat["value"]
|
||||
let out = out + "if (str_eq(" + subj_var + ", " + js_str_lit(v) + ")) return (" + body_c + "); "
|
||||
} else {
|
||||
if str_eq(pkind, "LitBool") {
|
||||
let v: String = pat["value"]
|
||||
let bv = "false"
|
||||
if str_eq(v, "true") { let bv = "true" }
|
||||
let out = out + "if (" + subj_var + " === " + bv + ") return (" + body_c + "); "
|
||||
} else {
|
||||
// unknown pattern → wildcard
|
||||
let out = out + "return (" + body_c + "); "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
let out = out + "return null; })(" + subj_c + ")"
|
||||
out
|
||||
}
|
||||
|
||||
// ── Variable scope tracking ───────────────────────────────────────────────────
|
||||
//
|
||||
// El allows `let x = ...` to redeclare in the same scope. JS would throw
|
||||
// with `let` (Identifier already declared). We track declared names and
|
||||
// emit bare `x = ...` on redeclaration, `let x = ...` first time.
|
||||
|
||||
fn js_list_contains(lst: [String], s: String) -> Bool {
|
||||
let n: Int = native_list_len(lst)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = native_list_get(lst, i)
|
||||
if item == s { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// ── Statement codegen ─────────────────────────────────────────────────────────
|
||||
|
||||
fn js_cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [String] {
|
||||
let kind: String = stmt["stmt"]
|
||||
|
||||
if kind == "Let" {
|
||||
let name: String = stmt["name"]
|
||||
let val = stmt["value"]
|
||||
let val_c: String = js_cg_expr(val)
|
||||
let ltype: String = stmt["type"]
|
||||
if str_eq(ltype, "Int") {
|
||||
js_add_int_name(name)
|
||||
}
|
||||
let vk: String = val["expr"]
|
||||
if str_eq(vk, "Int") {
|
||||
js_add_int_name(name)
|
||||
}
|
||||
if js_list_contains(declared, name) {
|
||||
js_emit_line(indent + name + " = " + val_c + ";")
|
||||
return declared
|
||||
} else {
|
||||
// Use `let` (not `const`) — El semantics allow rebinding.
|
||||
js_emit_line(indent + "let " + name + " = " + val_c + ";")
|
||||
return native_list_append(declared, name)
|
||||
}
|
||||
}
|
||||
|
||||
if kind == "Return" {
|
||||
let val = stmt["value"]
|
||||
let val_kind: String = val["expr"]
|
||||
if val_kind == "Nil" {
|
||||
js_emit_line(indent + "return null;")
|
||||
} else {
|
||||
let val_c: String = js_cg_expr(val)
|
||||
js_emit_line(indent + "return " + val_c + ";")
|
||||
}
|
||||
return declared
|
||||
}
|
||||
|
||||
// Bare reassignment: `name = expr`. Mirrors the C backend — emits a
|
||||
// plain JS assignment without `let` so we don't shadow an outer binding.
|
||||
if kind == "Assign" {
|
||||
let name: String = stmt["name"]
|
||||
let val = stmt["value"]
|
||||
let val_c: String = js_cg_expr(val)
|
||||
js_emit_line(indent + name + " = " + val_c + ";")
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "Expr" {
|
||||
let val = stmt["value"]
|
||||
let val_kind: String = val["expr"]
|
||||
if val_kind == "If" {
|
||||
js_cg_if_stmt(val, indent, declared)
|
||||
return declared
|
||||
}
|
||||
if val_kind == "For" {
|
||||
js_cg_for_stmt(val, indent, declared)
|
||||
return declared
|
||||
}
|
||||
let val_c: String = js_cg_expr(val)
|
||||
js_emit_line(indent + val_c + ";")
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "While" {
|
||||
let cond = stmt["cond"]
|
||||
let body = stmt["body"]
|
||||
let cond_c: String = js_cg_expr(cond)
|
||||
let cond_c = js_strip_outer_parens(cond_c)
|
||||
js_emit_line(indent + "while (" + cond_c + ") {")
|
||||
js_cg_stmts(body, indent + " ", native_list_clone(declared))
|
||||
js_emit_line(indent + "}")
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "For" {
|
||||
let item: String = stmt["item"]
|
||||
let list_expr = stmt["list"]
|
||||
let body = stmt["body"]
|
||||
js_cg_for_body(item, list_expr, body, indent, declared)
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "FnDef" { return declared }
|
||||
if kind == "TypeDef" { return declared }
|
||||
if kind == "EnumDef" { return declared }
|
||||
if kind == "Import" { 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
|
||||
// principal. See spec/codegen-js.md §7.
|
||||
let cname: String = stmt["name"]
|
||||
js_emit_line(indent + "// cgi block '" + cname + "' — no-op in JS target (server-side concept)")
|
||||
return declared
|
||||
}
|
||||
if kind == "ServiceBlock" {
|
||||
let sname: String = stmt["name"]
|
||||
js_emit_line(indent + "// service block '" + sname + "' — no-op in JS target")
|
||||
return declared
|
||||
}
|
||||
declared
|
||||
}
|
||||
|
||||
// Strip a single layer of surrounding parentheses from a JS expression string.
|
||||
fn js_strip_outer_parens(s: String) -> String {
|
||||
let chars: [String] = native_string_chars(s)
|
||||
let n: Int = native_list_len(chars)
|
||||
if n < 2 { return s }
|
||||
let first: String = native_list_get(chars, 0)
|
||||
let last: String = native_list_get(chars, n - 1)
|
||||
if first == "(" {
|
||||
if last == ")" {
|
||||
let depth = 1
|
||||
let i = 1
|
||||
let balanced = true
|
||||
while i < n - 1 {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if ch == "(" {
|
||||
let depth = depth + 1
|
||||
}
|
||||
if ch == ")" {
|
||||
let depth = depth - 1
|
||||
if depth == 0 {
|
||||
let balanced = false
|
||||
let i = n
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
if balanced {
|
||||
let inner = ""
|
||||
let j = 1
|
||||
while j < n - 1 {
|
||||
let ch: String = native_list_get(chars, j)
|
||||
let inner = inner + ch
|
||||
let j = j + 1
|
||||
}
|
||||
return inner
|
||||
}
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn js_cg_if_stmt(expr: Map<String, Any>, indent: String, declared: [String]) -> Void {
|
||||
let cond = expr["cond"]
|
||||
let then_stmts = expr["then"]
|
||||
let else_stmts = expr["else"]
|
||||
let has_else: Bool = expr["has_else"]
|
||||
let cond_c: String = js_cg_expr(cond)
|
||||
let cond_c = js_strip_outer_parens(cond_c)
|
||||
js_emit_line(indent + "if (" + cond_c + ") {")
|
||||
js_cg_stmts(then_stmts, indent + " ", native_list_clone(declared))
|
||||
if has_else {
|
||||
js_emit_line(indent + "} else {")
|
||||
js_cg_stmts(else_stmts, indent + " ", native_list_clone(declared))
|
||||
}
|
||||
js_emit_line(indent + "}")
|
||||
}
|
||||
|
||||
fn js_cg_for_body(item: String, list_expr: Map<String, Any>, body: [Map<String, Any>], indent: String, declared: [String]) -> Void {
|
||||
let list_c: String = js_cg_expr(list_expr)
|
||||
js_emit_line(indent + "for (const " + item + " of " + list_c + ") {")
|
||||
let body_decl = native_list_clone(declared)
|
||||
let body_decl = native_list_append(body_decl, item)
|
||||
js_cg_stmts(body, indent + " ", body_decl)
|
||||
js_emit_line(indent + "}")
|
||||
}
|
||||
|
||||
fn js_cg_for_stmt(expr: Map<String, Any>, indent: String, declared: [String]) -> Void {
|
||||
let item: String = expr["item"]
|
||||
let list_expr = expr["list"]
|
||||
let body = expr["body"]
|
||||
js_cg_for_body(item, list_expr, body, indent, declared)
|
||||
}
|
||||
|
||||
fn js_cg_stmts(stmts: [Map<String, Any>], indent: String, declared: [String]) -> [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 decl = js_cg_stmt(stmt, indent, decl)
|
||||
let i = i + 1
|
||||
}
|
||||
decl
|
||||
}
|
||||
|
||||
// ── Function declaration codegen ──────────────────────────────────────────────
|
||||
|
||||
fn js_params_str(params: [Map<String, Any>]) -> String {
|
||||
let n: Int = native_list_len(params)
|
||||
if n == 0 { return "" }
|
||||
let out = ""
|
||||
let i = 0
|
||||
while i < n {
|
||||
let param = native_list_get(params, i)
|
||||
let name: String = param["name"]
|
||||
if i > 0 {
|
||||
let out = out + ", "
|
||||
}
|
||||
let out = out + name
|
||||
let i = i + 1
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// Same implicit-return transform as the C backend.
|
||||
fn js_transform_implicit_return(body: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||
let n: Int = native_list_len(body)
|
||||
if n == 0 { return body }
|
||||
let last: Map<String, Any> = native_list_get(body, n - 1)
|
||||
let last_kind: String = last["stmt"]
|
||||
if last_kind == "Expr" {
|
||||
let val = last["value"]
|
||||
let val_kind: String = val["expr"]
|
||||
if val_kind == "If" { return body }
|
||||
if val_kind == "For" { return body }
|
||||
let new_body: [Map<String, Any>] = native_list_empty()
|
||||
let i = 0
|
||||
while i < n - 1 {
|
||||
let new_body = native_list_append(new_body, native_list_get(body, i))
|
||||
let i = i + 1
|
||||
}
|
||||
let return_stmt: Map<String, Any> = { "stmt": "Return", "value": val }
|
||||
let new_body = native_list_append(new_body, return_stmt)
|
||||
return new_body
|
||||
}
|
||||
body
|
||||
}
|
||||
|
||||
fn js_cg_fn(stmt: Map<String, Any>) -> Void {
|
||||
let fn_name: String = stmt["name"]
|
||||
let params = stmt["params"]
|
||||
let body = stmt["body"]
|
||||
let ret_type: String = stmt["ret_type"]
|
||||
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 + ") {")
|
||||
} else {
|
||||
js_emit_line("function " + fn_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_cg_stmts(body_xformed, " ", decl)
|
||||
js_emit_line("}")
|
||||
js_emit_blank()
|
||||
}
|
||||
|
||||
// ── Top-level codegen ─────────────────────────────────────────────────────────
|
||||
|
||||
fn js_is_fndef(stmt: Map<String, Any>) -> Bool {
|
||||
let kind: String = stmt["stmt"]
|
||||
if kind == "FnDef" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn js_is_top_level_decl(stmt: Map<String, Any>) -> Bool {
|
||||
let kind: String = stmt["stmt"]
|
||||
if kind == "TypeDef" { return true }
|
||||
if kind == "EnumDef" { return true }
|
||||
if kind == "Import" { return true }
|
||||
if kind == "CgiBlock" { return true }
|
||||
if kind == "ServiceBlock" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
|
||||
// Reset per-compile state.
|
||||
state_set("__js_int_names", "")
|
||||
state_set("__js_match_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.
|
||||
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;")
|
||||
js_emit_blank()
|
||||
|
||||
// Function definitions
|
||||
let n: Int = native_list_len(stmts)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
if js_is_fndef(stmt) {
|
||||
js_cg_fn(stmt)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Top-level statements (those that are not FnDef and not declarative)
|
||||
// run at module load. If the program defines `fn main`, we additionally
|
||||
// call main() at the end so the C-backend mental model of "fn main is
|
||||
// the entry point" carries over.
|
||||
let has_main = false
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk: String = stmt["stmt"]
|
||||
if str_eq(sk, "FnDef") {
|
||||
let fn_name: String = stmt["name"]
|
||||
if str_eq(fn_name, "main") {
|
||||
let has_main = true
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
let main_decl = native_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
if js_is_fndef(stmt) {
|
||||
// skip
|
||||
} else {
|
||||
if js_is_top_level_decl(stmt) {
|
||||
// skip
|
||||
} else {
|
||||
let main_decl = js_cg_stmt(stmt, "", main_decl)
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
if has_main {
|
||||
js_emit_blank()
|
||||
js_emit_line("main();")
|
||||
}
|
||||
|
||||
// Return empty string — output was streamed via println
|
||||
""
|
||||
}
|
||||
Reference in New Issue
Block a user