1use super::*;
2use std::cmp::Reverse;
3
4impl SymbolicExecutor {
5 pub fn new(config: SymbolicConfig) -> Self {
13 let solver = SmtLibSubprocessSolver::from_config(&config);
14 Self {
15 config,
16 cx: SymCx::new(),
17 solver,
18 deferred_incomplete: None,
19 deadline: None,
20 nested_deferred_mode: DeferredPathMode::Skip,
21 stateless_retry_safe: true,
22 }
23 }
24
25 fn reset_run_state(&mut self, use_wall_clock_deadline: bool) {
26 self.deferred_incomplete = None;
27 self.nested_deferred_mode = DeferredPathMode::Skip;
28 self.stateless_retry_safe = true;
29 self.deadline = if use_wall_clock_deadline {
30 self.config
31 .timeout
32 .filter(|seconds| *seconds > 0)
33 .map(|seconds| Instant::now() + Duration::from_secs(seconds.into()))
34 } else {
35 None
36 };
37 }
38
39 pub(super) fn check_timeout(&self) -> Result<(), SymbolicError> {
40 if let Some(deadline) = self.deadline
41 && Instant::now() >= deadline
42 {
43 return Err(SymbolicError::Timeout(self.config.timeout.unwrap_or_default()));
44 }
45 Ok(())
46 }
47
48 pub(super) fn defer_incomplete(&mut self, reason: &'static str) {
50 if self
51 .deferred_incomplete
52 .is_none_or(|reason| reason == DeferredIncomplete::HardArithmetic)
53 {
54 self.deferred_incomplete = Some(DeferredIncomplete::Unsupported(reason));
55 }
56 }
57
58 pub(super) fn defer_solver_unknown(&mut self) {
60 if self
61 .deferred_incomplete
62 .is_none_or(|reason| reason == DeferredIncomplete::HardArithmetic)
63 {
64 self.deferred_incomplete = Some(DeferredIncomplete::SolverUnknown);
65 }
66 }
67
68 pub(super) fn defer_hard_arithmetic(&mut self) {
70 self.deferred_incomplete.get_or_insert(DeferredIncomplete::HardArithmetic);
71 }
72
73 pub(super) fn is_sat_with_state(
74 &mut self,
75 state: &PathState,
76 constraints: &[SymBoolExpr],
77 ) -> Result<bool, SymbolicError> {
78 let replayable_storage = state.world.replay_storage_symbols();
79 self.solver.is_sat_with_replayable_storage(&mut self.cx, constraints, &replayable_storage)
80 }
81
82 pub(super) fn branch_is_sat_or_defer(
84 &mut self,
85 state: &PathState,
86 constraints: &[SymBoolExpr],
87 ) -> Result<bool, SymbolicError> {
88 match self.is_sat_with_state(state, constraints) {
89 Ok(feasible) => Ok(feasible),
90 Err(SymbolicError::SolverUnknown) => {
91 self.defer_solver_unknown();
92 Ok(false)
93 }
94 Err(err) => Err(err),
95 }
96 }
97
98 fn deferred_incomplete(&self) -> Option<(SymbolicStopReason, String)> {
100 match self.deferred_incomplete? {
101 DeferredIncomplete::Unsupported(reason) => Some((
102 SymbolicStopReason::Stuck,
103 format!("unsupported symbolic execution feature: {reason}"),
104 )),
105 DeferredIncomplete::SolverUnknown => {
106 Some((SymbolicStopReason::Timeout, "solver returned unknown".to_string()))
107 }
108 DeferredIncomplete::HardArithmetic => Some((
109 SymbolicStopReason::Timeout,
110 "nested hard arithmetic branch requires deferred SMT solving".to_string(),
111 )),
112 }
113 }
114
115 pub fn portfolio_diagnostics(&self) -> Option<PortfolioDiagnostics> {
117 self.solver.portfolio_diagnostics().cloned()
118 }
119
120 pub fn capture_diagnostics(&mut self) {
122 self.solver.capture_diagnostics();
123 }
124
125 pub fn take_diagnostics(&mut self) -> Option<String> {
127 self.solver.take_diagnostics()
128 }
129
130 pub fn set_query_observer(&mut self, observer: impl Fn(usize) + Send + Sync + 'static) {
132 self.solver.set_query_observer(Some(Box::new(observer)));
133 }
134
135 pub fn run<FEN: FoundryEvmNetwork>(
150 &mut self,
151 input: SymbolicRunInput<'_, FEN>,
152 ) -> SymbolicRunResult {
153 self.execute_run(input, None)
154 }
155
156 pub fn search_branch_target<FEN: FoundryEvmNetwork>(
162 &mut self,
163 input: SymbolicRunInput<'_, FEN>,
164 ) -> SymbolicBranchTargetSearchResult {
165 let mut candidates = Vec::new();
166 if input.branch_target.is_none() {
167 return SymbolicBranchTargetSearchResult {
168 candidates,
169 execution: SymbolicRunResult::Incomplete {
170 kind: SymbolicStopReason::Error,
171 reason: "branch target search requires a branch target".to_string(),
172 stats: SymbolicStats::default(),
173 },
174 };
175 }
176
177 let execution = self.execute_run(input, Some(&mut candidates));
178 if let SymbolicRunResult::Counterexample { args, calldata, .. } = &execution {
179 candidates.insert(
180 0,
181 SymbolicConcreteInput { args: args.clone(), calldata: calldata.clone() },
182 );
183 }
184 SymbolicBranchTargetSearchResult { candidates, execution }
185 }
186
187 fn execute_run<FEN: FoundryEvmNetwork>(
188 &mut self,
189 input: SymbolicRunInput<'_, FEN>,
190 mut branch_candidates: Option<&mut Vec<SymbolicConcreteInput>>,
191 ) -> SymbolicRunResult {
192 self.reset_run_state(false);
193 self.solver.clear_context_caches();
194 self.cx = SymCx::new();
195 if let Err(err) = self.solver.check_available() {
196 return SymbolicRunResult::Incomplete {
197 kind: err.stop_reason(),
198 reason: err.to_string(),
199 stats: SymbolicStats::default(),
200 };
201 }
202
203 let mut completed_paths = 0;
204 let first =
205 match self.run_inner(&input, branch_candidates.as_deref_mut(), &mut completed_paths) {
206 Ok(result) => result,
207 Err(err) => {
208 return SymbolicRunResult::Incomplete {
209 kind: err.stop_reason(),
210 reason: err.to_string(),
211 stats: self.stats_with_paths(completed_paths),
212 };
213 }
214 };
215
216 let retry_nested_deferred = branch_candidates.is_none()
219 && self.stateless_retry_safe
220 && matches!(self.deferred_incomplete, Some(DeferredIncomplete::HardArithmetic))
221 && matches!(
222 &first,
223 SymbolicRunResult::Incomplete {
224 kind: SymbolicStopReason::RevertAll | SymbolicStopReason::Timeout,
225 ..
226 }
227 );
228 if !retry_nested_deferred {
229 return first;
230 }
231
232 if let Err(err) = self.check_timeout() {
233 return SymbolicRunResult::Incomplete {
234 kind: err.stop_reason(),
235 reason: err.to_string(),
236 stats: self.stats_with_paths(completed_paths),
237 };
238 }
239
240 self.deferred_incomplete = None;
241 self.nested_deferred_mode = DeferredPathMode::Yield;
242 let prioritized = match self.run_inner(&input, None, &mut completed_paths) {
243 Ok(result) => result,
244 Err(err) => {
245 return SymbolicRunResult::Incomplete {
246 kind: err.stop_reason(),
247 reason: err.to_string(),
248 stats: self.stats_with_paths(completed_paths),
249 };
250 }
251 };
252 let drain_nested_deferred = self.stateless_retry_safe
253 && matches!(self.deferred_incomplete, Some(DeferredIncomplete::HardArithmetic))
254 && matches!(
255 &prioritized,
256 SymbolicRunResult::Incomplete {
257 kind: SymbolicStopReason::RevertAll | SymbolicStopReason::Timeout,
258 ..
259 }
260 );
261 if !drain_nested_deferred {
262 return prioritized;
263 }
264
265 if let Err(err) = self.check_timeout() {
266 return SymbolicRunResult::Incomplete {
267 kind: err.stop_reason(),
268 reason: err.to_string(),
269 stats: self.stats_with_paths(completed_paths),
270 };
271 }
272
273 self.deferred_incomplete = None;
274 self.nested_deferred_mode = DeferredPathMode::Drain;
275 match self.run_inner(&input, None, &mut completed_paths) {
276 Ok(result) => result,
277 Err(err) => SymbolicRunResult::Incomplete {
278 kind: err.stop_reason(),
279 reason: err.to_string(),
280 stats: self.stats_with_paths(completed_paths),
281 },
282 }
283 }
284
285 pub fn modeled_corpus_seed_indexes(
287 config: &SymbolicConfig,
288 function: &Function,
289 corpus_seeds: &[SymbolicConcreteInput],
290 ) -> Result<Vec<usize>, SymbolicError> {
291 let mut cx = SymCx::new();
292 let variants = SymbolicCalldata::variants(function, config, &mut cx)?;
293 let mut modeled = vec![false; corpus_seeds.len()];
294 for calldata in &variants {
295 for (idx, seed) in corpus_seeds.iter().enumerate() {
296 if !modeled[idx] && calldata.seed_model(&mut cx, seed).is_some() {
297 modeled[idx] = true;
298 }
299 }
300 }
301 Ok(modeled
302 .into_iter()
303 .enumerate()
304 .filter_map(|(idx, modeled)| modeled.then_some(idx))
305 .collect())
306 }
307
308 pub fn run_invariant<FEN: FoundryEvmNetwork>(
318 &mut self,
319 input: SymbolicInvariantRunInput<'_, FEN>,
320 ) -> SymbolicInvariantRunResult {
321 self.reset_run_state(true);
322 self.solver.clear_context_caches();
323 self.cx = SymCx::new();
324 if let Err(err) = self.solver.check_available() {
325 return SymbolicInvariantRunResult::Incomplete {
326 kind: err.stop_reason(),
327 reason: err.to_string(),
328 stats: SymbolicStats::default(),
329 };
330 }
331
332 match self.run_invariant_inner(input) {
333 Ok(result) => result,
334 Err(err) => SymbolicInvariantRunResult::Incomplete {
335 kind: err.stop_reason(),
336 reason: err.to_string(),
337 stats: self.solver.stats(),
338 },
339 }
340 }
341
342 pub fn search_invariant_candidates<FEN: FoundryEvmNetwork>(
348 &mut self,
349 input: SymbolicInvariantCandidateInput<'_, FEN>,
350 ) -> SymbolicInvariantCandidateSearchResult {
351 self.reset_run_state(true);
352 self.solver.clear_context_caches();
353 self.cx = SymCx::new();
354 if let Err(error) = self.solver.check_available() {
355 return SymbolicInvariantCandidateSearchResult {
356 candidates: Vec::new(),
357 limitation: Some(error.into()),
358 };
359 }
360
361 let mut candidates = Vec::new();
362 let mut limitation = None;
363 if let Err(error) =
364 self.search_invariant_candidates_inner(&input, &mut candidates, &mut limitation)
365 {
366 limitation.get_or_insert_with(|| error.into());
367 }
368 if limitation.is_none()
371 && let Some((kind, reason)) = self.deferred_incomplete()
372 {
373 limitation = Some(SymbolicInvariantSearchLimitation { kind, reason });
374 }
375
376 SymbolicInvariantCandidateSearchResult { candidates, limitation }
377 }
378
379 pub(super) fn run_inner<FEN: FoundryEvmNetwork>(
380 &mut self,
381 input: &SymbolicRunInput<'_, FEN>,
382 mut branch_candidates: Option<&mut Vec<SymbolicConcreteInput>>,
383 completed_paths: &mut usize,
384 ) -> Result<SymbolicRunResult, SymbolicError> {
385 let account = input
386 .executor
387 .backend()
388 .basic_ref(input.target)
389 .map_err(|err| SymbolicError::Backend(err.to_string()))?
390 .ok_or(SymbolicError::MissingAccount(input.target))?;
391 let bytecode = account.code.ok_or(SymbolicError::MissingCode(input.target))?;
392 let code = SymCode::from_bytecode(&mut self.cx, &bytecode);
393 let mut roots = Vec::new();
394 for calldata in SymbolicCalldata::variants(input.function, &self.config, &mut self.cx)? {
395 let corpus_seed_models = input
396 .corpus_seeds
397 .iter()
398 .filter_map(|seed| calldata.seed_model(&mut self.cx, seed).map(Arc::new))
399 .collect();
400 let mut root = PathState::new(
401 &mut self.cx,
402 input.target,
403 input.sender,
404 input.value,
405 calldata,
406 input.ffi_enabled,
407 );
408 root.set_corpus_seed_models(corpus_seed_models);
409 root.set_branch_target(input.branch_target);
410 root.apply_executor_env(&mut self.cx, input.executor);
411 root.world.set_storage_layout(self.config.storage_layout);
412 root.world.clear_transaction_scoped_state();
413 roots.push(root);
414 }
415 order_roots_by_corpus_seed_count(&mut roots, self.config.exploration_order);
416 let mut worklist = roots.into_iter().collect::<VecDeque<_>>();
417 let mut deferred_worklist = VecDeque::new();
418 let mut reverted_paths = 0usize;
419 let mut normal_paths = 0usize;
420 let mut success_input = None;
421 let path_limit = self.config.path_width() as usize;
422 let depth_limit = self.config.execution_depth() as usize;
423
424 while let Some(next) = self.pop_next_feasible_path(
425 &mut worklist,
426 &mut deferred_worklist,
427 DeferredPathMode::Drain,
428 )? {
429 let mut state = next.state;
430 if *completed_paths >= path_limit {
431 debug!(
432 completed_paths = *completed_paths,
433 path_limit, "symbolic path limit reached"
434 );
435 return Ok(SymbolicRunResult::Incomplete {
436 kind: SymbolicStopReason::Stuck,
437 reason: format!("symbolic path limit exceeded ({path_limit})"),
438 stats: self.stats_with_paths(*completed_paths),
439 });
440 }
441 if std::mem::take(&mut state.pending_storage_hook_revert) {
442 self.collect_branch_candidate(
443 branch_candidates.as_deref_mut(),
444 input.function,
445 &state,
446 )?;
447 *completed_paths += 1;
448 reverted_paths += 1;
449 continue;
450 }
451 let _path_span = trace_span!(
452 "symbolic_path",
453 completed_paths = *completed_paths,
454 worklist_size = worklist.len()
455 )
456 .entered();
457 trace!(
458 completed_paths = *completed_paths,
459 worklist_size = worklist.len(),
460 "exploring symbolic path"
461 );
462
463 loop {
464 self.check_timeout()?;
465 if state.depth >= depth_limit {
466 debug!(depth = state.depth, depth_limit, "symbolic depth limit reached");
467 return Ok(SymbolicRunResult::Incomplete {
468 kind: SymbolicStopReason::Stuck,
469 reason: format!("symbolic depth limit exceeded ({depth_limit})"),
470 stats: self.stats_with_paths(*completed_paths),
471 });
472 }
473 state.depth += 1;
474
475 let Some(op) = code.opcode(&mut self.cx, state.pc)? else {
476 if !state.expectations_satisfied() {
477 let Some((args, calldata_bytes)) = self
478 .materialize_stateless_counterexample_if_branch_target_satisfied(
479 state.root_calldata.as_ref().ok_or_else(|| {
480 SymbolicError::Unsupported("missing root symbolic calldata")
481 })?,
482 input.function,
483 &state,
484 )?
485 else {
486 *completed_paths += 1;
487 break;
488 };
489 return Ok(SymbolicRunResult::Counterexample {
490 args,
491 calldata: calldata_bytes,
492 stats: self.stats_with_paths(*completed_paths + 1),
493 });
494 }
495 let candidate = self.collect_branch_candidate(
496 branch_candidates.as_deref_mut(),
497 input.function,
498 &state,
499 )?;
500 if input.collect_success_input
501 && state.satisfies_branch_target()
502 && state.can_materialize_seed()
503 && success_input.as_ref().is_none_or(|(depth, _)| state.depth > *depth)
504 {
505 let input = match candidate {
506 Some(input) => input,
507 None => self.materialize_stateless_input(
508 state.root_calldata.as_ref().ok_or_else(|| {
509 SymbolicError::Unsupported("missing root symbolic calldata")
510 })?,
511 input.function,
512 &state,
513 )?,
514 };
515 success_input = Some((state.depth, input));
516 }
517 *completed_paths += 1;
518 break;
519 };
520
521 let _step_span = trace_span!("symbolic_step", pc = state.pc, op).entered();
522 match self.step(
523 input.executor,
524 &code,
525 code.jump_table(),
526 &mut state,
527 &mut worklist,
528 completed_paths,
529 op,
530 )? {
531 StepOutcome::Continue => {}
532 StepOutcome::Halt => {
533 if !state.expectations_satisfied() {
534 let Some((args, calldata_bytes)) = self
535 .materialize_stateless_counterexample_if_branch_target_satisfied(
536 state.root_calldata.as_ref().ok_or_else(|| {
537 SymbolicError::Unsupported("missing root symbolic calldata")
538 })?,
539 input.function,
540 &state,
541 )?
542 else {
543 *completed_paths += 1;
544 break;
545 };
546 return Ok(SymbolicRunResult::Counterexample {
547 args,
548 calldata: calldata_bytes,
549 stats: self.stats_with_paths(*completed_paths + 1),
550 });
551 }
552 let candidate = self.collect_branch_candidate(
553 branch_candidates.as_deref_mut(),
554 input.function,
555 &state,
556 )?;
557 if input.collect_success_input
558 && state.satisfies_branch_target()
559 && state.can_materialize_seed()
560 && success_input.as_ref().is_none_or(|(depth, _)| state.depth > *depth)
561 {
562 let input = match candidate {
563 Some(input) => input,
564 None => self.materialize_stateless_input(
565 state.root_calldata.as_ref().ok_or_else(|| {
566 SymbolicError::Unsupported("missing root symbolic calldata")
567 })?,
568 input.function,
569 &state,
570 )?,
571 };
572 success_input = Some((state.depth, input));
573 }
574 *completed_paths += 1;
575 normal_paths += 1;
576 break;
577 }
578 StepOutcome::Revert => {
579 self.collect_branch_candidate(
580 branch_candidates.as_deref_mut(),
581 input.function,
582 &state,
583 )?;
584 *completed_paths += 1;
585 reverted_paths += 1;
586 break;
587 }
588 StepOutcome::AssumeRejected => break,
589 StepOutcome::Forked => break,
590 StepOutcome::ExceptionalHalt | StepOutcome::Failure => {
591 let Some((args, calldata_bytes)) = self
592 .materialize_stateless_counterexample_if_branch_target_satisfied(
593 state.root_calldata.as_ref().ok_or_else(|| {
594 SymbolicError::Unsupported("missing root symbolic calldata")
595 })?,
596 input.function,
597 &state,
598 )?
599 else {
600 *completed_paths += 1;
601 break;
602 };
603 return Ok(SymbolicRunResult::Counterexample {
604 args,
605 calldata: calldata_bytes,
606 stats: self.stats_with_paths(*completed_paths + 1),
607 });
608 }
609 }
610 }
611 }
612
613 if normal_paths == 0 && reverted_paths > 0 {
614 debug!(completed_paths = *completed_paths, "all symbolic paths reverted");
615 return Ok(SymbolicRunResult::Incomplete {
616 kind: SymbolicStopReason::RevertAll,
617 reason: "all symbolic paths reverted".to_string(),
618 stats: self.stats_with_paths(*completed_paths),
619 });
620 }
621
622 if let Some((kind, reason)) = self.deferred_incomplete() {
623 return Ok(SymbolicRunResult::Incomplete {
624 kind,
625 reason,
626 stats: self.stats_with_paths(*completed_paths),
627 });
628 }
629
630 debug!(completed_paths = *completed_paths, "symbolic execution safe");
631 Ok(SymbolicRunResult::Safe {
632 stats: self.stats_with_paths(*completed_paths),
633 success_input: success_input.map(|(_, input)| input),
634 })
635 }
636
637 fn collect_branch_candidate(
638 &mut self,
639 candidates: Option<&mut Vec<SymbolicConcreteInput>>,
640 function: &Function,
641 state: &PathState,
642 ) -> Result<Option<SymbolicConcreteInput>, SymbolicError> {
643 let Some(candidates) = candidates else {
644 return Ok(None);
645 };
646 if !state.satisfies_branch_target() || !state.can_materialize_seed() {
647 return Ok(None);
648 }
649
650 let input = self.materialize_stateless_input(
651 state
652 .root_calldata
653 .as_ref()
654 .ok_or(SymbolicError::Unsupported("missing root symbolic calldata"))?,
655 function,
656 state,
657 )?;
658 candidates.push(input.clone());
659 Ok(Some(input))
660 }
661
662 fn materialize_stateless_counterexample_if_branch_target_satisfied(
663 &mut self,
664 calldata: &SymbolicCalldata,
665 function: &Function,
666 state: &PathState,
667 ) -> Result<Option<(Vec<DynSolValue>, Bytes)>, SymbolicError> {
668 if !state.satisfies_branch_target() {
669 return Ok(None);
670 }
671 self.materialize_stateless_counterexample(calldata, function, state).map(Some)
672 }
673
674 pub(super) fn materialize_stateless_counterexample(
675 &mut self,
676 calldata: &SymbolicCalldata,
677 function: &Function,
678 state: &PathState,
679 ) -> Result<(Vec<DynSolValue>, Bytes), SymbolicError> {
680 debug!(
681 constraint_count = state.constraints.len(),
682 "materializing counterexample from solver model"
683 );
684 self.materialize_stateless_input(calldata, function, state)
685 .map(|input| (input.args, input.calldata))
686 }
687
688 pub(super) fn materialize_stateless_input(
690 &mut self,
691 calldata: &SymbolicCalldata,
692 function: &Function,
693 state: &PathState,
694 ) -> Result<SymbolicConcreteInput, SymbolicError> {
695 let replayable_storage = state.world.replay_storage_symbols();
696 let model = self.solver.model_with_replayable_storage(
697 &mut self.cx,
698 &state.constraints,
699 &replayable_storage,
700 )?;
701 let args = calldata.model_to_args(&mut self.cx, &model)?;
702 let calldata_bytes = Bytes::from(function.abi_encode_input(&args)?);
703 Ok(SymbolicConcreteInput { args, calldata: calldata_bytes })
704 }
705
706 pub(super) fn run_invariant_inner<FEN: FoundryEvmNetwork>(
707 &mut self,
708 input: SymbolicInvariantRunInput<'_, FEN>,
709 ) -> Result<SymbolicInvariantRunResult, SymbolicError> {
710 if input.targets.is_empty() {
711 return Err(SymbolicError::Unsupported("symbolic invariant has no targets"));
712 }
713
714 let mut senders =
715 if input.senders.is_empty() { vec![input.sender] } else { input.senders.clone() };
716 senders.retain(|sender| !input.excluded_senders.contains(sender));
717 if senders.is_empty() {
718 return Err(SymbolicError::Unsupported("symbolic invariant senders are excluded"));
719 }
720 let after_invariant_for = |steps_len: usize| {
721 (steps_len == input.depth).then_some(input.after_invariant).flatten()
722 };
723 let mut completed_paths = 0usize;
724 let mut initial_state = PathState::empty(
725 &mut self.cx,
726 input.invariant_address,
727 input.sender,
728 input.ffi_enabled,
729 );
730 initial_state.apply_executor_env(&mut self.cx, input.executor);
731 initial_state.world.set_storage_layout(self.config.storage_layout);
732 let initial = SequencePath { state: initial_state, steps: Vec::new() };
733
734 if symbolic_invariant_should_check(0, input.depth, input.check_interval) {
735 for outcome in self.execute_invariant_check(
736 input.executor,
737 initial.state.clone(),
738 input.invariant_address,
739 input.sender,
740 input.invariant,
741 after_invariant_for(0),
742 &mut completed_paths,
743 )? {
744 if outcome.failed {
745 let (sequence, storage) =
746 self.materialize_sequence(&initial.steps, &outcome.state)?;
747 return Ok(SymbolicInvariantRunResult::Counterexample {
748 kind: SymbolicInvariantCounterexampleKind::Predicate,
749 sequence,
750 storage,
751 stats: self.stats_with_paths(completed_paths),
752 });
753 }
754 }
755 }
756
757 let path_limit = self.config.path_width() as usize;
758 let mut frontier = vec![initial];
759 for depth in 0..input.depth {
760 self.check_timeout()?;
761 let mut next_frontier = Vec::new();
762 for sequence in frontier {
763 self.check_timeout()?;
764 for (target_idx, target) in input.targets.iter().enumerate() {
765 for (sender_idx, sender) in senders.iter().copied().enumerate() {
766 self.check_timeout()?;
767 let prefix = format!("sequence_{depth}_{target_idx}_{sender_idx}");
768 let calldatas = SymbolicCalldata::variants_with_prefix(
769 &target.function,
770 &self.config,
771 &mut self.cx,
772 &prefix,
773 )?;
774 for calldata in calldatas {
775 let step = SequenceStepTemplate {
776 sender,
777 address: target.address,
778 contract_name: target.contract_name.clone(),
779 function: target.function.clone(),
780 calldata,
781 };
782 let calldata = step.calldata.call_data(&mut self.cx);
783 let constraints = step.calldata.constraints().to_vec();
784 let mut call = self.prepare_sequence_call(
785 input.executor,
786 sequence.state.clone(),
787 target.address,
788 sender,
789 &target.function,
790 calldata,
791 constraints,
792 )?;
793
794 while let Some(outcome) = self.execute_sequence_call_next(
795 input.executor,
796 &mut call,
797 &mut completed_paths,
798 )? {
799 let mut steps = sequence.steps.clone();
800 steps.push(step.clone());
801
802 match outcome.status {
803 CallStatus::Failure => {
804 let (sequence, storage) =
805 self.materialize_sequence(&steps, &outcome.state)?;
806 return Ok(SymbolicInvariantRunResult::Counterexample {
807 kind: SymbolicInvariantCounterexampleKind::Handler,
808 sequence,
809 storage,
810 stats: self.stats_with_paths(completed_paths),
811 });
812 }
813 CallStatus::Revert | CallStatus::ExceptionalHalt => {
814 if input.fail_on_revert {
815 let (sequence, storage) =
816 self.materialize_sequence(&steps, &outcome.state)?;
817 return Ok(
818 SymbolicInvariantRunResult::Counterexample {
819 kind: SymbolicInvariantCounterexampleKind::Predicate,
820 sequence,
821 storage,
822 stats: self.stats_with_paths(completed_paths),
823 },
824 );
825 }
826 let mut reverted_state = sequence.state.clone();
833 reverted_state
834 .take_reverted_top_level_effects(outcome.state);
835 if symbolic_invariant_should_check(
836 steps.len(),
837 input.depth,
838 input.check_interval,
839 ) {
840 for mut invariant_outcome in self
841 .execute_invariant_check(
842 input.executor,
843 reverted_state.clone(),
844 input.invariant_address,
845 input.sender,
846 input.invariant,
847 after_invariant_for(steps.len()),
848 &mut completed_paths,
849 )?
850 {
851 if invariant_outcome.failed {
852 let (sequence, storage) = self
853 .materialize_sequence(
854 &steps,
855 &invariant_outcome.state,
856 )?;
857 return Ok(
858 SymbolicInvariantRunResult::Counterexample {
859 kind: SymbolicInvariantCounterexampleKind::Predicate,
860 sequence,
861 storage,
862 stats: self
863 .stats_with_paths(completed_paths),
864 },
865 );
866 }
867 let mut state = reverted_state.clone();
868 state.take_noncommitting_check_state(
869 &mut invariant_outcome.state,
870 );
871 next_frontier.push(SequencePath {
872 state,
873 steps: steps.clone(),
874 });
875 }
876 } else {
877 next_frontier.push(SequencePath {
878 state: reverted_state,
879 steps: steps.clone(),
880 });
881 }
882 }
883 CallStatus::Success => {
884 if symbolic_invariant_should_check(
885 steps.len(),
886 input.depth,
887 input.check_interval,
888 ) {
889 for mut invariant_outcome in self
890 .execute_invariant_check(
891 input.executor,
892 outcome.state.clone(),
893 input.invariant_address,
894 input.sender,
895 input.invariant,
896 after_invariant_for(steps.len()),
897 &mut completed_paths,
898 )?
899 {
900 if invariant_outcome.failed {
901 let (sequence, storage) = self
902 .materialize_sequence(
903 &steps,
904 &invariant_outcome.state,
905 )?;
906 return Ok(
907 SymbolicInvariantRunResult::Counterexample {
908 kind: SymbolicInvariantCounterexampleKind::Predicate,
909 sequence,
910 storage,
911 stats: self
912 .stats_with_paths(completed_paths),
913 },
914 );
915 }
916 let mut state = outcome.state.clone();
917 state.take_noncommitting_check_state(
918 &mut invariant_outcome.state,
919 );
920 next_frontier.push(SequencePath {
921 state,
922 steps: steps.clone(),
923 });
924 }
925 } else {
926 next_frontier.push(SequencePath {
927 state: outcome.state,
928 steps: steps.clone(),
929 });
930 }
931 }
932 }
933
934 if completed_paths >= path_limit {
935 return Ok(SymbolicInvariantRunResult::Incomplete {
936 kind: SymbolicStopReason::Stuck,
937 reason: format!(
938 "symbolic path limit exceeded ({path_limit})"
939 ),
940 stats: self.stats_with_paths(completed_paths),
941 });
942 }
943 }
944 }
945 }
946 }
947 }
948
949 if next_frontier.is_empty() {
950 break;
951 }
952 frontier = next_frontier;
953 }
954
955 if let Some((kind, reason)) = self.deferred_incomplete() {
956 return Ok(SymbolicInvariantRunResult::Incomplete {
957 kind,
958 reason,
959 stats: self.stats_with_paths(completed_paths),
960 });
961 }
962
963 Ok(SymbolicInvariantRunResult::Safe(self.stats_with_paths(completed_paths)))
964 }
965
966 pub(super) fn stats_with_paths(&self, paths: usize) -> SymbolicStats {
967 let mut stats = self.solver.stats();
968 stats.paths = paths;
969 stats
970 }
971}
972
973fn order_roots_by_corpus_seed_count(roots: &mut [PathState], order: SymbolicExplorationOrder) {
974 let Some((first, rest)) = roots.split_first() else {
975 return;
976 };
977 if rest.iter().all(|root| root.corpus_seed_model_count() == first.corpus_seed_model_count()) {
978 return;
979 }
980
981 match order {
982 SymbolicExplorationOrder::Bfs => {
983 roots.sort_by_key(|root| Reverse(root.corpus_seed_model_count()));
984 }
985 SymbolicExplorationOrder::Dfs => {
986 roots.sort_by_key(PathState::corpus_seed_model_count);
987 }
988 }
989}
990
991const fn symbolic_invariant_should_check(
992 sequence_len: usize,
993 depth: usize,
994 check_interval: u32,
995) -> bool {
996 sequence_len == depth
997 || (check_interval != 0
998 && sequence_len != 0
999 && sequence_len.is_multiple_of(check_interval as usize))
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005
1006 #[test]
1007 fn stateless_runs_only_start_wall_clock_deadline_for_deferred_solver_phase() {
1008 let mut executor =
1009 SymbolicExecutor::new(SymbolicConfig { timeout: Some(1), ..Default::default() });
1010
1011 executor.reset_run_state(false);
1012 assert!(executor.deadline.is_none());
1013
1014 executor.reset_run_state(true);
1015 assert!(executor.deadline.is_some());
1016 }
1017
1018 #[test]
1019 fn hard_arithmetic_never_masks_a_specific_incomplete_reason() {
1020 let mut executor = SymbolicExecutor::new(SymbolicConfig::default());
1021
1022 executor.defer_hard_arithmetic();
1023 executor.defer_solver_unknown();
1024 assert_eq!(executor.deferred_incomplete, Some(DeferredIncomplete::SolverUnknown));
1025
1026 executor.reset_run_state(false);
1027 executor.defer_hard_arithmetic();
1028 executor.defer_incomplete("unsupported after hard arithmetic");
1029 assert_eq!(
1030 executor.deferred_incomplete,
1031 Some(DeferredIncomplete::Unsupported("unsupported after hard arithmetic"))
1032 );
1033
1034 executor.reset_run_state(false);
1035 executor.defer_solver_unknown();
1036 executor.defer_hard_arithmetic();
1037 assert_eq!(executor.deferred_incomplete, Some(DeferredIncomplete::SolverUnknown));
1038 }
1039}