| 79 | lookups: Vec<Lookup<Expr<G>>>, |
| 80 | lookup_group_size: usize| { |
| 81 | slot_widths.push(lookups.iter().map(|l| l.args.len()).collect()); |
| 82 | circuit_inputs.push(CircuitInputs { |
| 83 | main_width, |
| 84 | preprocessed, |
| 85 | constraints, |
| 86 | ext_constraints: vec![], |
| 87 | lookups, |
| 88 | lookup_group_size, |
| 89 | }); |
| 90 | }; |
| 91 | |
| 92 | // Constrained functions (ascending index). |
| 93 | for i in 0..toplevel.functions.len() { |
| 94 | if !toplevel.functions[i].constrained { |
| 95 | continue; |
| 96 | } |
| 97 | let (constraints, lookups) = toplevel.build_constraints(i); |
| 98 | // A branchless function's lookup arguments are sent raw (degree 1; |
| 99 | // see `ConstraintState::gate`), so two lookups fit in one chained |
| 100 | // accumulator step at degree 3 — within the degree the selector-gated |
| 101 | // constraints already pay for. Branching functions keep k = 1: their |
| 102 | // superposed arguments are degree 2, and grouping would push the |
| 103 | // logUp constraints past the quotient budget. |
| 104 | let group_size = |
| 105 | if toplevel.functions[i].layout.selectors == 1 && lookups.len() >= 2 { |
| 106 | 2 |
| 107 | } else { |
| 108 | 1 |
| 109 | }; |
| 110 | push_circuit( |
| 111 | constraints.width, |
| 112 | None, |
| 113 | constraints.zeros, |