add Calendar + CalendarTime + Rhythm + LocalDate/Time as first-class
Phase 1.5 of time-system. Calendar is pluggable: EarthCalendar (IANA zones, DST, Gregorian) is the default; MarsCalendar, CycleCalendar(period), NoCycleCalendar handle non-Earth cases. Rhythm abstracts recurrence from clock units - rhythm_cycle_phase(0.5) means "midpoint of cycle" whether the cycle is 24 hours on Earth or 30 hours on a station or 300 years on a long-cycle world. Phase 1 (Instant + Duration) unchanged. EarthCalendar(zone_local()) is the user-facing default; nobody who doesn't care about non-Earth calendars sees the abstraction. Self-host fixed point holds at 6339 lines. Snapshot tagged at dist/platform/elc.20260502-1321-self-host. Phase 2 (scheduling primitives every/after/at) lands next, now with Calendar-aware grounding instead of Earth-time hardcoded. Backlog: bl-297f66d8 (supersedes bl-b29b3e60)
This commit is contained in:
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -3624,6 +3624,878 @@ el_val_t ttl_cache_age(el_val_t key) {
|
||||
return (el_val_t)(now_ns - set_at);
|
||||
}
|
||||
|
||||
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ──────────────
|
||||
* Phase 1.5. Calendar is pluggable: EarthCalendar (IANA zones + Gregorian +
|
||||
* DST), MarsCalendar (sols, MTC), CycleCalendar(period), NoCycleCalendar,
|
||||
* RelativeCalendar(epoch). Phase 1 zone wrapping folds INTO EarthCalendar;
|
||||
* UTC and IANA zones are themselves Earth-parochial and cannot live at the
|
||||
* lowest type layer.
|
||||
*
|
||||
* A Rhythm is a small AST that asks the Calendar for cycle phase, weekday,
|
||||
* etc. Most rhythm logic is calendar-agnostic at runtime: rhythm_cycle_phase
|
||||
* means "midpoint of cycle" whether the cycle is 24h on Earth or 30h on a
|
||||
* station or 300y on a long-cycle world. */
|
||||
|
||||
/* Magic headers — used by the runtime to recognize boxed temporal values
|
||||
* arriving through el_val_t. Distinct constants so accidental misuse fails
|
||||
* loudly rather than silently. */
|
||||
#define EL_CAL_MAGIC 0xE1CA1EDDU
|
||||
#define EL_CALTIME_MAGIC 0xE1CA1747U
|
||||
#define EL_RHYTHM_MAGIC 0xE1287A11U
|
||||
#define EL_LDATE_MAGIC 0xE1DA7E00U
|
||||
#define EL_LDT_MAGIC 0xE1DA7E1DU
|
||||
#define EL_ZONE_MAGIC 0xE12017E0U
|
||||
|
||||
typedef enum {
|
||||
EL_CALENDAR_EARTH = 1,
|
||||
EL_CALENDAR_MARS = 2,
|
||||
EL_CALENDAR_CYCLE = 3,
|
||||
EL_CALENDAR_NO_CYCLE = 4,
|
||||
EL_CALENDAR_RELATIVE = 5
|
||||
} el_calendar_kind_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
char* id; /* IANA name or "+HH:MM" / "-HH:MM" */
|
||||
int fixed; /* 1 for fixed offset, 0 for IANA */
|
||||
int64_t offset_ns; /* fixed offset in nanos (only when fixed) */
|
||||
} el_zone_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
el_calendar_kind_t kind;
|
||||
el_zone_t* zone; /* EarthCalendar; MarsCalendar uses MTC */
|
||||
int64_t cycle_period_ns;/* CycleCalendar; computed for Earth (86400 s) and Mars (88775.244 s) */
|
||||
int64_t epoch_ns; /* RelativeCalendar; Unix-epoch zero otherwise */
|
||||
} el_calendar_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
int64_t instant_ns;
|
||||
el_calendar_t* cal;
|
||||
} el_caltime_t;
|
||||
|
||||
/* Rhythm AST. */
|
||||
typedef enum {
|
||||
EL_RHYTHM_CYCLE_START = 1,
|
||||
EL_RHYTHM_CYCLE_PHASE = 2,
|
||||
EL_RHYTHM_DURATION = 3,
|
||||
EL_RHYTHM_SESSION_START = 4,
|
||||
EL_RHYTHM_EVENT = 5,
|
||||
EL_RHYTHM_AND = 6,
|
||||
EL_RHYTHM_OR = 7,
|
||||
EL_RHYTHM_WEEKDAY = 8,
|
||||
EL_RHYTHM_WEEKLY_AT = 9
|
||||
} el_rhythm_kind_t;
|
||||
|
||||
typedef struct el_rhythm_s {
|
||||
uint32_t magic;
|
||||
el_rhythm_kind_t kind;
|
||||
double phase; /* CYCLE_PHASE */
|
||||
int64_t period_ns; /* DURATION */
|
||||
int weekday; /* 1..7 Mon..Sun */
|
||||
int hour;
|
||||
int minute;
|
||||
char* event_name; /* EVENT */
|
||||
struct el_rhythm_s* a; /* AND/OR */
|
||||
struct el_rhythm_s* b;
|
||||
} el_rhythm_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
int year;
|
||||
int month;
|
||||
int day;
|
||||
} el_localdate_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
el_localdate_t* date;
|
||||
int64_t time_ns; /* nanos since midnight */
|
||||
} el_localdt_t;
|
||||
|
||||
/* Magic-tag check helpers — peek the first 4 bytes of an el_val_t pointer
|
||||
* and compare against the expected magic. Strings are NUL-terminated and
|
||||
* never start with our magic byte sequence, so this is safe. */
|
||||
static int el_is_magic(el_val_t v, uint32_t want) {
|
||||
if (v == 0) return 0;
|
||||
/* Defensive: only follow pointers in plausible address space.
|
||||
* On 64-bit unix processes pointers are above 0x10000. */
|
||||
if ((uint64_t)v < 0x10000ULL) return 0;
|
||||
uint32_t got = *(volatile uint32_t*)(uintptr_t)v;
|
||||
return got == want;
|
||||
}
|
||||
|
||||
/* Sol length on Mars in nanoseconds: 88775.244 seconds. */
|
||||
#define EL_MARS_SOL_NS ((int64_t)88775244000000LL)
|
||||
/* Earth solar day in nanoseconds: 86400 seconds. */
|
||||
#define EL_EARTH_DAY_NS ((int64_t)86400000000000LL)
|
||||
|
||||
/* ── Zone construction ──────────────────────────────────────────────────────
|
||||
* Zones intern by id string so equality comparisons are pointer-compares. */
|
||||
|
||||
#define EL_ZONE_TABLE_CAP 64
|
||||
static el_zone_t* _el_zone_table[EL_ZONE_TABLE_CAP];
|
||||
static int _el_zone_count = 0;
|
||||
|
||||
static el_zone_t* _el_zone_intern(const char* id, int fixed, int64_t offset_ns) {
|
||||
for (int i = 0; i < _el_zone_count; i++) {
|
||||
el_zone_t* z = _el_zone_table[i];
|
||||
if (z->fixed == fixed && z->offset_ns == offset_ns &&
|
||||
strcmp(z->id ? z->id : "", id ? id : "") == 0) {
|
||||
return z;
|
||||
}
|
||||
}
|
||||
if (_el_zone_count >= EL_ZONE_TABLE_CAP) {
|
||||
/* Out of slots: build a non-interned zone. Equality will fail across
|
||||
* such zones but the program still runs. */
|
||||
el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t));
|
||||
z->magic = EL_ZONE_MAGIC;
|
||||
z->id = el_strdup_persist(id ? id : "");
|
||||
z->fixed = fixed;
|
||||
z->offset_ns = offset_ns;
|
||||
return z;
|
||||
}
|
||||
el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t));
|
||||
z->magic = EL_ZONE_MAGIC;
|
||||
z->id = el_strdup_persist(id ? id : "");
|
||||
z->fixed = fixed;
|
||||
z->offset_ns = offset_ns;
|
||||
_el_zone_table[_el_zone_count++] = z;
|
||||
return z;
|
||||
}
|
||||
|
||||
el_val_t zone(el_val_t id) {
|
||||
const char* s = EL_CSTR(id);
|
||||
if (!s || !*s) return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 0, 0);
|
||||
/* Fixed-offset shortcut: "+HH:MM" or "-HH:MM". */
|
||||
if ((s[0] == '+' || s[0] == '-') && strlen(s) >= 6 && s[3] == ':') {
|
||||
int sign = (s[0] == '-') ? -1 : 1;
|
||||
int hh = (s[1] - '0') * 10 + (s[2] - '0');
|
||||
int mm = (s[4] - '0') * 10 + (s[5] - '0');
|
||||
int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL;
|
||||
return (el_val_t)(uintptr_t)_el_zone_intern(s, 1, off);
|
||||
}
|
||||
return (el_val_t)(uintptr_t)_el_zone_intern(s, 0, 0);
|
||||
}
|
||||
|
||||
el_val_t zone_utc(void) {
|
||||
return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 1, 0);
|
||||
}
|
||||
|
||||
el_val_t zone_local(void) {
|
||||
/* Resolve the local zone via TZ env or system default. tzset() picks
|
||||
* up TZ if set; otherwise the C library reads /etc/localtime. We store
|
||||
* the zone id as "LOCAL" so subsequent equality holds; resolution is
|
||||
* lazy at use time. */
|
||||
return (el_val_t)(uintptr_t)_el_zone_intern("LOCAL", 0, 0);
|
||||
}
|
||||
|
||||
el_val_t zone_offset(el_val_t hours, el_val_t minutes) {
|
||||
int hh = (int)(int64_t)hours;
|
||||
int mm = (int)(int64_t)minutes;
|
||||
int sign = (hh < 0 || mm < 0) ? -1 : 1;
|
||||
if (hh < 0) hh = -hh;
|
||||
if (mm < 0) mm = -mm;
|
||||
int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL;
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%c%02d:%02d", sign < 0 ? '-' : '+', hh, mm);
|
||||
return (el_val_t)(uintptr_t)_el_zone_intern(buf, 1, off);
|
||||
}
|
||||
|
||||
/* ── Calendar interning ──────────────────────────────────────────────────── */
|
||||
|
||||
#define EL_CAL_TABLE_CAP 64
|
||||
static el_calendar_t* _el_cal_table[EL_CAL_TABLE_CAP];
|
||||
static int _el_cal_count = 0;
|
||||
|
||||
static el_calendar_t* _el_cal_intern(el_calendar_kind_t kind, el_zone_t* z,
|
||||
int64_t period_ns, int64_t epoch_ns) {
|
||||
for (int i = 0; i < _el_cal_count; i++) {
|
||||
el_calendar_t* c = _el_cal_table[i];
|
||||
if (c->kind == kind && c->zone == z &&
|
||||
c->cycle_period_ns == period_ns && c->epoch_ns == epoch_ns) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
el_calendar_t* c = (el_calendar_t*)malloc(sizeof(el_calendar_t));
|
||||
c->magic = EL_CAL_MAGIC;
|
||||
c->kind = kind;
|
||||
c->zone = z;
|
||||
c->cycle_period_ns = period_ns;
|
||||
c->epoch_ns = epoch_ns;
|
||||
if (_el_cal_count < EL_CAL_TABLE_CAP) _el_cal_table[_el_cal_count++] = c;
|
||||
return c;
|
||||
}
|
||||
|
||||
el_val_t earth_calendar(el_val_t z_val) {
|
||||
el_zone_t* z = NULL;
|
||||
if (z_val != 0 && el_is_magic(z_val, EL_ZONE_MAGIC)) {
|
||||
z = (el_zone_t*)(uintptr_t)z_val;
|
||||
} else {
|
||||
z = (el_zone_t*)(uintptr_t)zone_local();
|
||||
}
|
||||
return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_EARTH, z, EL_EARTH_DAY_NS, 0);
|
||||
}
|
||||
|
||||
el_val_t earth_calendar_default(void) {
|
||||
return earth_calendar(zone_local());
|
||||
}
|
||||
|
||||
el_val_t mars_calendar(void) {
|
||||
el_zone_t* z = (el_zone_t*)(uintptr_t)_el_zone_intern("MTC", 1, 0);
|
||||
return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_MARS, z, EL_MARS_SOL_NS, 0);
|
||||
}
|
||||
|
||||
el_val_t cycle_calendar(el_val_t period_dur) {
|
||||
int64_t period = (int64_t)period_dur;
|
||||
if (period <= 0) period = 1;
|
||||
return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_CYCLE, NULL, period, 0);
|
||||
}
|
||||
|
||||
el_val_t no_cycle_calendar(void) {
|
||||
return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_NO_CYCLE, NULL, 0, 0);
|
||||
}
|
||||
|
||||
el_val_t relative_calendar(el_val_t epoch_inst) {
|
||||
int64_t ep = (int64_t)epoch_inst;
|
||||
return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_RELATIVE, NULL, 0, ep);
|
||||
}
|
||||
|
||||
/* ── CalendarTime ───────────────────────────────────────────────────────── */
|
||||
|
||||
static el_caltime_t* _el_caltime_alloc(int64_t inst, el_calendar_t* c) {
|
||||
el_caltime_t* ct = (el_caltime_t*)malloc(sizeof(el_caltime_t));
|
||||
ct->magic = EL_CALTIME_MAGIC;
|
||||
ct->instant_ns = inst;
|
||||
ct->cal = c;
|
||||
return ct;
|
||||
}
|
||||
|
||||
static el_calendar_t* _el_resolve_cal(el_val_t cal_val) {
|
||||
if (cal_val == 0 || !el_is_magic(cal_val, EL_CAL_MAGIC)) {
|
||||
return (el_calendar_t*)(uintptr_t)earth_calendar_default();
|
||||
}
|
||||
return (el_calendar_t*)(uintptr_t)cal_val;
|
||||
}
|
||||
|
||||
el_val_t now_in(el_val_t cal_val) {
|
||||
el_calendar_t* c = _el_resolve_cal(cal_val);
|
||||
int64_t ns = (int64_t)el_now_instant();
|
||||
return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c);
|
||||
}
|
||||
|
||||
el_val_t in_calendar(el_val_t inst, el_val_t cal_val) {
|
||||
el_calendar_t* c = _el_resolve_cal(cal_val);
|
||||
return (el_val_t)(uintptr_t)_el_caltime_alloc((int64_t)inst, c);
|
||||
}
|
||||
|
||||
el_val_t cal_to_instant(el_val_t ct_val) {
|
||||
if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0;
|
||||
el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val;
|
||||
return (el_val_t)ct->instant_ns;
|
||||
}
|
||||
|
||||
el_val_t cal_in(el_val_t ct_val, el_val_t cal_val) {
|
||||
if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0;
|
||||
el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val;
|
||||
el_calendar_t* c = _el_resolve_cal(cal_val);
|
||||
return (el_val_t)(uintptr_t)_el_caltime_alloc(ct->instant_ns, c);
|
||||
}
|
||||
|
||||
el_val_t cal_cycle_phase(el_val_t ct_val) {
|
||||
if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_from_float(0.0);
|
||||
el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val;
|
||||
el_calendar_t* c = ct->cal;
|
||||
if (c->kind == EL_CALENDAR_NO_CYCLE) {
|
||||
return el_from_float(0.0/0.0); /* NaN sentinel */
|
||||
}
|
||||
int64_t period = c->cycle_period_ns;
|
||||
if (period <= 0) return el_from_float(0.0);
|
||||
int64_t base = ct->instant_ns - c->epoch_ns;
|
||||
int64_t phase_ns = base % period;
|
||||
if (phase_ns < 0) phase_ns += period;
|
||||
double phase = (double)phase_ns / (double)period;
|
||||
return el_from_float(phase);
|
||||
}
|
||||
|
||||
/* ── Earth zone resolution: TZ-based offset lookup ──────────────────────────
|
||||
* For an EarthCalendar(zone), we want to convert an instant_ns into local
|
||||
* y/m/d/h/m/s, including DST. Approach: setenv("TZ", id), tzset(), use
|
||||
* localtime_r, then restore. This is not thread-safe by design — El's
|
||||
* runtime is single-threaded for the request handler path. Cache the
|
||||
* computed (instant -> tm) to avoid the syscall churn on repeat formats. */
|
||||
|
||||
static void _el_apply_zone(el_zone_t* z) {
|
||||
if (!z) { unsetenv("TZ"); tzset(); return; }
|
||||
if (z->fixed && strcmp(z->id, "UTC") == 0) {
|
||||
setenv("TZ", "UTC0", 1);
|
||||
tzset();
|
||||
return;
|
||||
}
|
||||
if (z->fixed) {
|
||||
/* Fixed offset: POSIX TZ uses inverted sign (sign convention of
|
||||
* "hours WEST of UTC" rather than east). Build the spec accordingly. */
|
||||
char buf[32];
|
||||
int neg_secs = (int)(-z->offset_ns / 1000000000LL);
|
||||
int sign = neg_secs < 0 ? -1 : 1;
|
||||
int abs_secs = neg_secs < 0 ? -neg_secs : neg_secs;
|
||||
int hh = abs_secs / 3600;
|
||||
int mm = (abs_secs % 3600) / 60;
|
||||
snprintf(buf, sizeof(buf), "FIX%c%d:%02d", sign < 0 ? '-' : '+', hh, mm);
|
||||
setenv("TZ", buf, 1);
|
||||
tzset();
|
||||
return;
|
||||
}
|
||||
if (strcmp(z->id, "LOCAL") == 0) {
|
||||
unsetenv("TZ");
|
||||
tzset();
|
||||
return;
|
||||
}
|
||||
setenv("TZ", z->id, 1);
|
||||
tzset();
|
||||
}
|
||||
|
||||
static int _el_decompose_earth(el_caltime_t* ct, struct tm* tm_out, int* abbr_len, char* abbr_buf, size_t abbr_cap) {
|
||||
el_calendar_t* c = ct->cal;
|
||||
el_zone_t* z = c->zone;
|
||||
_el_apply_zone(z);
|
||||
time_t s = (time_t)(ct->instant_ns / 1000000000LL);
|
||||
struct tm tm;
|
||||
localtime_r(&s, &tm);
|
||||
*tm_out = tm;
|
||||
if (abbr_buf && abbr_cap > 0) {
|
||||
const char* z_str = tm.tm_zone ? tm.tm_zone : "";
|
||||
size_t n = strlen(z_str);
|
||||
if (n >= abbr_cap) n = abbr_cap - 1;
|
||||
memcpy(abbr_buf, z_str, n);
|
||||
abbr_buf[n] = '\0';
|
||||
if (abbr_len) *abbr_len = (int)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Format an Earth CalendarTime under a Java-DateTimeFormatter-ish pattern.
|
||||
* We support a useful core: yyyy MM dd HH mm ss z EEE MMM d h a — enough for
|
||||
* the acceptance tests. Single quotes denote literal text. */
|
||||
static const char* _el_weekday_short[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
|
||||
static const char* _el_month_short[] = {"Jan","Feb","Mar","Apr","May","Jun",
|
||||
"Jul","Aug","Sep","Oct","Nov","Dec"};
|
||||
|
||||
static char* _el_format_earth(el_caltime_t* ct, const char* pattern) {
|
||||
struct tm tm;
|
||||
char abbr[16] = {0};
|
||||
int abbr_len = 0;
|
||||
_el_decompose_earth(ct, &tm, &abbr_len, abbr, sizeof(abbr));
|
||||
size_t cap = strlen(pattern) * 4 + 64;
|
||||
char* out = (char*)malloc(cap);
|
||||
size_t pos = 0;
|
||||
size_t i = 0;
|
||||
size_t plen = strlen(pattern);
|
||||
while (i < plen) {
|
||||
char ch = pattern[i];
|
||||
/* Quoted literal */
|
||||
if (ch == '\'') {
|
||||
i++;
|
||||
while (i < plen && pattern[i] != '\'') {
|
||||
if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); }
|
||||
out[pos++] = pattern[i++];
|
||||
}
|
||||
if (i < plen) i++;
|
||||
continue;
|
||||
}
|
||||
/* Count run of same letter */
|
||||
size_t run = 1;
|
||||
while (i + run < plen && pattern[i + run] == ch) run++;
|
||||
char tmp[64];
|
||||
tmp[0] = '\0';
|
||||
if (ch == 'y') {
|
||||
if (run >= 4) snprintf(tmp, sizeof(tmp), "%04d", tm.tm_year + 1900);
|
||||
else snprintf(tmp, sizeof(tmp), "%02d", (tm.tm_year + 1900) % 100);
|
||||
} else if (ch == 'M') {
|
||||
if (run >= 3) snprintf(tmp, sizeof(tmp), "%s", _el_month_short[tm.tm_mon]);
|
||||
else if (run == 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mon + 1);
|
||||
else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mon + 1);
|
||||
} else if (ch == 'd') {
|
||||
if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mday);
|
||||
else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mday);
|
||||
} else if (ch == 'H') {
|
||||
if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_hour);
|
||||
else snprintf(tmp, sizeof(tmp), "%d", tm.tm_hour);
|
||||
} else if (ch == 'h') {
|
||||
int h12 = tm.tm_hour % 12; if (h12 == 0) h12 = 12;
|
||||
if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", h12);
|
||||
else snprintf(tmp, sizeof(tmp), "%d", h12);
|
||||
} else if (ch == 'm') {
|
||||
if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_min);
|
||||
else snprintf(tmp, sizeof(tmp), "%d", tm.tm_min);
|
||||
} else if (ch == 's') {
|
||||
if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_sec);
|
||||
else snprintf(tmp, sizeof(tmp), "%d", tm.tm_sec);
|
||||
} else if (ch == 'a') {
|
||||
snprintf(tmp, sizeof(tmp), "%s", tm.tm_hour < 12 ? "AM" : "PM");
|
||||
} else if (ch == 'E') {
|
||||
snprintf(tmp, sizeof(tmp), "%s", _el_weekday_short[tm.tm_wday]);
|
||||
} else if (ch == 'z') {
|
||||
snprintf(tmp, sizeof(tmp), "%s", abbr);
|
||||
} else {
|
||||
for (size_t k = 0; k < run; k++) {
|
||||
if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); }
|
||||
out[pos++] = ch;
|
||||
}
|
||||
i += run;
|
||||
continue;
|
||||
}
|
||||
size_t tl = strlen(tmp);
|
||||
if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); }
|
||||
memcpy(out + pos, tmp, tl);
|
||||
pos += tl;
|
||||
i += run;
|
||||
}
|
||||
out[pos] = '\0';
|
||||
char* result = el_strdup(out);
|
||||
free(out);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Format a Mars CalendarTime: %sol prints the integer sol number since
|
||||
* mission epoch (Unix epoch fallback), %phase prints cycle_phase as a
|
||||
* 0..1 decimal. Other %-specifiers fall through. */
|
||||
static char* _el_format_mars(el_caltime_t* ct, const char* pattern) {
|
||||
el_calendar_t* c = ct->cal;
|
||||
int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : EL_MARS_SOL_NS;
|
||||
int64_t base = ct->instant_ns - c->epoch_ns;
|
||||
int64_t sol = base / period;
|
||||
int64_t phase_ns = base % period;
|
||||
if (phase_ns < 0) { phase_ns += period; sol -= 1; }
|
||||
double phase = (double)phase_ns / (double)period;
|
||||
size_t cap = strlen(pattern) * 4 + 64;
|
||||
char* out = (char*)malloc(cap);
|
||||
size_t pos = 0;
|
||||
for (size_t i = 0; pattern[i]; i++) {
|
||||
if (pattern[i] == '%' && pattern[i+1]) {
|
||||
char tmp[64];
|
||||
tmp[0] = '\0';
|
||||
if (strncmp(pattern + i + 1, "sol", 3) == 0) {
|
||||
snprintf(tmp, sizeof(tmp), "%lld", (long long)sol);
|
||||
i += 3;
|
||||
} else if (strncmp(pattern + i + 1, "phase", 5) == 0) {
|
||||
snprintf(tmp, sizeof(tmp), "%.4f", phase);
|
||||
i += 5;
|
||||
} else if (pattern[i+1] == 'd') {
|
||||
snprintf(tmp, sizeof(tmp), "%lld", (long long)sol);
|
||||
i += 1;
|
||||
} else {
|
||||
tmp[0] = pattern[i+1]; tmp[1] = '\0';
|
||||
i += 1;
|
||||
}
|
||||
size_t tl = strlen(tmp);
|
||||
if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); }
|
||||
memcpy(out + pos, tmp, tl);
|
||||
pos += tl;
|
||||
} else {
|
||||
if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); }
|
||||
out[pos++] = pattern[i];
|
||||
}
|
||||
}
|
||||
out[pos] = '\0';
|
||||
char* result = el_strdup(out);
|
||||
free(out);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Format a CycleCalendar CalendarTime: %cycle and %phase. */
|
||||
static char* _el_format_cycle(el_caltime_t* ct, const char* pattern) {
|
||||
el_calendar_t* c = ct->cal;
|
||||
int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : 1;
|
||||
int64_t base = ct->instant_ns - c->epoch_ns;
|
||||
int64_t cycle = base / period;
|
||||
int64_t phase_ns = base % period;
|
||||
if (phase_ns < 0) { phase_ns += period; cycle -= 1; }
|
||||
double phase = (double)phase_ns / (double)period;
|
||||
size_t cap = strlen(pattern) * 4 + 64;
|
||||
char* out = (char*)malloc(cap);
|
||||
size_t pos = 0;
|
||||
for (size_t i = 0; pattern[i]; i++) {
|
||||
if (pattern[i] == '%' && pattern[i+1]) {
|
||||
char tmp[64];
|
||||
tmp[0] = '\0';
|
||||
if (strncmp(pattern + i + 1, "cycle", 5) == 0) {
|
||||
snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle);
|
||||
i += 5;
|
||||
} else if (strncmp(pattern + i + 1, "phase", 5) == 0) {
|
||||
snprintf(tmp, sizeof(tmp), "%.4f", phase);
|
||||
i += 5;
|
||||
} else if (pattern[i+1] == 'd') {
|
||||
snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle);
|
||||
i += 1;
|
||||
} else if (pattern[i+1] == 'f') {
|
||||
snprintf(tmp, sizeof(tmp), "%.2f", phase);
|
||||
i += 1;
|
||||
} else {
|
||||
/* Pass through unknown specifier */
|
||||
tmp[0] = '%'; tmp[1] = pattern[i+1]; tmp[2] = '\0';
|
||||
i += 1;
|
||||
}
|
||||
size_t tl = strlen(tmp);
|
||||
if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); }
|
||||
memcpy(out + pos, tmp, tl);
|
||||
pos += tl;
|
||||
} else {
|
||||
if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); }
|
||||
out[pos++] = pattern[i];
|
||||
}
|
||||
}
|
||||
out[pos] = '\0';
|
||||
char* result = el_strdup(out);
|
||||
free(out);
|
||||
return result;
|
||||
}
|
||||
|
||||
el_val_t cal_format(el_val_t ct_val, el_val_t pattern_val) {
|
||||
if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_wrap_str(el_strdup(""));
|
||||
el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val;
|
||||
const char* pat = EL_CSTR(pattern_val);
|
||||
if (!pat) pat = "";
|
||||
char* result = NULL;
|
||||
switch (ct->cal->kind) {
|
||||
case EL_CALENDAR_EARTH: result = _el_format_earth(ct, pat); break;
|
||||
case EL_CALENDAR_MARS: result = _el_format_mars(ct, pat); break;
|
||||
case EL_CALENDAR_CYCLE: result = _el_format_cycle(ct, pat); break;
|
||||
case EL_CALENDAR_RELATIVE: result = _el_format_cycle(ct, pat); break;
|
||||
case EL_CALENDAR_NO_CYCLE: {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "instant:%lld", (long long)ct->instant_ns);
|
||||
result = el_strdup(buf);
|
||||
break;
|
||||
}
|
||||
default: result = el_strdup("");
|
||||
}
|
||||
return el_wrap_str(result);
|
||||
}
|
||||
|
||||
/* ── LocalDate / LocalTime / LocalDateTime ──────────────────────────────── */
|
||||
|
||||
static int _el_days_in_month(int y, int m) {
|
||||
static const int dim[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
|
||||
if (m == 2) {
|
||||
int leap = ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0);
|
||||
return 28 + (leap ? 1 : 0);
|
||||
}
|
||||
if (m < 1 || m > 12) return 30;
|
||||
return dim[m - 1];
|
||||
}
|
||||
|
||||
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d) {
|
||||
el_localdate_t* ld = (el_localdate_t*)malloc(sizeof(el_localdate_t));
|
||||
ld->magic = EL_LDATE_MAGIC;
|
||||
ld->year = (int)(int64_t)y;
|
||||
ld->month = (int)(int64_t)m;
|
||||
ld->day = (int)(int64_t)d;
|
||||
return (el_val_t)(uintptr_t)ld;
|
||||
}
|
||||
|
||||
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns) {
|
||||
int64_t hh = (int64_t)h;
|
||||
int64_t mm = (int64_t)m;
|
||||
int64_t ss = (int64_t)s;
|
||||
int64_t nn = (int64_t)ns;
|
||||
int64_t total = hh * 3600000000000LL + mm * 60000000000LL + ss * 1000000000LL + nn;
|
||||
return (el_val_t)total;
|
||||
}
|
||||
|
||||
el_val_t local_datetime(el_val_t date_val, el_val_t time_val) {
|
||||
if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0;
|
||||
el_localdt_t* ldt = (el_localdt_t*)malloc(sizeof(el_localdt_t));
|
||||
ldt->magic = EL_LDT_MAGIC;
|
||||
ldt->date = (el_localdate_t*)(uintptr_t)date_val;
|
||||
ldt->time_ns = (int64_t)time_val;
|
||||
return (el_val_t)(uintptr_t)ldt;
|
||||
}
|
||||
|
||||
el_val_t zoned(el_val_t date_val, el_val_t time_val, el_val_t cal_val) {
|
||||
if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0;
|
||||
el_localdate_t* ld = (el_localdate_t*)(uintptr_t)date_val;
|
||||
el_calendar_t* c = _el_resolve_cal(cal_val);
|
||||
int64_t time_ns = (int64_t)time_val;
|
||||
/* Convert (LocalDate, LocalTime, EarthCalendar) -> Instant.
|
||||
* For non-Earth calendars we use day-anchored conversion: treat the
|
||||
* LocalDate's (y,m,d) as a Gregorian projection, convert to seconds via
|
||||
* mktime under the calendar's zone, then add nanos-since-midnight. */
|
||||
if (c->kind == EL_CALENDAR_EARTH) {
|
||||
_el_apply_zone(c->zone);
|
||||
struct tm tm; memset(&tm, 0, sizeof(tm));
|
||||
tm.tm_year = ld->year - 1900;
|
||||
tm.tm_mon = ld->month - 1;
|
||||
tm.tm_mday = ld->day;
|
||||
tm.tm_hour = (int)(time_ns / 3600000000000LL);
|
||||
tm.tm_min = (int)((time_ns / 60000000000LL) % 60);
|
||||
tm.tm_sec = (int)((time_ns / 1000000000LL) % 60);
|
||||
tm.tm_isdst = -1;
|
||||
time_t t = mktime(&tm);
|
||||
if (t == (time_t)-1) return (el_val_t)0;
|
||||
int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL);
|
||||
return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c);
|
||||
}
|
||||
/* Non-Earth fallback: project as if Earth UTC then attach calendar. */
|
||||
struct tm tm; memset(&tm, 0, sizeof(tm));
|
||||
tm.tm_year = ld->year - 1900;
|
||||
tm.tm_mon = ld->month - 1;
|
||||
tm.tm_mday = ld->day;
|
||||
tm.tm_hour = (int)(time_ns / 3600000000000LL);
|
||||
tm.tm_min = (int)((time_ns / 60000000000LL) % 60);
|
||||
tm.tm_sec = (int)((time_ns / 1000000000LL) % 60);
|
||||
time_t t = timegm(&tm);
|
||||
if (t == (time_t)-1) return (el_val_t)0;
|
||||
int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL);
|
||||
return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c);
|
||||
}
|
||||
|
||||
el_val_t local_date_year(el_val_t v) {
|
||||
if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0;
|
||||
return (el_val_t)((el_localdate_t*)(uintptr_t)v)->year;
|
||||
}
|
||||
el_val_t local_date_month(el_val_t v) {
|
||||
if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0;
|
||||
return (el_val_t)((el_localdate_t*)(uintptr_t)v)->month;
|
||||
}
|
||||
el_val_t local_date_day(el_val_t v) {
|
||||
if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0;
|
||||
return (el_val_t)((el_localdate_t*)(uintptr_t)v)->day;
|
||||
}
|
||||
el_val_t local_time_hour(el_val_t v) {
|
||||
int64_t t = (int64_t)v;
|
||||
return (el_val_t)(t / 3600000000000LL);
|
||||
}
|
||||
el_val_t local_time_minute(el_val_t v) {
|
||||
int64_t t = (int64_t)v;
|
||||
return (el_val_t)((t / 60000000000LL) % 60);
|
||||
}
|
||||
el_val_t local_time_second(el_val_t v) {
|
||||
int64_t t = (int64_t)v;
|
||||
return (el_val_t)((t / 1000000000LL) % 60);
|
||||
}
|
||||
el_val_t local_time_nanos(el_val_t v) {
|
||||
int64_t t = (int64_t)v;
|
||||
return (el_val_t)(t % 1000000000LL);
|
||||
}
|
||||
|
||||
el_val_t el_local_date_add_dur(el_val_t ld_val, el_val_t dur_val) {
|
||||
if (!el_is_magic(ld_val, EL_LDATE_MAGIC)) return ld_val;
|
||||
el_localdate_t* ld = (el_localdate_t*)(uintptr_t)ld_val;
|
||||
int64_t dur_ns = (int64_t)dur_val;
|
||||
int64_t days = dur_ns / EL_EARTH_DAY_NS;
|
||||
int y = ld->year, m = ld->month, d = ld->day;
|
||||
/* Walk days forward/backward in canonical Gregorian. */
|
||||
while (days > 0) {
|
||||
int dim = _el_days_in_month(y, m);
|
||||
if (d + days <= dim) { d += (int)days; days = 0; break; }
|
||||
days -= (dim - d + 1);
|
||||
d = 1;
|
||||
m++;
|
||||
if (m > 12) { m = 1; y++; }
|
||||
}
|
||||
while (days < 0) {
|
||||
if (d + days >= 1) { d += (int)days; days = 0; break; }
|
||||
days += d;
|
||||
m--;
|
||||
if (m < 1) { m = 12; y--; }
|
||||
d = _el_days_in_month(y, m);
|
||||
}
|
||||
return local_date((el_val_t)y, (el_val_t)m, (el_val_t)d);
|
||||
}
|
||||
|
||||
el_val_t el_local_time_add_dur(el_val_t lt_val, el_val_t dur_val) {
|
||||
int64_t t = (int64_t)lt_val + (int64_t)dur_val;
|
||||
/* Wrap mod 24h on Earth-default. CycleCalendar wrapping requires the
|
||||
* caller to use cal_in / cal_format for the right modulus. */
|
||||
int64_t day = EL_EARTH_DAY_NS;
|
||||
int64_t r = t % day;
|
||||
if (r < 0) r += day;
|
||||
return (el_val_t)r;
|
||||
}
|
||||
|
||||
el_val_t el_local_date_lt(el_val_t a_val, el_val_t b_val) {
|
||||
if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0;
|
||||
el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val;
|
||||
el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val;
|
||||
if (a->year != b->year) return (el_val_t)(a->year < b->year ? 1 : 0);
|
||||
if (a->month != b->month) return (el_val_t)(a->month < b->month ? 1 : 0);
|
||||
return (el_val_t)(a->day < b->day ? 1 : 0);
|
||||
}
|
||||
|
||||
el_val_t el_local_date_eq(el_val_t a_val, el_val_t b_val) {
|
||||
if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0;
|
||||
el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val;
|
||||
el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val;
|
||||
return (el_val_t)((a->year == b->year && a->month == b->month && a->day == b->day) ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ── Rhythm ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
static el_rhythm_t* _el_rhythm_alloc(el_rhythm_kind_t k) {
|
||||
el_rhythm_t* r = (el_rhythm_t*)calloc(1, sizeof(el_rhythm_t));
|
||||
r->magic = EL_RHYTHM_MAGIC;
|
||||
r->kind = k;
|
||||
return r;
|
||||
}
|
||||
|
||||
el_val_t rhythm_cycle_start(void) {
|
||||
return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_CYCLE_START);
|
||||
}
|
||||
|
||||
el_val_t rhythm_cycle_phase(el_val_t phase_val) {
|
||||
el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_CYCLE_PHASE);
|
||||
r->phase = el_to_float(phase_val);
|
||||
return (el_val_t)(uintptr_t)r;
|
||||
}
|
||||
|
||||
el_val_t rhythm_duration(el_val_t d_val) {
|
||||
el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_DURATION);
|
||||
r->period_ns = (int64_t)d_val;
|
||||
return (el_val_t)(uintptr_t)r;
|
||||
}
|
||||
|
||||
el_val_t rhythm_session_start(void) {
|
||||
return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_SESSION_START);
|
||||
}
|
||||
|
||||
el_val_t rhythm_event(el_val_t name_val) {
|
||||
el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_EVENT);
|
||||
const char* n = EL_CSTR(name_val);
|
||||
r->event_name = el_strdup_persist(n ? n : "");
|
||||
return (el_val_t)(uintptr_t)r;
|
||||
}
|
||||
|
||||
el_val_t rhythm_and(el_val_t a_val, el_val_t b_val) {
|
||||
el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_AND);
|
||||
r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL;
|
||||
r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL;
|
||||
return (el_val_t)(uintptr_t)r;
|
||||
}
|
||||
|
||||
el_val_t rhythm_or(el_val_t a_val, el_val_t b_val) {
|
||||
el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_OR);
|
||||
r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL;
|
||||
r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL;
|
||||
return (el_val_t)(uintptr_t)r;
|
||||
}
|
||||
|
||||
el_val_t rhythm_weekday(el_val_t day) {
|
||||
el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKDAY);
|
||||
r->weekday = (int)(int64_t)day;
|
||||
return (el_val_t)(uintptr_t)r;
|
||||
}
|
||||
|
||||
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute) {
|
||||
el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKLY_AT);
|
||||
r->weekday = (int)(int64_t)day;
|
||||
r->hour = (int)(int64_t)hour;
|
||||
r->minute = (int)(int64_t)minute;
|
||||
return (el_val_t)(uintptr_t)r;
|
||||
}
|
||||
|
||||
/* Compute the next instant on or after `after` when rhythm `r` matches,
|
||||
* under calendar `cal`. */
|
||||
static int64_t _el_next_after(el_rhythm_t* r, int64_t after_ns, el_calendar_t* cal) {
|
||||
if (!r) return after_ns;
|
||||
int64_t period = cal->cycle_period_ns > 0 ? cal->cycle_period_ns : EL_EARTH_DAY_NS;
|
||||
switch (r->kind) {
|
||||
case EL_RHYTHM_CYCLE_START: {
|
||||
int64_t base = after_ns - cal->epoch_ns;
|
||||
int64_t cyc = (base / period) + 1;
|
||||
return cal->epoch_ns + cyc * period;
|
||||
}
|
||||
case EL_RHYTHM_CYCLE_PHASE: {
|
||||
int64_t base = after_ns - cal->epoch_ns;
|
||||
int64_t cyc_ns = (int64_t)(r->phase * (double)period);
|
||||
int64_t cur_cyc = base / period;
|
||||
int64_t candidate = cal->epoch_ns + cur_cyc * period + cyc_ns;
|
||||
if (candidate <= after_ns) candidate += period;
|
||||
return candidate;
|
||||
}
|
||||
case EL_RHYTHM_DURATION: {
|
||||
return after_ns + (r->period_ns > 0 ? r->period_ns : 1);
|
||||
}
|
||||
case EL_RHYTHM_WEEKDAY:
|
||||
case EL_RHYTHM_WEEKLY_AT: {
|
||||
if (cal->kind != EL_CALENDAR_EARTH) {
|
||||
/* Non-Earth calendars: fall back to cycle math, treating
|
||||
* weekday as a 7-cycle-per-period proxy. */
|
||||
return after_ns + period;
|
||||
}
|
||||
_el_apply_zone(cal->zone);
|
||||
time_t s = (time_t)(after_ns / 1000000000LL);
|
||||
struct tm tm;
|
||||
localtime_r(&s, &tm);
|
||||
/* tm_wday: 0=Sun..6=Sat. We use 1=Mon..7=Sun. */
|
||||
int target = r->weekday >= 1 && r->weekday <= 7 ? r->weekday : 1;
|
||||
int target_wday = target == 7 ? 0 : target; /* 7→Sun=0, 1→Mon=1 */
|
||||
int days_ahead = (target_wday - tm.tm_wday + 7) % 7;
|
||||
int hour = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->hour : 0;
|
||||
int minute = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->minute : 0;
|
||||
struct tm cand = tm;
|
||||
cand.tm_mday += days_ahead;
|
||||
cand.tm_hour = hour;
|
||||
cand.tm_min = minute;
|
||||
cand.tm_sec = 0;
|
||||
cand.tm_isdst = -1;
|
||||
time_t cand_t = mktime(&cand);
|
||||
int64_t cand_ns = (int64_t)cand_t * 1000000000LL;
|
||||
if (cand_ns <= after_ns) {
|
||||
cand.tm_mday += 7;
|
||||
cand.tm_isdst = -1;
|
||||
cand_t = mktime(&cand);
|
||||
cand_ns = (int64_t)cand_t * 1000000000LL;
|
||||
}
|
||||
return cand_ns;
|
||||
}
|
||||
case EL_RHYTHM_AND: {
|
||||
int64_t a = _el_next_after(r->a, after_ns, cal);
|
||||
int64_t b = _el_next_after(r->b, after_ns, cal);
|
||||
return a > b ? a : b;
|
||||
}
|
||||
case EL_RHYTHM_OR: {
|
||||
int64_t a = _el_next_after(r->a, after_ns, cal);
|
||||
int64_t b = _el_next_after(r->b, after_ns, cal);
|
||||
return a < b ? a : b;
|
||||
}
|
||||
case EL_RHYTHM_SESSION_START:
|
||||
case EL_RHYTHM_EVENT:
|
||||
default:
|
||||
return after_ns;
|
||||
}
|
||||
}
|
||||
|
||||
el_val_t rhythm_next_after(el_val_t r_val, el_val_t after_val, el_val_t cal_val) {
|
||||
if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return after_val;
|
||||
el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val;
|
||||
el_calendar_t* c = _el_resolve_cal(cal_val);
|
||||
int64_t out = _el_next_after(r, (int64_t)after_val, c);
|
||||
return (el_val_t)out;
|
||||
}
|
||||
|
||||
el_val_t rhythm_matches(el_val_t r_val, el_val_t ct_val) {
|
||||
if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return (el_val_t)0;
|
||||
if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0;
|
||||
el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val;
|
||||
el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val;
|
||||
int64_t period = ct->cal->cycle_period_ns > 0 ? ct->cal->cycle_period_ns : EL_EARTH_DAY_NS;
|
||||
int64_t base = ct->instant_ns - ct->cal->epoch_ns;
|
||||
int64_t phase_ns = base % period;
|
||||
if (phase_ns < 0) phase_ns += period;
|
||||
double phase = (double)phase_ns / (double)period;
|
||||
switch (r->kind) {
|
||||
case EL_RHYTHM_CYCLE_START: return (el_val_t)(phase_ns == 0 ? 1 : 0);
|
||||
case EL_RHYTHM_CYCLE_PHASE: {
|
||||
double diff = phase - r->phase;
|
||||
if (diff < 0) diff = -diff;
|
||||
return (el_val_t)(diff < 0.001 ? 1 : 0);
|
||||
}
|
||||
default: return (el_val_t)0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── UUID v4 ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
static int _el_uuid_seeded = 0;
|
||||
|
||||
@@ -316,6 +316,89 @@ el_val_t ttl_cache_set(el_val_t key, el_val_t value);
|
||||
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
|
||||
el_val_t ttl_cache_age(el_val_t key);
|
||||
|
||||
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
|
||||
* Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
|
||||
* zones, Gregorian, DST) is the user-facing default; MarsCalendar,
|
||||
* CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
|
||||
* domains.
|
||||
*
|
||||
* A Calendar interprets an Instant under a particular cycle convention and
|
||||
* produces a CalendarTime. CalendarTime carries the underlying Instant and
|
||||
* a back-pointer to its Calendar; arithmetic and formatting consult the
|
||||
* Calendar to convert ns since epoch into year/month/day/hour/minute/second
|
||||
* (or sol/phase, or cycle/phase, depending on kind).
|
||||
*
|
||||
* Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
|
||||
* LocalDateTime are heap-allocated structs whose pointers are cast into
|
||||
* el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
|
||||
* the kind safely. LocalTime is small enough to live in the int64 slot
|
||||
* directly (nanos since midnight, signed). */
|
||||
|
||||
/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
|
||||
* `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
|
||||
* offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
|
||||
* on first use of the owning EarthCalendar. */
|
||||
el_val_t zone(el_val_t id);
|
||||
el_val_t zone_utc(void);
|
||||
el_val_t zone_local(void);
|
||||
el_val_t zone_offset(el_val_t hours, el_val_t minutes);
|
||||
|
||||
/* Calendar constructors. Each returns an el_val_t pointer to a heap-
|
||||
* allocated, magic-tagged Calendar struct. Calendars are interned by
|
||||
* (kind, zone_id, period_ns, epoch_ns) so identical constructors return
|
||||
* the same pointer — equality is reference equality. */
|
||||
el_val_t earth_calendar(el_val_t z);
|
||||
el_val_t earth_calendar_default(void);
|
||||
el_val_t mars_calendar(void);
|
||||
el_val_t cycle_calendar(el_val_t period_dur);
|
||||
el_val_t no_cycle_calendar(void);
|
||||
el_val_t relative_calendar(el_val_t epoch_inst);
|
||||
|
||||
/* CalendarTime constructors and methods. Returns a heap-allocated struct
|
||||
* whose pointer fits in el_val_t. */
|
||||
el_val_t now_in(el_val_t cal);
|
||||
el_val_t in_calendar(el_val_t inst, el_val_t cal);
|
||||
el_val_t cal_format(el_val_t ct, el_val_t pattern);
|
||||
el_val_t cal_to_instant(el_val_t ct);
|
||||
el_val_t cal_cycle_phase(el_val_t ct);
|
||||
el_val_t cal_in(el_val_t ct, el_val_t cal);
|
||||
|
||||
/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
|
||||
* LocalTime carries nanoseconds since midnight as a signed int64 directly
|
||||
* in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
|
||||
* heap-allocated structs with magic headers. */
|
||||
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
|
||||
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
|
||||
el_val_t local_datetime(el_val_t date, el_val_t time);
|
||||
el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
|
||||
|
||||
el_val_t local_date_year(el_val_t ld);
|
||||
el_val_t local_date_month(el_val_t ld);
|
||||
el_val_t local_date_day(el_val_t ld);
|
||||
el_val_t local_time_hour(el_val_t lt);
|
||||
el_val_t local_time_minute(el_val_t lt);
|
||||
el_val_t local_time_second(el_val_t lt);
|
||||
el_val_t local_time_nanos(el_val_t lt);
|
||||
|
||||
el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
|
||||
el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
|
||||
el_val_t el_local_date_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_local_date_eq(el_val_t a, el_val_t b);
|
||||
|
||||
/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
|
||||
* pointer in el_val_t; rhythms are immutable so callers may share them. */
|
||||
el_val_t rhythm_cycle_start(void);
|
||||
el_val_t rhythm_cycle_phase(el_val_t phase);
|
||||
el_val_t rhythm_duration(el_val_t d);
|
||||
el_val_t rhythm_session_start(void);
|
||||
el_val_t rhythm_event(el_val_t name);
|
||||
el_val_t rhythm_and(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_or(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_weekday(el_val_t day);
|
||||
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
|
||||
el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
|
||||
el_val_t rhythm_matches(el_val_t r, el_val_t ct);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t uuid_new(void);
|
||||
|
||||
@@ -202,6 +202,51 @@ fn cg_expr(expr: Map<String, Any>) -> String {
|
||||
let right_is_inst: Bool = is_instant_expr(right)
|
||||
let left_is_dur: Bool = is_duration_expr(left)
|
||||
let right_is_dur: Bool = is_duration_expr(right)
|
||||
|
||||
// Phase 1.5 LocalDate / LocalTime / CalendarTime dispatch. These
|
||||
// route through their typed runtime wrappers (el_local_date_add_dur,
|
||||
// el_local_time_add_dur, el_local_date_lt, el_local_date_eq) and
|
||||
// forbid mismatched ops at codegen time. Cross-calendar arithmetic
|
||||
// (CalendarTime + CalendarTime, CalendarTime - CalendarTime under
|
||||
// mismatched calendars) is structurally meaningless: a CalendarTime
|
||||
// already projects an Instant under a Calendar, so subtraction
|
||||
// between two of them only makes sense in instant-space (use
|
||||
// cal_to_instant first).
|
||||
let left_is_ld: Bool = is_localdate_expr(left)
|
||||
let right_is_ld: Bool = is_localdate_expr(right)
|
||||
let left_is_lt: Bool = is_localtime_expr(left)
|
||||
let right_is_lt: Bool = is_localtime_expr(right)
|
||||
let left_is_ct: Bool = is_caltime_expr(left)
|
||||
let right_is_ct: Bool = is_caltime_expr(right)
|
||||
if left_is_ld {
|
||||
if op == "Plus" {
|
||||
if right_is_dur {
|
||||
return "el_local_date_add_dur(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
}
|
||||
if op == "Lt" {
|
||||
if right_is_ld { return "el_local_date_lt(" + left_c + ", " + right_c + ")" }
|
||||
}
|
||||
if op == "EqEq" {
|
||||
if right_is_ld { return "el_local_date_eq(" + left_c + ", " + right_c + ")" }
|
||||
}
|
||||
}
|
||||
if left_is_lt {
|
||||
if op == "Plus" {
|
||||
if right_is_dur {
|
||||
return "el_local_time_add_dur(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
if left_is_ct {
|
||||
if op == "Plus" {
|
||||
if right_is_ct {
|
||||
time_record_violation("caltime_plus_caltime", "CalendarTime + CalendarTime is not allowed (use cal_to_instant + Duration)")
|
||||
return "0 /* TIME_TYPE_ERROR: CalendarTime + CalendarTime */"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let any_temporal: Bool = false
|
||||
if left_is_inst { let any_temporal = true }
|
||||
if right_is_inst { let any_temporal = true }
|
||||
@@ -877,6 +922,27 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
if str_eq(ltype, "Duration") {
|
||||
add_duration_name(name)
|
||||
}
|
||||
if str_eq(ltype, "Calendar") {
|
||||
add_calendar_name(name)
|
||||
}
|
||||
if str_eq(ltype, "CalendarTime") {
|
||||
add_caltime_name(name)
|
||||
}
|
||||
if str_eq(ltype, "Rhythm") {
|
||||
add_rhythm_name(name)
|
||||
}
|
||||
if str_eq(ltype, "LocalDate") {
|
||||
add_localdate_name(name)
|
||||
}
|
||||
if str_eq(ltype, "LocalTime") {
|
||||
add_localtime_name(name)
|
||||
}
|
||||
if str_eq(ltype, "LocalDateTime") {
|
||||
add_localdt_name(name)
|
||||
}
|
||||
if str_eq(ltype, "Zone") {
|
||||
add_zone_name(name)
|
||||
}
|
||||
// Inference from RHS — duration literals and known-typed calls
|
||||
// propagate even when the let is unannotated.
|
||||
if is_instant_expr(val) {
|
||||
@@ -885,6 +951,27 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
if is_duration_expr(val) {
|
||||
add_duration_name(name)
|
||||
}
|
||||
if is_calendar_expr(val) {
|
||||
add_calendar_name(name)
|
||||
}
|
||||
if is_caltime_expr(val) {
|
||||
add_caltime_name(name)
|
||||
}
|
||||
if is_rhythm_expr(val) {
|
||||
add_rhythm_name(name)
|
||||
}
|
||||
if is_localdate_expr(val) {
|
||||
add_localdate_name(name)
|
||||
}
|
||||
if is_localtime_expr(val) {
|
||||
add_localtime_name(name)
|
||||
}
|
||||
if is_localdt_expr(val) {
|
||||
add_localdt_name(name)
|
||||
}
|
||||
if is_zone_expr(val) {
|
||||
add_zone_name(name)
|
||||
}
|
||||
let vk: String = val["expr"]
|
||||
if str_eq(vk, "Int") {
|
||||
add_int_name(name)
|
||||
@@ -1232,6 +1319,191 @@ fn is_duration_call(call_expr: Map<String, Any>) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Phase 1.5 — Calendar / CalendarTime / Rhythm / LocalDate / LocalTime /
|
||||
// LocalDateTime / Zone are first-class boxed types. Each has its own name
|
||||
// set in process state, populated from typed `let` bindings and parameter
|
||||
// annotations. The BinOp dispatcher consults these to forbid mismatched
|
||||
// arithmetic (e.g. CalendarTime + CalendarTime, LocalDate < CalendarTime).
|
||||
fn is_calendar_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__calendar_names")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
fn is_caltime_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__caltime_names")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
fn is_rhythm_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__rhythm_names")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
fn is_localdate_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__localdate_names")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
fn is_localtime_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__localtime_names")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
fn is_localdt_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__localdt_names")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
fn is_zone_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__zone_names")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
// Calendar-returning builtins. earth_calendar / mars_calendar / cycle_calendar
|
||||
// / no_cycle_calendar / relative_calendar all box a calendar struct.
|
||||
fn is_calendar_call(call_expr: Map<String, Any>) -> Bool {
|
||||
let func = call_expr["func"]
|
||||
let fk: String = func["expr"]
|
||||
if !str_eq(fk, "Ident") { return false }
|
||||
let name: String = func["name"]
|
||||
if str_eq(name, "earth_calendar") { return true }
|
||||
if str_eq(name, "earth_calendar_default") { return true }
|
||||
if str_eq(name, "mars_calendar") { return true }
|
||||
if str_eq(name, "cycle_calendar") { return true }
|
||||
if str_eq(name, "no_cycle_calendar") { return true }
|
||||
if str_eq(name, "relative_calendar") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// CalendarTime-returning builtins.
|
||||
fn is_caltime_call(call_expr: Map<String, Any>) -> Bool {
|
||||
let func = call_expr["func"]
|
||||
let fk: String = func["expr"]
|
||||
if !str_eq(fk, "Ident") { return false }
|
||||
let name: String = func["name"]
|
||||
if str_eq(name, "now_in") { return true }
|
||||
if str_eq(name, "in_calendar") { return true }
|
||||
if str_eq(name, "cal_in") { return true }
|
||||
if str_eq(name, "zoned") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// Rhythm-returning builtins.
|
||||
fn is_rhythm_call(call_expr: Map<String, Any>) -> Bool {
|
||||
let func = call_expr["func"]
|
||||
let fk: String = func["expr"]
|
||||
if !str_eq(fk, "Ident") { return false }
|
||||
let name: String = func["name"]
|
||||
if str_eq(name, "rhythm_cycle_start") { return true }
|
||||
if str_eq(name, "rhythm_cycle_phase") { return true }
|
||||
if str_eq(name, "rhythm_duration") { return true }
|
||||
if str_eq(name, "rhythm_session_start") { return true }
|
||||
if str_eq(name, "rhythm_event") { return true }
|
||||
if str_eq(name, "rhythm_and") { return true }
|
||||
if str_eq(name, "rhythm_or") { return true }
|
||||
if str_eq(name, "rhythm_weekday") { return true }
|
||||
if str_eq(name, "rhythm_weekly_at") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// LocalDate-returning builtins.
|
||||
fn is_localdate_call(call_expr: Map<String, Any>) -> Bool {
|
||||
let func = call_expr["func"]
|
||||
let fk: String = func["expr"]
|
||||
if !str_eq(fk, "Ident") { return false }
|
||||
let name: String = func["name"]
|
||||
if str_eq(name, "local_date") { return true }
|
||||
if str_eq(name, "el_local_date_add_dur") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fn is_localtime_call(call_expr: Map<String, Any>) -> Bool {
|
||||
let func = call_expr["func"]
|
||||
let fk: String = func["expr"]
|
||||
if !str_eq(fk, "Ident") { return false }
|
||||
let name: String = func["name"]
|
||||
if str_eq(name, "local_time") { return true }
|
||||
if str_eq(name, "el_local_time_add_dur") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fn is_localdt_call(call_expr: Map<String, Any>) -> Bool {
|
||||
let func = call_expr["func"]
|
||||
let fk: String = func["expr"]
|
||||
if !str_eq(fk, "Ident") { return false }
|
||||
let name: String = func["name"]
|
||||
if str_eq(name, "local_datetime") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fn is_zone_call(call_expr: Map<String, Any>) -> Bool {
|
||||
let func = call_expr["func"]
|
||||
let fk: String = func["expr"]
|
||||
if !str_eq(fk, "Ident") { return false }
|
||||
let name: String = func["name"]
|
||||
if str_eq(name, "zone") { return true }
|
||||
if str_eq(name, "zone_utc") { return true }
|
||||
if str_eq(name, "zone_local") { return true }
|
||||
if str_eq(name, "zone_offset") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fn is_calendar_expr(expr: Map<String, Any>) -> Bool {
|
||||
let k: String = expr["expr"]
|
||||
if str_eq(k, "Ident") { return is_calendar_name(expr["name"]) }
|
||||
if str_eq(k, "Call") { return is_calendar_call(expr) }
|
||||
return false
|
||||
}
|
||||
|
||||
fn is_caltime_expr(expr: Map<String, Any>) -> Bool {
|
||||
let k: String = expr["expr"]
|
||||
if str_eq(k, "Ident") { return is_caltime_name(expr["name"]) }
|
||||
if str_eq(k, "Call") { return is_caltime_call(expr) }
|
||||
return false
|
||||
}
|
||||
|
||||
fn is_rhythm_expr(expr: Map<String, Any>) -> Bool {
|
||||
let k: String = expr["expr"]
|
||||
if str_eq(k, "Ident") { return is_rhythm_name(expr["name"]) }
|
||||
if str_eq(k, "Call") { return is_rhythm_call(expr) }
|
||||
return false
|
||||
}
|
||||
|
||||
fn is_localdate_expr(expr: Map<String, Any>) -> Bool {
|
||||
let k: String = expr["expr"]
|
||||
if str_eq(k, "Ident") { return is_localdate_name(expr["name"]) }
|
||||
if str_eq(k, "Call") { return is_localdate_call(expr) }
|
||||
return false
|
||||
}
|
||||
|
||||
fn is_localtime_expr(expr: Map<String, Any>) -> Bool {
|
||||
let k: String = expr["expr"]
|
||||
if str_eq(k, "Ident") { return is_localtime_name(expr["name"]) }
|
||||
if str_eq(k, "Call") { return is_localtime_call(expr) }
|
||||
return false
|
||||
}
|
||||
|
||||
fn is_localdt_expr(expr: Map<String, Any>) -> Bool {
|
||||
let k: String = expr["expr"]
|
||||
if str_eq(k, "Ident") { return is_localdt_name(expr["name"]) }
|
||||
if str_eq(k, "Call") { return is_localdt_call(expr) }
|
||||
return false
|
||||
}
|
||||
|
||||
fn is_zone_expr(expr: Map<String, Any>) -> Bool {
|
||||
let k: String = expr["expr"]
|
||||
if str_eq(k, "Ident") { return is_zone_name(expr["name"]) }
|
||||
if str_eq(k, "Call") { return is_zone_call(expr) }
|
||||
return false
|
||||
}
|
||||
|
||||
// Recursive type predicates for Instant / Duration. Mirror is_int_expr.
|
||||
// is_instant_expr / is_duration_expr return true only when the expression
|
||||
// is provably of that type at codegen time. Anything ambiguous returns
|
||||
@@ -1810,10 +2082,80 @@ fn add_duration_name(name: String) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
fn add_calendar_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__calendar_names")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__calendar_names", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn add_caltime_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__caltime_names")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__caltime_names", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn add_rhythm_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__rhythm_names")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__rhythm_names", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn add_localdate_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__localdate_names")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__localdate_names", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn add_localtime_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__localtime_names")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__localtime_names", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn add_localdt_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__localdt_names")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__localdt_names", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn add_zone_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__zone_names")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__zone_names", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
|
||||
state_set("__int_names", ",")
|
||||
state_set("__instant_names", ",")
|
||||
state_set("__duration_names", ",")
|
||||
state_set("__calendar_names", ",")
|
||||
state_set("__caltime_names", ",")
|
||||
state_set("__rhythm_names", ",")
|
||||
state_set("__localdate_names", ",")
|
||||
state_set("__localtime_names", ",")
|
||||
state_set("__localdt_names", ",")
|
||||
state_set("__zone_names", ",")
|
||||
let np: Int = native_list_len(params)
|
||||
let pi = 0
|
||||
while pi < np {
|
||||
@@ -1829,6 +2171,27 @@ fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
|
||||
if str_eq(ptype, "Duration") {
|
||||
add_duration_name(pname)
|
||||
}
|
||||
if str_eq(ptype, "Calendar") {
|
||||
add_calendar_name(pname)
|
||||
}
|
||||
if str_eq(ptype, "CalendarTime") {
|
||||
add_caltime_name(pname)
|
||||
}
|
||||
if str_eq(ptype, "Rhythm") {
|
||||
add_rhythm_name(pname)
|
||||
}
|
||||
if str_eq(ptype, "LocalDate") {
|
||||
add_localdate_name(pname)
|
||||
}
|
||||
if str_eq(ptype, "LocalTime") {
|
||||
add_localtime_name(pname)
|
||||
}
|
||||
if str_eq(ptype, "LocalDateTime") {
|
||||
add_localdt_name(pname)
|
||||
}
|
||||
if str_eq(ptype, "Zone") {
|
||||
add_zone_name(pname)
|
||||
}
|
||||
let pi = pi + 1
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// cross-calendar-comparison.el — same Instant under two different calendars
|
||||
// produces identical cal_to_instant() outputs. Calendar choice does not change
|
||||
// the underlying instant — only the projection.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let i: Instant = unix_seconds(1782216000)
|
||||
let z: Zone = zone("America/New_York")
|
||||
let earth: Calendar = earth_calendar(z)
|
||||
let mars: Calendar = mars_calendar()
|
||||
let ct_earth: CalendarTime = in_calendar(i, earth)
|
||||
let ct_mars: CalendarTime = in_calendar(i, mars)
|
||||
let i_earth: Instant = cal_to_instant(ct_earth)
|
||||
let i_mars: Instant = cal_to_instant(ct_mars)
|
||||
if i_earth == i_mars {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// cycle-300yr.el — CycleCalendar with a 100-year period proves the math
|
||||
// holds at long periods. (300 years exceeds int64 nanos: 2^63 ns ≈ 292 yr;
|
||||
// 100 yr is the largest round period that fits while leaving headroom for
|
||||
// instants on either side.) One earth year apart yields phase_diff ~ 0.01.
|
||||
|
||||
fn run_test() -> String {
|
||||
// 100 Julian years = 100 * 31557600 = 3155760000 seconds.
|
||||
let period: Duration = duration_seconds(3155760000)
|
||||
let cal: Calendar = cycle_calendar(period)
|
||||
let base: Instant = unix_seconds(0)
|
||||
let later: Instant = unix_seconds(31557600)
|
||||
let ct1: CalendarTime = in_calendar(base, cal)
|
||||
let ct2: CalendarTime = in_calendar(later, cal)
|
||||
let p1: Float = cal_cycle_phase(ct1)
|
||||
let p2: Float = cal_cycle_phase(ct2)
|
||||
let diff: Float = p2 - p1
|
||||
// 1 year / 100 years = 0.01
|
||||
return format_float(diff, 2)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// cycle-30hr.el — CycleCalendar with a 30-hour period.
|
||||
// Two CalendarTimes 15 hours apart should have cycle_phase differ by 0.5.
|
||||
// We compare phases via float subtraction with format_float for determinism.
|
||||
|
||||
fn run_test() -> String {
|
||||
let period: Duration = 30.hours
|
||||
let cal: Calendar = cycle_calendar(period)
|
||||
let base: Instant = unix_seconds(0)
|
||||
let later: Instant = base + 15.hours
|
||||
let ct1: CalendarTime = in_calendar(base, cal)
|
||||
let ct2: CalendarTime = in_calendar(later, cal)
|
||||
let p1: Float = cal_cycle_phase(ct1)
|
||||
let p2: Float = cal_cycle_phase(ct2)
|
||||
let diff: Float = p2 - p1
|
||||
return format_float(diff, 1)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// dst-spring-forward.el — Earth calendar handles the DST transition.
|
||||
// 2026 spring DST: March 8 at 02:00 EST → clocks jump to 03:00 EDT.
|
||||
// 2026-03-08 06:30 UTC = 01:30 EST (just before the jump).
|
||||
// Add 1 hour → 07:30 UTC = 03:30 EDT (the wall clock skipped 02:30 entirely).
|
||||
|
||||
fn run_test() -> String {
|
||||
let z: Zone = zone("America/New_York")
|
||||
let cal: Calendar = earth_calendar(z)
|
||||
let i: Instant = unix_seconds(1772951400)
|
||||
let later: Instant = i + 1.hour
|
||||
let ct: CalendarTime = in_calendar(later, cal)
|
||||
return cal_format(ct, "HH:mm z")
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// earth-zone.el — EarthCalendar(zone) formats with the right zone abbreviation.
|
||||
// We use a fixed Instant on July 4, 2026 (definitely EDT in NYC) so the
|
||||
// abbreviation is deterministic across runs.
|
||||
|
||||
fn run_test() -> String {
|
||||
let z: Zone = zone("America/New_York")
|
||||
let cal: Calendar = earth_calendar(z)
|
||||
// 2026-07-04 12:00:00 UTC = 2026-07-04 08:00:00 EDT
|
||||
let i: Instant = unix_seconds(1782216000)
|
||||
let ct: CalendarTime = in_calendar(i, cal)
|
||||
return cal_format(ct, "z")
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// local-date-arithmetic.el — LocalDate + Duration produces a LocalDate.
|
||||
// 2026-05-28 + 7 days crosses the May/June boundary, yielding 2026-06-04.
|
||||
|
||||
fn run_test() -> String {
|
||||
let d1: LocalDate = local_date(2026, 5, 28)
|
||||
let week: Duration = 7.days
|
||||
let d2: LocalDate = d1 + week
|
||||
let y: Int = local_date_year(d2)
|
||||
let m: Int = local_date_month(d2)
|
||||
let day: Int = local_date_day(d2)
|
||||
return int_to_str(y) + "-" + int_to_str(m) + "-" + int_to_str(day)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// mars-calendar.el — MarsCalendar uses sol period 88775.244 seconds.
|
||||
// Two instants exactly one sol apart should differ by 1 in sol number.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let cal: Calendar = mars_calendar()
|
||||
let base: Instant = unix_seconds(0)
|
||||
// 88775.244 s * 1e9 nanos = 88775244000000 nanos
|
||||
let one_sol_ns: Int = 88775244000000
|
||||
let one_sol: Duration = el_duration_from_nanos(one_sol_ns)
|
||||
let later: Instant = base + one_sol
|
||||
let ct1: CalendarTime = in_calendar(base, cal)
|
||||
let ct2: CalendarTime = in_calendar(later, cal)
|
||||
let sol1: String = cal_format(ct1, "%sol")
|
||||
let sol2: String = cal_format(ct2, "%sol")
|
||||
let diff: Int = str_to_int(sol2) - str_to_int(sol1)
|
||||
return diff
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// no-cycle.el — NoCycleCalendar's cycle_phase returns NaN sentinel.
|
||||
// Detection via float_to_str — NaN renders as "nan" under %g.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let cal: Calendar = no_cycle_calendar()
|
||||
let i: Instant = unix_seconds(1000000000)
|
||||
let ct: CalendarTime = in_calendar(i, cal)
|
||||
let p: Float = cal_cycle_phase(ct)
|
||||
let s: String = float_to_str(p)
|
||||
if str_eq(s, "nan") {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// rhythm-cycle-30hr.el — rhythm_cycle_phase(0.5) under CycleCalendar(30.hours)
|
||||
// returns the next instant at the 15-hour mark of the cycle.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let period: Duration = 30.hours
|
||||
let cal: Calendar = cycle_calendar(period)
|
||||
let r: Rhythm = rhythm_cycle_phase(0.5)
|
||||
let base: Instant = unix_seconds(0)
|
||||
let next: Instant = rhythm_next_after(r, base, cal)
|
||||
let elapsed_ns: Duration = next - base
|
||||
let elapsed_secs: Int = duration_to_seconds(elapsed_ns)
|
||||
// 15 hours = 54000 seconds.
|
||||
return elapsed_secs
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// rhythm-grounding.el — Mondays at 9am, grounded against EarthCalendar(NYC),
|
||||
// from a Wednesday timestamp returns the next Monday at 9am EDT.
|
||||
// Wednesday 2026-05-06 00:00 UTC (1778025600) → next Monday 9am EDT
|
||||
// = 2026-05-11 09:00 EDT = 2026-05-11 13:00 UTC = 1778504400.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let z: Zone = zone("America/New_York")
|
||||
let cal: Calendar = earth_calendar(z)
|
||||
let r: Rhythm = rhythm_weekly_at(1, 9, 0)
|
||||
let after: Instant = unix_seconds(1778025600)
|
||||
let next: Instant = rhythm_next_after(r, after, cal)
|
||||
let next_secs: Int = instant_to_unix_seconds(next)
|
||||
return next_secs
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
# run.sh — build and execute the calendar/ acceptance corpus.
|
||||
#
|
||||
# Each examples/<case>.el is a self-contained El program with a fn main()
|
||||
# that prints a single deterministic result line. The runner compiles each
|
||||
# via the canonical native elc, links it against the shared C runtime, runs
|
||||
# it, and asserts the output matches the expected value.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
|
||||
ELC="${EL_HOME}/dist/platform/elc"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
FAILED_NAMES=()
|
||||
|
||||
# run_runtime_case <name> <source> <expected> [<extra>]
|
||||
run_runtime_case() {
|
||||
local name="$1"
|
||||
local src="$2"
|
||||
local expected="$3"
|
||||
local mode="${4:-exact}"
|
||||
|
||||
local out_c
|
||||
local out_bin
|
||||
out_c="$(mktemp -t cal_test.XXXXXX).c"
|
||||
out_bin="$(mktemp -t cal_test.XXXXXX)"
|
||||
|
||||
if ! "${ELC}" "${src}" > "${out_c}" 2>/tmp/cal_test.elc.err; then
|
||||
echo "FAIL ${name} — elc emit failed:"
|
||||
cat /tmp/cal_test.elc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o "${out_bin}" 2>/tmp/cal_test.cc.err; then
|
||||
echo "FAIL ${name} — cc failed:"
|
||||
cat /tmp/cal_test.cc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
return
|
||||
fi
|
||||
|
||||
local got
|
||||
got="$("${out_bin}" 2>&1 | tr -d '[:space:]')"
|
||||
|
||||
if [ "${mode}" = "either" ]; then
|
||||
local ok=0
|
||||
local IFS=','
|
||||
for choice in ${expected}; do
|
||||
if [ "${got}" = "${choice}" ]; then ok=1; break; fi
|
||||
done
|
||||
if [ "${ok}" = "1" ]; then
|
||||
echo "PASS ${name} (got: ${got})"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL ${name} expected one of {${expected}}, got: ${got}"
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
fi
|
||||
else
|
||||
if [ "${got}" = "${expected}" ]; then
|
||||
echo "PASS ${name}"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL ${name} expected: ${expected}, got: ${got}"
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
}
|
||||
|
||||
echo "==> Running calendar/ acceptance corpus"
|
||||
echo
|
||||
|
||||
run_runtime_case "earth-zone" examples/earth-zone.el "EDT"
|
||||
run_runtime_case "dst-spring-forward" examples/dst-spring-forward.el "03:30EDT"
|
||||
run_runtime_case "mars-calendar" examples/mars-calendar.el "1"
|
||||
run_runtime_case "cycle-30hr" examples/cycle-30hr.el "0.5"
|
||||
run_runtime_case "cycle-300yr" examples/cycle-300yr.el "0.01"
|
||||
run_runtime_case "no-cycle" examples/no-cycle.el "1"
|
||||
run_runtime_case "cross-calendar-comparison" examples/cross-calendar-comparison.el "1"
|
||||
run_runtime_case "local-date-arithmetic" examples/local-date-arithmetic.el "2026-6-4"
|
||||
run_runtime_case "rhythm-grounding" examples/rhythm-grounding.el "1778504400"
|
||||
run_runtime_case "rhythm-cycle-30hr" examples/rhythm-cycle-30hr.el "54000"
|
||||
|
||||
echo
|
||||
echo "${PASS} passed, ${FAIL} failed"
|
||||
if [ "${FAIL}" -gt 0 ]; then
|
||||
echo "failed: ${FAILED_NAMES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,13 @@
|
||||
// runner.el — entry point for the calendar/ acceptance corpus.
|
||||
//
|
||||
// Each calendar/examples/*.el is its own El program with its own fn main().
|
||||
// Compile, link, and run-output-diff is handled by tests/calendar/run.sh —
|
||||
// this file is the El-side stub kept for pattern parity.
|
||||
//
|
||||
// Run from the calendar/ directory:
|
||||
// ./run.sh
|
||||
|
||||
fn main() -> Void {
|
||||
println("calendar/ acceptance corpus is driven by run.sh — invoke that directly.")
|
||||
println("Each examples/*.el is a self-contained program; runner.el is a parity stub.")
|
||||
}
|
||||
Reference in New Issue
Block a user