Open andrewrk opened 5 years ago
One possible solution, noted in https://github.com/ziglang/zig/issues/287#issuecomment-440488808 is:
Zig will detect when all control flow paths end with return foo;, where foo is the same in all the return expressions, and is declared in a way that allows it to reference the return value. In this case the variable declaration will reference the return value rather than be a stack allocation. The detection doesn't have to be very advanced, just good enough that it's easy to get the detection to happen when you are trying to.
[EDIT: changed my mind on this, I don't think these concerns matter, hiding comment]
re: a Zig "detection method", it's not clear to me how to avoid partial result-location updates if error paths exist:
fn barThatCanError() !Point {
return Point{
.x = 1,
.y = try getSomeValue(),
};
}
// contract: if this function fails it shall not modify result location
fn foo() !Point {
var result = try barThatCanError(); // BAD: var.x may be modified
result.setText("hello from foo");
return result;
}
an explicit syntax:
// contract: if this function fails it shall not modify result location
fn foo1() Point {
@result() = bar(); // OK: cannot fail
@result().setText("hello from foo");
return @result();
}
// contract: if this function fails it shall not modify result location
fn foo2() !Point {
var result = try barThatCanError(); // OK: we never modify result location
result.setText("hello from foo");
return result;
}
// contract: if this function fails it MAY leave result location in an undefined state
fn foo3() !Point {
@result() = try barThatCanError(); // OK: because our contract says so
@result().setText("hello from foo");
return @result();
}
The detection doesn't have to be very advanced, just good enough that it's easy to get the detection to happen when you are trying to.
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:
return
statement in a function refers to the same named variable, then that variable's address is the result location.break
statement values and the result location of a block.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 T
and the return type is ?T
or !T
.
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:
1) it's difficult to know how to make code work with the optimization 2) it's difficult to know when the optimization has been applied
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 @returnRef()
and/or @breakRef("mylabel")
.
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
const T = struct {
alloc: std.mem.allocator,
buffer: [128]u8,
fn init() T {
return {
@returnPtr().allocator = std.mem.fixedBufferAllocator(&@returnPtr().buffer);
};
}
};
var result = T.init();
Instead of doing what you have to do today:
var result: T = undefined;
result.init(); // `init` takes a `self` pointer
The return block would contain statements as usual but upon reaching the end of the return
block, the function would return.
Alternative syntax with return blocks (but same semantics)
return |*return_location| {
// ...
};
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 return
keyboard is used, the name is not used and can be omitted. Now we have the current syntax back, just with more flexible building blocks underneath.
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);
};
break
would become shorthand similar to return
:
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 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 Result
generic (error payloads) that is not intrinsic to the language. (See https://github.com/ziglang/zig/issues/2647)
Today, hand rolled Result
is unergonomic because it doesn't work with errdefer
so requires a lot of boilerplate to handle properly. If you can reference the return address though, you can remove a lot of the boilerplate though.
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 return
statement (N00byEdge's comment).
fn add(a: u32, b: u32) u32 |*c| { // must be pointer capture
c.* = a + b;
}
return
statements must not have an argument:
// 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, matklad's sqrt() implementation using this syntax: Removed. This could introduce undefined behavior if an error was returned after the defer
statement.
This could be further extended at some point to support return type semantics like @intCast
:
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. grep fn*
and friends receive noise to handle.
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:fn foo() Point { const result = bar(); return result; }
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 assert(&p == &result)
holds true
?
@nektro It looks like that's what the original proposal is https://github.com/ziglang/zig/issues/287#issuecomment-440488808
Zig will detect when all control flow paths end with
return foo;
, wherefoo
is the same in all the return expressions, and is declared in a way that allows it to reference the return value. In this case the variable declaration will reference the return value rather than be a stack allocation. The detection doesn't have to be very advanced, just good enough that it's easy to get the detection to happen when you are trying to.
Idk what changed.
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.