Skip to content

Commit e70bcae

Browse files
committed
Determine range constraints from fixed lookup.
1 parent 40aa582 commit e70bcae

File tree

6 files changed

+125
-33
lines changed

6 files changed

+125
-33
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

+24
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,30 @@ 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+
let Some(summands) = self
121+
.coefficients
122+
.iter()
123+
.map(|(var, coeff)| {
124+
let coeff = coeff.try_to_number()?;
125+
let rc = self.range_constraints.get(var)?;
126+
Some(rc.multiple(coeff))
127+
})
128+
.chain(std::iter::once(Some(self.offset.range_constraint())))
129+
.collect::<Option<Vec<_>>>()
130+
else {
131+
return Default::default();
132+
};
133+
summands
134+
.into_iter()
135+
.reduce(|c1, c2| c1.combine_sum(&c2))
136+
// We always have at least the offset.
137+
.unwrap()
138+
}
139+
116140
/// If this expression contains a single unknown variable, returns it.
117141
pub fn single_unknown_variable(&self) -> Option<&V> {
118142
if self.coefficients.len() == 1 {

executor/src/witgen/jit/witgen_inference.rs

+19-12
Original file line numberDiff line numberDiff line change
@@ -242,43 +242,49 @@ impl<'a, T: FieldElement, FixedEval: FixedEvaluator<T>> WitgenInference<'a, T, F
242242
}
243243
let evaluated = arguments
244244
.iter()
245-
.map(|a| {
246-
self.evaluate(a, row_offset)
247-
.and_then(|a| a.try_to_known().cloned())
248-
})
245+
.map(|a| self.evaluate(a, row_offset))
249246
.collect::<Vec<_>>();
250247
let range_constraints = evaluated
251248
.iter()
252249
.map(|e| e.as_ref().map(|e| e.range_constraint()).unwrap_or_default())
253250
.collect_vec();
254-
let known = evaluated.iter().map(|e| e.is_some()).collect();
251+
let known = evaluated
252+
.iter()
253+
.map(|e| e.as_ref().map(|e| e.try_to_known()).is_some())
254+
.collect();
255255

256-
if !can_process_call.can_process_call_fully(lookup_id, &known, &range_constraints) {
256+
let Some(new_range_constraints) =
257+
can_process_call.can_process_call_fully(lookup_id, &known, &range_constraints)
258+
else {
257259
log::trace!(
258260
"Sub-machine cannot process call fully (will retry later): {lookup_id}, arguments: {}",
259261
arguments.iter().zip(known).map(|(arg, known)| {
260262
format!("{arg} [{}]", if known { "known" } else { "unknown" })
261263
}).format(", "));
262264
return ProcessResult::empty();
263-
}
265+
};
266+
let mut effects = vec![];
264267
let vars = arguments
265268
.iter()
269+
.zip_eq(new_range_constraints)
266270
.enumerate()
267-
.map(|(index, arg)| {
271+
.map(|(index, (arg, new_rc))| {
268272
let var = Variable::MachineCallParam(MachineCallVariable {
269273
identity_id: lookup_id,
270274
row_offset,
271275
index,
272276
});
273277
self.assign_variable(arg, row_offset, var.clone());
278+
effects.push(Effect::RangeConstraint(var.clone(), new_rc.clone()));
274279
if known[index] {
275280
assert!(self.is_known(&var));
276281
}
277282
var
278283
})
279284
.collect_vec();
285+
effects.push(Effect::MachineCall(lookup_id, known, vars.clone()));
280286
ProcessResult {
281-
effects: vec![Effect::MachineCall(lookup_id, known, vars)],
287+
effects,
282288
complete: true,
283289
}
284290
}
@@ -523,16 +529,17 @@ pub trait FixedEvaluator<T: FieldElement>: Clone {
523529
}
524530

525531
pub trait CanProcessCall<T: FieldElement> {
526-
/// Returns true if a call to the machine that handles the given identity
532+
/// Returns Some(..) if a call to the machine that handles the given identity
527533
/// can always be processed with the given known inputs and range constraints
528534
/// on the parameters.
535+
/// The value in the Option is a vector of new range constraints.
529536
/// @see Machine::can_process_call
530537
fn can_process_call_fully(
531538
&self,
532539
_identity_id: u64,
533540
_known_inputs: &BitVec,
534541
_range_constraints: &[RangeConstraint<T>],
535-
) -> bool;
542+
) -> Option<Vec<RangeConstraint<T>>>;
536543
}
537544

538545
impl<T: FieldElement, Q: QueryCallback<T>> CanProcessCall<T> for &MutableState<'_, T, Q> {
@@ -541,7 +548,7 @@ impl<T: FieldElement, Q: QueryCallback<T>> CanProcessCall<T> for &MutableState<'
541548
identity_id: u64,
542549
known_inputs: &BitVec,
543550
range_constraints: &[RangeConstraint<T>],
544-
) -> bool {
551+
) -> Option<Vec<RangeConstraint<T>>> {
545552
MutableState::can_process_call_fully(self, identity_id, known_inputs, range_constraints)
546553
}
547554
}

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

+66-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,58 @@ 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 mut values = 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 Some(first) = values.next() else {
293+
// If the iterator is empty, we have no match, but it can always be answered,
294+
// so this is successful.
295+
// Unfortunately, we cannot express the "empty" range constraint.
296+
return Some(vec![Default::default(); range_constraints.len()]);
297+
};
298+
// If an item (including the first) is None, it means the match was not unique.
299+
let mut new_output_range_constraints = first?
300+
.iter()
301+
// min, max, mask
302+
.map(|v| (*v, *v, v.to_integer()))
303+
.collect_vec();
304+
for v in values {
305+
// If any value is None, the match was not unique.
306+
let v = v?;
307+
for ((min, max, mask), v) in new_output_range_constraints.iter_mut().zip_eq(v) {
308+
*min = (*min).min(*v);
309+
*max = (*max).max(*v);
310+
*mask |= v.to_integer();
311+
}
312+
}
313+
let mut new_output_range_constraints = new_output_range_constraints.into_iter();
314+
Some(
315+
known_arguments
316+
.iter()
317+
.map(|is_known| {
318+
if is_known {
319+
// We could also copy the input range constraint.
320+
RangeConstraint::default()
321+
} else {
322+
let (min, max, mask) = new_output_range_constraints.next().unwrap();
323+
RangeConstraint::from_range(min, max)
324+
.conjunction(&RangeConstraint::from_mask(mask))
325+
}
326+
})
327+
.collect(),
328+
)
279329
}
280330

281331
fn process_plookup<Q: crate::witgen::QueryCallback<T>>(
@@ -398,3 +448,14 @@ impl<'a, T: FieldElement> RangeConstraintSet<AlgebraicVariable<'a>, T>
398448
}
399449
}
400450
}
451+
452+
/// Checks that an array of values satisfies a set of range constraints.
453+
fn matches_range_constraint<T: FieldElement>(
454+
values: &[T],
455+
range_constraints: &[RangeConstraint<T>],
456+
) -> bool {
457+
values
458+
.iter()
459+
.zip_eq(range_constraints)
460+
.all(|(value, range_constraint)| range_constraint.allows_value(*value))
461+
}

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)

0 commit comments

Comments
 (0)