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}