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