openvm_circuit_primitives/is_less_than/mod.rs
1use openvm_stark_backend::{
2 interaction::InteractionBuilder,
3 p3_air::AirBuilder,
4 p3_field::{Field, FieldAlgebra},
5};
6
7use crate::{
8 var_range::{VariableRangeCheckerBus, VariableRangeCheckerChip},
9 SubAir, TraceSubRowGenerator,
10};
11
12#[cfg(test)]
13pub mod tests;
14
15// Aux cols are the same for assert_less_than and is_less_than
16pub use super::assert_less_than::LessThanAuxCols;
17
18/// The IO is typically provided with `T = AB::Expr` as external context.
19// This does not derive AlignedBorrow because it is usually **not** going to be
20// direct columns in an AIR.
21#[repr(C)]
22#[derive(Clone, Copy, Debug, Default)]
23pub struct IsLessThanIo<T> {
24 pub x: T,
25 pub y: T,
26 /// The boolean output, constrained to equal (x < y) when `count != 0`.
27 pub out: T,
28 /// Range checks are done with multiplicity `count`.
29 /// If `count == 0` then no range checks are done.
30 /// `count` is **assumed** to be boolean and must be constrained as such by
31 /// the caller.
32 ///
33 /// N.B.: in fact range checks could always be done, if the aux
34 /// subrow values are set to 0 when `count == 0`. This would slightly
35 /// simplify the range check interactions, although usually doesn't change
36 /// the overall constraint degree. It however leads to the annoyance that
37 /// you must update the RangeChecker's multiplicities even on dummy padding
38 /// rows. To improve quality of life,
39 /// we currently use this more complex constraint.
40 pub count: T,
41}
42impl<T> IsLessThanIo<T> {
43 pub fn new(x: impl Into<T>, y: impl Into<T>, out: impl Into<T>, count: impl Into<T>) -> Self {
44 Self {
45 x: x.into(),
46 y: y.into(),
47 out: out.into(),
48 count: count.into(),
49 }
50 }
51}
52
53/// This is intended for use as a **SubAir**, not as a standalone Air.
54///
55/// This SubAir constrains the boolean equal to 1 iff `x < y`, assuming
56/// the two numbers both have a max number of bits, given by `max_bits`.
57/// The SubAir compares the numbers by decomposing `y - x - 1 + 2^max_bits`
58/// into limbs of size `bus.range_max_bits`, and interacts with a
59/// `VariableRangeCheckerBus` to range check the decompositions.
60///
61/// The SubAir will own auxiliary columns to store the decomposed limbs.
62/// The number of limbs is `max_bits.div_ceil(bus.range_max_bits)`.
63///
64/// The expected max constraint degree of `eval` is
65/// deg(count) + max(1, deg(x), deg(y))
66///
67/// N.B.: AssertLtSubAir could be implemented by directly passing through
68/// to IsLtSubAir with `out = AB::Expr::ONE`. The only additional
69/// constraint in this air is `assert_bool(io.out)`. However since both Airs
70/// are fundamental and the constraints are simple, we opt to keep the two
71/// versions separate.
72#[derive(Copy, Clone, Debug)]
73pub struct IsLtSubAir {
74 /// The bus for sends to range chip
75 pub bus: VariableRangeCheckerBus,
76 /// The maximum number of bits for the numbers to compare
77 /// Soundness requirement: max_bits <= 29
78 /// max_bits > 29 doesn't work: the approach is to decompose and range check `y - x - 1 + 2^max_bits` is non-negative.
79 /// This requires 2^{max_bits+1} < |F|.
80 /// When F::bits() = 31, this implies max_bits <= 29.
81 pub max_bits: usize,
82 /// `decomp_limbs = max_bits.div_ceil(bus.range_max_bits)`.
83 pub decomp_limbs: usize,
84}
85
86impl IsLtSubAir {
87 pub fn new(bus: VariableRangeCheckerBus, max_bits: usize) -> Self {
88 assert!(max_bits <= 29); // see soundness requirement above
89 let decomp_limbs = max_bits.div_ceil(bus.range_max_bits);
90 Self {
91 bus,
92 max_bits,
93 decomp_limbs,
94 }
95 }
96
97 pub fn range_max_bits(&self) -> usize {
98 self.bus.range_max_bits
99 }
100
101 pub fn when_transition(self) -> IsLtWhenTransitionAir {
102 IsLtWhenTransitionAir(self)
103 }
104
105 /// FOR INTERNAL USE ONLY.
106 /// This AIR is only sound if interactions are enabled
107 ///
108 /// Constraints between `io` and `aux` are only enforced when `condition != 0`.
109 /// This means `aux` can be all zero independent on what `io` is by setting `condition = 0`.
110 #[inline(always)]
111 pub(crate) fn eval_without_range_checks<AB: AirBuilder>(
112 &self,
113 builder: &mut AB,
114 y_minus_x: impl Into<AB::Expr>,
115 out: impl Into<AB::Expr>,
116 condition: impl Into<AB::Expr>,
117 lower_decomp: &[AB::Var],
118 ) {
119 assert_eq!(lower_decomp.len(), self.decomp_limbs);
120 // this is the desired intermediate value (i.e. y - x - 1)
121 // deg(intermed_val) = deg(io)
122 let intermed_val =
123 y_minus_x.into() + AB::Expr::from_canonical_usize((1 << self.max_bits) - 1);
124
125 // Construct lower from lower_decomp:
126 // - each limb of lower_decomp will be range checked
127 // deg(lower) = 1
128 let lower = lower_decomp
129 .iter()
130 .enumerate()
131 .fold(AB::Expr::ZERO, |acc, (i, &val)| {
132 acc + val * AB::Expr::from_canonical_usize(1 << (i * self.range_max_bits()))
133 });
134
135 let out = out.into();
136 // constrain that the lower + out * 2^max_bits is the correct intermediate sum
137 let check_val = lower + out.clone() * AB::Expr::from_canonical_usize(1 << self.max_bits);
138 // the degree of this constraint is expected to be deg(count) + max(deg(intermed_val), deg(lower))
139 builder.when(condition).assert_eq(intermed_val, check_val);
140 builder.assert_bool(out);
141 }
142
143 #[inline(always)]
144 pub(crate) fn eval_range_checks<AB: InteractionBuilder>(
145 &self,
146 builder: &mut AB,
147 lower_decomp: &[AB::Var],
148 count: impl Into<AB::Expr>,
149 ) {
150 let count = count.into();
151 let mut bits_remaining = self.max_bits;
152 // we range check the limbs of the lower_decomp so that we know each element
153 // of lower_decomp has the correct number of bits
154 for limb in lower_decomp {
155 // the last limb might have fewer than `bus.range_max_bits` bits
156 let range_bits = bits_remaining.min(self.bus.range_max_bits);
157 self.bus
158 .range_check(*limb, range_bits)
159 .eval(builder, count.clone());
160 bits_remaining = bits_remaining.saturating_sub(self.bus.range_max_bits);
161 }
162 }
163}
164
165impl<AB: InteractionBuilder> SubAir<AB> for IsLtSubAir {
166 type AirContext<'a>
167 = (IsLessThanIo<AB::Expr>, &'a [AB::Var])
168 where
169 AB::Expr: 'a,
170 AB::Var: 'a,
171 AB: 'a;
172
173 // constrain that out == (x < y) when count != 0
174 // warning: send for range check must be included for the constraints to be sound
175 fn eval<'a>(
176 &'a self,
177 builder: &'a mut AB,
178 (io, lower_decomp): (IsLessThanIo<AB::Expr>, &'a [AB::Var]),
179 ) where
180 AB::Var: 'a,
181 AB::Expr: 'a,
182 {
183 // Note: every AIR that uses this sub-AIR must include the range checks for soundness
184 self.eval_range_checks(builder, lower_decomp, io.count.clone());
185 self.eval_without_range_checks(builder, io.y - io.x, io.out, io.count, lower_decomp);
186 }
187}
188
189/// The same subair as [IsLtSubAir] except that non-range check
190/// constraints are not imposed on the last row.
191/// Intended use case is for asserting less than between entries in
192/// adjacent rows.
193#[derive(Clone, Copy, Debug)]
194pub struct IsLtWhenTransitionAir(pub IsLtSubAir);
195
196impl<AB: InteractionBuilder> SubAir<AB> for IsLtWhenTransitionAir {
197 type AirContext<'a>
198 = (IsLessThanIo<AB::Expr>, &'a [AB::Var])
199 where
200 AB::Expr: 'a,
201 AB::Var: 'a,
202 AB: 'a;
203
204 /// Imposes the non-interaction constraints on all except the last row. This is
205 /// intended for use when the comparators `x, y` are on adjacent rows.
206 ///
207 /// This function does also enable the interaction constraints _on every row_.
208 /// The `eval_interactions` performs range checks on `lower_decomp` on every row, even
209 /// though in this AIR the lower_decomp is not used on the last row.
210 /// This simply means the trace generation must fill in the last row with numbers in
211 /// range (e.g., with zeros)
212 fn eval<'a>(
213 &'a self,
214 builder: &'a mut AB,
215 (io, lower_decomp): (IsLessThanIo<AB::Expr>, &'a [AB::Var]),
216 ) where
217 AB::Var: 'a,
218 AB::Expr: 'a,
219 {
220 self.0
221 .eval_range_checks(builder, lower_decomp, io.count.clone());
222 self.0.eval_without_range_checks(
223 &mut builder.when_transition(),
224 io.y - io.x,
225 io.out,
226 io.count,
227 lower_decomp,
228 );
229 }
230}
231
232impl<F: Field> TraceSubRowGenerator<F> for IsLtSubAir {
233 /// `(range_checker, x, y)`
234 type TraceContext<'a> = (&'a VariableRangeCheckerChip, u32, u32);
235 /// `(lower_decomp, out)`
236 type ColsMut<'a> = (&'a mut [F], &'a mut F);
237
238 /// Only use this when `count != 0`.
239 #[inline(always)]
240 fn generate_subrow<'a>(
241 &'a self,
242 (range_checker, x, y): (&'a VariableRangeCheckerChip, u32, u32),
243 (lower_decomp, out): (&'a mut [F], &'a mut F),
244 ) {
245 debug_assert_eq!(lower_decomp.len(), self.decomp_limbs);
246 debug_assert!(
247 x < (1 << self.max_bits),
248 "{x} has more than {} bits",
249 self.max_bits
250 );
251 debug_assert!(
252 y < (1 << self.max_bits),
253 "{y} has more than {} bits",
254 self.max_bits
255 );
256 *out = F::from_bool(x < y);
257
258 // obtain the lower_bits
259 let check_less_than = (1 << self.max_bits) + y - x - 1;
260 let lower_u32 = check_less_than & ((1 << self.max_bits) - 1);
261
262 // decompose lower_u32 into limbs and range check
263 range_checker.decompose(lower_u32, self.max_bits, lower_decomp);
264 }
265}