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 from system boot (CLOCK_MONOTONIC).
56/// Uses raw clock_gettime for consistent values across all threads -
57/// critical for timing when coroutines migrate between OS threads.
58///
59/// Note: Saturates at i64::MAX (~292 years of uptime) to prevent overflow.
60///
61/// # Safety
62/// - `stack` must be a valid stack pointer (may be null for empty stack)
63#[unsafe(no_mangle)]
64pub unsafe extern "C" fn patch_seq_time_nanos(stack: Stack) -> Stack {
65 let nanos = monotonic_nanos();
66 unsafe { push(stack, Value::Int(nanos)) }
67}
68
69/// Get raw monotonic nanoseconds from the system clock.
70///
71/// On Unix: Uses `clock_gettime(CLOCK_MONOTONIC)` directly to get absolute
72/// nanoseconds since boot. This is thread-independent - the same value is
73/// returned regardless of which OS thread calls it.
74///
75/// On Windows: Falls back to `Instant::now()` with a process-wide base time.
76/// This has a one-time initialization cost but is still thread-safe.
77#[inline]
78#[cfg(unix)]
79fn monotonic_nanos() -> i64 {
80 let mut ts = libc::timespec {
81 tv_sec: 0,
82 tv_nsec: 0,
83 };
84 // SAFETY: ts is a valid pointer to a timespec struct
85 unsafe {
86 libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts);
87 }
88 // Convert to nanoseconds, saturating at i64::MAX
89 // Explicit i64 casts for portability (tv_sec/tv_nsec types vary by platform)
90 #[allow(clippy::unnecessary_cast)] // Required for 32-bit platforms
91 let secs = (ts.tv_sec as i64).saturating_mul(1_000_000_000);
92 #[allow(clippy::unnecessary_cast)]
93 secs.saturating_add(ts.tv_nsec as i64)
94}
95
96/// Windows fallback using Instant with a process-wide base time.
97/// Uses OnceLock for thread-safe one-time initialization.
98#[inline]
99#[cfg(not(unix))]
100fn monotonic_nanos() -> i64 {
101 use std::sync::OnceLock;
102 use std::time::Instant;
103
104 static BASE: OnceLock<Instant> = OnceLock::new();
105 let base = BASE.get_or_init(Instant::now);
106 base.elapsed().as_nanos().try_into().unwrap_or(i64::MAX)
107}
108
109/// Sleep for a specified number of milliseconds
110///
111/// Stack effect: ( Int -- )
112///
113/// Yields the current strand to the scheduler while sleeping.
114/// Uses `may::coroutine::sleep` for cooperative scheduling.
115///
116/// # Safety
117/// - `stack` must be a valid, non-null stack pointer with an Int on top
118#[unsafe(no_mangle)]
119pub unsafe extern "C" fn patch_seq_time_sleep_ms(stack: Stack) -> Stack {
120 assert!(!stack.is_null(), "time.sleep-ms: stack is empty");
121
122 let (rest, value) = unsafe { pop(stack) };
123
124 match value {
125 Value::Int(ms) => {
126 if ms < 0 {
127 panic!("time.sleep-ms: duration must be non-negative, got {}", ms);
128 }
129
130 // Use may's coroutine-aware sleep for cooperative scheduling
131 may::coroutine::sleep(Duration::from_millis(ms as u64));
132
133 rest
134 }
135 _ => panic!(
136 "time.sleep-ms: expected Int duration on stack, got {:?}",
137 value
138 ),
139 }
140}
141
142// Public re-exports
143pub use patch_seq_time_nanos as time_nanos;
144pub use patch_seq_time_now as time_now;
145pub use patch_seq_time_sleep_ms as time_sleep_ms;
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 #[cfg(not(feature = "nanbox"))]
151 use crate::stack::pop;
152 use std::time::Instant;
153
154 // Unix timestamps in microseconds exceed the 44-bit NaN-boxing range
155 #[test]
156 #[cfg(not(feature = "nanbox"))]
157 fn test_time_now_returns_positive() {
158 unsafe {
159 let stack = crate::stack::alloc_test_stack();
160 let stack = patch_seq_time_now(stack);
161 let (_, value) = pop(stack);
162
163 match value {
164 Value::Int(micros) => {
165 // Should be a reasonable timestamp (after year 2020)
166 assert!(micros > 1_577_836_800_000_000); // 2020-01-01
167 }
168 _ => panic!("Expected Int"),
169 }
170 }
171 }
172
173 // Monotonic nanosecond counters can exceed the 44-bit NaN-boxing range
174 #[test]
175 #[cfg(not(feature = "nanbox"))]
176 fn test_time_nanos_monotonic() {
177 unsafe {
178 let stack = crate::stack::alloc_test_stack();
179 let stack = patch_seq_time_nanos(stack);
180 let (_, value1) = pop(stack);
181
182 // Small delay
183 std::thread::sleep(Duration::from_micros(100));
184
185 let stack = crate::stack::alloc_test_stack();
186 let stack = patch_seq_time_nanos(stack);
187 let (_, value2) = pop(stack);
188
189 match (value1, value2) {
190 (Value::Int(t1), Value::Int(t2)) => {
191 assert!(t2 > t1, "time.nanos should be monotonically increasing");
192 }
193 _ => panic!("Expected Int values"),
194 }
195 }
196 }
197
198 // Monotonic nanosecond counters can exceed the 44-bit NaN-boxing range
199 #[test]
200 #[cfg(not(feature = "nanbox"))]
201 fn test_time_nanos_cross_thread() {
202 // Verify monotonic_nanos is consistent across threads
203 use std::sync::mpsc;
204 use std::thread;
205
206 let (tx1, rx1) = mpsc::channel();
207 let (tx2, rx2) = mpsc::channel();
208
209 // Get time on main thread
210 let t1 = monotonic_nanos();
211
212 // Spawn thread, get time there
213 let handle = thread::spawn(move || {
214 let t2 = monotonic_nanos();
215 tx1.send(t2).unwrap();
216 rx2.recv().unwrap() // wait for main to continue
217 });
218
219 let t2 = rx1.recv().unwrap();
220
221 // Get time on main thread again
222 let t3 = monotonic_nanos();
223 tx2.send(()).unwrap();
224 handle.join().unwrap();
225
226 // All times should be monotonically increasing
227 assert!(t2 > t1, "t2 ({}) should be > t1 ({})", t2, t1);
228 assert!(t3 > t2, "t3 ({}) should be > t2 ({})", t3, t2);
229 }
230
231 #[test]
232 fn test_time_sleep_ms() {
233 unsafe {
234 // Push 1ms sleep duration
235 let stack = crate::stack::alloc_test_stack();
236 let stack = push(stack, Value::Int(1));
237
238 let start = Instant::now();
239 let _stack = patch_seq_time_sleep_ms(stack);
240 let elapsed = start.elapsed();
241
242 // Should sleep at least 1ms
243 assert!(elapsed >= Duration::from_millis(1));
244 // Stack should be empty after popping the duration
245 }
246 }
247}