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 +
79 /// 2^max_bits` is non-negative. 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),
139 // deg(lower))
140 builder.when(condition).assert_eq(intermed_val, check_val);
141 builder.assert_bool(out);
142 }
143
144 #[inline(always)]
145 pub(crate) fn eval_range_checks<AB: InteractionBuilder>(
146 &self,
147 builder: &mut AB,
148 lower_decomp: &[AB::Var],
149 count: impl Into<AB::Expr>,
150 ) {
151 let count = count.into();
152 let mut bits_remaining = self.max_bits;
153 // we range check the limbs of the lower_decomp so that we know each element
154 // of lower_decomp has the correct number of bits
155 for limb in lower_decomp {
156 // the last limb might have fewer than `bus.range_max_bits` bits
157 let range_bits = bits_remaining.min(self.bus.range_max_bits);
158 self.bus
159 .range_check(*limb, range_bits)
160 .eval(builder, count.clone());
161 bits_remaining = bits_remaining.saturating_sub(self.bus.range_max_bits);
162 }
163 }
164}
165
166impl<AB: InteractionBuilder> SubAir<AB> for IsLtSubAir {
167 type AirContext<'a>
168 = (IsLessThanIo<AB::Expr>, &'a [AB::Var])
169 where
170 AB::Expr: 'a,
171 AB::Var: 'a,
172 AB: 'a;
173
174 // constrain that out == (x < y) when count != 0
175 // warning: send for range check must be included for the constraints to be sound
176 fn eval<'a>(
177 &'a self,
178 builder: &'a mut AB,
179 (io, lower_decomp): (IsLessThanIo<AB::Expr>, &'a [AB::Var]),
180 ) where
181 AB::Var: 'a,
182 AB::Expr: 'a,
183 {
184 // Note: every AIR that uses this sub-AIR must include the range checks for soundness
185 self.eval_range_checks(builder, lower_decomp, io.count.clone());
186 self.eval_without_range_checks(builder, io.y - io.x, io.out, io.count, lower_decomp);
187 }
188}
189
190/// The same subair as [IsLtSubAir] except that non-range check
191/// constraints are not imposed on the last row.
192/// Intended use case is for asserting less than between entries in
193/// adjacent rows.
194#[derive(Clone, Copy, Debug)]
195pub struct IsLtWhenTransitionAir(pub IsLtSubAir);
196
197impl<AB: InteractionBuilder> SubAir<AB> for IsLtWhenTransitionAir {
198 type AirContext<'a>
199 = (IsLessThanIo<AB::Expr>, &'a [AB::Var])
200 where
201 AB::Expr: 'a,
202 AB::Var: 'a,
203 AB: 'a;
204
205 /// Imposes the non-interaction constraints on all except the last row. This is
206 /// intended for use when the comparators `x, y` are on adjacent rows.
207 ///
208 /// This function does also enable the interaction constraints _on every row_.
209 /// The `eval_interactions` performs range checks on `lower_decomp` on every row, even
210 /// though in this AIR the lower_decomp is not used on the last row.
211 /// This simply means the trace generation must fill in the last row with numbers in
212 /// range (e.g., with zeros)
213 fn eval<'a>(
214 &'a self,
215 builder: &'a mut AB,
216 (io, lower_decomp): (IsLessThanIo<AB::Expr>, &'a [AB::Var]),
217 ) where
218 AB::Var: 'a,
219 AB::Expr: 'a,
220 {
221 self.0
222 .eval_range_checks(builder, lower_decomp, io.count.clone());
223 self.0.eval_without_range_checks(
224 &mut builder.when_transition(),
225 io.y - io.x,
226 io.out,
227 io.count,
228 lower_decomp,
229 );
230 }
231}
232
233impl<F: Field> TraceSubRowGenerator<F> for IsLtSubAir {
234 /// `(range_checker, x, y)`
235 type TraceContext<'a> = (&'a VariableRangeCheckerChip, u32, u32);
236 /// `(lower_decomp, out)`
237 type ColsMut<'a> = (&'a mut [F], &'a mut F);
238
239 /// Only use this when `count != 0`.
240 #[inline(always)]
241 fn generate_subrow<'a>(
242 &'a self,
243 (range_checker, x, y): (&'a VariableRangeCheckerChip, u32, u32),
244 (lower_decomp, out): (&'a mut [F], &'a mut F),
245 ) {
246 debug_assert_eq!(lower_decomp.len(), self.decomp_limbs);
247 debug_assert!(
248 x < (1 << self.max_bits),
249 "{x} has more than {} bits",
250 self.max_bits
251 );
252 debug_assert!(
253 y < (1 << self.max_bits),
254 "{y} has more than {} bits",
255 self.max_bits
256 );
257 *out = F::from_bool(x < y);
258
259 // obtain the lower_bits
260 let check_less_than = (1 << self.max_bits) + y - x - 1;
261 let lower_u32 = check_less_than & ((1 << self.max_bits) - 1);
262
263 // decompose lower_u32 into limbs and range check
264 range_checker.decompose(lower_u32, self.max_bits, lower_decomp);
265 }
266}