Skip to main content

binius_frontend/builder/
mod.rs

1// Copyright 2025-2026 The Binius Developers
2// Copyright 2025 Irreducible Inc.
3use std::{
4	cell::{RefCell, RefMut},
5	collections::HashMap,
6	iter, mem,
7	rc::Rc,
8};
9
10use binius_core::{
11	constraint_system::{ConstraintSystem, ShiftVariant, ShiftedValueIndex, ValueSegment},
12	m4::ChipCall,
13	word::Word,
14};
15use cranelift_entity::EntitySet;
16use itertools::chain;
17
18use crate::{
19	artifact::{
20		chip::{ChipGadget, ChipRef, CircuitM4, EmbeddedCircuit},
21		circuit::Circuit,
22	},
23	eval_form::{self, BytecodeBuilder},
24	gates::Opcode,
25	ir::{
26		GateGraph, Wire, WireKind,
27		hints::{Hint, HintRegistry},
28		path::PathSpec,
29	},
30	lower::ConstraintBuilder,
31	pass::{
32		AlwaysFailingGateError, BuiltGates, const_prop, cse, dce, fusion,
33		layout::{
34			scratch_alloc::{ScratchAlloc, ScratchPolicy},
35			value_vec_alloc,
36		},
37		zero_fold,
38	},
39};
40
41mod gadget;
42#[cfg(test)]
43mod tests;
44
45use gadget::smul64;
46
47/// Which compiler passes run.
48///
49/// This is the only knob: a circuit compiles the same way whatever the process environment holds.
50/// A caller that wants a non-default pass set builds through [`CircuitBuilder::with_opts`],
51/// overriding the fields it cares about:
52///
53/// ```
54/// use binius_frontend::{CircuitBuilder, Options};
55///
56/// let mut opts = Options::default();
57/// opts.enable_gate_fusion = false;
58/// let builder = CircuitBuilder::with_opts(opts);
59/// ```
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61#[non_exhaustive]
62pub struct Options {
63	/// Inline linear definitions into the non-linear gates that consume them.
64	pub enable_gate_fusion: bool,
65	/// Fold gates whose inputs are all constants.
66	pub enable_constant_propagation: bool,
67	/// Collapse structurally identical gates.
68	pub enable_common_subexpression_elimination: bool,
69	/// Drop gates that cannot affect the constraint system.
70	pub enable_dead_code_elimination: bool,
71	/// Apply the identities that make an operation return a wire it already has.
72	///
73	/// - Covers a repeated operand, an absorbing or neutral constant operand, and a zero shift.
74	/// - Off means every operation emits its gate.
75	pub enable_algebraic_folding: bool,
76	/// Share scratch slots between values whose lifetimes do not overlap.
77	///
78	/// - Shrinks the uncommitted segment to the largest number of values alive at once.
79	/// - Changes no constraint, since an uncommitted value appears in no operand.
80	/// - An uncommitted value's slot can then be reused by a later one.
81	/// - Reading an uncommitted value back through a witness filler panics as a result.
82	pub enable_scratch_pooling: bool,
83	/// Forward past the gates a zero operand turns into the identity.
84	pub enable_zero_propagation: bool,
85}
86
87impl Default for Options {
88	fn default() -> Self {
89		Self {
90			enable_gate_fusion: true,
91			enable_constant_propagation: false,
92			enable_common_subexpression_elimination: true,
93			enable_dead_code_elimination: true,
94			enable_algebraic_folding: true,
95			// Sharing slots shrinks the uncommitted segment, saved once per instance.
96			// A witness filler now panics on a stale read instead of returning it silently.
97			enable_scratch_pooling: true,
98			enable_zero_propagation: true,
99		}
100	}
101}
102
103/// A chip call named by the wires it passes, before the build assigns them value indices.
104///
105/// A [`ChipCall`] reads its words out of the value vector, and which word a wire holds is only
106/// settled once the circuit is built.
107pub(crate) struct PendingCall {
108	chip: ChipRef,
109	inout: Vec<Wire>,
110}
111
112/// The chip serving a gadget, and how a call site orders the words it passes.
113#[derive(Clone)]
114pub(crate) struct RegisteredGadget {
115	chip: ChipRef,
116	/// Where each of the chip's inout words sits in the gadget's interface, which is its inputs
117	/// followed by its outputs.
118	///
119	/// The inout segment is ordered by wire creation, so a promoted output lands where its gate
120	/// ran rather than where it was promoted. Registration reads the positions back off the built
121	/// chip, and a call site permutes its words by them.
122	inout_order: Vec<usize>,
123}
124
125pub(crate) struct Shared {
126	pub(crate) graph: GateGraph,
127	pub(crate) opts: Options,
128	pub(crate) force_committed: EntitySet<Wire>,
129	pub(crate) hint_registry: HintRegistry,
130	/// The chips registered on the builder, indexed by chip ID.
131	pub(crate) chips: Vec<EmbeddedCircuit>,
132	/// The calls the built circuit makes, in the order they were emitted.
133	pub(crate) chip_calls: Vec<PendingCall>,
134	/// The chips serving a gadget, keyed by the gadget's name and dimensions.
135	pub(crate) chip_gadgets: HashMap<(&'static str, Vec<usize>), RegisteredGadget>,
136}
137
138/// Circuit builder for constructing zero-knowledge proof circuits.
139///
140/// `CircuitBuilder` provides the primary interface for constructing circuits in the Binius64
141/// proof system. The builder compiles imperative gate operations into a constraint system
142/// suitable for zero-knowledge proof generation.
143///
144/// # Circuit Model
145///
146/// A circuit represents computation as a directed acyclic graph where 64-bit values flow
147/// through gates via **wires**. Gates transform input wires to produce output wires.
148/// Methods like [`band`] and [`iadd_32`] add gates to the graph and return handles
149/// to output wires.
150///
151/// During [`build`], the gate graph compiles into ZERO, AND, IMUL, and BMUL constraints
152/// that the proof system operates on directly.
153///
154/// # Wire Types
155///
156/// Wires are handles to 64-bit values that exist during proof generation.
157/// During circuit construction, wires represent value placeholders.
158///
159/// **Constants** - Values known at compile time. Zero constraint cost as both prover
160/// and verifier know these values. Created with [`add_constant`].
161///
162/// **Public inputs/outputs** - Values visible to both prover and verifier.
163/// Form part of the proof statement (e.g., hash output in a preimage proof).
164/// Created with [`add_inout`](Self::add_inout).
165///
166/// **Private witnesses** - Values known only to the prover.
167/// The circuit proves knowledge of these values without revealing them
168/// (e.g., preimage in a hash proof). Created with [`add_witness`](Self::add_witness).
169///
170/// **Internal wires** - Created automatically by gate operations.
171/// Represent intermediate computation values.
172///
173/// # MSB-Boolean Convention
174///
175/// Boolean values encode in the most significant bit (bit 63) of a 64-bit word.
176/// MSB = 1 represents true, MSB = 0 represents false.
177/// The lower 63 bits are "don't care" values.
178///
179/// # Constraint Costs
180///
181/// **AND constraints** - Baseline unit of cost. Bitwise operations and comparisons
182/// generate 1-2 AND constraints.
183///
184/// **IMUL constraints** - 64-bit multiplication costs ~3-4× more than AND constraints.
185///
186/// **Committed values** - Each public input/output and witness adds to proof size
187/// (~0.2× of an AND constraint).
188///
189/// **Linear operations** - XOR and shifts generate virtual linear constraints.
190/// During compilation these either:
191/// - Fuse into adjacent non-linear gates (near-zero cost)
192/// - Materialize as ZERO constraints, which the Zero reduction discharges without a prover message
193///
194/// Gate fusion inlines compatible XOR expressions and shifts into existing AND gates.
195/// Incompatible operations (e.g., right shift into left shift) and heuristic limits
196/// prevent some fusions. XORs typically cost <0.1× of an AND constraint,
197/// shifts slightly more.
198///
199/// # Compilation
200///
201/// The builder uses reference-counted sharing internally. [`subcircuit`] returns
202/// a builder referencing the same graph with hierarchical naming.
203///
204/// [`build`] triggers compilation:
205/// 1. Validates the circuit structure
206/// 2. Runs optimization passes (constant propagation, gate fusion)
207/// 3. Generates the final constraint system
208///
209/// [`build`] consumes internal state and can only be called once per builder instance.
210///
211/// [`add_constant`]: Self::add_constant
212/// [`add_inout`]: Self::add_inout
213/// [`add_witness`]: Self::add_witness
214/// [`band`]: Self::band
215/// [`build`]: Self::build
216/// [`iadd_32`]: Self::iadd_32
217/// [`subcircuit`]: Self::subcircuit
218#[derive(Clone)]
219pub struct CircuitBuilder {
220	/// Current path at which this circuit builder is positioned.
221	current_path: PathSpec,
222	shared: Rc<RefCell<Shared>>,
223}
224
225impl Default for CircuitBuilder {
226	fn default() -> Self {
227		CircuitBuilder::new()
228	}
229}
230
231#[warn(missing_docs)]
232impl CircuitBuilder {
233	/// Create a new circuit builder with default options.
234	pub fn new() -> Self {
235		Self::with_opts(Options::default())
236	}
237
238	/// Create a new circuit builder with the given options.
239	pub fn with_opts(opts: Options) -> Self {
240		let graph = GateGraph::new();
241		let root = graph.path_spec_tree.root();
242		CircuitBuilder {
243			current_path: root,
244			shared: Rc::new(RefCell::new(Shared {
245				graph,
246				opts,
247				force_committed: EntitySet::new(),
248				hint_registry: HintRegistry::new(),
249				chips: Vec::new(),
250				chip_calls: Vec::new(),
251				chip_gadgets: HashMap::new(),
252			})),
253		}
254	}
255
256	/// Registers a chip the built circuit can delegate subrelations to, and returns a reference
257	/// naming it.
258	///
259	/// The registered system is flattened into the builder's own: its main circuit becomes the
260	/// chip the returned reference names, and the chips it calls follow, with the IDs inside it
261	/// shifted to their new slots. Each system occupies one contiguous run of IDs and calls only
262	/// into its own run, so the chips stay in the topological order [`CircuitM4::validate`]
263	/// requires.
264	///
265	/// The registered system's active-instance counts are dropped, and the instances its calls name
266	/// are left stale. They say how often its own main reached each chip, which says nothing about
267	/// how often this circuit will; [`Self::build_m4`] recounts the whole graph.
268	///
269	/// Only [`Self::build_m4`] returns the registered chips; [`Self::build`] rejects a builder
270	/// carrying any.
271	pub fn add_chip(&self, chip: CircuitM4) -> ChipRef {
272		let mut shared = self.shared.borrow_mut();
273		let chips = &mut shared.chips;
274
275		let chip_id = chips.len();
276		// The system's main takes the next slot and its own chips follow it, so an ID naming its
277		// chip `i` now names the slot one past main's.
278		let offset = chip_id + 1;
279		let CircuitM4 {
280			main,
281			chips: nested,
282		} = chip;
283		chips.extend(
284			iter::once(main)
285				.chain(nested.into_iter().map(|(embedded, _)| embedded))
286				.map(|mut embedded| {
287					for call in &mut embedded.chip_calls {
288						call.chip_id += offset;
289					}
290					embedded
291				}),
292		);
293		ChipRef::new(chip_id)
294	}
295
296	/// Calls a registered chip, passing the given wires as the inout words of one invocation.
297	///
298	/// The chip gains an instance serving this call, and that instance's inout values are these
299	/// wires' words. The chip's own constraints are what relate them, so this is how a circuit
300	/// delegates a relation rather than constraining it inline.
301	///
302	/// A chip does not say which of its inout words are inputs and which are outputs; a call
303	/// constrains them all alike. The expected shape computes the outputs from the inputs with a
304	/// hint and passes both: the hint hands the witness its values, and the call is the
305	/// constraint that makes them correct.
306	///
307	/// The build treats a call as a constraint on the words it names. Each wire passed takes a
308	/// committed word of the value vector, and the gate defining it stays live however little
309	/// else reads it. A call names words rather than expressions, though, so a linear argument
310	/// such as `a ^ b` is committed together with the ZERO constraint defining it, where a
311	/// constraint operand would have fused it in for free.
312	///
313	/// The wires are matched positionally against the callee's
314	/// [`Circuit::inout`](Circuit::inout), so they are given in that order and there is one per
315	/// inout word.
316	///
317	/// # Panics
318	///
319	/// Panics if the wire count differs from the callee's inout word count.
320	pub fn call_chip(&self, chip: ChipRef, inout: &[Wire]) {
321		let mut shared = self.shared.borrow_mut();
322
323		let n_inout = shared.chips[chip.chip_id()].circuit.inout().len();
324		assert_eq!(inout.len(), n_inout, "chip #{} takes {n_inout} inout words", chip.chip_id());
325
326		shared.chip_calls.push(PendingCall {
327			chip,
328			inout: inout.to_vec(),
329		});
330	}
331
332	/// Makes a gadget a chip of the circuit being built, so that every emission of it is a call.
333	///
334	/// The chip is the gadget as a system of its own: its inputs declared with
335	/// [`Self::add_inout`], its gates emitted by [`ChipGadget::build`], and its outputs promoted
336	/// with [`Self::mark_inout`]. Building it here rather than taking one built elsewhere is what
337	/// holds the chip's interface and the call sites' words to the same order.
338	///
339	/// The chip's own gates are emitted on a builder of its own, which registers no gadget. So a
340	/// gadget reaching [`Self::build_gadget`] for itself emits its gates inside its chip rather
341	/// than calling back into it.
342	///
343	/// The gadget is keyed by its [`NAME`](Hint::NAME) and `dimensions`: a later
344	/// [`Self::build_gadget`] on the same pair is a call to this chip, and any other emission is
345	/// gates as before. Registering is the whole of the opt-in, and it reaches every subcircuit,
346	/// since they build the same circuit.
347	///
348	/// Registering a gadget the circuit never builds leaves a chip nothing calls, which
349	/// [`CircuitM4::validate`] rejects. So a circuit registers the gadgets it goes on to use.
350	///
351	/// # Panics
352	///
353	/// Panics if the gadget is already registered for these dimensions, if its `build` returns a
354	/// number of outputs other than its [`shape`](Hint::shape) declares, or if its `build`
355	/// declares inout wires of its own, which the interface a call site passes cannot reach.
356	pub fn register_chip<G: ChipGadget>(&self, gadget: G, dimensions: &[usize]) {
357		let (n_in, _) = gadget.shape(dimensions);
358
359		let body = CircuitBuilder::new();
360		let inputs = (0..n_in).map(|_| body.add_inout()).collect::<Vec<_>>();
361		let outputs = body.build_gadget(gadget, dimensions, &inputs);
362		for &wire in &outputs {
363			body.mark_inout(wire);
364		}
365		let built = body.build_m4();
366
367		let interface = chain(&inputs, &outputs).copied().collect::<Vec<_>>();
368		let inout = built.main.circuit.inout();
369		assert_eq!(
370			inout.len(),
371			interface.len(),
372			"register_chip: gadget {} holds inout words beyond the {n_in} inputs and {} outputs \
373			 of its interface",
374			G::NAME,
375			outputs.len(),
376		);
377		let position = interface
378			.iter()
379			.enumerate()
380			.map(|(k, &wire)| (wire, k))
381			.collect::<HashMap<_, _>>();
382		let inout_order = inout.iter().map(|wire| position[wire]).collect::<Vec<_>>();
383
384		let chip = self.add_chip(built);
385
386		let mut shared = self.shared.borrow_mut();
387		let previous = shared
388			.chip_gadgets
389			.insert((G::NAME, dimensions.to_vec()), RegisteredGadget { chip, inout_order });
390		assert!(
391			previous.is_none(),
392			"register_chip: gadget {} is already a chip for dimensions {dimensions:?}",
393			G::NAME,
394		);
395	}
396
397	/// Emits a gadget, as a call to its chip where [`Self::register_chip`] has made it one and as
398	/// its gates otherwise.
399	///
400	/// A call passes the words the gadget relates rather than computing them, so the outputs come
401	/// from the gadget's own [`Hint`]: [`Self::call_hint`] hands the witness its values, and the
402	/// call is the constraint that makes them the ones the gadget's gates would have produced.
403	///
404	/// Either way the returned wires hold the gadget's outputs, so a caller reads the same wires
405	/// whichever way the gadget landed.
406	///
407	/// # Panics
408	///
409	/// Panics if `inputs.len()` or the gadget's output count differs from its
410	/// [`shape`](Hint::shape).
411	pub fn build_gadget<G: ChipGadget>(
412		&self,
413		gadget: G,
414		dimensions: &[usize],
415		inputs: &[Wire],
416	) -> Vec<Wire> {
417		let (n_in, n_out) = gadget.shape(dimensions);
418		assert_eq!(
419			inputs.len(),
420			n_in,
421			"build_gadget: gadget {} takes {n_in} inputs, given {}",
422			G::NAME,
423			inputs.len(),
424		);
425
426		let outputs = match self.registered_gadget(G::NAME, dimensions) {
427			Some(registered) => {
428				let outputs = self.call_hint(gadget, dimensions, inputs);
429				let interface = chain(inputs, &outputs).copied().collect::<Vec<_>>();
430				let inout = registered
431					.inout_order
432					.iter()
433					.map(|&k| interface[k])
434					.collect::<Vec<_>>();
435				self.call_chip(registered.chip, &inout);
436				outputs
437			}
438			None => gadget.build(self, dimensions, inputs),
439		};
440
441		assert_eq!(
442			outputs.len(),
443			n_out,
444			"build_gadget: gadget {} built {} outputs, its shape declares {n_out}",
445			G::NAME,
446			outputs.len(),
447		);
448		outputs
449	}
450
451	/// The chip serving a gadget, if one is registered for these dimensions.
452	///
453	/// Copies the entry out, since a caller goes on to emit gates against the same shared state.
454	fn registered_gadget(
455		&self,
456		name: &'static str,
457		dimensions: &[usize],
458	) -> Option<RegisteredGadget> {
459		self.shared
460			.borrow()
461			.chip_gadgets
462			.get(&(name, dimensions.to_vec()))
463			.cloned()
464	}
465
466	/// Returns the circuit built by this builder.
467	///
468	/// Consumes the builder, so building is a one-shot operation.
469	/// There is no builder left afterward for the type system to reject a second call on.
470	///
471	/// # Panics
472	///
473	/// Panics if a clone or a subcircuit still holds a live handle to the same shared state.
474	/// Only sole ownership can be unwrapped out of a reference count.
475	///
476	/// Panics if the builder carries a chip registered by [`Self::add_chip`].
477	/// Build that one with [`Self::build_m4`] instead.
478	///
479	/// Panics if an enabled constant-propagation pass finds an unsatisfiable gate.
480	pub fn build(self) -> Circuit {
481		self.try_build().unwrap_or_else(|err| panic!("{err}"))
482	}
483
484	/// Returns the circuit built by this builder.
485	/// Returns an error instead of panicking on an unsatisfiable constant gate.
486	///
487	/// # Panics
488	///
489	/// Panics if a clone or a subcircuit still holds a live handle to the same shared state.
490	/// Only sole ownership can be unwrapped out of a reference count.
491	///
492	/// Panics if the builder carries a chip registered by [`Self::add_chip`].
493	/// Build that one with [`Self::build_m4`] instead.
494	///
495	/// # Errors
496	///
497	/// Returns an error when an enabled constant-propagation pass finds an unsatisfiable gate.
498	pub fn try_build(self) -> Result<Circuit, AlwaysFailingGateError> {
499		let shared = self.into_shared();
500		assert!(
501			shared.chips.is_empty(),
502			"a builder carrying chips builds with CircuitBuilder::build_m4"
503		);
504		Self::compile(shared, &[])
505	}
506
507	/// Returns the chip-composed circuit built by this builder.
508	///
509	/// The built circuit is the main one, over the chips registered with [`Self::add_chip`] and
510	/// making the calls emitted by [`Self::call_chip`]. Each call's wires resolve to the value
511	/// indices the build assigned them, and each chip's active-instance count is counted off the
512	/// resulting call graph.
513	///
514	/// Consumes the builder, so building is a one-shot operation.
515	/// There is no builder left afterward for the type system to reject a second call on.
516	///
517	/// # Panics
518	///
519	/// Panics if a clone or a subcircuit still holds a live handle to the same shared state.
520	/// Only sole ownership can be unwrapped out of a reference count.
521	///
522	/// Panics if an enabled constant-propagation pass finds an unsatisfiable gate.
523	pub fn build_m4(self) -> CircuitM4 {
524		self.try_build_m4().unwrap_or_else(|err| panic!("{err}"))
525	}
526
527	/// Returns the chip-composed circuit built by this builder.
528	/// Returns an error instead of panicking on an unsatisfiable constant gate.
529	///
530	/// # Panics
531	///
532	/// Panics if a clone or a subcircuit still holds a live handle to the same shared state.
533	/// Only sole ownership can be unwrapped out of a reference count.
534	///
535	/// # Errors
536	///
537	/// Returns an error when an enabled constant-propagation pass finds an unsatisfiable gate.
538	pub fn try_build_m4(self) -> Result<CircuitM4, AlwaysFailingGateError> {
539		let mut shared = self.into_shared();
540		let chips = mem::take(&mut shared.chips);
541		let pending = mem::take(&mut shared.chip_calls);
542		let circuit = Self::compile(shared, &pending)?;
543
544		// The instances and the active-instance counts are the whole call graph's to settle, so
545		// both are left to `recompute_instances` below.
546		let chip_calls = pending
547			.into_iter()
548			.map(|call| ChipCall {
549				chip_id: call.chip.chip_id(),
550				first_instance: 0,
551				inout: call
552					.inout
553					.iter()
554					.map(|&wire| vec![ShiftedValueIndex::plain(circuit.witness_index(wire))])
555					.collect(),
556			})
557			.collect();
558
559		let mut circuit = CircuitM4 {
560			main: EmbeddedCircuit {
561				circuit,
562				chip_calls,
563			},
564			chips: chips.into_iter().map(|chip| (chip, 0)).collect(),
565		};
566		circuit.recompute_instances();
567		Ok(circuit)
568	}
569
570	/// Reclaims the state behind the shared handle, requiring sole ownership of it.
571	///
572	/// # Panics
573	///
574	/// Panics if a clone or a subcircuit still holds a live handle to the same shared state.
575	/// Only sole ownership can be unwrapped out of a reference count.
576	fn into_shared(self) -> Shared {
577		Rc::into_inner(self.shared)
578			.expect("a clone or subcircuit of this builder is still alive")
579			.into_inner()
580	}
581
582	/// Compiles the builder's state into a circuit, running every optimization pass it enables.
583	///
584	/// # Errors
585	///
586	/// Returns an error when an enabled constant-propagation pass finds an unsatisfiable gate.
587	fn compile(
588		shared: Shared,
589		chip_calls: &[PendingCall],
590	) -> Result<Circuit, AlwaysFailingGateError> {
591		let mut graph = shared.graph;
592
593		// A chip call is a constraint on the words its wires hold, but the compiler passes have
594		// no notion of a call. Folding its wires into the pinned set gives them the treatment a
595		// constraint's wires get: dead-code elimination keeps the gates defining them,
596		// common-subexpression elimination keeps them distinct, and gate fusion keeps a linear
597		// definition committed rather than inlining it away.
598		let mut pinned = shared.force_committed;
599		for wire in chip_calls.iter().flat_map(|call| &call.inout) {
600			pinned.insert(*wire);
601		}
602
603		// The all-one wire is seeded as the first constant when the graph is constructed.
604		let all_one = graph.all_one;
605
606		if cfg!(debug_assertions) {
607			// Every gate already had its shape asserted once when it was emitted.
608			// A release build cannot reach an invalid graph, so this re-walk is debug-only.
609			graph.validate(&shared.hint_registry);
610		}
611
612		// Run constant propagation optimization
613		if shared.opts.enable_constant_propagation {
614			const_prop::constant_propagation(&mut graph, &shared.hint_registry)?;
615		}
616
617		// Zero propagation: drop the gates a zero operand turns into the identity.
618		// This runs before the dead-code pass, which is what removes the gates it strands.
619		if shared.opts.enable_zero_propagation {
620			zero_fold::zero_propagation(&mut graph, &pinned, &shared.hint_registry);
621		}
622
623		// The gates that reach both the constraint system and the evaluation form.
624		// A pass left switched off excludes nothing.
625		let mut surviving = EntitySet::new();
626		surviving.extend(graph.gates.keys());
627
628		// Common-subexpression elimination: collapse structurally-identical gates.
629		// This runs first so the dead-code pass sees the canonicalized graph.
630		if shared.opts.enable_common_subexpression_elimination {
631			// A collapsed duplicate's outputs are read through the canonical gate.
632			for gate in cse::dedup_gates(&mut graph, &pinned, &shared.hint_registry).iter() {
633				surviving.remove(gate);
634			}
635		}
636
637		// Dead-code elimination: the gates that can affect the constraint system.
638		if shared.opts.enable_dead_code_elimination {
639			// A gate outside the live set only writes wires that nothing reads.
640			let live = dce::live_gates(&mut graph, &pinned, &shared.hint_registry);
641			for gate in graph.gates.keys() {
642				if !live.contains(gate) {
643					surviving.remove(gate);
644				}
645			}
646		}
647
648		let mut builder = ConstraintBuilder::new();
649		for gate_id in surviving.iter() {
650			gate_id.constrain(&graph, &mut builder, &shared.hint_registry);
651		}
652
653		// Perform fusion if the corresponding feature flag is turned on.
654		if shared.opts.enable_gate_fusion {
655			fusion::run_pass(&mut builder, &pinned);
656		}
657
658		let mut constrained_wires = builder.mark_used_wires();
659
660		// A chip call reads its words out of the value vector just as a constraint operand does,
661		// so the wires it names are committed on the same footing. This is what commits a hint
662		// output: no constraint defines it, and constraining it is the call's purpose.
663		for wire in chip_calls.iter().flat_map(|call| &call.inout) {
664			constrained_wires.insert(*wire);
665		}
666
667		// Collect the values no constraint operand mentions.
668		//
669		// Only a value the user declared is committed on its own account.
670		// Anything a gate produced is committed only if a constraint references it.
671		// Gate fusion is what decides that.
672		// The rest exist purely during witness evaluation, so they form the scratch segment.
673		let mut scratch_wires = EntitySet::new();
674		for (wire, kind) in graph.wires.iter() {
675			if matches!(kind, WireKind::Internal | WireKind::Scratch)
676				&& !constrained_wires.contains(wire)
677			{
678				scratch_wires.insert(wire);
679			}
680		}
681
682		// Lay the segment out under the selected policy.
683		// Slots are either one per value, or shared between disjoint lifetimes.
684		let scratch_policy = if shared.opts.enable_scratch_pooling {
685			ScratchPolicy::Pooled
686		} else {
687			ScratchPolicy::PerWire
688		};
689		let scratch_alloc = ScratchAlloc::new(&graph, &scratch_wires, scratch_policy);
690
691		// Allocate a place for each wire in the value vec layout.
692		//
693		// This gives us mappings from wires into the value indices, as well as the constant
694		// portion of the value vec.
695		let value_vec_alloc::Assignment {
696			wire_mapping,
697			value_vec_layout,
698			constants,
699			inout,
700		} = {
701			let mut value_vec_alloc = value_vec_alloc::Alloc::new(scratch_alloc.n_slots());
702			for (wire, kind) in graph.wires.iter() {
703				match kind {
704					WireKind::Constant(value) => {
705						value_vec_alloc.add_constant(wire, *value);
706					}
707					WireKind::Inout => value_vec_alloc.add_inout(wire),
708					WireKind::Witness => value_vec_alloc.add_witness(wire),
709					WireKind::Internal | WireKind::Scratch => {
710						// Unlike inout and witness those two are not declared by the user and thus
711						// are not required to appear in the value vec.
712						//
713						// Therefore, we ignore the initial designation internal <=> scratch and
714						// instead we look whether a wire is referenced in the constraint system
715						// or not. If it is referenced then we declare it as internal and put into
716						// the private section (witness). If it's not referenced we declare it as
717						// a scratch value.
718						//
719						// Note that the concept of wire kind outlived it's lifetime and should be
720						// reworked. This is left for the future.
721						if constrained_wires.contains(wire) {
722							value_vec_alloc.add_internal(wire);
723						} else {
724							value_vec_alloc.add_scratch(wire, scratch_alloc.slot(wire));
725						}
726					}
727				}
728			}
729			value_vec_alloc.into_assignment()
730		};
731
732		// Invariant: the all-one constant seeded at graph construction is the first constant.
733		// Downstream consumers reference it by the fixed index 0.
734		debug_assert_eq!(wire_mapping[all_one], binius_core::ValueIndex::constant(0));
735		debug_assert_eq!(constants.first(), Some(&Word::ALL_ONE));
736
737		let (mut zero_constraints, mut and_constraints, mut imul_constraints, mut bmul_constraints) =
738			builder.build(&wire_mapping);
739
740		// Filter zero constant terms from all operands. Any shift of Word::ZERO is zero, so
741		// terms referencing a zero constant contribute nothing to an XOR operand.
742		{
743			let operands = chain!(
744				zero_constraints.iter_mut().flat_map(|c| &mut c.0),
745				and_constraints.iter_mut().flat_map(|c| &mut c.0),
746				imul_constraints.iter_mut().flat_map(|c| &mut c.0),
747				bmul_constraints.iter_mut().flat_map(|c| &mut c.0),
748			);
749			for operand in operands {
750				operand.retain(|term: &binius_core::constraint_system::ShiftedValueIndex| {
751					let index = term.value_index;
752					index.segment() != ValueSegment::Constant
753						|| constants[index.index() as usize] != Word::ZERO
754				});
755			}
756		}
757
758		let cs = ConstraintSystem {
759			zero_constraints,
760			and_constraints,
761			imul_constraints,
762			bmul_constraints,
763			..value_vec_layout.constraint_system_shape(constants)
764		};
765		if cfg!(debug_assertions) {
766			// Validate that the resulting constraint system has a good shape.
767			cs.validate().unwrap();
768		}
769
770		// Build evaluation form (consumes the hint registry the user populated via call_hint).
771		let eval_form = eval_form::EvalForm::build(
772			&graph,
773			&surviving,
774			&wire_mapping,
775			&value_vec_layout,
776			shared.hint_registry,
777		);
778
779		// Passes above needed the whole graph.
780		// A circuit only reads back path names and a per-gate record, so the rest drops now.
781		let built_gates = BuiltGates::from_graph(graph);
782
783		Ok(Circuit::new(
784			built_gates,
785			cs,
786			value_vec_layout,
787			wire_mapping,
788			inout,
789			eval_form,
790			scratch_alloc.peak_live(),
791			scratch_policy == ScratchPolicy::Pooled,
792		))
793	}
794
795	/// Creates a reference to the same underlying circuit builder that is namespaced to the
796	/// given name.
797	///
798	/// This is useful for creating subcircuits within a larger circuit.
799	///
800	/// Note that this is the same builder instance, but with a different namespace, and that means
801	/// calling [`Self::build`] on the returned builder is going to build the whole circuit.
802	pub fn subcircuit(&self, name: impl AsRef<str>) -> CircuitBuilder {
803		let nested_path = self
804			.graph_mut()
805			.path_spec_tree
806			.extend(self.current_path, name.as_ref());
807		CircuitBuilder {
808			current_path: nested_path,
809			shared: self.shared.clone(),
810		}
811	}
812
813	/// Force commit the given wire.
814	///
815	/// This annotate the wire to be forcefully committed. This instructs optimization passes
816	/// (ATOW only gate fusion) to forcibly materialize wire.
817	pub fn force_commit(&self, wire: Wire) {
818		self.shared.borrow_mut().force_committed.insert(wire);
819	}
820
821	/// Promotes a gate-created wire to a public output.
822	///
823	/// The wire moves from the private segment to the inout one, joining the circuit's public
824	/// interface. This is what a circuit exposing a gadget's result wants: declaring a separate
825	/// inout wire and asserting the result against it costs a second committed word and a
826	/// constraint whenever the result is a wire that has to be committed anyway.
827	///
828	/// The value is still derived by the gate producing it, so a witness filler must *not* assign
829	/// it — unlike a wire from [`Self::add_inout`], which the filler is required to set.
830	///
831	/// Promoting also pins the wire, so this subsumes [`Self::force_commit`] rather than needing
832	/// it alongside.
833	///
834	/// # Position in the segment
835	///
836	/// The inout segment is ordered by wire creation, so a promoted wire follows the declared
837	/// inout wires and sits among its fellow promotions in the order their gates created them —
838	/// which is not necessarily the order they are promoted in. That is invisible to a caller
839	/// filling by [`Wire`], but a caller building the positional public-input vector a verifier
840	/// takes should read each index back with
841	/// [`Circuit::witness_index`](crate::Circuit::witness_index) rather than assume promotion
842	/// order.
843	///
844	/// # Panics
845	///
846	/// Panics unless the wire is a gate-created internal wire. A constant, an input, or an
847	/// already-public wire has nothing to promote.
848	pub fn mark_inout(&self, wire: Wire) {
849		{
850			let mut graph = self.graph_mut();
851			assert!(
852				matches!(graph.wire_kind(wire), WireKind::Internal),
853				"only a gate-created wire can be promoted to a public output"
854			);
855			graph.wires[wire] = WireKind::Inout;
856		}
857
858		// Dead-code elimination and CSE already treat an inout wire as observable, but gate fusion
859		// reads only the pinned set: without this it would inline a linear definition and leave the
860		// public word with no constraint defining it.
861		self.force_commit(wire);
862	}
863
864	fn graph_mut(&self) -> RefMut<'_, GateGraph> {
865		RefMut::map(self.shared.borrow_mut(), |shared| &mut shared.graph)
866	}
867
868	/// Creates a wire from a 64-bit word.
869	///
870	/// # Arguments
871	/// * `word` -  The word to add to the circuit.
872	///
873	/// # Returns
874	/// A `Wire` representing the constant value. The wire might be aliased because the constants
875	/// are deduplicated.
876	///
877	/// # Cost
878	///
879	/// Constants have no constraint cost - they are "free" in the circuit.
880	pub fn add_constant(&self, word: Word) -> Wire {
881		self.graph_mut().add_constant(word)
882	}
883
884	/// Creates a constant wire from a 64-bit unsigned integer.
885	///
886	/// This method adds a 64-bit constant value to the circuit. The constant is stored
887	/// as a `Word` and can be used in constraints and operations.
888	///
889	/// Constants are automatically deduplicated - multiple calls with the same value
890	/// will return the same wire.
891	///
892	/// # Arguments
893	/// * `c` - The 64-bit constant value to add to the circuit
894	///
895	/// # Returns
896	/// A `Wire` representing the constant value that can be used in circuit operations
897	pub fn add_constant_64(&self, c: u64) -> Wire {
898		self.add_constant(Word(c))
899	}
900
901	/// Creates a constant wire from an 8-bit value, zero-extended to 64 bits.
902	///
903	/// This method takes an 8-bit unsigned integer (byte) and zero-extends it to
904	/// a 64-bit value before adding it as a constant to the circuit. The resulting
905	/// wire contains the byte value in the lower 8 bits and zeros in the upper 56 bits.
906	/// This is commonly used for byte constants in circuits that process byte data.
907	///
908	/// # Arguments
909	/// * `c` - The 8-bit constant value (0-255) to add to the circuit
910	pub fn add_constant_zx_8(&self, c: u8) -> Wire {
911		self.add_constant(Word(c as u64))
912	}
913
914	/// Creates a public input/output wire.
915	///
916	/// Public wires form part of the proof statement and are visible to both prover and verifier.
917	/// They are committed in the public section of the value vector alongside constants.
918	///
919	/// The wire must be manually assigned a value using [`WitnessFiller`] before circuit
920	/// evaluation.
921	///
922	/// [`WitnessFiller`]: crate::WitnessFiller
923	pub fn add_inout(&self) -> Wire {
924		self.graph_mut().add_inout()
925	}
926
927	/// Creates a private input wire.
928	///
929	/// Private wires contain secret values known only to the prover. They are placed in the
930	/// private section of the value vector and are not revealed to the verifier.
931	///
932	/// The wire must be manually assigned a value using [`WitnessFiller`] before circuit
933	/// evaluation.
934	///
935	/// [`WitnessFiller`]: crate::WitnessFiller
936	pub fn add_witness(&self) -> Wire {
937		self.graph_mut().add_witness()
938	}
939
940	/// Bitwise AND.
941	///
942	/// Returns z = x & y
943	///
944	/// # Cost
945	///
946	/// 1 AND constraint, or none when an algebraic identity resolves it.
947	pub fn band(&self, x: Wire, y: Wire) -> Wire {
948		let mut shared = self.shared.borrow_mut();
949		// Identities that hold bit for bit, so they need no AND constraint:
950		//   x & x  -> x           c & d  -> fold
951		//   0 & y  -> 0           all-1 & y -> y
952		if shared.opts.enable_algebraic_folding {
953			if x == y {
954				return x;
955			}
956			match (const_of(&shared.graph, x), const_of(&shared.graph, y)) {
957				(Some(a), Some(b)) => return shared.graph.add_constant(Word(a.0 & b.0)),
958				(Some(a), _) if a == Word::ZERO => return x,
959				(Some(a), _) if a == Word::ALL_ONE => return y,
960				(_, Some(b)) if b == Word::ZERO => return y,
961				(_, Some(b)) if b == Word::ALL_ONE => return x,
962				_ => {}
963			}
964		}
965		let z = shared.graph.add_internal();
966		shared
967			.graph
968			.emit_gate(self.current_path, Opcode::Band, [x, y], [z]);
969		z
970	}
971
972	/// Bitwise XOR.
973	///
974	/// Returns z = x ^ y
975	///
976	/// # Cost
977	///
978	/// 1 linear constraint, or none when an algebraic identity resolves it.
979	pub fn bxor(&self, a: Wire, b: Wire) -> Wire {
980		let mut shared = self.shared.borrow_mut();
981		// Identities that hold bit for bit, so they need no linear constraint:
982		//   x ^ x  -> 0           c ^ d  -> fold
983		//   0 ^ b  -> b           a ^ 0  -> a
984		if shared.opts.enable_algebraic_folding {
985			if a == b {
986				return shared.graph.add_constant(Word::ZERO);
987			}
988			match (const_of(&shared.graph, a), const_of(&shared.graph, b)) {
989				(Some(x), Some(y)) => return shared.graph.add_constant(Word(x.0 ^ y.0)),
990				(Some(x), _) if x == Word::ZERO => return b,
991				(_, Some(y)) if y == Word::ZERO => return a,
992				_ => {}
993			}
994		}
995		let z = shared.graph.add_internal();
996		shared
997			.graph
998			.emit_gate(self.current_path, Opcode::Bxor, [a, b], [z]);
999		z
1000	}
1001
1002	/// Multi-way bitwise XOR operation.
1003	///
1004	/// Takes a variable-length slice of wires and XORs them all together.
1005	///
1006	/// Returns z = i ^ j ^ k ^ ...
1007	///
1008	/// # Cost
1009	///
1010	/// 1 linear constraint.
1011	pub fn bxor_multi(&self, wires: &[Wire]) -> Wire {
1012		assert!(!wires.is_empty(), "bxor_multi requires at least one input");
1013
1014		if wires.len() == 1 {
1015			return wires[0];
1016		}
1017
1018		if wires.len() == 2 {
1019			return self.bxor(wires[0], wires[1]);
1020		}
1021
1022		let mut shared = self.shared.borrow_mut();
1023		let z = shared.graph.add_internal();
1024		shared.graph.emit_gate_generic(
1025			self.current_path,
1026			Opcode::BxorMulti,
1027			wires.iter().copied(),
1028			[z],
1029			&[wires.len()],
1030			&[],
1031		);
1032		z
1033	}
1034
1035	/// Bitwise Not
1036	///
1037	/// Returns z = ~x
1038	///
1039	/// # Cost
1040	///
1041	/// 1 linear constraint.
1042	pub fn bnot(&self, a: Wire) -> Wire {
1043		let all_one = self.graph_mut().all_one;
1044		self.bxor(a, all_one)
1045	}
1046
1047	/// Bitwise OR.
1048	///
1049	/// Returns z = x | y
1050	///
1051	/// # Cost
1052	///
1053	/// 1 AND constraint, or none when an algebraic identity resolves it.
1054	pub fn bor(&self, a: Wire, b: Wire) -> Wire {
1055		let mut shared = self.shared.borrow_mut();
1056		// Identities that hold bit for bit, so they need no AND constraint:
1057		//   x | x  -> x           c | d  -> fold
1058		//   0 | b  -> b           all-1 | b -> all-1
1059		if shared.opts.enable_algebraic_folding {
1060			if a == b {
1061				return a;
1062			}
1063			match (const_of(&shared.graph, a), const_of(&shared.graph, b)) {
1064				(Some(x), Some(y)) => return shared.graph.add_constant(Word(x.0 | y.0)),
1065				(Some(x), _) if x == Word::ZERO => return b,
1066				(Some(x), _) if x == Word::ALL_ONE => return a,
1067				(_, Some(y)) if y == Word::ZERO => return a,
1068				(_, Some(y)) if y == Word::ALL_ONE => return b,
1069				_ => {}
1070			}
1071		}
1072		let z = shared.graph.add_internal();
1073		shared
1074			.graph
1075			.emit_gate(self.current_path, Opcode::Bor, [a, b], [z]);
1076		z
1077	}
1078
1079	/// Fused AND-XOR operation.
1080	///
1081	/// Computes (x & y) ^ w in a single gate.
1082	///
1083	/// Returns z = (x & y) ^ w
1084	///
1085	/// # Cost
1086	///
1087	/// 1 AND constraint.
1088	pub fn fax(&self, x: Wire, y: Wire, w: Wire) -> Wire {
1089		let mut shared = self.shared.borrow_mut();
1090		let z = shared.graph.add_internal();
1091		shared
1092			.graph
1093			.emit_gate(self.current_path, Opcode::Fax, [x, y, w], [z]);
1094		z
1095	}
1096
1097	/// Parallel 32-bit integer addition.
1098	///
1099	/// Performs simultaneous independent 32-bit additions on the upper and lower halves,
1100	/// discarding the carry-out.
1101	///
1102	/// # Cost
1103	///
1104	/// 1 AND constraint, 1 linear constraint.
1105	pub fn iadd_32(&self, a: Wire, b: Wire) -> Wire {
1106		let mut shared = self.shared.borrow_mut();
1107		let sum = shared.graph.add_internal();
1108		let cout = shared.graph.add_internal();
1109		shared
1110			.graph
1111			.emit_gate(self.current_path, Opcode::Iadd32, [a, b], [sum, cout]);
1112		sum
1113	}
1114
1115	/// Parallel 32-bit integer addition with carry-in and carry-out.
1116	///
1117	/// Performs simultaneous independent 32-bit additions on the upper and lower halves
1118	/// of the 64-bit word, with per-half carry-in and carry-out.
1119	///
1120	/// The carry-in for each half is taken from the MSB of that half in `cin`:
1121	/// bit 31 for the lower half, bit 63 for the upper half. The carry-out
1122	/// is a full carry word where bit 31 and bit 63 indicate the carry-out
1123	/// of the lower and upper halves respectively.
1124	///
1125	/// # Cost
1126	///
1127	/// 1 AND constraint, 1 linear constraint.
1128	pub fn iadd32_cin_cout(&self, a: Wire, b: Wire, cin: Wire) -> (Wire, Wire) {
1129		let mut shared = self.shared.borrow_mut();
1130		let sum = shared.graph.add_internal();
1131		let cout = shared.graph.add_internal();
1132		shared
1133			.graph
1134			.emit_gate(self.current_path, Opcode::Iadd32CinCout, [a, b, cin], [sum, cout]);
1135		(sum, cout)
1136	}
1137
1138	/// 64-bit integer addition with carry input and output.
1139	///
1140	/// Performs full 64-bit unsigned addition of two wires plus a carry input.
1141	///
1142	/// Returns `(sum, carry_out)` where:
1143	///
1144	/// - `sum` is the 64-bit result and
1145	/// - `carry_out` is a 64-bit word where every bit position with a carry is set to 1.
1146	///
1147	/// # Cost
1148	///
1149	/// - 1 AND constraint,
1150	/// - 1 linear constraint.
1151	pub fn iadd_cin_cout(&self, a: Wire, b: Wire, cin: Wire) -> (Wire, Wire) {
1152		let mut shared = self.shared.borrow_mut();
1153		let sum = shared.graph.add_internal();
1154		let cout = shared.graph.add_internal();
1155		shared
1156			.graph
1157			.emit_gate(self.current_path, Opcode::IaddCinCout, [a, b, cin], [sum, cout]);
1158		(sum, cout)
1159	}
1160
1161	/// 64-bit subtraction with borrow input and output.
1162	///
1163	/// Performs full 64-bit unsigned subtraction of two wires plus a borrow input.
1164	///
1165	/// Returns `(diff, borrow_out)` where:
1166	///
1167	/// - `diff` is the 64-bit result and
1168	/// - `borrow_out` is a 64-bit word where every bit position with a borrow is set to 1.
1169	///
1170	/// # Cost
1171	///
1172	/// - 1 AND constraint,
1173	/// - 1 linear constraint.
1174	pub fn isub_bin_bout(&self, a: Wire, b: Wire, bin: Wire) -> (Wire, Wire) {
1175		let mut shared = self.shared.borrow_mut();
1176		let diff = shared.graph.add_internal();
1177		let bout = shared.graph.add_internal();
1178		shared
1179			.graph
1180			.emit_gate(self.current_path, Opcode::IsubBinBout, [a, b, bin], [diff, bout]);
1181		(diff, bout)
1182	}
1183
1184	/// Emits one shift/rotate gate for the given variant and amount.
1185	///
1186	/// The variant and amount are carried as the gate's two immediates.
1187	/// The caller enforces the amount range.
1188	fn emit_shift(&self, variant: ShiftVariant, x: Wire, n: u32) -> Wire {
1189		let mut shared = self.shared.borrow_mut();
1190		// Identity in every variant:
1191		//   shift(x, 0) -> x
1192		if shared.opts.enable_algebraic_folding && n == 0 {
1193			return x;
1194		}
1195		let z = shared.graph.add_internal();
1196		shared.graph.emit_gate_generic(
1197			self.current_path,
1198			Opcode::Shift,
1199			[x],
1200			[z],
1201			&[],
1202			&[variant as u32, n],
1203		);
1204		z
1205	}
1206
1207	/// 32-bit half-wise rotate left.
1208	///
1209	/// Rotates the upper and lower 32-bit halves left independently by `n`.
1210	/// Bits do not cross the 32-bit lane boundary.
1211	///
1212	/// Returns `x ROTL32 n`
1213	///
1214	/// # Panics
1215	///
1216	/// Panics if n ≥ 32.
1217	///
1218	/// # Cost
1219	///
1220	/// 1 AND constraint (0 if n = 0).
1221	pub fn rotl32(&self, x: Wire, n: u32) -> Wire {
1222		assert!(n < 32, "rotate amount n={n} out of range");
1223		self.emit_shift(ShiftVariant::Rotr32, x, (32 - n) % 32)
1224	}
1225
1226	/// 32-bit half-wise rotate right.
1227	///
1228	/// Rotates the upper and lower 32-bit halves right independently by `n`.
1229	/// Bits do not cross the 32-bit lane boundary.
1230	///
1231	/// Returns `x ROTR32 n`
1232	///
1233	/// # Panics
1234	///
1235	/// Panics if n ≥ 32.
1236	///
1237	/// # Cost
1238	///
1239	/// 1 AND constraint (0 if n = 0).
1240	pub fn rotr32(&self, x: Wire, n: u32) -> Wire {
1241		assert!(n < 32, "rotate amount n={n} out of range");
1242		self.emit_shift(ShiftVariant::Rotr32, x, n)
1243	}
1244
1245	/// 64-bit rotate left.
1246	///
1247	/// Rotates a 64-bit value left by n positions. Bits shifted out on the left
1248	/// wrap around to the right.
1249	///
1250	/// Returns `x rotated left by n`
1251	///
1252	/// # Panics
1253	///
1254	/// Panics if n ≥ 64.
1255	///
1256	/// # Cost
1257	///
1258	/// 1 AND constraint (0 if n = 0).
1259	pub fn rotl(&self, x: Wire, n: u32) -> Wire {
1260		assert!(n < 64, "rotate amount n={n} out of range");
1261		self.emit_shift(ShiftVariant::Rotr, x, (64 - n) % 64)
1262	}
1263
1264	/// 64-bit rotate right.
1265	///
1266	/// Rotates a 64-bit value right by n positions. Bits shifted out on the right
1267	/// wrap around to the left.
1268	///
1269	/// Returns `x rotated right by n`
1270	///
1271	/// # Panics
1272	///
1273	/// Panics if n ≥ 64.
1274	///
1275	/// # Cost
1276	///
1277	/// 1 AND constraint (0 if n = 0).
1278	pub fn rotr(&self, x: Wire, n: u32) -> Wire {
1279		assert!(n < 64, "rotate amount n={n} out of range");
1280		self.emit_shift(ShiftVariant::Rotr, x, n)
1281	}
1282
1283	/// 32-bit half-wise logical right shift.
1284	///
1285	/// Shifts the upper and lower 32-bit halves right independently by `n`.
1286	/// Bits do not cross the 32-bit lane boundary.
1287	///
1288	/// Returns `x SRL32 n`
1289	///
1290	/// # Panics
1291	///
1292	/// Panics if n ≥ 32.
1293	///
1294	/// # Cost
1295	///
1296	/// 1 AND constraint (0 if n = 0).
1297	pub fn srl32(&self, x: Wire, n: u32) -> Wire {
1298		assert!(n < 32, "shift amount n={n} out of range");
1299		self.emit_shift(ShiftVariant::Srl32, x, n)
1300	}
1301
1302	/// 32-bit half-wise logical left shift.
1303	///
1304	/// Shifts the upper and lower 32-bit halves left independently by `n`.
1305	/// Bits do not cross the 32-bit lane boundary.
1306	///
1307	/// Returns `x SLL32 n`.
1308	///
1309	/// # Panics
1310	///
1311	/// Panics if `n ≥ 32`.
1312	///
1313	/// # Cost
1314	///
1315	/// 1 AND constraint (0 if n = 0).
1316	pub fn sll32(&self, x: Wire, n: u32) -> Wire {
1317		assert!(n < 32, "shift amount n={n} out of range for 32-bit half shift");
1318		self.emit_shift(ShiftVariant::Sll32, x, n)
1319	}
1320
1321	/// Logical left shift.
1322	///
1323	/// Shifts a 64-bit wire left by n bits, filling with zeros from the right.
1324	///
1325	/// Returns a << n
1326	///
1327	/// # Cost
1328	///
1329	/// 1 AND constraint (0 if n = 0).
1330	pub fn shl(&self, a: Wire, n: u32) -> Wire {
1331		assert!(n < 64, "shift amount n={n} out of range");
1332		self.emit_shift(ShiftVariant::Sll, a, n)
1333	}
1334
1335	/// Logical right shift.
1336	///
1337	/// Shifts a 64-bit wire right by n bits, filling with zeros from the left.
1338	///
1339	/// Returns a >> n
1340	///
1341	/// # Cost
1342	///
1343	/// 1 AND constraint (0 if n = 0).
1344	pub fn shr(&self, a: Wire, n: u32) -> Wire {
1345		assert!(n < 64, "shift amount n={n} out of range");
1346		self.emit_shift(ShiftVariant::Slr, a, n)
1347	}
1348
1349	/// Arithmetic right shift.
1350	///
1351	/// Shifts a 64-bit wire right by n bits, filling with the MSB from the left.
1352	///
1353	/// Returns a SAR n
1354	///
1355	/// # Cost
1356	///
1357	/// 1 AND constraint (0 if n = 0).
1358	pub fn sar(&self, a: Wire, n: u32) -> Wire {
1359		assert!(n < 64, "shift amount n={n} out of range");
1360		self.emit_shift(ShiftVariant::Sar, a, n)
1361	}
1362
1363	/// 32-bit half-wise arithmetic right shift.
1364	///
1365	/// Shifts the upper and lower 32-bit halves right independently by `n`,
1366	/// sign-extending each half from its own bit 31.
1367	///
1368	/// Returns `x SRA32 n`.
1369	///
1370	/// # Panics
1371	///
1372	/// Panics if `n ≥ 32`.
1373	///
1374	/// # Cost
1375	///
1376	/// 1 AND constraint (0 if n = 0).
1377	pub fn sra32(&self, a: Wire, n: u32) -> Wire {
1378		assert!(n < 32, "shift amount n={n} out of range for 32-bit half shift");
1379		self.emit_shift(ShiftVariant::Sra32, a, n)
1380	}
1381
1382	/// Equality assertion.
1383	///
1384	/// Asserts that two 64-bit wires are equal.
1385	///
1386	/// Takes wires x and y and enforces x == y.
1387	/// If the assertion fails, the circuit will report an error with the given name.
1388	///
1389	/// # Cost
1390	///
1391	/// 1 AND constraint.
1392	pub fn assert_eq(&self, name: impl AsRef<str>, x: Wire, y: Wire) {
1393		let name = name.as_ref();
1394		let mut graph = self.graph_mut();
1395		let gate = graph.emit_gate(self.current_path, Opcode::AssertEq, [x, y], []);
1396		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
1397		graph.assertion_names[gate] = path_spec;
1398	}
1399
1400	/// Vector equality assertion.
1401	///
1402	/// Asserts that two arrays of 64-bit wires are equal element-wise.
1403	///
1404	/// Takes wire arrays x and y and enforces `x[i] == y[i]` for all `i`.
1405	/// Each element assertion is named with the base name and index.
1406	///
1407	/// # Cost
1408	///
1409	/// N AND constraints (one per element).
1410	pub fn assert_eq_v<const N: usize>(&self, name: impl AsRef<str>, x: [Wire; N], y: [Wire; N]) {
1411		let base_name = name.as_ref();
1412		for i in 0..N {
1413			self.assert_eq(format!("{base_name}[{i}]"), x[i], y[i]);
1414		}
1415	}
1416
1417	/// Asserts that the given wire equals zero.
1418	///
1419	/// Enforces that `x = 0` exactly. Every bit of the 64-bit value must be zero.
1420	///
1421	/// # Cost
1422	///
1423	/// 1 AND constraint.
1424	pub fn assert_zero(&self, name: impl AsRef<str>, x: Wire) {
1425		let name = name.as_ref();
1426		let mut graph = self.graph_mut();
1427		let gate = graph.emit_gate(self.current_path, Opcode::AssertZero, [x], []);
1428		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
1429		graph.assertion_names[gate] = path_spec;
1430	}
1431
1432	/// Asserts that the given wire is not zero.
1433	///
1434	/// Enforces that `x ≠ 0`. At least one bit must be non-zero.
1435	///
1436	/// # Cost
1437	///
1438	/// 1 AND constraint.
1439	pub fn assert_non_zero(&self, name: impl AsRef<str>, x: Wire) {
1440		let name = name.as_ref();
1441		let mut graph = self.graph_mut();
1442		let gate = graph.emit_gate(self.current_path, Opcode::AssertNonZero, [x], []);
1443		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
1444		graph.assertion_names[gate] = path_spec;
1445	}
1446
1447	/// Asserts that the given wire's MSB (Most Significant Bit) is 0.
1448	///
1449	/// This treats the wire as an MSB-boolean where:
1450	/// - MSB = 0 → false (assertion passes)
1451	/// - MSB = 1 → true (assertion fails)
1452	///
1453	/// All bits except the MSB are ignored. This is commonly used with comparison
1454	/// results which return MSB-boolean values.
1455	///
1456	/// # Cost
1457	///
1458	/// 1 AND constraint.
1459	pub fn assert_false(&self, name: impl AsRef<str>, x: Wire) {
1460		let name = name.as_ref();
1461		let mut graph = self.graph_mut();
1462		let gate = graph.emit_gate(self.current_path, Opcode::AssertFalse, [x], []);
1463		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
1464		graph.assertion_names[gate] = path_spec;
1465	}
1466
1467	/// Asserts that the given wire's MSB (Most Significant Bit) is 1.
1468	///
1469	/// This treats the wire as an MSB-boolean where:
1470	/// - MSB = 1 → true (assertion passes)
1471	/// - MSB = 0 → false (assertion fails)
1472	///
1473	/// All bits except the MSB are ignored. This is commonly used with comparison
1474	/// results which return MSB-boolean values.
1475	///
1476	/// # Cost
1477	///
1478	/// 1 AND constraint.
1479	pub fn assert_true(&self, name: impl AsRef<str>, x: Wire) {
1480		let name = name.as_ref();
1481		let mut graph = self.graph_mut();
1482		let gate = graph.emit_gate(self.current_path, Opcode::AssertTrue, [x], []);
1483		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
1484		graph.assertion_names[gate] = path_spec;
1485	}
1486
1487	/// 64-bit × 64-bit → 128-bit unsigned multiplication.
1488	///
1489	/// Performs unsigned integer multiplication of two 64-bit values, producing
1490	/// a 128-bit result split into high and low 64-bit words.
1491	///
1492	/// Returns `(hi, lo)` where `a * b = (hi << 64) | lo`
1493	///
1494	/// # Cost
1495	///
1496	/// 1 IMUL constraint.
1497	pub fn imul(&self, a: Wire, b: Wire) -> (Wire, Wire) {
1498		let mut shared = self.shared.borrow_mut();
1499		let hi = shared.graph.add_internal();
1500		let lo = shared.graph.add_internal();
1501		shared
1502			.graph
1503			.emit_gate(self.current_path, Opcode::Imul, [a, b], [hi, lo]);
1504		(hi, lo)
1505	}
1506
1507	/// Multiplication in the GHASH field GF(2^128).
1508	///
1509	/// Multiplies two field elements, each carried by a `(lo, hi)` pair of 64-bit words — `lo`
1510	/// holds the coefficients of `1, X, …, X^63` and `hi` those of `X^64, …, X^127`.
1511	///
1512	/// Returns `(c_lo, c_hi)`, the product `(a_lo, a_hi) * (b_lo, b_hi)` in the same
1513	/// representation.
1514	///
1515	/// # Cost
1516	///
1517	/// - 1 BMUL constraint.
1518	pub fn bmul(&self, a_lo: Wire, a_hi: Wire, b_lo: Wire, b_hi: Wire) -> (Wire, Wire) {
1519		let mut shared = self.shared.borrow_mut();
1520		let c_lo = shared.graph.add_internal();
1521		let c_hi = shared.graph.add_internal();
1522		shared.graph.emit_gate(
1523			self.current_path,
1524			Opcode::Bmul,
1525			[a_lo, a_hi, b_lo, b_hi],
1526			[c_lo, c_hi],
1527		);
1528		(c_lo, c_hi)
1529	}
1530
1531	/// Conditional equality assertion.
1532	///
1533	/// Asserts that two 64-bit wires are equal only when a condition is true (MSB = 1).
1534	/// When the condition is false (MSB = 0), no constraint is enforced.
1535	///
1536	/// # Cost
1537	///
1538	/// 1 AND constraint.
1539	pub fn assert_eq_cond(&self, name: impl AsRef<str>, x: Wire, y: Wire, cond: Wire) {
1540		let name = name.as_ref();
1541		let mut graph = self.graph_mut();
1542		let gate = graph.emit_gate(self.current_path, Opcode::AssertEqCond, [x, y, cond], []);
1543		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
1544		graph.assertion_names[gate] = path_spec;
1545	}
1546
1547	/// Unsigned less-than comparison.
1548	///
1549	/// Compares two 64-bit wires as unsigned integers.
1550	///
1551	/// Returns:
1552	/// - a wire whose MSB-bool value is true if a < b
1553	/// - a wire whose MSB-bool value is false if a ≥ b
1554	///
1555	/// the non-most-significant bits of the output wire are undefined.
1556	///
1557	/// # Cost
1558	///
1559	/// - 1 AND constraint,
1560	/// - 1 linear constraint.
1561	pub fn icmp_ult(&self, x: Wire, y: Wire) -> Wire {
1562		let mut shared = self.shared.borrow_mut();
1563		let out_wire = shared.graph.add_internal();
1564		shared
1565			.graph
1566			.emit_gate(self.current_path, Opcode::IcmpUlt, [x, y], [out_wire]);
1567		out_wire
1568	}
1569
1570	/// Unsigned less-than-or-equal comparison.
1571	///
1572	/// Compares two 64-bit wires as unsigned integers.
1573	///
1574	/// Returns:
1575	/// - a wire whose MSB-bool value is true if x <= y
1576	/// - a wire whose MSB-bool value is false if x > y
1577	///
1578	/// the non-most-significant bits of the output wire are undefined.
1579	///
1580	/// # Cost
1581	///
1582	/// - 1 AND constraint,
1583	/// - 1 linear constraint.
1584	pub fn icmp_ule(&self, x: Wire, y: Wire) -> Wire {
1585		// x <= y is equivalent to !(y < x)
1586		let gt = self.icmp_ult(y, x);
1587		self.bnot(gt)
1588	}
1589
1590	/// Unsigned greater-than comparison.
1591	///
1592	/// Compares two 64-bit wires as unsigned integers.
1593	///
1594	/// Returns:
1595	/// - a wire whose MSB-bool value is true if x > y
1596	/// - a wire whose MSB-bool value is false if x <= y
1597	///
1598	/// the non-most-significant bits of the output wire are undefined.
1599	///
1600	/// # Cost
1601	///
1602	/// 1 AND constraint.
1603	pub fn icmp_ugt(&self, x: Wire, y: Wire) -> Wire {
1604		// x > y is equivalent to y < x.
1605		self.icmp_ult(y, x)
1606	}
1607
1608	/// Unsigned greater-than-or-equal comparison.
1609	///
1610	/// Compares two 64-bit wires as unsigned integers.
1611	///
1612	/// Returns:
1613	/// - a wire whose MSB-bool value is true if x >= y
1614	/// - a wire whose MSB-bool value is false if x < y
1615	///
1616	/// the non-most-significant bits of the output wire are undefined.
1617	///
1618	/// # Cost
1619	///
1620	/// - 1 AND constraint,
1621	/// - 1 linear constraint.
1622	pub fn icmp_uge(&self, x: Wire, y: Wire) -> Wire {
1623		// x >= y is equivalent to !(x < y)
1624		let lt = self.icmp_ult(x, y);
1625		self.bnot(lt)
1626	}
1627
1628	/// Equality comparison.
1629	///
1630	/// Compares two 64-bit wires for equality.
1631	///
1632	/// Returns:
1633	/// - a wire whose MSB-bool value is true if a == b
1634	/// - a wire whose MSB-bool value is false if a != b
1635	///
1636	/// the non-most-significant bits of the output wire are undefined.
1637	///
1638	/// # Cost
1639	///
1640	/// 1 AND constraint.
1641	pub fn icmp_eq(&self, x: Wire, y: Wire) -> Wire {
1642		let mut shared = self.shared.borrow_mut();
1643		let out_wire = shared.graph.add_internal();
1644		shared
1645			.graph
1646			.emit_gate(self.current_path, Opcode::IcmpEq, [x, y], [out_wire]);
1647		out_wire
1648	}
1649
1650	/// Inequality comparison.
1651	///
1652	/// Compares two 64-bit wires for inequality.
1653	///
1654	/// Returns:
1655	/// - a wire whose MSB-bool value is true if a != b
1656	/// - a wire whose MSB-bool value is false if a == b
1657	///
1658	/// the non-most-significant bits of the output wire are undefined.
1659	///
1660	/// # Cost
1661	///
1662	/// - 1 AND constraint,
1663	/// - 1 linear constraint.
1664	pub fn icmp_ne(&self, x: Wire, y: Wire) -> Wire {
1665		let eq = self.icmp_eq(x, y);
1666		self.bnot(eq)
1667	}
1668
1669	/// Byte extraction.
1670	///
1671	/// Extracts byte j from a 64-bit word (j=0 is least significant byte).
1672	///
1673	/// Returns the extracted byte (0-255) in the low 8 bits, with high 56 bits zero.
1674	///
1675	/// # Panics
1676	///
1677	/// Panics if j is greater than or equal to 8.
1678	///
1679	/// # Cost
1680	///
1681	/// - 1 AND constraint,
1682	/// - 1 linear constraint.
1683	pub fn extract_byte(&self, word: Wire, j: u32) -> Wire {
1684		assert!(j < 8, "byte index j={j} out of range");
1685
1686		// To extract the byte j out of 8 we want to generate a mask that will zero out all bits
1687		// except the ones in the j-th byte and then shift it to the rightmost position. We used
1688		// to have a gate for this but it's not necessary.
1689		let shift = j * 8;
1690		let mask = self.add_constant_64(0xff << shift);
1691		let masked = self.band(word, mask);
1692		self.shr(masked, shift)
1693	}
1694
1695	/// Select operation.
1696	///
1697	/// Returns `t` if `cond` is true (MSB-bit set), otherwise returns `f`.
1698	///
1699	/// # Cost
1700	///
1701	/// 1 BMUL constraint, or none when an algebraic identity resolves it.
1702	pub fn select(&self, cond: Wire, t: Wire, f: Wire) -> Wire {
1703		let mut shared = self.shared.borrow_mut();
1704		// Identities that need no BMUL constraint, with the condition read at bit 63:
1705		//   select(c, t, t) -> t        msb-set -> t        msb-clear -> f
1706		if shared.opts.enable_algebraic_folding {
1707			if t == f {
1708				return t;
1709			}
1710			if let Some(c) = const_of(&shared.graph, cond) {
1711				return if (c.0 >> 63) == 1 { t } else { f };
1712			}
1713		}
1714		let out = shared.graph.add_internal();
1715		shared
1716			.graph
1717			.emit_gate(self.current_path, Opcode::Select, [cond, t, f], [out]);
1718		out
1719	}
1720
1721	/// Invoke a [`Hint`] and emit the corresponding gate.
1722	///
1723	/// Registers `hint` in the builder's hint registry (keyed by `T::NAME`), allocates output
1724	/// wires according to `hint.shape(dimensions)`, and emits a generic hint gate. Returns the
1725	/// freshly allocated output wires.
1726	///
1727	/// `dimensions` is passed verbatim to [`Hint::shape`] and [`Hint::execute`]; it is the
1728	/// hint's parameterization (e.g., limb counts for a bignum hint).
1729	///
1730	/// The registry keys on the name alone.
1731	/// Only the first value passed under a name survives; a later value's fields are ignored.
1732	/// Per-call parameters belong in `dimensions`, not in the hint's fields.
1733	///
1734	/// # Panics
1735	///
1736	/// Panics if the declared arity or a dimension exceeds what the witness bytecode encodes.
1737	///
1738	/// Panics if `inputs.len()` does not match the hint's declared input arity.
1739	/// Panics if another hint name already holds this hint's id.
1740	pub fn call_hint<T: Hint>(&self, hint: T, dimensions: &[usize], inputs: &[Wire]) -> Vec<Wire> {
1741		let (n_in, n_out) = hint.shape(dimensions);
1742
1743		// A hint instruction names each of these in four bytes.
1744		let max = BytecodeBuilder::MAX_HINT_FIELD;
1745		assert!(
1746			n_in <= max
1747				&& n_out <= max
1748				&& dimensions.len() <= max
1749				&& dimensions.iter().all(|&dim| dim <= max),
1750			"call_hint: hint {} declares a shape the witness bytecode cannot encode \
1751			 ({n_in} inputs, {n_out} outputs, dimensions {dimensions:?}; each is capped at {max})",
1752			T::NAME,
1753		);
1754
1755		assert_eq!(
1756			inputs.len(),
1757			n_in,
1758			"call_hint: input arity mismatch for hint {} (expected {}, got {})",
1759			T::NAME,
1760			n_in,
1761			inputs.len(),
1762		);
1763
1764		let mut shared = self.shared.borrow_mut();
1765		let hint_id = shared.hint_registry.register(hint);
1766		let outputs: Vec<Wire> = (0..n_out).map(|_| shared.graph.add_internal()).collect();
1767		shared.graph.emit_hint_gate(
1768			self.current_path,
1769			hint_id,
1770			dimensions,
1771			inputs.iter().copied(),
1772			outputs.iter().copied(),
1773		);
1774
1775		outputs
1776	}
1777
1778	/// 64-bit unsigned integer addition, returning the sum and carry-out.
1779	///
1780	/// Addition with a carry-in is the general primitive.
1781	/// Plain addition is the special case where the carry-in is zero.
1782	///
1783	/// Returns `(sum, cout)` where:
1784	///
1785	/// - `sum` is the 64-bit result `a + b`.
1786	/// - `cout` has a set bit at every position where a carry occurred.
1787	///
1788	/// # Cost
1789	///
1790	/// - 1 AND constraint,
1791	/// - 1 linear constraint.
1792	pub fn iadd(&self, a: Wire, b: Wire) -> (Wire, Wire) {
1793		// Zero carry-in: the MSB of `cin` is the carry bit, and zero carries nothing.
1794		let cin = self.add_constant_64(0);
1795		self.iadd_cin_cout(a, b, cin)
1796	}
1797
1798	/// 64-bit × 64-bit → 128-bit signed multiplication.
1799	///
1800	/// Handles two's complement operands, including overflow cases.
1801	///
1802	/// Returns `(hi, lo)` where the signed product equals `(hi << 64) | lo`.
1803	///
1804	/// The high word is the sign extension of the product.
1805	pub fn smul(&self, a: Wire, b: Wire) -> (Wire, Wire) {
1806		smul64(self, a, b)
1807	}
1808}
1809
1810/// The compile-time value of a wire, when it is a constant.
1811///
1812/// Returns `None` for a wire whose value is only known at proving time.
1813/// Takes the graph by reference, so a caller already holding a borrow can call this directly.
1814fn const_of(graph: &GateGraph, wire: Wire) -> Option<Word> {
1815	match graph.wires[wire] {
1816		WireKind::Constant(word) => Some(word),
1817		_ => None,
1818	}
1819}