Should the `foo` function compile in the example code compile okay?

Now the foo function compiles okay, but should it?
And should the baz function fail to compile instead?

Or should the bar function compile okay?

pub fn foo(_: anyerror!u8) void {} // compiles okay

pub fn bar(x: anyerror!u8) void {
    _ = x; // error: error union is discarded
}

pub fn baz(x: anyerror!u8) void {
    _ = &x; // compiles okay
}

pub fn main() void {
    const v: anyerror!u8 = error.A;
    foo(v);
    bar(v);
    baz(v);
}

Your baz example made me a bit curious because there’s not a lot we can do to observe the value of an error, but we can observe their addresses, which I hadn’t considered before.

const std = @import("std");
pub fn errAddr(what: []const u8, x: *const anyerror!u8) void {
    _ = x.* catch 5;
    std.debug.print("{s}:\t{*}\n", .{ what, x });
}
// inserts space so it lines up with errAddr() which inevitably outputs a
pub fn valAddr(what: []const u8, x: *const u8) void {
    std.debug.print("{s}:\t{s}{*}\n", .{ what, [_]u8{' '} ** "anyerror!".len, x });
}
pub fn main() !void {
    errAddr("static constant value", &42);
    errAddr("static constant error", &error.A);

    const x: error{foo}!u8 = error.foo;
    const X: u8 = 42;
    valAddr("named constant value", &X);
    errAddr("named constant error", &x);

    var y: error{foo}!u8 = 2;
    errAddr("error{foo}!u8 with value", &y);
    valAddr("address of that value   ", &try y);
    std.debug.print("value error offset:   \t\t{d}\n", .{@intFromPtr(&try y) - @intFromPtr(&y)});
    y = error.foo;
    errAddr("error{foo}!u8 with error", &y);

    std.debug.print("sizeof( x): {d}\n", .{@sizeOf(@TypeOf(x))});
    std.debug.print("sizeof(u8): {d}\n", .{@sizeOf(u8)});
}

$ zig run test.zig
static constant value:  anyerror!u8@126f866
static constant error:  anyerror!u8@126f89e
named constant value:            u8@126f8dd
named constant error:   anyerror!u8@126f8a4
error{foo}!u8 with value:       anyerror!u8@7fff58866e34
address of that value   :                u8@7fff58866e36
value error offset:             2
error{foo}!u8 with error:       anyerror!u8@7fff58866e34
sizeof( x): 4
sizeof(u8): 1
$ zig version
0.16.0

It seems zig really is true to its idea of treating errors as values, with the value of an error (if present) enclosed in the error itself.

pub fn bar(x: anyerror!u8) void {
    _ = x; // error: error union is discarded
}

I feel like it all boils down to how permissive _ is. It’s weird for it to accept an error as a function parameter, but not assignment.

2 Likes

Personally, I think error union parameters of a function is not necessary to be handled in the function. It is the duty of its caller function to handle it. That means the bar function should also compile okay.

There is a difference between the way the thing is discarded in foo and bar.
In bar it is the general discard syntax. It’s a valid compilation error, because we (fortunately) aren’t able to just discard errors. They need to be explicitly discarded. The compiler could of course know that this directly comes from a function parameter instead of being the return value of a function, but at least to me this would be just an unnecessary special case that bloats the compilers code and will likely lead to bugs.
In foo the function parameter is discarded. This is different from above because there is just this case. I’d imagine it could be implemented in such a way that it doesn’t even look at the type, but this is just speculation.
I think baz should also be a compile error but maybe I’m missing something.

2 Likes

This one also compiles okay:

pub fn bar2(x: anyerror!u8) void {
    var y = x;
    y = y;
}

Alternatively:

pub fn ignore(x: anyerror!u8) void {                                                              
    _ = x catch {};                                                          
}

But it’s much better to do either:

pub fn check(x: anyerror!u8) void {                                                               
    _ = x catch unreachable;                                                    
}

pub fn propagate(x: anyerror!u8) !void {                                                          
    _ = try x;                                                                                    
}

So that errors don’t go unnoticed.

To stay true to Zig’s Style Guide I’d suggest assert instead of check, and to follow the std naming expect instead of propagate.

Keep in mind that unreachable isn’t meant to be caught outside of safe builds. If you want an error to stop the process, use catch @panic("...") instead.

  1. The section you linked is called ā€œDoc comment guidanceā€.
  2. I’m using these names because I don’t like calling things foo() and bar() if I can help it.
  3. Try compiling:
const std = @import("std");
const assert = std.debug.assert;

pub fn main(init: std.process.Init) void {
    _ = init;
    return;
}

fn assert(x: anyerror!u8) void {
    _ = x catch unreachable;
}

Though I appreciate the sentiment. It really does help when things are named well, rather than a gaggle of InternalStateManagerInterface() or puc_acquire or vsnprintf().

EDIT:

No, very specifically don’t do that! You will lose debugging information!

https://ziglang.org/documentation/master/#Error-Return-Traces

catch unreachable tells you where the error was originally ā€˜thrown’, catch @panic("...") only tells you the stack trace of the call to @panic()

The ā€œDoc comment guidanceā€ subsection is located under the section ā€œStyle Guideā€. I specifically linked to the subsection that suggest using assert as the vocabulary for checked illegal behavior.

I don’t know how you’re managing your namespaces, but if you actually encounter this case you can name it assertNoError instead.

You won’t get debug information if you hit undefined behavior. If you want debug information AND catch the error in both safe and unsafe builds, you’ll have to use std.debug.dumpErrorReturnTrace, there’s no way around it.

ā€œDoc commentsā€ refers specifically to the comments that start with three slashes, and are automatically turned into documentation.

e.g. for std.hash_map.HashMap.capacity

/// Returns the number of total elements which may be present before it is
/// no longer guaranteed that no allocations will be performed.
pub fn capacity(self: Self) Size {
    return self.unmanaged.capacity();
}

A doc comment with ā€˜assumed’.

/// Delete the entry with key pointed to by key_ptr from the hash map.
/// key_ptr is assumed to be a valid pointer to a key that is present
/// in the hash map.
///
/// TODO: answer the question in these doc comments, does this
/// increase the unused capacity by one?
pub fn removeByPtr(self: *Self, key_ptr: *K) void {
    self.unmanaged.removeByPtr(key_ptr);
}

A doc comment with ā€˜asserts’. Note the function name.

/// Asserts there is enough capacity to store the new key-value pair.
/// Clobbers any existing data. To detect if a put would clobber
/// existing data, see `getOrPutAssumeCapacity`.
pub fn putAssumeCapacity(self: *Self, key: K, value: V) void {
    if (@sizeOf(Context) != 0)
        @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call putAssumeCapacityContext instead.");
    return self.putAssumeCapacityContext(key, value, undefined);
}

If you think there’s a mistake, you should send a patch to https://codeberg.org/ziglang/zig/pulls

Thank you for pointing it out. I don’t know how I never noticed. It makes absolutely no sense to me why those ā€œassumeā€ functions are named in a manner completely contradictory with the documentation.

1 Like