use crate::f2::SparseBinaryMatrix;
use crate::qs::{AutoOr, FactorBaseEntry, MultiplierChoice, QsConfig, prepare_siqs};
use crate::{Natural, PARTS, jacobi_u64};
#[cfg(any(unix, windows))]
use crate::{PrimalityConfig, is_probable_prime};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::sync::Arc;
#[cfg(any(unix, windows))]
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
#[cfg(any(unix, windows))]
use std::sync::{Mutex, mpsc};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EnginePhase {
Preprocessing,
BuildingFactorBase,
Sieving,
LinearAlgebra,
Extracting,
}
#[derive(Clone, Copy, Debug)]
pub struct EngineProgress {
pub phase: EnginePhase,
pub polynomials: u64,
pub relations: usize,
pub target: usize,
pub workers: usize,
}
#[derive(Debug)]
pub enum EngineError {
Setup(String),
InsufficientRelations,
NoFactor,
Worker(String),
PolynomialSelection(String),
InvalidDependency,
ResourceLimit,
Cancelled,
}
impl fmt::Display for EngineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for EngineError {}
#[derive(Clone)]
struct Context {
n: Natural,
sieve_n: Natural,
base: Arc<[FactorBaseEntry]>,
pinv: Arc<[u64]>,
small_slack: usize,
interval_mod_p: Arc<[u32]>,
interval: i32,
target_a: Natural,
a_all: Arc<[usize]>,
a_pool: Arc<[usize]>,
a_factor_count: usize,
lp_bits: usize,
thresh_adj: i32,
single_limit: u64,
double_enabled: bool,
}
#[derive(Clone, Copy)]
enum LargePrime {
None,
One(u64),
Two(u64, u64),
}
impl LargePrime {
#[inline]
fn primes(self) -> ([u64; 2], usize) {
match self {
LargePrime::None => ([0, 0], 0),
LargePrime::One(a) => ([a, 0], 1),
LargePrime::Two(a, b) => ([a, b], 2),
}
}
}
#[derive(Clone)]
struct Relation {
root: Natural,
sign: bool,
powers: Vec<(u32, u16)>,
large: LargePrime,
}
#[derive(Clone)]
struct Column {
root: Natural,
sign: bool,
powers: Vec<(u32, u32)>,
extra_sqrt: Vec<u64>,
}
struct FamilyResult {
family: u64,
polynomials: u64,
relations: Vec<Relation>,
#[allow(dead_code)]
survivors: u64,
}
#[derive(Default)]
struct EngineScratch {
scores: Vec<u8>,
root1: Vec<u32>,
root2: Vec<u32>,
bainv: Vec<u32>,
candidates: Vec<u32>,
}
#[derive(Clone)]
pub struct EngineContext(Arc<Context>);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct EngineJob {
pub family: u64,
}
pub struct EngineJobResult {
inner: FamilyResult,
pub family: u64,
pub polynomials: u64,
pub relations: usize,
}
impl EngineJobResult {
pub fn to_bytes(&self) -> Vec<u8> {
let capacity = 20
+ self
.inner
.relations
.iter()
.map(|relation| {
PARTS * 8
+ 2
+ match relation.large {
LargePrime::None => 0,
LargePrime::One(_) => 8,
LargePrime::Two(_, _) => 16,
}
+ 4
+ relation.powers.len() * 6
})
.sum::<usize>();
let mut v = Vec::with_capacity(capacity);
v.extend_from_slice(&self.inner.family.to_le_bytes());
v.extend_from_slice(&self.inner.polynomials.to_le_bytes());
v.extend_from_slice(&(self.inner.relations.len() as u32).to_le_bytes());
for r in &self.inner.relations {
for limb in r.root.as_parts() {
v.extend_from_slice(&limb.to_le_bytes());
}
v.push(r.sign as u8);
match r.large {
LargePrime::None => v.push(0),
LargePrime::One(a) => {
v.push(1);
v.extend_from_slice(&a.to_le_bytes());
}
LargePrime::Two(a, b) => {
v.push(2);
v.extend_from_slice(&a.to_le_bytes());
v.extend_from_slice(&b.to_le_bytes());
}
}
v.extend_from_slice(&(r.powers.len() as u32).to_le_bytes());
for &(i, e) in &r.powers {
v.extend_from_slice(&i.to_le_bytes());
v.extend_from_slice(&e.to_le_bytes());
}
}
v
}
}
fn deserialize_family(b: &[u8]) -> Option<FamilyResult> {
struct Cur<'a> {
b: &'a [u8],
o: usize,
}
impl Cur<'_> {
fn take(&mut self, n: usize) -> Option<&[u8]> {
let s = self.b.get(self.o..self.o + n)?;
self.o += n;
Some(s)
}
fn u8(&mut self) -> Option<u8> {
Some(self.take(1)?[0])
}
fn u16(&mut self) -> Option<u16> {
Some(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
}
fn u32(&mut self) -> Option<u32> {
Some(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
}
fn u64(&mut self) -> Option<u64> {
Some(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
}
}
let mut c = Cur { b, o: 0 };
let family = c.u64()?;
let polynomials = c.u64()?;
let count = c.u32()? as usize;
let mut relations = Vec::with_capacity(count.min(1 << 20));
for _ in 0..count {
let root = Natural::from_le_bytes(c.take(PARTS * 8)?).ok()?;
let sign = c.u8()? != 0;
let large = match c.u8()? {
0 => LargePrime::None,
1 => LargePrime::One(c.u64()?),
2 => LargePrime::Two(c.u64()?, c.u64()?),
_ => return None,
};
let plen = c.u32()? as usize;
let mut powers = Vec::with_capacity(plen.min(1 << 16));
for _ in 0..plen {
let i = c.u32()?;
let e = c.u16()?;
powers.push((i, e));
}
relations.push(Relation {
root,
sign,
powers,
large,
});
}
Some(FamilyResult {
family,
polynomials,
relations,
survivors: 0,
})
}
fn rho_budget(bits: usize) -> u64 {
let p = crate::qs::parameters::engine_params(bits);
(p.factor_base_bound as u64 * p.sieve_half_width as u64 / 500_000).max(1_024)
}
#[cfg(test)]
pub(crate) mod stage_counts {
use std::cell::Cell;
thread_local! {
pub(crate) static RHO: Cell<usize> = const { Cell::new(0) };
pub(crate) static SIQS: Cell<usize> = const { Cell::new(0) };
}
pub(crate) fn bump(counter: &'static std::thread::LocalKey<Cell<usize>>) {
counter.with(|c| c.set(c.get() + 1));
}
pub(crate) fn reset() {
RHO.with(|c| c.set(0));
SIQS.with(|c| c.set(0));
}
pub(crate) fn rho() -> usize {
RHO.with(Cell::get)
}
pub(crate) fn siqs() -> usize {
SIQS.with(Cell::get)
}
}
const MAX_FAMILIES: u64 = 100_000;
pub fn prepare(n: Natural) -> Result<EngineContext, EngineError> {
let p = crate::qs::parameters::engine_params(n.bit_len());
let k = knuth_schroeppel(&n);
let sieve_n = n
.checked_mul(&Natural::from_u64(k))
.unwrap_or_else(|| n.clone());
let qcfg = QsConfig {
factor_base_bound: AutoOr::Value(p.factor_base_bound),
multiplier: MultiplierChoice::Value(k as u32),
..QsConfig::default()
};
let prepared = prepare_siqs(&n, &qcfg).map_err(|e| EngineError::Setup(e.to_string()))?;
let base: Arc<[FactorBaseEntry]> = prepared.factor_base().entries().to_vec().into();
let pinv: Arc<[u64]> = base.iter().map(|e| lemire_c(e.prime)).collect();
let small_slack = small_slack(&base, small_skip());
let interval_mod_p: Arc<[u32]> = base.iter().map(|e| p.sieve_half_width % e.prime).collect();
let target_a = sieve_n
.floor_sqrt()
.div_rem_u64(p.sieve_half_width as u64)
.unwrap()
.0;
let (a_all, a_pool, a_factor_count) = build_a_candidates(&base, &target_a);
let (single_limit, double_enabled) =
large_prime_policy(p.factor_base_bound, p.large_prime_mult);
let context = Arc::new(Context {
n,
sieve_n,
base,
pinv,
small_slack,
interval_mod_p,
interval: p.sieve_half_width as i32,
target_a,
a_all,
a_pool,
a_factor_count,
lp_bits: (64 - single_limit.leading_zeros()) as usize * if double_enabled { 2 } else { 1 },
thresh_adj: p.thresh_adj + env_delta("RUSQSIEVE_THRESH_ADJ"),
single_limit,
double_enabled,
});
if choose_a(&context, 0).is_none() {
let message = format!(
"polynomial-coefficient selection has no viable A for {}-bit input \
(factor base {}, target A {} bits, {} candidate primes)",
context.n.bit_len(),
context.base.len(),
context.target_a.bit_len(),
context.a_pool.len(),
);
return Err(EngineError::PolynomialSelection(message));
}
Ok(EngineContext(context))
}
const DOUBLE_LARGE_PRIMES: bool = false;
fn large_prime_policy(bound: u32, large_prime_mult: u32) -> (u64, bool) {
(
(bound as u64).saturating_mul(large_prime_mult as u64),
DOUBLE_LARGE_PRIMES,
)
}
pub fn execute(context: &EngineContext, job: EngineJob) -> EngineJobResult {
let mut scratch = EngineScratch::default();
let inner = sieve_family(&context.0, job.family, &mut scratch);
EngineJobResult {
family: inner.family,
polynomials: inner.polynomials,
relations: inner.relations.len(),
inner,
}
}
pub struct EngineSession {
context: EngineContext,
target: usize,
next_job: u64,
next_merge: u64,
polynomials: u64,
collector: RelationCollector,
buffered: BTreeMap<u64, FamilyResult>,
seen_a: HashSet<Natural>,
}
impl EngineSession {
pub fn new(context: EngineContext) -> Self {
let target = relation_target(context.0.base.len());
Self {
context,
target,
next_job: 0,
next_merge: 0,
polynomials: 0,
collector: RelationCollector::new(),
buffered: BTreeMap::new(),
seen_a: HashSet::new(),
}
}
pub fn take_jobs(&mut self, maximum: usize) -> Vec<EngineJob> {
if self.is_ready() {
return Vec::new();
}
let mut jobs = Vec::with_capacity(maximum);
while jobs.len() < maximum && self.next_job < MAX_FAMILIES {
jobs.push(EngineJob {
family: self.next_job,
});
self.next_job += 1;
}
jobs
}
pub fn submit(&mut self, result: EngineJobResult) {
self.buffered.insert(result.family, result.inner);
self.drain_buffered();
}
pub fn submit_bytes(&mut self, bytes: &[u8]) -> bool {
if let Some(fr) = deserialize_family(bytes) {
self.buffered.insert(fr.family, fr);
self.drain_buffered();
}
self.is_ready()
}
fn drain_buffered(&mut self) {
while let Some(r) = self.buffered.remove(&self.next_merge) {
self.next_merge += 1;
let unique =
choose_a(&self.context.0, r.family).is_some_and(|(a, _)| self.seen_a.insert(a));
if !unique {
continue;
}
self.polynomials += r.polynomials;
let n = &self.context.0.n;
for rel in r.relations {
self.collector.ingest(rel, n);
}
}
}
pub fn is_ready(&self) -> bool {
self.collector.columns.len() >= self.target
}
pub fn relations(&self) -> usize {
self.collector.columns.len()
}
pub fn target(&self) -> usize {
self.target
}
pub fn polynomials(&self) -> u64 {
self.polynomials
}
pub fn extract_factor(&self) -> Result<Natural, EngineError> {
extract(&self.context.0, &self.collector.columns)
}
}
fn extract(ctx: &Context, columns: &[Column]) -> Result<Natural, EngineError> {
let matrix_cols: Vec<Vec<u32>> = columns
.iter()
.map(|c| {
let mut v = Vec::new();
if c.sign {
v.push(0)
}
for &(i, e) in &c.powers {
if e & 1 != 0 {
v.push(i + 1)
}
}
v
})
.collect();
let matrix = SparseBinaryMatrix::from_columns(ctx.base.len() + 1, &matrix_cols)
.map_err(|_| EngineError::InvalidDependency)?;
let dependencies = matrix
.filtered_dependencies()
.map_err(|_| EngineError::ResourceLimit)?;
for dep in dependencies.iter() {
if !matrix.verify_dependency(dep) {
return Err(EngineError::InvalidDependency);
}
let mut x = Natural::ONE;
let mut y = Natural::ONE;
let mut sums = vec![0u32; ctx.base.len()];
for (j, c) in columns.iter().enumerate() {
if (dep[j / 64] >> (j % 64)) & 1 == 0 {
continue;
}
x = x.mul_mod(&c.root, &ctx.n);
for &lp in &c.extra_sqrt {
y = y.mul_mod(&Natural::from_u64(lp), &ctx.n);
}
for &(i, e) in &c.powers {
sums[i as usize] += e
}
}
for (e, &s) in ctx.base.iter().zip(&sums) {
for _ in 0..s / 2 {
y = y.mul_mod(&Natural::from_u64(e.prime as u64), &ctx.n)
}
}
let d = if x >= y {
x.wrapping_sub(&y)
} else {
y.wrapping_sub(&x)
};
let g = d.gcd(&ctx.n);
if !g.is_one() && g != ctx.n {
return Ok(g);
}
let g = x.add_mod(&y, &ctx.n).gcd(&ctx.n);
if !g.is_one() && g != ctx.n {
return Ok(g);
}
}
Err(EngineError::NoFactor)
}
fn relation_target(base_len: usize) -> usize {
#[cfg(any(unix, windows))]
if let Some(percent) = std::env::var("RUSQSIEVE_REL_PERCENT")
.ok()
.and_then(|s| s.parse::<usize>().ok())
{
return (base_len * percent.clamp(50, 110) / 100).max(64);
}
base_len + 64
}
#[cfg(any(unix, windows))]
pub fn factor(
mut n: Natural,
threads: usize,
mut progress: impl FnMut(EngineProgress) -> bool,
) -> Result<Vec<Natural>, EngineError> {
if n.is_zero() {
return Err(EngineError::Setup("zero has no prime factorization".into()));
}
let primality = PrimalityConfig::default();
let mut factors = Vec::new();
for &p in crate::smallfactor::small_primes() {
if p > 10_000 {
break;
}
loop {
let (q, r) = n.div_rem_u64(p as u64).unwrap();
if r != 0 {
break;
}
factors.push(Natural::from_u64(p as u64));
n = q
}
}
if n.is_one() {
return Ok(factors);
}
factor_node(n, threads.max(1), &primality, &mut progress, &mut factors)?;
factors.sort();
Ok(factors)
}
#[cfg(any(unix, windows))]
fn factor_node(
n: Natural,
threads: usize,
pc: &PrimalityConfig,
progress: &mut impl FnMut(EngineProgress) -> bool,
out: &mut Vec<Natural>,
) -> Result<(), EngineError> {
if !progress(EngineProgress {
phase: EnginePhase::Preprocessing,
polynomials: 0,
relations: 0,
target: 0,
workers: threads,
}) {
return Err(EngineError::Cancelled);
}
if n.is_one() {
return Ok(());
}
if let Some(v) = n.to_u64() {
let mut small = Vec::new();
let completed = crate::smallfactor::factor_u64_cancellable(v, &mut small, || {
!progress(EngineProgress {
phase: EnginePhase::Preprocessing,
polynomials: 0,
relations: 0,
target: 0,
workers: threads,
})
});
if !completed {
return Err(EngineError::Cancelled);
}
out.extend(small.into_iter().map(Natural::from_u64));
return Ok(());
}
if is_probable_prime(&n, pc) {
out.push(n);
return Ok(());
}
if let Some((root, k)) = n.perfect_power() {
let mut fs = Vec::new();
factor_node(root, threads, pc, progress, &mut fs)?;
for _ in 0..k {
out.extend(fs.iter().cloned())
}
return Ok(());
}
let d = match pollard_brent_natural(&n, rho_budget(n.bit_len()), || {
progress(EngineProgress {
phase: EnginePhase::Preprocessing,
polynomials: 0,
relations: 0,
target: 0,
workers: threads,
})
})? {
Some(factor) => {
#[cfg(test)]
stage_counts::bump(&stage_counts::RHO);
if std::env::var_os("RUSQSIEVE_PROFILE").is_some() {
eprintln!(
"PROFILE rho input_bits={} factor_bits={} siqs=false",
n.bit_len(),
factor.bit_len()
);
}
factor
}
None => find_factor(n.clone(), threads, progress)?,
};
if d.is_one() || d == n {
return Err(EngineError::NoFactor);
}
let q = n.div_rem(&d).unwrap().0;
factor_node(d, threads, pc, progress, out)?;
factor_node(q, threads, pc, progress, out)
}
#[cfg(any(unix, windows))]
fn find_factor(
n: Natural,
threads: usize,
progress: &mut impl FnMut(EngineProgress) -> bool,
) -> Result<Natural, EngineError> {
let threads = match n.bit_len() {
0..=128 => threads.min(2),
129..=160 => threads.min(16),
161..=184 => threads.min(48),
_ => threads,
}
.max(1);
if !progress(EngineProgress {
phase: EnginePhase::BuildingFactorBase,
polynomials: 0,
relations: 0,
target: 0,
workers: threads,
}) {
return Err(EngineError::Cancelled);
}
#[cfg(test)]
stage_counts::bump(&stage_counts::SIQS);
let prof = std::env::var_os("RUSQSIEVE_PROFILE").is_some();
let t_fb = std::time::Instant::now();
let ctx = prepare(n.clone())?.0;
let target = relation_target(ctx.base.len());
if prof {
eprintln!(
"PROFILE fb_build={:.3}s nfb={} interval={} target={}",
t_fb.elapsed().as_secs_f64(),
ctx.base.len(),
ctx.interval,
target,
);
}
let (job_tx, job_rx) = mpsc::channel::<Option<u64>>();
let job_rx = Arc::new(Mutex::new(job_rx));
let (res_tx, res_rx) = mpsc::channel();
let mut handles = Vec::new();
let cancellation = Arc::new(AtomicBool::new(false));
for _ in 0..threads {
let rx = job_rx.clone();
let tx = res_tx.clone();
let c = ctx.clone();
let cancellation = cancellation.clone();
handles.push(std::thread::spawn(move || {
let mut scratch = EngineScratch::default();
loop {
if cancellation.load(AtomicOrdering::Relaxed) {
break;
}
let job = rx.lock().unwrap_or_else(|e| e.into_inner()).recv();
match job {
Ok(Some(f)) => {
if cancellation.load(AtomicOrdering::Relaxed) {
break;
}
if tx.send(sieve_family(&c, f, &mut scratch)).is_err() {
break;
}
}
_ => break,
}
}
}))
}
drop(res_tx);
let mut next_send = 0u64;
let mut next_merge = 0u64;
let mut outstanding = 0usize;
for _ in 0..threads * 2 {
job_tx
.send(Some(next_send))
.map_err(|_| EngineError::Worker("worker job channel disconnected".into()))?;
next_send += 1;
outstanding += 1
}
let t_sieve = std::time::Instant::now();
let mut buffered = BTreeMap::new();
let mut collector = RelationCollector::new();
let mut polynomials = 0u64;
let mut total_survivors = 0u64;
let mut seen_a = HashSet::new();
let mut cancelled = false;
while collector.columns.len() < target && next_merge < MAX_FAMILIES && !cancelled {
let result = res_rx
.recv()
.map_err(|_| EngineError::Worker("worker result channel disconnected".into()))?;
outstanding -= 1;
buffered.insert(result.family, result);
while let Some(r) = buffered.remove(&next_merge) {
next_merge += 1;
let unique_a = choose_a(&ctx, r.family)
.map(|(a, _)| seen_a.insert(a))
.unwrap_or(false);
if !unique_a {
continue;
}
polynomials += r.polynomials;
total_survivors += r.survivors;
for rel in r.relations {
collector.ingest(rel, &n);
if collector.columns.len() >= target {
break;
}
}
if !progress(EngineProgress {
phase: EnginePhase::Sieving,
polynomials,
relations: collector.columns.len(),
target,
workers: threads,
}) {
cancelled = true;
cancellation.store(true, AtomicOrdering::Relaxed);
break;
}
}
while outstanding < threads * 2
&& next_send < MAX_FAMILIES
&& collector.columns.len() < target
{
job_tx
.send(Some(next_send))
.map_err(|_| EngineError::Worker("worker job channel disconnected".into()))?;
next_send += 1;
outstanding += 1
}
}
for _ in 0..threads {
let _ = job_tx.send(None);
}
drop(job_tx);
let mut first_panic = None;
for h in handles {
if let Err(payload) = h.join()
&& first_panic.is_none()
{
first_panic = Some(
payload
.downcast_ref::<&str>()
.map(|s| (*s).to_owned())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "worker panicked with a non-string payload".into()),
);
}
}
if let Some(message) = first_panic {
return Err(EngineError::Worker(message));
}
if cancelled {
return Err(EngineError::Cancelled);
}
if prof {
eprintln!(
"PROFILE sieve+collect={:.3}s polys={} families={} survivors={} partials={} cycles={} relations={}",
t_sieve.elapsed().as_secs_f64(),
polynomials,
next_merge,
total_survivors,
collector.forest.relations.len(),
collector.cycles,
collector.columns.len()
);
}
if !progress(EngineProgress {
phase: EnginePhase::LinearAlgebra,
polynomials,
relations: collector.columns.len(),
target,
workers: threads,
}) {
return Err(EngineError::Cancelled);
}
let t_la = std::time::Instant::now();
let result = extract(&ctx, &collector.columns);
if prof {
eprintln!(
"PROFILE extract(LA)={:.3}s columns={}",
t_la.elapsed().as_secs_f64(),
collector.columns.len()
);
}
if !progress(EngineProgress {
phase: EnginePhase::Extracting,
polynomials,
relations: collector.columns.len(),
target,
workers: threads,
}) {
return Err(EngineError::Cancelled);
}
result
}
fn sieve_family(ctx: &Context, family: u64, scratch: &mut EngineScratch) -> FamilyResult {
let empty = |family| FamilyResult {
family,
polynomials: 0,
relations: Vec::new(),
survivors: 0,
};
let Some((a, aidx)) = choose_a(ctx, family) else {
return empty(family);
};
let base = &ctx.base;
let nfb = base.len();
let s = aidx.len();
let nvar = (s - 1).min(9); let variants = 1u64 << nvar;
let mut bvals: Vec<Natural> = Vec::with_capacity(s);
for &i in &aidx {
let q = base[i as usize].prime;
let Some((ap, _)) = a.div_rem_u64(q as u64) else {
return empty(family);
};
let Some(apinv) = inv_u32(ap.mod_u64(q as u64) as u32, q) else {
return empty(family);
};
let mut coeff = (base[i as usize].sqrt_n as u64 * apinv as u64) % q as u64;
coeff = coeff.min(q as u64 - coeff);
bvals.push(ap.checked_mul(&Natural::from_u64(coeff)).unwrap());
}
let mut b = Natural::ZERO;
for bj in &bvals {
b = b.checked_add(bj).unwrap();
}
let mut bneg = false;
let two_full: Vec<Natural> = bvals[..nvar].iter().map(|bj| bj.wrapping_add(bj)).collect();
scratch.root1.clear();
scratch.root1.resize(nfb, u32::MAX);
scratch.root2.clear();
scratch.root2.resize(nfb, 0);
scratch.bainv.clear();
scratch.bainv.resize(nvar * nfb, 0);
for (idx, e) in base.iter().enumerate() {
let p = e.prime;
if p == 2 {
continue;
}
let ap = a.mod_u64(p as u64) as u32;
if ap == 0 {
continue; }
let Some(ainvp) = inv_u32(ap, p) else {
continue;
};
let mut bp = b.mod_u64(p as u64) as u32;
if bneg && bp != 0 {
bp = p - bp;
}
let xroot1 = mulmod_u32((e.sqrt_n + p - bp) % p, ainvp, p);
let xroot2 = mulmod_u32(((p - e.sqrt_n) % p + p - bp) % p, ainvp, p);
let r1 = add_mod_u32(xroot1, ctx.interval_mod_p[idx], p);
let r2 = add_mod_u32(xroot2, ctx.interval_mod_p[idx], p);
scratch.root1[idx] = r1.min(r2);
scratch.root2[idx] = r1.max(r2);
for (j, bj) in bvals.iter().take(nvar).enumerate() {
let bjp = bj.mod_u64(p as u64) as u32;
let two_bjp = (2 * bjp as u64 % p as u64) as u32;
scratch.bainv[j * nfb + idx] = mulmod_u32(two_bjp, ainvp, p);
}
}
let mut relations = Vec::new();
let mut survivors = 0u64;
for v in 0..variants {
survivors += sieve_one_poly(
ctx,
&a,
&b,
bneg,
&aidx,
&scratch.root1,
&scratch.root2,
&mut scratch.scores,
&mut scratch.candidates,
&mut relations,
) as u64;
if v + 1 >= variants {
break;
}
let j = (v + 1).trailing_zeros() as usize;
let gray = v ^ (v >> 1);
let flip_to_one = (gray >> j) & 1 == 0;
let add_bainv = if flip_to_one {
(b, bneg) = signed_add(&b, bneg, &two_full[j], true);
true
} else {
(b, bneg) = signed_add(&b, bneg, &two_full[j], false);
false
};
let off = j * nfb;
if add_bainv {
for idx in 0..nfb {
if scratch.root1[idx] == u32::MAX {
continue;
}
let p = base[idx].prime;
let d = scratch.bainv[off + idx];
let r1 = add_mod_u32(scratch.root1[idx], d, p);
let r2 = add_mod_u32(scratch.root2[idx], d, p);
scratch.root1[idx] = r1.min(r2);
scratch.root2[idx] = r1.max(r2);
}
} else {
for idx in 0..nfb {
if scratch.root1[idx] == u32::MAX {
continue;
}
let p = base[idx].prime;
let d = scratch.bainv[off + idx];
let r1 = sub_mod_u32(scratch.root1[idx], d, p);
let r2 = sub_mod_u32(scratch.root2[idx], d, p);
scratch.root1[idx] = r1.min(r2);
scratch.root2[idx] = r1.max(r2);
}
}
}
FamilyResult {
family,
polynomials: variants,
relations,
survivors,
}
}
fn signed_add(a: &Natural, aneg: bool, b: &Natural, bneg: bool) -> (Natural, bool) {
if aneg == bneg {
let sum = a.checked_add(b).expect("signed SIQS coefficient overflow");
let neg = aneg && !sum.is_zero();
(sum, neg)
} else if a >= b {
let diff = a.wrapping_sub(b);
let neg = aneg && !diff.is_zero();
(diff, neg)
} else {
let diff = b.wrapping_sub(a);
let neg = bneg && !diff.is_zero();
(diff, neg)
}
}
fn build_a_candidates(
base: &[FactorBaseEntry],
target_a: &Natural,
) -> (Arc<[usize]>, Arc<[usize]>, usize) {
let target_bits = target_a.bit_len();
let factor_count = target_bits.div_ceil(14).clamp(3, 10);
let ideal_bits = target_bits.div_ceil(factor_count);
let minimum_bits = ideal_bits.saturating_sub(1).max(2);
let all: Vec<usize> = base
.iter()
.enumerate()
.filter(|(_, e)| (32 - e.prime.leading_zeros()) as usize >= minimum_bits)
.map(|(i, _)| i)
.collect();
if all.len() < factor_count {
return (all.into(), Arc::from([]), factor_count);
}
let mut window = 1usize;
let pool = loop {
let candidates = all
.iter()
.copied()
.filter(|&i| {
let bits = (32 - base[i].prime.leading_zeros()) as usize;
bits.abs_diff(ideal_bits) <= window
})
.collect::<Vec<_>>();
if candidates.len() >= factor_count * 2 || window >= 31 {
break candidates;
}
window += 1;
};
debug_assert!(!pool.is_empty(), "choose_a constraints must be satisfiable");
(all.into(), pool.into(), factor_count)
}
fn choose_a(ctx: &Context, family: u64) -> Option<(Natural, Vec<u32>)> {
let all = &ctx.a_all;
let pool = &ctx.a_pool;
let factor_count = ctx.a_factor_count;
if all.len() < factor_count || pool.len() < factor_count {
return None;
}
let mut state = family ^ 0x9e3779b97f4a7c15;
let mut best = None;
for _ in 0..32 {
let mut a = Natural::ONE;
let mut idx = Vec::with_capacity(factor_count);
while idx.len() + 1 < factor_count {
state = xorshift(state);
let i = pool[state as usize % pool.len()];
if idx.contains(&(i as u32)) {
continue;
}
a = a.checked_mul(&Natural::from_u64(ctx.base[i].prime as u64))?;
idx.push(i as u32)
}
let desired_u64 = ctx.target_a.div_rem(&a)?.0.to_u64()?;
let last = all
.iter()
.copied()
.filter(|&i| !idx.contains(&(i as u32)))
.min_by_key(|&i| (ctx.base[i].prime as u64).abs_diff(desired_u64))?;
a = a.checked_mul(&Natural::from_u64(ctx.base[last].prime as u64))?;
idx.push(last as u32);
let close = a
.checked_mul(&Natural::from_u64(5))
.zip(ctx.target_a.checked_mul(&Natural::from_u64(4)))
.is_some_and(|(lhs, rhs)| lhs >= rhs)
&& ctx
.target_a
.checked_mul(&Natural::from_u64(5))
.zip(a.checked_mul(&Natural::from_u64(4)))
.is_some_and(|(lhs, rhs)| lhs >= rhs);
if close {
return Some((a, idx));
}
let distance = if a >= ctx.target_a {
a.wrapping_sub(&ctx.target_a)
} else {
ctx.target_a.wrapping_sub(&a)
};
if best
.as_ref()
.is_none_or(|(prior, _, _): &(Natural, Natural, Vec<u32>)| distance < *prior)
{
best = Some((distance, a, idx));
}
}
best.map(|(_, a, idx)| (a, idx))
}
fn small_skip() -> u32 {
static V: std::sync::OnceLock<u32> = std::sync::OnceLock::new();
*V.get_or_init(|| env_default("RUSQSIEVE_SMALL_SKIP", 100) as u32)
}
fn small_slack(base: &[FactorBaseEntry], skip: u32) -> usize {
base.iter()
.filter(|entry| entry.prime < skip)
.map(|entry| {
score_weight(entry.prime) as f64 / (entry.prime.saturating_sub(1).max(1)) as f64
})
.sum::<f64>()
.round() as usize
}
#[inline(always)]
fn score_weight(prime: u32) -> u8 {
(32 - prime.leading_zeros()) as u8
}
fn thresh_margin() -> i32 {
static V: std::sync::OnceLock<i32> = std::sync::OnceLock::new();
*V.get_or_init(|| env_default("RUSQSIEVE_THRESH_MARGIN", 0) as i32)
}
fn env_delta(name: &str) -> i32 {
#[cfg(any(unix, windows))]
{
std::env::var(name)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
#[cfg(not(any(unix, windows)))]
{
let _ = name;
0
}
}
fn env_default(name: &str, default: usize) -> usize {
#[cfg(any(unix, windows))]
{
std::env::var(name)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(default)
}
#[cfg(not(any(unix, windows)))]
{
let _ = name;
default
}
}
#[inline(always)]
fn sieve_root_pair<const SATURATING: bool>(
scores: &mut [u8],
root1: usize,
root2: usize,
step: usize,
weight: u8,
) {
#[inline(always)]
fn add<const SATURATING: bool>(slot: &mut u8, weight: u8) {
*slot = if SATURATING {
slot.saturating_add(weight)
} else {
slot.wrapping_add(weight)
};
}
let len = scores.len();
debug_assert!(root1 <= root2);
let diff = root2 - root1;
let mut pos = root1;
while pos + step + diff < len {
add::<SATURATING>(&mut scores[pos], weight);
add::<SATURATING>(&mut scores[pos + diff], weight);
pos += step;
add::<SATURATING>(&mut scores[pos], weight);
add::<SATURATING>(&mut scores[pos + diff], weight);
pos += step;
}
while pos + diff < len {
add::<SATURATING>(&mut scores[pos], weight);
add::<SATURATING>(&mut scores[pos + diff], weight);
pos += step;
}
while pos < len {
add::<SATURATING>(&mut scores[pos], weight);
pos += step;
}
}
#[allow(clippy::too_many_arguments)]
fn score_polynomial<const SATURATING: bool>(
ctx: &Context,
b: &Natural,
bneg: bool,
c: &Natural,
csign: bool,
root1: &[u32],
root2: &[u32],
scores: &mut [u8],
small_skip: u32,
) {
for (idx, e) in ctx.base.iter().enumerate() {
let p = e.prime;
if p == 2 || p < small_skip {
continue;
}
let pu = p as usize;
let weight = score_weight(p);
if root1[idx] != u32::MAX {
sieve_root_pair::<SATURATING>(
scores,
root1[idx] as usize,
root2[idx] as usize,
pu,
weight,
);
} else {
let mut bp = b.mod_u64(p as u64) as u32;
if bneg && bp != 0 {
bp = p - bp;
}
let denom = (2 * bp as u64 % p as u64) as u32;
let Some(inv) = inv_u32(denom, p) else {
continue;
};
let cm = c.mod_u64(p as u64) as u32;
let signed_c = if csign && cm != 0 { p - cm } else { cm };
let xroot = mulmod_u32(if signed_c == 0 { 0 } else { p - signed_c }, inv, p);
let mut pos = add_mod_u32(xroot, ctx.interval_mod_p[idx], p) as usize;
while pos < scores.len() {
scores[pos] = if SATURATING {
scores[pos].saturating_add(weight)
} else {
scores[pos].wrapping_add(weight)
};
pos += pu;
}
}
}
}
#[inline]
fn divide_out(q: &mut Natural, p: u64) -> u16 {
let mut count = 0;
while q.rem_u64(p) == 0 {
*q = q.div_rem_u64(p).unwrap().0;
count += 1;
}
count
}
fn collect_candidates(scores: &[u8], threshold: u8, candidates: &mut Vec<u32>) {
const HIGH: u64 = 0x8080_8080_8080_8080;
debug_assert!(
threshold >= 128,
"scores must be biased so that the threshold is the byte's high bit"
);
candidates.clear();
let mut chunks = scores.chunks_exact(8);
for (word_index, chunk) in chunks.by_ref().enumerate() {
let word = u64::from_ne_bytes(chunk.try_into().unwrap());
if word & HIGH == 0 {
continue;
}
let start = word_index * 8;
if threshold == 128 {
for (offset, &score) in chunk.iter().enumerate() {
if score & 0x80 != 0 {
candidates.push((start + offset) as u32);
}
}
} else {
for (offset, &score) in chunk.iter().enumerate() {
if score >= threshold {
candidates.push((start + offset) as u32);
}
}
}
}
let start = scores.len() - chunks.remainder().len();
for (offset, &score) in chunks.remainder().iter().enumerate() {
if score >= threshold {
candidates.push((start + offset) as u32);
}
}
}
#[allow(clippy::too_many_arguments)]
fn sieve_one_poly(
ctx: &Context,
a: &Natural,
b: &Natural,
bneg: bool,
aidx: &[u32],
root1: &[u32],
root2: &[u32],
scores: &mut Vec<u8>,
candidates: &mut Vec<u32>,
out: &mut Vec<Relation>,
) -> usize {
let base = &ctx.base;
let len = (ctx.interval as usize) * 2;
let small_skip = small_skip();
let bb = b.checked_mul(b).unwrap();
let (c, csign) = if bb >= ctx.sieve_n {
(bb.wrapping_sub(&ctx.sieve_n).div_rem(a).unwrap().0, false)
} else {
(ctx.sieve_n.wrapping_sub(&bb).div_rem(a).unwrap().0, true)
};
let g_bits = ctx.sieve_n.bit_len().saturating_sub(a.bit_len());
let threshold = (g_bits as i32 - ctx.lp_bits as i32 - ctx.small_slack as i32
+ thresh_margin()
+ ctx.thresh_adj)
.clamp(1, u8::MAX as i32) as u8;
let small_end = base.partition_point(|e| e.prime < small_skip);
let bias = 128u8.saturating_sub(threshold);
let scan_threshold = threshold.saturating_add(bias);
let smallest_scored = base.get(small_end).map_or(u32::MAX, |e| e.prime).max(3);
let scored_bits = (32 - smallest_scored.leading_zeros() - 1).max(1);
let score_bound = g_bits as u32 + g_bits as u32 / scored_bits + 1;
let exact_scores = bias as u32 + score_bound <= u8::MAX as u32;
scores.clear();
scores.resize(len, bias);
if exact_scores {
score_polynomial::<false>(ctx, b, bneg, &c, csign, root1, root2, scores, small_skip);
} else {
score_polynomial::<true>(ctx, b, bneg, &c, csign, root1, root2, scores, small_skip);
}
collect_candidates(scores, scan_threshold, candidates);
if candidates.is_empty() {
return 0;
}
let survivors = candidates.len();
let two_idx = base.iter().position(|e| e.prime == 2).map(|i| i as u32);
let mut powers_scratch = Vec::new();
for &posu in candidates.iter() {
let pos = posu as usize;
let score_target = if exact_scores {
u16::from(scores[pos].wrapping_sub(bias))
} else {
u16::MAX
};
let mut confirmed_score = 0u16;
let x = pos as i64 - ctx.interval as i64;
let xabs = x.unsigned_abs();
let ax = a.checked_mul(&Natural::from_u64(xabs)).unwrap();
let (t, tneg) = signed_add(&ax, x < 0, b, bneg);
let ax2 = ax.checked_mul(&Natural::from_u64(xabs)).unwrap();
let two_bx = b
.wrapping_add(b)
.checked_mul(&Natural::from_u64(xabs))
.unwrap();
let (gx, gxneg) = signed_add(&ax2, false, &two_bx, bneg ^ (x < 0));
let (mut q, sign) = signed_add(&gx, gxneg, &c, csign);
if q.is_zero() {
continue;
}
powers_scratch.clear();
powers_scratch.extend(aidx.iter().copied().map(|i| (i, 1)));
let record = |i: u32, count: u16, powers: &mut Vec<(u32, u16)>| {
if count == 0 {
return;
}
if let Some(v) = powers.iter_mut().find(|v| v.0 == i) {
v.1 += count;
} else {
powers.push((i, count));
}
};
if let Some(ti) = two_idx {
let c2 = q.trailing_zeros();
if c2 != 0 {
q >>= c2;
record(ti, c2 as u16, &mut powers_scratch);
}
}
for (i, e) in base[..small_end].iter().enumerate() {
let p = e.prime as u64;
if p == 2 {
continue;
}
let r1 = root1[i];
if r1 != u32::MAX {
let posmodp = fastmod(posu, e.prime, ctx.pinv[i]);
if posmodp != r1 && posmodp != root2[i] {
continue;
}
}
let count = divide_out(&mut q, p);
record(i as u32, count, &mut powers_scratch);
}
for &ai in aidx {
let p = base[ai as usize].prime as u64;
let count = divide_out(&mut q, p);
if count != 0 && p >= small_skip as u64 {
confirmed_score += u16::from(score_weight(p as u32));
}
record(ai, count, &mut powers_scratch);
}
for idx in small_end..base.len() {
if q.is_one() || confirmed_score >= score_target {
break;
}
let r1 = root1[idx];
if r1 == u32::MAX {
continue; }
let p = base[idx].prime;
let position_mod_p = fastmod(posu, p, ctx.pinv[idx]);
if position_mod_p != r1 && position_mod_p != root2[idx] {
continue;
}
let count = divide_out(&mut q, p as u64);
if count != 0 {
confirmed_score += u16::from(score_weight(p));
}
record(idx as u32, count, &mut powers_scratch);
}
let large = if q.is_one() {
LargePrime::None
} else if q.bit_len() > 64 {
continue;
} else {
match classify_cofactor(q.as_parts()[0], ctx.single_limit, ctx.double_enabled) {
Some(lp) => lp,
None => continue,
}
};
let mut root = t.div_rem(&ctx.n).unwrap().1;
if tneg && !root.is_zero() {
root = ctx.n.wrapping_sub(&root)
}
out.push(Relation {
root,
sign,
powers: core::mem::take(&mut powers_scratch),
large,
});
}
survivors
}
fn to_column(r: Relation) -> Column {
Column {
root: r.root,
sign: r.sign,
powers: r.powers.into_iter().map(|(i, e)| (i, e as u32)).collect(),
extra_sqrt: Vec::new(),
}
}
fn combine_cycle<'a>(rels: impl IntoIterator<Item = &'a Relation>, n: &Natural) -> Column {
let mut root = Natural::ONE;
let mut sign = false;
let mut powers: BTreeMap<u32, u32> = BTreeMap::new();
let mut lp: BTreeMap<u64, u32> = BTreeMap::new();
for r in rels {
root = root.mul_mod(&r.root, n);
sign ^= r.sign;
for &(i, e) in &r.powers {
*powers.entry(i).or_default() += e as u32;
}
let (ps, k) = r.large.primes();
for &p in &ps[..k] {
*lp.entry(p).or_default() += 1;
}
}
let mut extra_sqrt = Vec::new();
for (p, c) in lp {
for _ in 0..c / 2 {
extra_sqrt.push(p);
}
}
Column {
root,
sign,
powers: powers.into_iter().collect(),
extra_sqrt,
}
}
fn classify_cofactor(q: u64, single_limit: u64, double_enabled: bool) -> Option<LargePrime> {
if is_prime64(q) {
return (q <= single_limit).then_some(LargePrime::One(q));
}
if !double_enabled {
return None;
}
let d = pollard_u64(q)?;
let e = q / d;
if d > 1 && e > 1 && d <= single_limit && e <= single_limit && is_prime64(d) && is_prime64(e) {
Some(LargePrime::Two(d.min(e), d.max(e)))
} else {
None
}
}
#[cfg(any(unix, windows))]
fn pollard_brent_natural(
n: &Natural,
iteration_limit: u64,
mut keep_going: impl FnMut() -> bool,
) -> Result<Option<Natural>, EngineError> {
if n.is_even() {
return Ok(Some(Natural::from_u64(2)));
}
let mut iterations = 0u64;
for c_value in 1..=8u64 {
if iterations >= iteration_limit {
return Ok(None);
}
if !keep_going() {
return Err(EngineError::Cancelled);
}
let c = Natural::from_u64(c_value);
let mut y = Natural::from_u64(2);
let mut r = 1u64;
let mut g = Natural::ONE;
let mut x = Natural::ZERO;
let mut ys = Natural::ZERO;
while g.is_one() && iterations < iteration_limit {
x = y.clone();
for _ in 0..r {
y = y.mul_mod(&y, n).add_mod(&c, n);
iterations += 1;
if iterations >= iteration_limit {
break;
}
}
let mut k = 0u64;
while k < r && g.is_one() && iterations < iteration_limit {
if !keep_going() {
return Err(EngineError::Cancelled);
}
ys = y.clone();
let mut q = Natural::ONE;
let batch = (r - k).min(128);
for _ in 0..batch {
y = y.mul_mod(&y, n).add_mod(&c, n);
let difference = if x >= y {
x.wrapping_sub(&y)
} else {
y.wrapping_sub(&x)
};
if !difference.is_zero() {
q = q.mul_mod(&difference, n);
}
iterations += 1;
if iterations >= iteration_limit {
break;
}
}
g = q.gcd(n);
k += batch;
}
r = r.saturating_mul(2);
}
if g == *n {
loop {
if !keep_going() {
return Err(EngineError::Cancelled);
}
ys = ys.mul_mod(&ys, n).add_mod(&c, n);
let difference = if x >= ys {
x.wrapping_sub(&ys)
} else {
ys.wrapping_sub(&x)
};
g = difference.gcd(n);
iterations += 1;
if !g.is_one() || iterations >= iteration_limit {
break;
}
}
}
if !g.is_one() && g != *n {
return Ok(Some(g));
}
}
Ok(None)
}
struct Mont64 {
modulus: u64,
inverse: u64,
}
impl Mont64 {
fn new(modulus: u64) -> Self {
debug_assert!(modulus & 1 == 1);
let mut inverse = 1u64;
for _ in 0..6 {
inverse = inverse.wrapping_mul(2u64.wrapping_sub(modulus.wrapping_mul(inverse)));
}
Self {
modulus,
inverse: inverse.wrapping_neg(),
}
}
#[inline]
fn reduce(&self, value: u128) -> u64 {
let multiplier = (value as u64).wrapping_mul(self.inverse);
let (sum, carry) = value.overflowing_add(multiplier as u128 * self.modulus as u128);
let mut reduced = (sum >> 64) + ((carry as u128) << 64);
if reduced >= self.modulus as u128 {
reduced -= self.modulus as u128;
}
reduced as u64
}
#[inline]
fn mul(&self, a: u64, b: u64) -> u64 {
self.reduce(a as u128 * b as u128)
}
fn encode(&self, value: u64) -> u64 {
(((value % self.modulus) as u128) << 64)
.checked_rem(self.modulus as u128)
.unwrap() as u64
}
fn decode(&self, value: u64) -> u64 {
self.reduce(value as u128)
}
#[inline]
fn add(&self, a: u64, b: u64) -> u64 {
let (sum, overflow) = a.overflowing_add(b);
if overflow || sum >= self.modulus {
sum.wrapping_sub(self.modulus)
} else {
sum
}
}
}
fn pollard_u64(n: u64) -> Option<u64> {
if n.is_multiple_of(2) {
return Some(2);
}
let gcd = |mut a: u64, mut b: u64| {
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a
};
let mont = Mont64::new(n);
for c in 1..64 {
let c_mont = mont.encode(c);
let mut y = mont.encode(2);
let mut r = 1u64;
let mut g = 1u64;
let mut x = 0u64;
let mut ys = 0u64;
while g == 1 {
x = y;
for _ in 0..r {
y = mont.add(mont.mul(y, y), c_mont);
}
let mut k = 0;
while k < r && g == 1 {
ys = y;
let batch = (r - k).min(128);
let mut product = mont.encode(1);
for _ in 0..batch {
y = mont.add(mont.mul(y, y), c_mont);
let difference = x.abs_diff(y);
if difference != 0 {
product = mont.mul(product, difference);
}
}
g = gcd(product, n);
k += batch;
}
r = r.saturating_mul(2);
}
if g == n {
loop {
ys = mont.add(mont.mul(ys, ys), c_mont);
g = gcd(x.abs_diff(ys), n);
if g != 1 {
break;
}
}
}
if g != n {
return Some(g);
}
}
None
}
#[derive(Default)]
struct Forest {
id_of: HashMap<u64, u32>,
parent: Vec<u32>,
edge: Vec<Option<u32>>,
relations: Vec<Relation>,
}
impl Forest {
fn vertex(&mut self, prime: u64) -> u32 {
if let Some(&id) = self.id_of.get(&prime) {
return id;
}
let id = self.parent.len() as u32;
self.id_of.insert(prime, id);
self.parent.push(id);
self.edge.push(None);
id
}
fn root(&self, mut v: u32) -> u32 {
while self.parent[v as usize] != v {
v = self.parent[v as usize];
}
v
}
fn path(&self, mut v: u32, out: &mut Vec<u32>) {
while self.parent[v as usize] != v {
out.push(self.edge[v as usize].unwrap());
v = self.parent[v as usize];
}
}
fn reroot(&mut self, v: u32) {
let mut chain = vec![v];
let mut edges: Vec<u32> = Vec::new();
let mut c = v;
while self.parent[c as usize] != c {
edges.push(self.edge[c as usize].unwrap());
c = self.parent[c as usize];
chain.push(c);
}
self.parent[v as usize] = v;
self.edge[v as usize] = None;
for (i, e) in edges.into_iter().enumerate() {
self.parent[chain[i + 1] as usize] = chain[i];
self.edge[chain[i + 1] as usize] = Some(e);
}
}
fn link(&mut self, a: u32, b: u32, rel: Relation) {
self.reroot(b);
let relation_index = self.relations.len() as u32;
self.relations.push(rel);
self.parent[b as usize] = a;
self.edge[b as usize] = Some(relation_index);
}
}
struct RelationCollector {
forest: Forest,
columns: Vec<Column>,
cycles: usize,
}
impl RelationCollector {
fn new() -> Self {
Self {
forest: Forest::default(),
columns: Vec::new(),
cycles: 0,
}
}
fn ingest(&mut self, rel: Relation, n: &Natural) {
match rel.large {
LargePrime::None => self.columns.push(to_column(rel)),
LargePrime::One(p) => self.edge(p, 1, rel, n),
LargePrime::Two(a, b) if a == b => {
self.cycles += 1;
self.columns.push(combine_cycle([&rel], n))
}
LargePrime::Two(a, b) => self.edge(a, b, rel, n),
}
}
fn edge(&mut self, pa: u64, pb: u64, rel: Relation, n: &Natural) {
let va = self.forest.vertex(pa);
let vb = self.forest.vertex(pb);
if self.forest.root(va) == self.forest.root(vb) {
let mut path = Vec::new();
self.forest.path(va, &mut path);
self.forest.path(vb, &mut path);
self.cycles += 1;
self.columns.push(combine_cycle(
core::iter::once(&rel).chain(
path.iter()
.map(|&index| &self.forest.relations[index as usize]),
),
n,
));
} else {
self.forest.link(va, vb, rel);
}
}
}
fn inv_u32(a: u32, p: u32) -> Option<u32> {
if a == 0 {
return None;
}
if p == 2 {
return Some(1);
}
let (mut u, mut v) = (a, p);
let (mut x1, mut x2) = (1u64, 0u64);
let modulus = p as u64;
while u != 1 && v != 1 {
while u & 1 == 0 {
u >>= 1;
x1 = if x1 & 1 == 0 {
x1 >> 1
} else {
(x1 + modulus) >> 1
};
}
while v & 1 == 0 {
v >>= 1;
x2 = if x2 & 1 == 0 {
x2 >> 1
} else {
(x2 + modulus) >> 1
};
}
if u >= v {
u -= v;
x1 = if x1 >= x2 { x1 - x2 } else { x1 + modulus - x2 };
} else {
v -= u;
x2 = if x2 >= x1 { x2 - x1 } else { x2 + modulus - x1 };
}
}
Some(if u == 1 { x1 } else { x2 } as u32)
}
fn mulmod_u32(a: u32, b: u32, p: u32) -> u32 {
(a as u64 * b as u64 % p as u64) as u32
}
#[inline]
fn add_mod_u32(a: u32, b: u32, p: u32) -> u32 {
let sum = a as u64 + b as u64;
if sum >= p as u64 {
(sum - p as u64) as u32
} else {
sum as u32
}
}
#[inline]
fn sub_mod_u32(a: u32, b: u32, p: u32) -> u32 {
if a >= b { a - b } else { a + (p - b) }
}
#[inline]
fn lemire_c(p: u32) -> u64 {
(u64::MAX / p as u64) + 1
}
#[inline]
fn fastmod(a: u32, p: u32, c: u64) -> u32 {
let lowbits = c.wrapping_mul(a as u64);
((lowbits as u128 * p as u128) >> 64) as u32
}
fn xorshift(mut x: u64) -> u64 {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x
}
fn knuth_schroeppel(n: &Natural) -> u64 {
const MULTIPLIERS: [u64; 29] = [
1, 2, 3, 5, 6, 7, 10, 11, 13, 14, 15, 17, 19, 21, 22, 23, 26, 29, 30, 31, 33, 34, 35, 37,
38, 41, 42, 43, 47,
];
const KS_PRIMES: usize = 500;
let nmod8 = n.mod_u64(8);
let mut weights = [0.0f64; MULTIPLIERS.len()];
for (w, &k) in weights.iter_mut().zip(&MULTIPLIERS) {
let mod8 = (nmod8 * k) % 8;
let mut v = 0.346_573_59_f64; if mod8 == 1 {
v *= 4.0;
} else if mod8 == 5 {
v *= 2.0;
}
*w = v - (k as f64).ln() / 2.0;
}
let mut p = 3u64;
let mut seen = 0usize;
while seen < KS_PRIMES {
if is_prime64(p) {
seen += 1;
let nmod = n.mod_u64(p);
if nmod != 0 {
let logpdivp = (p as f64).ln() / p as f64;
let kron = jacobi_u64(nmod, p) as i32; for (w, &k) in weights.iter_mut().zip(&MULTIPLIERS) {
let km = k % p;
if km == 0 {
*w += logpdivp; } else if kron * jacobi_u64(km, p) as i32 == 1 {
*w += 2.0 * logpdivp; }
}
}
}
p += 2;
}
let mut best = f64::NEG_INFINITY;
let mut k = 1u64;
for (&w, &m) in weights.iter().zip(&MULTIPLIERS) {
if w > best {
best = w;
k = m;
}
}
k
}
fn is_prime64(n: u64) -> bool {
if n < 2 {
return false;
}
for p in [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37] {
if n == p {
return true;
}
if n.is_multiple_of(p) {
return false;
}
}
let (mut d, mut s) = (n - 1, 0);
while d % 2 == 0 {
d /= 2;
s += 1
}
for a in [2u64, 325, 9375, 28178, 450775, 9780504, 1795265022] {
if a % n == 0 {
continue;
}
let mut x = powmod64(a % n, d, n);
if x == 1 || x == n - 1 {
continue;
}
let mut ok = false;
for _ in 1..s {
x = (x as u128 * x as u128 % n as u128) as u64;
if x == n - 1 {
ok = true;
break;
}
}
if !ok {
return false;
}
}
true
}
fn powmod64(mut a: u64, mut e: u64, n: u64) -> u64 {
let mut r = 1;
while e != 0 {
if e & 1 != 0 {
r = (r as u128 * a as u128 % n as u128) as u64
}
a = (a as u128 * a as u128 % n as u128) as u64;
e >>= 1
}
r
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interval_translation_matches_the_signed_coordinate_shift() {
let p = Natural::from_u64(18_446_744_073_709_551_557);
let q = Natural::from_u64(18_446_744_073_709_551_533);
let ctx = prepare(p.checked_mul(&q).unwrap()).unwrap().0;
let params = crate::qs::parameters::engine_params(ctx.n.bit_len());
assert_eq!(ctx.interval, params.sieve_half_width as i32);
assert!(ctx.interval > 0);
for (entry, &translation) in ctx.base.iter().zip(ctx.interval_mod_p.iter()) {
let prime = entry.prime;
assert_eq!(translation, params.sieve_half_width % prime);
assert_eq!(translation, ctx.interval as u32 % prime);
for x in [
-(ctx.interval as i64),
-1,
0,
1,
(prime as i64) - 1,
ctx.interval as i64 - 1,
] {
let position = (x + ctx.interval as i64) as u64 % prime as u64;
let xmod = x.rem_euclid(prime as i64) as u32;
assert_eq!(
position as u32,
add_mod_u32(xmod, translation, prime),
"p={prime} x={x}"
);
}
}
}
#[test]
fn polynomial_selection_famine_is_diagnosed_without_searching() {
let base = [FactorBaseEntry {
prime: 3,
log_prime: 9,
sqrt_n: 1,
}];
let target = Natural::from_u64(1 << 40);
let (all, pool, count) = build_a_candidates(&base, &target);
assert!(
all.len() < count || pool.is_empty(),
"a one-prime factor base cannot supply {count} coefficient factors"
);
}
#[test]
fn precomputed_remainders_and_root_translation_are_exact() {
for p in 2u32..=10_000 {
let c = lemire_c(p);
for a in [0, 1, p - 1, p, p.saturating_add(1), u32::MAX] {
assert_eq!(fastmod(a, p, c), a % p, "p={p}, a={a}");
}
for a in [0, 1, p - 1] {
for b in [0, 1, p - 1] {
assert_eq!(add_mod_u32(a, b, p), (a as u64 + b as u64) as u32 % p);
}
}
}
}
#[test]
fn portable_jobs_are_deterministic() {
let p = Natural::from_u64(18_446_744_073_709_551_557);
let q = Natural::from_u64(18_446_744_073_709_551_533);
let context = prepare(p.checked_mul(&q).unwrap()).unwrap();
let a = execute(&context, EngineJob { family: 7 });
let b = execute(&context, EngineJob { family: 7 });
assert_eq!(a.family, b.family);
assert_eq!(a.polynomials, b.polynomials);
assert_eq!(a.relations, b.relations);
assert!(a.polynomials > 0);
}
#[cfg(any(unix, windows))]
#[test]
fn session_drops_duplicate_a_families_however_they_are_scheduled() {
use std::str::FromStr;
let n = Natural::from_str("668319744971798315493259725219859").unwrap();
let context = prepare(n).unwrap();
let mut session = EngineSession::new(context.clone());
let mut duplicates = 0;
for family in 0..64u64 {
let before = session.polynomials();
session.submit(execute(&context, EngineJob { family }));
if session.polynomials() == before {
duplicates += 1;
}
}
assert!(
duplicates > 0,
"this input is supposed to generate duplicate A values; the test has gone stale"
);
let mut seen = HashSet::new();
for family in 0..64u64 {
if let Some((a, _)) = choose_a(&context.0, family) {
seen.insert(a);
}
}
assert_eq!(
session.polynomials(),
seen.len() as u64 * (1 << (context.0.a_factor_count - 1).min(9)),
"accepted polynomial count does not match the number of distinct A values"
);
}
#[test]
fn collector_accepts_out_of_order_results() {
let p = Natural::from_u64(18_446_744_073_709_551_557);
let q = Natural::from_u64(18_446_744_073_709_551_533);
let context = prepare(p.checked_mul(&q).unwrap()).unwrap();
let mut session = EngineSession::new(context.clone());
let jobs = session.take_jobs(2);
session.submit(execute(&context, jobs[1]));
assert_eq!(session.polynomials(), 0);
session.submit(execute(&context, jobs[0]));
assert!(session.polynomials() > 0);
}
#[cfg(any(unix, windows))]
#[test]
fn full_parallel_engine_factors_128_bit_semiprime() {
let p = Natural::from_u64(18_446_744_073_709_551_557);
let q = Natural::from_u64(18_446_744_073_709_551_533);
let n = p.checked_mul(&q).unwrap();
let factors = factor(n.clone(), 2, |_| true).unwrap();
assert_eq!(factors, [q, p]);
assert_eq!(
factors
.iter()
.try_fold(Natural::ONE, |a, b| a.checked_mul(b)),
Some(n)
);
}
const DEAD_ZONE: [&str; 6] = [
"18446744400127067027", "635904368119925963561", "20988451891514649258347", "703713894016303629914563", "22921914745054882120472087", "648536833001811612107041493", ];
#[test]
fn siqs_builds_polynomials_across_the_dead_zone() {
use std::str::FromStr;
for case in DEAD_ZONE {
let n = Natural::from_str(case).unwrap();
let context =
prepare(n.clone()).unwrap_or_else(|e| panic!("{case} ({} bits): {e}", n.bit_len()));
assert!(
!context.0.a_pool.is_empty(),
"{case} ({} bits): empty A candidate pool",
n.bit_len()
);
let result = execute(&context, EngineJob { family: 0 });
assert!(
result.polynomials > 0,
"{case} ({} bits): no polynomials from family 0",
n.bit_len()
);
}
}
#[cfg(any(unix, windows))]
#[test]
fn siqs_alone_factors_the_dead_zone() {
use std::str::FromStr;
for case in DEAD_ZONE {
let n = Natural::from_str(case).unwrap();
let d = find_factor(n.clone(), 2, &mut |_| true)
.unwrap_or_else(|e| panic!("{case} ({} bits): {e}", n.bit_len()));
assert!(!d.is_one() && d != n, "{case}: trivial factor {d}");
assert!(
n.div_rem(&d).unwrap().1.is_zero(),
"{case}: {d} does not divide it"
);
}
}
#[cfg(any(unix, windows))]
#[test]
fn rho_stage_splits_unbalanced_inputs_without_entering_siqs() {
use std::str::FromStr;
let cases = [
"88948294177717782578521953992989251229",
"1185123569529286501965460691005493488051524107431",
"13695626177198106295200293487798368178679518660650179392786377544541",
];
for case in cases {
let n = Natural::from_str(case).unwrap();
stage_counts::reset();
let factors = factor(n.clone(), 2, |_| true).unwrap();
assert_eq!(
factors
.iter()
.try_fold(Natural::ONE, |acc, f| acc.checked_mul(f)),
Some(n.clone()),
"{case} did not factor back"
);
assert!(
stage_counts::rho() > 0,
"{case} ({} bits) did not reach the rho stage",
n.bit_len()
);
assert_eq!(
stage_counts::siqs(),
0,
"{case} ({} bits) entered SIQS",
n.bit_len()
);
}
}
#[cfg(any(unix, windows))]
#[test]
fn balanced_semiprimes_fall_through_rho_to_siqs() {
let p = Natural::from_u64(18_446_744_073_709_551_557);
let q = Natural::from_u64(18_446_744_073_709_551_533);
let n = p.checked_mul(&q).unwrap();
stage_counts::reset();
factor(n, 2, |_| true).unwrap();
assert!(stage_counts::siqs() > 0, "128-bit input skipped SIQS");
}
#[cfg(any(unix, windows))]
#[test]
fn rho_respects_its_total_iteration_budget() {
let p = Natural::from_u64(18_446_744_073_709_551_557);
let q = Natural::from_u64(18_446_744_073_709_551_533);
let n = p.checked_mul(&q).unwrap();
let mut polls = 0usize;
let started = std::time::Instant::now();
let result = pollard_brent_natural(&n, 4_096, || {
polls += 1;
true
})
.unwrap();
assert!(
result.is_none(),
"4096 iterations should not split a 128-bit balanced semiprime"
);
assert!(
started.elapsed() < std::time::Duration::from_secs(5),
"budget was not shared across constants ({polls} cancellation polls)"
);
}
#[test]
fn montgomery_brent_splits_fixed_cofactor_corpus() {
for (left, right) in [
(1_000_003u64, 1_000_033u64),
(15_485_863, 15_485_867),
(4_294_967_291, 4_294_967_279),
] {
let n = left * right;
let factor = pollard_u64(n).expect("Brent failed to split cofactor");
assert!(factor == left || factor == right, "{n} -> {factor}");
}
}
#[test]
#[ignore = "manual cofactor-split performance measurement"]
fn profile_pollard_u64() {
let n = 134_217_689u64 * 134_217_757u64;
let started = std::time::Instant::now();
for _ in 0..1_000 {
assert!(
pollard_u64(std::hint::black_box(n))
.map(std::hint::black_box)
.is_some()
);
}
eprintln!(
"BENCH pollard_u64_1000={:.6}s",
started.elapsed().as_secs_f64()
);
}
}