Skip to main content

foundry_evm_symbolic/runtime/solver/
mod.rs

1//! Solver orchestration, query scheduling, caching, and model validation.
2
3use super::*;
4use std::{
5    io::{BufRead, BufReader, Read},
6    process::{Child, ChildStdin, Output},
7    sync::mpsc::{Receiver, RecvTimeoutError},
8    thread::JoinHandle,
9};
10use wait_timeout::ChildExt;
11
12mod fallback;
13mod normalize;
14mod reasoning;
15mod smt;
16
17use fallback::{checked_mul_guard_branch_model, constraints_prefer_hard_arith_fallback_first};
18use normalize::{
19    NormalizationCache, constraints_are_directly_unsat, normalize_constraints_for_solver_cached,
20    sorted_bool_exprs_are_subset,
21};
22use reasoning::{
23    CheckedXorResult, checked_xor_result, product_monotonic_unsat_normalized,
24    remove_implied_monotonic_constraints,
25};
26use smt::write_smt_assertions;
27
28pub(crate) use fallback::{
29    fallback_bounded_model, fallback_single_var_model, hard_arith_fallback_model,
30};
31
32const Z3_QUERY_END: &str = "foundry-query-complete";
33
34/// Errors that arise when parsing or constructing solver commands from configuration.
35#[derive(Debug, thiserror::Error)]
36pub(crate) enum SolverConfigError {
37    /// The command string parsed to an empty argv.
38    #[error("symbolic solver command is empty")]
39    EmptyCommand,
40    /// The command string contains invalid shell quoting.
41    #[error("invalid shell quoting in symbolic solver command")]
42    InvalidShellQuoting,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub(crate) enum SolverOutcome {
47    Cancelled,
48    Error,
49    NotStarted,
50    SatAfterWinner,
51    SatInvalid,
52    SatValid,
53    TimeoutOrUnknown,
54    Unknown,
55    UnknownAfterWinner,
56    Unsat,
57    UnsatAfterWinner,
58    Unexpected,
59}
60
61impl fmt::Display for SolverOutcome {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        f.write_str(match self {
64            Self::Cancelled => "cancelled",
65            Self::Error => "error",
66            Self::NotStarted => "not-started",
67            Self::SatAfterWinner => "sat-after-winner",
68            Self::SatInvalid => "sat-invalid",
69            Self::SatValid => "sat-valid",
70            Self::TimeoutOrUnknown => "timeout-or-unknown",
71            Self::Unknown => "unknown",
72            Self::UnknownAfterWinner => "unknown-after-winner",
73            Self::Unsat => "unsat",
74            Self::UnsatAfterWinner => "unsat-after-winner",
75            Self::Unexpected => "unexpected",
76        })
77    }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub(crate) enum BranchFeasibility {
82    Sat,
83    Unsat,
84    NeedsSolver,
85}
86
87impl BranchFeasibility {
88    const fn into_result(self) -> Result<bool, SymbolicError> {
89        match self {
90            Self::Sat => Ok(true),
91            Self::Unsat => Ok(false),
92            Self::NeedsSolver => Err(SymbolicError::SolverUnknown),
93        }
94    }
95}
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub(crate) struct SolverCommand {
98    program: String,
99    args: Vec<String>,
100    display: String,
101    smt_timeout: bool,
102}
103
104impl SolverCommand {
105    /// Constructs a solver command from a program plus arguments.
106    pub(crate) fn new(parts: Vec<String>, smt_timeout: bool) -> Result<Self, SolverConfigError> {
107        let mut parts = parts.into_iter();
108        let Some(program) = parts.next().filter(|part| !part.is_empty()) else {
109            return Err(SolverConfigError::EmptyCommand);
110        };
111        let args = parts.collect::<Vec<_>>();
112        let display = std::iter::once(program.as_str())
113            .chain(args.iter().map(String::as_str))
114            .collect::<Vec<_>>()
115            .join(" ");
116        Ok(Self { program, args, display, smt_timeout })
117    }
118}
119
120pub(crate) struct SmtLibSubprocessSolver {
121    commands: Result<Vec<SolverCommand>, SolverConfigError>,
122    timeout: Option<u32>,
123    max_queries: usize,
124    queries: usize,
125    dump_smt: bool,
126    portfolio_scheduler: PortfolioScheduler,
127    heuristic_witnesses: usize,
128    replayable_storage: SymbolicVars,
129    normalization_cache: NormalizationCache,
130    sat_cache: HashMap<Vec<SymBoolExpr>, bool>,
131    model_cache: HashMap<Vec<SymBoolExpr>, SymbolicModel>,
132    sat_queries: usize,
133    model_queries: usize,
134    sat_cache_hits: usize,
135    model_cache_hits: usize,
136    smt_queries: usize,
137    solver_time: Duration,
138    smt_input_bytes: u64,
139    smt_max_query_bytes: u64,
140    smt_build_time: Duration,
141    smt_max_query_time: Duration,
142    z3_session: Option<Z3Session>,
143}
144
145impl SmtLibSubprocessSolver {
146    /// Constructs a subprocess solver from Foundry symbolic config.
147    pub(crate) fn from_config(config: &SymbolicConfig) -> Self {
148        Self {
149            commands: solver_commands_for_config(config),
150            timeout: config.timeout,
151            max_queries: config.max_solver_queries as usize,
152            queries: 0,
153            dump_smt: config.dump_smt,
154            portfolio_scheduler: PortfolioScheduler::default(),
155            heuristic_witnesses: 0,
156            replayable_storage: SymbolicVars::default(),
157            normalization_cache: NormalizationCache::default(),
158            sat_cache: HashMap::default(),
159            model_cache: HashMap::default(),
160            sat_queries: 0,
161            model_queries: 0,
162            sat_cache_hits: 0,
163            model_cache_hits: 0,
164            smt_queries: 0,
165            solver_time: Duration::ZERO,
166            smt_input_bytes: 0,
167            smt_max_query_bytes: 0,
168            smt_build_time: Duration::ZERO,
169            smt_max_query_time: Duration::ZERO,
170            z3_session: None,
171        }
172    }
173
174    /// Returns solver counters collected by this backend.
175    pub(crate) fn stats(&self) -> SymbolicStats {
176        SymbolicStats {
177            paths: 0,
178            solver_queries: self.queries,
179            smt_queries: self.smt_queries,
180            sat_queries: self.sat_queries,
181            model_queries: self.model_queries,
182            sat_cache_hits: self.sat_cache_hits,
183            model_cache_hits: self.model_cache_hits,
184            heuristic_witnesses: self.heuristic_witnesses,
185            solver_time_ms: self.solver_time.as_millis().try_into().unwrap_or(u64::MAX),
186            smt_input_bytes: self.smt_input_bytes,
187            smt_max_query_bytes: self.smt_max_query_bytes,
188            smt_build_time_ms: self.smt_build_time.as_millis().try_into().unwrap_or(u64::MAX),
189            smt_max_query_time_ms: self
190                .smt_max_query_time
191                .as_millis()
192                .try_into()
193                .unwrap_or(u64::MAX),
194        }
195    }
196
197    /// Clears cached expression keys tied to a previous symbolic context.
198    pub(crate) fn clear_context_caches(&mut self) {
199        self.normalization_cache.clear();
200        self.sat_cache.clear();
201        self.model_cache.clear();
202    }
203
204    /// Verifies that a configured solver can be invoked before exploration starts.
205    pub(crate) fn check_available(&self) -> Result<(), SymbolicError> {
206        let commands = self.commands()?;
207        let mut errors = Vec::new();
208        for command in commands {
209            let output = match Command::new(&command.program).arg("--version").output() {
210                Ok(output) => output,
211                Err(err) => {
212                    errors.push(format!("failed to execute `{}`: {err}", command.program));
213                    continue;
214                }
215            };
216            if output.status.success() {
217                return Ok(());
218            }
219            errors.push(format!("`{}` is not a usable SMT solver executable", command.program));
220        }
221        Err(SymbolicError::Solver(errors.join("; ")))
222    }
223
224    /// Returns satisfiability with path-local storage symbols that concrete replay can set.
225    pub(crate) fn is_sat_with_replayable_storage(
226        &mut self,
227        cx: &mut SymCx,
228        constraints: &[SymBoolExpr],
229        replayable_storage: &SymbolicVars,
230    ) -> Result<bool, SymbolicError> {
231        self.with_replayable_storage(replayable_storage, |solver| {
232            solver.is_sat_inner(cx, constraints, false).and_then(BranchFeasibility::into_result)
233        })
234    }
235
236    /// Returns branch feasibility with path-local storage symbols concrete replay can set.
237    pub(crate) fn branch_feasibility_with_replayable_storage(
238        &mut self,
239        cx: &mut SymCx,
240        constraints: &[SymBoolExpr],
241        replayable_storage: &SymbolicVars,
242    ) -> Result<BranchFeasibility, SymbolicError> {
243        self.with_replayable_storage(replayable_storage, |solver| {
244            solver.is_sat_inner(cx, constraints, true)
245        })
246    }
247
248    /// Returns a model with path-local storage symbols that concrete replay can set.
249    pub(crate) fn model_with_replayable_storage(
250        &mut self,
251        cx: &mut SymCx,
252        constraints: &[SymBoolExpr],
253        replayable_storage: &SymbolicVars,
254    ) -> Result<SymbolicModel, SymbolicError> {
255        self.with_replayable_storage(replayable_storage, |solver| solver.model(cx, constraints))
256    }
257
258    fn with_replayable_storage<T>(
259        &mut self,
260        replayable_storage: &SymbolicVars,
261        operation: impl FnOnce(&mut Self) -> T,
262    ) -> T {
263        let previous = std::mem::replace(&mut self.replayable_storage, replayable_storage.clone());
264        let result = operation(self);
265        self.replayable_storage = previous;
266        result
267    }
268
269    /// Returns variable assignments used to materialize inputs for concrete replay.
270    pub(crate) fn model(
271        &mut self,
272        cx: &mut SymCx,
273        constraints: &[SymBoolExpr],
274    ) -> Result<SymbolicModel, SymbolicError> {
275        // Local witnesses may decide the feasibility of gas-dependent branches, but a model assigns
276        // `gasleft()` a concrete value the engine cannot replay faithfully, so fail closed here.
277        if constraints.iter().any(SymBoolExpr::contains_gasleft) {
278            return Err(SymbolicError::Unsupported("GAS/gasleft() not modeled"));
279        }
280        self.model_queries += 1;
281        let smt_constraints =
282            normalize_constraints_for_solver_cached(cx, constraints, &mut self.normalization_cache);
283        let cache_key = smt_constraints.clone();
284
285        if self.sat_cache.get(&cache_key) == Some(&false) {
286            self.model_cache.remove(&cache_key);
287            trace!("model: normalized sat cache says unsat");
288            return Err(SymbolicError::Solver("counterexample path became unsat".to_string()));
289        }
290        if self.has_cached_unsat_subset(&cache_key) {
291            self.cache_sat_result(cache_key.clone(), false);
292            self.model_cache.remove(&cache_key);
293            trace!("model: normalized unsat subset cache hit");
294            return Err(SymbolicError::Solver("counterexample path became unsat".to_string()));
295        }
296
297        if let Some(model) = self.model_cache.get(&cache_key) {
298            if eval_model_constraints(constraints, model) {
299                let model = model.clone();
300                self.model_cache_hits += 1;
301                trace!("model: normalized cache hit");
302                self.cache_sat_result(cache_key.clone(), true);
303                return Ok(model);
304            }
305            trace!("model: normalized cache hit failed validation");
306        }
307        if self.model_cache.remove(&cache_key).is_some() {
308            self.sat_cache.remove(&cache_key);
309        }
310
311        self.reserve_query()?;
312        self.queries += 1;
313        let _span = trace_span!(
314            "solver_query",
315            query_id = self.queries,
316            constraint_count = constraints.len(),
317            kind = "model"
318        )
319        .entered();
320        trace!(query_id = self.queries, constraint_count = constraints.len(), "solver model");
321        if let Some(result) = checked_xor_result(constraints) {
322            match result {
323                CheckedXorResult::Unsat => {
324                    self.cache_sat_result(cache_key, false);
325                    return Err(SymbolicError::Solver(
326                        "counterexample path became unsat".to_string(),
327                    ));
328                }
329                CheckedXorResult::Model(model) => {
330                    self.cache_sat_result(cache_key.clone(), true);
331                    self.cache_model_result(cache_key, model.clone());
332                    return Ok(model);
333                }
334            }
335        }
336        if let Some(model) = fallback_single_var_model(&smt_constraints)
337            && eval_model_constraints(constraints, &model)
338        {
339            self.cache_sat_result(cache_key.clone(), true);
340            self.cache_model_result(cache_key, model.clone());
341            return Ok(model);
342        }
343        if let Some(model) = fallback_bounded_model(&smt_constraints)
344            && eval_model_constraints(constraints, &model)
345        {
346            self.cache_sat_result(cache_key.clone(), true);
347            self.cache_model_result(cache_key, model.clone());
348            return Ok(model);
349        }
350        if let Some(model) = checked_mul_guard_branch_model(
351            cx,
352            &smt_constraints,
353            constraints,
354            &self.replayable_storage,
355        ) {
356            trace!("model: validated constructive checked-multiply guard model");
357            self.cache_sat_result(cache_key.clone(), true);
358            return Ok(model);
359        }
360        if constraints_prefer_hard_arith_fallback_first(cx, &smt_constraints)
361            && let Some(model) =
362                validated_hard_arith_fallback_model(cx, &smt_constraints, constraints)
363        {
364            self.heuristic_witnesses += 1;
365            trace!("model: validated hard arithmetic fallback model before solver");
366            self.cache_sat_result(cache_key.clone(), true);
367            self.cache_model_result(cache_key, model.clone());
368            return Ok(model);
369        }
370        let output = match self.query_normalized(cx, &smt_constraints, true, constraints) {
371            Ok(output) => output,
372            Err(SymbolicError::SolverUnknown) => {
373                if let Some(model) =
374                    validated_hard_arith_fallback_model(cx, &smt_constraints, constraints)
375                {
376                    self.heuristic_witnesses += 1;
377                    trace!("model: validated hard arithmetic fallback model after solver unknown");
378                    self.cache_sat_result(cache_key.clone(), true);
379                    self.cache_model_result(cache_key, model.clone());
380                    return Ok(model);
381                }
382                return Err(SymbolicError::SolverUnknown);
383            }
384            Err(err) => return Err(err),
385        };
386        let mut lines = output.lines();
387        match lines.next().unwrap_or_default().trim() {
388            "sat" => {
389                let model = parse_and_validate_model(cx, &output, constraints)?;
390                self.cache_sat_result(cache_key.clone(), true);
391                self.cache_model_result(cache_key, model.clone());
392                Ok(model)
393            }
394            "unsat" => {
395                self.model_cache.remove(&cache_key);
396                self.cache_sat_result(cache_key, false);
397                Err(SymbolicError::Solver("counterexample path became unsat".to_string()))
398            }
399            "unknown" => {
400                if let Some(model) =
401                    validated_hard_arith_fallback_model(cx, &smt_constraints, constraints)
402                {
403                    self.heuristic_witnesses += 1;
404                    self.cache_sat_result(cache_key.clone(), true);
405                    self.cache_model_result(cache_key, model.clone());
406                    Ok(model)
407                } else {
408                    Err(SymbolicError::SolverUnknown)
409                }
410            }
411            other => Err(SymbolicError::Solver(format!("unexpected solver response `{other}`"))),
412        }
413    }
414
415    fn is_sat_inner(
416        &mut self,
417        cx: &mut SymCx,
418        constraints: &[SymBoolExpr],
419        defer_hard_arith_without_witness: bool,
420    ) -> Result<BranchFeasibility, SymbolicError> {
421        self.sat_queries += 1;
422        let smt_constraints =
423            normalize_sat_constraints(cx, constraints, &mut self.normalization_cache);
424        let cache_key = smt_constraints.clone();
425        if let Some(result) = self.sat_cache.get(&cache_key) {
426            self.sat_cache_hits += 1;
427            trace!(result, "is_sat: normalized cache hit");
428            return Ok(if *result { BranchFeasibility::Sat } else { BranchFeasibility::Unsat });
429        }
430        if self.has_cached_unsat_subset(&cache_key) {
431            self.sat_cache_hits += 1;
432            trace!("is_sat: normalized unsat subset cache hit");
433            self.cache_sat_result(cache_key, false);
434            return Ok(BranchFeasibility::Unsat);
435        }
436        if defer_hard_arith_without_witness
437            && let Some((condition, base)) = constraints.split_last()
438            && {
439                let normalized_base =
440                    normalize_sat_constraints(cx, base, &mut self.normalization_cache);
441                self.sat_cache.get(&normalized_base) == Some(&true)
442            }
443            && {
444                let mut complement = Vec::with_capacity(constraints.len());
445                complement.extend(base.iter().cloned());
446                complement.push(condition.clone().not(cx));
447                let normalized_complement =
448                    normalize_sat_constraints(cx, &complement, &mut self.normalization_cache);
449                self.has_cached_unsat_subset(&normalized_complement)
450            }
451        {
452            self.sat_cache_hits += 1;
453            trace!("is_sat: branch complement unsat cache hit");
454            self.cache_sat_result(cache_key, true);
455            return Ok(BranchFeasibility::Sat);
456        }
457
458        self.reserve_query()?;
459        self.queries += 1;
460        let _span = trace_span!(
461            "solver_query",
462            query_id = self.queries,
463            constraint_count = constraints.len(),
464            kind = "is_sat"
465        )
466        .entered();
467        trace!(query_id = self.queries, constraint_count = constraints.len(), "solver is_sat");
468        if constraints_are_directly_unsat(cx, &smt_constraints) {
469            trace!("is_sat: direct contradiction");
470            self.cache_sat_result(cache_key, false);
471            return Ok(BranchFeasibility::Unsat);
472        }
473        if product_monotonic_unsat_normalized(&smt_constraints) {
474            trace!("is_sat: monotonic product contradiction");
475            self.cache_sat_result(cache_key, false);
476            return Ok(BranchFeasibility::Unsat);
477        }
478        if let Some(result) = checked_xor_result(constraints) {
479            let sat = matches!(result, CheckedXorResult::Model(_));
480            trace!(sat, "is_sat: checked XOR specialist");
481            self.cache_sat_result(cache_key.clone(), sat);
482            if let CheckedXorResult::Model(model) = result {
483                self.cache_model_result(cache_key, model);
484            }
485            return Ok(if sat { BranchFeasibility::Sat } else { BranchFeasibility::Unsat });
486        }
487        if !constraints.is_empty()
488            && eval_model_constraints(constraints, &SymbolicModel::default())
489            && !constraints.iter().any(SymBoolExpr::contains_gasleft)
490        {
491            self.cache_sat_result(cache_key, true);
492            return Ok(BranchFeasibility::Sat);
493        }
494        if let Some(model) = fallback_single_var_model(&smt_constraints)
495            && eval_model_constraints(constraints, &model)
496        {
497            self.cache_sat_result(cache_key, true);
498            return Ok(BranchFeasibility::Sat);
499        }
500        if let Some(model) = fallback_bounded_model(&smt_constraints)
501            && eval_model_constraints(constraints, &model)
502        {
503            self.cache_sat_result(cache_key, true);
504            return Ok(BranchFeasibility::Sat);
505        }
506        if checked_mul_guard_branch_model(
507            cx,
508            &smt_constraints,
509            constraints,
510            &self.replayable_storage,
511        )
512        .is_some()
513        {
514            trace!("is_sat: validated constructive checked-multiply guard model");
515            self.cache_sat_result(cache_key, true);
516            return Ok(BranchFeasibility::Sat);
517        }
518        if constraints_prefer_hard_arith_fallback_first(cx, &smt_constraints) {
519            if validated_hard_arith_fallback_model(cx, &smt_constraints, constraints).is_some() {
520                self.heuristic_witnesses += 1;
521                trace!("is_sat: validated hard arithmetic fallback model before solver");
522                self.cache_sat_result(cache_key, true);
523                return Ok(BranchFeasibility::Sat);
524            }
525            if defer_hard_arith_without_witness {
526                trace!("is_sat: deferring hard arithmetic branch without local witness");
527                return Ok(BranchFeasibility::NeedsSolver);
528            }
529        }
530        let output = match self.query_normalized(cx, &smt_constraints, false, constraints) {
531            Ok(output) => output,
532            Err(SymbolicError::SolverUnknown) => {
533                if validated_hard_arith_fallback_model(cx, &smt_constraints, constraints).is_some()
534                {
535                    self.heuristic_witnesses += 1;
536                    trace!("is_sat: validated hard arithmetic fallback model after solver unknown");
537                    self.cache_sat_result(cache_key, true);
538                    return Ok(BranchFeasibility::Sat);
539                }
540                return Err(SymbolicError::SolverUnknown);
541            }
542            Err(err) => return Err(err),
543        };
544        match output.lines().next().unwrap_or_default().trim() {
545            "sat" => {
546                self.cache_sat_result(cache_key, true);
547                Ok(BranchFeasibility::Sat)
548            }
549            "unsat" => {
550                self.cache_sat_result(cache_key, false);
551                Ok(BranchFeasibility::Unsat)
552            }
553            "unknown" => {
554                if validated_hard_arith_fallback_model(cx, &smt_constraints, constraints).is_some()
555                {
556                    self.heuristic_witnesses += 1;
557                    self.cache_sat_result(cache_key, true);
558                    Ok(BranchFeasibility::Sat)
559                } else {
560                    Err(SymbolicError::SolverUnknown)
561                }
562            }
563            other => Err(SymbolicError::Solver(format!("unexpected solver response `{other}`"))),
564        }
565    }
566    /// Returns the resolved commands or the stored config error.
567    pub(crate) fn commands(&self) -> Result<&[SolverCommand], SymbolicError> {
568        self.commands
569            .as_ref()
570            .map(Vec::as_slice)
571            .map_err(|err| SymbolicError::Solver(err.to_string()))
572    }
573
574    pub(crate) const fn reserve_query(&self) -> Result<(), SymbolicError> {
575        if self.queries >= self.max_queries {
576            return Err(SymbolicError::SolverQueryLimit(self.max_queries));
577        }
578        Ok(())
579    }
580
581    fn cache_sat_result(&mut self, key: Vec<SymBoolExpr>, result: bool) {
582        cache_result(&mut self.sat_cache, key, result, SYMBOLIC_SOLVER_SAT_CACHE_MAX_ENTRIES);
583    }
584
585    fn cache_model_result(&mut self, key: Vec<SymBoolExpr>, model: SymbolicModel) {
586        cache_result(&mut self.model_cache, key, model, SYMBOLIC_SOLVER_MODEL_CACHE_MAX_ENTRIES);
587    }
588
589    /// Returns whether an already-proved unsat constraint set is a subset of `key`.
590    fn has_cached_unsat_subset(&self, key: &[SymBoolExpr]) -> bool {
591        self.sat_cache
592            .iter()
593            .any(|(cached_key, result)| !*result && sorted_bool_exprs_are_subset(cached_key, key))
594    }
595
596    /// Sends already-normalized constraints to the configured solver portfolio.
597    pub(crate) fn query_normalized(
598        &mut self,
599        cx: &SymCx,
600        smt_constraints: &[SymBoolExpr],
601        model: bool,
602        model_constraints: &[SymBoolExpr],
603    ) -> Result<String, SymbolicError> {
604        self.smt_queries += 1;
605        let build_started = Instant::now();
606        let mut vars = SymbolicVars::default();
607        for constraint in smt_constraints {
608            constraint.collect_vars(&mut vars);
609        }
610
611        let configured_commands = self.commands()?.to_vec();
612        let ordered_commands = self.portfolio_scheduler.ordered_commands(&configured_commands);
613        let commands =
614            ordered_commands.iter().map(|(_, command)| command.clone()).collect::<Vec<_>>();
615
616        let mut smt = String::with_capacity(256 + smt_constraints.len().saturating_mul(192));
617        smt.push_str("(set-logic QF_BV)\n");
618        if commands.iter().all(|command| command.smt_timeout)
619            && let Some(timeout) = self.timeout.filter(|timeout| *timeout > 0)
620        {
621            let _ = writeln!(smt, "(set-option :timeout {})", timeout.saturating_mul(1000));
622        }
623        for var in vars {
624            let name = cx.symbol_name(var);
625            let _ = writeln!(smt, "(declare-fun {name} () (_ BitVec 256))");
626        }
627        write_smt_assertions(cx, &mut smt, smt_constraints)?;
628        smt.push_str("(check-sat)\n");
629        if model {
630            smt.push_str("(get-model)\n");
631        }
632        let smt_bytes = smt.len().try_into().unwrap_or(u64::MAX);
633        self.smt_input_bytes = self.smt_input_bytes.saturating_add(smt_bytes);
634        self.smt_max_query_bytes = self.smt_max_query_bytes.max(smt_bytes);
635        self.smt_build_time += build_started.elapsed();
636        if self.dump_smt {
637            let query = self.queries;
638            let _ = writeln!(std::io::stderr(), "--- symbolic SMT query {query} ---\n{smt}");
639        }
640
641        let started = Instant::now();
642        let result = if let [command] = commands.as_slice()
643            && command.smt_timeout
644            && command.program == "z3"
645            && command.args == ["-in", "-smt2"]
646        {
647            let output = self.query_z3(command, &smt).into_result();
648            SolverCommandRun { output, summaries: Vec::new() }
649        } else {
650            run_solver_commands(
651                cx,
652                &commands,
653                &smt,
654                self.timeout,
655                model.then_some(model_constraints),
656            )
657        };
658        let query_time = started.elapsed();
659        self.solver_time += query_time;
660        self.smt_max_query_time = self.smt_max_query_time.max(query_time);
661        self.portfolio_scheduler.record(&ordered_commands, &result.summaries);
662        if self.dump_smt && !result.summaries.is_empty() {
663            let _ = write!(
664                std::io::stderr(),
665                "{}",
666                format_solver_portfolio_summaries(&result.summaries)
667            );
668        }
669        result.output
670    }
671
672    fn query_z3(&mut self, command: &SolverCommand, smt: &str) -> SolverProcessOutcome {
673        let mut session = match self.z3_session.take() {
674            Some(session) => session,
675            None => match Z3Session::spawn(command) {
676                Ok(session) => session,
677                Err(err) => return SolverProcessOutcome::Error(err),
678            },
679        };
680        let outcome = session.query(command, smt, self.timeout);
681        match outcome {
682            output @ SolverProcessOutcome::Output(_) => {
683                self.z3_session = Some(session);
684                output
685            }
686            SolverProcessOutcome::Error(_) => {
687                drop(session);
688                run_solver_process(command, smt, self.timeout, &AtomicBool::new(false))
689            }
690            other => other,
691        }
692    }
693}
694
695fn cache_result<K, V>(cache: &mut HashMap<K, V>, key: K, value: V, max_entries: usize)
696where
697    K: Eq + std::hash::Hash,
698{
699    let has_capacity = cache.len() < max_entries;
700    match cache.entry(key) {
701        alloy_primitives::map::Entry::Occupied(mut entry) => {
702            entry.insert(value);
703        }
704        alloy_primitives::map::Entry::Vacant(entry) if has_capacity => {
705            entry.insert(value);
706        }
707        alloy_primitives::map::Entry::Vacant(_) => {}
708    }
709}
710
711/// Normalizes satisfiability constraints and removes soundly redundant constraints.
712fn normalize_sat_constraints(
713    cx: &mut SymCx,
714    constraints: &[SymBoolExpr],
715    normalization_cache: &mut NormalizationCache,
716) -> Vec<SymBoolExpr> {
717    let constraints = remove_implied_monotonic_constraints(
718        normalize_constraints_for_solver_cached(cx, constraints, normalization_cache),
719    );
720    remove_witnessed_isolated_hash_constraints(cx, constraints)
721}
722
723/// Removes independently satisfiable constraints over one opaque hash symbol.
724///
725/// SMT treats symbolic hashes as free bit-vector symbols. If a constraint's only SMT symbol is a
726/// hash unused by other constraints, a concrete witness proves that it cannot affect conjunction
727/// satisfiability. The witness is discarded because hash values are not replayable inputs.
728fn remove_witnessed_isolated_hash_constraints(
729    cx: &mut SymCx,
730    constraints: Vec<SymBoolExpr>,
731) -> Vec<SymBoolExpr> {
732    if !constraints.iter().any(|constraint| {
733        constraint.visit_bool(|expr| {
734            matches!(expr.kind(), SymExprKind::Keccak { .. } | SymExprKind::Hash { .. })
735        })
736    }) {
737        return constraints;
738    }
739
740    let mut symbol_constraint_counts = HashMap::<Symbol, usize>::default();
741    let hash_candidates = constraints
742        .iter()
743        .map(|constraint| {
744            let mut symbols = SymbolicVars::default();
745            let contains_hash = collect_solver_vars(constraint, &mut symbols);
746            for symbol in &symbols {
747                *symbol_constraint_counts.entry(*symbol).or_default() += 1;
748            }
749            if contains_hash && symbols.len() == 1 { symbols.first().copied() } else { None }
750        })
751        .collect::<Vec<_>>();
752
753    constraints
754        .into_iter()
755        .zip(hash_candidates)
756        .filter_map(|(constraint, candidate)| {
757            let Some(symbol) =
758                candidate.filter(|symbol| symbol_constraint_counts.get(symbol) == Some(&1))
759            else {
760                return Some(constraint);
761            };
762            let abstracted = constraint.fold_exprs(cx, &mut |cx, expr| match expr.kind() {
763                SymExprKind::Keccak { name, .. } | SymExprKind::Hash { name, .. }
764                    if *name == symbol =>
765                {
766                    SymExpr::get_var(cx, symbol)
767                }
768                _ => expr,
769            });
770            let removable = fallback_single_var_model(std::slice::from_ref(&abstracted)).is_some();
771            (!removable).then_some(constraint)
772        })
773        .collect()
774}
775
776/// Collects variables as the SMT writer sees them, stopping at opaque hash leaves.
777fn collect_solver_vars(constraint: &SymBoolExpr, vars: &mut SymbolicVars) -> bool {
778    fn visit_bool(expr: &SymBoolExpr, vars: &mut SymbolicVars) -> bool {
779        match expr.kind() {
780            SymBoolExprKind::Const(_) => false,
781            SymBoolExprKind::Not(expr) => visit_bool(expr, vars),
782            SymBoolExprKind::And(exprs) => {
783                let mut contains_hash = false;
784                for expr in exprs.iter() {
785                    contains_hash |= visit_bool(expr, vars);
786                }
787                contains_hash
788            }
789            SymBoolExprKind::Cmp(_, left, right) => {
790                visit_word(left, vars) | visit_word(right, vars)
791            }
792        }
793    }
794
795    fn visit_word(expr: &SymExpr, vars: &mut SymbolicVars) -> bool {
796        match expr.kind() {
797            SymExprKind::Const(_) => false,
798            SymExprKind::Var(symbol) | SymExprKind::GasLeft(symbol) => {
799                vars.insert(*symbol);
800                false
801            }
802            SymExprKind::Keccak { name, .. } | SymExprKind::Hash { name, .. } => {
803                vars.insert(*name);
804                true
805            }
806            SymExprKind::Not(expr) => visit_word(expr, vars),
807            SymExprKind::BinOp(_, left, right) => visit_word(left, vars) | visit_word(right, vars),
808            SymExprKind::TernOp(_, left, right, modulus) => {
809                visit_word(left, vars) | visit_word(right, vars) | visit_word(modulus, vars)
810            }
811            SymExprKind::Ite(condition, then_expr, else_expr) => {
812                visit_bool(condition, vars)
813                    | visit_word(then_expr, vars)
814                    | visit_word(else_expr, vars)
815            }
816        }
817    }
818
819    visit_bool(constraint, vars)
820}
821
822/// Returns a hard-arithmetic fallback model only after validating it against original constraints.
823fn validated_hard_arith_fallback_model(
824    cx: &SymCx,
825    normalized_constraints: &[SymBoolExpr],
826    original_constraints: &[SymBoolExpr],
827) -> Option<SymbolicModel> {
828    let model = hard_arith_fallback_model(cx, normalized_constraints)?;
829    eval_model_constraints(original_constraints, &model).then_some(model)
830}
831
832#[derive(Clone, Debug, Default)]
833struct PortfolioScheduler {
834    history: Vec<VecDeque<PortfolioSchedulerSignal>>,
835}
836
837#[derive(Clone, Copy, Debug, PartialEq, Eq)]
838enum PortfolioSchedulerSignal {
839    Winner { speed_bonus: i64 },
840    InvalidModel,
841    Error,
842    Unknown,
843    Neutral,
844}
845
846impl PortfolioSchedulerSignal {
847    /// Returns the scheduler signal represented by one solver run summary.
848    fn from_summary(summary: &SolverRunSummary) -> Self {
849        let speed_bonus = PORTFOLIO_SCHEDULER_MAX_SPEED_BONUS.saturating_sub(
850            summary.elapsed.as_millis().min(PORTFOLIO_SCHEDULER_SPEED_BONUS_CAP_MS) as i64,
851        );
852        match (summary.winner, summary.outcome) {
853            (true, SolverOutcome::SatValid | SolverOutcome::Unsat) => Self::Winner { speed_bonus },
854            (_, SolverOutcome::SatInvalid) => Self::InvalidModel,
855            (_, SolverOutcome::Error | SolverOutcome::Unexpected) => Self::Error,
856            (_, SolverOutcome::Unknown | SolverOutcome::TimeoutOrUnknown) => Self::Unknown,
857            _ => Self::Neutral,
858        }
859    }
860
861    /// Returns the numeric score contribution for adaptive portfolio ordering.
862    const fn score(self) -> i64 {
863        match self {
864            Self::Winner { speed_bonus } => 1_000 + speed_bonus,
865            Self::InvalidModel => -1_000,
866            Self::Error => -750,
867            Self::Unknown => -250,
868            Self::Neutral => 0,
869        }
870    }
871}
872
873impl PortfolioScheduler {
874    /// Returns configured commands ordered by recent portfolio performance.
875    fn ordered_commands(&mut self, commands: &[SolverCommand]) -> Vec<(usize, SolverCommand)> {
876        self.history.resize_with(commands.len(), VecDeque::new);
877        let mut ordered = commands.iter().cloned().enumerate().collect::<Vec<_>>();
878        ordered.sort_by(|(left_index, _), (right_index, _)| {
879            self.score(*right_index)
880                .cmp(&self.score(*left_index))
881                .then_with(|| left_index.cmp(right_index))
882        });
883        ordered
884    }
885
886    /// Records one query's portfolio summaries against original configured solver indexes.
887    fn record(
888        &mut self,
889        ordered_commands: &[(usize, SolverCommand)],
890        summaries: &[SolverRunSummary],
891    ) {
892        for summary in summaries {
893            let Some(run_index) = summary.index else { continue };
894            let Some((configured_index, _)) = ordered_commands.get(run_index) else { continue };
895            let Some(history) = self.history.get_mut(*configured_index) else { continue };
896            let signal = PortfolioSchedulerSignal::from_summary(summary);
897            if matches!(signal, PortfolioSchedulerSignal::Neutral) {
898                continue;
899            }
900            history.push_back(signal);
901            if history.len() > PORTFOLIO_SCHEDULER_HISTORY {
902                history.pop_front();
903            }
904        }
905    }
906
907    /// Returns the recent-performance score for one configured solver index.
908    fn score(&self, index: usize) -> i64 {
909        self.history
910            .get(index)
911            .into_iter()
912            .flatten()
913            .rev()
914            .enumerate()
915            .map(|(age, signal)| {
916                let recency = PORTFOLIO_SCHEDULER_HISTORY
917                    .saturating_sub(age)
918                    .max(PORTFOLIO_SCHEDULER_MIN_RECENCY_WEIGHT as usize)
919                    as i64;
920                recency * signal.score()
921            })
922            .sum()
923    }
924}
925
926/// Returns the subprocess commands for the configured SMT solver setup.
927pub(crate) fn solver_commands_for_config(
928    config: &SymbolicConfig,
929) -> Result<Vec<SolverCommand>, SolverConfigError> {
930    if let Some(command) = config.solver_command.as_deref().filter(|command| !command.is_empty()) {
931        return Ok(vec![SolverCommand::new(split_solver_command(command)?, false)?]);
932    }
933
934    let portfolio = config
935        .solver_portfolio
936        .iter()
937        .map(|entry| entry.trim())
938        .filter(|entry| !entry.is_empty())
939        .collect::<Vec<_>>();
940    if !portfolio.is_empty() {
941        return portfolio.into_iter().map(solver_command_for_portfolio_entry).collect();
942    }
943
944    Ok(vec![named_solver_command(&config.solver)?])
945}
946
947/// Returns the default command for a known solver name.
948pub(crate) fn named_solver_command(solver: &str) -> Result<SolverCommand, SolverConfigError> {
949    let (parts, smt_timeout) = match solver {
950        "z3" => (vec!["z3", "-in", "-smt2"], true),
951        "yices" => (vec!["yices-smt2", "--bvconst-in-decimal"], false),
952        "cvc5" => (
953            vec![
954                "cvc5",
955                "--produce-models",
956                "--lang",
957                "smt2",
958                "--bv-print-consts-as-indexed-symbols",
959            ],
960            false,
961        ),
962        "cvc5-int" => (
963            vec![
964                "cvc5",
965                "--produce-models",
966                "--lang",
967                "smt2",
968                "--bv-print-consts-as-indexed-symbols",
969                "--solve-bv-as-int=iand",
970                "--iand-mode=bitwise",
971            ],
972            false,
973        ),
974        "bitwuzla" => (vec!["bitwuzla", "--produce-models"], false),
975        "bitwuzla-abs" => (vec!["bitwuzla", "--produce-models", "--abstraction"], false),
976        // Preserve existing behavior for custom z3-compatible executable names/paths.
977        custom => (vec![custom, "-in", "-smt2"], true),
978    };
979    let parts = parts.into_iter().map(str::to_string).collect::<Vec<_>>();
980    SolverCommand::new(parts, smt_timeout)
981}
982
983/// Returns the command for one configured portfolio entry.
984pub(crate) fn solver_command_for_portfolio_entry(
985    entry: &str,
986) -> Result<SolverCommand, SolverConfigError> {
987    if entry.chars().any(|ch| ch.is_whitespace() || matches!(ch, '"' | '\'' | '\\')) {
988        SolverCommand::new(split_solver_command(entry)?, false)
989    } else {
990        named_solver_command(entry)
991    }
992}
993
994/// Splits a shell-like solver command into argv parts.
995pub(crate) fn split_solver_command(command: &str) -> Result<Vec<String>, SolverConfigError> {
996    let parts = shlex::split(command).ok_or(SolverConfigError::InvalidShellQuoting)?;
997    if parts.is_empty() {
998        return Err(SolverConfigError::EmptyCommand);
999    }
1000
1001    Ok(parts)
1002}
1003
1004#[derive(Debug)]
1005enum SolverProcessOutcome {
1006    Output(String),
1007    Unknown,
1008    Cancelled,
1009    Error(String),
1010}
1011
1012impl SolverProcessOutcome {
1013    fn into_result(self) -> Result<String, SymbolicError> {
1014        match self {
1015            Self::Output(output) => Ok(output),
1016            Self::Unknown => Err(SymbolicError::SolverUnknown),
1017            Self::Cancelled => {
1018                warn!("solver query was cancelled");
1019                Err(SymbolicError::Solver("solver query was cancelled".to_string()))
1020            }
1021            Self::Error(err) => Err(SymbolicError::Solver(err)),
1022        }
1023    }
1024}
1025
1026#[derive(Debug)]
1027struct SolverProcessResult {
1028    index: usize,
1029    display: String,
1030    scheduled_after: Duration,
1031    started_after: Duration,
1032    elapsed: Duration,
1033    outcome: SolverProcessOutcome,
1034}
1035
1036#[derive(Debug)]
1037struct ScheduledSolver {
1038    index: usize,
1039    command: SolverCommand,
1040    launch_after: Duration,
1041}
1042
1043#[derive(Debug)]
1044struct SolverCommandRun {
1045    output: Result<String, SymbolicError>,
1046    summaries: Vec<SolverRunSummary>,
1047}
1048
1049#[derive(Debug)]
1050pub(crate) struct SolverRunSummary {
1051    index: Option<usize>,
1052    display: String,
1053    scheduled_after: Option<Duration>,
1054    started_after: Option<Duration>,
1055    elapsed: Duration,
1056    outcome: SolverOutcome,
1057    detail: Option<String>,
1058    winner: bool,
1059}
1060
1061impl SolverRunSummary {
1062    /// Builds a portfolio run summary with no detail or winner marker.
1063    pub(crate) const fn new(display: String, elapsed: Duration, outcome: SolverOutcome) -> Self {
1064        Self {
1065            index: None,
1066            display,
1067            scheduled_after: None,
1068            started_after: None,
1069            elapsed,
1070            outcome,
1071            detail: None,
1072            winner: false,
1073        }
1074    }
1075
1076    /// Attaches the configured portfolio order and launch delay to this summary.
1077    pub(crate) const fn with_schedule(
1078        mut self,
1079        index: usize,
1080        scheduled_after: Duration,
1081        started_after: Option<Duration>,
1082    ) -> Self {
1083        self.index = Some(index);
1084        self.scheduled_after = Some(scheduled_after);
1085        self.started_after = started_after;
1086        self
1087    }
1088
1089    fn with_detail(mut self, detail: String) -> Self {
1090        self.detail = Some(detail);
1091        self
1092    }
1093
1094    /// Marks this solver run as the portfolio result winner.
1095    pub(crate) const fn winner(mut self) -> Self {
1096        self.winner = true;
1097        self
1098    }
1099}
1100
1101/// Runs one or more solver commands and returns the first decisive SMT-LIB response.
1102fn run_solver_commands(
1103    cx: &SymCx,
1104    commands: &[SolverCommand],
1105    smt: &str,
1106    timeout: Option<u32>,
1107    model_constraints: Option<&[SymBoolExpr]>,
1108) -> SolverCommandRun {
1109    if commands.is_empty() {
1110        return SolverCommandRun {
1111            output: Err(SymbolicError::Solver("symbolic solver portfolio is empty".to_string())),
1112            summaries: Vec::new(),
1113        };
1114    }
1115    if commands.len() == 1 {
1116        let output =
1117            run_solver_process(&commands[0], smt, timeout, &AtomicBool::new(false)).into_result();
1118        return SolverCommandRun { output, summaries: Vec::new() };
1119    }
1120
1121    let cancel = Arc::new(AtomicBool::new(false));
1122    let (tx, rx) = mpsc::channel();
1123    thread::scope(|scope| {
1124        let started_at = Instant::now();
1125        let mut pending = scheduled_portfolio(commands);
1126        let mut running = 0usize;
1127
1128        let mut saw_unknown = false;
1129        let mut saw_unsat = false;
1130        let mut saw_invalid_sat_model = false;
1131        let mut errors = Vec::new();
1132        let mut decisive = None;
1133        let mut summaries = Vec::new();
1134
1135        while running > 0 || !pending.is_empty() {
1136            if decisive.is_none() {
1137                let now = started_at.elapsed();
1138                let mut launched = false;
1139                while pending
1140                    .front()
1141                    .is_some_and(|solver| solver.launch_after <= now || (running == 0 && !launched))
1142                {
1143                    let solver = pending.pop_front().expect("pending solver exists");
1144                    let tx = tx.clone();
1145                    let cancel = Arc::clone(&cancel);
1146                    let started_after = started_at.elapsed();
1147                    running += 1;
1148                    launched = true;
1149                    scope.spawn(move || {
1150                        let start = Instant::now();
1151                        let outcome = run_solver_process(&solver.command, smt, timeout, &cancel);
1152                        let _ = tx.send(SolverProcessResult {
1153                            index: solver.index,
1154                            display: solver.command.display,
1155                            scheduled_after: solver.launch_after,
1156                            started_after,
1157                            elapsed: start.elapsed(),
1158                            outcome,
1159                        });
1160                    });
1161                }
1162            }
1163
1164            if running == 0 {
1165                continue;
1166            }
1167
1168            let result = if decisive.is_none() {
1169                next_portfolio_launch_wait(started_at, &pending)
1170                    .map_or_else(|| rx.recv().ok(), |wait| rx.recv_timeout(wait).ok())
1171            } else {
1172                rx.recv().ok()
1173            };
1174            let Some(result) = result else {
1175                continue;
1176            };
1177            running = running.saturating_sub(1);
1178            let SolverProcessResult {
1179                index,
1180                display,
1181                scheduled_after,
1182                started_after,
1183                elapsed,
1184                outcome,
1185            } = result;
1186            if decisive.is_some() {
1187                summaries.push(summary_for_cancelled_solver_result(
1188                    index,
1189                    display,
1190                    scheduled_after,
1191                    started_after,
1192                    elapsed,
1193                    outcome,
1194                ));
1195                continue;
1196            }
1197            match outcome {
1198                SolverProcessOutcome::Output(output)
1199                    if output.lines().next().unwrap_or_default().trim() == "sat" =>
1200                {
1201                    if let Some(constraints) = model_constraints
1202                        && let Err(err) = validate_solver_model_output(cx, &output, constraints)
1203                    {
1204                        summaries.push(
1205                            SolverRunSummary::new(
1206                                display.clone(),
1207                                elapsed,
1208                                SolverOutcome::SatInvalid,
1209                            )
1210                            .with_schedule(index, scheduled_after, Some(started_after))
1211                            .with_detail(err.to_string()),
1212                        );
1213                        saw_invalid_sat_model = true;
1214                        errors.push(format!("{display}: {err}"));
1215                        continue;
1216                    }
1217                    summaries.push(
1218                        SolverRunSummary::new(display, elapsed, SolverOutcome::SatValid)
1219                            .with_schedule(index, scheduled_after, Some(started_after))
1220                            .winner(),
1221                    );
1222                    decisive = Some(output);
1223                    cancel.store(true, Ordering::SeqCst);
1224                    while let Some(solver) = pending.pop_front() {
1225                        summaries.push(summary_for_unstarted_solver(solver));
1226                    }
1227                }
1228                SolverProcessOutcome::Output(output)
1229                    if output.lines().next().unwrap_or_default().trim() == "unsat" =>
1230                {
1231                    summaries.push(
1232                        SolverRunSummary::new(display, elapsed, SolverOutcome::Unsat)
1233                            .with_schedule(index, scheduled_after, Some(started_after)),
1234                    );
1235                    saw_unsat = true;
1236                }
1237                SolverProcessOutcome::Output(output)
1238                    if output.lines().next().unwrap_or_default().trim() == "unknown" =>
1239                {
1240                    summaries.push(
1241                        SolverRunSummary::new(display, elapsed, SolverOutcome::Unknown)
1242                            .with_schedule(index, scheduled_after, Some(started_after)),
1243                    );
1244                    saw_unknown = true;
1245                }
1246                SolverProcessOutcome::Output(output) => {
1247                    let first_line = output.lines().next().unwrap_or_default().trim().to_string();
1248                    summaries.push(
1249                        SolverRunSummary::new(display.clone(), elapsed, SolverOutcome::Unexpected)
1250                            .with_schedule(index, scheduled_after, Some(started_after))
1251                            .with_detail(first_line.clone()),
1252                    );
1253                    errors.push(format!("{display}: unexpected solver response `{first_line}`"));
1254                }
1255                SolverProcessOutcome::Unknown => {
1256                    summaries.push(
1257                        SolverRunSummary::new(display, elapsed, SolverOutcome::TimeoutOrUnknown)
1258                            .with_schedule(index, scheduled_after, Some(started_after)),
1259                    );
1260                    saw_unknown = true;
1261                }
1262                SolverProcessOutcome::Cancelled => {
1263                    summaries.push(
1264                        SolverRunSummary::new(display, elapsed, SolverOutcome::Cancelled)
1265                            .with_schedule(index, scheduled_after, Some(started_after)),
1266                    );
1267                }
1268                SolverProcessOutcome::Error(err) => {
1269                    summaries.push(
1270                        SolverRunSummary::new(display.clone(), elapsed, SolverOutcome::Error)
1271                            .with_schedule(index, scheduled_after, Some(started_after))
1272                            .with_detail(err.clone()),
1273                    );
1274                    errors.push(format!("{display}: {err}"));
1275                }
1276            }
1277        }
1278
1279        if decisive.is_none()
1280            && saw_unsat
1281            && let Some(summary) =
1282                summaries.iter_mut().find(|summary| summary.outcome == SolverOutcome::Unsat)
1283        {
1284            summary.winner = true;
1285        }
1286
1287        let output = if let Some(output) = decisive {
1288            Ok(output)
1289        } else if saw_invalid_sat_model {
1290            Err(SymbolicError::Solver(errors.join("; ")))
1291        } else if saw_unsat {
1292            Ok("unsat\n".to_string())
1293        } else if saw_unknown {
1294            Err(SymbolicError::SolverUnknown)
1295        } else {
1296            Err(SymbolicError::Solver(errors.join("; ")))
1297        };
1298
1299        SolverCommandRun { output, summaries }
1300    })
1301}
1302
1303/// Returns the staged launch plan for a configured portfolio.
1304fn scheduled_portfolio(commands: &[SolverCommand]) -> VecDeque<ScheduledSolver> {
1305    commands
1306        .iter()
1307        .cloned()
1308        .enumerate()
1309        .map(|(index, command)| ScheduledSolver {
1310            index,
1311            command,
1312            launch_after: portfolio_launch_delay(index),
1313        })
1314        .collect()
1315}
1316
1317/// Returns when the solver at `index` should be started, relative to query start.
1318const fn portfolio_launch_delay(index: usize) -> Duration {
1319    match index {
1320        0 => Duration::ZERO,
1321        1 => SECOND_PORTFOLIO_SOLVER_DELAY,
1322        index => RESCUE_PORTFOLIO_SOLVER_DELAY.saturating_mul(index.saturating_sub(1) as u32),
1323    }
1324}
1325
1326/// Returns how long the supervisor can wait before the next pending solver is due.
1327fn next_portfolio_launch_wait(
1328    started_at: Instant,
1329    pending: &VecDeque<ScheduledSolver>,
1330) -> Option<Duration> {
1331    pending.front().map(|solver| {
1332        solver.launch_after.checked_sub(started_at.elapsed()).unwrap_or(Duration::ZERO)
1333    })
1334}
1335
1336/// Summarizes a solver that was never launched because the portfolio already won.
1337fn summary_for_unstarted_solver(solver: ScheduledSolver) -> SolverRunSummary {
1338    SolverRunSummary::new(solver.command.display, Duration::ZERO, SolverOutcome::NotStarted)
1339        .with_schedule(solver.index, solver.launch_after, None)
1340}
1341
1342/// Summarizes a solver result received after a portfolio winner was chosen.
1343fn summary_for_cancelled_solver_result(
1344    index: usize,
1345    display: String,
1346    scheduled_after: Duration,
1347    started_after: Duration,
1348    elapsed: Duration,
1349    outcome: SolverProcessOutcome,
1350) -> SolverRunSummary {
1351    let summary = match outcome {
1352        SolverProcessOutcome::Output(output)
1353            if output.lines().next().unwrap_or_default().trim() == "sat" =>
1354        {
1355            SolverRunSummary::new(display, elapsed, SolverOutcome::SatAfterWinner)
1356        }
1357        SolverProcessOutcome::Output(output)
1358            if output.lines().next().unwrap_or_default().trim() == "unsat" =>
1359        {
1360            SolverRunSummary::new(display, elapsed, SolverOutcome::UnsatAfterWinner)
1361        }
1362        SolverProcessOutcome::Output(output)
1363            if output.lines().next().unwrap_or_default().trim() == "unknown" =>
1364        {
1365            SolverRunSummary::new(display, elapsed, SolverOutcome::UnknownAfterWinner)
1366        }
1367        SolverProcessOutcome::Output(output) => {
1368            SolverRunSummary::new(display, elapsed, SolverOutcome::Unexpected)
1369                .with_detail(output.lines().next().unwrap_or_default().trim().to_string())
1370        }
1371        SolverProcessOutcome::Unknown => {
1372            SolverRunSummary::new(display, elapsed, SolverOutcome::TimeoutOrUnknown)
1373        }
1374        SolverProcessOutcome::Cancelled => {
1375            SolverRunSummary::new(display, elapsed, SolverOutcome::Cancelled)
1376        }
1377        SolverProcessOutcome::Error(err) => {
1378            SolverRunSummary::new(display, elapsed, SolverOutcome::Error).with_detail(err)
1379        }
1380    };
1381    summary.with_schedule(index, scheduled_after, Some(started_after))
1382}
1383
1384/// Formats solver portfolio outcome diagnostics.
1385fn format_solver_portfolio_summaries(summaries: &[SolverRunSummary]) -> String {
1386    let mut output = String::new();
1387    let _ = writeln!(output, "--- symbolic solver portfolio outcomes ---");
1388    for summary in summaries {
1389        let marker = if summary.winner { " winner" } else { "" };
1390        let schedule = summary.index.zip(summary.scheduled_after).map(|(index, delay)| {
1391            let started = summary
1392                .started_after
1393                .map(|started| format!(" started +{started:.3?}"))
1394                .unwrap_or_default();
1395            format!("#{} scheduled +{delay:.3?}{started} ", index + 1)
1396        });
1397        let _ = write!(
1398            output,
1399            "{}{}: {} in {:.3?}{}",
1400            schedule.as_deref().unwrap_or_default(),
1401            summary.display,
1402            summary.outcome,
1403            summary.elapsed,
1404            marker
1405        );
1406        if let Some(detail) = summary.detail.as_deref().filter(|detail| !detail.is_empty()) {
1407            let _ = write!(output, " ({detail})");
1408        }
1409        let _ = writeln!(output);
1410    }
1411    output
1412}
1413
1414struct Z3Session {
1415    child: SolverChild,
1416    stdin: ChildStdin,
1417    stdout: Receiver<Result<String, String>>,
1418    stderr: Receiver<String>,
1419    stdout_thread: Option<JoinHandle<()>>,
1420    stderr_thread: Option<JoinHandle<()>>,
1421}
1422
1423impl Z3Session {
1424    fn spawn(command: &SolverCommand) -> Result<Self, String> {
1425        let mut child = spawn_solver_process(command)?;
1426        let stdin = child.child_mut().stdin.take().expect("piped solver stdin is available");
1427        let stdout = child.child_mut().stdout.take().expect("piped solver stdout is available");
1428        let stderr = child.child_mut().stderr.take().expect("piped solver stderr is available");
1429
1430        let (stdout_tx, stdout_rx) = mpsc::channel();
1431        let stdout_thread = thread::spawn(move || {
1432            for line in BufReader::new(stdout).lines() {
1433                let line = line.map_err(|err| format!("failed to read solver output: {err}"));
1434                let failed = line.is_err();
1435                if stdout_tx.send(line).is_err() || failed {
1436                    break;
1437                }
1438            }
1439        });
1440        let (stderr_tx, stderr_rx) = mpsc::channel();
1441        let stderr_thread = thread::spawn(move || {
1442            let mut stderr = BufReader::new(stderr);
1443            let mut output = String::new();
1444            let _ = stderr.read_to_string(&mut output);
1445            let _ = stderr_tx.send(output);
1446        });
1447
1448        Ok(Self {
1449            child,
1450            stdin,
1451            stdout: stdout_rx,
1452            stderr: stderr_rx,
1453            stdout_thread: Some(stdout_thread),
1454            stderr_thread: Some(stderr_thread),
1455        })
1456    }
1457
1458    fn query(
1459        &mut self,
1460        command: &SolverCommand,
1461        smt: &str,
1462        timeout: Option<u32>,
1463    ) -> SolverProcessOutcome {
1464        if let Err(err) = self
1465            .stdin
1466            .write_all(b"(reset)\n")
1467            .and_then(|_| self.stdin.write_all(smt.as_bytes()))
1468            .and_then(|_| writeln!(self.stdin, "(echo \"{Z3_QUERY_END}\")"))
1469            .and_then(|_| self.stdin.flush())
1470        {
1471            return SolverProcessOutcome::Error(format!("failed to write solver query: {err}"));
1472        }
1473
1474        let started_at = Instant::now();
1475        let timeout = timeout
1476            .filter(|seconds| *seconds > 0)
1477            .map(|seconds| Duration::from_secs(seconds.into()));
1478        let mut output = String::new();
1479        loop {
1480            let Some(wait) = solver_wait_duration(started_at.elapsed(), timeout) else {
1481                return SolverProcessOutcome::Unknown;
1482            };
1483            match self.stdout.recv_timeout(wait) {
1484                Ok(Ok(line)) if line == Z3_QUERY_END => {
1485                    return SolverProcessOutcome::Output(output);
1486                }
1487                Ok(Ok(line)) => {
1488                    output.push_str(&line);
1489                    output.push('\n');
1490                }
1491                Ok(Err(err)) => return SolverProcessOutcome::Error(err),
1492                Err(RecvTimeoutError::Timeout) => {}
1493                Err(RecvTimeoutError::Disconnected) => {
1494                    let stderr =
1495                        self.stderr.recv_timeout(SOLVER_CANCEL_CHECK_INTERVAL).unwrap_or_default();
1496                    return match self.child.child_mut().try_wait() {
1497                        Ok(Some(status)) => SolverProcessOutcome::Error(solver_exit_error(
1498                            command, status, &output, &stderr,
1499                        )),
1500                        Ok(None) => SolverProcessOutcome::Error(
1501                            "solver stdout closed before the query completed".to_string(),
1502                        ),
1503                        Err(err) => SolverProcessOutcome::Error(format!(
1504                            "failed to query solver process status: {err}"
1505                        )),
1506                    };
1507                }
1508            }
1509        }
1510    }
1511}
1512
1513impl Drop for Z3Session {
1514    fn drop(&mut self) {
1515        self.child.terminate();
1516        if let Some(thread) = self.stdout_thread.take() {
1517            let _ = thread.join();
1518        }
1519        if let Some(thread) = self.stderr_thread.take() {
1520            let _ = thread.join();
1521        }
1522    }
1523}
1524
1525fn spawn_solver_process(command: &SolverCommand) -> Result<SolverChild, String> {
1526    Command::new(&command.program)
1527        .args(&command.args)
1528        .stdin(Stdio::piped())
1529        .stdout(Stdio::piped())
1530        .stderr(Stdio::piped())
1531        .spawn()
1532        .map(SolverChild::new)
1533        .map_err(|err| format!("failed to spawn `{}`: {err}", command.display))
1534}
1535
1536/// Runs one solver process to completion, timeout, or cooperative cancellation.
1537fn run_solver_process(
1538    command: &SolverCommand,
1539    smt: &str,
1540    timeout: Option<u32>,
1541    cancel: &AtomicBool,
1542) -> SolverProcessOutcome {
1543    let mut child = match spawn_solver_process(command) {
1544        Ok(child) => child,
1545        Err(err) => return SolverProcessOutcome::Error(err),
1546    };
1547
1548    if let Some(mut stdin) = child.child_mut().stdin.take()
1549        && let Err(err) = stdin.write_all(smt.as_bytes())
1550    {
1551        return SolverProcessOutcome::Error(format!("failed to write solver query: {err}"));
1552    }
1553
1554    let started_at = Instant::now();
1555    let timeout =
1556        timeout.filter(|seconds| *seconds > 0).map(|seconds| Duration::from_secs(seconds.into()));
1557    loop {
1558        if cancel.load(Ordering::SeqCst) {
1559            return SolverProcessOutcome::Cancelled;
1560        }
1561
1562        let Some(wait) = solver_wait_duration(started_at.elapsed(), timeout) else {
1563            return SolverProcessOutcome::Unknown;
1564        };
1565
1566        match child.child_mut().wait_timeout(wait) {
1567            Ok(Some(_)) => break,
1568            Ok(None) => {}
1569            Err(err) => {
1570                return SolverProcessOutcome::Error(format!(
1571                    "failed to wait for solver process: {err}"
1572                ));
1573            }
1574        }
1575    }
1576
1577    let output = match child.wait_with_output() {
1578        Ok(output) => output,
1579        Err(err) => {
1580            return SolverProcessOutcome::Error(format!("failed to read solver output: {err}"));
1581        }
1582    };
1583    let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
1584    if !output.status.success() {
1585        let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
1586        return SolverProcessOutcome::Error(solver_exit_error(
1587            command,
1588            output.status,
1589            &stdout,
1590            &stderr,
1591        ));
1592    }
1593    SolverProcessOutcome::Output(stdout)
1594}
1595
1596fn solver_wait_duration(elapsed: Duration, timeout: Option<Duration>) -> Option<Duration> {
1597    let Some(timeout) = timeout else {
1598        return Some(SOLVER_CANCEL_CHECK_INTERVAL);
1599    };
1600    let remaining = timeout.checked_sub(elapsed)?;
1601    if remaining.is_zero() { None } else { Some(remaining.min(SOLVER_CANCEL_CHECK_INTERVAL)) }
1602}
1603
1604struct SolverChild {
1605    child: Option<Child>,
1606}
1607
1608impl SolverChild {
1609    const fn new(child: Child) -> Self {
1610        Self { child: Some(child) }
1611    }
1612
1613    const fn child_mut(&mut self) -> &mut Child {
1614        self.child.as_mut().expect("solver child exists")
1615    }
1616
1617    fn wait_with_output(mut self) -> std::io::Result<Output> {
1618        self.child.take().expect("solver child exists").wait_with_output()
1619    }
1620
1621    fn terminate(&mut self) {
1622        if let Some(mut child) = self.child.take() {
1623            let _ = child.kill();
1624            let _ = child.wait();
1625        }
1626    }
1627}
1628
1629impl Drop for SolverChild {
1630    fn drop(&mut self) {
1631        self.terminate();
1632    }
1633}
1634
1635fn solver_exit_error(
1636    command: &SolverCommand,
1637    status: std::process::ExitStatus,
1638    stdout: &str,
1639    stderr: &str,
1640) -> String {
1641    let mut message = format!("`{}` exited with {status}", command.display);
1642    if !stderr.trim().is_empty() {
1643        message.push_str(": ");
1644        message.push_str(stderr.trim());
1645    }
1646    if !stdout.trim().is_empty() {
1647        message.push_str("; stdout: ");
1648        message.push_str(stdout.trim());
1649    }
1650    message
1651}
1652
1653pub(crate) fn parse_and_validate_model(
1654    cx: &SymCx,
1655    output: &str,
1656    constraints: &[SymBoolExpr],
1657) -> Result<SymbolicModel, SymbolicError> {
1658    let symbols = model_symbols_for_constraints(cx, constraints);
1659    let model = parse_model_with_symbols(output, &symbols)?;
1660    if eval_model_constraints(constraints, &model) {
1661        Ok(model)
1662    } else {
1663        let reason = if constraints.iter().any(SymBoolExpr::contains_keccak) {
1664            "solver model does not satisfy path constraints involving symbolic Keccak heuristic"
1665        } else {
1666            "solver model does not satisfy path constraints"
1667        };
1668        debug!(
1669            constraint_count = constraints.len(),
1670            reason, "solver model does not satisfy path constraints"
1671        );
1672        Err(SymbolicError::Solver(reason.to_string()))
1673    }
1674}
1675
1676pub(crate) fn validate_solver_model_output(
1677    cx: &SymCx,
1678    output: &str,
1679    constraints: &[SymBoolExpr],
1680) -> Result<(), SymbolicError> {
1681    parse_and_validate_model(cx, output, constraints).map(|_| ())
1682}
1683
1684fn parse_model_with_symbols(
1685    output: &str,
1686    symbols: &HashMap<String, Symbol>,
1687) -> Result<SymbolicModel, SymbolicError> {
1688    parse_model_with_symbol(output, |name| symbols.get(name).copied())
1689}
1690
1691fn parse_model_with_symbol(
1692    output: &str,
1693    mut symbol_for: impl FnMut(&str) -> Option<Symbol>,
1694) -> Result<SymbolicModel, SymbolicError> {
1695    let mut values = SymbolicModel::default();
1696    parse_model_values(output, |name, value| {
1697        if let Some(symbol) = symbol_for(name) {
1698            values.insert(symbol, value);
1699        }
1700    })?;
1701    Ok(values)
1702}
1703
1704fn parse_model_values(
1705    output: &str,
1706    mut insert_value: impl FnMut(&str, U256),
1707) -> Result<(), SymbolicError> {
1708    let mut tokens = output
1709        .split(|c: char| c.is_whitespace() || matches!(c, '(' | ')'))
1710        .filter(|token| !token.is_empty());
1711    while let Some(token) = tokens.next() {
1712        if token == "define-fun" {
1713            let Some(name) = tokens.next() else { continue };
1714            while let Some(value) = tokens.next() {
1715                if let Some(hex) = value.strip_prefix("#x") {
1716                    if hex.len() > 64 {
1717                        return Err(SymbolicError::Solver(
1718                            "solver hex model value exceeds 256 bits".to_string(),
1719                        ));
1720                    }
1721                    let mut bytes = [0u8; 32];
1722                    let decoded = alloy_primitives::hex::decode(hex).map_err(|err| {
1723                        SymbolicError::Solver(format!("invalid solver hex model value: {err}"))
1724                    })?;
1725                    let start = 32usize.saturating_sub(decoded.len());
1726                    bytes[start..start + decoded.len()].copy_from_slice(&decoded);
1727                    insert_value(name, U256::from_be_bytes(bytes));
1728                    break;
1729                }
1730                if let Some(binary) = value.strip_prefix("#b") {
1731                    if binary.len() > 256 {
1732                        return Err(SymbolicError::Solver(
1733                            "solver binary model value exceeds 256 bits".to_string(),
1734                        ));
1735                    }
1736                    let parsed = U256::from_str_radix(binary, 2).map_err(|err| {
1737                        SymbolicError::Solver(format!("invalid solver binary model value: {err}"))
1738                    })?;
1739                    insert_value(name, parsed);
1740                    break;
1741                }
1742                if value == "_"
1743                    && let Some(bv) = tokens.next().and_then(|v| v.strip_prefix("bv"))
1744                {
1745                    let parsed = U256::from_str_radix(bv, 10).map_err(|err| {
1746                        SymbolicError::Solver(format!("invalid solver decimal model value: {err}"))
1747                    })?;
1748                    insert_value(name, parsed);
1749                    break;
1750                }
1751            }
1752        }
1753    }
1754    Ok(())
1755}
1756
1757fn model_symbols_for_constraints(
1758    cx: &SymCx,
1759    constraints: &[SymBoolExpr],
1760) -> HashMap<String, Symbol> {
1761    let mut vars = SymbolicVars::default();
1762    for constraint in constraints {
1763        constraint.collect_vars(&mut vars);
1764    }
1765    vars.into_iter().map(|symbol| (cx.symbol_name(symbol).to_owned(), symbol)).collect()
1766}