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.

2 Likes

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.

4 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.

2 Likes

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

1 Like

Except that !noreturn indicates that a function may never return, !noreturn can be replaced with anyerror, right?

It looks a function may never return can return any type. So whether or not a function will never return has no relation to whether or not the function’s return type includes noreturn.

It may never return unless an error occurs, that is quite useful.

But no, it cannot be replaced by anyerror because 1) an inferred error set is not the same as “any error ever possible anywhere in the program”
2) a return type of just anyerror(or a defined error set), indicates the function will always return with an error

but having a noreturn type to indicate that is very useful to the caller because 1) its right there in the signature, no need to read docs or src to know that 2) it tells the compiler the return branch is inherently unreachable, enforces this by disallowing non error return in the function, and has optimisation and type system ramifications. None of your examples would compile if noreturn didn’t exist.

2 Likes

One example where Error!noreturn would make sense is std.process.replace. This function replaces the current process with another one and doesn’t return (to the original process) when successful. If it fails, it returns ReplaceError.

It’s currently not declared like that for some reason, but I suspect it’s just an oversight, because the doc comment explicitly states this behaviour.

6 Likes