1use super::{
7 ConstraintContext, HashMap, MAX_LOCAL_ANALYSIS_NODES, SymBinOp, SymBoolExpr, SymBoolExprKind,
8 SymCmpOp, SymExpr, SymExprKind, U256, WordInterval,
9};
10
11struct RoundingBounds<'a> {
13 anchor: &'a SymExpr,
14 below: U256,
15 above: U256,
16}
17
18impl ConstraintContext {
19 pub(super) fn rounded_product_operands(expr: &SymExpr) -> Option<(&SymExpr, U256)> {
21 let (quotient, factor) = Self::constant_mul_operands(expr)?;
22 let (dividend, divisor) = quotient.udiv_operands()?;
23 (divisor.as_const() == Some(factor) && !factor.is_zero()).then_some((dividend, factor))
24 }
25
26 fn rounding_bounds<'a>(
30 &self,
31 expr: &'a SymExpr,
32 anchor: Option<&SymExpr>,
33 ) -> Option<RoundingBounds<'a>> {
34 let mut remaining = MAX_LOCAL_ANALYSIS_NODES;
35 self.rounding_bounds_cached(expr, anchor, &mut HashMap::default(), &mut remaining)
36 }
37
38 fn rounding_bounds_cached<'a>(
39 &self,
40 expr: &'a SymExpr,
41 anchor: Option<&SymExpr>,
42 intervals: &mut HashMap<SymExpr, Option<WordInterval>>,
43 remaining: &mut usize,
44 ) -> Option<RoundingBounds<'a>> {
45 let (dividend, divisor) = Self::rounded_product_operands(expr)?;
46 let width = divisor - U256::ONE;
49 let raw = RoundingBounds { anchor: dividend, below: width, above: U256::ZERO };
50 if anchor == Some(dividend) {
51 return Some(raw);
52 }
53 let offset = match dividend.kind() {
54 SymExprKind::BinOp(SymBinOp::Add, anchor, bias)
55 if let Some(bias) = bias.as_const()
56 && bias <= width =>
57 {
58 Some((anchor, bias, bias))
59 }
60 SymExprKind::BinOp(SymBinOp::Sub, sum, one)
61 if one.as_const() == Some(U256::ONE)
62 && let SymExprKind::BinOp(SymBinOp::Add, anchor, bias) = sum.kind()
63 && bias.as_const() == Some(divisor) =>
64 {
65 Some((anchor, width, divisor))
67 }
68 _ => None,
69 };
70 if let Some((anchor, bias, addition)) = offset
71 && self
72 .interval_cached(anchor, intervals, remaining)
73 .is_some_and(|range| range.max.checked_add(addition).is_some())
74 {
75 return Some(RoundingBounds { anchor, below: width - bias, above: bias });
77 }
78 Some(raw)
81 }
82
83 pub(super) fn quotient_of_rounded_product<'a>(&self, expr: &'a SymExpr) -> Option<&'a SymExpr> {
85 let (numerator, divisor) = expr.udiv_operands()?;
86 let bounds = self.rounding_bounds(numerator, None)?;
87 let minimum =
88 divisor.as_const().or_else(|| self.unsigned_lower_bounds.get(divisor).copied())?;
89 if !bounds.below.is_zero() || bounds.above >= minimum {
90 return None;
91 }
92 let SymExprKind::BinOp(SymBinOp::Mul, left, right) = bounds.anchor.kind() else {
93 return None;
94 };
95 let value = if left == divisor {
96 right
97 } else if right == divisor {
98 left
99 } else {
100 return None;
101 };
102 self.mul_cannot_overflow_256(value, divisor).then_some(value)
105 }
106
107 pub(super) fn rounding_comparison_value(&self, expr: &SymBoolExpr) -> Option<bool> {
108 if let SymBoolExprKind::Not(inner) = expr.kind() {
109 return self.rounding_comparison_value(inner).map(|value| !value);
110 }
111 let SymBoolExprKind::Cmp(op, left, right) = expr.kind() else { return None };
112 for (rounded, anchor, op) in [
113 (left, right, *op),
114 (
115 right,
116 left,
117 match op {
118 SymCmpOp::Ult => SymCmpOp::Ugt,
119 SymCmpOp::Ule => SymCmpOp::Uge,
120 SymCmpOp::Ugt => SymCmpOp::Ult,
121 SymCmpOp::Uge => SymCmpOp::Ule,
122 other => *other,
123 },
124 ),
125 ] {
126 if let Some(bounds) = self.rounding_bounds(rounded, Some(anchor))
127 && bounds.anchor == anchor
128 {
129 match op {
130 SymCmpOp::Uge if bounds.below.is_zero() => return Some(true),
131 SymCmpOp::Ult if bounds.below.is_zero() => return Some(false),
132 SymCmpOp::Ule if bounds.above.is_zero() => return Some(true),
133 SymCmpOp::Ugt if bounds.above.is_zero() => return Some(false),
134 _ => {}
135 }
136 }
137 }
138 None
139 }
140
141 pub(super) fn rounding_error_interval(
143 &self,
144 left: &SymExpr,
145 right: &SymExpr,
146 intervals: &mut HashMap<SymExpr, Option<WordInterval>>,
147 remaining: &mut usize,
148 ) -> Option<WordInterval> {
149 if let Some(bounds) = self.rounding_bounds_cached(left, Some(right), intervals, remaining)
150 && bounds.anchor == right
151 && bounds.below.is_zero()
152 {
153 return Some(WordInterval { min: U256::ZERO, max: bounds.above });
154 }
155 if let Some(bounds) = self.rounding_bounds_cached(right, Some(left), intervals, remaining)
156 && bounds.anchor == left
157 && bounds.above.is_zero()
158 {
159 return Some(WordInterval { min: U256::ZERO, max: bounds.below });
160 }
161 None
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::{
168 super::{
169 SymCx, SymbolicModel, normalize_constraints_for_solver,
170 normalize_constraints_for_solver_cached,
171 },
172 *,
173 };
174
175 fn rounded(cx: &mut SymCx, value: &SymExpr, divisor: U256, bias: U256, split: bool) -> SymExpr {
176 let divisor_expr = SymExpr::constant(cx, divisor);
177 let bias_expr = SymExpr::constant(cx, if split { bias + U256::ONE } else { bias });
178 let dividend = SymExpr::binop(cx, SymBinOp::Add, value.clone(), bias_expr);
179 let dividend = if split {
180 let one = SymExpr::one(cx);
181 SymExpr::binop(cx, SymBinOp::Sub, dividend, one)
182 } else {
183 dividend
184 };
185 let quotient = SymExpr::binop(cx, SymBinOp::UDiv, dividend, divisor_expr.clone());
186 SymExpr::binop(cx, SymBinOp::Mul, quotient, divisor_expr)
187 }
188
189 #[test]
190 fn ceiling_rounding_proves_order_and_error_for_both_addition_forms() {
191 let mut cx = SymCx::new();
192 let value = SymExpr::var(&mut cx, "value");
193 let bound =
194 SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Ule, &value, U256::MAX - U256::from(37));
195 let complement = SymExpr::not(&mut cx, value.clone());
196 let complement_bound =
197 SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Uge, &complement, U256::from(37));
198 for bound in [bound, complement_bound] {
199 for split in [false, true] {
200 let rounded = rounded(&mut cx, &value, U256::from(37), U256::from(36), split);
201 let error = SymExpr::binop(&mut cx, SymBinOp::Sub, rounded.clone(), value.clone());
202 let order = SymBoolExpr::cmp(&mut cx, SymCmpOp::Uge, rounded, value.clone());
203 let error_bound =
204 SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Ult, &error, U256::from(37));
205 for property in [order, error_bound] {
206 let failure = property.not(&mut cx);
207 let normalized =
208 normalize_constraints_for_solver(&mut cx, &[bound.clone(), failure]);
209 assert_eq!(normalized, vec![SymBoolExpr::constant(&mut cx, false)]);
210 }
211 }
212 }
213 }
214
215 #[test]
216 fn safe_offsets_preserve_dividend_order_and_remainder() {
217 let mut cx = SymCx::new();
218 let value = SymExpr::var(&mut cx, "value");
219 let safe =
220 SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Ule, &value, U256::MAX - U256::from(37));
221 let mut cache = HashMap::default();
222 for bias in [U256::ZERO, U256::from(18), U256::from(36)] {
223 for split in [false, true] {
224 let rounded = rounded(&mut cx, &value, U256::from(37), bias, split);
225 let (dividend, _) = ConstraintContext::rounded_product_operands(&rounded).unwrap();
226 let error =
227 SymExpr::binop(&mut cx, SymBinOp::Sub, dividend.clone(), rounded.clone());
228 let order =
229 SymBoolExpr::cmp(&mut cx, SymCmpOp::Ule, rounded.clone(), dividend.clone());
230 let remainder =
231 SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Ult, &error, U256::from(37));
232 for property in [order, remainder] {
233 for bounded in [true, false, true] {
234 let mut constraints = vec![property.clone().not(&mut cx)];
235 if bounded {
236 constraints.push(safe.clone());
237 }
238 for _ in 0..2 {
239 let normalized = normalize_constraints_for_solver_cached(
240 &mut cx,
241 &constraints,
242 &mut cache,
243 );
244 assert_eq!(
245 normalized,
246 vec![SymBoolExpr::constant(&mut cx, false)],
247 "bias={bias}, split={split}, bounded={bounded}"
248 );
249 constraints.reverse();
250 }
251 }
252 }
253 }
254 }
255 }
256
257 #[test]
258 fn rounding_relations_bound_biased_dividends_at_word_boundaries() {
259 let mut cx = SymCx::new();
260 let value = SymExpr::var(&mut cx, "value");
261 for divisor in [U256::from(3), U256::from(37), (U256::ONE << 255) - U256::ONE, U256::MAX] {
262 for bias in [U256::ZERO, divisor / U256::from(2), divisor - U256::ONE] {
263 let bound =
264 SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Ule, &value, U256::MAX - bias);
265 let context = ConstraintContext::new(&[bound]);
266 let rounded = rounded(&mut cx, &value, divisor, bias, false);
267 let relation = context.rounding_bounds(&rounded, None).unwrap_or_else(|| {
268 panic!(
269 "missing relation: divisor={divisor}, bias={bias}, expression={rounded:?}"
270 )
271 });
272 assert_eq!(relation.anchor, &value);
273 let samples = (0..=255).map(U256::from).chain([
274 U256::ONE << 255,
275 U256::MAX - bias,
276 U256::MAX,
277 ]);
278 for input in samples.filter(|input| *input <= U256::MAX - bias) {
279 let mut model = SymbolicModel::default();
280 assert!(value.assign_model_value(&mut model, input));
281 let output = rounded.eval_model(&model).unwrap();
282 if output >= input {
283 assert!(output - input <= relation.above);
284 } else {
285 assert!(input - output <= relation.below);
286 }
287 }
288 }
289 }
290 }
291
292 #[test]
293 fn rounding_normalization_preserves_wrapping_and_signed_comparisons() {
294 let mut cx = SymCx::new();
295 let value = SymExpr::var(&mut cx, "value");
296 let mut cache = HashMap::default();
297 let safe =
298 SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Ule, &value, U256::MAX - U256::from(37));
299 for bias in [U256::ZERO, U256::from(36)] {
300 for split in [false, true] {
301 let rounded = rounded(&mut cx, &value, U256::from(37), bias, split);
302 let error = SymExpr::binop(&mut cx, SymBinOp::Sub, rounded.clone(), value.clone());
303 for op in [
304 SymCmpOp::Eq,
305 SymCmpOp::Ult,
306 SymCmpOp::Ule,
307 SymCmpOp::Ugt,
308 SymCmpOp::Uge,
309 SymCmpOp::Slt,
310 SymCmpOp::Sgt,
311 ] {
312 let comparison = SymBoolExpr::cmp(&mut cx, op, rounded.clone(), value.clone());
313 let error_bound =
314 SymBoolExpr::cmp_word_const(&mut cx, op, &error, U256::from(37));
315 for predicate in [comparison, error_bound] {
316 for predicate in [predicate.clone(), predicate.not(&mut cx)] {
317 for bounded in [true, false, true] {
318 let mut constraints = vec![predicate.clone()];
319 if bounded {
320 constraints.push(safe.clone());
321 }
322 for _ in 0..2 {
323 let normalized = normalize_constraints_for_solver_cached(
324 &mut cx,
325 &constraints,
326 &mut cache,
327 );
328 for input in [
329 U256::ZERO,
330 U256::ONE,
331 U256::from(36),
332 U256::from(37),
333 (U256::ONE << 255) - U256::ONE,
334 U256::ONE << 255,
335 U256::MAX - U256::from(37),
336 U256::MAX - U256::ONE,
337 U256::MAX,
338 ] {
339 let mut model = SymbolicModel::default();
340 assert!(value.assign_model_value(&mut model, input));
341 assert_eq!(
342 constraints
343 .iter()
344 .all(|c| c.eval_model(&model).unwrap()),
345 normalized
346 .iter()
347 .all(|c| c.eval_model(&model).unwrap()),
348 "bias={bias}, split={split}, op={op:?}, bounded={bounded}, input={input}"
349 );
350 }
351 constraints.reverse();
352 }
353 }
354 }
355 }
356 }
357 }
358 }
359 }
360
361 #[test]
362 fn wrapped_dividend_bound_does_not_establish_rounding_safety() {
363 let mut cx = SymCx::new();
364 let value = SymExpr::var(&mut cx, "value");
365 let rounded = rounded(&mut cx, &value, U256::from(37), U256::from(36), false);
366 let (dividend, _) = ConstraintContext::rounded_product_operands(&rounded).unwrap();
367 let bound = SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Ule, dividend, U256::from(36));
368 let order = SymBoolExpr::cmp(&mut cx, SymCmpOp::Ult, rounded, value.clone());
369 let constraints = vec![bound, order];
370 let normalized = normalize_constraints_for_solver(&mut cx, &constraints);
371 let mut model = SymbolicModel::default();
372 assert!(value.assign_model_value(&mut model, U256::MAX));
373 assert!(constraints.iter().all(|c| c.eval_model(&model).unwrap()));
374 assert!(normalized.iter().all(|c| c.eval_model(&model).unwrap()));
375 }
376}