Skip to main content

binius_frontend/artifact/
witness.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4//! Assigning a circuit's input wires, and what population reports when an assertion fails.
5
6use std::{
7	fmt,
8	ops::{Index, IndexMut},
9};
10
11use binius_core::{ValueVec, Word};
12use binius_utils::strided_array::StridedArray2DViewMut;
13
14use crate::{Circuit, Wire};
15
16/// A single assertion that did not hold while populating the witness.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct AssertionFailure {
19	/// The circuit path the assertion was declared under, such as `.sha256.round[3]`.
20	///
21	/// Empty for an assertion at the circuit root.
22	pub path: String,
23	/// What the assertion required, against the words it saw instead.
24	///
25	/// A diagnostic for a human to read.
26	/// Its wording is not part of the API, so do not match on it.
27	pub detail: String,
28}
29
30impl fmt::Display for AssertionFailure {
31	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32		if self.path.is_empty() {
33			f.write_str(&self.detail)
34		} else {
35			write!(f, "{}: {}", self.path, self.detail)
36		}
37	}
38}
39
40/// Witness population failed because the circuit is not satisfied.
41///
42/// Evaluation runs to completion rather than stopping at the first bad assertion.
43/// So a caller sees every violation at once.
44///
45/// The retained list is capped at [`MAX_ASSERTION_FAILURES`](crate::MAX_ASSERTION_FAILURES).
46/// [`Self::total`] counts every violation, capped or not.
47/// The two disagree exactly when the cap was reached.
48#[derive(Debug, thiserror::Error)]
49#[non_exhaustive]
50pub struct PopulateError {
51	/// The failures that were retained, in the order evaluation found them.
52	pub failures: Vec<AssertionFailure>,
53	/// How many assertions failed in total, which may exceed `failures.len()`.
54	pub total: usize,
55}
56
57impl fmt::Display for PopulateError {
58	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59		// No trailing newline: the caller owns how this is framed.
60		write!(f, "circuit not satisfied: {} assertion(s) failed", self.total)?;
61		for failure in &self.failures {
62			write!(f, "\n  {failure}")?;
63		}
64		let omitted = self.total.saturating_sub(self.failures.len());
65		if omitted > 0 {
66			write!(f, "\n  ... and {omitted} more, omitted")?;
67		}
68		Ok(())
69	}
70}
71
72/// A helper struct for filling witness values in a circuit.
73pub struct WitnessFiller<'a> {
74	pub(crate) circuit: &'a Circuit,
75	pub(crate) value_vec: ValueVec,
76}
77
78impl WitnessFiller<'_> {
79	/// Destruct the witness filler and extracts the underlying value vector.
80	pub fn into_value_vec(self) -> ValueVec {
81		self.value_vec
82	}
83
84	/// Returns a reference to the underlying value vector.
85	pub const fn value_vec(&self) -> &ValueVec {
86		&self.value_vec
87	}
88
89	/// Populates the given wires from bytes as little-endian packed 64-bit words.
90	///
91	/// If `bytes` is not a multiple of 8, the last word is zero-padded.
92	/// Any wires past those needed to hold `bytes` are filled with `Word::ZERO`.
93	///
94	/// # Panics
95	/// Panics if `bytes.len()` exceeds `wires.len() * 8`.
96	pub fn pack_bytes_le(&mut self, wires: &[Wire], bytes: &[u8]) {
97		let max_value_size = wires.len() * 8;
98		assert!(
99			bytes.len() <= max_value_size,
100			"bytes length {} exceeds maximum {}",
101			bytes.len(),
102			max_value_size
103		);
104
105		// Pack each 8-byte chunk into one little-endian word.
106		for (&wire, chunk) in std::iter::zip(wires, bytes.chunks(8)) {
107			let mut chunk_arr = [0u8; 8];
108			chunk_arr[..chunk.len()].copy_from_slice(chunk);
109			self[wire] = Word(u64::from_le_bytes(chunk_arr));
110		}
111
112		// Zero any wires the bytes did not reach.
113		for &wire in &wires[bytes.len().div_ceil(8)..] {
114			self[wire] = Word::ZERO;
115		}
116	}
117}
118
119impl Index<Wire> for WitnessFiller<'_> {
120	type Output = Word;
121
122	/// # Panics
123	///
124	/// Panics if the wire's storage is a pooled scratch slot shared with another value.
125	fn index(&self, wire: Wire) -> &Self::Output {
126		let index = self.circuit.witness_index(wire);
127		self.circuit.assert_not_pooled(wire, index);
128		&self.value_vec[index]
129	}
130}
131
132impl IndexMut<Wire> for WitnessFiller<'_> {
133	/// # Panics
134	///
135	/// Panics if the wire's storage is a pooled scratch slot shared with another value.
136	fn index_mut(&mut self, wire: Wire) -> &mut Self::Output {
137		let index = self.circuit.witness_index(wire);
138		self.circuit.assert_not_pooled(wire, index);
139		&mut self.value_vec[index]
140	}
141}
142
143/// Assigns witness input wires of one instance into a [`ValueTable`] working buffer.
144///
145/// Indexing by [`Wire`] targets that wire's row in the instance's column, mirroring the
146/// single-instance [`WitnessFiller`].
147///
148/// [`ValueTable`]: binius_core::ValueTable
149pub struct BatchWitnessFiller<'a, 'v> {
150	circuit: &'a Circuit,
151	values: &'a mut StridedArray2DViewMut<'v, Word>,
152	instance: usize,
153}
154
155impl<'a, 'v> BatchWitnessFiller<'a, 'v> {
156	/// A filler targeting one instance's column of a batch working buffer.
157	pub(crate) const fn new(
158		circuit: &'a Circuit,
159		values: &'a mut StridedArray2DViewMut<'v, Word>,
160		instance: usize,
161	) -> Self {
162		Self {
163			circuit,
164			values,
165			instance,
166		}
167	}
168}
169
170impl Index<Wire> for BatchWitnessFiller<'_, '_> {
171	type Output = Word;
172
173	fn index(&self, wire: Wire) -> &Self::Output {
174		&self.values[(self.circuit.witness_row(wire), self.instance)]
175	}
176}
177
178impl IndexMut<Wire> for BatchWitnessFiller<'_, '_> {
179	fn index_mut(&mut self, wire: Wire) -> &mut Self::Output {
180		let row = self.circuit.witness_row(wire);
181		&mut self.values[(row, self.instance)]
182	}
183}
184
185#[cfg(test)]
186mod tests {
187	use super::*;
188
189	fn failure(path: &str, detail: &str) -> AssertionFailure {
190		AssertionFailure {
191			path: path.to_string(),
192			detail: detail.to_string(),
193		}
194	}
195
196	#[test]
197	fn a_failure_at_the_root_renders_without_a_separator() {
198		// A root assertion has no path, so there is nothing to prefix and no stray colon.
199		assert_eq!(failure("", "Word(0x1) != Word(0x2)").to_string(), "Word(0x1) != Word(0x2)");
200	}
201
202	#[test]
203	fn a_nested_failure_renders_path_then_detail() {
204		// The path and the detail are stored apart; rendering is what joins them.
205		assert_eq!(
206			failure(".sha256.round", "Word(0x1) != 0").to_string(),
207			".sha256.round: Word(0x1) != 0"
208		);
209	}
210
211	#[test]
212	fn the_error_lists_every_retained_failure_and_never_ends_with_a_newline() {
213		// Invariant: a caller frames the message, so it must not arrive with its own line break.
214		let err = PopulateError {
215			failures: vec![failure(".a", "one"), failure(".b", "two")],
216			total: 2,
217		};
218		let rendered = err.to_string();
219		assert_eq!(rendered, "circuit not satisfied: 2 assertion(s) failed\n  .a: one\n  .b: two");
220		assert!(!rendered.ends_with('\n'));
221	}
222
223	#[test]
224	fn a_capped_error_reports_how_many_it_dropped() {
225		// `total` counts past the cap, so the difference is what the list does not show.
226		let err = PopulateError {
227			failures: vec![failure(".a", "one")],
228			total: 7,
229		};
230		assert_eq!(
231			err.to_string(),
232			"circuit not satisfied: 7 assertion(s) failed\n  .a: one\n  ... and 6 more, omitted"
233		);
234	}
235
236	#[test]
237	fn an_uncapped_error_reports_no_omissions() {
238		// Equal counts mean the cap was never reached, so no trailing note is added.
239		let err = PopulateError {
240			failures: vec![failure(".a", "one")],
241			total: 1,
242		};
243		assert_eq!(err.to_string(), "circuit not satisfied: 1 assertion(s) failed\n  .a: one");
244	}
245
246	#[test]
247	fn the_error_is_a_std_error() {
248		// The whole point of the type: it can cross an API boundary as a `dyn Error`.
249		let err = PopulateError {
250			failures: vec![failure(".a", "one")],
251			total: 1,
252		};
253		let boxed: Box<dyn std::error::Error> = Box::new(err);
254		assert!(boxed.to_string().starts_with("circuit not satisfied"));
255		assert!(boxed.source().is_none());
256	}
257}