Zig defer Patterns

26 Likes

Great post as always, thanks!

I’ve seen all of these except this here golden nugget - peak Zig for sure :joy: Nice find!

errdefer comptime unreachable
7 Likes

The Post Increment can be turned into Post Decrement, which is really handy for peak on iterators:

fn peak(self: *Iterator) ?Item {
    defer self.i -= 1;
    self.i += 1;
    if (self.i >= self.items.len) return null;
    return self.items[self.i];
}

The error logging section could mention that you can capture the error with errdefer |err|, which enables even more descriptive log messages:

const port = port: {
    errdefer |err| std.log.err("failed to read the port number: {!}", .{err});
    var buf: [fmt.count("{}\n", .{maxInt(u16)})]u8 = undefined;
    const len = try process.stdout.?.readAll(&buf);
    break :port try fmt.parseInt(u16, buf[0 .. len -| 1], 10);
};

(errdefer with captures is currently not mentioned explicitly in the language reference, only in the grammar, but it works and can be very useful when you need it.)

12 Likes

I guess we need to add that to Captures and Payloads

3 Likes

Whoaaaaaaaaaa, didn’t know that that’s possible, thanks!

6 Likes

Done, added that errdefer gem, too.

Perhaps unreachable deserves a doc listing its uses/misuses.

Apart from the one above, I’ve seen: catch unreachable to discard errors, unreachable; to indicate that a block never reaches its end, else => unreachable, to handle impossible switch cases. Are there any other places it can pop up?

2 Likes

Thank you!

Good idea!

Another thing I found interesting is that assert is implemented as:

pub fn assert(ok: bool) void {
    if (!ok) unreachable; // assertion failure
}
1 Like

Yeah, it’s even among the langref examples, which together do a great job at illustrating how unreachable emits a panic at runtime in Debug and ReleaseSafe modes (acts same as @panic("reached unreachable code")), while comptime unreachable results in an error at compile-time (acts same as @compileError("reached unreachable code")).

https://ziglang.org/documentation/0.11.0/#unreachable

2 Likes

What the langref doesn’t clarify is what happens if you do hit unreachable in ReleaseFast or ReleaseSmall modes. Looks like it might be UB, not sure.

Well, I guess unreachable does deserve its own doc here :grin:

1 Like

const next_index = self.i + 1 avoids the write. Using defer to roll-back state unconditionally is error prone in most cases where you can avoid mutating state of the iterator.

3 Likes

Edit: This has changed - there’s a good list of undefined behavior here: Documentation - The Zig Programming Language

I’m glad you brought this up - we should open a new thread about this because as far as I know, it’s undefined behaviour.

I brought up this exact problem almost a year ago and didn’t make much ground (I’m talking about finding documentation about undefined behaviour more generally here). It’s no ones fault - there just isn’t a lot of material that I was able to find that works as an exhaustive reference for undefined behaviour (and I don’t imagine there will be one until we get closer to 1.0).

That may have changed, but imo, it’s totally reasonable. Today’s undefined behaviour (especially where it’s not as obvious as unreachable) could change as the compiler changes - I don’t imagine they’ll spend a lot of time documenting that for us in the interim.

Anyhow, we should probably dedicate a new thread to that if we want to keep exploring the subject. Also, thanks for updating the doc!

2 Likes

Design by Contract with defer + assert is awesome!

True in this example, purposely made very simple to keep it conise. But if advancing the iterator involves mucho more than just incrementing a usize (usually by calling a method that mutates several things,) using a defer with a block to undo it all at once is less error-prone than manually undoing it all at each possible exit branch.

1 Like

so can’t do something like this right?

errdefer comptime unreachable;

// can't do this? anymore after right?

try some_fn_may_fail();

but something like this should be allowed?
some_fn_may_fail() catch unreachable;

That is Unreachable - error discarding misuse.

Instead use some_fn_may_fail() catch @panic("message");
Or make sure that it is some_fn_never_fails_ensured_through_invariants() catch unreachable; and ideally you would have asserts / tests that make sure these invariants work.

1 Like

hmm i see, so basically errdefer comptime unreachable; ensures that after this nothing else generates any sort of code which from the language prospective returns error? by language prospective? correct me if i am wrong, these errors are kind of doesn’t have any special meaning when compiled to machine code right, so they’re like special some sort of compiler intrinsic which tells the code generator to generate some error handling machine code based on the error type? perhaps i don’t have an idea how these errors are handled under the hood.

They are implemented with error sets, which can be thought of as special enums.
Take a look at this: Language Documentation - Error Set Type

Similar to how errdefer can capture the error, I’m curious if people have considered what new patterns might be enabled by allowing defer to capture the return value (or block expression value for block-level defers)?

4 Likes

Within a function that changes some state, you could also use defers to debug the state with debug prints. Something akin to this:

fn updateState(self: *State) {
  std.debug.print("before: {}\n", .{self});
  defer std.debug.print("after: {}\n", .{self});
  // ... code to update the state ...
}
2 Likes