From 56787453811d4fa090d4f25a4e75bc603f5e40d3 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Sun, 3 May 2026 15:39:48 -0500 Subject: [PATCH] =?UTF-8?q?add=20runtime/time.el,=20math.el,=20state.el=20?= =?UTF-8?q?=E2=80=94=20time,=20math,=20and=20state=20in=20El?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates the time, math/float, and in-process state surfaces from el-compiler/runtime/legacy/el_runtime.c to self-hosted El source: - runtime/time.el: time_now, sleep_secs/ms, time_to_parts (via pure-El Gregorian civil_from_days decomposition), time_format (ISO + strftime subset), time_add, time_diff, time_from_parts; full Instant/Duration nanosecond API (now, unix_seconds/millis, duration_seconds/millis, instant_to_iso8601, sleep_duration); TTL cache (ttl_cache_set/get/age backed by state); uuid_new / uuid_v4 via __uuid_v4 seed. - runtime/math.el: el_abs, el_max, el_min (Int); math_sqrt/log/ln/sin/cos/pi (Float seed wrappers); float_to_str, int_to_float, float_to_int, str_to_float, format_float (__format_float seed), decimal_round (half-away-from-zero via pure-El _pow10/_floor_f helpers). - runtime/state.el: state_set/get/del/keys thin wrappers over __state_* seeds; convenience helpers state_has and state_get_or. --- runtime/math.el | 170 ++++++++++++++++++++ runtime/state.el | 61 +++++++ runtime/time.el | 402 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 633 insertions(+) create mode 100644 runtime/math.el create mode 100644 runtime/state.el create mode 100644 runtime/time.el diff --git a/runtime/math.el b/runtime/math.el new file mode 100644 index 0000000..0df3493 --- /dev/null +++ b/runtime/math.el @@ -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 +} diff --git a/runtime/state.el b/runtime/state.el new file mode 100644 index 0000000..f8f5c67 --- /dev/null +++ b/runtime/state.el @@ -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 +} diff --git a/runtime/time.el b/runtime/time.el new file mode 100644 index 0000000..df4cf0e --- /dev/null +++ b/runtime/time.el @@ -0,0 +1,402 @@ +// 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() +}