1

I have a fairly complicated match statement (with nested ifs and the like) at the end of a function. Every branch of this should either explicitly return from the function, or call some -> ! function (such as process::exit).

For both communication to other programmers, and to protect myself from myself, I'd like to tell the compiler to assert that anything after this match is unreachable. I know it knows how to do this statically, since if I put code there I get compile-time warnings.

Two things I've tried:

  1. Assign the match statement to let _: ! = match .... However, the ! is still experimental, so this doesn't work

  2. Wrap the match in a closure move || -> ! { match ... }();. However, this restricts me from being able to just return from the parent function.


Specific details of my situation which don't necessarily apply generally:

  • Function in question is fn main() -> ()
  • Conditional logic must either diverge to a ()-returning function, or diverge to a !-returning function
  • Failure to do so indicates a path where an error is either not correctly handled or reported
  • return-ing functions from within the conditional logic need to be there to make use of values unwraped by matches
JMAA
  • 1,730
  • 13
  • 24
  • "I know it knows how to do this statically, since if I put code there I get compile-time warnings." So just turn warnings into errors and you're done? – mcarton Jun 16 '20 at 09:46
  • I don't know if it is possible statically, but for the doc aspect and runtime check you can always put an [`unreachable!()`](https://doc.rust-lang.org/std/macro.unreachable.html) after your match. – Jmb Jun 16 '20 at 09:57
  • @mcarton unfortunately, I want the opposite. I'd need the _absence_ of this warning to be an error – JMAA Jun 16 '20 at 09:58
  • @Jmb thanks, I've already done that (entirely to communicate with other programmers). vscode even helpfully greys out the line to indicate it is, in fact, unreachable :P – JMAA Jun 16 '20 at 09:59
  • 2
    It would really help to provide a concrete minimal example of what would like to improve here. What is the return type of the function? Considering that the `match` expression is the last one on the function, why not just leave the end of the function blank and use the outcome of this match as its final output? Match expressions are already required to be exhaustive. – E_net4 Jun 16 '20 at 10:04
  • 1
    I don't really understand the point of the question, [it's already an error to forget to return in rust](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6f7f17a3c9689a944f0baa267ebf6a42). – mcarton Jun 16 '20 at 10:21
  • Function is question is `main`, it returns `()`, which is perfectly valid to omit – JMAA Jun 16 '20 at 10:24
  • I am trying to assert that either the `main` function diverges to some other `()`-returning function (which happens deep within the conditional logic, because it needs values extracted by the matches), or that it diverges to a never-returning function. In my particular case, getting to the end of `main` means an error case hasn't been correctly reported. – JMAA Jun 16 '20 at 10:27
  • Maybe you can use an empty `enum Never {}` as a substitute for `!` and use your idea of `let _: Never = match ... `. – rodrigo Jun 16 '20 at 11:11
  • Functions that return `()` don't diverge. What's the difference between *diverging to some other `()`-returning function* and *not* diverging? Do you mean you have functions that are declared to return `()` but in fact diverge? (which is perfectly legal, type-wise) – trent Jun 16 '20 at 11:33
  • Put simply, I don't know how you can tell the difference between a `match` arm that correctly calls a function that returns `()`, and a `match` arm that incorrectly fails to handle an error case, given that both return `()`. Is the requirement just "each arm must call some function"? (which might be enforced syntactically, perhaps with a macro, but not in the type system) – trent Jun 16 '20 at 11:34
  • 1
    @trentcl Apologies, I may have misspoken (I don't necessarily know all of the corners of the definitions of these terms). I simply meant a path containing `return foo();`, where `foo() -> ()` and therefore the `match` statement will never complete (due to the `return`) -- I intended "diverge" to mean that. – JMAA Jun 16 '20 at 13:06
  • No need to apologize; your use of the term "diverge" was precise, I just didn't think of that interpretation (another reason it's good to provide a minimal reproducible example). – trent Jun 16 '20 at 13:27

1 Answers1

5

This only seems to become a problem because of a few exceptional quirks around the unit type ():

  1. () is the default when the return type is omitted from a function signature (so fn main() is equivalent to fn main() -> ());
  2. And even if you do not provide any expression to return, empty blocks or statements in code also evaluate to ().

The example below works because the semi-colon turns the expression 5 into a statement, the value of which is hence discarded.

fn foo() {
    5;
}

Recursively, it is easy for all match arms to evaluate to () when none of them yield an outcome of another type. This is the case when using return because the return statement creates a true divergence from the execution flow: it evaluates to the never type !, which coerces to any other type.

fn foo(bar: i32) {
    match bar {
        1 => {
            return do_good_things(); // coerces to () because of the default match arm
        }
        0 => {
            return do_other_things(); // coerces to () because of the default match arm
        }
        _ => {
            // arm evaluates to (), oops
        }
    }
}

The ubiquity of this unit type generally contributes to elegant code. In this case however, it may trigger a false positive when a more strict control flow is intended. There is no way for the compiler to resolve this unless we introduce another type to counter it.

Therefore, these solutions are possible:

  1. Use a different return type for the function. If there is nothing applicable to return (e.g. only side-effects), you can use nearly any type, but another unit type provides a better assurance that it becomes a zero-cost abstraction.

Playground

struct Check;

fn foo(bar: i32) -> Check {
    match bar {
        1 => {
            do_good_things();
            Check
        }
        0 => {
            do_other_things();
            return Check; // can use return
        }
        _ => {
            // error[E0308]: expected struct Check, found ()
        }
    }
}
  1. Do not use return or break statements, and establish that all of your match arms need to evaluate to something other than ().

Playground

struct Check;

fn foo(bar: i32) {
    let _: Check = match bar {
        1 => {
            do_good_things();
            Check
        }
        0 => {
            do_other_things();
            Check
        }
        _ => {
            // error[E0308]: expected struct Check, found ()
        }
    };
}
  1. The reverse: establish that the match expression evaluates to a zero type (one like the never type !), so that no match arm can return from it except using control flow statements such as break or return.

Playground

enum Nope {}

fn foo(bar: i32) {
    let _: Nope = match bar {
        1 => {
            return do_good_things();
        }
        0 => {
            return do_other_things();
        }
        _ => {
            // error[E0308]: expected enum `Nope`, found ()
        }
    };
}

See also:

E_net4
  • 27,810
  • 13
  • 101
  • 139
  • 1
    `type Check = Result<(), Never>; enum Never {}` makes it possible for the first option to work with `main` (which can only return things that implement `Terminate`). Of course you have to use `Ok(())` instead of `Check` inside the `match`. – trent Jun 16 '20 at 12:32