Skip to main content

polydat_core/numeric/
pcg.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! The PCG-RXS-M-XS generator, its O(log N) seek, and the Feistel
5//! cycle walk: the bodies of `pcg`, `pcg_stream`, and `cycle_walk`,
6//! which the native lowerings and the comprehension strategies call.
7
8use crate::derive_support::PolydatSetup;
9
10/// LCG multiplier for the 64-bit state.
11/// The LCG multiplier of the 64-bit state.
12pub const MULT: u64 = 6364136223846793005;
13
14/// Apply the RXS-M-XS output permutation to an LCG state.
15///
16/// This is the bit-mixing function that turns correlated LCG state
17/// into high-quality pseudo-random output.
18#[inline]
19pub fn pcg_output(state: u64) -> u64 {
20    let word = ((state >> ((state >> 59) + 5)) ^ state).wrapping_mul(12605985483714917081);
21    (word >> 43) ^ word
22}
23
24/// Seek to an arbitrary position in the PCG sequence in O(log N) time.
25///
26/// Uses the "distance" algorithm that exponentiates the LCG recurrence
27/// via repeated squaring, equivalent to computing `state_N` directly
28/// from `seed` without iterating through positions 0..N.
29///
30/// - `seed`: initial LCG state
31/// - `inc`: LCG increment (must be odd; typically `2 * stream + 1`)
32/// - `position`: the sequence index to seek to
33#[inline]
34pub fn pcg_seek(seed: u64, inc: u64, position: u64) -> u64 {
35    let mut cur_mult = MULT;
36    let mut cur_plus = inc;
37    let mut acc_mult: u64 = 1;
38    let mut acc_plus: u64 = 0;
39    let mut delta = position;
40    while delta > 0 {
41        if delta & 1 != 0 {
42            acc_mult = acc_mult.wrapping_mul(cur_mult);
43            acc_plus = acc_plus.wrapping_mul(cur_mult).wrapping_add(cur_plus);
44        }
45        cur_plus = cur_mult.wrapping_add(1).wrapping_mul(cur_plus);
46        cur_mult = cur_mult.wrapping_mul(cur_mult);
47        delta >>= 1;
48    }
49    let state = acc_mult.wrapping_mul(seed).wrapping_add(acc_plus);
50    pcg_output(state)
51}
52
53// =================================================================
54// Polydat Nodes
55// =================================================================
56
57/// Number of Feistel rounds. 6 rounds provides good diffusion.
58pub const FEISTEL_ROUNDS: usize = 6;
59
60/// Pre-computed Feistel state for the `cycle_walk` node. Built once at
61/// construction from `(range, seed, stream)` via the multi-source
62/// `#[poly_const]` setup; consumed read-only by every cycle and by
63/// the `compiled_u64` override's captured closure.
64pub struct CycleWalkState {
65    /// Number of bits per Feistel half (total domain is 2^(2*half_bits)).
66    pub half_bits: u32,
67    /// Bitmask for each half: `(1 << half_bits) - 1`.
68    pub half_mask: u64,
69    /// PCG-derived LCG increment, `2 * stream + 1`. Published to
70    /// the JIT classifier as the third element of `jit_constants`.
71    pub inc: u64,
72    /// Pre-computed round keys derived from seed and stream.
73    pub round_keys: [u64; FEISTEL_ROUNDS],
74}
75
76impl PolydatSetup for CycleWalkState {}
77
78/// Joint Feistel-state derivation. Single-call construction-time
79/// invocation per node instance; the macro emits the call inside
80/// the generated `CycleWalk::new(range, seed, stream)`.
81///
82/// Panics if `range` is 0 — preserves the construction-time
83/// validation contract from the pre-Phase-E hand-written form.
84pub fn build_cycle_walk_state(range: u64, seed: u64, stream: u64) -> CycleWalkState {
85    assert!(range > 0, "CycleWalk range must be > 0");
86    let inc = 2u64.wrapping_mul(stream).wrapping_add(1);
87
88    // Compute the total bit width needed, then round up to even
89    // so the Feistel halves are balanced.
90    let min_bits = if range <= 1 {
91        2 // minimum 2 bits for a balanced Feistel
92    } else {
93        let b = 64 - (range - 1).leading_zeros();
94        if !b.is_multiple_of(2) {
95            b + 1
96        } else {
97            b.max(2)
98        }
99    };
100    let half_bits = min_bits / 2;
101    let half_mask = (1u64 << half_bits) - 1;
102
103    // Derive round keys from seed and inc using the PCG itself.
104    let mut round_keys = [0u64; FEISTEL_ROUNDS];
105    for (i, key) in round_keys.iter_mut().enumerate() {
106        *key = pcg_seek(seed, inc, i as u64 + 1_000_000_000);
107    }
108
109    CycleWalkState {
110        half_bits,
111        half_mask,
112        inc,
113        round_keys,
114    }
115}
116
117#[inline]
118fn feistel_round_fn(half: u64, round_key: u64) -> u64 {
119    let x = half
120        .wrapping_mul(0x9E3779B97F4A7C15)
121        .wrapping_add(round_key);
122    let x = ((x >> 32) ^ x).wrapping_mul(0xD6E8FEB86659FD93);
123    (x >> 32) ^ x
124}
125
126/// Apply a balanced Feistel network: a bijection on `[0, 2^total_bits)`.
127///
128/// The value is split into two halves of `half_bits` each (total_bits
129/// is always even -- we round up). Standard 6-round balanced Feistel
130/// with pre-computed round keys ensures bijectivity.
131#[inline]
132fn feistel_encrypt(
133    value: u64,
134    half_bits: u32,
135    half_mask: u64,
136    round_keys: &[u64; FEISTEL_ROUNDS],
137) -> u64 {
138    let mut left = (value >> half_bits) & half_mask;
139    let mut right = value & half_mask;
140
141    for key in round_keys.iter() {
142        let new_right = left ^ (feistel_round_fn(right, *key) & half_mask);
143        left = right;
144        right = new_right;
145    }
146
147    (left << half_bits) | right
148}
149
150/// Apply cycle-walking with the Feistel bijection.
151///
152/// The input `value` is first reduced to `[0, range)` via modular
153/// reduction so that out-of-range inputs are accepted gracefully.
154/// Starting from a value in `[0, range)`, cycle-walking is guaranteed
155/// to terminate because the Feistel permutation's cycle through that
156/// value must re-enter `[0, range)`.
157#[inline]
158pub fn cycle_walk_inner(
159    mut value: u64,
160    range: u64,
161    half_bits: u32,
162    half_mask: u64,
163    round_keys: &[u64; FEISTEL_ROUNDS],
164) -> u64 {
165    if range == 1 {
166        return 0;
167    }
168    // Ensure we start in [0, range) so cycle-walk terminates.
169    value %= range;
170    loop {
171        value = feistel_encrypt(value, half_bits, half_mask, round_keys);
172        if value < range {
173            return value;
174        }
175    }
176}