Skip to content

Commit 0402f84

Browse files
committed
Build generic qm31 constants.
1 parent 8bc157a commit 0402f84

2 files changed

Lines changed: 107 additions & 0 deletions

File tree

crates/circuits/src/finalize_constants.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ fn finalize_constants_with_min_base(context: &mut Context<impl IValue>, min_base
103103
m31_base,
104104
qm31_basis,
105105
);
106+
// Build the remaining QM31 constants.
107+
decompose_qm31_constants(context, &mut qm31_constants, &mut m31_cache, m31_base, qm31_basis);
108+
assert!(qm31_constants.is_empty());
106109
}
107110

108111
/// Finds the largest integer N such that all values in [0, N] are present as constants.
@@ -252,3 +255,33 @@ fn decompose_broadcast_constants(
252255
false
253256
});
254257
}
258+
259+
fn decompose_qm31_constants(
260+
context: &mut Context<impl IValue>,
261+
qm31_constants: &mut IndexMap<QM31, Var>,
262+
m31_cache: &mut HashMap<M31, usize>,
263+
base: M31,
264+
qm_basis: [Var; 3],
265+
) {
266+
let [i_var, u_var, iu_var] = qm_basis;
267+
268+
for (qm31_value, qm31_var) in qm31_constants.drain(..) {
269+
let limbs = qm31_value.to_m31_array();
270+
let [a, b, c, d]: [Var; 4] = std::array::from_fn(|j| {
271+
let m31_val = limbs[j];
272+
if !m31_cache.contains_key(&m31_val) {
273+
build_m31_from_base(context, m31_cache, &mut IndexMap::new(), base, m31_val);
274+
}
275+
Var { idx: *m31_cache.get(&m31_val).unwrap() }
276+
});
277+
278+
// a + b*i + c*u + d*iu → output to reserved idx
279+
let first_half = eval!(context, (a) + ((b) * (i_var)));
280+
let second_half = eval!(context, ((c) * (u_var)) + ((d) * (iu_var)));
281+
context.circuit.add.push(Add {
282+
in0: first_half.idx,
283+
in1: second_half.idx,
284+
out: qm31_var.idx,
285+
});
286+
}
287+
}

crates/circuits/src/finalize_constants_test.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,77 @@ fn test_broadcast_decomposition() {
151151
context.circuit.check_yields();
152152
context.validate_circuit();
153153
}
154+
155+
#[test]
156+
fn test_mixed_m31_and_qm31_constants_small() {
157+
let mut context = TraceContext::default();
158+
// Add `u`.
159+
// TODO(Leo): remove this once `u` is added to the default constants.
160+
context.constant(qm31_from_u32s(0, 0, 1, 0));
161+
// General (non-broadcast, non-base-field) QM31 constant. All limbs (1, 2, 3, 4) live in the
162+
// chain, so no base decomposition is triggered.
163+
context.constant(qm31_from_u32s(1, 2, 3, 4));
164+
finalize_constants_with_min_base(&mut context, 5);
165+
166+
// The plus-one chain populates [5]..=[8] for values 2..=5. The QM31 basis allocates [9] = u*u,
167+
// [10] = u² - 2 = i, [11] = i*u = iu. The ones vector is built as ([1] + [10]) + ([2] + [11])
168+
// yielding wires [12], [13], [14] (unused here, since (1, 2, 3, 4) isn't a broadcast). The
169+
// general QM31 constant is then assembled as a + b*i + c*u + d*iu:
170+
// [15] = [5] * [10] (2 * i), [16] = [1] + [15] (1 + 2*i)
171+
// [17] = [6] * [2] (3 * u), [18] = [7] * [11] (4 * iu), [19] = [17] + [18]
172+
// [3] = [16] + [19] (finally constrain the constant).
173+
expect![[r#"
174+
[0] = [0] + [0]
175+
[2] = [2] + [0]
176+
[1] = [1] + [0]
177+
[5] = [1] + [1]
178+
[6] = [5] + [1]
179+
[7] = [6] + [1]
180+
[8] = [7] + [1]
181+
[12] = [1] + [10]
182+
[13] = [2] + [11]
183+
[14] = [12] + [13]
184+
[16] = [1] + [15]
185+
[19] = [17] + [18]
186+
[3] = [16] + [19]
187+
[10] = [9] - [5]
188+
[4] = [2] * [1]
189+
[9] = [2] * [2]
190+
[11] = [10] * [2]
191+
[15] = [5] * [10]
192+
[17] = [6] * [2]
193+
[18] = [7] * [11]
194+
[4] = [2]
195+
output [2]
196+
"#]]
197+
.assert_eq(&format!("{:?}", context.circuit));
198+
199+
// The reserved Var carries the assembled QM31 value; the partial sums hold the two halves.
200+
assert_eq!(context.get(Var { idx: 3 }), qm31_from_u32s(1, 2, 3, 4));
201+
assert_eq!(context.get(Var { idx: 16 }), qm31_from_u32s(1, 2, 0, 0));
202+
assert_eq!(context.get(Var { idx: 19 }), qm31_from_u32s(0, 0, 3, 4));
203+
context.circuit.check_yields();
204+
context.validate_circuit();
205+
}
206+
207+
#[test]
208+
fn test_mixed_m31_and_qm31_constants_large() {
209+
let mut context = TraceContext::default();
210+
// Add `u`.
211+
// TODO(Leo): remove this once `u` is added to the default constants.
212+
context.constant(qm31_from_u32s(0, 0, 1, 0));
213+
// Add constants of various types.
214+
context.constant(qm31_from_u32s(1000, 2000, 3000, 4000));
215+
context.constant(qm31_from_u32s(1, 1, 1, 1));
216+
context.constant(qm31_from_u32s(2, 2, 2, 2));
217+
context.constant(qm31_from_u32s(666, 666, 666, 666));
218+
context.constant(qm31_from_u32s(3456, 0, 0, 0));
219+
context.constant(qm31_from_u32s(7890, 0, 0, 0));
220+
context.constant(qm31_from_u32s(1234, 2, 3, 4));
221+
context.constant(qm31_from_u32s(0, 1234, 0, 0));
222+
context.constant(qm31_from_u32s(0, 0, 1234, 0));
223+
context.constant(qm31_from_u32s(0, 0, 0, 1234));
224+
finalize_constants(&mut context);
225+
context.circuit.check_yields();
226+
context.validate_circuit();
227+
}

0 commit comments

Comments
 (0)