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}