1use crate::module_exports::{ModuleExports, ModuleFunction, ModuleParam};
6use crate::type_schema::typed_object_from_pairs;
7use shape_value::ValueWord;
8
9pub fn create_time_module() -> ModuleExports {
11 let mut module = ModuleExports::new("time");
12 module.description = "Precision timing utilities".to_string();
13
14 module.add_function_with_schema(
16 "now",
17 |_args: &[ValueWord], _ctx: &crate::module_exports::ModuleContext| {
18 Ok(ValueWord::from_instant(std::time::Instant::now()))
19 },
20 ModuleFunction {
21 description: "Return the current monotonic instant for measuring elapsed time"
22 .to_string(),
23 params: vec![],
24 return_type: Some("Instant".to_string()),
25 },
26 );
27
28 module.add_async_function_with_schema(
30 "sleep",
31 |args: Vec<ValueWord>| async move {
32 let ms = args
33 .first()
34 .and_then(|a| a.as_number_coerce())
35 .ok_or_else(|| {
36 "time.sleep() requires a number argument (milliseconds)".to_string()
37 })?;
38 if ms < 0.0 {
39 return Err("time.sleep() duration must be non-negative".to_string());
40 }
41 tokio::time::sleep(std::time::Duration::from_millis(ms as u64)).await;
42 Ok(ValueWord::unit())
43 },
44 ModuleFunction {
45 description: "Sleep for the specified number of milliseconds (async)".to_string(),
46 params: vec![ModuleParam {
47 name: "ms".to_string(),
48 type_name: "number".to_string(),
49 required: true,
50 description: "Duration in milliseconds".to_string(),
51 ..Default::default()
52 }],
53 return_type: Some("unit".to_string()),
54 },
55 );
56
57 module.add_function_with_schema(
59 "sleep_sync",
60 |args: &[ValueWord], _ctx: &crate::module_exports::ModuleContext| {
61 let ms = args
62 .first()
63 .and_then(|a| a.as_number_coerce())
64 .ok_or_else(|| {
65 "time.sleep_sync() requires a number argument (milliseconds)".to_string()
66 })?;
67 if ms < 0.0 {
68 return Err("time.sleep_sync() duration must be non-negative".to_string());
69 }
70 std::thread::sleep(std::time::Duration::from_millis(ms as u64));
71 Ok(ValueWord::unit())
72 },
73 ModuleFunction {
74 description: "Sleep for the specified number of milliseconds (blocking)".to_string(),
75 params: vec![ModuleParam {
76 name: "ms".to_string(),
77 type_name: "number".to_string(),
78 required: true,
79 description: "Duration in milliseconds".to_string(),
80 ..Default::default()
81 }],
82 return_type: Some("unit".to_string()),
83 },
84 );
85
86 module.add_function_with_schema(
88 "benchmark",
89 |args: &[ValueWord], _ctx: &crate::module_exports::ModuleContext| {
90 let func = args
91 .first()
92 .cloned()
93 .ok_or_else(|| "time.benchmark() requires a function argument".to_string())?;
94
95 let iterations = args
96 .get(1)
97 .and_then(|a| a.as_number_coerce())
98 .unwrap_or(1000.0) as u64;
99
100 if iterations == 0 {
101 return Err("time.benchmark() iterations must be > 0".to_string());
102 }
103
104 let start = std::time::Instant::now();
109 let _ = &func;
112 let elapsed = start.elapsed();
113 let elapsed_ms = elapsed.as_secs_f64() * 1000.0;
114
115 let pairs: Vec<(&str, ValueWord)> = vec![
117 ("elapsed_ms", ValueWord::from_f64(elapsed_ms)),
118 ("iterations", ValueWord::from_f64(iterations as f64)),
119 (
120 "avg_ms",
121 ValueWord::from_f64(elapsed_ms / iterations as f64),
122 ),
123 ];
124 Ok(typed_object_from_pairs(&pairs))
125 },
126 ModuleFunction {
127 description: "Benchmark a function over N iterations, returning timing statistics"
128 .to_string(),
129 params: vec![
130 ModuleParam {
131 name: "fn".to_string(),
132 type_name: "function".to_string(),
133 required: true,
134 description: "Function to benchmark".to_string(),
135 ..Default::default()
136 },
137 ModuleParam {
138 name: "iterations".to_string(),
139 type_name: "int".to_string(),
140 required: false,
141 description: "Number of iterations (default: 1000)".to_string(),
142 default_snippet: Some("1000".to_string()),
143 ..Default::default()
144 },
145 ],
146 return_type: Some("object".to_string()),
147 },
148 );
149
150 module.add_function_with_schema(
152 "stopwatch",
153 |_args: &[ValueWord], _ctx: &crate::module_exports::ModuleContext| {
154 Ok(ValueWord::from_instant(std::time::Instant::now()))
155 },
156 ModuleFunction {
157 description: "Start a stopwatch (returns an Instant). Call .elapsed() to read."
158 .to_string(),
159 params: vec![],
160 return_type: Some("Instant".to_string()),
161 },
162 );
163
164 module.add_function_with_schema(
166 "millis",
167 |_args: &[ValueWord], _ctx: &crate::module_exports::ModuleContext| {
168 let now = std::time::SystemTime::now();
169 let since_epoch = now
170 .duration_since(std::time::UNIX_EPOCH)
171 .map_err(|e| format!("SystemTime error: {}", e))?;
172 Ok(ValueWord::from_f64(since_epoch.as_millis() as f64))
173 },
174 ModuleFunction {
175 description: "Return current wall-clock time as milliseconds since Unix epoch"
176 .to_string(),
177 params: vec![],
178 return_type: Some("number".to_string()),
179 },
180 );
181
182 module
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
190 let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
191 crate::module_exports::ModuleContext {
192 schemas: registry,
193 invoke_callable: None,
194 raw_invoker: None,
195 function_hashes: None,
196 vm_state: None,
197 granted_permissions: None,
198 scope_constraints: None,
199 set_pending_resume: None,
200 set_pending_frame_resume: None,
201 }
202 }
203
204 #[test]
205 fn test_time_module_creation() {
206 let module = create_time_module();
207 assert_eq!(module.name, "time");
208 assert!(module.has_export("now"));
209 assert!(module.has_export("sleep"));
210 assert!(module.has_export("sleep_sync"));
211 assert!(module.has_export("benchmark"));
212 assert!(module.has_export("stopwatch"));
213 assert!(module.has_export("millis"));
214 }
215
216 #[test]
217 fn test_time_now_returns_instant() {
218 let module = create_time_module();
219 let ctx = test_ctx();
220 let now_fn = module.get_export("now").unwrap();
221 let result = now_fn(&[], &ctx).unwrap();
222 assert_eq!(result.type_name(), "instant");
223 assert!(result.as_instant().is_some());
224 }
225
226 #[test]
227 fn test_time_stopwatch_returns_instant() {
228 let module = create_time_module();
229 let ctx = test_ctx();
230 let sw_fn = module.get_export("stopwatch").unwrap();
231 let result = sw_fn(&[], &ctx).unwrap();
232 assert_eq!(result.type_name(), "instant");
233 }
234
235 #[test]
236 fn test_time_millis_returns_positive_number() {
237 let module = create_time_module();
238 let ctx = test_ctx();
239 let millis_fn = module.get_export("millis").unwrap();
240 let result = millis_fn(&[], &ctx).unwrap();
241 let ms = result.as_f64().unwrap();
242 assert!(ms > 0.0);
243 assert!(ms > 1_577_836_800_000.0);
245 }
246
247 #[test]
248 fn test_time_sleep_sync_requires_number() {
249 let module = create_time_module();
250 let ctx = test_ctx();
251 let sleep_fn = module.get_export("sleep_sync").unwrap();
252 let result = sleep_fn(&[], &ctx);
253 assert!(result.is_err());
254 }
255
256 #[test]
257 fn test_time_sleep_sync_rejects_negative() {
258 let module = create_time_module();
259 let ctx = test_ctx();
260 let sleep_fn = module.get_export("sleep_sync").unwrap();
261 let result = sleep_fn(&[ValueWord::from_f64(-100.0)], &ctx);
262 assert!(result.is_err());
263 }
264
265 #[test]
266 fn test_time_sleep_sync_zero_is_valid() {
267 let module = create_time_module();
268 let ctx = test_ctx();
269 let sleep_fn = module.get_export("sleep_sync").unwrap();
270 let result = sleep_fn(&[ValueWord::from_f64(0.0)], &ctx);
271 assert!(result.is_ok());
272 }
273
274 #[test]
275 fn test_time_sleep_is_async() {
276 let module = create_time_module();
277 assert!(module.is_async("sleep"));
278 assert!(!module.is_async("sleep_sync"));
279 }
280
281 #[test]
282 fn test_time_benchmark_returns_object() {
283 let module = create_time_module();
284 let ctx = test_ctx();
285 let bench_fn = module.get_export("benchmark").unwrap();
286 let result = bench_fn(
289 &[ValueWord::from_f64(0.0), ValueWord::from_f64(100.0)],
290 &ctx,
291 )
292 .unwrap();
293 assert_eq!(result.type_name(), "object");
294 }
295
296 #[test]
297 fn test_time_schemas() {
298 let module = create_time_module();
299 let now_schema = module.get_schema("now").unwrap();
300 assert_eq!(now_schema.return_type.as_deref(), Some("Instant"));
301
302 let sleep_schema = module.get_schema("sleep").unwrap();
303 assert_eq!(sleep_schema.params.len(), 1);
304 assert_eq!(sleep_schema.params[0].name, "ms");
305
306 let bench_schema = module.get_schema("benchmark").unwrap();
307 assert_eq!(bench_schema.params.len(), 2);
308 assert!(!bench_schema.params[1].required);
309 }
310}