Skip to main content

binius_core/
error.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3//! Hosts error definitions for the core crate.
4
5use std::fmt;
6
7use crate::constraint_system::{Composition, ConstraintKind, ValueSegment};
8
9/// Constraint system related error.
10#[allow(missing_docs)] // errors are self-documenting
11#[derive(Debug, thiserror::Error)]
12pub enum ConstraintSystemError {
13	#[error("{constraint_kind} #{constraint_index} operand {operand_name} is malformed: {source}")]
14	ConstraintOperand {
15		constraint_kind: ConstraintKind,
16		constraint_index: usize,
17		operand_name: &'static str,
18		#[source]
19		source: OperandFault,
20	},
21	#[error("chip call #{call_index} has a malformed operand #{operand_index}: {source}")]
22	ChipCallOperand {
23		call_index: usize,
24		operand_index: usize,
25		#[source]
26		source: OperandFault,
27	},
28	#[error("{} calls chip {chip_id}, but the system has {n_chips} chips", ChipName(*chip_index))]
29	OutOfRangeChipId {
30		chip_index: Option<usize>,
31		chip_id: usize,
32		n_chips: usize,
33	},
34	#[error(
35		"{}'s call #{call_index} passes {arity} operands to chip {chip_id}, which has {n_inout} inout values",
36		ChipName(*chip_index)
37	)]
38	WrongCallArity {
39		chip_index: Option<usize>,
40		call_index: usize,
41		chip_id: usize,
42		arity: usize,
43		n_inout: usize,
44	},
45	#[error("chip #{chip_index} calls chip {callee}, which is not a later chip")]
46	CallOutOfOrder { chip_index: usize, callee: usize },
47	#[error(
48		"{}'s call #{call_index} names instance {first_instance}, but the call graph gives it {expected}",
49		ChipName(*chip_index)
50	)]
51	WrongCallInstance {
52		chip_index: Option<usize>,
53		call_index: usize,
54		first_instance: usize,
55		expected: usize,
56	},
57	#[error("chip #{chip_id} declares {declared} active instances, but {actual} calls claim it")]
58	WrongActiveInstanceCount {
59		chip_id: usize,
60		declared: usize,
61		actual: usize,
62	},
63	#[error("more invocations reach chip #{chip_id} than a usize can count")]
64	TooManyInstances { chip_id: usize },
65}
66
67/// Names the chip of an M4 system that a diagnostic is about: `chip #3`, or `the main chip`.
68///
69/// The main chip is not one of the numbered chips, so it has no index. The frontend's circuit form
70/// numbers its chips the same way, so its diagnostics name them through this too.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct ChipName(pub Option<usize>);
73
74impl fmt::Display for ChipName {
75	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76		match self.0 {
77			Some(chip_index) => write!(f, "chip #{chip_index}"),
78			None => f.write_str("the main chip"),
79		}
80	}
81}
82
83/// The way one term of an operand is malformed, said without naming where the operand sits.
84///
85/// A diagnostic pairs this with the position of the operand it checked, which differs between a
86/// constraint and a chip call.
87#[allow(missing_docs)] // errors are self-documenting
88#[derive(Debug, thiserror::Error)]
89pub enum OperandFault {
90	#[error("the shift is not canonical")]
91	NonCanonicalShift,
92	#[error("the shift amount n={shift_amount}>={max_amount}")]
93	ShiftAmountTooLarge {
94		shift_amount: usize,
95		max_amount: usize,
96	},
97	#[error("a lone shift sits in the outer slot; the canonical form places it inner")]
98	NonCanonicalShiftSequence,
99	#[error("a shift pair composes to {composition:?} rather than staying a pair")]
100	CollapsibleShiftSequence { composition: Composition },
101	#[error("it refers to a scratch value")]
102	ScratchValueIndex,
103	#[error("it refers to {segment:?} index {value_index} >= segment length {segment_len}")]
104	OutOfRangeValueIndex {
105		segment: ValueSegment,
106		value_index: u32,
107		segment_len: usize,
108	},
109}
110
111/// The arithmetic by which a single constraint fails on a value vector.
112///
113/// Every variant carries the operand words as the value vector evaluates them.
114/// The failing relation reads straight off the message, with no need to recompute it.
115#[derive(Debug, thiserror::Error)]
116pub enum ConstraintViolation {
117	/// An operand required to vanish holds a nonzero word.
118	#[error("{val:016x} != 0")]
119	Zero {
120		/// The word the operand evaluates to.
121		val: u64,
122	},
123	/// A conjunction of two operands disagrees with the operand claiming it.
124	#[error("({a:016x} & {b:016x}) ^ {c:016x} = {residue:016x} != 0")]
125	And {
126		/// The first operand of the conjunction.
127		a: u64,
128		/// The second operand of the conjunction.
129		b: u64,
130		/// The claimed conjunction.
131		c: u64,
132		/// The bits on which the claim and the conjunction differ.
133		residue: u64,
134	},
135	/// An integer product disagrees with the word pair claiming it.
136	#[error("{a:016x} * {b:016x} = {expected_hi:016x}{expected_lo:016x}, got {hi:016x}{lo:016x}")]
137	Imul {
138		/// The first factor.
139		a: u64,
140		/// The second factor.
141		b: u64,
142		/// The claimed low 64 bits of the product.
143		lo: u64,
144		/// The claimed high 64 bits of the product.
145		hi: u64,
146		/// The low 64 bits the product actually has.
147		expected_lo: u64,
148		/// The high 64 bits the product actually has.
149		expected_hi: u64,
150	},
151	/// A binary-field product disagrees with the element claiming it.
152	#[error("{a:032x} * {b:032x} = {expected:032x}, got {c:032x}")]
153	Bmul {
154		/// The first factor, with bit `i` holding the coefficient of `X^i`.
155		a: u128,
156		/// The second factor, with bit `i` holding the coefficient of `X^i`.
157		b: u128,
158		/// The claimed product.
159		c: u128,
160		/// The product the two factors actually have.
161		expected: u128,
162	},
163}
164
165impl ConstraintViolation {
166	/// Returns the kind of constraint that failed.
167	///
168	/// The kind follows from which relation was violated.
169	/// Storing it as a separate field would let the two disagree.
170	pub const fn kind(&self) -> ConstraintKind {
171		match self {
172			Self::Zero { .. } => ConstraintKind::Zero,
173			Self::And { .. } => ConstraintKind::And,
174			Self::Imul { .. } => ConstraintKind::Imul,
175			Self::Bmul { .. } => ConstraintKind::Bmul,
176		}
177	}
178}
179
180/// Reason a value vector fails to satisfy a constraint system.
181#[derive(Debug, thiserror::Error)]
182pub enum VerificationError {
183	/// A word declared as a constant opens to something else in the value vector.
184	///
185	/// Constraints read constants through the value vector.
186	/// A vector that opens one to the wrong word therefore satisfies a different system.
187	#[error(
188		"value {value_index} is {actual:016x}, but the system declares the constant {expected:016x}"
189	)]
190	ConstantMismatch {
191		/// Position of the disagreeing word in the value vector.
192		value_index: u32,
193		/// The word the system declares at that position.
194		expected: u64,
195		/// The word the value vector opens there.
196		actual: u64,
197	},
198	/// A constraint does not hold on the value vector.
199	#[error("{} #{constraint_index} is unsatisfied: {source}", source.kind())]
200	Unsatisfied {
201		/// Position of the constraint among those of its own kind, in storage order.
202		constraint_index: usize,
203		/// The relation that failed, carrying the words that failed it.
204		source: ConstraintViolation,
205	},
206}
207
208/// Names the chip instance an M4 diagnostic blames a call on, by chip index and instance.
209///
210/// The main chip runs once, so only a numbered chip's instance is worth naming. Unlike
211/// [`ChipName`], nothing outside this module names a caller, so this stays private to it.
212struct CallerName(Option<(usize, usize)>);
213
214impl fmt::Display for CallerName {
215	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216		match self.0 {
217			Some((chip_index, instance)) => write!(f, "chip #{chip_index} instance #{instance}"),
218			None => f.write_str("the main chip"),
219		}
220	}
221}
222
223/// Reason a witness fails to satisfy an M4 constraint system.
224///
225/// A witness is the main chip's value vector and one list of instance value vectors per chip.
226/// It must satisfy every chip's local constraints on every instance, and serve every chip call
227/// with the instance that call names.
228#[allow(missing_docs)] // errors are self-documenting
229#[derive(Debug, thiserror::Error)]
230pub enum VerificationM4Error {
231	#[error("the witness covers {n_witness_chips} chips, but the system has {n_chips}")]
232	WrongChipCount {
233		n_witness_chips: usize,
234		n_chips: usize,
235	},
236	#[error("the main chip is not satisfied: {0}")]
237	Main(#[from] VerificationError),
238	#[error("chip #{chip_id} instance #{instance} is not satisfied: {source}")]
239	ChipInstance {
240		chip_id: usize,
241		instance: usize,
242		#[source]
243		source: VerificationError,
244	},
245	#[error("chip #{chip_id} has {n_instances} instances, fewer than its {n_active} active ones")]
246	MissingInstances {
247		chip_id: usize,
248		n_instances: usize,
249		n_active: usize,
250	},
251	#[error(
252		"call #{call_index} of {} reaches chip #{chip_id} as invocation #{row}, passing {passed:016x} \
253		 as inout value {word}, but the instance holds {served:016x}",
254		CallerName(*caller)
255	)]
256	CallMismatch {
257		chip_id: usize,
258		row: usize,
259		/// The calling chip instance, or `None` for the main chip.
260		caller: Option<(usize, usize)>,
261		call_index: usize,
262		word: usize,
263		passed: u64,
264		served: u64,
265	},
266}