binius_circuits/bignum/addsub.rs
1// Copyright 2025 Irreducible Inc.
2use binius_core::word::Word;
3use binius_frontend::{CircuitBuilder, Wire};
4
5use super::biguint::BigUint;
6
7/// Add two equally-sized `BigUints`s with carry propagation.
8///
9/// See `add_with_carry_out`; this routine additionally asserts that no overflow happens.
10pub fn add(builder: &CircuitBuilder, a: &BigUint, b: &BigUint) -> BigUint {
11 let (sum, carry_out) = add_with_carry_out(builder, a, b);
12 // Assert that no overflow happened.
13 builder.assert_false("add_carry_out_zero", carry_out);
14 sum
15}
16
17/// Add two equally-sized `BigUints`s with carry propagation.
18///
19/// Computes `a + b` with proper carry handling between limbs. The result
20/// has the same number of limbs as the inputs. Overflow beyond the most
21/// significant limb is checked and must be zero.
22///
23/// # Arguments
24/// * `builder` - Circuit builder for constraint generation
25/// * `a` - First operand
26/// * `b` - Second operand (must have same number of limbs as `a`)
27///
28/// # Returns
29/// A tuple of `BigUint` sum of size equal to the inputs and a carry out boolean wire.
30///
31/// # Panics
32/// - Panics if `a` and `b` have different number of limbs
33pub fn add_with_carry_out(builder: &CircuitBuilder, a: &BigUint, b: &BigUint) -> (BigUint, Wire) {
34 assert_eq!(
35 a.limbs.len(),
36 b.limbs.len(),
37 "add_with_carry_out: inputs must have the same number of limbs"
38 );
39
40 let mut accumulator = vec![vec![]; a.limbs.len()];
41 for i in 0..a.limbs.len() {
42 accumulator[i].push(a.limbs[i]);
43 accumulator[i].push(b.limbs[i]);
44 }
45
46 let (sum, carry_out) = compute_stack_adds_with_carry_outs(builder, &accumulator);
47 assert_eq!(carry_out.len(), if a.limbs.is_empty() { 0 } else { 1 });
48 (
49 sum,
50 carry_out
51 .first()
52 .copied()
53 .unwrap_or_else(|| builder.add_constant(Word::ZERO)),
54 )
55}
56
57/// Subtracts two equally-sized `BigUints`s with carry propagation.
58///
59/// Computes `a - b` with proper borrow handling between limbs. The result
60/// has the same number of limbs as the inputs. Underflow beyond the most
61/// significant limb is checked and must be zero. This implies that `a >= b`
62/// and the difference remains unsigned.
63///
64/// # Arguments
65/// * `builder` - Circuit builder for constraint generation
66/// * `a` - minuend
67/// * `b` - subtrahend (must have same number of limbs as `a`)
68///
69/// # Returns
70/// Difference as a `BigUint` with the same number of limbs as the inputs
71///
72/// # Panics
73/// - Panics if `a` and `b` have different number of limbs
74pub fn sub(builder: &CircuitBuilder, a: &BigUint, b: &BigUint) -> BigUint {
75 let zero = builder.add_constant(Word::ZERO);
76 let (diff, borrow_out) = sub_with_borrow_out(builder, a, b);
77
78 // Assert the final borrow is zero (i.e no underflow).
79 //
80 // It requires checking the MSB of the `borrow_out`
81 let borrow_out_msb = builder.shr(borrow_out, 63);
82 builder.assert_eq("sub_borrow_out", borrow_out_msb, zero);
83
84 diff
85}
86
87/// Subtracts two equally-sized `BigUint`s modulo `2^(64 * limbs)`, returning the final borrow.
88///
89/// Unlike [`sub`], underflow is not rejected: the difference wraps and the returned wire
90/// carries the borrow out of the most significant limb in its MSB.
91pub(super) fn sub_with_borrow_out(
92 builder: &CircuitBuilder,
93 a: &BigUint,
94 b: &BigUint,
95) -> (BigUint, Wire) {
96 assert_eq!(a.limbs.len(), b.limbs.len(), "sub: inputs must have the same number of limbs");
97
98 let zero = builder.add_constant(Word::ZERO);
99
100 let mut diff_limbs = Vec::with_capacity(a.limbs.len());
101
102 let mut borrow_in = zero;
103 for (&a_limb, &b_limb) in a.limbs.iter().zip(&b.limbs) {
104 let (diff_limb, borrow_out) = builder.isub_bin_bout(a_limb, b_limb, borrow_in);
105 diff_limbs.push(diff_limb);
106
107 borrow_in = borrow_out;
108 }
109
110 (BigUint { limbs: diff_limbs }, borrow_in)
111}
112
113/// Computes multi-operand addition with carry propagation across limb positions.
114///
115/// See `compute_stack_adds_with_carry_outs`; this routine additionally asserts that no
116/// carry out happens.
117pub(super) fn compute_stack_adds(builder: &CircuitBuilder, limb_stacks: &[Vec<Wire>]) -> BigUint {
118 let (sum, carry_outs) = compute_stack_adds_with_carry_outs(builder, limb_stacks);
119
120 // Assert all final carries are zero (i.e no overflow).
121 //
122 // It is sufficient to check the MSB of each wire in `carries` because:
123 //
124 // - The `carries` vector stores carry_out from each iadd_cin_cout gate.
125 // - The carry bit for each addition is stored in the MSB of the carry_out wire.
126 for (i, carry_out) in carry_outs.into_iter().enumerate() {
127 builder.assert_false(format!("compute_stack_adds_carry_zero_{i}"), carry_out);
128 }
129
130 sum
131}
132
133/// Computes multi-operand addition with carry propagation across limb positions.
134///
135/// This function is the core of bignum arithmetic, handling the addition of multiple
136/// values at each limb position with proper carry propagation to higher limbs.
137/// It's used by other bignum operations to resolve partial products and sums.
138///
139/// # Arguments
140/// * `builder` - Circuit builder for constraint generation
141/// * `limb_stacks` - Array where `limb_stacks[i]` contains all values to be added at limb position
142/// `i`.
143///
144/// # Returns
145/// The sum produced by the addition chain, as well as a number of boolean wires representing carry
146/// outs in the most significant limb; converting these booleans to 0/1 integers and summing
147/// produces the value of overflow limb.
148pub(super) fn compute_stack_adds_with_carry_outs(
149 builder: &CircuitBuilder,
150 limb_stacks: &[Vec<Wire>],
151) -> (BigUint, Vec<Wire>) {
152 let mut sums = Vec::new();
153 let mut carries = Vec::new();
154 let zero = builder.add_constant(Word::ZERO);
155
156 for limb_stack in limb_stacks {
157 let mut limb_stack = limb_stack.clone();
158 let mut new_carries = Vec::new();
159
160 // Pad stack to handle incoming carries
161 if limb_stack.len() < carries.len() + 1 {
162 limb_stack.resize(carries.len() + 1, zero);
163 }
164
165 while limb_stack.len() >= 2 {
166 let carry_in = carries.pop().unwrap_or(zero);
167 let x = limb_stack.pop().expect("limb_stack.len() >= 2");
168 let y = limb_stack.pop().expect("limb_stack.len() >= 2");
169
170 let (sum, cout) = builder.iadd_cin_cout(x, y, carry_in);
171 limb_stack.push(sum);
172 new_carries.push(cout);
173 }
174
175 sums.push(limb_stack[0]);
176 assert!(limb_stack.len() == 1);
177 assert!(carries.is_empty());
178 carries = new_carries;
179 }
180
181 (BigUint { limbs: sums }, carries)
182}