Skip to main content

binius_frontend/artifact/
chip.rs

1// Copyright 2026 The Binius Developers
2
3//! Circuits composed out of chips, each generating the witness of one M4 chip.
4//!
5//! The witness itself is [`binius_core`]'s [`WitnessM4`], which is also where one is checked.
6
7use std::mem;
8
9use binius_compute::GlobalAllocator;
10use binius_core::{
11	ValueTable, Word, WordSource,
12	error::{ChipName, OperandFault},
13	eval_operand,
14	m4::{ChipCall, ConstraintSystemM4, EmbeddedConstraintSystem, WitnessM4},
15};
16use binius_utils::checked_arithmetics::log2_ceil_usize;
17
18use crate::{
19	Circuit, CircuitBuilder,
20	artifact::witness::{PopulateError, WitnessFiller},
21	eval_form::BatchPopulateError,
22	ir::{Wire, hints::Hint},
23};
24
25/// One chip of a [`CircuitM4`], as the circuit that generates its witness.
26///
27/// The chip's interface is its circuit's inout segment: a call site supplies those words
28/// positionally, and every value the chip holds beyond them is derived from them. An inout wire the
29/// call does not reach is filled with zero.
30///
31/// A wire promoted with [`CircuitBuilder::mark_inout`] serves
32/// as well as one declared with [`CircuitBuilder::add_inout`].
33/// Witness generation assigns every inout wire from the call data, then evaluation recomputes the
34/// promoted ones over it. Nothing checks that the two agree, and nothing has to: where they
35/// disagree the row stops matching the call site, which is what the chip call itself enforces.
36///
37/// That is what lets a caller pass a chip's output. The caller is populated whole before its calls
38/// are read, so it holds the output already — generally from a hint, whose correctness the chip
39/// call is what constrains.
40pub struct EmbeddedCircuit {
41	/// The circuit generating one instance of the chip.
42	pub circuit: Circuit,
43	/// The chips this one delegates subrelations to, one entry per call per instance.
44	pub chip_calls: Vec<ChipCall>,
45}
46
47/// A circuit composed of chips, as the circuits that generate their witnesses.
48///
49/// `main` is the entry point: it calls chips, but no chip ID names it, so nothing calls it. The
50/// chips have an ID equal to their index in `chips`.
51pub struct CircuitM4 {
52	/// The entry point, which runs once.
53	pub main: EmbeddedCircuit,
54	/// The chips, indexed by chip ID, each paired with its number of active instances.
55	///
56	/// A chip runs once per call that reaches it, and those instances are the active ones: only
57	/// they have their own chip calls enforced. The instances past them pad the count up to a
58	/// power of two.
59	///
60	/// The count is denormalized — it says what the call graph already says, and
61	/// [`Self::recompute_instances`] derives it, along with the instance each call claims.
62	/// [`Self::validate`] holds the two to each other.
63	pub chips: Vec<(EmbeddedCircuit, usize)>,
64}
65
66impl From<Circuit> for CircuitM4 {
67	/// Makes a circuit the whole system, as a main that calls no chips.
68	fn from(circuit: Circuit) -> Self {
69		Self {
70			main: EmbeddedCircuit {
71				circuit,
72				chip_calls: Vec::new(),
73			},
74			chips: Vec::new(),
75		}
76	}
77}
78
79impl CircuitM4 {
80	/// Checks that this system can be populated in one pass over the chips, in ID order.
81	///
82	/// Specifically checks that:
83	///
84	/// - every chip call names a chip of this system, passes no more operands than that chip has
85	///   inout values, and reads only committed words of its own caller;
86	/// - the chips are in topological order, each calling only chips with a higher ID, so every
87	///   caller of a chip is populated before the chip itself;
88	/// - each chip's declared active-instance count is the number of invocations that reach it, no
89	///   chip is left uncalled, and no count outgrows a `usize`;
90	/// - each call names the callee instance the call graph gives it.
91	///
92	/// A system that passes here lowers to one that passes
93	/// [`ConstraintSystemM4::validate`](binius_core::m4::ConstraintSystemM4::validate), which
94	/// requires the same ordering and additionally validates each chip's compiled constraint
95	/// system — the one thing here the compiler rather than this check keeps well-formed. Nothing
96	/// downstream therefore has to run the lowered check to know its preconditions hold.
97	pub fn validate(&self) -> Result<(), CircuitM4Error> {
98		let n_chips = self.chips.len();
99
100		self.validate_calls(None, &self.main)?;
101		for (chip_index, (chip, _)) in self.chips.iter().enumerate() {
102			self.validate_calls(Some(chip_index), chip)?;
103			for call in &chip.chip_calls {
104				if call.chip_id <= chip_index {
105					return Err(CircuitM4Error::CallOutOfOrder {
106						chip_index,
107						callee: call.chip_id,
108					});
109				}
110			}
111		}
112
113		// The invocations reaching each chip, counted the way [`Self::recompute_instances`] hands
114		// them out. Only main and lower-numbered chips call a chip, so a single pass in ID order
115		// sees every caller of chip `i` before it reads chip `i`'s own total.
116		let mut n_calls = vec![0usize; n_chips];
117		for (call_index, call) in self.main.chip_calls.iter().enumerate() {
118			Self::check_call_instance(None, call_index, call, &n_calls)?;
119			n_calls[call.chip_id] += 1;
120		}
121		for (chip_index, (chip, n_active)) in self.chips.iter().enumerate() {
122			if n_calls[chip_index] != *n_active {
123				return Err(CircuitM4Error::WrongActiveInstanceCount {
124					chip_index,
125					declared: *n_active,
126					actual: n_calls[chip_index],
127				});
128			}
129			if *n_active == 0 {
130				return Err(CircuitM4Error::NeverCalled { chip_index });
131			}
132
133			// Only the active instances of this chip have their calls enforced, so only they
134			// demand an instance of the callee.
135			//
136			// The counts multiply down the call graph — a chain whose every chip calls the next
137			// twice reaches `2^depth` — so a system of a few dozen chips can outgrow a `usize`.
138			// Counting it out unchecked would wrap to a small total that then agrees with a
139			// `recompute_instances` that wrapped the same way, and the system would go on to be
140			// populated against a count that is not the number of calls.
141			for (call_index, call) in chip.chip_calls.iter().enumerate() {
142				Self::check_call_instance(Some(chip_index), call_index, call, &n_calls)?;
143				n_calls[call.chip_id] = n_calls[call.chip_id].checked_add(*n_active).ok_or(
144					CircuitM4Error::TooManyInstances {
145						chip_index: call.chip_id,
146					},
147				)?;
148			}
149		}
150
151		Ok(())
152	}
153
154	/// Checks that one call names the callee instance the invocations counted so far leave it.
155	const fn check_call_instance(
156		chip_index: Option<usize>,
157		call_index: usize,
158		call: &ChipCall,
159		n_calls: &[usize],
160	) -> Result<(), CircuitM4Error> {
161		let expected = n_calls[call.chip_id];
162		if call.first_instance != expected {
163			return Err(CircuitM4Error::WrongCallInstance {
164				chip_index,
165				call_index,
166				first_instance: call.first_instance,
167				expected,
168			});
169		}
170		Ok(())
171	}
172
173	/// Checks one caller's calls: that each names a chip of this system, passes no more operands
174	/// than that chip has inout values, and reads only committed words of the caller's own value
175	/// vector.
176	///
177	/// A call may pass fewer operands than the callee takes: the inout values past them are
178	/// constrained to zero.
179	fn validate_calls(
180		&self,
181		chip_index: Option<usize>,
182		caller: &EmbeddedCircuit,
183	) -> Result<(), CircuitM4Error> {
184		let n_chips = self.chips.len();
185		let cs = caller.circuit.constraint_system();
186		for (call_index, call) in caller.chip_calls.iter().enumerate() {
187			if call.chip_id >= n_chips {
188				return Err(CircuitM4Error::OutOfRangeChipId {
189					chip_index,
190					chip_id: call.chip_id,
191					n_chips,
192				});
193			}
194			let n_inout = self.chips[call.chip_id]
195				.0
196				.circuit
197				.constraint_system()
198				.n_inout;
199			if call.inout.len() > n_inout {
200				return Err(CircuitM4Error::WrongCallArity {
201					chip_index,
202					call_index,
203					chip_id: call.chip_id,
204					arity: call.inout.len(),
205					n_inout,
206				});
207			}
208			for (operand_index, operand) in call.inout.iter().enumerate() {
209				if let Some(source) = cs.operand_fault(operand) {
210					return Err(CircuitM4Error::CallOperand {
211						chip_index,
212						call_index,
213						operand_index,
214						source,
215					});
216				}
217			}
218		}
219		Ok(())
220	}
221
222	/// Gives each call the callee instances it invokes, and each chip the count of the ones it is
223	/// left with.
224	///
225	/// A chip is invoked once per call site naming it, per active instance of the caller, and a
226	/// call site's invocations are consecutive instances of the callee: the caller's instance `i`
227	/// invokes the call's [`first_instance`](ChipCall::first_instance) plus `i`. Main runs once, so
228	/// each of its call sites claims a single instance.
229	///
230	/// This is what [`Self::validate`] checks the declared counts and instances against, so a
231	/// system whose call sites have just been written or rewritten passes it here rather than
232	/// counting by hand.
233	///
234	/// A count that outgrows a `usize` saturates rather than wrapping, so it stays too large for
235	/// the invocations that reach the chip and [`Self::validate`] reports it.
236	///
237	/// # Panics
238	///
239	/// Panics if a chip call names a chip this system does not have. Chips out of topological
240	/// order are not detected here: a call to a lower ID counts against a total already written
241	/// back, and [`Self::validate`] is what rejects the result.
242	pub fn recompute_instances(&mut self) {
243		let mut n_calls = vec![0usize; self.chips.len()];
244		for call in &mut self.main.chip_calls {
245			call.first_instance = n_calls[call.chip_id];
246			n_calls[call.chip_id] += 1;
247		}
248		// Only main and lower-numbered chips call a chip, so one pass in ID order settles chip
249		// `i`'s own total before reading it.
250		for chip_index in 0..self.chips.len() {
251			let n_active = n_calls[chip_index];
252			self.chips[chip_index].1 = n_active;
253
254			// Only the active instances of this chip have their calls enforced, so only they
255			// demand an instance of the callee.
256			for call in &mut self.chips[chip_index].0.chip_calls {
257				call.first_instance = n_calls[call.chip_id];
258				n_calls[call.chip_id] = n_calls[call.chip_id].saturating_add(n_active);
259			}
260		}
261	}
262
263	/// Lowers this system to the constraint-system form the proving protocol consumes.
264	///
265	/// Each circuit contributes its compiled constraint system; the chip calls and the
266	/// active-instance counts carry over unchanged.
267	pub fn to_constraint_system(&self) -> ConstraintSystemM4 {
268		let lower = |chip: &EmbeddedCircuit| EmbeddedConstraintSystem {
269			cs: chip.circuit.constraint_system().clone(),
270			chip_calls: chip.chip_calls.clone(),
271		};
272		ConstraintSystemM4 {
273			main: lower(&self.main),
274			chips: self
275				.chips
276				.iter()
277				.map(|(chip, n_active)| (lower(chip), *n_active))
278				.collect(),
279		}
280	}
281
282	/// Generates the witness for a whole system from the main circuit's inputs.
283	///
284	/// `fill_main` assigns the witness inputs of the main circuit; every other value in the system
285	/// is derived from them. Each chip's table holds one instance per invocation that reaches it,
286	/// each at the row the invoking call names. The instance count is rounded up to a power of two
287	/// by repeating the last invocation, which satisfies the chip because the invocation it copies
288	/// does.
289	///
290	/// # Panics
291	///
292	/// Panics if the system does not pass [`Self::validate`], which covers both the ordering this
293	/// walks the chips in and the well-formedness of the operands it evaluates.
294	pub fn generate_witness<F>(&self, fill_main: F) -> Result<WitnessM4, PopulateM4Error>
295	where
296		F: FnOnce(&mut WitnessFiller<'_>),
297	{
298		let mut main_witness_filler = self.main.circuit.new_witness_filler();
299		fill_main(&mut main_witness_filler);
300		self.main
301			.circuit
302			.populate_wire_witness(&mut main_witness_filler)?;
303
304		let main_values = main_witness_filler.into_value_vec();
305
306		// The instances of each chip, each written by the call that invokes it. Calls only run to
307		// higher IDs, so a chip's instances are all written by the time the pass below reaches it.
308		let mut pending = self
309			.chips
310			.iter()
311			.map(|(_, n_active)| vec![Vec::new(); *n_active])
312			.collect::<Vec<_>>();
313		for call in &self.main.chip_calls {
314			pending[call.chip_id][call.first_instance] = eval_call(&main_values, call);
315		}
316
317		let mut tables = Vec::<ValueTable>::with_capacity(self.chips.len());
318		for (chip_id, (chip, n_active)) in self.chips.iter().enumerate() {
319			let call_data = mem::take(&mut pending[chip_id]);
320
321			// Invariant checked in `CircuitM4::validate()`
322			assert!(*n_active > 0, "chip {chip_id} is never called");
323
324			let log_instances = log2_ceil_usize(call_data.len());
325			let table = chip
326				.circuit
327				.populate_batch_parallel(&GlobalAllocator, log_instances, |instance, filler| {
328					// Instances past the last invocation repeat it.
329					let inout = &call_data[instance.min(call_data.len() - 1)];
330					for (i, &wire) in chip.circuit.inout().iter().enumerate() {
331						filler[wire] = inout.get(i).copied().unwrap_or(Word::ZERO);
332					}
333				})
334				.map_err(|source| PopulateM4Error::Chip { chip_id, source })?;
335
336			// Read this chip's own calls off each active instance, for the chips after it to serve.
337			// `instance_words` reads each call's operands straight off the table's strided rows.
338			if !chip.chip_calls.is_empty() {
339				let constants = &chip.circuit.constraint_system().constants;
340				for instance in 0..*n_active {
341					let values = table.instance_words(instance, constants);
342					for call in &chip.chip_calls {
343						pending[call.chip_id][call.first_instance + instance] =
344							eval_call(&values, call);
345					}
346				}
347			}
348
349			tables.push(table);
350		}
351
352		Ok(WitnessM4 {
353			main: main_values,
354			tables,
355		})
356	}
357}
358
359/// Evaluates the inout operands of one chip call against the caller's values.
360fn eval_call(values: &impl WordSource, call: &ChipCall) -> Vec<Word> {
361	call.inout
362		.iter()
363		.map(|operand| eval_operand(values, operand))
364		.collect()
365}
366
367/// Reason the witness of an M4 circuit could not be generated.
368#[allow(missing_docs)] // errors are self-documenting
369#[derive(Debug, thiserror::Error)]
370pub enum PopulateM4Error {
371	#[error("the main circuit is not satisfied: {0}")]
372	Main(#[from] PopulateError),
373	#[error("chip #{chip_id} is not satisfied: {source}")]
374	Chip {
375		chip_id: usize,
376		#[source]
377		source: BatchPopulateError,
378	},
379}
380
381/// A chip of the system a [`CircuitBuilder`] is building.
382///
383/// [`CircuitBuilder::add_chip`] returns one for each chip it
384/// registers, and a call site names its callee by it. Registering further chips never moves a chip
385/// already registered, so a reference stays good for the rest of the build.
386#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
387pub struct ChipRef(usize);
388
389impl ChipRef {
390	/// Names the chip at the given index of [`CircuitM4::chips`].
391	pub(crate) const fn new(chip_id: usize) -> Self {
392		Self(chip_id)
393	}
394
395	/// Returns the chip's index in [`CircuitM4::chips`], which is what a [`ChipCall`] names it by.
396	///
397	/// Reading the index out is the only direction: a reference cannot be made from one, so every
398	/// reference names a chip that was registered.
399	pub const fn chip_id(self) -> usize {
400		self.0
401	}
402}
403
404/// A gadget the builder emits either as inline gates or as a call to a chip.
405///
406/// The gadget is written once, in [`build`](Self::build), and where it lands is the building
407/// circuit's to decide: [`CircuitBuilder::build_gadget`]
408/// emits the gates unless
409/// [`CircuitBuilder::register_chip`] has made the gadget a
410/// chip, in which case it emits a hint and a call constraining it.
411///
412/// The [`Hint`] half is what the chip path needs of a gadget beyond its gates. Its `NAME` and
413/// `dimensions` name which gadget a registered chip serves; [`shape`](Hint::shape) gives the arity
414/// of the gates and of the chip's interface alike; and [`execute`](Hint::execute) computes the
415/// outputs a call passes alongside its inputs.
416///
417/// So a `Hint::execute` and a `build` of the same gadget must agree on every input the circuit can
418/// reach them with. Where they disagree, the chip instance recomputes a word the call did not name,
419/// and only [`WitnessM4::verify`](binius_core::m4::WitnessM4::verify) reports it.
420///
421/// ```
422/// use binius_core::word::Word;
423/// use binius_frontend::{ChipGadget, CircuitBuilder, Hint, Wire};
424///
425/// /// The bitwise conjunction of two words.
426/// struct And;
427///
428/// impl Hint for And {
429///     const NAME: &'static str = "doc.and";
430///
431///     fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
432///         (2, 1)
433///     }
434///
435///     fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
436///         outputs[0] = Word(inputs[0].as_u64() & inputs[1].as_u64());
437///     }
438/// }
439///
440/// impl ChipGadget for And {
441///     fn build(&self, builder: &CircuitBuilder, _dims: &[usize], inputs: &[Wire]) -> Vec<Wire> {
442///         vec![builder.band(inputs[0], inputs[1])]
443///     }
444/// }
445///
446/// // Without a chip the gadget is its gates, and the circuit builds as any other.
447/// let builder = CircuitBuilder::new();
448/// let (a, b) = (builder.add_inout(), builder.add_inout());
449/// builder.build_gadget(And, &[], &[a, b]);
450/// builder.build();
451///
452/// // Registering the gadget is the whole of the opt-in: the same call is now a chip call.
453/// let builder = CircuitBuilder::new();
454/// builder.register_chip(And, &[]);
455/// let (a, b) = (builder.add_inout(), builder.add_inout());
456/// builder.build_gadget(And, &[], &[a, b]);
457/// builder.build_m4().validate().unwrap();
458/// ```
459pub trait ChipGadget: Hint {
460	/// Emits the gadget's gates, returning the outputs its
461	/// [`shape`](Hint::shape) declares.
462	///
463	/// Each returned wire must be gate-created: a chip promotes them with
464	/// [`CircuitBuilder::mark_inout`], which takes no other
465	/// kind. Returning an input or a constant unchanged is what that rules out.
466	fn build(&self, builder: &CircuitBuilder, dimensions: &[usize], inputs: &[Wire]) -> Vec<Wire>;
467}
468
469/// Reason an M4 circuit cannot be populated as it stands.
470#[allow(missing_docs)] // errors are self-documenting
471#[derive(Debug, thiserror::Error)]
472pub enum CircuitM4Error {
473	#[error("{} calls chip {chip_id}, but the system has {n_chips} chips", ChipName(*chip_index))]
474	OutOfRangeChipId {
475		chip_index: Option<usize>,
476		chip_id: usize,
477		n_chips: usize,
478	},
479	#[error(
480		"{}'s call #{call_index} passes {arity} operands to chip {chip_id}, which has {n_inout} inout values",
481		ChipName(*chip_index)
482	)]
483	WrongCallArity {
484		chip_index: Option<usize>,
485		call_index: usize,
486		chip_id: usize,
487		arity: usize,
488		n_inout: usize,
489	},
490	#[error(
491		"{}'s call #{call_index} has a malformed operand #{operand_index}: {source}",
492		ChipName(*chip_index)
493	)]
494	CallOperand {
495		chip_index: Option<usize>,
496		call_index: usize,
497		operand_index: usize,
498		#[source]
499		source: OperandFault,
500	},
501	#[error("chip #{chip_index} calls chip {callee}, which is not a later chip")]
502	CallOutOfOrder { chip_index: usize, callee: usize },
503	#[error(
504		"{}'s call #{call_index} names instance {first_instance}, but the call graph gives it {expected}",
505		ChipName(*chip_index)
506	)]
507	WrongCallInstance {
508		chip_index: Option<usize>,
509		call_index: usize,
510		first_instance: usize,
511		expected: usize,
512	},
513	#[error("chip #{chip_index} declares {declared} active instances, but {actual} calls reach it")]
514	WrongActiveInstanceCount {
515		chip_index: usize,
516		declared: usize,
517		actual: usize,
518	},
519	#[error("chip #{chip_index} is never called")]
520	NeverCalled { chip_index: usize },
521	#[error("more invocations reach chip #{chip_index} than a usize can count")]
522	TooManyInstances { chip_index: usize },
523}
524
525#[cfg(test)]
526mod tests {
527	use std::iter;
528
529	use binius_core::{ShiftedValueIndex, ValueIndex, VerificationM4Error, error::OperandFault};
530
531	use super::*;
532	use crate::{Circuit, CircuitBuilder, CircuitM4Error, EmbeddedCircuit, Wire};
533
534	/// A chip whose inout words are `(a, b, c)`, constrained by `c == a & b`.
535	///
536	/// It calls chip `callee` once per instance, forwarding `(c, c)`.
537	fn and_chip(callee: usize) -> EmbeddedCircuit {
538		let builder = CircuitBuilder::new();
539		let (a, b, c) = (builder.add_inout(), builder.add_inout(), builder.add_inout());
540		builder.assert_eq("and", builder.band(a, b), c);
541		let circuit = builder.build();
542
543		let forward_c = operand(&circuit, c);
544		EmbeddedCircuit {
545			chip_calls: vec![ChipCall {
546				chip_id: callee,
547				first_instance: 0,
548				inout: vec![forward_c.clone(), forward_c],
549			}],
550			circuit,
551		}
552	}
553
554	/// A chip whose inout words are `(a, b, c)`, constrained by `c == a & b`.
555	///
556	/// It calls chip `callee` twice per instance, forwarding `(a, a)` and then `(b, b)`.
557	fn twice_calling_and_chip(callee: usize) -> EmbeddedCircuit {
558		let builder = CircuitBuilder::new();
559		let (a, b, c) = (builder.add_inout(), builder.add_inout(), builder.add_inout());
560		builder.assert_eq("and", builder.band(a, b), c);
561		let circuit = builder.build();
562
563		let call = |wire| ChipCall {
564			chip_id: callee,
565			first_instance: 0,
566			inout: vec![operand(&circuit, wire), operand(&circuit, wire)],
567		};
568		let chip_calls = vec![call(a), call(b)];
569		EmbeddedCircuit {
570			circuit,
571			chip_calls,
572		}
573	}
574
575	/// A leaf chip whose inout words are `(a, b, a & b)`, the conjunction promoted rather than
576	/// declared and asserted against.
577	fn promoting_and_chip() -> EmbeddedCircuit {
578		let builder = CircuitBuilder::new();
579		let (a, b) = (builder.add_inout(), builder.add_inout());
580		builder.mark_inout(builder.band(a, b));
581		EmbeddedCircuit {
582			circuit: builder.build(),
583			chip_calls: vec![],
584		}
585	}
586
587	/// A leaf chip whose two inout words must be equal.
588	fn eq_chip() -> EmbeddedCircuit {
589		let builder = CircuitBuilder::new();
590		let (x, y) = (builder.add_inout(), builder.add_inout());
591		builder.assert_eq("eq", x, y);
592		EmbeddedCircuit {
593			circuit: builder.build(),
594			chip_calls: vec![],
595		}
596	}
597
598	/// The operand reading a single wire of a circuit's value vector.
599	fn operand(circuit: &Circuit, wire: Wire) -> Vec<ShiftedValueIndex> {
600		vec![ShiftedValueIndex::plain(circuit.witness_index(wire))]
601	}
602
603	/// A main circuit passing `n_calls` triples of its own witness wires to chip 0.
604	///
605	/// Triple `i` is `(a_i, b_i, a_i & b_i)`, so every call satisfies the chip it reaches.
606	fn main_circuit(n_calls: usize) -> (EmbeddedCircuit, Vec<(Wire, Wire)>) {
607		let builder = CircuitBuilder::new();
608		let inputs = (0..n_calls)
609			.map(|_| (builder.add_witness(), builder.add_witness()))
610			.collect::<Vec<_>>();
611		let conjunctions = inputs
612			.iter()
613			.map(|&(a, b)| {
614				let and = builder.band(a, b);
615				builder.mark_inout(and);
616				and
617			})
618			.collect::<Vec<_>>();
619		let circuit = builder.build();
620
621		let chip_calls = iter::zip(&inputs, &conjunctions)
622			.map(|(&(a, b), &and)| ChipCall {
623				chip_id: 0,
624				first_instance: 0,
625				inout: vec![
626					operand(&circuit, a),
627					operand(&circuit, b),
628					operand(&circuit, and),
629				],
630			})
631			.collect();
632
633		let main = EmbeddedCircuit {
634			circuit,
635			chip_calls,
636		};
637		(main, inputs)
638	}
639
640	/// A system whose main circuit calls chip 0 `n_calls` times, and whose chip 0 calls chip 1
641	/// once per instance.
642	fn system(n_calls: usize) -> (CircuitM4, Vec<(Wire, Wire)>) {
643		let (main, inputs) = main_circuit(n_calls);
644		let mut circuit = CircuitM4 {
645			main,
646			chips: vec![(and_chip(1), 0), (eq_chip(), 0)],
647		};
648		circuit.recompute_instances();
649		(circuit, inputs)
650	}
651
652	/// The inout words of one instance of a chip, read back off its table.
653	fn instance_inout(chip: &EmbeddedCircuit, table: &ValueTable, instance: usize) -> Vec<u64> {
654		let constants = &chip.circuit.constraint_system().constants;
655		let values = table.instance_value_vec(instance, constants);
656		chip.circuit
657			.inout()
658			.iter()
659			.map(|&wire| values[chip.circuit.witness_index(wire)].as_u64())
660			.collect()
661	}
662
663	// The whole path with nothing hand-assembled: a chip built by one builder, registered and
664	// called by another, and a witness generated off the result.
665	#[test]
666	fn generate_serves_the_calls_a_builder_emitted() {
667		// The chip constrains its third inout word to be the conjunction of the first two.
668		let chip = CircuitBuilder::new();
669		let (x, y, z) = (chip.add_inout(), chip.add_inout(), chip.add_inout());
670		chip.assert_eq("and", chip.band(x, y), z);
671
672		// Main delegates two conjunctions to it, passing each result alongside its operands.
673		let builder = CircuitBuilder::new();
674		let chip_ref = builder.add_chip(CircuitM4::from(chip.build()));
675		let inputs = (0..2)
676			.map(|_| (builder.add_witness(), builder.add_witness()))
677			.collect::<Vec<_>>();
678		for &(a, b) in &inputs {
679			builder.call_chip(chip_ref, &[a, b, builder.band(a, b)]);
680		}
681		let circuit = builder.build_m4();
682
683		assert_eq!(circuit.chips[0].1, 2);
684		circuit.validate().unwrap();
685
686		let words = [(0b1100u64, 0b1010u64), (0xff00, 0x0ff0)];
687		let witness = circuit
688			.generate_witness(|filler| {
689				for (&(a, b), &(a_word, b_word)) in iter::zip(&inputs, &words) {
690					filler[a] = Word(a_word);
691					filler[b] = Word(b_word);
692				}
693			})
694			.unwrap();
695
696		assert_eq!(witness.tables[0].n_instances(), 2);
697		for (instance, &(a, b)) in words.iter().enumerate() {
698			assert_eq!(
699				instance_inout(&circuit.chips[0].0, &witness.tables[0], instance),
700				vec![a, b, a & b]
701			);
702		}
703	}
704
705	#[test]
706	fn generate_fills_one_instance_per_call() {
707		let (circuit, inputs) = system(2);
708		circuit.validate().unwrap();
709
710		let words = [(0b1100u64, 0b1010u64), (0xff00, 0x0ff0)];
711		let witness = circuit
712			.generate_witness(|filler| {
713				for (&(a, b), &(a_word, b_word)) in iter::zip(&inputs, &words) {
714					filler[a] = Word(a_word);
715					filler[b] = Word(b_word);
716				}
717			})
718			.unwrap();
719
720		// Chip 0 serves main's two calls, in call order.
721		let (chip_0, chip_1) = (&circuit.chips[0].0, &circuit.chips[1].0);
722		assert_eq!(witness.tables[0].n_instances(), 2);
723		for (instance, &(a, b)) in words.iter().enumerate() {
724			assert_eq!(instance_inout(chip_0, &witness.tables[0], instance), vec![a, b, a & b]);
725		}
726
727		// Chip 1 serves chip 0's call from each of those instances, which forwards `(c, c)`.
728		assert_eq!(witness.tables[1].n_instances(), 2);
729		for (instance, &(a, b)) in words.iter().enumerate() {
730			assert_eq!(instance_inout(chip_1, &witness.tables[1], instance), vec![a & b, a & b]);
731		}
732
733		witness.verify(&circuit.to_constraint_system()).unwrap();
734	}
735
736	// A chip may promote an inout word instead of declaring it, which is what lets a chip return a
737	// result. Generation assigns every inout wire from the call data and evaluation then recomputes
738	// the promoted ones, so a row holds the chip's own word whatever the call passed. Nothing here
739	// checks the two agree; where they do not, the row stops matching the call site.
740	#[test]
741	fn generate_recomputes_a_promoted_inout_word() {
742		let (main, inputs) = main_circuit(1);
743		let mut circuit = CircuitM4 {
744			main,
745			chips: vec![(promoting_and_chip(), 1)],
746		};
747		circuit.validate().unwrap();
748
749		let (a, b) = inputs[0];
750		let fill = |filler: &mut WitnessFiller<'_>| {
751			filler[a] = Word(0b1100);
752			filler[b] = Word(0b1010);
753		};
754		let row = |circuit: &CircuitM4, witness: &WitnessM4| {
755			instance_inout(&circuit.chips[0].0, &witness.tables[0], 0)
756		};
757
758		let witness = circuit.generate_witness(fill).unwrap();
759		assert_eq!(row(&circuit, &witness), vec![0b1100, 0b1010, 0b1000]);
760		witness.verify(&circuit.to_constraint_system()).unwrap();
761
762		// Pass `a` as the third word, which the chip's conjunction disagrees with. Generation still
763		// succeeds, and the row still holds the conjunction rather than what the call passed — so
764		// the call no longer matches its instance, which is exactly what verification rejects.
765		circuit.main.chip_calls[0].inout[2] = operand(&circuit.main.circuit, a);
766		let witness = circuit.generate_witness(fill).unwrap();
767		assert_eq!(row(&circuit, &witness), vec![0b1100, 0b1010, 0b1000]);
768		let err = witness.verify(&circuit.to_constraint_system()).unwrap_err();
769		assert!(
770			matches!(
771				err,
772				VerificationM4Error::CallMismatch {
773					chip_id: 0,
774					row: 0,
775					caller: None,
776					word: 2,
777					..
778				}
779			),
780			"{err}"
781		);
782	}
783
784	// A caller with several instances making several calls to one callee is what tells the two
785	// apart: each call site claims a contiguous block of the callee's instances, one per instance
786	// of the caller, rather than the caller's instances taking consecutive rows. With one call per
787	// callee the two coincide and nothing would notice generation and verification disagreeing.
788	#[test]
789	fn generate_gives_each_call_site_a_contiguous_block_of_instances() {
790		let (main, inputs) = main_circuit(2);
791		let mut circuit = CircuitM4 {
792			main,
793			chips: vec![(twice_calling_and_chip(1), 0), (eq_chip(), 0)],
794		};
795		circuit.recompute_instances();
796		circuit.validate().unwrap();
797
798		// Chip 0 serves main's two calls, and each of its instances calls chip 1 twice.
799		assert_eq!(circuit.chips[0].1, 2);
800		assert_eq!(circuit.chips[1].1, 4);
801
802		let words = [(0b1100u64, 0b1010u64), (0xff00, 0x0ff0)];
803		let witness = circuit
804			.generate_witness(|filler| {
805				for (&(a, b), &(a_word, b_word)) in iter::zip(&inputs, &words) {
806					filler[a] = Word(a_word);
807					filler[b] = Word(b_word);
808				}
809			})
810			.unwrap();
811
812		// The first call site forwards `(a, a)` from each of chip 0's two instances, and the second
813		// forwards `(b, b)` from each, so the two blocks are `a`s then `b`s.
814		let expected = [words[0].0, words[1].0, words[0].1, words[1].1];
815		assert_eq!(witness.tables[1].n_instances(), 4);
816		for (instance, &word) in expected.iter().enumerate() {
817			assert_eq!(
818				instance_inout(&circuit.chips[1].0, &witness.tables[1], instance),
819				vec![word, word]
820			);
821		}
822
823		witness.verify(&circuit.to_constraint_system()).unwrap();
824	}
825
826	#[test]
827	fn generate_pads_the_instance_count_by_repeating_the_last_call() {
828		let (circuit, inputs) = system(3);
829		circuit.validate().unwrap();
830
831		let words = [(0b1100u64, 0b1010u64), (0xff00, 0x0ff0), (0xabcd, 0xdcba)];
832		let witness = circuit
833			.generate_witness(|filler| {
834				for (&(a, b), &(a_word, b_word)) in iter::zip(&inputs, &words) {
835					filler[a] = Word(a_word);
836					filler[b] = Word(b_word);
837				}
838			})
839			.unwrap();
840
841		// Three calls round up to four instances, the fourth repeating the third.
842		let chip_0 = &circuit.chips[0].0;
843		assert_eq!(witness.tables[0].n_instances(), 4);
844		let (a, b) = words[2];
845		assert_eq!(instance_inout(chip_0, &witness.tables[0], 3), vec![a, b, a & b]);
846
847		// The padding instances pass verification too: their local constraints hold, and no call
848		// claims them.
849		witness.verify(&circuit.to_constraint_system()).unwrap();
850	}
851
852	#[test]
853	fn generate_reports_the_chip_whose_calls_do_not_satisfy_it() {
854		let (mut circuit, inputs) = system(1);
855
856		// Pass `a` where chip 0 expects `a & b`, so no witness can serve the call.
857		let (a, b) = inputs[0];
858		circuit.main.chip_calls[0].inout[2] = operand(&circuit.main.circuit, a);
859
860		let err = circuit
861			.generate_witness(|filler| {
862				filler[a] = Word(0b1100);
863				filler[b] = Word(0b1010);
864			})
865			.unwrap_err();
866		assert!(matches!(err, PopulateM4Error::Chip { chip_id: 0, .. }), "{err}");
867	}
868
869	#[test]
870	fn validate_rejects_a_call_to_a_chip_that_does_not_exist() {
871		let (mut circuit, _) = system(1);
872		circuit.main.chip_calls[0].chip_id = 2;
873		assert!(matches!(
874			circuit.validate(),
875			Err(CircuitM4Error::OutOfRangeChipId {
876				chip_index: None,
877				chip_id: 2,
878				n_chips: 2,
879			})
880		));
881	}
882
883	#[test]
884	fn validate_rejects_chips_out_of_topological_order() {
885		let (mut circuit, _) = system(1);
886		// Chip 1 is a leaf; make it call chip 0, which is populated before it.
887		circuit.chips[1].0.chip_calls.push(ChipCall {
888			chip_id: 0,
889			first_instance: 0,
890			inout: vec![],
891		});
892		assert!(matches!(
893			circuit.validate(),
894			Err(CircuitM4Error::CallOutOfOrder {
895				chip_index: 1,
896				callee: 0,
897			})
898		));
899	}
900
901	#[test]
902	fn validate_rejects_a_wrong_active_instance_count() {
903		let (mut circuit, _) = system(2);
904		circuit.chips[1].1 = 1;
905		assert!(matches!(
906			circuit.validate(),
907			Err(CircuitM4Error::WrongActiveInstanceCount {
908				chip_index: 1,
909				declared: 1,
910				actual: 2,
911			})
912		));
913	}
914
915	// A call site claims one instance of its callee per instance of its caller, so the instance it
916	// names is the one the calls before it leave free. A graph edited without recomputing the
917	// instances leaves a call naming a row another one already claims.
918	#[test]
919	fn validate_rejects_a_call_naming_the_wrong_instance() {
920		let (mut circuit, _) = system(2);
921		circuit.main.chip_calls[1].first_instance = 0;
922		assert!(matches!(
923			circuit.validate(),
924			Err(CircuitM4Error::WrongCallInstance {
925				chip_index: None,
926				call_index: 1,
927				first_instance: 0,
928				expected: 1,
929			})
930		));
931	}
932
933	#[test]
934	fn validate_rejects_a_chip_nothing_calls() {
935		let (mut circuit, _) = system(1);
936		circuit.main.chip_calls.clear();
937		circuit.recompute_instances();
938		assert!(matches!(circuit.validate(), Err(CircuitM4Error::NeverCalled { chip_index: 0 })));
939	}
940
941	// An operand past the callee's interface has nowhere to land: generation drops it and
942	// verification never looks at it, so nothing downstream would report it.
943	#[test]
944	fn validate_rejects_a_call_passing_more_operands_than_the_callee_takes() {
945		let (mut circuit, inputs) = system(1);
946		let extra = operand(&circuit.main.circuit, inputs[0].0);
947		circuit.main.chip_calls[0].inout.push(extra);
948		assert!(matches!(
949			circuit.validate(),
950			Err(CircuitM4Error::WrongCallArity {
951				chip_index: None,
952				call_index: 0,
953				chip_id: 0,
954				arity: 4,
955				n_inout: 3,
956			})
957		));
958	}
959
960	// Scratch words are uncommitted temporaries, so a call reading one names a word no instance
961	// holds. `call_chip` pins its wires out of scratch, but `ChipCall` is built by hand too.
962	#[test]
963	fn validate_rejects_a_call_operand_naming_a_scratch_value() {
964		let (mut circuit, _) = system(1);
965		circuit.main.chip_calls[0].inout[2] =
966			vec![ShiftedValueIndex::plain(ValueIndex::scratch(0))];
967		assert!(matches!(
968			circuit.validate(),
969			Err(CircuitM4Error::CallOperand {
970				chip_index: None,
971				call_index: 0,
972				operand_index: 2,
973				source: OperandFault::ScratchValueIndex,
974			})
975		));
976	}
977
978	// Instance counts multiply down the call graph, so a chain of a few dozen chips outgrows a
979	// `usize`. Counting it out unchecked would wrap to a plausible-looking total.
980	#[test]
981	fn validate_rejects_an_instance_count_that_outgrows_a_usize() {
982		// A chain of 70 chips, each calling the next twice, so chip `i` is reached 2^i times.
983		let chip = |callee: Option<usize>| {
984			let builder = CircuitBuilder::new();
985			builder.add_inout();
986			EmbeddedCircuit {
987				circuit: builder.build(),
988				chip_calls: callee
989					.into_iter()
990					.flat_map(|chip_id| {
991						iter::repeat_with(move || ChipCall {
992							chip_id,
993							first_instance: 0,
994							inout: vec![],
995						})
996						.take(2)
997					})
998					.collect(),
999			}
1000		};
1001
1002		const DEPTH: usize = 70;
1003		let mut circuit = CircuitM4 {
1004			main: EmbeddedCircuit {
1005				circuit: CircuitBuilder::new().build(),
1006				chip_calls: vec![ChipCall {
1007					chip_id: 0,
1008					first_instance: 0,
1009					inout: vec![],
1010				}],
1011			},
1012			chips: (0..DEPTH)
1013				.map(|i| (chip((i + 1 < DEPTH).then_some(i + 1)), 0))
1014				.collect(),
1015		};
1016		circuit.recompute_instances();
1017
1018		// Chip 63 is reached 2^63 times and calls chip 64 twice, which is where the count leaves
1019		// the range.
1020		assert!(matches!(
1021			circuit.validate(),
1022			Err(CircuitM4Error::TooManyInstances { chip_index: 64 })
1023		));
1024	}
1025}