Repository: PolyhedraZK/ExpanderCompilerCollection
Pull request #139: Builtin to binary
Author: login: siq1
Created: 2025-04-09T18:53:43Z | Merged: 2025-04-14T03:33:16Z

This PR implements a built-in ToBinary, to replace `to_binary_hint`.


Diff:
diff --git a/circuit-std-rs/src/sha256/m31_utils.rs b/circuit-std-rs/src/sha256/m31_utils.rs
index aafe94df..5721312f 100644
--- a/circuit-std-rs/src/sha256/m31_utils.rs
+++ b/circuit-std-rs/src/sha256/m31_utils.rs
@@ -1,13 +1,9 @@
-use arith::Field;
 use expander_compiler::frontend::{
-    declare_circuit, Config, Define, Error, M31Config, RootAPI, Variable, M31,
+    declare_circuit, Config, Define, M31Config, RootAPI, Variable, M31,
 };
 
 #[cfg(test)]
-use expander_compiler::{
-    frontend::{compile, CompileOptions},
-    hints::registry::HintRegistry,
-};
+use expander_compiler::frontend::{compile, CompileOptions, EmptyHintCaller};
 
 use num_bigint::BigInt;
 use num_traits::cast::ToPrimitive;
@@ -219,13 +215,7 @@ pub fn to_binary<C: Config, B: RootAPI<C>>(
     x: Variable,
     n_bits: usize,
 ) -> Vec<Variable> {
-    let bits = api.new_hint("myhint.tobinary", &[x], n_bits);
-    for bit in bits.iter() {
-        api.assert_is_bool(*bit);
-    }
-    let sum = from_binary(api, bits.to_vec());
-    api.assert_is_equal(sum, x);
-    bits
+    api.to_binary(x, n_bits)
 }
 pub fn from_binary<C: Config, B: RootAPI<C>>(api: &mut B, bits: Vec<Variable>) -> Variable {
     let mut res = api.constant(0);
@@ -237,14 +227,6 @@ pub fn from_binary<C: Config, B: RootAPI<C>>(api: &mut B, bits: Vec<Variable>) -
     res
 }
 
