ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
35.37k stars 2.58k forks source link

result location: ability to refer to the return result location before the `return` statement #2765

Open andrewrk opened 5 years ago

andrewrk commented 5 years ago

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:

const Point = struct {
    x: i32,
    y: i32,
};

fn foo() Point {
    return bar();
}

fn bar() Point {
    return Point{
        .x = 1,
        .y = 2,
    };
}

test "result location" {
    var point = foo();
}

Previously, the code return bar() would introduce an extra copy, so the body of the function foo would needlessly copy the point before returning it. This copying would happen at every expression, recursively when the type is an aggregate type (such as struct). Now that the result location mechanism is merged into master, you can see that the foo function does not introduce an extra copy:

define internal fastcc void @foo(%Point* nonnull sret) unnamed_addr #2 !dbg !35 {
Entry:
  call fastcc void @bar(%Point* sret %0), !dbg !44
  ret void, !dbg !46
}

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 the return statement:

fn foo() Point {
    const result = bar();
    return result;
}

Now there is a copy, because the Result Location of bar() is the result local variable, rather than the return result location:

define internal fastcc void @foo(%Point* nonnull sret) unnamed_addr #2 !dbg !35 {
Entry:
  %result = alloca %Point, align 4
  call fastcc void @bar(%Point* sret %result), !dbg !47
  call void @llvm.dbg.declare(metadata %Point* %result, metadata !45, metadata !DIExpression()), !dbg !48
  %1 = bitcast %Point* %result to i8*, !dbg !49
  %2 = bitcast %Point* %0 to i8*, !dbg !49
  call void @llvm.memcpy.p0i8.p0i8.i64(i8* align 4 %2, i8* align 4 %1, i64 8, i1 false), !dbg !49
  ret void, !dbg !50
}

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.

andrewrk commented 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.

mikdusan commented 5 years ago

[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();
}
shawnl commented 5 years ago

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.

thejoshwolfe commented 4 years ago

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 T and the return type is ?T or !T.

marler8997 commented 3 years ago

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

N00byEdge commented 2 years ago

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.

N00byEdge commented 2 years ago

Alternative syntax with return blocks (but same semantics)

return |*return_location| {
  // ...
};

2022-07-26-20:22:48

whatisaphone commented 2 years ago

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;
};
N00byEdge commented 2 years ago

I definitely think that's interesting, but then with the result identifier as a pointer to the return value location.

matklad commented 1 year ago

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
}
MichaelBelousov commented 7 months ago

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;
}
sno2 commented 4 weeks ago

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) {
        // ...
    };
}
plaukiu commented 3 weeks ago

all suggestions that the function signature be invaded by an implementation detail are wrongful, imo. grep fn* and friends receive noise to handle.

nektro commented 3 weeks ago

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 the return 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 ?

nickelca commented 1 week ago

@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;, 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.

Idk what changed.