From ea95dfddc973e0b3951bb9c53da4ab08ee6521fe Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Wed, 5 Nov 2025 11:55:32 +0100 Subject: [PATCH 01/13] Fill witness hints --- circuit/Cargo.toml | 1 + circuit/src/builder/circuit_builder.rs | 91 ++++++++- .../builder/compiler/expression_lowerer.rs | 85 +++++++-- circuit/src/builder/compiler/mod.rs | 2 +- circuit/src/builder/errors.rs | 4 + circuit/src/builder/expression_builder.rs | 89 ++++++++- circuit/src/builder/mod.rs | 4 +- circuit/src/builder/public_input_tracker.rs | 56 ------ circuit/src/errors.rs | 3 + circuit/src/expr.rs | 5 +- circuit/src/lib.rs | 2 +- circuit/src/op.rs | 42 ++++- circuit/src/ops/hash.rs | 2 +- circuit/src/tables/runner.rs | 175 +++++++++++++++++- circuit/src/utils.rs | 110 ++++++++--- recursion/src/generation.rs | 20 +- recursion/src/lib.rs | 2 +- recursion/src/pcs/fri/targets.rs | 8 +- recursion/src/public_inputs.rs | 19 -- recursion/src/traits/challenger.rs | 17 +- 20 files changed, 572 insertions(+), 165 deletions(-) delete mode 100644 circuit/src/builder/public_input_tracker.rs diff --git a/circuit/Cargo.toml b/circuit/Cargo.toml index 99bb7e4..92d3052 100644 --- a/circuit/Cargo.toml +++ b/circuit/Cargo.toml @@ -21,6 +21,7 @@ p3-koala-bear.workspace = true p3-matrix.workspace = true p3-symmetric.workspace = true p3-uni-stark.workspace = true +p3-util.workspace = true rand.workspace = true # Other common dependencies diff --git a/circuit/src/builder/circuit_builder.rs b/circuit/src/builder/circuit_builder.rs index 3f557ef..2a4cf9b 100644 --- a/circuit/src/builder/circuit_builder.rs +++ b/circuit/src/builder/circuit_builder.rs @@ -5,10 +5,11 @@ use hashbrown::HashMap; use p3_field::{Field, PrimeCharacteristicRing}; use super::compiler::{ExpressionLowerer, NonPrimitiveLowerer, Optimizer}; -use super::{BuilderConfig, ExpressionBuilder, PublicInputTracker}; +use super::{BuilderConfig, ExpressionBuilder}; use crate::CircuitBuilderError; +use crate::builder::input_tracker::InputTracker; use crate::circuit::Circuit; -use crate::op::NonPrimitiveOpType; +use crate::op::{NonPrimitiveOpType, WitnessFiller}; use crate::ops::MmcsVerifyConfig; use crate::types::{ExprId, NonPrimitiveOpId, WitnessAllocator, WitnessId}; @@ -18,7 +19,7 @@ pub struct CircuitBuilder { expr_builder: ExpressionBuilder, /// Public input tracker - public_tracker: PublicInputTracker, + public_tracker: InputTracker, /// Witness index allocator witness_alloc: WitnessAllocator, @@ -50,7 +51,7 @@ where pub fn new() -> Self { Self { expr_builder: ExpressionBuilder::new(), - public_tracker: PublicInputTracker::new(), + public_tracker: InputTracker::new(), witness_alloc: WitnessAllocator::new(), non_primitive_ops: Vec::new(), config: BuilderConfig::new(), @@ -119,16 +120,27 @@ where self.public_tracker.count() } - /// Allocates a witness hint (uninitialized witness slot set during non-primitive execution). + /// Allocates multiple witness hints. #[must_use] - pub fn alloc_witness_hint(&mut self, label: &'static str) -> ExprId { - self.expr_builder.add_witness_hint(label) + pub fn alloc_witness_hints>( + &mut self, + filler: W, + label: &'static str, + ) -> Vec { + self.expr_builder.add_witness_hints(filler, label) } - /// Allocates multiple witness hints. + /// Allocates multiple witness hints without saying how they should be filled + /// TODO: Remove this function #[must_use] - pub fn alloc_witness_hints(&mut self, count: usize, label: &'static str) -> Vec { - self.expr_builder.add_witness_hints(count, label) + pub fn alloc_witness_hints_no_filler( + &mut self, + count: usize, + label: &'static str, + ) -> Vec { + (0..count) + .map(|_| self.expr_builder.add_witness_hint(label)) + .collect() } /// Adds a constant to the circuit (deduplicated). @@ -327,6 +339,7 @@ where self.expr_builder.graph(), self.expr_builder.pending_connects(), self.public_tracker.count(), + self.expr_builder.witness_hints_with_fillers(), self.witness_alloc, ); let (primitive_ops, public_rows, expr_to_widx, public_mappings, witness_count) = @@ -355,6 +368,9 @@ where #[cfg(test)] mod tests { + use alloc::boxed::Box; + use alloc::vec; + use p3_baby_bear::BabyBear; use super::*; @@ -627,6 +643,61 @@ mod tests { assert_eq!(circuit.witness_count, 2); assert_eq!(circuit.primitive_ops.len(), 2); } + + #[derive(Debug, Clone)] + struct MockFiller { + inputs: Vec, + } + + impl MockFiller { + pub fn new(input: ExprId) -> Self { + Self { + inputs: vec![input], + } + } + } + + impl WitnessFiller for MockFiller { + fn inputs(&self) -> &[ExprId] { + &self.inputs + } + + fn n_outputs(&self) -> usize { + 1 + } + + fn compute_outputs(&self, _inputs_val: Vec) -> Result, crate::CircuitError> { + Ok(vec![F::ONE]) + } + + fn boxed(&self) -> Box> { + Box::new(self.clone()) + } + } + #[test] + fn test_build_with_witness_hint() { + let mut builder = CircuitBuilder::::new(); + let a = builder.add_const(BabyBear::ZERO); + let mock_filler = MockFiller::new(a); + let b = builder.alloc_witness_hints(mock_filler, "a"); + assert_eq!(b.len(), 1); + let circuit = builder + .build() + .expect("Circuit with operations should build"); + + assert_eq!(circuit.witness_count, 2); + assert_eq!(circuit.primitive_ops.len(), 2); + + match &circuit.primitive_ops[1] { + crate::op::Op::Unconstrained { + inputs, outputs, .. + } => { + assert_eq!(*inputs, vec![WitnessId(0)]); + assert_eq!(*outputs, vec![WitnessId(1)]); + } + _ => panic!("Expected Unconstrained at index 0"), + } + } } #[cfg(test)] diff --git a/circuit/src/builder/compiler/expression_lowerer.rs b/circuit/src/builder/compiler/expression_lowerer.rs index 2488c11..fd40fdd 100644 --- a/circuit/src/builder/compiler/expression_lowerer.rs +++ b/circuit/src/builder/compiler/expression_lowerer.rs @@ -1,3 +1,5 @@ +use alloc::boxed::Box; +use alloc::string::ToString; use alloc::vec; use alloc::vec::Vec; @@ -8,6 +10,7 @@ use crate::Op; use crate::builder::CircuitBuilderError; use crate::builder::compiler::get_witness_id; use crate::expr::{Expr, ExpressionGraph}; +use crate::op::WitnessFiller; use crate::types::{ExprId, WitnessAllocator, WitnessId}; /// Sparse disjoint-set "find" with path compression over a HashMap (iterative). @@ -70,10 +73,15 @@ pub struct ExpressionLowerer<'a, F> { /// Number of public inputs public_input_count: usize, + /// The hint witnesses with their respective filler + witness_hints_with_fillers: &'a [HintsWithFiller], + /// Witness allocator witness_alloc: WitnessAllocator, } +pub type HintsWithFiller = (Vec, Box>); + impl<'a, F> ExpressionLowerer<'a, F> where F: Clone + PrimeCharacteristicRing + PartialEq + Eq + core::hash::Hash, @@ -83,12 +91,14 @@ where graph: &'a ExpressionGraph, pending_connects: &'a [(ExprId, ExprId)], public_input_count: usize, + witness_hints_with_fillers: &'a [HintsWithFiller], witness_alloc: WitnessAllocator, ) -> Self { Self { graph, pending_connects, public_input_count, + witness_hints_with_fillers, witness_alloc, } } @@ -162,7 +172,9 @@ where } }; - // Pass B: emit public inputs + // Pass B: emit public inputs and process witness hints + // TODO: We need to process witness hints here in order to get the right + // the right expr_idx for (expr_idx, expr) in self.graph.nodes().iter().enumerate() { if let Expr::Public(pos) = expr { let id = ExprId(expr_idx as u32); @@ -179,18 +191,57 @@ where } } + // Pass C: collect witness hints and emit `Unconstrained` operatios + for (expr_idx, _) in self + .graph + .nodes() + .iter() + .enumerate() + .filter(|(_, expr)| matches!(expr, Expr::Witness)) + { + let expr_id = ExprId(expr_idx as u32); + let out_widx = alloc_witness_id_for_expr(expr_idx); + expr_to_widx.insert(expr_id, out_widx); + } + for (witness_hints, filler) in self.witness_hints_with_fillers.iter() { + let inputs = filler + .inputs() + .iter() + .map(|expr_id| { + expr_to_widx + .get(expr_id) + .ok_or(CircuitBuilderError::MissingExprMapping { + expr_id: *expr_id, + context: "Unconstrained op".to_string(), + }) + .copied() + }) + .collect::, _>>()?; + let outputs = witness_hints + .iter() + .map(|expr_id| { + expr_to_widx + .get(expr_id) + .ok_or(CircuitBuilderError::MissingExprMapping { + expr_id: *expr_id, + context: "Unconstrained op".to_string(), + }) + .copied() + }) + .collect::, _>>()?; + primitive_ops.push(Op::Unconstrained { + inputs, + outputs, + filler: filler.clone(), + }) + } + // Pass C: emit arithmetic ops in creation order; tie outputs to class slot if connected for (expr_idx, expr) in self.graph.nodes().iter().enumerate() { let expr_id = ExprId(expr_idx as u32); match expr { - Expr::Const(_) | Expr::Public(_) => { /* handled above */ } - Expr::Witness => { - // Allocate a fresh witness slot (non-primitive op) - // Allows non-primitive operations to set values during execution that - // are not part of the central Witness bus. - let out_widx = alloc_witness_id_for_expr(expr_idx); - expr_to_widx.insert(expr_id, out_widx); - } + Expr::Const(_) | Expr::Public(_) | Expr::Witness => { /* handled above */ } + Expr::Add { lhs, rhs } => { let out_widx = alloc_witness_id_for_expr(expr_idx); let a_widx = get_witness_id( @@ -394,9 +445,11 @@ mod tests { let quot = graph.add_expr(Expr::Div { lhs: diff, rhs: p2 }); let connects = vec![]; + let witness_hints_with_fillers = vec![]; let alloc = WitnessAllocator::new(); - let lowerer = ExpressionLowerer::new(&graph, &connects, 3, alloc); + let lowerer = + ExpressionLowerer::new(&graph, &connects, 3, &witness_hints_with_fillers, alloc); let (prims, public_rows, expr_map, public_map, witness_count) = lowerer.lower().unwrap(); // Verify Primitives @@ -563,9 +616,11 @@ mod tests { // Group B: p1 ~ p2 ~ p3 (transitive) // Group C: sum ~ p4 (operation result shared) let connects = vec![(c_42, p0), (p1, p2), (p2, p3), (sum, p4)]; + let witness_hints_with_fillers = vec![]; let alloc = WitnessAllocator::new(); - let lowerer = ExpressionLowerer::new(&graph, &connects, 5, alloc); + let lowerer = + ExpressionLowerer::new(&graph, &connects, 5, &witness_hints_with_fillers, alloc); let (prims, public_rows, expr_map, public_map, witness_count) = lowerer.lower().unwrap(); // Verify Primitives @@ -703,8 +758,10 @@ mod tests { }); let connects = vec![]; + let witness_hints_with_fillers = vec![]; let alloc = WitnessAllocator::new(); - let lowerer = ExpressionLowerer::new(&graph, &connects, 0, alloc); + let lowerer = + ExpressionLowerer::new(&graph, &connects, 0, &witness_hints_with_fillers, alloc); let result = lowerer.lower(); assert!(result.is_err()); @@ -724,8 +781,10 @@ mod tests { }); let connects = vec![]; + let witness_hints_with_fillers = vec![]; let alloc = WitnessAllocator::new(); - let lowerer = ExpressionLowerer::new(&graph, &connects, 0, alloc); + let lowerer = + ExpressionLowerer::new(&graph, &connects, 0, &witness_hints_with_fillers, alloc); let result = lowerer.lower(); assert!(result.is_err()); diff --git a/circuit/src/builder/compiler/mod.rs b/circuit/src/builder/compiler/mod.rs index 4080e8f..ab3bcc6 100644 --- a/circuit/src/builder/compiler/mod.rs +++ b/circuit/src/builder/compiler/mod.rs @@ -4,7 +4,7 @@ mod expression_lowerer; mod non_primitive_lowerer; mod optimizer; -pub use expression_lowerer::ExpressionLowerer; +pub use expression_lowerer::{ExpressionLowerer, HintsWithFiller}; use hashbrown::HashMap; pub use non_primitive_lowerer::NonPrimitiveLowerer; pub use optimizer::Optimizer; diff --git a/circuit/src/builder/errors.rs b/circuit/src/builder/errors.rs index 1fff8df..f9449a1 100644 --- a/circuit/src/builder/errors.rs +++ b/circuit/src/builder/errors.rs @@ -23,6 +23,10 @@ pub enum CircuitBuilderError { got: usize, }, + /// Unconstrained op received an unexpected number of input expressions. + #[error("Expects exactly {expected} witness expressions, got {got}")] + UnconstrainedeOpArity { expected: String, got: usize }, + /// Non-primitive operation rejected by the active policy/profile. #[error("Operation {op:?} is not allowed by the current profile")] OpNotAllowed { op: NonPrimitiveOpType }, diff --git a/circuit/src/builder/expression_builder.rs b/circuit/src/builder/expression_builder.rs index ca6fc31..57a0fed 100644 --- a/circuit/src/builder/expression_builder.rs +++ b/circuit/src/builder/expression_builder.rs @@ -1,11 +1,15 @@ +use alloc::boxed::Box; #[cfg(debug_assertions)] use alloc::vec; use alloc::vec::Vec; use hashbrown::HashMap; +use itertools::Itertools; use p3_field::PrimeCharacteristicRing; +use crate::builder::compiler::HintsWithFiller; use crate::expr::{Expr, ExpressionGraph}; +use crate::op::WitnessFiller; use crate::types::ExprId; #[cfg(debug_assertions)] use crate::{AllocationEntry, AllocationType}; @@ -22,6 +26,9 @@ pub struct ExpressionBuilder { /// Equality constraints to enforce at lowering pending_connects: Vec<(ExprId, ExprId)>, + /// The witness hints together with theit witness fillers + witness_hitns_with_fillers: Vec<(Vec, Box>)>, + /// Debug log of all allocations #[cfg(debug_assertions)] allocation_log: Vec, @@ -50,6 +57,7 @@ where graph, const_pool, pending_connects: Vec::new(), + witness_hitns_with_fillers: Vec::new(), #[cfg(debug_assertions)] allocation_log: Vec::new(), #[cfg(debug_assertions)] @@ -100,6 +108,7 @@ where /// Adds a witness hint to the graph. /// It will allocate a `WitnessId` during lowering, with no primitive op. + /// TODO: Make this function private. #[allow(unused_variables)] #[must_use] pub fn add_witness_hint(&mut self, label: &'static str) -> ExprId { @@ -117,10 +126,22 @@ where expr_id } - /// Adds multiple witness hints. + /// Adds a witness hint to the graph. + /// It will allocate a `WitnessId` during lowering, with no primitive op. + #[allow(unused_variables)] #[must_use] - pub fn add_witness_hints(&mut self, count: usize, label: &'static str) -> Vec { - (0..count).map(|_| self.add_witness_hint(label)).collect() + pub fn add_witness_hints>( + &mut self, + filler: W, + label: &'static str, + ) -> Vec { + let n_outputs = filler.n_outputs(); + let expr_ids = (0..n_outputs) + .map(|_| self.graph.add_expr(Expr::Witness)) + .collect_vec(); + self.witness_hitns_with_fillers + .push((expr_ids.clone(), Box::new(filler))); + expr_ids } /// Adds an addition expression to the graph. @@ -208,6 +229,11 @@ where &self.pending_connects } + /// Returns a reference to the witness hints with fillers. + pub fn witness_hints_with_fillers(&self) -> &[HintsWithFiller] { + &self.witness_hitns_with_fillers + } + /// Logs a non-primitive operation allocation. #[cfg(debug_assertions)] pub fn log_non_primitive_op( @@ -285,6 +311,7 @@ where #[cfg(test)] mod tests { use p3_baby_bear::BabyBear; + use p3_field::Field; use super::*; @@ -527,6 +554,55 @@ mod tests { } } + #[derive(Debug, Clone)] + struct IdentityFiller { + inputs: Vec, + n_outputs: usize, + } + + impl IdentityFiller { + pub fn new(inputs: Vec) -> Self { + Self { + n_outputs: inputs.len(), + inputs, + } + } + } + + impl WitnessFiller for IdentityFiller { + fn inputs(&self) -> &[ExprId] { + &self.inputs + } + + fn n_outputs(&self) -> usize { + self.n_outputs + } + + fn compute_outputs(&self, inputs_val: Vec) -> Result, crate::CircuitError> { + Ok(inputs_val) + } + + fn boxed(&self) -> Box> { + Box::new(self.clone()) + } + } + #[test] + fn test_build_with_witness_hint() { + let mut builder = ExpressionBuilder::::new(); + let a = builder.add_const(BabyBear::ZERO, "a"); + let b = builder.add_const(BabyBear::ONE, "b"); + let id_filler = IdentityFiller::new(vec![a, b]); + let c = builder.add_witness_hints(id_filler, "c"); + assert_eq!(c.len(), 2); + + assert_eq!(builder.graph().nodes().len(), 4); + + match (&builder.graph().nodes()[2], &builder.graph().nodes()[3]) { + (Expr::Witness, Expr::Witness) => (), + _ => panic!("Expected Witness operation"), + } + } + #[test] fn test_nested_operations() { // Test nested operations: (a + b) * (c - d) @@ -534,8 +610,11 @@ mod tests { let a = builder.add_const(BabyBear::from_u64(1), "a"); let b = builder.add_const(BabyBear::from_u64(2), "b"); - let c = builder.add_const(BabyBear::from_u64(3), "c"); - let d = builder.add_const(BabyBear::from_u64(4), "d"); + + let id_filler = IdentityFiller::new(vec![a, b]); + let cd = builder.add_witness_hints(id_filler, "cd"); + let c = cd[0]; + let d = cd[1]; let sum = builder.add_add(a, b, "sum"); let diff = builder.add_sub(c, d, "diff"); diff --git a/circuit/src/builder/mod.rs b/circuit/src/builder/mod.rs index fb0ce6f..3f32633 100644 --- a/circuit/src/builder/mod.rs +++ b/circuit/src/builder/mod.rs @@ -5,10 +5,10 @@ pub mod compiler; mod config; mod errors; mod expression_builder; -mod public_input_tracker; +mod input_tracker; pub use circuit_builder::CircuitBuilder; pub use config::BuilderConfig; pub use errors::CircuitBuilderError; pub use expression_builder::ExpressionBuilder; -pub use public_input_tracker::PublicInputTracker; +pub use input_tracker::InputTracker; diff --git a/circuit/src/builder/public_input_tracker.rs b/circuit/src/builder/public_input_tracker.rs deleted file mode 100644 index e71bd21..0000000 --- a/circuit/src/builder/public_input_tracker.rs +++ /dev/null @@ -1,56 +0,0 @@ -/// Manages public input declarations and tracking. -#[derive(Debug, Clone, Default)] -pub struct PublicInputTracker { - /// The number of public inputs declared - count: usize, -} - -impl PublicInputTracker { - /// Creates a new public input tracker. - pub const fn new() -> Self { - Self { count: 0 } - } - - /// Allocates the next public input position. - /// - /// Returns the position of the newly allocated public input. - pub fn alloc(&mut self) -> usize { - let pos = self.count; - self.count += 1; - pos - } - - /// Returns the total count of public inputs. - pub const fn count(&self) -> usize { - self.count - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_public_input_tracker_basic() { - let mut tracker = PublicInputTracker::new(); - assert_eq!(tracker.count(), 0); - - let pos0 = tracker.alloc(); - assert_eq!(pos0, 0); - assert_eq!(tracker.count(), 1); - - let pos1 = tracker.alloc(); - assert_eq!(pos1, 1); - assert_eq!(tracker.count(), 2); - - let pos2 = tracker.alloc(); - assert_eq!(pos2, 2); - assert_eq!(tracker.count(), 3); - } - - #[test] - fn test_public_input_tracker_default() { - let tracker = PublicInputTracker::default(); - assert_eq!(tracker.count(), 0); - } -} diff --git a/circuit/src/errors.rs b/circuit/src/errors.rs index efc517f..1dce051 100644 --- a/circuit/src/errors.rs +++ b/circuit/src/errors.rs @@ -110,4 +110,7 @@ pub enum CircuitError { /// Invalid Circuit #[error("Failed to build circuit: {error}")] InvalidCircuit { error: CircuitBuilderError }, + + #[error("Unconstrained input length mismatch: expected {expected}, got {got}")] + UnconstrainedInputLengthMismatch { expected: usize, got: usize }, } diff --git a/circuit/src/expr.rs b/circuit/src/expr.rs index a2ee656..e42e54a 100644 --- a/circuit/src/expr.rs +++ b/circuit/src/expr.rs @@ -9,8 +9,9 @@ pub enum Expr { Const(F), /// Public input at declaration position Public(usize), - /// Witness hint - allocates a WitnessId without adding a primitive op - /// The value will be set during non-primitive execution (set-or-verify semantics) + /// Witness hints - allocates a WitnessId without adding a primitive op + /// The value will be set to an arbitrary value based on the values + /// of inputs. Witness, /// Addition of two expressions Add { lhs: ExprId, rhs: ExprId }, diff --git a/circuit/src/lib.rs b/circuit/src/lib.rs index 1f98aef..7679986 100644 --- a/circuit/src/lib.rs +++ b/circuit/src/lib.rs @@ -1,4 +1,4 @@ -#![no_std] +// #![no_std] extern crate alloc; #[cfg(debug_assertions)] diff --git a/circuit/src/op.rs b/circuit/src/op.rs index aafa25e..46932bc 100644 --- a/circuit/src/op.rs +++ b/circuit/src/op.rs @@ -6,10 +6,10 @@ use core::hash::Hash; use hashbrown::HashMap; use p3_field::Field; -use crate::CircuitError; use crate::ops::MmcsVerifyConfig; use crate::tables::MmcsPrivateData; use crate::types::{NonPrimitiveOpId, WitnessId}; +use crate::{CircuitError, ExprId}; /// Circuit operations. /// @@ -63,6 +63,16 @@ pub enum Op { out: WitnessId, }, + /// Load unconstrained values into the witness table + /// + /// Sets `witness[output]` for each `output` in `outputs`, to aribitrary values + /// defined by `filler` + Unconstrained { + inputs: Vec, + outputs: Vec, + filler: Box>, + }, + /// Non-primitive operation with executor-based dispatch NonPrimitiveOpWithExecutor { inputs: Vec>, @@ -95,6 +105,15 @@ impl Clone for Op { b: *b, out: *out, }, + Op::Unconstrained { + inputs, + outputs, + filler, + } => Op::Unconstrained { + inputs: inputs.clone(), + outputs: outputs.clone(), + filler: filler.boxed(), + }, Op::NonPrimitiveOpWithExecutor { inputs, outputs, @@ -379,3 +398,24 @@ impl Clone for Box> { self.boxed() } } + +/// A trait for defining how unconstrained data is set. +pub trait WitnessFiller: Debug { + /// Return the `ExprId` of the inputs + fn inputs(&self) -> &[ExprId]; + /// Returns number of outputs filled by this filler + fn n_outputs(&self) -> usize; + /// Compute the output given the inputs + /// # Arguments + /// * `inputs` - Input witness + fn compute_outputs(&self, inputs_val: Vec) -> Result, CircuitError>; + + /// Clone as trait object + fn boxed(&self) -> Box>; +} + +impl Clone for Box> { + fn clone(&self) -> Self { + self.boxed() + } +} diff --git a/circuit/src/ops/hash.rs b/circuit/src/ops/hash.rs index 9aad381..fc7d0a9 100644 --- a/circuit/src/ops/hash.rs +++ b/circuit/src/ops/hash.rs @@ -55,7 +55,7 @@ where fn add_hash_squeeze(&mut self, count: usize) -> Result, CircuitBuilderError> { self.ensure_op_enabled(NonPrimitiveOpType::HashSqueeze)?; - let outputs = self.alloc_witness_hints(count, "hash_squeeze_output"); + let outputs = self.alloc_witness_hints_no_filler(count, "hash_squeeze_output"); let _ = self.push_non_primitive_op( NonPrimitiveOpType::HashSqueeze, diff --git a/circuit/src/tables/runner.rs b/circuit/src/tables/runner.rs index 939b608..8e2f89d 100644 --- a/circuit/src/tables/runner.rs +++ b/circuit/src/tables/runner.rs @@ -2,6 +2,7 @@ use alloc::string::ToString; use alloc::vec::Vec; use alloc::{format, vec}; +use p3_util::zip_eq::zip_eq; use tracing::instrument; use super::Traces; @@ -189,6 +190,28 @@ impl CircuitRunner { self.set_witness(b, b_val)?; } } + Op::Unconstrained { + inputs, + outputs, + filler, + } => { + let inputs_val = inputs + .iter() + .map(|&input| self.get_witness(input)) + .collect::, _>>()?; + let outputs_val = filler.compute_outputs(inputs_val)?; + + for (&output, &output_val) in zip_eq( + outputs.iter(), + outputs_val.iter(), + CircuitError::UnconstrainedInputLengthMismatch { + expected: outputs.len(), + got: outputs_val.len(), + }, + )? { + self.set_witness(output, output_val)? + } + } Op::NonPrimitiveOpWithExecutor { .. } => { // Handled separately in execute_non_primitives } @@ -261,15 +284,19 @@ impl CircuitRunner { #[cfg(test)] mod tests { extern crate std; + use alloc::boxed::Box; use alloc::vec; + use alloc::vec::Vec; use std::println; use p3_baby_bear::BabyBear; use p3_field::extension::BinomialExtensionField; - use p3_field::{BasedVectorSpace, PrimeCharacteristicRing}; + use p3_field::{BasedVectorSpace, Field, PrimeCharacteristicRing}; use crate::builder::CircuitBuilder; + use crate::op::WitnessFiller; use crate::types::WitnessId; + use crate::{CircuitError, ExprId}; #[test] fn test_table_generation_basic() { @@ -412,6 +439,152 @@ mod tests { assert_eq!(traces.add_trace.result_index, vec![WitnessId(4)]); } + #[derive(Debug, Clone)] + /// Witness filler for finding x in a*x - b = 0 + struct TheSolutionWitnessFiller { + inputs: Vec, + } + + impl TheSolutionWitnessFiller { + pub fn new(a: ExprId, b: ExprId) -> Self { + Self { inputs: vec![a, b] } + } + } + + impl WitnessFiller for TheSolutionWitnessFiller { + fn inputs(&self) -> &[ExprId] { + &self.inputs + } + + fn n_outputs(&self) -> usize { + 1 + } + + fn compute_outputs(&self, inputs_val: Vec) -> Result, crate::CircuitError> { + if inputs_val.len() != self.inputs.len() { + Err(crate::CircuitError::UnconstrainedInputLengthMismatch { + expected: self.inputs.len(), + got: inputs_val.len(), + }) + } else { + let a = inputs_val[0]; + let b = inputs_val[1]; + let inv_a = a.try_inverse().ok_or(CircuitError::DivisionByZero)?; + let x = b * inv_a; + Ok(vec![x]) + } + } + + fn boxed(&self) -> Box> { + Box::new(self.clone()) + } + } + + #[test] + // Proves that we know x such that 37 * x - 111 = 0 + fn test_toy_example_37_times_x_minus_111_with_witness_hint() { + let mut builder = CircuitBuilder::new(); + + let c37 = builder.add_const(BabyBear::from_u64(37)); + let c111 = builder.add_const(BabyBear::from_u64(111)); + let filler = TheSolutionWitnessFiller::new(c37, c111); + let x = builder.alloc_witness_hints(filler, "x")[0]; + + let mul_result = builder.mul(c37, x); + let sub_result = builder.sub(mul_result, c111); + builder.assert_zero(sub_result); + + let circuit = builder.build().unwrap(); + println!("=== CIRCUIT PRIMITIVE OPERATIONS ==="); + for (i, prim) in circuit.primitive_ops.iter().enumerate() { + println!("{i}: {prim:?}"); + } + + let witness_count = circuit.witness_count; + let runner = circuit.runner(); + + let traces = runner.run().unwrap(); + + println!("\n=== WITNESS TRACE ==="); + for (i, (idx, val)) in traces + .witness_trace + .index + .iter() + .zip(traces.witness_trace.values.iter()) + .enumerate() + { + println!("Row {i}: WitnessId({idx}) = {val:?}"); + } + + println!("\n=== CONST TRACE ==="); + for (i, (idx, val)) in traces + .const_trace + .index + .iter() + .zip(traces.const_trace.values.iter()) + .enumerate() + { + println!("Row {i}: WitnessId({idx}) = {val:?}"); + } + + println!("\n=== PUBLIC TRACE ==="); + for (i, (idx, val)) in traces + .public_trace + .index + .iter() + .zip(traces.public_trace.values.iter()) + .enumerate() + { + println!("Row {i}: WitnessId({idx}) = {val:?}"); + } + + println!("\n=== MUL TRACE ==="); + for i in 0..traces.mul_trace.lhs_values.len() { + println!( + "Row {}: WitnessId({}) * WitnessId({}) -> WitnessId({}) | {:?} * {:?} -> {:?}", + i, + traces.mul_trace.lhs_index[i], + traces.mul_trace.rhs_index[i], + traces.mul_trace.result_index[i], + traces.mul_trace.lhs_values[i], + traces.mul_trace.rhs_values[i], + traces.mul_trace.result_values[i] + ); + } + + println!("\n=== ADD TRACE ==="); + for i in 0..traces.add_trace.lhs_values.len() { + println!( + "Row {}: WitnessId({}) + WitnessId({}) -> WitnessId({}) | {:?} + {:?} -> {:?}", + i, + traces.add_trace.lhs_index[i], + traces.add_trace.rhs_index[i], + traces.add_trace.result_index[i], + traces.add_trace.lhs_values[i], + traces.add_trace.rhs_values[i], + traces.add_trace.result_values[i] + ); + } + + // Verify trace structure + assert_eq!(traces.witness_trace.index.len(), witness_count as usize); + + // Should have constants: 0, 37, 111 + assert_eq!(traces.const_trace.values.len(), 3); + + // Should have no public input + assert!(traces.public_trace.values.is_empty()); + + // Should have one mul operation: 37 * x + assert_eq!(traces.mul_trace.lhs_values.len(), 1); + + // Encoded subtraction lands in the add table (result + rhs = lhs). + assert_eq!(traces.add_trace.lhs_values.len(), 1); + assert_eq!(traces.add_trace.lhs_index, vec![WitnessId(2)]); + assert_eq!(traces.add_trace.rhs_index, vec![WitnessId(0)]); + assert_eq!(traces.add_trace.result_index, vec![WitnessId(4)]); + } + #[test] fn test_extension_field_support() { type ExtField = BinomialExtensionField; diff --git a/circuit/src/utils.rs b/circuit/src/utils.rs index bb7a080..52d7172 100644 --- a/circuit/src/utils.rs +++ b/circuit/src/utils.rs @@ -1,10 +1,13 @@ +use alloc::boxed::Box; use alloc::vec; use alloc::vec::Vec; +use core::marker::PhantomData; -use p3_field::Field; +use p3_field::{ExtensionField, Field, PrimeField64}; use p3_uni_stark::{Entry, SymbolicExpression}; -use crate::{CircuitBuilder, ExprId}; +use crate::op::WitnessFiller; +use crate::{CircuitBuilder, CircuitError, ExprId}; /// Identifiers for special row selector flags in the circuit. #[derive(Clone, Copy, Debug)] @@ -126,26 +129,89 @@ pub fn reconstruct_index_from_bits( acc } +#[derive(Debug, Clone)] +/// Given a field element as input, decompose it into its little-endian bits and +/// fill witness hints with the binary decomposition. +/// +/// For a given input `input`, fills `n_bits` witness hints with `b_i` +/// such that that: +/// input = Σ b_i · 2^i +struct DecomposeToBitsFiller { + inputs: Vec, + n_bits: usize, + _phantom: PhantomData, +} + +impl DecomposeToBitsFiller { + pub fn new(input: ExprId, n_bits: usize) -> Result { + if n_bits > 64 { + return Err(CircuitError::UnconstrainedInputLengthMismatch { + expected: 64, + got: n_bits, + }); + } + Ok(Self { + inputs: vec![input], + n_bits, + _phantom: PhantomData, + }) + } +} + +impl> WitnessFiller for DecomposeToBitsFiller { + fn inputs(&self) -> &[ExprId] { + &self.inputs + } + + fn n_outputs(&self) -> usize { + self.n_bits + } + + fn compute_outputs(&self, inputs_val: Vec) -> Result, CircuitError> { + if inputs_val.len() != 1 { + return Err(crate::CircuitError::UnconstrainedInputLengthMismatch { + expected: 1, + got: inputs_val.len(), + }); + } + let val: u64 = inputs_val[0].as_basis_coefficients_slice()[0].as_canonical_u64(); + let bits = (0..self.n_bits) + .map(|i| F::from_bool(val >> i & 1 == 1)) + .collect(); + Ok(bits) + } + + fn boxed(&self) -> alloc::boxed::Box> { + Box::new(self.clone()) + } +} + /// Decompose a field element into its little-endian bits. /// /// For a given target `x`, this function creates `N_BITS` new boolean targets `b_i` /// and adds constraints to enforce that: /// x = Σ b_i · 2^i -pub fn decompose_to_bits( +pub fn decompose_to_bits, BF: PrimeField64>( builder: &mut CircuitBuilder, x: ExprId, n_bits: usize, -) -> Vec { +) -> Result, CircuitError> { builder.push_scope("decompose_to_bits"); - let mut bits = Vec::with_capacity(n_bits); - // Create bit witness variables - for _ in 0..n_bits { - let bit = builder.add_public_input(); // TODO: Should be witness - builder.assert_bool(bit); - bits.push(bit); - } + let filler = DecomposeToBitsFiller::new(x, n_bits)?; + let bits = builder.alloc_witness_hints(filler, "decompose_to_bits"); + + println!("nbits = {n_bits}"); + + // let mut bits = Vec::with_capacity(n_bits); + + // // Create bit witness variables + // for _ in 0..n_bits { + // let bit = builder.add_public_input(); // TODO: Should be witness + // builder.assert_bool(bit); + // bits.push(bit); + // } // Constrain that the bits reconstruct to the original element let reconstructed = reconstruct_index_from_bits(builder, &bits); @@ -153,7 +219,7 @@ pub fn decompose_to_bits( builder.pop_scope(); - bits + Ok(bits) } /// Helper to pad trace values to power-of-two height with zeros @@ -385,28 +451,18 @@ mod tests { let value = builder.add_const(BabyBear::from_u64(6)); // Binary: 110 // Decompose into 3 bits - this creates its own public inputs for the bits - let bits = decompose_to_bits::(&mut builder, value, 3); + let bits = decompose_to_bits::(&mut builder, value, 3).unwrap(); // Build and run the circuit let circuit = builder.build().expect("Failed to build circuit"); - let mut runner = circuit.runner(); + let runner = circuit.runner(); - // Set public inputs: expected bit decomposition of 6 (binary: 110) in little-endian - let public_inputs = vec![ - BabyBear::ZERO, // bit 0: 0 - BabyBear::ONE, // bit 1: 1 - BabyBear::ONE, // bit 2: 1 - ]; - - runner - .set_public_inputs(&public_inputs) - .expect("Failed to set public inputs"); let traces = runner.run().expect("Failed to run circuit"); // Verify the bits are correctly decomposed - 6 = [0,1,1] in little-endian - assert_eq!(traces.public_trace.values[0], BabyBear::ZERO); // bit 0 - assert_eq!(traces.public_trace.values[1], BabyBear::ONE); // bit 1 - assert_eq!(traces.public_trace.values[2], BabyBear::ONE); // bit 2 + assert_eq!(traces.witness_trace.values[3], BabyBear::ZERO); // bit 0 + assert_eq!(traces.witness_trace.values[4], BabyBear::ONE); // bit 1 + assert_eq!(traces.witness_trace.values[5], BabyBear::ONE); // bit 2 // Also verify that the returned bits have the expected length assert_eq!(bits.len(), 3); diff --git a/recursion/src/generation.rs b/recursion/src/generation.rs index 911a5b9..f1040b0 100644 --- a/recursion/src/generation.rs +++ b/recursion/src/generation.rs @@ -245,29 +245,15 @@ where if params.len() != 2 { return Err(GenerationError::InvalidParameterCount(params.len(), 2)); } - // Observe PoW and sample bits. - let pow_bits = params[0]; + // Check PoW witness. challenger.observe(opening_proof.pow_witness); - // Sample a challenge and decompose it into bits. Add all bits to the challenges. + // Sample a challenge H(transcript||pow_witness). Later the circuit + // checks that the challenge start wiht enough 0s. let rand_f: Val = challenger.sample(); let rand_usize = rand_f.as_canonical_biguint().to_u64_digits()[0] as usize; - // Get the bits. The total number of bits is the number of bits in a base field element. - let total_num_bits = Val::::bits(); - let rand_bits = (0..total_num_bits) - .map(|i| SC::Challenge::from_usize((rand_usize >> i) & 1)) - .collect::>(); - // Push the sampled challenge, along with the bits. challenges.push(SC::Challenge::from_usize(rand_usize)); - challenges.extend(rand_bits); - - // Check that the first bits are all 0. - let pow_challenge = rand_usize & ((1 << pow_bits) - 1); - - if pow_challenge != 0 { - return Err(GenerationError::InvalidPowWitness); - } let log_height_max = params[1]; let log_global_max_height = opening_proof.commit_phase_commits.len() + log_height_max; diff --git a/recursion/src/lib.rs b/recursion/src/lib.rs index 5b1601b..49cd1b5 100644 --- a/recursion/src/lib.rs +++ b/recursion/src/lib.rs @@ -1,6 +1,6 @@ //! Recursive proof verification for Plonky3 STARKs. -#![no_std] +// #![no_std] extern crate alloc; diff --git a/recursion/src/pcs/fri/targets.rs b/recursion/src/pcs/fri/targets.rs index 6e142ab..6b81403 100644 --- a/recursion/src/pcs/fri/targets.rs +++ b/recursion/src/pcs/fri/targets.rs @@ -7,7 +7,9 @@ use p3_circuit::CircuitBuilder; use p3_circuit::utils::{RowSelectorsTargets, decompose_to_bits}; use p3_commit::{BatchOpening, ExtensionMmcs, Mmcs, PolynomialSpace}; use p3_field::coset::TwoAdicMultiplicativeCoset; -use p3_field::{ExtensionField, Field, PackedValue, PrimeCharacteristicRing, TwoAdicField}; +use p3_field::{ + ExtensionField, Field, PackedValue, PrimeCharacteristicRing, PrimeField64, TwoAdicField, +}; use p3_fri::{CommitPhaseProofStep, FriProof, QueryProof, TwoAdicFriPcs}; use p3_merkle_tree::MerkleTreeMmcs; use p3_symmetric::{CryptographicHasher, Hash, PseudoCompressionFunction}; @@ -429,6 +431,7 @@ where RecursiveFriMmcs::Commitment: ObservableCommitment, SC::Challenger: GrindingChallenger, SC::Challenger: CanObserve, + Val: PrimeField64, { type VerifierParams = FriVerifierParams; type RecursiveProof = RecursiveFriProof< @@ -531,7 +534,8 @@ where let index_bits_per_query: Vec> = query_indices .iter() .map(|&index_target| { - let all_bits = decompose_to_bits(circuit, index_target, MAX_QUERY_INDEX_BITS); + let all_bits = + decompose_to_bits(circuit, index_target, MAX_QUERY_INDEX_BITS).unwrap(); all_bits.into_iter().take(log_max_height).collect() }) .collect(); diff --git a/recursion/src/public_inputs.rs b/recursion/src/public_inputs.rs index 4bb8589..1ad1c05 100644 --- a/recursion/src/public_inputs.rs +++ b/recursion/src/public_inputs.rs @@ -180,7 +180,6 @@ where /// 1. AIR public values /// 2. Proof values /// 3. All challenges (alpha, zeta, zeta_next, betas, query indices) - /// 4. Query index bit decompositions (MAX_QUERY_INDEX_BITS per query) pub fn build(self) -> Vec { let mut builder = PublicInputBuilder::new(); @@ -188,24 +187,6 @@ where builder.add_proof_values(self.proof_values); builder.add_challenges(self.challenges.iter().copied()); - // The circuit calls decompose_to_bits on each query index, - // which creates MAX_QUERY_INDEX_BITS additional public inputs per query - let num_regular_challenges = self.challenges.len() - self.num_queries; - for &query_index in &self.challenges[num_regular_challenges..] { - let coeffs = query_index.as_basis_coefficients_slice(); - let index_usize = coeffs[0].as_canonical_u64() as usize; - - // Add bit decomposition (MAX_QUERY_INDEX_BITS public inputs) - for k in 0..MAX_QUERY_INDEX_BITS { - let bit: EF = if (index_usize >> k) & 1 == 1 { - EF::ONE - } else { - EF::ZERO - }; - builder.add_challenge(bit); - } - } - builder.build() } } diff --git a/recursion/src/traits/challenger.rs b/recursion/src/traits/challenger.rs index 8ceb7dd..cf195a1 100644 --- a/recursion/src/traits/challenger.rs +++ b/recursion/src/traits/challenger.rs @@ -4,7 +4,7 @@ use alloc::vec::Vec; use p3_circuit::CircuitBuilder; use p3_circuit::utils::decompose_to_bits; -use p3_field::Field; +use p3_field::{ExtensionField, Field, PrimeField64}; use crate::Target; @@ -82,16 +82,19 @@ pub trait RecursiveChallenger { /// /// # Returns /// Vector of the first `num_bits` bits as targets (each in {0, 1}) - fn sample_public_bits( + fn sample_public_bits( &mut self, circuit: &mut CircuitBuilder, total_num_bits: usize, num_bits: usize, - ) -> Vec { + ) -> Vec + where + F: ExtensionField, + { let x = self.sample(circuit); // Decompose to bits (adds public inputs for each bit and verifies they reconstruct x) - let bits = decompose_to_bits(circuit, x, total_num_bits); + let bits = decompose_to_bits(circuit, x, total_num_bits).unwrap(); bits[..num_bits].to_vec() } @@ -106,13 +109,15 @@ pub trait RecursiveChallenger { /// - `witness_bits`: Number of leading bits that must be zero /// - `witness`: The proof-of-work witness target /// - `total_num_bits`: Total number of bits to decompose - fn check_witness( + fn check_witness( &mut self, circuit: &mut CircuitBuilder, witness_bits: usize, witness: Target, total_num_bits: usize, - ) { + ) where + F: ExtensionField, + { self.observe(circuit, witness); let bits = self.sample_public_bits(circuit, total_num_bits, witness_bits); From 1bdffb8c6eadf3a37112446988653f881f06e927 Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Thu, 6 Nov 2025 11:56:16 +0100 Subject: [PATCH 02/13] Fix build embedded --- circuit/src/tables/mmcs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/circuit/src/tables/mmcs.rs b/circuit/src/tables/mmcs.rs index a152498..ac01d78 100644 --- a/circuit/src/tables/mmcs.rs +++ b/circuit/src/tables/mmcs.rs @@ -3,6 +3,7 @@ use alloc::vec::Vec; use alloc::{format, vec}; use core::fmt::Debug; use core::iter; +use core::result::Result; use itertools::izip; use p3_field::{ExtensionField, Field}; From 5aae193acfdc3ee9080d86557c21338b0221ddde Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Thu, 6 Nov 2025 12:37:43 +0100 Subject: [PATCH 03/13] Minor --- circuit/src/builder/compiler/expression_lowerer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/circuit/src/builder/compiler/expression_lowerer.rs b/circuit/src/builder/compiler/expression_lowerer.rs index 1403f2f..1bdc345 100644 --- a/circuit/src/builder/compiler/expression_lowerer.rs +++ b/circuit/src/builder/compiler/expression_lowerer.rs @@ -190,12 +190,12 @@ where } // Pass C: collect witness hints and emit `Unconstrained` operatios - for (expr_idx, _) in self + for expr_idx in self .graph .nodes() .iter() .enumerate() - .filter(|(_, expr)| matches!(expr, Expr::Witness)) + .filter_map(|(expr_idx, expr)| matches!(expr, Expr::Witness).then(|| expr_idx)) { let expr_id = ExprId(expr_idx as u32); let out_widx = alloc_witness_id_for_expr(expr_idx); From 9f0dc4f425ba5e43be500b8653996d28f31bc9f1 Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Thu, 6 Nov 2025 13:46:01 +0100 Subject: [PATCH 04/13] Uncomment no_std --- circuit/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circuit/src/lib.rs b/circuit/src/lib.rs index 7679986..1f98aef 100644 --- a/circuit/src/lib.rs +++ b/circuit/src/lib.rs @@ -1,4 +1,4 @@ -// #![no_std] +#![no_std] extern crate alloc; #[cfg(debug_assertions)] From b17326dab5f9247c855481bb669ca902511d86bc Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Thu, 6 Nov 2025 13:50:38 +0100 Subject: [PATCH 05/13] Add missing import --- circuit/src/builder/circuit_builder.rs | 2 ++ circuit/src/builder/compiler/expression_lowerer.rs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/circuit/src/builder/circuit_builder.rs b/circuit/src/builder/circuit_builder.rs index c7a57b0..46eb821 100644 --- a/circuit/src/builder/circuit_builder.rs +++ b/circuit/src/builder/circuit_builder.rs @@ -767,6 +767,8 @@ mod tests { #[cfg(test)] mod proptests { + use alloc::vec; + use p3_baby_bear::BabyBear; use p3_field::PrimeCharacteristicRing; use proptest::prelude::*; diff --git a/circuit/src/builder/compiler/expression_lowerer.rs b/circuit/src/builder/compiler/expression_lowerer.rs index 1bdc345..f9cc453 100644 --- a/circuit/src/builder/compiler/expression_lowerer.rs +++ b/circuit/src/builder/compiler/expression_lowerer.rs @@ -195,7 +195,8 @@ where .nodes() .iter() .enumerate() - .filter_map(|(expr_idx, expr)| matches!(expr, Expr::Witness).then(|| expr_idx)) + .filter(|(_, expr)| matches!(expr, Expr::Witness)) + .map(|(expr_idx, _)| expr_idx) { let expr_id = ExprId(expr_idx as u32); let out_widx = alloc_witness_id_for_expr(expr_idx); From 21b5dfd4ea518643f6fd6a5846ec0a4263cb9b62 Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Fri, 7 Nov 2025 16:31:31 +0100 Subject: [PATCH 06/13] Fix topological order --- circuit-prover/src/prover.rs | 5 +- .../builder/compiler/expression_lowerer.rs | 98 +++++++++---------- circuit/src/builder/compiler/mod.rs | 2 +- circuit/src/builder/expression_builder.rs | 31 ++++-- circuit/src/expr.rs | 9 +- mmcs-air/src/air.rs | 4 +- 6 files changed, 80 insertions(+), 69 deletions(-) diff --git a/circuit-prover/src/prover.rs b/circuit-prover/src/prover.rs index d9d6ea4..90e3cea 100644 --- a/circuit-prover/src/prover.rs +++ b/circuit-prover/src/prover.rs @@ -11,7 +11,6 @@ //! detection of the binomial parameter `W` for extension-field multiplication. use alloc::vec; -use alloc::vec::Vec; use p3_circuit::tables::Traces; use p3_circuit::{CircuitBuilderError, CircuitError}; @@ -209,7 +208,7 @@ where fn prove_for_degree( &self, traces: &Traces, - pis: &Vec>, + pis: &[Val], w_binomial: Option>, ) -> Result, ProverError> where @@ -293,7 +292,7 @@ where fn verify_for_degree( &self, proof: &MultiTableProof, - pis: &Vec>, + pis: &[Val], w_binomial: Option>, ) -> Result<(), ProverError> { let table_packing = proof.table_packing; diff --git a/circuit/src/builder/compiler/expression_lowerer.rs b/circuit/src/builder/compiler/expression_lowerer.rs index f9cc453..63b599b 100644 --- a/circuit/src/builder/compiler/expression_lowerer.rs +++ b/circuit/src/builder/compiler/expression_lowerer.rs @@ -74,14 +74,12 @@ pub struct ExpressionLowerer<'a, F> { public_input_count: usize, /// The hint witnesses with their respective filler - hints_with_fillers: &'a [HintsWithFiller], + hints_with_fillers: &'a [Box>], /// Witness allocator witness_alloc: WitnessAllocator, } -pub type HintsWithFiller = (Vec, Box>); - impl<'a, F> ExpressionLowerer<'a, F> where F: Clone + PrimeCharacteristicRing + PartialEq + Eq + core::hash::Hash, @@ -91,7 +89,7 @@ where graph: &'a ExpressionGraph, pending_connects: &'a [(ExprId, ExprId)], public_input_count: usize, - hints_with_fillers: &'a [HintsWithFiller], + hints_with_fillers: &'a [Box>], witness_alloc: WitnessAllocator, ) -> Self { Self { @@ -189,58 +187,52 @@ where } } - // Pass C: collect witness hints and emit `Unconstrained` operatios - for expr_idx in self - .graph - .nodes() - .iter() - .enumerate() - .filter(|(_, expr)| matches!(expr, Expr::Witness)) - .map(|(expr_idx, _)| expr_idx) - { - let expr_id = ExprId(expr_idx as u32); - let out_widx = alloc_witness_id_for_expr(expr_idx); - expr_to_widx.insert(expr_id, out_widx); - } - for (witness_hints, filler) in self.hints_with_fillers.iter() { - let inputs = filler - .inputs() - .iter() - .map(|expr_id| { - expr_to_widx - .get(expr_id) - .ok_or(CircuitBuilderError::MissingExprMapping { - expr_id: *expr_id, - context: "Unconstrained op".to_string(), - }) - .copied() - }) - .collect::, _>>()?; - let outputs = witness_hints - .iter() - .map(|expr_id| { - expr_to_widx - .get(expr_id) - .ok_or(CircuitBuilderError::MissingExprMapping { - expr_id: *expr_id, - context: "Unconstrained op".to_string(), - }) - .copied() - }) - .collect::, _>>()?; - primitive_ops.push(Op::Unconstrained { - inputs, - outputs, - filler: filler.clone(), - }) - } - - // Pass D: emit arithmetic ops in creation order; tie outputs to class slot if connected + // Pass C: emit arithmetic ops in creation order; tie outputs to class slot if connected + let mut hints_sequence = vec![]; + let mut hints_wit_fillers_iter = self.hints_with_fillers.iter(); for (expr_idx, expr) in self.graph.nodes().iter().enumerate() { let expr_id = ExprId(expr_idx as u32); match expr { - Expr::Const(_) | Expr::Public(_) | Expr::Witness => { /* handled above */ } - + Expr::Const(_) | Expr::Public(_) => { /* handled above */ } + Expr::Witness { + has_filler: false, .. + } => { + let expr_id = ExprId(expr_idx as u32); + let out_widx = alloc_witness_id_for_expr(expr_idx); + expr_to_widx.insert(expr_id, out_widx); + hints_sequence.push(out_widx); + } + Expr::Witness { + last_hint, + has_filler: true, + } => { + let expr_id = ExprId(expr_idx as u32); + let out_widx = alloc_witness_id_for_expr(expr_idx); + expr_to_widx.insert(expr_id, out_widx); + hints_sequence.push(out_widx); + if *last_hint { + let filler = hints_wit_fillers_iter.next().expect("Hints with fillers can be only created with `alloc_witness_hints`, which in turn must have add a filler"); + let inputs = filler + .inputs() + .iter() + .map(|expr_id| { + expr_to_widx + .get(expr_id) + .ok_or(CircuitBuilderError::MissingExprMapping { + expr_id: *expr_id, + context: "Unconstrained op".to_string(), + }) + .copied() + }) + .collect::, _>>()?; + primitive_ops.push(Op::Unconstrained { + inputs, + outputs: hints_sequence, + filler: filler.clone(), + }); + hints_sequence = vec![]; + } + } Expr::Add { lhs, rhs } => { let out_widx = alloc_witness_id_for_expr(expr_idx); let a_widx = get_witness_id( diff --git a/circuit/src/builder/compiler/mod.rs b/circuit/src/builder/compiler/mod.rs index ab3bcc6..4080e8f 100644 --- a/circuit/src/builder/compiler/mod.rs +++ b/circuit/src/builder/compiler/mod.rs @@ -4,7 +4,7 @@ mod expression_lowerer; mod non_primitive_lowerer; mod optimizer; -pub use expression_lowerer::{ExpressionLowerer, HintsWithFiller}; +pub use expression_lowerer::ExpressionLowerer; use hashbrown::HashMap; pub use non_primitive_lowerer::NonPrimitiveLowerer; pub use optimizer::Optimizer; diff --git a/circuit/src/builder/expression_builder.rs b/circuit/src/builder/expression_builder.rs index 1271294..574994e 100644 --- a/circuit/src/builder/expression_builder.rs +++ b/circuit/src/builder/expression_builder.rs @@ -7,7 +7,6 @@ use hashbrown::HashMap; use itertools::Itertools; use p3_field::PrimeCharacteristicRing; -use crate::builder::compiler::HintsWithFiller; use crate::expr::{Expr, ExpressionGraph}; use crate::op::WitnessHintFiller; use crate::types::ExprId; @@ -27,7 +26,7 @@ pub struct ExpressionBuilder { pending_connects: Vec<(ExprId, ExprId)>, /// The witness hints together with theit witness fillers - hints_with_fillers: Vec<(Vec, Box>)>, + hints_with_fillers: Vec>>, /// Debug log of all allocations #[cfg(debug_assertions)] @@ -112,7 +111,10 @@ where #[allow(unused_variables)] #[must_use] pub fn add_witness_hint(&mut self, label: &'static str) -> ExprId { - let expr_id = self.graph.add_expr(Expr::Witness); + let expr_id = self.graph.add_expr(Expr::Witness { + last_hint: false, + has_filler: false, + }); #[cfg(debug_assertions)] self.allocation_log.push(AllocationEntry { @@ -138,10 +140,14 @@ where ) -> Vec { let n_outputs = filler.n_outputs(); let expr_ids = (0..n_outputs) - .map(|_| self.graph.add_expr(Expr::Witness)) + .map(|i| { + self.graph.add_expr(Expr::Witness { + last_hint: i == n_outputs - 1, + has_filler: true, + }) + }) .collect_vec(); - self.hints_with_fillers - .push((expr_ids.clone(), Box::new(filler))); + self.hints_with_fillers.push(Box::new(filler)); expr_ids } @@ -231,7 +237,7 @@ where } /// Returns a reference to the witness hints with fillers. - pub fn hints_with_fillers(&self) -> &[HintsWithFiller] { + pub fn hints_with_fillers(&self) -> &[Box>] { &self.hints_with_fillers } @@ -595,7 +601,16 @@ mod tests { assert_eq!(builder.graph().nodes().len(), 4); match (&builder.graph().nodes()[2], &builder.graph().nodes()[3]) { - (Expr::Witness, Expr::Witness) => (), + ( + Expr::Witness { + last_hint: false, + has_filler: true, + }, + Expr::Witness { + last_hint: true, + has_filler: true, + }, + ) => (), _ => panic!("Expected Witness operation"), } } diff --git a/circuit/src/expr.rs b/circuit/src/expr.rs index 40a5e50..b37bcec 100644 --- a/circuit/src/expr.rs +++ b/circuit/src/expr.rs @@ -10,8 +10,13 @@ pub enum Expr { /// Public input at declaration position Public(usize), /// Witness hints - allocates a WitnessId storing - /// a non-deterministic hint. - Witness, + /// a non-deterministic hint. The boolean indicates + /// wether this is the last witness in a sequence of hint. + /// A sequence of hints is one generated from a single filler. + /// TODO: The extra bool indicates whether this witness + /// has an associted filler. We should get rid of this + /// when all witness have an associated filler. + Witness { last_hint: bool, has_filler: bool }, /// Addition of two expressions Add { lhs: ExprId, rhs: ExprId }, /// Subtraction of two expressions diff --git a/mmcs-air/src/air.rs b/mmcs-air/src/air.rs index f23cd70..b019925 100644 --- a/mmcs-air/src/air.rs +++ b/mmcs-air/src/air.rs @@ -640,9 +640,9 @@ mod test { type MyConfig = StarkConfig; let config = MyConfig::new(pcs, challenger); - let proof = prove(&config, &air, trace, &vec![]); + let proof = prove(&config, &air, trace, &[]); // Verify the proof. - verify(&config, &air, &proof, &vec![]) + verify(&config, &air, &proof, &[]) } } From 9b90963683d41bbd63c07d274f90b35d692d9b76 Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Fri, 7 Nov 2025 17:47:36 +0100 Subject: [PATCH 07/13] Add DefaultHint --- .../builder/compiler/expression_lowerer.rs | 21 ++++------------ circuit/src/builder/expression_builder.rs | 17 ++----------- circuit/src/expr.rs | 5 +--- circuit/src/op.rs | 24 +++++++++++++++++++ 4 files changed, 32 insertions(+), 35 deletions(-) diff --git a/circuit/src/builder/compiler/expression_lowerer.rs b/circuit/src/builder/compiler/expression_lowerer.rs index 63b599b..61b9d08 100644 --- a/circuit/src/builder/compiler/expression_lowerer.rs +++ b/circuit/src/builder/compiler/expression_lowerer.rs @@ -10,7 +10,7 @@ use crate::Op; use crate::builder::CircuitBuilderError; use crate::builder::compiler::get_witness_id; use crate::expr::{Expr, ExpressionGraph}; -use crate::op::WitnessHintFiller; +use crate::op::{DefaultHint, WitnessHintFiller}; use crate::types::{ExprId, WitnessAllocator, WitnessId}; /// Sparse disjoint-set "find" with path compression over a HashMap (iterative). @@ -189,29 +189,18 @@ where // Pass C: emit arithmetic ops in creation order; tie outputs to class slot if connected let mut hints_sequence = vec![]; - let mut hints_wit_fillers_iter = self.hints_with_fillers.iter(); + let mut fillers_iter = self.hints_with_fillers.iter().cloned(); for (expr_idx, expr) in self.graph.nodes().iter().enumerate() { let expr_id = ExprId(expr_idx as u32); match expr { Expr::Const(_) | Expr::Public(_) => { /* handled above */ } - Expr::Witness { - has_filler: false, .. - } => { - let expr_id = ExprId(expr_idx as u32); - let out_widx = alloc_witness_id_for_expr(expr_idx); - expr_to_widx.insert(expr_id, out_widx); - hints_sequence.push(out_widx); - } - Expr::Witness { - last_hint, - has_filler: true, - } => { + Expr::Witness { last_hint } => { let expr_id = ExprId(expr_idx as u32); let out_widx = alloc_witness_id_for_expr(expr_idx); expr_to_widx.insert(expr_id, out_widx); hints_sequence.push(out_widx); if *last_hint { - let filler = hints_wit_fillers_iter.next().expect("Hints with fillers can be only created with `alloc_witness_hints`, which in turn must have add a filler"); + let filler = fillers_iter.next().unwrap_or(DefaultHint::boxed_default()); let inputs = filler .inputs() .iter() @@ -228,7 +217,7 @@ where primitive_ops.push(Op::Unconstrained { inputs, outputs: hints_sequence, - filler: filler.clone(), + filler, }); hints_sequence = vec![]; } diff --git a/circuit/src/builder/expression_builder.rs b/circuit/src/builder/expression_builder.rs index 574994e..5d2a98f 100644 --- a/circuit/src/builder/expression_builder.rs +++ b/circuit/src/builder/expression_builder.rs @@ -111,10 +111,7 @@ where #[allow(unused_variables)] #[must_use] pub fn add_witness_hint(&mut self, label: &'static str) -> ExprId { - let expr_id = self.graph.add_expr(Expr::Witness { - last_hint: false, - has_filler: false, - }); + let expr_id = self.graph.add_expr(Expr::Witness { last_hint: true }); #[cfg(debug_assertions)] self.allocation_log.push(AllocationEntry { @@ -143,7 +140,6 @@ where .map(|i| { self.graph.add_expr(Expr::Witness { last_hint: i == n_outputs - 1, - has_filler: true, }) }) .collect_vec(); @@ -601,16 +597,7 @@ mod tests { assert_eq!(builder.graph().nodes().len(), 4); match (&builder.graph().nodes()[2], &builder.graph().nodes()[3]) { - ( - Expr::Witness { - last_hint: false, - has_filler: true, - }, - Expr::Witness { - last_hint: true, - has_filler: true, - }, - ) => (), + (Expr::Witness { last_hint: false }, Expr::Witness { last_hint: true }) => (), _ => panic!("Expected Witness operation"), } } diff --git a/circuit/src/expr.rs b/circuit/src/expr.rs index b37bcec..c3e6e6e 100644 --- a/circuit/src/expr.rs +++ b/circuit/src/expr.rs @@ -13,10 +13,7 @@ pub enum Expr { /// a non-deterministic hint. The boolean indicates /// wether this is the last witness in a sequence of hint. /// A sequence of hints is one generated from a single filler. - /// TODO: The extra bool indicates whether this witness - /// has an associted filler. We should get rid of this - /// when all witness have an associated filler. - Witness { last_hint: bool, has_filler: bool }, + Witness { last_hint: bool }, /// Addition of two expressions Add { lhs: ExprId, rhs: ExprId }, /// Subtraction of two expressions diff --git a/circuit/src/op.rs b/circuit/src/op.rs index 6391991..b581ec0 100644 --- a/circuit/src/op.rs +++ b/circuit/src/op.rs @@ -1,4 +1,5 @@ use alloc::boxed::Box; +use alloc::vec; use alloc::vec::Vec; use core::fmt::Debug; use core::hash::Hash; @@ -417,6 +418,29 @@ impl Clone for Box> { } } +#[derive(Debug, Clone, Default)] +pub struct DefaultHint {} + +impl DefaultHint { + pub fn boxed_default() -> Box> { + Box::new(Self::default()) + } +} + +impl WitnessHintFiller for DefaultHint { + fn inputs(&self) -> &[ExprId] { + &[] + } + + fn n_outputs(&self) -> usize { + 1 + } + + fn compute_outputs(&self, _inputs_val: Vec) -> Result, CircuitError> { + Ok(vec![F::default()]) + } +} + // Object-safe "clone into Box" helper pub trait WitnessFillerClone { fn clone_box(&self) -> Box>; From bcdeb91ad6ad8a0a05545c909f98f9e15606d26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alonso=20Gonz=C3=A1lez?= Date: Fri, 7 Nov 2025 17:57:17 +0100 Subject: [PATCH 08/13] Apply suggestions from code review Co-authored-by: Robin Salen <30937548+Nashtare@users.noreply.github.com> --- circuit/src/builder/circuit_builder.rs | 2 +- circuit/src/builder/expression_builder.rs | 3 ++- circuit/src/op.rs | 2 +- circuit/src/tables/runner.rs | 4 ++-- circuit/src/utils.rs | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/circuit/src/builder/circuit_builder.rs b/circuit/src/builder/circuit_builder.rs index 46eb821..dc8af54 100644 --- a/circuit/src/builder/circuit_builder.rs +++ b/circuit/src/builder/circuit_builder.rs @@ -121,7 +121,7 @@ where self.public_tracker.count() } - /// Allocates multiple witness. Witness hints are placeholders for values that will later be provided by a + /// Allocates multiple witnesses. Witness hints are placeholders for values that will later be provided by a /// `filler`. #[must_use] pub fn alloc_witness_hints>( diff --git a/circuit/src/builder/expression_builder.rs b/circuit/src/builder/expression_builder.rs index 5d2a98f..768513b 100644 --- a/circuit/src/builder/expression_builder.rs +++ b/circuit/src/builder/expression_builder.rs @@ -581,10 +581,11 @@ mod tests { self.n_outputs } - fn compute_outputs(&self, inputs_val: Vec) -> Result, crate::CircuitError> { + fn compute_outputs(&self, inputs_val: Vec) -> Result, CircuitError> { Ok(inputs_val) } } + #[test] fn test_build_with_witness_hint() { let mut builder = ExpressionBuilder::::new(); diff --git a/circuit/src/op.rs b/circuit/src/op.rs index b581ec0..eb59207 100644 --- a/circuit/src/op.rs +++ b/circuit/src/op.rs @@ -66,7 +66,7 @@ pub enum Op { /// Load unconstrained values into the witness table /// - /// Sets `witness[output]`, for each `output` in `outputs`, to aribitrary values + /// Sets `witness[output]`, for each `output` in `outputs`, to arbitrary values /// defined by `filler` Unconstrained { inputs: Vec, diff --git a/circuit/src/tables/runner.rs b/circuit/src/tables/runner.rs index 38b9ea3..fb5c617 100644 --- a/circuit/src/tables/runner.rs +++ b/circuit/src/tables/runner.rs @@ -439,8 +439,8 @@ mod tests { } #[derive(Debug, Clone)] - /// The hint deined by x in an equation a*x - b = 0 - struct X { + /// The hint defined by x in an equation a*x - b = 0 + struct XHint { inputs: Vec, } diff --git a/circuit/src/utils.rs b/circuit/src/utils.rs index f9a1078..7f2ea11 100644 --- a/circuit/src/utils.rs +++ b/circuit/src/utils.rs @@ -168,7 +168,7 @@ impl> WitnessHintFiller for BinaryDec fn compute_outputs(&self, inputs_val: Vec) -> Result, CircuitError> { if inputs_val.len() != 1 { - return Err(crate::CircuitError::UnconstrainedOpInputLengthMismatch { + return Err(CircuitError::UnconstrainedOpInputLengthMismatch { expected: 1, got: inputs_val.len(), }); From f9ef0e8b8199d205d0dbe052e6ba67aea16c11ca Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Fri, 7 Nov 2025 18:00:49 +0100 Subject: [PATCH 09/13] Minor --- circuit/src/builder/expression_builder.rs | 1 + circuit/src/tables/runner.rs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/circuit/src/builder/expression_builder.rs b/circuit/src/builder/expression_builder.rs index 768513b..4ccceeb 100644 --- a/circuit/src/builder/expression_builder.rs +++ b/circuit/src/builder/expression_builder.rs @@ -317,6 +317,7 @@ mod tests { use p3_field::Field; use super::*; + use crate::CircuitError; #[test] fn test_new_builder_has_zero_constant() { diff --git a/circuit/src/tables/runner.rs b/circuit/src/tables/runner.rs index fb5c617..66d161e 100644 --- a/circuit/src/tables/runner.rs +++ b/circuit/src/tables/runner.rs @@ -444,13 +444,13 @@ mod tests { inputs: Vec, } - impl X { + impl XHint { pub fn new(a: ExprId, b: ExprId) -> Self { Self { inputs: vec![a, b] } } } - impl WitnessHintFiller for X { + impl WitnessHintFiller for XHint { fn inputs(&self) -> &[ExprId] { &self.inputs } @@ -482,7 +482,7 @@ mod tests { let c37 = builder.add_const(BabyBear::from_u64(37)); let c111 = builder.add_const(BabyBear::from_u64(111)); - let x_hint = X::new(c37, c111); + let x_hint = XHint::new(c37, c111); let x = builder.alloc_witness_hints(x_hint, "x")[0]; let mul_result = builder.mul(c37, x); From 2f948924902e9765b5c87a902853b3266cb15cb4 Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Fri, 7 Nov 2025 18:36:09 +0100 Subject: [PATCH 10/13] Address remaining reviews --- circuit/src/builder/circuit_builder.rs | 43 +++---------------- .../builder/compiler/expression_lowerer.rs | 8 ++-- circuit/src/op.rs | 12 +++--- circuit/src/utils.rs | 8 +--- 4 files changed, 21 insertions(+), 50 deletions(-) diff --git a/circuit/src/builder/circuit_builder.rs b/circuit/src/builder/circuit_builder.rs index dc8af54..8ec907c 100644 --- a/circuit/src/builder/circuit_builder.rs +++ b/circuit/src/builder/circuit_builder.rs @@ -10,7 +10,7 @@ use super::{BuilderConfig, ExpressionBuilder}; use crate::CircuitBuilderError; use crate::builder::public_input_tracker::PublicInputTracker; use crate::circuit::Circuit; -use crate::op::{NonPrimitiveOpType, WitnessHintFiller}; +use crate::op::{DefaultHint, NonPrimitiveOpType, WitnessHintFiller}; use crate::ops::MmcsVerifyConfig; use crate::types::{ExprId, NonPrimitiveOpId, WitnessAllocator, WitnessId}; @@ -140,9 +140,8 @@ where count: usize, label: &'static str, ) -> Vec { - (0..count) - .map(|_| self.expr_builder.add_witness_hint(label)) - .collect() + self.expr_builder + .add_witness_hints(DefaultHint { n_outputs: count }, label) } /// Adds a constant to the circuit (deduplicated). @@ -437,7 +436,6 @@ where #[cfg(test)] mod tests { use alloc::vec; - use alloc::vec::Vec; use p3_baby_bear::BabyBear; use p3_field::PrimeCharacteristicRing; @@ -713,39 +711,12 @@ mod tests { assert_eq!(circuit.primitive_ops.len(), 2); } - #[derive(Debug, Clone)] - struct ConstantHint { - inputs: Vec, - } - - impl ConstantHint { - pub fn new(input: ExprId) -> Self { - Self { - inputs: vec![input], - } - } - } - - impl WitnessHintFiller for ConstantHint { - fn inputs(&self) -> &[ExprId] { - &self.inputs - } - - fn n_outputs(&self) -> usize { - 1 - } - - fn compute_outputs(&self, _inputs_val: Vec) -> Result, crate::CircuitError> { - Ok(vec![F::from_usize(C)]) - } - } #[test] fn test_build_with_witness_hint() { let mut builder = CircuitBuilder::::new(); - let a = builder.add_const(BabyBear::ZERO); - let mock_filler = ConstantHint::<1>::new(a); - let b = builder.alloc_witness_hints(mock_filler, "a"); - assert_eq!(b.len(), 1); + let default_hint = DefaultHint { n_outputs: 1 }; + let a = builder.alloc_witness_hints(default_hint, "a"); + assert_eq!(a.len(), 1); let circuit = builder .build() .expect("Circuit with operations should build"); @@ -757,7 +728,7 @@ mod tests { crate::op::Op::Unconstrained { inputs, outputs, .. } => { - assert_eq!(*inputs, vec![WitnessId(0)]); + assert_eq!(*inputs, vec![]); assert_eq!(*outputs, vec![WitnessId(1)]); } _ => panic!("Expected Unconstrained at index 0"), diff --git a/circuit/src/builder/compiler/expression_lowerer.rs b/circuit/src/builder/compiler/expression_lowerer.rs index 61b9d08..2bb68ca 100644 --- a/circuit/src/builder/compiler/expression_lowerer.rs +++ b/circuit/src/builder/compiler/expression_lowerer.rs @@ -10,7 +10,7 @@ use crate::Op; use crate::builder::CircuitBuilderError; use crate::builder::compiler::get_witness_id; use crate::expr::{Expr, ExpressionGraph}; -use crate::op::{DefaultHint, WitnessHintFiller}; +use crate::op::WitnessHintFiller; use crate::types::{ExprId, WitnessAllocator, WitnessId}; /// Sparse disjoint-set "find" with path compression over a HashMap (iterative). @@ -187,7 +187,7 @@ where } } - // Pass C: emit arithmetic ops in creation order; tie outputs to class slot if connected + // Pass C: emit arithmetic and unconstrained ops in creation order; tie outputs to class slot if connected let mut hints_sequence = vec![]; let mut fillers_iter = self.hints_with_fillers.iter().cloned(); for (expr_idx, expr) in self.graph.nodes().iter().enumerate() { @@ -200,7 +200,9 @@ where expr_to_widx.insert(expr_id, out_widx); hints_sequence.push(out_widx); if *last_hint { - let filler = fillers_iter.next().unwrap_or(DefaultHint::boxed_default()); + let filler = fillers_iter.next().expect( + "By construction, every sequence of witness must haver one filler", + ); let inputs = filler .inputs() .iter() diff --git a/circuit/src/op.rs b/circuit/src/op.rs index eb59207..4807fec 100644 --- a/circuit/src/op.rs +++ b/circuit/src/op.rs @@ -419,25 +419,27 @@ impl Clone for Box> { } #[derive(Debug, Clone, Default)] -pub struct DefaultHint {} +pub struct DefaultHint { + pub n_outputs: usize, +} impl DefaultHint { - pub fn boxed_default() -> Box> { + pub fn boxed_default() -> Box> { Box::new(Self::default()) } } -impl WitnessHintFiller for DefaultHint { +impl WitnessHintFiller for DefaultHint { fn inputs(&self) -> &[ExprId] { &[] } fn n_outputs(&self) -> usize { - 1 + self.n_outputs } fn compute_outputs(&self, _inputs_val: Vec) -> Result, CircuitError> { - Ok(vec![F::default()]) + Ok(vec![F::default(); self.n_outputs]) } } diff --git a/circuit/src/utils.rs b/circuit/src/utils.rs index 7f2ea11..0d85d18 100644 --- a/circuit/src/utils.rs +++ b/circuit/src/utils.rs @@ -4,6 +4,7 @@ use core::marker::PhantomData; use p3_field::{ExtensionField, Field, PrimeField64}; use p3_uni_stark::{Entry, SymbolicExpression}; +use p3_util::log2_ceil_u64; use crate::op::WitnessHintFiller; use crate::{CircuitBuilder, CircuitError, ExprId}; @@ -167,16 +168,11 @@ impl> WitnessHintFiller for BinaryDec } fn compute_outputs(&self, inputs_val: Vec) -> Result, CircuitError> { - if inputs_val.len() != 1 { - return Err(CircuitError::UnconstrainedOpInputLengthMismatch { - expected: 1, - got: inputs_val.len(), - }); - } let val: u64 = inputs_val[0].as_basis_coefficients_slice()[0].as_canonical_u64(); let bits = (0..self.n_bits) .map(|i| F::from_bool(val >> i & 1 == 1)) .collect(); + debug_assert!(self.n_bits as u64 >= log2_ceil_u64(val)); Ok(bits) } } From 7f316797d4647ff8bb5eadbcfbc96661a261c9eb Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Fri, 7 Nov 2025 18:51:45 +0100 Subject: [PATCH 11/13] Change hints_with_fillers to hints_fillers --- .../builder/compiler/expression_lowerer.rs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/circuit/src/builder/compiler/expression_lowerer.rs b/circuit/src/builder/compiler/expression_lowerer.rs index 2bb68ca..16191dd 100644 --- a/circuit/src/builder/compiler/expression_lowerer.rs +++ b/circuit/src/builder/compiler/expression_lowerer.rs @@ -74,7 +74,7 @@ pub struct ExpressionLowerer<'a, F> { public_input_count: usize, /// The hint witnesses with their respective filler - hints_with_fillers: &'a [Box>], + hints_fillers: &'a [Box>], /// Witness allocator witness_alloc: WitnessAllocator, @@ -89,14 +89,14 @@ where graph: &'a ExpressionGraph, pending_connects: &'a [(ExprId, ExprId)], public_input_count: usize, - hints_with_fillers: &'a [Box>], + hints_fillers: &'a [Box>], witness_alloc: WitnessAllocator, ) -> Self { Self { graph, pending_connects, public_input_count, - hints_with_fillers, + hints_fillers, witness_alloc, } } @@ -189,7 +189,7 @@ where // Pass C: emit arithmetic and unconstrained ops in creation order; tie outputs to class slot if connected let mut hints_sequence = vec![]; - let mut fillers_iter = self.hints_with_fillers.iter().cloned(); + let mut fillers_iter = self.hints_fillers.iter().cloned(); for (expr_idx, expr) in self.graph.nodes().iter().enumerate() { let expr_id = ExprId(expr_idx as u32); match expr { @@ -427,10 +427,10 @@ mod tests { let quot = graph.add_expr(Expr::Div { lhs: diff, rhs: p2 }); let connects = vec![]; - let hints_with_fillers = vec![]; + let hints_fillers = vec![]; let alloc = WitnessAllocator::new(); - let lowerer = ExpressionLowerer::new(&graph, &connects, 3, &hints_with_fillers, alloc); + let lowerer = ExpressionLowerer::new(&graph, &connects, 3, &hints_fillers, alloc); let (prims, public_rows, expr_map, public_map, witness_count) = lowerer.lower().unwrap(); // Verify Primitives @@ -597,10 +597,10 @@ mod tests { // Group B: p1 ~ p2 ~ p3 (transitive) // Group C: sum ~ p4 (operation result shared) let connects = vec![(c_42, p0), (p1, p2), (p2, p3), (sum, p4)]; - let hints_with_fillers = vec![]; + let hints_fillers = vec![]; let alloc = WitnessAllocator::new(); - let lowerer = ExpressionLowerer::new(&graph, &connects, 5, &hints_with_fillers, alloc); + let lowerer = ExpressionLowerer::new(&graph, &connects, 5, &hints_fillers, alloc); let (prims, public_rows, expr_map, public_map, witness_count) = lowerer.lower().unwrap(); // Verify Primitives @@ -738,9 +738,9 @@ mod tests { }); let connects = vec![]; - let hints_with_fillers = vec![]; + let hints_fillers = vec![]; let alloc = WitnessAllocator::new(); - let lowerer = ExpressionLowerer::new(&graph, &connects, 0, &hints_with_fillers, alloc); + let lowerer = ExpressionLowerer::new(&graph, &connects, 0, &hints_fillers, alloc); let result = lowerer.lower(); assert!(result.is_err()); @@ -760,9 +760,9 @@ mod tests { }); let connects = vec![]; - let hints_with_fillers = vec![]; + let hints_fillers = vec![]; let alloc = WitnessAllocator::new(); - let lowerer = ExpressionLowerer::new(&graph, &connects, 0, &hints_with_fillers, alloc); + let lowerer = ExpressionLowerer::new(&graph, &connects, 0, &hints_fillers, alloc); let result = lowerer.lower(); assert!(result.is_err()); From 43465ba4fb30dd93caec6eda39e5bd44fb413873 Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Fri, 7 Nov 2025 18:54:08 +0100 Subject: [PATCH 12/13] Remove UnconstrainedeOpArity --- circuit/src/builder/errors.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/circuit/src/builder/errors.rs b/circuit/src/builder/errors.rs index f9449a1..1fff8df 100644 --- a/circuit/src/builder/errors.rs +++ b/circuit/src/builder/errors.rs @@ -23,10 +23,6 @@ pub enum CircuitBuilderError { got: usize, }, - /// Unconstrained op received an unexpected number of input expressions. - #[error("Expects exactly {expected} witness expressions, got {got}")] - UnconstrainedeOpArity { expected: String, got: usize }, - /// Non-primitive operation rejected by the active policy/profile. #[error("Operation {op:?} is not allowed by the current profile")] OpNotAllowed { op: NonPrimitiveOpType }, From 92084358033e0e336119a563280385f3f40d7446 Mon Sep 17 00:00:00 2001 From: Alonso Gonzalez Date: Fri, 7 Nov 2025 18:58:40 +0100 Subject: [PATCH 13/13] Fix comment --- circuit/src/expr.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/circuit/src/expr.rs b/circuit/src/expr.rs index c3e6e6e..c79088a 100644 --- a/circuit/src/expr.rs +++ b/circuit/src/expr.rs @@ -9,10 +9,10 @@ pub enum Expr { Const(F), /// Public input at declaration position Public(usize), - /// Witness hints - allocates a WitnessId storing - /// a non-deterministic hint. The boolean indicates - /// wether this is the last witness in a sequence of hint. - /// A sequence of hints is one generated from a single filler. + /// Witness hints — allocates a `WitnessId` representing a + /// non-deterministic hint. The boolean flag indicates whether + /// this is the last witness in a sequence of related hints, + /// where each sequence is produced through a shared generation process. Witness { last_hint: bool }, /// Addition of two expressions Add { lhs: ExprId, rhs: ExprId },