enum Never {}
fn make_closure(x: Never) -> impl Fn() {
move || match x {}
}
fn helper<F: Fn()>(_: impl Fn(Never) -> F) -> F {
unsafe { std::mem::MaybeUninit::uninit().assume_init() }
}
fn conjure_closure() -> impl Fn() {
helper(make_closure)
}
fn main() {
assert_eq!(0, size_of_val(&conjure_closure())); // UB here in edition 2018
assert_eq!(1, size_of_val(&Some(conjure_closure())));
conjure_closure()(); // UB here in edition 2021+
}
On edition 2021 or later, running the program in Miri results in both assertions passing, but UB only occurring after the closure is called:
error: Undefined Behavior: entering unreachable code
--> src/main.rs:4:19
|
4 | move || match x {}
| ^ Undefined Behavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: stack backtrace:
0: make_closure::{closure#0}
at src/main.rs:4:19: 4:20
1: main
at src/main.rs:18:5: 18:24
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
On edition 2018, running the program in Miri results in UB as soon as an instance of the closure is created:
error: Undefined Behavior: constructing invalid value of type {closure@src/main.rs:4:5: 4:12}: at .<captured-var(x)>, encountered a value of zero-variant enum `Never`
--> src/main.rs:8:14
|
8 | unsafe { std::mem::MaybeUninit::uninit().assume_init() }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: stack backtrace:
0: helper::<{closure@src/main.rs:4:5: 4:12}, fn(Never) -> impl Fn() {make_closure}>
at src/main.rs:8:14: 8:59
1: conjure_closure
at src/main.rs:12:5: 12:25
2: main
at src/main.rs:16:32: 16:49
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error
It appears that, in edition 2021 or later, due to closure precise captures the closure move || match x {} does not capture the variable x. Even though it does not capture x, it is nevertheless allowed to use the information that the value x "exists" inside the closure, to cause UB. On the other hand, it seems that because the type Never was not captured, the closure is considered to be inhabited by the compiler, so Option<Closure> has size 1.
This behavior seems strange, although I'm unsure if this is a bug.
cc @Nadrieril @RalfJung
Meta
Reproducible on the playground with version 1.98.0-nightly (2026-05-31 14210df0e27ccd7d9e6a)
On edition 2021 or later, running the program in Miri results in both assertions passing, but UB only occurring after the closure is called:
On edition 2018, running the program in Miri results in UB as soon as an instance of the closure is created:
It appears that, in edition 2021 or later, due to closure precise captures the closure
move || match x {}does not capture the variablex. Even though it does not capturex, it is nevertheless allowed to use the information that the valuex"exists" inside the closure, to cause UB. On the other hand, it seems that because the typeNeverwas not captured, the closure is considered to be inhabited by the compiler, soOption<Closure>has size 1.This behavior seems strange, although I'm unsure if this is a bug.
cc @Nadrieril @RalfJung
Meta
Reproducible on the playground with version
1.98.0-nightly (2026-05-31 14210df0e27ccd7d9e6a)