1use 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#[derive(Debug, thiserror::Error)]
36pub(crate) enum SolverConfigError {
37 #[error("symbolic solver command is empty")]
39 EmptyCommand,
40 #[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 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 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 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 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 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 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 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 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 pub(crate) fn model(
271 &mut self,
272 cx: &mut SymCx,
273 constraints: &[SymBoolExpr],
274 ) -> Result<SymbolicModel, SymbolicError> {
275 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 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 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 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
711fn 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
723fn 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
776fn 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
822fn 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 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 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 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 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 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
926pub(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
947pub(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 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
983pub(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
994pub(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 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 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 pub(crate) const fn winner(mut self) -> Self {
1096 self.winner = true;
1097 self
1098 }
1099}
1100
1101fn 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
1303fn 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
1317const 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
1326fn 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
1336fn 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
1342fn 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
1384fn 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
1536fn 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}