seq_runtime/
time_ops.rs

1//! Time operations for Seq
2//!
3//! Provides timing primitives for performance measurement and delays.
4//!
5//! # Usage from Seq
6//!
7//! ```seq
8//! time.now      # ( -- Int ) microseconds since epoch
9//! time.nanos    # ( -- Int ) nanoseconds (monotonic, for timing)
10//! 100 time.sleep-ms  # ( Int -- ) sleep for N milliseconds
11//! ```
12//!
13//! # Example: Measuring execution time
14//!
15//! ```seq
16//! : benchmark ( -- )
17//!   time.nanos    # start time
18//!   do-work
19//!   time.nanos    # end time
20//!   swap -        # elapsed nanos
21//!   1000000 /     # convert to ms
22//!   "Elapsed: " write
23//!   int->string write
24//!   "ms" write-line
25//! ;
26//! ```
27
28use crate::stack::{Stack, pop, push};
29use crate::value::Value;
30use std::time::{Duration, SystemTime, UNIX_EPOCH};
31
32/// Get current time in microseconds since Unix epoch
33///
34/// Stack effect: ( -- Int )
35///
36/// Returns wall-clock time. Good for timestamps.
37/// For measuring durations, prefer `time.nanos` which uses a monotonic clock.
38///
39/// # Safety
40/// - `stack` must be a valid stack pointer (may be null for empty stack)
41#[unsafe(no_mangle)]
42pub unsafe extern "C" fn patch_seq_time_now(stack: Stack) -> Stack {
43    let micros = SystemTime::now()
44        .duration_since(UNIX_EPOCH)
45        .map(|d| d.as_micros() as i64)
46        .unwrap_or(0);
47
48    unsafe { push(stack, Value::Int(micros)) }
49}
50
51/// Get monotonic nanoseconds for precise timing
52///
53/// Stack effect: ( -- Int )
54///
55/// Returns nanoseconds elapsed since the first call to this function.
56/// Uses CLOCK_MONOTONIC for thread-independent consistent values.
57/// Values start near zero for easier arithmetic.
58///
59/// # Safety
60/// - `stack` must be a valid stack pointer (may be null for empty stack)
61#[unsafe(no_mangle)]
62pub unsafe extern "C" fn patch_seq_time_nanos(stack: Stack) -> Stack {
63    let nanos = elapsed_nanos();
64    unsafe { push(stack, Value::Int(nanos)) }
65}
66
67/// Get elapsed nanoseconds since program start.
68///
69/// Thread-safe, consistent across all threads. Uses a lazily-initialized
70/// base time to ensure values start near zero.
71#[inline]
72fn elapsed_nanos() -> i64 {
73    use std::sync::atomic::{AtomicI64, Ordering};
74
75    // Base time is initialized on first call (value 0 means uninitialized)
76    static BASE_NANOS: AtomicI64 = AtomicI64::new(0);
77
78    let current = raw_monotonic_nanos();
79
80    // Try to read existing base time
81    let base = BASE_NANOS.load(Ordering::Relaxed);
82    if base != 0 {
83        return current.saturating_sub(base);
84    }
85
86    // First call: try to set the base time
87    match BASE_NANOS.compare_exchange(0, current, Ordering::Relaxed, Ordering::Relaxed) {
88        Ok(_) => 0,                                              // We set the base, elapsed is 0
89        Err(actual_base) => current.saturating_sub(actual_base), // Another thread set it
90    }
91}
92
93/// Get raw monotonic nanoseconds from the system clock.
94///
95/// On Unix: Uses `clock_gettime(CLOCK_MONOTONIC)` directly to get absolute
96/// nanoseconds since boot. This is thread-independent - the same value is
97/// returned regardless of which OS thread calls it.
98///
99/// On Windows: Falls back to `Instant::now()` with a process-wide base time.
100#[inline]
101#[cfg(unix)]
102fn raw_monotonic_nanos() -> i64 {
103    let mut ts = libc::timespec {
104        tv_sec: 0,
105        tv_nsec: 0,
106    };
107    // SAFETY: ts is a valid pointer to a timespec struct
108    unsafe {
109        libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts);
110    }
111    // Convert to nanoseconds, saturating at i64::MAX
112    // Explicit i64 casts for portability (tv_sec/tv_nsec types vary by platform)
113    #[allow(clippy::unnecessary_cast)] // Required for 32-bit platforms
114    let secs = (ts.tv_sec as i64).saturating_mul(1_000_000_000);
115    #[allow(clippy::unnecessary_cast)]
116    secs.saturating_add(ts.tv_nsec as i64)
117}
118
119/// Windows fallback using Instant with a process-wide base time.
120/// Uses OnceLock for thread-safe one-time initialization.
121#[inline]
122#[cfg(not(unix))]
123fn raw_monotonic_nanos() -> i64 {
124    use std::sync::OnceLock;
125    use std::time::Instant;
126
127    static BASE: OnceLock<Instant> = OnceLock::new();
128    let base = BASE.get_or_init(Instant::now);
129    base.elapsed().as_nanos().try_into().unwrap_or(i64::MAX)
130}
131
132/// Sleep for a specified number of milliseconds
133///
134/// Stack effect: ( Int -- )
135///
136/// Yields the current strand to the scheduler while sleeping.
137/// Uses `may::coroutine::sleep` for cooperative scheduling.
138///
139/// # Safety
140/// - `stack` must be a valid, non-null stack pointer with an Int on top
141#[unsafe(no_mangle)]
142pub unsafe extern "C" fn patch_seq_time_sleep_ms(stack: Stack) -> Stack {
143    assert!(!stack.is_null(), "time.sleep-ms: stack is empty");
144
145    let (rest, value) = unsafe { pop(stack) };
146
147    match value {
148        Value::Int(ms) => {
149            if ms < 0 {
150                panic!("time.sleep-ms: duration must be non-negative, got {}", ms);
151            }
152
153            // Use may's coroutine-aware sleep for cooperative scheduling
154            may::coroutine::sleep(Duration::from_millis(ms as u64));
155
156            rest
157        }
158        _ => panic!(
159            "time.sleep-ms: expected Int duration on stack, got {:?}",
160            value
161        ),
162    }
163}
164
165// Public re-exports
166pub use patch_seq_time_nanos as time_nanos;
167pub use patch_seq_time_now as time_now;
168pub use patch_seq_time_sleep_ms as time_sleep_ms;
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::stack::pop;
174    use std::time::Instant;
175
176    #[test]
177    fn test_time_now_returns_positive() {
178        unsafe {
179            let stack = crate::stack::alloc_test_stack();
180            let stack = patch_seq_time_now(stack);
181            let (_, value) = pop(stack);
182
183            match value {
184                Value::Int(micros) => {
185                    // Should be a reasonable timestamp (after year 2020)
186                    assert!(micros > 1_577_836_800_000_000); // 2020-01-01
187                }
188                _ => panic!("Expected Int"),
189            }
190        }
191    }
192
193    #[test]
194    fn test_time_nanos_monotonic() {
195        unsafe {
196            let stack = crate::stack::alloc_test_stack();
197            let stack = patch_seq_time_nanos(stack);
198            let (_, value1) = pop(stack);
199
200            // Small delay
201            std::thread::sleep(Duration::from_micros(100));
202
203            let stack = crate::stack::alloc_test_stack();
204            let stack = patch_seq_time_nanos(stack);
205            let (_, value2) = pop(stack);
206
207            match (value1, value2) {
208                (Value::Int(t1), Value::Int(t2)) => {
209                    assert!(t2 > t1, "time.nanos should be monotonically increasing");
210                }
211                _ => panic!("Expected Int values"),
212            }
213        }
214    }
215
216    #[test]
217    fn test_time_nanos_cross_thread() {
218        // Verify raw_monotonic_nanos is consistent across threads
219        use std::sync::mpsc;
220        use std::thread;
221
222        let (tx1, rx1) = mpsc::channel();
223        let (tx2, rx2) = mpsc::channel();
224
225        // Get time on main thread
226        let t1 = raw_monotonic_nanos();
227
228        // Spawn thread, get time there
229        let handle = thread::spawn(move || {
230            let t2 = raw_monotonic_nanos();
231            tx1.send(t2).unwrap();
232            rx2.recv().unwrap() // wait for main to continue
233        });
234
235        let t2 = rx1.recv().unwrap();
236
237        // Get time on main thread again
238        let t3 = raw_monotonic_nanos();
239        tx2.send(()).unwrap();
240        handle.join().unwrap();
241
242        // All times should be monotonically increasing
243        assert!(t2 > t1, "t2 ({}) should be > t1 ({})", t2, t1);
244        assert!(t3 > t2, "t3 ({}) should be > t2 ({})", t3, t2);
245    }
246
247    #[test]
248    fn test_time_sleep_ms() {
249        unsafe {
250            // Push 1ms sleep duration
251            let stack = crate::stack::alloc_test_stack();
252            let stack = push(stack, Value::Int(1));
253
254            let start = Instant::now();
255            let _stack = patch_seq_time_sleep_ms(stack);
256            let elapsed = start.elapsed();
257
258            // Should sleep at least 1ms
259            assert!(elapsed >= Duration::from_millis(1));
260            // Stack should be empty after popping the duration
261        }
262    }
263}