1use super::*;
4use solar_data_structures::{index::IndexVec, map::FxHashSet};
5use std::fmt as std_fmt;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct EvmIrVerifyError {
10 pub msg: String,
12}
13
14impl EvmIrVerifyError {
15 fn new(msg: impl Into<String>) -> Self {
16 Self { msg: msg.into() }
17 }
18
19 fn in_block(block: EvmIrBlockId, msg: impl Into<String>) -> Self {
20 Self::new(format!("block {}: {}", block.index(), msg.into()))
21 }
22}
23
24impl std_fmt::Display for EvmIrVerifyError {
25 fn fmt(&self, f: &mut std_fmt::Formatter<'_>) -> std_fmt::Result {
26 write!(f, "EVM IR verification failed: {}", self.msg)
27 }
28}
29
30impl std::error::Error for EvmIrVerifyError {}
31
32pub fn verify_evm_ir_module(module: &EvmIrModule) -> Result<(), EvmIrVerifyError> {
39 if !is_valid_ident(&module.name) {
40 return Err(EvmIrVerifyError::new(format!("invalid program name `{}`", module.name)));
41 }
42 if module.blocks.is_empty() {
43 return Err(EvmIrVerifyError::new("program has no blocks"));
44 }
45 let Some(entry) = module.entry_block else {
46 return Err(EvmIrVerifyError::new("program has no entry block"));
47 };
48 if !block_exists(module, entry) {
49 return Err(EvmIrVerifyError::new(format!(
50 "entry block `{}` is out of range",
51 entry.index()
52 )));
53 }
54
55 let mut labels = FxHashSet::default();
56 for (block_id, block) in module.blocks.iter_enumerated() {
57 if !is_valid_block_label(&block.label) {
58 return Err(EvmIrVerifyError::in_block(
59 block_id,
60 format!("invalid block label `{}`", block.label),
61 ));
62 }
63 if !labels.insert(block.label.as_str()) {
64 return Err(EvmIrVerifyError::in_block(
65 block_id,
66 format!("duplicate block label `{}`", block.label),
67 ));
68 }
69 if block.terminator.is_none() {
70 return Err(EvmIrVerifyError::in_block(block_id, "missing terminator"));
71 }
72 }
73
74 let mut value_names = FxHashSet::default();
75 for (_, value) in module.values.iter_enumerated() {
76 if !is_valid_value_name(&value.name) {
77 return Err(EvmIrVerifyError::new(format!("invalid value name `%{}`", value.name)));
78 }
79 if !value_names.insert(value.name.as_str()) {
80 return Err(EvmIrVerifyError::new(format!("duplicate value name `%{}`", value.name)));
81 }
82 }
83
84 let mut defined_values = FxHashSet::default();
85 for (block_id, block) in module.blocks.iter_enumerated() {
86 for inst in &block.instructions {
87 verify_instruction_shape(block_id, inst)?;
88 if let Some(result) = inst.result {
89 if !value_exists(module, result) {
90 return Err(EvmIrVerifyError::in_block(
91 block_id,
92 format!("result value `{}` is out of range", result.index()),
93 ));
94 }
95 if !defined_values.insert(result) {
96 return Err(EvmIrVerifyError::in_block(
97 block_id,
98 format!("value `%{}` is defined more than once", module.value(result).name),
99 ));
100 }
101 }
102 for operand in &inst.operands {
103 verify_operand(block_id, module, operand)?;
104 }
105 verify_metadata_is_untyped(block_id, &inst.metadata)?;
106 }
107 let term = block.terminator.as_ref().expect("checked above");
108 verify_terminator_shape(block_id, &term.kind)?;
109 visit_terminator_operands(&term.kind, |operand| {
110 verify_operand(block_id, module, operand)?;
111 Ok(())
112 })?;
113 visit_terminator_targets(&term.kind, |target| {
114 if !block_exists(module, target) {
115 return Err(EvmIrVerifyError::in_block(
116 block_id,
117 format!("target block `{}` is out of range", target.index()),
118 ));
119 }
120 Ok(())
121 })?;
122 verify_metadata_is_untyped(block_id, &term.metadata)?;
123 }
124
125 for (block_id, block) in module.blocks.iter_enumerated() {
126 for &value in &block.entry_stack {
127 if !value_exists(module, value) {
128 return Err(EvmIrVerifyError::in_block(
129 block_id,
130 format!("entry stack value `{}` is out of range", value.index()),
131 ));
132 }
133 if !defined_values.contains(&value) {
134 return Err(EvmIrVerifyError::in_block(
135 block_id,
136 format!("entry stack value `%{}` is never defined", module.value(value).name),
137 ));
138 }
139 }
140 for inst in &block.instructions {
141 for operand in &inst.operands {
142 verify_value_defined(block_id, module, operand, &defined_values)?;
143 }
144 }
145 let term = block.terminator.as_ref().expect("checked above");
146 visit_terminator_operands(&term.kind, |operand| {
147 verify_value_defined(block_id, module, operand, &defined_values)?;
148 Ok(())
149 })?;
150 }
151
152 verify_stack_consistency(module)?;
153
154 Ok(())
155}
156
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
165enum AbstractWord {
166 Value(EvmIrValueId),
168 Unknown,
171}
172
173struct ModelStack {
189 words: Vec<AbstractWord>,
191 infinite_floor: bool,
193}
194
195impl ModelStack {
196 fn len(&self) -> usize {
197 self.words.len()
198 }
199
200 fn ensure_depth(&mut self, depth: usize) -> bool {
203 if self.words.len() >= depth {
204 return true;
205 }
206 if !self.infinite_floor {
207 return false;
208 }
209 while self.words.len() < depth {
210 self.words.push(AbstractWord::Unknown);
211 }
212 true
213 }
214
215 fn push(&mut self, word: AbstractWord) {
216 self.words.insert(0, word);
217 }
218
219 fn contains(&self, word: AbstractWord) -> bool {
220 self.words.contains(&word) || self.infinite_floor
223 }
224}
225
226fn verify_stack_consistency(module: &EvmIrModule) -> Result<(), EvmIrVerifyError> {
247 if let Some(entry) = module.entry_block
248 && !module.blocks[entry].entry_stack.is_empty()
249 {
250 return Err(EvmIrVerifyError::in_block(
251 entry,
252 "entry block must start from an empty stack",
253 ));
254 }
255
256 let mut exit_stacks: IndexVec<EvmIrBlockId, Vec<AbstractWord>> =
257 IndexVec::with_capacity(module.blocks.len());
258 for (block_id, block) in module.blocks.iter_enumerated() {
259 let is_entry = module.entry_block == Some(block_id);
260 exit_stacks.push(simulate_block(module, block_id, block, is_entry)?);
261 }
262
263 for (block_id, block) in module.blocks.iter_enumerated() {
264 let exit = &exit_stacks[block_id];
265 let term = block.terminator.as_ref().expect("checked above");
266 let mut result = Ok(());
267 visit_terminator_targets(&term.kind, |succ| {
268 let succ_entry: Vec<AbstractWord> = module.blocks[succ]
269 .entry_stack
270 .iter()
271 .map(|&value| AbstractWord::Value(value))
272 .collect();
273 if !exit.starts_with(&succ_entry) {
274 result = Err(EvmIrVerifyError::in_block(
275 block_id,
276 format!(
277 "stack on edge to `{}` is inconsistent: successor declares incoming \
278 stack [{}] but predecessor leaves [{}]",
279 module.blocks[succ].label,
280 format_entry_stack(module, &module.blocks[succ].entry_stack),
281 format_abstract_stack(module, exit),
282 ),
283 ));
284 }
285 Ok::<(), EvmIrVerifyError>(())
286 })?;
287 result?;
288 }
289
290 Ok(())
291}
292
293fn simulate_block(
297 module: &EvmIrModule,
298 block_id: EvmIrBlockId,
299 block: &EvmIrBlock,
300 is_entry: bool,
301) -> Result<Vec<AbstractWord>, EvmIrVerifyError> {
302 let mut stack = ModelStack {
303 words: block.entry_stack.iter().map(|&value| AbstractWord::Value(value)).collect(),
304 infinite_floor: !is_entry,
305 };
306
307 for inst in &block.instructions {
308 simulate_instruction(module, block_id, inst, &mut stack)?;
309 }
310
311 let term = block.terminator.as_ref().expect("checked above");
312 simulate_terminator(module, block_id, &term.kind, &mut stack)?;
313 Ok(stack.words)
314}
315
316fn simulate_instruction(
317 module: &EvmIrModule,
318 block_id: EvmIrBlockId,
319 inst: &EvmIrInstruction,
320 stack: &mut ModelStack,
321) -> Result<(), EvmIrVerifyError> {
322 match &inst.kind {
323 EvmIrInstructionKind::Stack(op) => apply_physical_stack_op(block_id, *op, stack),
324 EvmIrInstructionKind::Operation(_) if is_encoded_push_instruction(inst) => {
325 stack.push(result_word(inst));
328 Ok(())
329 }
330 EvmIrInstructionKind::Operation(_) if !inst.operands.is_empty() => {
331 for operand in &inst.operands {
336 if let EvmIrOperand::Value(value) = operand
337 && !stack.contains(AbstractWord::Value(*value))
338 {
339 return Err(EvmIrVerifyError::in_block(
340 block_id,
341 format!(
342 "operand `%{}` of `{}` is not live on the stack",
343 module.value(*value).name,
344 inst.mnemonic()
345 ),
346 ));
347 }
348 }
349 if inst.result.is_some() {
350 stack.push(result_word(inst));
351 }
352 Ok(())
353 }
354 EvmIrInstructionKind::Operation(_) => {
355 let effect =
358 inst.metadata.stack.unwrap_or_else(|| default_instruction_stack_effect(inst));
359 apply_effect(block_id, inst, effect, stack)
360 }
361 }
362}
363
364fn apply_effect(
365 block_id: EvmIrBlockId,
366 inst: &EvmIrInstruction,
367 effect: EvmIrStackEffect,
368 stack: &mut ModelStack,
369) -> Result<(), EvmIrVerifyError> {
370 let inputs = usize::from(effect.inputs);
371 if !stack.ensure_depth(inputs) {
372 return Err(EvmIrVerifyError::in_block(
373 block_id,
374 format!(
375 "`{}` consumes {} stack words but only {} are available",
376 inst.mnemonic(),
377 effect.inputs,
378 stack.len()
379 ),
380 ));
381 }
382 stack.words.drain(0..inputs);
383 for index in 0..effect.outputs {
384 let word = if index == 0 { result_word(inst) } else { AbstractWord::Unknown };
385 stack.push(word);
386 }
387 Ok(())
388}
389
390fn apply_physical_stack_op(
391 block_id: EvmIrBlockId,
392 op: EvmIrStackOp,
393 stack: &mut ModelStack,
394) -> Result<(), EvmIrVerifyError> {
395 match op {
396 EvmIrStackOp::Dup(n) => {
397 let depth = usize::from(n);
398 if !stack.ensure_depth(depth) {
399 return Err(EvmIrVerifyError::in_block(
400 block_id,
401 format!("`dup{n}` reaches depth {n} but the stack has {}", stack.len()),
402 ));
403 }
404 let word = stack.words[depth - 1];
405 stack.push(word);
406 }
407 EvmIrStackOp::Swap(n) => {
408 let depth = usize::from(n);
409 if !stack.ensure_depth(depth + 1) {
410 return Err(EvmIrVerifyError::in_block(
411 block_id,
412 format!("`swap{n}` reaches depth {n} but the stack has {}", stack.len()),
413 ));
414 }
415 stack.words.swap(0, depth);
416 }
417 EvmIrStackOp::Pop => {
418 if !stack.ensure_depth(1) {
419 return Err(EvmIrVerifyError::in_block(block_id, "`pop` on an empty stack"));
420 }
421 stack.words.remove(0);
422 }
423 }
424 Ok(())
425}
426
427fn simulate_terminator(
428 module: &EvmIrModule,
429 block_id: EvmIrBlockId,
430 kind: &EvmIrTerminatorKind,
431 stack: &mut ModelStack,
432) -> Result<(), EvmIrVerifyError> {
433 let mut result = Ok(());
439 visit_terminator_operands(kind, |operand| {
440 if let EvmIrOperand::Value(value) = operand
441 && !stack.contains(AbstractWord::Value(*value))
442 {
443 result = Err(EvmIrVerifyError::in_block(
444 block_id,
445 format!(
446 "terminator operand `%{}` is not live on the stack",
447 module.value(*value).name
448 ),
449 ));
450 }
451 Ok::<(), EvmIrVerifyError>(())
452 })?;
453 result?;
454
455 apply_terminator_effect(block_id, kind, stack)
456}
457
458fn apply_terminator_effect(
459 block_id: EvmIrBlockId,
460 kind: &EvmIrTerminatorKind,
461 stack: &mut ModelStack,
462) -> Result<(), EvmIrVerifyError> {
463 let mut consumed = FxHashSet::default();
464 visit_terminator_operands(kind, |operand| {
465 if let EvmIrOperand::Value(value) = operand
466 && consumed.insert(*value)
467 {
468 consume_stack_value(block_id, kind, *value, stack)?;
469 }
470 Ok::<(), EvmIrVerifyError>(())
471 })?;
472
473 let effect = default_terminator_stack_effect(kind);
474 let remaining_inputs = usize::from(effect.inputs).saturating_sub(consumed.len());
475 if !stack.ensure_depth(remaining_inputs) {
476 return Err(EvmIrVerifyError::in_block(
477 block_id,
478 format!(
479 "`{}` consumes {} stack words but only {} are available",
480 terminator_name(kind),
481 effect.inputs,
482 stack.len()
483 ),
484 ));
485 }
486 stack.words.drain(0..remaining_inputs);
487 Ok(())
488}
489
490fn consume_stack_value(
491 block_id: EvmIrBlockId,
492 kind: &EvmIrTerminatorKind,
493 value: EvmIrValueId,
494 stack: &mut ModelStack,
495) -> Result<(), EvmIrVerifyError> {
496 let needle = AbstractWord::Value(value);
497 let Some(index) = stack.words.iter().position(|word| *word == needle) else {
498 return Err(EvmIrVerifyError::in_block(
499 block_id,
500 format!(
501 "`{}` consumes an operand that is not live on the stack",
502 terminator_name(kind)
503 ),
504 ));
505 };
506 stack.words.remove(index);
507 Ok(())
508}
509
510fn terminator_name(kind: &EvmIrTerminatorKind) -> &'static str {
511 match kind {
512 EvmIrTerminatorKind::Fallthrough(_) => "fallthrough",
513 EvmIrTerminatorKind::Jump(_) => "jump",
514 EvmIrTerminatorKind::Branch { .. } => "br",
515 EvmIrTerminatorKind::Switch { .. } => "switch",
516 EvmIrTerminatorKind::Return { .. } => "return",
517 EvmIrTerminatorKind::Revert { .. } => "revert",
518 EvmIrTerminatorKind::Stop => "stop",
519 EvmIrTerminatorKind::Invalid => "invalid",
520 EvmIrTerminatorKind::SelfDestruct { .. } => "selfdestruct",
521 EvmIrTerminatorKind::RawOpcode(_) => "terminal",
522 }
523}
524
525fn result_word(inst: &EvmIrInstruction) -> AbstractWord {
527 inst.result.map(AbstractWord::Value).unwrap_or(AbstractWord::Unknown)
528}
529
530fn format_entry_stack(module: &EvmIrModule, stack: &[EvmIrValueId]) -> String {
531 stack
532 .iter()
533 .map(|&value| format!("%{}", module.value(value).name))
534 .collect::<Vec<_>>()
535 .join(", ")
536}
537
538fn format_abstract_stack(module: &EvmIrModule, stack: &[AbstractWord]) -> String {
539 stack
540 .iter()
541 .map(|word| match word {
542 AbstractWord::Value(value) => format!("%{}", module.value(*value).name),
543 AbstractWord::Unknown => "<word>".to_string(),
544 })
545 .collect::<Vec<_>>()
546 .join(", ")
547}
548
549fn verify_instruction_shape(
550 block_id: EvmIrBlockId,
551 inst: &EvmIrInstruction,
552) -> Result<(), EvmIrVerifyError> {
553 if let EvmIrInstructionKind::Stack(op) = &inst.kind {
554 let expected = op.stack_effect();
555 if inst.result.is_some() {
556 return Err(EvmIrVerifyError::in_block(
557 block_id,
558 format!("physical stack op `{}` cannot define an SSA value", op.mnemonic()),
559 ));
560 }
561 if !inst.operands.is_empty() {
562 return Err(EvmIrVerifyError::in_block(
563 block_id,
564 format!("physical stack op `{}` cannot have operands", op.mnemonic()),
565 ));
566 }
567 if let Some(effect) = inst.metadata.stack
568 && effect != expected
569 {
570 return Err(EvmIrVerifyError::in_block(
571 block_id,
572 format!(
573 "physical stack op `{}` has stack effect {}->{}, expected {}->{}",
574 op.mnemonic(),
575 effect.inputs,
576 effect.outputs,
577 expected.inputs,
578 expected.outputs
579 ),
580 ));
581 }
582 } else if is_encoded_push_instruction(inst) {
583 if inst.operands.len() != 1 {
584 return Err(EvmIrVerifyError::in_block(
585 block_id,
586 format!("`{}` must have one operand", inst.mnemonic()),
587 ));
588 }
589 if matches!(inst.operands[0], EvmIrOperand::Value(_)) {
590 return Err(EvmIrVerifyError::in_block(
591 block_id,
592 format!("`{}` cannot take a stack value operand", inst.mnemonic()),
593 ));
594 }
595 } else {
596 if operandless_operation_needs_stack_metadata(inst) {
597 return Err(EvmIrVerifyError::in_block(
598 block_id,
599 format!(
600 "operand-cleared instruction `{}` must declare an explicit stack effect",
601 inst.mnemonic()
602 ),
603 ));
604 }
605 for operand in &inst.operands {
606 if !matches!(operand, EvmIrOperand::Value(_)) {
607 return Err(EvmIrVerifyError::in_block(
608 block_id,
609 "non-`push` instruction operands must be stack values",
610 ));
611 }
612 }
613 }
614 Ok(())
615}
616
617fn operandless_operation_needs_stack_metadata(inst: &EvmIrInstruction) -> bool {
618 inst.operands.is_empty()
619 && inst.metadata.stack.is_none()
620 && matches!(
621 &inst.kind,
622 EvmIrInstructionKind::Operation(mnemonic)
623 if !(inst.result.is_some() && is_zero_input_operation(mnemonic))
624 )
625}
626
627fn is_zero_input_operation(mnemonic: &str) -> bool {
628 matches!(
629 mnemonic,
630 "address"
631 | "origin"
632 | "caller"
633 | "callvalue"
634 | "calldatasize"
635 | "codesize"
636 | "gasprice"
637 | "coinbase"
638 | "timestamp"
639 | "number"
640 | "prevrandao"
641 | "difficulty"
642 | "gaslimit"
643 | "chainid"
644 | "selfbalance"
645 | "basefee"
646 | "blobbasefee"
647 | "returndatasize"
648 | "msize"
649 | "gas"
650 | "pc"
651 )
652}
653
654fn verify_terminator_shape(
655 block_id: EvmIrBlockId,
656 kind: &EvmIrTerminatorKind,
657) -> Result<(), EvmIrVerifyError> {
658 match kind {
659 EvmIrTerminatorKind::Branch { condition, .. } => {
660 verify_stack_value_operand(block_id, condition, "branch condition")?
661 }
662 EvmIrTerminatorKind::Switch { value, cases, .. } => {
663 verify_stack_value_operand(block_id, value, "switch value")?;
664 for (case, _) in cases {
665 if !matches!(case, EvmIrOperand::Immediate(_)) {
666 return Err(EvmIrVerifyError::in_block(
667 block_id,
668 "switch case values must be immediates",
669 ));
670 }
671 }
672 }
673 EvmIrTerminatorKind::Return { offset, size }
674 | EvmIrTerminatorKind::Revert { offset, size } => {
675 verify_stack_value_operand(block_id, offset, "memory offset")?;
676 verify_stack_value_operand(block_id, size, "memory size")?;
677 }
678 EvmIrTerminatorKind::SelfDestruct { recipient } => {
679 verify_stack_value_operand(block_id, recipient, "selfdestruct recipient")?
680 }
681 EvmIrTerminatorKind::Fallthrough(_)
682 | EvmIrTerminatorKind::Jump(_)
683 | EvmIrTerminatorKind::Stop
684 | EvmIrTerminatorKind::Invalid
685 | EvmIrTerminatorKind::RawOpcode(_) => {}
686 }
687 Ok(())
688}
689
690fn verify_stack_value_operand(
691 block_id: EvmIrBlockId,
692 operand: &EvmIrOperand,
693 what: &str,
694) -> Result<(), EvmIrVerifyError> {
695 if matches!(operand, EvmIrOperand::Value(_)) {
696 return Ok(());
697 }
698 Err(EvmIrVerifyError::in_block(block_id, format!("{what} must be a stack value")))
699}
700
701fn verify_metadata_is_untyped(
702 block_id: EvmIrBlockId,
703 metadata: &EvmIrMetadata,
704) -> Result<(), EvmIrVerifyError> {
705 for item in &metadata.attrs {
706 if matches!(item.key.as_str(), "type" | "ty" | "result_ty" | "mir_type") {
707 return Err(EvmIrVerifyError::in_block(
708 block_id,
709 format!("EVM IR is untyped; metadata key `{}` is not allowed", item.key),
710 ));
711 }
712 }
713 Ok(())
714}
715
716fn verify_operand(
717 block_id: EvmIrBlockId,
718 module: &EvmIrModule,
719 operand: &EvmIrOperand,
720) -> Result<(), EvmIrVerifyError> {
721 match operand {
722 EvmIrOperand::Value(value) if !value_exists(module, *value) => {
723 Err(EvmIrVerifyError::in_block(
724 block_id,
725 format!("value `{}` is out of range", value.index()),
726 ))
727 }
728 EvmIrOperand::Block(block) if !block_exists(module, *block) => {
729 Err(EvmIrVerifyError::in_block(
730 block_id,
731 format!("block `{}` is out of range", block.index()),
732 ))
733 }
734 _ => Ok(()),
735 }
736}
737
738fn verify_value_defined(
739 block_id: EvmIrBlockId,
740 module: &EvmIrModule,
741 operand: &EvmIrOperand,
742 defined_values: &FxHashSet<EvmIrValueId>,
743) -> Result<(), EvmIrVerifyError> {
744 if let EvmIrOperand::Value(value) = operand
745 && !defined_values.contains(value)
746 {
747 return Err(EvmIrVerifyError::in_block(
748 block_id,
749 format!("value `%{}` is used but never defined", module.value(*value).name),
750 ));
751 }
752 Ok(())
753}
754
755fn block_exists(module: &EvmIrModule, block: EvmIrBlockId) -> bool {
756 block.index() < module.blocks.len()
757}
758
759fn value_exists(module: &EvmIrModule, value: EvmIrValueId) -> bool {
760 value.index() < module.values.len()
761}
762
763fn visit_terminator_operands<E>(
764 kind: &EvmIrTerminatorKind,
765 mut visit: impl FnMut(&EvmIrOperand) -> Result<(), E>,
766) -> Result<(), E> {
767 match kind {
768 EvmIrTerminatorKind::Fallthrough(_)
769 | EvmIrTerminatorKind::Jump(_)
770 | EvmIrTerminatorKind::Stop
771 | EvmIrTerminatorKind::Invalid
772 | EvmIrTerminatorKind::RawOpcode(_) => {}
773 EvmIrTerminatorKind::Branch { condition, .. } => visit(condition)?,
774 EvmIrTerminatorKind::Switch { value, cases, .. } => {
775 visit(value)?;
776 for (case, _) in cases {
777 visit(case)?;
778 }
779 }
780 EvmIrTerminatorKind::Return { offset, size }
781 | EvmIrTerminatorKind::Revert { offset, size } => {
782 visit(offset)?;
783 visit(size)?;
784 }
785 EvmIrTerminatorKind::SelfDestruct { recipient } => visit(recipient)?,
786 }
787 Ok(())
788}
789
790fn visit_terminator_targets<E>(
791 kind: &EvmIrTerminatorKind,
792 mut visit: impl FnMut(EvmIrBlockId) -> Result<(), E>,
793) -> Result<(), E> {
794 match kind {
795 EvmIrTerminatorKind::Fallthrough(target) | EvmIrTerminatorKind::Jump(target) => {
796 visit(*target)?
797 }
798 EvmIrTerminatorKind::Branch { then_block, else_block, .. } => {
799 visit(*then_block)?;
800 visit(*else_block)?;
801 }
802 EvmIrTerminatorKind::Switch { default, cases, .. } => {
803 visit(*default)?;
804 for (_, target) in cases {
805 visit(*target)?;
806 }
807 }
808 EvmIrTerminatorKind::Return { .. }
809 | EvmIrTerminatorKind::Revert { .. }
810 | EvmIrTerminatorKind::Stop
811 | EvmIrTerminatorKind::Invalid
812 | EvmIrTerminatorKind::SelfDestruct { .. }
813 | EvmIrTerminatorKind::RawOpcode(_) => {}
814 }
815 Ok(())
816}