Skip to main content

binius_ip_prover/fracaddcheck/
fraction.rs

1// Copyright 2026 The Binius Developers
2
3//! The numerator/denominator pair a fractional-addition instance is built from.
4
5use binius_field::Field;
6
7/// A numerator paired with its denominator.
8///
9/// Fractional addition never touches one half alone:
10///
11/// ```text
12///     a_0/b_0 + a_1/b_1 = (a_0*b_1 + a_1*b_0) / (b_0*b_1)
13/// ```
14///
15/// So the two travel together, through every layer and every claim.
16/// Pairing them in one type makes a half-formed pair unrepresentable.
17/// A bare tuple leaves each caller to remember which element is which.
18///
19/// `T` is whatever stands for one half.
20/// A scalar gives a claimed fraction; a column buffer gives one layer of the circuit.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct Fraction<T> {
23	/// The numerator.
24	pub num: T,
25	/// The denominator.
26	pub den: T,
27}
28
29impl<T> Fraction<T> {
30	/// Pairs a numerator with a denominator.
31	pub const fn new(num: T, den: T) -> Self {
32		Self { num, den }
33	}
34
35	/// Borrows both halves, so a fraction can be read without moving out of it.
36	pub const fn as_ref(&self) -> Fraction<&T> {
37		Fraction {
38			num: &self.num,
39			den: &self.den,
40		}
41	}
42
43	/// Applies `f` to each half.
44	///
45	/// This is how a fraction changes representation, such as a pair of layer buffers reduced to
46	/// the pair of scalars at their root.
47	pub fn map<U>(self, mut f: impl FnMut(T) -> U) -> Fraction<U> {
48		Fraction {
49			num: f(self.num),
50			den: f(self.den),
51		}
52	}
53}
54
55impl<F: Field> Fraction<F> {
56	/// The zero fraction $0/1$.
57	///
58	/// This is the additive identity of fractional addition: adding it changes nothing.
59	/// So it is what a padding leaf holds when a batch lifts a shallow tree to its depth, and what
60	/// fills the selector slots past the last real instance.
61	pub const ZERO: Self = Self {
62		num: F::ZERO,
63		den: F::ONE,
64	};
65}
66
67impl<T> From<(T, T)> for Fraction<T> {
68	fn from((num, den): (T, T)) -> Self {
69		Self { num, den }
70	}
71}
72
73impl<T> From<Fraction<T>> for (T, T) {
74	fn from(Fraction { num, den }: Fraction<T>) -> Self {
75		(num, den)
76	}
77}
78
79#[cfg(test)]
80mod tests {
81	use binius_field::FieldOps;
82	use binius_math::test_utils::{Packed128b, random_scalars};
83	use rand::prelude::*;
84
85	use super::*;
86
87	type F = <Packed128b as FieldOps>::Scalar;
88
89	#[test]
90	fn zero_fraction_is_the_additive_identity() {
91		let mut rng = StdRng::seed_from_u64(0);
92		let [num, den]: [F; 2] = random_scalars::<F>(&mut rng, 2)
93			.try_into()
94			.expect("two scalars");
95		let f = Fraction::new(num, den);
96
97		// Adding 0/1 to a/b by the fractional-addition rule must give back a/b unchanged.
98		let pad = Fraction::<F>::ZERO;
99		let sum = Fraction::new(f.num * pad.den + pad.num * f.den, f.den * pad.den);
100		assert_eq!(sum, f);
101	}
102
103	#[test]
104	fn tuple_conversion_round_trips() {
105		let mut rng = StdRng::seed_from_u64(1);
106		let [num, den]: [F; 2] = random_scalars::<F>(&mut rng, 2)
107			.try_into()
108			.expect("two scalars");
109
110		let tuple: (F, F) = Fraction::new(num, den).into();
111		assert_eq!(tuple, (num, den));
112		assert_eq!(Fraction::from(tuple), Fraction::new(num, den));
113	}
114
115	#[test]
116	fn map_and_as_ref_reach_both_halves() {
117		let f = Fraction::new(vec![1u8, 2], vec![3u8]);
118		assert_eq!(f.as_ref().map(Vec::len), Fraction::new(2, 1));
119	}
120}