Tokio: Warning about unreachable code when combining tokio::select and async function that returns `!`

Created on 17 Jul 2020  ยท  4Comments  ยท  Source: tokio-rs/tokio

Version
v0.2.21

rust:
stable-x86_64-pc-windows-gnu
rustc 1.44.1 (c7087fe00 2020-06-17)

Platform
Windows 10 1909 64-bit

Description
I have an async function that drives an infinite loop, and I declared it async and its return type is !.

Consider this minimal example:

use std::time::Duration;

async fn one() -> ! {
    loop {
        tokio::time::delay_for(Duration::from_secs(1)).await;
        println!("tick");
    }
}

async fn two() {
    tokio::time::delay_for(Duration::from_secs(3)).await;
}

#[tokio::main]
async fn main() {
    tokio::select! {
        _ = one() => {},
        _ = two() => {}
    };

    println!("finished.");
}

playground link: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=2924dce7469017c6534d72ab407bdf07

When compiling it, I get the following compiler warning:

warning: unreachable statement
  --> src/main.rs:16:5
   |
16 | /     tokio::select! {
17 | |         _ = one() => {},
18 | |         _ = two() => {}
19 | |     };
   | |      ^
   | |      |
   | |______unreachable statement
   |        any code following this `match` expression is unreachable, as all arms diverge
   |
   = note: `#[warn(unreachable_code)]` on by default
   = note: this warning originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

I think the warning is silly because this is a perfectly valid way of using tokio::select!, to stop an infinite async task.
I would expect there to be no warning.

I dug a bit into it using cargo expand, and this is what the above example generated (removed/replaced some imports of std internals to make it compile):

use std::time::Duration;
async fn one() -> ! {
    loop {
        tokio::time::delay_for(Duration::from_secs(1)).await;
        println!("tick");
    }
}
async fn two() {
    tokio::time::delay_for(Duration::from_secs(3)).await;
}
fn main() {
    tokio::runtime::Builder::new()
        .basic_scheduler()
        .threaded_scheduler()
        .enable_all()
        .build()
        .unwrap()
        .block_on(async {
            {
                mod util {
                    pub(super) enum Out<_0, _1> {
                        _0(_0),
                        _1(_1),
                        Disabled,
                    }
                    pub(super) type Mask = u8;
                }

                use ::tokio::macros::support::Future;
                use ::tokio::macros::support::Pin;
                use ::tokio::macros::support::Poll::{Pending, Ready};
                const BRANCHES: u32 = 2;
                let mut disabled: util::Mask = Default::default();

                if !true {
                    let mask = 1 << 0;
                    disabled |= mask;
                }

                if !true {
                    let mask = 1 << 1;
                    disabled |= mask;
                }

                let mut output = {
                    let mut futures = (one(), two());
                    ::tokio::macros::support::poll_fn(|cx| {
                        let mut is_pending = false;
                        let start = ::tokio::macros::support::thread_rng_n(BRANCHES);
                        for i in 0..BRANCHES {
                            let branch = (start + i) % BRANCHES;
                            match branch {
                                0 => {
                                    let mask = 1 << branch;
                                    if disabled & mask == mask {
                                        continue;
                                    }
                                    let (fut, ..) = &mut futures;
                                    let mut fut = unsafe { Pin::new_unchecked(fut) };
                                    let out = match fut.poll(cx) {
                                        Ready(out) => out,
                                        Pending => {
                                            is_pending = true;
                                            continue;
                                        }
                                    };
                                    disabled |= mask;
                                    #[allow(unused_variables)]
                                    match &out {
                                        _ => {}
                                        _ => continue,
                                    }
                                    return Ready(util::Out::_0(out));
                                }
                                1 => {
                                    let mask = 1 << branch;
                                    if disabled & mask == mask {
                                        continue;
                                    }
                                    let (_, fut, ..) = &mut futures;
                                    let mut fut = unsafe { Pin::new_unchecked(fut) };
                                    let out = match fut.poll(cx) {
                                        Ready(out) => out,
                                        Pending => {
                                            is_pending = true;
                                            continue;
                                        }
                                    };
                                    disabled |= mask;
                                    #[allow(unused_variables)]
                                    match &out {
                                        _ => {}
                                        _ => continue,
                                    }
                                    return Ready(util::Out::_1(out));
                                }
                                _ => panic!(),
                            }
                        }
                        if is_pending {
                            Pending
                        } else {
                            Ready(util::Out::Disabled)
                        }
                    })
                    .await
                };
                match output {
                    util::Out::_0(_) => {}
                    util::Out::_1(_) => {}
                    util::Out::Disabled => panic!(),
                    _ => panic!(),
                }
            };

            println!("finished");
        })
}

