Skip to main content

p3_lookup/
debug_util.rs

1//! Debug helpers to inspect lookup witnesses.
2//!
3//! They recompute every lookup tuple from the raw traces and assert that the
4//! resulting multiset is balanced (total multiplicity 0). Any mismatch will
5//! be reported with its location.
6
7use alloc::string::{String, ToString};
8use alloc::vec::Vec;
9use alloc::{format, vec};
10
11use hashbrown::HashMap;
12use p3_air::{AirBuilder, PermutationAirBuilder, RowWindow};
13use p3_field::Field;
14use p3_matrix::Matrix;
15use p3_matrix::dense::{RowMajorMatrix, RowMajorMatrixView};
16use p3_matrix::stack::VerticalPair;
17
18use crate::lookup_traits::{Kind, Lookup, symbolic_to_expr};
19
20/// All inputs required to replay lookup evaluations for one AIR instance.
21pub struct LookupDebugInstance<'a, F: Field> {
22    /// Main execution trace for the AIR.
23    pub main_trace: &'a RowMajorMatrix<F>,
24    /// Optional preprocessed columns associated with the AIR.
25    pub preprocessed_trace: &'a Option<RowMajorMatrix<F>>,
26    /// Public inputs provided to the AIR.
27    pub public_values: &'a [F],
28    /// Lookup contexts registered by the AIR.
29    pub lookups: &'a [Lookup<F>],
30    /// Challenges used for the lookup permutation argument.
31    pub permutation_challenges: &'a [F],
32}
33
34/// Location information used in debug messages.
35#[allow(unused)] // Only used in panic message upon mismatch.
36#[derive(Clone, Debug)]
37struct Location {
38    instance: usize,
39    lookup: usize,
40    row: usize,
41}
42
43/// Accumulates tuples and their multiplicities, tracking where each was seen.
44#[derive(Default)]
45struct MultiSet<F: Field> {
46    /// Key: field-element tuple. Value: (net multiplicity, source locations).
47    entries: HashMap<Vec<F>, (F, Vec<Location>)>,
48}
49
50impl<F: Field> MultiSet<F> {
51    /// Record one occurrence of a tuple with the given multiplicity.
52    /// Zero-multiplicity entries are silently skipped.
53    fn add(&mut self, key: Vec<F>, multiplicity: F, location: Location) {
54        if multiplicity.is_zero() {
55            return;
56        }
57
58        self.entries
59            .entry(key)
60            .and_modify(|(total, locations)| {
61                *total += multiplicity;
62                locations.push(location.clone());
63            })
64            .or_insert_with(|| (multiplicity, vec![location]));
65    }
66
67    /// Panic if any tuple has a non-zero net multiplicity.
68    ///
69    /// Entries are sorted lexicographically before checking so that the
70    /// *first* reported mismatch is deterministic (hash-map iteration
71    /// order is not).
72    fn assert_empty(&self, label: &str) {
73        let mut entries: Vec<_> = self.entries.iter().collect();
74        entries.sort_by(|(a, _), (b, _)| {
75            let a_str: Vec<String> = a.iter().map(|v| v.to_string()).collect();
76            let b_str: Vec<String> = b.iter().map(|v| v.to_string()).collect();
77            a_str.cmp(&b_str)
78        });
79        for (key, (total, locations)) in entries {
80            if !total.is_zero() {
81                let rendered_key: Vec<String> = key.iter().map(|v| v.to_string()).collect();
82                panic!(
83                    "Lookup mismatch ({label}): tuple {:?} has net multiplicity {:?}. Locations: {:?}",
84                    rendered_key, total, locations
85                );
86            }
87        }
88    }
89}
90
91/// Recompute all lookup tuples/multiplicities from the traces and assert that
92/// every lookup represents a balanced multiset equality.
93///
94/// - Local lookups are checked independently per instance.
95/// - Global lookups are grouped by interaction name; every tuple's total
96///   multiplicity across all participants must be zero.
97pub fn check_lookups<F: Field>(instances: &[LookupDebugInstance<'_, F>]) {
98    // 1) Check each local lookup independently.
99    for (instance_idx, instance) in instances.iter().enumerate() {
100        for (lookup_idx, lookup) in instance.lookups.iter().enumerate() {
101            if matches!(lookup.kind, Kind::Local) {
102                let mut multiset = MultiSet::default();
103                accumulate_lookup(instance_idx, lookup_idx, instance, lookup, &mut multiset);
104                multiset.assert_empty(&format!(
105                    "instance {instance_idx} local lookup {lookup_idx}"
106                ));
107            }
108        }
109    }
110
111    // 2) Aggregate all global lookups that share the same interaction name,
112    //    then verify each group sums to zero.
113    //    A name-to-index map gives O(1) group lookups instead of a linear scan.
114    let mut global_sets: Vec<(String, MultiSet<F>)> = Vec::new();
115    let mut global_index: HashMap<String, usize> = HashMap::new();
116
117    for (instance_idx, instance) in instances.iter().enumerate() {
118        for (lookup_idx, lookup) in instance.lookups.iter().enumerate() {
119            if let Kind::Global(name) = &lookup.kind {
120                let idx = *global_index.entry(name.clone()).or_insert_with(|| {
121                    global_sets.push((name.clone(), MultiSet::default()));
122                    global_sets.len() - 1
123                });
124
125                accumulate_lookup(
126                    instance_idx,
127                    lookup_idx,
128                    instance,
129                    lookup,
130                    &mut global_sets[idx].1,
131                );
132            }
133        }
134    }
135
136    for (name, multiset) in global_sets {
137        multiset.assert_empty(&format!("global lookup '{name}'"));
138    }
139}
140
141fn accumulate_lookup<F: Field>(
142    instance_idx: usize,
143    lookup_idx: usize,
144    instance: &LookupDebugInstance<'_, F>,
145    lookup: &Lookup<F>,
146    multiset: &mut MultiSet<F>,
147) {
148    let height = instance.main_trace.height();
149
150    for row in 0..height {
151        let local_main = instance.main_trace.row_slice(row).unwrap();
152        let next_main = instance.main_trace.row_slice((row + 1) % height).unwrap();
153        let main_rows = VerticalPair::new(
154            RowMajorMatrixView::new_row(&*local_main),
155            RowMajorMatrixView::new_row(&*next_main),
156        );
157
158        let preprocessed_rows_data = instance.preprocessed_trace.as_ref().map(|prep| {
159            (
160                prep.row_slice(row).unwrap(),
161                prep.row_slice((row + 1) % height).unwrap(),
162            )
163        });
164        let preprocessed_rows = match preprocessed_rows_data.as_ref() {
165            Some((prep_local, prep_next)) => VerticalPair::new(
166                RowMajorMatrixView::new_row(&**prep_local),
167                RowMajorMatrixView::new_row(&**prep_next),
168            ),
169            None => VerticalPair::new(
170                RowMajorMatrixView::new(&[], 0),
171                RowMajorMatrixView::new(&[], 0),
172            ),
173        };
174
175        let builder = MiniLookupBuilder {
176            main: main_rows,
177            preprocessed: RowWindow::from_two_rows(
178                preprocessed_rows.top.values,
179                preprocessed_rows.bottom.values,
180            ),
181            public_values: instance.public_values,
182            permutation_challenges: instance.permutation_challenges,
183            row,
184            height,
185        };
186
187        for (tuple_idx, elements) in lookup.element_exprs.iter().enumerate() {
188            let key = elements
189                .iter()
190                .map(|expr| symbolic_to_expr(&builder, expr))
191                .collect::<Vec<_>>();
192
193            let multiplicity = symbolic_to_expr(&builder, &lookup.multiplicities_exprs[tuple_idx]);
194
195            multiset.add(
196                key,
197                multiplicity,
198                Location {
199                    instance: instance_idx,
200                    lookup: lookup_idx,
201                    row,
202                },
203            );
204        }
205    }
206}
207
208struct MiniLookupBuilder<'a, F: Field> {
209    main: VerticalPair<RowMajorMatrixView<'a, F>, RowMajorMatrixView<'a, F>>,
210    preprocessed: RowWindow<'a, F>,
211    public_values: &'a [F],
212    permutation_challenges: &'a [F],
213    row: usize,
214    height: usize,
215}
216
217impl<'a, F: Field> AirBuilder for MiniLookupBuilder<'a, F> {
218    type F = F;
219    type Expr = F;
220    type Var = F;
221    type PreprocessedWindow = RowWindow<'a, F>;
222    type MainWindow = RowWindow<'a, F>;
223    type PublicVar = F;
224
225    fn main(&self) -> Self::MainWindow {
226        RowWindow::from_two_rows(self.main.top.values, self.main.bottom.values)
227    }
228
229    fn preprocessed(&self) -> &Self::PreprocessedWindow {
230        &self.preprocessed
231    }
232
233    fn is_first_row(&self) -> Self::Expr {
234        F::from_bool(self.row == 0)
235    }
236
237    fn is_last_row(&self) -> Self::Expr {
238        F::from_bool(self.row + 1 == self.height)
239    }
240
241    fn is_transition_window(&self, size: usize) -> Self::Expr {
242        assert!(size <= 2, "only two-row windows are supported, got {size}");
243        F::from_bool(self.row + 1 < self.height)
244    }
245
246    fn assert_zero<I: Into<Self::Expr>>(&mut self, _x: I) {}
247
248    fn public_values(&self) -> &[Self::PublicVar] {
249        self.public_values
250    }
251}
252
253impl<'a, F: Field> p3_air::ExtensionBuilder for MiniLookupBuilder<'a, F> {
254    type EF = F;
255    type ExprEF = F;
256    type VarEF = F;
257
258    fn assert_zero_ext<I: Into<Self::ExprEF>>(&mut self, _x: I) {}
259}
260
261impl<'a, F: Field> PermutationAirBuilder for MiniLookupBuilder<'a, F> {
262    type MP = RowWindow<'a, F>;
263    type RandomVar = F;
264
265    type PermutationVar = F;
266
267    fn permutation(&self) -> Self::MP {
268        // Empty slices; permutation columns are not needed for debug evals.
269        RowWindow::from_two_rows(&[], &[])
270    }
271
272    fn permutation_randomness(&self) -> &[Self::RandomVar] {
273        self.permutation_challenges
274    }
275
276    fn permutation_values(&self) -> &[Self::PermutationVar] {
277        &[]
278    }
279}