Skip to content

Commit f6b0b7b

Browse files
chrisethgeorgwiese
andauthored
Determine range constraints from fixed lookup. (#2300)
Co-authored-by: Georg Wiese <georgwiese@gmail.com>
1 parent 71eaf88 commit f6b0b7b

8 files changed

+173
-34
lines changed

executor/src/witgen/data_structures/mutable_state.rs

+3-6
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,12 @@ impl<'a, T: FieldElement, Q: QueryCallback<T>> MutableState<'a, T, Q> {
5656
identity_id: u64,
5757
known_inputs: &BitVec,
5858
range_constraints: &[RangeConstraint<T>],
59-
) -> bool {
59+
) -> Option<Vec<RangeConstraint<T>>> {
6060
// TODO We are currently ignoring bus interaction (also, but not only because there is no
6161
// unique machine responsible for handling a bus send), so just answer "false" if the identity
6262
// has no responsible machine.
63-
self.responsible_machine(identity_id)
64-
.ok()
65-
.is_some_and(|mut machine| {
66-
machine.can_process_call_fully(identity_id, known_inputs, range_constraints)
67-
})
63+
let mut machine = self.responsible_machine(identity_id).ok()?;
64+
machine.can_process_call_fully(identity_id, known_inputs, range_constraints)
6865
}
6966

7067
/// Call the machine responsible for the right-hand-side of an identity given its ID

executor/src/witgen/jit/affine_symbolic_expression.rs

+21
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,27 @@ impl<T: FieldElement, V: Ord + Clone + Display> AffineSymbolicExpression<T, V> {
113113
}
114114
}
115115

116+
/// Returns the range constraint of the whole expression.
117+
/// This only works for simple expressions since all coefficients
118+
/// must be known numbers.
119+
pub fn range_constraint(&self) -> RangeConstraint<T> {
120+
self.coefficients
121+
.iter()
122+
.map(|(var, coeff)| {
123+
let coeff = coeff.try_to_number()?;
124+
let rc = self.range_constraints.get(var)?;
125+
Some(rc.multiple(coeff))
126+
})
127+
.collect::<Option<Vec<_>>>()
128+
.and_then(|summands| {
129+
summands
130+
.into_iter()
131+
.chain(std::iter::once(self.offset.range_constraint()))
132+
.reduce(|c1, c2| c1.combine_sum(&c2))
133+
})
134+
.unwrap_or_default()
135+
}
136+
116137
/// If this expression contains a single unknown variable, returns it.
117138
pub fn single_unknown_variable(&self) -> Option<&V> {
118139
if self.coefficients.len() == 1 {

executor/src/witgen/jit/single_step_processor.rs

+36
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,40 @@ if (VM::instr_add[0] == 1) {
205205
}"
206206
);
207207
}
208+
209+
#[test]
210+
fn range_constraints_from_lookup() {
211+
let input = "
212+
namespace VM(256);
213+
let instr_add: col;
214+
let instr_mul: col;
215+
let pc: col;
216+
217+
col fixed LINE = [0, 1] + [2]*;
218+
col fixed INSTR_ADD = [0, 1] + [0]*;
219+
col fixed INSTR_MUL = [1, 0] + [1]*;
220+
221+
pc' = pc + 1;
222+
instr_add = 0;
223+
[ pc, instr_add, instr_mul ] in [ LINE, INSTR_ADD, INSTR_MUL ];
224+
225+
";
226+
let code = generate_single_step(input, "Main").unwrap();
227+
// After the machine call, we should have a direct assignment `VM::instr_mul[1] = 1`,
228+
// instead of just an assignment from the call variable.
229+
// This is because the fixed lookup machine can already provide a range constraint.
230+
// For reasons of processing order, the call variable will also be assigned
231+
// right before the call.
232+
assert_eq!(
233+
format_code(&code),
234+
"\
235+
VM::pc[1] = (VM::pc[0] + 1);
236+
VM::instr_add[1] = 0;
237+
call_var(2, 1, 0) = VM::pc[1];
238+
call_var(2, 1, 1) = 0;
239+
call_var(2, 1, 2) = 1;
240+
machine_call(2, [Known(call_var(2, 1, 0)), Known(call_var(2, 1, 1)), Unknown(call_var(2, 1, 2))]);
241+
VM::instr_mul[1] = 1;"
242+
);
243+
}
208244
}