playground link: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=cbaf73e335a70623c88feaa64e4aa8a9

Where the error is now (on the expanded version):

warning: unreachable statement
  --> src\main.rs:67:37
   |
60 |   ...                   let out = match fut.poll(cx) {
   |  _________________________________-
61 | | ...                       Ready(out) => out,
62 | | ...                       Pending => {
63 | | ...                           is_pending = true;
64 | | ...                           continue;
65 | | ...                       }
66 | | ...                   };
   | |_______________________- any code following this `match` expression is unreachable, as all arms diverge
67 |   ...                   disabled |= mask;
   |                         ^^^^^^^^^^^^^^^^^ unreachable statement
   |
   = note: `#[warn(unreachable_code)]` on by default
A-tokio C-bug M-macros

Most helpful comment

I think that we just want an #[allow(unreachable_code)] in the right place.

All 4 comments

Hi, @RAnders00 ! I've tried to follow your lead.

First, this is exactly what you wrote (I'm copying again to compare it)

use std::time::Duration;

async fn one() -> ! {
    loop {
        tokio::time::delay_for(Duration::from_secs(1)).await;
        println!("tick");
    }
}

async fn two() {
    tokio::time::delay_for(Duration::from_secs(3)).await;
}

#[tokio::main]
async fn main() {
    tokio::select! {
        _ = one() => {},
        _ = two() => {}
    };

    println!("finished.");
}

And this expands to:

use std::time::Duration;

async fn one() -> ! {
    loop {
        tokio::time::delay_for(Duration::from_secs(1)).await;
        println!("tick");
    }
}

async fn two() {
    tokio::time::delay_for(Duration::from_secs(3)).await;
}

#[tokio::main]
async fn main() {
    tokio::select! {
        _ = one() => {},
        _ = two() => {}
    };

    println!("finished.");
}
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std;
use std::time::Duration;
async fn one() -> ! {
    loop {
        tokio::time::delay_for(Duration::from_secs(1)).await;
        {
            ::std::io::_print(::core::fmt::Arguments::new_v1(
                &["tick\n"],
                &match () {
                    () => [],
                },
            ));
        };
    }
}
async fn two() {
    tokio::time::delay_for(Duration::from_secs(3)).await;
}
fn main() {
    tokio::runtime::Runtime::new().unwrap().block_on(async {
        {
            {
                mod util {
                    pub(super) enum Out<_0, _1> {
                        _0(_0),
                        _1(_1),
                        Disabled,
                    }
                    pub(super) type Mask = u8;
                }
                use ::tokio::macros::support::Future;
                use ::tokio::macros::support::Pin;
                use ::tokio::macros::support::Poll::{Ready, Pending};
                const BRANCHES: u32 = 2;
                let mut disabled: util::Mask = Default::default();
                if !true {
                    let mask = 1 << 0;
                    disabled |= mask;
                }
                if !true {
                    let mask = 1 << 1;
                    disabled |= mask;
                }
                let mut output = {
                    let mut futures = (one(), two());
                    ::tokio::macros::support::poll_fn(|cx| {
                        let mut is_pending = false;
                        let start = ::tokio::macros::support::thread_rng_n(BRANCHES);
                        for i in 0..BRANCHES {
                            let branch = (start + i) % BRANCHES;
                            match branch {
                                0 => {
                                    let mask = 1 << branch;
                                    if disabled & mask == mask {
                                        continue;
                                    }
                                    let (fut, ..) = &mut futures;
                                    let mut fut = unsafe { Pin::new_unchecked(fut) };
                                    let out = match fut.poll(cx) {
                                        Ready(out) => out,
                                        Pending => {
                                            is_pending = true;
                                            continue;
                                        }
                                    };
                                    disabled |= mask;
                                    #[allow(unused_variables)]
                                    match &out {
                                        _ => {}
                                        _ => continue,
                                    }
                                    return Ready(util::Out::_0(out));
                                }
                                1 => {
                                    let mask = 1 << branch;
                                    if disabled & mask == mask {
                                        continue;
                                    }
                                    let (_, fut, ..) = &mut futures;
                                    let mut fut = unsafe { Pin::new_unchecked(fut) };
                                    let out = match fut.poll(cx) {
                                        Ready(out) => out,
                                        Pending => {
                                            is_pending = true;
                                            continue;
                                        }
                                    };
                                    disabled |= mask;
                                    #[allow(unused_variables)]
                                    match &out {
                                        _ => {}
                                        _ => continue,
                                    }
                                    return Ready(util::Out::_1(out));
                                }
                                _ => ::std::rt::begin_panic_fmt(&::core::fmt::Arguments::new_v1(
                                    &["internal error: entered unreachable code: "],
                                    &match (
                                        &"reaching this means there probably is an off by one bug",
                                    ) {
                                        (arg0,) => [::core::fmt::ArgumentV1::new(
                                            arg0,
                                            ::core::fmt::Display::fmt,
                                        )],
                                    },
                                )),
                            }
                        }
                        if is_pending {
                            Pending
                        } else {
                            Ready(util::Out::Disabled)
                        }
                    })
                    .await
                };
                match output {
                    util::Out::_0(_) => {}
                    util::Out::_1(_) => {}
                    util::Out::Disabled => {
                        ::std::rt::begin_panic("internal error: entered unreachable code")
                    }
                    _ => ::std::rt::begin_panic_fmt(&::core::fmt::Arguments::new_v1(
                        &["internal error: entered unreachable code: "],
                        &match (&"failed to match bind",) {
                            (arg0,) => [::core::fmt::ArgumentV1::new(
                                arg0,
                                ::core::fmt::Display::fmt,
                            )],
                        },
                    )),
                }
            };
            {
                ::std::io::_print(::core::fmt::Arguments::new_v1(
                    &["finished.\n"],
                    &match () {
                        () => [],
                    },
                ));
            };
        }
    })
}

And we actually get:

warning: unreachable statement
  --> src/main.rs:70:37
   |
63 |   ...                   let out = match fut.poll(cx) {
   |  _________________________________-
64 | | ...                       Ready(out) => out,
65 | | ...                       Pending => {
66 | | ...                           is_pending = true;
67 | | ...                           continue;
68 | | ...                       }
69 | | ...                   };
   | |_______________________- any code following this `match` expression is unreachable, as all arms diverge
70 |   ...                   disabled |= mask;
   |                         ^^^^^^^^^^^^^^^^^ unreachable statement

However, making a small adjustment the warning disappears:

7c7
< async fn one() -> ! {
---
> async fn one() {

You can check it here

The _expanded_ version:

โฏ cat without-exl/src/main.rs
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std;
use std::time::Duration;
async fn one() {
    loop {
        tokio::time::delay_for(Duration::from_secs(1)).await;
        {
            ::std::io::_print(::core::fmt::Arguments::new_v1(
                &["tick\n"],
                &match () {
                    () => [],
                },
            ));
        };
    }
}
async fn two() {
    tokio::time::delay_for(Duration::from_secs(3)).await;
}
fn main() {
    tokio::runtime::Runtime::new().unwrap().block_on(async {
        {
            {
                mod util {
                    pub(super) enum Out<_0, _1> {
                        _0(_0),
                        _1(_1),
                        Disabled,
                    }
                    pub(super) type Mask = u8;
                }
                use ::tokio::macros::support::Future;
                use ::tokio::macros::support::Pin;
                use ::tokio::macros::support::Poll::{Ready, Pending};
                const BRANCHES: u32 = 2;
                let mut disabled: util::Mask = Default::default();
                if !true {
                    let mask = 1 << 0;
                    disabled |= mask;
                }
                if !true {
                    let mask = 1 << 1;
                    disabled |= mask;
                }
                let mut output = {
                    let mut futures = (one(), two());
                    ::tokio::macros::support::poll_fn(|cx| {
                        let mut is_pending = false;
                        let start = ::tokio::macros::support::thread_rng_n(BRANCHES);
                        for i in 0..BRANCHES {
                            let branch = (start + i) % BRANCHES;
                            match branch {
                                0 => {
                                    let mask = 1 << branch;
                                    if disabled & mask == mask {
                                        continue;
                                    }
                                    let (fut, ..) = &mut futures;
                                    let mut fut = unsafe { Pin::new_unchecked(fut) };
                                    let out = match fut.poll(cx) {
                                        Ready(out) => out,
                                        Pending => {
                                            is_pending = true;
                                            continue;
                                        }
                                    };
                                    disabled |= mask;
                                    #[allow(unused_variables)]
                                    match &out {
                                        _ => {}
                                        _ => continue,
                                    }
                                    return Ready(util::Out::_0(out));
                                }
                                1 => {
                                    let mask = 1 << branch;
                                    if disabled & mask == mask {
                                        continue;
                                    }
                                    let (_, fut, ..) = &mut futures;
                                    let mut fut = unsafe { Pin::new_unchecked(fut) };
                                    let out = match fut.poll(cx) {
                                        Ready(out) => out,
                                        Pending => {
                                            is_pending = true;
                                            continue;
                                        }
                                    };
                                    disabled |= mask;
                                    #[allow(unused_variables)]
                                    match &out {
                                        _ => {}
                                        _ => continue,
                                    }
                                    return Ready(util::Out::_1(out));
                                }
                                _ => ::std::rt::begin_panic_fmt(&::core::fmt::Arguments::new_v1(
                                    &["internal error: entered unreachable code: "],
                                    &match (
                                        &"reaching this means there probably is an off by one bug",
                                    ) {
                                        (arg0,) => [::core::fmt::ArgumentV1::new(
                                            arg0,
                                            ::core::fmt::Display::fmt,
                                        )],
                                    },
                                )),
                            }
                        }
                        if is_pending {
                            Pending
                        } else {
                            Ready(util::Out::Disabled)
                        }
                    })
                    .await
                };
                match output {
                    util::Out::_0(_) => {}
                    util::Out::_1(_) => {}
                    util::Out::Disabled => {
                        ::std::rt::begin_panic("internal error: entered unreachable code")
                    }
                    _ => ::std::rt::begin_panic_fmt(&::core::fmt::Arguments::new_v1(
                        &["internal error: entered unreachable code: "],
                        &match (&"failed to match bind",) {
                            (arg0,) => [::core::fmt::ArgumentV1::new(
                                arg0,
                                ::core::fmt::Display::fmt,
                            )],
                        },
                    )),
                }
            };
            {
                ::std::io::_print(::core::fmt::Arguments::new_v1(
                    &["finished.\n"],
                    &match () {
                        () => [],
                    },
                ));
            };
        }
    })
}

So honestly, I'm in doubt whether this is a tokio or rust issue.

Seems that the _issue_ comes from yielding the future's output value, when being Ready. This happens in the following line, on the macro select!: https://github.com/tokio-rs/tokio/blob/2bc6bc14a82dc4c8d447521005e044028ae199fe/tokio/src/macros/select.rs#L391

So what is happening is that the return type of one() is !. So because out is of type !, then Rust assumes that the rest of the code is not reachable. This explains why, also when changing the signature to being async fn one() -> (), the warning disappears.

Any ideas on how to move forward? @Darksonn

I think that we just want an #[allow(unreachable_code)] in the right place.

Closing -- fixed in #2678

Was this page helpful?
0 / 5 - 0 ratings

Related issues

najamelan picture najamelan  ยท  4Comments

peterhuene picture peterhuene  ยท  4Comments

carllerche picture carllerche  ยท  5Comments

davidbarsky picture davidbarsky  ยท  3Comments

hawkw picture hawkw  ยท  5Comments