-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve block-local error handling #5610
Comments
I realized after posting this that my code is actually leaking memory if the fn addArgument(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource, arg: ?[*:0]u8) callconv(.C) void {
const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource)));
const id = c.wl_resource_get_id(wl_resource);
const allocator = self.server.allocator;
const owned_slice = std.mem.dupe(allocator, u8, std.mem.span(arg.?)) catch {
c.wl_client_post_no_memory(wl_client);
return;
};
// errdefer allocator.free(owned_slice); <-- wouldn't get called as no error is returned
// say there was a third call that could also OOM
const owned2 = std.mem.dupe(allocator, u8, std.mem.span(arg.?)) catch {
c.wl_client_post_no_memory(wl_client);
allocator.free(owned_slice); // instead we must manually clean up
return;
};
self.args_map.get(id).?.value.append(owned_slice) catch {
c.wl_client_post_no_memory(wl_client);
allocator.free(owned_slice); // instead we must manually clean up
allocator.free(owned2); // both slices, wish i could used errdefer here
return;
};
} With the proposed catch blocks, we can use fn addArgument(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource, arg: ?[*:0]u8) callconv(.C) void {
const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource)));
const id = c.wl_resource_get_id(wl_resource);
const allocator = self.server.allocator;
{
const owned_slice = try std.mem.dupe(allocator, u8, std.mem.span(arg.?));
errdefer allocator.free(owned_slice); // runs if the block is broken with an error
const owned2 = try std.mem.dupe(allocator, u8, std.mem.span(arg.?));
errdefer allocator.free(owned2); // runs if the block is broken with an error
try self.args_map.get(id).?.value.append(owned_slice);
} catch {
c.wl_client_post_no_memory(wl_client);
return;
};
} I think this strengthens the argument for the proposed syntax. |
It's a little more typing, but there is another option not considered here, which is the desugared form of your suggestion. Unfortunately it doesn't work. The errdefer clauses here generate no code, I guess because the block doesn't actually return. fn addArgument(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource, arg: ?[*:0]u8) callconv(.C) void {
const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource)));
const id = c.wl_resource_get_id(wl_resource);
const allocator = self.server.allocator;
@as(error{OutOfMemory}!void, blk: {
const owned_slice = std.mem.dupe(allocator, u8, std.mem.span(arg.?)) catch |err| break :blk err;
errdefer allocator.free(owned_slice); // never runs :(
const owned2 = std.mem.dupe(allocator, u8, std.mem.span(arg.?)) catch |err| break :blk err;
errdefer allocator.free(owned2); // never runs :(
self.args_map.get(id).?.value.append(owned_slice) catch |err| break :blk err;
}) catch {
c.wl_client_post_no_memory(wl_client);
return;
};
} Having the
With these changes, there's no longer anything special about the catch block. It's just a block that evaluates to an error union and a normal catch clause to handle the error, plus some syntactic sugar to break with an error and a guarantee that errdefers will run when doing so. |
I think your changes make a lot of sense, they are much more tightly defined than the original proposal and result in more explicit code without becoming cumbersome. They also stay closer to the current semantics of the language. With your proposed amendments we get the following which I find quite nice: fn addArgument(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource, arg: ?[*:0]u8) callconv(.C) void {
const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource)));
const id = c.wl_resource_get_id(wl_resource);
const allocator = self.server.allocator;
blk: {
const owned_slice = try_local :blk std.mem.dupe(allocator, u8, std.mem.span(arg.?));
errdefer allocator.free(owned_slice);
const owned2 = try_local :blk std.mem.dupe(allocator, u8, std.mem.span(arg.?));
errdefer allocator.free(owned2);
try_local :blk self.args_map.get(id).?.value.append(owned_slice);
} catch {
c.wl_client_post_no_memory(wl_client);
return;
};
} |
How do you get case-specific error handling on one of the calls, and also invocation of the common error handler? Or get case-specific error handling that aborts the block but doesn't invoke the common handler? In #5421 I called for a reconsideration of Zig error handling semantics, to find ones simpler and more flexible. The above proposes an additional mode, further complicating them. Also, a more general purpose (and perhaps simpler) path to the above feature is to allow non-local returns within nested anonymous functions. I imagine that's been considered for Zig already? |
Both of these are possible, with @SpexGuy's modifications the changes to the semantics of zig are minimal. fn addArgument(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource, arg: ?[*:0]u8) callconv(.C) void {
const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource)));
const id = c.wl_resource_get_id(wl_resource);
const allocator = self.server.allocator;
blk: {
const owned_slice = std.mem.dupe(allocator, u8, std.mem.span(arg.?)) catch |err| {
// case-specific error handling
break :blk err; // Break the block with the err, the catch on the block is invoked.
}
errdefer allocator.free(owned_slice);
const owned2 = std.mem.dupe(allocator, u8, std.mem.span(arg.?)) catch |err|
// do something
break :blk; // Break the block with value void, catch on the block is not invoked.
}
errdefer allocator.free(owned2);
try_local :blk self.args_map.get(id).?.value.append(owned_slice);
} catch {
c.wl_client_post_no_memory(wl_client);
return;
};
} |
catch
blocks
The new title isn't very readable. Maybe that should be the summary in the text? The title is something like "let catch clause apply to a block" or "shared error handlers with try_local and catch". If |
|
Ah, but that doesn't apply to if/else/for blocks? If Zig gets |
No, it does apply. There's nothing special about if/else/for/while blocks in zig. |
As an alternative to introducing a new keyword for catch |e| return e; but when given a label as in catch |e| break :blk e; I'm not convinced that this is a good idea as it introduces some inconsistency to |
Sorry this is off topic, but if
Apologies for any syntax glitches. |
Correct, that pattern doesn't behave the same way in Zig. This is the equivalent Zig code: const is_default_file = i == null;
if (is_default_file) { i = try open(...); }
defer if (is_default_file) i.close();
// use i |
I am all for overloading |
I have one issue with (simply) overloading fn doSomething() !void {
const possible_value = blk: {
var value = try :blk getValue();
try modifyValue(&value); // whoops! forgot the :blk label
break :blk value;
};
if (possible_value) |val|
try someOperation(val);
else |err|
try std.io.getStdErr().writer().print("Failed: {}", .{err});
} Inside the try :fn something(); // would conflict with a block named `fn:`
try fn something(); // it would be strange to put a keyword there but maybe it makes sense
try @fnBlock() something(); // a builtin? Don't know if it would make sense since it's control flow management |
Hmmm. Good point. A similar also appeared in another proposal (I forget which) at a recent design meeting. The best solution considered for that problem has an analogue here: |
Would it be then The syntax with the try(fn) maybe();
try(:blk) maybe(); Could be better? I think it at least shows that the |
No, the analogous syntax for explicit function would be Thinking about it now though, it's ambiguous -- Every option considered has the potential to either introduce ambiguity or break existing code silently, except the original |
Motivation
Consider the following example from my code:
The error handling here requires code duplication if I want to keep things in one function. I can't preform the error handling at the callsite either since this function is a callback being passed to a C library. The amount of code being duplicated for error handling is not too large in this case, but it is easy to imagine a case in which it could grow larger. My only option to avoid this code duplication is to create a helper function:
Doing this for every callback I wish to register gets annoying and feels like unnecessary boilerplate.
Proposal
EDIT: the original proposal isn't great, see @SpexGuy's comment for the much better revised version.
To summarize the revised proposal:
errdefer
to mean "runs if the containing block yields an error to any outside block". Both returning and breaking with error count as yielding an error to an outside block, as long as the evaluated type of the outside block is an error union. (this is to stay consistent with the current behavior if a function returns an error value but not an error union).try_local
, used astry_local :blk foo();
which is sugar forfoo() catch |e| break :blk e;
. As mentioned in a comment below, thetry
keyword could instead be overloaded to have the described behavior if given a label as intry :blk foo()
.I think both 1 and 2 are unambiguously good changes. The addition of a new keyword for 3. may not be worth it, though it would certainly be welcome in the examples I've presented here.
With the revised proposal, the above example would look like this:
Original proposal
Doing this for every callback I wish to register gets annoying and feels like unnecessary boilerplate. As an alternative, I propose allowing
catch
to be used on blocks, which would allow expressing this control flow like so:Catch blocks have simple rules:
Consider this an alternative to #5421 solving some of the same issues in a way that is in my opinion more consistent with the rest of the language.
Drawbacks
The major disadvantage to this proposal is that it changes the behavior of the
try
keyword based on the scope it is used in.try
is currently syntactic sugar forcatch |err| return err;
and changing this complicates things. However I think the proposed semantics oftry
are straightforward enough.The text was updated successfully, but these errors were encountered: