Skip to main content

binius_circuits/float64/
add.rs

1// Copyright 2025 Irreducible Inc.
2use binius_core::Word;
3use binius_frontend::{CircuitBuilder, Wire};
4
5use super::utils::*;
6
7/// IEEE-754 double (binary64) addition built from small testable blocks.
8/// Returns a 64-bit IEEE-754 encoding.
9///
10/// Behavior summary:
11/// - Fully handles normals, subnormals, zeros (signed), infinities, NaNs
12/// - Rounding mode: round-to-nearest, ties-to-even
13/// - Sticky tracking across alignment and underflow right-shifts
14/// - Exact cancellation (+x) + (-x) → +0
15/// - Overflow → ±Inf with computed sign
16pub fn float64_add(builder: &CircuitBuilder, a: Wire, b: Wire) -> Wire {
17	// unpack & classify
18	let pa = fp64_unpack(builder, a);
19	let pb = fp64_unpack(builder, b);
20
21	// extended sig & effective exp
22	let (sig_a, exp_a) = fp64_ext_sig_and_exp(builder, &pa);
23	let (sig_b, exp_b) = fp64_ext_sig_and_exp(builder, &pb);
24
25	// order by exponent
26	let (
27		sig_a_ordered,
28		exp_a_ordered,
29		sign_a_ordered,
30		sig_b_ordered,
31		exp_b_ordered,
32		sign_b_ordered,
33		_swapped,
34	) = fp64_order_by_exp(builder, sig_a, exp_a, pa.sign, sig_b, exp_b, pb.sign);
35
36	// alignment with sticky→bit0
37	let d = isub(builder, exp_a_ordered, exp_b_ordered);
38	let s_b = fp64_align_with_sticky(builder, sig_b_ordered, d);
39
40	// same/different sign split
41	let diff_sign = builder.bxor(sign_a_ordered, sign_b_ordered);
42
43	// add path
44	let (sum_norm, exp_add) = fp64_add_path(builder, sig_a_ordered, s_b, exp_a_ordered);
45
46	// sub path
47	let (diff_norm, exp_sub, sign_sub, mags_equal) =
48		fp64_sub_path(builder, sig_a_ordered, s_b, exp_a_ordered, sign_a_ordered, sign_b_ordered);
49
50	// merge + cancellation -> +0
51	let (res_sig, res_exp, res_sign) = fp64_merge_and_cancel(
52		builder,
53		diff_sign,
54		sum_norm,
55		exp_add,
56		sign_a_ordered,
57		diff_norm,
58		exp_sub,
59		sign_sub,
60		mags_equal,
61	);
62
63	// underflow pre-round right-shift if exp<=0
64	let (sig_round_base, exp_round_base, exp_lt_1) =
65		fp64_underflow_shift(builder, res_sig, res_exp);
66
67	// round to nearest even
68	let (mant_final_53, exp_after_round, mant_overflow_mask) =
69		fp64_round_rne(builder, sig_round_base, exp_round_base);
70
71	// Pack finite or overflow to inf.
72	// Subnormal regime if:
73	//   - we were below 1 (exp_lt_1), OR
74	//   - base exponent == 1 and the integer bit (bit 63) is 0 (no hidden 1)
75	// `bnot` already carries the negated integer bit in the MSB, and every consumer
76	// below reads only the MSB, so extracting bit 63 and comparing it against zero
77	// would spend two AND constraints for nothing.
78	let msb_is_zero = builder.bnot(sig_round_base); // MSB-bool
79	let base_is_one = builder.icmp_eq(exp_round_base, one(builder));
80	let in_sub_regime = builder.bor(exp_lt_1, builder.band(base_is_one, msb_is_zero));
81	let stayed_sub_mask = builder.band(in_sub_regime, builder.bnot(mant_overflow_mask));
82	let finite_or_inf =
83		fp64_pack_finite_or_inf(builder, res_sign, mant_final_53, exp_after_round, stayed_sub_mask);
84
85	// operand-driven specials (NaN, operand infinities)
86	fp64_finish_specials(builder, &pa, &pb, finite_or_inf)
87}
88
89/// Build an **extended significand** and **effective exponent**.
90///
91/// Convention:
92/// - We keep the integer bit at position 63 and leave **11 low bits** for G/R/S and headroom. i.e.,
93///   `sig_ext = significand << 11`.
94///
95/// For normals:  sig = ((1<<52) | frac) << 11,  exp_eff = exp
96/// For subnorms:  sig = (frac           ) << 11, exp_eff = 1
97///
98/// Input:
99/// - `p`: parts from `fp64_unpack`
100///
101/// Output:
102/// - `(sig_ext, exp_eff)` using the above convention.
103fn fp64_ext_sig_and_exp(b: &CircuitBuilder, p: &Fp64Parts) -> (Wire, Wire) {
104	let one52 = b.add_constant_64(1u64 << 52);
105
106	let sig_norm = b.shl(b.bor(one52, p.frac), 11);
107	let sig_sub = b.shl(p.frac, 11);
108	let sig = b.select(p.is_norm, sig_norm, sig_sub);
109
110	let exp_eff = b.select(p.is_norm, p.exp, one(b)); // subnormals use exp=1
111	(sig, exp_eff)
112}
113
114/// Order two operands by **effective exponent** (A has >= exponent).
115///
116/// Input:
117/// - `(sig_a, exp_a, sign_a)`, `(sig_b, exp_b, sign_b)`
118///
119/// Output:
120/// - `(sig_a, exp_a, sign_a, sig_b, exp_b, sign_b, swapped_mask)` where `swapped_mask` is all-1 if
121///   we swapped A/B (i.e., A.exp < B.exp).
122fn fp64_order_by_exp(
123	b: &CircuitBuilder,
124	sig_a: Wire,
125	exp_a: Wire,
126	sign_a: Wire,
127	sig_b: Wire,
128	exp_b: Wire,
129	sign_b: Wire,
130) -> (Wire, Wire, Wire, Wire, Wire, Wire, Wire) {
131	let a_lt_b = b.icmp_ult(exp_a, exp_b);
132
133	let exp_a_ordered = b.select(a_lt_b, exp_b, exp_a);
134	let exp_b_ordered = b.select(a_lt_b, exp_a, exp_b);
135	let sig_a_ordered = b.select(a_lt_b, sig_b, sig_a);
136	let sig_b_ordered = b.select(a_lt_b, sig_a, sig_b);
137	let sign_a_ordered = b.select(a_lt_b, sign_b, sign_a);
138	let sign_b_ordered = b.select(a_lt_b, sign_a, sign_b);
139
140	(
141		sig_a_ordered,
142		exp_a_ordered,
143		sign_a_ordered,
144		sig_b_ordered,
145		exp_b_ordered,
146		sign_b_ordered,
147		a_lt_b,
148	)
149}
150
151/// Align `sig_b` to `exp_a` by right shifting with **sticky** folded to bit 0.
152///
153/// Input:
154/// - `sig_b`: extended significand per our convention
155/// - `d   = exp_a - exp_b`
156/// - Saturates large shifts to 63; accumulates sticky over all discarded bits.
157///
158/// Output:
159/// - `aligned` where bit0 = old_bit0 | sticky (i.e., precise sticky folding).
160fn fp64_align_with_sticky(b: &CircuitBuilder, sig_b: Wire, d: Wire) -> Wire {
161	let (mut v, sticky) = var_shr_with_sticky(b, sig_b, d, true);
162
163	// (v & ~1) | (v & 1) | (sticky & 1) == v | (sticky & 1): the fold only ever
164	// sets bit 0, so clearing it first is dead work.
165	v = b.bor(v, b.band(sticky, one(b)));
166	v
167}
168
169/// Magnitude add path (same signs), with one-bit renormalize if carry=1.
170///
171/// Input:
172/// - `sig_a`, `s_b`, `exp_a`
173///
174/// Output:
175/// - `(sum_norm, exp_add)` If carry occurs, result is shifted right by 1; new sticky = (old R) |
176///   (old S).
177fn fp64_add_path(b: &CircuitBuilder, sig_a: Wire, s_b: Wire, exp_a: Wire) -> (Wire, Wire) {
178	let (sum_raw, carry_mask) = b.iadd(sig_a, s_b);
179
180	// Detect final carry-out (bit63 of carry mask) as 0/1 and as a select mask
181	let carry_bit01 = bit_lsb(b, carry_mask, 63); // 0/1
182	let carry_sel_mask = bit_msb01(b, carry_mask, 63); // use carry's MSB directly as MSB-bool
183
184	// if carry: shift 1 and update sticky := old R | old S
185	let sum_shift1 = b.shr(sum_raw, 1);
186	let old_r = bit_lsb(b, sum_raw, 9);
187	let old_s = b.band(sum_raw, one(b));
188	let new_s = b.bor(old_r, old_s);
189
190	// Inject carry into bit63 when renormalizing: (carry_bit01 << 63)
191	let carry_hi = b.shl(carry_bit01, 63);
192	let sum_shift1_with_carry = b.bor(sum_shift1, carry_hi);
193
194	let keep = b.bnot(one(b));
195	let sum_shift1clr = b.band(sum_shift1_with_carry, keep);
196	let sum_norm = b.select(carry_sel_mask, b.bor(sum_shift1clr, new_s), sum_raw);
197
198	// Increment exponent by carry (0/1)
199	let exp_add = b.iadd(exp_a, carry_bit01).0;
200	(sum_norm, exp_add)
201}
202
203/// Magnitude subtraction path (different signs), normalized by CLZ.
204///
205/// Chooses `big-small` by comparing magnitudes; sign takes the sign of the larger magnitude.
206/// Normalizes by shifting left up to `min(clz(diff), exp_a-1)`.
207///
208/// Input:
209/// - `sig_a`, `s_b`, `exp_a`, `sign_a`, `sign_b`
210///
211/// Output:
212/// - `(diff_norm, exp_sub, sign_sub, mags_equal_mask)`
213///   - If `sig_a == s_b`, `diff_norm` may be 0; caller can force +0 behavior using
214///     `mags_equal_mask`.
215fn fp64_sub_path(
216	b: &CircuitBuilder,
217	sig_a: Wire,
218	s_b: Wire,
219	exp_a: Wire,
220	sign_a: Wire,
221	sign_b: Wire,
222) -> (Wire, Wire, Wire, Wire) {
223	let a_lt_b = b.icmp_ult(sig_a, s_b);
224	let big = b.select(a_lt_b, s_b, sig_a);
225	let small = b.select(a_lt_b, sig_a, s_b);
226	let diff_raw = isub(b, big, small);
227	let sign_sub = b.select(a_lt_b, sign_b, sign_a);
228	let mags_eq = b.icmp_eq(sig_a, s_b);
229
230	let lz = clz64(b, diff_raw);
231	let c64 = b.add_constant_64(64);
232	let c63 = b.add_constant_64(63);
233	let lt64 = b.icmp_ult(lz, c64);
234	let lz_clamped = b.select(lt64, lz, c63);
235
236	let exp_a_m1 = isub(b, exp_a, one(b));
237	let lz_lt_expm1 = b.icmp_ult(lz_clamped, exp_a_m1);
238	let sh = b.select(lz_lt_expm1, lz_clamped, exp_a_m1);
239
240	let diff_norm = var_shl(b, diff_raw, sh);
241	let exp_sub = isub(b, exp_a, sh);
242
243	(diff_norm, exp_sub, sign_sub, mags_eq)
244}
245
246/// Merge add/sub paths and handle **exact cancellation → +0**.
247///
248/// Input:
249/// - `same_sign_mask`
250/// - add: `(sum_norm, exp_add, sign_a)`
251/// - sub: `(diff_norm, exp_sub, sign_sub, mags_equal_mask)`
252///
253/// Output:
254/// - `(res_sig, res_exp, res_sign)` with cancellation mapped to +0.
255#[allow(clippy::too_many_arguments)]
256fn fp64_merge_and_cancel(
257	b: &CircuitBuilder,
258	diff_sign: Wire,
259	sum_norm: Wire,
260	exp_add: Wire,
261	sign_a: Wire,
262	diff_norm: Wire,
263	exp_sub: Wire,
264	sign_sub: Wire,
265	mags_equal: Wire,
266) -> (Wire, Wire, Wire) {
267	let res_sig_pre = b.select(diff_sign, diff_norm, sum_norm);
268	let res_exp_pre = b.select(diff_sign, exp_sub, exp_add);
269	let res_sign_pre = b.select(diff_sign, sign_sub, sign_a);
270
271	// different signs and equal magnitudes => +0
272	let cancel = b.band(diff_sign, mags_equal);
273
274	let res_sig = b.select(cancel, zero(b), res_sig_pre);
275	let res_exp = b.select(cancel, zero(b), res_exp_pre); // exp ignored for zero
276	let res_sign = b.select(cancel, zero(b), res_sign_pre); // +0
277	(res_sig, res_exp, res_sign)
278}
279
280/// Final special-case selection: NaN and operand Infinities.
281///
282/// Precedence: **NaN > operand-Inf > (finite/overflow result)**.
283///
284/// - If any NaN, or (+Inf)+(-Inf), return canonical quiet NaN: 0x7FF8_0000_0000_0000
285/// - Else if any operand is Inf, return that infinity with its operand sign
286/// - Else return `finite_or_inf`
287///
288/// Inputs:
289/// - `pa`, `pb`: parts from `fp64_unpack` for a and b
290/// - `finite_or_inf`: result from `fp64_pack_finite_or_inf` (includes overflow to Inf)
291///
292/// Output:
293/// - final packed double
294fn fp64_finish_specials(
295	b: &CircuitBuilder,
296	pa: &Fp64Parts,
297	pb: &Fp64Parts,
298	finite_or_inf: Wire,
299) -> Wire {
300	let qnan = b.add_constant_64(0x7FF8_0000_0000_0000);
301	let exp_2047 = b.add_constant_64(0x7FF);
302	let inf_payload = b.shl(exp_2047, 52);
303
304	let diff_sign = b.bxor(pa.sign, pb.sign);
305	let opp_inf_nan = b.band(b.band(pa.is_inf, pb.is_inf), diff_sign);
306	let any_nan = b.bor(pa.is_nan, pb.is_nan);
307	let nan_mask = b.bor(any_nan, opp_inf_nan);
308
309	// If any operand is infinity (with either same signs or only one inf), pick its sign.
310	let any_inf = b.bor(pa.is_inf, pb.is_inf);
311	let inf_sign = b.select(pb.is_inf, pb.sign, pa.sign);
312	let packed_inf = b.bor(b.band(inf_sign, b.add_constant(Word::MSB_ONE)), inf_payload);
313
314	let with_inf = b.select(any_inf, packed_inf, finite_or_inf);
315	b.select(nan_mask, qnan, with_inf)
316}
317
318#[cfg(test)]
319mod tests {
320	use binius_core::word::Word;
321
322	use super::*;
323	use crate::float64::utils::tests::{
324		f64_bits_semantic_eq, ref_fp64_pack_finite_or_inf, ref_fp64_round_rne,
325		ref_fp64_underflow_shift, ref_fp64_unpack,
326	};
327
328	fn ref_fp64_ext_sig_and_exp(_sign: u64, exp: u64, frac: u64, is_norm: u64) -> (u64, u64) {
329		let sig = if is_norm != 0 {
330			((1u64 << 52) | frac) << 11
331		} else {
332			frac << 11
333		};
334
335		let exp_eff = if is_norm != 0 { exp } else { 1 };
336
337		(sig, exp_eff)
338	}
339
340	fn ref_fp64_order_by_exp(
341		sig_a: u64,
342		exp_a: u64,
343		sign_a: u64,
344		sig_b: u64,
345		exp_b: u64,
346		sign_b: u64,
347	) -> (u64, u64, u64, u64, u64, u64, u64) {
348		if exp_a < exp_b {
349			(sig_b, exp_b, sign_b, sig_a, exp_a, sign_a, 1u64 << 63)
350		} else {
351			(sig_a, exp_a, sign_a, sig_b, exp_b, sign_b, 0)
352		}
353	}
354
355	fn ref_fp64_align_with_sticky(sig_b: u64, d: u64) -> u64 {
356		let d_eff = std::cmp::min(d, 63);
357		if d_eff == 0 {
358			return sig_b;
359		}
360
361		let shifted = sig_b >> d_eff;
362		let lost_bits = sig_b & ((1u64 << d_eff) - 1);
363		let sticky = if lost_bits != 0 { 1 } else { 0 };
364
365		// Set bit 0 to be old bit 0 OR sticky
366		let old_bit0 = shifted & 1;
367		let new_bit0 = old_bit0 | sticky;
368		(shifted & !1) | new_bit0
369	}
370
371	fn ref_fp64_add_path(sig_a: u64, s_b: u64, exp_a: u64) -> (u64, u64) {
372		// Reference implementation following the provided pseudocode exactly
373		// function fp64_add_path(sig_a, s_b, exp_a) -> (sum_norm, exp_add):
374
375		// 1) Add magnitudes
376		let sum_raw = sig_a.wrapping_add(s_b);
377
378		// 2) Detect 1-bit carry out of the 64-bit lane
379		// (classic unsigned carry detection)
380		let carry01 = if sum_raw < sig_a { 1 } else { 0 };
381
382		// 3) If carry: renormalize by shifting right one, and update sticky := old_R | old_S
383		if carry01 == 1 {
384			// Old rounding geometry before the shift
385			let old_r = (sum_raw >> 9) & 1; // bit 9
386			let old_s = sum_raw & 1; // bit 0 (already a sticky-OR)
387
388			// Shift right and preserve the carry into bit 63
389			let shifted = (sum_raw >> 1) | (1u64 << 63);
390			let new_s = old_r | old_s; // new sticky for bit 0 after shift
391			let sum_norm = (shifted & !1) | new_s; // clear bit0, then OR in newS
392
393			let exp_add = exp_a + 1;
394
395			(sum_norm, exp_add)
396		} else {
397			// No carry: already normalized
398			let sum_norm = sum_raw;
399			let exp_add = exp_a;
400
401			(sum_norm, exp_add)
402		}
403	}
404
405	fn ref_fp64_sub_path(
406		sig_a: u64,
407		s_b: u64,
408		exp_a: u64,
409		sign_a: u64,
410		sign_b: u64,
411	) -> (u64, u64, u64, u64) {
412		// Match the circuit logic exactly
413		let a_lt_b = if sig_a < s_b { 1u64 << 63 } else { 0 };
414		let big = if a_lt_b != 0 { s_b } else { sig_a };
415		let small = if a_lt_b != 0 { sig_a } else { s_b };
416		let diff_raw = big.wrapping_sub(small);
417		let sign_sub = if a_lt_b != 0 { sign_b } else { sign_a };
418		let mags_eq = if sig_a == s_b { 1u64 << 63 } else { 0 };
419
420		// Use clz64 reference implementation
421		let lz = if diff_raw == 0 {
422			64
423		} else {
424			diff_raw.leading_zeros() as u64
425		};
426		let c64 = 64;
427		let c63 = 63;
428		let lt64 = if lz < c64 { 1u64 << 63 } else { 0 };
429		let lz_clamped = if lt64 != 0 { lz } else { c63 };
430
431		let exp_a_m1 = exp_a.wrapping_sub(1);
432		let lz_lt_expm1 = if lz_clamped < exp_a_m1 { 1u64 << 63 } else { 0 };
433		let sh = if lz_lt_expm1 != 0 {
434			lz_clamped
435		} else {
436			exp_a_m1
437		};
438
439		// Use variable left shift
440		let shift_amt = std::cmp::min(sh, 63); // Simulate var_shl behavior
441		let diff_norm = diff_raw << shift_amt;
442		let exp_sub = exp_a.wrapping_sub(sh);
443
444		(diff_norm, exp_sub, sign_sub, mags_eq)
445	}
446
447	#[allow(clippy::too_many_arguments)]
448	fn ref_fp64_merge_and_cancel(
449		diff_sign: u64,
450		sum_norm: u64,
451		exp_add: u64,
452		sign_a: u64,
453		diff_norm: u64,
454		exp_sub: u64,
455		sign_sub: u64,
456		mags_equal: u64,
457	) -> (u64, u64, u64) {
458		let (res_sig_pre, res_exp_pre, res_sign_pre) = if diff_sign & (1u64 << 63) == 0 {
459			(sum_norm, exp_add, sign_a)
460		} else {
461			(diff_norm, exp_sub, sign_sub)
462		};
463
464		// Handle exact cancellation -> +0
465		let cancel = if diff_sign & (1u64 << 63) != 0 && mags_equal != 0 {
466			1u64 << 63
467		} else {
468			0
469		};
470
471		let res_sig = if cancel != 0 { 0 } else { res_sig_pre };
472		let res_exp = if cancel != 0 { 0 } else { res_exp_pre };
473		let res_sign = if cancel != 0 { 0 } else { res_sign_pre };
474
475		(res_sig, res_exp, res_sign)
476	}
477
478	fn ref_fp64_finish_specials(
479		pa_is_nan: u64,
480		pa_is_inf: u64,
481		pa_sign_msb: u64,
482		pb_is_nan: u64,
483		pb_is_inf: u64,
484		pb_sign_msb: u64,
485		finite_or_inf: u64,
486	) -> u64 {
487		let qnan = 0x7FF8_0000_0000_0000u64;
488
489		let any_nan = (pa_is_nan | pb_is_nan) != 0;
490		let same_sign = pa_sign_msb == pb_sign_msb;
491		let opp_inf_nan = (pa_is_inf != 0) && (pb_is_inf != 0) && !same_sign;
492		let nan_case = any_nan || opp_inf_nan;
493
494		if nan_case {
495			return qnan;
496		}
497
498		let any_inf = (pa_is_inf | pb_is_inf) != 0;
499		if any_inf {
500			let inf_msb = if pb_is_inf != 0 {
501				pb_sign_msb
502			} else {
503				pa_sign_msb
504			} & (1u64 << 63);
505			let inf_payload = 0x7FFu64 << 52;
506			return inf_msb | inf_payload;
507		}
508
509		finite_or_inf
510	}
511
512	fn ref_float64_add(a_bits: u64, b_bits: u64) -> u64 {
513		// Unpack & classify operands
514		let pa = ref_fp64_unpack(a_bits);
515		let pb = ref_fp64_unpack(b_bits);
516
517		// Extended significands and effective exponents
518		let (sig_a0, exp_a0) = ref_fp64_ext_sig_and_exp(pa.sign, pa.exp, pa.frac, pa.is_norm);
519		let (sig_b0, exp_b0) = ref_fp64_ext_sig_and_exp(pb.sign, pb.exp, pb.frac, pb.is_norm);
520
521		// Order by exponent
522		let (sig_a, exp_a, sign_a, sig_b, exp_b, sign_b, _swapped) =
523			ref_fp64_order_by_exp(sig_a0, exp_a0, pa.sign, sig_b0, exp_b0, pb.sign);
524
525		// Align B to A with sticky folded into bit0
526		let d = exp_a.wrapping_sub(exp_b);
527		let s_b_align = ref_fp64_align_with_sticky(sig_b, d);
528
529		// Choose path by sign (both MSB-bools already)
530		let diff_sign = sign_a ^ sign_b;
531		let (sum_norm, exp_add) = ref_fp64_add_path(sig_a, s_b_align, exp_a);
532		let (diff_norm, exp_sub, sign_sub, mags_equal) =
533			ref_fp64_sub_path(sig_a, s_b_align, exp_a, sign_a, sign_b);
534
535		// Merge + exact cancellation to +0
536		let (res_sig, res_exp, res_sign) = ref_fp64_merge_and_cancel(
537			diff_sign, sum_norm, exp_add, sign_a, diff_norm, exp_sub, sign_sub, mags_equal,
538		);
539
540		// Pre-round underflow handling
541		let (sig_round_base, exp_round_base, exp_lt_1) = ref_fp64_underflow_shift(res_sig, res_exp);
542
543		// Round to nearest ties-to-even
544		let (mant_final_53, exp_after_round, mant_overflow_mask) =
545			ref_fp64_round_rne(sig_round_base, exp_round_base);
546
547		// Pack finite or overflow to Inf
548		// Subnormal regime if:
549		//   - we were below 1 (exp_lt_1), OR
550		//   - base exponent == 1 and the integer bit (bit 63) is 0 (no hidden 1)
551		let msb = (sig_round_base >> 63) & 1;
552		let base_is_one = exp_round_base == 1;
553		let in_sub_regime = (exp_lt_1 != 0) || (base_is_one && msb == 0);
554		let stayed_sub_mask = if in_sub_regime && mant_overflow_mask == 0 {
555			1u64 << 63
556		} else {
557			0
558		};
559		let finite_or_inf =
560			ref_fp64_pack_finite_or_inf(res_sign, mant_final_53, exp_after_round, stayed_sub_mask);
561
562		// Final specials overlay
563		ref_fp64_finish_specials(
564			pa.is_nan,
565			pa.is_inf,
566			pa.sign,
567			pb.is_nan,
568			pb.is_inf,
569			pb.sign,
570			finite_or_inf,
571		)
572	}
573
574	#[test]
575	fn test_fp64_align_with_sticky() {
576		let test_cases = [
577			// (sig_b, d, expected_result)
578			(0x8000000000000000u64, 1), // Simple shift right by 1
579			(0x8000000000000001u64, 1), // Shift right by 1 with sticky
580			(0xFFFFFFFFFFFFFFFFu64, 4), // Shift right by 4 with sticky
581			(0x1000000000000000u64, 0), // No shift
582			(0x123456789ABCDEFFu64, 8), // Shift by 8, lost bits set sticky
583		];
584
585		for (sig_b, d) in test_cases {
586			let builder = CircuitBuilder::new();
587			let sig_b_wire = builder.add_inout();
588			let d_wire = builder.add_inout();
589			let result = fp64_align_with_sticky(&builder, sig_b_wire, d_wire);
590			let expected_wire = builder.add_inout();
591			builder.assert_eq("align_result", result, expected_wire);
592
593			let circuit = builder.build();
594			let mut w = circuit.new_witness_filler();
595			w[sig_b_wire] = Word(sig_b);
596			w[d_wire] = Word(d);
597			w[expected_wire] = Word(ref_fp64_align_with_sticky(sig_b, d));
598
599			circuit.populate_wire_witness(&mut w).unwrap();
600			let cs = circuit.constraint_system();
601			cs.verify(&w.into_value_vec()).unwrap();
602		}
603	}
604
605	#[test]
606	fn test_iadd_simple() {
607		let builder = CircuitBuilder::new();
608		let a = builder.add_inout();
609		let b = builder.add_inout();
610		let (result, _) = builder.iadd(a, b);
611		let expected = builder.add_inout();
612		builder.assert_eq("iadd_result", result, expected);
613
614		let circuit = builder.build();
615		let mut w = circuit.new_witness_filler();
616		w[a] = Word(1000);
617		w[b] = Word(1);
618		w[expected] = Word(1001);
619
620		circuit.populate_wire_witness(&mut w).unwrap();
621		let cs = circuit.constraint_system();
622		cs.verify(&w.into_value_vec()).unwrap();
623	}
624
625	#[test]
626	fn test_fp64_add_path() {
627		let test_cases = [
628			// (sig_a, s_b, exp_a)
629			(0x8000000000000000u64, 0x4000000000000000u64, 1023), // Normal addition
630			(0xFFFFFFFFFFFFFFFFu64, 0x0000000000000001u64, 1000), // Addition with carry
631			(0x1000000000000000u64, 0x1000000000000000u64, 500),  // Equal values
632			// 1.5 + 1.5 geometry in extended sig space: each is 0xC000.., expect renorm carry
633			(0xC000000000000000u64, 0xC000000000000000u64, 1023),
634		];
635
636		for (sig_a, s_b, exp_a) in test_cases.iter() {
637			let builder = CircuitBuilder::new();
638			let sig_a_wire = builder.add_inout();
639			let s_b_wire = builder.add_inout();
640			let exp_a_wire = builder.add_inout();
641			let (sum_norm, exp_add) = fp64_add_path(&builder, sig_a_wire, s_b_wire, exp_a_wire);
642			let expected_sum = builder.add_inout();
643			let expected_exp = builder.add_inout();
644			builder.assert_eq("sum_norm", sum_norm, expected_sum);
645			builder.assert_eq("exp_add", exp_add, expected_exp);
646
647			let circuit = builder.build();
648			let mut w = circuit.new_witness_filler();
649			w[sig_a_wire] = Word(*sig_a);
650			w[s_b_wire] = Word(*s_b);
651			w[exp_a_wire] = Word(*exp_a);
652
653			let (ref_sum, ref_exp) = ref_fp64_add_path(*sig_a, *s_b, *exp_a);
654			w[expected_sum] = Word(ref_sum);
655			w[expected_exp] = Word(ref_exp);
656
657			circuit.populate_wire_witness(&mut w).unwrap();
658			let cs = circuit.constraint_system();
659			cs.verify(&w.into_value_vec()).unwrap();
660		}
661	}
662
663	#[test]
664	fn test_fp64_sub_path() {
665		let test_cases = [
666			// (sig_a, s_b, exp_a, sign_a, sign_b)
667			(0x8000000000000000u64, 0x4000000000000000u64, 1023, 0, 1), // Normal subtraction
668			(0x4000000000000000u64, 0x8000000000000000u64, 1020, 0, 1), // B > A
669			(0x8000000000000000u64, 0x8000000000000000u64, 1023, 0, 1), // Equal magnitudes
670			(0x1000000000000000u64, 0x0800000000000000u64, 1000, 1, 0), // Small difference
671		];
672
673		for (sig_a, s_b, exp_a, sign_a, sign_b) in test_cases {
674			let builder = CircuitBuilder::new();
675			let sig_a_wire = builder.add_inout();
676			let s_b_wire = builder.add_inout();
677			let exp_a_wire = builder.add_inout();
678			let sign_a_wire = builder.add_inout();
679			let sign_b_wire = builder.add_inout();
680
681			let (diff_norm, exp_sub, sign_sub, mags_equal) =
682				fp64_sub_path(&builder, sig_a_wire, s_b_wire, exp_a_wire, sign_a_wire, sign_b_wire);
683
684			let expected_diff = builder.add_inout();
685			let expected_exp = builder.add_inout();
686			let expected_sign = builder.add_inout();
687			let expected_mags = builder.add_inout();
688			let bool_mask = builder.add_constant(Word::MSB_ONE);
689
690			builder.assert_eq("diff_norm", diff_norm, expected_diff);
691			builder.assert_eq("exp_sub", exp_sub, expected_exp);
692			builder.assert_eq("sign_sub", sign_sub, expected_sign);
693			builder.assert_eq("mags_equal", builder.band(mags_equal, bool_mask), expected_mags);
694
695			let circuit = builder.build();
696			let mut w = circuit.new_witness_filler();
697			w[sig_a_wire] = Word(sig_a);
698			w[s_b_wire] = Word(s_b);
699			w[exp_a_wire] = Word(exp_a);
700			w[sign_a_wire] = Word(sign_a);
701			w[sign_b_wire] = Word(sign_b);
702
703			let (ref_diff, ref_exp, ref_sign, ref_mags) =
704				ref_fp64_sub_path(sig_a, s_b, exp_a, sign_a, sign_b);
705			w[expected_diff] = Word(ref_diff);
706			w[expected_exp] = Word(ref_exp);
707			w[expected_sign] = Word(ref_sign);
708			w[expected_mags] = Word(ref_mags);
709
710			circuit.populate_wire_witness(&mut w).unwrap();
711			let cs = circuit.constraint_system();
712			cs.verify(&w.into_value_vec()).unwrap();
713		}
714	}
715
716	#[test]
717	fn test_fp64_merge_and_cancel() {
718		let test_cases = [
719			// (same_sign, sum_norm, exp_add, sign_a, diff_norm, exp_sub, sign_sub, mags_equal)
720			(1u64 << 63, 0x8000000000000000u64, 1024, 0, 0x4000000000000000u64, 1020, 1, 0), /* Same sign - use add path */
721			(0, 0x8000000000000000u64, 1024, 0, 0x4000000000000000u64, 1020, 1, 0),          /* Different
722			                                                                                  * sign - use
723			                                                                                  * sub path */
724			(0, 0x8000000000000000u64, 1024, 0, 0x4000000000000000u64, 1020, 1, 1u64 << 63), /* Exact cancellation */
725		];
726
727		for (same_sign, sum_norm, exp_add, sign_a, diff_norm, exp_sub, sign_sub, mags_equal) in
728			test_cases
729		{
730			let builder = CircuitBuilder::new();
731			let same_sign_wire = builder.add_inout();
732			let sum_norm_wire = builder.add_inout();
733			let exp_add_wire = builder.add_inout();
734			let sign_a_wire = builder.add_inout();
735			let diff_norm_wire = builder.add_inout();
736			let exp_sub_wire = builder.add_inout();
737			let sign_sub_wire = builder.add_inout();
738			let mags_equal_wire = builder.add_inout();
739
740			let (res_sig, res_exp, res_sign) = fp64_merge_and_cancel(
741				&builder,
742				same_sign_wire,
743				sum_norm_wire,
744				exp_add_wire,
745				sign_a_wire,
746				diff_norm_wire,
747				exp_sub_wire,
748				sign_sub_wire,
749				mags_equal_wire,
750			);
751
752			let expected_sig = builder.add_inout();
753			let expected_exp = builder.add_inout();
754			let expected_sign = builder.add_inout();
755
756			builder.assert_eq("res_sig", res_sig, expected_sig);
757			builder.assert_eq("res_exp", res_exp, expected_exp);
758			builder.assert_eq("res_sign", res_sign, expected_sign);
759
760			let circuit = builder.build();
761			let mut w = circuit.new_witness_filler();
762			w[same_sign_wire] = Word(same_sign);
763			w[sum_norm_wire] = Word(sum_norm);
764			w[exp_add_wire] = Word(exp_add);
765			w[sign_a_wire] = Word(sign_a);
766			w[diff_norm_wire] = Word(diff_norm);
767			w[exp_sub_wire] = Word(exp_sub);
768			w[sign_sub_wire] = Word(sign_sub);
769			w[mags_equal_wire] = Word(mags_equal);
770
771			let (ref_sig, ref_exp, ref_sign) = ref_fp64_merge_and_cancel(
772				same_sign, sum_norm, exp_add, sign_a, diff_norm, exp_sub, sign_sub, mags_equal,
773			);
774			w[expected_sig] = Word(ref_sig);
775			w[expected_exp] = Word(ref_exp);
776			w[expected_sign] = Word(ref_sign);
777
778			circuit.populate_wire_witness(&mut w).unwrap();
779			let cs = circuit.constraint_system();
780			cs.verify(&w.into_value_vec()).unwrap();
781		}
782	}
783
784	#[test]
785	fn test_fp64_pack_finite_or_inf() {
786		let test_cases = [
787			// (sign, mant_final_53, exp_after_round, stayed_sub_mask)
788			(0, 0x1000000000000000u64, 1023, 0), // Normal positive number
789			(1, 0x1800000000000000u64, 1000, 0), // Normal negative number
790			(0, 0x0000000000001000u64, 1, 1u64 << 63), // Subnormal positive
791			(0, 0x1000000000000000u64, 0x7FF, 0), // Overflow to infinity
792		];
793
794		for (sign, mant_final_53, exp_after_round, stayed_sub_mask) in test_cases {
795			let builder = CircuitBuilder::new();
796			let sign_wire = builder.add_inout();
797			let mant_wire = builder.add_inout();
798			let exp_wire = builder.add_inout();
799			let stayed_sub_wire = builder.add_inout();
800
801			// Convert 0/1 sign input into MSB-bool for the packer
802			let sign_msb = builder.shl(sign_wire, 63);
803			let result =
804				fp64_pack_finite_or_inf(&builder, sign_msb, mant_wire, exp_wire, stayed_sub_wire);
805
806			let expected_result = builder.add_inout();
807			builder.assert_eq("pack_result", result, expected_result);
808
809			let circuit = builder.build();
810			let mut w = circuit.new_witness_filler();
811			w[sign_wire] = Word(sign);
812			w[mant_wire] = Word(mant_final_53);
813			w[exp_wire] = Word(exp_after_round);
814			w[stayed_sub_wire] = Word(stayed_sub_mask);
815
816			let ref_result = ref_fp64_pack_finite_or_inf(
817				sign << 63,
818				mant_final_53,
819				exp_after_round,
820				stayed_sub_mask,
821			);
822			w[expected_result] = Word(ref_result);
823
824			circuit.populate_wire_witness(&mut w).unwrap();
825			let cs = circuit.constraint_system();
826			cs.verify(&w.into_value_vec()).unwrap();
827		}
828	}
829
830	#[test]
831	fn test_fp64_finish_specials() {
832		let test_cases = [
833			// (pa_is_nan, pa_is_inf, pa_sign, pb_is_nan, pb_is_inf, pb_sign, finite_or_inf)
834			(0, 0, 0, 0, 0, 0, 0x3FF0000000000000u64), // Normal case
835			(1u64 << 63, 0, 0, 0, 0, 0, 0x4000000000000000u64), // A is NaN
836			(0, 0, 0, 1u64 << 63, 0, 1, 0x4000000000000000u64), // B is NaN
837			(0, 1u64 << 63, 0, 0, 0, 1, 0x4000000000000000u64), // A is +inf
838			(0, 0, 1, 0, 1u64 << 63, 1, 0x4000000000000000u64), // B is -inf
839			(0, 1u64 << 63, 0, 0, 1u64 << 63, 1, 0x4000000000000000u64), // +inf + (-inf) = NaN
840		];
841
842		for (pa_is_nan, pa_is_inf, pa_sign, pb_is_nan, pb_is_inf, pb_sign, finite_or_inf) in
843			test_cases
844		{
845			let builder = CircuitBuilder::new();
846			let pa = Fp64Parts {
847				sign: builder.add_inout(),
848				exp: builder.add_inout(),
849				frac: builder.add_inout(),
850				is_nan: builder.add_inout(),
851				is_inf: builder.add_inout(),
852				is_zero: builder.add_inout(),
853				is_sub: builder.add_inout(),
854				is_norm: builder.add_inout(),
855			};
856			let pb = Fp64Parts {
857				sign: builder.add_inout(),
858				exp: builder.add_inout(),
859				frac: builder.add_inout(),
860				is_nan: builder.add_inout(),
861				is_inf: builder.add_inout(),
862				is_zero: builder.add_inout(),
863				is_sub: builder.add_inout(),
864				is_norm: builder.add_inout(),
865			};
866			let finite_or_inf_wire = builder.add_inout();
867
868			let result = fp64_finish_specials(&builder, &pa, &pb, finite_or_inf_wire);
869			let expected_result = builder.add_inout();
870			builder.assert_eq("finish_result", result, expected_result);
871
872			let circuit = builder.build();
873			let mut w = circuit.new_witness_filler();
874
875			// Fill in all the pa/pb fields (most are unused for this test)
876			// Provide MSB-bool sign values
877			w[pa.sign] = Word(pa_sign << 63);
878			w[pa.exp] = Word(0);
879			w[pa.frac] = Word(0);
880			w[pa.is_nan] = Word(pa_is_nan);
881			w[pa.is_inf] = Word(pa_is_inf);
882			w[pa.is_zero] = Word(0);
883			w[pa.is_sub] = Word(0);
884			w[pa.is_norm] = Word(0);
885
886			w[pb.sign] = Word(pb_sign << 63);
887			w[pb.exp] = Word(0);
888			w[pb.frac] = Word(0);
889			w[pb.is_nan] = Word(pb_is_nan);
890			w[pb.is_inf] = Word(pb_is_inf);
891			w[pb.is_zero] = Word(0);
892			w[pb.is_sub] = Word(0);
893			w[pb.is_norm] = Word(0);
894
895			w[finite_or_inf_wire] = Word(finite_or_inf);
896
897			let ref_result = ref_fp64_finish_specials(
898				pa_is_nan,
899				pa_is_inf,
900				pa_sign << 63,
901				pb_is_nan,
902				pb_is_inf,
903				pb_sign << 63,
904				finite_or_inf,
905			);
906			w[expected_result] = Word(ref_result);
907
908			circuit.populate_wire_witness(&mut w).unwrap();
909			let cs = circuit.constraint_system();
910			cs.verify(&w.into_value_vec()).unwrap();
911		}
912	}
913
914	#[test]
915	fn test_fp64_ext_sig_and_exp() {
916		let test_values = vec![1.0f64, -1.0f64, 2.0f64, f64::MIN_POSITIVE, 0.0f64];
917
918		for val in test_values {
919			let builder = CircuitBuilder::new();
920			let input = builder.add_inout();
921			let parts = fp64_unpack(&builder, input);
922			let (sig, exp_eff) = fp64_ext_sig_and_exp(&builder, &parts);
923			let expected_sig = builder.add_inout();
924			let expected_exp = builder.add_inout();
925
926			builder.assert_eq("sig", sig, expected_sig);
927			builder.assert_eq("exp_eff", exp_eff, expected_exp);
928
929			let circuit = builder.build();
930			let mut w = circuit.new_witness_filler();
931			w[input] = Word(val.to_bits());
932
933			// Get reference values
934			let unpack_result = ref_fp64_unpack(val.to_bits());
935			let (exp_sig, exp_exp) = ref_fp64_ext_sig_and_exp(
936				unpack_result.sign,
937				unpack_result.exp,
938				unpack_result.frac,
939				unpack_result.is_norm,
940			);
941
942			w[expected_sig] = Word(exp_sig);
943			w[expected_exp] = Word(exp_exp);
944
945			circuit.populate_wire_witness(&mut w).unwrap();
946			let cs = circuit.constraint_system();
947			cs.verify(&w.into_value_vec()).unwrap();
948		}
949	}
950
951	#[test]
952	fn test_fp64_order_by_exp() {
953		let test_cases = [
954			// (sig_a, exp_a, sign_a, sig_b, exp_b, sign_b)
955			(100, 5, 1, 200, 10, 0),  // a < b, should swap
956			(100, 10, 1, 200, 5, 0),  // a >= b, no swap
957			(300, 15, 0, 400, 15, 1), // equal exp, no swap
958		];
959
960		for (sig_a_val, exp_a_val, sign_a_val, sig_b_val, exp_b_val, sign_b_val) in test_cases {
961			let builder = CircuitBuilder::new();
962			let sig_a = builder.add_inout();
963			let exp_a = builder.add_inout();
964			let sign_a = builder.add_inout();
965			let sig_b = builder.add_inout();
966			let exp_b = builder.add_inout();
967			let sign_b = builder.add_inout();
968
969			let (sig_a_out, exp_a_out, sign_a_out, sig_b_out, exp_b_out, sign_b_out, swapped) =
970				fp64_order_by_exp(&builder, sig_a, exp_a, sign_a, sig_b, exp_b, sign_b);
971
972			let expected_sig_a = builder.add_inout();
973			let expected_exp_a = builder.add_inout();
974			let expected_sign_a = builder.add_inout();
975			let expected_sig_b = builder.add_inout();
976			let expected_exp_b = builder.add_inout();
977			let expected_sign_b = builder.add_inout();
978			let expected_swapped = builder.add_inout();
979			let bool_mask = builder.add_constant(Word::MSB_ONE);
980
981			builder.assert_eq("sig_a", sig_a_out, expected_sig_a);
982			builder.assert_eq("exp_a", exp_a_out, expected_exp_a);
983			builder.assert_eq("sign_a", sign_a_out, expected_sign_a);
984			builder.assert_eq("sig_b", sig_b_out, expected_sig_b);
985			builder.assert_eq("exp_b", exp_b_out, expected_exp_b);
986			builder.assert_eq("sign_b", sign_b_out, expected_sign_b);
987			builder.assert_eq("swapped", builder.band(swapped, bool_mask), expected_swapped);
988
989			let circuit = builder.build();
990			let mut w = circuit.new_witness_filler();
991
992			w[sig_a] = Word(sig_a_val);
993			w[exp_a] = Word(exp_a_val);
994			w[sign_a] = Word(sign_a_val);
995			w[sig_b] = Word(sig_b_val);
996			w[exp_b] = Word(exp_b_val);
997			w[sign_b] = Word(sign_b_val);
998
999			// Get expected values from reference implementation
1000			let (exp_sig_a, exp_exp_a, exp_sign_a, exp_sig_b, exp_exp_b, exp_sign_b, exp_swapped) =
1001				ref_fp64_order_by_exp(
1002					sig_a_val, exp_a_val, sign_a_val, sig_b_val, exp_b_val, sign_b_val,
1003				);
1004
1005			w[expected_sig_a] = Word(exp_sig_a);
1006			w[expected_exp_a] = Word(exp_exp_a);
1007			w[expected_sign_a] = Word(exp_sign_a);
1008			w[expected_sig_b] = Word(exp_sig_b);
1009			w[expected_exp_b] = Word(exp_exp_b);
1010			w[expected_sign_b] = Word(exp_sign_b);
1011			w[expected_swapped] = Word(exp_swapped);
1012
1013			circuit.populate_wire_witness(&mut w).unwrap();
1014			let cs = circuit.constraint_system();
1015			cs.verify(&w.into_value_vec()).unwrap();
1016		}
1017	}
1018
1019	#[test]
1020	fn test_float64_add_basic() {
1021		let tiny_sub = f64::from_bits(1); // smallest subnormal 2^-1074
1022		let half_ulp_at_1 = 2f64.powi(-53);
1023		let one_ulp_at_1 = 2f64.powi(-52);
1024		let big = 1e300f64;
1025		let small = 1e-300f64;
1026		let min_norm = f64::MIN_POSITIVE; // 2^-1022
1027		let half_min_norm = min_norm / 2.0; // subnormal
1028
1029		let test_cases: &[(f64, f64)] = &[
1030			(0.0, 0.0),
1031			(0.0, -0.0),
1032			(-0.0, -0.0),
1033			(1.0, -1.0),
1034			(-1.0, 1.0),
1035			(1.0, 2.0),
1036			(1.5, 1.5),
1037			(2.0, -0.5),
1038			// Rounding behavior near 1.0
1039			(1.0, half_ulp_at_1), // tie, LSB even -> stays 1.0
1040			(1.0, one_ulp_at_1),  // round up -> nextafter(1, +inf)
1041			// Subnormals and underflow path
1042			(tiny_sub, tiny_sub),           // sticky folding
1043			(half_min_norm, half_min_norm), // rises to min normal
1044			(min_norm, tiny_sub),           // tiny add that should not lose sticky
1045			// Large exponent differences (alignment saturates + sticky accumulates)
1046			(1.0, 2f64.powi(-1000)),
1047			(2f64.powi(100), 2f64.powi(-1000)),
1048			// Big/small normals
1049			(big, big),   // overflow to +inf
1050			(-big, -big), // overflow to -inf
1051			(small, small),
1052			// Infinities
1053			(f64::INFINITY, 1.0),
1054			(-f64::INFINITY, -1.0),
1055			(f64::INFINITY, -f64::INFINITY), // -> qNaN
1056			// NaNs (canonical qNaN regardless of payload)
1057			(f64::NAN, 1.0),
1058			(1.0, f64::NAN),
1059			(f64::NAN, f64::NAN),
1060		];
1061
1062		for (i, (a_val, b_val)) in test_cases.iter().copied().enumerate() {
1063			let ref_result = ref_float64_add(a_val.to_bits(), b_val.to_bits());
1064			let native = (a_val + b_val).to_bits();
1065			assert!(
1066				f64_bits_semantic_eq(ref_result, native),
1067				"Test case {}: pseudo {} != native {}",
1068				i,
1069				ref_result,
1070				native
1071			);
1072
1073			let builder = CircuitBuilder::new();
1074			let a = builder.add_inout();
1075			let b = builder.add_inout();
1076			let result = float64_add(&builder, a, b);
1077			let expected = builder.add_inout();
1078			builder.assert_eq(format!("float64_add_case_{}", i), result, expected);
1079
1080			let circuit = builder.build();
1081			let mut w = circuit.new_witness_filler();
1082			w[a] = Word(a_val.to_bits());
1083			w[b] = Word(b_val.to_bits());
1084
1085			let expected_val = ref_result;
1086			w[expected] = Word(expected_val);
1087
1088			circuit.populate_wire_witness(&mut w).unwrap();
1089			let cs = circuit.constraint_system();
1090			cs.verify(&w.into_value_vec()).unwrap();
1091		}
1092	}
1093}