executor/src/witgen/jit/witgen_inference.rs

+19-12
Original file line numberDiff line numberDiff line change
@@ -233,43 +233,49 @@ impl<'a, T: FieldElement, FixedEval: FixedEvaluator<T>> WitgenInference<'a, T, F
233233
}
234234
let evaluated = arguments
235235
.iter()
236-
.map(|a| {
237-
self.evaluate(a, row_offset)
238-
.and_then(|a| a.try_to_known().cloned())
239-
})
236+
.map(|a| self.evaluate(a, row_offset))
240237
.collect::<Vec<_>>();
241238
let range_constraints = evaluated
242239
.iter()
243240
.map(|e| e.as_ref().map(|e| e.range_constraint()).unwrap_or_default())
244241
.collect_vec();
245-
let known = evaluated.iter().map(|e| e.is_some()).collect();
242+
let known: BitVec = evaluated
243+
.iter()
244+
.map(|e| e.as_ref().and_then(|e| e.try_to_known()).is_some())
245+
.collect();
246246

247-
if !can_process_call.can_process_call_fully(lookup_id, &known, &range_constraints) {
247+
let Some(new_range_constraints) =
248+
can_process_call.can_process_call_fully(lookup_id, &known, &range_constraints)
249+
else {
248250
log::trace!(
249251
"Sub-machine cannot process call fully (will retry later): {lookup_id}, arguments: {}",
250252
arguments.iter().zip(known).map(|(arg, known)| {
251253
format!("{arg} [{}]", if known { "known" } else { "unknown" })
252254
}).format(", "));
253255
return ProcessResult::empty();
254-
}
256+
};
257+
let mut effects = vec![];
255258
let vars = arguments
256259
.iter()
260+
.zip_eq(new_range_constraints)
257261
.enumerate()
258-
.map(|(index, arg)| {
262+
.map(|(index, (arg, new_rc))| {
259263
let var = Variable::MachineCallParam(MachineCallVariable {
260264
identity_id: lookup_id,
261265
row_offset,
262266
index,
263267
});
264268
self.assign_variable(arg, row_offset, var.clone());
269+
effects.push(Effect::RangeConstraint(var.clone(), new_rc.clone()));
265270
if known[index] {
266271
assert!(self.is_known(&var));
267272
}
268273
var
269274
})
270275
.collect_vec();
276+
effects.push(Effect::MachineCall(lookup_id, known, vars.clone()));
271277
ProcessResult {
272-
effects: vec![Effect::MachineCall(lookup_id, known, vars)],
278+
effects,
273279
complete: true,
274280
}
275281
}
@@ -514,16 +520,17 @@ pub trait FixedEvaluator<T: FieldElement>: Clone {
514520
}
515521

516522
pub trait CanProcessCall<T: FieldElement> {
517-
/// Returns true if a call to the machine that handles the given identity
523+
/// Returns Some(..) if a call to the machine that handles the given identity
518524
/// can always be processed with the given known inputs and range constraints
519525
/// on the parameters.
526+
/// The value in the Option is a vector of new range constraints.
520527
/// @see Machine::can_process_call
521528
fn can_process_call_fully(
522529
&self,
523530
_identity_id: u64,
524531
_known_inputs: &BitVec,
525532
_range_constraints: &[RangeConstraint<T>],
526-
) -> bool;
533+
) -> Option<Vec<RangeConstraint<T>>>;
527534
}
528535

529536
impl<T: FieldElement, Q: QueryCallback<T>> CanProcessCall<T> for &MutableState<'_, T, Q> {
@@ -532,7 +539,7 @@ impl<T: FieldElement, Q: QueryCallback<T>> CanProcessCall<T> for &MutableState<'
532539
identity_id: u64,
533540
known_inputs: &BitVec,
534541
range_constraints: &[RangeConstraint<T>],
535-
) -> bool {
542+
) -> Option<Vec<RangeConstraint<T>>> {
536543
MutableState::can_process_call_fully(self, identity_id, known_inputs, range_constraints)
537544
}
538545
}

