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
- 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.
- If you must query a block mid-construction, guard with `block.terminator.as_ref()` (Option) instead of the asserting accessor.
- 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.
- 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
- A BasicBlockData is only valid for reads once its `terminator` field is `Some`; treat `None` as 'still under construction'.
- When you construct or clone BasicBlockData, always set the terminator in the same statement — never leave a half-built block reachable by other code.
- In any pass that removes or rewrites blocks (inline, unreachable-block pruning), re-establish the terminator before the block becomes observable to other passes.
- If you traverse blocks produced by an upstream pass, assert/validate `terminator.is_some()` at the boundary rather than relying on the accessor's expect().
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
- destructed mir constant of adt without variant idx
- expected subslice projection on fixed-size array
- projecting to field of non-ADT {ty}
- range should be nonempty
- there must be provenance somewhere here
AI-assisted analysis of rust-lang/rust@22057b88b0 (2026-08-03).
Data as JSON: /data/errors/e932b9eb7ba56908.json.
Report an issue: GitHub.