Why `?` and `orelse unreachable` behave inconsistently under ReleaseFast/ReleaseSmall

pub fn main() !void {
    var mm = m();
    std.debug.print("mm: {any}\n", .{mm});
    std.debug.print("type of mm: {any}\n", .{@TypeOf(mm)});

    std.debug.print("{s}\n", .{"-" ** 60});

    var nn = n();
    std.debug.print("nn: {any}\n", .{nn});
    std.debug.print("type of nn: {any}\n", .{@TypeOf(nn)});

const Point = struct { x: i32, y: u32 };

fn m() Point {
    var a: ?Point = null;
    return a.?;

fn n() Point {
    var a: ?Point = null;
    return a orelse unreachable;
  • outpuit in ReleaseFast/ReleaseSmall mode (mm is printed and nn is not)
mm: main.Point{ .x = 0, .y = 0 }
type of mm: main.Point

I think you’re running into undefined behavior here - from kristoff-it on the old zig reddit:

Absolutely not. unreachable marks that a code path is impossible to trigger. In safe modes you will get a panic if you got it wrong, while in unsafe modes (ReleaseFast, ReleaseSmall) you will potentially get a misbehaving program.

Your point is well taken though - it’s strange that in the documentation it says that it’s equivalent to unreachable. @kristoff any thoughts?

1 Like

I tried it in godbolt
It indeed seems to produce different code, both in debug and release.
orelse unreachable produces more code in debug, which I guess makes sense given that .? is a more specific concept.
However llvm seems to be better at optimizing orelse unreachable

But I personally think it’s fine that they behave differently, given that it’s only noticable within the bounds of undefined behavior.

1 Like

Also I’m confused, if .? is different from else reachable, why .? behaves differently in releaseSafe and releaseFast :thinking:

I’m fairly confident this is a bug. Unless the semantics of .? changed recently, it really is supposed to be equivalent to catch unreachable.

1 Like

It’s equivalent when the behavior is defined - we’re in UB land here, so anything goes, including “maybe break the program and maybe don’t”. x.? and x catch unreachable will likely be lowered by the compiler in slightly different (but semantically equivalent provided x is non-null) ways, allowing the UB to behave differently here.


good point