Skip to content

Commit d4f9ca6

Browse files
authored
Range constraints from failing lookups. (#2444)
Identities of the kind `[ b * clock_0 + c * clock_1 ] in [ byte ];`, which are used in the arithmetic machine, are currently not process by auto-witjitgen. The idea is that this results on range constraints for different columns on different rows. The mechanism in place previously would only return range constraints in case a lookup can always be processed, but this is not the case here: We will never be able to determine a value just from the lookup, the only information we can get here are range constraints. TODO: - [x] it might be that we will never be able to process the lookup (i.e. emit a call), in that case we should still add the assertion, because that is what is supposed to be checked. Maybe we should also detect that it is just a range constraint after all? Oh I think we will need to make the call in the end, once we know the value by other means.
1 parent 40318d5 commit d4f9ca6

8 files changed

+187
-133
lines changed

executor/src/witgen/data_structures/mutable_state.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ impl<'a, T: FieldElement, Q: QueryCallback<T>> MutableState<'a, T, Q> {
5555
&self,
5656
identity_id: u64,
5757
known_inputs: &BitVec,
58-
range_constraints: &[RangeConstraint<T>],
59-
) -> Option<Vec<RangeConstraint<T>>> {
60-
let mut machine = self.responsible_machine(identity_id).unwrap();
58+
range_constraints: Vec<RangeConstraint<T>>,
59+
) -> (bool, Vec<RangeConstraint<T>>) {
60+
let mut machine = self.responsible_machine(identity_id).ok().unwrap();
6161
machine.can_process_call_fully(self, identity_id, known_inputs, range_constraints)
6262
}
6363

executor/src/witgen/jit/block_machine_processor.rs

+38
Original file line numberDiff line numberDiff line change
@@ -426,4 +426,42 @@ params[3] = main_binary::C[3];"
426426
params[1] = Sub::b[0];"
427427
);
428428
}
429+
430+
#[test]
431+
fn complex_fixed_lookup_range_constraint() {
432+
let input = "
433+
namespace main(256);
434+
col witness a, b, c;
435+
[a, b, c] is SubM.sel $ [SubM.a, SubM.b, SubM.c];
436+
namespace SubM(256);
437+
col witness a, b, c;
438+
let sel: col = |i| (i + 1) % 2;
439+
let clock_0: col = |i| i % 2;
440+
let clock_1: col = |i| (i + 1) % 2;
441+
let byte: col = |i| i & 0xff;
442+
[ b * clock_0 + c * clock_1 ] in [ byte ];
443+
(b' - b) * sel = 0;
444+
(c' - c) * sel = 0;
445+
a = b * 256 + c;
446+
";
447+
let code = generate_for_block_machine(input, "SubM", 1, 2)
448+
.unwrap()
449+
.code;
450+
assert_eq!(
451+
format_code(&code),
452+
"SubM::a[0] = params[0];
453+
SubM::b[0] = ((SubM::a[0] & 0xff00) // 256);
454+
SubM::c[0] = (SubM::a[0] & 0xff);
455+
assert (SubM::a[0] & 0xffffffffffff0000) == 0;
456+
params[1] = SubM::b[0];
457+
params[2] = SubM::c[0];
458+
call_var(1, 0, 0) = SubM::c[0];
459+
machine_call(1, [Known(call_var(1, 0, 0))]);
460+
SubM::b[1] = SubM::b[0];
461+
call_var(1, 1, 0) = SubM::b[1];
462+
SubM::c[1] = SubM::c[0];
463+
machine_call(1, [Known(call_var(1, 1, 0))]);
464+
SubM::a[1] = ((SubM::b[1] * 256) + SubM::c[1]);"
465+
);
466+
}
429467
}

executor/src/witgen/jit/witgen_inference.rs

+16-20
Original file line numberDiff line numberDiff line change
@@ -333,24 +333,20 @@ impl<'a, T: FieldElement, FixedEval: FixedEvaluator<T>> WitgenInference<'a, T, F
333333
.collect_vec();
334334
let known: BitVec = arguments.iter().map(|v| self.is_known(v)).collect();
335335

336-
let Some(new_range_constraints) =
337-
can_process_call.can_process_call_fully(lookup_id, &known, &range_constraints)
338-
else {
339-
return ProcessResult::empty();
340-
};
341-
let effects = arguments
336+
let (can_process, range_constraints) =
337+
can_process_call.can_process_call_fully(lookup_id, &known, range_constraints);
338+
339+
let mut effects = arguments
342340
.iter()
343-
.zip_eq(new_range_constraints)
341+
.zip_eq(range_constraints)
344342
.map(|(var, new_rc)| Effect::RangeConstraint(var.clone(), new_rc.clone()))
345-
.chain(std::iter::once(Effect::MachineCall(
346-
lookup_id,
347-
known,
348-
arguments.to_vec(),
349-
)))
350-
.collect();
343+
.collect_vec();
344+
if can_process {
345+
effects.push(Effect::MachineCall(lookup_id, known, arguments.to_vec()));
346+
}
351347
ProcessResult {
352348
effects,
353-
complete: true,
349+
complete: can_process,
354350
}
355351
}
356352

@@ -619,26 +615,26 @@ pub trait FixedEvaluator<T: FieldElement>: Clone {
619615
}
620616

621617
pub trait CanProcessCall<T: FieldElement>: Clone {
622-
/// Returns Some(..) if a call to the machine that handles the given identity
618+
/// Returns (true, _) if a call to the machine that handles the given identity
623619
/// can always be processed with the given known inputs and range constraints
624620
/// on the parameters.
625-
/// The value in the Option is a vector of new range constraints.
621+
/// The second return value is a vector of new range constraints.
626622
/// @see Machine::can_process_call
627623
fn can_process_call_fully(
628624
&self,
629625
_identity_id: u64,
630626
_known_inputs: &BitVec,
631-
_range_constraints: &[RangeConstraint<T>],
632-
) -> Option<Vec<RangeConstraint<T>>>;
627+
_range_constraints: Vec<RangeConstraint<T>>,
628+
) -> (bool, Vec<RangeConstraint<T>>);
633629
}
634630

