Skip to main content

solar_codegen/backend/evm/ir/
verify.rs

1//! EVM IR verifier.
2
3use super::*;
4use solar_data_structures::{index::IndexVec, map::FxHashSet};
5use std::fmt as std_fmt;
6
7/// An error produced while validating EVM IR.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct EvmIrVerifyError {
10    /// Human-readable validation failure.
11    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
32/// Verifies basic EVM IR invariants.
33///
34/// # Errors
35///
36/// Returns an [`EvmIrVerifyError`] if the module contains invalid references,
37/// duplicate definitions, missing terminators, or malformed identifiers.
38pub 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/// One abstract stack word tracked by the consistency simulator.
158///
159/// Words carry their value identity when known so cross-block edges can compare
160/// the exact words a predecessor leaves with those a successor declares. Words
161/// produced by `push` or by an extra output of a multi-result op have no SSA
162/// name and are modeled as [`AbstractWord::Unknown`]; two `Unknown` words are
163/// never considered equal across an edge.
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
165enum AbstractWord {
166    /// A word with a known SSA value identity.
167    Value(EvmIrValueId),
168    /// An anonymous word (a `push` immediate or a synthesized output) whose
169    /// identity is not an SSA value.
170    Unknown,
171}
172
173/// An abstract model stack: known words (top first) over an optional implicit
174/// floor of predecessor-inherited words.
175///
176/// EVM basic blocks share one runtime stack. A block that is reachable from
177/// predecessors may consume words those predecessors left below the portion the
178/// block declares as its `entry_stack`. The production backend exploits this:
179/// it does not declare an `entry_stack` and models opcodes as stack-neutral,
180/// relying on physical `push`/`dup`/`swap`/`pop` to thread an implicitly
181/// inherited stack between blocks. To stay sound without rejecting that
182/// convention, non-entry blocks model an **unbounded floor** of unknown words
183/// below `words`: an op that reaches past the known words draws fresh
184/// [`AbstractWord::Unknown`] floor words instead of underflowing.
185///
186/// The entry block has no predecessors, so its floor is empty (`infinite_floor`
187/// is `false`) and physical-op underflow is a real error and is rejected.
188struct ModelStack {
189    /// Known words, index 0 is the top of stack.
190    words: Vec<AbstractWord>,
191    /// Whether unknown words may be drawn from below `words`.
192    infinite_floor: bool,
193}
194
195impl ModelStack {
196    fn len(&self) -> usize {
197        self.words.len()
198    }
199
200    /// Ensures at least `depth` words are modeled, materializing implicit floor
201    /// words when allowed. Returns `false` if the stack is genuinely too shallow.
202    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        // A live word may be sitting in the implicit floor; over-approximate by
221        // treating any value as reachable when a floor is present.
222        self.words.contains(&word) || self.infinite_floor
223    }
224}
225
226/// Simulates each block's stack and checks cross-block edge consistency.
227///
228/// For every block we start from its declared `entry_stack` (top first), apply
229/// each instruction's stack effect to a [`ModelStack`] of word identities, apply
230/// the terminator's effect, and record the resulting exit stack. Physical stack
231/// ops (`dupN`/`swapN`/`pop`) are applied precisely; on the entry block (which
232/// has no implicit floor) underflows and out-of-range depths are rejected.
233///
234/// Then, for every CFG edge `pred -> succ`, the successor's declared
235/// `entry_stack` must be a **prefix** of the predecessor's exit stack (both top
236/// first): the words a successor declares as incoming are exactly the top `k`
237/// words the predecessor leaves, in order. The predecessor may leave additional
238/// words below them — a successor only names the prefix it consumes. The entry
239/// block must start from an empty stack.
240///
241/// Limitation: a *scheduled* operation has its operands cleared and does not
242/// record a stack effect, so its input arity is no longer recoverable from the
243/// instruction alone; such ops fall back to [`default_instruction_stack_effect`]
244/// (inputs = 0). This does not affect the cross-block check, which compares a
245/// successor's declared value identities against the predecessor's exit words.
246fn 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
293/// Computes a block's exit stack, rejecting any entry-block physical-stack-op
294/// underflow or out-of-range depth and any reference to a word not live on the
295/// model stack.
296fn 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            // An encoded `push` adds one word: its SSA result if it has one,
326            // otherwise an anonymous immediate word.
327            stack.push(result_word(inst));
328            Ok(())
329        }
330        EvmIrInstructionKind::Operation(_) if !inst.operands.is_empty() => {
331            // Unscheduled op: its value operands are still present, so they must
332            // be live on the model stack. They are not consumed (the operands
333            // sit on the stack until scheduling clears them); the result, if
334            // any, is pushed on top.
335            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            // Scheduled op: operands cleared. Pop its declared inputs and push
356            // its outputs.
357            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    // A terminator that still carries value operands is unscheduled: those
434    // operands must be live. We still apply the terminator's stack effect to the
435    // abstract exit stack: even in virtual form, branch/switch/return/revert
436    // consume their operand words at runtime, so successors must not be allowed
437    // to claim those consumed words as incoming stack values.
438    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
525/// The word a result-producing instruction leaves on top.
526fn 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}