-pub fn to_binary_hint(x: &[M31], y: &mut [M31]) -> Result<(), Error> {
-    let t = x[0].to_u256();
-    for (i, k) in y.iter_mut().enumerate() {
-        *k = M31::from_u256(t >> i as u32 & 1);
-    }
-    Ok(())
-}
-
 pub fn big_is_zero<C: Config, B: RootAPI<C>>(api: &mut B, k: usize, in_: &[Variable]) -> Variable {
     let mut total = api.constant(k as u32);
     for val in in_.iter().take(k) {
@@ -355,9 +337,6 @@ impl Define<M31Config> for IDIVMODBITCircuit<Variable> {
 }
 #[test]
 fn test_idiv_mod_bit() {
-    //register hints
-    let mut hint_registry = HintRegistry::<M31>::new();
-    hint_registry.register("myhint.tobinary", to_binary_hint);
     //compile and test
     let compile_result = compile(&IDIVMODBITCircuit::default(), CompileOptions::default()).unwrap();
     let assignment = IDIVMODBITCircuit::<M31> {
@@ -367,7 +346,7 @@ fn test_idiv_mod_bit() {
     };
     let witness = compile_result
         .witness_solver
-        .solve_witness_with_hints(&assignment, &mut hint_registry)
+        .solve_witness_with_hints(&assignment, &mut EmptyHintCaller)
         .unwrap();
     let output = compile_result.layered_circuit.run(&witness);
     assert_eq!(output, vec![true]);
@@ -396,9 +375,6 @@ impl Define<M31Config> for BITCONVERTCircuit<Variable> {
 }
 #[test]
 fn test_bit_convert() {
-    //register hints
-    let mut hint_registry = HintRegistry::<M31>::new();
-    hint_registry.register("myhint.tobinary", to_binary_hint);
     //compile and test
     let compile_result = compile(&BITCONVERTCircuit::default(), CompileOptions::default()).unwrap();
     let assignment = BITCONVERTCircuit::<M31> {
@@ -418,7 +394,7 @@ fn test_bit_convert() {
     };
     let witness = compile_result
         .witness_solver
-        .solve_witness_with_hints(&assignment, &mut hint_registry)
+        .solve_witness_with_hints(&assignment, &mut EmptyHintCaller)
         .unwrap();
     let output = compile_result.layered_circuit.run(&witness);
     assert_eq!(output, vec![true]);
diff --git a/circuit-std-rs/src/utils.rs b/circuit-std-rs/src/utils.rs
index 827845ef..3250de3f 100644
--- a/circuit-std-rs/src/utils.rs
+++ b/circuit-std-rs/src/utils.rs
@@ -8,7 +8,6 @@ use crate::{
         inverse_e12_hint, inverse_e2_hint, inverse_e6_hint, mul_hint, simple_rangecheck_hint,
     },
     logup::{query_count_by_key_hint, query_count_hint, rangeproof_hint},
-    sha256::m31_utils::to_binary_hint,
 };
 
 pub fn simple_select<C: Config, B: RootAPI<C>>(
@@ -41,7 +40,6 @@ pub fn simple_lookup2<C: Config, B: RootAPI<C>>(
 }
 
 pub fn register_hint(hint_registry: &mut HintRegistry<M31>) {
-    hint_registry.register("myhint.tobinary", to_binary_hint);
     hint_registry.register("myhint.mulhint", mul_hint);
     hint_registry.register("myhint.simple_rangecheck_hint", simple_rangecheck_hint);
     hint_registry.register("myhint.querycounthint", query_count_hint);
diff --git a/circuit-std-rs/tests/sha256_m31.rs b/circuit-std-rs/tests/sha256_m31.rs
index 508a9133..4519fbf9 100644
--- a/circuit-std-rs/tests/sha256_m31.rs
+++ b/circuit-std-rs/tests/sha256_m31.rs
@@ -1,4 +1,4 @@
-use circuit_std_rs::{sha256::m31::sha256_37bytes, sha256::m31_utils::to_binary_hint};
+use circuit_std_rs::sha256::m31::sha256_37bytes;
 use expander_compiler::frontend::*;
 use extra::*;
 use sha2::{Digest, Sha256};
@@ -32,8 +32,6 @@ impl Define<M31Config> for SHA25637BYTESCircuit<Variable> {
 
 #[test]
 fn test_sha256_37bytes() {
-    let mut hint_registry = HintRegistry::<M31>::new();
-    hint_registry.register("myhint.tobinary", to_binary_hint);
     let compile_result =
         compile(&SHA25637BYTESCircuit::default(), CompileOptions::default()).unwrap();
     for i in 0..1 {
@@ -50,7 +48,7 @@ fn test_sha256_37bytes() {
         }
         let witness = compile_result
             .witness_solver
-            .solve_witness_with_hints(&assignment, &mut hint_registry)
+            .solve_witness_with_hints(&assignment, &mut EmptyHintCaller)
             .unwrap();
         let output = compile_result.layered_circuit.run(&witness);
         assert_eq!(output, vec![true]);
@@ -59,8 +57,6 @@ fn test_sha256_37bytes() {
 
 #[test]
 fn debug_sha256_37bytes() {
-    let mut hint_registry = HintRegistry::<M31>::new();
-    hint_registry.register("myhint.tobinary", to_binary_hint);
     let data = [255; 37];
     let mut hash = Sha256::new();
     hash.update(data);
@@ -72,5 +68,9 @@ fn debug_sha256_37bytes() {
     for i in 0..32 {
         assignment.output[i] = M31::from(output[i] as u32);
     }
-    debug_eval(&SHA25637BYTESCircuit::default(), &assignment, hint_registry);
+    debug_eval(
+        &SHA25637BYTESCircuit::default(),
+        &assignment,
+        EmptyHintCaller,
+    );
 }
diff --git a/circuit-std-rs/tests/sha2_m31.rs b/circuit-std-rs/tests/sha2_m31.rs
index 508a9133..4519fbf9 100644
--- a/circuit-std-rs/tests/sha2_m31.rs
+++ b/circuit-std-rs/tests/sha2_m31.rs
@@ -1,4 +1,4 @@
-use circuit_std_rs::{sha256::m31::sha256_37bytes, sha256::m31_utils::to_binary_hint};
+use circuit_std_rs::sha256::m31::sha256_37bytes;
 use expander_compiler::frontend::*;
 use extra::*;
 use sha2::{Digest, Sha256};
@@ -32,8 +32,6 @@ impl Define<M31Config> for SHA25637BYTESCircuit<Variable> {
 
 #[test]
 fn test_sha256_37bytes() {
-    let mut hint_registry = HintRegistry::<M31>::new();
-    hint_registry.register("myhint.tobinary", to_binary_hint);
     let compile_result =
         compile(&SHA25637BYTESCircuit::default(), CompileOptions::default()).unwrap();
     for i in 0..1 {
@@ -50,7 +48,7 @@ fn test_sha256_37bytes() {
         }
         let witness = compile_result
             .witness_solver
-            .solve_witness_with_hints(&assignment, &mut hint_registry)
+            .solve_witness_with_hints(&assignment, &mut EmptyHintCaller)
             .unwrap();
         let output = compile_result.layered_circuit.run(&witness);
         assert_eq!(output, vec![true]);
@@ -59,8 +57,6 @@ fn test_sha256_37bytes() {
 
 #[test]
 fn debug_sha256_37bytes() {
-    let mut hint_registry = HintRegistry::<M31>::new();
-    hint_registry.register("myhint.tobinary", to_binary_hint);
     let data = [255; 37];
     let mut hash = Sha256::new();
     hash.update(data);
@@ -72,5 +68,9 @@ fn debug_sha256_37bytes() {
     for i in 0..32 {
         assignment.output[i] = M31::from(output[i] as u32);
     }
-    debug_eval(&SHA25637BYTESCircuit::default(), &assignment, hint_registry);
+    debug_eval(
+        &SHA25637BYTESCircuit::default(),
+        &assignment,
+        EmptyHintCaller,
+    );
 }
diff --git a/expander_compiler/src/builder/basic.rs b/expander_compiler/src/builder/basic.rs
index faf57a88..4e787043 100644
--- a/expander_compiler/src/builder/basic.rs
+++ b/expander_compiler/src/builder/basic.rs
@@ -101,6 +101,12 @@ impl PartialOrd for LinMeta {
     }
 }
 
+pub enum InsnTransformResult<C: Config, IrcOut: IrConfig<Config = C>> {
+    Insn(IrcOut::Instruction),
+    Vars(Vec<usize>),
+    Err(Error),
+}
+
 pub trait InsnTransformAndExecute<
     'a,
     C: Config,
@@ -111,7 +117,7 @@ pub trait InsnTransformAndExecute<
     fn transform_in_to_out(
         &mut self,
         in_insn: &IrcIn::Instruction,
-    ) -> Result<IrcOut::Instruction, Error>;
+    ) -> InsnTransformResult<C, IrcOut>;
     fn execute_out<'b>(
         &mut self,
         out_insn: &IrcOut::Instruction,
@@ -325,6 +331,16 @@ where
     pub fn push_insn(&mut self, out_insn: IrcOut::Instruction) -> Option<usize> {
         self.push_insn_with_root(out_insn, None)
     }
+    pub fn push_insn_multi_out(&mut self, out_insn: IrcOut::Instruction) -> Vec<usize> {
+        let num_out = out_insn.num_outputs();
+        self.out_insns.push(out_insn.clone());
+        self.execute_out(&out_insn, None);
+        let mut out_var_ids = Vec::new();
+        for i in 0..num_out {
+            out_var_ids.push(self.out_var_exprs.len() - num_out + i);
+        }
+        out_var_ids
+    }
 
     fn process_insn<'b>(
         &mut self,
@@ -335,12 +351,20 @@ where
         'a: 'b,
     {
         let in_mapped = in_insn.replace_vars(|x| self.in_to_out[x]);
-        let out_insn = self.transform_in_to_out(&in_mapped)?;
-        assert_eq!(out_insn.num_outputs(), in_insn.num_outputs());
-        let start_id = self.out_var_exprs.len();
-        self.push_insn_with_root(out_insn, Some(root));
-        for i in 0..in_insn.num_outputs() {
-            self.in_to_out.push(start_id + i);
+        match self.transform_in_to_out(&in_mapped) {
+            InsnTransformResult::Insn(out_insn) => {
+                assert_eq!(out_insn.num_outputs(), in_insn.num_outputs());
+                let start_id = self.out_var_exprs.len();
+                self.push_insn_with_root(out_insn, Some(root));
+                for i in 0..in_insn.num_outputs() {
+                    self.in_to_out.push(start_id + i);
+                }
+            }
+            InsnTransformResult::Vars(vars) => {
+                assert_eq!(vars.len(), in_insn.num_outputs());
+                self.in_to_out.extend(vars);
+            }
+            InsnTransformResult::Err(err) => return Err(err),
         }
         Ok(())
     }
diff --git a/expander_compiler/src/builder/final_build.rs b/expander_compiler/src/builder/final_build.rs
index 9cf22d31..1a446184 100644
--- a/expander_compiler/src/builder/final_build.rs
+++ b/expander_compiler/src/builder/final_build.rs
@@ -10,7 +10,7 @@ use crate::utils::error::Error;
 
 use super::basic::{
     process_circuit, to_really_single, try_get_really_single_id, ConstraintStatus,
-    InsnTransformAndExecute, RootBuilder,
+    InsnTransformAndExecute, InsnTransformResult, RootBuilder,
 };
 
 type IrcIn<C> = ir::hint_less::Irc<C>;
@@ -20,8 +20,8 @@ type InsnOut<C> = ir::hint_less::Instruction<C>;
 type Builder<'a, C> = super::basic::Builder<'a, C, IrcIn<C>, IrcOut<C>>;
 
 impl<'a, C: Config> InsnTransformAndExecute<'a, C, IrcIn<C>, IrcOut<C>> for Builder<'a, C> {
-    fn transform_in_to_out(&mut self, in_insn: &InsnIn<C>) -> Result<InsnOut<C>, Error> {
-        Ok(in_insn.clone())
+    fn transform_in_to_out(&mut self, in_insn: &InsnIn<C>) -> InsnTransformResult<C, IrcOut<C>> {
+        InsnTransformResult::Insn(in_insn.clone())
     }
 
     fn transform_in_con_to_out(&mut self, in_con: &RawConstraint) -> Result<RawConstraint, Error> {
diff --git a/expander_compiler/src/builder/hint_normalize.rs b/expander_compiler/src/builder/hint_normalize.rs
index 9cc87f5c..c781cfc0 100644
--- a/expander_compiler/src/builder/hint_normalize.rs
+++ b/expander_compiler/src/builder/hint_normalize.rs
@@ -15,7 +15,9 @@ use crate::{
     hints::BuiltinHintIds,
 };
 
-use super::basic::{process_root_circuit, InsnTransformAndExecute, RootBuilder};
+use super::basic::{
+    process_root_circuit, InsnTransformAndExecute, InsnTransformResult, RootBuilder,
+};
 
 type IrcIn<C> = ir::source::Irc<C>;
 type IrcOut<C> = ir::hint_normalized::Irc<C>;
@@ -63,14 +65,11 @@ impl<'a, C: Config> Builder<'a, C> {
     fn push_mul(&mut self, a: usize, b: usize) -> usize {
         self.push_insn(InsnOut::Mul(vec![a, b])).unwrap()
     }
-    fn copy(&mut self, a: usize) -> InsnOut<C> {
-        InsnOut::LinComb(LinComb {
-            terms: vec![LinCombTerm {
-                coef: CircuitField::<C>::one(),
-                var: a,
-            }],
-            constant: CircuitField::<C>::zero(),
-        })
+    fn copy(&mut self, a: usize) -> InsnTransformResult<C, IrcOut<C>> {
+        self.copys(&[a])
+    }
+    fn copys(&mut self, a: &[usize]) -> InsnTransformResult<C, IrcOut<C>> {
+        InsnTransformResult::Vars(a.to_vec())
     }
     fn bool_cond(&mut self, a: usize) -> usize {
         let one = self.push_const(CircuitField::<C>::one());
@@ -88,15 +87,17 @@ impl<'a, C: Config> Builder<'a, C> {
 }
 
 impl<'a, C: Config> InsnTransformAndExecute<'a, C, IrcIn<C>, IrcOut<C>> for Builder<'a, C> {
-    fn transform_in_to_out(&mut self, in_insn: &InsnIn<C>) -> Result<InsnOut<C>, Error> {
+    fn transform_in_to_out(&mut self, in_insn: &InsnIn<C>) -> InsnTransformResult<C, IrcOut<C>> {
         use ir::source::Instruction::*;
-        Ok(match in_insn {
+        InsnTransformResult::Insn(match in_insn {
             LinComb(lcs) => InsnOut::LinComb(lcs.clone()),
             Mul(vars) => InsnOut::Mul(vars.clone()),
             Div { x, y, checked } => match self.constant_value(*y) {
                 Some(yv) => {
                     if yv.is_zero() {
-                        return Err(Error::UserError("division by zero constant".to_string()));
+                        return InsnTransformResult::Err(Error::UserError(
+                            "division by zero constant".to_string(),
+                        ));
                     }
                     let y = self.push_const(yv.inv().unwrap());
                     InsnOut::Mul(vec![*x, y])
@@ -126,7 +127,7 @@ impl<'a, C: Config> InsnTransformAndExecute<'a, C, IrcIn<C>, IrcOut<C>> for Buil
                         let multy = self.push_mul(*y, div_res);
                         let subx = self.push_sub(multy, *x);
                         self.assert((), subx);
-                        self.copy(div_res)
+                        return self.copy(div_res);
                     }
                 }
             },
@@ -155,7 +156,7 @@ impl<'a, C: Config> InsnTransformAndExecute<'a, C, IrcIn<C>, IrcOut<C>> for Buil
                         .unwrap(),
                 };
                 self.mark_bool(res);
-                self.copy(res)
+                return self.copy(res);
             }
             IsZero(x) => {
                 if let Some(xv) = self.constant_value(*x) {
@@ -178,7 +179,7 @@ impl<'a, C: Config> InsnTransformAndExecute<'a, C, IrcIn<C>, IrcOut<C>> for Buil
                     let xm = self.push_mul(*x, m);
                     self.assert((), xm);
                     self.mark_bool(m);
-                    self.copy(m)
+                    return self.copy(m);
                 }
             }
             Commit(_) => {
@@ -208,7 +209,10 @@ impl<'a, C: Config> InsnTransformAndExecute<'a, C, IrcIn<C>, IrcOut<C>> for Buil
                 let xc = self.constant_value(*x);
                 let yc = self.constant_value(*y);
                 if let (Some(xv), Some(yv)) = (&xc, &yc) {
-                    InsnOut::ConstantLike(Coef::Constant(op.eval(xv, yv)?))
+                    InsnOut::ConstantLike(Coef::Constant(match op.eval(xv, yv) {
+                        Ok(v) => v,
+                        Err(e) => return InsnTransformResult::Err(e),
+                    }))
                 } else {
                     use ir::source::UnconstrainedBinOpType::*;
                     let op = match op {
@@ -244,9 +248,9 @@ impl<'a, C: Config> InsnTransformAndExecute<'a, C, IrcIn<C>, IrcOut<C>> for Buil
             } => {
                 if let Some(cond) = self.constant_value(*cond) {
                     if cond.is_zero() {
-                        self.copy(*if_false)
+                        return self.copy(*if_false);
                     } else {
-                        self.copy(*if_true)
+                        return self.copy(*if_true);
                     }
                 } else {
                     InsnOut::Hint {
@@ -260,6 +264,43 @@ impl<'a, C: Config> InsnTransformAndExecute<'a, C, IrcIn<C>, IrcOut<C>> for Buil
                 gate_type: *gate_type,
                 inputs: inputs.clone(),
             },
+            ToBinary { x, num_bits } => {
+                let bits = self.push_insn_multi_out(InsnOut::Hint {
+                    hint_id: BuiltinHintIds::ToBinary as u64 as usize,
+                    inputs: vec![*x],
+                    num_outputs: *num_bits,
+                });
+                let mut sum = self.push_const(CircuitField::<C>::zero());
+                let mut coef = CircuitField::<C>::one();
+                for &bit in bits.iter() {
+                    self.assert_bool(bit);
+                    let bit = self
+                        .push_insn(InsnOut::LinComb(expr::LinComb {
+                            terms: vec![LinCombTerm { coef, var: bit }],
+                            constant: CircuitField::<C>::zero(),
+                        }))
+                        .unwrap();
+                    sum = self.push_add(sum, bit);
+                    coef = coef.double();
+                }
+                let sum = self
+                    .push_insn(InsnOut::LinComb(expr::LinComb {
+                        terms: vec![
+                            LinCombTerm {
+                                coef: -CircuitField::<C>::one(),
+                                var: sum,
+                            },
+                            LinCombTerm {
+                                coef: CircuitField::<C>::one(),
+                                var: *x,
+                            },
+                        ],
+                        constant: CircuitField::<C>::zero(),
+                    }))
+                    .unwrap();
+                self.assert((), sum);
+                return self.copys(&bits);
+            }
         })
     }
 
@@ -411,9 +452,11 @@ mod tests {
                             .map(|_| CField::random_unsafe(&mut rand::thread_rng()))
                             .collect();
                         let e1 = root.eval_unsafe_with_errors(inputs.clone());
-                        let e2 = root_processed.eval_unsafe_with_errors(inputs);
                         if e1.is_ok() {
+                            let e2 = root_processed.eval_unsafe_with_errors(inputs);
                             assert_eq!(e2, e1);
+                        } else if e1.as_ref().err().unwrap().is_internal() {
+                            panic!("{:?}", e1);
                         }
                     }
                 }
@@ -452,9 +495,11 @@ mod tests {
                             .map(|_| CField::random_unsafe(&mut rand::thread_rng()))
                             .collect();
                         let e1 = root.eval_unsafe_with_errors(inputs.clone());
-                        let e2 = root_processed.eval_unsafe_with_errors(inputs);
                         if e1.is_ok() {
+                            let e2 = root_processed.eval_unsafe_with_errors(inputs);
                             assert_eq!(e2, e1);
+                        } else if e1.as_ref().err().unwrap().is_internal() {
+                            panic!("{:?}", e1);
                         }
                     }
                 }
diff --git a/expander_compiler/src/circuit/ir/dest/mul_fanout_limit.rs b/expander_compiler/src/circuit/ir/dest/mul_fanout_limit.rs
index 4666a725..e787e3c0 100644
--- a/expander_compiler/src/circuit/ir/dest/mul_fanout_limit.rs
+++ b/expander_compiler/src/circuit/ir/dest/mul_fanout_limit.rs
@@ -445,36 +445,45 @@ mod tests {
     fn full_fanout_test_and_dump() {
         use crate::circuit::ir::common::rand_gen::{RandomCircuitConfig, RandomRange};
 
-        let config = RandomCircuitConfig {
-            seed: 2,
-            num_circuits: RandomRange { min: 20, max: 20 },
-            num_inputs: RandomRange { min: 1, max: 3 },
-            num_instructions: RandomRange { min: 30, max: 50 },
-            num_constraints: RandomRange { min: 0, max: 5 },
-            num_outputs: RandomRange { min: 1, max: 3 },
-            num_terms: RandomRange { min: 1, max: 5 },
-            sub_circuit_prob: 0.05,
-        };
-        let root = crate::circuit::ir::source::RootCircuit::<C>::random(&config);
-        assert_eq!(root.validate(), Ok(()));
-        let (_, circuit) = crate::compile::compile_with_options::<_, NormalInputType>(
-            &root,
-            crate::compile::CompileOptions::default().with_mul_fanout_limit(256),
-        )
-        .unwrap();
-        assert_eq!(circuit.validate(), Ok(()));
-        for segment in circuit.segments.iter() {
-            let mut ref_num = vec![0; segment.num_inputs.get(0)];
-            for m in segment.gate_muls.iter() {
-                ref_num[m.inputs[0].offset] += 1;
-                ref_num[m.inputs[1].offset] += 1;
-            }
-            for x in ref_num.iter() {
-                assert!(*x <= 256);
+        for i in 0..1000 {
+            let config = RandomCircuitConfig {
+                seed: i + 100000,
+                num_circuits: RandomRange { min: 20, max: 20 },
+                num_inputs: RandomRange { min: 1, max: 3 },
+                num_instructions: RandomRange { min: 30, max: 50 },
+                num_constraints: RandomRange { min: 0, max: 5 },
+                num_outputs: RandomRange { min: 1, max: 3 },
+                num_terms: RandomRange { min: 1, max: 5 },
+                sub_circuit_prob: 0.05,
+            };
+            let root = crate::circuit::ir::source::RootCircuit::<C>::random(&config);
+            assert_eq!(root.validate(), Ok(()));
+            match crate::compile::compile_with_options::<_, NormalInputType>(
+                &root,
+                crate::compile::CompileOptions::default().with_mul_fanout_limit(256),
+            ) {
+                Err(e) => {
+                    if e.is_internal() {
+                        panic!("{:?}", e);
+                    }
+                }
+                Ok((_, circuit)) => {
+                    assert_eq!(circuit.validate(), Ok(()));
+                    for segment in circuit.segments.iter() {
+                        let mut ref_num = vec![0; segment.num_inputs.get(0)];
+                        for m in segment.gate_muls.iter() {
+                            ref_num[m.inputs[0].offset] += 1;
+                            ref_num[m.inputs[1].offset] += 1;
+                        }
+                        for x in ref_num.iter() {
+                            assert!(*x <= 256);
+                        }
+                    }
+
+                    let mut buf = Vec::new();
+                    circuit.serialize_into(&mut buf).unwrap();
+                }
             }
         }
-
-        let mut buf = Vec::new();
-        circuit.serialize_into(&mut buf).unwrap();
     }
 }
diff --git a/expander_compiler/src/circuit/ir/source/mod.rs b/expander_compiler/src/circuit/ir/source/mod.rs
index af5fb9f6..7ca84662 100644
--- a/expander_compiler/src/circuit/ir/source/mod.rs
+++ b/expander_compiler/src/circuit/ir/source/mod.rs
@@ -4,7 +4,7 @@ use crate::{
     circuit::{config::Config, layered::Coef},
     field::{Field, FieldArith},
     frontend::CircuitField,
-    hints::{self, circom_shift_l_impl, circom_shift_r_impl},
+    hints::{self, circom_shift_l_impl, circom_shift_r_impl, to_binary},
     utils::error::Error,
 };
 
@@ -60,6 +60,10 @@ pub enum Instruction<C: Config> {
         gate_type: usize,
         inputs: Vec<usize>,
     },
+    ToBinary {
+        x: usize,
+        num_bits: usize,
+    },
 }
 
 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
@@ -164,6 +168,7 @@ impl<C: Config> common::Instruction<C> for Instruction<C> {
                 if_false,
             } => vec![*cond, *if_true, *if_false],
             Instruction::CustomGate { inputs, .. } => inputs.clone(),
+            Instruction::ToBinary { x, .. } => vec![*x],
         }
     }
     fn num_outputs(&self) -> usize {
@@ -180,6 +185,7 @@ impl<C: Config> common::Instruction<C> for Instruction<C> {
             Instruction::UnconstrainedBinOp { .. } => 1,
             Instruction::UnconstrainedSelect { .. } => 1,
             Instruction::CustomGate { .. } => 1,
+            Instruction::ToBinary { num_bits, .. } => *num_bits,
         }
     }
     fn as_sub_circuit_call(&self) -> Option<(usize, &Vec<usize>, usize)> {
@@ -254,6 +260,10 @@ impl<C: Config> common::Instruction<C> for Instruction<C> {
                 gate_type: *gate_type,
                 inputs: inputs.iter().map(|i| f(*i)).collect(),
             },
+            Instruction::ToBinary { x, num_bits } => Instruction::ToBinary {
+                x: f(*x),
+                num_bits: *num_bits,
+            },
         }
     }
     fn from_kx_plus_b(x: usize, k: CircuitField<C>, b: CircuitField<C>) -> Self {
@@ -294,6 +304,15 @@ impl<C: Config> common::Instruction<C> for Instruction<C> {
                     ))
                 }
             }
+            Instruction::ToBinary { num_bits, .. } => {
+                if *num_bits > 0 {
+                    Ok(())
+                } else {
+                    Err(Error::InternalError(
+                        "to_binary instruction must have at least 1 bit".to_string(),
+                    ))
+                }
+            }
             _ => Ok(()),
         }
     }
@@ -383,6 +402,10 @@ impl<C: Config> common::Instruction<C> for Instruction<C> {
                     hints::stub_impl(*gate_type, &inputs.iter().map(|i| values[*i]).collect(), 1);
                 EvalResult::Values(outputs)
             }
+            Instruction::ToBinary { x, num_bits } => match to_binary(values[*x], *num_bits) {
+                Ok(outputs) => EvalResult::Values(outputs),
+                Err(e) => EvalResult::Error(e),
+            },
         }
     }
 }
diff --git a/expander_compiler/src/circuit/ir/source/serde.rs b/expander_compiler/src/circuit/ir/source/serde.rs
index dc69e154..e8491b07 100644
--- a/expander_compiler/src/circuit/ir/source/serde.rs
+++ b/expander_compiler/src/circuit/ir/source/serde.rs
@@ -84,6 +84,11 @@ impl<C: Config> ExpSerde for Instruction<C> {
                 gate_type.serialize_into(&mut writer)?;
                 inputs.serialize_into(&mut writer)?;
             }
+            Instruction::ToBinary { x, num_bits } => {
+                13u8.serialize_into(&mut writer)?;
+                x.serialize_into(&mut writer)?;
+                num_bits.serialize_into(&mut writer)?;
+            }
         };
         Ok(())
     }
@@ -167,6 +172,10 @@ impl<C: Config> ExpSerde for Instruction<C> {
                 gate_type: usize::deserialize_from(&mut reader)?,
                 inputs: Vec::<usize>::deserialize_from(&mut reader)?,
             },
+            13 => Instruction::ToBinary {
+                x: usize::deserialize_from(&mut reader)?,
+                num_bits: usize::deserialize_from(&mut reader)?,
+            },
             _ => {
                 return Err(IoError::new(
                     std::io::ErrorKind::InvalidData,
diff --git a/expander_compiler/src/circuit/ir/source/tests.rs b/expander_compiler/src/circuit/ir/source/tests.rs
index 4303cbec..b0f738ff 100644
--- a/expander_compiler/src/circuit/ir/source/tests.rs
+++ b/expander_compiler/src/circuit/ir/source/tests.rs
@@ -53,7 +53,7 @@ impl<C: Config> RandomInstruction for Instruction<C> {
                     num_terms.random(&mut rnd).max(1),
                 )
             };
-            if rnd.gen::<f64>() < 0.8 {
+            if rnd.gen::<f64>() < 0.8 || num_outputs != 1 {
                 super::Instruction::Hint {
                     hint_id,
                     inputs: (0..num_inputs)
@@ -96,13 +96,13 @@ impl<C: Config> RandomInstruction for Instruction<C> {
                 y: rnd.next_u64() as usize % num_vars + 1,
                 op,
             }
-        } else if prob1 < 0.92 {
+        } else if prob1 < 0.91 {
             super::Instruction::UnconstrainedBinOp {
                 x: rnd.next_u64() as usize % num_vars + 1,
                 y: rnd.next_u64() as usize % num_vars + 1,
                 op: super::UnconstrainedBinOpType::Div,
             }
-        } else {
+        } else if prob1 < 0.98 {
             let op = match rnd.next_u64() % 3 {
                 0 => super::UnconstrainedBinOpType::BitAnd,
                 1 => super::UnconstrainedBinOpType::BitOr,
@@ -114,6 +114,12 @@ impl<C: Config> RandomInstruction for Instruction<C> {
                 y: rnd.next_u64() as usize % num_vars + 1,
                 op,
             }
+        } else {
+            super::Instruction::ToBinary {
+                x: rnd.next_u64() as usize % num_vars + 1,
+                num_bits: [1, 3, 66, 266, 267, 268, 270, 300, 300, 300]
+                    [rnd.next_u64() as usize % 10],
+            }
         }
     }
 }
diff --git a/expander_compiler/src/frontend/api.rs b/expander_compiler/src/frontend/api.rs
index f8bf9177..62bf4041 100644
--- a/expander_compiler/src/frontend/api.rs
+++ b/expander_compiler/src/frontend/api.rs
@@ -1,3 +1,5 @@
+use arith::Field;
+
 use crate::circuit::config::Config;
 
 use super::{
@@ -35,6 +37,11 @@ pub trait BasicAPI<C: Config> {
         self.div(1, x, true)
     }
     fn is_zero(&mut self, x: impl ToVariableOrValue<CircuitField<C>>) -> Variable;
+    fn to_binary(
+        &mut self,
+        x: impl ToVariableOrValue<CircuitField<C>>,
+        num_bits: usize,
+    ) -> Vec<Variable>;
     fn assert_is_zero(&mut self, x: impl ToVariableOrValue<CircuitField<C>>);
     fn assert_is_non_zero(&mut self, x: impl ToVariableOrValue<CircuitField<C>>);
     fn assert_is_bool(&mut self, x: impl ToVariableOrValue<CircuitField<C>>);
@@ -68,6 +75,21 @@ pub trait BasicAPI<C: Config> {
         &mut self,
         x: impl ToVariableOrValue<CircuitField<C>>,
     ) -> Option<CircuitField<C>>;
+
+    #[allow(clippy::wrong_self_convention)]
+    fn from_binary(&mut self, xs: &[Variable]) -> Variable {
+        if xs.is_empty() {
+            return self.constant(0);
+        }
+        let mut res = xs[0];
+        let mut mul = CircuitField::<C>::one();
+        for x in xs.iter().skip(1) {
+            mul = mul.double();
+            let tmp = self.mul(mul, x);
+            res = self.add(res, tmp);
+        }
+        res
+    }
 }
 
 pub trait UnconstrainedAPI<C: Config> {
diff --git a/expander_compiler/src/frontend/builder.rs b/expander_compiler/src/frontend/builder.rs
index 1fda6851..afc074be 100644
--- a/expander_compiler/src/frontend/builder.rs
+++ b/expander_compiler/src/frontend/builder.rs
@@ -365,6 +365,22 @@ impl<C: Config> BasicAPI<C> for Builder<C> {
         self.new_var()
     }
 
+    fn to_binary(
+        &mut self,
+        x: impl ToVariableOrValue<CircuitField<C>>,
+        num_bits: usize,
+    ) -> Vec<Variable> {
+        let xc = self.constant_value(x.clone());
+        if let Some(xv) = xc {
+            let values = hints::to_binary(xv, num_bits).unwrap();
+            return values.iter().map(|v| self.constant(*v)).collect();
+        }
+        let x = self.convert_to_variable(x);
+        self.instructions
+            .push(SourceInstruction::ToBinary { x: x.id, num_bits });
+        (0..num_bits).map(|_| self.new_var()).collect()
+    }
+
     fn assert_is_zero(&mut self, x: impl ToVariableOrValue<CircuitField<C>>) {
         let xc = self.constant_value(x.clone());
         if let Some(xv) = xc {
@@ -565,6 +581,14 @@ impl<C: Config> BasicAPI<C> for RootBuilder<C> {
         self.last_builder().is_zero(x)
     }
 
+    fn to_binary(
+        &mut self,
+        x: impl ToVariableOrValue<CircuitField<C>>,
+        num_bits: usize,
+    ) -> Vec<Variable> {
+        self.last_builder().to_binary(x, num_bits)
+    }
+
     fn assert_is_zero(&mut self, x: impl ToVariableOrValue<CircuitField<C>>) {
         self.last_builder().assert_is_zero(x)
     }
diff --git a/expander_compiler/src/frontend/debug.rs b/expander_compiler/src/frontend/debug.rs
index b031a563..916bed71 100644
--- a/expander_compiler/src/frontend/debug.rs
+++ b/expander_compiler/src/frontend/debug.rs
@@ -9,7 +9,10 @@ use crate::{
         },
     },
     field::FieldArith,
-    hints::registry::{hint_key_to_id, HintCaller},
+    hints::{
+        self,
+        registry::{hint_key_to_id, HintCaller},
+    },
 };
 
 use super::{
@@ -118,6 +121,18 @@ impl<C: Config, H: HintCaller<CircuitField<C>>> BasicAPI<C> for DebugBuilder<C,
         let x = self.convert_to_id(x);
         self.eval_ir_insn(IrInstruction::IsZero(x))
     }
+    fn to_binary(
+        &mut self,
+        x: impl ToVariableOrValue<CircuitField<C>>,
+        num_bits: usize,
+    ) -> Vec<Variable> {
+        let x = self.convert_to_id(x);
+        hints::to_binary(self.values[x], num_bits)
+            .unwrap()
+            .into_iter()
+            .map(|v| self.return_as_variable(v))
+            .collect()
+    }
     fn assert_is_zero(&mut self, x: impl ToVariableOrValue<CircuitField<C>>) {
         let x = self.convert_to_value(x);
         assert!(x.is_zero());
diff --git a/expander_compiler/src/hints/builtin.rs b/expander_compiler/src/hints/builtin.rs
index 76dc5ebe..e0949be4 100644
--- a/expander_compiler/src/hints/builtin.rs
+++ b/expander_compiler/src/hints/builtin.rs
@@ -26,6 +26,7 @@ pub enum BuiltinHintIds {
     GreaterEq,
     Lesser,
     Greater,
+    ToBinary,
 }
 
 #[cfg(not(target_pointer_width = "64"))]
@@ -59,6 +60,7 @@ impl BuiltinHintIds {
             x if x == BuiltinHintIds::GreaterEq as u64 as usize => Some(BuiltinHintIds::GreaterEq),
             x if x == BuiltinHintIds::Lesser as u64 as usize => Some(BuiltinHintIds::Lesser),
             x if x == BuiltinHintIds::Greater as u64 as usize => Some(BuiltinHintIds::Greater),
+            x if x == BuiltinHintIds::ToBinary as u64 as usize => Some(BuiltinHintIds::ToBinary),
             _ => None,
         }
     }
@@ -136,6 +138,18 @@ fn validate_builtin_hint(
                 ));
             }
         }
+        BuiltinHintIds::ToBinary => {
+            if num_inputs != 1 {
+                return Err(Error::InternalError(
+                    "to_binary requires exactly 1 input".to_string(),
+                ));
+            }
+            if num_outputs == 0 {
+                return Err(Error::InternalError(
+                    "to_binary requires at least 1 output".to_string(),
+                ));
+            }
+        }
     }
     Ok(())
 }
@@ -221,6 +235,7 @@ pub fn impl_builtin_hint<F: Field>(
         BuiltinHintIds::GreaterEq => binop_hint(inputs, |x, y| F::from((x >= y) as u32)),
         BuiltinHintIds::Lesser => binop_hint(inputs, |x, y| F::from((x < y) as u32)),
         BuiltinHintIds::Greater => binop_hint(inputs, |x, y| F::from((x > y) as u32)),
+        BuiltinHintIds::ToBinary => to_binary(inputs[0], num_outputs).unwrap(), // TODO: error propagation
     }
 }
 
@@ -235,6 +250,21 @@ fn binop_hint_on_u256<F: Field, G: Fn(U256, U256) -> U256>(inputs: &[F], f: G) -
     vec![F::from_u256(z_u256)]
 }
 
+pub fn to_binary<F: Field>(x: F, num_outputs: usize) -> Result<Vec<F>, Error> {
+    let mut outputs = Vec::with_capacity(num_outputs);
+    let mut y = x.to_u256();
+    for _ in 0..num_outputs {
+        outputs.push(F::from_u256(y & U256::from(1u32)));
+        y >>= 1;
+    }
+    if y != U256::ZERO {
+        return Err(Error::UserError(
+            "to_binary hint input too large".to_string(),
+        ));
+    }
+    Ok(outputs)
+}
+
 pub fn stub_impl<F: Field>(hint_id: usize, inputs: &Vec<F>, num_outputs: usize) -> Vec<F> {
     match BuiltinHintIds::from_usize(hint_id) {
         Some(hint_id) => impl_builtin_hint(hint_id, inputs, num_outputs),
@@ -274,6 +304,9 @@ pub fn random_builtin(mut rand: impl RngCore) -> (usize, usize, usize) {
                 BuiltinHintIds::Select => {
                     return (hint_id as usize, 3, 1);
                 }
+                BuiltinHintIds::ToBinary => {
+                    return (hint_id as usize, 1, 300);
+                }
             }
         }
     }
diff --git a/expander_compiler/tests/to_binary_builtin.rs b/expander_compiler/tests/to_binary_builtin.rs
new file mode 100644
index 00000000..4b1e3178
--- /dev/null
+++ b/expander_compiler/tests/to_binary_builtin.rs
@@ -0,0 +1,46 @@
+use expander_compiler::frontend::*;
+
+declare_circuit!(Circuit {
+    input: PublicVariable,
+});
+
+impl Define<M31Config> for Circuit<Variable> {
+    fn define<Builder: RootAPI<M31Config>>(&self, builder: &mut Builder) {
+        let bits = builder.to_binary(self.input, 8);
+        let x = builder.from_binary(&bits);
+        builder.assert_is_equal(x, self.input);
+    }
+}
+
+#[test]
+fn test_small() {
+    let compile_result = compile(&Circuit::default(), CompileOptions::default()).unwrap();
+    for i in 0..256 {
+        let assignment = Circuit::<M31> {
+            input: M31::from(i as u32),
+        };
+        let witness = compile_result
+            .witness_solver
+            .solve_witness_with_hints(&assignment, &mut EmptyHintCaller)
+            .unwrap();
+        let output = compile_result.layered_circuit.run(&witness);
+        assert_eq!(output, vec![i < 256]);
+    }
+}
+
+#[test]
+#[should_panic]
+fn test_big() {
+    let compile_result = compile(&Circuit::default(), CompileOptions::default()).unwrap();
+    for i in 256..257 {
+        let assignment = Circuit::<M31> {
+            input: M31::from(i as u32),
+        };
+        let witness = compile_result
+            .witness_solver
+            .solve_witness_with_hints(&assignment, &mut EmptyHintCaller)
+            .unwrap();
+        let output = compile_result.layered_circuit.run(&witness);
+        assert_eq!(output, vec![i < 256]);
+    }
+}
diff --git a/expander_compiler/tests/to_binary_hint.rs b/expander_compiler/tests/to_binary_hint.rs
index b4078411..bf25df04 100644
--- a/expander_compiler/tests/to_binary_hint.rs
+++ b/expander_compiler/tests/to_binary_hint.rs
@@ -8,7 +8,7 @@ declare_circuit!(Circuit {
 });
 
 fn to_binary<C: Config>(api: &mut impl RootAPI<C>, x: Variable, n_bits: usize) -> Vec<Variable> {
-    let bits = api.new_hint("myhint.tobinary", &[x], n_bits);
+    let bits = api.new_hint("your_hint_namespace.sub_namespace.tobinary", &[x], n_bits);
     for bit in bits.iter() {
         api.assert_is_bool(*bit);
     }
@@ -46,7 +46,7 @@ fn to_binary_hint(x: &[M31], y: &mut [M31]) -> Result<(), Error> {
 #[test]
 fn test_300() {
     let mut hint_registry = HintRegistry::<M31>::new();
-    hint_registry.register("myhint.tobinary", to_binary_hint);
+    hint_registry.register("your_hint_namespace.sub_namespace.tobinary", to_binary_hint);
 
     let compile_result = compile(&Circuit::default(), CompileOptions::default()).unwrap();
     for i in 0..300 {
@@ -68,7 +68,7 @@ fn test_300_closure() {
     let call_count = Rc::new(RefCell::new(0));
     let call_count_clone = call_count.clone();
     hint_registry.register(
-        "myhint.tobinary",
+        "your_hint_namespace.sub_namespace.tobinary",
         move |x: &[M31], y: &mut [M31]| -> Result<(), Error> {
             *call_count_clone.borrow_mut() += 1;
             let t = x[0].to_u256();


Reviews:
user: login: DreamWuGit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T09:49:19Z
user: login: DreamWuGit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T09:54:34Z
user: login: DreamWuGit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T10:06:49Z
user: login: DreamWuGit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T10:08:48Z
user: login: DreamWuGit
body: overall LGTE with some comments. Thx!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T10:10:15Z
user: login: siq1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T02:40:17Z
user: login: siq1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T02:41:31Z
user: login: siq1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T02:41:51Z
user: login: siq1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T02:44:05Z
user: login: siq1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T03:06:12Z

Review comments:
diff_hunk: @@ -410,9 +451,11 @@ mod tests {
                             .map(|_| CField::random_unsafe(&mut rand::thread_rng()))
                             .collect();
                         let e1 = root.eval_unsafe_with_errors(inputs.clone());
-                        let e2 = root_processed.eval_unsafe_with_errors(inputs);
                         if e1.is_ok() {
path: expander_compiler/src/builder/hint_normalize.rs
user: login: DreamWuGit
body: if e1 is not ok, should be error case, can omit `else if e1.as_ref().err().unwrap().is_internal()`? . just ```
 else 
{                            panic!("{:?}", e1);
}
```
created_at: 2025-04-13T09:49:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -94,13 +94,13 @@ impl<C: Config> RandomInstruction for Instruction<C> {
                 y: rnd.next_u64() as usize % num_vars + 1,
                 op,
             }
-        } else if prob1 < 0.92 {
+        } else if prob1 < 0.91 {
path: expander_compiler/src/circuit/ir/source/tests.rs
user: login: DreamWuGit
body: the comparison from 0.92 to 0.91 is because of  addition` || num_outputs != 1` in line 54?
created_at: 2025-04-13T09:54:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -235,6 +250,21 @@ fn binop_hint_on_u256<F: Field, G: Fn(U256, U256) -> U256>(inputs: &[F], f: G) -
     vec![F::from_u256(z_u256)]
 }
 
+pub fn to_binary<F: Field>(x: F, num_outputs: usize) -> Result<Vec<F>, Error> {
+    let mut outputs = Vec::with_capacity(num_outputs);
path: expander_compiler/src/hints/builtin.rs
user: login: DreamWuGit
body: add max checking num_outputs <= 32 ?
created_at: 2025-04-13T10:06:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+use expander_compiler::frontend::*;
+
+declare_circuit!(Circuit {
+    input: PublicVariable,
+});
+
+fn from_binary<C: Config>(api: &mut impl RootAPI<C>, bits: Vec<Variable>) -> Variable {
path: expander_compiler/tests/to_binary_builtin.rs
user: login: DreamWuGit
body: `from_binary` seems worthy to make it as built in hints to me. it is reverse operation of 'to_binary_hint"
created_at: 2025-04-13T10:08:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+use expander_compiler::frontend::*;
+
+declare_circuit!(Circuit {
+    input: PublicVariable,
+});
+
+fn from_binary<C: Config>(api: &mut impl RootAPI<C>, bits: Vec<Variable>) -> Variable {
path: expander_compiler/tests/to_binary_builtin.rs
user: login: siq1
body: I think so. I'll add it as a builder function.
created_at: 2025-04-14T02:40:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -235,6 +250,21 @@ fn binop_hint_on_u256<F: Field, G: Fn(U256, U256) -> U256>(inputs: &[F], f: G) -
     vec![F::from_u256(z_u256)]
 }
 
+pub fn to_binary<F: Field>(x: F, num_outputs: usize) -> Result<Vec<F>, Error> {
+    let mut outputs = Vec::with_capacity(num_outputs);
path: expander_compiler/src/hints/builtin.rs
user: login: siq1
body: 1. Field size may >= 32 bits
2. I think to_binary with num_outputs > field_size is sometimes worthy
created_at: 2025-04-14T02:41:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -94,13 +94,13 @@ impl<C: Config> RandomInstruction for Instruction<C> {
                 y: rnd.next_u64() as usize % num_vars + 1,
                 op,
             }
-        } else if prob1 < 0.92 {
+        } else if prob1 < 0.91 {
path: expander_compiler/src/circuit/ir/source/tests.rs
user: login: siq1
body: No, it's because I added a new item below
created_at: 2025-04-14T02:41:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -410,9 +451,11 @@ mod tests {
                             .map(|_| CField::random_unsafe(&mut rand::thread_rng()))
                             .collect();
                         let e1 = root.eval_unsafe_with_errors(inputs.clone());
-                        let e2 = root_processed.eval_unsafe_with_errors(inputs);
                         if e1.is_ok() {
path: expander_compiler/src/builder/hint_normalize.rs
user: login: siq1
body: The generated circuit `e1` may have some illegal computation, like division by constant `0`. It returns a user error instead of internal error.
created_at: 2025-04-14T02:44:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+use expander_compiler::frontend::*;
+
+declare_circuit!(Circuit {
+    input: PublicVariable,
+});
+
+fn from_binary<C: Config>(api: &mut impl RootAPI<C>, bits: Vec<Variable>) -> Variable {
path: expander_compiler/tests/to_binary_builtin.rs
user: login: siq1
body: Implemented in [8bc8612](https://github.com/PolyhedraZK/ExpanderCompilerCollection/pull/139/commits/8bc8612a473dce6a7dd4d1dc38bfb5b8b4ab358b)
created_at: 2025-04-14T03:06:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: siq1
created_at: 2025-04-09T18:54:58Z
body: I will update #103 after this is merged.
author_association: CONTRIBUTOR
user: login: DreamWuGit
created_at: 2025-04-10T02:02:02Z
body: will have a look today ! 
author_association: CONTRIBUTOR