Skip to main content

foundry_evm_symbolic/runtime/solver/normalize/
mod.rs

1//! Constraint and expression normalization for solver queries.
2
3use super::*;
4
5mod polynomial;
6mod rounding;
7
8use polynomial::polynomial_identity;
9
10/// Reuses context-free normalization results while retaining per-query contextual rewrites.
11pub(super) fn normalize_constraints_for_solver_cached(
12    cx: &mut SymCx,
13    constraints: &[SymBoolExpr],
14    normalization_cache: &mut HashMap<SymBoolExpr, SymBoolExpr>,
15) -> Vec<SymBoolExpr> {
16    normalize_constraints_for_solver_with(cx, constraints, |cx, constraint| {
17        if let Some(normalized) = normalization_cache.get(constraint) {
18            return normalized.clone();
19        }
20        let normalized = normalize_bool_for_solver(cx, constraint.clone());
21        // These are strong hash-consed handles, so bound their lifetime like the SAT cache.
22        if normalization_cache.len() < SYMBOLIC_SOLVER_SAT_CACHE_MAX_ENTRIES {
23            normalization_cache.insert(constraint.clone(), normalized.clone());
24        }
25        normalized
26    })
27}
28
29fn normalize_constraints_for_solver_with(
30    cx: &mut SymCx,
31    constraints: &[SymBoolExpr],
32    mut normalize: impl FnMut(&mut SymCx, &SymBoolExpr) -> SymBoolExpr,
33) -> Vec<SymBoolExpr> {
34    let mut changed_conjuncts = HashSet::default();
35    let normalized = normalize_constraint_batch(
36        constraints.iter().map(|constraint| {
37            let normalized = normalize(cx, constraint);
38            if normalized != *constraint {
39                mark_conjuncts(&normalized, &mut changed_conjuncts);
40            }
41            normalized
42        }),
43        constraints.len(),
44    );
45    if matches!(normalized.as_slice(), [expr] if expr.as_const() == Some(false)) {
46        return normalized;
47    }
48
49    // Context-dependent rewrites must not contribute facts to the context that proves them. Mark
50    // candidates by syntax rather than by whether the full context happens to prove a rewrite:
51    // contradictory bounds can make an interval unavailable until another candidate is removed.
52    let retained_count = normalized
53        .iter()
54        .filter(|constraint| !ConstraintContext::requires_independent_context(constraint))
55        .count();
56    let retained = normalized
57        .iter()
58        .filter(|constraint| !ConstraintContext::requires_independent_context(constraint));
59    let context =
60        ConstraintContext::from_constraints_with_lower_bounds(retained, retained_count, false);
61    let normalized_len = normalized.len();
62    let normalized = normalize_constraint_batch(
63        normalized.into_iter().map(|constraint| {
64            let changed = changed_conjuncts.contains(&constraint);
65            context.normalize_bool(cx, constraint, changed)
66        }),
67        normalized_len,
68    );
69    normalize_bounded_comparisons(cx, normalized)
70}
71
72/// Simplifies predicates using only the other, still-retained conjuncts.
73fn normalize_bounded_comparisons(
74    cx: &mut SymCx,
75    mut constraints: Vec<SymBoolExpr>,
76) -> Vec<SymBoolExpr> {
77    // Later predicates can expose guards needed by earlier ones. Revisit the retained
78    // conjunction, but bound the work; unfinished simplification is still sound SMT input.
79    for _ in 0..MAX_CONTEXTUAL_PASSES {
80        let previous = constraints.clone();
81        let mut index = 0;
82        while index < constraints.len() {
83            let context = ConstraintContext::for_rewrite(cx, &constraints, index);
84            constraints[index] = context.normalize_bool(cx, constraints[index].clone(), false);
85            // Revisit each newly exposed conjunct with the retained supporting facts. Otherwise a
86            // division rewrite can leave a simple contradiction hidden until the SMT fallback.
87            if let SymBoolExprKind::And(terms) = constraints[index].kind() {
88                let terms = terms.to_vec();
89                constraints.splice(index..=index, terms);
90                continue;
91            }
92            match context.bounded_bool_value(&constraints[index]) {
93                Some(false) => return vec![SymBoolExpr::constant(cx, false)],
94                Some(true) => {
95                    constraints.remove(index);
96                }
97                None => index += 1,
98            }
99        }
100        // Contextual rewrites may change sort order or expose a conjunction.
101        let count = constraints.len();
102        constraints = normalize_constraint_batch(constraints, count);
103        if constraints == previous || constraints.iter().any(|c| c.as_const() == Some(false)) {
104            break;
105        }
106    }
107    constraints
108}
109
110fn mark_conjuncts(expr: &SymBoolExpr, out: &mut HashSet<SymBoolExpr>) {
111    let mut pending = vec![expr.clone()];
112    while let Some(expr) = pending.pop() {
113        if !out.insert(expr.clone()) {
114            continue;
115        }
116        if let SymBoolExprKind::And(values) = expr.kind() {
117            pending.extend(values.iter().cloned());
118        }
119    }
120}
121
122fn normalize_constraint_batch(
123    constraints: impl IntoIterator<Item = SymBoolExpr>,
124    capacity: usize,
125) -> Vec<SymBoolExpr> {
126    let mut normalized = Vec::with_capacity(capacity);
127    for constraint in constraints {
128        if constraint.as_const() == Some(false) {
129            return vec![constraint];
130        }
131        constraint.push_normalized_conjuncts(&mut normalized);
132    }
133    sort_dedup_bool_exprs(&mut normalized);
134    normalized
135}
136
137fn sort_dedup_bool_exprs(exprs: &mut Vec<SymBoolExpr>) {
138    // Hash-consing already caches deterministic structural hashes. Only render full structural
139    // keys for the exceedingly rare case where two distinct expressions collide.
140    exprs.sort_unstable_by(bool_expr_cmp);
141    exprs.dedup();
142}
143
144fn bool_expr_cmp(left: &SymBoolExpr, right: &SymBoolExpr) -> std::cmp::Ordering {
145    if left == right {
146        return std::cmp::Ordering::Equal;
147    }
148    left.stable_hash_cmp(right)
149        .then_with(|| bool_structural_key(left).cmp(&bool_structural_key(right)))
150}
151
152fn bool_structural_key(expr: &SymBoolExpr) -> String {
153    let mut key = String::new();
154    write_bool_structural_key(&mut key, expr);
155    key
156}
157
158fn write_bool_structural_key(out: &mut String, expr: &SymBoolExpr) {
159    match expr.kind() {
160        SymBoolExprKind::Const(value) => {
161            let _ = write!(out, "0:{value}");
162        }
163        SymBoolExprKind::Not(value) => {
164            out.push_str("1:");
165            write_bool_structural_key(out, value);
166        }
167        SymBoolExprKind::And(values) => {
168            let _ = write!(out, "2:{}:", values.len());
169            for value in values.iter() {
170                write_bool_structural_key(out, value);
171                out.push(';');
172            }
173        }
174        SymBoolExprKind::Cmp(op, left, right) => {
175            let _ = write!(out, "3:{}:", cmp_op_key(*op));
176            write_expr_structural_key(out, left);
177            out.push(':');
178            write_expr_structural_key(out, right);
179        }
180    }
181}
182
183fn write_expr_structural_key(out: &mut String, expr: &SymExpr) {
184    match expr.kind() {
185        SymExprKind::Const(value) => {
186            let _ = write!(out, "0:{value:064x}");
187        }
188        SymExprKind::Var(name) => {
189            let _ = write!(out, "1:{}", name.id());
190        }
191        SymExprKind::GasLeft(symbol) => {
192            let _ = write!(out, "2:{}", symbol.id());
193        }
194        SymExprKind::Keccak { name, len, bytes } => {
195            let _ = write!(out, "3:{}:", name.id());
196            write_expr_structural_key(out, len);
197            write_exprs_structural_key(out, bytes);
198        }
199        SymExprKind::Hash { name, algorithm, bytes } => {
200            let _ = write!(out, "4:{}:{algorithm}:", name.id());
201            write_exprs_structural_key(out, bytes);
202        }
203        SymExprKind::Not(value) => {
204            out.push_str("5:");
205            write_expr_structural_key(out, value);
206        }
207        SymExprKind::BinOp(op, left, right) => {
208            let _ = write!(out, "6:{}:", expr_binop_key(*op));
209            write_expr_structural_key(out, left);
210            out.push(':');
211            write_expr_structural_key(out, right);
212        }
213        SymExprKind::TernOp(op, left, right, modulus) => {
214            let _ = write!(out, "7:{}:", expr_ternop_key(*op));
215            write_expr_structural_key(out, left);
216            out.push(':');
217            write_expr_structural_key(out, right);
218            out.push(':');
219            write_expr_structural_key(out, modulus);
220        }
221        SymExprKind::Ite(condition, then_expr, else_expr) => {
222            out.push_str("9:");
223            write_bool_structural_key(out, condition);
224            out.push(':');
225            write_expr_structural_key(out, then_expr);
226            out.push(':');
227            write_expr_structural_key(out, else_expr);
228        }
229    }
230}
231
232fn write_exprs_structural_key(out: &mut String, exprs: &[SymExpr]) {
233    let _ = write!(out, "{}:", exprs.len());
234    for expr in exprs {
235        write_expr_structural_key(out, expr);
236        out.push(';');
237    }
238}
239
240const fn cmp_op_key(op: SymCmpOp) -> u8 {
241    match op {
242        SymCmpOp::Eq => 0,
243        SymCmpOp::Ult => 1,
244        SymCmpOp::Ugt => 2,
245        SymCmpOp::Ule => 3,
246        SymCmpOp::Uge => 4,
247        SymCmpOp::Slt => 5,
248        SymCmpOp::Sgt => 6,
249    }
250}
251
252const fn expr_binop_key(op: SymBinOp) -> u8 {
253    match op {
254        SymBinOp::Add => 0,
255        SymBinOp::Sub => 1,
256        SymBinOp::Mul => 2,
257        SymBinOp::UDiv => 3,
258        SymBinOp::URem => 4,
259        SymBinOp::SDiv => 5,
260        SymBinOp::SRem => 6,
261        SymBinOp::And => 7,
262        SymBinOp::Or => 8,
263        SymBinOp::Xor => 9,
264        SymBinOp::Shl => 10,
265        SymBinOp::Shr => 11,
266        SymBinOp::Sar => 12,
267    }
268}
269
270const fn expr_ternop_key(op: SymTernOp) -> u8 {
271    match op {
272        SymTernOp::AddMod => 0,
273        SymTernOp::MulMod => 1,
274    }
275}
276
277/// Returns whether canonically ordered normalized constraints contain a direct contradiction.
278pub(super) fn constraints_are_directly_unsat(cx: &mut SymCx, constraints: &[SymBoolExpr]) -> bool {
279    let mut derived = Vec::new();
280    for constraint in constraints {
281        let Some(fact) = bitwise_bool_word_fact(cx, constraint) else {
282            continue;
283        };
284        if let SymBoolExprKind::And(values) = fact.kind() {
285            // A positive conjunction implies each member independently. Retain the aggregate for
286            // exact matches, but expose its members to the direct contradiction check as well.
287            derived.extend(values.iter().cloned());
288        }
289        derived.push(fact);
290    }
291    let contains = |expected: &SymBoolExpr| {
292        constraints.binary_search_by(|candidate| bool_expr_cmp(candidate, expected)).is_ok()
293            || derived.contains(expected)
294    };
295    constraints.iter().chain(&derived).any(|constraint| match constraint.kind() {
296        SymBoolExprKind::Const(false) => true,
297        SymBoolExprKind::Not(inner)
298            if let SymBoolExprKind::And(values) = inner.kind()
299                && values.iter().all(&contains) =>
300        {
301            true
302        }
303        SymBoolExprKind::Not(inner) => contains(inner),
304        _ => {
305            let negated = constraint.clone().not(cx);
306            contains(&negated)
307        }
308    })
309}
310
311fn bitwise_bool_word_fact(cx: &mut SymCx, constraint: &SymBoolExpr) -> Option<SymBoolExpr> {
312    match constraint.kind() {
313        SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right)
314            if right.as_const().is_some_and(|value| value.is_zero()) =>
315        {
316            left.bitwise_bool_word_condition(cx).map(|condition| condition.not(cx))
317        }
318        SymBoolExprKind::Not(inner) => {
319            let SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) = inner.kind() else {
320                return None;
321            };
322            if !right.as_const().is_some_and(|value| value.is_zero()) {
323                return None;
324            }
325            left.bitwise_bool_word_condition(cx)
326        }
327        _ => None,
328    }
329}
330
331/// Returns whether every expression in `subset` appears in `superset`.
332pub(super) fn sorted_bool_exprs_are_subset(
333    subset: &[SymBoolExpr],
334    superset: &[SymBoolExpr],
335) -> bool {
336    if subset.len() > superset.len() {
337        return false;
338    }
339
340    let superset: HashSet<_> = superset.iter().collect();
341    subset.iter().all(|expected| superset.contains(expected))
342}
343
344/// Normalizes one boolean expression into an equivalent, solver-friendlier form.
345pub(crate) fn normalize_bool_for_solver(cx: &mut SymCx, expr: SymBoolExpr) -> SymBoolExpr {
346    expr.fold(cx, &mut normalize_bool_node_for_solver)
347}
348
349impl SymBoolExpr {
350    fn push_normalized_conjuncts(self, out: &mut Vec<Self>) {
351        match self.kind() {
352            SymBoolExprKind::Const(true) => {}
353            SymBoolExprKind::And(values) => {
354                for value in values.iter().cloned() {
355                    value.push_normalized_conjuncts(out);
356                }
357            }
358            _ => out.push(self),
359        }
360    }
361}
362
363fn normalize_bool_node_for_solver(cx: &mut SymCx, expr: SymBoolExpr) -> SymBoolExpr {
364    if let Some(normalized) = expr.normalize_udiv_for_solver(cx) {
365        return normalized;
366    }
367
368    match expr.kind() {
369        SymBoolExprKind::Not(value) => match value.kind() {
370            SymBoolExprKind::Cmp(SymCmpOp::Ult, left, right)
371                if matches!(left.kind(), SymExprKind::Not(_)) =>
372            {
373                normalize_cmp_for_solver(cx, SymCmpOp::Ule, right.clone(), left.clone())
374            }
375            _ => expr,
376        },
377        SymBoolExprKind::Cmp(op, left, right) => {
378            let left = normalize_expr_for_solver(cx, left.clone());
379            let right = normalize_expr_for_solver(cx, right.clone());
380            if *op == SymCmpOp::Eq && polynomial_identity(&left, &right) {
381                return SymBoolExpr::constant(cx, true);
382            }
383            let normalized = normalize_cmp_for_solver(cx, *op, left, right);
384            normalized.normalize_udiv_for_solver(cx).unwrap_or(normalized)
385        }
386        _ => expr,
387    }
388}
389
390fn normalize_cmp_for_solver(
391    cx: &mut SymCx,
392    op: SymCmpOp,
393    left: SymExpr,
394    right: SymExpr,
395) -> SymBoolExpr {
396    if op == SymCmpOp::Eq {
397        for (quotient, expected) in [(&left, &right), (&right, &left)] {
398            if let Some((denominator, value)) =
399                ConstraintContext::mul_div_identity_operands(quotient, expected)
400                && let Some(factor) = denominator.as_const().filter(|value| !value.is_zero())
401            {
402                // For constant k > 0, (x * k mod 2^256) / k == x iff x <= MAX / k.
403                // The quotient cannot exceed MAX / k; conversely this bound prevents wrapping.
404                // Retain that exact bound instead of asking SMT to solve the overflow check.
405                // `SymExpr::binop` folds a zero factor away, so the non-zero filter is only a
406                // defensive guard against `MAX / 0` should that folding ever change.
407                return SymBoolExpr::cmp_word_const(cx, SymCmpOp::Ule, value, U256::MAX / factor);
408            }
409        }
410        if right.as_const().is_some_and(|value| value.is_zero())
411            && let SymExprKind::BinOp(SymBinOp::Sub, minuend, subtrahend) = left.kind()
412        {
413            // Word subtraction is zero exactly when both operands are equal, including at the
414            // modular boundary. Solc commonly lowers optimized equality checks to this shape.
415            return SymBoolExpr::eq(cx, minuend.clone(), subtrahend.clone());
416        }
417        if left.as_const().is_some_and(|value| value.is_zero())
418            && let SymExprKind::BinOp(SymBinOp::Sub, minuend, subtrahend) = right.kind()
419        {
420            return SymBoolExpr::eq(cx, minuend.clone(), subtrahend.clone());
421        }
422    }
423
424    let (left, right) =
425        if matches!(op, SymCmpOp::Ult | SymCmpOp::Ule | SymCmpOp::Ugt | SymCmpOp::Uge) {
426            // Complement reverses unsigned order: ~x = MAX - x. Move it onto
427            // the constant so interval analysis can see Solidity's addition guard.
428            match (left.kind(), right.kind()) {
429                (SymExprKind::Not(value), SymExprKind::Const(limit)) => {
430                    (SymExpr::constant(cx, !*limit), value.clone())
431                }
432                (SymExprKind::Const(limit), SymExprKind::Not(value)) => {
433                    (value.clone(), SymExpr::constant(cx, !*limit))
434                }
435                _ => (left, right),
436            }
437        } else {
438            (left, right)
439        };
440
441    match op {
442        // `a > b => b < a`.
443        SymCmpOp::Ugt => SymBoolExpr::cmp(cx, SymCmpOp::Ult, right, left),
444        // `a >= b => b <= a`.
445        SymCmpOp::Uge => SymBoolExpr::cmp(cx, SymCmpOp::Ule, right, left),
446        // `a >s b => b <s a`.
447        SymCmpOp::Sgt => SymBoolExpr::cmp(cx, SymCmpOp::Slt, right, left),
448        SymCmpOp::Eq | SymCmpOp::Ult | SymCmpOp::Ule | SymCmpOp::Slt => {
449            SymBoolExpr::cmp(cx, op, left, right)
450        }
451    }
452}
453
454/// Simple facts learned from the normalized conjunction currently being queried.
455#[derive(Default)]
456pub(super) struct ConstraintContext {
457    upper_bounds: HashMap<SymExpr, U256>,
458    lower_bounds: HashMap<SymExpr, U256>,
459    unsigned_lower_bounds: HashMap<SymExpr, U256>,
460    exact_values: HashMap<SymExpr, U256>,
461    conflicting_exact_values: HashSet<SymExpr>,
462    non_wrapping_products: HashSet<(SymExpr, SymExpr)>,
463}
464
465#[derive(Clone, Copy)]
466struct WordInterval {
467    min: U256,
468    max: U256,
469}
470
471// These analyses are solver optimizations, so exceeding their local work budget must only make
472// them decline a rewrite. Keeping the bound shared and private prevents deeply nested bytecode
473// expressions from turning a proof shortcut into unbounded Rust recursion.
474const MAX_LOCAL_ANALYSIS_NODES: usize = 256;
475const MAX_CONTEXTUAL_PASSES: usize = 4;
476
477impl WordInterval {
478    fn new(min: U256, max: U256) -> Option<Self> {
479        (min <= max).then_some(Self { min, max })
480    }
481
482    const fn exact(value: U256) -> Self {
483        Self { min: value, max: value }
484    }
485
486    fn with_bounds(self, lower: Option<U256>, upper: Option<U256>) -> Option<Self> {
487        Self::new(
488            self.min.max(lower.unwrap_or(U256::ZERO)),
489            self.max.min(upper.unwrap_or(U256::MAX)),
490        )
491    }
492}
493
494impl ConstraintContext {
495    pub(super) fn new(constraints: &[SymBoolExpr]) -> Self {
496        Self::from_constraints(constraints.iter(), constraints.len())
497    }
498
499    fn from_constraints<'a>(
500        constraints: impl Clone + Iterator<Item = &'a SymBoolExpr>,
501        constraint_count: usize,
502    ) -> Self {
503        Self::from_constraints_with_lower_bounds(constraints, constraint_count, true)
504    }
505
506    /// Builds a rewrite context from the other retained conjuncts, never the predicate itself.
507    fn for_rewrite(cx: &mut SymCx, constraints: &[SymBoolExpr], index: usize) -> Self {
508        let supporting = constraints
509            .iter()
510            .enumerate()
511            .filter_map(|(i, constraint)| (i != index).then_some(constraint));
512        let mut context = Self::from_constraints(supporting.clone(), constraints.len() - 1);
513        // One successful product guard may bound an operand used in another guard.
514        for _ in 0..MAX_CONTEXTUAL_PASSES {
515            let mut changed = false;
516            for constraint in supporting.clone() {
517                changed |= context.record_non_wrapping_product(cx, constraint);
518            }
519            if !changed {
520                break;
521            }
522        }
523        // Product bounds can then turn scaled zero checks into exact operand facts.
524        for constraint in supporting {
525            context.record_scaled_zero_fact(cx, constraint);
526        }
527        context
528    }
529
530    fn from_constraints_with_lower_bounds<'a>(
531        constraints: impl Clone + Iterator<Item = &'a SymBoolExpr>,
532        constraint_count: usize,
533        promote_unsigned_bounds: bool,
534    ) -> Self {
535        let mut context = Self::default();
536        for constraint in constraints.clone() {
537            context.record_exact_value_constraint(constraint);
538            context.record_upper_bound_constraint(constraint);
539            context.record_lower_bound_constraint(constraint);
540            context.record_unsigned_lower_bound_constraint(constraint, promote_unsigned_bounds);
541        }
542        // A bounded number of rounds closes ordinary order chains. Relational propagation keeps
543        // strict comparisons weak (`a < b` propagates only `a <= upper(b)`), so inconsistent
544        // cycles cannot tighten a bound one integer at a time across the uint256 domain.
545        for _ in 0..constraint_count {
546            let mut changed = false;
547            for constraint in constraints.clone() {
548                changed |= context.propagate_order_bounds(constraint);
549            }
550            if !changed {
551                break;
552            }
553        }
554        context
555    }
556
557    /// Conservatively identifies every conjunct that path facts may rewrite.
558    fn requires_independent_context(expr: &SymBoolExpr) -> bool {
559        let root_candidate = match expr.kind() {
560            SymBoolExprKind::Cmp(op, left, right) => match op {
561                SymCmpOp::Eq => {
562                    Self::mul_div_identity_operands(left, right).is_some()
563                        || Self::mul_div_identity_operands(right, left).is_some()
564                        || Self::masked_word_side_eq_self_shape(left, right).is_some()
565                        || Self::masked_word_side_eq_self_shape(right, left).is_some()
566                }
567                SymCmpOp::Ult | SymCmpOp::Ule => {
568                    Self::udiv_comparison_operands(*op, left, right).is_some()
569                }
570                SymCmpOp::Slt | SymCmpOp::Sgt => true,
571                SymCmpOp::Ugt | SymCmpOp::Uge => false,
572            },
573            SymBoolExprKind::Not(value) => match value.kind() {
574                SymBoolExprKind::Cmp(op, left, right)
575                    if Self::udiv_comparison_operands(*op, left, right).is_some() =>
576                {
577                    true
578                }
579                SymBoolExprKind::Cmp(SymCmpOp::Slt | SymCmpOp::Sgt, _, _) => true,
580                _ => value.zero_check_operand().is_some_and(|word| {
581                    matches!(word.kind(), SymExprKind::BinOp(SymBinOp::Or, _, _))
582                }),
583            },
584            SymBoolExprKind::Const(_) | SymBoolExprKind::And(_) => false,
585        };
586        root_candidate
587            || expr.contains_udiv()
588            || expr.visit_bool(|word| matches!(word.kind(), SymExprKind::Ite(_, _, _)))
589    }
590
591    fn normalize_bool(
592        &self,
593        cx: &mut SymCx,
594        expr: SymBoolExpr,
595        context_free_changed: bool,
596    ) -> SymBoolExpr {
597        let may_normalize_word = !self.is_exact_value_constraint(&expr)
598            && expr.visit_bool(|word| self.may_normalize_word(word));
599        let expr = if may_normalize_word {
600            let expr = expr.fold_exprs(cx, &mut |cx, expr| self.normalize_word(cx, expr));
601            normalize_bool_for_solver(cx, expr)
602        } else if context_free_changed {
603            // The first pass can create new Boolean predicates, such as an overflow comparison
604            // while eliminating a division. Normalize those predicates before applying facts.
605            normalize_bool_for_solver(cx, expr)
606        } else {
607            expr
608        };
609        if let Some(normalized) = self.normalize_signed_add_comparison(cx, &expr) {
610            return normalized;
611        }
612        if let Some(value) = self.rounding_comparison_value(&expr) {
613            return SymBoolExpr::constant(cx, value);
614        }
615        if let SymBoolExprKind::Not(value) = expr.kind()
616            && let Some(normalized) = self.normalize_signed_add_comparison(cx, value)
617        {
618            return normalized.not(cx);
619        }
620        if let SymBoolExprKind::Cmp(op, left, right) = expr.kind()
621            && let Some(normalized) = self.normalize_udiv_comparison(cx, *op, left, right)
622        {
623            return normalized;
624        }
625        if let SymBoolExprKind::Not(value) = expr.kind()
626            && let SymBoolExprKind::Cmp(op, left, right) = value.kind()
627            && let Some(normalized) = self.normalize_udiv_comparison(cx, *op, left, right)
628        {
629            return normalized.not(cx);
630        }
631
632        match expr.kind() {
633            SymBoolExprKind::Not(value) if self.unsigned_bool_always_true(value) => {
634                SymBoolExpr::constant(cx, false)
635            }
636            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right)
637                if self.mul_div_identity(left, right) || self.mul_div_identity(right, left) =>
638            {
639                SymBoolExpr::constant(cx, true)
640            }
641            SymBoolExprKind::Not(value)
642                if matches!(
643                    value.kind(),
644                    SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right)
645                        if self.mul_div_identity(left, right)
646                            || self.mul_div_identity(right, left)
647                ) =>
648            {
649                SymBoolExpr::constant(cx, false)
650            }
651            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right)
652                if self.masked_word_eq_self(left, right) =>
653            {
654                // `x & mask == x => true` when the current context proves `x <= mask`.
655                SymBoolExpr::constant(cx, true)
656            }
657            SymBoolExprKind::Not(value) if self.masked_eq_self_condition(value) => {
658                // `x & mask != x => false` when the current context proves `x <= mask`.
659                SymBoolExpr::constant(cx, false)
660            }
661            _ if expr
662                .zero_check_operand()
663                .is_some_and(|left| self.word_bool_always_true(cx, left)) =>
664            {
665                // `always_true_word == 0 => false`.
666                SymBoolExpr::constant(cx, false)
667            }
668            SymBoolExprKind::Not(value)
669                if value
670                    .zero_check_operand()
671                    .is_some_and(|left| self.word_bool_always_true(cx, left)) =>
672            {
673                // `always_true_word != 0 => true`.
674                SymBoolExpr::constant(cx, true)
675            }
676            _ => expr,
677        }
678    }
679
680    fn record_exact_value_constraint(&mut self, constraint: &SymBoolExpr) {
681        let SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) = constraint.kind() else {
682            return;
683        };
684        let Some((expr, value)) = const_side_bound(left, right) else {
685            return;
686        };
687        if !matches!(expr.kind(), SymExprKind::Var(_))
688            || self.conflicting_exact_values.contains(expr)
689        {
690            return;
691        }
692        if self.exact_values.get(expr).is_some_and(|current| *current != value) {
693            self.exact_values.remove(expr);
694            self.conflicting_exact_values.insert(expr.clone());
695        } else {
696            self.exact_values.insert(expr.clone(), value);
697        }
698    }
699
700    fn may_normalize_word(&self, expr: &SymExpr) -> bool {
701        if self.exact_values.contains_key(expr)
702            || Self::mul_div_operands(expr).is_some()
703            || Self::ceil_div_product(expr).is_some()
704        {
705            return true;
706        }
707        match expr.kind() {
708            SymExprKind::Ite(_, _, _) => true,
709            SymExprKind::BinOp(SymBinOp::Or, left, right) => {
710                left.as_const() == Some(U256::ONE) || right.as_const() == Some(U256::ONE)
711            }
712            SymExprKind::BinOp(SymBinOp::Mul, _, _) => Self::constant_mul_operands(expr)
713                .is_some_and(|(value, _)| Self::constant_mul_operands(value).is_some()),
714            SymExprKind::BinOp(SymBinOp::UDiv, numerator, denominator) => {
715                Self::rounded_product_operands(numerator).is_some()
716                    || (denominator.as_const().is_some_and(|value| !value.is_zero())
717                        && Self::constant_mul_operands(numerator).is_some())
718            }
719            _ => false,
720        }
721    }
722
723    fn normalize_word(&self, cx: &mut SymCx, expr: SymExpr) -> SymExpr {
724        if let Some(value) = self.exact_values.get(&expr).copied() {
725            return SymExpr::constant(cx, value);
726        }
727        if let SymExprKind::Ite(condition, then_value, else_value) = expr.kind() {
728            if let Some(value) = self.bounded_bool_value(condition) {
729                return if value { then_value.clone() } else { else_value.clone() };
730            }
731            if let Some(condition) = self.normalize_signed_add_comparison(cx, condition) {
732                return SymExpr::ite(cx, condition, then_value.clone(), else_value.clone());
733            }
734        }
735        if let Some(value) = self.quotient_of_rounded_product(&expr) {
736            return value.clone();
737        }
738        if let SymExprKind::BinOp(SymBinOp::And, value, mask) = expr.kind()
739            && mask.as_const() == Some(U256::ONE)
740            && value.normalized_bool_word_condition(cx).is_some()
741        {
742            return value.clone();
743        }
744        if let SymExprKind::BinOp(SymBinOp::Or, left, right) = expr.kind()
745            && ((left.as_const() == Some(U256::ONE)
746                && right.normalized_bool_word_condition(cx).is_some())
747                || (right.as_const() == Some(U256::ONE)
748                    && left.normalized_bool_word_condition(cx).is_some()))
749        {
750            return SymExpr::one(cx);
751        }
752        if let Some((value, outer_factor)) = Self::constant_mul_operands(&expr)
753            && let Some((value, inner_factor)) = Self::constant_mul_operands(value)
754        {
755            let factor = SymExpr::constant(cx, inner_factor.wrapping_mul(outer_factor));
756            return SymExpr::binop(cx, SymBinOp::Mul, value.clone(), factor);
757        }
758        if let Some((value, factor)) =
759            self.exact_ceil_div_factor(&expr).or_else(|| self.exact_scaled_div_factor(&expr))
760        {
761            let factor = SymExpr::constant(cx, factor);
762            return SymExpr::binop(cx, SymBinOp::Mul, value.clone(), factor);
763        }
764        if let Some((denominator, other)) = Self::mul_div_operands(&expr)
765            && self.interval(denominator).is_some_and(|interval| !interval.min.is_zero())
766            && self.mul_cannot_overflow_256(denominator, other)
767        {
768            return other.clone();
769        }
770        expr
771    }
772
773    fn bounded_bool_value(&self, expr: &SymBoolExpr) -> Option<bool> {
774        match expr.kind() {
775            SymBoolExprKind::Const(value) => Some(*value),
776            SymBoolExprKind::Not(value) => self.bounded_bool_value(value).map(|value| !value),
777            SymBoolExprKind::Cmp(op, left, right) => {
778                let left = self.interval(left)?;
779                let right = self.interval(right)?;
780                if *op == SymCmpOp::Eq {
781                    return if left.max < right.min || right.max < left.min {
782                        Some(false)
783                    } else if left.min == left.max && right.min == right.max {
784                        Some(left.min == right.min)
785                    } else {
786                        None
787                    };
788                }
789                if matches!(op, SymCmpOp::Slt | SymCmpOp::Sgt)
790                    && (left.min.bit(255) != left.max.bit(255)
791                        || right.min.bit(255) != right.max.bit(255))
792                {
793                    return None;
794                }
795                let (always, possible) =
796                    if matches!(op, SymCmpOp::Ult | SymCmpOp::Ule | SymCmpOp::Slt) {
797                        (op.eval(left.max, right.min), op.eval(left.min, right.max))
798                    } else {
799                        (op.eval(left.min, right.max), op.eval(left.max, right.min))
800                    };
801                if always {
802                    Some(true)
803                } else if !possible {
804                    Some(false)
805                } else {
806                    None
807                }
808            }
809            SymBoolExprKind::And(_) => None,
810        }
811    }
812
813    /// Simplifies signed addition guards once the signs of the summands are established.
814    fn normalize_signed_add_comparison(
815        &self,
816        cx: &mut SymCx,
817        expr: &SymBoolExpr,
818    ) -> Option<SymBoolExpr> {
819        let SymBoolExprKind::Cmp(op, left, right) = expr.kind() else { return None };
820        let (sum, base) = match op {
821            SymCmpOp::Slt => (left, right),
822            SymCmpOp::Sgt => (right, left),
823            _ => return None,
824        };
825        let signed_max = U256::MAX >> 1;
826        if let Some((_, increment)) = sum.add_with_operand(base) {
827            let base_range = self.interval(base)?;
828            let increment_range = self.interval(increment)?;
829            // Opposite-sign addition cannot overflow: adding a nonnegative value cannot make
830            // a negative summand smaller, and adding a negative value makes a nonnegative one
831            // smaller.
832            if base_range.min > signed_max && increment_range.max <= signed_max {
833                return Some(SymBoolExpr::constant(cx, false));
834            }
835            if base_range.max <= signed_max && increment_range.min > signed_max {
836                return Some(SymBoolExpr::constant(cx, true));
837            }
838        } else if base.as_const() != Some(U256::ZERO) {
839            return None;
840        }
841        if base.as_const() == Some(U256::ZERO)
842            && let SymExprKind::BinOp(SymBinOp::Add, left, right) = sum.kind()
843        {
844            for (positive, negative) in [(left, right), (right, left)] {
845                if let SymExprKind::BinOp(SymBinOp::Sub, zero, amount) = negative.kind()
846                    && zero.as_const() == Some(U256::ZERO)
847                    && self.interval(positive).is_some_and(|range| range.max <= signed_max)
848                    && self.interval(amount).is_some_and(|range| range.max <= signed_max)
849                {
850                    // For a,b in [0, int256::MAX], signed(a + (-b)) < 0 iff a < b.
851                    return Some(SymBoolExpr::cmp_word_expr(
852                        cx,
853                        SymCmpOp::Ult,
854                        positive,
855                        amount.clone(),
856                    ));
857                }
858            }
859        }
860        // Use the sum's canonical operand order for both the overflow guard and a subsequent
861        // signed-to-unsigned cast. Their conditions must normalize to the same predicate.
862        let SymExprKind::BinOp(SymBinOp::Add, increment, base) = sum.kind() else {
863            return None;
864        };
865        if self.interval(base)?.max > signed_max || self.interval(increment)?.max > signed_max {
866            return None;
867        }
868        // With nonnegative summands their unsigned sum cannot wrap. It is signed-less than a
869        // summand exactly when it crosses the signed maximum; equality (zero increment) is safe.
870        let limit = SymExpr::constant(cx, signed_max);
871        let remaining = SymExpr::binop(cx, SymBinOp::Sub, limit, base.clone());
872        Some(SymBoolExpr::cmp(cx, SymCmpOp::Ult, remaining, increment.clone()))
873    }
874
875    fn exact_ceil_div_factor<'a>(&self, expr: &'a SymExpr) -> Option<(&'a SymExpr, U256)> {
876        let (product, denominator) = Self::ceil_div_product(expr)?;
877        let (value, multiplier) = Self::constant_mul_operands(product)?;
878        self.max_scaled_product(value, multiplier)?.checked_add(denominator)?;
879        let factor = multiplier.checked_div(denominator)?;
880        (multiplier % denominator).is_zero().then_some((value, factor))
881    }
882
883    /// Recognizes `(product + scale - 1) / scale` without assuming the arithmetic cannot wrap.
884    fn ceil_div_product(expr: &SymExpr) -> Option<(&SymExpr, U256)> {
885        let (numerator, denominator) = expr.udiv_operands()?;
886        let scale = denominator.as_const().filter(|value| !value.is_zero())?;
887        if let SymExprKind::BinOp(SymBinOp::Sub, sum, one) = numerator.kind()
888            && one.as_const() == Some(U256::ONE)
889            && let SymExprKind::BinOp(SymBinOp::Add, product, rounding) = sum.kind()
890            && rounding.as_const() == Some(scale)
891            && matches!(product.kind(), SymExprKind::BinOp(SymBinOp::Mul, _, _))
892        {
893            Some((product, scale))
894        } else {
895            None
896        }
897    }
898
899    fn exact_scaled_div_factor<'a>(&self, expr: &'a SymExpr) -> Option<(&'a SymExpr, U256)> {
900        let (numerator, denominator) = expr.udiv_operands()?;
901        let denominator = denominator.as_const().filter(|value| !value.is_zero())?;
902        let (value, multiplier) = Self::constant_mul_operands(numerator)?;
903        if !(multiplier % denominator).is_zero()
904            || self.max_scaled_product(value, multiplier).is_none()
905        {
906            return None;
907        }
908        Some((value, multiplier / denominator))
909    }
910
911    fn max_scaled_product(&self, value: &SymExpr, multiplier: U256) -> Option<U256> {
912        self.interval(value)?.max.checked_mul(multiplier)
913    }
914
915    fn constant_mul_operands(expr: &SymExpr) -> Option<(&SymExpr, U256)> {
916        let SymExprKind::BinOp(SymBinOp::Mul, left, right) = expr.kind() else {
917            return None;
918        };
919        const_side_bound(left, right)
920    }
921
922    fn is_exact_value_constraint(&self, constraint: &SymBoolExpr) -> bool {
923        let SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) = constraint.kind() else {
924            return false;
925        };
926        const_side_bound(left, right)
927            .is_some_and(|(expr, value)| self.exact_values.get(expr).copied() == Some(value))
928    }
929
930    fn masked_eq_self_condition(&self, expr: &SymBoolExpr) -> bool {
931        match expr.kind() {
932            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => {
933                self.masked_word_eq_self(left, right)
934            }
935            _ => false,
936        }
937    }
938
939    fn masked_word_eq_self(&self, left: &SymExpr, right: &SymExpr) -> bool {
940        self.masked_word_side_eq_self(left, right) || self.masked_word_side_eq_self(right, left)
941    }
942
943    fn masked_word_side_eq_self(&self, masked: &SymExpr, value: &SymExpr) -> bool {
944        Self::masked_word_side_eq_self_shape(masked, value)
945            .is_some_and(|bits| self.unsigned_bits(value) <= bits)
946    }
947
948    fn masked_word_side_eq_self_shape(masked: &SymExpr, value: &SymExpr) -> Option<usize> {
949        let SymExprKind::BinOp(SymBinOp::And, left, right) = masked.kind() else {
950            return None;
951        };
952        let (source, mask) = const_side_bound(left, right)?;
953        let bits = mask_low_bits(mask)?;
954        (source == value).then_some(bits)
955    }
956
957    fn record_upper_bound_constraint(&mut self, constraint: &SymBoolExpr) {
958        if let Some((expr, bound)) = self.upper_bound_constraint(constraint) {
959            self.record_upper_bound(expr.clone(), bound);
960        }
961    }
962
963    fn record_upper_bound(&mut self, expr: SymExpr, bound: U256) -> bool {
964        match self.upper_bounds.entry(expr) {
965            alloy_primitives::map::Entry::Occupied(mut entry) if bound < *entry.get() => {
966                entry.insert(bound);
967                true
968            }
969            alloy_primitives::map::Entry::Vacant(entry) => {
970                entry.insert(bound);
971                true
972            }
973            alloy_primitives::map::Entry::Occupied(_) => false,
974        }
975    }
976
977    fn record_lower_bound_constraint(&mut self, constraint: &SymBoolExpr) {
978        if let Some((expr, bound)) = self.lower_bound_constraint(constraint) {
979            self.record_lower_bound(expr.clone(), bound);
980        }
981    }
982
983    fn record_unsigned_lower_bound_constraint(
984        &mut self,
985        constraint: &SymBoolExpr,
986        promote_to_interval: bool,
987    ) {
988        if let Some((expr, bound)) = self.unsigned_lower_bound_constraint(constraint) {
989            let entry = self.unsigned_lower_bounds.entry(expr.clone()).or_default();
990            *entry = (*entry).max(bound);
991            // Batch normalization keeps theorem bounds separate from general intervals.
992            // A rewrite supported only by other retained conjuncts may also use these bounds
993            // for interval deductions.
994            if promote_to_interval {
995                self.record_lower_bound(expr.clone(), bound);
996            }
997        }
998    }
999
1000    fn unsigned_lower_bound_constraint<'a>(
1001        &self,
1002        constraint: &'a SymBoolExpr,
1003    ) -> Option<(&'a SymExpr, U256)> {
1004        match constraint.kind() {
1005            SymBoolExprKind::Cmp(op, left, right) => match *op {
1006                SymCmpOp::Eq => const_side_bound(left, right),
1007                SymCmpOp::Ult => {
1008                    left.as_const()?.checked_add(U256::ONE).map(|bound| (right, bound))
1009                }
1010                SymCmpOp::Ule => left.as_const().map(|bound| (right, bound)),
1011                SymCmpOp::Ugt => {
1012                    right.as_const()?.checked_add(U256::ONE).map(|bound| (left, bound))
1013                }
1014                SymCmpOp::Uge => right.as_const().map(|bound| (left, bound)),
1015                SymCmpOp::Slt | SymCmpOp::Sgt => None,
1016            },
1017            SymBoolExprKind::Not(value) => match value.kind() {
1018                SymBoolExprKind::Cmp(SymCmpOp::Ult, left, right) => {
1019                    right.as_const().map(|bound| (left, bound))
1020                }
1021                SymBoolExprKind::Cmp(SymCmpOp::Ule, left, right) => {
1022                    right.as_const()?.checked_add(U256::ONE).map(|bound| (left, bound))
1023                }
1024                SymBoolExprKind::Cmp(SymCmpOp::Ugt, left, right) => {
1025                    left.as_const().map(|bound| (right, bound))
1026                }
1027                SymBoolExprKind::Cmp(SymCmpOp::Uge, left, right) => {
1028                    left.as_const()?.checked_add(U256::ONE).map(|bound| (right, bound))
1029                }
1030                _ => None,
1031            },
1032            _ => None,
1033        }
1034    }
1035
1036    fn record_lower_bound(&mut self, expr: SymExpr, bound: U256) -> bool {
1037        match self.lower_bounds.entry(expr) {
1038            alloy_primitives::map::Entry::Occupied(mut entry) if bound > *entry.get() => {
1039                entry.insert(bound);
1040                true
1041            }
1042            alloy_primitives::map::Entry::Vacant(entry) => {
1043                entry.insert(bound);
1044                true
1045            }
1046            alloy_primitives::map::Entry::Occupied(_) => false,
1047        }
1048    }
1049
1050    fn propagate_order_bounds(&mut self, constraint: &SymBoolExpr) -> bool {
1051        match constraint.kind() {
1052            SymBoolExprKind::Cmp(op, left, right) => match op {
1053                SymCmpOp::Ult | SymCmpOp::Ule => self.propagate_less_or_equal_bounds(left, right),
1054                SymCmpOp::Ugt | SymCmpOp::Uge => self.propagate_less_or_equal_bounds(right, left),
1055                SymCmpOp::Eq => {
1056                    let changed = self.propagate_less_or_equal_bounds(left, right);
1057                    self.propagate_less_or_equal_bounds(right, left) || changed
1058                }
1059                SymCmpOp::Slt | SymCmpOp::Sgt => false,
1060            },
1061            SymBoolExprKind::Not(value) => match value.kind() {
1062                SymBoolExprKind::Cmp(op, left, right) => match op {
1063                    SymCmpOp::Ult | SymCmpOp::Ule => {
1064                        self.propagate_less_or_equal_bounds(right, left)
1065                    }
1066                    SymCmpOp::Ugt | SymCmpOp::Uge => {
1067                        self.propagate_less_or_equal_bounds(left, right)
1068                    }
1069                    SymCmpOp::Eq | SymCmpOp::Slt | SymCmpOp::Sgt => false,
1070                },
1071                _ => false,
1072            },
1073            SymBoolExprKind::Const(_) | SymBoolExprKind::And(_) => false,
1074        }
1075    }
1076
1077    /// Propagates interval bounds through the known unsigned relation `left <= right`.
1078    fn propagate_less_or_equal_bounds(&mut self, left: &SymExpr, right: &SymExpr) -> bool {
1079        let upper = self.upper_bounds.get(right).copied();
1080        let lower = self.lower_bounds.get(left).copied();
1081        let upper_changed = upper.is_some_and(|bound| self.record_upper_bound(left.clone(), bound));
1082        let lower_changed =
1083            lower.is_some_and(|bound| self.record_lower_bound(right.clone(), bound));
1084        upper_changed || lower_changed
1085    }
1086
1087    fn upper_bound_constraint<'a>(
1088        &self,
1089        constraint: &'a SymBoolExpr,
1090    ) -> Option<(&'a SymExpr, U256)> {
1091        match constraint.kind() {
1092            SymBoolExprKind::Cmp(op, left, right) => match *op {
1093                SymCmpOp::Eq => const_side_bound(left, right),
1094                SymCmpOp::Ult => match (left.as_const(), right.as_const()) {
1095                    (_, Some(bound)) => (!bound.is_zero()).then(|| (left, bound - U256::ONE)),
1096                    _ => None,
1097                },
1098                SymCmpOp::Ule => match (left.as_const(), right.as_const()) {
1099                    (_, Some(bound)) => Some((left, bound)),
1100                    _ => None,
1101                },
1102                SymCmpOp::Ugt => match (left.as_const(), right.as_const()) {
1103                    (Some(bound), _) => (!bound.is_zero()).then(|| (right, bound - U256::ONE)),
1104                    _ => None,
1105                },
1106                SymCmpOp::Uge => match (left.as_const(), right.as_const()) {
1107                    (Some(bound), _) => Some((right, bound)),
1108                    _ => None,
1109                },
1110                SymCmpOp::Slt | SymCmpOp::Sgt => None,
1111            },
1112            SymBoolExprKind::Not(value) => match value.kind() {
1113                SymBoolExprKind::Cmp(op, left, right) => match *op {
1114                    SymCmpOp::Ugt => match (left.as_const(), right.as_const()) {
1115                        (_, Some(bound)) => Some((left, bound)),
1116                        _ => None,
1117                    },
1118                    SymCmpOp::Uge => match (left.as_const(), right.as_const()) {
1119                        (_, Some(bound)) => (!bound.is_zero()).then(|| (left, bound - U256::ONE)),
1120                        _ => None,
1121                    },
1122                    SymCmpOp::Ult => match (left.as_const(), right.as_const()) {
1123                        (Some(bound), _) => Some((right, bound)),
1124                        _ => None,
1125                    },
1126                    SymCmpOp::Ule => match (left.as_const(), right.as_const()) {
1127                        (Some(bound), _) => (!bound.is_zero()).then(|| (right, bound - U256::ONE)),
1128                        _ => None,
1129                    },
1130                    SymCmpOp::Eq | SymCmpOp::Slt | SymCmpOp::Sgt => None,
1131                },
1132                _ => None,
1133            },
1134            SymBoolExprKind::Const(_) | SymBoolExprKind::And(_) => None,
1135        }
1136    }
1137
1138    fn lower_bound_constraint<'a>(
1139        &self,
1140        constraint: &'a SymBoolExpr,
1141    ) -> Option<(&'a SymExpr, U256)> {
1142        match constraint.kind() {
1143            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => const_side_bound(left, right),
1144            SymBoolExprKind::Not(value) => match value.kind() {
1145                SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => {
1146                    if right.as_const().is_some_and(|value| value.is_zero()) {
1147                        Some((left, U256::ONE))
1148                    } else if left.as_const().is_some_and(|value| value.is_zero()) {
1149                        Some((right, U256::ONE))
1150                    } else {
1151                        None
1152                    }
1153                }
1154                _ => None,
1155            },
1156            _ => None,
1157        }
1158    }
1159
1160    fn unsigned_bool_always_true(&self, expr: &SymBoolExpr) -> bool {
1161        match expr.kind() {
1162            SymBoolExprKind::Cmp(op, left, right) => {
1163                self.unsigned_cmp_always_true(*op, left, right)
1164            }
1165            _ => false,
1166        }
1167    }
1168
1169    fn unsigned_cmp_always_true(&self, op: SymCmpOp, left: &SymExpr, right: &SymExpr) -> bool {
1170        if op == SymCmpOp::Eq
1171            && (self.mul_div_identity(left, right) || self.mul_div_identity(right, left))
1172        {
1173            return true;
1174        }
1175        let Some(left) = self.interval(left) else {
1176            return false;
1177        };
1178        let Some(right) = self.interval(right) else {
1179            return false;
1180        };
1181        match op {
1182            SymCmpOp::Ult => left.max < right.min,
1183            SymCmpOp::Ule => left.max <= right.min,
1184            SymCmpOp::Ugt => left.min > right.max,
1185            SymCmpOp::Uge => left.min >= right.max,
1186            SymCmpOp::Eq | SymCmpOp::Slt | SymCmpOp::Sgt => false,
1187        }
1188    }
1189
1190    fn mul_div_identity(&self, quotient: &SymExpr, expected: &SymExpr) -> bool {
1191        let Some((denominator, other)) = Self::mul_div_identity_operands(quotient, expected) else {
1192            return false;
1193        };
1194
1195        self.interval(denominator).is_some_and(|interval| !interval.min.is_zero())
1196            && self.mul_cannot_overflow_256(denominator, other)
1197    }
1198
1199    fn mul_div_identity_operands<'a>(
1200        quotient: &'a SymExpr,
1201        expected: &SymExpr,
1202    ) -> Option<(&'a SymExpr, &'a SymExpr)> {
1203        let (denominator, other) = Self::mul_div_operands(quotient)?;
1204        (other == expected).then_some((denominator, other))
1205    }
1206
1207    fn mul_div_operands(quotient: &SymExpr) -> Option<(&SymExpr, &SymExpr)> {
1208        let (numerator, denominator) = quotient.udiv_operands()?;
1209        let SymExprKind::BinOp(SymBinOp::Mul, left, right) = numerator.kind() else {
1210            return None;
1211        };
1212        let other = if left == denominator {
1213            right
1214        } else if right == denominator {
1215            left
1216        } else {
1217            return None;
1218        };
1219        Some((denominator, other))
1220    }
1221
1222    fn udiv_comparison_operands<'a>(
1223        op: SymCmpOp,
1224        left: &'a SymExpr,
1225        right: &'a SymExpr,
1226    ) -> Option<(&'a SymExpr, &'a SymExpr, &'a SymExpr, bool)> {
1227        if !matches!(op, SymCmpOp::Ult | SymCmpOp::Ule) {
1228            return None;
1229        }
1230        if let Some((numerator, denominator)) = left.udiv_operands()
1231            && denominator.as_const().is_some_and(|value| !value.is_zero())
1232            && !right.contains_udiv()
1233        {
1234            return Some((numerator, denominator, right, true));
1235        }
1236        if let Some((numerator, denominator)) = right.udiv_operands()
1237            && denominator.as_const().is_some_and(|value| !value.is_zero())
1238            && !left.contains_udiv()
1239        {
1240            return Some((numerator, denominator, left, false));
1241        }
1242        None
1243    }
1244
1245    fn normalize_udiv_comparison(
1246        &self,
1247        cx: &mut SymCx,
1248        op: SymCmpOp,
1249        left: &SymExpr,
1250        right: &SymExpr,
1251    ) -> Option<SymBoolExpr> {
1252        let (numerator, denominator, threshold, quotient_on_left) =
1253            Self::udiv_comparison_operands(op, left, right)?;
1254        let increment_threshold =
1255            matches!((op, quotient_on_left), (SymCmpOp::Ule, true) | (SymCmpOp::Ult, false));
1256        let threshold = if increment_threshold {
1257            // Prove the successor cannot wrap before constructing the word addition.
1258            self.interval(threshold)?.max.checked_add(U256::ONE)?;
1259            let one = SymExpr::one(cx);
1260            SymExpr::binop(cx, SymBinOp::Add, threshold.clone(), one)
1261        } else {
1262            threshold.clone()
1263        };
1264        if !self.mul_cannot_overflow_256(&threshold, denominator) {
1265            return None;
1266        }
1267
1268        let scaled_threshold = SymExpr::binop(cx, SymBinOp::Mul, threshold, denominator.clone());
1269        Some(if quotient_on_left {
1270            // `n / d < k => n < k * d`; `n / d <= k => n < (k + 1) * d`.
1271            SymBoolExpr::cmp_word_expr(cx, SymCmpOp::Ult, numerator, scaled_threshold)
1272        } else {
1273            // `k <= n / d => k * d <= n`; `k < n / d => (k + 1) * d <= n`.
1274            SymBoolExpr::cmp(cx, SymCmpOp::Ule, scaled_threshold, numerator.clone())
1275        })
1276    }
1277
1278    fn interval(&self, expr: &SymExpr) -> Option<WordInterval> {
1279        let mut intervals = HashMap::default();
1280        let mut remaining = MAX_LOCAL_ANALYSIS_NODES;
1281        self.interval_cached(expr, &mut intervals, &mut remaining)
1282    }
1283
1284    fn interval_cached(
1285        &self,
1286        expr: &SymExpr,
1287        intervals: &mut HashMap<SymExpr, Option<WordInterval>>,
1288        remaining: &mut usize,
1289    ) -> Option<WordInterval> {
1290        if let Some(interval) = intervals.get(expr) {
1291            return *interval;
1292        }
1293
1294        let lower = self.lower_bounds.get(expr).copied();
1295        let upper = self.upper_bounds.get(expr).copied();
1296        let explicit_bounds = || {
1297            if lower.is_none() && upper.is_none() {
1298                return None;
1299            }
1300            WordInterval::new(lower.unwrap_or(U256::ZERO), upper.unwrap_or(U256::MAX))
1301        };
1302        if *remaining == 0 {
1303            let interval = explicit_bounds();
1304            intervals.insert(expr.clone(), interval);
1305            return interval;
1306        }
1307        *remaining -= 1;
1308
1309        let interval =
1310            self.structural_interval(expr, intervals, remaining).or_else(explicit_bounds);
1311        let interval = interval.and_then(|interval| interval.with_bounds(lower, upper));
1312        intervals.insert(expr.clone(), interval);
1313        interval
1314    }
1315
1316    fn structural_interval(
1317        &self,
1318        expr: &SymExpr,
1319        intervals: &mut HashMap<SymExpr, Option<WordInterval>>,
1320        remaining: &mut usize,
1321    ) -> Option<WordInterval> {
1322        match expr.kind() {
1323            SymExprKind::Const(value) => Some(WordInterval::exact(*value)),
1324            SymExprKind::BinOp(SymBinOp::And, left, right) => {
1325                let mask = left.as_const().or_else(|| right.as_const())?;
1326                Some(WordInterval { min: U256::ZERO, max: mask })
1327            }
1328            SymExprKind::BinOp(SymBinOp::Add, left, right) => {
1329                let left = self.interval_cached(left, intervals, remaining)?;
1330                let right = self.interval_cached(right, intervals, remaining)?;
1331                Some(WordInterval {
1332                    min: left.min.checked_add(right.min)?,
1333                    max: left.max.checked_add(right.max)?,
1334                })
1335            }
1336            SymExprKind::BinOp(SymBinOp::Sub, left, right) => {
1337                if let Some(interval) =
1338                    self.rounding_error_interval(left, right, intervals, remaining)
1339                {
1340                    return Some(interval);
1341                }
1342                let left = self.interval_cached(left, intervals, remaining)?;
1343                let right = self.interval_cached(right, intervals, remaining)?;
1344                if left.max < right.min {
1345                    // Every subtraction wraps exactly once, so the unsigned image is contiguous.
1346                    return Some(WordInterval {
1347                        min: left.min.wrapping_sub(right.max),
1348                        max: left.max.wrapping_sub(right.min),
1349                    });
1350                }
1351                if left.min < right.max {
1352                    return None;
1353                }
1354                Some(WordInterval {
1355                    min: left.min.checked_sub(right.max)?,
1356                    max: left.max.checked_sub(right.min)?,
1357                })
1358            }
1359            SymExprKind::BinOp(SymBinOp::Mul, left, right) => {
1360                let guarded = self.has_non_wrapping_product(left, right);
1361                let left = self.interval_cached(left, intervals, remaining)?;
1362                let right = self.interval_cached(right, intervals, remaining)?;
1363                Some(WordInterval {
1364                    min: left.min.checked_mul(right.min)?,
1365                    // A retained overflow guard correlates the factors. Their independent
1366                    // maxima can overflow even though every feasible product fits.
1367                    max: left
1368                        .max
1369                        .checked_mul(right.max)
1370                        .or_else(|| guarded.then_some(U256::MAX))?,
1371                })
1372            }
1373            SymExprKind::BinOp(SymBinOp::UDiv, numerator, denominator) => {
1374                // Even an otherwise unbounded numerator is a uint256 word. Division by a
1375                // positive denominator bounds the quotient regardless of numerator wrapping.
1376                let numerator = self
1377                    .interval_cached(numerator, intervals, remaining)
1378                    .unwrap_or(WordInterval { min: U256::ZERO, max: U256::MAX });
1379                let denominator = self.interval_cached(denominator, intervals, remaining)?;
1380                if denominator.min.is_zero() {
1381                    return None;
1382                }
1383                Some(WordInterval {
1384                    min: numerator.min / denominator.max,
1385                    max: numerator.max / denominator.min,
1386                })
1387            }
1388            SymExprKind::BinOp(SymBinOp::Shr, value, shift) => {
1389                let shift = shift.as_const()?;
1390                if shift >= U256::from(256) {
1391                    return Some(WordInterval::exact(U256::ZERO));
1392                }
1393                let value = self.interval_cached(value, intervals, remaining)?;
1394                let shift = shift.to::<usize>();
1395                Some(WordInterval { min: value.min >> shift, max: value.max >> shift })
1396            }
1397            SymExprKind::Ite(_, left, right) => {
1398                let left = self.interval_cached(left, intervals, remaining)?;
1399                let right = self.interval_cached(right, intervals, remaining)?;
1400                Some(WordInterval { min: left.min.min(right.min), max: left.max.max(right.max) })
1401            }
1402            _ => None,
1403        }
1404    }
1405}
1406
1407fn const_side_bound<'a>(left: &'a SymExpr, right: &'a SymExpr) -> Option<(&'a SymExpr, U256)> {
1408    right
1409        .as_const()
1410        .map(|value| (left, value))
1411        .or_else(|| left.as_const().map(|value| (right, value)))
1412}
1413
1414/// Normalizes one word expression into an equivalent, solver-friendlier form.
1415pub(crate) fn normalize_expr_for_solver(cx: &mut SymCx, expr: SymExpr) -> SymExpr {
1416    if expr.contains_ite() { expr.fold(cx, &mut normalize_expr_node_for_solver) } else { expr }
1417}
1418
1419fn normalize_expr_node_for_solver(cx: &mut SymCx, expr: SymExpr) -> SymExpr {
1420    match expr.kind() {
1421        SymExprKind::Ite(cond, left, right) => {
1422            normalize_ite_expr_for_solver(cx, cond.clone(), left.clone(), right.clone())
1423        }
1424        _ => expr,
1425    }
1426}
1427
1428fn normalize_ite_expr_for_solver(
1429    cx: &mut SymCx,
1430    cond: SymBoolExpr,
1431    left: SymExpr,
1432    right: SymExpr,
1433) -> SymExpr {
1434    let cond = normalize_bool_for_solver(cx, cond);
1435    if left == right {
1436        // `ite(c, a, a) => a`.
1437        return left;
1438    }
1439    if left.as_const() == Some(U256::ONE)
1440        && right.normalized_bool_word_condition(cx).as_ref() == Some(&cond)
1441    {
1442        // `ite(c, 1, bool_word(c)) => bool_word(c)`.
1443        return right;
1444    }
1445    if right.as_const().is_some_and(|value| value.is_zero())
1446        && left.normalized_bool_word_condition(cx).as_ref() == Some(&cond)
1447    {
1448        // `ite(c, bool_word(c), 0) => bool_word(c)`.
1449        return left;
1450    }
1451    SymExpr::ite(cx, cond, left, right)
1452}
1453
1454impl SymExpr {
1455    fn word_bool_always_true(&self, cx: &mut SymCx) -> bool {
1456        ConstraintContext::default().word_bool_always_true(cx, self)
1457    }
1458}
1459
1460impl SymBoolExpr {
1461    fn normalize_udiv_for_solver(&self, cx: &mut SymCx) -> Option<Self> {
1462        if let SymBoolExprKind::Cmp(op, left, right) = self.kind()
1463            && let Some(normalized) = Self::normalize_const_over_self_udiv_cmp(cx, *op, left, right)
1464        {
1465            return Some(normalized);
1466        }
1467        if let SymBoolExprKind::Not(value) = self.kind()
1468            && let SymBoolExprKind::Cmp(op, left, right) = value.kind()
1469            && let Some(normalized) = Self::normalize_const_over_self_udiv_cmp(cx, *op, left, right)
1470        {
1471            return Some(normalized.not(cx));
1472        }
1473
1474        match self.kind() {
1475            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right)
1476                if right.as_const().is_some_and(|value| value.is_zero()) =>
1477            {
1478                left.normalized_bool_word_condition(cx).map(|value| value.not(cx)).or_else(|| {
1479                    if left.word_bool_always_true(cx) {
1480                        // `always_true_word == 0 => false`.
1481                        Some(Self::constant(cx, false))
1482                    } else {
1483                        let zero = SymExpr::zero(cx);
1484                        Self::normalize_udiv_eq_zero(cx, left, &zero)
1485                    }
1486                })
1487            }
1488            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right)
1489                if right.as_const() == Some(U256::ONE) =>
1490            {
1491                // `bool_word(c) == 1 => c`.
1492                left.normalized_bool_word_condition(cx)
1493            }
1494            SymBoolExprKind::Not(value) => match value.kind() {
1495                SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right)
1496                    if right.as_const().is_some_and(|value| value.is_zero()) =>
1497                {
1498                    if left.word_bool_always_true(cx) {
1499                        // `always_true_word != 0 => true`.
1500                        Some(Self::constant(cx, true))
1501                    } else {
1502                        let zero = SymExpr::zero(cx);
1503                        Self::normalize_udiv_eq_zero(cx, left, &zero).map(|value| value.not(cx))
1504                    }
1505                }
1506                SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => {
1507                    Self::normalize_udiv_eq_zero(cx, left, right).map(|value| value.not(cx))
1508                }
1509                SymBoolExprKind::Cmp(op, left, right) => {
1510                    Self::normalize_add_overflow_cmp(cx, *op, left, right)
1511                        .map(|value| value.not(cx))
1512                        .or_else(|| {
1513                            Self::normalize_udiv_cmp(cx, *op, left, right)
1514                                .map(|value| value.not(cx))
1515                        })
1516                }
1517                _ => None,
1518            },
1519            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => {
1520                Self::normalize_udiv_eq_zero(cx, left, right)
1521            }
1522            SymBoolExprKind::Cmp(op, left, right) => {
1523                Self::normalize_add_overflow_cmp(cx, *op, left, right)
1524                    .or_else(|| Self::normalize_udiv_cmp(cx, *op, left, right))
1525            }
1526            SymBoolExprKind::Const(_) | SymBoolExprKind::And(_) => None,
1527        }
1528    }
1529
1530    fn normalize_add_overflow_cmp(
1531        cx: &mut SymCx,
1532        op: SymCmpOp,
1533        left: &SymExpr,
1534        right: &SymExpr,
1535    ) -> Option<Self> {
1536        if let Some(normalized) = Self::normalize_sub_underflow_cmp(cx, op, left, right) {
1537            return Some(normalized);
1538        }
1539        // Strict forms test overflow and non-strict forms its complement; addition wraps iff the
1540        // increment exceeds `~base`.
1541        let (base, increment, overflow) = match op {
1542            SymCmpOp::Ugt => {
1543                right.add_with_operand(left).map(|(_, increment)| (left, increment, true))
1544            }
1545            SymCmpOp::Ult => {
1546                left.add_with_operand(right).map(|(_, increment)| (right, increment, true))
1547            }
1548            SymCmpOp::Uge => {
1549                left.add_with_operand(right).map(|(_, increment)| (right, increment, false))
1550            }
1551            SymCmpOp::Ule => {
1552                right.add_with_operand(left).map(|(_, increment)| (left, increment, false))
1553            }
1554            SymCmpOp::Eq | SymCmpOp::Slt | SymCmpOp::Sgt => None,
1555        }?;
1556        if base.unsigned_bits().max(increment.unsigned_bits()).saturating_add(1) <= 256 {
1557            return Some(Self::constant(cx, !overflow));
1558        }
1559
1560        let limit = match base.kind() {
1561            SymExprKind::BinOp(SymBinOp::Sub, max, value) if max.as_const() == Some(U256::MAX) => {
1562                value.clone()
1563            }
1564            _ => SymExpr::not(cx, base.clone()),
1565        };
1566        Some(if overflow {
1567            Self::cmp(cx, SymCmpOp::Ult, limit, increment.clone())
1568        } else {
1569            Self::cmp_word_expr(cx, SymCmpOp::Ule, increment, limit)
1570        })
1571    }
1572
1573    fn normalize_sub_underflow_cmp(
1574        cx: &mut SymCx,
1575        op: SymCmpOp,
1576        left: &SymExpr,
1577        right: &SymExpr,
1578    ) -> Option<Self> {
1579        let (base, difference, underflow) = match op {
1580            SymCmpOp::Ult => (left, right, true),
1581            SymCmpOp::Ugt => (right, left, true),
1582            SymCmpOp::Ule => (right, left, false),
1583            SymCmpOp::Uge => (left, right, false),
1584            _ => return None,
1585        };
1586        let SymExprKind::BinOp(SymBinOp::Sub, minuend, subtrahend) = difference.kind() else {
1587            return None;
1588        };
1589        if minuend != base {
1590            return None;
1591        }
1592        // Unsigned modular subtraction wraps exactly when the subtrahend exceeds the minuend.
1593        Some(if underflow {
1594            Self::cmp_word_expr(cx, SymCmpOp::Ult, base, subtrahend.clone())
1595        } else {
1596            Self::cmp_word_expr(cx, SymCmpOp::Ule, subtrahend, base.clone())
1597        })
1598    }
1599
1600    fn normalize_udiv_eq_zero(cx: &mut SymCx, left: &SymExpr, right: &SymExpr) -> Option<Self> {
1601        if right.as_const().is_some_and(|value| value.is_zero())
1602            && let Some(condition) = left.normalize_eq_zero_for_solver(cx)
1603        {
1604            // `word_bool(c) == 0 => !c`.
1605            return Some(condition);
1606        }
1607        None
1608    }
1609
1610    fn normalize_udiv_cmp(
1611        cx: &mut SymCx,
1612        op: SymCmpOp,
1613        left: &SymExpr,
1614        right: &SymExpr,
1615    ) -> Option<Self> {
1616        match op {
1617            SymCmpOp::Ugt => match (left.as_const(), right.as_const()) {
1618                // `a > 0 => a != 0`.
1619                (_, Some(value)) if value.is_zero() => left
1620                    .normalize_ne_zero_for_solver(cx)
1621                    .or_else(|| Some(Self::eq_zero(cx, left).not(cx))),
1622                // `1 > a => a == 0`.
1623                (Some(value), _) if value == U256::ONE => right
1624                    .normalize_eq_zero_for_solver(cx)
1625                    .or_else(|| Some(Self::eq_zero(cx, right))),
1626                _ => None,
1627            },
1628            SymCmpOp::Uge => match (left.as_const(), right.as_const()) {
1629                // `a >= 1 => a != 0`.
1630                (_, Some(value)) if value == U256::ONE => left
1631                    .normalize_ne_zero_for_solver(cx)
1632                    .or_else(|| Some(Self::eq_zero(cx, left).not(cx))),
1633                // `0 >= a => a == 0`.
1634                (Some(value), _) if value.is_zero() => right
1635                    .normalize_eq_zero_for_solver(cx)
1636                    .or_else(|| Some(Self::eq_zero(cx, right))),
1637                _ => None,
1638            },
1639            SymCmpOp::Ule => match (left.as_const(), right.as_const()) {
1640                // `a <= 0 => a == 0`.
1641                (_, Some(value)) if value.is_zero() => {
1642                    left.normalize_eq_zero_for_solver(cx).or_else(|| Some(Self::eq_zero(cx, left)))
1643                }
1644                // `1 <= a => a != 0`.
1645                (Some(value), _) if value == U256::ONE => right
1646                    .normalize_ne_zero_for_solver(cx)
1647                    .or_else(|| Some(Self::eq_zero(cx, right).not(cx))),
1648                _ => None,
1649            },
1650            SymCmpOp::Ult => match (left.as_const(), right.as_const()) {
1651                // `a < 1 => a == 0`.
1652                (_, Some(value)) if value == U256::ONE => {
1653                    left.normalize_eq_zero_for_solver(cx).or_else(|| Some(Self::eq_zero(cx, left)))
1654                }
1655                // `0 < a => a != 0`.
1656                (Some(value), _) if value.is_zero() => right
1657                    .normalize_ne_zero_for_solver(cx)
1658                    .or_else(|| Some(Self::eq_zero(cx, right).not(cx))),
1659                _ => None,
1660            },
1661            SymCmpOp::Eq | SymCmpOp::Slt | SymCmpOp::Sgt => None,
1662        }
1663    }
1664
1665    fn normalize_const_over_self_udiv_cmp(
1666        cx: &mut SymCx,
1667        op: SymCmpOp,
1668        left: &SymExpr,
1669        right: &SymExpr,
1670    ) -> Option<Self> {
1671        let (value, quotient, complement) = match op {
1672            // `a <= c / a`.
1673            SymCmpOp::Ule => (left, right, false),
1674            // `c / a < a`, the complement of `a <= c / a`.
1675            SymCmpOp::Ult => (right, left, true),
1676            SymCmpOp::Eq | SymCmpOp::Ugt | SymCmpOp::Uge | SymCmpOp::Slt | SymCmpOp::Sgt => {
1677                return None;
1678            }
1679        };
1680        let (numerator, denominator) = match quotient.kind() {
1681            SymExprKind::BinOp(SymBinOp::UDiv, numerator, denominator) => (numerator, denominator),
1682            SymExprKind::Ite(condition, zero, division)
1683                if zero.as_const().is_some_and(|value| value.is_zero()) =>
1684            {
1685                let (numerator, denominator) = division.udiv_operands()?;
1686                if condition.zero_check_operand() != Some(denominator) {
1687                    return None;
1688                }
1689                (numerator, denominator)
1690            }
1691            _ => return None,
1692        };
1693        if denominator != value {
1694            return None;
1695        }
1696
1697        let threshold = numerator.as_const()?.root(2);
1698        let threshold = SymExpr::constant(cx, threshold);
1699        Some(if complement {
1700            Self::cmp(cx, SymCmpOp::Ult, threshold, value.clone())
1701        } else {
1702            Self::cmp_word_expr(cx, SymCmpOp::Ule, value, threshold)
1703        })
1704    }
1705
1706    fn eq_zero(cx: &mut SymCx, expr: &SymExpr) -> Self {
1707        let zero = SymExpr::zero(cx);
1708        Self::eq(cx, expr.clone(), zero)
1709    }
1710}
1711
1712impl SymExpr {
1713    fn normalized_bool_word_condition(&self, cx: &mut SymCx) -> Option<SymBoolExpr> {
1714        self.strip_low_byte_mask()
1715            .bool_word_condition()
1716            .map(|condition| normalize_bool_for_solver(cx, condition))
1717    }
1718
1719    fn add_with_operand<'a>(&'a self, operand: &Self) -> Option<(&'a Self, &'a Self)> {
1720        let SymExprKind::BinOp(SymBinOp::Add, left, right) = self.kind() else {
1721            return None;
1722        };
1723        if left == operand {
1724            Some((left, right))
1725        } else if right == operand {
1726            Some((right, left))
1727        } else {
1728            None
1729        }
1730    }
1731
1732    fn normalize_eq_zero_for_solver(&self, cx: &mut SymCx) -> Option<SymBoolExpr> {
1733        if let Some((numerator, denominator)) = self.udiv_operands() {
1734            // `a / b == 0 => b == 0 || a < b`.
1735            return Some(Self::udiv_zero_condition(cx, numerator, denominator));
1736        }
1737        if let SymExprKind::Ite(condition, then_expr, else_expr) = self.kind() {
1738            let then_zero = match then_expr.normalize_eq_zero_for_solver(cx) {
1739                Some(condition) => condition,
1740                None => {
1741                    let then_expr = normalize_expr_for_solver(cx, then_expr.clone());
1742                    let zero = Self::zero(cx);
1743                    SymBoolExpr::eq(cx, then_expr, zero)
1744                }
1745            };
1746            let else_zero = match else_expr.normalize_eq_zero_for_solver(cx) {
1747                Some(condition) => condition,
1748                None => {
1749                    let else_expr = normalize_expr_for_solver(cx, else_expr.clone());
1750                    let zero = Self::zero(cx);
1751                    SymBoolExpr::eq(cx, else_expr, zero)
1752                }
1753            };
1754            if then_zero.contains_udiv() || else_zero.contains_udiv() {
1755                return None;
1756            }
1757            // `ite(c, a, b) == 0 => (c && a == 0) || (!c && b == 0)`.
1758            let condition = normalize_bool_for_solver(cx, condition.clone());
1759            let then_condition = SymBoolExpr::and(cx, vec![condition.clone(), then_zero]);
1760            let not_condition = condition.not(cx);
1761            let else_condition = SymBoolExpr::and(cx, vec![not_condition, else_zero]);
1762            return Some(SymBoolExpr::or(cx, vec![then_condition, else_condition]));
1763        }
1764        None
1765    }
1766
1767    fn normalize_ne_zero_for_solver(&self, cx: &mut SymCx) -> Option<SymBoolExpr> {
1768        if let Some((numerator, denominator)) = self.udiv_operands() {
1769            // `a / b != 0 => b != 0 && a >= b`.
1770            return Some(Self::udiv_nonzero_condition(cx, numerator, denominator));
1771        }
1772        if let SymExprKind::Ite(condition, then_expr, else_expr) = self.kind() {
1773            let then_nonzero = match then_expr.normalize_ne_zero_for_solver(cx) {
1774                Some(condition) => condition,
1775                None => {
1776                    let then_expr = normalize_expr_for_solver(cx, then_expr.clone());
1777                    let zero = Self::zero(cx);
1778                    SymBoolExpr::eq(cx, then_expr, zero).not(cx)
1779                }
1780            };
1781            let else_nonzero = match else_expr.normalize_ne_zero_for_solver(cx) {
1782                Some(condition) => condition,
1783                None => {
1784                    let else_expr = normalize_expr_for_solver(cx, else_expr.clone());
1785                    let zero = Self::zero(cx);
1786                    SymBoolExpr::eq(cx, else_expr, zero).not(cx)
1787                }
1788            };
1789            if then_nonzero.contains_udiv() || else_nonzero.contains_udiv() {
1790                return None;
1791            }
1792            // `ite(c, a, b) != 0 => (c && a != 0) || (!c && b != 0)`.
1793            let condition = normalize_bool_for_solver(cx, condition.clone());
1794            let then_condition = SymBoolExpr::and(cx, vec![condition.clone(), then_nonzero]);
1795            let not_condition = condition.not(cx);
1796            let else_condition = SymBoolExpr::and(cx, vec![not_condition, else_nonzero]);
1797            return Some(SymBoolExpr::or(cx, vec![then_condition, else_condition]));
1798        }
1799        None
1800    }
1801
1802    fn udiv_zero_condition(cx: &mut SymCx, numerator: &Self, denominator: &Self) -> SymBoolExpr {
1803        let numerator = normalize_expr_for_solver(cx, numerator.clone());
1804        let denominator = normalize_expr_for_solver(cx, denominator.clone());
1805        let zero = Self::zero(cx);
1806        let denominator_zero = SymBoolExpr::eq(cx, denominator.clone(), zero);
1807        let below_denominator = SymBoolExpr::cmp(cx, SymCmpOp::Ult, numerator, denominator);
1808        SymBoolExpr::or(cx, vec![denominator_zero, below_denominator])
1809    }
1810
1811    fn udiv_nonzero_condition(cx: &mut SymCx, numerator: &Self, denominator: &Self) -> SymBoolExpr {
1812        let numerator = normalize_expr_for_solver(cx, numerator.clone());
1813        let denominator = normalize_expr_for_solver(cx, denominator.clone());
1814        let zero = Self::zero(cx);
1815        let denominator_nonzero = SymBoolExpr::eq(cx, denominator.clone(), zero).not(cx);
1816        let at_least_denominator = SymBoolExpr::cmp(cx, SymCmpOp::Uge, numerator, denominator);
1817        SymBoolExpr::and(cx, vec![denominator_nonzero, at_least_denominator])
1818    }
1819}
1820
1821impl ConstraintContext {
1822    fn word_bool_always_true(&self, cx: &mut SymCx, expr: &SymExpr) -> bool {
1823        let mut terms = Vec::new();
1824        expr.push_or_terms(&mut terms);
1825        if terms.len() <= 1 {
1826            return false;
1827        }
1828
1829        let bool_terms = terms
1830            .iter()
1831            .filter_map(|term| term.normalized_bool_word_condition(cx))
1832            .collect::<Vec<_>>();
1833        if bool_terms.iter().any(|term| {
1834            let negated = term.clone().not(cx);
1835            bool_terms.contains(&negated)
1836        }) {
1837            // `c || !c => true`.
1838            return true;
1839        }
1840        for zero_term in &bool_terms {
1841            if bool_terms
1842                .iter()
1843                .any(|term| self.checked_mul_guard_for_zero_condition(term, zero_term))
1844            {
1845                // `a == 0 || guarded_mul_div(a) => true`.
1846                return true;
1847            }
1848        }
1849        false
1850    }
1851
1852    /// Records operand facts from independently justified, non-wrapping scaled zero checks.
1853    fn record_scaled_zero_fact(&mut self, cx: &mut SymCx, constraint: &SymBoolExpr) {
1854        let (condition, nonzero) = match constraint.kind() {
1855            SymBoolExprKind::Not(inner) => (inner, true),
1856            _ => (constraint, false),
1857        };
1858        if matches!(condition.kind(), SymBoolExprKind::Cmp(SymCmpOp::Ult, _, _))
1859            && let Some(value) = self.bounded_zero_check_operand(condition).cloned()
1860        {
1861            if nonzero {
1862                self.record_lower_bound(value, U256::ONE);
1863            } else {
1864                self.record_upper_bound(value.clone(), U256::ZERO);
1865                let zero = SymExpr::zero(cx);
1866                let exact = SymBoolExpr::eq(cx, value, zero);
1867                self.record_exact_value_constraint(&exact);
1868            }
1869        }
1870    }
1871
1872    /// Recognizes zero checks exposed by normalizing a scaled balance division.
1873    fn bounded_zero_check_operand<'a>(&self, expr: &'a SymBoolExpr) -> Option<&'a SymExpr> {
1874        if let Some(value) = expr.zero_check_operand() {
1875            return Some(value);
1876        }
1877        let SymBoolExprKind::Cmp(SymCmpOp::Ult, product, limit) = expr.kind() else {
1878            return None;
1879        };
1880        let (value, scale) = Self::constant_mul_operands(product)?;
1881        // x * scale < scale iff x == 0, but only for a positive scale and no wrap.
1882        if scale.is_zero() || limit.as_const() != Some(scale) {
1883            return None;
1884        }
1885        self.max_scaled_product(value, scale)?;
1886        Some(value)
1887    }
1888
1889    /// Checks a zero predicate against the actual divisor of a multiplication guard.
1890    fn zero_check_for_operand(&self, condition: &SymBoolExpr, operand: &SymExpr) -> bool {
1891        if self.bounded_zero_check_operand(condition) == Some(operand) {
1892            return true;
1893        }
1894        // For a positive constant d, n / d == 0 iff n < d. Normalization
1895        // exposes this comparison before the Solidity multiplication guard.
1896        if let Some((numerator, denominator)) = operand.udiv_operands()
1897            && denominator.as_const().is_some_and(|d| !d.is_zero())
1898            && let SymBoolExprKind::Cmp(SymCmpOp::Ult, left, right) = condition.kind()
1899        {
1900            return left == numerator && right == denominator;
1901        }
1902        false
1903    }
1904
1905    fn checked_mul_guard_for_zero_condition(
1906        &self,
1907        expr: &SymBoolExpr,
1908        zero_condition: &SymBoolExpr,
1909    ) -> bool {
1910        let SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) = expr.kind() else {
1911            return false;
1912        };
1913        [(left, right), (right, left)].into_iter().any(|(quotient, expected)| {
1914            matches!(quotient.kind(), SymExprKind::Ite(_, _, _))
1915                && self
1916                    .checked_quotient_factors(quotient, expected, Some(zero_condition))
1917                    .is_some_and(|(left, right)| self.mul_cannot_overflow_256(left, right))
1918        })
1919    }
1920
1921    /// Matches the quotient side shared by multiplication guard proofs and retained guard facts.
1922    fn checked_quotient_factors<'a>(
1923        &self,
1924        quotient: &'a SymExpr,
1925        expected: &SymExpr,
1926        zero_condition: Option<&SymBoolExpr>,
1927    ) -> Option<(&'a SymExpr, &'a SymExpr)> {
1928        let (quotient, branch_condition) =
1929            if let SymExprKind::Ite(condition, zero, quotient) = quotient.kind() {
1930                if zero.as_const() != Some(U256::ZERO) || zero_condition.is_none() {
1931                    return None;
1932                }
1933                (quotient, Some(condition))
1934            } else {
1935                (quotient, None)
1936            };
1937        let (divisor, other) = Self::mul_div_identity_operands(quotient, expected)?;
1938        (zero_condition.is_none_or(|condition| self.zero_check_for_operand(condition, divisor))
1939            && branch_condition
1940                .is_none_or(|condition| self.zero_check_for_operand(condition, divisor)))
1941        .then_some((divisor, other))
1942    }
1943
1944    /// Learns multiplication safety from a retained successful Solidity overflow check.
1945    fn record_non_wrapping_product(&mut self, cx: &mut SymCx, constraint: &SymBoolExpr) -> bool {
1946        let fact = bitwise_bool_word_fact(cx, constraint).unwrap_or_else(|| constraint.clone());
1947        let factors = if let Some(factors) = self.checked_product_factors(&fact, None) {
1948            Some(factors)
1949        } else if let SymBoolExprKind::Not(inner) = fact.kind()
1950            && let SymBoolExprKind::And(terms) = inner.kind()
1951            && terms.len() == 2
1952        {
1953            // Only the exact two-way disjunction is a multiplication guard. An additional
1954            // alternative would let this predicate hold even when the product wraps.
1955            let first = terms[0].clone().not(cx);
1956            let second = terms[1].clone().not(cx);
1957            self.checked_product_factors(&second, Some(&first))
1958                .or_else(|| self.checked_product_factors(&first, Some(&second)))
1959        } else {
1960            None
1961        };
1962        if let Some((left, right)) = factors {
1963            // A successful product fits in one word. A positive lower bound on either
1964            // factor therefore bounds the other, even if it started as a full-width word.
1965            // The supporting guard remains in the conjunction; it cannot prove itself.
1966            let mut changed = false;
1967            for (value, factor) in [(&left, &right), (&right, &left)] {
1968                if let Some(range) = self.interval(factor)
1969                    && !range.min.is_zero()
1970                {
1971                    changed |= self.record_upper_bound(value.clone(), U256::MAX / range.min);
1972                }
1973            }
1974            self.non_wrapping_products.insert((left, right)) || changed
1975        } else {
1976            false
1977        }
1978    }
1979
1980    fn checked_product_factors(
1981        &self,
1982        predicate: &SymBoolExpr,
1983        zero_condition: Option<&SymBoolExpr>,
1984    ) -> Option<(SymExpr, SymExpr)> {
1985        let SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) = predicate.kind() else {
1986            return None;
1987        };
1988        for (quotient, expected) in [(left, right), (right, left)] {
1989            if let Some((divisor, other)) =
1990                self.checked_quotient_factors(quotient, expected, zero_condition)
1991            {
1992                // For divisor > 0, (divisor * other mod 2^256) / divisor == other
1993                // implies the true product fits. For divisor == 0 the product is zero.
1994                return Some((divisor.clone(), other.clone()));
1995            }
1996        }
1997        None
1998    }
1999
2000    fn has_non_wrapping_product(&self, left: &SymExpr, right: &SymExpr) -> bool {
2001        self.non_wrapping_products.contains(&(left.clone(), right.clone()))
2002            || self.non_wrapping_products.contains(&(right.clone(), left.clone()))
2003    }
2004
2005    pub(super) fn mul_cannot_overflow_256(&self, left: &SymExpr, right: &SymExpr) -> bool {
2006        if self.has_non_wrapping_product(left, right) {
2007            return true;
2008        }
2009        let mut intervals = HashMap::default();
2010        let mut remaining = MAX_LOCAL_ANALYSIS_NODES;
2011        if self
2012            .interval_cached(left, &mut intervals, &mut remaining)
2013            .zip(self.interval_cached(right, &mut intervals, &mut remaining))
2014            .is_some_and(|(left, right)| left.max.checked_mul(right.max).is_some())
2015        {
2016            return true;
2017        }
2018
2019        let mut bit_widths = HashMap::default();
2020        let mut remaining = MAX_LOCAL_ANALYSIS_NODES;
2021        self.unsigned_bits_cached(left, &mut bit_widths, &mut remaining)
2022            .zip(self.unsigned_bits_cached(right, &mut bit_widths, &mut remaining))
2023            .is_some_and(|(left, right)| left.saturating_add(right) <= 256)
2024    }
2025
2026    pub(super) fn unsigned_bits(&self, expr: &SymExpr) -> usize {
2027        let mut bit_widths = HashMap::default();
2028        let mut remaining = MAX_LOCAL_ANALYSIS_NODES;
2029        self.unsigned_bits_cached(expr, &mut bit_widths, &mut remaining).unwrap_or(256)
2030    }
2031
2032    fn unsigned_bits_cached(
2033        &self,
2034        expr: &SymExpr,
2035        bit_widths: &mut HashMap<SymExpr, usize>,
2036        remaining: &mut usize,
2037    ) -> Option<usize> {
2038        if let Some(bits) = bit_widths.get(expr) {
2039            return Some(*bits);
2040        }
2041        if *remaining == 0 {
2042            return None;
2043        }
2044        *remaining -= 1;
2045
2046        let bits = match expr.kind() {
2047            SymExprKind::Const(value) => value.bit_len().max(1),
2048            SymExprKind::Var(_)
2049            | SymExprKind::GasLeft(_)
2050            | SymExprKind::Keccak { .. }
2051            | SymExprKind::Hash { .. }
2052            | SymExprKind::Not(_) => 256,
2053            SymExprKind::BinOp(SymBinOp::And, left, right) => {
2054                if let Some(mask) = right.as_const() {
2055                    self.unsigned_bits_cached(left, bit_widths, remaining)?.min(mask.bit_len())
2056                } else {
2057                    256
2058                }
2059            }
2060            SymExprKind::BinOp(SymBinOp::Add, left, right) => self
2061                .unsigned_bits_cached(left, bit_widths, remaining)?
2062                .max(self.unsigned_bits_cached(right, bit_widths, remaining)?)
2063                .saturating_add(1)
2064                .min(256),
2065            SymExprKind::BinOp(SymBinOp::Mul, left, right) => self
2066                .unsigned_bits_cached(left, bit_widths, remaining)?
2067                .saturating_add(self.unsigned_bits_cached(right, bit_widths, remaining)?)
2068                .min(256),
2069            SymExprKind::BinOp(SymBinOp::UDiv, left, _) => {
2070                self.unsigned_bits_cached(left, bit_widths, remaining)?
2071            }
2072            SymExprKind::Ite(_, left, right) => self
2073                .unsigned_bits_cached(left, bit_widths, remaining)?
2074                .max(self.unsigned_bits_cached(right, bit_widths, remaining)?),
2075            _ => 256,
2076        };
2077
2078        let bits = self
2079            .upper_bounds
2080            .get(expr)
2081            .copied()
2082            .map(|bound| bits.min(bound.bit_len().max(1)))
2083            .unwrap_or(bits);
2084        bit_widths.insert(expr.clone(), bits);
2085        Some(bits)
2086    }
2087}