Skip to main content

kimchi/circuits/lookup/
lookups.rs

1use crate::{
2    circuits::{
3        domains::EvaluationDomains,
4        gate::{CircuitGate, CurrOrNext, GateType},
5        lookup::{
6            index::LookupSelectors,
7            tables::{
8                combine_table_entry, get_table, GateLookupTable, LookupTable, RANGE_CHECK_TABLE_ID,
9                XOR_TABLE_ID,
10            },
11        },
12    },
13    collections::BTreeSet,
14};
15use alloc::{vec, vec::Vec};
16use ark_ff::{Field, One, PrimeField, Zero};
17use ark_poly::{EvaluationDomain, Evaluations as E, Radix2EvaluationDomain as D};
18use core::ops::{Mul, Neg};
19use o1_utils::field_helpers::i32_to_field;
20use serde::{Deserialize, Serialize};
21use strum_macros::EnumIter;
22
23type Evaluations<Field> = E<Field, D<Field>>;
24
25//~ Lookups patterns are extremely flexible and can be configured in a number of ways.
26//~ Every type of lookup is a JointLookup -- to create a single lookup your create a
27//~ JointLookup that contains one SingleLookup.
28//~
29//~ Generally, the patterns of lookups possible are
30//~   * Multiple lookups per row
31//~    `JointLookup { }, ...,  JointLookup { }`
32//~   * Multiple values in each lookup (via joining, think of it like a tuple)
33//~    `JoinLookup { SingleLookup { }, ..., SingleLookup { } }`
34//~   * Multiple columns combined in linear combination to create each value
35//~    `JointLookup { SingleLookup { value: vec![(scale1, col1), ..., (scale2, col2)] } }`
36//~   * Any combination of these
37
38fn max_lookups_per_row(kinds: LookupPatterns) -> usize {
39    kinds
40        .into_iter()
41        .fold(0, |acc, x| core::cmp::max(x.max_lookups_per_row(), acc))
42}
43
44/// Flags for each of the built-in lookup patterns.
45///
46/// Each pattern corresponds to a specific lookup table and constraint
47/// configuration. When enabled, the corresponding lookup table is included
48/// in the circuit and lookup constraints are generated.
49///
50/// Typically computed via [`Self::from_gates`] by scanning circuit gates.
51#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
52#[cfg_attr(
53    feature = "ocaml_types",
54    derive(ocaml::IntoValue, ocaml::FromValue, ocaml_gen::Struct)
55)]
56#[cfg_attr(feature = "wasm_types", wasm_bindgen::prelude::wasm_bindgen)]
57pub struct LookupPatterns {
58    /// Enables XOR lookup table for bitwise XOR operations.
59    pub xor: bool,
60    /// Enables generic lookup pattern for user-defined tables.
61    pub lookup: bool,
62    /// Enables range check lookup table for 12-bit range constraints.
63    pub range_check: bool,
64    /// Enables lookup tables for foreign field multiplication.
65    pub foreign_field_mul: bool,
66}
67
68impl IntoIterator for LookupPatterns {
69    type Item = LookupPattern;
70    type IntoIter = alloc::vec::IntoIter<Self::Item>;
71
72    fn into_iter(self) -> Self::IntoIter {
73        // Destructor pattern to make sure we add new lookup patterns.
74        let LookupPatterns {
75            xor,
76            lookup,
77            range_check,
78            foreign_field_mul,
79        } = self;
80
81        let mut patterns = Vec::with_capacity(5);
82
83        if xor {
84            patterns.push(LookupPattern::Xor)
85        }
86        if lookup {
87            patterns.push(LookupPattern::Lookup)
88        }
89        if range_check {
90            patterns.push(LookupPattern::RangeCheck)
91        }
92        if foreign_field_mul {
93            patterns.push(LookupPattern::ForeignFieldMul)
94        }
95        patterns.into_iter()
96    }
97}
98
99impl core::ops::Index<LookupPattern> for LookupPatterns {
100    type Output = bool;
101
102    fn index(&self, index: LookupPattern) -> &Self::Output {
103        match index {
104            LookupPattern::Xor => &self.xor,
105            LookupPattern::Lookup => &self.lookup,
106            LookupPattern::RangeCheck => &self.range_check,
107            LookupPattern::ForeignFieldMul => &self.foreign_field_mul,
108        }
109    }
110}
111
112impl core::ops::IndexMut<LookupPattern> for LookupPatterns {
113    fn index_mut(&mut self, index: LookupPattern) -> &mut Self::Output {
114        match index {
115            LookupPattern::Xor => &mut self.xor,
116            LookupPattern::Lookup => &mut self.lookup,
117            LookupPattern::RangeCheck => &mut self.range_check,
118            LookupPattern::ForeignFieldMul => &mut self.foreign_field_mul,
119        }
120    }
121}
122
123impl LookupPatterns {
124    /// Detects which lookup patterns are used by scanning circuit gates.
125    ///
126    /// Each gate type may require specific lookup patterns. This method
127    /// iterates through all gates and enables the corresponding pattern
128    /// flags based on [`LookupPattern::from_gate`].
129    pub fn from_gates<F: PrimeField>(gates: &[CircuitGate<F>]) -> LookupPatterns {
130        let mut kinds = LookupPatterns::default();
131        for g in gates.iter() {
132            for r in &[CurrOrNext::Curr, CurrOrNext::Next] {
133                if let Some(lookup_pattern) = LookupPattern::from_gate(g.typ, *r) {
134                    kinds[lookup_pattern] = true;
135                }
136            }
137        }
138        kinds
139    }
140
141    /// Returns `true` if any enabled pattern uses joint lookups (multiple
142    /// columns combined into a single lookup).
143    pub fn joint_lookups_used(&self) -> bool {
144        for lookup_pattern in *self {
145            if lookup_pattern.max_joint_size() > 1 {
146                return true;
147            }
148        }
149        false
150    }
151}
152
153/// Configuration for lookup-related features in a circuit.
154///
155/// Lookups allow efficient verification that witness values exist in
156/// precomputed tables. This struct tracks which lookup capabilities are
157/// needed, enabling kimchi to include only the required lookup machinery.
158#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
159#[cfg_attr(
160    feature = "ocaml_types",
161    derive(ocaml::IntoValue, ocaml::FromValue, ocaml_gen::Struct)
162)]
163#[cfg_attr(feature = "wasm_types", wasm_bindgen::prelude::wasm_bindgen)]
164pub struct LookupFeatures {
165    /// Which lookup patterns are enabled. See [`LookupPatterns`].
166    pub patterns: LookupPatterns,
167    /// Whether any enabled pattern uses joint lookups (multiple columns
168    /// combined into a single lookup value).
169    pub joint_lookup_used: bool,
170    /// Whether the circuit uses runtime lookup tables (tables whose contents
171    /// are provided at proving time rather than being fixed at setup).
172    pub uses_runtime_tables: bool,
173}
174
175impl LookupFeatures {
176    /// Detects lookup features by scanning circuit gates.
177    ///
178    /// Determines which lookup patterns are used and whether joint lookups
179    /// are needed based on the gate types present in the circuit.
180    pub fn from_gates<F: PrimeField>(gates: &[CircuitGate<F>], uses_runtime_tables: bool) -> Self {
181        let patterns = LookupPatterns::from_gates(gates);
182
183        let joint_lookup_used = patterns.joint_lookups_used();
184
185        LookupFeatures {
186            patterns,
187            uses_runtime_tables,
188            joint_lookup_used,
189        }
190    }
191}
192
193/// Describes the desired lookup configuration.
194#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
195#[cfg_attr(feature = "wasm_types", wasm_bindgen::prelude::wasm_bindgen)]
196pub struct LookupInfo {
197    /// The maximum length of an element of `kinds`. This can be computed from `kinds`.
198    pub max_per_row: usize,
199    /// The maximum joint size of any joint lookup in a constraint in `kinds`. This can be computed from `kinds`.
200    pub max_joint_size: u32,
201    /// The features enabled for this lookup configuration
202    pub features: LookupFeatures,
203}
204
205impl LookupInfo {
206    /// Create the default lookup configuration.
207    pub fn create(features: LookupFeatures) -> Self {
208        let max_per_row = max_lookups_per_row(features.patterns);
209
210        LookupInfo {
211            max_joint_size: features
212                .patterns
213                .into_iter()
214                .fold(0, |acc, v| core::cmp::max(acc, v.max_joint_size())),
215            max_per_row,
216            features,
217        }
218    }
219
220    pub fn create_from_gates<F: PrimeField>(
221        gates: &[CircuitGate<F>],
222        uses_runtime_tables: bool,
223    ) -> Option<Self> {
224        let features = LookupFeatures::from_gates(gates, uses_runtime_tables);
225
226        if features.patterns == LookupPatterns::default() {
227            None
228        } else {
229            Some(Self::create(features))
230        }
231    }
232
233    /// Each entry in `kinds` has a corresponding selector polynomial that controls whether that
234    /// lookup kind should be enforced at a given row. This computes those selector polynomials.
235    pub fn selector_polynomials_and_tables<F: PrimeField>(
236        &self,
237        domain: &EvaluationDomains<F>,
238        gates: &[CircuitGate<F>],
239    ) -> (LookupSelectors<Evaluations<F>>, Vec<LookupTable<F>>) {
240        let n = domain.d1.size();
241
242        let mut selector_values = LookupSelectors::default();
243        for kind in self.features.patterns {
244            selector_values[kind] = Some(vec![F::zero(); n]);
245        }
246
247        let mut gate_tables = BTreeSet::new();
248
249        let mut update_selector = |lookup_pattern, i| {
250            let selector = selector_values[lookup_pattern]
251                .as_mut()
252                .unwrap_or_else(|| panic!("has selector for {lookup_pattern:?}"));
253            selector[i] = F::one();
254        };
255
256        // TODO: is take(n) useful here? I don't see why we need this
257        for (i, gate) in gates.iter().enumerate().take(n) {
258            let typ = gate.typ;
259
260            if let Some(lookup_pattern) = LookupPattern::from_gate(typ, CurrOrNext::Curr) {
261                update_selector(lookup_pattern, i);
262                if let Some(table_kind) = lookup_pattern.table() {
263                    gate_tables.insert(table_kind);
264                }
265            }
266            if let Some(lookup_pattern) = LookupPattern::from_gate(typ, CurrOrNext::Next) {
267                update_selector(lookup_pattern, i + 1);
268                if let Some(table_kind) = lookup_pattern.table() {
269                    gate_tables.insert(table_kind);
270                }
271            }
272        }
273
274        // Actually, don't need to evaluate over domain 8 here.
275        // TODO: so why do it :D?
276        let selector_values8: LookupSelectors<_> = selector_values.map(|v| {
277            E::<F, D<F>>::from_vec_and_domain(v, domain.d1)
278                .interpolate()
279                .evaluate_over_domain(domain.d8)
280        });
281        let res_tables: Vec<_> = gate_tables.into_iter().map(get_table).collect();
282        (selector_values8, res_tables)
283    }
284
285    /// For each row in the circuit, which lookup-constraints should be enforced at that row.
286    pub fn by_row<F: PrimeField>(&self, gates: &[CircuitGate<F>]) -> Vec<Vec<JointLookupSpec<F>>> {
287        let mut kinds = vec![vec![]; gates.len() + 1];
288        for i in 0..gates.len() {
289            let typ = gates[i].typ;
290
291            if let Some(lookup_pattern) = LookupPattern::from_gate(typ, CurrOrNext::Curr) {
292                kinds[i] = lookup_pattern.lookups();
293            }
294            if let Some(lookup_pattern) = LookupPattern::from_gate(typ, CurrOrNext::Next) {
295                kinds[i + 1] = lookup_pattern.lookups();
296            }
297        }
298        kinds
299    }
300}
301
302/// A position in the circuit relative to a given row.
303#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
304pub struct LocalPosition {
305    pub row: CurrOrNext,
306    pub column: usize,
307}
308
309/// Look up a single value in a lookup table. The value may be computed as a linear
310/// combination of locally-accessible cells.
311#[derive(Clone, Serialize, Deserialize)]
312pub struct SingleLookup<F> {
313    /// Linear combination of local-positions
314    pub value: Vec<(F, LocalPosition)>,
315}
316
317impl<F: Copy> SingleLookup<F> {
318    /// Evaluate the linear combination specifying the lookup value to a field element.
319    pub fn evaluate<K, G: Fn(LocalPosition) -> K>(&self, eval: G) -> K
320    where
321        K: Zero,
322        K: Mul<F, Output = K>,
323    {
324        self.value
325            .iter()
326            .fold(K::zero(), |acc, (c, p)| acc + eval(*p) * *c)
327    }
328}
329
330/// The table ID associated with a particular lookup
331#[derive(Clone, Serialize, Deserialize, Debug)]
332pub enum LookupTableID {
333    /// Look up the value from the given fixed table ID
334    Constant(i32),
335    /// Look up the value in the table with ID given by the value in the witness column
336    WitnessColumn(usize),
337}
338
339/// A spec for checking that the given vector belongs to a vector-valued lookup table.
340#[derive(Clone, Serialize, Deserialize, Debug)]
341pub struct JointLookup<SingleLookup, LookupTableID> {
342    /// The ID for the table associated with this lookup.
343    /// Positive IDs are intended to be used for the fixed tables associated with individual gates,
344    /// with negative IDs reserved for gates defined by the particular constraint system to avoid
345    /// accidental collisions.
346    pub table_id: LookupTableID,
347    pub entry: Vec<SingleLookup>,
348}
349
350/// A spec for checking that the given vector belongs to a vector-valued lookup table, where the
351/// components of the vector are computed from a linear combination of locally-accessible cells.
352pub type JointLookupSpec<F> = JointLookup<SingleLookup<F>, LookupTableID>;
353
354/// A concrete value or representation of a lookup.
355pub type JointLookupValue<F> = JointLookup<F, F>;
356
357impl<F: Zero + One + Clone + Neg<Output = F> + From<u64>> JointLookupValue<F> {
358    /// Evaluate the combined value of a joint-lookup.
359    pub fn evaluate(&self, joint_combiner: &F, table_id_combiner: &F) -> F {
360        combine_table_entry(
361            joint_combiner,
362            table_id_combiner,
363            self.entry.iter(),
364            &self.table_id,
365        )
366    }
367}
368
369impl<F: Copy> JointLookup<SingleLookup<F>, LookupTableID> {
370    /// Reduce linear combinations in the lookup entries to a single value, resolving local
371    /// positions using the given function.
372    pub fn reduce<K, G: Fn(LocalPosition) -> K>(&self, eval: &G) -> JointLookupValue<K>
373    where
374        K: Zero,
375        K: Mul<F, Output = K>,
376        K: Neg<Output = K>,
377        K: From<u64>,
378    {
379        let table_id = match self.table_id {
380            LookupTableID::Constant(table_id) => i32_to_field(table_id),
381            LookupTableID::WitnessColumn(column) => eval(LocalPosition {
382                row: CurrOrNext::Curr,
383                column,
384            }),
385        };
386        JointLookup {
387            table_id,
388            entry: self.entry.iter().map(|s| s.evaluate(eval)).collect(),
389        }
390    }
391
392    /// Evaluate the combined value of a joint-lookup, resolving local positions using the given
393    /// function.
394    pub fn evaluate<K, G: Fn(LocalPosition) -> K>(
395        &self,
396        joint_combiner: &K,
397        table_id_combiner: &K,
398        eval: &G,
399    ) -> K
400    where
401        K: Zero + One + Clone,
402        K: Mul<F, Output = K>,
403        K: Neg<Output = K>,
404        K: From<u64>,
405    {
406        self.reduce(eval)
407            .evaluate(joint_combiner, table_id_combiner)
408    }
409}
410
411#[derive(
412    Copy, Clone, Serialize, Deserialize, Debug, EnumIter, PartialEq, Eq, PartialOrd, Ord, Hash,
413)]
414#[cfg_attr(
415    feature = "ocaml_types",
416    derive(ocaml::IntoValue, ocaml::FromValue, ocaml_gen::Enum)
417)]
418pub enum LookupPattern {
419    Xor,
420    Lookup,
421    RangeCheck,
422    ForeignFieldMul,
423}
424
425impl LookupPattern {
426    /// Returns the maximum number of lookups per row that are used by the pattern.
427    pub fn max_lookups_per_row(&self) -> usize {
428        match self {
429            LookupPattern::Xor | LookupPattern::RangeCheck | LookupPattern::ForeignFieldMul => 4,
430            LookupPattern::Lookup => 3,
431        }
432    }
433
434    /// Returns the maximum number of values that are used in any vector lookup in this pattern.
435    pub fn max_joint_size(&self) -> u32 {
436        match self {
437            LookupPattern::Xor => 3,
438            LookupPattern::Lookup => 2,
439            LookupPattern::ForeignFieldMul | LookupPattern::RangeCheck => 1,
440        }
441    }
442
443    /// Returns the layout of the lookups used by this pattern.
444    ///
445    /// # Panics
446    ///
447    /// Will panic if `multiplicative inverse` operation fails.
448    pub fn lookups<F: Field>(&self) -> Vec<JointLookupSpec<F>> {
449        let curr_row = |column| LocalPosition {
450            row: CurrOrNext::Curr,
451            column,
452        };
453        match self {
454            LookupPattern::Xor => {
455                (0..4)
456                    .map(|i| {
457                        // each row represents an XOR operation
458                        // where l XOR r = o
459                        //
460                        // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
461                        // - - - l - - - r - - -  o  -  -  -
462                        // - - - - l - - - r - -  -  o  -  -
463                        // - - - - - l - - - r -  -  -  o  -
464                        // - - - - - - l - - - r  -  -  -  o
465                        let left = curr_row(3 + i);
466                        let right = curr_row(7 + i);
467                        let output = curr_row(11 + i);
468                        let l = |loc: LocalPosition| SingleLookup {
469                            value: vec![(F::one(), loc)],
470                        };
471                        JointLookup {
472                            table_id: LookupTableID::Constant(XOR_TABLE_ID),
473                            entry: vec![l(left), l(right), l(output)],
474                        }
475                    })
476                    .collect()
477            }
478            LookupPattern::Lookup => {
479                (0..3)
480                    .map(|i| {
481                        // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
482                        // - i v - - - - - - - -  -  -  -  -
483                        // - - - i v - - - - - -  -  -  -  -
484                        // - - - - - i v - - - -  -  -  -  -
485                        let index = curr_row(2 * i + 1);
486                        let value = curr_row(2 * i + 2);
487                        let l = |loc: LocalPosition| SingleLookup {
488                            value: vec![(F::one(), loc)],
489                        };
490                        JointLookup {
491                            table_id: LookupTableID::WitnessColumn(0),
492                            entry: vec![l(index), l(value)],
493                        }
494                    })
495                    .collect()
496            }
497            LookupPattern::RangeCheck => {
498                (3..=6)
499                    .map(|column| {
500                        //   0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
501                        //   - - - L L L L - - - -  -  -  -  -
502                        JointLookup {
503                            table_id: LookupTableID::Constant(RANGE_CHECK_TABLE_ID),
504                            entry: vec![SingleLookup {
505                                value: vec![(F::one(), curr_row(column))],
506                            }],
507                        }
508                    })
509                    .collect()
510            }
511            LookupPattern::ForeignFieldMul => {
512                (7..=10)
513                    .map(|col| {
514                        // curr and next (in next carry0 is in w(7))
515                        //   0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
516                        //   - - - - - - - L L L L  -  -  -  -
517                        //    * Constrain w(7), w(8), w(9), w(10) to 12-bits
518                        JointLookup {
519                            table_id: LookupTableID::Constant(RANGE_CHECK_TABLE_ID),
520                            entry: vec![SingleLookup {
521                                value: vec![(F::one(), curr_row(col))],
522                            }],
523                        }
524                    })
525                    .collect()
526            }
527        }
528    }
529
530    /// Returns the lookup table used by the pattern, or `None` if no specific table is rqeuired.
531    pub fn table(&self) -> Option<GateLookupTable> {
532        match self {
533            LookupPattern::Xor => Some(GateLookupTable::Xor),
534            LookupPattern::Lookup => None,
535            LookupPattern::RangeCheck => Some(GateLookupTable::RangeCheck),
536            LookupPattern::ForeignFieldMul => Some(GateLookupTable::RangeCheck),
537        }
538    }
539
540    /// Returns the lookup pattern used by a [`GateType`] on a given row (current or next).
541    pub fn from_gate(gate_type: GateType, curr_or_next: CurrOrNext) -> Option<Self> {
542        use CurrOrNext::{Curr, Next};
543        use GateType::*;
544        match (gate_type, curr_or_next) {
545            (Lookup, Curr) => Some(LookupPattern::Lookup),
546            (RangeCheck0, Curr) | (RangeCheck1, Curr | Next) | (Rot64, Curr) => {
547                Some(LookupPattern::RangeCheck)
548            }
549            (ForeignFieldMul, Curr | Next) => Some(LookupPattern::ForeignFieldMul),
550            (Xor16, Curr) => Some(LookupPattern::Xor),
551            _ => None,
552        }
553    }
554}
555
556impl GateType {
557    /// Which lookup-patterns should be applied on which rows.
558    pub fn lookup_kinds() -> Vec<LookupPattern> {
559        vec![
560            LookupPattern::Xor,
561            LookupPattern::Lookup,
562            LookupPattern::RangeCheck,
563            LookupPattern::ForeignFieldMul,
564        ]
565    }
566}
567
568#[test]
569fn lookup_pattern_constants_correct() {
570    use strum::IntoEnumIterator;
571
572    for pat in LookupPattern::iter() {
573        let lookups = pat.lookups::<mina_curves::pasta::Fp>();
574        let max_joint_size = lookups
575            .iter()
576            .map(|lookup| lookup.entry.len())
577            .max()
578            .unwrap_or(0);
579        // NB: We include pat in the assertions so that the test will print out which pattern failed
580        assert_eq!((pat, pat.max_lookups_per_row()), (pat, lookups.len()));
581        assert_eq!((pat, pat.max_joint_size()), (pat, max_joint_size as u32));
582    }
583}
584
585#[cfg(feature = "wasm_types")]
586pub mod wasm {
587    use super::*;
588
589    #[wasm_bindgen::prelude::wasm_bindgen]
590    impl LookupPatterns {
591        #[wasm_bindgen::prelude::wasm_bindgen(constructor)]
592        pub fn new(
593            xor: bool,
594            lookup: bool,
595            range_check: bool,
596            foreign_field_mul: bool,
597        ) -> LookupPatterns {
598            LookupPatterns {
599                xor,
600                lookup,
601                range_check,
602                foreign_field_mul,
603            }
604        }
605    }
606
607    #[wasm_bindgen::prelude::wasm_bindgen]
608    impl LookupFeatures {
609        #[wasm_bindgen::prelude::wasm_bindgen(constructor)]
610        pub fn new(
611            patterns: LookupPatterns,
612            joint_lookup_used: bool,
613            uses_runtime_tables: bool,
614        ) -> LookupFeatures {
615            LookupFeatures {
616                patterns,
617                joint_lookup_used,
618                uses_runtime_tables,
619            }
620        }
621    }
622
623    #[wasm_bindgen::prelude::wasm_bindgen]
624    impl LookupInfo {
625        #[wasm_bindgen::prelude::wasm_bindgen(constructor)]
626        pub fn new(
627            max_per_row: usize,
628            max_joint_size: u32,
629            features: LookupFeatures,
630        ) -> LookupInfo {
631            LookupInfo {
632                max_per_row,
633                max_joint_size,
634                features,
635            }
636        }
637    }
638}