Skip to main content

binius_circuits/float64/
utils.rs

1// Copyright 2025 Irreducible Inc.
2use binius_core::Word;
3use binius_frontend::{CircuitBuilder, Wire};
4
5/// Simple view of a decoded binary64 payload.
6#[derive(Clone, Copy)]
7pub struct Fp64Parts {
8	pub sign: Wire, // Sign as MSB-bool (bit63 set for negative, 0 otherwise)
9	pub exp: Wire,  // unbiased field bits (0..=0x7FF)
10	pub frac: Wire, // 52-bit payload
11
12	// Common classifiers as MSB-bools (true = MSB=1, false = 0):
13	pub is_nan: Wire,  // exp==0x7FF && frac!=0
14	pub is_inf: Wire,  // exp==0x7FF && frac==0
15	pub is_zero: Wire, // exp==0 && frac==0
16	pub is_sub: Wire,  // subnormal: exp==0 && frac!=0
17	pub is_norm: Wire, // normal: exp!=0 && exp!=0x7FF
18}
19
20/// Creates a wire containing the constant value 0.
21pub fn zero(b: &CircuitBuilder) -> Wire {
22	b.add_constant_64(0)
23}
24
25/// Creates a wire containing the constant value 1.
26pub fn one(b: &CircuitBuilder) -> Wire {
27	b.add_constant_64(1)
28}
29
30/// Extracts bit `i` from `x` as an MSB-bool (true = bit63 set, false = 0).
31///
32/// Returns `((x >> i) & 1) << 63`.
33#[inline]
34pub fn bit_msb01(b: &CircuitBuilder, x: Wire, i: u32) -> Wire {
35	b.shl(bit_lsb(b, x, i), 63)
36}
37
38/// Moves MSB to LSB position zeroing all other bits.
39#[inline]
40pub fn msb_to_lsb01(b: &CircuitBuilder, b_msb: Wire) -> Wire {
41	b.shr(b_msb, 63)
42}
43
44/// Performs integer subtraction: `a - b`.
45///
46/// This is a wrapper around the circuit builder's integer subtraction that handles
47/// borrow-in/borrow-out automatically with zero borrow-in.
48pub fn isub(builder: &CircuitBuilder, a: Wire, b: Wire) -> Wire {
49	let (d, _b) = builder.isub_bin_bout(a, b, zero(builder));
50	d
51}
52
53/// Extracts bit `i` from `x` as a 0/1 value.
54///
55/// Returns `(x >> i) & 1`.
56pub fn bit_lsb(b: &CircuitBuilder, x: Wire, i: u32) -> Wire {
57	b.band(b.shr(x, i), one(b))
58}
59
60/// Performs variable right shift with sticky bit tracking.
61///
62/// This implements a barrel shifter that can shift by any amount 0-63,
63/// with optional saturation. All bits shifted out are OR'd together to
64/// create a "sticky" bit that tracks whether any precision was lost.
65///
66/// # Parameters
67/// - `x`: Value to shift
68/// - `d`: Shift amount (0-63, or optionally saturated at 63)
69/// - `saturate_at_63`: If true, shifts >= 64 are treated as 63; if false, they wrap
70///
71/// # Returns
72/// - First wire: The shifted value
73/// - Second wire: Sticky bit (all-1 mask if any bits were lost, all-0 otherwise)
74pub fn var_shr_with_sticky(
75	b: &CircuitBuilder,
76	x: Wire,
77	d: Wire,
78	saturate_at_63: bool,
79) -> (Wire, Wire) {
80	let c63 = b.add_constant_64(63);
81	let c64 = b.add_constant_64(64);
82	let d_eff = if saturate_at_63 {
83		let lt64 = b.icmp_ult(d, c64);
84		b.select(lt64, d, c63)
85	} else {
86		b.band(d, c63)
87	};
88
89	let mut v = x;
90	let mut sticky01 = zero(b); // 0/1 sticky
91
92	// Stage 32 (bit 5)
93	{
94		let cond01 = bit_lsb(b, d_eff, 5);
95		let cond = bit_msb01(b, d_eff, 5);
96		let lost = b.band(v, b.add_constant_64((1u64 << 32) - 1));
97		let lost_ne0 = b.icmp_ne(lost, zero(b));
98		let lost_nz01 = msb_to_lsb01(b, lost_ne0);
99		sticky01 = b.bor(sticky01, b.band(lost_nz01, cond01));
100		let shifted = b.shr(v, 32);
101		v = b.select(cond, shifted, v);
102	}
103	// Stage 16 (bit 4)
104	{
105		let cond01 = bit_lsb(b, d_eff, 4);
106		let cond = bit_msb01(b, d_eff, 4);
107		let lost = b.band(v, b.add_constant_64((1u64 << 16) - 1));
108		let lost_ne0 = b.icmp_ne(lost, zero(b));
109		let lost_nz01 = msb_to_lsb01(b, lost_ne0);
110		sticky01 = b.bor(sticky01, b.band(lost_nz01, cond01));
111		let shifted = b.shr(v, 16);
112		v = b.select(cond, shifted, v);
113	}
114	// Stage 8 (bit 3)
115	{
116		let cond01 = bit_lsb(b, d_eff, 3);
117		let cond = bit_msb01(b, d_eff, 3);
118		let lost = b.band(v, b.add_constant_64((1u64 << 8) - 1));
119		let lost_ne0 = b.icmp_ne(lost, zero(b));
120		let lost_nz01 = msb_to_lsb01(b, lost_ne0);
121		sticky01 = b.bor(sticky01, b.band(lost_nz01, cond01));
122		let shifted = b.shr(v, 8);
123		v = b.select(cond, shifted, v);
124	}
125	// Stage 4 (bit 2)
126	{
127		let cond01 = bit_lsb(b, d_eff, 2);
128		let cond = bit_msb01(b, d_eff, 2);
129		let lost = b.band(v, b.add_constant_64((1u64 << 4) - 1));
130		let lost_ne0 = b.icmp_ne(lost, zero(b));
131		let lost_nz01 = msb_to_lsb01(b, lost_ne0);
132		sticky01 = b.bor(sticky01, b.band(lost_nz01, cond01));
133		let shifted = b.shr(v, 4);
134		v = b.select(cond, shifted, v);
135	}
136	// Stage 2 (bit 1)
137	{
138		let cond01 = bit_lsb(b, d_eff, 1);
139		let cond = bit_msb01(b, d_eff, 1);
140		let lost = b.band(v, b.add_constant_64((1u64 << 2) - 1));
141		let lost_ne0 = b.icmp_ne(lost, zero(b));
142		let lost_nz01 = msb_to_lsb01(b, lost_ne0);
143		sticky01 = b.bor(sticky01, b.band(lost_nz01, cond01));
144		let shifted = b.shr(v, 2);
145		v = b.select(cond, shifted, v);
146	}
147	// Stage 1 (bit 0)
148	{
149		let cond01 = bit_lsb(b, d_eff, 0);
150		let cond = bit_msb01(b, d_eff, 0);
151		let lost = b.band(v, b.add_constant_64(1));
152		let lost_ne0 = b.icmp_ne(lost, zero(b));
153		let lost_nz01 = msb_to_lsb01(b, lost_ne0);
154		sticky01 = b.bor(sticky01, b.band(lost_nz01, cond01));
155		let shifted = b.shr(v, 1);
156		v = b.select(cond, shifted, v);
157	}
158
159	(v, sticky01)
160}
161
162/// Performs variable left shift.
163///
164/// This implements a barrel shifter that can shift left by any amount 0-63.
165/// Shift amounts >= 64 are masked to 0-63 range (i.e., `d & 63`).
166///
167/// # Parameters
168/// - `x`: Value to shift
169/// - `d`: Shift amount (effectively `d & 63`)
170///
171/// # Returns
172/// The left-shifted value `x << (d & 63)`
173pub fn var_shl(b: &CircuitBuilder, x: Wire, d: Wire) -> Wire {
174	let d_eff = b.band(d, b.add_constant_64(63));
175	let mut v = x;
176
177	{
178		let cond = bit_msb01(b, d_eff, 5); // 32
179		let shifted = b.shl(v, 32);
180		v = b.select(cond, shifted, v);
181	}
182	{
183		let cond = bit_msb01(b, d_eff, 4); // 16
184		let shifted = b.shl(v, 16);
185		v = b.select(cond, shifted, v);
186	}
187	{
188		let cond = bit_msb01(b, d_eff, 3); // 8
189		let shifted = b.shl(v, 8);
190		v = b.select(cond, shifted, v);
191	}
192	{
193		let cond = bit_msb01(b, d_eff, 2); // 4
194		let shifted = b.shl(v, 4);
195		v = b.select(cond, shifted, v);
196	}
197	{
198		let cond = bit_msb01(b, d_eff, 1); // 2
199		let shifted = b.shl(v, 2);
200		v = b.select(cond, shifted, v);
201	}
202	{
203		let cond = bit_msb01(b, d_eff, 0); // 1
204		let shifted = b.shl(v, 1);
205		v = b.select(cond, shifted, v);
206	}
207
208	v
209}
210
211/// Count leading zeroes in `x`
212pub fn clz64(b: &CircuitBuilder, x: Wire) -> Wire {
213	let mut n = zero(b);
214	let mut y = x;
215
216	// step(32)
217	{
218		let t = b.shr(y, 32);
219		let z = b.icmp_eq(t, zero(b));
220		let add32 = b.add_constant_64(32);
221		n = b.iadd(n, b.select(z, add32, zero(b))).0;
222		y = b.select(z, b.shl(y, 32), y);
223	}
224	// step(16)
225	{
226		let t = b.shr(y, 48);
227		let z = b.icmp_eq(t, zero(b));
228		let add16 = b.add_constant_64(16);
229		n = b.iadd(n, b.select(z, add16, zero(b))).0;
230		y = b.select(z, b.shl(y, 16), y);
231	}
232	// step(8)
233	{
234		let t = b.shr(y, 56);
235		let z = b.icmp_eq(t, zero(b));
236		let add8 = b.add_constant_64(8);
237		n = b.iadd(n, b.select(z, add8, zero(b))).0;
238		y = b.select(z, b.shl(y, 8), y);
239	}
240	// step(4)
241	{
242		let t = b.shr(y, 60);
243		let z = b.icmp_eq(t, zero(b));
244		let add4 = b.add_constant_64(4);
245		n = b.iadd(n, b.select(z, add4, zero(b))).0;
246		y = b.select(z, b.shl(y, 4), y);
247	}
248	// step(2)
249	{
250		let t = b.shr(y, 62);
251		let z = b.icmp_eq(t, zero(b));
252		let add2 = b.add_constant_64(2);
253		n = b.iadd(n, b.select(z, add2, zero(b))).0;
254		y = b.select(z, b.shl(y, 2), y);
255	}
256	// step(1)
257	{
258		let t = b.shr(y, 63);
259		let z = b.icmp_eq(t, zero(b));
260		n = b.iadd(n, b.select(z, one(b), zero(b))).0;
261	}
262	n
263}
264
265/// Build the 53-bit integer significand and effective exponent.
266///
267/// For multiplication, we use 53-bit integers (including hidden bit for normals)
268/// rather than the 64-bit extended format used for addition.
269///
270/// - Normals: `sig53 = (1<<52) | frac`, `exp_eff = exp`
271/// - Subnormals: `sig53 = frac`, `exp_eff = 1`
272///
273/// # Parameters
274/// - `p`: Parts from `fp64_unpack`
275///
276/// # Returns
277/// - `(sig53, exp_eff)`: 53-bit significand and effective exponent
278pub fn fp64_sig53_and_exp(b: &CircuitBuilder, p: &Fp64Parts) -> (Wire, Wire) {
279	let one52 = b.add_constant_64(1u64 << 52);
280	let sig_norm = b.bor(one52, p.frac);
281	let sig = b.select(p.is_norm, sig_norm, p.frac);
282	let exp_eff = b.select(p.is_norm, p.exp, one(b)); // subnormals use exp=1
283	(sig, exp_eff)
284}
285
286/// Right-shift a 128-bit value by a small constant and return the low 64 bits.
287///
288/// Given `p = (hi << 64) | lo`, computes `(p >> s).lo64 = (lo >> s) | (hi << (64 - s))`
289///
290/// # Parameters
291/// - `hi`: High 64 bits of 128-bit value
292/// - `lo`: Low 64 bits of 128-bit value
293/// - `s`: Shift amount (must be 0 < s < 64)
294///
295/// # Returns
296/// Low 64 bits of the right-shifted result
297#[inline]
298pub fn shr128_to_u64_const(b: &CircuitBuilder, hi: Wire, lo: Wire, s: u32) -> Wire {
299	debug_assert!(s > 0 && s < 64);
300	let lo_part = b.shr(lo, s);
301	let hi_part = b.shl(hi, 64 - s);
302	b.bor(lo_part, hi_part)
303}
304
305/// Extract sticky bit from the k least-significant bits of a value.
306///
307/// Returns 0/1 (in LSB) indicating whether any of the k least-significant
308/// bits of `lo` are set to 1.
309///
310/// # Parameters
311/// - `lo`: Input value
312/// - `k`: Number of LSBs to check (must be k < 64)
313///
314/// # Returns
315/// 0/1 value: 1 if any of k LSBs are set, 0 otherwise
316#[inline]
317pub fn sticky_from_low_k(b: &CircuitBuilder, lo: Wire, k: u32) -> Wire {
318	debug_assert!(k < 64);
319	let mask = b.add_constant_64((1u64 << k) - 1);
320	let ne0 = b.icmp_ne(b.band(lo, mask), zero(b));
321	msb_to_lsb01(b, ne0)
322}
323
324/// Unpack and classify a binary64 word.
325///
326/// Input:
327/// - `x`: 64-bit IEEE-754 encoding
328///
329/// Output:
330/// - `Fp64Parts` with fields:
331///   - `sign`: MSB-bool of sign bit (i.e., `x & (1<<63)`, either 0 or 0x8000..)
332///   - `exp = (x >> 52) & 0x7FF`
333///   - `frac = x & ((1<<52)-1)`
334///   - `is_nan`: exp==0x7FF && frac!=0
335///   - `is_inf`: exp==0x7FF && frac==0
336///   - `is_zero`: exp==0 && frac==0
337///   - `is_sub`: exp==0 && frac!=0
338///   - `is_norm`: exp!=0 && exp!=0x7FF
339///
340/// All booleans are in MSB-bool format.
341pub fn fp64_unpack(b: &CircuitBuilder, x: Wire) -> Fp64Parts {
342	let exp_m = b.add_constant_64(0x7FF);
343	let frac_m = b.add_constant_64((1u64 << 52) - 1);
344
345	let sign = x;
346	let exp = b.band(b.shr(x, 52), exp_m);
347	let frac = b.band(x, frac_m);
348
349	let exp_is_max = b.icmp_eq(exp, exp_m); // MSB-bool
350	let exp_is_zero = b.icmp_eq(exp, zero(b)); // MSB-bool
351	let frac_is_zero = b.icmp_eq(frac, zero(b)); // MSB-bool
352
353	let is_nan = b.band(exp_is_max, b.bnot(frac_is_zero));
354	let is_inf = b.band(exp_is_max, frac_is_zero);
355	let is_zero = b.band(exp_is_zero, frac_is_zero);
356	let is_sub = b.band(exp_is_zero, b.bnot(frac_is_zero));
357	let is_norm = b.bnot(b.bor(exp_is_max, exp_is_zero));
358
359	Fp64Parts {
360		sign,
361		exp,
362		frac,
363		is_nan,
364		is_inf,
365		is_zero,
366		is_sub,
367		is_norm,
368	}
369}
370
371/// Pre-round **underflow** handling: if `exp<=0`, right shift by `k=1-exp`
372/// and fold sticky into bit0. Also prepare the exponent for rounding geometry (Exp=1).
373///
374/// Input: `(res_sig, res_exp)`
375///
376/// Output:
377/// - `(sig_round_base, exp_round_base, exp_lt_1_mask)` where `exp_round_base = (exp<=0 ? 1 : exp)`.
378pub fn fp64_underflow_shift(
379	b: &CircuitBuilder,
380	res_sig: Wire,
381	res_exp: Wire,
382) -> (Wire, Wire, Wire) {
383	let c1 = one(b);
384
385	let exp_lt_1 = b.icmp_ult(res_exp, c1);
386	let k = isub(b, c1, res_exp); // 1 - exp
387	let k_use = b.select(exp_lt_1, k, zero(b));
388
389	let (mut sig_u, st) = var_shr_with_sticky(b, res_sig, k_use, true);
390	// (sig_u & ~1) | (sig_u & 1) | (st & 1) == sig_u | (st & 1): the sticky fold
391	// only ever sets bit 0, so clearing it first is dead work.
392	sig_u = b.bor(sig_u, b.band(st, c1));
393
394	let sig_round_base = b.select(exp_lt_1, sig_u, res_sig);
395	let exp_round_base = b.select(exp_lt_1, c1, res_exp);
396	(sig_round_base, exp_round_base, exp_lt_1)
397}
398
399/// Round-to-nearest, ties-to-even (RN-even).
400///
401/// Geometry:
402/// - Integer bit at 63; we interpret bits:
403///   - LSB of target mantissa at bit 11
404///   - Guard=10, Round=9, Sticky=bit 0 (already folded)
405///
406/// Input: `(sig_base, exp_base)`
407///
408/// Output:
409/// - `(mant_final_53, exp_after_round, mant_overflow_mask)`
410///   - `mant_final_53` is a 53-bit value (includes hidden 1 for normals)
411///   - If mant overflowed to 54 bits, we shift right 1 and increment exponent.
412pub fn fp64_round_rne(b: &CircuitBuilder, sig_base: Wire, exp_base: Wire) -> (Wire, Wire, Wire) {
413	let lsb = bit_lsb(b, sig_base, 11);
414	let g: Wire = bit_lsb(b, sig_base, 10);
415	let r = bit_lsb(b, sig_base, 9);
416	let s = b.band(sig_base, one(b));
417	let r_or_s = b.bor(r, s);
418	let tie_or_gt = b.bor(r_or_s, lsb);
419	let round_up01 = b.band(g, tie_or_gt); // 0/1
420
421	let mant_trunc = b.shr(sig_base, 11);
422	let mant_rounded = b.iadd(mant_trunc, round_up01).0;
423
424	let overflow01 = bit_lsb(b, mant_rounded, 53); // 0/1
425	let overflow_msb = bit_msb01(b, mant_rounded, 53);
426	let mant_final_53 = b.select(overflow_msb, b.shr(mant_rounded, 1), mant_rounded);
427	let exp_after = b.iadd(exp_base, overflow01).0;
428
429	(mant_final_53, exp_after, overflow_msb)
430}
431
432/// Pack finite (normal / subnormal) and apply overflow-to-Inf if needed.
433///
434/// Input:
435/// - `sign` (0/1), `mant_final_53`, `exp_after_round`
436/// - `stayed_sub_mask`: all-1 iff we are in subnormal regime **and** there was no mantissa overflow
437///
438/// Output:
439/// - `finite_or_inf`: packed 64-bit result (finite or +/−Inf on overflow)
440pub fn fp64_pack_finite_or_inf(
441	b: &CircuitBuilder,
442	sign: Wire,
443	mant_final_53: Wire,
444	exp_after_round: Wire,
445	stayed_sub_mask: Wire,
446) -> Wire {
447	let frac_m = b.add_constant_64((1u64 << 52) - 1);
448	let exp_2047 = b.add_constant_64(0x7FF);
449
450	let frac = b.band(mant_final_53, frac_m);
451	let sign = b.band(sign, b.add_constant(Word::MSB_ONE));
452	let packed_sub = b.bor(sign, frac);
453
454	let exp_sh = b.shl(exp_after_round, 52);
455	let packed_norm = b.bor(b.bor(sign, exp_sh), frac);
456
457	let finite_packed = b.select(stayed_sub_mask, packed_sub, packed_norm);
458
459	// If mantissa is zero, the result is a signed zero regardless of exp_after_round.
460	let mant_is_zero = b.icmp_eq(mant_final_53, zero(b));
461	let finite_or_zero = b.select(mant_is_zero, sign, finite_packed);
462
463	// overflow_to_inf when exp_after_round >= 2047
464	let overflow_to_inf = b.icmp_ule(exp_2047, exp_after_round);
465	let inf_payload = b.shl(exp_2047, 52);
466	let packed_inf = b.bor(sign, inf_payload);
467
468	b.select(overflow_to_inf, packed_inf, finite_or_zero)
469}
470
471#[cfg(test)]
472pub mod tests {
473	use binius_core::word::Word;
474	use proptest::bool;
475
476	use super::*;
477
478	// Reference implementations for testing (shared between add.rs and utils.rs tests)
479	pub struct Fp64UnpackResult {
480		pub sign: u64,
481		pub exp: u64,
482		pub frac: u64,
483		pub is_nan: u64,  // MSB-bool
484		pub is_inf: u64,  // MSB-bool
485		pub is_norm: u64, // MSB-bool
486	}
487
488	pub fn ref_fp64_unpack(x: u64) -> Fp64UnpackResult {
489		let sign = x & (1u64 << 63);
490		let exp = (x >> 52) & 0x7FF;
491		let frac = x & ((1u64 << 52) - 1);
492
493		let exp_is_max = if exp == 0x7FF { 1u64 << 63 } else { 0 };
494		let exp_is_zero = if exp == 0 { 1u64 << 63 } else { 0 };
495		let frac_is_zero = if frac == 0 { 1u64 << 63 } else { 0 };
496
497		// MSB-bool logic
498		let is_nan = if exp_is_max != 0 && frac_is_zero == 0 {
499			1u64 << 63
500		} else {
501			0
502		};
503		let is_inf = if exp_is_max != 0 && frac_is_zero != 0 {
504			1u64 << 63
505		} else {
506			0
507		};
508		let is_norm = if exp_is_max == 0 && exp_is_zero == 0 {
509			1u64 << 63
510		} else {
511			0
512		};
513
514		Fp64UnpackResult {
515			sign,
516			exp,
517			frac,
518			is_nan,
519			is_inf,
520			is_norm,
521		}
522	}
523
524	pub fn ref_fp64_underflow_shift(res_sig: u64, res_exp: u64) -> (u64, u64, u64) {
525		let exp_lt_1 = if res_exp < 1 { 1u64 << 63 } else { 0 };
526
527		if exp_lt_1 != 0 {
528			let k = 1u64.wrapping_sub(res_exp);
529			let k_use = std::cmp::min(k, 63);
530
531			let sig_shifted = res_sig >> k_use;
532			let lost_bits = res_sig & ((1u64 << k_use) - 1);
533			let sticky = if lost_bits != 0 { 1 } else { 0 };
534			let bit0 = (sig_shifted & 1) | sticky;
535			let sig_u = (sig_shifted & !1) | bit0;
536
537			(sig_u, 1, exp_lt_1)
538		} else {
539			(res_sig, res_exp, exp_lt_1)
540		}
541	}
542
543	pub fn ref_fp64_round_rne(sig_base: u64, exp_base: u64) -> (u64, u64, u64) {
544		let lsb = (sig_base >> 11) & 1;
545		let g = (sig_base >> 10) & 1;
546		let r = (sig_base >> 9) & 1;
547		let s = sig_base & 1;
548
549		let r_or_s = r | s;
550		let tie_or_gt = r_or_s | lsb;
551		let round_up = g & tie_or_gt;
552
553		let mant_trunc = sig_base >> 11;
554		let mant_rounded = mant_trunc.wrapping_add(round_up);
555
556		let overflow = (mant_rounded >> 53) & 1;
557		let overflow_msb = if overflow != 0 { 1u64 << 63 } else { 0 };
558		let mant_final_53 = if overflow_msb != 0 {
559			mant_rounded >> 1
560		} else {
561			mant_rounded
562		};
563		let exp_after = exp_base.wrapping_add(overflow);
564
565		(mant_final_53, exp_after, overflow_msb)
566	}
567
568	pub fn ref_fp64_pack_finite_or_inf(
569		sign_msb: u64,
570		mant_final_53: u64,
571		exp_after_round: u64,
572		stayed_sub_mask_msb: u64,
573	) -> u64 {
574		let sign_hi = sign_msb & (1u64 << 63);
575		let frac = mant_final_53 & ((1u64 << 52) - 1);
576
577		let packed_sub = sign_hi | frac;
578		let exp_sh = exp_after_round << 52;
579		let packed_norm = sign_hi | exp_sh | frac;
580
581		let finite_packed = if stayed_sub_mask_msb != 0 {
582			packed_sub
583		} else {
584			packed_norm
585		};
586
587		// If mantissa is zero, this is a signed zero regardless of exponent
588		let finite_or_zero = if mant_final_53 == 0 {
589			sign_hi
590		} else {
591			finite_packed
592		};
593
594		let overflow_to_inf = exp_after_round >= 0x7FF;
595		if overflow_to_inf {
596			let inf_payload = 0x7FFu64 << 52;
597			sign_hi | inf_payload
598		} else {
599			finite_or_zero
600		}
601	}
602
603	#[test]
604	fn test_bit_lsb() {
605		let builder = CircuitBuilder::new();
606		let input = builder.add_inout();
607		let output0 = bit_lsb(&builder, input, 0);
608		let output1 = bit_lsb(&builder, input, 1);
609		let output63 = bit_lsb(&builder, input, 63);
610		let expected0 = builder.add_inout();
611		let expected1 = builder.add_inout();
612		let expected63 = builder.add_inout();
613		builder.assert_eq("test_output0", output0, expected0);
614		builder.assert_eq("test_output1", output1, expected1);
615		builder.assert_eq("test_output63", output63, expected63);
616
617		let circuit = builder.build();
618
619		let test_cases = [
620			(0b101, 1, 0, 0),              // bit 0=1, bit 1=0, bit 63=0
621			(0b110, 0, 1, 0),              // bit 0=0, bit 1=1, bit 63=0
622			(0x8000000000000001, 1, 0, 1), // bit 0=1, bit 1=0, bit 63=1
623		];
624
625		for (val, exp0, exp1, exp63) in test_cases {
626			let mut w = circuit.new_witness_filler();
627			w[input] = Word(val);
628			w[expected0] = Word(exp0);
629			w[expected1] = Word(exp1);
630			w[expected63] = Word(exp63);
631
632			circuit.populate_wire_witness(&mut w).unwrap();
633			let cs = circuit.constraint_system();
634			cs.verify(&w.into_value_vec()).unwrap();
635		}
636	}
637
638	#[test]
639	fn test_clz64() {
640		let builder = CircuitBuilder::new();
641		let input = builder.add_inout();
642		let output = clz64(&builder, input);
643		let expected = builder.add_inout();
644		builder.assert_eq("test_output", output, expected);
645
646		let circuit = builder.build();
647
648		let test_cases = [
649			(0x8000000000000000u64, 0),  // Top bit set
650			(0x4000000000000000u64, 1),  // Second bit set
651			(0x0000000000000001u64, 63), // Only bottom bit set
652			(0xFFFFFFFFFFFFFFFFu64, 0),  // All bits set
653			(0x0000000000008000u64, 48), // Bit 15 set
654		];
655
656		for (val, expected_clz) in test_cases {
657			let mut w = circuit.new_witness_filler();
658			w[input] = Word(val);
659			w[expected] = Word(expected_clz);
660
661			circuit.populate_wire_witness(&mut w).unwrap();
662			let cs = circuit.constraint_system();
663			cs.verify(&w.into_value_vec()).unwrap();
664		}
665	}
666
667	#[test]
668	fn test_var_shl() {
669		let builder = CircuitBuilder::new();
670		let input = builder.add_inout();
671		let shift = builder.add_inout();
672		let output = var_shl(&builder, input, shift);
673		let expected = builder.add_inout();
674		builder.assert_eq("test_output", output, expected);
675
676		let circuit = builder.build();
677
678		let test_cases = [
679			(1, 0, 1),        // No shift
680			(1, 1, 2),        // Shift left by 1
681			(1, 8, 256),      // Shift left by 8
682			(0xFF, 4, 0xFF0), // Shift 0xFF left by 4
683			(1, 64, 1),       // Shift amount wraps (64 & 63 = 0)
684		];
685
686		for (val, shift_amt, expected_result) in test_cases {
687			let mut w = circuit.new_witness_filler();
688			w[input] = Word(val);
689			w[shift] = Word(shift_amt);
690			w[expected] = Word(expected_result);
691
692			circuit.populate_wire_witness(&mut w).unwrap();
693			let cs = circuit.constraint_system();
694			cs.verify(&w.into_value_vec()).unwrap();
695		}
696	}
697
698	#[test]
699	fn test_var_shr_with_sticky() {
700		let builder = CircuitBuilder::new();
701		let input = builder.add_inout();
702		let shift = builder.add_inout();
703		let (output, sticky) = var_shr_with_sticky(&builder, input, shift, false);
704		let expected_out = builder.add_inout();
705		let expected_sticky = builder.add_inout();
706		builder.assert_eq("test_output", output, expected_out);
707		builder.assert_eq("test_sticky", sticky, expected_sticky);
708
709		let circuit = builder.build();
710
711		let test_cases = [
712			(8, 1, 4, 0),      // 8 >> 1 = 4, no bits lost
713			(7, 1, 3, 1),      // 7 >> 1 = 3, bit lost (sticky = 1)
714			(0xFF, 4, 0xF, 1), // 0xFF >> 4 = 0xF, bits lost
715			(0xF0, 4, 0xF, 0), // 0xF0 >> 4 = 0xF, no bits lost
716		];
717
718		for (val, shift_amt, expected_result, expected_sticky_val) in test_cases {
719			let mut w = circuit.new_witness_filler();
720			w[input] = Word(val);
721			w[shift] = Word(shift_amt);
722			w[expected_out] = Word(expected_result);
723			w[expected_sticky] = Word(expected_sticky_val);
724
725			circuit.populate_wire_witness(&mut w).unwrap();
726			let cs = circuit.constraint_system();
727			cs.verify(&w.into_value_vec()).unwrap();
728		}
729	}
730
731	#[test]
732	fn test_fp64_sig53_and_exp() {
733		// fp64_unpack is now available through the parent module import
734
735		let test_values = vec![1.0f64, -1.0f64, 2.0f64, f64::MIN_POSITIVE, 0.5f64];
736
737		for val in test_values {
738			let builder = CircuitBuilder::new();
739			let input = builder.add_inout();
740			let parts = fp64_unpack(&builder, input);
741			let (sig, exp_eff) = fp64_sig53_and_exp(&builder, &parts);
742			let expected_sig = builder.add_inout();
743			let expected_exp = builder.add_inout();
744
745			builder.assert_eq("sig", sig, expected_sig);
746			builder.assert_eq("exp_eff", exp_eff, expected_exp);
747
748			let circuit = builder.build();
749			let mut w = circuit.new_witness_filler();
750			w[input] = Word(val.to_bits());
751
752			// Reference calculation
753			let bits = val.to_bits();
754			let exp = (bits >> 52) & 0x7FF;
755			let frac = bits & ((1u64 << 52) - 1);
756			let is_norm = exp != 0 && exp != 0x7FF;
757
758			let (exp_sig, exp_exp) = if is_norm {
759				((1u64 << 52) | frac, exp)
760			} else {
761				(frac, 1)
762			};
763
764			w[expected_sig] = Word(exp_sig);
765			w[expected_exp] = Word(exp_exp);
766
767			circuit.populate_wire_witness(&mut w).unwrap();
768			let cs = circuit.constraint_system();
769			cs.verify(&w.into_value_vec()).unwrap();
770		}
771	}
772
773	#[test]
774	fn test_shr128_to_u64_const() {
775		let test_cases = [
776			// (hi, lo, shift, expected)
777			(0x0123456789ABCDEFu64, 0xFEDCBA9876543210u64, 8),
778			(0xFFFFFFFFFFFFFFFFu64, 0x0000000000000000u64, 32),
779			(0x0000000000000000u64, 0xFFFFFFFFFFFFFFFFu64, 16),
780			(0x8000000000000000u64, 0x0000000000000001u64, 1),
781			(0x1234567890ABCDEFu64, 0x1111111111111111u64, 4),
782		];
783
784		for (hi, lo, shift) in test_cases {
785			let builder = CircuitBuilder::new();
786			let hi_wire = builder.add_inout();
787			let lo_wire = builder.add_inout();
788			let result = shr128_to_u64_const(&builder, hi_wire, lo_wire, shift);
789			let expected_wire = builder.add_inout();
790			builder.assert_eq("shr128_result", result, expected_wire);
791
792			let circuit = builder.build();
793			let mut w = circuit.new_witness_filler();
794			w[hi_wire] = Word(hi);
795			w[lo_wire] = Word(lo);
796
797			// Reference calculation: ((hi as u128) << 64 | lo as u128) >> shift
798			let p = ((hi as u128) << 64) | (lo as u128);
799			let expected = (p >> shift) as u64;
800			w[expected_wire] = Word(expected);
801
802			circuit.populate_wire_witness(&mut w).unwrap();
803			let cs = circuit.constraint_system();
804			cs.verify(&w.into_value_vec()).unwrap();
805		}
806	}
807
808	#[test]
809	fn test_sticky_from_low_k() {
810		let test_cases = [
811			// (value, k, expected)
812			(0x0000000000000000u64, 4, 0), // No bits set
813			(0x0000000000000001u64, 1, 1), // Bit 0 set
814			(0x0000000000000002u64, 1, 0), // Bit 0 not set, but bit 1 is
815			(0x0000000000000002u64, 2, 1), // Bit 1 set in 2 LSBs
816			(0x000000000000000Fu64, 4, 1), // All 4 LSBs set
817			(0x0000000000000010u64, 4, 0), // Bit 4 set, but not in 4 LSBs
818			(0x00000000000000FFu64, 8, 1), // All 8 LSBs set
819			(0x0000000000000100u64, 8, 0), // Bit 8 set, but not in 8 LSBs
820		];
821
822		for (value, k, expected_val) in test_cases {
823			let builder = CircuitBuilder::new();
824			let input = builder.add_inout();
825			let result = sticky_from_low_k(&builder, input, k);
826			let expected = builder.add_inout();
827			builder.assert_eq("sticky_result", result, expected);
828
829			let circuit = builder.build();
830			let mut w = circuit.new_witness_filler();
831			w[input] = Word(value);
832			w[expected] = Word(expected_val);
833
834			circuit.populate_wire_witness(&mut w).unwrap();
835			let cs = circuit.constraint_system();
836			cs.verify(&w.into_value_vec()).unwrap();
837		}
838	}
839
840	#[test]
841	fn test_fp64_unpack() {
842		let test_values = vec![
843			0.0f64,
844			-0.0f64,
845			1.0f64,
846			-1.0f64,
847			f64::INFINITY,
848			f64::NEG_INFINITY,
849			f64::NAN,
850			f64::MIN_POSITIVE,
851		];
852
853		for val in test_values {
854			let builder = CircuitBuilder::new();
855			let input = builder.add_inout();
856			let result = fp64_unpack(&builder, input);
857
858			// Create expected outputs
859			let expected_sign = builder.add_inout();
860			let expected_exp = builder.add_inout();
861			let expected_frac = builder.add_inout();
862			let expected_is_nan = builder.add_inout();
863			let expected_is_inf = builder.add_inout();
864			let expected_is_norm = builder.add_inout();
865
866			// Compare only the MSB of sign; sign is represented as MSB-bool
867			let bool_mask = builder.add_constant(Word::MSB_ONE);
868			builder.assert_eq("sign", builder.band(result.sign, bool_mask), expected_sign);
869			builder.assert_eq("exp", result.exp, expected_exp);
870			builder.assert_eq("frac", result.frac, expected_frac);
871			builder.assert_eq("is_nan", builder.band(result.is_nan, bool_mask), expected_is_nan);
872			builder.assert_eq("is_inf", builder.band(result.is_inf, bool_mask), expected_is_inf);
873			builder.assert_eq("is_norm", builder.band(result.is_norm, bool_mask), expected_is_norm);
874
875			let circuit = builder.build();
876			let mut w = circuit.new_witness_filler();
877			w[input] = Word(val.to_bits());
878
879			// Get expected values from reference implementation
880			let result = ref_fp64_unpack(val.to_bits());
881
882			w[expected_sign] = Word(result.sign);
883			w[expected_exp] = Word(result.exp);
884			w[expected_frac] = Word(result.frac);
885			w[expected_is_nan] = Word(result.is_nan);
886			w[expected_is_inf] = Word(result.is_inf);
887			w[expected_is_norm] = Word(result.is_norm);
888
889			circuit.populate_wire_witness(&mut w).unwrap();
890			let cs = circuit.constraint_system();
891			cs.verify(&w.into_value_vec()).unwrap();
892		}
893	}
894
895	#[test]
896	fn test_fp64_underflow_shift() {
897		let test_cases = [
898			// (res_sig, res_exp)
899			(0x8000000000000000u64, 5),            // Normal case, no underflow
900			(0x8000000000000000u64, 0),            // Underflow case, exp = 0
901			(0x4000000000000000u64, -5i64 as u64), // Significant underflow
902		];
903
904		for (res_sig, res_exp) in test_cases {
905			let builder = CircuitBuilder::new();
906			let res_sig_wire = builder.add_inout();
907			let res_exp_wire = builder.add_inout();
908
909			let (sig_round_base, exp_round_base, exp_lt_1) =
910				fp64_underflow_shift(&builder, res_sig_wire, res_exp_wire);
911
912			let expected_sig = builder.add_inout();
913			let expected_exp = builder.add_inout();
914			let expected_lt1 = builder.add_inout();
915			let bool_mask = builder.add_constant(Word::MSB_ONE);
916
917			builder.assert_eq("sig_round_base", sig_round_base, expected_sig);
918			builder.assert_eq("exp_round_base", exp_round_base, expected_exp);
919			builder.assert_eq("exp_lt_1", builder.band(exp_lt_1, bool_mask), expected_lt1);
920
921			let circuit = builder.build();
922			let mut w = circuit.new_witness_filler();
923			w[res_sig_wire] = Word(res_sig);
924			w[res_exp_wire] = Word(res_exp);
925
926			let (ref_sig, ref_exp, ref_lt1) = ref_fp64_underflow_shift(res_sig, res_exp);
927			w[expected_sig] = Word(ref_sig);
928			w[expected_exp] = Word(ref_exp);
929			w[expected_lt1] = Word(ref_lt1);
930
931			circuit.populate_wire_witness(&mut w).unwrap();
932			let cs = circuit.constraint_system();
933			cs.verify(&w.into_value_vec()).unwrap();
934		}
935	}
936
937	/// Helper: semantic equality for f64 bit patterns, treating any-NaN as equal
938	pub fn f64_bits_semantic_eq(a_bits: u64, b_bits: u64) -> bool {
939		let a = f64::from_bits(a_bits);
940		let b = f64::from_bits(b_bits);
941		if a.is_nan() && b.is_nan() {
942			true
943		} else {
944			a_bits == b_bits
945		}
946	}
947
948	#[test]
949	fn test_fp64_round_rne() {
950		let test_cases = [
951			// (sig_base, exp_base) - test round-to-nearest-even
952			(0x8000000000000000u64, 1023), // No rounding needed
953			(0x8000000000000800u64, 1023), /* Round up (G=1, R=0, S=0, LSB=0 -> ties to even =
954			                                * no round) */
955			(0x8000000000001800u64, 1023), // Round up (G=1, R=1, S=0 -> round up)
956			(0x8000000000000C00u64, 1023), // Round up (G=1, R=0, S=1 -> round up)
957		];
958
959		for (sig_base, exp_base) in test_cases {
960			let builder = CircuitBuilder::new();
961			let sig_base_wire = builder.add_inout();
962			let exp_base_wire = builder.add_inout();
963
964			let (mant_final_53, exp_after_round, mant_overflow_mask) =
965				fp64_round_rne(&builder, sig_base_wire, exp_base_wire);
966
967			let expected_mant = builder.add_inout();
968			let expected_exp = builder.add_inout();
969			let expected_overflow = builder.add_inout();
970
971			builder.assert_eq("mant_final_53", mant_final_53, expected_mant);
972			builder.assert_eq("exp_after_round", exp_after_round, expected_exp);
973			builder.assert_eq("mant_overflow_mask", mant_overflow_mask, expected_overflow);
974
975			let circuit = builder.build();
976			let mut w = circuit.new_witness_filler();
977			w[sig_base_wire] = Word(sig_base);
978			w[exp_base_wire] = Word(exp_base);
979
980			let (ref_mant, ref_exp, ref_overflow) = ref_fp64_round_rne(sig_base, exp_base);
981			w[expected_mant] = Word(ref_mant);
982			w[expected_exp] = Word(ref_exp);
983			w[expected_overflow] = Word(ref_overflow);
984
985			circuit.populate_wire_witness(&mut w).unwrap();
986			let cs = circuit.constraint_system();
987			cs.verify(&w.into_value_vec()).unwrap();
988		}
989	}
990}