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}