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:
@@ -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