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