1use polydat::ast::CompiledU64Op;
26#[cfg(test)]
27use polydat::ast::{PolydatNode, Value};
28
29pub use polydat::numeric::pcg::{
34 CycleWalkState, FEISTEL_ROUNDS, MULT, build_cycle_walk_state, cycle_walk_inner, pcg_output,
35 pcg_seek,
36};
37
38#[polydat::polydat_node(category = Permutation)]
54fn pcg(
55 input: u64,
56 #[poly_default(0u64)] seed: Const<u64>,
57 #[poly_default(0u64)] stream: Const<u64>,
58) -> u64 {
59 let inc = 2u64.wrapping_mul(*stream).wrapping_add(1);
60 pcg_seek(*seed, inc, input)
61}
62
63#[polydat::polydat_node(category = Permutation)]
78fn pcg_stream(input: u64, stream: u64, #[poly_default(0u64)] seed: Const<u64>) -> u64 {
79 let inc = 2u64.wrapping_mul(stream).wrapping_add(1);
80 pcg_seek(*seed, inc, input)
81}
82
83fn cycle_walk_jit(node: &CycleWalk) -> CompiledU64Op {
89 let range = node.range;
90 let half_bits = node.state.half_bits;
91 let half_mask = node.state.half_mask;
92 let round_keys = node.state.round_keys;
93 Box::new(move |inputs, outputs| {
94 outputs[0] = cycle_walk_inner(inputs[0], range, half_bits, half_mask, &round_keys);
95 })
96}
97
98fn cycle_walk_jit_constants(node: &CycleWalk) -> Vec<u64> {
99 vec![node.range, node.seed, node.state.inc]
100}
101
102#[polydat::polydat_node(
126 category = Permutation,
127 compiled_u64 = cycle_walk_jit,
128 jit_constants = cycle_walk_jit_constants,
129)]
130fn cycle_walk(
131 position: u64,
132 range: Const<u64>,
133 #[poly_default(0u64)] seed: Const<u64>,
134 #[poly_default(0u64)] stream: Const<u64>,
135 #[poly_const(build_cycle_walk_state, from = (range, seed, stream))] state: &CycleWalkState,
136) -> u64 {
137 let _ = seed;
138 let _ = stream;
139 cycle_walk_inner(
140 position,
141 *range,
142 state.half_bits,
143 state.half_mask,
144 &state.round_keys,
145 )
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use std::collections::HashSet;
152
153 #[test]
156 fn pcg_output_deterministic() {
157 let a = pcg_output(123456789);
159 let b = pcg_output(123456789);
160 assert_eq!(a, b);
161 }
162
163 #[test]
164 fn pcg_seek_position_zero_vs_one() {
165 let seed = 42u64;
166 let inc = 1u64; let v0 = pcg_seek(seed, inc, 0);
168 let v1 = pcg_seek(seed, inc, 1);
169 assert_ne!(v0, v1, "different positions must produce different values");
170 }
171
172 #[test]
173 fn pcg_seek_deterministic() {
174 let seed = 0xDEAD_BEEF;
175 let inc = 3;
176 let a = pcg_seek(seed, inc, 1000);
177 let b = pcg_seek(seed, inc, 1000);
178 assert_eq!(a, b);
179 }
180
181 #[test]
182 fn pcg_seek_sequential_matches_step() {
183 let seed = 77u64;
186 let inc = 5u64;
187 let n = 50u64;
188
189 let mut state = seed;
191 for _ in 0..n {
192 state = state.wrapping_mul(MULT).wrapping_add(inc);
193 }
194 let stepped = pcg_output(state);
195
196 let seeked = pcg_seek(seed, inc, n);
197 assert_eq!(
198 stepped, seeked,
199 "seek({n}) must match {n} sequential LCG steps"
200 );
201 }
202
203 #[test]
206 fn pcg_node_deterministic() {
207 let node = Pcg::new(42, 0);
208 let mut out = [Value::None];
209 node.eval(&[Value::U64(100)], &mut out);
210 let first = out[0].as_u64();
211 node.eval(&[Value::U64(100)], &mut out);
212 assert_eq!(
213 first,
214 out[0].as_u64(),
215 "same position must give same result"
216 );
217 }
218
219 #[test]
220 fn pcg_node_different_positions() {
221 let node = Pcg::new(42, 0);
222 let mut out1 = [Value::None];
223 let mut out2 = [Value::None];
224 node.eval(&[Value::U64(0)], &mut out1);
225 node.eval(&[Value::U64(1)], &mut out2);
226 assert_ne!(out1[0].as_u64(), out2[0].as_u64());
227 }
228
229 #[test]
230 fn pcg_node_different_seeds() {
231 let a = Pcg::new(1, 0);
232 let b = Pcg::new(2, 0);
233 let mut out_a = [Value::None];
234 let mut out_b = [Value::None];
235 a.eval(&[Value::U64(50)], &mut out_a);
236 b.eval(&[Value::U64(50)], &mut out_b);
237 assert_ne!(
238 out_a[0].as_u64(),
239 out_b[0].as_u64(),
240 "different seeds should produce different values"
241 );
242 }
243
244 #[test]
245 fn pcg_node_different_streams() {
246 let a = Pcg::new(42, 0);
247 let b = Pcg::new(42, 1);
248 let mut out_a = [Value::None];
249 let mut out_b = [Value::None];
250 a.eval(&[Value::U64(50)], &mut out_a);
251 b.eval(&[Value::U64(50)], &mut out_b);
252 assert_ne!(
253 out_a[0].as_u64(),
254 out_b[0].as_u64(),
255 "different streams should produce different values"
256 );
257 }
258
259 #[test]
260 fn pcg_compiled_matches_eval() {
261 let node = Pcg::new(99, 7);
262 let compiled = node.compiled_u64().expect("Pcg must provide compiled_u64");
263 for pos in 0..100u64 {
264 let mut eval_out = [Value::None];
265 node.eval(&[Value::U64(pos)], &mut eval_out);
266 let mut comp_out = [0u64];
267 compiled(&[pos], &mut comp_out);
268 assert_eq!(
269 eval_out[0].as_u64(),
270 comp_out[0],
271 "compiled and eval must agree at position {pos}"
272 );
273 }
274 }
275
276 #[test]
277 fn pcg_jit_constants() {
278 let node = Pcg::new(42, 7);
283 let consts = node.jit_constants();
284 assert_eq!(consts.len(), 2);
285 assert_eq!(consts[0], 42, "first constant is seed");
286 assert_eq!(
287 consts[1], 7,
288 "second constant is stream (inc = 2*stream+1 derived in body)"
289 );
290 }
291
292 #[test]
295 fn pcg_stream_deterministic() {
296 let node = PcgStream::new(42);
297 let mut out = [Value::None];
298 node.eval(&[Value::U64(100), Value::U64(3)], &mut out);
299 let first = out[0].as_u64();
300 node.eval(&[Value::U64(100), Value::U64(3)], &mut out);
301 assert_eq!(first, out[0].as_u64());
302 }
303
304 #[test]
305 fn pcg_stream_independence() {
306 let node = PcgStream::new(42);
307 let mut out_a = [Value::None];
308 let mut out_b = [Value::None];
309 node.eval(&[Value::U64(50), Value::U64(0)], &mut out_a);
310 node.eval(&[Value::U64(50), Value::U64(1)], &mut out_b);
311 assert_ne!(
312 out_a[0].as_u64(),
313 out_b[0].as_u64(),
314 "different stream_ids should produce different values"
315 );
316 }
317
318 #[test]
319 fn pcg_stream_matches_fixed_pcg() {
320 let fixed = Pcg::new(42, 5);
323 let dynamic = PcgStream::new(42);
324 for pos in 0..50u64 {
325 let mut f_out = [Value::None];
326 let mut d_out = [Value::None];
327 fixed.eval(&[Value::U64(pos)], &mut f_out);
328 dynamic.eval(&[Value::U64(pos), Value::U64(5)], &mut d_out);
329 assert_eq!(
330 f_out[0].as_u64(),
331 d_out[0].as_u64(),
332 "PcgStream must match Pcg for same seed/stream at position {pos}"
333 );
334 }
335 }
336
337 #[test]
338 fn pcg_stream_compiled_matches_eval() {
339 let node = PcgStream::new(99);
340 let compiled = node
341 .compiled_u64()
342 .expect("PcgStream must provide compiled_u64");
343 for pos in 0..50u64 {
344 for stream in 0..5u64 {
345 let mut eval_out = [Value::None];
346 node.eval(&[Value::U64(pos), Value::U64(stream)], &mut eval_out);
347 let mut comp_out = [0u64];
348 compiled(&[pos, stream], &mut comp_out);
349 assert_eq!(
350 eval_out[0].as_u64(),
351 comp_out[0],
352 "compiled and eval must agree at pos={pos}, stream={stream}"
353 );
354 }
355 }
356 }
357
358 #[test]
361 fn cycle_walk_bounded() {
362 let node = CycleWalk::new(100, 42, 0);
363 let mut out = [Value::None];
364 for i in 0..200u64 {
365 node.eval(&[Value::U64(i)], &mut out);
366 assert!(
367 out[0].as_u64() < 100,
368 "output {} >= range 100",
369 out[0].as_u64()
370 );
371 }
372 }
373
374 #[test]
375 fn cycle_walk_deterministic() {
376 let node = CycleWalk::new(1000, 42, 0);
377 let mut out = [Value::None];
378 node.eval(&[Value::U64(77)], &mut out);
379 let first = out[0].as_u64();
380 node.eval(&[Value::U64(77)], &mut out);
381 assert_eq!(first, out[0].as_u64());
382 }
383
384 #[test]
385 fn cycle_walk_bijective_small() {
386 let range = 50u64;
389 let node = CycleWalk::new(range, 42, 0);
390 let mut seen = HashSet::new();
391 let mut out = [Value::None];
392 for i in 0..range {
393 node.eval(&[Value::U64(i)], &mut out);
394 let v = out[0].as_u64();
395 assert!(v < range, "output {v} out of range [0, {range})");
396 assert!(seen.insert(v), "duplicate output {v} at position {i}");
397 }
398 assert_eq!(
399 seen.len(),
400 range as usize,
401 "must produce exactly {range} distinct values"
402 );
403 }
404
405 #[test]
406 fn cycle_walk_bijective_power_of_two() {
407 let range = 64u64;
409 let node = CycleWalk::new(range, 123, 7);
410 let mut seen = HashSet::new();
411 let mut out = [Value::None];
412 for i in 0..range {
413 node.eval(&[Value::U64(i)], &mut out);
414 let v = out[0].as_u64();
415 assert!(v < range);
416 assert!(seen.insert(v), "duplicate at {i}");
417 }
418 assert_eq!(seen.len(), range as usize);
419 }
420
421 #[test]
422 fn cycle_walk_compiled_matches_eval() {
423 let node = CycleWalk::new(200, 42, 3);
424 let compiled = node
425 .compiled_u64()
426 .expect("CycleWalk must provide compiled_u64");
427 for pos in 0..200u64 {
428 let mut eval_out = [Value::None];
429 node.eval(&[Value::U64(pos)], &mut eval_out);
430 let mut comp_out = [0u64];
431 compiled(&[pos], &mut comp_out);
432 assert_eq!(
433 eval_out[0].as_u64(),
434 comp_out[0],
435 "compiled and eval must agree at position {pos}"
436 );
437 }
438 }
439
440 #[test]
441 fn cycle_walk_jit_constants() {
442 let node = CycleWalk::new(500, 42, 7);
443 let consts = node.jit_constants();
444 assert_eq!(consts.len(), 3);
445 assert_eq!(consts[0], 500, "first constant is range");
446 assert_eq!(consts[1], 42, "second constant is seed");
447 assert_eq!(consts[2], 2 * 7 + 1, "third constant is inc");
448 }
449
450 #[test]
451 #[should_panic(expected = "range must be > 0")]
452 fn cycle_walk_zero_range_panics() {
453 CycleWalk::new(0, 42, 0);
454 }
455
456 #[test]
457 fn cycle_walk_range_one() {
458 let node = CycleWalk::new(1, 42, 0);
460 let mut out = [Value::None];
461 for i in 0..10u64 {
462 node.eval(&[Value::U64(i)], &mut out);
463 assert_eq!(out[0].as_u64(), 0);
464 }
465 }
466}