Unreachable

Then, before building for production, all I need to do is a quick find or grep for unreachable and then decide how to handle the errors.

This means you did not design for handling errors and you’re instead treating them as exceptional cases which is another issue in itself.

they give me the impression of a highly restricted use of this part of the language, pretty much removing its usefulness in the develop → test → debug phase.

The issue is that that’s exactly the case with unreachable as it’s asserting that the path may not be reached allowing the compiler to play tetris if it ever happens. unreachable for switch prongs is completely fine as long as you guarantee they really are unreachable by either preconditions for the function or checks above the switch. Other uses of unreachable within switch is a wrong use of the tool (this is an absolute due to what it communicates to the compiler). Using it to “discard” was the issue as unreachable is taken as a guarantee that it won’t happen thus if mayFail() actually fails then it could start playing tetris, fire all missles, write a song, and so on as it’s undefined what happens after. Safety checked undefined behaviour doesn’t change that it’s undefined behaviour even if it may feel convenient when prototyping.

A large number of cases in std need to be checked (e.g there’s open discussion on handle impossible errors from the kernel with error.Unexpected instead of unreachable · Issue #6389 · ziglang/zig · GitHub and linked tickets) as quite a few cases may not be unreachable thus std cannot really be used as a quality guide until the process of auditing it has completed (even after there may be issues).

Can this happen in Debug or ReleaseSafe? If not, then this Doc clearly covers this in the first bullet point of its intro.

Sounds like unreachable is like unsafe in rust, where you are asserting to the compiler some invariants that the compiler can’t itself prove.

1 Like
pub fn func(min: u8, max: u8) u8 {
    // same as `if (min > max) unreachable;`
    std.debug.assert(min <= max);

    var byte: u8 = 0;
    while (byte <= max) : (byte += 1) {
        if (byte >= min) {
            return byte;
        }
    }

    // explicit `unreachable` is required to avoid the error:
    // "function with non-void return type 'u8' implicitly returns"
    unreachable;
}

I’m not sure if I think this is terribly bad, I actually think it illustrates the idea of unreachable being used to assert invariants the compiler can’t/doesn’t keep track of. However it is a recursive use of unreachable. The Second unreachable is only true because there is a previous case (in the assert) where a state was asserted as unreachable. In unsafe release modes, your second assertion would not necessarily be true, and open you up to UB. Would this example be as clear, illustrate the idea, and not contain a hidden problem:

pub fn func(min: u8, max: u8) u8 {
    // same as `if (min > max) unreachable;`
    if (max < min) { 
        @panic("Min can not be greater than max") 
    };

    var byte: u8 = 0;
    while (byte <= max) : (byte += 1) {
        if (byte >= min) {
            return byte;
        }
    }

    // explicit `unreachable` is required to avoid the error:
    // "function with non-void return type 'u8' implicitly returns"
    unreachable;
}

Agreed. I think this kind of use is due to the lack of an unimplemented kind of mechanism in the language. Maybe the best alternative is to just:

@compileError("unimplemented");

The mayFail() catch unreachable text is wrong no matter what the intro says. Including it will have beginners go down the wrong path regardless of the intro as it doesn’t make it any more “correct”. unreachable being reached is illegal behaviour and not an alternative to @panic("this should never fail").

I don’t think communication is working here so I’ll just give up.

1 Like

I don’t like the func example because it seems contrived, the while loop seems unnecessary, isn’t this the same?:

pub fn func(min: u8, max: u8) u8 {
    std.debug.assert(min <= max);
    return min;
}

I think the function always (asserts / hits undefined behavior) or returns min.

Can be used to indicate that certain, or remaining, switch cases cannot happen.

I think that solves the discussion about the switch cases?
The programmer states/promises/asserts to the compiler that those cases can’t happen.
“be handled” seems like it implies that they could happen just can’t be handled, but in that case you would use @panic.

3 Likes

The Language Reference has an example when talking about inline. Maybe it can be used instead.

fn withFor(any: AnySlice) usize {
    const Tag = @typeInfo(AnySlice).Union.tag_type.?;
    inline for (@typeInfo(Tag).Enum.fields) |field| {
        // With `inline for` the function gets generated as
        // a series of `if` statements relying on the optimizer
        // to convert it to a switch.
        if (field.value == @intFromEnum(any)) {
            return @field(any, field.name).len;
        }
    }
    // When using `inline for` the compiler doesn't know that every
    // possible case has been handled requiring an explicit `unreachable`.
    unreachable;
}
2 Likes

Thanks everyone for your input! I tried to incorporate all of it. Please feel free to point out anything else.

4 Likes

Thank you, I think it now is very well written!
Also thank you to everyone contributing!

4 Likes

Last note, catch unreachable can be used when you statically know that an error will not occur for that call. That is, mayFail() catch unreachable cannot fail due to an implementation detail you know about but cannot communicate to the compiler. It’s not ideal (better to add a function which asserts success) but it’s a valid case.

3 Likes

The catch unreachable already is the assertion.

Think of list.appendAssumeCapacity() which asserts a specific thing rather than “cannot fail”. Intent is clear and less likely to be violated over future updates to the codebase.