I know @TypeOf in unusual sometimes, but I'm not sure if consistency should made here

Should T1 == T2 in the following code?

const E = error {A, B};

fn foo(b: bool) E!noreturn {
    return if (b) E.A
    else while (true) {};
}

var c = false;

pub fn main() void {
    const T1 = @TypeOf(
        foo(c)
    );
    const T2 = @TypeOf(
        if (c) E.A
        else while (true) {}
    );
    const T3 = @TypeOf(
        if (c) E.A
        else error.C
    );
    @import("std").debug.print(
        \\{}
        \\{}
        \\{}
        \\
        , .{
            T1, // error{A,B}!noreturn
            T2, // error{A,B}
            T3, // error{A,B,C}
        }
    );
}

No, T1 is error union, T2 is error set.

1 Like

Some notes:

  • I think you shouldn’t worry too much about the E.A syntax since it might be going away, although the Zig team hasn’t decided yet.
  • Although error{ A, B }!noreturn and error{ A, B } are distinct types, they are effectively equivalent: each type has the same number of possible values. In fact, the choice to use an error union type as the return type of foo is arbitrary, and the following code is valid:
    const E = error{ A, B };
    
    fn foo(b: bool) E {
        return if (b) E.A else while (true) {};
    }
    
2 Likes

To elaborate, noreturn can coerce to any type, this is what is happening in for T2.

But, as with all coercion, you can control it by providing result types, this is what is happening in T1.

@zigo, you are just assuming the situations are equivalent when they are not.

3 Likes

If we take a look at resolvePeerTypesInner and PeerResolveStrategy in src/Sema.zig (if Codeberg lets you load it), we can see that peer types are resolved (roughly) according to the following order of precedence:

  1. noreturn + T (any type) = T
  2. @TypeOf(undefined) + T = T
  3. error{A} + error{B} = error{A,B}
  4. error{A} + error{B}!T = error{A,B}!T
  5. error{A} + T = error{A}!T
  6. error{A}!T + error{B}!T = error{A,B}!T
  7. …(draw the rest of the owl)…

The noreturn + T resolution pair has the highest precedence of all, so therefore error{A} + noreturn favors error{A} over error{A}!noreturn. This is easy to reason with and does not feel contradictory or unexpected to me.

Had the order of precedence been swapped around and error sets prioritized, then the following code

fn f(x: u32) E {
    return switch (x) {
        1 => error.A,
        2 => error.B,
        else => unreachable,
    };
}

might have resulted in a compile error like expected type 'error{A,B}', found 'error{A,B}!noreturn'. I think this outcome would have felt less intuitive than status quo.

1 Like

I’m wondering whether or not noreturn should be allowed to use as the payload type of error unions. Currently, I haven’t found a meaningful use case of ErrSet ! noreturn.

Theres plenty of functions that make sense as !noreturn