restructure: move el compiler content into lang/
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
// channel.el — Go-style channels for El
|
||||
//
|
||||
// Channels are the communication primitive for concurrent El programs.
|
||||
// Threads send values into a channel; other threads receive them.
|
||||
// Channels are typed by convention — all values are Strings.
|
||||
//
|
||||
// Backed by four seed primitives in el_runtime.c:
|
||||
// __channel_new(capacity) -> Int create channel; cap=0 = unbounded
|
||||
// __channel_send(ch, msg) push msg; blocks if bounded and full
|
||||
// __channel_recv(ch) -> String pop msg; blocks until available; "" on close
|
||||
// __channel_try_recv(ch) -> String non-blocking pop; "" if empty
|
||||
// __channel_close(ch) mark closed; wake all blocked recvers
|
||||
//
|
||||
// Usage:
|
||||
// let ch: Int = channel_new(10) // buffered channel, capacity 10
|
||||
// spawn("producer", int_to_str(ch))
|
||||
// let msg: String = channel_recv(ch)
|
||||
|
||||
// ── Core channel API ─────────────────────────────────────────────────────────
|
||||
|
||||
// channel_new — create a channel with the given buffer capacity.
|
||||
//
|
||||
// capacity: 0 = unbounded (never blocks sender)
|
||||
// N = bounded buffer of N messages (sender blocks when full)
|
||||
//
|
||||
// Returns a channel handle (Int) to pass to send/recv/close.
|
||||
fn channel_new(capacity: Int) -> Int {
|
||||
return __channel_new(capacity)
|
||||
}
|
||||
|
||||
// channel_send — send a message into the channel.
|
||||
//
|
||||
// Blocks if the channel is bounded and full.
|
||||
// No-op if the channel is already closed.
|
||||
fn channel_send(ch: Int, msg: String) {
|
||||
__channel_send(ch, msg)
|
||||
}
|
||||
|
||||
// channel_recv — receive the next message from the channel.
|
||||
//
|
||||
// Blocks until a message is available.
|
||||
// Returns "" when the channel is closed and all buffered messages are drained.
|
||||
// The "" sentinel signals end-of-stream to consumers in a loop.
|
||||
fn channel_recv(ch: Int) -> String {
|
||||
return __channel_recv(ch)
|
||||
}
|
||||
|
||||
// channel_try_recv — non-blocking receive.
|
||||
//
|
||||
// Returns the next message if one is available, or "" if the channel is empty.
|
||||
// Does not block. Callers must distinguish "" (empty) from a legitimate ""
|
||||
// message by convention — use a non-empty sentinel in the message protocol.
|
||||
fn channel_try_recv(ch: Int) -> String {
|
||||
return __channel_try_recv(ch)
|
||||
}
|
||||
|
||||
// channel_close — signal that no more messages will be sent.
|
||||
//
|
||||
// After close, channel_recv continues to drain buffered messages then
|
||||
// returns "" on every subsequent call. channel_send on a closed channel
|
||||
// is a no-op (the message is dropped).
|
||||
fn channel_close(ch: Int) {
|
||||
__channel_close(ch)
|
||||
}
|
||||
|
||||
// ── channel_pipeline ─────────────────────────────────────────────────────────
|
||||
|
||||
// channel_pipeline — producer/consumer pipeline with parallel workers.
|
||||
//
|
||||
// Reads messages from in_ch, applies fn_name to each, writes results to out_ch.
|
||||
// Spawns `workers` concurrent worker threads — each drains in_ch independently,
|
||||
// so messages are processed in arrival order within each worker but not globally.
|
||||
//
|
||||
// fn_name must be an El fn with signature (String) -> String.
|
||||
//
|
||||
// Call channel_close(in_ch) to signal EOF. Workers exit when they receive "".
|
||||
// The caller must also close out_ch after all workers finish (via join).
|
||||
//
|
||||
// let in_ch: Int = channel_new(0)
|
||||
// let out_ch: Int = channel_new(0)
|
||||
// channel_pipeline(in_ch, out_ch, "process_item", 4)
|
||||
// channel_send(in_ch, "work-1")
|
||||
// channel_close(in_ch)
|
||||
// let result: String = channel_recv(out_ch)
|
||||
fn channel_pipeline(in_ch: Int, out_ch: Int, fn_name: String, workers: Int) {
|
||||
let i: Int = 0
|
||||
while i < workers {
|
||||
let arg: String = "{\"in_ch\":" + int_to_str(in_ch) +
|
||||
",\"out_ch\":" + int_to_str(out_ch) +
|
||||
",\"fn\":\"" + fn_name + "\"}"
|
||||
let _tid: Int = spawn("_channel_worker", arg)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// _channel_worker — internal worker for channel_pipeline.
|
||||
//
|
||||
// Reads messages from in_ch until it receives "" (closed+empty), applies
|
||||
// fn_name to each, and writes results to out_ch. Runs in its own thread
|
||||
// (spawned by channel_pipeline).
|
||||
fn _channel_worker(arg: String) -> String {
|
||||
let in_ch: Int = str_to_int(json_get(arg, "in_ch"))
|
||||
let out_ch: Int = str_to_int(json_get(arg, "out_ch"))
|
||||
let fn_name: String = json_get(arg, "fn")
|
||||
let running: Bool = true
|
||||
while running {
|
||||
let msg: String = channel_recv(in_ch)
|
||||
if str_eq(msg, "") {
|
||||
let running = false
|
||||
} else {
|
||||
// Spawn fn_name in a child thread so it cannot block the worker loop.
|
||||
let tid: Int = spawn(fn_name, msg)
|
||||
let result: String = join(tid)
|
||||
channel_send(out_ch, result)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── channel_drain ────────────────────────────────────────────────────────────
|
||||
|
||||
// channel_drain — collect all messages from ch into a list.
|
||||
//
|
||||
// Reads until the channel is closed and empty (recv returns "").
|
||||
// Returns a [String] of all messages received.
|
||||
//
|
||||
// Typical usage: close the channel from the producer side, then call
|
||||
// channel_drain from the consumer to collect results.
|
||||
fn channel_drain(ch: Int) -> [String] {
|
||||
let results: [String] = el_list_empty()
|
||||
let running: Bool = true
|
||||
while running {
|
||||
let msg: String = channel_recv(ch)
|
||||
if str_eq(msg, "") {
|
||||
let running = false
|
||||
} else {
|
||||
let results = el_list_append(results, msg)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// ── channel_fan_out ───────────────────────────────────────────────────────────
|
||||
|
||||
// channel_fan_out — send every item in a list into a channel.
|
||||
//
|
||||
// items: [String] — items to send
|
||||
// ch: Int — destination channel
|
||||
//
|
||||
// Sends all items then closes the channel to signal end-of-stream.
|
||||
// Intended for the producer side of a pipeline:
|
||||
//
|
||||
// channel_fan_out(items, in_ch)
|
||||
// let results: [String] = channel_drain(out_ch)
|
||||
fn channel_fan_out(items: [String], ch: Int) {
|
||||
let n: Int = el_list_len(items)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
channel_send(ch, item)
|
||||
let i = i + 1
|
||||
}
|
||||
channel_close(ch)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// runtime/engram.el — El wrapper for the engram graph store
|
||||
//
|
||||
// Thin wrappers over the __engram_* seed primitives defined in el_seed.c.
|
||||
// Each function delegates directly to the corresponding seed — no logic here.
|
||||
// The seed layer owns all storage, indexing, and graph traversal.
|
||||
//
|
||||
// Dependencies: runtime/string.el, runtime/json.el
|
||||
|
||||
// --- Node creation ---
|
||||
|
||||
fn engram_node(content: String, node_type: String, salience: Float) -> String {
|
||||
return __engram_node(content, node_type, salience)
|
||||
}
|
||||
|
||||
fn engram_node_full(content: String, nt: String, sal: Float, imp: Float,
|
||||
source: String, lang: String, ts: Int, tags: String) -> String {
|
||||
return __engram_node_full(content, nt, sal, imp, source, lang, ts, tags)
|
||||
}
|
||||
|
||||
// --- Node retrieval ---
|
||||
|
||||
fn engram_get_node(id: String) -> String {
|
||||
return __engram_get_node(id)
|
||||
}
|
||||
|
||||
fn engram_node_count() -> Int {
|
||||
return __engram_node_count()
|
||||
}
|
||||
|
||||
// --- Node lifecycle ---
|
||||
|
||||
fn engram_strengthen(id: String) -> Bool {
|
||||
return __engram_strengthen(id)
|
||||
}
|
||||
|
||||
fn engram_forget(id: String) -> Bool {
|
||||
return __engram_forget(id)
|
||||
}
|
||||
|
||||
// --- Search and scan ---
|
||||
|
||||
fn engram_search(query: String, limit: Int) -> String {
|
||||
return __engram_search(query, limit)
|
||||
}
|
||||
|
||||
fn engram_scan_nodes(limit: Int, offset: Int) -> String {
|
||||
return __engram_scan_nodes(limit, offset)
|
||||
}
|
||||
|
||||
fn engram_scan_nodes_json(limit: Int, offset: Int) -> String {
|
||||
return __engram_scan_nodes_json(limit, offset)
|
||||
}
|
||||
|
||||
// --- Graph edges ---
|
||||
|
||||
fn engram_connect(from: String, to: String, rel: String, weight: Float) -> Bool {
|
||||
return __engram_connect(from, to, rel, weight)
|
||||
}
|
||||
|
||||
fn engram_edge_between(a: String, b: String) -> String {
|
||||
return __engram_edge_between(a, b)
|
||||
}
|
||||
|
||||
// --- Graph traversal ---
|
||||
|
||||
fn engram_neighbors(id: String) -> String {
|
||||
return __engram_neighbors(id)
|
||||
}
|
||||
|
||||
fn engram_neighbors_filtered(id: String, rel: String, min_w: Float) -> String {
|
||||
return __engram_neighbors_filtered(id, rel, min_w)
|
||||
}
|
||||
|
||||
fn engram_activate(query: String, depth: Int) -> String {
|
||||
return __engram_activate(query, depth)
|
||||
}
|
||||
|
||||
fn engram_activate_json(query: String, limit: Int) -> String {
|
||||
return __engram_activate_json(query, limit)
|
||||
}
|
||||
|
||||
// --- Generation ---
|
||||
|
||||
fn generate(form: String) -> String {
|
||||
return __generate(form)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// runtime/env.el — environment and process
|
||||
// Covers: environment variables, command-line args, process exit, in-process
|
||||
// state store, UUID generation, and list convenience helpers.
|
||||
|
||||
// env — read an environment variable. Returns "" if the variable is not set.
|
||||
fn env(key: String) -> String {
|
||||
return __env_get(key)
|
||||
}
|
||||
|
||||
// args — command-line arguments as a list of strings.
|
||||
// __args_json returns a JSON array (e.g. ["prog","arg1","arg2"]).
|
||||
// The list is built by iterating over the array.
|
||||
fn args() -> [String] {
|
||||
let json: String = __args_json()
|
||||
let n: Int = json_array_len(json)
|
||||
let result: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = json_array_get_string(json, i)
|
||||
let result = el_list_append(result, item)
|
||||
let i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// exit_program — terminate the process with the given exit code.
|
||||
fn exit_program(code: Int) {
|
||||
__exit_program(code)
|
||||
}
|
||||
|
||||
// ── List convenience helpers ───────────────────────────────────────────────
|
||||
|
||||
// get — index into a list. Thin alias for el_list_get used throughout the
|
||||
// El stdlib so call sites read like `get(lst, i)` rather than the verbose form.
|
||||
fn get(lst: [String], i: Int) -> String {
|
||||
return el_list_get(lst, i)
|
||||
}
|
||||
|
||||
// len — length of a list.
|
||||
fn len(lst: [String]) -> Int {
|
||||
return el_list_len(lst)
|
||||
}
|
||||
|
||||
// ── In-process key-value state store ──────────────────────────────────────
|
||||
|
||||
// state_set — store a string value under key.
|
||||
fn state_set(key: String, val: String) {
|
||||
__state_set(key, val)
|
||||
}
|
||||
|
||||
// state_get — retrieve value for key; returns "" if key not present.
|
||||
fn state_get(key: String) -> String {
|
||||
return __state_get(key)
|
||||
}
|
||||
|
||||
// state_del — remove key from the store.
|
||||
fn state_del(key: String) {
|
||||
__state_del(key)
|
||||
}
|
||||
|
||||
// state_keys — all keys currently in the store as a JSON array string.
|
||||
fn state_keys() -> String {
|
||||
return __state_keys()
|
||||
}
|
||||
|
||||
// ── DHARMA runtime helpers ─────────────────────────────────────────────────
|
||||
|
||||
// config — read a configuration value from the environment.
|
||||
// Returns "" if the variable is not set. Alias for env().
|
||||
fn config(key: String) -> String {
|
||||
return __env_get(key)
|
||||
}
|
||||
|
||||
// log_info — write an [INFO] log line to stdout.
|
||||
fn log_info(msg: String) {
|
||||
__println("[INFO] " + msg)
|
||||
}
|
||||
|
||||
// log_warn — write a [WARN] log line to stdout.
|
||||
fn log_warn(msg: String) {
|
||||
__println("[WARN] " + msg)
|
||||
}
|
||||
|
||||
// list_len — return the number of elements in a list. Alias for el_list_len.
|
||||
fn list_len(lst: [String]) -> Int {
|
||||
return el_list_len(lst)
|
||||
}
|
||||
|
||||
// list_get — return the element at index i in a list. Alias for el_list_get.
|
||||
fn list_get(lst: [String], i: Int) -> String {
|
||||
return el_list_get(lst, i)
|
||||
}
|
||||
|
||||
// ── UUID generation ────────────────────────────────────────────────────────
|
||||
|
||||
// uuid_new — generate a new random UUID v4.
|
||||
fn uuid_new() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
// uuid_v4 — alias for uuid_new(); explicit version name for callers that
|
||||
// need to be precise about the UUID variant.
|
||||
fn uuid_v4() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// runtime/exec.el — subprocess execution
|
||||
// All four names resolve to the same seed primitives so callers can use
|
||||
// whichever name matches their mental model of the operation.
|
||||
|
||||
// exec — run a shell command, capture stdout, return as String.
|
||||
// Blocks until the subprocess exits (30-second wall-clock deadline in the
|
||||
// seed layer). Returns "" on any error.
|
||||
fn exec(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
|
||||
// exec_bg — fire-and-forget subprocess. Returns immediately; no stdout.
|
||||
fn exec_bg(cmd: String) {
|
||||
__exec_bg(cmd)
|
||||
}
|
||||
|
||||
// exec_command — alias for exec(); preferred when callers care about side
|
||||
// effects (e.g. invoking a build tool) rather than captured output.
|
||||
fn exec_command(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
|
||||
// exec_capture — alias for exec(); preferred when callers explicitly want
|
||||
// to capture and process stdout.
|
||||
fn exec_capture(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// runtime/fs.el — filesystem operations
|
||||
// Thin El wrappers over seed primitives; no logic beyond what is needed
|
||||
// to present a clean API. The heavy lifting lives in el_runtime.c.
|
||||
|
||||
fn fs_read(path: String) -> String {
|
||||
return __fs_read(path)
|
||||
}
|
||||
|
||||
fn fs_write(path: String, content: String) -> Bool {
|
||||
return __fs_write(path, content)
|
||||
}
|
||||
|
||||
fn fs_exists(path: String) -> Bool {
|
||||
return __fs_exists(path)
|
||||
}
|
||||
|
||||
fn fs_mkdir(path: String) -> Bool {
|
||||
return __fs_mkdir(path)
|
||||
}
|
||||
|
||||
fn fs_write_bytes(path: String, bytes: String, n: Int) -> Bool {
|
||||
return __fs_write_bytes(path, bytes, n)
|
||||
}
|
||||
|
||||
// fs_list — return list of filenames in a directory.
|
||||
// __fs_list_raw returns a newline-separated string (possibly with a trailing
|
||||
// newline); callers that need a clean list should filter empty strings.
|
||||
fn fs_list(path: String) -> [String] {
|
||||
let raw: String = __fs_list_raw(path)
|
||||
return str_split(raw, "\n")
|
||||
}
|
||||
|
||||
// fs_list_json — return a JSON array of filenames in a directory.
|
||||
// Empty strings produced by a trailing newline are stripped before encoding.
|
||||
fn fs_list_json(path: String) -> String {
|
||||
let items: [String] = fs_list(path)
|
||||
let n: Int = el_list_len(items)
|
||||
let clean: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let trimmed: String = str_trim(item)
|
||||
if !str_eq(trimmed, "") {
|
||||
let clean = el_list_append(clean, "\"" + trimmed + "\"")
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return json_build_array(clean)
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
// runtime/http.el — El HTTP client and server wrappers
|
||||
//
|
||||
// Thin El layer over seed primitives. All network I/O is performed by the
|
||||
// seed; this file provides the public API that El programs import.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __http_do(method, url, body, headers_json, timeout_ms) -> String
|
||||
// __http_do_to_file(method, url, body, headers_json, out_path) -> Bool
|
||||
// __http_do_map(method, url, body, headers_map, timeout_ms) -> String
|
||||
// __http_do_map_to_file(method, url, body, headers_map, out_path) -> Bool
|
||||
// __http_serve(port, handler_name)
|
||||
// __http_serve_v2(port, handler_name)
|
||||
// __http_response(status, headers_json, body) -> String
|
||||
// __env_get(key) -> String
|
||||
//
|
||||
// NOTE FOR SEED AGENT: __http_do_map and __http_do_map_to_file must be added
|
||||
// to the seed. They are identical to __http_do / __http_do_to_file except
|
||||
// they accept an ElMap directly for headers instead of a pre-serialised JSON
|
||||
// string. This avoids needing map iteration in El (which has no for-loop or
|
||||
// map iterator primitive). The seed implementation maps to headers_from_map()
|
||||
// in el_runtime.c.
|
||||
//
|
||||
// Other builtins used:
|
||||
// str_eq(a, b) -> Bool
|
||||
// str_to_int(s) -> Int
|
||||
|
||||
// ── Timeout helper ────────────────────────────────────────────────────────────
|
||||
|
||||
// el_http_timeout_ms returns the configured HTTP timeout in milliseconds.
|
||||
// Reads EL_HTTP_TIMEOUT_MS from the environment; defaults to 60000 (60s).
|
||||
// Returns 60000 if the env var is absent, empty, or non-positive.
|
||||
fn el_http_timeout_ms() -> Int {
|
||||
let v: String = __env_get("EL_HTTP_TIMEOUT_MS")
|
||||
if str_eq(v, "") { return 60000 }
|
||||
let n: Int = str_to_int(v)
|
||||
if n <= 0 { return 60000 }
|
||||
return n
|
||||
}
|
||||
|
||||
// ── HTTP client — simple variants ────────────────────────────────────────────
|
||||
|
||||
// http_get performs an HTTP GET request and returns the response body.
|
||||
// On transport failure the seed returns an error JSON fragment.
|
||||
fn http_get(url: String) -> String {
|
||||
return __http_do("GET", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post performs an HTTP POST request with the given body.
|
||||
// No Content-Type header is set; use http_post_json for JSON payloads.
|
||||
fn http_post(url: String, body: String) -> String {
|
||||
return __http_do("POST", url, body, "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_json performs an HTTP POST request with Content-Type:
|
||||
// application/json. body must be a valid JSON string.
|
||||
fn http_post_json(url: String, body: String) -> String {
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_delete performs an HTTP DELETE request and returns the response body.
|
||||
fn http_delete(url: String) -> String {
|
||||
return __http_do("DELETE", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — header map variants ────────────────────────────────────────
|
||||
//
|
||||
// These accept a Map<String, String> of request headers. The seed's
|
||||
// __http_do_map converts the ElMap to a curl_slist internally, matching
|
||||
// the headers_from_map() logic in el_runtime.c.
|
||||
|
||||
// http_get_with_headers performs an HTTP GET with caller-supplied headers.
|
||||
fn http_get_with_headers(url: String, headers: Map<String, String>) -> String {
|
||||
return __http_do_map("GET", url, "", headers, el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_with_headers performs an HTTP POST with caller-supplied headers.
|
||||
fn http_post_with_headers(url: String, body: String, headers: Map<String, String>) -> String {
|
||||
return __http_do_map("POST", url, body, headers, el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_form_auth performs an HTTP POST with
|
||||
// Content-Type: application/x-www-form-urlencoded and an Authorization
|
||||
// header built from auth_header (the caller passes the full header value,
|
||||
// e.g. "Bearer <token>" or "Basic <base64>").
|
||||
//
|
||||
// Mirrors http_post_form_auth in el_runtime.c: two headers are injected,
|
||||
// Content-Type is always set; Authorization is omitted when auth_header is "".
|
||||
fn http_post_form_auth(url: String, form_body: String, auth_header: String) -> String {
|
||||
if str_eq(auth_header, "") {
|
||||
return __http_do("POST", url, form_body, "{\"Content-Type\":\"application/x-www-form-urlencoded\"}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("POST", url, form_body, "{\"Content-Type\":\"application/x-www-form-urlencoded\",\"Authorization\":\"" + auth_header + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — streaming to file ──────────────────────────────────────────
|
||||
//
|
||||
// These route the response body directly to a file via the seed, bypassing
|
||||
// the El string layer. This preserves embedded NUL bytes in binary payloads
|
||||
// (audio, images, etc.) — an El string would truncate at the first NUL.
|
||||
// Returns true on success, false on any transport or I/O error.
|
||||
|
||||
// http_post_to_file performs an HTTP POST and streams the response body to
|
||||
// output_path. Useful for large or binary response payloads.
|
||||
fn http_post_to_file(url: String, body: String, headers: Map<String, String>, output_path: String) -> Bool {
|
||||
return __http_do_map_to_file("POST", url, body, headers, output_path)
|
||||
}
|
||||
|
||||
// http_get_to_file performs an HTTP GET and streams the response body to
|
||||
// output_path. Useful for large or binary response payloads.
|
||||
fn http_get_to_file(url: String, headers: Map<String, String>, output_path: String) -> Bool {
|
||||
return __http_do_map_to_file("GET", url, "", headers, output_path)
|
||||
}
|
||||
|
||||
// ── HTTP server ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// El programs call http_set_handler(name) to register which El function
|
||||
// handles requests, then http_serve(port, name) to start listening.
|
||||
// The seed resolves handler names via dlsym — every El fn compiles to a
|
||||
// global C symbol with the same name, so self-registration works without
|
||||
// any El-level registry.
|
||||
//
|
||||
// v2 widens the handler signature from
|
||||
// (method, path, body) -> String
|
||||
// to
|
||||
// (method, path, headers_map, body) -> String
|
||||
// so handlers can inspect incoming headers. Use http_serve_v2 +
|
||||
// http_set_handler_v2 for v2 handlers.
|
||||
|
||||
// http_set_handler registers name as the active v1 request handler.
|
||||
// The seed resolves the symbol via dlsym at call time; no El-level
|
||||
// registration is needed. This is a no-op at the El layer.
|
||||
fn http_set_handler(name: String) {
|
||||
// no-op: the seed handles handler registration via dlsym
|
||||
}
|
||||
|
||||
// http_serve starts an HTTP/1.1 server on port, dispatching every request
|
||||
// to handler (a v1 handler: fn(method, path, body) -> String).
|
||||
// Blocks forever. Accepts both IPv4 and IPv6 (dual-stack).
|
||||
fn http_serve(port: Int, handler: String) {
|
||||
__http_serve(port, handler)
|
||||
}
|
||||
|
||||
// http_set_handler_v2 registers name as the active v2 request handler.
|
||||
// No-op at the El layer; the seed uses dlsym.
|
||||
fn http_set_handler_v2(name: String) {
|
||||
// no-op: the seed handles handler registration via dlsym
|
||||
}
|
||||
|
||||
// http_serve_v2 starts an HTTP/1.1 server on port, dispatching every
|
||||
// request to handler (a v2 handler: fn(method, path, headers, body) ->
|
||||
// String). Blocks forever. Accepts both IPv4 and IPv6 (dual-stack).
|
||||
fn http_serve_v2(port: Int, handler: String) {
|
||||
__http_serve_v2(port, handler)
|
||||
}
|
||||
|
||||
// ── Response construction ─────────────────────────────────────────────────────
|
||||
|
||||
// http_response builds a structured response envelope that the HTTP server
|
||||
// runtime unpacks into a real HTTP response with the given status code and
|
||||
// headers. status must be 100–599 (defaults to 200 outside that range).
|
||||
// headers_json must be a JSON object literal (e.g. "{}" or
|
||||
// "{\"Content-Type\":\"text/html\"}"); body is the response body string.
|
||||
//
|
||||
// The envelope format is:
|
||||
// {"el_http_response":1,"status":<n>,"headers":<obj>,"body":"<escaped>"}
|
||||
// The runtime detects this prefix and unpacks it; plain string returns from
|
||||
// handlers are still supported and are sent as HTTP 200 with auto-detected
|
||||
// Content-Type.
|
||||
fn http_response(status: Int, headers_json: String, body: String) -> String {
|
||||
return __http_response(status, headers_json, body)
|
||||
}
|
||||
|
||||
// ── HTTP client — PATCH ───────────────────────────────────────────────────────
|
||||
|
||||
// http_patch performs an HTTP PATCH request with Content-Type: application/json.
|
||||
fn http_patch(url: String, body: String) -> String {
|
||||
return __http_do("PATCH", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — Engram variants (optional API key) ──────────────────────────
|
||||
//
|
||||
// These are used by dharma's db.el to talk to Engram nodes.
|
||||
// The key parameter is the X-API-Key header value; pass "" for no auth.
|
||||
|
||||
// http_post_engram performs an HTTP POST with Content-Type: application/json
|
||||
// and an optional X-API-Key header. If key is "" no auth header is added.
|
||||
fn http_post_engram(url: String, key: String, body: String) -> String {
|
||||
if str_eq(key, "") {
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\",\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_get_engram performs an HTTP GET with an optional X-API-Key header.
|
||||
fn http_get_engram(url: String, key: String) -> String {
|
||||
if str_eq(key, "") {
|
||||
return __http_do("GET", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("GET", url, "", "{\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── SSE — Server-Sent Events streaming ───────────────────────────────────────
|
||||
//
|
||||
// Usage pattern for an SSE handler:
|
||||
//
|
||||
// fn my_handler(method: String, path: String, headers: Map<String, String>, body: String) -> String {
|
||||
// let fd: Int = http_conn_fd()
|
||||
// http_sse_open(fd)
|
||||
// http_sse_send(fd, "hello")
|
||||
// http_sse_send(fd, "world")
|
||||
// http_sse_close(fd)
|
||||
// return http_sse_sentinel()
|
||||
// }
|
||||
//
|
||||
// The sentinel return value tells http_serve_v2 NOT to close the connection
|
||||
// automatically — the handler already closed it via http_sse_close.
|
||||
|
||||
// http_conn_fd returns the raw file descriptor for the current HTTP connection.
|
||||
// Only valid inside an http_serve_v2 handler, before the handler returns.
|
||||
// Use with http_sse_open / http_sse_send / http_sse_close for streaming.
|
||||
fn http_conn_fd() -> Int {
|
||||
return __http_conn_fd()
|
||||
}
|
||||
|
||||
// http_sse_open sends SSE response headers on the current connection,
|
||||
// keeping it open for streaming. Call once at the start of an SSE handler.
|
||||
// Returns true on success.
|
||||
fn http_sse_open(fd: Int) -> Bool {
|
||||
return __http_sse_open(fd)
|
||||
}
|
||||
|
||||
// http_sse_send writes one SSE event to the connection.
|
||||
// data should not contain newlines (they are added automatically).
|
||||
// Returns true if the write succeeded (client still connected).
|
||||
fn http_sse_send(fd: Int, data: String) -> Bool {
|
||||
return __http_sse_send(fd, data)
|
||||
}
|
||||
|
||||
// http_sse_close closes the SSE connection.
|
||||
fn http_sse_close(fd: Int) {
|
||||
__http_sse_close(fd)
|
||||
return
|
||||
}
|
||||
|
||||
// http_sse_sentinel is the return value an SSE handler must return
|
||||
// to tell the HTTP server NOT to close the connection automatically.
|
||||
// The handler takes ownership of the fd and closes it via http_sse_close.
|
||||
fn http_sse_sentinel() -> String {
|
||||
return "__sse__"
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// runtime/json.el — El JSON operations
|
||||
//
|
||||
// Thin El wrappers over seed JSON primitives, plus pure-El builders and
|
||||
// helpers. Each function here corresponds to (and replaces) a C function
|
||||
// from el-compiler/runtime/legacy/el_runtime.c (lines 2692–3333).
|
||||
//
|
||||
// Seed primitives consumed by this module:
|
||||
// __json_get(json, key) -> String (value as string)
|
||||
// __json_get_raw(json, key) -> String (raw JSON token)
|
||||
// __json_parse_map(s) -> Map<String, Any>
|
||||
// __json_stringify_val(v) -> String
|
||||
// __json_array_len(arr) -> Int
|
||||
// __json_array_get(arr, i) -> String (element as JSON fragment)
|
||||
// __json_array_get_string(arr, i) -> String (element as string value)
|
||||
// __json_set(json, key, value) -> String (JSON mutation)
|
||||
// __str_to_int(s) -> Int
|
||||
// __str_to_float(s) -> Float
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — thin wrappers that delegate directly to seed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_get — extract a value from a JSON object as a string.
|
||||
// Supports dot-path traversal ("a.b.c") and array indices ("items.0.name").
|
||||
fn json_get(json: String, key: String) -> String {
|
||||
return __json_get(json, key)
|
||||
}
|
||||
|
||||
// json_get_raw — extract a raw JSON token (the un-decoded fragment) for a key.
|
||||
// Useful when the caller wants to pass a sub-object to another JSON function.
|
||||
fn json_get_raw(json: String, key: String) -> String {
|
||||
return __json_get_raw(json, key)
|
||||
}
|
||||
|
||||
// json_parse — parse a JSON string into a Map<String, Any>.
|
||||
// Arrays become ElList; objects become ElMap; scalars are typed values.
|
||||
fn json_parse(s: String) -> Map<String, Any> {
|
||||
return __json_parse_map(s)
|
||||
}
|
||||
|
||||
// json_stringify — serialize an El value (ElMap, ElList, String, Int) to JSON.
|
||||
fn json_stringify(v: Any) -> String {
|
||||
return __json_stringify_val(v)
|
||||
}
|
||||
|
||||
// json_array_len — return the number of elements in a JSON array string.
|
||||
fn json_array_len(arr: String) -> Int {
|
||||
return __json_array_len(arr)
|
||||
}
|
||||
|
||||
// json_array_get — return the i-th element of a JSON array as a JSON fragment.
|
||||
// Nested objects and arrays are returned verbatim. Out-of-range -> "".
|
||||
fn json_array_get(arr: String, i: Int) -> String {
|
||||
return __json_array_get(arr, i)
|
||||
}
|
||||
|
||||
// json_array_get_string — return the i-th element of a JSON array as a plain
|
||||
// string value (quotes and escape sequences removed). Non-string elements
|
||||
// and out-of-range indices yield "".
|
||||
fn json_array_get_string(arr: String, i: Int) -> String {
|
||||
return __json_array_get_string(arr, i)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Typed extractors — delegate to seed then convert
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_get_string — extract a string value for a key.
|
||||
// Equivalent to json_get but named explicitly for readability.
|
||||
fn json_get_string(json: String, key: String) -> String {
|
||||
return __json_get(json, key)
|
||||
}
|
||||
|
||||
// json_get_int — extract an integer value for a key.
|
||||
fn json_get_int(json: String, key: String) -> Int {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_to_int(s)
|
||||
}
|
||||
|
||||
// json_get_float — extract a floating-point value for a key.
|
||||
fn json_get_float(json: String, key: String) -> Float {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_to_float(s)
|
||||
}
|
||||
|
||||
// json_get_bool — extract a boolean value for a key.
|
||||
// Returns true only when the raw JSON token is the literal "true".
|
||||
fn json_get_bool(json: String, key: String) -> Bool {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_eq(s, "true")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_set — set or insert a key/value pair in a JSON object string.
|
||||
// If the key already exists its value is replaced in-place; otherwise the
|
||||
// pair is appended before the closing brace. The value must already be a
|
||||
// valid JSON-encoded string (e.g. a quoted string, number, or sub-object).
|
||||
fn json_set(json: String, key: String, value: String) -> String {
|
||||
return __json_set(json, key, value)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure-El builders — no seed call required
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_build_object — build a JSON object from alternating key/value strings.
|
||||
//
|
||||
// keys_and_values must contain an even number of elements laid out as:
|
||||
// [key0, val0, key1, val1, ...]
|
||||
//
|
||||
// Both keys and values are assumed to be plain strings that will be
|
||||
// double-quoted and JSON-escaped by this function. Pass a pre-encoded
|
||||
// number or sub-object as the value if you need non-string JSON types.
|
||||
//
|
||||
// Example:
|
||||
// json_build_object(["name", "alice", "role", "admin"])
|
||||
// -> {"name":"alice","role":"admin"}
|
||||
fn json_build_object(keys_and_values: [String]) -> String {
|
||||
let n: Int = el_list_len(keys_and_values)
|
||||
let result: String = "{"
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let key: String = el_list_get(keys_and_values, i)
|
||||
let val: String = el_list_get(keys_and_values, i + 1)
|
||||
let sep: String = if i == 0 { "" } else { "," }
|
||||
let escaped_key: String = json_escape_string(key)
|
||||
let escaped_val: String = json_escape_string(val)
|
||||
let result = result + sep + "\"" + escaped_key + "\":\"" + escaped_val + "\""
|
||||
let i = i + 2
|
||||
}
|
||||
return result + "}"
|
||||
}
|
||||
|
||||
// json_build_array — build a JSON array from a list of already-JSON-encoded
|
||||
// strings.
|
||||
//
|
||||
// Each element in items must be a valid JSON fragment (quoted string, number,
|
||||
// object, array, or literal). The function joins them with commas and wraps
|
||||
// the result in brackets.
|
||||
//
|
||||
// Example:
|
||||
// json_build_array(["\"alice\"", "\"bob\""])
|
||||
// -> ["alice","bob"]
|
||||
fn json_build_array(items: [String]) -> String {
|
||||
let n: Int = el_list_len(items)
|
||||
let result: String = "["
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let sep: String = if i == 0 { "" } else { "," }
|
||||
let result = result + sep + item
|
||||
let i = i + 1
|
||||
}
|
||||
return result + "]"
|
||||
}
|
||||
|
||||
// json_array_push — append a pre-encoded JSON element to a JSON array string.
|
||||
// elem must be a valid JSON fragment (e.g. "\"foo\"" or "42").
|
||||
// Returns a new JSON array string with elem appended.
|
||||
// Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]"
|
||||
fn json_array_push(arr: String, elem: String) -> String {
|
||||
let n: Int = json_array_len(arr)
|
||||
if n == 0 {
|
||||
return "[" + elem + "]"
|
||||
}
|
||||
// arr ends with ']'; insert before it
|
||||
let inner_end: Int = str_last_index_of(arr, "]")
|
||||
if inner_end < 0 {
|
||||
return "[" + elem + "]"
|
||||
}
|
||||
let prefix: String = str_slice(arr, 0, inner_end)
|
||||
return prefix + "," + elem + "]"
|
||||
}
|
||||
|
||||
// json_escape_string — escape a raw string so it can be safely embedded as a
|
||||
// JSON string value.
|
||||
//
|
||||
// Characters escaped: backslash, double-quote, newline, carriage return, tab.
|
||||
// The returned value does NOT include surrounding double-quotes; wrap it in
|
||||
// quotes if you need a complete JSON string literal.
|
||||
fn json_escape_string(s: String) -> String {
|
||||
let s1: String = str_replace(s, "\\", "\\\\")
|
||||
let s2: String = str_replace(s1, "\"", "\\\"")
|
||||
let s3: String = str_replace(s2, "\n", "\\n")
|
||||
let s4: String = str_replace(s3, "\r", "\\r")
|
||||
let s5: String = str_replace(s4, "\t", "\\t")
|
||||
return s5
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DHARMA byte decoding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// bytes_to_str — decode a JSON array of integer byte values back to a string.
|
||||
// "[104,105]" -> "hi"
|
||||
// Inverse of str_to_bytes (defined in string.el). Defined here because it
|
||||
// depends on json_array_len and json_array_get_string which live in this file.
|
||||
fn bytes_to_str(arr: String) -> String {
|
||||
let n: Int = json_array_len(arr)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let elem: String = json_array_get_string(arr, i)
|
||||
let b: Int = __str_to_int(elem)
|
||||
out = __str_set_char(out, i, b)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// runtime/manifest.el — El runtime module manifest
|
||||
//
|
||||
// Load order for runtime compilation. Each module may depend on modules
|
||||
// listed before it. The build system concatenates these in this order,
|
||||
// then compiles the combined source.
|
||||
//
|
||||
// Modules:
|
||||
// 1. runtime/string.el — string operations (no dependencies)
|
||||
// 2. runtime/math.el — numeric/float operations (no dependencies)
|
||||
// 3. runtime/state.el — in-process key-value (no dependencies)
|
||||
// 4. runtime/env.el — environment, process, args, uuid
|
||||
// 5. runtime/fs.el — filesystem operations (depends: string)
|
||||
// 6. runtime/exec.el — subprocess execution (depends: string)
|
||||
// 7. runtime/time.el — time, date, calendar (depends: string, math)
|
||||
// 8. runtime/json.el — JSON operations (depends: string)
|
||||
// 9. runtime/http.el — HTTP client+server (depends: string, json)
|
||||
// 10. runtime/engram.el — graph store (depends: string, json)
|
||||
// 11. runtime/thread.el — threading, parallel_map (depends: all above)
|
||||
// 12. runtime/collections.el — list/map higher-level ops (depends: string)
|
||||
//
|
||||
// Build command (from el/ root):
|
||||
// cat runtime/string.el runtime/math.el runtime/state.el runtime/env.el \
|
||||
// runtime/fs.el runtime/exec.el runtime/time.el runtime/json.el \
|
||||
// runtime/http.el runtime/engram.el runtime/thread.el \
|
||||
// runtime/collections.el \
|
||||
// <user-program.el> > combined.el
|
||||
// ./dist/platform/elc combined.el > output.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o output output.c el-compiler/runtime/el_seed.c
|
||||
|
||||
// This file itself is not compiled — it is documentation only.
|
||||
fn runtime_version() -> String {
|
||||
return "2.0.0-el-native"
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// runtime/math.el — Float math, integer utilities, and numeric conversions.
|
||||
//
|
||||
// Implements the math/float surface from el-compiler/runtime/legacy/el_runtime.c
|
||||
// (lines 303–305 for el_abs/max/min, lines 4725–4771 for float/format ops)
|
||||
// in pure El, using seed primitives.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __sqrt_f(f: Float) -> Float
|
||||
// __log_f(f: Float) -> Float
|
||||
// __ln_f(f: Float) -> Float
|
||||
// __sin_f(f: Float) -> Float
|
||||
// __cos_f(f: Float) -> Float
|
||||
// __pi_f() -> Float
|
||||
// __float_to_str(f: Float) -> String
|
||||
// __str_to_float(s: String) -> Float
|
||||
// __int_to_str(n: Int) -> String
|
||||
// __str_to_int(s: String) -> Int
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Integer math — el_abs, el_max, el_min.
|
||||
//
|
||||
// Matches legacy el_abs, el_max, el_min (lines 303–305).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// el_abs — absolute value of an integer.
|
||||
fn el_abs(n: Int) -> Int {
|
||||
if n < 0 { return -n }
|
||||
return n
|
||||
}
|
||||
|
||||
// el_max — larger of two integers.
|
||||
fn el_max(a: Int, b: Int) -> Int {
|
||||
if a > b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// el_min — smaller of two integers.
|
||||
fn el_min(a: Int, b: Int) -> Int {
|
||||
if a < b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Float math — thin wrappers over seed primitives.
|
||||
//
|
||||
// Matches legacy math_sqrt, math_log, math_ln, math_sin, math_cos, math_pi
|
||||
// (lines 4766–4771).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// math_sqrt — square root.
|
||||
fn math_sqrt(f: Float) -> Float {
|
||||
return __sqrt_f(f)
|
||||
}
|
||||
|
||||
// math_log — base-10 logarithm.
|
||||
fn math_log(f: Float) -> Float {
|
||||
return __log_f(f)
|
||||
}
|
||||
|
||||
// math_ln — natural logarithm.
|
||||
fn math_ln(f: Float) -> Float {
|
||||
return __ln_f(f)
|
||||
}
|
||||
|
||||
// math_sin — sine (radians).
|
||||
fn math_sin(f: Float) -> Float {
|
||||
return __sin_f(f)
|
||||
}
|
||||
|
||||
// math_cos — cosine (radians).
|
||||
fn math_cos(f: Float) -> Float {
|
||||
return __cos_f(f)
|
||||
}
|
||||
|
||||
// math_pi — the constant π.
|
||||
fn math_pi() -> Float {
|
||||
return __pi_f()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Float conversions — float_to_str, int_to_float, float_to_int, str_to_float.
|
||||
//
|
||||
// Matches legacy float_to_str, int_to_float, float_to_int, str_to_float
|
||||
// (lines 4725–4762).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// float_to_str — format a float using %g (shortest exact representation).
|
||||
// Matches legacy float_to_str() → snprintf "%g".
|
||||
fn float_to_str(f: Float) -> String {
|
||||
return __float_to_str(f)
|
||||
}
|
||||
|
||||
// int_to_float — convert an integer to a float.
|
||||
// Matches legacy int_to_float() → (double)(int64_t)n.
|
||||
fn int_to_float(n: Int) -> Float {
|
||||
return __int_to_float(n)
|
||||
}
|
||||
|
||||
// float_to_int — truncate a float to an integer (toward zero).
|
||||
// Matches legacy float_to_int() → (int64_t)el_to_float(f).
|
||||
fn float_to_int(f: Float) -> Int {
|
||||
return __float_to_int(f)
|
||||
}
|
||||
|
||||
// str_to_float — parse a float from a string. Returns 0.0 on failure.
|
||||
// Matches legacy str_to_float() → strtod(str, NULL).
|
||||
fn str_to_float(s: String) -> Float {
|
||||
return __str_to_float(s)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// format_float — format a float to a fixed number of decimal places.
|
||||
//
|
||||
// decimals is clamped to [0, 30]. Matches legacy format_float() → snprintf "%.*f".
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn format_float(f: Float, decimals: Int) -> String {
|
||||
let d: Int = decimals
|
||||
if d < 0 { d = 0 }
|
||||
if d > 30 { d = 30 }
|
||||
// Delegate to seed; the seed exposes __format_float(f, d) -> String.
|
||||
// This matches snprintf(buf, 128, "%.*f", d, v) in the legacy runtime.
|
||||
return __format_float(f, d)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// decimal_round — round a float to d decimal places (half-away-from-zero).
|
||||
//
|
||||
// Matches legacy decimal_round():
|
||||
// mul = pow(10, d)
|
||||
// r = (v >= 0 ? floor(v*mul + 0.5) : -floor(-v*mul + 0.5)) / mul
|
||||
//
|
||||
// We implement pow(10, d) via a loop (d <= 15, so at most 15 multiplications).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// _pow10 — 10^n as a Float for n in [0, 15].
|
||||
fn _pow10(n: Int) -> Float {
|
||||
let result: Float = 1.0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
result = result * 10.0
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// _floor_f — floor of a float: largest integer <= f.
|
||||
// Uses __float_to_int (truncation) with correction for negative non-integers.
|
||||
fn _floor_f(f: Float) -> Float {
|
||||
let t: Int = __float_to_int(f)
|
||||
let tf: Float = __int_to_float(t)
|
||||
// if f was negative and not already an integer, subtract 1
|
||||
if f < 0.0 {
|
||||
if tf > f {
|
||||
return tf - 1.0
|
||||
}
|
||||
}
|
||||
return tf
|
||||
}
|
||||
|
||||
fn decimal_round(f: Float, decimals: Int) -> Float {
|
||||
let d: Int = decimals
|
||||
if d < 0 { d = 0 }
|
||||
if d > 15 { d = 15 }
|
||||
let mul: Float = _pow10(d)
|
||||
if f >= 0.0 {
|
||||
return _floor_f(f * mul + 0.5) / mul
|
||||
}
|
||||
return 0.0 - _floor_f((0.0 - f) * mul + 0.5) / mul
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// runtime/state.el — In-process key/value store.
|
||||
//
|
||||
// Thin El wrappers over the __state_* seed primitives. The backing store is
|
||||
// a process-wide hash map maintained by the El runtime (formerly el_runtime.c
|
||||
// lines 4632–4721: state_set, state_get, state_del, state_keys).
|
||||
//
|
||||
// Keys and values are Strings. Values are persistent across request boundaries
|
||||
// within the same process instance (they survive individual request lifetimes).
|
||||
// Concurrent access is serialized by the runtime; these wrappers are lock-free
|
||||
// from El's perspective.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __state_set(key: String, val: String)
|
||||
// __state_get(key: String) -> String
|
||||
// __state_del(key: String)
|
||||
// __state_keys() -> String (JSON array of key strings)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — set / get / del / keys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// state_set — store val under key. Overwrites any existing value.
|
||||
fn state_set(key: String, val: String) {
|
||||
__state_set(key, val)
|
||||
}
|
||||
|
||||
// state_get — retrieve the value for key. Returns "" if key is absent.
|
||||
fn state_get(key: String) -> String {
|
||||
return __state_get(key)
|
||||
}
|
||||
|
||||
// state_del — remove key from the store. No-op if key does not exist.
|
||||
fn state_del(key: String) {
|
||||
__state_del(key)
|
||||
}
|
||||
|
||||
// state_keys — return a JSON array string of all current keys.
|
||||
// e.g. ["foo","bar","baz"]
|
||||
// Matches legacy state_keys() which returns an ElList (here serialized as JSON).
|
||||
fn state_keys() -> String {
|
||||
return __state_keys()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Convenience helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// state_has — true if key is present (value is non-empty string).
|
||||
// Note: a key set to "" is indistinguishable from absent via state_get alone.
|
||||
fn state_has(key: String) -> Bool {
|
||||
let v: String = state_get(key)
|
||||
if str_eq(v, "") { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// state_get_or — return val for key, or default_val if key is absent.
|
||||
fn state_get_or(key: String, default_val: String) -> String {
|
||||
let v: String = state_get(key)
|
||||
if str_eq(v, "") { return default_val }
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// stdlib.el — El standard library master import file.
|
||||
//
|
||||
// Import this single file to get the full El runtime in the correct
|
||||
// dependency order. El programs can do:
|
||||
//
|
||||
// import "../foundation/el/runtime/stdlib.el"
|
||||
//
|
||||
// or, if El is installed via tools/install.sh:
|
||||
//
|
||||
// import "/usr/local/el/runtime/stdlib.el"
|
||||
//
|
||||
// Note: test.el is intentionally NOT included here — it is dev-only
|
||||
// and should be imported explicitly in test files only.
|
||||
//
|
||||
// Dependency order (each module may depend on earlier ones):
|
||||
// string — no deps
|
||||
// math — no deps
|
||||
// time — no deps
|
||||
// env — no deps
|
||||
// fs — no deps
|
||||
// exec — no deps
|
||||
// json — depends on string
|
||||
// http — depends on string, json
|
||||
// state — no deps
|
||||
// thread — depends on exec
|
||||
// channel — depends on thread, state
|
||||
// engram — depends on http, json, string
|
||||
// manifest — depends on fs, json, string
|
||||
|
||||
import "string.el"
|
||||
import "math.el"
|
||||
import "time.el"
|
||||
import "env.el"
|
||||
import "fs.el"
|
||||
import "exec.el"
|
||||
import "json.el"
|
||||
import "http.el"
|
||||
import "state.el"
|
||||
import "thread.el"
|
||||
import "channel.el"
|
||||
import "engram.el"
|
||||
import "manifest.el"
|
||||
@@ -0,0 +1,907 @@
|
||||
// runtime/string.el — String operations implemented in El.
|
||||
//
|
||||
// All functions delegate character-level work to the seed primitives declared
|
||||
// in el_seed.c. No C is written here; this is pure El source that compiles
|
||||
// to C via the normal El pipeline.
|
||||
//
|
||||
// Seed primitives used (provided by el_seed.c):
|
||||
// __str_len(s) -> Int
|
||||
// __str_char_at(s, i) -> Int (char code at byte index i)
|
||||
// __str_alloc(n) -> String (n-byte zero-filled mutable buffer)
|
||||
// __str_set_char(s, i, c) -> String (mutate s[i]=c, return s)
|
||||
// __str_cmp(a, b) -> Int (strcmp)
|
||||
// __str_ncmp(a, b, n) -> Int (strncmp)
|
||||
// __str_concat_raw(a, b) -> String
|
||||
// __str_slice_raw(s, lo, hi) -> String (substring copy [lo, hi))
|
||||
// __int_to_str(n) -> String
|
||||
// __str_to_int(s) -> Int
|
||||
// __float_to_str(f) -> String
|
||||
// __str_to_float(s) -> Float
|
||||
// __println(s)
|
||||
// __print(s)
|
||||
// __readline() -> String
|
||||
// __url_encode(s) -> String
|
||||
// __url_decode(s) -> String
|
||||
|
||||
// ── I/O ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn println(s: String) -> Void {
|
||||
__println(s)
|
||||
}
|
||||
|
||||
fn print(s: String) -> Void {
|
||||
__print(s)
|
||||
}
|
||||
|
||||
fn readline() -> String {
|
||||
return __readline()
|
||||
}
|
||||
|
||||
// ── Type conversions ──────────────────────────────────────────────────────────
|
||||
|
||||
fn int_to_str(n: Int) -> String {
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
fn str_to_int(s: String) -> Int {
|
||||
return __str_to_int(s)
|
||||
}
|
||||
|
||||
fn float_to_str(f: Float) -> String {
|
||||
return __float_to_str(f)
|
||||
}
|
||||
|
||||
fn str_to_float(s: String) -> Float {
|
||||
return __str_to_float(s)
|
||||
}
|
||||
|
||||
fn bool_to_str(b: Bool) -> String {
|
||||
if b { return "true" }
|
||||
return "false"
|
||||
}
|
||||
|
||||
// ── URL encoding ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn url_encode(s: String) -> String {
|
||||
return __url_encode(s)
|
||||
}
|
||||
|
||||
fn url_decode(s: String) -> String {
|
||||
return __url_decode(s)
|
||||
}
|
||||
|
||||
// ── Math ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn el_abs(n: Int) -> Int {
|
||||
if n < 0 { return 0 - n }
|
||||
return n
|
||||
}
|
||||
|
||||
fn el_max(a: Int, b: Int) -> Int {
|
||||
if a > b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
fn el_min(a: Int, b: Int) -> Int {
|
||||
if a < b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// ── Core string primitives ────────────────────────────────────────────────────
|
||||
|
||||
fn str_len(s: String) -> Int {
|
||||
return __str_len(s)
|
||||
}
|
||||
|
||||
fn str_eq(a: String, b: String) -> Bool {
|
||||
return __str_cmp(a, b) == 0
|
||||
}
|
||||
|
||||
fn str_concat(a: String, b: String) -> String {
|
||||
return __str_concat_raw(a, b)
|
||||
}
|
||||
|
||||
fn str_slice(s: String, start: Int, end: Int) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let lo: Int = start
|
||||
if lo < 0 { lo = 0 }
|
||||
if lo > slen { lo = slen }
|
||||
let hi: Int = end
|
||||
if hi < lo { hi = lo }
|
||||
if hi > slen { hi = slen }
|
||||
return __str_slice_raw(s, lo, hi)
|
||||
}
|
||||
|
||||
// ── Whitespace helpers (internal) ─────────────────────────────────────────────
|
||||
//
|
||||
// _is_ws: returns true for ASCII whitespace (space, tab, \n, \r, \f, \v).
|
||||
|
||||
fn _is_ws(c: Int) -> Bool {
|
||||
if c == 32 { return true } // space
|
||||
if c == 9 { return true } // tab
|
||||
if c == 10 { return true } // \n
|
||||
if c == 13 { return true } // \r
|
||||
if c == 12 { return true } // \f
|
||||
if c == 11 { return true } // \v
|
||||
return false
|
||||
}
|
||||
|
||||
// Scan forward from index 0; return index of first byte not in whitespace,
|
||||
// or n if the entire string is whitespace.
|
||||
fn _find_first_non_ws(s: String, n: Int) -> Int {
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
if !_is_ws(__str_char_at(s, i)) { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Scan backward from index n-1; return index of last non-whitespace byte,
|
||||
// or -1 if the entire string is whitespace.
|
||||
fn _find_last_non_ws(s: String, n: Int) -> Int {
|
||||
let i: Int = n - 1
|
||||
while i >= 0 {
|
||||
if !_is_ws(__str_char_at(s, i)) { return i }
|
||||
i = i - 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Comparison and search ─────────────────────────────────────────────────────
|
||||
|
||||
fn str_starts_with(s: String, prefix: String) -> Bool {
|
||||
let plen: Int = __str_len(prefix)
|
||||
let slen: Int = __str_len(s)
|
||||
if plen > slen { return false }
|
||||
return __str_ncmp(s, prefix, plen) == 0
|
||||
}
|
||||
|
||||
fn str_ends_with(s: String, suffix: String) -> Bool {
|
||||
let slen: Int = __str_len(s)
|
||||
let suflen: Int = __str_len(suffix)
|
||||
if suflen > slen { return false }
|
||||
let tail: String = __str_slice_raw(s, slen - suflen, slen)
|
||||
return __str_cmp(tail, suffix) == 0
|
||||
}
|
||||
|
||||
fn str_contains(s: String, sub: String) -> Bool {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return true }
|
||||
if sublen > slen { return false }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 { return true }
|
||||
i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn str_index_of(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return 0 }
|
||||
if sublen > slen { return -1 }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fn str_last_index_of(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return slen }
|
||||
if sublen > slen { return -1 }
|
||||
let last: Int = -1
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
last = i
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
fn str_index_of_all(s: String, sub: String) -> [Int] {
|
||||
let result: [Int] = el_list_empty()
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return result }
|
||||
if sublen > slen { return result }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
result = el_list_append(result, i)
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Return the byte index of the first character in s that appears in any_of,
|
||||
// or -1 if none found.
|
||||
fn str_find_chars(s: String, any_of: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let alen: Int = __str_len(any_of)
|
||||
if alen == 0 { return -1 }
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let j: Int = 0
|
||||
while j < alen {
|
||||
if c == __str_char_at(any_of, j) { return i }
|
||||
j = j + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Character access ──────────────────────────────────────────────────────────
|
||||
|
||||
// Return a one-character string at byte index i, or "" if out of range.
|
||||
fn str_char_at(s: String, i: Int) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if i < 0 { return "" }
|
||||
if i >= slen { return "" }
|
||||
return __str_slice_raw(s, i, i + 1)
|
||||
}
|
||||
|
||||
// Return the char code (byte value) at byte index i, or 0 if out of range.
|
||||
fn str_char_code(s: String, i: Int) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
if i < 0 { return 0 }
|
||||
if i >= slen { return 0 }
|
||||
return __str_char_at(s, i)
|
||||
}
|
||||
|
||||
// ── Case conversion ───────────────────────────────────────────────────────────
|
||||
|
||||
fn str_to_upper(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// a-z (97-122) -> A-Z (65-90): subtract 32
|
||||
if c >= 97 {
|
||||
if c <= 122 { c = c - 32 }
|
||||
}
|
||||
out = __str_set_char(out, i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn str_to_lower(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// A-Z (65-90) -> a-z (97-122): add 32
|
||||
if c >= 65 {
|
||||
if c <= 90 { c = c + 32 }
|
||||
}
|
||||
out = __str_set_char(out, i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Aliases used in existing El codebases.
|
||||
fn str_lower(s: String) -> String {
|
||||
return str_to_lower(s)
|
||||
}
|
||||
|
||||
fn str_upper(s: String) -> String {
|
||||
return str_to_upper(s)
|
||||
}
|
||||
|
||||
// ── Whitespace trimming ───────────────────────────────────────────────────────
|
||||
|
||||
fn str_trim(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let lo: Int = _find_first_non_ws(s, n)
|
||||
if lo == n { return "" }
|
||||
let hi: Int = _find_last_non_ws(s, n)
|
||||
return __str_slice_raw(s, lo, hi + 1)
|
||||
}
|
||||
|
||||
fn str_lstrip(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let lo: Int = _find_first_non_ws(s, n)
|
||||
if lo == n { return "" }
|
||||
return __str_slice_raw(s, lo, n)
|
||||
}
|
||||
|
||||
fn str_rstrip(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let hi: Int = _find_last_non_ws(s, n)
|
||||
if hi < 0 { return "" }
|
||||
return __str_slice_raw(s, 0, hi + 1)
|
||||
}
|
||||
|
||||
// ── Replacement ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn str_replace(s: String, from: String, to: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let flen: Int = __str_len(from)
|
||||
if flen == 0 { return s }
|
||||
if slen == 0 { return s }
|
||||
// Scan s left-to-right; emit `to` on each match, otherwise emit one byte.
|
||||
let result: String = ""
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
// Try to match `from` at position i
|
||||
if i + flen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + flen)
|
||||
if __str_cmp(window, from) == 0 {
|
||||
result = __str_concat_raw(result, to)
|
||||
i = i + flen
|
||||
} else {
|
||||
let ch: String = __str_slice_raw(s, i, i + 1)
|
||||
result = __str_concat_raw(result, ch)
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
// Not enough bytes left for a match — emit remainder and stop.
|
||||
let tail: String = __str_slice_raw(s, i, slen)
|
||||
result = __str_concat_raw(result, tail)
|
||||
i = slen
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── Repetition and reversal ───────────────────────────────────────────────────
|
||||
|
||||
fn str_repeat(s: String, n: Int) -> String {
|
||||
if n <= 0 { return "" }
|
||||
let slen: Int = __str_len(s)
|
||||
if slen == 0 { return "" }
|
||||
let result: String = ""
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
result = __str_concat_raw(result, s)
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Byte-reverse (correct for ASCII; for multi-byte UTF-8 codepoints this
|
||||
// reverses bytes within a codepoint, which is intentional at this tier —
|
||||
// Phase 2 will add grapheme-aware reversal).
|
||||
fn str_reverse(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
out = __str_set_char(out, n - 1 - i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ── Prefix/suffix stripping ───────────────────────────────────────────────────
|
||||
|
||||
fn str_strip_prefix(s: String, prefix: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let plen: Int = __str_len(prefix)
|
||||
if plen == 0 { return s }
|
||||
if plen > slen { return s }
|
||||
if __str_ncmp(s, prefix, plen) == 0 {
|
||||
return __str_slice_raw(s, plen, slen)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
fn str_strip_suffix(s: String, suffix: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let suflen: Int = __str_len(suffix)
|
||||
if suflen == 0 { return s }
|
||||
if suflen > slen { return s }
|
||||
let tail: String = __str_slice_raw(s, slen - suflen, slen)
|
||||
if __str_cmp(tail, suffix) == 0 {
|
||||
return __str_slice_raw(s, 0, slen - suflen)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Strip leading and trailing bytes whose char code appears in `chars`.
|
||||
fn str_strip_chars(s: String, chars: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let clen: Int = __str_len(chars)
|
||||
if slen == 0 { return "" }
|
||||
if clen == 0 { return s }
|
||||
let lo: Int = _find_first_not_in_charset(s, chars, slen, clen)
|
||||
if lo == slen { return "" }
|
||||
let hi: Int = _find_last_not_in_charset(s, chars, slen, clen)
|
||||
return __str_slice_raw(s, lo, hi + 1)
|
||||
}
|
||||
|
||||
// Internal: true if char code `c` is present in the charset string.
|
||||
fn _char_in_set(c: Int, chars: String, clen: Int) -> Bool {
|
||||
let j: Int = 0
|
||||
while j < clen {
|
||||
if c == __str_char_at(chars, j) { return true }
|
||||
j = j + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn _find_first_not_in_charset(s: String, chars: String, slen: Int, clen: Int) -> Int {
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if !_char_in_set(__str_char_at(s, i), chars, clen) { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return slen
|
||||
}
|
||||
|
||||
fn _find_last_not_in_charset(s: String, chars: String, slen: Int, clen: Int) -> Int {
|
||||
let i: Int = slen - 1
|
||||
while i >= 0 {
|
||||
if !_char_in_set(__str_char_at(s, i), chars, clen) { return i }
|
||||
i = i - 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Padding ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// Pad s on the left to `width` total chars, repeating `pad` cyclically.
|
||||
fn str_pad_left(s: String, width: Int, pad: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if slen >= width { return s }
|
||||
let plen: Int = __str_len(pad)
|
||||
if plen == 0 { return s }
|
||||
let need: Int = width - slen
|
||||
let prefix: String = ""
|
||||
let i: Int = 0
|
||||
while i < need {
|
||||
// Select pad character at position (i mod plen)
|
||||
let pad_idx: Int = i - (i / plen) * plen
|
||||
let pc: String = __str_slice_raw(pad, pad_idx, pad_idx + 1)
|
||||
prefix = __str_concat_raw(prefix, pc)
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(prefix, s)
|
||||
}
|
||||
|
||||
// Pad s on the right to `width` total chars, repeating `pad` cyclically.
|
||||
fn str_pad_right(s: String, width: Int, pad: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if slen >= width { return s }
|
||||
let plen: Int = __str_len(pad)
|
||||
if plen == 0 { return s }
|
||||
let need: Int = width - slen
|
||||
let suffix: String = ""
|
||||
let i: Int = 0
|
||||
while i < need {
|
||||
let pad_idx: Int = i - (i / plen) * plen
|
||||
let pc: String = __str_slice_raw(pad, pad_idx, pad_idx + 1)
|
||||
suffix = __str_concat_raw(suffix, pc)
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(s, suffix)
|
||||
}
|
||||
|
||||
// ── Counting ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// Count non-overlapping occurrences of `sub` in `s`. Empty sub returns 0.
|
||||
fn str_count(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return 0 }
|
||||
if sublen > slen { return 0 }
|
||||
let count: Int = 0
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
count = count + 1
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Byte count — alias of str_len.
|
||||
fn str_count_bytes(s: String) -> Int {
|
||||
return __str_len(s)
|
||||
}
|
||||
|
||||
// UTF-8 codepoint count: count bytes that are NOT continuation bytes (10xxxxxx).
|
||||
// Continuation bytes have the pattern 10xxxxxx = 0x80..0xBF (128..191).
|
||||
fn str_count_chars(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// Continuation bytes are in range [128, 191]; skip them.
|
||||
// All other bytes (< 128 ASCII, or >= 192 leading bytes) start a codepoint.
|
||||
if c < 128 {
|
||||
count = count + 1
|
||||
} else {
|
||||
if c >= 192 { count = count + 1 }
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count newline-delimited lines. A trailing newline does NOT add an extra empty line.
|
||||
fn str_count_lines(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return 0 }
|
||||
let count: Int = 0
|
||||
let has_content: Bool = false
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
has_content = true
|
||||
if c == 10 { // \n
|
||||
count = count + 1
|
||||
has_content = false
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
if has_content { count = count + 1 }
|
||||
return count
|
||||
}
|
||||
|
||||
// Count whitespace-delimited words (non-empty tokens).
|
||||
fn str_count_words(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let in_word: Bool = false
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if _is_ws(c) {
|
||||
in_word = false
|
||||
} else {
|
||||
if !in_word {
|
||||
in_word = true
|
||||
count = count + 1
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count ASCII letters [A-Za-z].
|
||||
fn str_count_letters(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c >= 65 {
|
||||
if c <= 90 { count = count + 1 } // A-Z
|
||||
}
|
||||
if c >= 97 {
|
||||
if c <= 122 { count = count + 1 } // a-z
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count ASCII decimal digits [0-9].
|
||||
fn str_count_digits(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c >= 48 {
|
||||
if c <= 57 { count = count + 1 } // '0'-'9'
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// ── Character classification ──────────────────────────────────────────────────
|
||||
//
|
||||
// For all predicates: empty string -> false.
|
||||
// Multi-char string: ALL bytes must satisfy the predicate.
|
||||
|
||||
fn is_letter(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 65 { if c <= 90 { ok = true } } // A-Z
|
||||
if c >= 97 { if c <= 122 { ok = true } } // a-z
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_digit(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 48 { return false } // '0'
|
||||
if c > 57 { return false } // '9'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_alphanumeric(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 48 { if c <= 57 { ok = true } } // 0-9
|
||||
if c >= 65 { if c <= 90 { ok = true } } // A-Z
|
||||
if c >= 97 { if c <= 122 { ok = true } } // a-z
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_whitespace(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
if !_is_ws(__str_char_at(s, i)) { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ASCII punctuation: 33-47, 58-64, 91-96, 123-126.
|
||||
fn is_punctuation(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 33 { if c <= 47 { ok = true } }
|
||||
if c >= 58 { if c <= 64 { ok = true } }
|
||||
if c >= 91 { if c <= 96 { ok = true } }
|
||||
if c >= 123 { if c <= 126 { ok = true } }
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_uppercase(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 65 { return false } // 'A'
|
||||
if c > 90 { return false } // 'Z'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_lowercase(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 97 { return false } // 'a'
|
||||
if c > 122 { return false } // 'z'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Splitting ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn str_split(s: String, sep: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let slen: Int = __str_len(s)
|
||||
let seplen: Int = __str_len(sep)
|
||||
// Empty separator: return the whole string as a single element.
|
||||
if seplen == 0 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let part_start: Int = 0
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if i + seplen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + seplen)
|
||||
if __str_cmp(window, sep) == 0 {
|
||||
let part: String = __str_slice_raw(s, part_start, i)
|
||||
result = el_list_append(result, part)
|
||||
i = i + seplen
|
||||
part_start = i
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
// Append remaining tail (may be empty string if s ended with sep).
|
||||
let tail: String = __str_slice_raw(s, part_start, slen)
|
||||
result = el_list_append(result, tail)
|
||||
return result
|
||||
}
|
||||
|
||||
// Split into at most n parts. The nth part (index n-1) contains the remainder
|
||||
// verbatim, including any further separators. n <= 0 returns []. n == 1
|
||||
// returns [s].
|
||||
fn str_split_n(s: String, sep: String, n: Int) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
if n <= 0 { return result }
|
||||
if n == 1 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let slen: Int = __str_len(s)
|
||||
let seplen: Int = __str_len(sep)
|
||||
if seplen == 0 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let part_start: Int = 0
|
||||
let parts: Int = 0
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if parts >= n - 1 {
|
||||
// Reached the split limit — stop splitting, emit the rest below.
|
||||
i = slen
|
||||
} else {
|
||||
if i + seplen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + seplen)
|
||||
if __str_cmp(window, sep) == 0 {
|
||||
let part: String = __str_slice_raw(s, part_start, i)
|
||||
result = el_list_append(result, part)
|
||||
i = i + seplen
|
||||
part_start = i
|
||||
parts = parts + 1
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remainder verbatim.
|
||||
let tail: String = __str_slice_raw(s, part_start, slen)
|
||||
result = el_list_append(result, tail)
|
||||
return result
|
||||
}
|
||||
|
||||
// Split on newlines. \r\n is folded to \n. Trailing empty line after a
|
||||
// final \n is dropped — so "a\nb\n" yields ["a", "b"], not ["a", "b", ""].
|
||||
fn str_split_lines(s: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return result }
|
||||
let line_start: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c == 10 { // \n
|
||||
let lend: Int = i
|
||||
// Fold \r\n: if the byte before \n is \r, exclude it.
|
||||
if lend > line_start {
|
||||
if __str_char_at(s, lend - 1) == 13 { lend = lend - 1 }
|
||||
}
|
||||
let line: String = __str_slice_raw(s, line_start, lend)
|
||||
result = el_list_append(result, line)
|
||||
line_start = i + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
// Trailing content with no terminating \n.
|
||||
if line_start < n {
|
||||
let line: String = __str_slice_raw(s, line_start, n)
|
||||
result = el_list_append(result, line)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Split into a list of one-byte strings (byte-level chars).
|
||||
fn str_split_chars(s: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let n: Int = __str_len(s)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let ch: String = __str_slice_raw(s, i, i + 1)
|
||||
result = el_list_append(result, ch)
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── Joining ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// Join a list of strings with a separator between consecutive elements.
|
||||
// Empty list yields "". Non-string elements should not be passed here.
|
||||
fn str_join(parts: [String], sep: String) -> String {
|
||||
let n: Int = el_list_len(parts)
|
||||
if n == 0 { return "" }
|
||||
let result: String = el_list_get(parts, 0)
|
||||
let i: Int = 1
|
||||
while i < n {
|
||||
result = __str_concat_raw(result, sep)
|
||||
result = __str_concat_raw(result, el_list_get(parts, i))
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── DHARMA byte encoding (str_to_bytes) ──────────────────────────────────────
|
||||
//
|
||||
// str_to_bytes — encode a string as a JSON array of unsigned byte values.
|
||||
// "hi" -> "[104,105]"
|
||||
// Used by db.el to store content in Engram JSON nodes as a byte array.
|
||||
// Note: bytes_to_str (the inverse) is defined in json.el because it depends
|
||||
// on json_array_get_string which is defined there.
|
||||
fn str_to_bytes(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "[]" }
|
||||
let result: String = "["
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let b: Int = __str_char_at(s, i)
|
||||
if i > 0 { result = __str_concat_raw(result, ",") }
|
||||
result = __str_concat_raw(result, __int_to_str(b))
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(result, "]")
|
||||
}
|
||||
|
||||
// ── Cryptographic hashing ─────────────────────────────────────────────────────
|
||||
|
||||
// hash_sha256 — return the SHA-256 hex digest of a string.
|
||||
// Delegates to the __sha256_hex seed primitive.
|
||||
fn hash_sha256(s: String) -> String {
|
||||
return __sha256_hex(s)
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
// runtime/test.el — El test framework: assertions, registration, and runner.
|
||||
//
|
||||
// Provides a minimal but complete test harness for El programs. No external
|
||||
// dependencies. Written entirely in El using existing runtime primitives.
|
||||
//
|
||||
// ── Quick-start ──────────────────────────────────────────────────────────────
|
||||
//
|
||||
// 1. Write a test function with the standard test signature:
|
||||
//
|
||||
// fn test_str_eq_works(_: String) -> String {
|
||||
// assert_true(str_eq("a", "a"), "same strings are equal")
|
||||
// assert_false(str_eq("a", "b"), "different strings are not equal")
|
||||
// return ""
|
||||
// }
|
||||
//
|
||||
// 2. Register it and run:
|
||||
//
|
||||
// fn main() -> Void {
|
||||
// test_case("str_eq works", "test_str_eq_works")
|
||||
// test_run_all()
|
||||
// }
|
||||
//
|
||||
// Test functions must have the signature (String) -> String. The argument is
|
||||
// a dummy passed by the threading mechanism (see runtime/thread.el) and should
|
||||
// be ignored. The return value is likewise ignored — results flow through the
|
||||
// state-based assertion primitives.
|
||||
//
|
||||
// ── State keys ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// _test_cases JSON array of {"name":"...","fn":"..."}
|
||||
// _test_pass_count Int as string — total passing assertions
|
||||
// _test_fail_count Int as string — total failing assertions
|
||||
// _test_failures JSON array of failure message strings
|
||||
// _test_current Name of the test case currently executing
|
||||
//
|
||||
// ── Dependencies ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// runtime/string.el — str_eq, str_concat, str_contains, int_to_str, ...
|
||||
// runtime/state.el — state_set, state_get
|
||||
// runtime/json.el — json_array_len, json_array_get_string, json_get,
|
||||
// json_escape_string
|
||||
// runtime/thread.el — spawn, join (for dynamic dispatch via dlsym)
|
||||
|
||||
// ── Internal: JSON array helpers ─────────────────────────────────────────────
|
||||
//
|
||||
// _test_json_append — append a quoted, escaped string element to a JSON array.
|
||||
//
|
||||
// Given an existing JSON array string (e.g. '["a","b"]') and a plain string
|
||||
// value, returns a new array with the value appended (e.g. '["a","b","c"]').
|
||||
//
|
||||
// The array must be non-empty — always init with "[]" before calling.
|
||||
fn _test_json_append(arr: String, val: String) -> String {
|
||||
let escaped: String = json_escape_string(val)
|
||||
let inner: String = str_slice(arr, 1, str_len(arr) - 1)
|
||||
if str_eq(inner, "") {
|
||||
return "[\"" + escaped + "\"]"
|
||||
}
|
||||
return "[" + inner + ",\"" + escaped + "\"]"
|
||||
}
|
||||
|
||||
// _test_json_obj_append — append a raw JSON object string to a JSON array.
|
||||
//
|
||||
// Used to build the _test_cases list where each element is already a
|
||||
// JSON object (not a plain string).
|
||||
fn _test_json_obj_append(arr: String, obj: String) -> String {
|
||||
let inner: String = str_slice(arr, 1, str_len(arr) - 1)
|
||||
if str_eq(inner, "") {
|
||||
return "[" + obj + "]"
|
||||
}
|
||||
return "[" + inner + "," + obj + "]"
|
||||
}
|
||||
|
||||
// ── Test registration ─────────────────────────────────────────────────────────
|
||||
|
||||
// test_case — register a named test case.
|
||||
//
|
||||
// name: Human-readable test case name (shown in output).
|
||||
// fn_name: Name of a top-level El function with signature
|
||||
// (String) -> String. The function should call assertion
|
||||
// primitives from this module. The String arg it receives is ""
|
||||
// and its return value is ignored.
|
||||
//
|
||||
// Test cases are stored in the _test_cases state key and executed in
|
||||
// registration order by test_run_all().
|
||||
fn test_case(name: String, fn_name: String) {
|
||||
let arr: String = state_get("_test_cases")
|
||||
if str_eq(arr, "") { arr = "[]" }
|
||||
let escaped_name: String = json_escape_string(name)
|
||||
let escaped_fn: String = json_escape_string(fn_name)
|
||||
let obj: String = "{\"name\":\"" + escaped_name + "\",\"fn\":\"" + escaped_fn + "\"}"
|
||||
let arr = _test_json_obj_append(arr, obj)
|
||||
state_set("_test_cases", arr)
|
||||
}
|
||||
|
||||
// ── Test state helpers ────────────────────────────────────────────────────────
|
||||
|
||||
// _test_init — reset all test counters and failure lists.
|
||||
//
|
||||
// Called at the start of test_run_all(). Safe to call multiple times.
|
||||
fn _test_init() {
|
||||
state_set("_test_pass_count", "0")
|
||||
state_set("_test_fail_count", "0")
|
||||
state_set("_test_failures", "[]")
|
||||
state_set("_test_current", "")
|
||||
}
|
||||
|
||||
// _test_inc_pass — increment the global pass counter by 1.
|
||||
fn _test_inc_pass() {
|
||||
let n: Int = str_to_int(state_get("_test_pass_count"))
|
||||
state_set("_test_pass_count", int_to_str(n + 1))
|
||||
}
|
||||
|
||||
// _test_inc_fail — increment the global fail counter by 1.
|
||||
fn _test_inc_fail() {
|
||||
let n: Int = str_to_int(state_get("_test_fail_count"))
|
||||
state_set("_test_fail_count", int_to_str(n + 1))
|
||||
}
|
||||
|
||||
// test_pass — record a passing assertion for the current test case.
|
||||
//
|
||||
// Increments the pass counter. Called internally by assertions.
|
||||
fn test_pass(name: String) {
|
||||
_test_inc_pass()
|
||||
}
|
||||
|
||||
// test_fail — record a failing assertion for the current test case.
|
||||
//
|
||||
// name: assertion label or description (usually the msg parameter)
|
||||
// msg: detailed failure message including expected/got values
|
||||
//
|
||||
// Increments the fail counter and appends the message to _test_failures.
|
||||
// Also prints the failure immediately for visibility.
|
||||
fn test_fail(name: String, msg: String) {
|
||||
_test_inc_fail()
|
||||
let failures: String = state_get("_test_failures")
|
||||
if str_eq(failures, "") { failures = "[]" }
|
||||
let entry: String = " " + msg
|
||||
let failures = _test_json_append(failures, entry)
|
||||
state_set("_test_failures", failures)
|
||||
println(" FAIL: " + msg)
|
||||
}
|
||||
|
||||
// ── Assertions ────────────────────────────────────────────────────────────────
|
||||
|
||||
// assert_true — assert that condition is true.
|
||||
//
|
||||
// condition: the boolean value to test
|
||||
// msg: description shown on failure
|
||||
fn assert_true(condition: Bool, msg: String) {
|
||||
if condition {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected true, got false")
|
||||
}
|
||||
|
||||
// assert_false — assert that condition is false.
|
||||
//
|
||||
// condition: the boolean value to test
|
||||
// msg: description shown on failure
|
||||
fn assert_false(condition: Bool, msg: String) {
|
||||
if !condition {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected false, got true")
|
||||
}
|
||||
|
||||
// assert_eq — assert that two strings are equal.
|
||||
//
|
||||
// a, b: strings to compare
|
||||
// msg: description shown on failure (quoted values appended automatically)
|
||||
fn assert_eq(a: String, b: String, msg: String) {
|
||||
if str_eq(a, b) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected \"" + b + "\", got \"" + a + "\"")
|
||||
}
|
||||
|
||||
// assert_int_eq — assert that two integers are equal.
|
||||
//
|
||||
// a, b: integers to compare
|
||||
// msg: description shown on failure
|
||||
fn assert_int_eq(a: Int, b: Int, msg: String) {
|
||||
if a == b {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected " + int_to_str(b) + ", got " + int_to_str(a))
|
||||
}
|
||||
|
||||
// assert_neq — assert that two strings are NOT equal.
|
||||
//
|
||||
// a, b: strings to compare
|
||||
// msg: description shown on failure
|
||||
fn assert_neq(a: String, b: String, msg: String) {
|
||||
if !str_eq(a, b) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected values to differ, but both are \"" + a + "\"")
|
||||
}
|
||||
|
||||
// assert_contains — assert that string s contains substring sub.
|
||||
//
|
||||
// s: haystack string
|
||||
// sub: needle substring
|
||||
// msg: description shown on failure
|
||||
fn assert_contains(s: String, sub: String, msg: String) {
|
||||
if str_contains(s, sub) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not contain \"" + sub + "\"")
|
||||
}
|
||||
|
||||
// assert_starts_with — assert that string s starts with prefix.
|
||||
//
|
||||
// s: string to inspect
|
||||
// prefix: expected prefix
|
||||
// msg: description shown on failure
|
||||
fn assert_starts_with(s: String, prefix: String, msg: String) {
|
||||
if str_starts_with(s, prefix) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not start with \"" + prefix + "\"")
|
||||
}
|
||||
|
||||
// assert_ends_with — assert that string s ends with suffix.
|
||||
//
|
||||
// s: string to inspect
|
||||
// suffix: expected suffix
|
||||
// msg: description shown on failure
|
||||
fn assert_ends_with(s: String, suffix: String, msg: String) {
|
||||
if str_ends_with(s, suffix) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not end with \"" + suffix + "\"")
|
||||
}
|
||||
|
||||
// fail — unconditional test failure.
|
||||
//
|
||||
// msg: failure message shown in output
|
||||
//
|
||||
// Use when a code path that must not be reached is reached, or when an
|
||||
// expected exception did not occur.
|
||||
fn fail(msg: String) {
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg)
|
||||
}
|
||||
|
||||
// ── Runner ────────────────────────────────────────────────────────────────────
|
||||
|
||||
// _test_run_one — execute a single registered test case by name.
|
||||
//
|
||||
// name: the human-readable test case name (set as _test_current)
|
||||
// fn_name: the El function to invoke via the thread mechanism
|
||||
//
|
||||
// Sets _test_current so that assertions inside the test function know which
|
||||
// test they belong to. Spawns and immediately joins the test function in a
|
||||
// child thread (same dlsym mechanism as parallel_map) so dynamic dispatch
|
||||
// works without needing closures.
|
||||
fn _test_run_one(name: String, fn_name: String) {
|
||||
state_set("_test_current", name)
|
||||
let before_fail: Int = str_to_int(state_get("_test_fail_count"))
|
||||
let tid: Int = __thread_create(fn_name, "")
|
||||
__thread_join(tid)
|
||||
let after_fail: Int = str_to_int(state_get("_test_fail_count"))
|
||||
if after_fail == before_fail {
|
||||
println("[test] " + name + " ... PASS")
|
||||
} else {
|
||||
println("[test] " + name + " ... FAIL")
|
||||
}
|
||||
}
|
||||
|
||||
// test_run_all — execute all registered test cases and print a summary.
|
||||
//
|
||||
// Iterates through every test case registered via test_case(), runs each one,
|
||||
// prints per-test PASS/FAIL status, then prints a summary line.
|
||||
//
|
||||
// Returns the total number of failing assertions. Exit with this value to
|
||||
// signal CI failure:
|
||||
//
|
||||
// fn main() -> Int {
|
||||
// test_case("str_eq", "test_str_eq")
|
||||
// return test_run_all()
|
||||
// }
|
||||
fn test_run_all() -> Int {
|
||||
_test_init()
|
||||
|
||||
let cases: String = state_get("_test_cases")
|
||||
if str_eq(cases, "") { cases = "[]" }
|
||||
|
||||
let n: Int = json_array_len(cases)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let entry: String = json_array_get(cases, i)
|
||||
let name: String = json_get(entry, "name")
|
||||
let fn_name: String = json_get(entry, "fn")
|
||||
_test_run_one(name, fn_name)
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
let pass_count: Int = str_to_int(state_get("_test_pass_count"))
|
||||
let fail_count: Int = str_to_int(state_get("_test_fail_count"))
|
||||
println("[test] Summary: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed")
|
||||
|
||||
return fail_count
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// thread.el — El native threading model
|
||||
//
|
||||
// First-class parallelism for El. Eliminates bash fan-out hacks for parallel
|
||||
// HTTP dispatch, concurrent processing pipelines, and any other workload that
|
||||
// benefits from concurrent execution.
|
||||
//
|
||||
// Built on four seed primitives exposed by el_seed.c via dlsym+pthread:
|
||||
// __thread_create(fn_name, arg) -> Int spawn thread, return tid
|
||||
// __thread_join(tid) -> String join thread, return result
|
||||
// __mutex_new() -> Int allocate a mutex, return handle
|
||||
// __mutex_lock(m) lock mutex
|
||||
// __mutex_unlock(m) unlock mutex
|
||||
//
|
||||
// Every El fn compiles to a global C symbol. __thread_create uses dlsym to
|
||||
// look up the function by name and run it in a pthread. This means any El fn
|
||||
// with signature (String) -> String is directly threadable.
|
||||
|
||||
// ── Core primitives ──────────────────────────────────────────────────────────
|
||||
|
||||
// spawn — launch an El function in a new thread.
|
||||
//
|
||||
// fn_name: the name of an El fn with signature (String) -> String
|
||||
// arg: the argument to pass to that fn
|
||||
//
|
||||
// Returns a thread id (tid) that can be passed to join().
|
||||
// The El function must be a top-level fn — its C symbol must be globally
|
||||
// visible so dlsym can resolve it.
|
||||
fn spawn(fn_name: String, arg: String) -> Int {
|
||||
return __thread_create(fn_name, arg)
|
||||
}
|
||||
|
||||
// join — wait for a thread to finish and return its result.
|
||||
//
|
||||
// tid: the thread id returned by spawn()
|
||||
//
|
||||
// Blocks until the thread completes. Returns the String value the thread
|
||||
// function returned.
|
||||
fn join(tid: Int) -> String {
|
||||
return __thread_join(tid)
|
||||
}
|
||||
|
||||
// ── parallel_map ─────────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_map — map an El function over a list of strings concurrently.
|
||||
//
|
||||
// items: [String] — the input list
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
//
|
||||
// Spawns one thread per item. All threads run concurrently. Joins each thread
|
||||
// in input order, so the output list preserves the same order as the input.
|
||||
//
|
||||
// This is the core primitive that replaces bash fan-out for parallel HTTP.
|
||||
// Example — dispatch to N rooms at once:
|
||||
// let responses: [String] = parallel_map(room_payloads, "dispatch_to_room")
|
||||
fn parallel_map(items: [String], fn_name: String) -> [String] {
|
||||
let n: Int = el_list_len(items)
|
||||
|
||||
// Phase 1: spawn all threads and collect tids in order.
|
||||
let tids: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let tid: Int = spawn(fn_name, item)
|
||||
// Store tid as string so we can hold it in [String].
|
||||
// int_to_str is available as a builtin.
|
||||
let tids = el_list_append(tids, int_to_str(tid))
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Phase 2: join all threads in order, collecting results.
|
||||
let results: [String] = el_list_empty()
|
||||
let j = 0
|
||||
while j < n {
|
||||
let tid_str: String = el_list_get(tids, j)
|
||||
let tid: Int = str_to_int(tid_str)
|
||||
let result: String = join(tid)
|
||||
let results = el_list_append(results, result)
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ── parallel_map_json ────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_map_json — parallel_map over a JSON array string.
|
||||
//
|
||||
// items_json: String — a JSON array of strings, e.g. '["a","b","c"]'
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
//
|
||||
// Parses the JSON array into an [String], runs parallel_map, then serialises
|
||||
// the result list back to a JSON array string. Both input and output are JSON
|
||||
// strings — the common El inter-service format.
|
||||
//
|
||||
// Example:
|
||||
// let out_json: String = parallel_map_json(rooms_json, "dispatch_to_room")
|
||||
fn parallel_map_json(items_json: String, fn_name: String) -> String {
|
||||
let n: Int = json_array_len(items_json)
|
||||
|
||||
// Unpack JSON array into [String].
|
||||
let items: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = json_array_get(items_json, i)
|
||||
let items = el_list_append(items, item)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Run the concurrent map.
|
||||
let results: [String] = parallel_map(items, fn_name)
|
||||
|
||||
// Repack results into a JSON array string.
|
||||
let m: Int = el_list_len(results)
|
||||
let out: String = "["
|
||||
let j = 0
|
||||
while j < m {
|
||||
let val: String = el_list_get(results, j)
|
||||
if j > 0 {
|
||||
let out = out + ","
|
||||
}
|
||||
// Each result is treated as a raw JSON value (object, array, or
|
||||
// quoted string as returned by the worker fn).
|
||||
let out = out + val
|
||||
let j = j + 1
|
||||
}
|
||||
let out = out + "]"
|
||||
return out
|
||||
}
|
||||
|
||||
// ── parallel_filter ──────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_filter — keep items where fn_name returns "true", concurrently.
|
||||
//
|
||||
// items: [String] — the input list
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
// that returns "true" to keep the item or anything else
|
||||
// to discard it
|
||||
//
|
||||
// Runs the predicate fn on all items in parallel. Collects results in order,
|
||||
// preserving the relative order of kept items.
|
||||
fn parallel_filter(items: [String], fn_name: String) -> [String] {
|
||||
let n: Int = el_list_len(items)
|
||||
|
||||
// Spawn a predicate thread for every item.
|
||||
let tids: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let tid: Int = spawn(fn_name, item)
|
||||
let tids = el_list_append(tids, int_to_str(tid))
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Join in order, keep item if the predicate returned "true".
|
||||
let kept: [String] = el_list_empty()
|
||||
let j = 0
|
||||
while j < n {
|
||||
let item: String = el_list_get(items, j)
|
||||
let tid_str: String = el_list_get(tids, j)
|
||||
let tid: Int = str_to_int(tid_str)
|
||||
let verdict: String = join(tid)
|
||||
if str_eq(verdict, "true") {
|
||||
let kept = el_list_append(kept, item)
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
return kept
|
||||
}
|
||||
|
||||
// ── HTTP helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
// fire_http_post — worker fn for parallel_posts.
|
||||
//
|
||||
// Expects arg to be a JSON object with "url" and "body" keys:
|
||||
// {"url":"https://...","body":"{...}"}
|
||||
//
|
||||
// Returns the HTTP response body string. Registered as a global El fn so
|
||||
// parallel_map can locate it via dlsym.
|
||||
fn fire_http_post(arg: String) -> String {
|
||||
let url: String = json_get(arg, "url")
|
||||
let body: String = json_get(arg, "body")
|
||||
return http_post(url, body)
|
||||
}
|
||||
|
||||
// parallel_posts — fire a list of HTTP POSTs concurrently.
|
||||
//
|
||||
// requests: [String] — each element is a JSON object {"url":"...","body":"..."}
|
||||
//
|
||||
// Returns [String] of response bodies in the same order as the input.
|
||||
//
|
||||
// Example — fan out to N room endpoints at once:
|
||||
// let reqs: [String] = el_list_empty()
|
||||
// let reqs = el_list_append(reqs, "{\"url\":\"http://room-a/dispatch\",\"body\":\"" + payload + "\"}")
|
||||
// let reqs = el_list_append(reqs, "{\"url\":\"http://room-b/dispatch\",\"body\":\"" + payload + "\"}")
|
||||
// let responses: [String] = parallel_posts(reqs)
|
||||
fn parallel_posts(requests: [String]) -> [String] {
|
||||
return parallel_map(requests, "fire_http_post")
|
||||
}
|
||||
|
||||
// ── Mutex helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// with_mutex — call fn_name(arg) while holding mutex m.
|
||||
//
|
||||
// m: Int — mutex handle returned by __mutex_new()
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
// arg: String — argument to pass to fn_name
|
||||
//
|
||||
// Locks the mutex, spawns fn_name(arg) in a child thread, joins to collect
|
||||
// the result, then unlocks. The mutex is held across the entire duration of
|
||||
// fn_name's execution, serializing concurrent callers.
|
||||
//
|
||||
// Note: fn_name must NOT itself acquire the same mutex — that would deadlock.
|
||||
// This is the standard reentrant-mutex caveat.
|
||||
//
|
||||
// Usage:
|
||||
// let m: Int = __mutex_new()
|
||||
// let result: String = with_mutex(m, "update_shared_state", payload)
|
||||
fn with_mutex(m: Int, fn_name: String, arg: String) -> String {
|
||||
__mutex_lock(m)
|
||||
let tid: Int = spawn(fn_name, arg)
|
||||
let result: String = join(tid)
|
||||
__mutex_unlock(m)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
// runtime/time.el — Time operations, sleep, and formatting.
|
||||
//
|
||||
// Implements the time surface from el-compiler/runtime/legacy/el_runtime.c
|
||||
// (lines 3334–3440, 3471–3656) in pure El, using seed primitives.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __time_now_ns() -> Int (nanoseconds since Unix epoch)
|
||||
// __sleep_ms(n: Int)
|
||||
// __int_to_str(n: Int) -> String
|
||||
// __str_to_int(s: String) -> Int
|
||||
// __float_to_str(f: Float) -> String
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — now / sleep
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// time_now — milliseconds since Unix epoch (UTC). Matches legacy time_now().
|
||||
fn time_now() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// time_now_utc — same as time_now; UTC alias kept for compatibility.
|
||||
fn time_now_utc() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// now_ns — nanoseconds since Unix epoch. Matches el_now_instant().
|
||||
fn now_ns() -> Int {
|
||||
return __time_now_ns()
|
||||
}
|
||||
|
||||
// unix_timestamp — whole seconds since Unix epoch. Matches unix_timestamp().
|
||||
fn unix_timestamp() -> Int {
|
||||
return __time_now_ns() / 1000000000
|
||||
}
|
||||
|
||||
// sleep_secs — block for n seconds. Clamps negatives to 0.
|
||||
fn sleep_secs(n: Int) {
|
||||
if n < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(n * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// sleep_ms — block for n milliseconds. Clamps negatives to 0.
|
||||
fn sleep_ms(n: Int) {
|
||||
if n < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(n)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gregorian decomposition helpers — pure integer arithmetic.
|
||||
//
|
||||
// Algorithm: civil date from days since Unix epoch (1970-01-01).
|
||||
// Based on Howard Hinnant's public-domain civil_from_days formula
|
||||
// (http://howardhinnant.github.io/date_algorithms.html), which the legacy
|
||||
// gmtime_r call performs under the hood.
|
||||
//
|
||||
// We expose the pieces as private helpers (leading underscore convention).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// _is_leap — 1 if year y is a Gregorian leap year, 0 otherwise.
|
||||
fn _is_leap(y: Int) -> Int {
|
||||
if y % 400 == 0 { return 1 }
|
||||
if y % 100 == 0 { return 0 }
|
||||
if y % 4 == 0 { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
// _days_in_month — number of days in month m of year y (m: 1..12).
|
||||
fn _days_in_month(y: Int, m: Int) -> Int {
|
||||
if m == 1 { return 31 }
|
||||
if m == 2 {
|
||||
if _is_leap(y) == 1 { return 29 }
|
||||
return 28
|
||||
}
|
||||
if m == 3 { return 31 }
|
||||
if m == 4 { return 30 }
|
||||
if m == 5 { return 31 }
|
||||
if m == 6 { return 30 }
|
||||
if m == 7 { return 31 }
|
||||
if m == 8 { return 31 }
|
||||
if m == 9 { return 30 }
|
||||
if m == 10 { return 31 }
|
||||
if m == 11 { return 30 }
|
||||
return 31
|
||||
}
|
||||
|
||||
// _pad2 — zero-pad an integer to at least 2 digits.
|
||||
fn _pad2(n: Int) -> String {
|
||||
if n < 10 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _pad4 — zero-pad an integer to at least 4 digits.
|
||||
fn _pad4(n: Int) -> String {
|
||||
if n < 10 { return "000" + __int_to_str(n) }
|
||||
if n < 100 { return "00" + __int_to_str(n) }
|
||||
if n < 1000 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _pad3 — zero-pad an integer to at least 3 digits (milliseconds).
|
||||
fn _pad3(n: Int) -> String {
|
||||
if n < 10 { return "00" + __int_to_str(n) }
|
||||
if n < 100 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _civil_year_month_day — decompose days-since-epoch (z, may be negative)
|
||||
// into year/month/day using the civil_from_days algorithm.
|
||||
// Returns a JSON object: {"year":Y,"month":M,"day":D}
|
||||
fn _civil_ymd(z: Int) -> String {
|
||||
// shift epoch to 0000-03-01 (makes leap-day math clean)
|
||||
let zz: Int = z + 719468
|
||||
// era: 400-year block
|
||||
let era: Int = zz / 146097
|
||||
if zz < 0 {
|
||||
era = (zz - 146096) / 146097
|
||||
}
|
||||
let doe: Int = zz - era * 146097 // day-of-era [0, 146096]
|
||||
let yoe: Int = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365 // year-of-era [0, 399]
|
||||
let y: Int = yoe + era * 400
|
||||
let doy: Int = doe - (365 * yoe + yoe / 4 - yoe / 100) // day-of-year [0, 365]
|
||||
let mp: Int = (5 * doy + 2) / 153 // month in [0, 11] from March
|
||||
let d: Int = doy - (153 * mp + 2) / 5 + 1 // day [1, 31]
|
||||
let m: Int = mp + 3
|
||||
if mp >= 10 { m = mp - 9 }
|
||||
if mp >= 10 { y = y + 1 }
|
||||
return "{\"year\":" + __int_to_str(y) + ",\"month\":" + __int_to_str(m) + ",\"day\":" + __int_to_str(d) + "}"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_to_parts — decompose a millisecond timestamp into UTC components.
|
||||
//
|
||||
// Returns a JSON string:
|
||||
// {"year":Y,"month":M,"day":D,"hour":H,"minute":M,"second":S,"ms":MS}
|
||||
//
|
||||
// Matches legacy time_to_parts() which returns an ElMap with the same keys.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_to_parts(ts: Int) -> String {
|
||||
let ms_raw: Int = ts % 1000
|
||||
let ms: Int = ms_raw
|
||||
if ms_raw < 0 { ms = ms_raw + 1000 }
|
||||
|
||||
let s_raw: Int = ts / 1000
|
||||
let s: Int = s_raw
|
||||
if ms_raw < 0 { s = s_raw - 1 }
|
||||
|
||||
// seconds within the day and days since epoch
|
||||
let sec_of_day: Int = s % 86400
|
||||
let day_z: Int = s / 86400
|
||||
// handle negative: floor division
|
||||
if sec_of_day < 0 {
|
||||
sec_of_day = sec_of_day + 86400
|
||||
day_z = day_z - 1
|
||||
}
|
||||
|
||||
let hour: Int = sec_of_day / 3600
|
||||
let rem: Int = sec_of_day % 3600
|
||||
let minute: Int = rem / 60
|
||||
let second: Int = rem % 60
|
||||
|
||||
// date components via civil decomposition
|
||||
let ymd: String = _civil_ymd(day_z)
|
||||
let year: Int = __str_to_int(json_get(ymd, "year"))
|
||||
let month: Int = __str_to_int(json_get(ymd, "month"))
|
||||
let day: Int = __str_to_int(json_get(ymd, "day"))
|
||||
|
||||
return "{\"year\":" + __int_to_str(year) +
|
||||
",\"month\":" + __int_to_str(month) +
|
||||
",\"day\":" + __int_to_str(day) +
|
||||
",\"hour\":" + __int_to_str(hour) +
|
||||
",\"minute\":" + __int_to_str(minute) +
|
||||
",\"second\":" + __int_to_str(second) +
|
||||
",\"ms\":" + __int_to_str(ms) + "}"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_format — format a millisecond timestamp as a string.
|
||||
//
|
||||
// fmt "ISO" (or empty) → "YYYY-MM-DDTHH:MM:SS.mmmZ" (ISO 8601 UTC)
|
||||
// Other fmt values are passed as a strftime-style hint; the El runtime
|
||||
// implements the most common tokens. Unsupported tokens are passed through.
|
||||
//
|
||||
// Matches legacy time_format().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_format(ts: Int, fmt: String) -> String {
|
||||
let parts: String = time_to_parts(ts)
|
||||
let y: Int = __str_to_int(json_get(parts, "year"))
|
||||
let mo: Int = __str_to_int(json_get(parts, "month"))
|
||||
let d: Int = __str_to_int(json_get(parts, "day"))
|
||||
let h: Int = __str_to_int(json_get(parts, "hour"))
|
||||
let mi: Int = __str_to_int(json_get(parts, "minute"))
|
||||
let s: Int = __str_to_int(json_get(parts, "second"))
|
||||
let ms: Int = __str_to_int(json_get(parts, "ms"))
|
||||
|
||||
// ISO 8601 UTC: YYYY-MM-DDTHH:MM:SS.mmmZ
|
||||
if str_eq(fmt, "ISO") {
|
||||
return _pad4(y) + "-" + _pad2(mo) + "-" + _pad2(d) +
|
||||
"T" + _pad2(h) + ":" + _pad2(mi) + ":" + _pad2(s) +
|
||||
"." + _pad3(ms) + "Z"
|
||||
}
|
||||
if str_eq(fmt, "") {
|
||||
return _pad4(y) + "-" + _pad2(mo) + "-" + _pad2(d) +
|
||||
"T" + _pad2(h) + ":" + _pad2(mi) + ":" + _pad2(s) +
|
||||
"." + _pad3(ms) + "Z"
|
||||
}
|
||||
|
||||
// strftime-subset: replace common tokens
|
||||
let out: String = fmt
|
||||
let out = str_replace(out, "%Y", _pad4(y))
|
||||
let out = str_replace(out, "%m", _pad2(mo))
|
||||
let out = str_replace(out, "%d", _pad2(d))
|
||||
let out = str_replace(out, "%H", _pad2(h))
|
||||
let out = str_replace(out, "%M", _pad2(mi))
|
||||
let out = str_replace(out, "%S", _pad2(s))
|
||||
let out = str_replace(out, "%3N", _pad3(ms))
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_from_parts — construct a ms timestamp from seconds + nanosecond offset.
|
||||
//
|
||||
// Matches legacy time_from_parts(secs, ns, tz) — tz is ignored (UTC assumed).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_from_parts(secs: Int, ns: Int, tz: String) -> Int {
|
||||
return secs * 1000 + ns / 1000000
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_add — add a duration to a millisecond timestamp.
|
||||
//
|
||||
// unit: "ms" | "sec" | "min" | "hour" | "day"
|
||||
// Matches legacy time_add().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_add(ts: Int, n: Int, unit: String) -> Int {
|
||||
if str_eq(unit, "ms") { return ts + n }
|
||||
if str_eq(unit, "sec") { return ts + n * 1000 }
|
||||
if str_eq(unit, "min") { return ts + n * 60000 }
|
||||
if str_eq(unit, "hour") { return ts + n * 3600000 }
|
||||
if str_eq(unit, "day") { return ts + n * 86400000 }
|
||||
// default: treat as ms
|
||||
return ts + n
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_diff — compute the difference between two millisecond timestamps.
|
||||
//
|
||||
// Returns ts2 - ts1 in the given unit.
|
||||
// unit: "ms" | "sec" | "min" | "hour" | "day"
|
||||
// Matches legacy time_diff().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_diff(ts1: Int, ts2: Int, unit: String) -> Int {
|
||||
let d: Int = ts2 - ts1
|
||||
if str_eq(unit, "ms") { return d }
|
||||
if str_eq(unit, "sec") { return d / 1000 }
|
||||
if str_eq(unit, "min") { return d / 60000 }
|
||||
if str_eq(unit, "hour") { return d / 3600000 }
|
||||
if str_eq(unit, "day") { return d / 86400000 }
|
||||
return d
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Instant / Duration — nanosecond-precision temporal types.
|
||||
//
|
||||
// These match the el_now_instant, duration_seconds, duration_millis, etc.
|
||||
// family from legacy lines 3471–3656. Both Instant and Duration are Int
|
||||
// (nanoseconds); the type distinction is at the call-site convention level.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// now — current Instant in nanoseconds. Alias for __time_now_ns().
|
||||
fn now() -> Int {
|
||||
return __time_now_ns()
|
||||
}
|
||||
|
||||
// unix_seconds — Instant from whole seconds since epoch.
|
||||
fn unix_seconds(n: Int) -> Int {
|
||||
return n * 1000000000
|
||||
}
|
||||
|
||||
// unix_millis — Instant from milliseconds since epoch.
|
||||
fn unix_millis(n: Int) -> Int {
|
||||
return n * 1000000
|
||||
}
|
||||
|
||||
// instant_to_unix_seconds — convert Instant nanoseconds to whole seconds.
|
||||
fn instant_to_unix_seconds(i: Int) -> Int {
|
||||
return i / 1000000000
|
||||
}
|
||||
|
||||
// instant_to_unix_millis — convert Instant nanoseconds to milliseconds.
|
||||
fn instant_to_unix_millis(i: Int) -> Int {
|
||||
return i / 1000000
|
||||
}
|
||||
|
||||
// instant_to_iso8601 — format an Instant (nanoseconds) as ISO 8601 UTC.
|
||||
fn instant_to_iso8601(i: Int) -> String {
|
||||
let ms: Int = i / 1000000
|
||||
return time_format(ms, "ISO")
|
||||
}
|
||||
|
||||
// duration_seconds — Duration from n whole seconds.
|
||||
fn duration_seconds(n: Int) -> Int {
|
||||
return n * 1000000000
|
||||
}
|
||||
|
||||
// duration_millis — Duration from n milliseconds.
|
||||
fn duration_millis(n: Int) -> Int {
|
||||
return n * 1000000
|
||||
}
|
||||
|
||||
// duration_nanos — Duration from n nanoseconds (identity).
|
||||
fn duration_nanos(n: Int) -> Int {
|
||||
return n
|
||||
}
|
||||
|
||||
// duration_to_seconds — convert a Duration (nanoseconds) to whole seconds.
|
||||
fn duration_to_seconds(d: Int) -> Int {
|
||||
return d / 1000000000
|
||||
}
|
||||
|
||||
// duration_to_millis — convert a Duration (nanoseconds) to milliseconds.
|
||||
fn duration_to_millis(d: Int) -> Int {
|
||||
return d / 1000000
|
||||
}
|
||||
|
||||
// duration_to_nanos — return the Duration as nanoseconds (identity).
|
||||
fn duration_to_nanos(d: Int) -> Int {
|
||||
return d
|
||||
}
|
||||
|
||||
// sleep_duration — sleep for a Duration (nanoseconds). Clamps negatives to 0.
|
||||
fn sleep_duration(dur: Int) {
|
||||
let ms: Int = dur / 1000000
|
||||
if ms < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(ms)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TTL cache — time-bounded key/value backed by state.
|
||||
//
|
||||
// Matches legacy ttl_cache_set / ttl_cache_get / ttl_cache_age (lines 3663–3717).
|
||||
// max_age is a Duration (nanoseconds).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ttl_cache_set — store a value and record the current Instant for TTL checks.
|
||||
fn ttl_cache_set(key: String, value: String) {
|
||||
state_set(key, value)
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let now_str: String = __int_to_str(__time_now_ns())
|
||||
state_set(stamp_key, now_str)
|
||||
}
|
||||
|
||||
// ttl_cache_get — return value if age < max_age (nanoseconds), else "".
|
||||
fn ttl_cache_get(key: String, max_age: Int) -> String {
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let sv: String = state_get(stamp_key)
|
||||
if str_eq(sv, "") { return "" }
|
||||
let set_at: Int = __str_to_int(sv)
|
||||
let now_ns: Int = __time_now_ns()
|
||||
let age: Int = now_ns - set_at
|
||||
if age < 0 { return "" }
|
||||
if age > max_age { return "" }
|
||||
return state_get(key)
|
||||
}
|
||||
|
||||
// ttl_cache_age — nanoseconds since a key was last set (INT_MAX sentinel if missing).
|
||||
fn ttl_cache_age(key: String) -> Int {
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let sv: String = state_get(stamp_key)
|
||||
if str_eq(sv, "") { return 9223372036854775807 }
|
||||
let set_at: Int = __str_to_int(sv)
|
||||
return __time_now_ns() - set_at
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// uuid_new / uuid_v4 — generate a UUID v4 string.
|
||||
//
|
||||
// Delegates to the __uuid_v4() seed primitive.
|
||||
// Matches legacy uuid_new() / uuid_v4() (lines 4602–4621).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn uuid_new() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
fn uuid_v4() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DHARMA-compatible aliases — millisecond-precision timestamps.
|
||||
//
|
||||
// now_millis, unix_timestamp_ms, and time_now_ms all return the same value:
|
||||
// milliseconds since the Unix epoch. They exist because different parts of
|
||||
// the dharma codebase use different names for the same concept.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// now_millis — milliseconds since Unix epoch. Alias for time_now().
|
||||
fn now_millis() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// unix_timestamp_ms — same as now_millis.
|
||||
fn unix_timestamp_ms() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// time_now_ms — same as now_millis.
|
||||
fn time_now_ms() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
Reference in New Issue
Block a user