Skip to main content

foundry_evm_symbolic/runtime/expr/
bool.rs

1use super::{hashcons::HashConsed, *};
2use foundry_evm::revm::interpreter::instructions::i256::i256_cmp;
3
4/// Bounds both the number of distinct word nodes inspected by constant-ITE equality expansion and
5/// the unfolded size of the Boolean expression it could produce.
6const MAX_CONSTANT_ITE_EQ_NODES: usize = 128;
7const MAX_CONSTANT_ITE_EQ_UNFOLDED_NODES: usize = 8 * 1024;
8
9#[derive(Clone, PartialEq, Eq, Hash)]
10pub(crate) struct SymBoolExpr {
11    pub(in crate::runtime::expr) kind: HashConsed<SymBoolExprKind>,
12}
13
14impl fmt::Debug for SymBoolExpr {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        self.kind().fmt(f)
17    }
18}
19
20#[derive(Clone, Debug, PartialEq, Eq, Hash)]
21pub(in crate::runtime) enum SymBoolExprKind {
22    Const(bool),
23    Not(SymBoolExpr),
24    And(Arc<[SymBoolExpr]>),
25    Cmp(SymCmpOp, SymExpr, SymExpr),
26}
27
28impl SymBoolExpr {
29    #[inline]
30    pub(in crate::runtime) fn stable_hash_cmp(&self, other: &Self) -> std::cmp::Ordering {
31        self.kind.stable_hash_cmp(&other.kind)
32    }
33
34    pub(in crate::runtime) fn kind(&self) -> &SymBoolExprKind {
35        self.kind.value()
36    }
37
38    pub(in crate::runtime) fn from_kind(cx: &mut SymCx, kind: SymBoolExprKind) -> Self {
39        cx.mk_bool_kind(kind)
40    }
41
42    pub(crate) fn constant(cx: &mut SymCx, value: bool) -> Self {
43        cx.cached_bool(value)
44    }
45
46    pub(crate) fn cmp_word_const(
47        cx: &mut SymCx,
48        op: SymCmpOp,
49        word: &SymExpr,
50        value: U256,
51    ) -> Self {
52        if let Some(word) = word.as_const() {
53            Self::constant(cx, op.eval(word, value))
54        } else {
55            let value = SymExpr::constant(cx, value);
56            Self::cmp(cx, op, word.clone(), value)
57        }
58    }
59
60    pub(crate) fn eq_word_const(cx: &mut SymCx, word: &SymExpr, value: U256) -> Self {
61        if let Some(word) = word.as_const() {
62            Self::constant(cx, word == value)
63        } else {
64            let value = SymExpr::constant(cx, value);
65            Self::eq(cx, word.clone(), value)
66        }
67    }
68
69    pub(crate) fn eq(cx: &mut SymCx, left: SymExpr, right: SymExpr) -> Self {
70        Self::cmp(cx, SymCmpOp::Eq, left, right)
71    }
72
73    pub(crate) fn cmp(cx: &mut SymCx, op: SymCmpOp, left: SymExpr, right: SymExpr) -> Self {
74        if let (
75            SymExprKind::Ite(left_condition, left_then, left_else),
76            SymExprKind::Ite(right_condition, right_then, right_else),
77        ) = (left.kind(), right.kind())
78            && left_condition == right_condition
79            && let (Some(left_then), Some(left_else), Some(right_then), Some(right_else)) = (
80                left_then.as_const(),
81                left_else.as_const(),
82                right_then.as_const(),
83                right_else.as_const(),
84            )
85        {
86            // Compare aligned constant-arm ITEs pointwise without expanding either expression.
87            return match (op.eval(left_then, right_then), op.eval(left_else, right_else)) {
88                (true, true) => Self::constant(cx, true),
89                (false, false) => Self::constant(cx, false),
90                (true, false) => left_condition.clone(),
91                (false, true) => Self::not_bool(cx, left_condition.clone()),
92            };
93        }
94
95        match op {
96            SymCmpOp::Eq => {
97                if let Some(condition) = Self::ite_eq_arm(cx, &left, &right)
98                    .or_else(|| Self::ite_eq_arm(cx, &right, &left))
99                {
100                    return condition;
101                }
102                match (left.kind(), right.kind()) {
103                    // `a == a => true`.
104                    _ if left == right => Self::constant(cx, true),
105                    (SymExprKind::Const(left), SymExprKind::Const(right)) => {
106                        // `const == const => const`.
107                        Self::constant(cx, left == right)
108                    }
109                    (_, SymExprKind::Const(right_value)) => {
110                        if let Some(condition) = Self::bool_word_eq_const(cx, &left, *right_value) {
111                            return condition;
112                        }
113                        if let Some(left_value) = left.known_word() {
114                            // `known(a) == const => const`.
115                            return Self::constant(cx, left_value == *right_value);
116                        }
117                        // `a == b => ordered(a, b)`.
118                        let (left, right) = SymExpr::ordered_commutative_operands(left, right);
119                        Self::from_kind(cx, SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right))
120                    }
121                    (SymExprKind::Const(left_value), _) => {
122                        if let Some(condition) = Self::bool_word_eq_const(cx, &right, *left_value) {
123                            return condition;
124                        }
125                        if let Some(right_value) = right.known_word() {
126                            // `const == known(a) => const`.
127                            return Self::constant(cx, *left_value == right_value);
128                        }
129                        // `a == b => ordered(a, b)`.
130                        let (left, right) = SymExpr::ordered_commutative_operands(left, right);
131                        Self::from_kind(cx, SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right))
132                    }
133                    (
134                        SymExprKind::Keccak { len: left_len, bytes: left_bytes, .. },
135                        SymExprKind::Keccak { len: right_len, bytes: right_bytes, .. },
136                    ) if left_bytes.len() == right_bytes.len() => {
137                        // `keccak(a) == keccak(b) => len(a) == len(b) && bytes(a) == bytes(b)`.
138                        let mut conditions =
139                            vec![Self::eq(cx, left_len.clone(), right_len.clone())];
140                        conditions.extend(
141                            left_bytes
142                                .iter()
143                                .cloned()
144                                .zip(right_bytes.iter().cloned())
145                                .map(|(left, right)| Self::eq(cx, left, right)),
146                        );
147                        Self::and(cx, conditions)
148                    }
149                    (
150                        SymExprKind::Hash { algorithm: left_algorithm, bytes: left_bytes, .. },
151                        SymExprKind::Hash {
152                            algorithm: right_algorithm, bytes: right_bytes, ..
153                        },
154                    ) if left_algorithm == right_algorithm
155                        && left_bytes.len() == right_bytes.len() =>
156                    {
157                        // `hash(a) == hash(b) => bytes(a) == bytes(b)`.
158                        let conditions = left_bytes
159                            .iter()
160                            .cloned()
161                            .zip(right_bytes.iter().cloned())
162                            .map(|(left, right)| Self::eq(cx, left, right))
163                            .collect();
164                        Self::and(cx, conditions)
165                    }
166                    _ => {
167                        // `a == b => ordered(a, b)`.
168                        let (left, right) = SymExpr::ordered_commutative_operands(left, right);
169                        Self::from_kind(cx, SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right))
170                    }
171                }
172            }
173            SymCmpOp::Ult => match (left.kind(), right.kind()) {
174                // `a < a => false`.
175                _ if left == right => Self::constant(cx, false),
176                (SymExprKind::Const(left), SymExprKind::Const(right)) => {
177                    // `const < const => const`.
178                    Self::constant(cx, op.eval(*left, *right))
179                }
180                // `a < 0 => false`.
181                (_, SymExprKind::Const(value)) if value.is_zero() => Self::constant(cx, false),
182                // `MAX < a => false`.
183                (SymExprKind::Const(value), _) if *value == U256::MAX => Self::constant(cx, false),
184                // `a < a & low_mask => false`.
185                _ if low_masked_source_any(&right) == Some(&left) => Self::constant(cx, false),
186                _ => Self::from_kind(cx, SymBoolExprKind::Cmp(op, left, right)),
187            },
188            SymCmpOp::Ugt => match (left.kind(), right.kind()) {
189                // `a > a => false`.
190                _ if left == right => Self::constant(cx, false),
191                (SymExprKind::Const(left), SymExprKind::Const(right)) => {
192                    // `const > const => const`.
193                    Self::constant(cx, op.eval(*left, *right))
194                }
195                // `0 > a => false`.
196                (SymExprKind::Const(value), _) if value.is_zero() => Self::constant(cx, false),
197                // `a > MAX => false`.
198                (_, SymExprKind::Const(value)) if *value == U256::MAX => Self::constant(cx, false),
199                // `a & low_mask > a => false`.
200                _ if low_masked_source_any(&left) == Some(&right) => Self::constant(cx, false),
201                _ => Self::from_kind(cx, SymBoolExprKind::Cmp(op, left, right)),
202            },
203            SymCmpOp::Ule => match (left.kind(), right.kind()) {
204                // `a <= a => true`.
205                _ if left == right => Self::constant(cx, true),
206                (SymExprKind::Const(left), SymExprKind::Const(right)) => {
207                    // `const <= const => const`.
208                    Self::constant(cx, op.eval(*left, *right))
209                }
210                // `0 <= a => true`.
211                (SymExprKind::Const(value), _) if value.is_zero() => Self::constant(cx, true),
212                // `a <= MAX => true`.
213                (_, SymExprKind::Const(value)) if *value == U256::MAX => Self::constant(cx, true),
214                // `a & low_mask <= a => true`.
215                _ if low_masked_source_any(&left) == Some(&right) => Self::constant(cx, true),
216                _ => Self::from_kind(cx, SymBoolExprKind::Cmp(op, left, right)),
217            },
218            SymCmpOp::Uge => match (left.kind(), right.kind()) {
219                // `a >= a => true`.
220                _ if left == right => Self::constant(cx, true),
221                (SymExprKind::Const(left), SymExprKind::Const(right)) => {
222                    // `const >= const => const`.
223                    Self::constant(cx, op.eval(*left, *right))
224                }
225                // `a >= 0 => true`.
226                (_, SymExprKind::Const(value)) if value.is_zero() => Self::constant(cx, true),
227                // `MAX >= a => true`.
228                (SymExprKind::Const(value), _) if *value == U256::MAX => Self::constant(cx, true),
229                // `a >= a & low_mask => true`.
230                _ if low_masked_source_any(&right) == Some(&left) => Self::constant(cx, true),
231                _ => Self::from_kind(cx, SymBoolExprKind::Cmp(op, left, right)),
232            },
233            SymCmpOp::Slt | SymCmpOp::Sgt => match (left.kind(), right.kind()) {
234                // `a <s a => false`, `a >s a => false`.
235                _ if left == right => Self::constant(cx, false),
236                (SymExprKind::Const(left), SymExprKind::Const(right)) => {
237                    // `const <s const => const`.
238                    Self::constant(cx, op.eval(*left, *right))
239                }
240                _ => Self::from_kind(cx, SymBoolExprKind::Cmp(op, left, right)),
241            },
242        }
243    }
244
245    pub(crate) fn and(cx: &mut SymCx, values: Vec<Self>) -> Self {
246        let mut out = Vec::new();
247        for value in values {
248            match value.kind() {
249                // `true && a => a`.
250                SymBoolExprKind::Const(true) => {}
251                // `false && a => false`.
252                SymBoolExprKind::Const(false) => return Self::constant(cx, false),
253                // `(a && b) && c => a && b && c`.
254                SymBoolExprKind::And(values) => out.extend(values.iter().cloned()),
255                _ => out.push(value),
256            }
257        }
258        if out.is_empty() {
259            // `and() => true`.
260            Self::constant(cx, true)
261        } else if out.len() == 1 {
262            // `and(a) => a`.
263            out.pop().expect("single item exists")
264        } else {
265            Self::from_kind(cx, SymBoolExprKind::And(out.into()))
266        }
267    }
268
269    pub(crate) fn or(cx: &mut SymCx, values: Vec<Self>) -> Self {
270        let mut out = Vec::new();
271        for value in values {
272            match value.kind() {
273                // `false || a => a`.
274                SymBoolExprKind::Const(false) => {}
275                // `true || a => true`.
276                SymBoolExprKind::Const(true) => return Self::constant(cx, true),
277                _ => out.push(value),
278            }
279        }
280        if out.is_empty() {
281            // `or() => false`.
282            Self::constant(cx, false)
283        } else if out.len() == 1 {
284            // `or(a) => a`.
285            out.pop().expect("single item exists")
286        } else {
287            // `a || b => !(!a && !b)`.
288            let values = out.into_iter().map(|value| Self::not_bool(cx, value)).collect();
289            let and = Self::and(cx, values);
290            Self::not_bool(cx, and)
291        }
292    }
293
294    pub(crate) fn not_bool(cx: &mut SymCx, value: Self) -> Self {
295        match value.kind() {
296            // `!const => const`.
297            SymBoolExprKind::Const(value) => Self::constant(cx, !*value),
298            // `!!a => a`.
299            SymBoolExprKind::Not(value) => value.clone(),
300            _ => Self::from_kind(cx, SymBoolExprKind::Not(value)),
301        }
302    }
303
304    fn bool_word_eq_const(cx: &mut SymCx, word: &SymExpr, value: U256) -> Option<Self> {
305        let SymExprKind::Ite(condition, then_expr, else_expr) = word.kind() else { return None };
306        match (then_expr.as_const(), else_expr.as_const()) {
307            (Some(then_value), Some(else_value))
308                if then_value == U256::from(1) && else_value.is_zero() =>
309            {
310                Some(if value.is_zero() {
311                    Self::not_bool(cx, condition.clone())
312                } else if value == U256::from(1) {
313                    condition.clone()
314                } else {
315                    Self::constant(cx, false)
316                })
317            }
318            (Some(then_value), Some(else_value))
319                if then_value.is_zero() && else_value == U256::from(1) =>
320            {
321                Some(if value.is_zero() {
322                    condition.clone()
323                } else if value == U256::from(1) {
324                    Self::not_bool(cx, condition.clone())
325                } else {
326                    Self::constant(cx, false)
327                })
328            }
329            _ => None,
330        }
331    }
332
333    fn ite_eq_arm(cx: &mut SymCx, conditional: &SymExpr, expected: &SymExpr) -> Option<Self> {
334        let SymExprKind::Ite(condition, then_expr, else_expr) = conditional.kind() else {
335            return None;
336        };
337        if then_expr == expected {
338            let else_matches = Self::eq(cx, else_expr.clone(), expected.clone());
339            return Some(Self::or(cx, vec![condition.clone(), else_matches]));
340        }
341        if else_expr == expected {
342            let then_matches = Self::eq(cx, then_expr.clone(), expected.clone());
343            let condition = Self::not_bool(cx, condition.clone());
344            return Some(Self::or(cx, vec![condition, then_matches]));
345        }
346        if let Some(expected_value) = expected.as_const() {
347            let mut cost_cache = HashMap::default();
348            let mut remaining = MAX_CONSTANT_ITE_EQ_NODES;
349            if Self::constant_ite_eq_unfolded_nodes(conditional, &mut cost_cache, &mut remaining)
350                .is_none()
351            {
352                let (left, right) =
353                    SymExpr::ordered_commutative_operands(conditional.clone(), expected.clone());
354                return Some(Self::from_kind(cx, SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right)));
355            }
356
357            let mut cache = HashMap::default();
358            let mut remaining = MAX_CONSTANT_ITE_EQ_NODES;
359            return Self::constant_ite_eq(
360                cx,
361                conditional,
362                expected_value,
363                &mut cache,
364                &mut remaining,
365            );
366        }
367        None
368    }
369
370    /// Computes a conservative upper bound for the unfolded Boolean result without interning it.
371    ///
372    /// Cached child costs are deliberately added again at each use. This keeps construction of a
373    /// small shared word DAG from creating a Boolean DAG that becomes exponential when a later
374    /// consumer traverses occurrences instead of identities.
375    fn constant_ite_eq_unfolded_nodes(
376        expr: &SymExpr,
377        cache: &mut HashMap<SymExpr, Option<usize>>,
378        remaining: &mut usize,
379    ) -> Option<usize> {
380        if let Some(cached) = cache.get(expr) {
381            return *cached;
382        }
383        if *remaining == 0 {
384            cache.insert(expr.clone(), None);
385            return None;
386        }
387        *remaining -= 1;
388
389        let result = match expr.kind() {
390            SymExprKind::Const(_) => Some(1),
391            SymExprKind::Ite(condition, then_expr, else_expr) => {
392                let then_nodes = Self::constant_ite_eq_unfolded_nodes(then_expr, cache, remaining)?;
393                let else_nodes = Self::constant_ite_eq_unfolded_nodes(else_expr, cache, remaining)?;
394
395                let mut pending = vec![condition];
396                let mut condition_nodes = 0usize;
397                while let Some(condition) = pending.pop() {
398                    condition_nodes = condition_nodes.checked_add(1)?;
399                    if condition_nodes > MAX_CONSTANT_ITE_EQ_UNFOLDED_NODES {
400                        return None;
401                    }
402                    match condition.kind() {
403                        SymBoolExprKind::Not(value) => pending.push(value),
404                        SymBoolExprKind::And(values) => pending.extend(values.iter()),
405                        SymBoolExprKind::Const(_) | SymBoolExprKind::Cmp(_, _, _) => {}
406                    }
407                }
408
409                // Two selected branches and the `or` encoding add at most seven Boolean wrapper
410                // occurrences around both children and two occurrences of the condition.
411                then_nodes
412                    .checked_add(else_nodes)
413                    .and_then(|nodes| nodes.checked_add(2 * condition_nodes))
414                    .and_then(|nodes| nodes.checked_add(7))
415                    .filter(|nodes| *nodes <= MAX_CONSTANT_ITE_EQ_UNFOLDED_NODES)
416            }
417            _ => None,
418        };
419        cache.insert(expr.clone(), result);
420        result
421    }
422
423    fn constant_ite_eq(
424        cx: &mut SymCx,
425        expr: &SymExpr,
426        expected: U256,
427        cache: &mut HashMap<SymExpr, Option<Self>>,
428        remaining: &mut usize,
429    ) -> Option<Self> {
430        if let Some(cached) = cache.get(expr) {
431            return cached.clone();
432        }
433        if *remaining == 0 {
434            cache.insert(expr.clone(), None);
435            return None;
436        }
437        *remaining -= 1;
438
439        let result = match expr.kind() {
440            SymExprKind::Const(value) => Some(Self::constant(cx, *value == expected)),
441            SymExprKind::Ite(condition, then_expr, else_expr) => {
442                let condition = condition.clone();
443                let then_matches = Self::constant_ite_eq(cx, then_expr, expected, cache, remaining);
444                let else_matches = Self::constant_ite_eq(cx, else_expr, expected, cache, remaining);
445                match (then_matches, else_matches) {
446                    (Some(then_matches), Some(else_matches)) => {
447                        let then_selected =
448                            Self::and_ite_branch(cx, condition.clone(), then_matches);
449                        let condition = Self::not_bool(cx, condition);
450                        let else_selected = Self::and_ite_branch(cx, condition, else_matches);
451                        Some(Self::or(cx, vec![then_selected, else_selected]))
452                    }
453                    _ => None,
454                }
455            }
456            _ => None,
457        };
458        cache.insert(expr.clone(), result.clone());
459        result
460    }
461
462    /// Builds one selected ITE branch without flattening a cached child conjunction.
463    ///
464    /// Flattening here makes a linear ITE chain retain conjunctions of lengths `1..n` in the
465    /// per-call cache. Keeping this pair binary preserves the shared DAG and leaves flattening to
466    /// solver constraint normalization, where only the final expression is expanded.
467    fn and_ite_branch(cx: &mut SymCx, condition: Self, branch: Self) -> Self {
468        match (condition.as_const(), branch.as_const()) {
469            (Some(false), _) | (_, Some(false)) => Self::constant(cx, false),
470            (Some(true), _) => branch,
471            (_, Some(true)) => condition,
472            _ if condition == branch => condition,
473            _ => Self::from_kind(cx, SymBoolExprKind::And(vec![condition, branch].into())),
474        }
475    }
476
477    pub(crate) fn as_const(&self) -> Option<bool> {
478        match self.kind() {
479            SymBoolExprKind::Const(value) => Some(*value),
480            _ => None,
481        }
482    }
483
484    pub(in crate::runtime) fn zero_check_operand(&self) -> Option<&SymExpr> {
485        match self.kind() {
486            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right)
487                if right.as_const().is_some_and(|value| value.is_zero()) =>
488            {
489                Some(left)
490            }
491            _ => None,
492        }
493    }
494
495    pub(crate) fn contains_keccak(&self) -> bool {
496        self.visit_bool(|expr| matches!(expr.kind(), SymExprKind::Keccak { .. }))
497    }
498
499    pub(crate) fn contains_gasleft(&self) -> bool {
500        self.visit_bool(|expr| matches!(expr.kind(), SymExprKind::GasLeft(_)))
501    }
502
503    pub(crate) fn contains_udiv(&self) -> bool {
504        self.visit_bool(|expr| expr.contains_udiv())
505    }
506
507    pub(crate) fn implies_unsigned_less_or_equal(
508        &self,
509        expected: bool,
510        left: &SymExpr,
511        right: &SymExpr,
512        remaining: &mut usize,
513    ) -> bool {
514        if left == right {
515            return true;
516        }
517        let Some(next) = remaining.checked_sub(1) else { return false };
518        *remaining = next;
519
520        match self.kind() {
521            SymBoolExprKind::Not(value) => {
522                value.implies_unsigned_less_or_equal(!expected, left, right, remaining)
523            }
524            SymBoolExprKind::And(values) if expected => values
525                .iter()
526                .any(|value| value.implies_unsigned_less_or_equal(true, left, right, remaining)),
527            SymBoolExprKind::Cmp(op, fact_left, fact_right) => match (*op, expected) {
528                (SymCmpOp::Ult | SymCmpOp::Ule, true) => fact_left == left && fact_right == right,
529                (SymCmpOp::Uge | SymCmpOp::Ugt, true) => fact_right == left && fact_left == right,
530                (SymCmpOp::Ult | SymCmpOp::Ule, false) => fact_right == left && fact_left == right,
531                (SymCmpOp::Uge | SymCmpOp::Ugt, false) => fact_left == left && fact_right == right,
532                (SymCmpOp::Eq, true) => {
533                    (fact_left == left && fact_right == right)
534                        || (fact_right == left && fact_left == right)
535                }
536                (SymCmpOp::Eq | SymCmpOp::Slt | SymCmpOp::Sgt, _) => false,
537            },
538            SymBoolExprKind::Const(_) | SymBoolExprKind::And(_) => false,
539        }
540    }
541
542    pub(crate) fn forces_expr_const_with_context(
543        &self,
544        expr: &SymExpr,
545        context: &[Self],
546    ) -> Option<U256> {
547        match self.kind() {
548            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => match right.kind() {
549                SymExprKind::Const(value) => left.equality_forces_const(*value, expr, context),
550                _ => None,
551            },
552            SymBoolExprKind::Not(value) => match value.kind() {
553                SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => match right.kind() {
554                    SymExprKind::Const(value) if value.is_zero() => {
555                        left.nonzero_forces_const(expr, context)
556                    }
557                    _ => None,
558                },
559                SymBoolExprKind::Not(value) => value.forces_expr_const_with_context(expr, context),
560                _ => None,
561            },
562            SymBoolExprKind::And(values) => {
563                values.iter().find_map(|value| value.forces_expr_const_with_context(expr, context))
564            }
565            _ => None,
566        }
567    }
568
569    pub(crate) fn upper_bound_usize(&self, expr: &SymExpr) -> Option<usize> {
570        match self.kind() {
571            SymBoolExprKind::Const(_) | SymBoolExprKind::Not(_) => None,
572            SymBoolExprKind::And(values) => {
573                let mut bound: Option<usize> = None;
574                for value in values.iter() {
575                    if let Some(candidate) = value.upper_bound_usize(expr) {
576                        bound = Some(bound.map_or(candidate, |bound| bound.min(candidate)));
577                    }
578                }
579                bound
580            }
581            SymBoolExprKind::Cmp(op, left, right) => {
582                if *op == SymCmpOp::Eq {
583                    return match (left == expr, right == expr) {
584                        (true, _) => right.as_const().and_then(|value| usize::try_from(value).ok()),
585                        (_, true) => left.as_const().and_then(|value| usize::try_from(value).ok()),
586                        _ => None,
587                    };
588                }
589                if left == expr {
590                    match *op {
591                        SymCmpOp::Ult => right
592                            .as_const()
593                            .and_then(|bound| (!bound.is_zero()).then(|| bound - U256::from(1)))
594                            .and_then(|value| usize::try_from(value).ok()),
595                        SymCmpOp::Ule => {
596                            right.as_const().and_then(|value| usize::try_from(value).ok())
597                        }
598                        _ => None,
599                    }
600                } else if right == expr {
601                    match *op {
602                        SymCmpOp::Ugt => left
603                            .as_const()
604                            .and_then(|bound| (!bound.is_zero()).then(|| bound - U256::from(1)))
605                            .and_then(|value| usize::try_from(value).ok()),
606                        SymCmpOp::Uge => {
607                            left.as_const().and_then(|value| usize::try_from(value).ok())
608                        }
609                        _ => None,
610                    }
611                } else {
612                    None
613                }
614            }
615        }
616    }
617
618    pub(crate) fn eval_model<M: SymbolicModelLookup + ?Sized>(
619        &self,
620        model: &M,
621    ) -> Result<bool, SymbolicError> {
622        ModelEvaluator::new(model).eval_bool(self)
623    }
624
625    pub(crate) fn eval_model_if_complete<M: SymbolicModelLookup + ?Sized>(
626        &self,
627        model: &M,
628    ) -> Result<Option<bool>, SymbolicError> {
629        let mut vars = SymbolicVars::default();
630        self.collect_eval_vars(&mut vars);
631        if vars.iter().copied().all(|var| model.contains_name(var)) {
632            self.eval_model(model).map(Some)
633        } else {
634            Ok(None)
635        }
636    }
637
638    /// Visits all word expressions contained in this boolean expression.
639    pub(crate) fn visit_exprs<B>(
640        &self,
641        visitor: &mut impl FnMut(&SymExpr) -> ControlFlow<B>,
642    ) -> ControlFlow<B> {
643        match self.kind() {
644            SymBoolExprKind::Const(_) => {}
645            SymBoolExprKind::Not(value) => value.visit_exprs(visitor)?,
646            SymBoolExprKind::And(values) => {
647                for value in values.iter() {
648                    value.visit_exprs(visitor)?;
649                }
650            }
651            SymBoolExprKind::Cmp(_, left, right) => {
652                left.visit(visitor)?;
653                right.visit(visitor)?;
654            }
655        }
656        ControlFlow::Continue(())
657    }
658
659    pub(crate) fn visit_bool(&self, mut visitor: impl FnMut(&SymExpr) -> bool) -> bool {
660        self.visit_exprs(&mut |expr| {
661            if visitor(expr) { ControlFlow::Break(()) } else { ControlFlow::Continue(()) }
662        })
663        .is_break()
664    }
665
666    /// Visits each distinct word node at most once.
667    pub(crate) fn visit_unique_bool(&self, mut visitor: impl FnMut(&SymExpr) -> bool) -> bool {
668        let mut pending_bools = vec![self.clone()];
669        let mut pending_words = Vec::new();
670        let mut visited_bools = HashSet::<Self>::default();
671        let mut visited_words = HashSet::<SymExpr>::default();
672
673        loop {
674            if let Some(expr) = pending_bools.pop() {
675                if !visited_bools.insert(expr.clone()) {
676                    continue;
677                }
678                match expr.kind() {
679                    SymBoolExprKind::Const(_) => {}
680                    SymBoolExprKind::Not(value) => pending_bools.push(value.clone()),
681                    SymBoolExprKind::And(values) => {
682                        pending_bools.extend(values.iter().cloned());
683                    }
684                    SymBoolExprKind::Cmp(_, left, right) => {
685                        pending_words.push(left.clone());
686                        pending_words.push(right.clone());
687                    }
688                }
689                continue;
690            }
691
692            let Some(expr) = pending_words.pop() else { return false };
693            if !visited_words.insert(expr.clone()) {
694                continue;
695            }
696            if visitor(&expr) {
697                return true;
698            }
699            match expr.kind() {
700                SymExprKind::Const(_) | SymExprKind::Var(_) | SymExprKind::GasLeft(_) => {}
701                SymExprKind::Keccak { len, bytes, .. } => {
702                    pending_words.push(len.clone());
703                    pending_words.extend(bytes.iter().cloned());
704                }
705                SymExprKind::Hash { bytes, .. } => {
706                    pending_words.extend(bytes.iter().cloned());
707                }
708                SymExprKind::Not(value) => pending_words.push(value.clone()),
709                SymExprKind::BinOp(_, left, right) => {
710                    pending_words.push(left.clone());
711                    pending_words.push(right.clone());
712                }
713                SymExprKind::TernOp(_, left, right, modulus) => {
714                    pending_words.push(left.clone());
715                    pending_words.push(right.clone());
716                    pending_words.push(modulus.clone());
717                }
718                SymExprKind::Ite(condition, left, right) => {
719                    pending_bools.push(condition.clone());
720                    pending_words.push(left.clone());
721                    pending_words.push(right.clone());
722                }
723            }
724        }
725    }
726
727    /// Rewrites each distinct Boolean node once in bottom-up order.
728    ///
729    /// The folder must return the same result for every occurrence of one hash-consed node.
730    pub(crate) fn fold(
731        &self,
732        cx: &mut SymCx,
733        folder: &mut impl FnMut(&mut SymCx, Self) -> Self,
734    ) -> Self {
735        let mut folded = HashMap::default();
736        self.fold_cached(cx, folder, &mut folded)
737    }
738
739    fn fold_cached<'a>(
740        &'a self,
741        cx: &mut SymCx,
742        folder: &mut impl FnMut(&mut SymCx, Self) -> Self,
743        folded: &mut HashMap<&'a Self, Self>,
744    ) -> Self {
745        if let Some(expr) = folded.get(self) {
746            return expr.clone();
747        }
748
749        let expr = match self.kind() {
750            SymBoolExprKind::Const(_) => self.clone(),
751            SymBoolExprKind::Not(value) => {
752                let value = value.fold_cached(cx, folder, folded);
753                Self::not_bool(cx, value)
754            }
755            SymBoolExprKind::And(values) => {
756                let values =
757                    values.iter().map(|value| value.fold_cached(cx, folder, folded)).collect();
758                Self::and(cx, values)
759            }
760            SymBoolExprKind::Cmp(op, left, right) => {
761                Self::cmp(cx, *op, left.clone(), right.clone())
762            }
763        };
764        let expr = folder(cx, expr);
765        folded.insert(self, expr.clone());
766        expr
767    }
768
769    /// Rewrites each distinct word node once while rebuilding this Boolean expression.
770    ///
771    /// The folder must return the same result for every occurrence of one hash-consed node.
772    pub(crate) fn fold_exprs(
773        &self,
774        cx: &mut SymCx,
775        folder: &mut impl FnMut(&mut SymCx, SymExpr) -> SymExpr,
776    ) -> Self {
777        let mut folded = ExpressionFoldCache::default();
778        self.fold_exprs_cached(cx, folder, &mut folded)
779    }
780
781    pub(in crate::runtime::expr) fn fold_exprs_cached<'a>(
782        &'a self,
783        cx: &mut SymCx,
784        folder: &mut impl FnMut(&mut SymCx, SymExpr) -> SymExpr,
785        folded: &mut ExpressionFoldCache<'a>,
786    ) -> Self {
787        if let Some(expr) = folded.bools.get(self) {
788            return expr.clone();
789        }
790
791        let expr = match self.kind() {
792            SymBoolExprKind::Const(_) => self.clone(),
793            SymBoolExprKind::Not(value) => {
794                let value = value.fold_exprs_cached(cx, folder, folded);
795                Self::not_bool(cx, value)
796            }
797            SymBoolExprKind::And(values) => {
798                let values = values
799                    .iter()
800                    .map(|value| value.fold_exprs_cached(cx, folder, folded))
801                    .collect();
802                Self::and(cx, values)
803            }
804            SymBoolExprKind::Cmp(op, left, right) => {
805                let left = left.fold_cached(cx, folder, folded);
806                let right = right.fold_cached(cx, folder, folded);
807                Self::cmp(cx, *op, left, right)
808            }
809        };
810        folded.bools.insert(self, expr.clone());
811        expr
812    }
813
814    #[cfg(test)]
815    pub(crate) fn raw_and(cx: &mut SymCx, values: Vec<Self>) -> Self {
816        Self::from_kind(cx, SymBoolExprKind::And(values.into()))
817    }
818
819    pub(crate) fn cmp_word_expr(
820        cx: &mut SymCx,
821        op: SymCmpOp,
822        word: &SymExpr,
823        expr: SymExpr,
824    ) -> Self {
825        Self::cmp(cx, op, word.clone(), expr)
826    }
827
828    pub(crate) fn not(self, cx: &mut SymCx) -> Self {
829        Self::not_bool(cx, self)
830    }
831
832    pub(crate) fn collect_vars(&self, vars: &mut SymbolicVars) {
833        let _ = self.visit_exprs(&mut |expr| {
834            if let Some(var) = expr.kind().get_var() {
835                vars.insert(var);
836            }
837            ControlFlow::<()>::Continue(())
838        });
839    }
840
841    pub(crate) fn collect_eval_vars(&self, vars: &mut SymbolicVars) {
842        let _ = self.visit_exprs(&mut |expr| {
843            if let Some(var) = expr.kind().get_eval_var() {
844                vars.insert(var);
845            }
846            ControlFlow::<()>::Continue(())
847        });
848    }
849
850    pub(crate) fn smt(&self, cx: &SymCx) -> String {
851        let mut smt = String::new();
852        self.write_smt(cx, &mut smt);
853        smt
854    }
855
856    pub(in crate::runtime::expr) fn write_smt(&self, cx: &SymCx, out: &mut String) {
857        match self.kind() {
858            SymBoolExprKind::Const(value) => out.push_str(if *value { "true" } else { "false" }),
859            SymBoolExprKind::Not(value) => {
860                out.push_str("(not ");
861                value.write_smt(cx, out);
862                out.push(')');
863            }
864            SymBoolExprKind::And(values) => {
865                out.push_str("(and");
866                for value in values.iter() {
867                    out.push(' ');
868                    value.write_smt(cx, out);
869                }
870                out.push(')');
871            }
872            SymBoolExprKind::Cmp(op, left, right) => {
873                let _ = write!(out, "({} ", op.smt());
874                left.write_smt(cx, out);
875                out.push(' ');
876                right.write_smt(cx, out);
877                out.push(')');
878            }
879        }
880    }
881}
882
883#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
884pub(crate) enum SymCmpOp {
885    Eq,
886    Ult,
887    Ugt,
888    Ule,
889    Uge,
890    Slt,
891    Sgt,
892}
893
894impl SymCmpOp {
895    pub(crate) const fn smt(self) -> &'static str {
896        match self {
897            Self::Eq => "=",
898            Self::Ult => "bvult",
899            Self::Ugt => "bvugt",
900            Self::Ule => "bvule",
901            Self::Uge => "bvuge",
902            Self::Slt => "bvslt",
903            Self::Sgt => "bvsgt",
904        }
905    }
906
907    pub(crate) fn eval(self, left: U256, right: U256) -> bool {
908        match self {
909            Self::Eq => left == right,
910            Self::Ult => left < right,
911            Self::Ugt => left > right,
912            Self::Ule => left <= right,
913            Self::Uge => left >= right,
914            Self::Slt => i256_cmp(&left, &right).is_lt(),
915            Self::Sgt => i256_cmp(&left, &right).is_gt(),
916        }
917    }
918}
919
920#[cfg(test)]
921mod tests {
922    use super::*;
923
924    #[test]
925    fn unique_word_visitor_deduplicates_shared_dag() {
926        let mut cx = SymCx::new();
927        let mut shared = SymExpr::var(&mut cx, "shared");
928        for _ in 0..16 {
929            shared = SymExpr::binop(&mut cx, SymBinOp::Add, shared.clone(), shared);
930        }
931        let zero = SymExpr::zero(&mut cx);
932        let condition = SymBoolExpr::eq(&mut cx, shared, zero);
933        let mut visits = 0;
934
935        assert!(!condition.visit_unique_bool(|_| {
936            visits += 1;
937            false
938        }));
939        assert_eq!(visits, 18);
940    }
941
942    #[test]
943    fn constant_ite_equality_rejects_exponential_shared_dag() {
944        let mut cx = SymCx::new();
945        let x = SymExpr::var(&mut cx, "x");
946        let y = SymExpr::var(&mut cx, "y");
947        let first = SymBoolExpr::cmp(&mut cx, SymCmpOp::Ult, x.clone(), y.clone());
948        let second = SymBoolExpr::cmp(&mut cx, SymCmpOp::Ugt, x.clone(), y.clone());
949        let third = SymBoolExpr::cmp(&mut cx, SymCmpOp::Eq, x, y);
950        let zero = SymExpr::zero(&mut cx);
951        let one = SymExpr::one(&mut cx);
952        let mut shared = SymExpr::ite(&mut cx, first.clone(), zero.clone(), one.clone());
953
954        for _ in 0..32 {
955            let left = SymExpr::ite(&mut cx, first.clone(), shared.clone(), zero.clone());
956            let right = SymExpr::ite(&mut cx, second.clone(), shared.clone(), one.clone());
957            shared = SymExpr::ite(&mut cx, third.clone(), left, right);
958        }
959
960        let raw = SymBoolExpr::from_kind(
961            &mut cx,
962            SymBoolExprKind::Cmp(SymCmpOp::Eq, shared.clone(), one.clone()),
963        );
964        let expanded = SymBoolExpr::eq(&mut cx, shared, one);
965        assert_eq!(expanded, raw);
966    }
967
968    #[test]
969    fn constant_ite_equality_keeps_linear_chain_linear() {
970        let mut cx = SymCx::new();
971        let zero = SymExpr::zero(&mut cx);
972        let one = SymExpr::one(&mut cx);
973        let mut value = zero.clone();
974        for index in 0..64 {
975            let selector = SymExpr::var(&mut cx, &format!("selector_{index}"));
976            let condition = SymBoolExpr::eq_word_const(&mut cx, &selector, U256::ZERO);
977            value = SymExpr::ite(&mut cx, condition, value, one.clone());
978        }
979
980        let expanded = SymBoolExpr::eq(&mut cx, value, zero);
981        let mut pending = vec![expanded];
982        let mut visited = HashSet::<SymBoolExpr>::default();
983        while let Some(expr) = pending.pop() {
984            if !visited.insert(expr.clone()) {
985                continue;
986            }
987            match expr.kind() {
988                SymBoolExprKind::Not(value) => pending.push(value.clone()),
989                SymBoolExprKind::And(values) => {
990                    assert!(values.len() <= 2);
991                    pending.extend(values.iter().cloned());
992                }
993                SymBoolExprKind::Const(_) | SymBoolExprKind::Cmp(_, _, _) => {}
994            }
995        }
996        assert!(visited.len() < 2 * 64);
997    }
998
999    #[test]
1000    fn constant_ite_equality_stops_at_expansion_budget() {
1001        let mut cx = SymCx::new();
1002        let zero = SymExpr::zero(&mut cx);
1003        let one = SymExpr::one(&mut cx);
1004        let mut value = zero;
1005        for index in 0..MAX_CONSTANT_ITE_EQ_NODES {
1006            let selector = SymExpr::var(&mut cx, &format!("selector_{index}"));
1007            let condition = SymBoolExpr::eq_word_const(&mut cx, &selector, U256::ZERO);
1008            value = SymExpr::ite(&mut cx, condition, value, one.clone());
1009        }
1010        let two = SymExpr::constant(&mut cx, U256::from(2));
1011        let comparison = SymBoolExpr::eq(&mut cx, value, two);
1012
1013        assert!(matches!(comparison.kind(), SymBoolExprKind::Cmp(SymCmpOp::Eq, _, _)));
1014    }
1015}