rust-lang/rust · critical

invalid terminator state

Error message

invalid terminator state

What it means

`BasicBlockData::terminator()` is an accessor that asserts the block's optional `terminator` field is `Some`. rustc_middle documents that a basic block's terminator must be set once the block is finished, so calling this on a block still under construction (or one produced by a buggy MIR pass that forgot to set a terminator) violates the invariant and panics with 'invalid terminator state'.

Source

Thrown at compiler/rustc_middle/src/mir/mod.rs:1368

        statements: Vec<Statement<'tcx>>,
        terminator: Option<Terminator<'tcx>>,
        is_cleanup: bool,
    ) -> BasicBlockData<'tcx> {
        BasicBlockData {
            statements,
            after_last_stmt_debuginfos: StmtDebugInfos::default(),
            terminator,
            is_cleanup,
        }
    }

    /// Accessor for terminator.
    ///
    /// Terminator may not be None after construction of the basic block is complete. This accessor
    /// provides a convenient way to reach the terminator.
    #[inline]
    pub fn terminator(&self) -> &Terminator<'tcx> {
        self.terminator.as_ref().expect("invalid terminator state")
    }

    #[inline]
    pub fn terminator_mut(&mut self) -> &mut Terminator<'tcx> {
        self.terminator.as_mut().expect("invalid terminator state")
    }

    /// Does the block have no statements and an unreachable terminator?
    #[inline]
    pub fn is_empty_unreachable(&self) -> bool {
        self.statements.is_empty() && matches!(self.terminator().kind, TerminatorKind::Unreachable)
    }

    /// Like [`Terminator::successors`] but tries to use information available from the [`Instance`]
    /// to skip successors like the `false` side of an `if const {`.
    ///
    /// This is used to implement [`traversal::mono_reachable`] and
    /// [`traversal::mono_reachable_reverse_postorder`].

View on GitHub (pinned to 22057b88b0)

Solutions

  1. Audit the MIR pass that constructed or mutated the block and ensure it always sets a terminator (e.g. `Unreachable`, `Return`, or `Goto`) before the block is observed by another pass.
  2. If you must query a block mid-construction, guard with `block.terminator.as_ref()` (Option) instead of the asserting accessor.
  3. Reproduce with `-Zmir-opt-level=0` to rule out an optimizer pass; if it only fails at higher levels, bisect with `-Zmir-opt-level=N` and report the offending pass.
  4. Dump MIR with `-Zunpretty=mir` per pass (`-Zdump-mir=all`) to find the exact pass leaving a terminator-less block.

Example fix

// before
let bb = body.basic_blocks_mut().push(BasicBlockData::new_stmts(
    stmts, None, false,
));
let term = body[bb].terminator(); // panic: invalid terminator state

// after
let bb = body.basic_blocks_mut().push(BasicBlockData::new_stmts(
    stmts,
    Some(Terminator {
        source_info,
        kind: TerminatorKind::Unreachable,
    }),
    false,
));
let term = body[bb].terminator();
Defensive patterns

Strategy: validation

Validate before calling

// `BasicBlockData::terminator()` calls `.expect("invalid terminator state")`.
// Validate the block is fully built BEFORE touching the accessor.
use rustc_middle::mir::BasicBlockData;
fn block_ready_for_use<'tcx>(bb: &BasicBlockData<'tcx>) -> bool {
    bb.terminator.is_some() // Option<Terminator<'tcx>>
}
// Usage:
//   if block_ready_for_use(&block) {
//       let term = block.terminator();
//   } else {
//       // block is still under construction: insert/assign a terminator first
//   }

Type guard

// Narrow an incompletely-built block to a usable one without panicking.
use rustc_middle::mir::{BasicBlockData, Terminator};
fn as_complete_block<'tcx>(bb: &BasicBlockData<'tcx>) -> Option<&Terminator<'tcx>> {
    bb.terminator.as_ref() // None instead of panic
}
// Caller pattern:
//   match as_complete_block(&block) {
//       Some(term) => { /* safe */ }
//       None => { /* finish building: block.terminator = Some(...); */ }
//   }

Prevention

When it happens

Trigger: Triggered by any code path — MIR traversal, optimization pass, or consumer — that calls `body.basic_blocks[block].terminator()` on a `BasicBlockData` whose `terminator` field is `None`. Most often hit by a MIR transformation that builds a new block via `BasicBlockData::new_stmts(stmts, None, is_cleanup)` or removes a terminator without re-adding one, then immediately queries it.

Common situations: Writing a custom `MirPass` or rustc plugin that inserts or rewrites basic blocks; mir-opt instrumentation that walks blocks before a pass finishes; out-of-tree Cranelift/gcc backend consuming half-built MIR; in-tree regression where an optimization drops the terminator edge case.

Related errors


AI-assisted analysis of rust-lang/rust@22057b88b0 (2026-08-03). Data as JSON: /data/errors/e932b9eb7ba56908.json. Report an issue: GitHub.