doge_runtime/stdlib/
roll.rs1use std::cell::RefCell;
12use std::rc::Rc;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::time::{SystemTime, UNIX_EPOCH};
15
16use crate::error::{DogeError, DogeResult};
17use crate::stdlib::int_arg;
18use crate::value::Value;
19
20const FLOAT_DIVISOR: f64 = (1u64 << 53) as f64;
23
24static SEED_COUNTER: AtomicU64 = AtomicU64::new(0);
27
28thread_local! {
29 static RNG: RefCell<Xoshiro256ss> = RefCell::new(Xoshiro256ss::from_entropy());
32}
33
34struct SplitMix64(u64);
37
38impl SplitMix64 {
39 fn next(&mut self) -> u64 {
40 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
41 let mut z = self.0;
42 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
43 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
44 z ^ (z >> 31)
45 }
46}
47
48struct Xoshiro256ss {
50 s: [u64; 4],
51}
52
53impl Xoshiro256ss {
54 fn from_seed(seed: u64) -> Self {
56 let mut sm = SplitMix64(seed);
57 Xoshiro256ss {
58 s: [sm.next(), sm.next(), sm.next(), sm.next()],
59 }
60 }
61
62 fn from_entropy() -> Self {
65 let nanos = SystemTime::now()
66 .duration_since(UNIX_EPOCH)
67 .map(|d| d.as_nanos() as u64)
68 .unwrap_or(0);
69 let count = SEED_COUNTER.fetch_add(1, Ordering::Relaxed);
70 Xoshiro256ss::from_seed(nanos ^ count.wrapping_mul(0x9E37_79B9_7F4A_7C15))
71 }
72
73 fn next_u64(&mut self) -> u64 {
74 let result = self.s[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
75 let t = self.s[1] << 17;
76 self.s[2] ^= self.s[0];
77 self.s[3] ^= self.s[1];
78 self.s[1] ^= self.s[2];
79 self.s[0] ^= self.s[3];
80 self.s[2] ^= t;
81 self.s[3] = self.s[3].rotate_left(45);
82 result
83 }
84
85 fn below(&mut self, bound: u64) -> u64 {
89 let mut x = self.next_u64();
90 let mut m = (x as u128).wrapping_mul(bound as u128);
91 let mut low = m as u64;
92 if low < bound {
93 let threshold = bound.wrapping_neg() % bound;
94 while low < threshold {
95 x = self.next_u64();
96 m = (x as u128).wrapping_mul(bound as u128);
97 low = m as u64;
98 }
99 }
100 (m >> 64) as u64
101 }
102
103 fn next_float(&mut self) -> f64 {
105 (self.next_u64() >> 11) as f64 / FLOAT_DIVISOR
106 }
107}
108
109fn with_rng<T>(f: impl FnOnce(&mut Xoshiro256ss) -> T) -> T {
111 RNG.with(|rng| f(&mut rng.borrow_mut()))
112}
113
114fn list_arg<'a>(fname: &str, v: &'a Value) -> DogeResult<&'a Rc<RefCell<Vec<Value>>>> {
117 match v {
118 Value::List(items) => Ok(items),
119 _ => Err(DogeError::type_error(format!(
120 "roll.{fname} needs a List, got {}",
121 v.describe()
122 ))),
123 }
124}
125
126pub fn roll_seed(n: &Value) -> DogeResult {
129 let seed = int_arg("roll", "seed", n)?;
130 with_rng(|rng| *rng = Xoshiro256ss::from_seed(seed as u64));
131 Ok(Value::None)
132}
133
134pub fn roll_int(low: &Value, high: &Value) -> DogeResult {
137 let low = int_arg("roll", "int", low)?;
138 let high = int_arg("roll", "int", high)?;
139 if low > high {
140 return Err(DogeError::value_error(format!(
141 "roll.int needs low <= high, but {low} > {high} — swap the bounds"
142 )));
143 }
144 let span = (high as i128 - low as i128 + 1) as u128;
145 let draw = if span > u64::MAX as u128 {
146 with_rng(|rng| rng.next_u64())
147 } else {
148 with_rng(|rng| rng.below(span as u64))
149 };
150 Ok(Value::Int((low as i128 + draw as i128) as i64))
151}
152
153pub fn roll_float() -> DogeResult {
155 Ok(Value::Float(with_rng(|rng| rng.next_float())))
156}
157
158pub fn roll_choice(list: &Value) -> DogeResult {
161 let items = list_arg("choice", list)?.borrow();
162 if items.is_empty() {
163 return Err(DogeError::value_error(
164 "roll.choice needs a non-empty List — there is nothing to choose from",
165 ));
166 }
167 let index = with_rng(|rng| rng.below(items.len() as u64)) as usize;
168 Ok(items[index].clone())
169}
170
171pub fn roll_shuffle(list: &Value) -> DogeResult {
175 let mut items: Vec<Value> = list_arg("shuffle", list)?.borrow().clone();
176 let len = items.len();
177 with_rng(|rng| fisher_yates(rng, &mut items, len));
178 Ok(Value::list(items))
179}
180
181pub fn roll_sample(list: &Value, k: &Value) -> DogeResult {
185 let items: Vec<Value> = list_arg("sample", list)?.borrow().clone();
186 let k = int_arg("roll", "sample", k)?;
187 if k < 0 || k as u128 > items.len() as u128 {
188 return Err(DogeError::value_error(format!(
189 "roll.sample needs 0 <= k <= {}, got {k}",
190 items.len()
191 )));
192 }
193 let mut items = items;
194 let k = k as usize;
195 with_rng(|rng| fisher_yates(rng, &mut items, k));
196 items.truncate(k);
197 Ok(Value::list(items))
198}
199
200fn fisher_yates(rng: &mut Xoshiro256ss, items: &mut [Value], count: usize) {
204 let len = items.len();
205 for i in 0..count.min(len) {
206 let j = i + rng.below((len - i) as u64) as usize;
207 items.swap(i, j);
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::error::ErrorKind;
215
216 fn seed(n: i64) {
217 roll_seed(&Value::Int(n)).unwrap();
218 }
219
220 fn as_int(v: Value) -> i64 {
221 match v {
222 Value::Int(n) => n,
223 other => panic!("expected an Int, got {other:?}"),
224 }
225 }
226
227 fn as_list(v: Value) -> Vec<Value> {
228 match v {
229 Value::List(items) => items.borrow().clone(),
230 other => panic!("expected a List, got {other:?}"),
231 }
232 }
233
234 #[test]
235 fn same_seed_reproduces_the_sequence() {
236 seed(1234);
237 let first: Vec<i64> = (0..10)
238 .map(|_| as_int(roll_int(&Value::Int(0), &Value::Int(1_000_000)).unwrap()))
239 .collect();
240 seed(1234);
241 let second: Vec<i64> = (0..10)
242 .map(|_| as_int(roll_int(&Value::Int(0), &Value::Int(1_000_000)).unwrap()))
243 .collect();
244 assert_eq!(first, second);
245 }
246
247 #[test]
248 fn int_stays_within_the_inclusive_range() {
249 seed(7);
250 for _ in 0..1000 {
251 let n = as_int(roll_int(&Value::Int(-3), &Value::Int(3)).unwrap());
252 assert!((-3..=3).contains(&n), "{n} out of range");
253 }
254 }
255
256 #[test]
257 fn int_with_equal_bounds_is_that_bound() {
258 seed(7);
259 assert_eq!(as_int(roll_int(&Value::Int(5), &Value::Int(5)).unwrap()), 5);
260 }
261
262 #[test]
263 fn int_low_above_high_is_value_error() {
264 assert_eq!(
265 roll_int(&Value::Int(5), &Value::Int(1)).unwrap_err().kind,
266 ErrorKind::ValueError
267 );
268 }
269
270 #[test]
271 fn float_stays_in_the_unit_interval() {
272 seed(99);
273 for _ in 0..1000 {
274 let f = match roll_float().unwrap() {
275 Value::Float(f) => f,
276 other => panic!("expected a Float, got {other:?}"),
277 };
278 assert!((0.0..1.0).contains(&f), "{f} out of range");
279 }
280 }
281
282 #[test]
283 fn choice_returns_a_member_and_rejects_empty() {
284 seed(42);
285 let list = Value::list(vec![Value::Int(10), Value::Int(20), Value::Int(30)]);
286 for _ in 0..100 {
287 let n = as_int(roll_choice(&list).unwrap());
288 assert!([10, 20, 30].contains(&n));
289 }
290 assert_eq!(
291 roll_choice(&Value::list(vec![])).unwrap_err().kind,
292 ErrorKind::ValueError
293 );
294 }
295
296 #[test]
297 fn shuffle_is_a_permutation_that_leaves_the_input_untouched() {
298 seed(2024);
299 let list = Value::list((0..8).map(Value::Int).collect());
300 let shuffled = as_list(roll_shuffle(&list).unwrap());
301 let after: Vec<i64> = as_list(list).into_iter().map(as_int).collect();
303 assert_eq!(after, (0..8).collect::<Vec<_>>());
304 let mut got: Vec<i64> = shuffled.into_iter().map(as_int).collect();
306 got.sort_unstable();
307 assert_eq!(got, (0..8).collect::<Vec<_>>());
308 }
309
310 #[test]
311 fn sample_draws_k_distinct_positions() {
312 seed(2024);
313 let list = Value::list((0..10).map(Value::Int).collect());
314 let picked = as_list(roll_sample(&list, &Value::Int(4)).unwrap());
315 assert_eq!(picked.len(), 4);
316 let mut values: Vec<i64> = picked.into_iter().map(as_int).collect();
317 values.sort_unstable();
318 values.dedup();
319 assert_eq!(values.len(), 4, "sampled positions should be distinct");
320 }
321
322 #[test]
323 fn sample_bounds_are_value_errors() {
324 let list = Value::list((0..3).map(Value::Int).collect());
325 assert_eq!(
326 roll_sample(&list, &Value::Int(4)).unwrap_err().kind,
327 ErrorKind::ValueError
328 );
329 assert_eq!(
330 roll_sample(&list, &Value::Int(-1)).unwrap_err().kind,
331 ErrorKind::ValueError
332 );
333 }
334
335 #[test]
336 fn sample_of_zero_is_empty_and_of_all_is_a_permutation() {
337 seed(1);
338 let list = Value::list((0..5).map(Value::Int).collect());
339 assert!(as_list(roll_sample(&list, &Value::Int(0)).unwrap()).is_empty());
340 let all = as_list(roll_sample(&list, &Value::Int(5)).unwrap());
341 let mut values: Vec<i64> = all.into_iter().map(as_int).collect();
342 values.sort_unstable();
343 assert_eq!(values, (0..5).collect::<Vec<_>>());
344 }
345
346 #[test]
347 fn wrong_arg_types_are_type_errors() {
348 assert_eq!(
349 roll_seed(&Value::str("x")).unwrap_err().kind,
350 ErrorKind::TypeError
351 );
352 assert_eq!(
353 roll_int(&Value::str("x"), &Value::Int(1)).unwrap_err().kind,
354 ErrorKind::TypeError
355 );
356 assert_eq!(
357 roll_choice(&Value::Int(1)).unwrap_err().kind,
358 ErrorKind::TypeError
359 );
360 assert_eq!(
361 roll_shuffle(&Value::Int(1)).unwrap_err().kind,
362 ErrorKind::TypeError
363 );
364 assert_eq!(
365 roll_sample(&Value::Int(1), &Value::Int(1))
366 .unwrap_err()
367 .kind,
368 ErrorKind::TypeError
369 );
370 }
371}