From b438ab18ef6c071e944541dd827950543a0d0dc5 Mon Sep 17 00:00:00 2001 From: "sm.wu" Date: Fri, 7 Nov 2025 09:55:41 +0800 Subject: [PATCH 01/10] expression to arithmetics --- .../src/expression/utils.rs | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/crates/multilinear_extensions/src/expression/utils.rs b/crates/multilinear_extensions/src/expression/utils.rs index fe680be..d01fed3 100644 --- a/crates/multilinear_extensions/src/expression/utils.rs +++ b/crates/multilinear_extensions/src/expression/utils.rs @@ -190,3 +190,156 @@ pub fn expr_convert_to_witins( Expression::Challenge(..) => (), } } + +pub const DagLoadWit: usize = 0; +pub const DagLoadScalar: usize = 1; +pub const DagAdd: usize = 2; +pub const DagMul: usize = 3; + +pub fn expr_compression_to_dag( + expr: &Expression, + challenges_offset: usize, + constant_offset: usize, +) -> ( + Vec, + Vec, + Vec>, + Vec>, +) { + let mut dag = vec![]; + let mut constant = vec![]; + let mut instance_scalar = vec![]; + let mut challenges = vec![]; + expr_compression_to_dag_helper( + &mut dag, + &mut instance_scalar, + challenges_offset, + &mut challenges, + constant_offset, + &mut constant, + expr, + ); + (dag, instance_scalar, challenges, constant) +} + +fn expr_compression_to_dag_helper( + dag: &mut Vec, + instance_scalar: &mut Vec, + challenges_offset: usize, + challenges: &mut Vec>, + constant_offset: usize, + constant: &mut Vec>, + expr: &Expression, +) -> (usize, usize) { + // (max_degree, max_depth) + match expr { + Expression::Fixed(_) => unimplemented!(), + Expression::WitIn(wit_id) => { + dag.extend(vec![DagLoadWit as u32, *wit_id as u32]); + (1, 1) + } + Expression::StructuralWitIn(_, ..) => unimplemented!(), + Expression::Instance(_) => unimplemented!(), + Expression::InstanceScalar(inst) => { + instance_scalar.push(inst.clone()); + dag.extend(vec![DagLoadScalar as u32, instance_scalar.len() as u32 - 1]); + (0, 1) + } + Expression::Constant(value) => { + constant.push(value.clone()); + dag.extend(vec![ + DagLoadScalar as u32, + (constant_offset + constant.len()) as u32 - 1, + ]); + (0, 1) + } + Expression::Sum(a, b) => { + let (max_degree_a, max_depth_a) = expr_compression_to_dag_helper( + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + a, + ); + let (max_degree_b, max_depth_b) = expr_compression_to_dag_helper( + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + b, + ); + dag.extend(vec![DagAdd as u32]); + ( + max_degree_a.max(max_degree_b), + (max_depth_a + 1).max(max_depth_b), + ) // 1 comes from store result of `a` + } + Expression::Product(a, b) => { + let (max_degree_a, max_depth_a) = expr_compression_to_dag_helper( + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + a, + ); + let (max_degree_b, max_depth_b) = expr_compression_to_dag_helper( + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + b, + ); + dag.extend(vec![DagMul as u32]); + ( + max_degree_a + max_degree_b, + (max_depth_a + 1).max(max_depth_b), + ) // 1 comes from store result of `a` + } + Expression::ScaledSum(x, a, b) => { + expr_compression_to_dag_helper( + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + x, + ); + expr_compression_to_dag_helper( + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + a, + ); + dag.extend(vec![DagMul as u32]); + expr_compression_to_dag_helper( + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + b, + ); + dag.extend(vec![DagAdd as u32]); + } + c @ Expression::Challenge(..) => { + challenges.push(c.clone()); + dag.extend(vec![ + DagLoadScalar as u32, + (challenges_offset + challenges.len()) as u32 - 1, + ]) + } + } +} From a0224e53ff6a31a1f52ca65ac0782e0308af7a6e Mon Sep 17 00:00:00 2001 From: "sm.wu" Date: Fri, 7 Nov 2025 16:33:49 +0800 Subject: [PATCH 02/10] return max degree and depth --- .../src/expression/utils.rs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/crates/multilinear_extensions/src/expression/utils.rs b/crates/multilinear_extensions/src/expression/utils.rs index d01fed3..d9dd23d 100644 --- a/crates/multilinear_extensions/src/expression/utils.rs +++ b/crates/multilinear_extensions/src/expression/utils.rs @@ -205,12 +205,13 @@ pub fn expr_compression_to_dag( Vec, Vec>, Vec>, + (usize, usize) ) { let mut dag = vec![]; let mut constant = vec![]; let mut instance_scalar = vec![]; let mut challenges = vec![]; - expr_compression_to_dag_helper( + let (max_degree, max_depth) = expr_compression_to_dag_helper( &mut dag, &mut instance_scalar, challenges_offset, @@ -219,7 +220,7 @@ pub fn expr_compression_to_dag( &mut constant, expr, ); - (dag, instance_scalar, challenges, constant) + (dag, instance_scalar, challenges, constant, (max_degree, max_depth)) } fn expr_compression_to_dag_helper( @@ -275,7 +276,7 @@ fn expr_compression_to_dag_helper( dag.extend(vec![DagAdd as u32]); ( max_degree_a.max(max_degree_b), - (max_depth_a + 1).max(max_depth_b), + max_depth_a.max(max_depth_b + 1), ) // 1 comes from store result of `a` } Expression::Product(a, b) => { @@ -300,11 +301,11 @@ fn expr_compression_to_dag_helper( dag.extend(vec![DagMul as u32]); ( max_degree_a + max_degree_b, - (max_depth_a + 1).max(max_depth_b), + max_depth_a.max(max_depth_b + 1), ) // 1 comes from store result of `a` } Expression::ScaledSum(x, a, b) => { - expr_compression_to_dag_helper( + let (max_degree_x, max_depth_x) = expr_compression_to_dag_helper( dag, instance_scalar, challenges_offset, @@ -313,7 +314,7 @@ fn expr_compression_to_dag_helper( constant, x, ); - expr_compression_to_dag_helper( + let (max_degree_a, max_depth_a) = expr_compression_to_dag_helper( dag, instance_scalar, challenges_offset, @@ -322,8 +323,10 @@ fn expr_compression_to_dag_helper( constant, a, ); + let xa_degree = max_degree_x + max_degree_a; + let ax_max_depth = max_depth_x.max(max_depth_a + 1); dag.extend(vec![DagMul as u32]); - expr_compression_to_dag_helper( + let (max_degree_b, max_depth_b) = expr_compression_to_dag_helper( dag, instance_scalar, challenges_offset, @@ -333,13 +336,18 @@ fn expr_compression_to_dag_helper( b, ); dag.extend(vec![DagAdd as u32]); + ( + xa_degree.max(max_degree_b), + (ax_max_depth).max(max_depth_b + 1), + ) // 1 comes from store result of `ax` } c @ Expression::Challenge(..) => { challenges.push(c.clone()); dag.extend(vec![ DagLoadScalar as u32, (challenges_offset + challenges.len()) as u32 - 1, - ]) + ]); + (0, 1) } } } From 88d3f40bf81ef5799bfb7289e40497ece439795e Mon Sep 17 00:00:00 2001 From: "sm.wu" Date: Fri, 7 Nov 2025 19:54:50 +0800 Subject: [PATCH 03/10] simplify api --- .../src/expression/utils.rs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/multilinear_extensions/src/expression/utils.rs b/crates/multilinear_extensions/src/expression/utils.rs index d9dd23d..f205d3f 100644 --- a/crates/multilinear_extensions/src/expression/utils.rs +++ b/crates/multilinear_extensions/src/expression/utils.rs @@ -198,8 +198,6 @@ pub const DagMul: usize = 3; pub fn expr_compression_to_dag( expr: &Expression, - challenges_offset: usize, - constant_offset: usize, ) -> ( Vec, Vec, @@ -211,10 +209,28 @@ pub fn expr_compression_to_dag( let mut constant = vec![]; let mut instance_scalar = vec![]; let mut challenges = vec![]; + // traverse first time to collect offset + let _ = expr_compression_to_dag_helper( + &mut dag, + &mut instance_scalar, + 0, + &mut challenges, + 0, + &mut constant, + expr, + ); + + let challenge_offset = instance_scalar.len(); + let constant_offset = instance_scalar.len() + challenges.len(); + + dag.truncate(0); + constant.truncate(0); + instance_scalar.truncate(0); + challenges.truncate(0); let (max_degree, max_depth) = expr_compression_to_dag_helper( &mut dag, &mut instance_scalar, - challenges_offset, + challenge_offset, &mut challenges, constant_offset, &mut constant, From 5bd62cc74e17b4a72409ace27a666746b32d0b2a Mon Sep 17 00:00:00 2001 From: "sm.wu" Date: Fri, 7 Nov 2025 20:08:17 +0800 Subject: [PATCH 04/10] dedup constant & challenge --- .../src/expression/utils.rs | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/crates/multilinear_extensions/src/expression/utils.rs b/crates/multilinear_extensions/src/expression/utils.rs index f205d3f..5b8f3cd 100644 --- a/crates/multilinear_extensions/src/expression/utils.rs +++ b/crates/multilinear_extensions/src/expression/utils.rs @@ -1,3 +1,5 @@ +use std::collections::hash_map::Entry; +use std::collections::HashMap; use super::{Expression, StructuralWitIn, WitIn}; use crate::{Fixed, Instance, WitnessId, combine_cumulative_either, monomial::Term}; use either::Either; @@ -205,6 +207,8 @@ pub fn expr_compression_to_dag( Vec>, (usize, usize) ) { + let mut constant_dedup = HashMap::new(); + let mut challenges_dedup = HashMap::new(); let mut dag = vec![]; let mut constant = vec![]; let mut instance_scalar = vec![]; @@ -217,6 +221,8 @@ pub fn expr_compression_to_dag( &mut challenges, 0, &mut constant, + &mut challenges_dedup, + &mut constant_dedup, expr, ); @@ -227,6 +233,8 @@ pub fn expr_compression_to_dag( constant.truncate(0); instance_scalar.truncate(0); challenges.truncate(0); + challenges_dedup.clear(); + constant_dedup.clear(); let (max_degree, max_depth) = expr_compression_to_dag_helper( &mut dag, &mut instance_scalar, @@ -234,6 +242,8 @@ pub fn expr_compression_to_dag( &mut challenges, constant_offset, &mut constant, + &mut challenges_dedup, + &mut constant_dedup, expr, ); (dag, instance_scalar, challenges, constant, (max_degree, max_depth)) @@ -246,6 +256,8 @@ fn expr_compression_to_dag_helper( challenges: &mut Vec>, constant_offset: usize, constant: &mut Vec>, + challenges_dedup: &mut HashMap, u32>, + constant_dedup: &mut HashMap, u32>, expr: &Expression, ) -> (usize, usize) { // (max_degree, max_depth) @@ -263,11 +275,17 @@ fn expr_compression_to_dag_helper( (0, 1) } Expression::Constant(value) => { - constant.push(value.clone()); - dag.extend(vec![ - DagLoadScalar as u32, - (constant_offset + constant.len()) as u32 - 1, - ]); + let constant_id = match constant_dedup.entry(value.clone()) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + constant.push(value.clone()); + let id = (constant_offset + constant.len()) as u32 - 1; + entry.insert(id); + id + } + }; + + dag.extend([DagLoadScalar as u32, constant_id]); (0, 1) } Expression::Sum(a, b) => { @@ -278,6 +296,8 @@ fn expr_compression_to_dag_helper( challenges, constant_offset, constant, + challenges_dedup, + constant_dedup, a, ); let (max_degree_b, max_depth_b) = expr_compression_to_dag_helper( @@ -287,6 +307,8 @@ fn expr_compression_to_dag_helper( challenges, constant_offset, constant, + challenges_dedup, + constant_dedup, b, ); dag.extend(vec![DagAdd as u32]); @@ -303,6 +325,8 @@ fn expr_compression_to_dag_helper( challenges, constant_offset, constant, + challenges_dedup, + constant_dedup, a, ); let (max_degree_b, max_depth_b) = expr_compression_to_dag_helper( @@ -312,6 +336,8 @@ fn expr_compression_to_dag_helper( challenges, constant_offset, constant, + challenges_dedup, + constant_dedup, b, ); dag.extend(vec![DagMul as u32]); @@ -328,6 +354,8 @@ fn expr_compression_to_dag_helper( challenges, constant_offset, constant, + challenges_dedup, + constant_dedup, x, ); let (max_degree_a, max_depth_a) = expr_compression_to_dag_helper( @@ -337,6 +365,8 @@ fn expr_compression_to_dag_helper( challenges, constant_offset, constant, + challenges_dedup, + constant_dedup, a, ); let xa_degree = max_degree_x + max_degree_a; @@ -349,6 +379,8 @@ fn expr_compression_to_dag_helper( challenges, constant_offset, constant, + challenges_dedup, + constant_dedup, b, ); dag.extend(vec![DagAdd as u32]); @@ -358,11 +390,17 @@ fn expr_compression_to_dag_helper( ) // 1 comes from store result of `ax` } c @ Expression::Challenge(..) => { - challenges.push(c.clone()); - dag.extend(vec![ - DagLoadScalar as u32, - (challenges_offset + challenges.len()) as u32 - 1, - ]); + let challenge_id = match challenges_dedup.entry(c.clone()) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + challenges.push(c.clone()); + let id = (challenges_offset + challenges.len()) as u32 - 1; + entry.insert(id); + id + } + }; + + dag.extend([DagLoadScalar as u32, challenge_id]); (0, 1) } } From 444176cf4b55f652451fa72af52f4385f01c0981 Mon Sep 17 00:00:00 2001 From: "sm.wu" Date: Sun, 9 Nov 2025 23:20:26 +0800 Subject: [PATCH 05/10] Dag in node structure --- .../src/expression/utils.rs | 95 ++++++++++++++++--- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/crates/multilinear_extensions/src/expression/utils.rs b/crates/multilinear_extensions/src/expression/utils.rs index 5b8f3cd..034ad78 100644 --- a/crates/multilinear_extensions/src/expression/utils.rs +++ b/crates/multilinear_extensions/src/expression/utils.rs @@ -5,6 +5,7 @@ use crate::{Fixed, Instance, WitnessId, combine_cumulative_either, monomial::Ter use either::Either; use ff_ext::ExtensionField; use itertools::Itertools; +use serde::{Deserialize, Serialize}; impl WitIn { pub fn assign(&self, instance: &mut [E::BaseField], value: E::BaseField) { @@ -198,13 +199,23 @@ pub const DagLoadScalar: usize = 1; pub const DagAdd: usize = 2; pub const DagMul: usize = 3; +#[derive(Clone, Debug, Serialize, Deserialize)] +#[repr(C)] +pub struct Node { + pub op: u32, + pub left_id: u32, + pub right_id: u32, + pub out: u32, +} + pub fn expr_compression_to_dag( expr: &Expression, ) -> ( - Vec, + Vec, Vec, Vec>, Vec>, + u32, (usize, usize) ) { let mut constant_dedup = HashMap::new(); @@ -213,6 +224,7 @@ pub fn expr_compression_to_dag( let mut constant = vec![]; let mut instance_scalar = vec![]; let mut challenges = vec![]; + let mut stack_pos: u32 = 0; // traverse first time to collect offset let _ = expr_compression_to_dag_helper( &mut dag, @@ -223,6 +235,7 @@ pub fn expr_compression_to_dag( &mut constant, &mut challenges_dedup, &mut constant_dedup, + &mut stack_pos, expr, ); @@ -235,6 +248,7 @@ pub fn expr_compression_to_dag( challenges.truncate(0); challenges_dedup.clear(); constant_dedup.clear(); + stack_pos = 0; let (max_degree, max_depth) = expr_compression_to_dag_helper( &mut dag, &mut instance_scalar, @@ -244,13 +258,14 @@ pub fn expr_compression_to_dag( &mut constant, &mut challenges_dedup, &mut constant_dedup, + &mut stack_pos, expr, ); - (dag, instance_scalar, challenges, constant, (max_degree, max_depth)) + (dag, instance_scalar, challenges, constant, stack_pos, (max_degree, max_depth)) } fn expr_compression_to_dag_helper( - dag: &mut Vec, + dag: &mut Vec, instance_scalar: &mut Vec, challenges_offset: usize, challenges: &mut Vec>, @@ -258,20 +273,33 @@ fn expr_compression_to_dag_helper( constant: &mut Vec>, challenges_dedup: &mut HashMap, u32>, constant_dedup: &mut HashMap, u32>, + stack_pos: &mut u32, expr: &Expression, ) -> (usize, usize) { // (max_degree, max_depth) match expr { Expression::Fixed(_) => unimplemented!(), Expression::WitIn(wit_id) => { - dag.extend(vec![DagLoadWit as u32, *wit_id as u32]); + dag.push(Node { + op: DagLoadWit as u32, + left_id: *wit_id as u32, + right_id: 0, + out: *stack_pos, + }); + *stack_pos += 1; (1, 1) } Expression::StructuralWitIn(_, ..) => unimplemented!(), Expression::Instance(_) => unimplemented!(), Expression::InstanceScalar(inst) => { instance_scalar.push(inst.clone()); - dag.extend(vec![DagLoadScalar as u32, instance_scalar.len() as u32 - 1]); + dag.push(Node { + op: DagLoadScalar as u32, + left_id: instance_scalar.len() as u32 - 1, + right_id: 0, + out: *stack_pos, + }); + *stack_pos += 1; (0, 1) } Expression::Constant(value) => { @@ -284,8 +312,13 @@ fn expr_compression_to_dag_helper( id } }; - - dag.extend([DagLoadScalar as u32, constant_id]); + dag.push(Node { + op: DagLoadScalar as u32, + left_id: constant_id, + right_id: 0, + out: *stack_pos, + }); + *stack_pos += 1; (0, 1) } Expression::Sum(a, b) => { @@ -298,6 +331,7 @@ fn expr_compression_to_dag_helper( constant, challenges_dedup, constant_dedup, + stack_pos, a, ); let (max_degree_b, max_depth_b) = expr_compression_to_dag_helper( @@ -309,9 +343,16 @@ fn expr_compression_to_dag_helper( constant, challenges_dedup, constant_dedup, + stack_pos, b, ); - dag.extend(vec![DagAdd as u32]); + dag.push(Node { + op: DagAdd as u32, + left_id: *stack_pos-2, + right_id: *stack_pos-1, + out: *stack_pos-2, + }); + *stack_pos -= 1; ( max_degree_a.max(max_degree_b), max_depth_a.max(max_depth_b + 1), @@ -327,6 +368,7 @@ fn expr_compression_to_dag_helper( constant, challenges_dedup, constant_dedup, + stack_pos, a, ); let (max_degree_b, max_depth_b) = expr_compression_to_dag_helper( @@ -338,9 +380,16 @@ fn expr_compression_to_dag_helper( constant, challenges_dedup, constant_dedup, + stack_pos, b, ); - dag.extend(vec![DagMul as u32]); + dag.push(Node { + op: DagMul as u32, + left_id: *stack_pos-2, + right_id: *stack_pos-1, + out: *stack_pos-2, + }); + *stack_pos -= 1; ( max_degree_a + max_degree_b, max_depth_a.max(max_depth_b + 1), @@ -356,6 +405,7 @@ fn expr_compression_to_dag_helper( constant, challenges_dedup, constant_dedup, + stack_pos, x, ); let (max_degree_a, max_depth_a) = expr_compression_to_dag_helper( @@ -367,11 +417,18 @@ fn expr_compression_to_dag_helper( constant, challenges_dedup, constant_dedup, + stack_pos, a, ); let xa_degree = max_degree_x + max_degree_a; let ax_max_depth = max_depth_x.max(max_depth_a + 1); - dag.extend(vec![DagMul as u32]); + dag.push(Node { + op: DagMul as u32, + left_id: *stack_pos-2, + right_id: *stack_pos-1, + out: *stack_pos-2, + }); + *stack_pos -= 1; let (max_degree_b, max_depth_b) = expr_compression_to_dag_helper( dag, instance_scalar, @@ -381,9 +438,16 @@ fn expr_compression_to_dag_helper( constant, challenges_dedup, constant_dedup, + stack_pos, b, ); - dag.extend(vec![DagAdd as u32]); + dag.push(Node { + op: DagAdd as u32, + left_id: *stack_pos-2, + right_id: *stack_pos-1, + out: *stack_pos-2, + }); + *stack_pos -= 1; ( xa_degree.max(max_degree_b), (ax_max_depth).max(max_depth_b + 1), @@ -399,8 +463,13 @@ fn expr_compression_to_dag_helper( id } }; - - dag.extend([DagLoadScalar as u32, challenge_id]); + dag.push(Node { + op: DagLoadScalar as u32, + left_id: challenge_id, + right_id: 0, + out: *stack_pos, + }); + *stack_pos += 1; (0, 1) } } From 24b4570e99d126ccbfd96059459c5aba9cf5f94f Mon Sep 17 00:00:00 2001 From: "sm.wu" Date: Mon, 10 Nov 2025 21:17:43 +0800 Subject: [PATCH 06/10] with zero/one optimisation --- .../src/expression/utils.rs | 497 +++++++++++++----- 1 file changed, 367 insertions(+), 130 deletions(-) diff --git a/crates/multilinear_extensions/src/expression/utils.rs b/crates/multilinear_extensions/src/expression/utils.rs index 034ad78..0dc8797 100644 --- a/crates/multilinear_extensions/src/expression/utils.rs +++ b/crates/multilinear_extensions/src/expression/utils.rs @@ -1,11 +1,11 @@ -use std::collections::hash_map::Entry; -use std::collections::HashMap; use super::{Expression, StructuralWitIn, WitIn}; use crate::{Fixed, Instance, WitnessId, combine_cumulative_either, monomial::Term}; use either::Either; use ff_ext::ExtensionField; use itertools::Itertools; +use p3::field::Field; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; impl WitIn { pub fn assign(&self, instance: &mut [E::BaseField], value: E::BaseField) { @@ -208,6 +208,16 @@ pub struct Node { pub out: u32, } +fn is_one(e: &Expression) -> bool { + matches!(e, Expression::Constant(v) + if v.map_either(|b| b.is_one(), |x| x.is_one()).into_inner()) +} + +fn is_zero(e: &Expression) -> bool { + matches!(e, Expression::Constant(v) + if v.map_either(|b| b.is_zero(), |x| x.is_zero()).into_inner()) +} + pub fn expr_compression_to_dag( expr: &Expression, ) -> ( @@ -216,7 +226,7 @@ pub fn expr_compression_to_dag( Vec>, Vec>, u32, - (usize, usize) + (usize, usize), ) { let mut constant_dedup = HashMap::new(); let mut challenges_dedup = HashMap::new(); @@ -249,7 +259,7 @@ pub fn expr_compression_to_dag( challenges_dedup.clear(); constant_dedup.clear(); stack_pos = 0; - let (max_degree, max_depth) = expr_compression_to_dag_helper( + let Some((max_degree, max_depth)) = expr_compression_to_dag_helper( &mut dag, &mut instance_scalar, challenge_offset, @@ -260,8 +270,17 @@ pub fn expr_compression_to_dag( &mut constant_dedup, &mut stack_pos, expr, - ); - (dag, instance_scalar, challenges, constant, stack_pos, (max_degree, max_depth)) + ) else { + panic!("zero expression expr {expr}") + }; + ( + dag, + instance_scalar, + challenges, + constant, + stack_pos, + (max_degree, max_depth), + ) } fn expr_compression_to_dag_helper( @@ -275,7 +294,7 @@ fn expr_compression_to_dag_helper( constant_dedup: &mut HashMap, u32>, stack_pos: &mut u32, expr: &Expression, -) -> (usize, usize) { +) -> Option<(usize, usize)> { // (max_degree, max_depth) match expr { Expression::Fixed(_) => unimplemented!(), @@ -287,7 +306,7 @@ fn expr_compression_to_dag_helper( out: *stack_pos, }); *stack_pos += 1; - (1, 1) + Some((1, 1)) } Expression::StructuralWitIn(_, ..) => unimplemented!(), Expression::Instance(_) => unimplemented!(), @@ -300,18 +319,22 @@ fn expr_compression_to_dag_helper( out: *stack_pos, }); *stack_pos += 1; - (0, 1) + Some((0, 1)) } Expression::Constant(value) => { - let constant_id = match constant_dedup.entry(value.clone()) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - constant.push(value.clone()); - let id = (constant_offset + constant.len()) as u32 - 1; - entry.insert(id); - id - } - }; + // if zero, skip entirely + let is_zero = value + .map_either(|b| b.is_zero(), |e| e.is_zero()) + .into_inner(); + if is_zero { + return None; + } + + let constant_id = *constant_dedup.entry(value.clone()).or_insert_with(|| { + constant.push(value.clone()); + (constant_offset + constant.len() - 1) as u32 + }); + dag.push(Node { op: DagLoadScalar as u32, left_id: constant_id, @@ -319,10 +342,10 @@ fn expr_compression_to_dag_helper( out: *stack_pos, }); *stack_pos += 1; - (0, 1) + Some((0, 1)) } Expression::Sum(a, b) => { - let (max_degree_a, max_depth_a) = expr_compression_to_dag_helper( + let lhs = expr_compression_to_dag_helper( dag, instance_scalar, challenges_offset, @@ -334,7 +357,7 @@ fn expr_compression_to_dag_helper( stack_pos, a, ); - let (max_degree_b, max_depth_b) = expr_compression_to_dag_helper( + let rhs = expr_compression_to_dag_helper( dag, instance_scalar, challenges_offset, @@ -346,123 +369,228 @@ fn expr_compression_to_dag_helper( stack_pos, b, ); - dag.push(Node { - op: DagAdd as u32, - left_id: *stack_pos-2, - right_id: *stack_pos-1, - out: *stack_pos-2, - }); - *stack_pos -= 1; - ( - max_degree_a.max(max_degree_b), - max_depth_a.max(max_depth_b + 1), - ) // 1 comes from store result of `a` + + match (lhs, rhs) { + (None, None) => None, // 0 + 0 = 0 + (Some(x), None) | (None, Some(x)) => Some(x), // a + 0 = a + (Some((da, dep_a)), Some((db, dep_b))) => { + dag.push(Node { + op: DagAdd as u32, + left_id: *stack_pos - 2, + right_id: *stack_pos - 1, + out: *stack_pos - 2, + }); + *stack_pos -= 1; + Some((da.max(db), dep_a.max(dep_b + 1))) // 1 comes from store result of `a` + } + } } Expression::Product(a, b) => { - let (max_degree_a, max_depth_a) = expr_compression_to_dag_helper( - dag, - instance_scalar, - challenges_offset, - challenges, - constant_offset, - constant, - challenges_dedup, - constant_dedup, - stack_pos, - a, - ); - let (max_degree_b, max_depth_b) = expr_compression_to_dag_helper( - dag, - instance_scalar, - challenges_offset, - challenges, - constant_offset, - constant, - challenges_dedup, - constant_dedup, - stack_pos, - b, - ); - dag.push(Node { - op: DagMul as u32, - left_id: *stack_pos-2, - right_id: *stack_pos-1, - out: *stack_pos-2, - }); - *stack_pos -= 1; - ( - max_degree_a + max_degree_b, - max_depth_a.max(max_depth_b + 1), - ) // 1 comes from store result of `a` + // ---- identity short-circuits BEFORE recursion (so we don't push junk) ---- + if is_zero(a) || is_zero(b) { + // nothing pushed, caller treats None as 0 + None + } else if is_one(a) { + // 1 * b = b (evaluate only b; it will land at the current top) + expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, b, + ) + } else if is_one(b) { + // a * 1 = a (evaluate only a) + expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, a, + ) + } else { + // ---- general case: evaluate a, then b, emit MUL into (top-2), pop 1 ---- + let lhs = expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, a, + ); + let rhs = expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, b, + ); + + match (lhs, rhs) { + (None, _) | (_, None) => None, // defensive (shouldn’t reach due to early zero) + (Some((da, dep_a)), Some((db, dep_b))) => { + dag.push(Node { + op: DagMul as u32, + left_id: *stack_pos - 2, + right_id: *stack_pos - 1, + out: *stack_pos - 2, // overwrite lhs slot + }); + *stack_pos -= 1; // consume rhs + Some((da + db, dep_a.max(dep_b + 1))) + } + } + } } Expression::ScaledSum(x, a, b) => { - let (max_degree_x, max_depth_x) = expr_compression_to_dag_helper( - dag, - instance_scalar, - challenges_offset, - challenges, - constant_offset, - constant, - challenges_dedup, - constant_dedup, - stack_pos, - x, + // algebraic simplifications BEFORE recursion: + // x*a + b => + // if x==0 or a==0 -> 0 + b = b + // if x==1 -> a + b + // if a==1 -> x + b + // if b==0 -> x*a + // + // We’ll implement these in order, so we only evaluate what's needed. + + if is_zero(x) || is_zero(a) { + // becomes b + return expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, b, + ); + } + + if is_one(x) { + // 1*a + b = a + b + let lhs_a = expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, a, + ); + let rhs_b = expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, b, + ); + + return match (lhs_a, rhs_b) { + (None, None) => None, + (Some(x), None) | (None, Some(x)) => Some(x), + (Some((da, dep_a)), Some((db, dep_b))) => { + dag.push(Node { + op: DagAdd as u32, + left_id: *stack_pos - 2, + right_id: *stack_pos - 1, + out: *stack_pos - 2, + }); + *stack_pos -= 1; + Some((da.max(db), dep_a.max(dep_b + 1))) + } + }; + } + + if is_one(a) { + // x*1 + b = x + b + let lhs_x = expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, x, + ); + let rhs_b = expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, b, + ); + + return match (lhs_x, rhs_b) { + (None, None) => None, + (Some(x), None) | (None, Some(x)) => Some(x), + (Some((dx, dep_x)), Some((db, dep_b))) => { + dag.push(Node { + op: DagAdd as u32, + left_id: *stack_pos - 2, + right_id: *stack_pos - 1, + out: *stack_pos - 2, + }); + *stack_pos -= 1; + Some((dx.max(db), dep_x.max(dep_b + 1))) + } + }; + } + + if is_zero(b) { + // general product without identities since x!=0, a!=0 here + // x*a + 0 = x*a + let lhs_x = expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, x, + ); + let lhs_a = expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, a, + ); + + return match (lhs_x, lhs_a) { + (None, _) | (_, None) => None, // defensive + (Some((dx, dep_x)), Some((da, dep_a))) => { + dag.push(Node { + op: DagMul as u32, + left_id: *stack_pos - 2, + right_id: *stack_pos - 1, + out: *stack_pos - 2, + }); + *stack_pos -= 1; + Some((dx + da, dep_x.max(dep_a + 1))) + } + }; + } + + // General case: compute (x*a) then + b + let lhs_x = expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, x, ); - let (max_degree_a, max_depth_a) = expr_compression_to_dag_helper( - dag, - instance_scalar, - challenges_offset, - challenges, - constant_offset, - constant, - challenges_dedup, - constant_dedup, - stack_pos, - a, + let lhs_a = expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, a, ); - let xa_degree = max_degree_x + max_degree_a; - let ax_max_depth = max_depth_x.max(max_depth_a + 1); - dag.push(Node { - op: DagMul as u32, - left_id: *stack_pos-2, - right_id: *stack_pos-1, - out: *stack_pos-2, - }); - *stack_pos -= 1; - let (max_degree_b, max_depth_b) = expr_compression_to_dag_helper( - dag, - instance_scalar, - challenges_offset, - challenges, - constant_offset, - constant, - challenges_dedup, - constant_dedup, - stack_pos, - b, + + let mul = match (lhs_x, lhs_a) { + (None, _) | (_, None) => None, // x or a simplified to 0 above; defensive + (Some((dx, dep_x)), Some((da, dep_a))) => { + dag.push(Node { + op: DagMul as u32, + left_id: *stack_pos - 2, + right_id: *stack_pos - 1, + out: *stack_pos - 2, + }); + *stack_pos -= 1; + Some((dx + da, dep_x.max(dep_a + 1))) + } + }; + + let rhs_b = expr_compression_to_dag_helper( + dag, instance_scalar, challenges_offset, challenges, + constant_offset, constant, challenges_dedup, constant_dedup, + stack_pos, b, ); - dag.push(Node { - op: DagAdd as u32, - left_id: *stack_pos-2, - right_id: *stack_pos-1, - out: *stack_pos-2, - }); - *stack_pos -= 1; - ( - xa_degree.max(max_degree_b), - (ax_max_depth).max(max_depth_b + 1), - ) // 1 comes from store result of `ax` + + match (mul, rhs_b) { + (None, None) => None, + (Some(xa), None) | (None, Some(xa)) => Some(xa), + (Some((dm, dep_m)), Some((db, dep_b))) => { + dag.push(Node { + op: DagAdd as u32, + left_id: *stack_pos - 2, + right_id: *stack_pos - 1, + out: *stack_pos - 2, + }); + *stack_pos -= 1; + Some((dm.max(db), dep_m.max(dep_b + 1))) + } + } } c @ Expression::Challenge(..) => { - let challenge_id = match challenges_dedup.entry(c.clone()) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - challenges.push(c.clone()); - let id = (challenges_offset + challenges.len()) as u32 - 1; - entry.insert(id); - id - } - }; + let challenge_id = *challenges_dedup.entry(c.clone()).or_insert_with(|| { + challenges.push(c.clone()); + (challenges_offset + challenges.len() - 1) as u32 + }); + dag.push(Node { op: DagLoadScalar as u32, left_id: challenge_id, @@ -470,7 +598,116 @@ fn expr_compression_to_dag_helper( out: *stack_pos, }); *stack_pos += 1; - (0, 1) + Some((0, 1)) } } } + + +#[cfg(test)] +mod tests { + use either::Either; + use itertools::Itertools; + use ff_ext::{BabyBearExt4, ExtensionField}; + use p3::babybear::BabyBear; + use p3::field::FieldAlgebra; + use crate::{power_sequence, Expression, Instance, ToExpr}; + use crate::utils::{expr_compression_to_dag, Node}; + + type E = BabyBearExt4; + type B = BabyBear; + + fn extract_num_add_mul(expr: &Expression) -> (Vec, Vec, Vec>, Vec::BaseField, E>>, u32, (i32, i32), (usize, usize)) { + let ( + dag, + instance_scalar_expr, + challenges_expr, + constant_expr, + stack_top, + (max_degree, max_dag_depth), + ) = expr_compression_to_dag(&expr); + + let mut num_add = 0; + let mut num_mul = 0; + + for node in &dag { + match node.op { + 0 => (), // skip wit index + 1 => (), // skip scalar index + 2 => { + num_add += 1; + } + 3 => { + num_mul += 1; + } + op => panic!("unknown op {op}"), + } + } + + ( + dag, + instance_scalar_expr, + challenges_expr, + constant_expr, + stack_top, + (num_add, num_mul), + (max_degree, max_dag_depth), + ) + } + #[test] + fn test_normal_expr_compression_to_dag_helper() { + let a = Expression::::WitIn(0); + let b = Expression::::WitIn(1); + let s2 = Expression::::Constant(Either::Left(B::from_canonical_u32(2))); + let s3 = Expression::::Constant(Either::Left(B::from_canonical_u32(3))); + let s4 = Expression::::Constant(Either::Left(B::from_canonical_u32(4))); + + let e: Expression = s3.expr() * (s2.expr() * a.expr() * b.expr() + s4.expr()); + let ( + dag, + instance_scalar_expr, + challenges_expr, + constant_expr, + stack_top, + (num_add, num_mul), + (max_degree, max_dag_depth), + ) = extract_num_add_mul(&e); + + assert_eq!(constant_expr.len(), 3); + assert!(challenges_expr.is_empty()); + assert_eq!(num_add, 1); + assert_eq!(num_mul, 3); + assert_eq!(max_degree, 2); + + } + + #[test] + fn test_challenge_expr_compression_to_dag_helper() { + let a = Expression::::WitIn(0); + let b = Expression::::WitIn(1); + let c = Expression::::Challenge(2, 1, E::ONE, E::ZERO); + let alpha = Expression::::Challenge(0, 1, E::ONE, E::ZERO); + let pow_c = power_sequence(c); + + // alpha * (1 * a + c * b) + // will be optimized as alpha * (a + c * b) + let e: Expression = vec![a.expr(), b.expr()].into_iter().zip(pow_c).map(|(e1, e2)| e1.expr()*e2.expr()).sum::>(); + let e = e * alpha; + let ( + dag, + instance_scalar_expr, + challenges_expr, + constant_expr, + stack_top, + (num_add, num_mul), + (max_degree, max_dag_depth), + ) = extract_num_add_mul(&e); + + assert_eq!(constant_expr.len(), 0); // 1 was absorbed + assert_eq!(challenges_expr.len(), 2); + assert_eq!(num_add, 1); + assert_eq!(num_mul, 2); + assert_eq!(max_degree, 1); + + } +} \ No newline at end of file From d94dd4dab43894e2a364b2d92b5a4c3f93a1861a Mon Sep 17 00:00:00 2001 From: "sm.wu" Date: Tue, 11 Nov 2025 14:38:00 +0800 Subject: [PATCH 07/10] build dag from monomial terms --- .../src/expression/utils.rs | 195 +++++++++++++++++- 1 file changed, 192 insertions(+), 3 deletions(-) diff --git a/crates/multilinear_extensions/src/expression/utils.rs b/crates/multilinear_extensions/src/expression/utils.rs index 0dc8797..733372a 100644 --- a/crates/multilinear_extensions/src/expression/utils.rs +++ b/crates/multilinear_extensions/src/expression/utils.rs @@ -5,7 +5,7 @@ use ff_ext::ExtensionField; use itertools::Itertools; use p3::field::Field; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; impl WitIn { pub fn assign(&self, instance: &mut [E::BaseField], value: E::BaseField) { @@ -585,7 +585,10 @@ fn expr_compression_to_dag_helper( } } } - c @ Expression::Challenge(..) => { + c @ Expression::Challenge(challenge_id, _power, scalar, offset) => { + if *scalar == E::ZERO && *offset == E::ZERO { + return None + } let challenge_id = *challenges_dedup.entry(c.clone()).or_insert_with(|| { challenges.push(c.clone()); (challenges_offset + challenges.len() - 1) as u32 @@ -603,16 +606,168 @@ fn expr_compression_to_dag_helper( } } +// trie +#[derive(Default)] +struct TrieNode { + children: BTreeMap, // Sorted keys: commutative grouping + scalar_indices: Vec, +} +pub fn build_factored_dag_commutative( + terms: &[Term, Expression>], +) -> (Vec, Vec>, Option, u32) { + let mut root = TrieNode::default(); + let mut scalars: Vec> = Vec::new(); + + // ---- Step 1: canonicalize products (commutative) ---- + for term in terms { + let mut ids: Vec = term + .product + .iter() + .filter_map(|e| match e { + Expression::WitIn(id) => Some(*id), + e => unimplemented!("unknown expression {e}"), + }) + .collect(); + ids.sort(); // ensure a*b == b*a + // we assume witiness being shared will be made with larger id + // so we build the prefix tree with larger id go first + ids.reverse(); + + let mut cur = &mut root; + for wid in ids { + cur = cur.children.entry(wid).or_default(); + } + + let idx = scalars.len(); + scalars.push(term.scalar.clone()); + cur.scalar_indices.push(idx); + } + + // ---- Step 2: emit DAG (stack semantics) ---- + let mut dag = Vec::new(); + let mut stack_top: u32 = 0; + let mut max_stack_depth: u32 = 0; + + fn push(stack_top: &mut u32, max_depth: &mut u32) -> u32 { + let out = *stack_top; + *stack_top += 1; + *max_depth = (*max_depth).max(*stack_top); + out + } + + fn pop2_push1(stack_top: &mut u32) -> (u32, u32, u32) { + let left = *stack_top - 2; + let right = *stack_top - 1; + let out = left; + *stack_top -= 1; + (left, right, out) + } + + fn emit( + node: &TrieNode, + dag: &mut Vec, + stack_top: &mut u32, + max_depth: &mut u32, + ) -> Option { + let mut acc_child: Option = None; + + // Recurse into children (witness factors) + for (&wid, child) in &node.children { + let child_out = emit::(child, dag, stack_top, max_depth); + + // LOAD_WIT: push + let out = push(stack_top, max_depth); + dag.push(Node { + op: DagLoadWit as u32, + left_id: wid as u32, + right_id: 0, + out, + }); + + // If child exists, multiply with it + if let Some(rhs) = child_out { + let (left, right, out) = pop2_push1(stack_top); + dag.push(Node { + op: DagMul as u32, + left_id: left, + right_id: right, + out, + }); + acc_child = Some(match acc_child { + None => out, + Some(_) => { + let (l, r, out) = pop2_push1(stack_top); + dag.push(Node { + op: DagAdd as u32, + left_id: l, + right_id: r, + out, + }); + out + } + }); + } else { + acc_child = Some(out); + } + } + + // Handle scalar accumulation at leaf + let mut acc_scalar: Option = None; + for &idx in &node.scalar_indices { + let out = push(stack_top, max_depth); + dag.push(Node { + op: DagLoadScalar as u32, + left_id: idx as u32, + right_id: 0, + out, + }); + acc_scalar = Some(match acc_scalar { + None => out, + Some(_) => { + let (l, r, out) = pop2_push1(stack_top); + dag.push(Node { + op: DagAdd as u32, + left_id: l, + right_id: r, + out, + }); + out + } + }); + } + + // Merge both child and scalar accumulations + match (acc_scalar, acc_child) { + (Some(_), Some(_)) => { + let (l, r, out) = pop2_push1(stack_top); + dag.push(Node { + op: DagAdd as u32, + left_id: l, + right_id: r, + out, + }); + Some(out) + } + (Some(s), None) => Some(s), + (None, Some(c)) => Some(c), + (None, None) => None, + } + } + + let final_out = emit::(&root, &mut dag, &mut stack_top, &mut max_stack_depth); + (dag, scalars, final_out, max_stack_depth) +} #[cfg(test)] mod tests { + use std::ops::Neg; use either::Either; use itertools::Itertools; use ff_ext::{BabyBearExt4, ExtensionField}; use p3::babybear::BabyBear; use p3::field::FieldAlgebra; use crate::{power_sequence, Expression, Instance, ToExpr}; - use crate::utils::{expr_compression_to_dag, Node}; + use crate::utils::{build_factored_dag_commutative, expr_compression_to_dag, Node}; type E = BabyBearExt4; type B = BabyBear; @@ -710,4 +865,38 @@ mod tests { assert_eq!(max_degree, 1); } + + #[test] + fn test_build_factored_dag_commutative() { + // w1 * (c2 * (2 + w0*c1 -1)) + let w0 = Expression::::WitIn(0); + let w1 = Expression::::WitIn(1); + let c1 = Expression::::Challenge(0, 1, E::ONE, E::ZERO); + let c2 = Expression::::Challenge(2, 1, E::ONE, E::ZERO); + let constant_2 = Expression::::Constant(Either::Left(B::from_canonical_u32(2))); + let constant_negative_1 = Expression::::Constant(Either::Left(B::from_canonical_u32(1).neg())); + + let e: Expression = w1.expr() * (c2.expr() * (constant_2.expr() + w0.expr() * c1.expr() - constant_negative_1.expr())); + let e_monomials = e.get_monomial_terms(); + let (dag, coeffs, final_out, _)= build_factored_dag_commutative(&e_monomials); + + let mut num_add = 0; + let mut num_mul = 0; + + for node in &dag { + match node.op { + 0 => (), // skip wit index + 1 => (), // skip scalar index + 2 => { + num_add += 1; + } + 3 => { + num_mul += 1; + } + op => panic!("unknown op {op}"), + } + } + assert_eq!(num_add, 1); + assert_eq!(num_mul, 3); + } } \ No newline at end of file From e5f7c87175efd581cf38a86da38b38978b278b2c Mon Sep 17 00:00:00 2001 From: "sm.wu" Date: Tue, 11 Nov 2025 16:21:44 +0800 Subject: [PATCH 08/10] tune api interface --- .../multilinear_extensions/src/expression/utils.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/multilinear_extensions/src/expression/utils.rs b/crates/multilinear_extensions/src/expression/utils.rs index 733372a..fe9a43b 100644 --- a/crates/multilinear_extensions/src/expression/utils.rs +++ b/crates/multilinear_extensions/src/expression/utils.rs @@ -585,7 +585,7 @@ fn expr_compression_to_dag_helper( } } } - c @ Expression::Challenge(challenge_id, _power, scalar, offset) => { + c @ Expression::Challenge(_, _power, scalar, offset) => { if *scalar == E::ZERO && *offset == E::ZERO { return None } @@ -614,6 +614,7 @@ struct TrieNode { } pub fn build_factored_dag_commutative( terms: &[Term, Expression>], + hint_shared_witin_lower_id: bool, ) -> (Vec, Vec>, Option, u32) { let mut root = TrieNode::default(); let mut scalars: Vec> = Vec::new(); @@ -629,9 +630,11 @@ pub fn build_factored_dag_commutative( }) .collect(); ids.sort(); // ensure a*b == b*a - // we assume witiness being shared will be made with larger id - // so we build the prefix tree with larger id go first - ids.reverse(); + if !hint_shared_witin_lower_id { + // witiness being shared will be made with larger id + // so we build the prefix tree with larger id go first + ids.reverse(); + } let mut cur = &mut root; for wid in ids { @@ -685,7 +688,7 @@ pub fn build_factored_dag_commutative( }); // If child exists, multiply with it - if let Some(rhs) = child_out { + if let Some(_) = child_out { let (left, right, out) = pop2_push1(stack_top); dag.push(Node { op: DagMul as u32, From 1162b9545da6be816da391c02a0da2bd6b19f102 Mon Sep 17 00:00:00 2001 From: "sm.wu" Date: Thu, 13 Nov 2025 21:50:39 +0800 Subject: [PATCH 09/10] chores: rename & clippy --- .../src/expression/utils.rs | 156 +++++++++--------- 1 file changed, 77 insertions(+), 79 deletions(-) diff --git a/crates/multilinear_extensions/src/expression/utils.rs b/crates/multilinear_extensions/src/expression/utils.rs index fe9a43b..4445652 100644 --- a/crates/multilinear_extensions/src/expression/utils.rs +++ b/crates/multilinear_extensions/src/expression/utils.rs @@ -194,10 +194,10 @@ pub fn expr_convert_to_witins( } } -pub const DagLoadWit: usize = 0; -pub const DagLoadScalar: usize = 1; -pub const DagAdd: usize = 2; -pub const DagMul: usize = 3; +pub const DAG_LOAD_WIT: usize = 0; +pub const DAG_LOAD_SCALAR: usize = 1; +pub const DAG_ADD: usize = 2; +pub const DAG_MUL: usize = 3; #[derive(Clone, Debug, Serialize, Deserialize)] #[repr(C)] @@ -218,6 +218,7 @@ fn is_zero(e: &Expression) -> bool { if v.map_either(|b| b.is_zero(), |x| x.is_zero()).into_inner()) } +#[allow(clippy::type_complexity)] pub fn expr_compression_to_dag( expr: &Expression, ) -> ( @@ -283,6 +284,9 @@ pub fn expr_compression_to_dag( ) } + +/// convert expression +#[allow(clippy::too_many_arguments, dead_code)] fn expr_compression_to_dag_helper( dag: &mut Vec, instance_scalar: &mut Vec, @@ -300,7 +304,7 @@ fn expr_compression_to_dag_helper( Expression::Fixed(_) => unimplemented!(), Expression::WitIn(wit_id) => { dag.push(Node { - op: DagLoadWit as u32, + op: DAG_LOAD_WIT as u32, left_id: *wit_id as u32, right_id: 0, out: *stack_pos, @@ -308,12 +312,12 @@ fn expr_compression_to_dag_helper( *stack_pos += 1; Some((1, 1)) } - Expression::StructuralWitIn(_, ..) => unimplemented!(), + Expression::StructuralWitIn(..) => unimplemented!(), Expression::Instance(_) => unimplemented!(), Expression::InstanceScalar(inst) => { - instance_scalar.push(inst.clone()); + instance_scalar.push(*inst); dag.push(Node { - op: DagLoadScalar as u32, + op: DAG_LOAD_SCALAR as u32, left_id: instance_scalar.len() as u32 - 1, right_id: 0, out: *stack_pos, @@ -330,13 +334,13 @@ fn expr_compression_to_dag_helper( return None; } - let constant_id = *constant_dedup.entry(value.clone()).or_insert_with(|| { - constant.push(value.clone()); + let constant_id = *constant_dedup.entry(*value).or_insert_with(|| { + constant.push(*value); (constant_offset + constant.len() - 1) as u32 }); dag.push(Node { - op: DagLoadScalar as u32, + op: DAG_LOAD_SCALAR as u32, left_id: constant_id, right_id: 0, out: *stack_pos, @@ -375,7 +379,7 @@ fn expr_compression_to_dag_helper( (Some(x), None) | (None, Some(x)) => Some(x), // a + 0 = a (Some((da, dep_a)), Some((db, dep_b))) => { dag.push(Node { - op: DagAdd as u32, + op: DAG_ADD as u32, left_id: *stack_pos - 2, right_id: *stack_pos - 1, out: *stack_pos - 2, @@ -421,7 +425,7 @@ fn expr_compression_to_dag_helper( (None, _) | (_, None) => None, // defensive (shouldn’t reach due to early zero) (Some((da, dep_a)), Some((db, dep_b))) => { dag.push(Node { - op: DagMul as u32, + op: DAG_MUL as u32, left_id: *stack_pos - 2, right_id: *stack_pos - 1, out: *stack_pos - 2, // overwrite lhs slot @@ -469,7 +473,7 @@ fn expr_compression_to_dag_helper( (Some(x), None) | (None, Some(x)) => Some(x), (Some((da, dep_a)), Some((db, dep_b))) => { dag.push(Node { - op: DagAdd as u32, + op: DAG_ADD as u32, left_id: *stack_pos - 2, right_id: *stack_pos - 1, out: *stack_pos - 2, @@ -498,7 +502,7 @@ fn expr_compression_to_dag_helper( (Some(x), None) | (None, Some(x)) => Some(x), (Some((dx, dep_x)), Some((db, dep_b))) => { dag.push(Node { - op: DagAdd as u32, + op: DAG_ADD as u32, left_id: *stack_pos - 2, right_id: *stack_pos - 1, out: *stack_pos - 2, @@ -527,7 +531,7 @@ fn expr_compression_to_dag_helper( (None, _) | (_, None) => None, // defensive (Some((dx, dep_x)), Some((da, dep_a))) => { dag.push(Node { - op: DagMul as u32, + op: DAG_MUL as u32, left_id: *stack_pos - 2, right_id: *stack_pos - 1, out: *stack_pos - 2, @@ -554,7 +558,7 @@ fn expr_compression_to_dag_helper( (None, _) | (_, None) => None, // x or a simplified to 0 above; defensive (Some((dx, dep_x)), Some((da, dep_a))) => { dag.push(Node { - op: DagMul as u32, + op: DAG_MUL as u32, left_id: *stack_pos - 2, right_id: *stack_pos - 1, out: *stack_pos - 2, @@ -575,7 +579,7 @@ fn expr_compression_to_dag_helper( (Some(xa), None) | (None, Some(xa)) => Some(xa), (Some((dm, dep_m)), Some((db, dep_b))) => { dag.push(Node { - op: DagAdd as u32, + op: DAG_ADD as u32, left_id: *stack_pos - 2, right_id: *stack_pos - 1, out: *stack_pos - 2, @@ -595,7 +599,7 @@ fn expr_compression_to_dag_helper( }); dag.push(Node { - op: DagLoadScalar as u32, + op: DAG_LOAD_SCALAR as u32, left_id: challenge_id, right_id: 0, out: *stack_pos, @@ -624,8 +628,8 @@ pub fn build_factored_dag_commutative( let mut ids: Vec = term .product .iter() - .filter_map(|e| match e { - Expression::WitIn(id) => Some(*id), + .map(|e| match e { + Expression::WitIn(id) => *id, e => unimplemented!("unknown expression {e}"), }) .collect(); @@ -681,17 +685,17 @@ pub fn build_factored_dag_commutative( // LOAD_WIT: push let out = push(stack_top, max_depth); dag.push(Node { - op: DagLoadWit as u32, + op: DAG_LOAD_WIT as u32, left_id: wid as u32, right_id: 0, out, }); // If child exists, multiply with it - if let Some(_) = child_out { + if child_out.is_some() { let (left, right, out) = pop2_push1(stack_top); dag.push(Node { - op: DagMul as u32, + op: DAG_MUL as u32, left_id: left, right_id: right, out, @@ -701,7 +705,7 @@ pub fn build_factored_dag_commutative( Some(_) => { let (l, r, out) = pop2_push1(stack_top); dag.push(Node { - op: DagAdd as u32, + op: DAG_ADD as u32, left_id: l, right_id: r, out, @@ -719,7 +723,7 @@ pub fn build_factored_dag_commutative( for &idx in &node.scalar_indices { let out = push(stack_top, max_depth); dag.push(Node { - op: DagLoadScalar as u32, + op: DAG_LOAD_SCALAR as u32, left_id: idx as u32, right_id: 0, out, @@ -730,7 +734,7 @@ pub fn build_factored_dag_commutative( Some(_) => { let (l, r, out) = pop2_push1(stack_top); dag.push(Node { - op: DagAdd as u32, + op: DAG_ADD as u32, left_id: l, right_id: r, out, @@ -745,7 +749,7 @@ pub fn build_factored_dag_commutative( (Some(_), Some(_)) => { let (l, r, out) = pop2_push1(stack_top); dag.push(Node { - op: DagAdd as u32, + op: DAG_ADD as u32, left_id: l, right_id: r, out, @@ -761,21 +765,43 @@ pub fn build_factored_dag_commutative( let final_out = emit::(&root, &mut dag, &mut stack_top, &mut max_stack_depth); (dag, scalars, final_out, max_stack_depth) } + +pub fn dag_stats(dag: &[Node]) -> (u32, u32) { + + let mut num_add = 0; + let mut num_mul = 0; + + for node in dag { + match node.op as usize { + DAG_LOAD_WIT => (), // skip wit index + DAG_LOAD_SCALAR => (), // skip scalar index + DAG_ADD => { + num_add += 1; + } + DAG_MUL => { + num_mul += 1; + } + op => panic!("unknown op {op}"), + } + } + + (num_add, num_mul) +} #[cfg(test)] mod tests { use std::ops::Neg; use either::Either; - use itertools::Itertools; use ff_ext::{BabyBearExt4, ExtensionField}; use p3::babybear::BabyBear; use p3::field::FieldAlgebra; use crate::{power_sequence, Expression, Instance, ToExpr}; - use crate::utils::{build_factored_dag_commutative, expr_compression_to_dag, Node}; + use crate::utils::{build_factored_dag_commutative, dag_stats, expr_compression_to_dag, Node}; type E = BabyBearExt4; type B = BabyBear; - fn extract_num_add_mul(expr: &Expression) -> (Vec, Vec, Vec>, Vec::BaseField, E>>, u32, (i32, i32), (usize, usize)) { + #[allow(clippy::type_complexity)] + fn extract_num_add_mul(expr: &Expression) -> (Vec, Vec, Vec>, Vec::BaseField, E>>, u32, (u32, u32), (usize, usize)) { let ( dag, instance_scalar_expr, @@ -783,24 +809,9 @@ mod tests { constant_expr, stack_top, (max_degree, max_dag_depth), - ) = expr_compression_to_dag(&expr); + ) = expr_compression_to_dag(expr); - let mut num_add = 0; - let mut num_mul = 0; - - for node in &dag { - match node.op { - 0 => (), // skip wit index - 1 => (), // skip scalar index - 2 => { - num_add += 1; - } - 3 => { - num_mul += 1; - } - op => panic!("unknown op {op}"), - } - } + let stats = dag_stats(&dag); ( dag, @@ -808,7 +819,7 @@ mod tests { challenges_expr, constant_expr, stack_top, - (num_add, num_mul), + stats, (max_degree, max_dag_depth), ) } @@ -822,13 +833,13 @@ mod tests { let e: Expression = s3.expr() * (s2.expr() * a.expr() * b.expr() + s4.expr()); let ( - dag, - instance_scalar_expr, + _dag, + _instance_scalar_expr, challenges_expr, constant_expr, - stack_top, + _stack_top, (num_add, num_mul), - (max_degree, max_dag_depth), + (max_degree, _max_dag_depth), ) = extract_num_add_mul(&e); assert_eq!(constant_expr.len(), 3); @@ -852,13 +863,13 @@ mod tests { let e: Expression = vec![a.expr(), b.expr()].into_iter().zip(pow_c).map(|(e1, e2)| e1.expr()*e2.expr()).sum::>(); let e = e * alpha; let ( - dag, - instance_scalar_expr, + _dag, + _instance_scalar_expr, challenges_expr, constant_expr, - stack_top, + _stack_top, (num_add, num_mul), - (max_degree, max_dag_depth), + (max_degree, _max_dag_depth), ) = extract_num_add_mul(&e); assert_eq!(constant_expr.len(), 0); // 1 was absorbed @@ -871,35 +882,22 @@ mod tests { #[test] fn test_build_factored_dag_commutative() { - // w1 * (c2 * (2 + w0*c1 -1)) + // w1 * (c1 * (2 + w0*c0 -1)) let w0 = Expression::::WitIn(0); let w1 = Expression::::WitIn(1); - let c1 = Expression::::Challenge(0, 1, E::ONE, E::ZERO); - let c2 = Expression::::Challenge(2, 1, E::ONE, E::ZERO); + let c0 = Expression::::Challenge(0, 1, E::ONE, E::ZERO); + let c1 = Expression::::Challenge(1, 1, E::ONE, E::ZERO); let constant_2 = Expression::::Constant(Either::Left(B::from_canonical_u32(2))); let constant_negative_1 = Expression::::Constant(Either::Left(B::from_canonical_u32(1).neg())); - let e: Expression = w1.expr() * (c2.expr() * (constant_2.expr() + w0.expr() * c1.expr() - constant_negative_1.expr())); + let e: Expression = w1.expr() * (c1.expr() * (constant_2.expr() + w0.expr() * c0.expr() - constant_negative_1.expr())); + // it will be converted to w1 * (w0 * c0' + c1'), where c0' = c0 * c1, c1' = c1 let e_monomials = e.get_monomial_terms(); - let (dag, coeffs, final_out, _)= build_factored_dag_commutative(&e_monomials); + + let (dag, _coeffs, _final_out, _)= build_factored_dag_commutative(&e_monomials, false); - let mut num_add = 0; - let mut num_mul = 0; - - for node in &dag { - match node.op { - 0 => (), // skip wit index - 1 => (), // skip scalar index - 2 => { - num_add += 1; - } - 3 => { - num_mul += 1; - } - op => panic!("unknown op {op}"), - } - } + let (num_add, num_mul) = dag_stats(&dag); assert_eq!(num_add, 1); - assert_eq!(num_mul, 3); + assert_eq!(num_mul, 2); } } \ No newline at end of file From 8f7a13c0a5dfbfb5c20d44f3dab3ae3fe8addc9e Mon Sep 17 00:00:00 2001 From: "sm.wu" Date: Thu, 13 Nov 2025 21:57:07 +0800 Subject: [PATCH 10/10] fmt --- .../src/expression/utils.rs | 238 +++++++++++++----- 1 file changed, 175 insertions(+), 63 deletions(-) diff --git a/crates/multilinear_extensions/src/expression/utils.rs b/crates/multilinear_extensions/src/expression/utils.rs index 4445652..04a8e83 100644 --- a/crates/multilinear_extensions/src/expression/utils.rs +++ b/crates/multilinear_extensions/src/expression/utils.rs @@ -284,7 +284,6 @@ pub fn expr_compression_to_dag( ) } - /// convert expression #[allow(clippy::too_many_arguments, dead_code)] fn expr_compression_to_dag_helper( @@ -397,28 +396,56 @@ fn expr_compression_to_dag_helper( } else if is_one(a) { // 1 * b = b (evaluate only b; it will land at the current top) expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, b, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + b, ) } else if is_one(b) { // a * 1 = a (evaluate only a) expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, a, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + a, ) } else { // ---- general case: evaluate a, then b, emit MUL into (top-2), pop 1 ---- let lhs = expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, a, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + a, ); let rhs = expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, b, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + b, ); match (lhs, rhs) { @@ -428,9 +455,9 @@ fn expr_compression_to_dag_helper( op: DAG_MUL as u32, left_id: *stack_pos - 2, right_id: *stack_pos - 1, - out: *stack_pos - 2, // overwrite lhs slot + out: *stack_pos - 2, // overwrite lhs slot }); - *stack_pos -= 1; // consume rhs + *stack_pos -= 1; // consume rhs Some((da + db, dep_a.max(dep_b + 1))) } } @@ -449,23 +476,44 @@ fn expr_compression_to_dag_helper( if is_zero(x) || is_zero(a) { // becomes b return expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, b, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + b, ); } if is_one(x) { // 1*a + b = a + b let lhs_a = expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, a, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + a, ); let rhs_b = expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, b, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + b, ); return match (lhs_a, rhs_b) { @@ -487,14 +535,28 @@ fn expr_compression_to_dag_helper( if is_one(a) { // x*1 + b = x + b let lhs_x = expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, x, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + x, ); let rhs_b = expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, b, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + b, ); return match (lhs_x, rhs_b) { @@ -517,14 +579,28 @@ fn expr_compression_to_dag_helper( // general product without identities since x!=0, a!=0 here // x*a + 0 = x*a let lhs_x = expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, x, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + x, ); let lhs_a = expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, a, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + a, ); return match (lhs_x, lhs_a) { @@ -544,14 +620,28 @@ fn expr_compression_to_dag_helper( // General case: compute (x*a) then + b let lhs_x = expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, x, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + x, ); let lhs_a = expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, a, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + a, ); let mul = match (lhs_x, lhs_a) { @@ -569,9 +659,16 @@ fn expr_compression_to_dag_helper( }; let rhs_b = expr_compression_to_dag_helper( - dag, instance_scalar, challenges_offset, challenges, - constant_offset, constant, challenges_dedup, constant_dedup, - stack_pos, b, + dag, + instance_scalar, + challenges_offset, + challenges, + constant_offset, + constant, + challenges_dedup, + constant_dedup, + stack_pos, + b, ); match (mul, rhs_b) { @@ -591,7 +688,7 @@ fn expr_compression_to_dag_helper( } c @ Expression::Challenge(_, _power, scalar, offset) => { if *scalar == E::ZERO && *offset == E::ZERO { - return None + return None; } let challenge_id = *challenges_dedup.entry(c.clone()).or_insert_with(|| { challenges.push(c.clone()); @@ -767,13 +864,12 @@ pub fn build_factored_dag_commutative( } pub fn dag_stats(dag: &[Node]) -> (u32, u32) { - let mut num_add = 0; let mut num_mul = 0; for node in dag { match node.op as usize { - DAG_LOAD_WIT => (), // skip wit index + DAG_LOAD_WIT => (), // skip wit index DAG_LOAD_SCALAR => (), // skip scalar index DAG_ADD => { num_add += 1; @@ -789,19 +885,30 @@ pub fn dag_stats(dag: &[Node]) -> (u32, u32) { } #[cfg(test)] mod tests { - use std::ops::Neg; + use crate::{ + Expression, Instance, ToExpr, power_sequence, + utils::{Node, build_factored_dag_commutative, dag_stats, expr_compression_to_dag}, + }; use either::Either; use ff_ext::{BabyBearExt4, ExtensionField}; - use p3::babybear::BabyBear; - use p3::field::FieldAlgebra; - use crate::{power_sequence, Expression, Instance, ToExpr}; - use crate::utils::{build_factored_dag_commutative, dag_stats, expr_compression_to_dag, Node}; + use p3::{babybear::BabyBear, field::FieldAlgebra}; + use std::ops::Neg; type E = BabyBearExt4; type B = BabyBear; #[allow(clippy::type_complexity)] - fn extract_num_add_mul(expr: &Expression) -> (Vec, Vec, Vec>, Vec::BaseField, E>>, u32, (u32, u32), (usize, usize)) { + fn extract_num_add_mul( + expr: &Expression, + ) -> ( + Vec, + Vec, + Vec>, + Vec::BaseField, E>>, + u32, + (u32, u32), + (usize, usize), + ) { let ( dag, instance_scalar_expr, @@ -847,7 +954,6 @@ mod tests { assert_eq!(num_add, 1); assert_eq!(num_mul, 3); assert_eq!(max_degree, 2); - } #[test] @@ -860,7 +966,11 @@ mod tests { // alpha * (1 * a + c * b) // will be optimized as alpha * (a + c * b) - let e: Expression = vec![a.expr(), b.expr()].into_iter().zip(pow_c).map(|(e1, e2)| e1.expr()*e2.expr()).sum::>(); + let e: Expression = vec![a.expr(), b.expr()] + .into_iter() + .zip(pow_c) + .map(|(e1, e2)| e1.expr() * e2.expr()) + .sum::>(); let e = e * alpha; let ( _dag, @@ -877,7 +987,6 @@ mod tests { assert_eq!(num_add, 1); assert_eq!(num_mul, 2); assert_eq!(max_degree, 1); - } #[test] @@ -888,16 +997,19 @@ mod tests { let c0 = Expression::::Challenge(0, 1, E::ONE, E::ZERO); let c1 = Expression::::Challenge(1, 1, E::ONE, E::ZERO); let constant_2 = Expression::::Constant(Either::Left(B::from_canonical_u32(2))); - let constant_negative_1 = Expression::::Constant(Either::Left(B::from_canonical_u32(1).neg())); + let constant_negative_1 = + Expression::::Constant(Either::Left(B::from_canonical_u32(1).neg())); - let e: Expression = w1.expr() * (c1.expr() * (constant_2.expr() + w0.expr() * c0.expr() - constant_negative_1.expr())); - // it will be converted to w1 * (w0 * c0' + c1'), where c0' = c0 * c1, c1' = c1 + let e: Expression = w1.expr() + * (c1.expr() + * (constant_2.expr() + w0.expr() * c0.expr() - constant_negative_1.expr())); + // it will be converted to w1 * (w0 * c0' + c1'), where c0' = c0 * c1, c1' = c1 let e_monomials = e.get_monomial_terms(); - - let (dag, _coeffs, _final_out, _)= build_factored_dag_commutative(&e_monomials, false); + + let (dag, _coeffs, _final_out, _) = build_factored_dag_commutative(&e_monomials, false); let (num_add, num_mul) = dag_stats(&dag); assert_eq!(num_add, 1); assert_eq!(num_mul, 2); } -} \ No newline at end of file +}