Skip to main content

ruvector_collections/
primality.rs

1//! Deterministic Miller-Rabin primality plus tabled fast paths for the
2//! power-of-two-aligned cases that dominate ruvector's hot paths.
3//!
4//! Designed for ADR-151 (PIAL — Prime-Indexed Acceleration Layer). Five
5//! consumers (shard router, HNSW buckets, sparsifier strides, mincut LSH,
6//! pi-brain witness chain) get one shared utility and zero new external
7//! dependencies.
8//!
9//! # Determinism
10//!
11//! | Range | Witnesses | Result |
12//! |-------|-----------|--------|
13//! | `n < 2^32` | `{2, 7, 61}` (Pomerance/Selfridge/Wagstaff) | Deterministic |
14//! | `n < 2^64` | `{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37}` (Sinclair, 2011) | Deterministic |
15//! | `n < 2^128` | 40 random rounds (`unstable-u128` feature) | `Pr[err] < 2⁻⁸⁰` |
16//!
17//! Pinned-pseudoprime regressions in `tests/primality_pseudoprimes.rs`
18//! protect the deterministic ranges from witness-set "optimizations".
19//!
20//! # Hot vs cold paths
21//!
22//! Three of PIAL's five sites request primes near *fixed* power-of-two
23//! sizes. Those calls hit [`prev_prime_below_pow2`] / [`next_prime_above_pow2`]
24//! — a single L1-cached load, ~1 ns. The two unpredictable sites (LSH
25//! universe, witness ephemeral primes) use the general MR descent at
26//! ~250 ns. Both are cold.
27//!
28//! Crucially the table is generated at build time from this very module's
29//! [`is_prime_u64`], so MR remains the source of truth.
30
31// Pull in the deterministic Miller-Rabin kernel that build.rs also uses.
32// Same code, same answers — that's the whole point.
33include!("primality_kernel.rs");
34
35// Pull in the build-time-generated tables (PRIMES_BELOW_2K, PRIMES_ABOVE_2K).
36include!(concat!(env!("OUT_DIR"), "/prime_tables.rs"));
37
38/// Returns `true` iff `n` is prime. Deterministic for all `u32`.
39///
40/// Uses the Pomerance/Selfridge/Wagstaff witness set `{2, 7, 61}` via the
41/// shared u64 path.
42#[inline]
43pub fn is_prime_u32(n: u32) -> bool {
44    mr_is_prime_u32(n)
45}
46
47/// Returns `true` iff `n` is prime. Deterministic for all `u64`.
48///
49/// Uses Sinclair's 2011 witness set
50/// `{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37}` — known to be sufficient
51/// for the entire `u64` range. Allocation-free.
52#[inline]
53pub fn is_prime_u64(n: u64) -> bool {
54    mr_is_prime_u64(n)
55}
56
57/// Largest prime strictly less than `2^k`, for `k ∈ [8, 64]`.
58///
59/// Single L1-cached table load (~1 ns). Use this whenever the caller knows
60/// the size is a power of two — shard routers, HNSW bucket sizing,
61/// sparsifier strides.
62///
63/// # Panics (debug only)
64///
65/// Debug-asserts `8 <= k <= 64`.
66#[inline]
67pub fn prev_prime_below_pow2(k: u32) -> u64 {
68    debug_assert!((8..=64).contains(&k), "k out of table range [8, 64]");
69    PRIMES_BELOW_2K[(k - 8) as usize]
70}
71
72/// Smallest prime strictly greater than `2^k`, for `k ∈ [8, 63]`.
73///
74/// Symmetric companion to [`prev_prime_below_pow2`]. The `k = 64` entry of
75/// the underlying table is a sentinel (no `u64` prime exists greater than
76/// `2^64`); callers must not request it.
77///
78/// # Panics (debug only)
79///
80/// Debug-asserts `8 <= k <= 63`.
81#[inline]
82pub fn next_prime_above_pow2(k: u32) -> u64 {
83    debug_assert!(
84        (8..=63).contains(&k),
85        "k out of table range [8, 63]; PRIMES_ABOVE_2K[64] is a sentinel"
86    );
87    PRIMES_ABOVE_2K[(k - 8) as usize]
88}
89
90/// Largest prime strictly less than `n`. Returns `0` if no such `u64` prime
91/// exists (i.e. `n <= 2`).
92///
93/// Routes power-of-two-aligned inputs (`n = 2^k`, `k ∈ [8, 64]`) to the
94/// table; everything else falls through to a Miller-Rabin descent.
95#[inline]
96pub fn prev_prime_u64(n: u64) -> u64 {
97    if n.is_power_of_two() {
98        let k = n.trailing_zeros();
99        if (8..=64).contains(&k) {
100            return PRIMES_BELOW_2K[(k - 8) as usize];
101        }
102    }
103    mr_prev_prime_u64(n)
104}
105
106/// Smallest prime strictly greater than `n`. Returns `0` if `n` is at or
107/// above the largest `u64` prime (`u64::MAX - 58`).
108///
109/// Routes power-of-two-aligned inputs (`n = 2^k`, `k ∈ [8, 63]`) to the
110/// table; everything else falls through to a Miller-Rabin descent.
111#[inline]
112pub fn next_prime_u64(n: u64) -> u64 {
113    if n.is_power_of_two() {
114        let k = n.trailing_zeros();
115        if (8..=63).contains(&k) {
116            return PRIMES_ABOVE_2K[(k - 8) as usize];
117        }
118    }
119    mr_next_prime_u64(n)
120}
121
122/// Derives a deterministic ephemeral prime from `seed`, suitable for the
123/// pi-brain witness chain (ADR-151 §4.4).
124///
125/// Maps the seed into the odd lower-2⁶¹ window then walks up to the next
126/// prime. The 2⁶¹ ceiling keeps results well inside `u64` even after the
127/// MR walk and lets downstream consumers store the value in a single
128/// 64-bit field with room to spare.
129#[inline]
130pub fn ephemeral_prime(seed: u64) -> u64 {
131    let mask = (1u64 << 61) - 1;
132    let s = (seed | 1) & mask;
133    if mr_is_prime_u64(s) {
134        s
135    } else {
136        // Bounded: the prime gap below 2^61 is far smaller than the
137        // remaining headroom to u64::MAX, so this never returns 0.
138        mr_next_prime_u64(s)
139    }
140}
141
142// ── Probabilistic u128 mode (opt-in) ─────────────────────────────────────
143
144/// Probabilistic Miller-Rabin for `u128`. Soundness error `< 4^-rounds`;
145/// `rounds = 40` gives `< 2⁻⁸⁰`, adequate for hashing but **not** a
146/// cryptographic prime generator (see ADR-151 "Security Considerations").
147///
148/// Gated behind the `unstable-u128` feature: WASM `u128` codegen is ~5×
149/// slower than native and we keep it out of default bundles.
150#[cfg(feature = "unstable-u128")]
151pub fn is_prime_u128(n: u128, rounds: u8) -> bool {
152    if n < 2 {
153        return false;
154    }
155    // Cheap divisibility screen — also catches every n that fits in u64
156    // and is one of the Sinclair witnesses.
157    const SMALL_PRIMES: [u128; 12] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37];
158    for &p in &SMALL_PRIMES {
159        if n == p {
160            return true;
161        }
162        if n.is_multiple_of(p) {
163            return false;
164        }
165    }
166    // If n fits in u64, defer to the deterministic path.
167    if n <= u64::MAX as u128 {
168        return mr_is_prime_u64(n as u64);
169    }
170
171    // n > u64::MAX, n odd, coprime to first 12 primes. Decompose n - 1.
172    let nm1 = n - 1;
173    let s = nm1.trailing_zeros();
174    let d = nm1 >> s;
175
176    // Tiny inline LCG seeded from n so the test is reproducible across runs.
177    // Numerical-Recipes-style multiplier; we only need uniformity, not crypto.
178    let mut state: u128 = n ^ 0x9E37_79B9_7F4A_7C15_F39C_C060_5CED_C835u128;
179    for _ in 0..rounds {
180        state = state
181            .wrapping_mul(6364136223846793005)
182            .wrapping_add(1442695040888963407);
183        // Witness in [2, n-2].
184        let a = 2u128 + (state % (n - 3));
185        if mr_is_composite_u128(n, d, s, a) {
186            return false;
187        }
188    }
189    true
190}
191
192#[cfg(feature = "unstable-u128")]
193#[inline]
194fn mr_is_composite_u128(n: u128, d: u128, s: u32, a: u128) -> bool {
195    let mut x = powmod_u128(a, d, n);
196    if x == 1 || x == n - 1 {
197        return false;
198    }
199    for _ in 0..s.saturating_sub(1) {
200        x = mulmod_u128(x, x, n);
201        if x == n - 1 {
202            return false;
203        }
204    }
205    true
206}
207
208#[cfg(feature = "unstable-u128")]
209#[inline]
210fn powmod_u128(mut base: u128, mut exp: u128, m: u128) -> u128 {
211    if m == 1 {
212        return 0;
213    }
214    let mut acc: u128 = 1 % m;
215    base %= m;
216    while exp > 0 {
217        if exp & 1 == 1 {
218            acc = mulmod_u128(acc, base, m);
219        }
220        exp >>= 1;
221        if exp > 0 {
222            base = mulmod_u128(base, base, m);
223        }
224    }
225    acc
226}
227
228// Russian-peasant mulmod for u128 — works for any m < 2^128 without a u256.
229#[cfg(feature = "unstable-u128")]
230#[inline]
231fn mulmod_u128(mut a: u128, mut b: u128, m: u128) -> u128 {
232    let mut acc: u128 = 0;
233    a %= m;
234    while b > 0 {
235        if b & 1 == 1 {
236            acc = mod_add_u128(acc, a, m);
237        }
238        a = mod_add_u128(a, a, m);
239        b >>= 1;
240    }
241    acc
242}
243
244#[cfg(feature = "unstable-u128")]
245#[inline]
246fn mod_add_u128(a: u128, b: u128, m: u128) -> u128 {
247    // Pre: a < m, b < m, m may be > 2^127. Computed (a + b) mod m without
248    // a u256 by detecting wrapping overflow.
249    let sum = a.wrapping_add(b);
250    if sum < a || sum >= m {
251        sum.wrapping_sub(m)
252    } else {
253        sum
254    }
255}
256
257// ── Internal sanity tests (run with the rest of the crate's unit tests) ──
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn small_primes_under_100() {
265        let known: [u64; 25] = [
266            2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83,
267            89, 97,
268        ];
269        for n in 0u64..100 {
270            assert_eq!(is_prime_u64(n), known.contains(&n), "is_prime_u64({n})");
271        }
272    }
273
274    #[test]
275    fn edges() {
276        assert!(!is_prime_u64(0));
277        assert!(!is_prime_u64(1));
278        assert!(!is_prime_u64(u64::MAX));
279        assert!(is_prime_u64(u64::MAX - 58), "largest u64 prime");
280    }
281
282    #[test]
283    fn table_index_round_trip() {
284        // The most heavily-used shard-router entry.
285        assert_eq!(prev_prime_below_pow2(32), 4_294_967_291);
286        // Smallest table entry.
287        assert_eq!(prev_prime_below_pow2(8), 251);
288        // Largest table entry.
289        assert_eq!(prev_prime_below_pow2(64), u64::MAX - 58);
290    }
291
292    #[cfg(feature = "unstable-u128")]
293    #[test]
294    fn u128_probabilistic_smoke() {
295        use super::is_prime_u128;
296        // Defers to deterministic u64 path for n <= u64::MAX.
297        assert!(is_prime_u128(7, 40));
298        assert!(!is_prime_u128(9, 40));
299        assert!(is_prime_u128(u64::MAX as u128 - 58, 40));
300        // True u128 path: 2^89 - 1 is a Mersenne prime.
301        let m89: u128 = (1u128 << 89) - 1;
302        assert!(is_prime_u128(m89, 40), "M_89 = 2^89 - 1 is prime");
303        // Composite just above 2^64.
304        let composite: u128 = (1u128 << 65) + 1; // = 3 * 11 * 67 * ... (divisible by 3)
305        assert!(!is_prime_u128(composite, 40));
306    }
307
308    #[test]
309    fn ephemeral_prime_is_prime_for_assorted_seeds() {
310        for seed in [0u64, 1, 42, 0xDEAD_BEEF, u64::MAX, 1_000_003] {
311            let p = ephemeral_prime(seed);
312            assert!(is_prime_u64(p), "ephemeral_prime({seed}) = {p} not prime");
313            // Loose upper bound: largest known prime gap below 2^64 is well under 2^31,
314            // so anything below 2^62 means the walk stayed near its 2^61 starting window.
315            assert!(p < (1u64 << 62), "ephemeral_prime overshot expected window");
316        }
317    }
318}