executor/src/witgen/machines/double_sorted_witness_machine_32.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ impl<'a, T: FieldElement> Machine<'a, T> for DoubleSortedWitnesses32<'a, T> {
191191
identity_id: u64,
192192
known_arguments: &BitVec,
193193
range_constraints: &[RangeConstraint<T>],
194-
) -> bool {
194+
) -> Option<Vec<RangeConstraint<T>>> {
195195
assert!(self.parts.connections.contains_key(&identity_id));
196196
assert_eq!(known_arguments.len(), 4);
197197
assert_eq!(range_constraints.len(), 4);
@@ -200,17 +200,18 @@ impl<'a, T: FieldElement> Machine<'a, T> for DoubleSortedWitnesses32<'a, T> {
200200

201201
// We need to known operation_id, step and address for all calls.
202202
if !known_arguments[0] || !known_arguments[1] || !known_arguments[2] {
203-
return false;
203+
return None;
204204
}
205205

206206
// For the value, it depends: If we write, we need to know it, if we read we do not need to know it.
207207
if known_arguments[3] {
208208
// It is known, so we are good anyway.
209-
true
209+
Some(vec![RangeConstraint::unconstrained(); 4])
210210
} else {
211211
// It is not known, so we can only process if we do not write.
212-
!range_constraints[0].allows_value(T::from(OPERATION_ID_BOOTLOADER_WRITE))
213-
&& !range_constraints[0].allows_value(T::from(OPERATION_ID_WRITE))
212+
(!range_constraints[0].allows_value(T::from(OPERATION_ID_BOOTLOADER_WRITE))
213+
&& !range_constraints[0].allows_value(T::from(OPERATION_ID_WRITE)))
214+
.then(|| vec![RangeConstraint::unconstrained(); 4])
214215
}
215216
}
216217

executor/src/witgen/machines/fixed_lookup_machine.rs

+80-5
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,10 @@ impl<'a, T: FieldElement> Machine<'a, T> for FixedLookup<'a, T> {
260260
&mut self,
261261
identity_id: u64,
262262
known_arguments: &BitVec,
263-
_range_constraints: &[RangeConstraint<T>],
264-
) -> bool {
263+
range_constraints: &[RangeConstraint<T>],
264+
) -> Option<Vec<RangeConstraint<T>>> {
265265
if !Self::is_responsible(&self.connections[&identity_id]) {
266-
return false;
266+
return None;
267267
}
268268
let index = self
269269
.indices
@@ -274,8 +274,72 @@ impl<'a, T: FieldElement> Machine<'a, T> for FixedLookup<'a, T> {
274274
.or_insert_with_key(|application| {
275275
create_index(self.fixed_data, application, &self.connections)
276276
});
277-
// Check that if there is a match, it is unique.
278-
index.values().all(|value| value.0.is_some())
277+
let input_range_constraints = known_arguments
278+
.iter()
279+
.zip_eq(range_constraints)
280+
.filter_map(|(is_known, range_constraint)| is_known.then_some(range_constraint.clone()))
281+
.collect_vec();
282+
283+
// Now only consider the index entries that match the input range constraints,
284+
// see that the result is unique and determine new output range constraints.
285+
let values_matching_input_constraints = index
286+
.iter()
287+
.filter(|(key, _)| matches_range_constraint(key, &input_range_constraints))
288+
.map(|(_, value)| {
289+
let (_, values) = value.get()?;
290+
Some(values)
291+
});
292+
let mut new_range_constraints: Option<Vec<(T, T, T::Integer)>> = None;
293+
for values in values_matching_input_constraints {
294+
// If any value is None, it means the lookup does not have a unique answer,
295+
// and thus we cannot process the call.
296+
let values = values?;
297+
new_range_constraints = Some(match new_range_constraints {
298+
// First item, turn each value into (min, max, mask).
299+
None => values
300+
.iter()
301+
.map(|v| (*v, *v, v.to_integer()))
302+
.collect_vec(),
303+
// Reduce range constraint by disjunction.
304+
Some(mut acc) => {
305+
for ((min, max, mask), v) in acc.iter_mut().zip_eq(values) {
306+
*min = (*min).min(*v);
307+
*max = (*max).max(*v);
308+
*mask |= v.to_integer();
309+
}
310+
acc
311+
}
312+
})
313+
}
314+
Some(match new_range_constraints {
315+
None => {
316+
// The iterator was empty, i.e. there are no inputs in the index matching the
317+
// range constraints.
318+
// This means that every call like this will lead to a fatal error, but there is
319+
// enough information in the inputs to hanlde the call. Unfortunately, there is
320+
// no way to signal this in the return type, yet.
321+
// TODO(#2324): change this.
322+
// We just return the input range constraints to signal "everything allright".
323+
range_constraints.to_vec()
324+
}
325+
Some(new_output_range_constraints) => {
326+
let mut new_output_range_constraints = new_output_range_constraints.into_iter();
327+
known_arguments
328+
.iter()
329+
.enumerate()
330+
.map(|(i, is_known)| {
331+
if is_known {
332+
// Just copy the input range constraint.
333+
range_constraints[i].clone()
334+
} else {
335+
let (min, max, mask) = new_output_range_constraints.next().unwrap();
336+
RangeConstraint::from_range(min, max)
337+
.conjunction(&RangeConstraint::from_mask(mask))
338+
}
339+
})
340+
.collect()
341+
}
342+
})
279343
}
280344

