Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Determine range constraints from fixed lookup. #2300

Merged
merged 11 commits into from
Jan 13, 2025
19 changes: 8 additions & 11 deletions executor/src/witgen/jit/affine_symbolic_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,24 +117,21 @@ impl<T: FieldElement, V: Ord + Clone + Display> AffineSymbolicExpression<T, V> {
/// This only works for simple expressions since all coefficients
/// must be known numbers.
pub fn range_constraint(&self) -> RangeConstraint<T> {
let Some(summands) = self
.coefficients
self.coefficients
.iter()
.map(|(var, coeff)| {
let coeff = coeff.try_to_number()?;
let rc = self.range_constraints.get(var)?;
Some(rc.multiple(coeff))
})
.chain(std::iter::once(Some(self.offset.range_constraint())))
.collect::<Option<Vec<_>>>()
else {
return Default::default();
};
summands
.into_iter()
.reduce(|c1, c2| c1.combine_sum(&c2))
// We always have at least the offset.
.unwrap()
.and_then(|summands| {
summands
.into_iter()
.chain(std::iter::once(self.offset.range_constraint()))
.reduce(|c1, c2| c1.combine_sum(&c2))
})
.unwrap_or_default()
}

/// If this expression contains a single unknown variable, returns it.
Expand Down
2 changes: 1 addition & 1 deletion executor/src/witgen/jit/single_step_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ if (VM::instr_add[0] == 1) {
let code = generate_single_step(input, "Main").unwrap();
// After the machine call, we should have a direct assignment `VM::instr_mul[1] = 1`,
// instead of just an assignment from the call variable.
// This is because the fixed lookup machine can alread provide a range constraint.
// This is because the fixed lookup machine can already provide a range constraint.
// For reasons of processing order, the call variable will also be assigned
// right before the call.
assert_eq!(
Expand Down
87 changes: 50 additions & 37 deletions executor/src/witgen/machines/fixed_lookup_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,50 +282,63 @@ impl<'a, T: FieldElement> Machine<'a, T> for FixedLookup<'a, T> {

// Now only consider the index entries that match the input range constraints,
// see that the result is unique and determine new output range constraints.
let mut values = index
let values_matching_input_constraints = index
.iter()
.filter(|(key, _)| matches_range_constraint(key, &input_range_constraints))
.map(|(_, value)| {
let (_, values) = value.get()?;
Some(values)
});
let Some(first) = values.next() else {
// If the iterator is empty, we have no match, but it can always be answered,
// so this is successful.
// Unfortunately, we cannot express the "empty" range constraint.
return Some(vec![Default::default(); range_constraints.len()]);
};
// If an item (including the first) is None, it means the match was not unique.
let mut new_output_range_constraints = first?
.iter()
// min, max, mask
.map(|v| (*v, *v, v.to_integer()))
.collect_vec();
for v in values {
// If any value is None, the match was not unique.
let v = v?;
for ((min, max, mask), v) in new_output_range_constraints.iter_mut().zip_eq(v) {
*min = (*min).min(*v);
*max = (*max).max(*v);
*mask |= v.to_integer();
}
}
let mut new_output_range_constraints = new_output_range_constraints.into_iter();
Some(
known_arguments
.iter()
.map(|is_known| {
if is_known {
// We could also copy the input range constraint.
RangeConstraint::default()
} else {
let (min, max, mask) = new_output_range_constraints.next().unwrap();
RangeConstraint::from_range(min, max)
.conjunction(&RangeConstraint::from_mask(mask))
let mut new_range_constraints: Option<Vec<(T, T, T::Integer)>> = None;
for values in values_matching_input_constraints {
// If any value is None, it means the lookup does not have a unique answer,
// and thus we cannot process the call.
let values = values?;
new_range_constraints = Some(match new_range_constraints {
// First item, turn each value into (min, max, mask).
None => values
.iter()
.map(|v| (*v, *v, v.to_integer()))
.collect_vec(),
Some(mut acc) => {
for ((min, max, mask), v) in acc.iter_mut().zip_eq(values) {
*min = (*min).min(*v);
*max = (*max).max(*v);
*mask |= v.to_integer();
}
})
.collect(),
)
acc
}
})
}
Some(match new_range_constraints {
None => {
// The iterator was empty, i.e. there are no inputs in the index matching the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand whether this is a case that would happen in practice (for example because some branches are actually impossible), or whether hitting this path would mean we can't complete the code generation. If the latter, maybe we can at least log an error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can happen if we decide to branch, then derive further concrete values, do a lookup again and then discover that we do not have these values in the lookup table at all.

// range constraints.
// This means that every call like this will lead to a fatal error, but there is
// enough information in the inputs to hanlde the call. Unfortunately, there is
// no way to signal this in the return type, yet.
// TODO change this.
// We just return the input range constraints to signal "everything allright".
range_constraints.to_vec()
}
Some(new_output_range_constraints) => {
let mut new_output_range_constraints = new_output_range_constraints.into_iter();
known_arguments
.iter()
.enumerate()
.map(|(i, is_known)| {
if is_known {
// Just copy the input range constraint.
range_constraints[i].clone()
} else {
let (min, max, mask) = new_output_range_constraints.next().unwrap();
RangeConstraint::from_range(min, max)
.conjunction(&RangeConstraint::from_mask(mask))
}
})
.collect()
}
})
}

fn process_plookup<Q: crate::witgen::QueryCallback<T>>(
Expand Down
Loading