Is anyerror valid when used with function pointers?

Consider the following function:

fn runFn(fn: *const fn () anyerror) []u8 {
    return @errorName(fn());
}

Since an anyerror is a global enum of sorts, can this ever produce an incorrect error name when a runtime-known function is passed to runFn?

Yes but due to how things are parsed/interned there are some surprises here… here’s an example…

// all have the same value name
const ErrorA = error { Test };
const ErrorB = error { Test };
const ErrorC = error { Test };
const ErrorD = error { Test };

// infer two sets A and B
fn inferred(b: bool) anyerror {
    return if (b) ErrorA.Test else ErrorB.Test;
}

// infer two sets C and D
fn inferred2(b: bool) anyerror {
    return if (b) ErrorC.Test else ErrorD.Test;
}

pub fn main() !void {

    var ptr: *const fn(bool) anyerror = inferred;
    
    // A and B
    const inf1 = ptr(true);
    const inf2 = ptr(false);

    ptr = inferred2;
    
    // D and C
    const inf3 = ptr(true);
    const inf4 = ptr(false);

    std.debug.print(
        \\
        \\ErrorA           - ErrorName: {s}, Code: {}
        \\ErrorB           - ErrorName: {s}, Code: {}
        \\ErrorC           - ErrorName: {s}, Code: {}
        \\ErrorD           - ErrorName: {s}, Code: {}
        \\Inferred(true)   - ErrorName: {s}, Code: {}
        \\Inferred(false)  - ErrorName: {s}, Code: {}
        \\Inferred2(true)  - ErrorName: {s}, Code: {}
        \\Inferred2(false) - ErrorName: {s}, Code: {}
        \\
        , .{
        @errorName(ErrorA.Test), @intFromError(ErrorA.Test),
        @errorName(ErrorB.Test), @intFromError(ErrorB.Test),
        @errorName(ErrorC.Test), @intFromError(ErrorC.Test),
        @errorName(ErrorD.Test), @intFromError(ErrorD.Test),
        @errorName(inf1), @intFromError(inf1),
        @errorName(inf2), @intFromError(inf2),
        @errorName(inf3), @intFromError(inf3),
        @errorName(inf4), @intFromError(inf4),
    });
    //// try going the other way...

    const name_a: []const u8 = @errorName(ErrorA.Test);
    const name_b: []const u8 = @errorName(ErrorB.Test);
    const name_c: []const u8 = @errorName(ErrorC.Test);
    const name_d: []const u8 = @errorName(ErrorD.Test);
    const name_e: []const u8 = @errorName(inf1);
    const name_f: []const u8 = @errorName(inf2);
    const name_g: []const u8 = @errorName(inf3);
    const name_h: []const u8 = @errorName(inf4);

    std.debug.print(
        \\
        \\ErrorA           - Address: {*}
        \\ErrorB           - Address: {*}
        \\ErrorC           - Address: {*}
        \\ErrorD           - Address: {*}
        \\Inferred(true)   - Address: {*}
        \\Inferred(false)  - Address: {*}
        \\Inferred2(true)  - Address: {*}
        \\Inferred2(false) - Address: {*}
        \\
        , .{
            name_a.ptr,
            name_b.ptr,
            name_c.ptr,
            name_d.ptr,
            name_e.ptr,
            name_f.ptr,
            name_g.ptr,
            name_h.ptr,
    });
}

On my system, this outputs:

ErrorA           - ErrorName: Test, Code: 1
ErrorB           - ErrorName: Test, Code: 1
ErrorC           - ErrorName: Test, Code: 1
ErrorD           - ErrorName: Test, Code: 1
Inferred(true)   - ErrorName: Test, Code: 1
Inferred(false)  - ErrorName: Test, Code: 1
Inferred2(true)  - ErrorName: Test, Code: 1
Inferred2(false) - ErrorName: Test, Code: 1

ErrorA           - Address: u8@10c8c06
ErrorB           - Address: u8@10c8c06
ErrorC           - Address: u8@10c8c06
ErrorD           - Address: u8@10c8c06
Inferred(true)   - Address: u8@100e840
Inferred(false)  - Address: u8@100e840
Inferred2(true)  - Address: u8@100e840
Inferred2(false) - Address: u8@100e840

So what’s going on here?

You can see that they all have the same integer value, but the 4 explicit errors have the same address and the 4 inferred errors all have the same address too. However, the explicit vs inferred errors are not the same address.

We can actually see in the InternPool.zig file the following code…

    error_set_type: ErrorSetType,
    /// The payload is the function body, either a `func_decl` or `func_instance`.
    inferred_error_set_type: Index,

So they’re definitely being handled as unique cases.

Anyhow, there is a difference between how the memory is segmented, but anything that goes into the global error set gets pushed into that pool of memory.

Basically yes, you are getting the names from the same memory pool that needs to find the correct segment via that pool. Any time you add an additional error to the pool, the pool grows and has another value it needs to represent.

Last example - you can see that when I change the value for ErrorD.Test to ErrorD.Best:

ErrorA           - ErrorName: Test, Code: 1
ErrorB           - ErrorName: Test, Code: 1
ErrorC           - ErrorName: Test, Code: 1
ErrorD           - ErrorName: Best, Code: 2
Inferred(true)   - ErrorName: Test, Code: 1
Inferred(false)  - ErrorName: Test, Code: 1
Inferred2(true)  - ErrorName: Test, Code: 1
Inferred2(false) - ErrorName: Best, Code: 2

ErrorA           - Address: u8@10c8c16
ErrorB           - Address: u8@10c8c16
ErrorC           - Address: u8@10c8c16
ErrorD           - Address: u8@10c8c1b
Inferred(true)   - Address: u8@100e864
Inferred(false)  - Address: u8@100e864
Inferred2(true)  - Address: u8@100e864
Inferred2(false) - Address: u8@100e8e4

Each unique name gets it’s own memory in the global error set (and the explicit sets). Same results for ReleaseFast. A function pointer that returns from the global error set has the same behaviour as using a function directly (same memory pool). Hope that helps!

2 Likes