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