1use super::*;
4use std::{
5 io::{BufRead, BufReader, Read},
6 process::{Child, ChildStdin, Output},
7 sync::mpsc::{Receiver, RecvTimeoutError},
8 thread::JoinHandle,
9};
10use wait_timeout::ChildExt;
11
12mod fallback;
13mod normalize;
14mod reasoning;
15mod smt;
16
17use fallback::{checked_mul_guard_branch_model, constraints_prefer_hard_arith_fallback_first};
18use normalize::{
19 constraints_are_directly_unsat, normalize_constraints_for_solver_cached,
20 sorted_bool_exprs_are_subset,
21};
22use reasoning::{product_monotonic_unsat_normalized, remove_implied_monotonic_constraints};
23use smt::write_smt_assertions;
24
25pub(crate) use fallback::{
26 fallback_single_var_model, fallback_two_var_model, hard_arith_fallback_model,
27};
28
29#[cfg(test)]
30pub(crate) use normalize::{
31 normalize_bool_for_solver, normalize_constraints_for_solver, normalize_expr_for_solver,
32};
33#[cfg(test)]
34pub(crate) use reasoning::product_monotonic_unsat;
35
36const Z3_QUERY_END: &str = "foundry-query-complete";
37
38#[derive(Debug, thiserror::Error)]
40pub(crate) enum SolverConfigError {
41 #[error("symbolic solver command is empty")]
43 EmptyCommand,
44 #[error("invalid shell quoting in symbolic solver command")]
46 InvalidShellQuoting,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
50pub(crate) enum SolverOutcome {
51 Cancelled,
52 Error,
53 NotStarted,
54 SatAfterWinner,
55 SatInvalid,
56 SatValid,
57 TimeoutOrUnknown,
58 Unknown,
59 UnknownAfterWinner,
60 Unsat,
61 UnsatAfterWinner,
62 Unexpected,
63}
64
65impl SolverOutcome {
66 const fn as_str(self) -> &'static str {
68 match self {
69 Self::Cancelled => "cancelled",
70 Self::Error => "error",
71 Self::NotStarted => "not-started",
72 Self::SatAfterWinner => "sat-after-winner",
73 Self::SatInvalid => "sat-invalid",
74 Self::SatValid => "sat-valid",
75 Self::TimeoutOrUnknown => "timeout-or-unknown",
76 Self::Unknown => "unknown",
77 Self::UnknownAfterWinner => "unknown-after-winner",
78 Self::Unsat => "unsat",
79 Self::UnsatAfterWinner => "unsat-after-winner",
80 Self::Unexpected => "unexpected",
81 }
82 }
83}
84
85impl fmt::Display for SolverOutcome {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 f.write_str(self.as_str())
88 }
89}
90
91pub(crate) type QueryObserver = Box<dyn Fn(usize) + Send + Sync + 'static>;
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub(crate) enum BranchFeasibility {
95 Sat,
96 Unsat,
97 NeedsSolver,
98}
99
100impl BranchFeasibility {
101 const fn from_bool(sat: bool) -> Self {
102 if sat { Self::Sat } else { Self::Unsat }
103 }
104
105 const fn into_result(self) -> Result<bool, SymbolicError> {
106 match self {
107 Self::Sat => Ok(true),
108 Self::Unsat => Ok(false),
109 Self::NeedsSolver => Err(SymbolicError::SolverUnknown),
110 }
111 }
112}
113#[derive(Clone, Debug, PartialEq, Eq)]
114pub(crate) struct SolverCommand {
115 program: String,
116 args: Vec<String>,
117 display: String,
118 smt_timeout: bool,
119}
120
121impl SolverCommand {
122 pub(crate) fn new(parts: Vec<String>, smt_timeout: bool) -> Result<Self, SolverConfigError> {
124 let mut parts = parts.into_iter();
125 let Some(program) = parts.next().filter(|part| !part.is_empty()) else {
126 return Err(SolverConfigError::EmptyCommand);
127 };
128 let args = parts.collect::<Vec<_>>();
129 let display = std::iter::once(program.as_str())
130 .chain(args.iter().map(String::as_str))
131 .collect::<Vec<_>>()
132 .join(" ");
133 Ok(Self { program, args, display, smt_timeout })
134 }
135
136 #[cfg(test)]
137 pub(crate) fn program(&self) -> &str {
138 &self.program
139 }
140
141 #[cfg(test)]
142 pub(crate) fn args(&self) -> &[String] {
143 &self.args
144 }
145
146 #[cfg(test)]
147 pub(crate) const fn smt_timeout(&self) -> bool {
148 self.smt_timeout
149 }
150}
151
152pub(crate) struct SmtLibSubprocessSolver {
153 commands: Result<Vec<SolverCommand>, SolverConfigError>,
154 timeout: Option<u32>,
155 max_queries: usize,
156 queries: usize,
157 query_observer: Option<QueryObserver>,
158 dump_smt: bool,
159 portfolio_scheduler: PortfolioScheduler,
160 portfolio_diagnostics: PortfolioDiagnostics,
161 captured_diagnostics: Option<String>,
162 heuristic_witnesses: usize,
163 replayable_storage: SymbolicVars,
164 normalization_cache: HashMap<SymBoolExpr, SymBoolExpr>,
165 sat_cache: HashMap<Vec<SymBoolExpr>, bool>,
166 model_cache: HashMap<Vec<SymBoolExpr>, SymbolicModel>,
167 sat_queries: usize,
168 model_queries: usize,
169 sat_cache_hits: usize,
170 model_cache_hits: usize,
171 smt_queries: usize,
172 solver_time: Duration,
173 smt_input_bytes: u64,
174 smt_max_query_bytes: u64,
175 smt_build_time: Duration,
176 smt_max_query_time: Duration,
177 z3_session: Option<Z3Session>,
178}
179
180impl SmtLibSubprocessSolver {
181 pub(crate) fn new(
182 commands: Result<Vec<SolverCommand>, SolverConfigError>,
183 timeout: Option<u32>,
184 max_queries: usize,
185 dump_smt: bool,
186 ) -> Self {
187 Self {
188 commands,
189 timeout,
190 max_queries,
191 queries: 0,
192 query_observer: None,
193 dump_smt,
194 portfolio_scheduler: PortfolioScheduler::default(),
195 portfolio_diagnostics: PortfolioDiagnostics::default(),
196 captured_diagnostics: None,
197 heuristic_witnesses: 0,
198 replayable_storage: SymbolicVars::default(),
199 normalization_cache: HashMap::default(),
200 sat_cache: HashMap::default(),
201 model_cache: HashMap::default(),
202 sat_queries: 0,
203 model_queries: 0,
204 sat_cache_hits: 0,
205 model_cache_hits: 0,
206 smt_queries: 0,
207 solver_time: Duration::ZERO,
208 smt_input_bytes: 0,
209 smt_max_query_bytes: 0,
210 smt_build_time: Duration::ZERO,
211 smt_max_query_time: Duration::ZERO,
212 z3_session: None,
213 }
214 }
215
216 pub(crate) fn from_config(config: &SymbolicConfig) -> Self {
218 Self::new(
219 solver_commands_for_config(config),
220 config.timeout,
221 config.max_solver_queries as usize,
222 config.dump_smt,
223 )
224 }
225
226 pub(crate) fn stats(&self) -> SymbolicStats {
228 SymbolicStats {
229 paths: 0,
230 solver_queries: self.queries,
231 smt_queries: self.smt_queries,
232 sat_queries: self.sat_queries,
233 model_queries: self.model_queries,
234 sat_cache_hits: self.sat_cache_hits,
235 model_cache_hits: self.model_cache_hits,
236 heuristic_witnesses: self.heuristic_witnesses,
237 solver_time_ms: self.solver_time.as_millis().try_into().unwrap_or(u64::MAX),
238 smt_input_bytes: self.smt_input_bytes,
239 smt_max_query_bytes: self.smt_max_query_bytes,
240 smt_build_time_ms: self.smt_build_time.as_millis().try_into().unwrap_or(u64::MAX),
241 smt_max_query_time_ms: self
242 .smt_max_query_time
243 .as_millis()
244 .try_into()
245 .unwrap_or(u64::MAX),
246 }
247 }
248
249 pub(crate) fn set_query_observer(&mut self, observer: Option<QueryObserver>) {
251 self.query_observer = observer;
252 }
253
254 pub(crate) fn portfolio_diagnostics(&self) -> Option<&PortfolioDiagnostics> {
256 (!self.portfolio_diagnostics.is_empty()).then_some(&self.portfolio_diagnostics)
257 }
258
259 pub(crate) fn capture_diagnostics(&mut self) {
261 self.captured_diagnostics.get_or_insert_with(String::new);
262 }
263
264 pub(crate) fn take_diagnostics(&mut self) -> Option<String> {
266 self.captured_diagnostics.take().filter(|diagnostics| !diagnostics.is_empty())
267 }
268
269 pub(crate) fn clear_context_caches(&mut self) {
271 self.normalization_cache.clear();
272 self.sat_cache.clear();
273 self.model_cache.clear();
274 }
275
276 #[cfg(test)]
278 pub(crate) const fn heuristic_witnesses(&self) -> usize {
279 self.heuristic_witnesses
280 }
281
282 pub(crate) fn check_available(&self) -> Result<(), SymbolicError> {
284 let commands = self.commands()?;
285 let mut errors = Vec::new();
286 for command in commands {
287 let output = match Command::new(&command.program).arg("--version").output() {
288 Ok(output) => output,
289 Err(err) => {
290 errors.push(format!("failed to execute `{}`: {err}", command.program));
291 continue;
292 }
293 };
294 if output.status.success() {
295 return Ok(());
296 }
297 errors.push(format!("`{}` is not a usable SMT solver executable", command.program));
298 }
299 Err(SymbolicError::Solver(errors.join("; ")))
300 }
301
302 #[cfg(test)]
303 pub(crate) fn is_sat(
304 &mut self,
305 cx: &mut SymCx,
306 constraints: &[SymBoolExpr],
307 ) -> Result<bool, SymbolicError> {
308 self.is_sat_inner(cx, constraints, false)?.into_result()
309 }
310
311 pub(crate) fn is_sat_with_replayable_storage(
313 &mut self,
314 cx: &mut SymCx,
315 constraints: &[SymBoolExpr],
316 replayable_storage: &SymbolicVars,
317 ) -> Result<bool, SymbolicError> {
318 let previous = std::mem::replace(&mut self.replayable_storage, replayable_storage.clone());
319 let result =
320 self.is_sat_inner(cx, constraints, false).and_then(BranchFeasibility::into_result);
321 self.replayable_storage = previous;
322 result
323 }
324
325 #[cfg(test)]
326 pub(crate) fn is_sat_branch(
327 &mut self,
328 cx: &mut SymCx,
329 constraints: &[SymBoolExpr],
330 ) -> Result<bool, SymbolicError> {
331 self.is_sat_inner(cx, constraints, true)?.into_result()
332 }
333
334 pub(crate) fn branch_feasibility_with_replayable_storage(
336 &mut self,
337 cx: &mut SymCx,
338 constraints: &[SymBoolExpr],
339 replayable_storage: &SymbolicVars,
340 ) -> Result<BranchFeasibility, SymbolicError> {
341 let previous = std::mem::replace(&mut self.replayable_storage, replayable_storage.clone());
342 let result = self.is_sat_inner(cx, constraints, true);
343 self.replayable_storage = previous;
344 result
345 }
346
347 pub(crate) fn model_with_replayable_storage(
349 &mut self,
350 cx: &mut SymCx,
351 constraints: &[SymBoolExpr],
352 replayable_storage: &SymbolicVars,
353 ) -> Result<SymbolicModel, SymbolicError> {
354 let previous = std::mem::replace(&mut self.replayable_storage, replayable_storage.clone());
355 let result = self.model(cx, constraints);
356 self.replayable_storage = previous;
357 result
358 }
359
360 pub(crate) fn model(
362 &mut self,
363 cx: &mut SymCx,
364 constraints: &[SymBoolExpr],
365 ) -> Result<SymbolicModel, SymbolicError> {
366 if constraints.iter().any(SymBoolExpr::contains_gasleft) {
369 return Err(SymbolicError::Unsupported("GAS/gasleft() not modeled"));
370 }
371 self.model_queries += 1;
372 let smt_constraints =
373 normalize_constraints_for_solver_cached(cx, constraints, &mut self.normalization_cache);
374 let cache_key = smt_constraints.clone();
375
376 if self.sat_cache.get(&cache_key) == Some(&false) {
377 self.model_cache.remove(&cache_key);
378 trace!("model: normalized sat cache says unsat");
379 return Err(SymbolicError::Solver("counterexample path became unsat".to_string()));
380 }
381 if self.has_cached_unsat_subset(&cache_key) {
382 self.cache_sat_result(cache_key.clone(), false);
383 self.model_cache.remove(&cache_key);
384 trace!("model: normalized unsat subset cache hit");
385 return Err(SymbolicError::Solver("counterexample path became unsat".to_string()));
386 }
387
388 if let Some(model) = self.model_cache.get(&cache_key) {
389 if model_satisfies_constraints(model, constraints) {
390 let model = model.clone();
391 self.model_cache_hits += 1;
392 trace!("model: normalized cache hit");
393 self.cache_sat_result(cache_key.clone(), true);
394 return Ok(model);
395 }
396 trace!("model: normalized cache hit failed validation");
397 }
398 if self.model_cache.remove(&cache_key).is_some() {
399 self.sat_cache.remove(&cache_key);
400 }
401
402 self.reserve_query()?;
403 self.record_query();
404 let _span = trace_span!(
405 "solver_query",
406 query_id = self.queries,
407 constraint_count = constraints.len(),
408 kind = "model"
409 )
410 .entered();
411 trace!(query_id = self.queries, constraint_count = constraints.len(), "solver model");
412 if let Some(model) = fallback_single_var_model(&smt_constraints)
413 && model_satisfies_constraints(&model, constraints)
414 {
415 self.cache_sat_result(cache_key.clone(), true);
416 self.cache_model_result(cache_key, model.clone());
417 return Ok(model);
418 }
419 if let Some(model) = fallback_two_var_model(&smt_constraints)
420 && model_satisfies_constraints(&model, constraints)
421 {
422 self.cache_sat_result(cache_key.clone(), true);
423 self.cache_model_result(cache_key, model.clone());
424 return Ok(model);
425 }
426 if let Some(model) = checked_mul_guard_branch_model(
427 cx,
428 &smt_constraints,
429 constraints,
430 &self.replayable_storage,
431 ) {
432 trace!("model: validated constructive checked-multiply guard model");
433 self.cache_sat_result(cache_key.clone(), true);
434 return Ok(model);
435 }
436 if constraints_prefer_hard_arith_fallback_first(cx, &smt_constraints)
437 && let Some(model) =
438 validated_hard_arith_fallback_model(cx, &smt_constraints, constraints)
439 {
440 self.heuristic_witnesses += 1;
441 trace!("model: validated hard arithmetic fallback model before solver");
442 self.cache_sat_result(cache_key.clone(), true);
443 self.cache_model_result(cache_key, model.clone());
444 return Ok(model);
445 }
446 let output = match self.query_normalized(cx, &smt_constraints, true, constraints) {
447 Ok(output) => output,
448 Err(SymbolicError::SolverUnknown) => {
449 if let Some(model) =
450 validated_hard_arith_fallback_model(cx, &smt_constraints, constraints)
451 {
452 self.heuristic_witnesses += 1;
453 trace!("model: validated hard arithmetic fallback model after solver unknown");
454 self.cache_sat_result(cache_key.clone(), true);
455 self.cache_model_result(cache_key, model.clone());
456 return Ok(model);
457 }
458 return Err(SymbolicError::SolverUnknown);
459 }
460 Err(err) => return Err(err),
461 };
462 let mut lines = output.lines();
463 match lines.next().unwrap_or_default().trim() {
464 "sat" => {
465 let model = parse_and_validate_model(cx, &output, constraints)?;
466 self.cache_sat_result(cache_key.clone(), true);
467 self.cache_model_result(cache_key, model.clone());
468 Ok(model)
469 }
470 "unsat" => {
471 self.model_cache.remove(&cache_key);
472 self.cache_sat_result(cache_key, false);
473 Err(SymbolicError::Solver("counterexample path became unsat".to_string()))
474 }
475 "unknown" => {
476 if let Some(model) =
477 validated_hard_arith_fallback_model(cx, &smt_constraints, constraints)
478 {
479 self.heuristic_witnesses += 1;
480 self.cache_sat_result(cache_key.clone(), true);
481 self.cache_model_result(cache_key, model.clone());
482 Ok(model)
483 } else {
484 Err(SymbolicError::SolverUnknown)
485 }
486 }
487 other => Err(SymbolicError::Solver(format!("unexpected solver response `{other}`"))),
488 }
489 }
490
491 fn is_sat_inner(
492 &mut self,
493 cx: &mut SymCx,
494 constraints: &[SymBoolExpr],
495 defer_hard_arith_without_witness: bool,
496 ) -> Result<BranchFeasibility, SymbolicError> {
497 self.sat_queries += 1;
498 let smt_constraints =
499 normalize_sat_constraints(cx, constraints, &mut self.normalization_cache);
500 let cache_key = smt_constraints.clone();
501 if let Some(result) = self.sat_cache.get(&cache_key) {
502 self.sat_cache_hits += 1;
503 trace!(result, "is_sat: normalized cache hit");
504 return Ok(BranchFeasibility::from_bool(*result));
505 }
506 if self.has_cached_unsat_subset(&cache_key) {
507 self.sat_cache_hits += 1;
508 trace!("is_sat: normalized unsat subset cache hit");
509 self.cache_sat_result(cache_key, false);
510 return Ok(BranchFeasibility::Unsat);
511 }
512 if defer_hard_arith_without_witness
513 && let Some((condition, base)) = constraints.split_last()
514 && {
515 let normalized_base =
516 normalize_sat_constraints(cx, base, &mut self.normalization_cache);
517 self.sat_cache.get(&normalized_base) == Some(&true)
518 }
519 && {
520 let mut complement = Vec::with_capacity(constraints.len());
521 complement.extend(base.iter().cloned());
522 complement.push(condition.clone().not(cx));
523 let normalized_complement =
524 normalize_sat_constraints(cx, &complement, &mut self.normalization_cache);
525 self.has_cached_unsat_subset(&normalized_complement)
526 }
527 {
528 self.sat_cache_hits += 1;
529 trace!("is_sat: branch complement unsat cache hit");
530 self.cache_sat_result(cache_key, true);
531 return Ok(BranchFeasibility::Sat);
532 }
533
534 self.reserve_query()?;
535 self.record_query();
536 let _span = trace_span!(
537 "solver_query",
538 query_id = self.queries,
539 constraint_count = constraints.len(),
540 kind = "is_sat"
541 )
542 .entered();
543 trace!(query_id = self.queries, constraint_count = constraints.len(), "solver is_sat");
544 if constraints_are_directly_unsat(cx, &smt_constraints) {
545 trace!("is_sat: direct contradiction");
546 self.cache_sat_result(cache_key, false);
547 return Ok(BranchFeasibility::Unsat);
548 }
549 if product_monotonic_unsat_normalized(&smt_constraints) {
550 trace!("is_sat: monotonic product contradiction");
551 self.cache_sat_result(cache_key, false);
552 return Ok(BranchFeasibility::Unsat);
553 }
554 if !constraints.is_empty()
555 && model_satisfies_constraints(&SymbolicModel::default(), constraints)
556 && !constraints.iter().any(SymBoolExpr::contains_gasleft)
557 {
558 self.cache_sat_result(cache_key, true);
559 return Ok(BranchFeasibility::Sat);
560 }
561 if let Some(model) = fallback_single_var_model(&smt_constraints)
562 && model_satisfies_constraints(&model, constraints)
563 {
564 self.cache_sat_result(cache_key, true);
565 return Ok(BranchFeasibility::Sat);
566 }
567 if let Some(model) = fallback_two_var_model(&smt_constraints)
568 && model_satisfies_constraints(&model, constraints)
569 {
570 self.cache_sat_result(cache_key, true);
571 return Ok(BranchFeasibility::Sat);
572 }
573 if checked_mul_guard_branch_model(
574 cx,
575 &smt_constraints,
576 constraints,
577 &self.replayable_storage,
578 )
579 .is_some()
580 {
581 trace!("is_sat: validated constructive checked-multiply guard model");
582 self.cache_sat_result(cache_key, true);
583 return Ok(BranchFeasibility::Sat);
584 }
585 if constraints_prefer_hard_arith_fallback_first(cx, &smt_constraints) {
586 if validated_hard_arith_fallback_model(cx, &smt_constraints, constraints).is_some() {
587 self.heuristic_witnesses += 1;
588 trace!("is_sat: validated hard arithmetic fallback model before solver");
589 self.cache_sat_result(cache_key, true);
590 return Ok(BranchFeasibility::Sat);
591 }
592 if defer_hard_arith_without_witness {
593 trace!("is_sat: deferring hard arithmetic branch without local witness");
594 return Ok(BranchFeasibility::NeedsSolver);
595 }
596 }
597 let output = match self.query_normalized(cx, &smt_constraints, false, constraints) {
598 Ok(output) => output,
599 Err(SymbolicError::SolverUnknown) => {
600 if validated_hard_arith_fallback_model(cx, &smt_constraints, constraints).is_some()
601 {
602 self.heuristic_witnesses += 1;
603 trace!("is_sat: validated hard arithmetic fallback model after solver unknown");
604 self.cache_sat_result(cache_key, true);
605 return Ok(BranchFeasibility::Sat);
606 }
607 return Err(SymbolicError::SolverUnknown);
608 }
609 Err(err) => return Err(err),
610 };
611 match output.lines().next().unwrap_or_default().trim() {
612 "sat" => {
613 self.cache_sat_result(cache_key, true);
614 Ok(BranchFeasibility::Sat)
615 }
616 "unsat" => {
617 self.cache_sat_result(cache_key, false);
618 Ok(BranchFeasibility::Unsat)
619 }
620 "unknown" => {
621 if validated_hard_arith_fallback_model(cx, &smt_constraints, constraints).is_some()
622 {
623 self.heuristic_witnesses += 1;
624 self.cache_sat_result(cache_key, true);
625 Ok(BranchFeasibility::Sat)
626 } else {
627 Err(SymbolicError::SolverUnknown)
628 }
629 }
630 other => Err(SymbolicError::Solver(format!("unexpected solver response `{other}`"))),
631 }
632 }
633 pub(crate) fn commands(&self) -> Result<&[SolverCommand], SymbolicError> {
635 self.commands
636 .as_ref()
637 .map(Vec::as_slice)
638 .map_err(|err| SymbolicError::Solver(err.to_string()))
639 }
640
641 fn emit_diagnostic(&mut self, diagnostic: fmt::Arguments<'_>) {
643 if let Some(captured_diagnostics) = &mut self.captured_diagnostics {
644 let _ = captured_diagnostics.write_fmt(diagnostic);
645 } else {
646 let mut stderr = std::io::stderr().lock();
647 let _ = stderr.write_fmt(diagnostic);
648 }
649 }
650
651 pub(crate) const fn reserve_query(&self) -> Result<(), SymbolicError> {
652 if self.queries >= self.max_queries {
653 return Err(SymbolicError::SolverQueryLimit(self.max_queries));
654 }
655 Ok(())
656 }
657
658 fn record_query(&mut self) {
660 self.queries += 1;
661 if let Some(observer) = &self.query_observer {
662 observer(self.queries);
663 }
664 }
665
666 fn cache_sat_result(&mut self, key: Vec<SymBoolExpr>, result: bool) {
668 let has_capacity = self.sat_cache.len() < SYMBOLIC_SOLVER_SAT_CACHE_MAX_ENTRIES;
669 match self.sat_cache.entry(key) {
670 alloy_primitives::map::Entry::Occupied(mut entry) => {
671 entry.insert(result);
672 }
673 alloy_primitives::map::Entry::Vacant(entry) if has_capacity => {
674 entry.insert(result);
675 }
676 alloy_primitives::map::Entry::Vacant(_) => {}
677 }
678 }
679
680 fn cache_model_result(&mut self, key: Vec<SymBoolExpr>, model: SymbolicModel) {
682 let has_capacity = self.model_cache.len() < SYMBOLIC_SOLVER_MODEL_CACHE_MAX_ENTRIES;
683 match self.model_cache.entry(key) {
684 alloy_primitives::map::Entry::Occupied(mut entry) => {
685 entry.insert(model);
686 }
687 alloy_primitives::map::Entry::Vacant(entry) if has_capacity => {
688 entry.insert(model);
689 }
690 alloy_primitives::map::Entry::Vacant(_) => {}
691 }
692 }
693
694 fn has_cached_unsat_subset(&self, key: &[SymBoolExpr]) -> bool {
696 self.sat_cache
697 .iter()
698 .any(|(cached_key, result)| !*result && sorted_bool_exprs_are_subset(cached_key, key))
699 }
700
701 pub(crate) fn query_normalized(
703 &mut self,
704 cx: &SymCx,
705 smt_constraints: &[SymBoolExpr],
706 model: bool,
707 model_constraints: &[SymBoolExpr],
708 ) -> Result<String, SymbolicError> {
709 self.smt_queries += 1;
710 let build_started = Instant::now();
711 let mut vars = SymbolicVars::default();
712 for constraint in smt_constraints {
713 constraint.collect_vars(&mut vars);
714 }
715
716 let configured_commands = self.commands()?.to_vec();
717 let ordered_commands = self.portfolio_scheduler.ordered_commands(&configured_commands);
718 let commands =
719 ordered_commands.iter().map(|(_, command)| command.clone()).collect::<Vec<_>>();
720
721 let mut smt = String::with_capacity(256 + smt_constraints.len().saturating_mul(192));
722 smt.push_str("(set-logic QF_BV)\n");
723 if commands.iter().all(|command| command.smt_timeout)
724 && let Some(timeout) = self.timeout.filter(|timeout| *timeout > 0)
725 {
726 let _ = writeln!(smt, "(set-option :timeout {})", timeout.saturating_mul(1000));
727 }
728 for var in vars {
729 let name = cx.symbol_name(var);
730 let _ = writeln!(smt, "(declare-fun {name} () (_ BitVec 256))");
731 }
732 write_smt_assertions(cx, &mut smt, smt_constraints)?;
733 smt.push_str("(check-sat)\n");
734 if model {
735 smt.push_str("(get-model)\n");
736 }
737 let smt_bytes = smt.len().try_into().unwrap_or(u64::MAX);
738 self.smt_input_bytes = self.smt_input_bytes.saturating_add(smt_bytes);
739 self.smt_max_query_bytes = self.smt_max_query_bytes.max(smt_bytes);
740 self.smt_build_time += build_started.elapsed();
741 if self.dump_smt {
742 let query = self.queries;
743 self.emit_diagnostic(format_args!("--- symbolic SMT query {query} ---\n{smt}\n"));
744 }
745
746 let started = Instant::now();
747 let result = if let [command] = commands.as_slice()
748 && command.smt_timeout
749 && command.program == "z3"
750 && command.args == ["-in", "-smt2"]
751 {
752 let output = self.query_z3(command, &smt).into_result();
753 SolverCommandRun { output, summaries: Vec::new() }
754 } else {
755 run_solver_commands(
756 cx,
757 &commands,
758 &smt,
759 self.timeout,
760 model.then_some(model_constraints),
761 )
762 };
763 let query_time = started.elapsed();
764 self.solver_time += query_time;
765 self.smt_max_query_time = self.smt_max_query_time.max(query_time);
766 self.portfolio_scheduler.record(&ordered_commands, &result.summaries);
767 if self.dump_smt {
768 self.portfolio_diagnostics.record(&result.summaries);
769 if !result.summaries.is_empty() {
770 self.emit_diagnostic(format_args!(
771 "{}",
772 format_solver_portfolio_summaries(&result.summaries)
773 ));
774 }
775 }
776 result.output
777 }
778
779 fn query_z3(&mut self, command: &SolverCommand, smt: &str) -> SolverProcessOutcome {
780 let mut session = match self.z3_session.take() {
781 Some(session) => session,
782 None => match Z3Session::spawn(command) {
783 Ok(session) => session,
784 Err(err) => return SolverProcessOutcome::Error(err),
785 },
786 };
787 let outcome = session.query(command, smt, self.timeout);
788 match outcome {
789 output @ SolverProcessOutcome::Output(_) => {
790 self.z3_session = Some(session);
791 output
792 }
793 SolverProcessOutcome::Error(_) => {
794 drop(session);
795 run_solver_process(command, smt, self.timeout, &AtomicBool::new(false))
796 }
797 other => other,
798 }
799 }
800}
801
802fn normalize_sat_constraints(
804 cx: &mut SymCx,
805 constraints: &[SymBoolExpr],
806 normalization_cache: &mut HashMap<SymBoolExpr, SymBoolExpr>,
807) -> Vec<SymBoolExpr> {
808 let constraints = remove_implied_monotonic_constraints(
809 normalize_constraints_for_solver_cached(cx, constraints, normalization_cache),
810 );
811 remove_witnessed_isolated_hash_constraints(cx, constraints)
812}
813
814fn remove_witnessed_isolated_hash_constraints(
820 cx: &mut SymCx,
821 constraints: Vec<SymBoolExpr>,
822) -> Vec<SymBoolExpr> {
823 if !constraints.iter().any(|constraint| {
824 constraint.visit_bool(|expr| {
825 matches!(expr.kind(), SymExprKind::Keccak { .. } | SymExprKind::Hash { .. })
826 })
827 }) {
828 return constraints;
829 }
830
831 let mut symbol_constraint_counts = HashMap::<Symbol, usize>::default();
832 let hash_candidates = constraints
833 .iter()
834 .map(|constraint| {
835 let mut symbols = SymbolicVars::default();
836 let contains_hash = collect_solver_vars(constraint, &mut symbols);
837 for symbol in &symbols {
838 *symbol_constraint_counts.entry(*symbol).or_default() += 1;
839 }
840 if contains_hash && symbols.len() == 1 { symbols.first().copied() } else { None }
841 })
842 .collect::<Vec<_>>();
843
844 constraints
845 .into_iter()
846 .zip(hash_candidates)
847 .filter_map(|(constraint, candidate)| {
848 let Some(symbol) =
849 candidate.filter(|symbol| symbol_constraint_counts.get(symbol) == Some(&1))
850 else {
851 return Some(constraint);
852 };
853 let abstracted = constraint.fold_exprs(cx, &mut |cx, expr| match expr.kind() {
854 SymExprKind::Keccak { name, .. } | SymExprKind::Hash { name, .. }
855 if *name == symbol =>
856 {
857 SymExpr::get_var(cx, symbol)
858 }
859 _ => expr,
860 });
861 let removable = fallback_single_var_model(std::slice::from_ref(&abstracted)).is_some();
862 (!removable).then_some(constraint)
863 })
864 .collect()
865}
866
867fn collect_solver_vars(constraint: &SymBoolExpr, vars: &mut SymbolicVars) -> bool {
869 fn visit_bool(expr: &SymBoolExpr, vars: &mut SymbolicVars) -> bool {
870 match expr.kind() {
871 SymBoolExprKind::Const(_) => false,
872 SymBoolExprKind::Not(expr) => visit_bool(expr, vars),
873 SymBoolExprKind::And(exprs) => {
874 let mut contains_hash = false;
875 for expr in exprs.iter() {
876 contains_hash |= visit_bool(expr, vars);
877 }
878 contains_hash
879 }
880 SymBoolExprKind::Cmp(_, left, right) => {
881 visit_word(left, vars) | visit_word(right, vars)
882 }
883 }
884 }
885
886 fn visit_word(expr: &SymExpr, vars: &mut SymbolicVars) -> bool {
887 match expr.kind() {
888 SymExprKind::Const(_) => false,
889 SymExprKind::Var(symbol) | SymExprKind::GasLeft(symbol) => {
890 vars.insert(*symbol);
891 false
892 }
893 SymExprKind::Keccak { name, .. } | SymExprKind::Hash { name, .. } => {
894 vars.insert(*name);
895 true
896 }
897 SymExprKind::Not(expr) => visit_word(expr, vars),
898 SymExprKind::BinOp(_, left, right) => visit_word(left, vars) | visit_word(right, vars),
899 SymExprKind::TernOp(_, left, right, modulus) => {
900 visit_word(left, vars) | visit_word(right, vars) | visit_word(modulus, vars)
901 }
902 SymExprKind::Ite(condition, then_expr, else_expr) => {
903 visit_bool(condition, vars)
904 | visit_word(then_expr, vars)
905 | visit_word(else_expr, vars)
906 }
907 }
908 }
909
910 visit_bool(constraint, vars)
911}
912
913#[cfg(test)]
914#[test]
915fn removes_only_witnessed_isolated_hash_constraints() {
916 let mut cx = SymCx::new();
917 let input = SymExpr::var(&mut cx, "input");
918 let hash = keccak_word(&mut cx, vec![input.clone()]);
919 let modulus = SymExpr::constant(&mut cx, U256::MAX);
920 let mulmod = SymExpr::ternop(&mut cx, SymTernOp::MulMod, hash.clone(), hash.clone(), modulus);
921 let hash_branch = SymBoolExpr::eq_word_const(&mut cx, &mulmod, U256::ZERO);
922 let preimage_constraint = SymBoolExpr::eq_word_const(&mut cx, &input, U256::from(1));
923
924 let remaining = remove_witnessed_isolated_hash_constraints(
925 &mut cx,
926 vec![hash_branch.clone(), preimage_constraint.clone()],
927 );
928 assert_eq!(remaining, vec![preimage_constraint]);
929
930 let shared_hash_constraint = SymBoolExpr::eq_word_const(&mut cx, &hash, U256::from(1));
931 let remaining = remove_witnessed_isolated_hash_constraints(
932 &mut cx,
933 vec![hash_branch, shared_hash_constraint],
934 );
935 assert_eq!(remaining.len(), 2, "a shared hash symbol is not an independent component");
936}
937
938fn validated_hard_arith_fallback_model(
940 cx: &SymCx,
941 normalized_constraints: &[SymBoolExpr],
942 original_constraints: &[SymBoolExpr],
943) -> Option<SymbolicModel> {
944 let model = hard_arith_fallback_model(cx, normalized_constraints)?;
945 model_satisfies_constraints(&model, original_constraints).then_some(model)
946}
947
948fn model_satisfies_constraints(
950 model: &(impl SymbolicModelLookup + ?Sized),
951 constraints: &[SymBoolExpr],
952) -> bool {
953 eval_model_constraints(constraints, model)
954}
955
956#[derive(Clone, Debug, Default)]
957struct PortfolioScheduler {
958 history: Vec<VecDeque<PortfolioSchedulerSignal>>,
959}
960
961#[derive(Clone, Copy, Debug, PartialEq, Eq)]
962enum PortfolioSchedulerSignal {
963 Winner { speed_bonus: i64 },
964 InvalidModel,
965 Error,
966 Unknown,
967 Neutral,
968}
969
970impl PortfolioSchedulerSignal {
971 fn from_summary(summary: &SolverRunSummary) -> Self {
973 let speed_bonus = PORTFOLIO_SCHEDULER_MAX_SPEED_BONUS.saturating_sub(
974 summary.elapsed.as_millis().min(PORTFOLIO_SCHEDULER_SPEED_BONUS_CAP_MS) as i64,
975 );
976 match (summary.winner, summary.outcome) {
977 (true, SolverOutcome::SatValid | SolverOutcome::Unsat) => Self::Winner { speed_bonus },
978 (_, SolverOutcome::SatInvalid) => Self::InvalidModel,
979 (_, SolverOutcome::Error | SolverOutcome::Unexpected) => Self::Error,
980 (_, SolverOutcome::Unknown | SolverOutcome::TimeoutOrUnknown) => Self::Unknown,
981 _ => Self::Neutral,
982 }
983 }
984
985 const fn is_neutral(self) -> bool {
987 matches!(self, Self::Neutral)
988 }
989
990 const fn score(self) -> i64 {
992 match self {
993 Self::Winner { speed_bonus } => 1_000 + speed_bonus,
994 Self::InvalidModel => -1_000,
995 Self::Error => -750,
996 Self::Unknown => -250,
997 Self::Neutral => 0,
998 }
999 }
1000}
1001
1002impl PortfolioScheduler {
1003 fn ordered_commands(&mut self, commands: &[SolverCommand]) -> Vec<(usize, SolverCommand)> {
1005 self.ensure_len(commands.len());
1006 let mut ordered = commands.iter().cloned().enumerate().collect::<Vec<_>>();
1007 ordered.sort_by(|(left_index, _), (right_index, _)| {
1008 self.score(*right_index)
1009 .cmp(&self.score(*left_index))
1010 .then_with(|| left_index.cmp(right_index))
1011 });
1012 ordered
1013 }
1014
1015 fn record(
1017 &mut self,
1018 ordered_commands: &[(usize, SolverCommand)],
1019 summaries: &[SolverRunSummary],
1020 ) {
1021 for summary in summaries {
1022 let Some(run_index) = summary.index else { continue };
1023 let Some((configured_index, _)) = ordered_commands.get(run_index) else { continue };
1024 let Some(history) = self.history.get_mut(*configured_index) else { continue };
1025 let signal = PortfolioSchedulerSignal::from_summary(summary);
1026 if signal.is_neutral() {
1027 continue;
1028 }
1029 history.push_back(signal);
1030 if history.len() > PORTFOLIO_SCHEDULER_HISTORY {
1031 history.pop_front();
1032 }
1033 }
1034 }
1035
1036 fn ensure_len(&mut self, len: usize) {
1038 self.history.resize_with(len, VecDeque::new);
1039 }
1040
1041 fn score(&self, index: usize) -> i64 {
1043 self.history
1044 .get(index)
1045 .into_iter()
1046 .flatten()
1047 .rev()
1048 .enumerate()
1049 .map(|(age, signal)| {
1050 let recency = PORTFOLIO_SCHEDULER_HISTORY
1051 .saturating_sub(age)
1052 .max(PORTFOLIO_SCHEDULER_MIN_RECENCY_WEIGHT as usize)
1053 as i64;
1054 recency * signal.score()
1055 })
1056 .sum()
1057 }
1058}
1059
1060pub(crate) fn solver_commands_for_config(
1062 config: &SymbolicConfig,
1063) -> Result<Vec<SolverCommand>, SolverConfigError> {
1064 if let Some(command) = config.solver_command.as_deref().filter(|command| !command.is_empty()) {
1065 return Ok(vec![SolverCommand::new(split_solver_command(command)?, false)?]);
1066 }
1067
1068 let portfolio = config
1069 .solver_portfolio
1070 .iter()
1071 .map(|entry| entry.trim())
1072 .filter(|entry| !entry.is_empty())
1073 .collect::<Vec<_>>();
1074 if !portfolio.is_empty() {
1075 return portfolio.into_iter().map(solver_command_for_portfolio_entry).collect();
1076 }
1077
1078 Ok(vec![named_solver_command(&config.solver)?])
1079}
1080
1081pub(crate) fn solver_portfolio_availability_warning(config: &SymbolicConfig) -> Option<String> {
1083 if config.solver_command.as_deref().is_some_and(|command| !command.trim().is_empty())
1084 || config.solver_portfolio.iter().all(|entry| entry.trim().is_empty())
1085 {
1086 return None;
1087 }
1088
1089 let commands = solver_commands_for_config(config).ok()?;
1090 let unavailable = commands
1091 .iter()
1092 .filter_map(|command| {
1093 solver_command_availability_error(command)
1094 .map(|err| format!("`{}` ({err})", command.display))
1095 })
1096 .collect::<Vec<_>>();
1097 if unavailable.is_empty() {
1098 return None;
1099 }
1100
1101 let suffix = if unavailable.len() == commands.len() {
1102 "No configured portfolio entries are currently available."
1103 } else {
1104 "Available portfolio entries will still be used."
1105 };
1106 Some(format!(
1107 "Symbolic solver portfolio is degraded; unavailable entries: {}. {suffix}",
1108 unavailable.join("; ")
1109 ))
1110}
1111
1112pub(crate) fn named_solver_command(solver: &str) -> Result<SolverCommand, SolverConfigError> {
1114 let (parts, smt_timeout) = match solver {
1115 "z3" => (vec!["z3", "-in", "-smt2"], true),
1116 "yices" => (vec!["yices-smt2", "--bvconst-in-decimal"], false),
1117 "cvc5" => (
1118 vec![
1119 "cvc5",
1120 "--produce-models",
1121 "--lang",
1122 "smt2",
1123 "--bv-print-consts-as-indexed-symbols",
1124 ],
1125 false,
1126 ),
1127 "cvc5-int" => (
1128 vec![
1129 "cvc5",
1130 "--produce-models",
1131 "--lang",
1132 "smt2",
1133 "--bv-print-consts-as-indexed-symbols",
1134 "--solve-bv-as-int=iand",
1135 "--iand-mode=bitwise",
1136 ],
1137 false,
1138 ),
1139 "bitwuzla" => (vec!["bitwuzla", "--produce-models"], false),
1140 "bitwuzla-abs" => (vec!["bitwuzla", "--produce-models", "--abstraction"], false),
1141 custom => (vec![custom, "-in", "-smt2"], true),
1143 };
1144 let parts = parts.into_iter().map(str::to_string).collect::<Vec<_>>();
1145 SolverCommand::new(parts, smt_timeout)
1146}
1147
1148pub(crate) fn solver_command_for_portfolio_entry(
1150 entry: &str,
1151) -> Result<SolverCommand, SolverConfigError> {
1152 if entry.chars().any(|ch| ch.is_whitespace() || matches!(ch, '"' | '\'' | '\\')) {
1153 SolverCommand::new(split_solver_command(entry)?, false)
1154 } else {
1155 named_solver_command(entry)
1156 }
1157}
1158
1159pub(crate) fn split_solver_command(command: &str) -> Result<Vec<String>, SolverConfigError> {
1161 let parts = shlex::split(command).ok_or(SolverConfigError::InvalidShellQuoting)?;
1162 if parts.is_empty() {
1163 return Err(SolverConfigError::EmptyCommand);
1164 }
1165
1166 Ok(parts)
1167}
1168
1169fn solver_command_availability_error(command: &SolverCommand) -> Option<String> {
1171 let output = match Command::new(&command.program).arg("--version").output() {
1172 Ok(output) => output,
1173 Err(err) => return Some(format!("failed to execute `{}`: {err}", command.program)),
1174 };
1175 (!output.status.success())
1176 .then(|| format!("`{}` is not a usable SMT solver executable", command.program))
1177}
1178
1179#[derive(Debug)]
1180enum SolverProcessOutcome {
1181 Output(String),
1182 Unknown,
1183 Cancelled,
1184 Error(String),
1185}
1186
1187impl SolverProcessOutcome {
1188 fn into_result(self) -> Result<String, SymbolicError> {
1189 match self {
1190 Self::Output(output) => Ok(output),
1191 Self::Unknown => Err(SymbolicError::SolverUnknown),
1192 Self::Cancelled => {
1193 warn!("solver query was cancelled");
1194 Err(SymbolicError::Solver("solver query was cancelled".to_string()))
1195 }
1196 Self::Error(err) => Err(SymbolicError::Solver(err)),
1197 }
1198 }
1199}
1200
1201#[derive(Debug)]
1202struct SolverProcessResult {
1203 index: usize,
1204 display: String,
1205 scheduled_after: Duration,
1206 started_after: Duration,
1207 elapsed: Duration,
1208 outcome: SolverProcessOutcome,
1209}
1210
1211#[derive(Debug)]
1212struct ScheduledSolver {
1213 index: usize,
1214 command: SolverCommand,
1215 launch_after: Duration,
1216}
1217
1218#[derive(Debug)]
1219struct SolverCommandRun {
1220 output: Result<String, SymbolicError>,
1221 summaries: Vec<SolverRunSummary>,
1222}
1223
1224#[derive(Debug)]
1225pub(crate) struct SolverRunSummary {
1226 index: Option<usize>,
1227 display: String,
1228 scheduled_after: Option<Duration>,
1229 started_after: Option<Duration>,
1230 elapsed: Duration,
1231 outcome: SolverOutcome,
1232 detail: Option<String>,
1233 winner: bool,
1234}
1235
1236impl SolverRunSummary {
1237 pub(crate) const fn new(display: String, elapsed: Duration, outcome: SolverOutcome) -> Self {
1239 Self {
1240 index: None,
1241 display,
1242 scheduled_after: None,
1243 started_after: None,
1244 elapsed,
1245 outcome,
1246 detail: None,
1247 winner: false,
1248 }
1249 }
1250
1251 pub(crate) const fn with_schedule(
1253 mut self,
1254 index: usize,
1255 scheduled_after: Duration,
1256 started_after: Option<Duration>,
1257 ) -> Self {
1258 self.index = Some(index);
1259 self.scheduled_after = Some(scheduled_after);
1260 self.started_after = started_after;
1261 self
1262 }
1263
1264 fn with_detail(mut self, detail: String) -> Self {
1265 self.detail = Some(detail);
1266 self
1267 }
1268
1269 pub(crate) const fn winner(mut self) -> Self {
1271 self.winner = true;
1272 self
1273 }
1274}
1275
1276#[derive(Clone, Debug, Default)]
1277pub struct PortfolioDiagnostics {
1278 queries: usize,
1279 solver_runs: usize,
1280 rescue_runs: usize,
1281 non_primary_wins: usize,
1282 rescue_wins: usize,
1283 not_started: usize,
1284 cancelled_after_winner: usize,
1285 invalid_models: usize,
1286 solver_errors: usize,
1287 winner_counts: HashMap<String, usize>,
1288 launch_counts: HashMap<String, usize>,
1289 outcome_counts: HashMap<SolverOutcome, usize>,
1290}
1291
1292impl PortfolioDiagnostics {
1293 pub const fn is_empty(&self) -> bool {
1295 self.queries == 0
1296 }
1297
1298 pub(crate) fn record(&mut self, summaries: &[SolverRunSummary]) {
1300 if summaries.len() <= 1 {
1301 return;
1302 }
1303
1304 self.queries += 1;
1305 for summary in summaries {
1306 *self.outcome_counts.entry(summary.outcome).or_default() += 1;
1307 if summary.started_after.is_some() {
1308 self.solver_runs += 1;
1309 *self.launch_counts.entry(summary.display.clone()).or_default() += 1;
1310 if summary.index.is_some_and(|index| index >= 2) {
1311 self.rescue_runs += 1;
1312 }
1313 }
1314
1315 match summary.outcome {
1316 SolverOutcome::NotStarted => self.not_started += 1,
1317 SolverOutcome::Cancelled
1318 | SolverOutcome::SatAfterWinner
1319 | SolverOutcome::UnsatAfterWinner
1320 | SolverOutcome::UnknownAfterWinner => self.cancelled_after_winner += 1,
1321 SolverOutcome::SatInvalid => self.invalid_models += 1,
1322 SolverOutcome::Error => self.solver_errors += 1,
1323 _ => {}
1324 }
1325
1326 if summary.winner {
1327 *self.winner_counts.entry(summary.display.clone()).or_default() += 1;
1328 if summary.index.is_some_and(|index| index > 0) {
1329 self.non_primary_wins += 1;
1330 }
1331 if summary.index.is_some_and(|index| index >= 2) {
1332 self.rescue_wins += 1;
1333 }
1334 }
1335 }
1336 }
1337
1338 pub fn merge(&mut self, other: &Self) {
1340 self.queries += other.queries;
1341 self.solver_runs += other.solver_runs;
1342 self.rescue_runs += other.rescue_runs;
1343 self.non_primary_wins += other.non_primary_wins;
1344 self.rescue_wins += other.rescue_wins;
1345 self.not_started += other.not_started;
1346 self.cancelled_after_winner += other.cancelled_after_winner;
1347 self.invalid_models += other.invalid_models;
1348 self.solver_errors += other.solver_errors;
1349 merge_counts(&mut self.winner_counts, &other.winner_counts);
1350 merge_counts(&mut self.launch_counts, &other.launch_counts);
1351 merge_counts(&mut self.outcome_counts, &other.outcome_counts);
1352 }
1353}
1354
1355impl fmt::Display for PortfolioDiagnostics {
1356 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1357 if self.is_empty() {
1358 return Ok(());
1359 }
1360
1361 writeln!(f, "--- symbolic solver portfolio summary ---")?;
1362 writeln!(f, "queries: {}", self.queries)?;
1363 writeln!(f, "solver runs: {}", self.solver_runs)?;
1364 writeln!(f, "rescue solver runs: {}", self.rescue_runs)?;
1365 writeln!(f, "not-started solver runs: {}", self.not_started)?;
1366 writeln!(f, "non-primary wins: {}", self.non_primary_wins)?;
1367 writeln!(f, "rescue wins: {}", self.rescue_wins)?;
1368 writeln!(f, "cancelled after winner: {}", self.cancelled_after_winner)?;
1369 writeln!(f, "invalid models: {}", self.invalid_models)?;
1370 writeln!(f, "solver errors: {}", self.solver_errors)?;
1371 if !self.winner_counts.is_empty() {
1372 writeln!(f, "winner counts:")?;
1373 let mut counts = self.winner_counts.iter().collect::<Vec<_>>();
1374 counts.sort_by_key(|(solver, _)| *solver);
1375 for (solver, count) in counts {
1376 writeln!(f, " {solver}: {count}")?;
1377 }
1378 }
1379 if !self.launch_counts.is_empty() {
1380 writeln!(f, "launch counts:")?;
1381 let mut counts = self.launch_counts.iter().collect::<Vec<_>>();
1382 counts.sort_by_key(|(solver, _)| *solver);
1383 for (solver, count) in counts {
1384 writeln!(f, " {solver}: {count}")?;
1385 }
1386 }
1387 writeln!(f, "outcome counts:")?;
1388 let mut counts = self.outcome_counts.iter().collect::<Vec<_>>();
1389 counts.sort_by_key(|(outcome, _)| **outcome);
1390 for (outcome, count) in counts {
1391 writeln!(f, " {outcome}: {count}")?;
1392 }
1393 Ok(())
1394 }
1395}
1396
1397fn merge_counts<K: Eq + std::hash::Hash + Clone>(
1398 base: &mut HashMap<K, usize>,
1399 other: &HashMap<K, usize>,
1400) {
1401 for (key, count) in other {
1402 *base.entry(key.clone()).or_default() += count;
1403 }
1404}
1405
1406fn run_solver_commands(
1408 cx: &SymCx,
1409 commands: &[SolverCommand],
1410 smt: &str,
1411 timeout: Option<u32>,
1412 model_constraints: Option<&[SymBoolExpr]>,
1413) -> SolverCommandRun {
1414 if commands.is_empty() {
1415 return SolverCommandRun {
1416 output: Err(SymbolicError::Solver("symbolic solver portfolio is empty".to_string())),
1417 summaries: Vec::new(),
1418 };
1419 }
1420 if commands.len() == 1 {
1421 let output =
1422 run_solver_process(&commands[0], smt, timeout, &AtomicBool::new(false)).into_result();
1423 return SolverCommandRun { output, summaries: Vec::new() };
1424 }
1425
1426 let cancel = Arc::new(AtomicBool::new(false));
1427 let (tx, rx) = mpsc::channel();
1428 thread::scope(|scope| {
1429 let started_at = Instant::now();
1430 let mut pending = scheduled_portfolio(commands);
1431 let mut running = 0usize;
1432
1433 let mut saw_unknown = false;
1434 let mut saw_unsat = false;
1435 let mut saw_invalid_sat_model = false;
1436 let mut errors = Vec::new();
1437 let mut decisive = None;
1438 let mut summaries = Vec::new();
1439
1440 while running > 0 || !pending.is_empty() {
1441 if decisive.is_none() {
1442 let now = started_at.elapsed();
1443 let mut launched = false;
1444 while pending
1445 .front()
1446 .is_some_and(|solver| solver.launch_after <= now || (running == 0 && !launched))
1447 {
1448 let solver = pending.pop_front().expect("pending solver exists");
1449 let tx = tx.clone();
1450 let cancel = Arc::clone(&cancel);
1451 let started_after = started_at.elapsed();
1452 running += 1;
1453 launched = true;
1454 scope.spawn(move || {
1455 let start = Instant::now();
1456 let outcome = run_solver_process(&solver.command, smt, timeout, &cancel);
1457 let _ = tx.send(SolverProcessResult {
1458 index: solver.index,
1459 display: solver.command.display,
1460 scheduled_after: solver.launch_after,
1461 started_after,
1462 elapsed: start.elapsed(),
1463 outcome,
1464 });
1465 });
1466 }
1467 }
1468
1469 if running == 0 {
1470 continue;
1471 }
1472
1473 let result = if decisive.is_none() {
1474 next_portfolio_launch_wait(started_at, &pending)
1475 .map_or_else(|| rx.recv().ok(), |wait| rx.recv_timeout(wait).ok())
1476 } else {
1477 rx.recv().ok()
1478 };
1479 let Some(result) = result else {
1480 continue;
1481 };
1482 running = running.saturating_sub(1);
1483 let SolverProcessResult {
1484 index,
1485 display,
1486 scheduled_after,
1487 started_after,
1488 elapsed,
1489 outcome,
1490 } = result;
1491 if decisive.is_some() {
1492 summaries.push(summary_for_cancelled_solver_result(
1493 index,
1494 display,
1495 scheduled_after,
1496 started_after,
1497 elapsed,
1498 outcome,
1499 ));
1500 continue;
1501 }
1502 match outcome {
1503 SolverProcessOutcome::Output(output) if solver_output_is_sat(&output) => {
1504 if let Some(constraints) = model_constraints
1505 && let Err(err) = validate_solver_model_output(cx, &output, constraints)
1506 {
1507 summaries.push(
1508 SolverRunSummary::new(
1509 display.clone(),
1510 elapsed,
1511 SolverOutcome::SatInvalid,
1512 )
1513 .with_schedule(index, scheduled_after, Some(started_after))
1514 .with_detail(err.to_string()),
1515 );
1516 saw_invalid_sat_model = true;
1517 errors.push(format!("{display}: {err}"));
1518 continue;
1519 }
1520 summaries.push(
1521 SolverRunSummary::new(display, elapsed, SolverOutcome::SatValid)
1522 .with_schedule(index, scheduled_after, Some(started_after))
1523 .winner(),
1524 );
1525 decisive = Some(output);
1526 cancel.store(true, Ordering::SeqCst);
1527 while let Some(solver) = pending.pop_front() {
1528 summaries.push(summary_for_unstarted_solver(solver));
1529 }
1530 }
1531 SolverProcessOutcome::Output(output) if solver_output_is_unsat(&output) => {
1532 summaries.push(
1533 SolverRunSummary::new(display, elapsed, SolverOutcome::Unsat)
1534 .with_schedule(index, scheduled_after, Some(started_after)),
1535 );
1536 saw_unsat = true;
1537 }
1538 SolverProcessOutcome::Output(output) if solver_output_is_unknown(&output) => {
1539 summaries.push(
1540 SolverRunSummary::new(display, elapsed, SolverOutcome::Unknown)
1541 .with_schedule(index, scheduled_after, Some(started_after)),
1542 );
1543 saw_unknown = true;
1544 }
1545 SolverProcessOutcome::Output(output) => {
1546 let first_line = first_solver_line(&output).to_string();
1547 summaries.push(
1548 SolverRunSummary::new(display.clone(), elapsed, SolverOutcome::Unexpected)
1549 .with_schedule(index, scheduled_after, Some(started_after))
1550 .with_detail(first_line.clone()),
1551 );
1552 errors.push(format!("{display}: unexpected solver response `{first_line}`"));
1553 }
1554 SolverProcessOutcome::Unknown => {
1555 summaries.push(
1556 SolverRunSummary::new(display, elapsed, SolverOutcome::TimeoutOrUnknown)
1557 .with_schedule(index, scheduled_after, Some(started_after)),
1558 );
1559 saw_unknown = true;
1560 }
1561 SolverProcessOutcome::Cancelled => {
1562 summaries.push(
1563 SolverRunSummary::new(display, elapsed, SolverOutcome::Cancelled)
1564 .with_schedule(index, scheduled_after, Some(started_after)),
1565 );
1566 }
1567 SolverProcessOutcome::Error(err) => {
1568 summaries.push(
1569 SolverRunSummary::new(display.clone(), elapsed, SolverOutcome::Error)
1570 .with_schedule(index, scheduled_after, Some(started_after))
1571 .with_detail(err.clone()),
1572 );
1573 errors.push(format!("{display}: {err}"));
1574 }
1575 }
1576 }
1577
1578 if decisive.is_none()
1579 && saw_unsat
1580 && let Some(summary) =
1581 summaries.iter_mut().find(|summary| summary.outcome == SolverOutcome::Unsat)
1582 {
1583 summary.winner = true;
1584 }
1585
1586 let output = if let Some(output) = decisive {
1587 Ok(output)
1588 } else if saw_invalid_sat_model {
1589 Err(SymbolicError::Solver(errors.join("; ")))
1590 } else if saw_unsat {
1591 Ok("unsat\n".to_string())
1592 } else if saw_unknown {
1593 Err(SymbolicError::SolverUnknown)
1594 } else {
1595 Err(SymbolicError::Solver(errors.join("; ")))
1596 };
1597
1598 SolverCommandRun { output, summaries }
1599 })
1600}
1601
1602fn scheduled_portfolio(commands: &[SolverCommand]) -> VecDeque<ScheduledSolver> {
1604 commands
1605 .iter()
1606 .cloned()
1607 .enumerate()
1608 .map(|(index, command)| ScheduledSolver {
1609 index,
1610 command,
1611 launch_after: portfolio_launch_delay(index),
1612 })
1613 .collect()
1614}
1615
1616const fn portfolio_launch_delay(index: usize) -> Duration {
1618 match index {
1619 0 => Duration::ZERO,
1620 1 => SECOND_PORTFOLIO_SOLVER_DELAY,
1621 index => RESCUE_PORTFOLIO_SOLVER_DELAY.saturating_mul(index.saturating_sub(1) as u32),
1622 }
1623}
1624
1625fn next_portfolio_launch_wait(
1627 started_at: Instant,
1628 pending: &VecDeque<ScheduledSolver>,
1629) -> Option<Duration> {
1630 pending.front().map(|solver| {
1631 solver.launch_after.checked_sub(started_at.elapsed()).unwrap_or(Duration::ZERO)
1632 })
1633}
1634
1635fn summary_for_unstarted_solver(solver: ScheduledSolver) -> SolverRunSummary {
1637 SolverRunSummary::new(solver.command.display, Duration::ZERO, SolverOutcome::NotStarted)
1638 .with_schedule(solver.index, solver.launch_after, None)
1639}
1640
1641fn summary_for_cancelled_solver_result(
1643 index: usize,
1644 display: String,
1645 scheduled_after: Duration,
1646 started_after: Duration,
1647 elapsed: Duration,
1648 outcome: SolverProcessOutcome,
1649) -> SolverRunSummary {
1650 let summary = match outcome {
1651 SolverProcessOutcome::Output(output) if solver_output_is_sat(&output) => {
1652 SolverRunSummary::new(display, elapsed, SolverOutcome::SatAfterWinner)
1653 }
1654 SolverProcessOutcome::Output(output) if solver_output_is_unsat(&output) => {
1655 SolverRunSummary::new(display, elapsed, SolverOutcome::UnsatAfterWinner)
1656 }
1657 SolverProcessOutcome::Output(output) if solver_output_is_unknown(&output) => {
1658 SolverRunSummary::new(display, elapsed, SolverOutcome::UnknownAfterWinner)
1659 }
1660 SolverProcessOutcome::Output(output) => {
1661 SolverRunSummary::new(display, elapsed, SolverOutcome::Unexpected)
1662 .with_detail(first_solver_line(&output).to_string())
1663 }
1664 SolverProcessOutcome::Unknown => {
1665 SolverRunSummary::new(display, elapsed, SolverOutcome::TimeoutOrUnknown)
1666 }
1667 SolverProcessOutcome::Cancelled => {
1668 SolverRunSummary::new(display, elapsed, SolverOutcome::Cancelled)
1669 }
1670 SolverProcessOutcome::Error(err) => {
1671 SolverRunSummary::new(display, elapsed, SolverOutcome::Error).with_detail(err)
1672 }
1673 };
1674 summary.with_schedule(index, scheduled_after, Some(started_after))
1675}
1676
1677fn format_solver_portfolio_summaries(summaries: &[SolverRunSummary]) -> String {
1679 let mut output = String::new();
1680 let _ = writeln!(output, "--- symbolic solver portfolio outcomes ---");
1681 for summary in summaries {
1682 let marker = if summary.winner { " winner" } else { "" };
1683 let schedule = summary.index.zip(summary.scheduled_after).map(|(index, delay)| {
1684 let started = summary
1685 .started_after
1686 .map(|started| format!(" started +{started:.3?}"))
1687 .unwrap_or_default();
1688 format!("#{} scheduled +{delay:.3?}{started} ", index + 1)
1689 });
1690 let _ = write!(
1691 output,
1692 "{}{}: {} in {:.3?}{}",
1693 schedule.as_deref().unwrap_or_default(),
1694 summary.display,
1695 summary.outcome,
1696 summary.elapsed,
1697 marker
1698 );
1699 if let Some(detail) = summary.detail.as_deref().filter(|detail| !detail.is_empty()) {
1700 let _ = write!(output, " ({detail})");
1701 }
1702 let _ = writeln!(output);
1703 }
1704 output
1705}
1706
1707struct Z3Session {
1708 child: SolverChild,
1709 stdin: ChildStdin,
1710 stdout: Receiver<Result<String, String>>,
1711 stderr: Receiver<String>,
1712 stdout_thread: Option<JoinHandle<()>>,
1713 stderr_thread: Option<JoinHandle<()>>,
1714}
1715
1716impl Z3Session {
1717 fn spawn(command: &SolverCommand) -> Result<Self, String> {
1718 let mut child = spawn_solver_process(command)?;
1719 let stdin = child.child_mut().stdin.take().expect("piped solver stdin is available");
1720 let stdout = child.child_mut().stdout.take().expect("piped solver stdout is available");
1721 let stderr = child.child_mut().stderr.take().expect("piped solver stderr is available");
1722
1723 let (stdout_tx, stdout_rx) = mpsc::channel();
1724 let stdout_thread = thread::spawn(move || {
1725 for line in BufReader::new(stdout).lines() {
1726 let line = line.map_err(|err| format!("failed to read solver output: {err}"));
1727 let failed = line.is_err();
1728 if stdout_tx.send(line).is_err() || failed {
1729 break;
1730 }
1731 }
1732 });
1733 let (stderr_tx, stderr_rx) = mpsc::channel();
1734 let stderr_thread = thread::spawn(move || {
1735 let mut stderr = BufReader::new(stderr);
1736 let mut output = String::new();
1737 let _ = stderr.read_to_string(&mut output);
1738 let _ = stderr_tx.send(output);
1739 });
1740
1741 Ok(Self {
1742 child,
1743 stdin,
1744 stdout: stdout_rx,
1745 stderr: stderr_rx,
1746 stdout_thread: Some(stdout_thread),
1747 stderr_thread: Some(stderr_thread),
1748 })
1749 }
1750
1751 fn query(
1752 &mut self,
1753 command: &SolverCommand,
1754 smt: &str,
1755 timeout: Option<u32>,
1756 ) -> SolverProcessOutcome {
1757 if let Err(err) = self
1758 .stdin
1759 .write_all(b"(reset)\n")
1760 .and_then(|_| self.stdin.write_all(smt.as_bytes()))
1761 .and_then(|_| writeln!(self.stdin, "(echo \"{Z3_QUERY_END}\")"))
1762 .and_then(|_| self.stdin.flush())
1763 {
1764 return SolverProcessOutcome::Error(format!("failed to write solver query: {err}"));
1765 }
1766
1767 let started_at = Instant::now();
1768 let timeout = timeout
1769 .filter(|seconds| *seconds > 0)
1770 .map(|seconds| Duration::from_secs(seconds.into()));
1771 let mut output = String::new();
1772 loop {
1773 let Some(wait) = solver_wait_duration(started_at.elapsed(), timeout) else {
1774 return SolverProcessOutcome::Unknown;
1775 };
1776 match self.stdout.recv_timeout(wait) {
1777 Ok(Ok(line)) if line == Z3_QUERY_END => {
1778 return SolverProcessOutcome::Output(output);
1779 }
1780 Ok(Ok(line)) => {
1781 output.push_str(&line);
1782 output.push('\n');
1783 }
1784 Ok(Err(err)) => return SolverProcessOutcome::Error(err),
1785 Err(RecvTimeoutError::Timeout) => {}
1786 Err(RecvTimeoutError::Disconnected) => {
1787 let stderr =
1788 self.stderr.recv_timeout(SOLVER_CANCEL_CHECK_INTERVAL).unwrap_or_default();
1789 return match self.child.child_mut().try_wait() {
1790 Ok(Some(status)) => SolverProcessOutcome::Error(solver_exit_error(
1791 command, status, &output, &stderr,
1792 )),
1793 Ok(None) => SolverProcessOutcome::Error(
1794 "solver stdout closed before the query completed".to_string(),
1795 ),
1796 Err(err) => SolverProcessOutcome::Error(format!(
1797 "failed to query solver process status: {err}"
1798 )),
1799 };
1800 }
1801 }
1802 }
1803 }
1804}
1805
1806impl Drop for Z3Session {
1807 fn drop(&mut self) {
1808 self.child.terminate();
1809 if let Some(thread) = self.stdout_thread.take() {
1810 let _ = thread.join();
1811 }
1812 if let Some(thread) = self.stderr_thread.take() {
1813 let _ = thread.join();
1814 }
1815 }
1816}
1817
1818fn spawn_solver_process(command: &SolverCommand) -> Result<SolverChild, String> {
1819 Command::new(&command.program)
1820 .args(&command.args)
1821 .stdin(Stdio::piped())
1822 .stdout(Stdio::piped())
1823 .stderr(Stdio::piped())
1824 .spawn()
1825 .map(SolverChild::new)
1826 .map_err(|err| format!("failed to spawn `{}`: {err}", command.display))
1827}
1828
1829fn run_solver_process(
1831 command: &SolverCommand,
1832 smt: &str,
1833 timeout: Option<u32>,
1834 cancel: &AtomicBool,
1835) -> SolverProcessOutcome {
1836 let mut child = match spawn_solver_process(command) {
1837 Ok(child) => child,
1838 Err(err) => return SolverProcessOutcome::Error(err),
1839 };
1840
1841 if let Some(mut stdin) = child.child_mut().stdin.take()
1842 && let Err(err) = stdin.write_all(smt.as_bytes())
1843 {
1844 return SolverProcessOutcome::Error(format!("failed to write solver query: {err}"));
1845 }
1846
1847 let started_at = Instant::now();
1848 let timeout =
1849 timeout.filter(|seconds| *seconds > 0).map(|seconds| Duration::from_secs(seconds.into()));
1850 loop {
1851 if cancel.load(Ordering::SeqCst) {
1852 return SolverProcessOutcome::Cancelled;
1853 }
1854
1855 let Some(wait) = solver_wait_duration(started_at.elapsed(), timeout) else {
1856 return SolverProcessOutcome::Unknown;
1857 };
1858
1859 match child.child_mut().wait_timeout(wait) {
1860 Ok(Some(_)) => break,
1861 Ok(None) => {}
1862 Err(err) => {
1863 return SolverProcessOutcome::Error(format!(
1864 "failed to wait for solver process: {err}"
1865 ));
1866 }
1867 }
1868 }
1869
1870 let output = match child.wait_with_output() {
1871 Ok(output) => output,
1872 Err(err) => {
1873 return SolverProcessOutcome::Error(format!("failed to read solver output: {err}"));
1874 }
1875 };
1876 let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
1877 if !output.status.success() {
1878 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
1879 return SolverProcessOutcome::Error(solver_exit_error(
1880 command,
1881 output.status,
1882 &stdout,
1883 &stderr,
1884 ));
1885 }
1886 SolverProcessOutcome::Output(stdout)
1887}
1888
1889fn solver_wait_duration(elapsed: Duration, timeout: Option<Duration>) -> Option<Duration> {
1890 let Some(timeout) = timeout else {
1891 return Some(SOLVER_CANCEL_CHECK_INTERVAL);
1892 };
1893 let remaining = timeout.checked_sub(elapsed)?;
1894 if remaining.is_zero() { None } else { Some(remaining.min(SOLVER_CANCEL_CHECK_INTERVAL)) }
1895}
1896
1897struct SolverChild {
1898 child: Option<Child>,
1899}
1900
1901impl SolverChild {
1902 const fn new(child: Child) -> Self {
1903 Self { child: Some(child) }
1904 }
1905
1906 const fn child_mut(&mut self) -> &mut Child {
1907 self.child.as_mut().expect("solver child exists")
1908 }
1909
1910 fn wait_with_output(mut self) -> std::io::Result<Output> {
1911 self.child.take().expect("solver child exists").wait_with_output()
1912 }
1913
1914 fn terminate(&mut self) {
1915 if let Some(mut child) = self.child.take() {
1916 let _ = child.kill();
1917 let _ = child.wait();
1918 }
1919 }
1920}
1921
1922impl Drop for SolverChild {
1923 fn drop(&mut self) {
1924 self.terminate();
1925 }
1926}
1927
1928fn solver_exit_error(
1929 command: &SolverCommand,
1930 status: std::process::ExitStatus,
1931 stdout: &str,
1932 stderr: &str,
1933) -> String {
1934 let mut message = format!("`{}` exited with {status}", command.display);
1935 if !stderr.trim().is_empty() {
1936 message.push_str(": ");
1937 message.push_str(stderr.trim());
1938 }
1939 if !stdout.trim().is_empty() {
1940 message.push_str("; stdout: ");
1941 message.push_str(stdout.trim());
1942 }
1943 message
1944}
1945
1946fn solver_output_is_sat(output: &str) -> bool {
1947 first_solver_line(output) == "sat"
1948}
1949
1950fn solver_output_is_unsat(output: &str) -> bool {
1951 first_solver_line(output) == "unsat"
1952}
1953
1954fn solver_output_is_unknown(output: &str) -> bool {
1955 first_solver_line(output) == "unknown"
1956}
1957
1958fn first_solver_line(output: &str) -> &str {
1959 output.lines().next().unwrap_or_default().trim()
1960}
1961
1962pub(crate) fn parse_and_validate_model(
1963 cx: &SymCx,
1964 output: &str,
1965 constraints: &[SymBoolExpr],
1966) -> Result<SymbolicModel, SymbolicError> {
1967 let symbols = model_symbols_for_constraints(cx, constraints);
1968 let model = parse_model_with_symbols(output, &symbols)?;
1969 if eval_model_constraints(constraints, &model) {
1970 Ok(model)
1971 } else {
1972 let reason = if constraints.iter().any(SymBoolExpr::contains_keccak) {
1973 "solver model does not satisfy path constraints involving symbolic Keccak heuristic"
1974 } else {
1975 "solver model does not satisfy path constraints"
1976 };
1977 debug!(
1978 constraint_count = constraints.len(),
1979 reason, "solver model does not satisfy path constraints"
1980 );
1981 Err(SymbolicError::Solver(reason.to_string()))
1982 }
1983}
1984
1985pub(crate) fn validate_solver_model_output(
1986 cx: &SymCx,
1987 output: &str,
1988 constraints: &[SymBoolExpr],
1989) -> Result<(), SymbolicError> {
1990 parse_and_validate_model(cx, output, constraints).map(|_| ())
1991}
1992
1993#[cfg(test)]
1994pub(crate) fn parse_model(output: &str) -> Result<BTreeMap<String, U256>, SymbolicError> {
1995 let mut values = BTreeMap::new();
1996 parse_model_values(output, |name, value| {
1997 values.insert(name.to_owned(), value);
1998 })?;
1999 Ok(values)
2000}
2001
2002fn parse_model_with_symbols(
2003 output: &str,
2004 symbols: &HashMap<String, Symbol>,
2005) -> Result<SymbolicModel, SymbolicError> {
2006 parse_model_with_symbol(output, |name| symbols.get(name).copied())
2007}
2008
2009fn parse_model_with_symbol(
2010 output: &str,
2011 mut symbol_for: impl FnMut(&str) -> Option<Symbol>,
2012) -> Result<SymbolicModel, SymbolicError> {
2013 let mut values = SymbolicModel::default();
2014 parse_model_values(output, |name, value| {
2015 if let Some(symbol) = symbol_for(name) {
2016 values.insert(symbol, value);
2017 }
2018 })?;
2019 Ok(values)
2020}
2021
2022fn parse_model_values(
2023 output: &str,
2024 mut insert_value: impl FnMut(&str, U256),
2025) -> Result<(), SymbolicError> {
2026 let mut tokens = output
2027 .split(|c: char| c.is_whitespace() || matches!(c, '(' | ')'))
2028 .filter(|token| !token.is_empty());
2029 while let Some(token) = tokens.next() {
2030 if token == "define-fun" {
2031 let Some(name) = tokens.next() else { continue };
2032 while let Some(value) = tokens.next() {
2033 if let Some(hex) = value.strip_prefix("#x") {
2034 if hex.len() > 64 {
2035 return Err(SymbolicError::Solver(
2036 "solver hex model value exceeds 256 bits".to_string(),
2037 ));
2038 }
2039 let mut bytes = [0u8; 32];
2040 let decoded = alloy_primitives::hex::decode(hex).map_err(|err| {
2041 SymbolicError::Solver(format!("invalid solver hex model value: {err}"))
2042 })?;
2043 let start = 32usize.saturating_sub(decoded.len());
2044 bytes[start..start + decoded.len()].copy_from_slice(&decoded);
2045 insert_value(name, U256::from_be_bytes(bytes));
2046 break;
2047 }
2048 if let Some(binary) = value.strip_prefix("#b") {
2049 if binary.len() > 256 {
2050 return Err(SymbolicError::Solver(
2051 "solver binary model value exceeds 256 bits".to_string(),
2052 ));
2053 }
2054 let parsed = U256::from_str_radix(binary, 2).map_err(|err| {
2055 SymbolicError::Solver(format!("invalid solver binary model value: {err}"))
2056 })?;
2057 insert_value(name, parsed);
2058 break;
2059 }
2060 if value == "_"
2061 && let Some(bv) = tokens.next().and_then(|v| v.strip_prefix("bv"))
2062 {
2063 let parsed = U256::from_str_radix(bv, 10).map_err(|err| {
2064 SymbolicError::Solver(format!("invalid solver decimal model value: {err}"))
2065 })?;
2066 insert_value(name, parsed);
2067 break;
2068 }
2069 }
2070 }
2071 }
2072 Ok(())
2073}
2074
2075fn model_symbols_for_constraints(
2076 cx: &SymCx,
2077 constraints: &[SymBoolExpr],
2078) -> HashMap<String, Symbol> {
2079 let mut vars = SymbolicVars::default();
2080 for constraint in constraints {
2081 constraint.collect_vars(&mut vars);
2082 }
2083 vars.into_iter().map(|symbol| (cx.symbol_name(symbol).to_owned(), symbol)).collect()
2084}
2085
2086#[cfg(test)]
2087#[test]
2088fn z3_session_resets_and_reuses_the_process() {
2089 let command = named_solver_command("z3").unwrap();
2090 if solver_command_availability_error(&command).is_some() {
2091 return;
2092 }
2093
2094 let mut solver = SmtLibSubprocessSolver::new(Ok(vec![command]), Some(5), 2, false);
2095 let mut cx = SymCx::new();
2096 let value = SymExpr::var(&mut cx, "value");
2097 let one = SymExpr::one(&mut cx);
2098 let constraints = vec![SymBoolExpr::eq(&mut cx, value, one)];
2099
2100 assert_eq!(solver.query_normalized(&cx, &constraints, false, &constraints).unwrap(), "sat\n");
2101 let pid = solver.z3_session.as_mut().unwrap().child.child_mut().id();
2102 assert_eq!(solver.query_normalized(&cx, &constraints, false, &constraints).unwrap(), "sat\n");
2103 assert_eq!(solver.z3_session.as_mut().unwrap().child.child_mut().id(), pid);
2104}