281345
fn process_plookup<Q: crate::witgen::QueryCallback<T>>(
@@ -398,3 +462,14 @@ impl<'a, T: FieldElement> RangeConstraintSet<AlgebraicVariable<'a>, T>
398462
}
399463
}
400464
}
465+
466+
/// Checks that an array of values satisfies a set of range constraints.
467+
fn matches_range_constraint<T: FieldElement>(
468+
values: &[T],
469+
range_constraints: &[RangeConstraint<T>],
470+
) -> bool {
471+
values
472+
.iter()
473+
.zip_eq(range_constraints)
474+
.all(|(value, range_constraint)| range_constraint.allows_value(*value))
475+
}

executor/src/witgen/machines/mod.rs

+7-5
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,23 @@ pub trait Machine<'a, T: FieldElement>: Send + Sync {
5555
);
5656
}
5757

58-
/// Returns true if this machine can alway fully process a call via the given
58+
/// Returns Some(..) if this machine can alway fully process a call via the given
5959
/// identity, the set of known arguments and a list of range constraints
6060
/// on the parameters. Note that the range constraints can be imposed both
6161
/// on inputs and on outputs.
62-
/// If this returns true, then corresponding calls to `process_lookup_direct`
62+
/// If this returns Some(..), then corresponding calls to `process_lookup_direct`
6363
/// are safe.
64+
/// The value returned inside the option is a vector of range constraints on the arguments,
65+
/// which again can be imposed both on inputs and on outputs.
6466
/// The function requires `&mut self` because it usually builds an index structure
6567
/// or something similar.
6668
fn can_process_call_fully(
6769
&mut self,
6870
_identity_id: u64,
6971
_known_arguments: &BitVec,
7072
_range_constraints: &[RangeConstraint<T>],
71-
) -> bool {
72-
false
73+
) -> Option<Vec<RangeConstraint<T>>> {
74+
None
7375
}
7476

7577
/// Like `process_plookup`, but also records the time spent in this machine.
@@ -188,7 +190,7 @@ impl<'a, T: FieldElement> Machine<'a, T> for KnownMachine<'a, T> {
188190
identity_id: u64,
189191
known_arguments: &BitVec,
190192
range_constraints: &[RangeConstraint<T>],
191-
) -> bool {
193+
) -> Option<Vec<RangeConstraint<T>>> {
192194
match self {
193195
KnownMachine::SecondStageMachine(m) => {
194196
m.can_process_call_fully(identity_id, known_arguments, range_constraints)

pipeline/tests/asm.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ fn permutation_to_block() {
467467
}
468468

469469
#[test]
470-
#[should_panic = "called `Result::unwrap()` on an `Err` value: Linear constraint is not satisfiable: 18446744069414584320 != 0"]
470+
#[should_panic = "Column main_bin::pc is not stackable in a 1-row block, conflict in rows 0 and 1"]
471471
fn permutation_to_vm() {
472472
// TODO: witgen issue: Machine incorrectly detected as block machine.
473473
let f = "asm/permutations/vm_to_vm.asm";

0 commit comments

Comments
 (0)