Skip to main content

logicaffeine_compile/semantics/
temporal.rs

1//! Calendar arithmetic (Howard Hinnant's algorithms) and the clock.
2
3use std::cell::Cell;
4
5use crate::interpreter::RuntimeValue;
6
7thread_local! {
8    /// Test-clock override: (days since epoch, nanos since epoch). When set,
9    /// `today`/`now` read it instead of the system clock — differential tests
10    /// inject it so both engines see the same instant.
11    static FIXED_CLOCK: Cell<Option<(i32, i64)>> = const { Cell::new(None) };
12}
13
14/// Pin `today`/`now` to a fixed instant (tests).
15pub fn set_fixed_clock(days_since_epoch: i32, nanos_since_epoch: i64) {
16    FIXED_CLOCK.with(|c| c.set(Some((days_since_epoch, nanos_since_epoch))));
17}
18
19/// Restore the system clock.
20pub fn clear_fixed_clock() {
21    FIXED_CLOCK.with(|c| c.set(None));
22}
23
24/// The `today` builtin identifier.
25pub fn today() -> RuntimeValue {
26    if let Some((days, _)) = FIXED_CLOCK.with(|c| c.get()) {
27        return RuntimeValue::Date(days);
28    }
29    #[cfg(not(target_arch = "wasm32"))]
30    {
31        use std::time::{SystemTime, UNIX_EPOCH};
32        let duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
33        RuntimeValue::Date((duration.as_secs() / 86400) as i32)
34    }
35    #[cfg(target_arch = "wasm32")]
36    {
37        RuntimeValue::Date(0) // Placeholder for WASM
38    }
39}
40
41/// The `now` builtin identifier.
42pub fn now() -> RuntimeValue {
43    if let Some((_, nanos)) = FIXED_CLOCK.with(|c| c.get()) {
44        return RuntimeValue::Moment(nanos);
45    }
46    #[cfg(not(target_arch = "wasm32"))]
47    {
48        use std::time::{SystemTime, UNIX_EPOCH};
49        let duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
50        RuntimeValue::Moment(duration.as_nanos() as i64)
51    }
52    #[cfg(target_arch = "wasm32")]
53    {
54        RuntimeValue::Moment(0) // Placeholder for WASM
55    }
56}
57
58/// Add months and days to a date (calendar-aware).
59/// Uses Howard Hinnant's date algorithms for correct month-end handling
60/// (e.g. Jan 31 + 1 month → Feb 28/29).
61pub fn date_add_span(days_since_epoch: i32, months: i32, days: i32) -> i32 {
62    // Convert days since epoch to (year, month, day).
63    let z = days_since_epoch + 719468;
64    let era = if z >= 0 { z / 146097 } else { (z - 146096) / 146097 };
65    let doe = (z - era * 146097) as u32;
66    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
67    let y = yoe as i32 + era * 400;
68    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
69    let mp = (5 * doy + 2) / 153;
70    let d = doy - (153 * mp + 2) / 5 + 1;
71    let m = if mp < 10 { mp + 3 } else { mp - 9 };
72    let mut year = y + if m <= 2 { 1 } else { 0 };
73    let mut month = m as i32;
74    let mut day = d as i32;
75
76    // Add months (wrapping i32 — the Int spec applies to span math too).
77    let total_months = year.wrapping_mul(12).wrapping_add(month - 1).wrapping_add(months);
78    year = total_months / 12;
79    month = total_months % 12 + 1;
80    if month <= 0 {
81        month += 12;
82        year -= 1;
83    }
84
85    // Clamp day to valid range for the new month.
86    let dim = days_in_month(year, month);
87    if day > dim {
88        day = dim;
89    }
90
91    // Convert back to days since epoch.
92    let yp = year - if month <= 2 { 1 } else { 0 };
93    let era2 = if yp >= 0 { yp / 400 } else { (yp - 399) / 400 };
94    let yoe2 = (yp - era2 * 400) as u32;
95    let mp2 = if month > 2 { month as u32 - 3 } else { month as u32 + 9 };
96    let doy2 = (153 * mp2 + 2) / 5 + day as u32 - 1;
97    let doe2 = yoe2 * 365 + yoe2 / 4 - yoe2 / 100 + doy2;
98    let result = era2 * 146097 + doe2 as i32 - 719468;
99
100    // Add days.
101    result.wrapping_add(days)
102}
103
104/// Add a **calendar span** (`months` then `days`) to a SmoothUTC instant — the *civil* (wall-clock)
105/// operation: months clamp at end-of-month and respect leap years, the time-of-day rides along
106/// untouched. Distinct from adding a physical `Duration` (which is just nanosecond arithmetic).
107/// Delegates to the same `base::temporal::add_span` the AOT mirror uses, so the tiers cannot diverge.
108pub fn moment_add_span(nanos_since_epoch: i64, months: i32, days: i32) -> i64 {
109    let dt = logicaffeine_base::temporal::civil_from_unix_nanos(nanos_since_epoch);
110    let shifted = logicaffeine_base::temporal::add_span(dt, months as i64, days as i64);
111    logicaffeine_base::temporal::unix_nanos_from_civil(shifted)
112}
113
114/// The number of days in a given month (1-indexed).
115pub fn days_in_month(year: i32, month: i32) -> i32 {
116    match month {
117        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
118        4 | 6 | 9 | 11 => 30,
119        2 => {
120            let is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
121            if is_leap {
122                29
123            } else {
124                28
125            }
126        }
127        _ => 30, // Fallback
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn month_end_clamps() {
137        // 2024-01-31 is day 19753 since epoch; +1 month clamps to 2024-02-29 (leap).
138        let jan31_2024 = 19753;
139        let feb29_2024 = date_add_span(jan31_2024, 1, 0);
140        assert_eq!(feb29_2024 - jan31_2024, 29);
141    }
142
143    #[test]
144    fn days_in_month_handles_leap_years() {
145        assert_eq!(days_in_month(2024, 2), 29);
146        assert_eq!(days_in_month(2023, 2), 28);
147        assert_eq!(days_in_month(2000, 2), 29);
148        assert_eq!(days_in_month(1900, 2), 28);
149        assert_eq!(days_in_month(2024, 4), 30);
150        assert_eq!(days_in_month(2024, 12), 31);
151    }
152}