Skip to main content

shape_runtime/
stdlib_time.rs

1//! Native `time` module for precision timing.
2//!
3//! Exports: time.now(), time.sleep(ms), time.benchmark(fn, iterations)
4//!
5//! Phase 1.B (ADR-006 §2.7.4): the variadic `register_typed_function` /
6//! `register_typed_async_function` helpers are re-introduced at the
7//! [`KindedSlot`] shape (see `marshal.rs`). Bodies are migrated to use
8//! the current `TypedReturn` / `ConcreteReturn` taxonomy in place of
9//! the deleted `TypedReturn::Instant` / `TypedReturn::Unit` /
10//! `TypedReturn::F64` / `TypedReturn::TypedObject(_, TypedReturn::*)`
11//! shapes.
12
13use crate::module_exports::{ModuleExports, ModuleParam};
14use crate::typed_module_exports::{
15    ConcreteReturn, ConcreteType, TypedReturn, register_typed_async_function,
16    register_typed_function,
17};
18use shape_value::KindedSlot;
19
20/// Read a `KindedSlot` argument as `f64` for time-module APIs.
21///
22/// Phase 1.B variadic shim: the bodies receive `KindedSlot`s whose
23/// per-position kind is the body's contract (Phase 2c lands proper
24/// per-position kind threading from the schema). Until then, time-
25/// module bodies treat each numeric arg as `f64` raw bits — the
26/// pre-bulldozer `as_number_coerce()` shape.
27fn slot_as_f64(slot: &KindedSlot) -> Option<f64> {
28    Some(slot.slot().as_f64())
29}
30
31/// Create the `time` module with precision timing functions.
32pub fn create_time_module() -> ModuleExports {
33    let mut module = ModuleExports::new("std::core::time");
34    module.description = "Precision timing utilities".to_string();
35
36    // time.now() -> Instant
37    register_typed_function(
38        &mut module,
39        "now",
40        "Return the current monotonic instant for measuring elapsed time",
41        vec![],
42        ConcreteType::Instant,
43        |_args, _ctx| {
44            Ok(TypedReturn::Concrete(ConcreteReturn::Instant(
45                std::time::Instant::now(),
46            )))
47        },
48    );
49
50    // time.sleep(ms: number) -> unit (async via tokio).
51    register_typed_async_function(
52        &mut module,
53        "sleep",
54        "Sleep for the specified number of milliseconds (async)",
55        vec![ModuleParam {
56            name: "ms".to_string(),
57            type_name: "number".to_string(),
58            required: true,
59            description: "Duration in milliseconds".to_string(),
60            ..Default::default()
61        }],
62        ConcreteType::Unit,
63        |args: Vec<KindedSlot>| async move {
64            let ms = args.first().and_then(slot_as_f64).ok_or_else(|| {
65                "time.sleep() requires a number argument (milliseconds)".to_string()
66            })?;
67            if ms < 0.0 {
68                return Err("time.sleep() duration must be non-negative".to_string());
69            }
70            tokio::time::sleep(std::time::Duration::from_millis(ms as u64)).await;
71            Ok(TypedReturn::Concrete(ConcreteReturn::Unit))
72        },
73    );
74
75    // time.sleep_sync(ms: number) -> unit
76    register_typed_function(
77        &mut module,
78        "sleep_sync",
79        "Sleep for the specified number of milliseconds (blocking)",
80        vec![ModuleParam {
81            name: "ms".to_string(),
82            type_name: "number".to_string(),
83            required: true,
84            description: "Duration in milliseconds".to_string(),
85            ..Default::default()
86        }],
87        ConcreteType::Unit,
88        |args, _ctx| {
89            let ms = args.first().and_then(slot_as_f64).ok_or_else(|| {
90                "time.sleep_sync() requires a number argument (milliseconds)".to_string()
91            })?;
92            if ms < 0.0 {
93                return Err("time.sleep_sync() duration must be non-negative".to_string());
94            }
95            std::thread::sleep(std::time::Duration::from_millis(ms as u64));
96            Ok(TypedReturn::Concrete(ConcreteReturn::Unit))
97        },
98    );
99
100    // time.benchmark(fn, iterations?) -> object { elapsed_ms, iterations, avg_ms }
101    register_typed_function(
102        &mut module,
103        "benchmark",
104        "Benchmark a function over N iterations, returning timing statistics",
105        vec![
106            ModuleParam {
107                name: "fn".to_string(),
108                type_name: "function".to_string(),
109                required: true,
110                description: "Function to benchmark".to_string(),
111                ..Default::default()
112            },
113            ModuleParam {
114                name: "iterations".to_string(),
115                type_name: "int".to_string(),
116                required: false,
117                description: "Number of iterations (default: 1000)".to_string(),
118                default_snippet: Some("1000".to_string()),
119                ..Default::default()
120            },
121        ],
122        ConcreteType::TypedObject,
123        |args, _ctx| {
124            let _func = args
125                .first()
126                .ok_or_else(|| "time.benchmark() requires a function argument".to_string())?;
127
128            let iterations = args.get(1).and_then(slot_as_f64).unwrap_or(1000.0) as u64;
129
130            if iterations == 0 {
131                return Err("time.benchmark() iterations must be > 0".to_string());
132            }
133
134            // Module-level benchmark just measures wrapper overhead. The
135            // VM-level benchmark builtin executes the callable in a loop;
136            // this body cannot call back into the VM.
137            let start = std::time::Instant::now();
138            let elapsed = start.elapsed();
139            let elapsed_ms = elapsed.as_secs_f64() * 1000.0;
140
141            Ok(TypedReturn::TypedObject(vec![
142                ("elapsed_ms".to_string(), ConcreteReturn::F64(elapsed_ms)),
143                (
144                    "iterations".to_string(),
145                    ConcreteReturn::F64(iterations as f64),
146                ),
147                (
148                    "avg_ms".to_string(),
149                    ConcreteReturn::F64(elapsed_ms / iterations as f64),
150                ),
151            ]))
152        },
153    );
154
155    // time.stopwatch() -> Instant (alias for now())
156    register_typed_function(
157        &mut module,
158        "stopwatch",
159        "Start a stopwatch (returns an Instant). Call .elapsed() to read.",
160        vec![],
161        ConcreteType::Instant,
162        |_args, _ctx| {
163            Ok(TypedReturn::Concrete(ConcreteReturn::Instant(
164                std::time::Instant::now(),
165            )))
166        },
167    );
168
169    // time.millis() -> number (current epoch millis, for wall-clock timestamps)
170    register_typed_function(
171        &mut module,
172        "millis",
173        "Return current wall-clock time as milliseconds since Unix epoch",
174        vec![],
175        ConcreteType::Number,
176        |_args, _ctx| {
177            let now = std::time::SystemTime::now();
178            let since_epoch = now
179                .duration_since(std::time::UNIX_EPOCH)
180                .map_err(|e| format!("SystemTime error: {}", e))?;
181            Ok(TypedReturn::Concrete(ConcreteReturn::F64(
182                since_epoch.as_millis() as f64,
183            )))
184        },
185    );
186
187    module
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_time_module_creation() {
196        let module = create_time_module();
197        assert_eq!(module.name, "std::core::time");
198        assert!(module.has_export("now"));
199        assert!(module.has_export("sleep"));
200        assert!(module.has_export("sleep_sync"));
201        assert!(module.has_export("benchmark"));
202        assert!(module.has_export("stopwatch"));
203        assert!(module.has_export("millis"));
204    }
205
206    #[test]
207    fn test_time_sleep_is_async() {
208        let module = create_time_module();
209        assert!(module.is_async("sleep"));
210        assert!(!module.is_async("sleep_sync"));
211    }
212
213    #[test]
214    fn test_time_schemas() {
215        let module = create_time_module();
216        let now_schema = module.get_schema("now").unwrap();
217        assert_eq!(now_schema.return_type.as_deref(), Some("Instant"));
218
219        let sleep_schema = module.get_schema("sleep").unwrap();
220        assert_eq!(sleep_schema.params.len(), 1);
221        assert_eq!(sleep_schema.params[0].name, "ms");
222
223        let bench_schema = module.get_schema("benchmark").unwrap();
224        assert_eq!(bench_schema.params.len(), 2);
225        assert!(!bench_schema.params[1].required);
226    }
227
228    #[test]
229    fn test_time_typed_registry_populated() {
230        let module = create_time_module();
231        let typed = module.typed_exports();
232        assert!(typed.get("now").is_some());
233        assert!(typed.get("sleep_sync").is_some());
234        assert!(typed.get("benchmark").is_some());
235        assert!(typed.get("stopwatch").is_some());
236        assert!(typed.get("millis").is_some());
237        // sleep is async — separate registry.
238        assert!(typed.get_async("sleep").is_some());
239        assert!(typed.get("sleep").is_none());
240    }
241
242    // Behavioural tests (`invoke_export(...)` / typed return bit-checks)
243    // were deleted in Phase 2b alongside `module.invoke_export`. The
244    // typed dispatch path lives in shape-vm now; behavioural coverage
245    // returns when shape-vm Cluster #4 lands its kind-threaded slot
246    // tests on top of the rebuilt typed module exports.
247}