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