diff --git a/dist/platform/elc b/dist/platform/elc index b4d96ab..7992a4e 100755 Binary files a/dist/platform/elc and b/dist/platform/elc differ diff --git a/dist/platform/elc.20260502-1321-self-host b/dist/platform/elc.20260502-1321-self-host new file mode 100755 index 0000000..7992a4e Binary files /dev/null and b/dist/platform/elc.20260502-1321-self-host differ diff --git a/el-compiler/runtime/el_runtime.c b/el-compiler/runtime/el_runtime.c index 4c5a2d3..da8ab3f 100644 --- a/el-compiler/runtime/el_runtime.c +++ b/el-compiler/runtime/el_runtime.c @@ -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; diff --git a/el-compiler/runtime/el_runtime.h b/el-compiler/runtime/el_runtime.h index 6939d01..a5c2733 100644 --- a/el-compiler/runtime/el_runtime.h +++ b/el-compiler/runtime/el_runtime.h @@ -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); diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index 5d1d9dc..a40d04d 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -202,6 +202,51 @@ fn cg_expr(expr: Map) -> 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, 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, 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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]) -> 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]) -> 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 diff --git a/tests/calendar/examples/cross-calendar-comparison.el b/tests/calendar/examples/cross-calendar-comparison.el new file mode 100644 index 0000000..98ebb89 --- /dev/null +++ b/tests/calendar/examples/cross-calendar-comparison.el @@ -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())) +} diff --git a/tests/calendar/examples/cycle-300yr.el b/tests/calendar/examples/cycle-300yr.el new file mode 100644 index 0000000..1d7e80e --- /dev/null +++ b/tests/calendar/examples/cycle-300yr.el @@ -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()) +} diff --git a/tests/calendar/examples/cycle-30hr.el b/tests/calendar/examples/cycle-30hr.el new file mode 100644 index 0000000..4355d5e --- /dev/null +++ b/tests/calendar/examples/cycle-30hr.el @@ -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()) +} diff --git a/tests/calendar/examples/dst-spring-forward.el b/tests/calendar/examples/dst-spring-forward.el new file mode 100644 index 0000000..31b50a1 --- /dev/null +++ b/tests/calendar/examples/dst-spring-forward.el @@ -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()) +} diff --git a/tests/calendar/examples/earth-zone.el b/tests/calendar/examples/earth-zone.el new file mode 100644 index 0000000..6ae7505 --- /dev/null +++ b/tests/calendar/examples/earth-zone.el @@ -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()) +} diff --git a/tests/calendar/examples/local-date-arithmetic.el b/tests/calendar/examples/local-date-arithmetic.el new file mode 100644 index 0000000..19d5f14 --- /dev/null +++ b/tests/calendar/examples/local-date-arithmetic.el @@ -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()) +} diff --git a/tests/calendar/examples/mars-calendar.el b/tests/calendar/examples/mars-calendar.el new file mode 100644 index 0000000..138b00c --- /dev/null +++ b/tests/calendar/examples/mars-calendar.el @@ -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())) +} diff --git a/tests/calendar/examples/no-cycle.el b/tests/calendar/examples/no-cycle.el new file mode 100644 index 0000000..0d374d0 --- /dev/null +++ b/tests/calendar/examples/no-cycle.el @@ -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())) +} diff --git a/tests/calendar/examples/rhythm-cycle-30hr.el b/tests/calendar/examples/rhythm-cycle-30hr.el new file mode 100644 index 0000000..e02e9ac --- /dev/null +++ b/tests/calendar/examples/rhythm-cycle-30hr.el @@ -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())) +} diff --git a/tests/calendar/examples/rhythm-grounding.el b/tests/calendar/examples/rhythm-grounding.el new file mode 100644 index 0000000..3788214 --- /dev/null +++ b/tests/calendar/examples/rhythm-grounding.el @@ -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())) +} diff --git a/tests/calendar/run.sh b/tests/calendar/run.sh new file mode 100755 index 0000000..fd3e49d --- /dev/null +++ b/tests/calendar/run.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# run.sh — build and execute the calendar/ acceptance corpus. +# +# Each examples/.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 [] +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 diff --git a/tests/calendar/runner.el b/tests/calendar/runner.el new file mode 100644 index 0000000..0b58c77 --- /dev/null +++ b/tests/calendar/runner.el @@ -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.") +}