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