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 run<FEN: FoundryEvmNetwork>(
130 &mut self,
131 input: SymbolicRunInput<'_, FEN>,
132 ) -> SymbolicRunResult {
133 self.execute_run(input, None)
134 }
135
136 pub fn search_branch_target<FEN: FoundryEvmNetwork>(
142 &mut self,
143 input: SymbolicRunInput<'_, FEN>,
144 ) -> SymbolicBranchTargetSearchResult {
145 let mut candidates = Vec::new();
146 if input.branch_target.is_none() {
147 return SymbolicBranchTargetSearchResult {
148 candidates,
149 execution: SymbolicRunResult::Incomplete {
150 kind: SymbolicStopReason::Error,
151 reason: "branch target search requires a branch target".to_string(),
152 stats: SymbolicStats::default(),
153 },
154 };
155 }
156
157 let execution = self.execute_run(input, Some(&mut candidates));
158 if let SymbolicRunResult::Counterexample { args, calldata, .. } = &execution {
159 candidates.insert(
160 0,
161 SymbolicConcreteInput { args: args.clone(), calldata: calldata.clone() },
162 );
163 }
164 SymbolicBranchTargetSearchResult { candidates, execution }
165 }
166
167 fn execute_run<FEN: FoundryEvmNetwork>(
168 &mut self,
169 input: SymbolicRunInput<'_, FEN>,
170 mut branch_candidates: Option<&mut Vec<SymbolicConcreteInput>>,
171 ) -> SymbolicRunResult {
172 self.reset_run_state(false);
173 self.solver.clear_context_caches();
174 self.cx = SymCx::new();
175 if let Err(err) = self.solver.check_available() {
176 return SymbolicRunResult::Incomplete {
177 kind: err.stop_reason(),
178 reason: err.to_string(),
179 stats: SymbolicStats::default(),
180 };
181 }
182
183 let mut completed_paths = 0;
184 let first =
185 match self.run_inner(&input, branch_candidates.as_deref_mut(), &mut completed_paths) {
186 Ok(result) => result,
187 Err(err) => {
188 return SymbolicRunResult::Incomplete {
189 kind: err.stop_reason(),
190 reason: err.to_string(),
191 stats: self.stats_with_paths(completed_paths),
192 };
193 }
194 };
195
196 let retry_nested_deferred = branch_candidates.is_none()
199 && self.stateless_retry_safe
200 && matches!(self.deferred_incomplete, Some(DeferredIncomplete::HardArithmetic))
201 && matches!(
202 &first,
203 SymbolicRunResult::Incomplete {
204 kind: SymbolicStopReason::RevertAll | SymbolicStopReason::Timeout,
205 ..
206 }
207 );
208 if !retry_nested_deferred {
209 return first;
210 }
211
212 if let Err(err) = self.check_timeout() {
213 return SymbolicRunResult::Incomplete {
214 kind: err.stop_reason(),
215 reason: err.to_string(),
216 stats: self.stats_with_paths(completed_paths),
217 };
218 }
219
220 self.deferred_incomplete = None;
221 self.nested_deferred_mode = DeferredPathMode::Yield;
222 let prioritized = match self.run_inner(&input, None, &mut completed_paths) {
223 Ok(result) => result,
224 Err(err) => {
225 return SymbolicRunResult::Incomplete {
226 kind: err.stop_reason(),
227 reason: err.to_string(),
228 stats: self.stats_with_paths(completed_paths),
229 };
230 }
231 };
232 let drain_nested_deferred = self.stateless_retry_safe
233 && matches!(self.deferred_incomplete, Some(DeferredIncomplete::HardArithmetic))
234 && matches!(
235 &prioritized,
236 SymbolicRunResult::Incomplete {
237 kind: SymbolicStopReason::RevertAll | SymbolicStopReason::Timeout,
238 ..
239 }
240 );
241 if !drain_nested_deferred {
242 return prioritized;
243 }
244
245 if let Err(err) = self.check_timeout() {
246 return SymbolicRunResult::Incomplete {
247 kind: err.stop_reason(),
248 reason: err.to_string(),
249 stats: self.stats_with_paths(completed_paths),
250 };
251 }
252
253 self.deferred_incomplete = None;
254 self.nested_deferred_mode = DeferredPathMode::Drain;
255 match self.run_inner(&input, None, &mut completed_paths) {
256 Ok(result) => result,
257 Err(err) => SymbolicRunResult::Incomplete {
258 kind: err.stop_reason(),
259 reason: err.to_string(),
260 stats: self.stats_with_paths(completed_paths),
261 },
262 }
263 }
264
265 pub fn modeled_corpus_seed_indexes(
267 config: &SymbolicConfig,
268 function: &Function,
269 corpus_seeds: &[SymbolicConcreteInput],
270 ) -> Result<Vec<usize>, SymbolicError> {
271 let mut cx = SymCx::new();
272 let variants = SymbolicCalldata::variants(function, config, &mut cx)?;
273 let mut modeled = vec![false; corpus_seeds.len()];
274 for calldata in &variants {
275 for (idx, seed) in corpus_seeds.iter().enumerate() {
276 if !modeled[idx] && calldata.seed_model(&mut cx, seed).is_some() {
277 modeled[idx] = true;
278 }
279 }
280 }
281 Ok(modeled
282 .into_iter()
283 .enumerate()
284 .filter_map(|(idx, modeled)| modeled.then_some(idx))
285 .collect())
286 }
287
288 pub fn run_invariant<FEN: FoundryEvmNetwork>(
298 &mut self,
299 input: SymbolicInvariantRunInput<'_, FEN>,
300 ) -> SymbolicInvariantRunResult {
301 self.reset_run_state(true);
302 self.solver.clear_context_caches();
303 self.cx = SymCx::new();
304 if let Err(err) = self.solver.check_available() {
305 return SymbolicInvariantRunResult::Incomplete {
306 kind: err.stop_reason(),
307 reason: err.to_string(),
308 stats: SymbolicStats::default(),
309 };
310 }
311
312 match self.run_invariant_inner(input) {
313 Ok(result) => result,
314 Err(err) => SymbolicInvariantRunResult::Incomplete {
315 kind: err.stop_reason(),
316 reason: err.to_string(),
317 stats: self.solver.stats(),
318 },
319 }
320 }
321
322 pub fn search_invariant_candidates<FEN: FoundryEvmNetwork>(
328 &mut self,
329 input: SymbolicInvariantCandidateInput<'_, FEN>,
330 ) -> SymbolicInvariantCandidateSearchResult {
331 self.reset_run_state(true);
332 self.solver.clear_context_caches();
333 self.cx = SymCx::new();
334 if let Err(error) = self.solver.check_available() {
335 return SymbolicInvariantCandidateSearchResult {
336 candidates: Vec::new(),
337 limitation: Some(error.into()),
338 };
339 }
340
341 let mut candidates = Vec::new();
342 let mut limitation = None;
343 if let Err(error) =
344 self.search_invariant_candidates_inner(&input, &mut candidates, &mut limitation)
345 {
346 limitation = Some(error.into());
347 }
348 if limitation.is_none()
351 && let Some((kind, reason)) = self.deferred_incomplete()
352 {
353 limitation = Some(SymbolicInvariantSearchLimitation { kind, reason });
354 }
355
356 SymbolicInvariantCandidateSearchResult { candidates, limitation }
357 }
358
359 pub(super) fn run_inner<FEN: FoundryEvmNetwork>(
360 &mut self,
361 input: &SymbolicRunInput<'_, FEN>,
362 mut branch_candidates: Option<&mut Vec<SymbolicConcreteInput>>,
363 completed_paths: &mut usize,
364 ) -> Result<SymbolicRunResult, SymbolicError> {
365 let account = input
366 .executor
367 .backend()
368 .basic_ref(input.target)
369 .map_err(|err| SymbolicError::Backend(err.to_string()))?
370 .ok_or(SymbolicError::MissingAccount(input.target))?;
371 let bytecode = account.code.ok_or(SymbolicError::MissingCode(input.target))?;
372 let code = SymCode::from_bytecode(&mut self.cx, &bytecode);
373 let mut roots = Vec::new();
374 for calldata in SymbolicCalldata::variants(input.function, &self.config, &mut self.cx)? {
375 let corpus_seed_models = input
376 .corpus_seeds
377 .iter()
378 .filter_map(|seed| calldata.seed_model(&mut self.cx, seed).map(Arc::new))
379 .collect();
380 let mut root = PathState::new(
381 &mut self.cx,
382 input.target,
383 input.sender,
384 input.value,
385 calldata,
386 input.ffi_enabled,
387 );
388 root.set_corpus_seed_models(corpus_seed_models);
389 root.set_branch_target(input.branch_target);
390 root.apply_executor_env(&mut self.cx, input.executor);
391 root.world.set_storage_layout(self.config.storage_layout);
392 root.world.clear_transaction_scoped_state();
393 roots.push(root);
394 }
395 order_roots_by_corpus_seed_count(&mut roots, self.config.exploration_order);
396 let mut worklist = roots.into_iter().collect::<VecDeque<_>>();
397 let mut deferred_worklist = VecDeque::new();
398 let mut reverted_paths = 0usize;
399 let mut normal_paths = 0usize;
400 let mut success_input = None;
401 let path_limit = self.config.path_width() as usize;
402 let depth_limit = self.config.execution_depth() as usize;
403
404 while let Some(next) = self.pop_next_feasible_path(
405 &mut worklist,
406 &mut deferred_worklist,
407 DeferredPathMode::Drain,
408 )? {
409 let mut state = next.state;
410 if *completed_paths >= path_limit {
411 debug!(
412 completed_paths = *completed_paths,
413 path_limit, "symbolic path limit reached"
414 );
415 return Ok(SymbolicRunResult::Incomplete {
416 kind: SymbolicStopReason::Stuck,
417 reason: format!("symbolic path limit exceeded ({path_limit})"),
418 stats: self.stats_with_paths(*completed_paths),
419 });
420 }
421 if std::mem::take(&mut state.pending_storage_hook_revert) {
422 self.collect_branch_candidate(
423 branch_candidates.as_deref_mut(),
424 input.function,
425 &state,
426 )?;
427 *completed_paths += 1;
428 reverted_paths += 1;
429 continue;
430 }
431 let _path_span = trace_span!(
432 "symbolic_path",
433 completed_paths = *completed_paths,
434 worklist_size = worklist.len()
435 )
436 .entered();
437 trace!(
438 completed_paths = *completed_paths,
439 worklist_size = worklist.len(),
440 "exploring symbolic path"
441 );
442
443 loop {
444 self.check_timeout()?;
445 if state.depth >= depth_limit {
446 debug!(depth = state.depth, depth_limit, "symbolic depth limit reached");
447 return Ok(SymbolicRunResult::Incomplete {
448 kind: SymbolicStopReason::Stuck,
449 reason: format!("symbolic depth limit exceeded ({depth_limit})"),
450 stats: self.stats_with_paths(*completed_paths),
451 });
452 }
453 state.depth += 1;
454
455 let outcome = match code.opcode(&mut self.cx, state.pc)? {
456 Some(op) => {
457 let _step_span = trace_span!("symbolic_step", pc = state.pc, op).entered();
458 self.step(
459 input.executor,
460 &code,
461 code.jump_table(),
462 &mut state,
463 &mut worklist,
464 completed_paths,
465 op,
466 )?
467 }
468 None => StepOutcome::Halt,
469 };
470 match outcome {
471 StepOutcome::Continue => {}
472 StepOutcome::AssumeRejected | StepOutcome::Forked => break,
473 StepOutcome::Revert => {
474 self.collect_branch_candidate(
475 branch_candidates.as_deref_mut(),
476 input.function,
477 &state,
478 )?;
479 *completed_paths += 1;
480 reverted_paths += 1;
481 break;
482 }
483 StepOutcome::Halt if state.expectations_satisfied() => {
484 let candidate = self.collect_branch_candidate(
485 branch_candidates.as_deref_mut(),
486 input.function,
487 &state,
488 )?;
489 if input.collect_success_input
490 && state.satisfies_branch_target()
491 && state.can_materialize_seed()
492 && success_input.as_ref().is_none_or(|(depth, _)| state.depth > *depth)
493 {
494 let input = match candidate {
495 Some(input) => input,
496 None => self.materialize_root_input(input.function, &state)?,
497 };
498 success_input = Some((state.depth, input));
499 }
500 *completed_paths += 1;
501 normal_paths += 1;
502 break;
503 }
504 StepOutcome::Halt | StepOutcome::ExceptionalHalt | StepOutcome::Failure => {
505 if !state.satisfies_branch_target() {
506 *completed_paths += 1;
507 break;
508 }
509 debug!(
510 constraint_count = state.constraints.len(),
511 "materializing counterexample from solver model"
512 );
513 let SymbolicConcreteInput { args, calldata } =
514 self.materialize_root_input(input.function, &state)?;
515 return Ok(SymbolicRunResult::Counterexample {
516 args,
517 calldata,
518 stats: self.stats_with_paths(*completed_paths + 1),
519 });
520 }
521 }
522 }
523 }
524
525 if normal_paths == 0 && reverted_paths > 0 {
526 debug!(completed_paths = *completed_paths, "all symbolic paths reverted");
527 return Ok(SymbolicRunResult::Incomplete {
528 kind: SymbolicStopReason::RevertAll,
529 reason: "all symbolic paths reverted".to_string(),
530 stats: self.stats_with_paths(*completed_paths),
531 });
532 }
533
534 if let Some((kind, reason)) = self.deferred_incomplete() {
535 return Ok(SymbolicRunResult::Incomplete {
536 kind,
537 reason,
538 stats: self.stats_with_paths(*completed_paths),
539 });
540 }
541
542 if normal_paths == 0 {
543 return Ok(SymbolicRunResult::Incomplete {
544 kind: SymbolicStopReason::Stuck,
545 reason: "no successful symbolic paths".to_string(),
546 stats: self.stats_with_paths(*completed_paths),
547 });
548 }
549
550 debug!(completed_paths = *completed_paths, "symbolic execution safe");
551 Ok(SymbolicRunResult::Safe {
552 stats: self.stats_with_paths(*completed_paths),
553 success_input: success_input.map(|(_, input)| input),
554 })
555 }
556
557 fn collect_branch_candidate(
558 &mut self,
559 candidates: Option<&mut Vec<SymbolicConcreteInput>>,
560 function: &Function,
561 state: &PathState,
562 ) -> Result<Option<SymbolicConcreteInput>, SymbolicError> {
563 let Some(candidates) = candidates else {
564 return Ok(None);
565 };
566 if !state.satisfies_branch_target() || !state.can_materialize_seed() {
567 return Ok(None);
568 }
569
570 let input = self.materialize_root_input(function, state)?;
571 candidates.push(input.clone());
572 Ok(Some(input))
573 }
574
575 fn materialize_root_input(
577 &mut self,
578 function: &Function,
579 state: &PathState,
580 ) -> Result<SymbolicConcreteInput, SymbolicError> {
581 let calldata = state
582 .root_calldata
583 .as_ref()
584 .ok_or(SymbolicError::Unsupported("missing root symbolic calldata"))?;
585 let replayable_storage = state.world.replay_storage_symbols();
586 let model = self.solver.model_with_replayable_storage(
587 &mut self.cx,
588 &state.constraints,
589 &replayable_storage,
590 )?;
591 let args = calldata.model_to_args(&mut self.cx, &model)?;
592 let calldata_bytes = Bytes::from(function.abi_encode_input(&args)?);
593 Ok(SymbolicConcreteInput { args, calldata: calldata_bytes })
594 }
595
596 pub(super) fn run_invariant_inner<FEN: FoundryEvmNetwork>(
597 &mut self,
598 input: SymbolicInvariantRunInput<'_, FEN>,
599 ) -> Result<SymbolicInvariantRunResult, SymbolicError> {
600 if input.targets.is_empty() {
601 return Err(SymbolicError::Unsupported("symbolic invariant has no targets"));
602 }
603
604 let mut senders =
605 if input.senders.is_empty() { vec![input.sender] } else { input.senders.clone() };
606 senders.retain(|sender| !input.excluded_senders.contains(sender));
607 if senders.is_empty() {
608 return Err(SymbolicError::Unsupported("symbolic invariant senders are excluded"));
609 }
610 let after_invariant_for = |steps_len: usize| {
611 (steps_len == input.depth).then_some(input.after_invariant).flatten()
612 };
613 let mut completed_paths = 0usize;
614 let mut initial_state = PathState::empty(
615 &mut self.cx,
616 input.invariant_address,
617 input.sender,
618 input.ffi_enabled,
619 );
620 initial_state.apply_executor_env(&mut self.cx, input.executor);
621 initial_state.world.set_storage_layout(self.config.storage_layout);
622 let initial = SequencePath { state: initial_state, steps: Vec::new() };
623
624 if symbolic_invariant_should_check(0, input.depth, input.check_interval) {
625 for outcome in self.execute_invariant_check(
626 input.executor,
627 initial.state.clone(),
628 input.invariant_address,
629 input.sender,
630 input.invariant,
631 after_invariant_for(0),
632 &mut completed_paths,
633 )? {
634 if outcome.failed {
635 let (sequence, storage) =
636 self.materialize_sequence(&initial.steps, &outcome.state)?;
637 return Ok(SymbolicInvariantRunResult::Counterexample {
638 kind: SymbolicInvariantCounterexampleKind::Predicate,
639 sequence,
640 storage,
641 stats: self.stats_with_paths(completed_paths),
642 });
643 }
644 }
645 }
646
647 let path_limit = self.config.path_width() as usize;
648 let mut frontier = vec![initial];
649 for depth in 0..input.depth {
650 self.check_timeout()?;
651 let mut next_frontier = Vec::new();
652 for sequence in frontier {
653 self.check_timeout()?;
654 for (target_idx, target) in input.targets.iter().enumerate() {
655 for (sender_idx, sender) in senders.iter().copied().enumerate() {
656 self.check_timeout()?;
657 let prefix = format!("sequence_{depth}_{target_idx}_{sender_idx}");
658 let calldatas = SymbolicCalldata::variants_with_prefix(
659 &target.function,
660 &self.config,
661 &mut self.cx,
662 &prefix,
663 )?;
664 for calldata in calldatas {
665 let step = SequenceStepTemplate {
666 sender,
667 address: target.address,
668 contract_name: target.contract_name.clone(),
669 function: target.function.clone(),
670 calldata,
671 };
672 let calldata = step.calldata.call_data(&mut self.cx);
673 let constraints = step.calldata.constraints().to_vec();
674 let mut call = self.prepare_sequence_call(
675 input.executor,
676 sequence.state.clone(),
677 target.address,
678 sender,
679 &target.function,
680 calldata,
681 constraints,
682 )?;
683
684 while let Some(outcome) = self.execute_sequence_call_next(
685 input.executor,
686 &mut call,
687 &mut completed_paths,
688 )? {
689 let mut steps = sequence.steps.clone();
690 steps.push(step.clone());
691
692 let post_state = match outcome.status {
693 CallStatus::Failure => {
694 let (sequence, storage) =
695 self.materialize_sequence(&steps, &outcome.state)?;
696 return Ok(SymbolicInvariantRunResult::Counterexample {
697 kind: SymbolicInvariantCounterexampleKind::Handler,
698 sequence,
699 storage,
700 stats: self.stats_with_paths(completed_paths),
701 });
702 }
703 CallStatus::Revert | CallStatus::ExceptionalHalt => {
704 if input.fail_on_revert {
705 let (sequence, storage) =
706 self.materialize_sequence(&steps, &outcome.state)?;
707 return Ok(
708 SymbolicInvariantRunResult::Counterexample {
709 kind: SymbolicInvariantCounterexampleKind::Predicate,
710 sequence,
711 storage,
712 stats: self.stats_with_paths(completed_paths),
713 },
714 );
715 }
716 let mut reverted_state = sequence.state.clone();
723 reverted_state
724 .take_reverted_top_level_effects(outcome.state);
725 reverted_state
726 }
727 CallStatus::Success => outcome.state,
728 };
729 if symbolic_invariant_should_check(
730 steps.len(),
731 input.depth,
732 input.check_interval,
733 ) {
734 for mut invariant_outcome in self.execute_invariant_check(
735 input.executor,
736 post_state.clone(),
737 input.invariant_address,
738 input.sender,
739 input.invariant,
740 after_invariant_for(steps.len()),
741 &mut completed_paths,
742 )? {
743 if invariant_outcome.failed {
744 let (sequence, storage) = self.materialize_sequence(
745 &steps,
746 &invariant_outcome.state,
747 )?;
748 return Ok(SymbolicInvariantRunResult::Counterexample {
749 kind: SymbolicInvariantCounterexampleKind::Predicate,
750 sequence,
751 storage,
752 stats: self.stats_with_paths(completed_paths),
753 });
754 }
755 let mut state = post_state.clone();
756 state.take_noncommitting_check_state(
757 &mut invariant_outcome.state,
758 );
759 next_frontier
760 .push(SequencePath { state, steps: steps.clone() });
761 }
762 } else {
763 next_frontier.push(SequencePath { state: post_state, steps });
764 }
765
766 if completed_paths >= path_limit {
767 return Ok(SymbolicInvariantRunResult::Incomplete {
768 kind: SymbolicStopReason::Stuck,
769 reason: format!(
770 "symbolic path limit exceeded ({path_limit})"
771 ),
772 stats: self.stats_with_paths(completed_paths),
773 });
774 }
775 }
776 }
777 }
778 }
779 }
780
781 if next_frontier.is_empty() {
782 break;
783 }
784 frontier = next_frontier;
785 }
786
787 if let Some((kind, reason)) = self.deferred_incomplete() {
788 return Ok(SymbolicInvariantRunResult::Incomplete {
789 kind,
790 reason,
791 stats: self.stats_with_paths(completed_paths),
792 });
793 }
794
795 Ok(SymbolicInvariantRunResult::Safe(self.stats_with_paths(completed_paths)))
796 }
797
798 pub(super) fn stats_with_paths(&self, paths: usize) -> SymbolicStats {
799 let mut stats = self.solver.stats();
800 stats.paths = paths;
801 stats
802 }
803}
804
805fn order_roots_by_corpus_seed_count(roots: &mut [PathState], order: SymbolicExplorationOrder) {
806 let Some((first, rest)) = roots.split_first() else {
807 return;
808 };
809 if rest.iter().all(|root| root.corpus_seed_model_count() == first.corpus_seed_model_count()) {
810 return;
811 }
812
813 match order {
814 SymbolicExplorationOrder::Bfs => {
815 roots.sort_by_key(|root| Reverse(root.corpus_seed_model_count()));
816 }
817 SymbolicExplorationOrder::Dfs => {
818 roots.sort_by_key(PathState::corpus_seed_model_count);
819 }
820 }
821}
822
823const fn symbolic_invariant_should_check(
824 sequence_len: usize,
825 depth: usize,
826 check_interval: u32,
827) -> bool {
828 sequence_len == depth
829 || (check_interval != 0
830 && sequence_len != 0
831 && sequence_len.is_multiple_of(check_interval as usize))
832}