-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
result location: ability to refer to the return result location before the return
statement
#2765
Comments
One possible solution, noted in #287 (comment) is:
|
This comment has been minimized.
This comment has been minimized.
This is important, because you I don't think you can make this work all the time without phi nodes. |
discussed with @SpexGuy and @andrewrk . The concrete proposal is:
This proposal only applies to functions where the type of the named variable is exactly equal to the return type. See also #2761 for when the variable is of type |
Another idea to consider would be to introduce "return value reference semantics". The difference with this would be that we'd be introducing a way to explicitly state the intention of setting the return value before we return from the function rather than relying on the optimizer to elide a temporary copy of the return value. The problem with relying on an optimization is that:
For example, the proposed semantics of eliding this second copy of the return value may cover some cases but miss out on others. This would mean that some code that is able to be optimized won't be, but not because the code can't be optimized, rather, it's the result of an inadequate optimization design. This means the code needs to be structured in a way to conform to the optimization design, which likely only supports a subset of possible scenarios that could be optimized. Futhermore, making code fit this optimization design is difficult because it requires dealing with the 2 difficult issues listed above. By making the feature explicit, code will be forced to be written in such a way as to accommodate the desired effect. Making this intention explicit means that all possible cases will be supported and the developer knows that their intention is being communicated. As for the syntax, we could use builtin functions for this such as fn foo() Point {
@returnRef().* = bar();
}
...
const foo = init: {
@breakRef("init").* = bar();
break :init;
}; Note that once a return value has been set, the code can return/break from the function/scope without specifying a value because the value has already been set. The compiler can use normal code flow analysis to be sure that the return value has been set to know when it's ok to omit the return/break value. Also note that with these builtin return/break value references, the references can be explicitly forwarded to other functions. For example: pub fn foo() [3000]u8 {
bar(@returnRef()); // Note that @TypeOf(@returnRef()) is *[3000]u8
}
pub fn bar(s: []u8) void {
//...
} P.S. also note that adding explicit support for accessing the return value doesn't mean we couldn't also add the proposed optimization, both mechanisms have their uses and could exist alongside each other |
Or a slight variation: Add a return block which is the only place for return location pointers to be accessed
Instead of doing what you have to do today:
The return block would contain statements as usual but upon reaching the end of the |
What about giving the result location a name like any other variable? fn makeArray() result: []u8 {
fillArray(result);
}
fn fillArray(array: []u8) { ... } Of course, that becomes awkward for the common case of returning a value directly, so return-with-value should be retained as shorthand to keep existing code working as it already does today. fn add(x: i32, y: i32) result: i32 {
return x + y;
// is shorthand for:
result = x + y;
return;
} Also in the common case, where the fn add(x: i32, y: i32) i32 {
return x + y;
// is shorthand for:
<unnamed_result_location> = x + y;
return;
} This would extend nicely to blocks. Like functions, blocks would get their own result variables: const a = blk: {
fillArray(blk);
};
const b = blk: {
break :blk 1;
// is shorthand for:
blk = 1;
break :blk;
}; Although if this feature existed it might be more natural to write the above like this: const b = blk: {
blk = 1;
}; |
I definitely think that's interesting, but then with the result identifier as a pointer to the return value location. |
This commit does two things which seem unrelated at first, but, together, solve a miscompilation, and potentially slightly speed up compiler perf, at the expense of making #2765 trickier to implement in the future. Sema: avoid returning a false positive for whether an inferred error set is comptime-known to be empty. AstGen: mark function calls as not being interested in a result location. This prevents the test case "ret_ptr doesn't cause own inferred error set to be resolved" from being regressed. If we want to accept and implement #2765 in the future, it will require solving this problem a different way, but the principle of YAGNI tells us to go ahead with this change. Old ZIR looks like this: %97 = ret_ptr() %101 = store_node(%97, %100) %102 = load(%97) %103 = ret_is_non_err(%102) New ZIR looks like this: %97 = ret_type() %101 = as_node(%97, %100) %102 = ret_is_non_err(%101) closes #15669
This commit does two things which seem unrelated at first, but, together, solve a miscompilation, and potentially slightly speed up compiler perf, at the expense of making #2765 trickier to implement in the future. Sema: avoid returning a false positive for whether an inferred error set is comptime-known to be empty. AstGen: mark function calls as not being interested in a result location. This prevents the test case "ret_ptr doesn't cause own inferred error set to be resolved" from being regressed. If we want to accept and implement #2765 in the future, it will require solving this problem a different way, but the principle of YAGNI tells us to go ahead with this change. Old ZIR looks like this: %97 = ret_ptr() %101 = store_node(%97, %100) %102 = load(%97) %103 = ret_is_non_err(%102) New ZIR looks like this: %97 = ret_type() %101 = as_node(%97, %100) %102 = ret_is_non_err(%101) closes #15669
This is mostly tangential curiosity, but having an explicit syntax for result of the functions gives us require/ensure from design-by-contract for free: fn sqrt(x: i32): i32 {
assert(x >= 0); // precondition
defer assert(@result() * @result() <= x) // postcondition
} |
Perhaps this would provide the necessary piece to implement an ergonomic Today, hand rolled fn Result(comptime T: type, comptime E: type) type {
return union (enum) { ok: T, err: E };
}
fn future_maybe(alloc: std.mem.Allocator) Result(i32, []const u8) {
var mylist = std.ArrayList(i32).init(alloc);
mylist.append(2);
// this is basically implementing errdefer yourself on top of a custom union
defer if (@return() == .err) mylist.deinit();
// return your error pretty much directly
erroringFunction() catch return .{ .err = "oh no" };
return .{.ok = 10};
}
// here is what I do today:
fn today(alloc: std.mem.Allocator) Result(i32, []const u8) {
var result: Result(i32, []const u8) = .{.err = "no valid result yet"};
var mylist = std.ArrayList(i32).init(alloc);
mylist.append(2);
defer if (result == .err) mylist.deinit();
erroringFunction() catch {
result = .{ .err = "oh no" };
return result;
}
result = .{.ok = 10};
return result;
} |
We could also reuse the existing capture syntax but instead at a function-level instead of for the fn add(a: u32, b: u32) u32 |*c| { // must be pointer capture
c.* = a + b;
}
// bad:
fn add(a: u32, b: u32) u32 |*c| {
if (a == 0 and b == 0) {
return a + b;
// error: function definitions with captured return addresses must not have return arguments
}
c.* = a + b;
}
// good:
fn add(a: u32, b: u32) u32 |*c| {
if (a == 0 and b == 0) {
c.* = 0;
return;
}
c.* = a + b;
} Also, This could be further extended at some point to support return type semantics like fn intCast(a: anytype) anytype |*result| {
const Target = @typeInfo(@TypeOf(result)).Pointer.child;
result.* = switch (Target) {
// ...
};
} |
all suggestions that the function signature be invaded by an implementation detail are wrongful, imo. |
rather than adding syntax what if Zig decided that that wasnt the case and that the value returned is put into the result location intrinsically even if it is put into an intermediate. if we additionally have: const p = foo(); in practice that would mean |
This issue is split from #287.
Now that we have result location semantics, the following code does not introduce an intermediate value with a copy:
Previously, the code
return bar()
would introduce an extra copy, so the body of the functionfoo
would needlessly copy the point before returning it. This copying would happen at every expression, recursively when the type is an aggregate type (such asstruct
). Now that the result location mechanism is merged into master, you can see that thefoo
function does not introduce an extra copy:However, if you capture the result in a variable and then return the variable, there is an intermediate value - the
result
variable - which is copied at thereturn
statement:Now there is a copy, because the Result Location of
bar()
is theresult
local variable, rather than the return result location:This issue is to make it so that there is a way to refer to the result location, and even call methods on it, before returning it, all without introducing an intermediate value.
For the issue about getting rid of intermediate values when optionals and error unions are involved, see #2761.
The text was updated successfully, but these errors were encountered: