Files
2026-05-05 01:38:51 -05:00

426 lines
14 KiB
EmacsLisp
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// runtime/time.el Time operations, sleep, and formatting.
//
// Implements the time surface from el-compiler/runtime/legacy/el_runtime.c
// (lines 33343440, 34713656) 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 34713656. 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 36633717).
// 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 46024621).
// ---------------------------------------------------------------------------
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
}