Skip to main content

binius_frontend/compiler/
mod.rs

1// Copyright 2025-2026 The Binius Developers
2// Copyright 2025 Irreducible Inc.
3use std::{
4	cell::{RefCell, RefMut},
5	rc::Rc,
6};
7
8use binius_core::{constraint_system::ConstraintSystem, word::Word};
9use cranelift_entity::EntitySet;
10
11use crate::compiler::{
12	circuit::Circuit,
13	constraint_builder::ConstraintBuilder,
14	gate_graph::{GateGraph, WireKind},
15	hints::{
16		BigUintDivideHint, BigUintModPowHint, Hint, HintRegistry, ModInverseHint,
17		Secp256k1EndosplitHint,
18	},
19	pathspec::PathSpec,
20};
21
22mod gate;
23use gate::Opcode;
24
25pub mod circuit;
26pub mod const_prop;
27pub mod constraint_builder;
28mod dump;
29pub mod eval_form;
30mod gate_fusion;
31mod gate_graph;
32pub mod hints;
33mod pathspec;
34#[cfg(test)]
35mod tests;
36mod value_vec_alloc;
37
38pub use gate_graph::Wire;
39
40/// Options for the compiler.
41pub(crate) struct Options {
42	enable_gate_fusion: bool,
43	enable_constant_propagation: bool,
44}
45
46// Shut up clippy since this is just so happens to be derivable for now.
47#[allow(clippy::derivable_impls)]
48impl Default for Options {
49	fn default() -> Self {
50		Self {
51			enable_gate_fusion: true,
52			enable_constant_propagation: false,
53		}
54	}
55}
56
57impl Options {
58	fn from_env() -> Self {
59		// This is a very temporary solution for now.
60		//
61		// We do not expect those feature sets to soak here for too long neither we expect that
62		// the features are going to be detected using the environment variables.
63		let mut opts = Self::default();
64		if std::env::var("MONBIJOU_CONSTPROP").is_ok() {
65			opts.enable_constant_propagation = true;
66		}
67		opts
68	}
69}
70
71pub(crate) struct Shared {
72	pub(crate) graph: GateGraph,
73	pub(crate) opts: Options,
74	pub(crate) force_committed: EntitySet<Wire>,
75	pub(crate) hint_registry: HintRegistry,
76}
77
78/// Circuit builder for constructing zero-knowledge proof circuits.
79///
80/// `CircuitBuilder` provides the primary interface for constructing circuits in the Binius64
81/// proof system. The builder compiles imperative gate operations into a constraint system
82/// suitable for zero-knowledge proof generation.
83///
84/// # Circuit Model
85///
86/// A circuit represents computation as a directed acyclic graph where 64-bit values flow
87/// through gates via **wires**. Gates transform input wires to produce output wires.
88/// Methods like [`band`] and [`iadd_32`] add gates to the graph and return handles
89/// to output wires.
90///
91/// During [`build`], the gate graph compiles into AND and MUL constraints
92/// that the proof system operates on directly.
93///
94/// # Wire Types
95///
96/// Wires are handles to 64-bit values that exist during proof generation.
97/// During circuit construction, wires represent value placeholders.
98///
99/// **Constants** - Values known at compile time. Zero constraint cost as both prover
100/// and verifier know these values. Created with [`add_constant`].
101///
102/// **Public inputs/outputs** - Values visible to both prover and verifier.
103/// Form part of the proof statement (e.g., hash output in a preimage proof).
104/// Created with [`add_inout`](Self::add_inout).
105///
106/// **Private witnesses** - Values known only to the prover.
107/// The circuit proves knowledge of these values without revealing them
108/// (e.g., preimage in a hash proof). Created with [`add_witness`](Self::add_witness).
109///
110/// **Internal wires** - Created automatically by gate operations.
111/// Represent intermediate computation values.
112///
113/// # MSB-Boolean Convention
114///
115/// Boolean values encode in the most significant bit (bit 63) of a 64-bit word.
116/// MSB = 1 represents true, MSB = 0 represents false.
117/// The lower 63 bits are "don't care" values.
118///
119/// # Constraint Costs
120///
121/// **AND constraints** - Baseline unit of cost. Bitwise operations and comparisons
122/// generate 1-2 AND constraints.
123///
124/// **MUL constraints** - 64-bit multiplication costs ~3-4× more than AND constraints.
125///
126/// **Committed values** - Each public input/output and witness adds to proof size
127/// (~0.2× of an AND constraint).
128///
129/// **Linear operations** - XOR and shifts generate virtual linear constraints.
130/// During compilation these either:
131/// - Fuse into adjacent non-linear gates (near-zero cost)
132/// - Materialize as AND constraints
133///
134/// Gate fusion inlines compatible XOR expressions and shifts into existing AND gates.
135/// Incompatible operations (e.g., right shift into left shift) and heuristic limits
136/// prevent some fusions. XORs typically cost <0.1× of an AND constraint,
137/// shifts slightly more.
138///
139/// # Compilation
140///
141/// The builder uses reference-counted sharing internally. [`subcircuit`] returns
142/// a builder referencing the same graph with hierarchical naming.
143///
144/// [`build`] triggers compilation:
145/// 1. Validates the circuit structure
146/// 2. Runs optimization passes (constant propagation, gate fusion)
147/// 3. Generates the final constraint system
148///
149/// [`build`] consumes internal state and can only be called once per builder instance.
150///
151/// [`add_constant`]: Self::add_constant
152/// [`add_inout`]: Self::add_inout
153/// [`add_witness`]: Self::add_witness
154/// [`band`]: Self::band
155/// [`build`]: Self::build
156/// [`iadd_32`]: Self::iadd_32
157/// [`subcircuit`]: Self::subcircuit
158#[derive(Clone)]
159pub struct CircuitBuilder {
160	/// Current path at which this circuit builder is positioned.
161	current_path: PathSpec,
162	shared: Rc<RefCell<Option<Shared>>>,
163}
164
165impl Default for CircuitBuilder {
166	fn default() -> Self {
167		CircuitBuilder::new()
168	}
169}
170
171#[warn(missing_docs)]
172impl CircuitBuilder {
173	/// Create a new circuit builder with default options.
174	pub fn new() -> Self {
175		let opts = Options::from_env();
176		Self::with_opts(opts)
177	}
178
179	pub(crate) fn with_opts(opts: Options) -> Self {
180		let graph = GateGraph::new();
181		let root = graph.path_spec_tree.root();
182		CircuitBuilder {
183			current_path: root,
184			shared: Rc::new(RefCell::new(Some(Shared {
185				graph,
186				opts,
187				force_committed: EntitySet::new(),
188				hint_registry: HintRegistry::new(),
189			}))),
190		}
191	}
192
193	/// Returns the circuit built by this builder.
194	///
195	/// Note that cloning the circuit builder only clones the reference and as such is treated
196	/// as a shallow copy.
197	///
198	/// # Preconditions
199	///
200	/// Must be called only once.
201	pub fn build(&self) -> Circuit {
202		let all_one = self.add_constant(Word::ALL_ONE);
203		let shared = self.shared.borrow_mut().take();
204
205		let Some(shared) = shared else {
206			panic!("CircuitBuilder::build called twice");
207		};
208		let mut graph = shared.graph;
209
210		graph.validate(&shared.hint_registry);
211
212		// Run constant propagation optimization
213		if shared.opts.enable_constant_propagation {
214			let replaced = const_prop::constant_propagation(&mut graph, &shared.hint_registry);
215			if replaced > 0 {
216				eprintln!("Constant propagation: replaced {} wires with constants", replaced);
217			}
218		}
219
220		let mut builder = ConstraintBuilder::new();
221		for (gate_id, _) in graph.gates.iter() {
222			gate::constrain(gate_id, &graph, &mut builder);
223		}
224
225		// Perform fusion if the corresponding feature flag is turned on.
226		if shared.opts.enable_gate_fusion {
227			gate_fusion::run_pass(&mut builder, &shared.force_committed, all_one);
228		}
229
230		let constrained_wires = builder.mark_used_wires();
231
232		// Allocate a place for each wire in the value vec layout.
233		//
234		// This gives us mappings from wires into the value indices, as well as the constant
235		// portion of the value vec.
236		let value_vec_alloc::Assignment {
237			wire_mapping,
238			value_vec_layout,
239			constants,
240		} = {
241			let mut value_vec_alloc = value_vec_alloc::Alloc::new();
242			for (wire, wire_data) in graph.wires.iter() {
243				match wire_data.kind {
244					WireKind::Constant(ref value) => {
245						value_vec_alloc.add_constant(wire, *value);
246					}
247					WireKind::Inout => value_vec_alloc.add_inout(wire),
248					WireKind::Witness => value_vec_alloc.add_witness(wire),
249					WireKind::Internal | WireKind::Scratch => {
250						// Unlike inout and witness those two are not declared by the user and thus
251						// are not required to appear in the value vec.
252						//
253						// Therefore, we ignore the initial designation internal <=> scratch and
254						// instead we look whether a wire is referenced in the constraint system
255						// or not. If it is referenced then we declare it as internal and put into
256						// the private section (witness). If it's not referenced we declare it as
257						// a scratch value.
258						//
259						// Note that the concept of wire kind outlived it's lifetime and should be
260						// reworked. This is left for the future.
261						if constrained_wires.contains(wire) {
262							value_vec_alloc.add_internal(wire);
263						} else {
264							value_vec_alloc.add_scratch(wire);
265						}
266					}
267				}
268			}
269			value_vec_alloc.into_assignment()
270		};
271		let (and_constraints, mul_constraints) = builder.build(&wire_mapping, all_one);
272
273		let cs =
274			ConstraintSystem::new(constants, value_vec_layout, and_constraints, mul_constraints);
275		if cfg!(debug_assertions) {
276			// Validate that the resulting constraint system has a good shape.
277			cs.validate().unwrap();
278		}
279
280		// Build evaluation form (consumes the hint registry the user populated via call_hint).
281		let eval_form = eval_form::EvalForm::build(&graph, &wire_mapping, shared.hint_registry);
282
283		Circuit::new(graph, cs, wire_mapping, eval_form)
284	}
285
286	/// Creates a reference to the same underlying circuit builder that is namespaced to the
287	/// given name.
288	///
289	/// This is useful for creating subcircuits within a larger circuit.
290	///
291	/// Note that this is the same builder instance, but with a different namespace, and that means
292	/// calling [`Self::build`] on the returned builder is going to build the whole circuit.
293	pub fn subcircuit(&self, name: impl Into<String>) -> CircuitBuilder {
294		let nested_path = self
295			.graph_mut()
296			.path_spec_tree
297			.extend(self.current_path, name);
298		CircuitBuilder {
299			current_path: nested_path,
300			shared: self.shared.clone(),
301		}
302	}
303
304	/// Force commit the given wire.
305	///
306	/// This annotate the wire to be forcefully committed. This instructs optimization passes
307	/// (ATOW only gate fusion) to forcibly materialize wire.
308	pub fn force_commit(&self, wire: Wire) {
309		self.shared
310			.borrow_mut()
311			.as_mut()
312			.unwrap()
313			.force_committed
314			.insert(wire);
315	}
316
317	fn graph_mut(&self) -> RefMut<'_, GateGraph> {
318		RefMut::map(self.shared.borrow_mut(), |shared| &mut shared.as_mut().unwrap().graph)
319	}
320
321	/// Creates a wire from a 64-bit word.
322	///
323	/// # Arguments
324	/// * `word` -  The word to add to the circuit.
325	///
326	/// # Returns
327	/// A `Wire` representing the constant value. The wire might be aliased because the constants
328	/// are deduplicated.
329	///
330	/// # Cost
331	///
332	/// Constants have no constraint cost - they are "free" in the circuit.
333	pub fn add_constant(&self, word: Word) -> Wire {
334		self.graph_mut().add_constant(word)
335	}
336
337	/// Creates a constant wire from a 64-bit unsigned integer.
338	///
339	/// This method adds a 64-bit constant value to the circuit. The constant is stored
340	/// as a `Word` and can be used in constraints and operations.
341	///
342	/// Constants are automatically deduplicated - multiple calls with the same value
343	/// will return the same wire.
344	///
345	/// # Arguments
346	/// * `c` - The 64-bit constant value to add to the circuit
347	///
348	/// # Returns
349	/// A `Wire` representing the constant value that can be used in circuit operations
350	pub fn add_constant_64(&self, c: u64) -> Wire {
351		self.add_constant(Word(c))
352	}
353
354	/// Creates a constant wire from an 8-bit value, zero-extended to 64 bits.
355	///
356	/// This method takes an 8-bit unsigned integer (byte) and zero-extends it to
357	/// a 64-bit value before adding it as a constant to the circuit. The resulting
358	/// wire contains the byte value in the lower 8 bits and zeros in the upper 56 bits.
359	/// This is commonly used for byte constants in circuits that process byte data.
360	///
361	/// # Arguments
362	/// * `c` - The 8-bit constant value (0-255) to add to the circuit
363	pub fn add_constant_zx_8(&self, c: u8) -> Wire {
364		self.add_constant(Word(c as u64))
365	}
366
367	/// Creates a public input/output wire.
368	///
369	/// Public wires form part of the proof statement and are visible to both prover and verifier.
370	/// They are committed in the public section of the value vector alongside constants.
371	///
372	/// The wire must be manually assigned a value using [`WitnessFiller`] before circuit
373	/// evaluation.
374	///
375	/// [`WitnessFiller`]: crate::compiler::circuit::WitnessFiller
376	pub fn add_inout(&self) -> Wire {
377		self.graph_mut().add_inout()
378	}
379
380	/// Creates a private input wire.
381	///
382	/// Private wires contain secret values known only to the prover. They are placed in the
383	/// private section of the value vector and are not revealed to the verifier.
384	///
385	/// The wire must be manually assigned a value using [`WitnessFiller`] before circuit
386	/// evaluation.
387	///
388	/// [`WitnessFiller`]: crate::compiler::circuit::WitnessFiller
389	pub fn add_witness(&self) -> Wire {
390		self.graph_mut().add_witness()
391	}
392
393	/// Adds a wire similar to [`Self::add_witness`]. Internal wires are meant to designate wires
394	/// that are prunable.
395	fn add_internal(&self) -> Wire {
396		self.graph_mut().add_internal()
397	}
398
399	/// Bitwise AND.
400	///
401	/// Returns z = x & y
402	///
403	/// # Cost
404	///
405	/// 1 AND constraint.
406	pub fn band(&self, x: Wire, y: Wire) -> Wire {
407		let z = self.add_internal();
408		let mut graph = self.graph_mut();
409		graph.emit_gate(self.current_path, Opcode::Band, [x, y], [z]);
410		z
411	}
412
413	/// Bitwise XOR.
414	///
415	/// Returns z = x ^ y
416	///
417	/// # Cost
418	///
419	/// 1 linear constraint.
420	pub fn bxor(&self, a: Wire, b: Wire) -> Wire {
421		let z = self.add_internal();
422		let mut graph = self.graph_mut();
423		graph.emit_gate(self.current_path, Opcode::Bxor, [a, b], [z]);
424		z
425	}
426
427	/// Multi-way bitwise XOR operation.
428	///
429	/// Takes a variable-length slice of wires and XORs them all together.
430	///
431	/// Returns z = i ^ j ^ k ^ ...
432	///
433	/// # Cost
434	///
435	/// 1 linear constraint.
436	pub fn bxor_multi(&self, wires: &[Wire]) -> Wire {
437		assert!(!wires.is_empty(), "bxor_multi requires at least one input");
438
439		if wires.len() == 1 {
440			return wires[0];
441		}
442
443		if wires.len() == 2 {
444			return self.bxor(wires[0], wires[1]);
445		}
446
447		let z = self.add_internal();
448		let mut graph = self.graph_mut();
449		graph.emit_gate_generic(
450			self.current_path,
451			Opcode::BxorMulti,
452			wires.iter().copied(),
453			[z],
454			&[wires.len()],
455			&[],
456		);
457		z
458	}
459
460	/// Bitwise Not
461	///
462	/// Returns z = ~x
463	///
464	/// # Cost
465	///
466	/// 1 linear constraint.
467	pub fn bnot(&self, a: Wire) -> Wire {
468		let all_one = self.add_constant(Word::ALL_ONE);
469		self.bxor(a, all_one)
470	}
471
472	/// Bitwise OR.
473	///
474	/// Returns z = x | y
475	///
476	/// # Cost
477	///
478	/// 1 AND constraint.
479	pub fn bor(&self, a: Wire, b: Wire) -> Wire {
480		let z = self.add_internal();
481		let mut graph = self.graph_mut();
482		graph.emit_gate(self.current_path, Opcode::Bor, [a, b], [z]);
483		z
484	}
485
486	/// Fused AND-XOR operation.
487	///
488	/// Computes (x & y) ^ w in a single gate.
489	///
490	/// Returns z = (x & y) ^ w
491	///
492	/// # Cost
493	///
494	/// 1 AND constraint.
495	pub fn fax(&self, x: Wire, y: Wire, w: Wire) -> Wire {
496		let z = self.add_internal();
497		let mut graph = self.graph_mut();
498		graph.emit_gate(self.current_path, Opcode::Fax, [x, y, w], [z]);
499		z
500	}
501
502	/// Parallel 32-bit integer addition.
503	///
504	/// Performs simultaneous independent 32-bit additions on the upper and lower halves,
505	/// discarding the carry-out.
506	///
507	/// # Cost
508	///
509	/// 1 AND constraint, 1 linear constraint.
510	pub fn iadd_32(&self, a: Wire, b: Wire) -> Wire {
511		let sum = self.add_internal();
512		let cout = self.add_internal();
513		let mut graph = self.graph_mut();
514		graph.emit_gate(self.current_path, Opcode::Iadd32, [a, b], [sum, cout]);
515		sum
516	}
517
518	/// Parallel 32-bit integer addition with carry-in and carry-out.
519	///
520	/// Performs simultaneous independent 32-bit additions on the upper and lower halves
521	/// of the 64-bit word, with per-half carry-in and carry-out.
522	///
523	/// The carry-in for each half is taken from the MSB of that half in `cin`:
524	/// bit 31 for the lower half, bit 63 for the upper half. The carry-out
525	/// is a full carry word where bit 31 and bit 63 indicate the carry-out
526	/// of the lower and upper halves respectively.
527	///
528	/// # Cost
529	///
530	/// 1 AND constraint, 1 linear constraint.
531	pub fn iadd32_cin_cout(&self, a: Wire, b: Wire, cin: Wire) -> (Wire, Wire) {
532		let sum = self.add_internal();
533		let cout = self.add_internal();
534		let mut graph = self.graph_mut();
535		graph.emit_gate(self.current_path, Opcode::Iadd32CinCout, [a, b, cin], [sum, cout]);
536		(sum, cout)
537	}
538
539	/// 64-bit integer addition returning the sum and carry-out.
540	///
541	/// # Cost
542	///
543	/// - 1 AND constraint,
544	/// - 1 linear constraint.
545	pub fn iadd(&self, a: Wire, b: Wire) -> (Wire, Wire) {
546		let sum = self.add_internal();
547		let cout = self.add_internal();
548		let mut graph = self.graph_mut();
549		graph.emit_gate(self.current_path, Opcode::Iadd, [a, b], [sum, cout]);
550		(sum, cout)
551	}
552
553	/// 64-bit integer addition with carry input and output.
554	///
555	/// Performs full 64-bit unsigned addition of two wires plus a carry input.
556	///
557	/// Returns `(sum, carry_out)` where:
558	///
559	/// - `sum` is the 64-bit result and
560	/// - `carry_out` is a 64-bit word where every bit position with a carry is set to 1.
561	///
562	/// # Cost
563	///
564	/// - 1 AND constraint,
565	/// - 1 linear constraint.
566	pub fn iadd_cin_cout(&self, a: Wire, b: Wire, cin: Wire) -> (Wire, Wire) {
567		let sum = self.add_internal();
568		let cout = self.add_internal();
569		let mut graph = self.graph_mut();
570		graph.emit_gate(self.current_path, Opcode::IaddCinCout, [a, b, cin], [sum, cout]);
571		(sum, cout)
572	}
573
574	/// 64-bit subtraction with borrow input and output.
575	///
576	/// Performs full 64-bit unsigned subtraction of two wires plus a borrow input.
577	///
578	/// Returns `(diff, borrow_out)` where:
579	///
580	/// - `diff` is the 64-bit result and
581	/// - `borrow_out` is a 64-bit word where every bit position with a borrow is set to 1.
582	///
583	/// # Cost
584	///
585	/// - 1 AND constraint,
586	/// - 1 linear constraint.
587	pub fn isub_bin_bout(&self, a: Wire, b: Wire, bin: Wire) -> (Wire, Wire) {
588		let diff = self.add_internal();
589		let bout = self.add_internal();
590		let mut graph = self.graph_mut();
591		graph.emit_gate(self.current_path, Opcode::IsubBinBout, [a, b, bin], [diff, bout]);
592		(diff, bout)
593	}
594
595	/// 32-bit half-wise rotate left.
596	///
597	/// Rotates the upper and lower 32-bit halves left independently by `n`.
598	/// Bits do not cross the 32-bit lane boundary.
599	///
600	/// Returns `x ROTL32 n`
601	///
602	/// # Panics
603	///
604	/// Panics if n ≥ 32.
605	///
606	/// # Cost
607	///
608	/// 1 AND constraint (0 if n = 0).
609	pub fn rotl32(&self, x: Wire, n: u32) -> Wire {
610		assert!(n < 32, "rotate amount n={n} out of range");
611		if n == 0 {
612			return x;
613		}
614		let z = self.add_internal();
615		let mut graph = self.graph_mut();
616		graph.emit_gate_imm(self.current_path, Opcode::Rotr32, [x], [z], 32 - n);
617		z
618	}
619
620	/// 32-bit half-wise rotate right.
621	///
622	/// Rotates the upper and lower 32-bit halves right independently by `n`.
623	/// Bits do not cross the 32-bit lane boundary.
624	///
625	/// Returns `x ROTR32 n`
626	///
627	/// # Panics
628	///
629	/// Panics if n ≥ 32.
630	///
631	/// # Cost
632	///
633	/// 1 AND constraint (0 if n = 0).
634	pub fn rotr32(&self, x: Wire, n: u32) -> Wire {
635		assert!(n < 32, "rotate amount n={n} out of range");
636		if n == 0 {
637			return x;
638		}
639
640		let z = self.add_internal();
641		let mut graph = self.graph_mut();
642		graph.emit_gate_imm(self.current_path, Opcode::Rotr32, [x], [z], n);
643		z
644	}
645
646	/// 64-bit rotate left.
647	///
648	/// Rotates a 64-bit value left by n positions. Bits shifted out on the left
649	/// wrap around to the right.
650	///
651	/// Returns `x rotated left by n`
652	///
653	/// # Panics
654	///
655	/// Panics if n ≥ 64.
656	///
657	/// # Cost
658	///
659	/// 1 AND constraint (0 if n = 0).
660	pub fn rotl(&self, x: Wire, n: u32) -> Wire {
661		assert!(n < 64, "rotate amount n={n} out of range");
662		if n == 0 {
663			return x;
664		}
665		let z = self.add_internal();
666		let mut graph = self.graph_mut();
667		graph.emit_gate_imm(self.current_path, Opcode::Rotr, [x], [z], 64 - n);
668		z
669	}
670
671	/// 64-bit rotate right.
672	///
673	/// Rotates a 64-bit value right by n positions. Bits shifted out on the right
674	/// wrap around to the left.
675	///
676	/// Returns `x rotated right by n`
677	///
678	/// # Panics
679	///
680	/// Panics if n ≥ 64.
681	///
682	/// # Cost
683	///
684	/// 1 AND constraint (0 if n = 0).
685	pub fn rotr(&self, x: Wire, n: u32) -> Wire {
686		assert!(n < 64, "rotate amount n={n} out of range");
687		if n == 0 {
688			return x;
689		}
690
691		let z = self.add_internal();
692		let mut graph = self.graph_mut();
693		graph.emit_gate_imm(self.current_path, Opcode::Rotr, [x], [z], n);
694		z
695	}
696
697	/// 32-bit half-wise logical right shift.
698	///
699	/// Shifts the upper and lower 32-bit halves right independently by `n`.
700	/// Bits do not cross the 32-bit lane boundary.
701	///
702	/// Returns `x SRL32 n`
703	///
704	/// # Panics
705	///
706	/// Panics if n ≥ 32.
707	///
708	/// # Cost
709	///
710	/// 1 AND constraint.
711	pub fn srl32(&self, x: Wire, n: u32) -> Wire {
712		assert!(n < 32, "shift amount n={n} out of range");
713
714		let z = self.add_internal();
715		let mut graph = self.graph_mut();
716		graph.emit_gate_imm(self.current_path, Opcode::Srl32, [x], [z], n);
717		z
718	}
719
720	/// 32-bit half-wise logical left shift.
721	///
722	/// Shifts the upper and lower 32-bit halves left independently by `n`.
723	/// Bits do not cross the 32-bit lane boundary.
724	///
725	/// Returns `x SLL32 n`.
726	///
727	/// # Panics
728	///
729	/// Panics if `n ≥ 32`.
730	///
731	/// # Cost
732	///
733	/// 1 AND constraint.
734	pub fn sll32(&self, x: Wire, n: u32) -> Wire {
735		assert!(n < 32, "shift amount n={n} out of range for 32-bit half shift");
736
737		let z = self.add_internal();
738		let mut graph = self.graph_mut();
739		graph.emit_gate_imm(self.current_path, Opcode::Sll32, [x], [z], n);
740		z
741	}
742
743	/// Logical left shift.
744	///
745	/// Shifts a 64-bit wire left by n bits, filling with zeros from the right.
746	///
747	/// Returns a << n
748	///
749	/// # Cost
750	///
751	/// 1 AND constraint.
752	pub fn shl(&self, a: Wire, n: u32) -> Wire {
753		assert!(n < 64, "shift amount n={n} out of range");
754		let z = self.add_internal();
755		let mut graph = self.graph_mut();
756		graph.emit_gate_imm(self.current_path, Opcode::Shl, [a], [z], n);
757		z
758	}
759
760	/// Logical right shift.
761	///
762	/// Shifts a 64-bit wire right by n bits, filling with zeros from the left.
763	///
764	/// Returns a >> n
765	///
766	/// # Cost
767	///
768	/// 1 AND constraint.
769	pub fn shr(&self, a: Wire, n: u32) -> Wire {
770		assert!(n < 64, "shift amount n={n} out of range");
771		let z = self.add_internal();
772		let mut graph = self.graph_mut();
773		graph.emit_gate_imm(self.current_path, Opcode::Shr, [a], [z], n);
774		z
775	}
776
777	/// Arithmetic right shift.
778	///
779	/// Shifts a 64-bit wire right by n bits, filling with the MSB from the left.
780	///
781	/// Returns a SAR n
782	///
783	/// # Cost
784	///
785	/// 1 AND constraint.
786	pub fn sar(&self, a: Wire, n: u32) -> Wire {
787		assert!(n < 64, "shift amount n={n} out of range");
788		let z = self.add_internal();
789		let mut graph = self.graph_mut();
790		graph.emit_gate_imm(self.current_path, Opcode::Sar, [a], [z], n);
791		z
792	}
793
794	/// 32-bit half-wise arithmetic right shift.
795	///
796	/// Shifts the upper and lower 32-bit halves right independently by `n`,
797	/// sign-extending each half from its own bit 31.
798	///
799	/// Returns `x SRA32 n`.
800	///
801	/// # Panics
802	///
803	/// Panics if `n ≥ 32`.
804	///
805	/// # Cost
806	///
807	/// 1 AND constraint.
808	pub fn sra32(&self, a: Wire, n: u32) -> Wire {
809		assert!(n < 32, "shift amount n={n} out of range for 32-bit half shift");
810		let z = self.add_internal();
811		let mut graph = self.graph_mut();
812		graph.emit_gate_imm(self.current_path, Opcode::Sra32, [a], [z], n);
813		z
814	}
815
816	/// Equality assertion.
817	///
818	/// Asserts that two 64-bit wires are equal.
819	///
820	/// Takes wires x and y and enforces x == y.
821	/// If the assertion fails, the circuit will report an error with the given name.
822	///
823	/// # Cost
824	///
825	/// 1 AND constraint.
826	pub fn assert_eq(&self, name: impl Into<String>, x: Wire, y: Wire) {
827		let mut graph = self.graph_mut();
828		let gate = graph.emit_gate(self.current_path, Opcode::AssertEq, [x, y], []);
829		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
830		graph.assertion_names[gate] = path_spec;
831	}
832
833	/// Vector equality assertion.
834	///
835	/// Asserts that two arrays of 64-bit wires are equal element-wise.
836	///
837	/// Takes wire arrays x and y and enforces `x[i] == y[i]` for all `i`.
838	/// Each element assertion is named with the base name and index.
839	///
840	/// # Cost
841	///
842	/// N AND constraints (one per element).
843	pub fn assert_eq_v<const N: usize>(&self, name: impl Into<String>, x: [Wire; N], y: [Wire; N]) {
844		let base_name = name.into();
845		for i in 0..N {
846			self.assert_eq(format!("{base_name}[{i}]"), x[i], y[i]);
847		}
848	}
849
850	/// Asserts that the given wire equals zero.
851	///
852	/// Enforces that `x = 0` exactly. Every bit of the 64-bit value must be zero.
853	///
854	/// # Cost
855	///
856	/// 1 AND constraint.
857	pub fn assert_zero(&self, name: impl Into<String>, x: Wire) {
858		let mut graph = self.graph_mut();
859		let gate = graph.emit_gate(self.current_path, Opcode::AssertZero, [x], []);
860		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
861		graph.assertion_names[gate] = path_spec;
862	}
863
864	/// Asserts that the given wire is not zero.
865	///
866	/// Enforces that `x ≠ 0`. At least one bit must be non-zero.
867	///
868	/// # Cost
869	///
870	/// 1 AND constraint.
871	pub fn assert_non_zero(&self, name: impl Into<String>, x: Wire) {
872		let mut graph = self.graph_mut();
873		let gate = graph.emit_gate(self.current_path, Opcode::AssertNonZero, [x], []);
874		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
875		graph.assertion_names[gate] = path_spec;
876	}
877
878	/// Asserts that the given wire's MSB (Most Significant Bit) is 0.
879	///
880	/// This treats the wire as an MSB-boolean where:
881	/// - MSB = 0 → false (assertion passes)
882	/// - MSB = 1 → true (assertion fails)
883	///
884	/// All bits except the MSB are ignored. This is commonly used with comparison
885	/// results which return MSB-boolean values.
886	///
887	/// # Cost
888	///
889	/// 1 AND constraint.
890	pub fn assert_false(&self, name: impl Into<String>, x: Wire) {
891		let mut graph = self.graph_mut();
892		let gate = graph.emit_gate(self.current_path, Opcode::AssertFalse, [x], []);
893		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
894		graph.assertion_names[gate] = path_spec;
895	}
896
897	/// Asserts that the given wire's MSB (Most Significant Bit) is 1.
898	///
899	/// This treats the wire as an MSB-boolean where:
900	/// - MSB = 1 → true (assertion passes)
901	/// - MSB = 0 → false (assertion fails)
902	///
903	/// All bits except the MSB are ignored. This is commonly used with comparison
904	/// results which return MSB-boolean values.
905	///
906	/// # Cost
907	///
908	/// 1 AND constraint.
909	pub fn assert_true(&self, name: impl Into<String>, x: Wire) {
910		let mut graph = self.graph_mut();
911		let gate = graph.emit_gate(self.current_path, Opcode::AssertTrue, [x], []);
912		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
913		graph.assertion_names[gate] = path_spec;
914	}
915
916	/// 64-bit × 64-bit → 128-bit unsigned multiplication.
917	///
918	/// Performs unsigned integer multiplication of two 64-bit values, producing
919	/// a 128-bit result split into high and low 64-bit words.
920	///
921	/// Returns `(hi, lo)` where `a * b = (hi << 64) | lo`
922	///
923	/// # Cost
924	///
925	/// - 1 MUL constraint,
926	/// - 1 AND constraint (for security check).
927	pub fn imul(&self, a: Wire, b: Wire) -> (Wire, Wire) {
928		let hi = self.add_internal();
929		let lo = self.add_internal();
930		let mut graph = self.graph_mut();
931		graph.emit_gate(self.current_path, Opcode::Imul, [a, b], [hi, lo]);
932		(hi, lo)
933	}
934
935	/// 64-bit × 64-bit → 128-bit signed multiplication.
936	///
937	/// Performs signed integer multiplication of two 64-bit values, producing
938	/// a 128-bit result split into high and low 64-bit words. Correctly handles
939	/// two's complement signed integers including overflow cases.
940	///
941	/// Returns `(hi, lo)` where the signed multiplication result equals `(hi << 64) | lo`.
942	/// The high word is sign-extended based on the product's sign.
943	///
944	/// # Cost
945	///
946	/// - 1 MUL constraint
947	/// - 7 AND constraints (2 for sign corrections, 4 for modular additions, 1 for low word
948	///   equality).
949	pub fn smul(&self, a: Wire, b: Wire) -> (Wire, Wire) {
950		let hi = self.add_internal();
951		let lo = self.add_internal();
952		let mut graph = self.graph_mut();
953		graph.emit_gate(self.current_path, Opcode::Smul, [a, b], [hi, lo]);
954		(hi, lo)
955	}
956
957	/// Conditional equality assertion.
958	///
959	/// Asserts that two 64-bit wires are equal only when a condition is true (MSB = 1).
960	/// When the condition is false (MSB = 0), no constraint is enforced.
961	///
962	/// # Cost
963	///
964	/// 1 AND constraint.
965	pub fn assert_eq_cond(&self, name: impl Into<String>, x: Wire, y: Wire, cond: Wire) {
966		let mut graph = self.graph_mut();
967		let gate = graph.emit_gate(self.current_path, Opcode::AssertEqCond, [x, y, cond], []);
968		let path_spec = graph.path_spec_tree.extend(self.current_path, name);
969		graph.assertion_names[gate] = path_spec;
970	}
971
972	/// Unsigned less-than comparison.
973	///
974	/// Compares two 64-bit wires as unsigned integers.
975	///
976	/// Returns:
977	/// - a wire whose MSB-bool value is true if a < b
978	/// - a wire whose MSB-bool value is false if a ≥ b
979	///
980	/// the non-most-significant bits of the output wire are undefined.
981	///
982	/// # Cost
983	///
984	/// - 1 AND constraint,
985	/// - 1 linear constraint.
986	pub fn icmp_ult(&self, x: Wire, y: Wire) -> Wire {
987		let out_wire = self.add_internal();
988		let mut graph = self.graph_mut();
989		graph.emit_gate(self.current_path, Opcode::IcmpUlt, [x, y], [out_wire]);
990		out_wire
991	}
992
993	/// Unsigned less-than-or-equal comparison.
994	///
995	/// Compares two 64-bit wires as unsigned integers.
996	///
997	/// Returns:
998	/// - a wire whose MSB-bool value is true if x <= y
999	/// - a wire whose MSB-bool value is false if x > y
1000	///
1001	/// the non-most-significant bits of the output wire are undefined.
1002	///
1003	/// # Cost
1004	///
1005	/// - 1 AND constraint,
1006	/// - 1 linear constraint.
1007	pub fn icmp_ule(&self, x: Wire, y: Wire) -> Wire {
1008		// x <= y is equivalent to !(y < x)
1009		let gt = self.icmp_ult(y, x);
1010		self.bnot(gt)
1011	}
1012
1013	/// Unsigned greater-than comparison.
1014	///
1015	/// Compares two 64-bit wires as unsigned integers.
1016	///
1017	/// Returns:
1018	/// - a wire whose MSB-bool value is true if x > y
1019	/// - a wire whose MSB-bool value is false if x <= y
1020	///
1021	/// the non-most-significant bits of the output wire are undefined.
1022	///
1023	/// # Cost
1024	///
1025	/// 1 AND constraint.
1026	pub fn icmp_ugt(&self, x: Wire, y: Wire) -> Wire {
1027		// x > y is equivalent to y < x.
1028		self.icmp_ult(y, x)
1029	}
1030
1031	/// Unsigned greater-than-or-equal comparison.
1032	///
1033	/// Compares two 64-bit wires as unsigned integers.
1034	///
1035	/// Returns:
1036	/// - a wire whose MSB-bool value is true if x >= y
1037	/// - a wire whose MSB-bool value is false if x < y
1038	///
1039	/// the non-most-significant bits of the output wire are undefined.
1040	///
1041	/// # Cost
1042	///
1043	/// - 1 AND constraint,
1044	/// - 1 linear constraint.
1045	pub fn icmp_uge(&self, x: Wire, y: Wire) -> Wire {
1046		// x >= y is equivalent to !(x < y)
1047		let lt = self.icmp_ult(x, y);
1048		self.bnot(lt)
1049	}
1050
1051	/// Equality comparison.
1052	///
1053	/// Compares two 64-bit wires for equality.
1054	///
1055	/// Returns:
1056	/// - a wire whose MSB-bool value is true if a == b
1057	/// - a wire whose MSB-bool value is false if a != b
1058	///
1059	/// the non-most-significant bits of the output wire are undefined.
1060	///
1061	/// # Cost
1062	///
1063	/// 1 AND constraint.
1064	pub fn icmp_eq(&self, x: Wire, y: Wire) -> Wire {
1065		let out_wire = self.add_internal();
1066		let mut graph = self.graph_mut();
1067		graph.emit_gate(self.current_path, Opcode::IcmpEq, [x, y], [out_wire]);
1068		out_wire
1069	}
1070
1071	/// Inequality comparison.
1072	///
1073	/// Compares two 64-bit wires for inequality.
1074	///
1075	/// Returns:
1076	/// - a wire whose MSB-bool value is true if a != b
1077	/// - a wire whose MSB-bool value is false if a == b
1078	///
1079	/// the non-most-significant bits of the output wire are undefined.
1080	///
1081	/// # Cost
1082	///
1083	/// - 1 AND constraint,
1084	/// - 1 linear constraint.
1085	pub fn icmp_ne(&self, x: Wire, y: Wire) -> Wire {
1086		let eq = self.icmp_eq(x, y);
1087		self.bnot(eq)
1088	}
1089
1090	/// Byte extraction.
1091	///
1092	/// Extracts byte j from a 64-bit word (j=0 is least significant byte).
1093	///
1094	/// Returns the extracted byte (0-255) in the low 8 bits, with high 56 bits zero.
1095	///
1096	/// # Panics
1097	///
1098	/// Panics if j is greater than or equal to 8.
1099	///
1100	/// # Cost
1101	///
1102	/// - 1 AND constraint,
1103	/// - 1 linear constraint.
1104	pub fn extract_byte(&self, word: Wire, j: u32) -> Wire {
1105		assert!(j < 8, "byte index j={j} out of range");
1106
1107		// To extract the byte j out of 8 we want to generate a mask that will zero out all bits
1108		// except the ones in the j-th byte and then shift it to the rightmost position. We used
1109		// to have a gate for this but it's not necessary.
1110		let shift = j * 8;
1111		let mask = self.add_constant_64(0xff << shift);
1112		let masked = self.band(word, mask);
1113		self.shr(masked, shift)
1114	}
1115
1116	/// Select operation.
1117	///
1118	/// Returns `t` if `cond` is true (MSB-bit set), otherwise returns `f`.
1119	///
1120	/// # Cost
1121	///
1122	/// 1 AND constraint.
1123	pub fn select(&self, cond: Wire, t: Wire, f: Wire) -> Wire {
1124		let out = self.add_internal();
1125		let mut graph = self.graph_mut();
1126		graph.emit_gate(self.current_path, Opcode::Select, [cond, t, f], [out]);
1127		out
1128	}
1129
1130	/// Invoke a [`Hint`] and emit the corresponding gate.
1131	///
1132	/// Registers `hint` in the builder's hint registry (idempotent, keyed by `T::NAME`),
1133	/// allocates output wires according to `hint.shape(dimensions)`, and emits a
1134	/// generic hint gate. Returns the freshly allocated output wires.
1135	///
1136	/// `dimensions` is passed verbatim to [`Hint::shape`] and [`Hint::execute`]; it is the
1137	/// hint's parameterization (e.g., limb counts for a bignum hint).
1138	///
1139	/// # Panics
1140	///
1141	/// Panics if `inputs.len()` does not match the hint's declared input arity.
1142	pub fn call_hint<T: Hint>(&self, hint: T, dimensions: &[usize], inputs: &[Wire]) -> Vec<Wire> {
1143		let (n_in, n_out) = hint.shape(dimensions);
1144		assert_eq!(
1145			inputs.len(),
1146			n_in,
1147			"call_hint: input arity mismatch for hint {} (expected {}, got {})",
1148			T::NAME,
1149			n_in,
1150			inputs.len(),
1151		);
1152
1153		let hint_id = self
1154			.shared
1155			.borrow_mut()
1156			.as_mut()
1157			.expect("CircuitBuilder used after build")
1158			.hint_registry
1159			.register(hint);
1160
1161		let outputs: Vec<Wire> = (0..n_out).map(|_| self.add_internal()).collect();
1162
1163		let mut graph = self.graph_mut();
1164		graph.emit_hint_gate(
1165			self.current_path,
1166			hint_id,
1167			dimensions,
1168			inputs.iter().copied(),
1169			outputs.iter().copied(),
1170		);
1171
1172		outputs
1173	}
1174
1175	/// BigUint division.
1176	///
1177	/// Returns `(quotient, remainder)` of the division of `dividend` by `divisor`.
1178	///
1179	/// This is a hint - a deterministic computation that happens only on the prover side.
1180	/// The result should be additionally constrained by using bignum circuits to check that
1181	/// `remainder + divisor * quotient == dividend`.
1182	pub fn biguint_divide_hint(
1183		&self,
1184		dividend: &[Wire],
1185		divisor: &[Wire],
1186	) -> (Vec<Wire>, Vec<Wire>) {
1187		let inputs: Vec<Wire> = dividend.iter().chain(divisor).copied().collect();
1188		let mut out =
1189			self.call_hint(BigUintDivideHint::new(), &[dividend.len(), divisor.len()], &inputs);
1190		let remainder = out.split_off(dividend.len());
1191		(out, remainder)
1192	}
1193
1194	/// Modular exponentiation.
1195	///
1196	/// Computes `(base^exp) % modulus`.
1197	/// This is a hint - a deterministic computation that happens only on the prover side.
1198	/// The result should be additionally constrained using bignum circuits.
1199	pub fn biguint_mod_pow_hint(&self, base: &[Wire], exp: &[Wire], modulus: &[Wire]) -> Vec<Wire> {
1200		let inputs: Vec<Wire> = base.iter().chain(exp).chain(modulus).copied().collect();
1201		self.call_hint(BigUintModPowHint::new(), &[base.len(), exp.len(), modulus.len()], &inputs)
1202	}
1203
1204	/// Modular inverse.
1205	///
1206	/// Computes the modular inverse of `base` modulo `modulus`.
1207	/// Returns a pair `(quotient, inverse)` where both numbers are Bézout coefficients when
1208	/// `base` and `modulus` are coprime. Both numbers are set to zero if `gcd(base, modulus) > 1`.
1209	///
1210	/// This is a hint - a deterministic computation that happens only on the prover side.
1211	/// The result should be additionally constrained by using bignum circuits to check that
1212	/// `base * inverse = 1 + quotient * modulus`.
1213	pub fn mod_inverse_hint(&self, base: &[Wire], modulus: &[Wire]) -> (Vec<Wire>, Vec<Wire>) {
1214		let inputs: Vec<Wire> = base.iter().chain(modulus).copied().collect();
1215		let mut out = self.call_hint(ModInverseHint::new(), &[base.len(), modulus.len()], &inputs);
1216		let inverse = out.split_off(modulus.len());
1217		(out, inverse)
1218	}
1219
1220	/// Secp256k1 endomorphism split
1221	///
1222	/// The curve has an endomorphism `λ (x, y) = (βx, y)` where `λ³=1 (mod n)`
1223	/// and `β³=1 (mod p)` (`n` being the scalar field modulus and `p` coordinate field one).
1224	///
1225	/// For a 256-bit scalar `k` it is possible to split it into `k1` and `k2` such that
1226	/// `k1 + λ k2 = k (mod n)` and both `k1` and `k2` are no farther than `2^128` from zero.
1227	///
1228	/// The `k` scalar is represented by four 64-bit limbs in little endian order. The return value
1229	/// is quadruple of `(k1_neg, k2_neg, k1_abs, k2_abs)` where `k1_neg` and `k2_neg` are
1230	/// MSB-bools indicating whether `k1_abs` or `k2_abs`, respectively, should be negated.
1231	/// `k1_abs` and `k2_abs` are at most 128 bits and are represented with two 64-bit limbs.
1232	/// When `k` cannot be represented in this way (any valid scalar can, so it has to be modulus
1233	/// or above), both `k1_abs` and `k2_abs` are assigned zero values.
1234	///
1235	/// This is a hint - a deterministic computation that happens only on the prover side.
1236	/// The result should be additionally constrained by using bignum circuits to check that
1237	/// `k1 + λ k2 = k (mod n)`.
1238	pub fn secp256k1_endomorphism_split_hint(
1239		&self,
1240		k: &[Wire],
1241	) -> (Wire, Wire, [Wire; 2], [Wire; 2]) {
1242		assert_eq!(k.len(), 4);
1243		let out = self.call_hint(Secp256k1EndosplitHint::new(), &[], k);
1244		let [k1_neg, k2_neg, k1_abs0, k1_abs1, k2_abs0, k2_abs1] = out.as_slice() else {
1245			panic!("Secp256k1EndosplitHint must return 6 wires");
1246		};
1247		(*k1_neg, *k2_neg, [*k1_abs0, *k1_abs1], [*k2_abs0, *k2_abs1])
1248	}
1249}