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    !constraint
696        .visit_exprs(&mut |_| {
697            if *remaining_support_visits == 0 {
698                return ControlFlow::Break(());
699            }
700            *remaining_support_visits -= 1;
701            ControlFlow::Continue(())
702        })
703        .is_break()
704}
705
706fn complete_model_with_zeroes(
707    constraints: &[SymBoolExpr],
708    model: &mut SymbolicModel,
709    remaining_support_visits: &mut usize,
710) -> bool {
711    let mut vars = SymbolicVars::default();
712    for constraint in constraints {
713        if !charge_support_constraint(constraint, remaining_support_visits) {
714            return false;
715        }
716        constraint.collect_eval_vars(&mut vars);
717    }
718    for var in vars {
719        model.entry(var).or_default();
720    }
721    true
722}
723
724fn complete_default_support_constraint(
725    constraint: &SymBoolExpr,
726    model: &mut SymbolicModel,
727) -> bool {
728    complete_support_bool(constraint, model, false, true)
729}
730
731fn complete_support_bool(
732    constraint: &SymBoolExpr,
733    model: &mut SymbolicModel,
734    inverted: bool,
735    defaults_only: bool,
736) -> bool {
737    match constraint.kind() {
738        SymBoolExprKind::Const(_) => false,
739        SymBoolExprKind::Not(value) => {
740            complete_support_bool(value, model, !inverted, defaults_only)
741        }
742        SymBoolExprKind::And(values) if !inverted => {
743            let mut changed = false;
744            for value in values.iter() {
745                changed |= complete_support_bool(value, model, false, defaults_only);
746            }
747            changed
748        }
749        SymBoolExprKind::Cmp(op, left, right) => {
750            let Some(op) = support_cmp_op(*op, inverted) else {
751                return false;
752            };
753            if defaults_only {
754                complete_default_support_comparison(op, left, right, model)
755            } else {
756                complete_support_comparison(op, left, right, model)
757            }
758        }
759        SymBoolExprKind::And(_) => false,
760    }
761}
762
763const fn support_cmp_op(op: SymCmpOp, inverted: bool) -> Option<SymCmpOp> {
764    if !inverted {
765        return Some(op);
766    }
767
768    match op {
769        SymCmpOp::Ult => Some(SymCmpOp::Uge),
770        SymCmpOp::Ugt => Some(SymCmpOp::Ule),
771        SymCmpOp::Ule => Some(SymCmpOp::Ugt),
772        SymCmpOp::Uge => Some(SymCmpOp::Ult),
773        SymCmpOp::Eq | SymCmpOp::Slt | SymCmpOp::Sgt => None,
774    }
775}
776
777fn complete_support_comparison(
778    op: SymCmpOp,
779    left: &SymExpr,
780    right: &SymExpr,
781    model: &mut SymbolicModel,
782) -> bool {
783    if complete_checked_sub_guard(op, left, right, model) {
784        return true;
785    }
786    if let Ok(Some(value)) = left.eval_model_if_complete(model)
787        && let Some(target) = support_target_for_known_left(op, value)
788    {
789        return right.assign_model_value(model, target);
790    }
791    if let Ok(Some(value)) = right.eval_model_if_complete(model)
792        && let Some(target) = support_target_for_known_right(op, value)
793    {
794        return left.assign_model_value(model, target);
795    }
796    false
797}
798
799fn complete_default_support_comparison(
800    op: SymCmpOp,
801    left: &SymExpr,
802    right: &SymExpr,
803    model: &mut SymbolicModel,
804) -> bool {
805    complete_checked_add_guard(op, left, right, model)
806}
807
808fn complete_checked_sub_guard(
809    op: SymCmpOp,
810    left: &SymExpr,
811    right: &SymExpr,
812    model: &mut SymbolicModel,
813) -> bool {
814    match op {
815        SymCmpOp::Uge => assign_checked_sub_minuend(left, right, model),
816        SymCmpOp::Ule => assign_checked_sub_minuend(right, left, model),
817        _ => false,
818    }
819}
820
821fn assign_checked_sub_minuend(
822    minuend: &SymExpr,
823    sub_expr: &SymExpr,
824    model: &mut SymbolicModel,
825) -> bool {
826    let SymExprKind::BinOp(SymBinOp::Sub, sub_minuend, amount) = sub_expr.kind() else {
827        return false;
828    };
829    if sub_minuend != minuend {
830        return false;
831    }
832    let Ok(Some(amount)) = amount.eval_model_if_complete(model) else {
833        return false;
834    };
835    minuend.assign_model_value(model, amount)
836}
837
838fn complete_checked_add_guard(
839    op: SymCmpOp,
840    left: &SymExpr,
841    right: &SymExpr,
842    model: &mut SymbolicModel,
843) -> bool {
844    match op {
845        SymCmpOp::Uge => assign_checked_add_base(left, right, model),
846        SymCmpOp::Ule => assign_checked_add_base(right, left, model),
847        _ => false,
848    }
849}
850
851fn assign_checked_add_base(sum: &SymExpr, base: &SymExpr, model: &mut SymbolicModel) -> bool {
852    let SymExprKind::BinOp(SymBinOp::Add, left, right) = sum.kind() else {
853        return false;
854    };
855    if left == base && right.eval_model_if_complete(model).ok().flatten().is_some() {
856        return base.assign_model_value(model, U256::ZERO);
857    }
858    if right == base && left.eval_model_if_complete(model).ok().flatten().is_some() {
859        return base.assign_model_value(model, U256::ZERO);
860    }
861    false
862}
863
864const fn support_target_for_known_left(op: SymCmpOp, value: U256) -> Option<U256> {
865    match op {
866        SymCmpOp::Eq | SymCmpOp::Ule | SymCmpOp::Uge => Some(value),
867        SymCmpOp::Ult => value.checked_add(U256::ONE),
868        SymCmpOp::Ugt => value.checked_sub(U256::ONE),
869        SymCmpOp::Slt | SymCmpOp::Sgt => None,
870    }
871}
872
873const fn support_target_for_known_right(op: SymCmpOp, value: U256) -> Option<U256> {
874    match op {
875        SymCmpOp::Eq | SymCmpOp::Ule | SymCmpOp::Uge => Some(value),
876        SymCmpOp::Ult => value.checked_sub(U256::ONE),
877        SymCmpOp::Ugt => value.checked_add(U256::ONE),
878        SymCmpOp::Slt | SymCmpOp::Sgt => None,
879    }
880}
881
882fn fallback_partial_model_satisfies_known_constraints(
883    constraints: &[SymBoolExpr],
884    constraint_vars: &[SymbolicVars],
885    searched_vars: &SymbolicVars,
886    model: &SymbolicModel,
887) -> bool {
888    constraints.iter().zip(constraint_vars).all(|(constraint, vars)| {
889        !vars.is_subset(searched_vars)
890            || !vars.iter().all(|var| model.contains_name(*var))
891            || constraint.eval_model(model).unwrap_or(false)
892    })
893}
894
895fn collect_bool_fallback_vars(expr: &SymBoolExpr, vars: &mut SymbolicVars) {
896    let _ = expr.visit_exprs(&mut |expr| {
897        if let Some(var) = expr.kind().get_eval_var() {
898            vars.insert(var);
899        }
900        ControlFlow::<()>::Continue(())
901    });
902}
903
904fn collect_bool_hard_arith_vars(expr: &SymBoolExpr, vars: &mut SymbolicVars) {
905    let _ = expr.visit_exprs(&mut |expr| {
906        if is_hard_arith_node(expr) {
907            expr.collect_eval_vars(vars);
908        }
909        ControlFlow::<()>::Continue(())
910    });
911}
912
913pub(crate) fn fallback_single_var_model(constraints: &[SymBoolExpr]) -> Option<SymbolicModel> {
914    let mut vars = SymbolicVars::default();
915    let mut constants = HashSet::<U256>::default();
916    for constraint in constraints {
917        constraint.collect_vars(&mut vars);
918        collect_bool_constants(constraint, &mut constants);
919    }
920    let mut constants = constants.into_iter().collect::<Vec<_>>();
921    constants.sort_unstable();
922
923    let var = if vars.len() == 1 { *vars.iter().next()? } else { return None };
924    let hints = MaskHints::for_var(&var, constraints);
925    if !(hints.one & hints.zero).is_zero() {
926        return None;
927    }
928
929    let mut model = SymbolicModel::default();
930    let mut remaining_support_visits = usize::MAX;
931    if complete_fallback_support_model(constraints, &mut model, &mut remaining_support_visits)
932        && model.len() == 1
933        && model.contains_key(&var)
934    {
935        return Some(model);
936    }
937
938    for candidate in [
939        U256::ZERO,
940        U256::ONE,
941        U256::from(2),
942        U256::MAX,
943        U256::MAX - U256::ONE,
944        U256::MAX - U256::from(2),
945    ] {
946        let mut model = SymbolicModel::default();
947        model.insert(var, (candidate | hints.one) & !hints.zero);
948        if eval_model_constraints(constraints, &model) {
949            return Some(model);
950        }
951    }
952
953    let mut candidates = HashSet::<U256>::default();
954    for constant in constants.iter().copied() {
955        push_fallback_candidate(&mut candidates, constant, hints);
956        push_fallback_candidate(&mut candidates, constant.wrapping_add(U256::ONE), hints);
957        push_fallback_candidate(&mut candidates, constant.wrapping_sub(U256::ONE), hints);
958    }
959
960    for bit in 0..256 {
961        let power = U256::ONE << bit;
962        push_fallback_candidate(&mut candidates, power, hints);
963        for constant in constants.iter().copied().take(64) {
964            push_fallback_candidate(&mut candidates, power | constant, hints);
965            push_fallback_candidate(&mut candidates, power.wrapping_add(constant), hints);
966        }
967    }
968
969    let mut candidates = candidates.into_iter().collect::<Vec<_>>();
970    candidates.sort_unstable();
971    for candidate in candidates {
972        let mut model = SymbolicModel::default();
973        model.insert(var, candidate);
974        if eval_model_constraints(constraints, &model) {
975            return Some(model);
976        }
977    }
978
979    None
980}
981
982/// Searches a bounded Cartesian portfolio and returns only an evaluator-validated SAT witness.
983///
984/// Unsupported expressions and exhausted search return `None`, leaving the external solver as the
985/// authoritative fallback.
986pub(crate) fn fallback_bounded_model(constraints: &[SymBoolExpr]) -> Option<SymbolicModel> {
987    if constraints.iter().any(SymBoolExpr::contains_hard_arith) {
988        return None;
989    }
990
991    let mut vars = SymbolicVars::default();
992    for constraint in constraints {
993        collect_bool_fallback_vars(constraint, &mut vars);
994        if vars.len() > FALLBACK_MODEL_MAX_VARS {
995            return None;
996        }
997    }
998    if vars.len() < 2 {
999        return None;
1000    }
1001    if constraints.iter().any(SymBoolExpr::contains_symbolic_hash)
1002        || constraints.iter().any(SymBoolExpr::contains_gasleft)
1003    {
1004        return None;
1005    }
1006    let mut constants = HashSet::<U256>::default();
1007    for constraint in constraints {
1008        collect_bool_constants(constraint, &mut constants);
1009    }
1010    let mut constants = constants.into_iter().collect::<Vec<_>>();
1011    constants.sort_unstable();
1012    let vars = vars.into_iter().collect::<Vec<_>>();
1013    let candidates = vars
1014        .iter()
1015        .map(|var| fallback_candidates_for_var(var, constraints, &constants))
1016        .collect::<Option<Vec<_>>>()?;
1017    let searched_vars = vars.iter().copied().collect::<SymbolicVars>();
1018    let constraint_vars = constraints
1019        .iter()
1020        .map(|constraint| {
1021            let mut vars = SymbolicVars::default();
1022            constraint.collect_vars(&mut vars);
1023            vars
1024        })
1025        .collect::<Vec<_>>();
1026    let search = FallbackSearch {
1027        constraints,
1028        constraint_vars: &constraint_vars,
1029        searched_vars: &searched_vars,
1030        vars: &vars,
1031        candidates: &candidates,
1032        max_assignments: FALLBACK_MODEL_MAX_ASSIGNMENTS,
1033    };
1034    let mut model = SymbolicModel::default();
1035    let mut assignments = 0usize;
1036    search.model(0, &mut model, &mut assignments)
1037}
1038
1039fn push_fallback_candidate(candidates: &mut HashSet<U256>, candidate: U256, hints: MaskHints) {
1040    candidates.insert((candidate | hints.one) & !hints.zero);
1041}
1042
1043fn collect_bool_constants(expr: &SymBoolExpr, constants: &mut HashSet<U256>) {
1044    let _ = expr.visit_exprs(&mut |expr| {
1045        if let SymExprKind::Const(value) = expr.kind() {
1046            constants.insert(*value);
1047        }
1048        ControlFlow::<()>::Continue(())
1049    });
1050}
1051
1052#[derive(Clone, Copy, Debug, Default)]
1053struct MaskHints {
1054    one: U256,
1055    zero: U256,
1056}
1057
1058impl MaskHints {
1059    fn for_var(var: &Symbol, constraints: &[SymBoolExpr]) -> Self {
1060        let mut hints = Self::default();
1061        for constraint in constraints {
1062            hints.apply_bool(var, constraint, false);
1063        }
1064        hints
1065    }
1066
1067    fn apply_bool(&mut self, var: &Symbol, expr: &SymBoolExpr, inverted: bool) {
1068        match expr.kind() {
1069            SymBoolExprKind::Const(_) => {}
1070            SymBoolExprKind::Not(value) => self.apply_bool(var, value, !inverted),
1071            SymBoolExprKind::And(values) if !inverted => {
1072                for value in values.iter() {
1073                    self.apply_bool(var, value, false);
1074                }
1075            }
1076            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => {
1077                self.apply_equality(var, left, right, inverted)
1078            }
1079            SymBoolExprKind::Cmp(_, _, _) | SymBoolExprKind::And(_) => {}
1080        }
1081    }
1082
1083    fn apply_equality(&mut self, var: &Symbol, left: &SymExpr, right: &SymExpr, inverted: bool) {
1084        if let Some(mask) =
1085            zero_mask_equality(var, left, right).or_else(|| zero_mask_equality(var, right, left))
1086        {
1087            if inverted {
1088                if mask.is_power_of_two() {
1089                    self.one |= mask;
1090                }
1091            } else {
1092                self.zero |= mask;
1093            }
1094        }
1095    }
1096}
1097
1098fn zero_mask_equality(var: &Symbol, masked: &SymExpr, zero: &SymExpr) -> Option<U256> {
1099    if !zero.as_const().is_some_and(|value| value.is_zero()) {
1100        return None;
1101    }
1102    match masked.kind() {
1103        SymExprKind::BinOp(SymBinOp::And, left, right)
1104            if left.kind().get_var().is_some_and(|name| &name == var) =>
1105        {
1106            right.as_const()
1107        }
1108        _ => None,
1109    }
1110}