# How Sway's IR Verification Pass Validates Compiled Code: A Deep Dive into the Module Verifier

> Learn how Sway's IR verification pass validates compiled code by checking modules, functions, and instructions for structural integrity and type safety. Get detailed diagnostics before code generation.

- Repository: [Fuel Labs/sway](https://github.com/FuelLabs/sway)
- Tags: deep-dive
- Published: 2026-03-04

---

**Sway's IR verification pass validates compiled code by hierarchically checking modules, functions, blocks, and instructions for structural integrity and type safety, returning detailed `IrError` diagnostics before code generation.**

Sway's intermediate representation (IR) serves as the bridge between high-level Sway code and FuelVM bytecode. Before handing IR to any backend, Sway's IR verification pass ensures structural and type-correctness across the entire `Context`. This verification system, implemented in the [FuelLabs/sway](https://github.com/FuelLabs/sway) repository, acts as a gatekeeper that prevents malformed IR from reaching code generation.

## Module-Level Verification in Sway's IR

The entry point for Sway's IR verification pass is the `module_verifier` function in [`sway-ir/src/verify.rs`](https://github.com/FuelLabs/sway/blob/main/sway-ir/src/verify.rs). This function acts as a standard pass that the pass manager can invoke:

```rust
pub fn module_verifier(
    context: &Context,
    _analyses: &AnalysisResults,
    module: Module,
) -> Result<AnalysisResult, IrError> {
    context.verify_module(module)?;
    Ok(Box::new(ModuleVerifierResult))
}

```

The real work happens in `Context::verify_module`, which performs two major checks:

1. **Function verification**: Iterates over every function in the module and calls `verify_function` to validate function-level invariants.
2. **Global variable validation**: Ensures non-mutable global variables have initializers, preventing undefined references.

```rust
fn verify_module(&self, module: Module) -> Result<(), IrError> {
    for function in module.function_iter(self) {
        self.verify_function(module, function)?;
    }
    // Globals must have initializers unless mutable
    for global in &self.modules[module.0].global_variables {
        if !global.1.is_mutable(self) && global.1.get_initializer(self).is_none() {
            let global_name = module.lookup_global_variable_name(self, global.1);
            return Err(IrError::VerifyGlobalMissingInitializer(
                global_name.unwrap_or_else(|| "<unknown>".to_owned()),
            ));
        }
    }
    Ok(())
}

```

If any check fails, the verifier returns an `IrError` variant such as `VerifyGlobalMissingInitializer`, pinpointing the offending global variable.

## Function-Level Verification

Once module-level checks pass, Sway's IR verification pass examines each function via `verify_function` in [`sway-ir/src/verify.rs`](https://github.com/FuelLabs/sway/blob/main/sway-ir/src/verify.rs). This method validates structural integrity before descending into individual blocks:

```rust
fn verify_function(&self, cur_module: Module, function: Function) -> Result<(), IrError> {
    // Parent module consistency
    if function.get_module(self) != cur_module { … }

    // Entry block must have no predecessors
    let entry_block = function.get_entry_block(self);
    if entry_block.num_predecessors(self) != 0 { … }

    // Function arguments must match entry‑block arguments
    if function.num_args(self) != entry_block.num_args(self) { … }
    for ((_, func_arg), block_arg) in function.args_iter(self).zip(entry_block.arg_iter(self)) {
        if func_arg != block_arg { … }
    }

    // Verify every block in the function
    for block in function.block_iter(self) {
        self.verify_block(cur_module, function, block)?;
    }

    // Verify function‑level metadata
    self.verify_metadata(function.get_metadata(self))?;
    Ok(())
}

```

Key validations at this stage include:

- **Module ownership**: Functions must belong to the module being verified (`InconsistentParent` error if not).
- **Entry block integrity**: The entry block must have no predecessors (it must be the root of the control flow graph).
- **Argument consistency**: Function arguments must exactly match the entry block's arguments in both count and type.
- **Metadata validation**: Function-level metadata undergoes `verify_metadata` checks.

## Block-Level Verification

After function validation, Sway's IR verification pass examines each basic block via `verify_block`. This method ensures that blocks are well-formed before inspecting individual instructions:

```rust
fn verify_block(
    &self,
    cur_module: Module,
    cur_function: Function,
    cur_block: Block,
) -> Result<(), IrError> {
    // Block‑function relationship
    if cur_block.get_function(self) != cur_function { … }

    // Empty, unreferenced blocks are ignored
    if cur_block.num_instructions(self) <= 1 && cur_block.num_predecessors(self) == 0 {
        return Ok(());
    }

    // Verify block arguments match their declaration order
    for (arg_idx, arg_val) in cur_block.arg_iter(self).enumerate() {
        match self.values[arg_val.0].value {
            ValueDatum::Argument(BlockArgument { idx, .. }) if idx == arg_idx => (),
            _ => return Err(IrError::VerifyBlockArgMalformed),
        }
    }

    // Verify all instructions inside the block
    let r = InstructionVerifier { … }.verify_instructions();

    // If an error occurs, pretty‑print the offending block/value for debugging
    if let Err(error) = &r { … }

    // Terminator checks – exactly one terminator and it must be the last instruction
    let (last_is_term, num_terms) = cur_block
        .instruction_iter(self)
        .fold((false, 0), |(_, n), ins| {
            if ins.is_terminator(self) { (true, n + 1) } else { (false, n) }
        });
    if !last_is_term {
        Err(IrError::MissingTerminator(cur_block.get_label(self).clone()))
    } else if num_terms != 1 {
        Err(IrError::MisplacedTerminator(cur_block.get_label(self).clone()))
    } else {
        Ok(())
    }
}

```

Critical block-level checks include:

- **Parent consistency**: Blocks must belong to the function under verification.
- **Argument ordering**: Block arguments must appear in the order declared in the IR (`VerifyBlockArgMalformed` if violated).
- **Terminator rules**: Each block must contain exactly one terminator instruction, and it must be the final instruction (`MissingTerminator` or `MisplacedTerminator` errors if violated).
- **Dead block handling**: Empty blocks with no predecessors are silently ignored to avoid noise from unreachable code.

## Instruction-Level Verification

The granular validation occurs in `InstructionVerifier::verify_instructions`, which dispatches to op-specific validators. For each instruction, the verifier checks parent consistency, operand type compatibility, and operation-specific constraints.

### Binary Operation Validation

The `verify_binary_op` function demonstrates type-specific validation for arithmetic and bitwise operations:

```rust
fn verify_binary_op(
    &self,
    op: &BinaryOpKind,
    arg1: &Value,
    arg2: &Value,
) -> Result<(), IrError> {
    let arg1_ty = arg1.get_type(self.context).ok_or(IrError::VerifyBinaryOpIncorrectArgType)?;
    let arg2_ty = arg2.get_type(self.context).ok_or(IrError::VerifyBinaryOpIncorrectArgType)?;

    match op {
        BinaryOpKind::Lsh | BinaryOpKind::Rsh => {
            // Shift RHS may be any uint, LHS must be uint or b256
            let is_lhs_ok = arg1_ty.is_uint(self.context) || arg1_ty.is_b256(self.context);
            if !is_lhs_ok || !arg2_ty.is_uint(self.context) {
                return Err(IrError::VerifyBinaryOpIncorrectArgType);
            }
        }
        BinaryOpKind::Add | BinaryOpKind::Sub => {
            // Either (uint, uint) or (ptr, u64)
            if !(arg1_ty.eq(self.context, &arg2_ty) && arg1_ty.is_uint(self.context)
                || arg1_ty.is_ptr(self.context) && arg2_ty.is_uint64(self.context))
            {
                return Err(IrError::VerifyBinaryOpIncorrectArgType);
            }
        }
        // ... other ops omitted for brevity …
    }
    Ok(())
}

```

### Operation-Specific Validators

The verifier includes specialized checks for various instruction types:

| Operation | Validation Logic |
|-----------|------------------|
| **BitCast** | Source and target types must be ≤ 64 bits (`verify_bitcast`). |
| **Call** | Callee must belong to the same module and argument types must match the function signature (`verify_call`). |
| **Load/Store** | Operand must be a pointer and stored/loaded types must agree (`verify_load`, `verify_store`). |
| **FuelVM-specific** | `verify_gtf`, `verify_log`, and `verify_state_*` ensure correct operand families and immediate ranges for FuelVM operations. |
| **Metadata** | Struct tags must be valid identifiers (`verify_metadata`). |

All validators return specific `IrError` variants that include context such as function names, block labels, and offending values to facilitate debugging.

## Integration with the Pass Manager

The module verifier is registered as a standard analysis pass in [`sway-ir/src/verify.rs`](https://github.com/FuelLabs/sway/blob/main/sway-ir/src/verify.rs):

```rust
pub fn create_module_verifier_pass() -> Pass {
    Pass {
        name: MODULE_VERIFIER_NAME,
        descr: "Verify module",
        deps: vec![],
        runner: ScopedPass::ModulePass(PassMutability::Analysis(module_verifier)),
    }
}

```

The pass manager loads this pass during pipeline construction (referenced in [`pass_manager.rs`](https://github.com/FuelLabs/sway/blob/main/pass_manager.rs)). When the Sway compiler finishes IR generation, the pass manager executes the verifier before any backend passes, guaranteeing that only well-formed IR proceeds to code generation.

## Practical Usage Examples

### Direct Verification via Context

You can invoke the verifier directly on a `Context` to validate IR before further processing:

```rust
use sway_ir::{Context, Backtrace, ExperimentalFeatures};

let mut ctx = Context::new(&source_engine, ExperimentalFeatures::default(), Backtrace::All);
// ... build or deserialize the IR ...

// Validate the whole program
if let Err(err) = ctx.verify() {
    eprintln!("IR verification failed: {}", err);
}

```

The call to `ctx.verify()` triggers the module verifier for every module in the context, returning detailed error information if validation fails.

### Using the Pass Manager

As implemented in the Sway compiler, you can register the verifier as part of a transformation pipeline:

```rust
use sway_ir::pass_manager::PassManager;

let mut pm = PassManager::new(&mut ctx);
pm.register(sway_ir::verify::create_module_verifier_pass());
// ... register other passes like `create_demotion_pass`, `create_codegen_pass`, etc.

pm.run(); // `module_verifier` runs automatically as part of the pipeline

```

The verifier functions as a standard pass that can be inserted anywhere in the pipeline, though it typically runs early to catch errors before expensive optimization or code generation passes execute.

## Summary

- **Sway's IR verification pass** operates hierarchically, validating modules, functions, blocks, and instructions in sequence.
- **Module-level checks** ensure functions belong to the correct module and global variables have required initializers.
- **Function-level validation** verifies entry block integrity, argument consistency, and metadata correctness.
- **Block-level rules** enforce single terminator placement, proper argument ordering, and parent consistency.
- **Instruction-level verification** dispatches to op-specific validators that check type compatibility, operand constraints, and FuelVM-specific requirements.
- **Integration** occurs through the standard pass manager, with the verifier running before backend code generation to guarantee sound IR.

## Frequently Asked Questions

### What happens when Sway's IR verification pass finds an error?

When the verifier detects an invariant violation, it immediately returns an `IrError` variant specific to the failure type, such as `VerifyGlobalMissingInitializer`, `MissingTerminator`, or `VerifyBinaryOpIncorrectArgType`. The error includes context like the function name, block label, and offending value, allowing the compiler to produce precise diagnostic messages that pinpoint exactly where the IR became malformed.

### Can I run Sway's IR verification pass independently of the full compiler pipeline?

Yes, the verification pass can be invoked directly through the `Context::verify()` method, which iterates over all modules and runs the full verification suite. Alternatively, you can instantiate a `PassManager`, register the verifier using `create_module_verifier_pass()`, and execute it as part of a custom transformation pipeline. This flexibility allows IR tools and debugging utilities to validate code without running the entire Sway compiler frontend.

### How does the verifier handle FuelVM-specific operations?

The instruction-level verification includes specialized validators for FuelVM-specific operations such as `verify_gtf`, `verify_log`, and `verify_state_*`. These functions check that operands belong to the correct value families (e.g., pointers, unsigned integers) and that immediate values fall within valid ranges required by the FuelVM specification. This ensures that the generated IR is not only structurally sound but also compatible with the target virtual machine's constraints.

### What is the relationship between block arguments and function arguments in the verifier?

The verifier enforces that function arguments must exactly match the entry block's arguments in both count and type. During function-level verification, the code iterates through `function.args_iter()` and `entry_block.arg_iter()` simultaneously, comparing each pair. If any mismatch occurs, the verifier returns an `InconsistentParent` or type error. This strict coupling ensures that the IR maintains a consistent view of data flow between the function signature and its entry point.