use std::{
collections::{BTreeMap, HashSet},
fmt::{self, Debug},
};
use group::ff::Field;
use super::{
metadata,
metadata::{DebugColumn, DebugVirtualCell},
util::{self, AnyQuery},
MockProver, Region,
};
use crate::{
dev::{metadata::Constraint, Instance, Value},
plonk::{Any, Column, ConstraintSystem, Expression, Gate},
};
mod emitter;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum FailureLocation {
InRegion {
region: metadata::Region,
offset: usize,
},
OutsideRegion {
row: usize,
},
}
impl fmt::Display for FailureLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InRegion { region, offset } => write!(f, "in {region} at offset {offset}"),
Self::OutsideRegion { row } => {
write!(f, "outside any region, on row {row}")
}
}
}
}
impl FailureLocation {
pub(super) fn get_debug_column(&self, metadata: metadata::Column) -> DebugColumn {
match self {
Self::InRegion { region, .. } => {
DebugColumn::from((metadata, region.column_annotations.as_ref()))
}
_ => DebugColumn::from((metadata, None)),
}
}
pub(super) fn find_expressions<'a, F: Field>(
cs: &ConstraintSystem<F>,
regions: &[Region],
failure_row: usize,
failure_expressions: impl Iterator<Item = &'a Expression<F>>,
) -> Self {
let failure_columns: HashSet<Column<Any>> = failure_expressions
.flat_map(|expression| {
expression.evaluate(
&|_| vec![],
&|_| panic!("virtual selectors are removed during optimization"),
&|query| vec![cs.fixed_queries[query.index.unwrap()].0.into()],
&|query| vec![cs.advice_queries[query.index.unwrap()].0.into()],
&|query| vec![cs.instance_queries[query.index.unwrap()].0.into()],
&|_| vec![],
&|a| a,
&|mut a, mut b| {
a.append(&mut b);
a
},
&|mut a, mut b| {
a.append(&mut b);
a
},
&|a, _| a,
)
})
.collect();
Self::find(regions, failure_row, failure_columns)
}
pub(super) fn find(
regions: &[Region],
failure_row: usize,
failure_columns: HashSet<Column<Any>>,
) -> Self {
regions
.iter()
.enumerate()
.find(|(_, r)| {
if let Some((start, end)) = r.rows {
(start..=end).contains(&failure_row) && !failure_columns.is_disjoint(&r.columns)
} else {
false
}
})
.map(|(r_i, r)| FailureLocation::InRegion {
region: (r_i, r.name.clone(), r.annotations.clone()).into(),
offset: failure_row - r.rows.unwrap().0,
})
.unwrap_or_else(|| FailureLocation::OutsideRegion { row: failure_row })
}
}
#[derive(PartialEq, Eq)]
pub enum VerifyFailure {
CellNotAssigned {
gate: metadata::Gate,
region: metadata::Region,
gate_offset: usize,
column: Column<Any>,
offset: isize,
},
InstanceCellNotAssigned {
gate: metadata::Gate,
region: metadata::Region,
gate_offset: usize,
column: Column<Instance>,
row: usize,
},
ConstraintNotSatisfied {
constraint: metadata::Constraint,
location: FailureLocation,
cell_values: Vec<(metadata::VirtualCell, String)>,
},
ConstraintPoisoned {
constraint: metadata::Constraint,
},
Lookup {
name: String,
lookup_index: usize,
location: FailureLocation,
},
Permutation {
column: metadata::Column,
location: FailureLocation,
},
}
impl fmt::Display for VerifyFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CellNotAssigned {
gate,
region,
gate_offset,
column,
offset,
} => {
write!(
f,
"{} uses {} at offset {}, which requires cell in column {:?} at offset {} with annotation {:?} to be assigned.",
region, gate, gate_offset, column, offset, region.get_column_annotation((*column).into())
)
}
Self::InstanceCellNotAssigned {
gate,
region,
gate_offset,
column,
row,
} => {
write!(
f,
"{region} uses {gate} at offset {gate_offset}, which requires cell in instance column {column:?} at row {row} to be assigned.",
)
}
Self::ConstraintNotSatisfied {
constraint,
location,
cell_values,
} => {
writeln!(f, "{constraint} is not satisfied {location}")?;
for (dvc, value) in cell_values.iter().map(|(vc, string)| {
let ann_map = match location {
FailureLocation::InRegion { region, offset: _ } => {
®ion.column_annotations
}
_ => &None,
};
(DebugVirtualCell::from((vc, ann_map.as_ref())), string)
}) {
writeln!(f, "- {dvc} = {value}")?;
}
Ok(())
}
Self::ConstraintPoisoned { constraint } => {
write!(
f,
"{constraint} is active on an unusable row - missing selector?"
)
}
Self::Lookup {
name,
lookup_index,
location,
} => {
write!(
f,
"Lookup {name}(index: {lookup_index}) is not satisfied {location}",
)
}
Self::Permutation { column, location } => {
write!(
f,
"Equality constraint not satisfied by cell ({}, {})",
location.get_debug_column(*column),
location
)
}
}
}
}
impl Debug for VerifyFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VerifyFailure::ConstraintNotSatisfied {
constraint,
location,
cell_values,
} => {
#[allow(dead_code)]
#[derive(Debug)]
struct ConstraintCaseDebug {
constraint: Constraint,
location: FailureLocation,
cell_values: Vec<(DebugVirtualCell, String)>,
}
let ann_map = match location {
FailureLocation::InRegion { region, offset: _ } => {
region.column_annotations.clone()
}
_ => None,
};
let debug = ConstraintCaseDebug {
constraint: constraint.clone(),
location: location.clone(),
cell_values: cell_values
.iter()
.map(|(vc, value)| {
(
DebugVirtualCell::from((vc, ann_map.as_ref())),
value.clone(),
)
})
.collect(),
};
write!(f, "{debug:#?}")
}
_ => write!(f, "{self:#}"),
}
}
}
fn render_cell_not_assigned<F: Field>(
gates: &[Gate<F>],
gate: &metadata::Gate,
region: &metadata::Region,
gate_offset: usize,
column: Column<Any>,
offset: isize,
) {
let mut columns = BTreeMap::<metadata::Column, usize>::default();
let mut layout = BTreeMap::<i32, BTreeMap<metadata::Column, _>>::default();
for (i, cell) in gates[gate.index].queried_cells().iter().enumerate() {
let cell_column = cell.column.into();
*columns.entry(cell_column).or_default() += 1;
layout
.entry(cell.rotation.0)
.or_default()
.entry(cell_column)
.or_insert_with(|| {
if cell.column == column && gate_offset as i32 + cell.rotation.0 == offset as i32 {
"X".to_string()
} else {
format!("x{i}")
}
});
}
eprintln!("error: cell not assigned");
emitter::render_cell_layout(
" ",
&FailureLocation::InRegion {
region: region.clone(),
offset: gate_offset,
},
&columns,
&layout,
|row_offset, rotation| {
if (row_offset.unwrap() + rotation) as isize == offset {
eprint!(" <--{{ X marks the spot! 🦜");
}
},
);
eprintln!();
eprintln!(
" Gate '{}' (applied at offset {}) queries these cells.",
gate.name, gate_offset
);
}
fn render_constraint_not_satisfied<F: Field>(
gates: &[Gate<F>],
constraint: &metadata::Constraint,
location: &FailureLocation,
cell_values: &[(metadata::VirtualCell, String)],
) {
let mut columns = BTreeMap::<metadata::Column, usize>::default();
let mut layout = BTreeMap::<i32, BTreeMap<metadata::Column, _>>::default();
for (i, (cell, _)) in cell_values.iter().enumerate() {
*columns.entry(cell.column).or_default() += 1;
layout
.entry(cell.rotation)
.or_default()
.entry(cell.column)
.or_insert(format!("x{i}"));
}
eprintln!("error: constraint not satisfied");
emitter::render_cell_layout(" ", location, &columns, &layout, |_, rotation| {
if rotation == 0 {
eprint!(" <--{{ Gate '{}' applied here", constraint.gate.name);
}
});
eprintln!();
eprintln!(" Constraint '{}':", constraint.name);
eprintln!(
" {} = 0",
emitter::expression_to_string(
&gates[constraint.gate.index].polynomials()[constraint.index],
&layout
)
);
eprintln!();
eprintln!(" Assigned cell values:");
for (i, (_, value)) in cell_values.iter().enumerate() {
eprintln!(" x{i} = {value}");
}
}
fn render_lookup<F: Field>(
prover: &MockProver<F>,
name: &str,
lookup_index: usize,
location: &FailureLocation,
) {
let n = prover.n as i32;
let cs = &prover.cs;
let lookup = &cs.lookups[lookup_index];
let row = match location {
FailureLocation::InRegion { region, offset } => {
prover.regions[region.index].rows.unwrap().0 + offset
}
FailureLocation::OutsideRegion { row } => *row,
} as i32;
let lookup_columns = lookup.table_expressions.iter().map(|expr| {
expr.evaluate(
&|f| format! {"Const: {f:#?}"},
&|s| format! {"S{}", s.0},
&|query| {
format!(
"{:?}",
prover
.cs
.general_column_annotations
.get(&metadata::Column::from((Any::Fixed, query.column_index)))
.cloned()
.unwrap_or_else(|| format!("F{}", query.column_index()))
)
},
&|query| {
format!(
"{:?}",
prover
.cs
.general_column_annotations
.get(&metadata::Column::from((Any::advice(), query.column_index)))
.cloned()
.unwrap_or_else(|| format!("A{}", query.column_index()))
)
},
&|query| {
format!(
"{:?}",
prover
.cs
.general_column_annotations
.get(&metadata::Column::from((Any::Instance, query.column_index)))
.cloned()
.unwrap_or_else(|| format!("I{}", query.column_index()))
)
},
&|challenge| format! {"C{}", challenge.index()},
&|query| format! {"-{query}"},
&|a, b| format! {"{a} + {b}"},
&|a, b| format! {"{a} * {b}"},
&|a, b| format! {"{a} * {b:?}"},
)
});
fn cell_value<'a, F: Field, Q: Into<AnyQuery> + Copy>(
load: impl Fn(Q) -> Value<F> + 'a,
) -> impl Fn(Q) -> BTreeMap<metadata::VirtualCell, String> + 'a {
move |query| {
let AnyQuery {
column_type,
column_index,
rotation,
..
} = query.into();
Some((
((column_type, column_index).into(), rotation.0).into(),
match load(query) {
Value::Real(v) => util::format_value(v),
Value::Poison => unreachable!(),
},
))
.into_iter()
.collect()
}
}
eprintln!("error: lookup input does not exist in table");
eprint!(" (");
for i in 0..lookup.input_expressions.len() {
eprint!("{}L{}", if i == 0 { "" } else { ", " }, i);
}
eprint!(") ∉ (");
for (i, column) in lookup_columns.enumerate() {
eprint!("{}{}", if i == 0 { "" } else { ", " }, column);
}
eprintln!(")");
eprintln!();
eprintln!(" Lookup '{name}' inputs:");
for (i, input) in lookup.input_expressions.iter().enumerate() {
let cell_values = input.evaluate(
&|_| BTreeMap::default(),
&|_| panic!("virtual selectors are removed during optimization"),
&cell_value(&util::load(n, row, &cs.fixed_queries, &prover.fixed)),
&cell_value(&util::load(n, row, &cs.advice_queries, &prover.advice)),
&cell_value(&util::load_instance(
n,
row,
&cs.instance_queries,
&prover.instance,
)),
&|_| BTreeMap::default(),
&|a| a,
&|mut a, mut b| {
a.append(&mut b);
a
},
&|mut a, mut b| {
a.append(&mut b);
a
},
&|a, _| a,
);
let mut columns = BTreeMap::<metadata::Column, usize>::default();
let mut layout = BTreeMap::<i32, BTreeMap<metadata::Column, _>>::default();
for (i, (cell, _)) in cell_values.iter().enumerate() {
*columns.entry(cell.column).or_default() += 1;
layout
.entry(cell.rotation)
.or_default()
.entry(cell.column)
.or_insert(format!("x{i}"));
}
if i != 0 {
eprintln!();
}
eprintln!(
" L{} = {}",
i,
emitter::expression_to_string(input, &layout)
);
eprintln!(" ^");
emitter::render_cell_layout(" | ", location, &columns, &layout, |_, rotation| {
if rotation == 0 {
eprint!(" <--{{ Lookup '{name}' inputs queried here");
}
});
eprintln!(" |");
eprintln!(" | Assigned cell values:");
for (i, (_, value)) in cell_values.iter().enumerate() {
eprintln!(" | x{i} = {value}");
}
}
}
impl VerifyFailure {
pub(super) fn emit<F: Field>(&self, prover: &MockProver<F>) {
match self {
Self::CellNotAssigned {
gate,
region,
gate_offset,
column,
offset,
} => render_cell_not_assigned(
&prover.cs.gates,
gate,
region,
*gate_offset,
*column,
*offset,
),
Self::ConstraintNotSatisfied {
constraint,
location,
cell_values,
} => {
render_constraint_not_satisfied(&prover.cs.gates, constraint, location, cell_values)
}
Self::Lookup {
name,
lookup_index,
location,
} => render_lookup(prover, name, *lookup_index, location),
_ => eprintln!("{self}"),
}
}
}