635631
impl<T: FieldElement, Q: QueryCallback<T>> CanProcessCall<T> for &MutableState<'_, T, Q> {
636632
fn can_process_call_fully(
637633
&self,
638634
identity_id: u64,
639635
known_inputs: &BitVec,
640-
range_constraints: &[RangeConstraint<T>],
641-
) -> Option<Vec<RangeConstraint<T>>> {
636+
range_constraints: Vec<RangeConstraint<T>>,
637+
) -> (bool, Vec<RangeConstraint<T>>) {
642638
MutableState::can_process_call_fully(self, identity_id, known_inputs, range_constraints)
643639
}
644640
}

executor/src/witgen/machines/block_machine.rs

+11-5
Original file line numberDiff line numberDiff line change
@@ -170,18 +170,24 @@ impl<'a, T: FieldElement> Machine<'a, T> for BlockMachine<'a, T> {
170170
can_process: impl CanProcessCall<T>,
171171
identity_id: u64,
172172
known_arguments: &BitVec,
173-
range_constraints: &[RangeConstraint<T>],
174-
) -> Option<Vec<RangeConstraint<T>>> {
173+
range_constraints: Vec<RangeConstraint<T>>,
174+
) -> (bool, Vec<RangeConstraint<T>>) {
175175
// We use the input range constraints to see if there is a column
176176
// containing the substring "operation_id" which is constrained to a
177177
// single value and use that value as part of the cache key.
178178
let operation_id = self.find_operation_id(identity_id).and_then(|index| {
179179
let v = range_constraints[index].try_to_single_value()?;
180180
Some((index, v))
181181
});
182-
self.function_cache
183-
.compile_cached(can_process, identity_id, known_arguments, operation_id)
184-
.map(|r| r.range_constraints.clone())
182+
match self.function_cache.compile_cached(
183+
can_process,
184+
identity_id,
185+
known_arguments,
186+
operation_id,
187+
) {
188+
Some(entry) => (true, entry.range_constraints.clone()),
189+
None => (false, range_constraints),
190+
}
185191
}
186192

187193
fn process_lookup_direct<'b, 'c, Q: QueryCallback<T>>(

executor/src/witgen/machines/double_sorted_witness_machine_32.rs

+9-9
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,8 @@ impl<'a, T: FieldElement> Machine<'a, T> for DoubleSortedWitnesses32<'a, T> {
197197
_can_process: impl CanProcessCall<T>,
198198
identity_id: u64,
199199
known_arguments: &BitVec,
200-
range_constraints: &[RangeConstraint<T>],
201-
) -> Option<Vec<RangeConstraint<T>>> {
200+
range_constraints: Vec<RangeConstraint<T>>,
201+
) -> (bool, Vec<RangeConstraint<T>>) {
202202
assert!(self.parts.connections.contains_key(&identity_id));
203203
assert_eq!(known_arguments.len(), 4);
204204
assert_eq!(range_constraints.len(), 4);
@@ -207,19 +207,19 @@ impl<'a, T: FieldElement> Machine<'a, T> for DoubleSortedWitnesses32<'a, T> {
207207

208208
// We need to known operation_id, step and address for all calls.
209209
if !known_arguments[0] || !known_arguments[1] || !known_arguments[2] {
210-
return None;
210+
return (false, range_constraints);
211211
}
212212

213213
// For the value, it depends: If we write, we need to know it, if we read we do not need to know it.
214-
if known_arguments[3] {
214+
let can_answer = if known_arguments[3] {
215215
// It is known, so we are good anyway.
216-
Some(vec![RangeConstraint::unconstrained(); 4])
216+
true
217217
} else {
218218
// It is not known, so we can only process if we do not write.
219-
(!range_constraints[0].allows_value(T::from(OPERATION_ID_BOOTLOADER_WRITE))
220-
&& !range_constraints[0].allows_value(T::from(OPERATION_ID_WRITE)))
221-
.then(|| vec![RangeConstraint::unconstrained(); 4])
222-
}
219+
!range_constraints[0].allows_value(T::from(OPERATION_ID_BOOTLOADER_WRITE))
220+
&& !range_constraints[0].allows_value(T::from(OPERATION_ID_WRITE))
221+
};
222+
(can_answer, range_constraints)
223223
}
224224

225225
fn process_lookup_direct<'b, 'c, Q: QueryCallback<T>>(

0 commit comments

Comments
 (0)