mod extract;
mod siqs;
mod wire;
use crate::f2::SparseBinaryMatrix;
use crate::factor::FactorTuning;
#[cfg(any(unix, windows))]
use crate::natural::MontgomeryContext;
use crate::qs::{AutoOr, FactorBaseEntry, MultiplierChoice, QsConfig, prepare_factor_base};
use crate::{Natural, PARTS, jacobi_u64};
#[cfg(any(unix, windows))]
use crate::{PrimalityConfig, WitnessPolicy, 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,
relation_percent: Option<usize>,
small_skip: u32,
threshold_margin: i32,
profile: 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,
}
#[cfg(feature = "fuzzing")]
pub(crate) fn validate_worker_packet(bytes: &[u8]) -> bool {
wire::deserialize_family(bytes).is_some()
}
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, tuning: &FactorTuning) -> Result<EngineContext, EngineError> {
let mut p = crate::qs::parameters::engine_params(n.bit_len());
if let Some(bound) = tuning.factor_base_bound {
p.factor_base_bound = bound;
}
if let Some(half_width) = tuning.sieve_half_width {
p.sieve_half_width = half_width;
}
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),
};
let prepared = prepare_factor_base(&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_skip = tuning.small_skip.unwrap_or(100);
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) = siqs::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 + tuning.threshold_adjustment.unwrap_or(0),
single_limit,
double_enabled,
relation_percent: tuning.relation_percent,
small_skip,
threshold_margin: tuning.threshold_margin.unwrap_or(0),
profile: tuning.profile,
});
if siqs::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 = siqs::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(), context.0.relation_percent);
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) = wire::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 = siqs::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::extract(&self.context.0, &self.collector.columns)
}
}
fn relation_target(base_len: usize, percent: Option<usize>) -> usize {
if let Some(percent) = percent {
return (base_len * percent.clamp(100, 110) / 100).max(base_len + 1);
}
base_len + 64
}
#[cfg(any(unix, windows))]
pub fn factor(
mut n: Natural,
threads: usize,
tuning: &FactorTuning,
witness_seed: Option<[u8; 32]>,
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 mut primality = PrimalityConfig::default();
if let Some(seed) = witness_seed {
primality.witnesses = WitnessPolicy::Seeded { seed };
}
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,
tuning,
&mut progress,
&mut factors,
)?;
factors.sort();
Ok(factors)
}
#[cfg(any(unix, windows))]
fn factor_node(
n: Natural,
threads: usize,
pc: &PrimalityConfig,
tuning: &FactorTuning,
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, tuning, 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 tuning.profile {
eprintln!(
"PROFILE rho input_bits={} factor_bits={} siqs=false",
n.bit_len(),
factor.bit_len()
);
}
factor
}
None => find_factor(n.clone(), threads, tuning, progress)?,
};
if d.is_one() || d == n {
return Err(EngineError::NoFactor);
}
let q = n.div_rem(&d).unwrap().0;
factor_node(d, threads, pc, tuning, progress, out)?;
factor_node(q, threads, pc, tuning, progress, out)
}
#[cfg(any(unix, windows))]
fn find_factor(
n: Natural,
threads: usize,
tuning: &FactorTuning,
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 = tuning.profile;
let t_fb = std::time::Instant::now();
let ctx = prepare(n.clone(), tuning)?.0;
let target = relation_target(ctx.base.len(), ctx.relation_percent);
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(siqs::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 = siqs::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::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 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
}
#[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 = ctx.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
+ ctx.threshold_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) = siqs::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) = siqs::signed_add(&ax2, false, &two_bx, bneg ^ (x < 0));
let (mut q, sign) = siqs::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 crate::u64math::is_prime(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
&& crate::u64math::is_prime(d)
&& crate::u64math::is_prime(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 montgomery = MontgomeryContext::new(n).expect("rho modulus is odd and engine-sized");
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 = montgomery.encode(&Natural::from_u64(c_value));
let mut y = montgomery.encode(&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 = montgomery.add(&montgomery.square(&y), &c);
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 = montgomery.one();
let batch = (r - k).min(128);
for _ in 0..batch {
y = montgomery.add(&montgomery.square(&y), &c);
let difference = if x >= y {
x.wrapping_sub(&y)
} else {
y.wrapping_sub(&x)
};
if !difference.is_zero() {
q = montgomery.multiply(&q, &difference);
}
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 = montgomery.add(&montgomery.square(&ys), &c);
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 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 crate::u64math::is_prime(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
}
#[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(), &FactorTuning::default())
.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) = siqs::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(), &FactorTuning::default()).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, &FactorTuning::default()).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, _)) = siqs::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(), &FactorTuning::default()).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, &FactorTuning::default(), None, |_| 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(), &FactorTuning::default())
.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, &FactorTuning::default(), &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, &FactorTuning::default(), None, |_| 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, &FactorTuning::default(), None, |_| 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()
);
}
}