Skip to main content

shape_runtime/stdlib/
deterministic.rs

1//! Deterministic runtime for sandbox mode.
2//!
3//! Provides a seeded PRNG and virtual clock so that sandbox executions are
4//! fully reproducible: same code + same seed = identical output.
5//!
6//! When `sandbox.deterministic = true`, the VM routes `time.millis()` and
7//! random functions through this module instead of real system sources.
8
9use rand::Rng;
10use rand::SeedableRng;
11use rand_chacha::ChaCha8Rng;
12
13/// Default clock increment per `current_time_ms()` call: 1 ms in nanoseconds.
14const DEFAULT_CLOCK_INCREMENT_NS: u64 = 1_000_000;
15
16/// Deterministic runtime providing seeded randomness and a virtual clock.
17///
18/// All state is fully determined by the initial `seed`. The virtual clock
19/// starts at 0 and advances by a fixed increment on each read, so even the
20/// "current time" is reproducible.
21pub struct DeterministicRuntime {
22    rng: ChaCha8Rng,
23    virtual_clock_ns: u64,
24    clock_increment_ns: u64,
25    seed: u64,
26}
27
28impl DeterministicRuntime {
29    /// Create a new deterministic runtime with the given seed.
30    ///
31    /// The virtual clock starts at 0 and advances by 1 ms per call to
32    /// `current_time_ms()`.
33    pub fn new(seed: u64) -> Self {
34        Self {
35            rng: ChaCha8Rng::seed_from_u64(seed),
36            virtual_clock_ns: 0,
37            clock_increment_ns: DEFAULT_CLOCK_INCREMENT_NS,
38            seed,
39        }
40    }
41
42    /// Create with a custom clock increment (in nanoseconds).
43    pub fn with_clock_increment(seed: u64, clock_increment_ns: u64) -> Self {
44        Self {
45            rng: ChaCha8Rng::seed_from_u64(seed),
46            virtual_clock_ns: 0,
47            clock_increment_ns,
48            seed,
49        }
50    }
51
52    /// The seed used to initialize this runtime.
53    pub fn seed(&self) -> u64 {
54        self.seed
55    }
56
57    /// Generate the next random `f64` in `[0.0, 1.0)`.
58    pub fn next_random_f64(&mut self) -> f64 {
59        self.rng.r#gen::<f64>()
60    }
61
62    /// Generate the next random `f64` in `[min, max)`.
63    pub fn next_random_range(&mut self, min: f64, max: f64) -> f64 {
64        min + self.rng.r#gen::<f64>() * (max - min)
65    }
66
67    /// Return the current virtual time in milliseconds, then advance the
68    /// clock by the configured increment.
69    ///
70    /// This mirrors the semantics of `time.millis()` — each call returns a
71    /// monotonically increasing value.
72    pub fn current_time_ms(&mut self) -> f64 {
73        let ms = self.virtual_clock_ns as f64 / 1_000_000.0;
74        self.virtual_clock_ns += self.clock_increment_ns;
75        ms
76    }
77
78    /// Manually advance the virtual clock by `ns` nanoseconds.
79    pub fn advance_clock(&mut self, ns: u64) {
80        self.virtual_clock_ns += ns;
81    }
82
83    /// Reset the runtime to its initial state (same seed).
84    pub fn reset(&mut self) {
85        self.rng = ChaCha8Rng::seed_from_u64(self.seed);
86        self.virtual_clock_ns = 0;
87    }
88}
89
90// ============================================================================
91// Tests
92// ============================================================================
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn deterministic_random_same_seed() {
100        let mut a = DeterministicRuntime::new(42);
101        let mut b = DeterministicRuntime::new(42);
102        for _ in 0..100 {
103            assert_eq!(a.next_random_f64(), b.next_random_f64());
104        }
105    }
106
107    #[test]
108    fn different_seeds_differ() {
109        let mut a = DeterministicRuntime::new(1);
110        let mut b = DeterministicRuntime::new(2);
111        // Statistically impossible for all 100 to be equal with different seeds.
112        let any_differ = (0..100).any(|_| a.next_random_f64() != b.next_random_f64());
113        assert!(any_differ);
114    }
115
116    #[test]
117    fn random_range() {
118        let mut rt = DeterministicRuntime::new(0);
119        for _ in 0..1000 {
120            let v = rt.next_random_range(10.0, 20.0);
121            assert!((10.0..20.0).contains(&v), "out of range: {v}");
122        }
123    }
124
125    #[test]
126    fn virtual_clock_monotonic() {
127        let mut rt = DeterministicRuntime::new(0);
128        let mut prev = -1.0;
129        for _ in 0..100 {
130            let now = rt.current_time_ms();
131            assert!(now > prev, "clock not monotonic: {now} <= {prev}");
132            prev = now;
133        }
134    }
135
136    #[test]
137    fn virtual_clock_starts_at_zero() {
138        let mut rt = DeterministicRuntime::new(0);
139        assert_eq!(rt.current_time_ms(), 0.0);
140    }
141
142    #[test]
143    fn virtual_clock_default_increment() {
144        let mut rt = DeterministicRuntime::new(0);
145        let t0 = rt.current_time_ms(); // 0.0, then advances by 1ms
146        let t1 = rt.current_time_ms(); // 1.0, then advances by 1ms
147        assert_eq!(t0, 0.0);
148        assert_eq!(t1, 1.0);
149    }
150
151    #[test]
152    fn virtual_clock_custom_increment() {
153        let mut rt = DeterministicRuntime::with_clock_increment(0, 500_000); // 0.5ms
154        let t0 = rt.current_time_ms();
155        let t1 = rt.current_time_ms();
156        assert_eq!(t0, 0.0);
157        assert_eq!(t1, 0.5);
158    }
159
160    #[test]
161    fn advance_clock_manually() {
162        let mut rt = DeterministicRuntime::new(0);
163        rt.advance_clock(5_000_000_000); // 5 seconds
164        let ms = rt.current_time_ms();
165        assert_eq!(ms, 5000.0);
166    }
167
168    #[test]
169    fn reset_restores_initial_state() {
170        let mut rt = DeterministicRuntime::new(42);
171        let first_rand = rt.next_random_f64();
172        let first_time = rt.current_time_ms();
173
174        // Advance state
175        for _ in 0..50 {
176            rt.next_random_f64();
177            rt.current_time_ms();
178        }
179
180        rt.reset();
181        assert_eq!(rt.next_random_f64(), first_rand);
182        assert_eq!(rt.current_time_ms(), first_time);
183    }
184
185    #[test]
186    fn seed_accessor() {
187        let rt = DeterministicRuntime::new(12345);
188        assert_eq!(rt.seed(), 12345);
189    }
190}