Skip to main content

foundry_evm_symbolic/runtime/solver/
normalize.rs

1use super::*;
2
3/// Normalizes path constraints into an equivalent, solver-friendlier form.
4pub(crate) fn normalize_constraints_for_solver(constraints: &[BoolExpr]) -> Vec<BoolExpr> {
5    let normalized = normalize_constraint_batch(
6        constraints.iter().cloned().map(normalize_bool_for_solver),
7        constraints.len(),
8    );
9    if matches!(normalized.as_slice(), [BoolExpr::Const(false)]) {
10        return normalized;
11    }
12
13    let context = ConstraintContext::new(&normalized);
14    let normalized_len = normalized.len();
15    normalize_constraint_batch(
16        normalized.into_iter().map(|constraint| context.normalize_bool(constraint)),
17        normalized_len,
18    )
19}
20
21fn normalize_constraint_batch(
22    constraints: impl IntoIterator<Item = BoolExpr>,
23    capacity: usize,
24) -> Vec<BoolExpr> {
25    let mut normalized = Vec::with_capacity(capacity);
26    for constraint in constraints {
27        if matches!(constraint, BoolExpr::Const(false)) {
28            return vec![BoolExpr::Const(false)];
29        }
30        collect_normalized_conjunct(constraint, &mut normalized);
31    }
32    normalized.sort();
33    normalized.dedup();
34    normalized
35}
36
37fn collect_normalized_conjunct(expr: BoolExpr, out: &mut Vec<BoolExpr>) {
38    match expr {
39        BoolExpr::Const(true) => {}
40        BoolExpr::And(values) => {
41            for value in values {
42                collect_normalized_conjunct(value, out);
43            }
44        }
45        value => out.push(value),
46    }
47}
48
49/// Normalizes one boolean expression into an equivalent, solver-friendlier form.
50pub(crate) fn normalize_bool_for_solver(expr: BoolExpr) -> BoolExpr {
51    if let Some(normalized) = normalize_udiv_bool_for_solver(&expr) {
52        return normalized;
53    }
54
55    match expr {
56        BoolExpr::Const(value) => BoolExpr::Const(value),
57        BoolExpr::Not(value) => normalize_bool_for_solver(*value).not(),
58        BoolExpr::And(values) => {
59            BoolExpr::and(values.into_iter().map(normalize_bool_for_solver).collect())
60        }
61        BoolExpr::Eq(left, right) => {
62            let normalized =
63                BoolExpr::Eq(normalize_expr_for_solver(left), normalize_expr_for_solver(right));
64            normalize_udiv_bool_for_solver(&normalized).unwrap_or_else(|| match normalized {
65                BoolExpr::Eq(left, right) => BoolExpr::eq(left, right),
66                _ => unreachable!(),
67            })
68        }
69        BoolExpr::Cmp(op, left, right) => {
70            let normalized = BoolExpr::Cmp(
71                op,
72                normalize_expr_for_solver(left),
73                normalize_expr_for_solver(right),
74            );
75            normalize_udiv_bool_for_solver(&normalized).unwrap_or(normalized)
76        }
77    }
78}
79
80/// Simple facts learned from the normalized conjunction currently being queried.
81#[derive(Default)]
82struct ConstraintContext {
83    upper_bounds: BTreeMap<Expr, U256>,
84}
85
86impl ConstraintContext {
87    fn new(constraints: &[BoolExpr]) -> Self {
88        let mut context = Self::default();
89        for constraint in constraints {
90            context.record_upper_bound_constraint(constraint);
91        }
92        context
93    }
94
95    fn upper_bound(&self, expr: &Expr) -> Option<U256> {
96        self.upper_bounds.get(expr).copied()
97    }
98
99    fn normalize_bool(&self, expr: BoolExpr) -> BoolExpr {
100        match &expr {
101            BoolExpr::Eq(left, Expr::Const(value)) | BoolExpr::Eq(Expr::Const(value), left)
102                if value.is_zero() && self.word_bool_always_true(left) =>
103            {
104                BoolExpr::Const(false)
105            }
106            BoolExpr::Not(value) => match value.as_ref() {
107                BoolExpr::Eq(left, Expr::Const(value)) | BoolExpr::Eq(Expr::Const(value), left)
108                    if value.is_zero() && self.word_bool_always_true(left) =>
109                {
110                    BoolExpr::Const(true)
111                }
112                _ => expr,
113            },
114            _ => expr,
115        }
116    }
117
118    fn record_upper_bound_constraint(&mut self, constraint: &BoolExpr) {
119        if let Some((expr, bound)) = self.upper_bound_constraint(constraint) {
120            self.record_upper_bound(expr.clone(), bound);
121        }
122    }
123
124    fn record_upper_bound(&mut self, expr: Expr, bound: U256) {
125        self.upper_bounds
126            .entry(expr)
127            .and_modify(|existing| *existing = (*existing).min(bound))
128            .or_insert(bound);
129    }
130
131    fn upper_bound_constraint<'a>(&self, constraint: &'a BoolExpr) -> Option<(&'a Expr, U256)> {
132        match constraint {
133            BoolExpr::Eq(left, Expr::Const(value)) | BoolExpr::Eq(Expr::Const(value), left) => {
134                Some((left, *value))
135            }
136            BoolExpr::Cmp(op, left, right) => match (*op, left, right) {
137                (BoolExprOp::Ult, expr, Expr::Const(bound)) => {
138                    (!bound.is_zero()).then(|| (expr, *bound - U256::from(1)))
139                }
140                (BoolExprOp::Ule, expr, Expr::Const(bound)) => Some((expr, *bound)),
141                (BoolExprOp::Ugt, Expr::Const(bound), expr) => {
142                    (!bound.is_zero()).then(|| (expr, *bound - U256::from(1)))
143                }
144                (BoolExprOp::Uge, Expr::Const(bound), expr) => Some((expr, *bound)),
145                _ => None,
146            },
147            BoolExpr::Not(value) => match value.as_ref() {
148                BoolExpr::Cmp(BoolExprOp::Ugt, left, Expr::Const(bound)) => Some((left, *bound)),
149                BoolExpr::Cmp(BoolExprOp::Uge, left, Expr::Const(bound)) => {
150                    (!bound.is_zero()).then(|| (left, *bound - U256::from(1)))
151                }
152                BoolExpr::Cmp(BoolExprOp::Ult, Expr::Const(bound), right) => Some((right, *bound)),
153                BoolExpr::Cmp(BoolExprOp::Ule, Expr::Const(bound), right) => {
154                    (!bound.is_zero()).then(|| (right, *bound - U256::from(1)))
155                }
156                _ => None,
157            },
158            BoolExpr::Eq(_, _) | BoolExpr::Const(_) | BoolExpr::And(_) => None,
159        }
160    }
161}
162
163/// Normalizes one word expression into an equivalent, solver-friendlier form.
164pub(crate) fn normalize_expr_for_solver(expr: Expr) -> Expr {
165    if let Some(rebuilt) = rebuild_word_from_extracted_byte_terms(&expr)
166        && rebuilt != expr
167    {
168        return normalize_expr_for_solver(rebuilt);
169    }
170
171    match expr {
172        Expr::Const(_)
173        | Expr::Var(_)
174        | Expr::GasLeft(_)
175        | Expr::Keccak { .. }
176        | Expr::Hash { .. } => expr,
177        Expr::Not(value) => Expr::Not(Box::new(normalize_expr_for_solver(*value))),
178        Expr::Op(op, left, right) => {
179            let left = normalize_expr_for_solver(*left);
180            let right = normalize_expr_for_solver(*right);
181            if matches!(op, ExprOp::Add | ExprOp::Mul | ExprOp::And | ExprOp::Or | ExprOp::Xor)
182                && right < left
183            {
184                Expr::op(op, right, left)
185            } else {
186                Expr::op(op, left, right)
187            }
188        }
189        Expr::Ite(cond, left, right) => normalize_ite_expr_for_solver(*cond, *left, *right),
190    }
191}
192
193/// Normalizes a word-valued conditional expression.
194pub(crate) fn normalize_ite_expr_for_solver(cond: BoolExpr, left: Expr, right: Expr) -> Expr {
195    let cond = normalize_bool_for_solver(cond);
196    let left = normalize_expr_for_solver(left);
197    let right = normalize_expr_for_solver(right);
198    if left == right {
199        return left;
200    }
201    if let Some(condition) = guarded_self_div_word_condition(&cond, &left, &right) {
202        return word_from_bool_expr(condition);
203    }
204    if matches!(left, Expr::Const(value) if value == U256::from(1))
205        && bool_from_word_expr(&right).as_ref() == Some(&cond)
206    {
207        return right;
208    }
209    if matches!(right, Expr::Const(value) if value.is_zero())
210        && bool_from_word_expr(&left).as_ref() == Some(&cond)
211    {
212        return left;
213    }
214    Expr::Ite(Box::new(cond), Box::new(left), Box::new(right))
215}
216
217/// Converts a boolean condition into its 0/1 word representation.
218fn word_from_bool_expr(condition: BoolExpr) -> Expr {
219    Expr::Ite(
220        Box::new(condition),
221        Box::new(Expr::Const(U256::from(1))),
222        Box::new(Expr::Const(U256::ZERO)),
223    )
224}
225
226/// Returns the boolean represented by `a == 0 ? 0 : a / a`.
227fn guarded_self_div_word_condition(cond: &BoolExpr, left: &Expr, right: &Expr) -> Option<BoolExpr> {
228    if matches!(left, Expr::Const(value) if value.is_zero())
229        && self_div_expr_matches_zero_check(cond, right)
230    {
231        return Some(cond.clone().not());
232    }
233    None
234}
235
236/// Returns whether `expr` is `a / a` for the operand guarded by `cond`.
237fn self_div_expr_matches_zero_check(cond: &BoolExpr, expr: &Expr) -> bool {
238    let Some(zero_operand) = zero_check_operand(cond) else { return false };
239    let Some((numerator, denominator)) = udiv_operands(expr) else { return false };
240    numerator == zero_operand && denominator == zero_operand
241}
242
243/// Rebuilds a word from OR-ed byte-extraction terms when the source is recoverable.
244pub(crate) fn rebuild_word_from_extracted_byte_terms(expr: &Expr) -> Option<Expr> {
245    let mut terms = Vec::new();
246    collect_or_terms(expr, &mut terms);
247    if terms.len() <= 1 {
248        return None;
249    }
250
251    let mut source = None;
252    let mut seen = [false; 32];
253    for term in terms {
254        if matches!(term, Expr::Const(value) if value.is_zero()) {
255            continue;
256        }
257        let (term_source, index) = extracted_shifted_byte_term(term)?;
258        match &source {
259            Some(source) if source != &term_source => return None,
260            Some(_) => {}
261            None => source = Some(term_source),
262        }
263        seen[index] = true;
264    }
265
266    let source = source?;
267    for (index, seen) in seen.into_iter().enumerate() {
268        if !seen && expr_known_byte(&source, index) != Some(0) {
269            return None;
270        }
271    }
272    Some(source)
273}
274
275/// Flattens nested bitwise-OR expressions into their leaf terms.
276pub(crate) fn collect_or_terms<'a>(expr: &'a Expr, terms: &mut Vec<&'a Expr>) {
277    match expr {
278        Expr::Op(ExprOp::Or, left, right) => {
279            collect_or_terms(left, terms);
280            collect_or_terms(right, terms);
281        }
282        expr => terms.push(expr),
283    }
284}
285
286/// Returns the source word and byte index for one shifted extracted-byte term.
287pub(crate) fn extracted_shifted_byte_term(term: &Expr) -> Option<(Expr, usize)> {
288    match term {
289        Expr::Op(ExprOp::Shl, byte, shift) => {
290            let Expr::Const(shift) = shift.as_ref() else { return None };
291            let shift = shift.to::<usize>();
292            if shift % 8 != 0 || shift > 248 {
293                return None;
294            }
295            let index = 31 - shift / 8;
296            let source = extracted_unshifted_byte_source(byte, index)?;
297            Some((source, index))
298        }
299        term => extracted_unshifted_byte_source(term, 31).map(|source| (source, 31)),
300    }
301}
302
303/// Returns the source word for an unshifted byte extraction at `index`.
304pub(crate) fn extracted_unshifted_byte_source(term: &Expr, index: usize) -> Option<Expr> {
305    let expr = strip_low_byte_mask(term)?;
306    if index == 31 {
307        return Some(expr.clone());
308    }
309    let Expr::Op(ExprOp::Shr, source, shift) = expr else { return None };
310    let Expr::Const(shift) = shift.as_ref() else { return None };
311    (*shift == U256::from((31 - index) * 8)).then(|| *source.clone())
312}
313
314/// Rewrites exact EVM unsigned-division zero/nonzero predicates without `bvudiv`.
315pub(crate) fn normalize_udiv_bool_for_solver(expr: &BoolExpr) -> Option<BoolExpr> {
316    match expr {
317        BoolExpr::Eq(left, Expr::Const(value)) if value.is_zero() => {
318            bool_from_word_expr(left).map(BoolExpr::not).or_else(|| {
319                if word_bool_always_true(left) {
320                    Some(BoolExpr::Const(false))
321                } else {
322                    normalize_udiv_eq_zero(left, &Expr::Const(U256::ZERO))
323                }
324            })
325        }
326        BoolExpr::Eq(Expr::Const(value), right) if value.is_zero() => {
327            bool_from_word_expr(right).map(BoolExpr::not).or_else(|| {
328                if word_bool_always_true(right) {
329                    Some(BoolExpr::Const(false))
330                } else {
331                    normalize_udiv_eq_zero(&Expr::Const(U256::ZERO), right)
332                }
333            })
334        }
335        BoolExpr::Eq(left, Expr::Const(value)) if *value == U256::from(1) => {
336            bool_from_word_expr(left)
337        }
338        BoolExpr::Eq(Expr::Const(value), right) if *value == U256::from(1) => {
339            bool_from_word_expr(right)
340        }
341        BoolExpr::Not(value) => match value.as_ref() {
342            BoolExpr::Cmp(op, left, right) => {
343                normalize_add_overflow_cmp_for_solver(*op, left, right)
344                    .map(BoolExpr::not)
345                    .or_else(|| normalize_udiv_cmp_for_solver(*op, left, right).map(BoolExpr::not))
346            }
347            BoolExpr::Eq(left, Expr::Const(value)) if value.is_zero() => {
348                if word_bool_always_true(left) {
349                    Some(BoolExpr::Const(true))
350                } else {
351                    normalize_udiv_eq_zero(left, &Expr::Const(U256::ZERO)).map(BoolExpr::not)
352                }
353            }
354            BoolExpr::Eq(Expr::Const(value), right) if value.is_zero() => {
355                if word_bool_always_true(right) {
356                    Some(BoolExpr::Const(true))
357                } else {
358                    normalize_udiv_eq_zero(&Expr::Const(U256::ZERO), right).map(BoolExpr::not)
359                }
360            }
361            BoolExpr::Eq(left, right) => normalize_udiv_eq_zero(left, right).map(BoolExpr::not),
362            _ => None,
363        },
364        BoolExpr::Eq(left, right) => normalize_udiv_eq_zero(left, right),
365        BoolExpr::Cmp(op, left, right) => normalize_add_overflow_cmp_for_solver(*op, left, right)
366            .or_else(|| normalize_udiv_cmp_for_solver(*op, left, right)),
367        BoolExpr::Const(_) | BoolExpr::And(_) => None,
368    }
369}
370
371/// Extracts the boolean condition represented by a word-valued `0`/`1` expression.
372pub(crate) fn bool_from_word_expr(expr: &Expr) -> Option<BoolExpr> {
373    let expr = strip_low_byte_mask(expr)?;
374    let Expr::Ite(condition, then_expr, else_expr) = expr else { return None };
375    match (then_expr.as_ref(), else_expr.as_ref()) {
376        (Expr::Const(then_value), Expr::Const(else_value))
377            if *then_value == U256::from(1) && else_value.is_zero() =>
378        {
379            Some(normalize_bool_for_solver((**condition).clone()))
380        }
381        (Expr::Const(then_value), Expr::Const(else_value))
382            if then_value.is_zero() && *else_value == U256::from(1) =>
383        {
384            Some(normalize_bool_for_solver((**condition).clone()).not())
385        }
386        _ => None,
387    }
388}
389
390/// Returns whether a word expression syntactically contains unsigned division.
391pub(crate) fn expr_contains_udiv(expr: &Expr) -> bool {
392    match expr {
393        Expr::Const(_) | Expr::Var(_) | Expr::GasLeft(_) => false,
394        Expr::Keccak { len, bytes, .. } => {
395            expr_contains_udiv(len) || bytes.iter().any(expr_contains_udiv)
396        }
397        Expr::Hash { bytes, .. } => bytes.iter().any(expr_contains_udiv),
398        Expr::Not(value) => expr_contains_udiv(value),
399        Expr::Op(op, left, right) => {
400            matches!(op, ExprOp::UDiv) || expr_contains_udiv(left) || expr_contains_udiv(right)
401        }
402        Expr::Ite(condition, then_expr, else_expr) => {
403            bool_contains_udiv(condition)
404                || expr_contains_udiv(then_expr)
405                || expr_contains_udiv(else_expr)
406        }
407    }
408}
409
410/// Returns whether a boolean expression syntactically contains unsigned division.
411pub(crate) fn bool_contains_udiv(expr: &BoolExpr) -> bool {
412    match expr {
413        BoolExpr::Const(_) => false,
414        BoolExpr::Not(value) => bool_contains_udiv(value),
415        BoolExpr::And(values) => values.iter().any(bool_contains_udiv),
416        BoolExpr::Eq(left, right) | BoolExpr::Cmp(_, left, right) => {
417            expr_contains_udiv(left) || expr_contains_udiv(right)
418        }
419    }
420}
421
422/// Rewrites exact unsigned-addition overflow checks when operand bounds preclude overflow.
423pub(crate) fn normalize_add_overflow_cmp_for_solver(
424    op: BoolExprOp,
425    left: &Expr,
426    right: &Expr,
427) -> Option<BoolExpr> {
428    match op {
429        BoolExprOp::Ugt if add_overflow_check(left, right) => Some(BoolExpr::Const(false)),
430        BoolExprOp::Ult if add_overflow_check(right, left) => Some(BoolExpr::Const(false)),
431        _ => None,
432    }
433}
434
435/// Returns whether `left > left + increment` is an impossible overflow check.
436pub(crate) fn add_overflow_check(left: &Expr, right: &Expr) -> bool {
437    let Some((base, increment)) = add_with_operand(right, left) else { return false };
438    base == left && add_cannot_overflow_256(base, increment)
439}
440
441/// Returns the operands if `expr` is an addition involving `operand`.
442pub(crate) fn add_with_operand<'a>(expr: &'a Expr, operand: &Expr) -> Option<(&'a Expr, &'a Expr)> {
443    let Expr::Op(ExprOp::Add, left, right) = expr else { return None };
444    if left.as_ref() == operand {
445        Some((left, right))
446    } else if right.as_ref() == operand {
447        Some((right, left))
448    } else {
449        None
450    }
451}
452
453/// Returns whether unsigned addition of two expressions cannot overflow 256 bits.
454pub(crate) fn add_cannot_overflow_256(left: &Expr, right: &Expr) -> bool {
455    expr_unsigned_bits(left).max(expr_unsigned_bits(right)).saturating_add(1) <= 256
456}
457
458/// Returns whether a word-valued boolean expression is an exact tautology.
459pub(crate) fn word_bool_always_true(expr: &Expr) -> bool {
460    ConstraintContext::default().word_bool_always_true(expr)
461}
462
463/// Converts one `0`/`1` word boolean term into its boolean condition.
464pub(crate) fn word_bool_term(expr: &Expr) -> Option<&BoolExpr> {
465    let Expr::Ite(condition, then_expr, else_expr) = expr else { return None };
466    match (then_expr.as_ref(), else_expr.as_ref()) {
467        (Expr::Const(then_value), Expr::Const(else_value))
468            if *then_value == U256::from(1) && else_value.is_zero() =>
469        {
470            Some(condition)
471        }
472        _ => None,
473    }
474}
475
476/// Returns the operand tested by `operand == 0`.
477pub(crate) fn zero_check_operand(expr: &BoolExpr) -> Option<&Expr> {
478    match expr {
479        BoolExpr::Eq(left, Expr::Const(value)) if value.is_zero() => Some(left),
480        BoolExpr::Eq(Expr::Const(value), right) if value.is_zero() => Some(right),
481        _ => None,
482    }
483}
484
485impl ConstraintContext {
486    fn word_bool_always_true(&self, expr: &Expr) -> bool {
487        let mut terms = Vec::new();
488        collect_or_terms(expr, &mut terms);
489        if terms.len() <= 1 {
490            return false;
491        }
492
493        let bool_terms = terms.iter().filter_map(|term| word_bool_term(term)).collect::<Vec<_>>();
494        if bool_terms.iter().any(|term| {
495            let negated = (*term).clone().not();
496            bool_terms.iter().any(|other| **other == negated)
497        }) {
498            return true;
499        }
500        for zero_term in &bool_terms {
501            let Some(zero_operand) = zero_check_operand(zero_term) else { continue };
502            if bool_terms.iter().any(|term| self.checked_mul_guard_for_operand(term, zero_operand))
503            {
504                return true;
505            }
506        }
507        false
508    }
509
510    fn checked_mul_guard_for_operand(&self, expr: &BoolExpr, zero_operand: &Expr) -> bool {
511        let BoolExpr::Eq(left, right) = expr else { return false };
512        self.checked_mul_guard_side(left, right, zero_operand)
513            || self.checked_mul_guard_side(right, left, zero_operand)
514    }
515
516    fn checked_mul_guard_side(
517        &self,
518        div_expr: &Expr,
519        expected: &Expr,
520        zero_operand: &Expr,
521    ) -> bool {
522        let Expr::Ite(condition, then_expr, else_expr) = div_expr else { return false };
523        if zero_check_operand(condition).is_none_or(|operand| operand != zero_operand) {
524            return false;
525        }
526        if !matches!(then_expr.as_ref(), Expr::Const(value) if value.is_zero()) {
527            return false;
528        }
529        let Some((numerator, denominator)) = udiv_operands(else_expr) else { return false };
530        if denominator != zero_operand {
531            return false;
532        }
533        let Expr::Op(ExprOp::Mul, left, right) = numerator else { return false };
534        let other = if left.as_ref() == zero_operand {
535            right.as_ref()
536        } else if right.as_ref() == zero_operand {
537            left.as_ref()
538        } else {
539            return false;
540        };
541        other == expected && self.mul_cannot_overflow_256(zero_operand, other)
542    }
543
544    fn mul_cannot_overflow_256(&self, left: &Expr, right: &Expr) -> bool {
545        self.expr_unsigned_bits(left).saturating_add(self.expr_unsigned_bits(right)) <= 256
546    }
547
548    fn expr_unsigned_bits(&self, expr: &Expr) -> usize {
549        let bits = match expr {
550            Expr::Const(_)
551            | Expr::Var(_)
552            | Expr::GasLeft(_)
553            | Expr::Keccak { .. }
554            | Expr::Hash { .. }
555            | Expr::Not(_) => expr_unsigned_bits(expr),
556            Expr::Op(ExprOp::And, left, right) => match (left.as_ref(), right.as_ref()) {
557                (expr, Expr::Const(mask)) | (Expr::Const(mask), expr) => {
558                    self.expr_unsigned_bits(expr).min(mask.bit_len())
559                }
560                _ => 256,
561            },
562            Expr::Op(ExprOp::Add, left, right) => self
563                .expr_unsigned_bits(left)
564                .max(self.expr_unsigned_bits(right))
565                .saturating_add(1)
566                .min(256),
567            Expr::Op(ExprOp::Mul, left, right) => self
568                .expr_unsigned_bits(left)
569                .saturating_add(self.expr_unsigned_bits(right))
570                .min(256),
571            Expr::Op(ExprOp::UDiv, left, _) => self.expr_unsigned_bits(left),
572            Expr::Ite(_, left, right) => {
573                self.expr_unsigned_bits(left).max(self.expr_unsigned_bits(right))
574            }
575            _ => 256,
576        };
577
578        self.upper_bound(expr).map(|bound| bits.min(bound.bit_len().max(1))).unwrap_or(bits)
579    }
580}
581
582/// Returns whether unsigned multiplication of two expressions cannot overflow 256 bits.
583pub(crate) fn mul_cannot_overflow_256(left: &Expr, right: &Expr) -> bool {
584    expr_unsigned_bits(left).saturating_add(expr_unsigned_bits(right)) <= 256
585}
586
587/// Returns a conservative unsigned bit-width upper bound for an expression.
588pub(crate) fn expr_unsigned_bits(expr: &Expr) -> usize {
589    match expr {
590        Expr::Const(value) => value.bit_len().max(1),
591        Expr::Op(ExprOp::And, left, right) => match (left.as_ref(), right.as_ref()) {
592            (expr, Expr::Const(mask)) | (Expr::Const(mask), expr) => {
593                expr_unsigned_bits(expr).min(mask.bit_len())
594            }
595            _ => 256,
596        },
597        Expr::Op(ExprOp::Add, left, right) => {
598            expr_unsigned_bits(left).max(expr_unsigned_bits(right)).saturating_add(1).min(256)
599        }
600        Expr::Op(ExprOp::Mul, left, right) => {
601            expr_unsigned_bits(left).saturating_add(expr_unsigned_bits(right)).min(256)
602        }
603        Expr::Op(ExprOp::UDiv, left, _) => expr_unsigned_bits(left),
604        Expr::Ite(_, left, right) => expr_unsigned_bits(left).max(expr_unsigned_bits(right)),
605        _ => 256,
606    }
607}
608
609/// Rewrites `udiv(a, b) == 0` predicates using EVM division-by-zero semantics.
610pub(crate) fn normalize_udiv_eq_zero(left: &Expr, right: &Expr) -> Option<BoolExpr> {
611    if matches!(right, Expr::Const(value) if value.is_zero())
612        && let Some(condition) = normalize_expr_eq_zero_for_solver(left)
613    {
614        return Some(condition);
615    }
616    if matches!(left, Expr::Const(value) if value.is_zero())
617        && let Some(condition) = normalize_expr_eq_zero_for_solver(right)
618    {
619        return Some(condition);
620    }
621    None
622}
623
624/// Rewrites `expr == 0` when `expr` contains exactly-normalizable unsigned division.
625pub(crate) fn normalize_expr_eq_zero_for_solver(expr: &Expr) -> Option<BoolExpr> {
626    if let Some((numerator, denominator)) = udiv_operands(expr) {
627        return Some(udiv_zero_condition(numerator, denominator));
628    }
629    if let Expr::Ite(condition, then_expr, else_expr) = expr {
630        let then_zero = normalize_expr_eq_zero_for_solver(then_expr).unwrap_or_else(|| {
631            BoolExpr::eq(normalize_expr_for_solver((**then_expr).clone()), Expr::Const(U256::ZERO))
632        });
633        let else_zero = normalize_expr_eq_zero_for_solver(else_expr).unwrap_or_else(|| {
634            BoolExpr::eq(normalize_expr_for_solver((**else_expr).clone()), Expr::Const(U256::ZERO))
635        });
636        if bool_contains_udiv(&then_zero) || bool_contains_udiv(&else_zero) {
637            return None;
638        }
639        return Some(BoolExpr::or(vec![
640            BoolExpr::and(vec![normalize_bool_for_solver((**condition).clone()), then_zero]),
641            BoolExpr::and(vec![normalize_bool_for_solver((**condition).clone()).not(), else_zero]),
642        ]));
643    }
644    None
645}
646
647/// Rewrites `expr != 0` when `expr` contains exactly-normalizable unsigned division.
648pub(crate) fn normalize_expr_ne_zero_for_solver(expr: &Expr) -> Option<BoolExpr> {
649    if let Some((numerator, denominator)) = udiv_operands(expr) {
650        return Some(udiv_nonzero_condition(numerator, denominator));
651    }
652    if let Expr::Ite(condition, then_expr, else_expr) = expr {
653        let then_nonzero = normalize_expr_ne_zero_for_solver(then_expr).unwrap_or_else(|| {
654            BoolExpr::eq(normalize_expr_for_solver((**then_expr).clone()), Expr::Const(U256::ZERO))
655                .not()
656        });
657        let else_nonzero = normalize_expr_ne_zero_for_solver(else_expr).unwrap_or_else(|| {
658            BoolExpr::eq(normalize_expr_for_solver((**else_expr).clone()), Expr::Const(U256::ZERO))
659                .not()
660        });
661        if bool_contains_udiv(&then_nonzero) || bool_contains_udiv(&else_nonzero) {
662            return None;
663        }
664        return Some(BoolExpr::or(vec![
665            BoolExpr::and(vec![normalize_bool_for_solver((**condition).clone()), then_nonzero]),
666            BoolExpr::and(vec![
667                normalize_bool_for_solver((**condition).clone()).not(),
668                else_nonzero,
669            ]),
670        ]));
671    }
672    None
673}
674
675/// Rewrites `udiv(a, b)` zero/nonzero comparisons using EVM division-by-zero semantics.
676pub(crate) fn normalize_udiv_cmp_for_solver(
677    op: BoolExprOp,
678    left: &Expr,
679    right: &Expr,
680) -> Option<BoolExpr> {
681    match (op, left, right) {
682        (BoolExprOp::Ugt, div, Expr::Const(value)) if value.is_zero() => {
683            normalize_expr_ne_zero_for_solver(div)
684        }
685        (BoolExprOp::Uge, div, Expr::Const(value)) if *value == U256::from(1) => {
686            normalize_expr_ne_zero_for_solver(div)
687        }
688        (BoolExprOp::Ule, div, Expr::Const(value)) if value.is_zero() => {
689            normalize_expr_eq_zero_for_solver(div)
690        }
691        (BoolExprOp::Ult, div, Expr::Const(value)) if *value == U256::from(1) => {
692            normalize_expr_eq_zero_for_solver(div)
693        }
694        (BoolExprOp::Ult, Expr::Const(value), div) if value.is_zero() => {
695            normalize_expr_ne_zero_for_solver(div)
696        }
697        (BoolExprOp::Ule, Expr::Const(value), div) if *value == U256::from(1) => {
698            normalize_expr_ne_zero_for_solver(div)
699        }
700        (BoolExprOp::Uge, Expr::Const(value), div) if value.is_zero() => {
701            normalize_expr_eq_zero_for_solver(div)
702        }
703        (BoolExprOp::Ugt, Expr::Const(value), div) if *value == U256::from(1) => {
704            normalize_expr_eq_zero_for_solver(div)
705        }
706        _ => None,
707    }
708}
709
710/// Returns the operands for an unsigned division expression.
711pub(crate) fn udiv_operands(expr: &Expr) -> Option<(&Expr, &Expr)> {
712    match expr {
713        Expr::Op(ExprOp::UDiv, numerator, denominator) => Some((numerator, denominator)),
714        _ => None,
715    }
716}
717
718/// Builds the exact condition for EVM `udiv(numerator, denominator) == 0`.
719pub(crate) fn udiv_zero_condition(numerator: &Expr, denominator: &Expr) -> BoolExpr {
720    BoolExpr::or(vec![
721        BoolExpr::eq(normalize_expr_for_solver(denominator.clone()), Expr::Const(U256::ZERO)),
722        BoolExpr::cmp(
723            BoolExprOp::Ult,
724            normalize_expr_for_solver(numerator.clone()),
725            normalize_expr_for_solver(denominator.clone()),
726        ),
727    ])
728}
729
730/// Builds the exact condition for EVM `udiv(numerator, denominator) != 0`.
731pub(crate) fn udiv_nonzero_condition(numerator: &Expr, denominator: &Expr) -> BoolExpr {
732    BoolExpr::and(vec![
733        BoolExpr::eq(normalize_expr_for_solver(denominator.clone()), Expr::Const(U256::ZERO)).not(),
734        BoolExpr::cmp(
735            BoolExprOp::Uge,
736            normalize_expr_for_solver(numerator.clone()),
737            normalize_expr_for_solver(denominator.clone()),
738        ),
739    ])
740}