Skip to main content

foundry_evm_symbolic/runtime/solver/fallback/
mod.rs

1//! Bounded fallback model generation for hard arithmetic constraints.
2
3use super::*;
4
5impl SymBoolExpr {
6    pub(crate) fn contains_hard_arith(&self) -> bool {
7        self.visit_bool(is_hard_arith_node)
8    }
9
10    fn contains_symbolic_hash(&self) -> bool {
11        self.visit_bool(|expr| matches!(expr.kind(), SymExprKind::Hash { .. }))
12    }
13}
14
15impl SymExpr {
16    fn contains_var(&self) -> bool {
17        self.visit_bool(|expr| {
18            matches!(
19                expr.kind(),
20                SymExprKind::Var(_) | SymExprKind::Keccak { .. } | SymExprKind::Hash { .. }
21            )
22        })
23    }
24}
25
26fn is_hard_arith_node(expr: &SymExpr) -> bool {
27    match expr.kind() {
28        SymExprKind::BinOp(SymBinOp::Mul, left, right) => {
29            left.contains_var() && right.contains_var()
30        }
31        SymExprKind::BinOp(
32            SymBinOp::UDiv | SymBinOp::URem | SymBinOp::SDiv | SymBinOp::SRem,
33            left,
34            right,
35        ) => left.contains_var() || right.contains_var(),
36        SymExprKind::TernOp(_, left, right, modulus) => {
37            left.contains_var() || right.contains_var() || modulus.contains_var()
38        }
39        _ => false,
40    }
41}
42
43/// Returns whether local hard-arithmetic search should run before asking the solver.
44pub(crate) fn constraints_prefer_hard_arith_fallback_first(
45    cx: &SymCx,
46    constraints: &[SymBoolExpr],
47) -> bool {
48    if !constraints.iter().any(SymBoolExpr::contains_hard_arith)
49        || constraints.iter().any(SymBoolExpr::contains_symbolic_hash)
50    {
51        return false;
52    }
53
54    let mut vars = SymbolicVars::default();
55    for constraint in constraints {
56        collect_bool_fallback_vars(constraint, &mut vars);
57    }
58    let vars = fallback_search_vars(cx, vars, constraints);
59    !vars.is_empty() && vars.len() <= HARD_ARITH_FALLBACK_MAX_VARS
60}
61
62pub(crate) fn hard_arith_fallback_model(
63    cx: &SymCx,
64    constraints: &[SymBoolExpr],
65) -> Option<SymbolicModel> {
66    if !constraints.iter().any(SymBoolExpr::contains_hard_arith)
67        || constraints.iter().any(SymBoolExpr::contains_symbolic_hash)
68    {
69        return None;
70    }
71
72    let mut vars = SymbolicVars::default();
73    let mut constants = HashSet::<U256>::default();
74    for constraint in constraints {
75        collect_bool_fallback_vars(constraint, &mut vars);
76        collect_bool_constants(constraint, &mut constants);
77    }
78    let mut constants = constants.into_iter().collect::<Vec<_>>();
79    constants.sort_unstable();
80    let vars = fallback_search_vars(cx, vars, constraints);
81    if vars.is_empty() || vars.len() > HARD_ARITH_FALLBACK_MAX_VARS {
82        return None;
83    }
84
85    let candidates = vars
86        .iter()
87        .map(|var| fallback_candidates_for_var(var, constraints, &constants))
88        .collect::<Option<Vec<_>>>()?;
89    let searched_vars = vars.iter().copied().collect::<SymbolicVars>();
90    let constraint_vars = constraints
91        .iter()
92        .map(|constraint| {
93            let mut vars = SymbolicVars::default();
94            constraint.collect_vars(&mut vars);
95            vars
96        })
97        .collect::<Vec<_>>();
98    let mut model = SymbolicModel::default();
99    let mut assignments = 0usize;
100    let search = FallbackSearch {
101        constraints,
102        constraint_vars: &constraint_vars,
103        searched_vars: &searched_vars,
104        vars: &vars,
105        candidates: &candidates,
106        max_assignments: HARD_ARITH_FALLBACK_MAX_ASSIGNMENTS,
107    };
108    search.model(0, &mut model, &mut assignments)
109}
110
111// Constructive checked-multiply modeling is optional. Bound repeated support scans to keep a miss
112// from consuming more work than the solver fallback it is intended to avoid.
113const MAX_CHECKED_MUL_SUPPORT_VISITS: usize = 256;
114
115/// Constructs and validates a concrete model for a checked-multiply guard branch.
116///
117/// Solidity's guard is `x == 0 || (x * y) / x == y`. The assignments below represent its
118/// semantic cases directly: the zero disjunct, a nonzero exact product, and wrapping products in
119/// either operand order. Simple support constraints are completed first so an exact operand value
120/// from the path is preserved instead of being overwritten by the semantic default. This does not
121/// perform the generic bounded candidate search, and a model is returned only when it satisfies
122/// every original constraint.
123pub(super) fn checked_mul_guard_branch_model(
124    cx: &SymCx,
125    constraints: &[SymBoolExpr],
126    original_constraints: &[SymBoolExpr],
127    replayable_storage: &SymbolicVars,
128) -> Option<SymbolicModel> {
129    let mut eval_vars = SymbolicVars::default();
130    for constraint in original_constraints {
131        constraint.collect_eval_vars(&mut eval_vars);
132    }
133    if eval_vars
134        .iter()
135        .any(|var| !cx.is_replayable_input(*var) && !replayable_storage.contains(var))
136    {
137        return None;
138    }
139
140    let mut remaining_support_visits = MAX_CHECKED_MUL_SUPPORT_VISITS;
141    let mut candidates = Vec::new();
142    let mut seen = HashSet::<&SymBoolExpr>::default();
143    let mut pending = Vec::new();
144    for constraint in constraints {
145        if !seen.insert(constraint) {
146            continue;
147        }
148        if remaining_support_visits == 0 {
149            return None;
150        }
151        remaining_support_visits -= 1;
152        if let Some(candidate) = checked_mul_guard_branch(constraint) {
153            candidates.push(candidate);
154        }
155        match constraint.kind() {
156            SymBoolExprKind::Not(inner) => pending.push(inner),
157            SymBoolExprKind::And(values) => pending.extend(values.iter()),
158            SymBoolExprKind::Const(_) | SymBoolExprKind::Cmp(_, _, _) => {}
159        }
160    }
161
162    let mut nested = Vec::new();
163    while let Some(constraint) = pending.pop() {
164        if !seen.insert(constraint) {
165            continue;
166        }
167        if remaining_support_visits == 0 {
168            return None;
169        }
170        remaining_support_visits -= 1;
171        nested.push(constraint);
172        match constraint.kind() {
173            SymBoolExprKind::Not(inner) => pending.push(inner),
174            SymBoolExprKind::And(values) => pending.extend(values.iter()),
175            SymBoolExprKind::Const(_) | SymBoolExprKind::Cmp(_, _, _) => {}
176        }
177    }
178    for constraint in nested.into_iter().rev() {
179        if let Some(candidate) = checked_mul_guard_branch(constraint) {
180            candidates.push(candidate);
181        }
182    }
183
184    for (zero_operand, expected, guard_is_true) in candidates {
185        let assignments = if guard_is_true {
186            [(U256::ZERO, U256::ZERO), (U256::ONE, U256::ONE)]
187        } else {
188            [(U256::MAX, U256::from(2)), (U256::from(2), U256::MAX)]
189        };
190        for (zero_default, expected_default) in assignments {
191            let seed_orders = [
192                [(&zero_operand, zero_default), (&expected, expected_default)],
193                [(&expected, expected_default), (&zero_operand, zero_default)],
194            ];
195            for seeds in seed_orders {
196                let mut model = SymbolicModel::default();
197                if !propagate_fallback_support_constraints(
198                    constraints,
199                    &mut model,
200                    &mut remaining_support_visits,
201                ) {
202                    if remaining_support_visits == 0 {
203                        return None;
204                    }
205                    continue;
206                }
207                let mut valid = true;
208                for (operand, default) in seeds {
209                    let assigned = match operand.eval_model_if_complete(&model) {
210                        Ok(Some(_)) => true,
211                        Ok(None) => operand.assign_model_value(&mut model, default),
212                        Err(_) => false,
213                    };
214                    if !assigned {
215                        valid = false;
216                        break;
217                    }
218                    if !propagate_fallback_support_constraints(
219                        constraints,
220                        &mut model,
221                        &mut remaining_support_visits,
222                    ) {
223                        if remaining_support_visits == 0 {
224                            return None;
225                        }
226                        valid = false;
227                        break;
228                    }
229                }
230                if valid {
231                    if eval_vars.iter().all(|var| model.contains_name(*var)) {
232                        let valid = original_constraints.iter().all(|constraint| {
233                            charge_support_constraint(constraint, &mut remaining_support_visits)
234                                && constraint.eval_model(&model).unwrap_or(false)
235                        });
236                        if valid {
237                            return Some(model);
238                        }
239                        if remaining_support_visits == 0 {
240                            return None;
241                        }
242                        continue;
243                    }
244                    if complete_fallback_support_model(
245                        constraints,
246                        &mut model,
247                        &mut remaining_support_visits,
248                    ) && complete_model_with_zeroes(
249                        original_constraints,
250                        &mut model,
251                        &mut remaining_support_visits,
252                    ) {
253                        let valid = original_constraints.iter().all(|constraint| {
254                            charge_support_constraint(constraint, &mut remaining_support_visits)
255                                && constraint.eval_model(&model).unwrap_or(false)
256                        });
257                        if valid {
258                            return Some(model);
259                        }
260                    }
261                    if remaining_support_visits == 0 {
262                        return None;
263                    }
264                }
265            }
266        }
267    }
268    None
269}
270
271fn checked_mul_guard_branch(constraint: &SymBoolExpr) -> Option<(SymExpr, SymExpr, bool)> {
272    match constraint.kind() {
273        SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => {
274            checked_mul_guard_word_comparison(left, right)
275                .map(|(zero_operand, expected)| (zero_operand, expected, false))
276        }
277        SymBoolExprKind::Not(inner) => match inner.kind() {
278            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => {
279                checked_mul_guard_word_comparison(left, right)
280                    .map(|(zero_operand, expected)| (zero_operand, expected, true))
281            }
282            SymBoolExprKind::And(values) => checked_mul_guard_conjunction(values)
283                .map(|(zero_operand, expected)| (zero_operand, expected, true)),
284            _ => None,
285        },
286        SymBoolExprKind::And(values) => checked_mul_guard_conjunction(values)
287            .map(|(zero_operand, expected)| (zero_operand, expected, false)),
288        _ => None,
289    }
290}
291
292fn checked_mul_guard_word_comparison(
293    left: &SymExpr,
294    right: &SymExpr,
295) -> Option<(SymExpr, SymExpr)> {
296    let guard_word = if right.as_const().is_some_and(|value| value.is_zero()) {
297        left
298    } else if left.as_const().is_some_and(|value| value.is_zero()) {
299        right
300    } else {
301        return None;
302    };
303    let SymExprKind::BinOp(SymBinOp::Or, left, right) = guard_word.kind() else {
304        return None;
305    };
306
307    for (quotient_word, zero_word) in [(left, right), (right, left)] {
308        let Some(quotient_matches) = quotient_word.bool_word_condition() else {
309            continue;
310        };
311        let Some(zero_condition) = zero_word.bool_word_condition() else {
312            continue;
313        };
314        let Some((zero_operand, expected, quotient_zero_condition)) =
315            checked_mul_guard_operands(&quotient_matches)
316        else {
317            continue;
318        };
319        if zero_condition == quotient_zero_condition {
320            return Some((zero_operand, expected));
321        }
322    }
323    None
324}
325
326fn checked_mul_guard_conjunction(values: &[SymBoolExpr]) -> Option<(SymExpr, SymExpr)> {
327    for value in values {
328        let SymBoolExprKind::Not(quotient_matches) = value.kind() else {
329            continue;
330        };
331        let Some((zero_operand, expected, zero_condition)) =
332            checked_mul_guard_operands(quotient_matches)
333        else {
334            continue;
335        };
336        let contains_negated_zero_condition = values.iter().any(
337            |value| matches!(value.kind(), SymBoolExprKind::Not(inner) if inner == &zero_condition),
338        );
339        if contains_negated_zero_condition {
340            return Some((zero_operand, expected));
341        }
342    }
343    None
344}
345
346fn checked_mul_guard_operands(condition: &SymBoolExpr) -> Option<(SymExpr, SymExpr, SymBoolExpr)> {
347    let SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) = condition.kind() else {
348        return None;
349    };
350    for (guarded_quotient, expected) in [(left, right), (right, left)] {
351        let SymExprKind::Ite(zero_condition, zero, quotient) = guarded_quotient.kind() else {
352            continue;
353        };
354        if !zero.as_const().is_some_and(|value| value.is_zero()) {
355            continue;
356        }
357        let Some(zero_operand) = zero_condition.zero_check_operand() else {
358            continue;
359        };
360        let Some((numerator, denominator)) = quotient.udiv_operands() else {
361            continue;
362        };
363        if denominator != zero_operand {
364            continue;
365        }
366        let SymExprKind::BinOp(SymBinOp::Mul, product_left, product_right) = numerator.kind()
367        else {
368            continue;
369        };
370        if (product_left == denominator && product_right == expected)
371            || (product_right == denominator && product_left == expected)
372        {
373            return Some((zero_operand.clone(), expected.clone(), zero_condition.clone()));
374        }
375    }
376    None
377}
378
379fn fallback_search_vars(
380    cx: &SymCx,
381    vars: SymbolicVars,
382    constraints: &[SymBoolExpr],
383) -> Vec<Symbol> {
384    if vars.len() <= HARD_ARITH_FALLBACK_MAX_VARS {
385        return vars.into_iter().collect();
386    }
387
388    let hard_arith_vars = hard_arith_fallback_vars(constraints);
389    if !hard_arith_vars.is_empty() && hard_arith_vars.len() <= HARD_ARITH_FALLBACK_MAX_VARS {
390        let mut vars = hard_arith_vars;
391        add_zero_invalid_support_vars(&mut vars, constraints);
392        return vars.into_iter().collect();
393    }
394
395    vars.into_iter()
396        .filter(|var| {
397            let var = cx.symbol_name(*var);
398            var.starts_with("calldata")
399                || var.starts_with("sequence")
400                || var.starts_with("create_address")
401                || var.starts_with("create2_address")
402                || !var.contains('_')
403        })
404        .collect()
405}
406
407fn hard_arith_fallback_vars(constraints: &[SymBoolExpr]) -> SymbolicVars {
408    let mut vars = SymbolicVars::default();
409    for constraint in constraints {
410        collect_bool_hard_arith_vars(constraint, &mut vars);
411    }
412    vars
413}
414
415fn add_zero_invalid_support_vars(vars: &mut SymbolicVars, constraints: &[SymBoolExpr]) {
416    let zero_model = SymbolicModel::default();
417    for constraint in constraints {
418        if constraint.eval_model(&zero_model).unwrap_or(false) {
419            continue;
420        }
421        // Scalar bounds can be completed constructively. Spending a search slot on them can
422        // miss a valid endpoint (e.g. int256::MAX + 1) that is outside the candidate budget.
423        let (inner, inverted) = match constraint.kind() {
424            SymBoolExprKind::Not(inner) => (inner, true),
425            _ => (constraint, false),
426        };
427        if let SymBoolExprKind::Cmp(op, left, right) = inner.kind()
428            && support_cmp_op(*op, inverted)
429                .is_some_and(|op| !matches!(op, SymCmpOp::Slt | SymCmpOp::Sgt))
430            && [(left, right), (right, left)].into_iter().any(|(variable, bound)| {
431                if !matches!(variable.kind(), SymExprKind::Var(_)) {
432                    return false;
433                }
434                let mut dependencies = SymbolicVars::default();
435                bound.collect_eval_vars(&mut dependencies);
436                dependencies.is_subset(vars)
437            })
438        {
439            continue;
440        }
441
442        let mut constraint_vars = SymbolicVars::default();
443        constraint.collect_vars(&mut constraint_vars);
444        let missing =
445            constraint_vars.iter().filter(|var| !vars.contains(*var)).copied().collect::<Vec<_>>();
446        if vars.len() + missing.len() > HARD_ARITH_FALLBACK_MAX_VARS {
447            continue;
448        }
449        vars.extend(missing);
450    }
451}
452
453fn fallback_candidates_for_var(
454    var: &Symbol,
455    constraints: &[SymBoolExpr],
456    constants: &[U256],
457) -> Option<Vec<U256>> {
458    let hints = MaskHints::for_var(var, constraints);
459    if !(hints.one & hints.zero).is_zero() {
460        return None;
461    }
462
463    let mut candidates = HashSet::<U256>::default();
464    for candidate in [
465        U256::ZERO,
466        U256::ONE,
467        U256::from(2),
468        U256::from(3),
469        U256::MAX,
470        U256::MAX - U256::ONE,
471        U256::MAX - U256::from(2),
472    ] {
473        push_fallback_candidate(&mut candidates, candidate, hints);
474    }
475
476    for constant in constants.iter().copied() {
477        push_fallback_candidate(&mut candidates, constant, hints);
478        push_fallback_candidate(&mut candidates, constant.wrapping_add(U256::ONE), hints);
479        push_fallback_candidate(&mut candidates, constant.wrapping_sub(U256::ONE), hints);
480        if candidates.len() >= FALLBACK_MODEL_MAX_CANDIDATES_PER_VAR {
481            break;
482        }
483    }
484
485    for bit in 0..256 {
486        let power = U256::ONE << bit;
487        push_fallback_candidate(&mut candidates, power, hints);
488        if candidates.len() >= FALLBACK_MODEL_MAX_CANDIDATES_PER_VAR {
489            break;
490        }
491    }
492
493    let mut candidates = candidates.into_iter().collect::<Vec<_>>();
494    candidates.sort_unstable();
495    candidates.truncate(FALLBACK_MODEL_MAX_CANDIDATES_PER_VAR);
496    Some(candidates)
497}
498
499struct FallbackSearch<'a> {
500    constraints: &'a [SymBoolExpr],
501    constraint_vars: &'a [SymbolicVars],
502    searched_vars: &'a SymbolicVars,
503    vars: &'a [Symbol],
504    candidates: &'a [Vec<U256>],
505    max_assignments: usize,
506}
507
508impl FallbackSearch<'_> {
509    fn model(
510        &self,
511        index: usize,
512        model: &mut SymbolicModel,
513        assignments: &mut usize,
514    ) -> Option<SymbolicModel> {
515        if index == self.vars.len() {
516            let mut completed = model.clone();
517            let mut remaining_support_visits = usize::MAX;
518            if complete_fallback_support_model(
519                self.constraints,
520                &mut completed,
521                &mut remaining_support_visits,
522            ) {
523                return Some(completed);
524            }
525            // Greedy completion can choose one endpoint before seeing a tighter bound. Retry
526            // from the search assignment after intersecting all currently evaluable bounds.
527            let mut completed = model.clone();
528            return (seed_bounded_support_vars(self.constraints, &mut completed)
529                && complete_fallback_support_model(
530                    self.constraints,
531                    &mut completed,
532                    &mut remaining_support_visits,
533                ))
534            .then_some(completed);
535        }
536
537        for candidate in &self.candidates[index] {
538            if *assignments >= self.max_assignments {
539                return None;
540            }
541            *assignments += 1;
542            model.insert(self.vars[index], *candidate);
543            if fallback_partial_model_satisfies_known_constraints(
544                self.constraints,
545                self.constraint_vars,
546                self.searched_vars,
547                model,
548            ) && let Some(model) = self.model(index + 1, model, assignments)
549            {
550                return Some(model);
551            }
552        }
553        model.remove(&self.vars[index]);
554        None
555    }
556}
557
558/// Seeds only unassigned scalar variables; every resulting witness is still fully validated.
559fn seed_bounded_support_vars(constraints: &[SymBoolExpr], model: &mut SymbolicModel) -> bool {
560    let mut bounds: HashMap<Symbol, (U256, U256)> = HashMap::default();
561    for constraint in constraints {
562        let (constraint, inverted) = match constraint.kind() {
563            SymBoolExprKind::Not(inner) => (inner, true),
564            _ => (constraint, false),
565        };
566        let SymBoolExprKind::Cmp(op, left, right) = constraint.kind() else { continue };
567        let Some(mut op) = support_cmp_op(*op, inverted) else { continue };
568        if matches!(op, SymCmpOp::Slt | SymCmpOp::Sgt) {
569            continue;
570        }
571        let (var, known) = if let SymExprKind::Var(var) = left.kind()
572            && !model.contains_name(*var)
573            && let Ok(Some(value)) = right.eval_model_if_complete(model)
574        {
575            (*var, value)
576        } else if let SymExprKind::Var(var) = right.kind()
577            && !model.contains_name(*var)
578            && let Ok(Some(value)) = left.eval_model_if_complete(model)
579        {
580            op = match op {
581                SymCmpOp::Ult => SymCmpOp::Ugt,
582                SymCmpOp::Ule => SymCmpOp::Uge,
583                SymCmpOp::Ugt => SymCmpOp::Ult,
584                SymCmpOp::Uge => SymCmpOp::Ule,
585                other => other,
586            };
587            (*var, value)
588        } else {
589            continue;
590        };
591        let Some(value) = support_target_for_known_right(op, known) else { return false };
592        let (lower, upper) = bounds.entry(var).or_insert((U256::ZERO, U256::MAX));
593        match op {
594            SymCmpOp::Eq => {
595                *lower = (*lower).max(value);
596                *upper = (*upper).min(value);
597            }
598            SymCmpOp::Ule | SymCmpOp::Ult => *upper = (*upper).min(value),
599            SymCmpOp::Uge | SymCmpOp::Ugt => *lower = (*lower).max(value),
600            SymCmpOp::Slt | SymCmpOp::Sgt => continue,
601        }
602        if lower > upper {
603            return false;
604        }
605    }
606    if bounds.is_empty() {
607        return false;
608    }
609    for (var, (lower, _)) in bounds {
610        model.insert(var, lower);
611    }
612    true
613}
614
615fn complete_fallback_support_model(
616    constraints: &[SymBoolExpr],
617    model: &mut SymbolicModel,
618    remaining_support_visits: &mut usize,
619) -> bool {
620    for _ in 0..constraints.len() {
621        let Some(mut changed) =
622            complete_support_constraints_once(constraints, model, remaining_support_visits)
623        else {
624            return false;
625        };
626        if changed {
627            continue;
628        }
629        // Default checked-add bases to zero only after exact/lower-bound completions had a chance
630        // to assign a stronger value required by another constraint.
631        for constraint in constraints {
632            if !charge_support_constraint(constraint, remaining_support_visits) {
633                return false;
634            }
635            match constraint.eval_model_if_complete(model) {
636                Ok(Some(true)) => {}
637                Ok(Some(false)) | Err(_) => return false,
638                Ok(None) => {
639                    changed |= complete_default_support_constraint(constraint, model);
640                }
641            }
642        }
643        if !changed {
644            break;
645        }
646    }
647    constraints.iter().all(|constraint| {
648        charge_support_constraint(constraint, remaining_support_visits)
649            && constraint.eval_model(model).unwrap_or(false)
650    })
651}
652
653fn propagate_fallback_support_constraints(
654    constraints: &[SymBoolExpr],
655    model: &mut SymbolicModel,
656    remaining_support_visits: &mut usize,
657) -> bool {
658    for _ in 0..constraints.len() {
659        match complete_support_constraints_once(constraints, model, remaining_support_visits) {
660            Some(true) => {}
661            Some(false) => return true,
662            None => return false,
663        }
664    }
665    true
666}
667
668fn complete_support_constraints_once(
669    constraints: &[SymBoolExpr],
670    model: &mut SymbolicModel,
671    remaining_support_visits: &mut usize,
672) -> Option<bool> {
673    let mut changed = false;
674    for constraint in constraints {
675        if !charge_support_constraint(constraint, remaining_support_visits) {
676            return None;
677        }
678        match constraint.eval_model_if_complete(model) {
679            Ok(Some(true)) => {}
680            Ok(Some(false)) | Err(_) => return None,
681            Ok(None) => changed |= complete_support_bool(constraint, model, false, false),
682        }
683    }
684    Some(changed)
685}
686
687fn charge_support_constraint(
688    constraint: &SymBoolExpr,
689    remaining_support_visits: &mut usize,
690) -> bool {
691    if *remaining_support_visits == 0 {
692        return false;
693    }
694    *remaining_support_visits -= 1;
695    // Charge the distinct word nodes inspected by the DAG visitors, not repeated occurrences.
696    !constraint
697        .visit_exprs(&mut |_| {
698            if *remaining_support_visits == 0 {
699                return ControlFlow::Break(());
700            }
701            *remaining_support_visits -= 1;
702            ControlFlow::Continue(())
703        })
704        .is_break()
705}
706
707fn complete_model_with_zeroes(
708    constraints: &[SymBoolExpr],
709    model: &mut SymbolicModel,
710    remaining_support_visits: &mut usize,
711) -> bool {
712    let mut vars = SymbolicVars::default();
713    for constraint in constraints {
714        if !charge_support_constraint(constraint, remaining_support_visits) {
715            return false;
716        }
717        constraint.collect_eval_vars(&mut vars);
718    }
719    for var in vars {
720        model.entry(var).or_default();
721    }
722    true
723}
724
725fn complete_default_support_constraint(
726    constraint: &SymBoolExpr,
727    model: &mut SymbolicModel,
728) -> bool {
729    complete_support_bool(constraint, model, false, true)
730}
731
732fn complete_support_bool(
733    constraint: &SymBoolExpr,
734    model: &mut SymbolicModel,
735    inverted: bool,
736    defaults_only: bool,
737) -> bool {
738    match constraint.kind() {
739        SymBoolExprKind::Const(_) => false,
740        SymBoolExprKind::Not(value) => {
741            complete_support_bool(value, model, !inverted, defaults_only)
742        }
743        SymBoolExprKind::And(values) if !inverted => {
744            let mut changed = false;
745            for value in values.iter() {
746                changed |= complete_support_bool(value, model, false, defaults_only);
747            }
748            changed
749        }
750        SymBoolExprKind::Cmp(op, left, right) => {
751            let Some(op) = support_cmp_op(*op, inverted) else {
752                return false;
753            };
754            if defaults_only {
755                complete_default_support_comparison(op, left, right, model)
756            } else {
757                complete_support_comparison(op, left, right, model)
758            }
759        }
760        SymBoolExprKind::And(_) => false,
761    }
762}
763
764const fn support_cmp_op(op: SymCmpOp, inverted: bool) -> Option<SymCmpOp> {
765    if !inverted {
766        return Some(op);
767    }
768
769    match op {
770        SymCmpOp::Ult => Some(SymCmpOp::Uge),
771        SymCmpOp::Ugt => Some(SymCmpOp::Ule),
772        SymCmpOp::Ule => Some(SymCmpOp::Ugt),
773        SymCmpOp::Uge => Some(SymCmpOp::Ult),
774        SymCmpOp::Eq | SymCmpOp::Slt | SymCmpOp::Sgt => None,
775    }
776}
777
778fn complete_support_comparison(
779    op: SymCmpOp,
780    left: &SymExpr,
781    right: &SymExpr,
782    model: &mut SymbolicModel,
783) -> bool {
784    if complete_checked_sub_guard(op, left, right, model) {
785        return true;
786    }
787    if let Ok(Some(value)) = left.eval_model_if_complete(model)
788        && let Some(target) = support_target_for_known_left(op, value)
789    {
790        return right.assign_model_value(model, target);
791    }
792    if let Ok(Some(value)) = right.eval_model_if_complete(model)
793        && let Some(target) = support_target_for_known_right(op, value)
794    {
795        return left.assign_model_value(model, target);
796    }
797    false
798}
799
800fn complete_default_support_comparison(
801    op: SymCmpOp,
802    left: &SymExpr,
803    right: &SymExpr,
804    model: &mut SymbolicModel,
805) -> bool {
806    complete_checked_add_guard(op, left, right, model)
807}
808
809fn complete_checked_sub_guard(
810    op: SymCmpOp,
811    left: &SymExpr,
812    right: &SymExpr,
813    model: &mut SymbolicModel,
814) -> bool {
815    match op {
816        SymCmpOp::Uge => assign_checked_sub_minuend(left, right, model),
817        SymCmpOp::Ule => assign_checked_sub_minuend(right, left, model),
818        _ => false,
819    }
820}
821
822fn assign_checked_sub_minuend(
823    minuend: &SymExpr,
824    sub_expr: &SymExpr,
825    model: &mut SymbolicModel,
826) -> bool {
827    let SymExprKind::BinOp(SymBinOp::Sub, sub_minuend, amount) = sub_expr.kind() else {
828        return false;
829    };
830    if sub_minuend != minuend {
831        return false;
832    }
833    let Ok(Some(amount)) = amount.eval_model_if_complete(model) else {
834        return false;
835    };
836    minuend.assign_model_value(model, amount)
837}
838
839fn complete_checked_add_guard(
840    op: SymCmpOp,
841    left: &SymExpr,
842    right: &SymExpr,
843    model: &mut SymbolicModel,
844) -> bool {
845    match op {
846        SymCmpOp::Uge => assign_checked_add_base(left, right, model),
847        SymCmpOp::Ule => assign_checked_add_base(right, left, model),
848        _ => false,
849    }
850}
851
852fn assign_checked_add_base(sum: &SymExpr, base: &SymExpr, model: &mut SymbolicModel) -> bool {
853    let SymExprKind::BinOp(SymBinOp::Add, left, right) = sum.kind() else {
854        return false;
855    };
856    if left == base && right.eval_model_if_complete(model).ok().flatten().is_some() {
857        return base.assign_model_value(model, U256::ZERO);
858    }
859    if right == base && left.eval_model_if_complete(model).ok().flatten().is_some() {
860        return base.assign_model_value(model, U256::ZERO);
861    }
862    false
863}
864
865const fn support_target_for_known_left(op: SymCmpOp, value: U256) -> Option<U256> {
866    match op {
867        SymCmpOp::Eq | SymCmpOp::Ule | SymCmpOp::Uge => Some(value),
868        SymCmpOp::Ult => value.checked_add(U256::ONE),
869        SymCmpOp::Ugt => value.checked_sub(U256::ONE),
870        SymCmpOp::Slt | SymCmpOp::Sgt => None,
871    }
872}
873
874const fn support_target_for_known_right(op: SymCmpOp, value: U256) -> Option<U256> {
875    match op {
876        SymCmpOp::Eq | SymCmpOp::Ule | SymCmpOp::Uge => Some(value),
877        SymCmpOp::Ult => value.checked_sub(U256::ONE),
878        SymCmpOp::Ugt => value.checked_add(U256::ONE),
879        SymCmpOp::Slt | SymCmpOp::Sgt => None,
880    }
881}
882
883fn fallback_partial_model_satisfies_known_constraints(
884    constraints: &[SymBoolExpr],
885    constraint_vars: &[SymbolicVars],
886    searched_vars: &SymbolicVars,
887    model: &SymbolicModel,
888) -> bool {
889    constraints.iter().zip(constraint_vars).all(|(constraint, vars)| {
890        !vars.is_subset(searched_vars)
891            || !vars.iter().all(|var| model.contains_name(*var))
892            || constraint.eval_model(model).unwrap_or(false)
893    })
894}
895
896fn collect_bool_fallback_vars(expr: &SymBoolExpr, vars: &mut SymbolicVars) {
897    let _ = expr.visit_exprs::<()>(&mut |expr| {
898        vars.extend(expr.kind().get_eval_var());
899        ControlFlow::Continue(())
900    });
901}
902
903fn collect_bool_hard_arith_vars(expr: &SymBoolExpr, vars: &mut SymbolicVars) {
904    let _ = expr.visit_exprs::<()>(&mut |expr| {
905        if is_hard_arith_node(expr) {
906            expr.collect_eval_vars(vars);
907        }
908        ControlFlow::Continue(())
909    });
910}
911
912pub(crate) fn fallback_single_var_model(constraints: &[SymBoolExpr]) -> Option<SymbolicModel> {
913    let mut vars = SymbolicVars::default();
914    let mut constants = HashSet::<U256>::default();
915    for constraint in constraints {
916        constraint.collect_vars(&mut vars);
917        collect_bool_constants(constraint, &mut constants);
918    }
919    let mut constants = constants.into_iter().collect::<Vec<_>>();
920    constants.sort_unstable();
921
922    let var = if vars.len() == 1 { *vars.iter().next()? } else { return None };
923    let hints = MaskHints::for_var(&var, constraints);
924    if !(hints.one & hints.zero).is_zero() {
925        return None;
926    }
927
928    let mut model = SymbolicModel::default();
929    let mut remaining_support_visits = usize::MAX;
930    if complete_fallback_support_model(constraints, &mut model, &mut remaining_support_visits)
931        && model.len() == 1
932        && model.contains_key(&var)
933    {
934        return Some(model);
935    }
936
937    for candidate in [
938        U256::ZERO,
939        U256::ONE,
940        U256::from(2),
941        U256::MAX,
942        U256::MAX - U256::ONE,
943        U256::MAX - U256::from(2),
944    ] {
945        let mut model = SymbolicModel::default();
946        model.insert(var, (candidate | hints.one) & !hints.zero);
947        if eval_model_constraints(constraints, &model) {
948            return Some(model);
949        }
950    }
951
952    let mut candidates = HashSet::<U256>::default();
953    for constant in constants.iter().copied() {
954        push_fallback_candidate(&mut candidates, constant, hints);
955        push_fallback_candidate(&mut candidates, constant.wrapping_add(U256::ONE), hints);
956        push_fallback_candidate(&mut candidates, constant.wrapping_sub(U256::ONE), hints);
957    }
958
959    for bit in 0..256 {
960        let power = U256::ONE << bit;
961        push_fallback_candidate(&mut candidates, power, hints);
962        for constant in constants.iter().copied().take(64) {
963            push_fallback_candidate(&mut candidates, power | constant, hints);
964            push_fallback_candidate(&mut candidates, power.wrapping_add(constant), hints);
965        }
966    }
967
968    let mut candidates = candidates.into_iter().collect::<Vec<_>>();
969    candidates.sort_unstable();
970    for candidate in candidates {
971        let mut model = SymbolicModel::default();
972        model.insert(var, candidate);
973        if eval_model_constraints(constraints, &model) {
974            return Some(model);
975        }
976    }
977
978    None
979}
980
981/// Searches a bounded Cartesian portfolio and returns only an evaluator-validated SAT witness.
982///
983/// Unsupported expressions and exhausted search return `None`, leaving the external solver as the
984/// authoritative fallback.
985pub(crate) fn fallback_bounded_model(constraints: &[SymBoolExpr]) -> Option<SymbolicModel> {
986    if constraints.iter().any(SymBoolExpr::contains_hard_arith) {
987        return None;
988    }
989
990    let mut vars = SymbolicVars::default();
991    for constraint in constraints {
992        collect_bool_fallback_vars(constraint, &mut vars);
993        if vars.len() > FALLBACK_MODEL_MAX_VARS {
994            return None;
995        }
996    }
997    if vars.len() < 2 {
998        return None;
999    }
1000    if constraints.iter().any(SymBoolExpr::contains_symbolic_hash)
1001        || constraints.iter().any(SymBoolExpr::contains_gasleft)
1002    {
1003        return None;
1004    }
1005    let mut constants = HashSet::<U256>::default();
1006    for constraint in constraints {
1007        collect_bool_constants(constraint, &mut constants);
1008    }
1009    let mut constants = constants.into_iter().collect::<Vec<_>>();
1010    constants.sort_unstable();
1011    let vars = vars.into_iter().collect::<Vec<_>>();
1012    let candidates = vars
1013        .iter()
1014        .map(|var| fallback_candidates_for_var(var, constraints, &constants))
1015        .collect::<Option<Vec<_>>>()?;
1016    let searched_vars = vars.iter().copied().collect::<SymbolicVars>();
1017    let constraint_vars = constraints
1018        .iter()
1019        .map(|constraint| {
1020            let mut vars = SymbolicVars::default();
1021            constraint.collect_vars(&mut vars);
1022            vars
1023        })
1024        .collect::<Vec<_>>();
1025    let search = FallbackSearch {
1026        constraints,
1027        constraint_vars: &constraint_vars,
1028        searched_vars: &searched_vars,
1029        vars: &vars,
1030        candidates: &candidates,
1031        max_assignments: FALLBACK_MODEL_MAX_ASSIGNMENTS,
1032    };
1033    let mut model = SymbolicModel::default();
1034    let mut assignments = 0usize;
1035    search.model(0, &mut model, &mut assignments)
1036}
1037
1038fn push_fallback_candidate(candidates: &mut HashSet<U256>, candidate: U256, hints: MaskHints) {
1039    candidates.insert((candidate | hints.one) & !hints.zero);
1040}
1041
1042fn collect_bool_constants(expr: &SymBoolExpr, constants: &mut HashSet<U256>) {
1043    let _ = expr.visit_exprs::<()>(&mut |expr| {
1044        if let SymExprKind::Const(value) = expr.kind() {
1045            constants.insert(*value);
1046        }
1047        ControlFlow::Continue(())
1048    });
1049}
1050
1051#[derive(Clone, Copy, Debug, Default)]
1052struct MaskHints {
1053    one: U256,
1054    zero: U256,
1055}
1056
1057impl MaskHints {
1058    fn for_var(var: &Symbol, constraints: &[SymBoolExpr]) -> Self {
1059        let mut hints = Self::default();
1060        for constraint in constraints {
1061            hints.apply_bool(var, constraint, false);
1062        }
1063        hints
1064    }
1065
1066    fn apply_bool(&mut self, var: &Symbol, expr: &SymBoolExpr, inverted: bool) {
1067        match expr.kind() {
1068            SymBoolExprKind::Const(_) => {}
1069            SymBoolExprKind::Not(value) => self.apply_bool(var, value, !inverted),
1070            SymBoolExprKind::And(values) if !inverted => {
1071                for value in values.iter() {
1072                    self.apply_bool(var, value, false);
1073                }
1074            }
1075            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => {
1076                self.apply_equality(var, left, right, inverted)
1077            }
1078            SymBoolExprKind::Cmp(_, _, _) | SymBoolExprKind::And(_) => {}
1079        }
1080    }
1081
1082    fn apply_equality(&mut self, var: &Symbol, left: &SymExpr, right: &SymExpr, inverted: bool) {
1083        if let Some(mask) =
1084            zero_mask_equality(var, left, right).or_else(|| zero_mask_equality(var, right, left))
1085        {
1086            if inverted {
1087                if mask.is_power_of_two() {
1088                    self.one |= mask;
1089                }
1090            } else {
1091                self.zero |= mask;
1092            }
1093        }
1094    }
1095}
1096
1097fn zero_mask_equality(var: &Symbol, masked: &SymExpr, zero: &SymExpr) -> Option<U256> {
1098    if !zero.as_const().is_some_and(|value| value.is_zero()) {
1099        return None;
1100    }
1101    match masked.kind() {
1102        SymExprKind::BinOp(SymBinOp::And, left, right)
1103            if left.kind().get_var().is_some_and(|name| &name == var) =>
1104        {
1105            right.as_const()
1106        }
1107        _ => None,
1108    }
1109}